import { parseJsonWithRepair } from "@oh-my-pi/pi-utils"; import type { Message, ToolCall } from "../types"; import { asRecord, mintToolCallId, partialSuffixOverlapAny } from "./coercion"; import dialectPrompt from "./deepseek.md" with { type: "text" }; import { assistantTranscriptParts, collectToolResultRun, messageContentText, stringifyJson } from "./rendering"; import type { DialectDefinition, DialectRenderOptions, DialectToolResult, InbandScanEvent, InbandScanner, InbandScannerOptions, } from "./types"; export const DEEPSEEK_TOOL_CALLS_BEGIN = "<|tool▁calls▁begin|>"; export const DEEPSEEK_TOOL_CALLS_END = "<|tool▁calls▁end|>"; export const DEEPSEEK_TOOL_CALL_BEGIN = "<|tool▁call▁begin|>"; export const DEEPSEEK_TOOL_CALL_END = "<|tool▁call▁end|>"; export const DEEPSEEK_TOOL_SEPARATOR = "<|tool▁sep|>"; const DEEPSEEK_TOOL_OUTPUT_BEGIN = "<|tool▁output▁begin|>"; const DEEPSEEK_TOOL_OUTPUT_END = "<|tool▁output▁end|>"; const DEEPSEEK_BOS = "<|begin▁of▁sentence|>"; const DEEPSEEK_USER = "<|User|>"; const DEEPSEEK_ASSISTANT = "<|Assistant|>"; const DEEPSEEK_EOS = "<|end▁of▁sentence|>"; const THINK_OPEN = ""; const THINK_CLOSE = ""; const LEGACY_TOOL_TYPE = "function"; const LEGACY_JSON_FENCE = "```json"; const CODE_FENCE = "```"; const DSML_TOOL_CALLS_OPEN_FULLWIDTH = "<|DSML|tool_calls>"; const DSML_TOOL_CALLS_CLOSE_FULLWIDTH = ""; const DSML_TOOL_CALLS_OPEN_ASCII = "<|DSML|tool_calls>"; const DSML_TOOL_CALLS_CLOSE_ASCII = ""; const CONTROL_TOKENS = [ DEEPSEEK_BOS, DEEPSEEK_EOS, "<|▁pad▁|>", DEEPSEEK_USER, DEEPSEEK_ASSISTANT, "<|EOT|>", "<|search▁begin|>", "<|search▁end|>", "<|fim▁hole|>", "<|fim▁begin|>", "<|fim▁end|>", "<|tool▁outputs▁begin|>", "<|tool▁outputs▁end|>", DEEPSEEK_TOOL_OUTPUT_BEGIN, DEEPSEEK_TOOL_OUTPUT_END, ] as const; const OUTSIDE_TOKENS = [ DEEPSEEK_TOOL_CALLS_BEGIN, DEEPSEEK_TOOL_CALLS_END, DEEPSEEK_TOOL_CALL_BEGIN, THINK_OPEN, THINK_CLOSE, DSML_TOOL_CALLS_OPEN_FULLWIDTH, DSML_TOOL_CALLS_OPEN_ASCII, DSML_TOOL_CALLS_CLOSE_FULLWIDTH, DSML_TOOL_CALLS_CLOSE_ASCII, ...CONTROL_TOKENS, ] as const; const SECTION_TOKENS = [DEEPSEEK_TOOL_CALL_BEGIN, DEEPSEEK_TOOL_CALLS_END] as const; const DSML_SECTION_TOKENS = [ DSML_TOOL_CALLS_CLOSE_FULLWIDTH, DSML_TOOL_CALLS_CLOSE_ASCII, "<|DSML|invoke", "<|DSML|invoke", ] as const; const DSML_INVOKE_TOKENS = ["", "", "<|DSML|parameter", "<|DSML|parameter"] as const; const DSML_PARAMETER_CLOSE_TOKENS = ["", ""] as const; type State = | "outside" | "thinking" | "section" | "header" | "args" | "legacyName" | "legacyArgs" | "dsmlSection" | "dsmlInvoke" | "dsmlParam"; export class DeepSeekInbandScanner implements InbandScanner { #buffer = ""; #state: State = "outside"; #parseThinking: boolean; #inToolSection = false; #id = ""; #name = ""; #thinking = ""; #dsmlArgs: Record = {}; #dsmlParamName = ""; #dsmlParamIsString = true; #dsmlParamRaw = ""; #rawBlock = ""; #stripLeadingWhitespace = false; constructor(options: InbandScannerOptions = {}) { this.#parseThinking = options.parseThinking ?? true; } feed(text: string): InbandScanEvent[] { if (text.length === 0) return []; this.#buffer += text; return this.#consume(false); } flush(): InbandScanEvent[] { return this.#consume(true); } #consume(final: boolean): InbandScanEvent[] { const events: InbandScanEvent[] = []; while (this.#buffer.length > 0) { if (this.#state === "outside") { this.#consumeOutside(final, events); if (this.#state !== "outside" && this.#buffer.length > 0) continue; break; } if (this.#state === "thinking") { this.#consumeThinking(final, events); if (!final && this.#state === "thinking") break; continue; } if (this.#state === "section") { if (!this.#consumeSection(final)) break; continue; } if (this.#state === "header") { if (!this.#consumeHeader(final, events)) break; continue; } if (this.#state === "legacyName") { if (!this.#consumeLegacyName(final, events)) break; continue; } if (this.#state === "args" || this.#state === "legacyArgs") { if (!this.#consumeArgs(final, events)) break; continue; } if (this.#state === "dsmlSection") { if (!this.#consumeDsmlSection(final, events)) break; continue; } if (this.#state === "dsmlInvoke") { if (!this.#consumeDsmlInvoke(final, events)) break; continue; } if (!this.#consumeDsmlParam(final, events)) break; } if (final && this.#state === "thinking") this.#endThinking(events); if (final && this.#buffer.length === 0 && this.#rawBlock.length > 0) this.#rawBlock = ""; return events; } #consumeOutside(final: boolean, events: InbandScanEvent[]): void { while (this.#buffer.length > 0) { if (this.#stripLeadingWhitespace) { // A chat-template control token (e.g. `<|Assistant|>`) was just dropped; // swallow the template whitespace that trails it so it never leaks into // visible text. Whitespace can't begin another token, so eager trim is safe. const trimmed = this.#buffer.replace(/^\s+/u, ""); if (trimmed.length === 0) { this.#buffer = ""; return; } this.#buffer = trimmed; this.#stripLeadingWhitespace = false; } const match = findEarliestToken(this.#buffer, OUTSIDE_TOKENS); if (!match) { const hold = final ? 0 : partialSuffixOverlapAny(this.#buffer, OUTSIDE_TOKENS); const emit = this.#buffer.slice(0, this.#buffer.length - hold); if (emit.length > 0) events.push({ type: "text", text: emit }); this.#buffer = this.#buffer.slice(this.#buffer.length - hold); return; } if (match.index > 0) events.push({ type: "text", text: this.#buffer.slice(0, match.index) }); this.#buffer = this.#buffer.slice(match.index); if (this.#buffer.startsWith(DEEPSEEK_TOOL_CALLS_BEGIN)) { this.#buffer = this.#buffer.slice(DEEPSEEK_TOOL_CALLS_BEGIN.length); this.#inToolSection = true; this.#state = "section"; return; } if (this.#buffer.startsWith(DEEPSEEK_TOOL_CALL_BEGIN)) { this.#buffer = this.#buffer.slice(DEEPSEEK_TOOL_CALL_BEGIN.length); this.#rawBlock = DEEPSEEK_TOOL_CALL_BEGIN; this.#inToolSection = false; this.#state = "header"; return; } if (this.#buffer.startsWith(THINK_OPEN)) { this.#buffer = this.#buffer.slice(THINK_OPEN.length); this.#state = "thinking"; this.#thinking = ""; if (this.#parseThinking) events.push({ type: "thinkingStart" }); return; } if ( this.#buffer.startsWith(DSML_TOOL_CALLS_OPEN_FULLWIDTH) || this.#buffer.startsWith(DSML_TOOL_CALLS_OPEN_ASCII) ) { const openToken = this.#buffer.startsWith(DSML_TOOL_CALLS_OPEN_FULLWIDTH) ? DSML_TOOL_CALLS_OPEN_FULLWIDTH : DSML_TOOL_CALLS_OPEN_ASCII; this.#buffer = this.#buffer.slice(openToken.length); this.#state = "dsmlSection"; return; } const control = this.#matchingControlToken(); if (control) { this.#buffer = this.#buffer.slice(control.length); this.#stripLeadingWhitespace = true; continue; } this.#buffer = this.#buffer.slice(match.token.length); } } #consumeThinking(final: boolean, events: InbandScanEvent[]): void { const close = this.#buffer.indexOf(THINK_CLOSE); if (close === -1) { const hold = final ? 0 : partialSuffixOverlapAny(this.#buffer, [THINK_CLOSE]); this.#emitThinking(this.#buffer.slice(0, this.#buffer.length - hold), events); this.#buffer = this.#buffer.slice(this.#buffer.length - hold); if (final) this.#endThinking(events); return; } this.#emitThinking(this.#buffer.slice(0, close), events); this.#buffer = this.#buffer.slice(close + THINK_CLOSE.length); this.#endThinking(events); } #consumeSection(final: boolean): boolean { while (this.#buffer.length > 0) { this.#skipWhitespace(); if (this.#buffer.startsWith(DEEPSEEK_TOOL_CALLS_END)) { this.#buffer = this.#buffer.slice(DEEPSEEK_TOOL_CALLS_END.length); this.#inToolSection = false; this.#state = "outside"; return true; } if (this.#buffer.startsWith(DEEPSEEK_TOOL_CALL_BEGIN)) { this.#buffer = this.#buffer.slice(DEEPSEEK_TOOL_CALL_BEGIN.length); this.#rawBlock = DEEPSEEK_TOOL_CALL_BEGIN; this.#state = "header"; return true; } if (!final && partialSuffixOverlapAny(this.#buffer, SECTION_TOKENS) === this.#buffer.length) return false; if (this.#buffer.length === 0) return false; this.#buffer = this.#buffer.slice(1); } return final; } #consumeHeader(final: boolean, events: InbandScanEvent[]): boolean { const sep = this.#buffer.indexOf(DEEPSEEK_TOOL_SEPARATOR); if (sep === -1) { if (final) this.#resetTool(); return false; } const rawHead = this.#buffer.slice(0, sep + DEEPSEEK_TOOL_SEPARATOR.length); const head = this.#buffer.slice(0, sep).trim(); this.#rawBlock += rawHead; this.#buffer = this.#buffer.slice(rawHead.length); if (head === LEGACY_TOOL_TYPE) { this.#state = "legacyName"; return true; } this.#startTool(head, events); this.#state = "args"; return true; } #consumeLegacyName(final: boolean, events: InbandScanEvent[]): boolean { const fence = this.#buffer.indexOf(LEGACY_JSON_FENCE); if (fence === -1) { if (final) this.#resetTool(); return false; } const rawName = this.#buffer.slice(0, fence + LEGACY_JSON_FENCE.length); const name = this.#buffer.slice(0, fence).trim(); this.#rawBlock += rawName; this.#buffer = this.#buffer.slice(rawName.length); this.#rawBlock += this.#dropOneLineBreak(); this.#startTool(name, events); this.#state = "legacyArgs"; return true; } #consumeArgs(final: boolean, events: InbandScanEvent[]): boolean { const end = this.#buffer.indexOf(DEEPSEEK_TOOL_CALL_END); if (end === -1) { if (final) this.#resetTool(); return false; } let rawArgs = this.#buffer.slice(0, end); if (this.#state === "legacyArgs") { const fence = rawArgs.lastIndexOf(CODE_FENCE); if (fence !== -1) rawArgs = rawArgs.slice(0, fence); } const rawTail = this.#buffer.slice(0, end + DEEPSEEK_TOOL_CALL_END.length); this.#rawBlock += rawTail; events.push({ type: "toolEnd", id: this.#id, name: this.#name, arguments: this.#parseArgs(rawArgs), rawBlock: this.#rawBlock, }); this.#buffer = this.#buffer.slice(rawTail.length); this.#resetTool(this.#inToolSection ? "section" : "outside"); return true; } #consumeDsmlSection(final: boolean, events: InbandScanEvent[]): boolean { while (this.#buffer.length > 0) { this.#skipWhitespace(); const close = this.#matchingDsmlClose(DSML_TOOL_CALLS_CLOSE_FULLWIDTH, DSML_TOOL_CALLS_CLOSE_ASCII); if (close) { this.#buffer = this.#buffer.slice(close.length); this.#state = "outside"; return true; } const invoke = this.#matchDsmlOpen("invoke"); if (invoke) { this.#rawBlock = invoke.raw; this.#name = invoke.name; this.#id = mintToolCallId(); this.#dsmlArgs = {}; events.push({ type: "toolStart", id: this.#id, name: this.#name }); this.#state = "dsmlInvoke"; return true; } if (!final) { if ( (this.#buffer.startsWith("<|DSML|invoke") || this.#buffer.startsWith("<|DSML|invoke")) && !this.#buffer.includes(">") ) return false; if (partialSuffixOverlapAny(this.#buffer, DSML_SECTION_TOKENS) === this.#buffer.length) return false; } if (this.#buffer.length === 0) return false; this.#buffer = this.#buffer.slice(1); } return final; } #consumeDsmlInvoke(final: boolean, events: InbandScanEvent[]): boolean { while (this.#buffer.length > 0) { const skipped = this.#skipWhitespace(); if (skipped.length > 0) this.#rawBlock += skipped; const close = this.#matchingDsmlClose("", ""); if (close) { this.#rawBlock += close; this.#buffer = this.#buffer.slice(close.length); events.push({ type: "toolEnd", id: this.#id, name: this.#name, arguments: this.#dsmlArgs, rawBlock: this.#rawBlock, }); this.#resetDsmlTool(); this.#state = "dsmlSection"; return true; } const param = this.#matchDsmlOpen("parameter"); if (param) { this.#rawBlock += param.raw; this.#dsmlParamName = param.name; this.#dsmlParamIsString = param.stringAttr !== "false"; this.#state = "dsmlParam"; return true; } if (!final) { if ( (this.#buffer.startsWith("<|DSML|parameter") || this.#buffer.startsWith("<|DSML|parameter")) && !this.#buffer.includes(">") ) return false; if (partialSuffixOverlapAny(this.#buffer, DSML_INVOKE_TOKENS) === this.#buffer.length) return false; } const consumed = this.#buffer[0]!; this.#rawBlock += consumed; this.#buffer = this.#buffer.slice(1); } return final; } #consumeDsmlParam(final: boolean, events: InbandScanEvent[]): boolean { const close = findEarliestToken(this.#buffer, DSML_PARAMETER_CLOSE_TOKENS); if (!close) { const hold = final ? 0 : partialSuffixOverlapAny(this.#buffer, DSML_PARAMETER_CLOSE_TOKENS); const chunk = this.#buffer.slice(0, this.#buffer.length - hold); this.#streamDsmlParam(chunk, events); this.#buffer = this.#buffer.slice(this.#buffer.length - hold); if (final) this.#resetDsmlTool(); return false; } this.#streamDsmlParam(this.#buffer.slice(0, close.index), events); this.#dsmlArgs[this.#dsmlParamName] = coerceDsmlValue(this.#dsmlParamRaw, this.#dsmlParamIsString); this.#rawBlock += close.token; this.#buffer = this.#buffer.slice(close.index + close.token.length); this.#dsmlParamName = ""; this.#dsmlParamIsString = true; this.#dsmlParamRaw = ""; this.#state = "dsmlInvoke"; return true; } #startTool(name: string, events: InbandScanEvent[]): void { this.#name = name; this.#id = mintToolCallId(); events.push({ type: "toolStart", id: this.#id, name: this.#name }); } #streamDsmlParam(chunk: string, events: InbandScanEvent[]): void { if (chunk.length === 0) return; this.#dsmlParamRaw += chunk; this.#rawBlock += chunk; events.push({ type: "toolArgDelta", id: this.#id, name: this.#name, key: this.#dsmlParamName, delta: chunk }); } #emitThinking(delta: string, events: InbandScanEvent[]): void { if (delta.length === 0) return; if (this.#parseThinking) { this.#thinking += delta; events.push({ type: "thinkingDelta", delta }); } else { events.push({ type: "text", text: delta }); } } #endThinking(events: InbandScanEvent[]): void { if (this.#parseThinking) events.push({ type: "thinkingEnd", thinking: this.#thinking }); this.#thinking = ""; this.#state = "outside"; } #parseArgs(rawArgs: string): Record { const trimmed = rawArgs.trim(); if (trimmed.length === 0) return {}; try { return asRecord(parseJsonWithRepair(trimmed)); } catch { return {}; } } #skipWhitespace(): string { let i = 0; while (i < this.#buffer.length && /\s/.test(this.#buffer[i]!)) i++; if (i === 0) return ""; const skipped = this.#buffer.slice(0, i); this.#buffer = this.#buffer.slice(i); return skipped; } #dropOneLineBreak(): string { if (this.#buffer.startsWith("\r\n")) { this.#buffer = this.#buffer.slice(2); return "\r\n"; } if (this.#buffer.startsWith("\n")) { this.#buffer = this.#buffer.slice(1); return "\n"; } return ""; } #matchingControlToken(): string | undefined { if (this.#buffer.startsWith(DEEPSEEK_TOOL_CALLS_END)) return DEEPSEEK_TOOL_CALLS_END; if (this.#buffer.startsWith(THINK_CLOSE)) return THINK_CLOSE; if (this.#buffer.startsWith(DSML_TOOL_CALLS_CLOSE_FULLWIDTH)) return DSML_TOOL_CALLS_CLOSE_FULLWIDTH; if (this.#buffer.startsWith(DSML_TOOL_CALLS_CLOSE_ASCII)) return DSML_TOOL_CALLS_CLOSE_ASCII; for (const token of CONTROL_TOKENS) { if (this.#buffer.startsWith(token)) return token; } return undefined; } #matchingDsmlClose(fullwidth: string, ascii: string): string | undefined { if (this.#buffer.startsWith(fullwidth)) return fullwidth; if (this.#buffer.startsWith(ascii)) return ascii; return undefined; } #matchDsmlOpen( kind: "invoke" | "parameter", ): { name: string; stringAttr: string | undefined; raw: string } | undefined { if (!this.#buffer.startsWith(`<|DSML|${kind}`) && !this.#buffer.startsWith(`<|DSML|${kind}`)) return undefined; const end = this.#buffer.indexOf(">"); if (end === -1) return undefined; const tag = this.#buffer.slice(0, end + 1); const name = /\sname="([^"]*)"/.exec(tag)?.[1]; if (name === undefined) return undefined; const stringAttr = /\sstring="(true|false)"/.exec(tag)?.[1]; this.#buffer = this.#buffer.slice(end + 1); return { name, stringAttr, raw: tag }; } #resetTool(next: State = "outside"): void { this.#state = next; this.#id = ""; this.#name = ""; this.#rawBlock = ""; } #resetDsmlTool(): void { this.#id = ""; this.#name = ""; this.#dsmlArgs = {}; this.#dsmlParamName = ""; this.#dsmlParamIsString = true; this.#dsmlParamRaw = ""; this.#rawBlock = ""; } } function findEarliestToken(text: string, tokens: readonly string[]): { index: number; token: string } | undefined { let bestIndex = -1; let bestToken = ""; for (const token of tokens) { const index = text.indexOf(token); if (index === -1) continue; if (bestIndex === -1 || index < bestIndex || (index === bestIndex && token.length > bestToken.length)) { bestIndex = index; bestToken = token; } } return bestIndex === -1 ? undefined : { index: bestIndex, token: bestToken }; } function coerceDsmlValue(raw: string, isString: boolean): unknown { if (isString) return raw; const trimmed = raw.trim(); if (trimmed.length === 0) return raw; try { return parseJsonWithRepair(trimmed); } catch { return raw; } } function renderToolCall(call: ToolCall, _options: DialectRenderOptions = {}): string { return `${DEEPSEEK_TOOL_CALL_BEGIN}${call.name}${DEEPSEEK_TOOL_SEPARATOR}${stringifyJson(call.arguments)}${DEEPSEEK_TOOL_CALL_END}`; } function renderAssistantToolCalls(calls: readonly ToolCall[], options: DialectRenderOptions = {}): string { if (calls.length === 0) return ""; const body = calls.map(call => renderToolCall(call, options)).join(""); return `${DEEPSEEK_TOOL_CALLS_BEGIN}${body}${DEEPSEEK_TOOL_CALLS_END}`; } function renderToolResults(results: readonly DialectToolResult[], _options: DialectRenderOptions = {}): string { return results.map(result => `${DEEPSEEK_TOOL_OUTPUT_BEGIN}${result.text}${DEEPSEEK_TOOL_OUTPUT_END}`).join("\n"); } function renderThinking(text: string): string { if (!text) return ""; return `${THINK_OPEN}\n${text}\n${THINK_CLOSE}`; } function renderTranscript(messages: readonly Message[], options: DialectRenderOptions = {}): string { if (messages.length === 0) return ""; let out = DEEPSEEK_BOS; for (let i = 0; i < messages.length; ) { const message = messages[i]!; if (message.role === "assistant") { const parts = assistantTranscriptParts(message); out += `${DEEPSEEK_ASSISTANT}${renderThinking(parts.thinking)}${parts.text}${renderAssistantToolCalls(parts.toolCalls, options)}${DEEPSEEK_EOS}`; i++; continue; } if (message.role === "toolResult") { const run = collectToolResultRun(messages, i); out += renderToolResults(run.results, options); i = run.next; continue; } if (message.role === "developer") out += messageContentText(message.content); else out += `${DEEPSEEK_USER}${messageContentText(message.content)}`; i++; } return out; } const definition: DialectDefinition = { dialect: "deepseek", prompt: dialectPrompt, createScanner: options => new DeepSeekInbandScanner(options), renderToolCall, renderAssistantToolCalls, renderToolResults, renderThinking, renderTranscript, }; export default definition;