fix(agents): cap DeepSeek DSML recovery buffer at 256 KB (#117175)

Co-authored-by: Sebastien Tardif <1413412+SebTardif@users.noreply.github.com>
This commit is contained in:
Sebastien Tardif
2026-08-02 03:12:38 -04:00
committed by GitHub
parent 70394e190c
commit b300a0cfb3
2 changed files with 607 additions and 19 deletions

View File

@@ -881,63 +881,153 @@ const DEEPSEEK_DSML_TOOL_OPEN_TOKENS = DEEPSEEK_DSML_BARS.flatMap((bar) =>
const DEEPSEEK_DSML_TOOL_CLOSE_TOKENS = DEEPSEEK_DSML_BARS.flatMap((bar) =>
DEEPSEEK_DSML_TOOL_KINDS.map((kind) => `</${bar}DSML${bar}${kind}>`),
);
const DEEPSEEK_DSML_INVOKE_OPEN_PREFIXES = DEEPSEEK_DSML_BARS.map(
(bar) => `<${bar}DSML${bar}invoke`,
);
const DEEPSEEK_DSML_INVOKE_CLOSE_TOKENS = DEEPSEEK_DSML_BARS.map(
(bar) => `</${bar}DSML${bar}invoke>`,
);
const DEEPSEEK_DSML_TOOL_MAX_OPEN_TOKEN_LEN = Math.max(
...DEEPSEEK_DSML_TOOL_OPEN_TOKENS.map((token) => token.length),
);
const DEEPSEEK_DSML_RECOVERY_MAX_BOUNDARY_LEN = Math.max(
...DEEPSEEK_DSML_TOOL_OPEN_TOKENS.map((token) => token.length),
...DEEPSEEK_DSML_TOOL_CLOSE_TOKENS.map((token) => token.length),
...DEEPSEEK_DSML_INVOKE_OPEN_PREFIXES.map((token) => token.length),
...DEEPSEEK_DSML_INVOKE_CLOSE_TOKENS.map((token) => token.length),
);
// Match MAX_TOOL_CALL_ARGUMENT_BUFFER_BYTES / MAX_POST_TOOL_CALL_BUFFER_BYTES.
const MAX_DSML_RECOVERY_BUFFER_BYTES = 256_000;
const DEEPSEEK_DSML_SCAN_BATCH_CHARS = 64 * 1_024;
type DeepSeekDsmlToolBlockScanState = {
offset: number;
mode: "outer" | "invoke-open" | "invoke-body";
invokeOpenStart: number;
};
function createDeepSeekDsmlToolCallRecoverer() {
let buffer = "";
let bufferBytes = 0;
let bufferEndsWithHighSurrogate = false;
let pendingScanChars = 0;
let activeOpenToken: string | null = null;
let blockScanState: DeepSeekDsmlToolBlockScanState = {
offset: 0,
mode: "outer",
invokeOpenStart: -1,
};
const resetBlockScan = () => {
activeOpenToken = null;
pendingScanChars = 0;
blockScanState = { offset: 0, mode: "outer", invokeOpenStart: -1 };
};
const consume = (final: boolean): DeepSeekDsmlRecoveredPart[] => {
const output: DeepSeekDsmlRecoveredPart[] = [];
while (buffer) {
const open = findEarliestStringToken(buffer, DEEPSEEK_DSML_TOOL_OPEN_TOKENS);
const open = activeOpenToken
? { index: 0, token: activeOpenToken }
: findEarliestStringToken(buffer, DEEPSEEK_DSML_TOOL_OPEN_TOKENS);
if (!open) {
resetBlockScan();
if (final) {
output.push({ kind: "text", text: buffer });
buffer = "";
bufferBytes = 0;
bufferEndsWithHighSurrogate = false;
return output;
}
const keep = longestDeepSeekDsmlToolOpenPrefixSuffixLength(buffer);
const emitLength = buffer.length - keep;
if (emitLength > 0) {
output.push({ kind: "text", text: buffer.slice(0, emitLength) });
buffer = buffer.slice(emitLength);
const emitted = buffer.slice(0, emitLength);
output.push({ kind: "text", text: emitted });
bufferBytes -= Buffer.byteLength(emitted, "utf8");
buffer = buffer.slice(emitted.length);
if (!buffer) {
bufferEndsWithHighSurrogate = false;
}
}
return output;
}
if (open.index > 0) {
output.push({ kind: "text", text: buffer.slice(0, open.index) });
buffer = buffer.slice(open.index);
const prefix = buffer.slice(0, open.index);
output.push({ kind: "text", text: prefix });
bufferBytes -= Buffer.byteLength(prefix, "utf8");
buffer = buffer.slice(prefix.length);
resetBlockScan();
}
const afterOpen = buffer.slice(open.token.length);
const close = findEarliestStringToken(afterOpen, DEEPSEEK_DSML_TOOL_CLOSE_TOKENS);
activeOpenToken = open.token;
if (blockScanState.offset === 0) {
blockScanState.offset = open.token.length;
}
const blockScan = scanDeepSeekDsmlToolBlock(
buffer,
open.token.replace("<", "</"),
open.token.length,
blockScanState,
);
if (blockScan.kind === "nested-open") {
throw new Error("Nested DeepSeek DSML recovery wrappers are not supported");
}
const close = blockScan.kind === "close" ? blockScan : null;
if (!close) {
if (final) {
output.push({ kind: "text", text: buffer });
buffer = "";
bufferBytes = 0;
bufferEndsWithHighSurrogate = false;
return output;
}
if (bufferBytes > MAX_DSML_RECOVERY_BUFFER_BYTES) {
throw new Error("Exceeded DeepSeek DSML recovery buffer limit");
}
return output;
}
const body = afterOpen.slice(0, close.index);
const blockLength = open.token.length + close.index + close.token.length;
resetBlockScan();
const body = buffer.slice(open.token.length, close.index);
const blockText = buffer.slice(0, close.index + close.token.length);
const blockBytes = Buffer.byteLength(blockText, "utf8");
if (blockBytes > MAX_DSML_RECOVERY_BUFFER_BYTES) {
throw new Error("Exceeded DeepSeek DSML recovery buffer limit");
}
const recoveredToolCalls = parseDeepSeekDsmlToolCallBlock(body);
if (recoveredToolCalls.length > 0) {
output.push(...recoveredToolCalls);
} else {
output.push({ kind: "text", text: buffer.slice(0, blockLength) });
output.push({ kind: "text", text: blockText });
}
bufferBytes -= Buffer.byteLength(blockText, "utf8");
buffer = buffer.slice(blockText.length);
if (!buffer) {
bufferEndsWithHighSurrogate = false;
}
buffer = buffer.slice(blockLength);
}
return output;
};
return {
push(chunk: string) {
const append = utf8ByteLengthForAppend(bufferEndsWithHighSurrogate, chunk);
bufferBytes += append.bytes;
bufferEndsWithHighSurrogate = append.endsWithHighSurrogate;
buffer += chunk;
pendingScanChars += chunk.length;
if (
activeOpenToken &&
pendingScanChars < DEEPSEEK_DSML_SCAN_BATCH_CHARS &&
!chunk.includes("<") &&
!chunk.includes(">") &&
bufferBytes <= MAX_DSML_RECOVERY_BUFFER_BYTES
) {
return [];
}
pendingScanChars = 0;
return consume(false);
},
flush() {
@@ -948,23 +1038,23 @@ function createDeepSeekDsmlToolCallRecoverer() {
function parseDeepSeekDsmlToolCallBlock(body: string): RecoveredDeepSeekDsmlToolCall[] {
const toolCalls: RecoveredDeepSeekDsmlToolCall[] = [];
const invokeOpenRegex = /<[|]DSML[|]invoke\b([^>]*)>/g;
const invokeOpenRegex = /<[|]DSML[|]invoke\b([^<>]*)>/g;
let openMatch: RegExpExecArray | null;
while ((openMatch = invokeOpenRegex.exec(body)) !== null) {
const invokeName = parseXmlAttribute(openMatch[1] ?? "", "name");
if (!invokeName) {
continue;
}
const invokeBodyStart = openMatch.index + openMatch[0].length;
const invokeClose = findEarliestStringToken(body.slice(invokeBodyStart), [
"</|DSML|invoke>",
"</DSMLinvoke>",
]);
if (!invokeClose) {
continue;
break;
}
const invokeBody = body.slice(invokeBodyStart, invokeBodyStart + invokeClose.index);
invokeOpenRegex.lastIndex = invokeBodyStart + invokeClose.index + invokeClose.token.length;
const invokeName = parseXmlAttribute(openMatch[1] ?? "", "name");
if (!invokeName) {
continue;
}
const parsedArguments = parseDeepSeekDsmlInvokeArguments(invokeBody);
if (!parsedArguments) {
continue;
@@ -1043,10 +1133,10 @@ function decodeDeepSeekDsmlText(value: string): string {
.replaceAll("&amp;", "&");
}
function findEarliestStringToken(text: string, tokens: readonly string[]) {
function findEarliestStringToken(text: string, tokens: readonly string[], fromIndex = 0) {
let best: { index: number; token: string } | null = null;
for (const token of tokens) {
const index = text.indexOf(token);
const index = text.indexOf(token, fromIndex);
if (index !== -1 && (!best || index < best.index)) {
best = { index, token };
}
@@ -1054,6 +1144,108 @@ function findEarliestStringToken(text: string, tokens: readonly string[]) {
return best;
}
function scanDeepSeekDsmlToolBlock(
text: string,
closeToken: string,
contentStartIndex: number,
state: DeepSeekDsmlToolBlockScanState,
):
| { kind: "close"; index: number; token: string }
| { kind: "nested-open"; index: number; token: string }
| { kind: "incomplete" } {
while (state.offset < text.length) {
if (state.mode === "invoke-open") {
const nextOpen = text.indexOf("<", state.offset);
const nextClose = text.indexOf(">", state.offset);
if (nextClose === -1 && nextOpen === -1) {
state.offset = text.length;
return { kind: "incomplete" };
}
if (nextOpen !== -1 && (nextClose === -1 || nextOpen < nextClose)) {
state.mode = "outer";
state.offset = nextOpen;
state.invokeOpenStart = -1;
continue;
}
const invokeOpenTag = text.slice(state.invokeOpenStart, nextClose + 1);
if (!/^<[|]DSML[|]invoke\b[^<>]*>$/.test(invokeOpenTag)) {
state.mode = "outer";
state.offset = state.invokeOpenStart + 1;
state.invokeOpenStart = -1;
continue;
}
state.mode = "invoke-body";
state.offset = nextClose + 1;
state.invokeOpenStart = -1;
continue;
}
if (state.mode === "invoke-body") {
const invokeClose = findEarliestStringToken(
text,
DEEPSEEK_DSML_INVOKE_CLOSE_TOKENS,
state.offset,
);
if (!invokeClose) {
state.offset = Math.max(0, text.length - DEEPSEEK_DSML_RECOVERY_MAX_BOUNDARY_LEN + 1);
return { kind: "incomplete" };
}
state.mode = "outer";
state.offset = invokeClose.index + invokeClose.token.length;
continue;
}
const toolOpen = findEarliestStringToken(text, DEEPSEEK_DSML_TOOL_OPEN_TOKENS, state.offset);
const toolCloseIndex = text.indexOf(closeToken, state.offset);
const invokeOpen = findEarliestStringToken(
text,
DEEPSEEK_DSML_INVOKE_OPEN_PREFIXES,
state.offset,
);
const next = [
toolOpen ? { kind: "nested-open" as const, ...toolOpen } : null,
toolCloseIndex === -1
? null
: { kind: "close" as const, index: toolCloseIndex, token: closeToken },
invokeOpen ? { kind: "invoke-open" as const, ...invokeOpen } : null,
]
.filter((candidate) => candidate !== null)
.toSorted((left, right) => left.index - right.index)[0];
if (!next) {
state.offset = Math.max(
contentStartIndex,
text.length - DEEPSEEK_DSML_RECOVERY_MAX_BOUNDARY_LEN + 1,
);
return { kind: "incomplete" };
}
if (next.kind === "invoke-open") {
state.mode = "invoke-open";
state.invokeOpenStart = next.index;
state.offset = next.index + next.token.length;
continue;
}
return next;
}
return { kind: "incomplete" };
}
function utf8ByteLengthForAppend(bufferEndsWithHighSurrogate: boolean, chunk: string) {
let bytes = Buffer.byteLength(chunk, "utf8");
if (!chunk) {
return { bytes, endsWithHighSurrogate: bufferEndsWithHighSurrogate };
}
const nextCodeUnit = chunk.charCodeAt(0);
if (bufferEndsWithHighSurrogate && nextCodeUnit >= 0xdc00 && nextCodeUnit <= 0xdfff) {
// Each isolated surrogate counts as three UTF-8 bytes; the joined scalar is four.
bytes -= 2;
}
const finalCodeUnit = chunk.charCodeAt(chunk.length - 1);
return {
bytes,
endsWithHighSurrogate: finalCodeUnit >= 0xd800 && finalCodeUnit <= 0xdbff,
};
}
function longestDeepSeekDsmlToolOpenPrefixSuffixLength(text: string) {
const maxLength = Math.min(text.length, DEEPSEEK_DSML_TOOL_MAX_OPEN_TOKEN_LEN - 1);
for (let length = maxLength; length > 0; length -= 1) {

View File

@@ -275,6 +275,402 @@ describe("openai transport stream", () => {
expect(JSON.stringify(events)).not.toContain("DSML");
});
it("rejects an oversized DeepSeek DSML block when the crossing chunk contains its close", async () => {
const model = createDeepSeekCompletionsModel();
const output = createAssistantOutput(model);
const events: CapturedStreamEvent[] = [];
const prefix = '<|DSML|tool_calls><|DSML|invoke name="read">{"path":"';
const suffix = '"}</|DSML|invoke></|DSML|tool_calls>';
const padding = "x".repeat(256_001 - Buffer.byteLength(prefix + suffix, "utf8"));
const content = prefix + padding + suffix + " after";
expect(Buffer.byteLength(prefix + padding + suffix, "utf8")).toBe(256_001);
const chunks = Array.from({ length: Math.ceil(content.length / 4096) }, (_, index) =>
content.slice(index * 4096, (index + 1) * 4096),
);
await expect(
testing.processOpenAICompletionsStream(
streamChunks(
chunks.map((contentChunk, index) =>
makeCompletionsChunk(
{ content: contentChunk },
index === chunks.length - 1 ? "stop" : null,
),
),
),
output,
model,
{ push: (event) => events.push(event as CapturedStreamEvent) },
),
).rejects.toThrow("Exceeded DeepSeek DSML recovery buffer limit");
expect(events.filter((event) => event.type?.startsWith("toolcall_"))).toEqual([]);
});
it("rejects an oversized DeepSeek DSML recovery buffer using UTF-8 bytes", async () => {
const model = createDeepSeekCompletionsModel();
const output = createAssistantOutput(model);
// 200k "é": .length under 256k string units, UTF-8 bytes over 256k.
const multibyteBody =
'<DSMLinvoke name="session_status"><DSMLparameter name="key" string="true">' +
"\u00E9".repeat(200_000) +
"</DSMLparameter></DSMLinvoke>";
await expect(
testing.processOpenAICompletionsStream(
streamChunks([
makeCompletionsChunk(
{
content: "<DSMLtool_calls>" + multibyteBody,
},
"stop",
),
]),
output,
model,
{ push() {} },
),
).rejects.toThrow("Exceeded DeepSeek DSML recovery buffer limit");
});
it("counts split surrogate pairs exactly at the DeepSeek DSML recovery cap", async () => {
const model = createDeepSeekCompletionsModel();
const output = createAssistantOutput(model);
const prefix = '<|DSML|tool_calls><|DSML|invoke name="read">{"path":"';
const suffix = '"}</|DSML|invoke>';
const outerClose = "</|DSML|tool_calls>";
const emoji = "\u{1f600}";
const padding = "x".repeat(
256_000 - Buffer.byteLength(prefix + emoji + suffix + outerClose, "utf8"),
);
const beforeSplit = prefix + padding + "\uD83D";
const afterSplit = "\uDE00" + suffix;
expect(Buffer.byteLength(beforeSplit + afterSplit + outerClose, "utf8")).toBe(256_000);
await testing.processOpenAICompletionsStream(
streamChunks([
makeCompletionsChunk({ content: beforeSplit }),
makeCompletionsChunk({ content: afterSplit }),
makeCompletionsChunk({ content: outerClose }, "stop"),
]),
output,
model,
{ push() {} },
);
expect(output.stopReason).toBe("toolUse");
expect(output.content).toEqual([
{
type: "toolCall",
id: expect.stringMatching(/^call_[0-9a-f]{24}$/),
name: "read",
arguments: { path: padding + emoji },
},
]);
});
it("surfaces an oversized DeepSeek DSML recovery buffer as a transport error", async () => {
const server = createServer((req, res) => {
req.resume();
req.on("end", () => {
res.writeHead(200, {
"content-type": "text/event-stream; charset=utf-8",
"cache-control": "no-cache",
connection: "keep-alive",
});
for (const chunk of [
makeCompletionsChunk({
content: "<|DSML|tool_calls>" + "x".repeat(300_000),
}),
makeCompletionsChunk({}, "stop"),
]) {
res.write(`data: ${JSON.stringify(chunk)}\n\n`);
}
res.end("data: [DONE]\n\n");
});
});
await new Promise<void>((resolve) => {
server.listen(0, "127.0.0.1", resolve);
});
try {
const address = server.address();
if (!address || typeof address === "string") {
throw new Error("Missing loopback server address");
}
const model = makeCompletionsModel({
...createDeepSeekCompletionsModel(),
baseUrl: `http://127.0.0.1:${address.port}/v1`,
});
const stream = createOpenAICompletionsTransportStreamFn()(
model,
{
systemPrompt: "system",
messages: [{ role: "user", content: "Read the file", timestamp: Date.now() }],
tools: [],
} as never,
{ apiKey: "test-key" } as never,
);
const events: Array<{
type: string;
reason?: string;
error?: { errorMessage?: string; content?: unknown[] };
}> = [];
for await (const event of stream as AsyncIterable<(typeof events)[number]>) {
events.push(event);
}
expect(events).toContainEqual(
expect.objectContaining({
type: "error",
reason: "error",
error: expect.objectContaining({
errorMessage: "Exceeded DeepSeek DSML recovery buffer limit",
content: [],
}),
}),
);
expect(events.filter((event) => event.type === "toolcall_start")).toEqual([]);
expect(events.filter((event) => event.type === "toolcall_delta")).toEqual([]);
} finally {
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
it("fails before a later DSML call after overflow can be authorized", async () => {
const model = createDeepSeekCompletionsModel();
const output = createAssistantOutput(model);
const laterCall =
'<|DSML|tool_calls><|DSML|invoke name="read">{"path":"/tmp/repro.md"}</|DSML|invoke></|DSML|tool_calls>';
await expect(
testing.processOpenAICompletionsStream(
streamChunks([
makeCompletionsChunk({
content: "<|DSML|tool_calls>" + "x".repeat(256_001),
}),
makeCompletionsChunk({
content: "</|DSML|function_calls> after " + laterCall,
}),
makeCompletionsChunk({}, "tool_calls"),
]),
output,
model,
{ push() {} },
),
).rejects.toThrow("Exceeded DeepSeek DSML recovery buffer limit");
});
it("does not carry surrogate accounting across emitted visible text", async () => {
const model = createDeepSeekCompletionsModel();
const output = createAssistantOutput(model);
await expect(
testing.processOpenAICompletionsStream(
streamChunks([
makeCompletionsChunk({ content: "\ud83d" }),
makeCompletionsChunk({ content: "\ude00" }),
makeCompletionsChunk({
content: "<|DSML|tool_calls>" + "x".repeat(256_001),
}),
makeCompletionsChunk({}, "stop"),
]),
output,
model,
{ push() {} },
),
).rejects.toThrow("Exceeded DeepSeek DSML recovery buffer limit");
});
it("rejects a nested DSML wrapper before the original outer close", async () => {
const model = createDeepSeekCompletionsModel();
const output = createAssistantOutput(model);
const events: CapturedStreamEvent[] = [];
await expect(
testing.processOpenAICompletionsStream(
streamChunks([
makeCompletionsChunk({ content: "<|DSML|tool_calls><|DSML|tool_" }),
makeCompletionsChunk({
content:
'calls><|DSML|invoke name="read">{"path":"/tmp/nested.md"}</|DSML|invoke></|DSML|tool_calls>',
}),
makeCompletionsChunk({ content: "</|DSML|tool_calls>" }, "stop"),
]),
output,
model,
{
push(event) {
events.push(event as CapturedStreamEvent);
},
},
),
).rejects.toThrow("Nested DeepSeek DSML recovery wrappers are not supported");
expect(events.filter((event) => event.type?.startsWith("toolcall_") === true)).toEqual([]);
expect(output.content.some((part) => part.type === "toolCall")).toBe(false);
});
it("does not accept a different DSML wrapper kind as the outer close", async () => {
const model = createDeepSeekCompletionsModel();
const output = createAssistantOutput(model);
await testing.processOpenAICompletionsStream(
streamChunks([
makeCompletionsChunk({
content:
'<|DSML|tool_calls><|DSML|invoke name="read">{"path":"/tmp/mismatch.md"}</|DSML|invoke></|DSML|function_calls>',
}),
makeCompletionsChunk({}, "stop"),
]),
output,
model,
{ push() {} },
);
expect(output.stopReason).toBe("stop");
expect(output.content.some((part) => part.type === "toolCall")).toBe(false);
});
it("does not rewind across the outer opener after a short first body chunk", async () => {
const model = createDeepSeekCompletionsModel();
const output = createAssistantOutput(model);
await testing.processOpenAICompletionsStream(
streamChunks([
makeCompletionsChunk({ content: "<|DSML|tool_calls>\n" }),
makeCompletionsChunk({
content:
'<|DSML|invoke name="read">{"path":"/tmp/fragmented.md"}</|DSML|invoke></|DSML|tool_calls>',
}),
makeCompletionsChunk({}, "stop"),
]),
output,
model,
{ push() {} },
);
expect(output.stopReason).toBe("toolUse");
expect(output.content).toEqual([
{
type: "toolCall",
id: expect.stringMatching(/^call_[0-9a-f]{24}$/),
name: "read",
arguments: { path: "/tmp/fragmented.md" },
},
]);
});
it("treats DSML-looking text inside a parameter value as payload", async () => {
const model = createDeepSeekCompletionsModel();
const output = createAssistantOutput(model);
const content =
'<DSMLtool_calls><DSMLinvoke name="message">' +
'<DSMLparameter name="text" string="true">' +
"literal <DSMLtool_calls> marker" +
"</DSMLparameter></DSMLinvoke></DSMLtool_calls>";
const chunks = Array.from(content, (char) => makeCompletionsChunk({ content: char }));
chunks.push(makeCompletionsChunk({}, "stop"));
await testing.processOpenAICompletionsStream(streamChunks(chunks), output, model, {
push() {},
});
expect(output.stopReason).toBe("toolUse");
expect(output.content).toEqual([
{
type: "toolCall",
id: expect.stringMatching(/^call_[0-9a-f]{24}$/),
name: "message",
arguments: { text: "literal <DSMLtool_calls> marker" },
},
]);
});
it("ignores an incomplete invoke-like prefix before a valid invoke", async () => {
const model = createDeepSeekCompletionsModel();
const output = createAssistantOutput(model);
const content =
"<|DSML|tool_calls>literal <|DSML|invoke marker " +
'<|DSML|invoke name="read">{"path":"/tmp/valid.md"}</|DSML|invoke>' +
"</|DSML|tool_calls>";
await testing.processOpenAICompletionsStream(
streamChunks(
Array.from(content, (char) => makeCompletionsChunk({ content: char })).concat([
makeCompletionsChunk({}, "stop"),
]),
),
output,
model,
{ push() {} },
);
expect(output.stopReason).toBe("toolUse");
expect(output.content).toEqual([
{
type: "toolCall",
id: expect.stringMatching(/^call_[0-9a-f]{24}$/),
name: "read",
arguments: { path: "/tmp/valid.md" },
},
]);
});
it("does not recover a nested call hidden inside a nameless invoke", async () => {
const model = createDeepSeekCompletionsModel();
const output = createAssistantOutput(model);
const events: CapturedStreamEvent[] = [];
const content =
"<|DSML|tool_calls><|DSML|invoke><|DSML|tool_calls>" +
'<|DSML|invoke name="read">{"path":"/tmp/bypass"}</|DSML|invoke>' +
"</|DSML|tool_calls>";
await testing.processOpenAICompletionsStream(
streamChunks([makeCompletionsChunk({ content }), makeCompletionsChunk({}, "stop")]),
output,
model,
{
push(event) {
events.push(event as CapturedStreamEvent);
},
},
);
expect(output.stopReason).toBe("stop");
expect(events.filter((event) => event.type?.startsWith("toolcall_") === true)).toEqual([]);
expect(output.content.some((part) => part.type === "toolCall")).toBe(false);
});
it("treats DSML-looking text inside JSON arguments as payload", async () => {
const model = createDeepSeekCompletionsModel();
const output = createAssistantOutput(model);
const content =
'<|DSML|tool_calls><|DSML|invoke name="message">' +
'{"text":"literal <|DSML|tool_calls> marker"}' +
"</|DSML|invoke></|DSML|tool_calls>";
await testing.processOpenAICompletionsStream(
streamChunks([makeCompletionsChunk({ content }, "stop")]),
output,
model,
{ push() {} },
);
expect(output.stopReason).toBe("toolUse");
expect(output.content).toEqual([
{
type: "toolCall",
id: expect.stringMatching(/^call_[0-9a-f]{24}$/),
name: "message",
arguments: { text: "literal <|DSML|tool_calls> marker" },
},
]);
});
it.each([
{ finishReason: "length", stopReason: "length" },
{ finishReason: "content_filter", stopReason: "error" },