





















@@ -47,6 +47,86 @@ async function invokeProviderStream(params: {
4747return await Promise.resolve(streamFn({} as never, {} as never, {} as never));
4848}
494950+type ToolCallRepairCaseResult = {
51+partialArgs: unknown;
52+streamedArgs: unknown;
53+endMessageArgs: unknown;
54+finalArgs: unknown;
55+result: unknown;
56+finalMessage: unknown;
57+};
58+59+async function runToolCallRepairCase(params: {
60+toolName?: string;
61+delta: string;
62+provider?: string;
63+modelApi?: string;
64+}): Promise<ToolCallRepairCaseResult> {
65+const toolName = params.toolName ?? "write";
66+const partialToolCall = { type: "functionCall", name: toolName, arguments: {} };
67+const streamedToolCall = { type: "functionCall", name: toolName, arguments: {} };
68+const endMessageToolCall = { type: "functionCall", name: toolName, arguments: {} };
69+const finalToolCall = { type: "functionCall", name: toolName, arguments: {} };
70+const partialMessage = { role: "assistant", content: [partialToolCall] };
71+const endMessage = { role: "assistant", content: [endMessageToolCall] };
72+const finalMessage = { role: "assistant", content: [finalToolCall] };
73+74+const stream = await invokeProviderStream({
75+provider: params.provider ?? "openai-compatible",
76+modelApi: params.modelApi ?? "openai-completions",
77+baseFn: () =>
78+createFakeStream({
79+events: [
80+{
81+type: "toolcall_delta",
82+contentIndex: 0,
83+delta: `.functions.${toolName}:0 `,
84+partial: partialMessage,
85+},
86+{
87+type: "toolcall_delta",
88+contentIndex: 0,
89+delta: params.delta,
90+partial: partialMessage,
91+},
92+{
93+type: "toolcall_end",
94+contentIndex: 0,
95+toolCall: streamedToolCall,
96+partial: partialMessage,
97+message: endMessage,
98+},
99+],
100+resultMessage: finalMessage,
101+}),
102+});
103+104+for await (const item of stream) {
105+// drain
106+}
107+const result = await stream.result();
108+109+return {
110+partialArgs: partialToolCall.arguments,
111+streamedArgs: streamedToolCall.arguments,
112+endMessageArgs: endMessageToolCall.arguments,
113+finalArgs: finalToolCall.arguments,
114+ result,
115+ finalMessage,
116+};
117+}
118+119+function expectAllToolCallArgs(
120+result: ToolCallRepairCaseResult,
121+expectedArgs: Record<string, unknown>,
122+): void {
123+expect(result.partialArgs).toEqual(expectedArgs);
124+expect(result.streamedArgs).toEqual(expectedArgs);
125+expect(result.endMessageArgs).toEqual(expectedArgs);
126+expect(result.finalArgs).toEqual(expectedArgs);
127+expect(result.result).toBe(result.finalMessage);
128+}
129+50130describe("shouldRepairMalformedToolCallArguments", () => {
51131it("keeps the repair enabled for kimi providers on anthropic-messages", () => {
52132expect(
@@ -182,4 +262,79 @@ describe("openai-completions malformed tool-call argument repair", () => {
182262expect(result).toBe(finalMessage);
183263},
184264);
265+266+it("repairs smart-quoted edit args with CJK, markdown, and inner smart quotes", async () => {
267+const expectedContent =
268+'更新 **草稿** with “smart”, “sure” and code "x"\nJSON-ish “alpha”, “path”: “ignored” snippet\nSee [“quoted”](https://example.test)\nconst re = /\\d+/;\n内部内容';
269+const result = await runToolCallRepairCase({
270+toolName: "edit",
271+delta: String.raw` {“path”:“notes/报告.md”,“oldText”:“旧的 **草稿**”,“newText”:“更新 **草稿** with “smart”, “sure” and code "x"
272+JSON-ish “alpha”, “path”: “ignored” snippet
273+See [“quoted”](https://example.test)
274+const re = /\d+/;
275+内部内容”}`,
276+});
277+278+expectAllToolCallArgs(result, {
279+path: "notes/报告.md",
280+oldText: "旧的 **草稿**",
281+newText: expectedContent,
282+});
283+});
284+285+it("preserves smart quotes inside ASCII-delimited JSON content with trailing junk", async () => {
286+const result = await runToolCallRepairCase({
287+toolName: "read",
288+delta: '{"path":"notes/日志.md","content":"包含“内部”与 **重点** 字样"}x',
289+});
290+291+expectAllToolCallArgs(result, {
292+path: "notes/日志.md",
293+content: "包含“内部”与 **重点** 字样",
294+});
295+});
296+297+it("repairs smart-quoted command args that use workdir", async () => {
298+const result = await runToolCallRepairCase({
299+toolName: "exec",
300+delta: "{“command“:“pwd“,“workdir“:“/tmp“}",
301+});
302+303+expectAllToolCallArgs(result, { command: "pwd", workdir: "/tmp" });
304+});
305+306+it("keeps duplicate-looking smart-quoted args inside content", async () => {
307+const result = await runToolCallRepairCase({
308+delta: String.raw` {“path”:“safe.txt”,“content”:“text ”, “path”: “other.txt””}`,
309+});
310+311+expectAllToolCallArgs(result, {
312+path: "safe.txt",
313+content: "text ”, “path”: “other.txt”",
314+});
315+});
316+317+it("keeps unknown member-looking prose inside smart-quoted content", async () => {
318+const result = await runToolCallRepairCase({
319+delta: String.raw` {“path”:“safe.txt”,“content”:“Use ”, “foo”: “bar” in prose”}`,
320+});
321+322+expectAllToolCallArgs(result, {
323+path: "safe.txt",
324+content: "Use ”, “foo”: “bar” in prose",
325+});
326+expect(result.finalArgs).not.toHaveProperty("foo");
327+});
328+329+it("keeps member-looking prose inside mixed ASCII-key smart-quoted content", async () => {
330+const result = await runToolCallRepairCase({
331+delta: String.raw` {"path":"safe.txt","content":“Use ”, “foo”: “bar” in prose”}`,
332+});
333+334+expectAllToolCallArgs(result, {
335+path: "safe.txt",
336+content: "Use ”, “foo”: “bar” in prose",
337+});
338+expect(result.finalArgs).not.toHaveProperty("foo");
339+});
185340});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。