Documentation
¶
Overview ¶
Package dsml encodes and decodes DeepSeek DSML tool-calling markup.
DSML (the markup DeepSeek V4 uses for structured tool calls) is not part of the libds4 chat-template API. This package renders the DSML *fragments* that libds4 does not — a "## Tools" system-prompt section and assistant tool-call blocks — and parses an assistant completion back into a typed ParsedMessage. It does NOT render the surrounding chat envelope (begin/end markers, user/assistant roles, thinking tags); libds4's own chat helpers own that, and re-implementing it here would duplicate and drift from it.
The package is pure text processing: no FFI, no engine, standard library only. Callers compose its output with libds4's chat helpers — prepend a rendered tools section to the system message content, append a rendered tool-call block to assistant-history content.
Index ¶
- func RenderAssistantTurn(content, reasoning, toolCalls string, thinking, replayReasoning bool) string
- func RenderToolCall(call ToolCall) (string, error)
- func RenderToolCalls(calls []ToolCall) (string, error)
- func RenderToolResult(content string) (string, error)
- func RenderToolsSection(tools []Tool) (string, error)
- func RepairCompletion(text string) (string, bool)
- func ToolSyntaxErrorMessage(detail string) string
- func WrapToolCalls(invokes []string) string
- type ParsedMessage
- type ReplayStore
- type StreamDecoder
- type StreamEvent
- type StreamEventType
- type Tool
- type ToolCall
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func RenderAssistantTurn ¶ added in v0.5.1
func RenderAssistantTurn(content, reasoning, toolCalls string, thinking, replayReasoning bool) string
RenderAssistantTurn renders one assistant history turn as raw chat-template text — role marker, think block, visible content, rendered tool calls, and the end-of-sentence terminator — for rendered-chat tokenization, which maps the special markers (including |DSML|) to their vocab token ids.
Mirroring upstream ds4's render_chat_prompt_text: when thinking is enabled and replayReasoning is true (the turn is in tool context or follows the last user turn), the reasoning is re-rendered inside <think>...</think> — for reasoning models, tool-call turns must keep their reasoning when replayed, and dropping it also breaks KV prefix reuse against the live session. Otherwise the think slot is rendered closed, the DeepSeek convention for prior-turn reasoning.
func RenderToolCall ¶
RenderToolCall renders one assistant "<|DSML|invoke>" block.
func RenderToolCalls ¶
RenderToolCalls renders an assistant "<|DSML|tool_calls>" block. The caller appends the result to an assistant message's content when replaying tool-call history into a multi-turn prompt. It returns "" for no calls.
func RenderToolResult ¶
RenderToolResult wraps one tool result payload the way DeepSeek/ds4 expect tool outputs to appear in the next user turn. Tool output is treated as data: normal '<', '>', '&', DSML text, and control-token-looking text are preserved. Only the exact </tool_result> sentinel is escaped so the payload cannot break out of the wrapper.
DeepSeek V4's rendered DSML format does not include a tool name or call ID in <tool_result>; result correlation is positional. When returning results for multiple assistant tool calls, emit the result blocks in the same order as the assistant's <invoke> blocks.
func RenderToolsSection ¶
RenderToolsSection renders the "## Tools" instruction block for the given tools. The caller prepends the result to the system message content before passing the system message to libds4's chat helpers. An empty tool list renders nothing (an empty string), so callers need not special-case it.
func RepairCompletion ¶ added in v0.5.1
RepairCompletion repairs a completion whose DSML tool-calls block was truncated by the token limit, mirroring upstream ds4's try_repair_dsml. Generation that stops mid-stanza leaves unclosed parameter/invoke/tool_calls tags; appending the missing closers in reverse nesting order turns the truncated suffix back into a parseable block. Near-miss markers are normalized first so a sampled typo and a truncation in the same block both recover, and the returned text is the normalized form.
Tags before the last </think> are not counted: DSML quoted inside reasoning is not executable and would inflate the counts into false repairs. Repair is refused when any closing tag outnumbers its opener — extra closers are not a truncation pattern.
It returns the repaired text and true when a repair was applied, or the input unchanged and false otherwise. Callers should re-parse the repaired text and keep their original result if the re-parse still yields no calls.
func ToolSyntaxErrorMessage ¶ added in v0.5.1
ToolSyntaxErrorMessage renders the tool-error payload sent back to the model when its DSML tool call could not be parsed, mirroring upstream ds4's invalid-DSML error suffix. detail is the parse failure (typically ParsedMessage.MalformedReason) and may be empty. The payload is plain text: wrap it as a tool result (or send it as a "tool" role message) so the model sees the failure where it expects tool output, then retries or answers normally.
func WrapToolCalls ¶
WrapToolCalls wraps rendered invoke blocks in a "<|DSML|tool_calls>" block.
Types ¶
type ParsedMessage ¶
type ParsedMessage struct {
// Role is set to "assistant" by ParseCompletion.
Role string
// Content is the assistant's user-facing reply, trimmed.
Content string
// ReasoningContent is the thinking-mode reasoning block, trimmed. It is
// empty when ParseCompletion is called with thinking == false.
ReasoningContent string
// ToolCalls holds any tool calls the completion requested.
ToolCalls []ToolCall
// MalformedReason is the parse failure that degraded a tool-calls stanza
// to plain content, and is empty for clean output. Callers can feed it to
// ToolSyntaxErrorMessage to ask the model for a corrected call.
MalformedReason string
}
ParsedMessage is the decoded result of one assistant completion.
func ParseCompletion ¶
func ParseCompletion(text string, thinking bool) (ParsedMessage, error)
ParseCompletion parses one assistant completion (raw model output) into a ParsedMessage.
When thinking is true, DSML is executable only after the final </think>. If the model has not closed thinking yet, the text is returned as reasoning and no tool calls are parsed. The completion may end either with the explicit <|end▁of▁sentence|> marker or simply at end-of-input.
type ReplayStore ¶
type ReplayStore struct {
// contains filtered or unexported fields
}
ReplayStore keeps an exact sampled DSML tool_calls block for tool-call IDs so later prompt renders can replay the original bytes instead of canonicalizing JSON back into DSML.
func NewReplayStore ¶
func NewReplayStore(maxIDs int) *ReplayStore
NewReplayStore creates a bounded in-memory exact-DSML replay map.
func (*ReplayStore) Lookup ¶
func (s *ReplayStore) Lookup(id string) (string, bool)
Lookup returns the exact sampled DSML tool_calls block for id when available.
func (*ReplayStore) LookupBlock ¶
func (s *ReplayStore) LookupBlock(ids []string) (string, bool)
LookupBlock returns a replay block when every id exists and maps to the same exact sampled DSML tool_calls block.
func (*ReplayStore) Remember ¶
func (s *ReplayStore) Remember(id, exact string) error
Remember stores one tool-call ID to exact sampled DSML tool_calls block mapping.
type StreamDecoder ¶
type StreamDecoder struct {
// contains filtered or unexported fields
}
StreamDecoder incrementally parses assistant output and emits streaming events. It is NOT safe for concurrent use.
Tool-call events are buffered internally until the enclosing </tool_calls> block is fully validated. This guarantees that callers never see tool events for a block that ParseCompletion would reject as malformed. If malformed DSML is detected, the decoder falls back to raw-content mode and replays any consumed text as content deltas.
func NewStreamDecoder ¶
func NewStreamDecoder(thinking bool) *StreamDecoder
NewStreamDecoder returns a decoder that consumes assistant output incrementally. thinking has the same meaning as in ParseCompletion.
func (*StreamDecoder) Close ¶
func (d *StreamDecoder) Close() ([]StreamEvent, ParsedMessage, error)
Close finalizes the stream and returns any trailing events plus the fully assembled ParsedMessage (identical to what ParseCompletion would return for the concatenated input).
func (*StreamDecoder) Done ¶ added in v0.5.1
func (d *StreamDecoder) Done() bool
Done reports whether the explicit <|end▁of▁sentence|> marker has been consumed. Generation past this point produces no further usable output.
func (*StreamDecoder) ToolBlockClosed ¶ added in v0.5.1
func (d *StreamDecoder) ToolBlockClosed() bool
ToolBlockClosed reports whether a complete </tool_calls> tag has been consumed. Once the block closes, the only valid continuation for the completion is whitespace and end-of-sentence, so a caller driving generation can stop sampling as soon as this returns true instead of paying for trailing tokens. The signal reverts to false if trailing non-whitespace text later degrades the block to raw content.
func (*StreamDecoder) ToolStanzaInThinking ¶ added in v0.5.1
func (d *StreamDecoder) ToolStanzaInThinking() bool
ToolStanzaInThinking reports whether a complete tool-calls stanza opening has appeared inside a still-unclosed <think> block. The decoder treats DSML inside reasoning as non-executable, so without intervention such a turn runs to the token limit and the call is dropped at parse time. A caller driving generation can recover by force-feeding "</think>\n\n" into the session (upstream ds4's think-tool recovery): measured on the real model, that position predicts a fresh stanza opening strongly enough that the call restarts cleanly on the executable side, while the dangling opening stays harmlessly inside reasoning. Only a complete opening triggers — quoted fragments and a lone "<" keep decoding untouched. The signal clears once decoding leaves the thinking state.
func (*StreamDecoder) Write ¶
func (d *StreamDecoder) Write(chunk string) []StreamEvent
Write feeds the next chunk of decoded model text and returns any events that became complete. Chunk boundaries are arbitrary — a marker may be split across calls.
type StreamEvent ¶
type StreamEvent struct {
Type StreamEventType
Index int // tool-call index for tool-related events
Delta string // text fragment for delta events
Name string // tool name for EventToolCallStart
Arguments string // final JSON arguments for EventToolCallEnd
}
StreamEvent carries one incremental update from the decoder.
type StreamEventType ¶
type StreamEventType int
StreamEventType classifies the kind of incremental update.
const ( // EventReasoningDelta carries a fragment of the assistant's reasoning. EventReasoningDelta StreamEventType = iota // EventContentDelta carries a fragment of the assistant's user-facing reply. EventContentDelta // EventToolCallStart signals the beginning of a new tool call. Name holds // the tool name and Index is the 0-based tool-call position. EventToolCallStart // EventToolCallArgumentsDelta carries a live JSON fragment. For exact final // arguments, use EventToolCallEnd.Arguments. EventToolCallArgumentsDelta // EventToolCallEnd signals the completion of a tool call at Index. // Arguments holds the final authoritative JSON arguments object. EventToolCallEnd )
type Tool ¶
type Tool struct {
// Name is the tool's callable name. It must not contain a double-quote
// character.
Name string
// Description explains what the tool does.
Description string
// Parameters is the JSON Schema for the tool's parameters object.
// An empty value is rendered as "{}".
Parameters json.RawMessage
}
Tool is an OpenAI-style function/tool schema.
type ToolCall ¶
type ToolCall struct {
// Name is the invoked tool's name. It must not contain a double-quote
// character.
Name string
// Arguments holds the call arguments as a JSON object string.
Arguments string
// Exact is the exact sampled DSML "<|DSML|tool_calls>...</|DSML|tool_calls>"
// block when this call came from ParseCompletion.
Exact string
}
ToolCall is one parsed or to-be-rendered tool invocation.