diff --git a/src/api/providers/utils/__tests__/kimi-tool-call-extractor.spec.ts b/src/api/providers/utils/__tests__/kimi-tool-call-extractor.spec.ts index d85b6cb519..85ef680a19 100644 --- a/src/api/providers/utils/__tests__/kimi-tool-call-extractor.spec.ts +++ b/src/api/providers/utils/__tests__/kimi-tool-call-extractor.spec.ts @@ -7,11 +7,16 @@ import { describe("kimi-tool-call-extractor", () => { describe("hasKimiEmbeddedToolCalls", () => { - it("should return true when content contains tool call markers", () => { + it("should return true when content contains tool call markers with delimiters", () => { const content = "Some reasoning <|tool_calls_section_begin|> stuff <|tool_calls_section_end|>" expect(hasKimiEmbeddedToolCalls(content)).toBe(true) }) + it("should return true when content contains tool call markers without delimiters", () => { + const content = "Some reasoning tool_calls_section_begin stuff tool_calls_section_end" + expect(hasKimiEmbeddedToolCalls(content)).toBe(true) + }) + it("should return false when content does not contain tool call markers", () => { const content = "Just regular reasoning content without any tool calls" expect(hasKimiEmbeddedToolCalls(content)).toBe(false) @@ -23,7 +28,7 @@ describe("kimi-tool-call-extractor", () => { }) describe("extractKimiToolCalls", () => { - it("should extract single tool call from reasoning content", () => { + it("should extract single tool call from reasoning content with delimiters", () => { const content = `Some reasoning here <|tool_calls_section_begin|> <|tool_call_begin|>functions.read_file:0<|tool_call_argument_begin|>{"files":[{"path":"test.txt"}]}<|tool_call_end|> @@ -41,7 +46,30 @@ More content after` arguments: '{"files":[{"path":"test.txt"}]}', }, }) - expect(result.cleanedReasoningContent).not.toContain("<|tool_calls_section_begin|>") + expect(result.cleanedReasoningContent).not.toContain("tool_calls_section_begin") + expect(result.cleanedReasoningContent).toContain("Some reasoning here") + expect(result.cleanedReasoningContent).toContain("More content after") + }) + + it("should extract single tool call from reasoning content without delimiters", () => { + const content = `Some reasoning here +tool_calls_section_begin +tool_call_begin functions.read_file:0 tool_call_argument_begin {"files":[{"path":"test.txt"}]} tool_call_end +tool_calls_section_end +More content after` + + const result = extractKimiToolCalls(content) + + expect(result.toolCalls).toHaveLength(1) + expect(result.toolCalls[0]).toEqual({ + id: "kimi-functions.read_file:0", + type: "function", + function: { + name: "read_file", + arguments: '{"files":[{"path":"test.txt"}]}', + }, + }) + expect(result.cleanedReasoningContent).not.toContain("tool_calls_section_begin") expect(result.cleanedReasoningContent).toContain("Some reasoning here") expect(result.cleanedReasoningContent).toContain("More content after") }) @@ -110,9 +138,9 @@ More content after` const result = extractKimiToolCalls(content) // The cleaned content should not contain tool call markers - expect(result.cleanedReasoningContent).not.toContain("<|tool_calls_section_begin|>") - expect(result.cleanedReasoningContent).not.toContain("<|tool_calls_section_end|>") - expect(result.cleanedReasoningContent).not.toContain("<|tool_call_begin|>") + expect(result.cleanedReasoningContent).not.toContain("tool_calls_section_begin") + expect(result.cleanedReasoningContent).not.toContain("tool_calls_section_end") + expect(result.cleanedReasoningContent).not.toContain("tool_call_begin") expect(result.cleanedReasoningContent).toContain("Before tool calls") expect(result.cleanedReasoningContent).toContain("After tool calls") }) @@ -147,6 +175,18 @@ End reasoning` expect(result.toolCalls[0].function.name).toBe("tool1") expect(result.toolCalls[1].function.name).toBe("tool2") }) + + it("should handle markers without delimiters", () => { + const content = `Some reasoning +tool_calls_section_begin +tool_call_begin functions.read_file:0 tool_call_argument_begin {"path":"test.txt"} tool_call_end +tool_calls_section_end` + + const result = extractKimiToolCalls(content) + + expect(result.toolCalls).toHaveLength(1) + expect(result.toolCalls[0].function.name).toBe("read_file") + }) }) describe("isKimiThinkingModel", () => { diff --git a/src/api/providers/utils/kimi-tool-call-extractor.ts b/src/api/providers/utils/kimi-tool-call-extractor.ts index d89546e082..740e59cf6e 100644 --- a/src/api/providers/utils/kimi-tool-call-extractor.ts +++ b/src/api/providers/utils/kimi-tool-call-extractor.ts @@ -6,6 +6,9 @@ * - <|tool_call_begin|> ... <|tool_call_end|> wraps each individual tool call * - <|tool_call_argument_begin|> marks the start of arguments JSON * + * Note: The model may output these markers with or without the <| |> delimiters, + * so patterns are designed to handle both formats. + * * Format example: * <|tool_calls_section_begin|> * <|tool_call_begin|>functions.read_file:0<|tool_call_argument_begin|>{"files":[{"path":"test.txt"}]}<|tool_call_end|> @@ -36,9 +39,11 @@ export interface KimiToolCallExtractionResult { /** * Checks if the content contains Kimi K2 Thinking model's embedded tool call markers. + * Note: The model may output markers with or without <| |> delimiters, so we check + * for the core marker text only. */ export function hasKimiEmbeddedToolCalls(content: string): boolean { - return content.includes("<|tool_calls_section_begin|>") + return content.includes("tool_calls_section_begin") } /** @@ -58,7 +63,8 @@ export function extractKimiToolCalls(content: string): KimiToolCallExtractionRes const toolCalls: KimiToolCall[] = [] // Pattern to match tool call sections - const sectionPattern = /<\|tool_calls_section_begin\|>(.*?)<\|tool_calls_section_end\|>/gs + // Note: Handles both <|marker|> and marker formats (delimiters are optional) + const sectionPattern = /(?:<\|)?tool_calls_section_begin(?:\|>)?(.*?)(?:<\|)?tool_calls_section_end(?:\|>)?/gs const toolCallSections = content.match(sectionPattern) if (!toolCallSections || toolCallSections.length === 0) { @@ -70,8 +76,9 @@ export function extractKimiToolCalls(content: string): KimiToolCallExtractionRes // Pattern to extract individual tool calls // Format: <|tool_call_begin|>functions.tool_name:index<|tool_call_argument_begin|>JSON_ARGS<|tool_call_end|> + // Note: Handles both <|marker|> and marker formats (delimiters are optional) const funcCallPattern = - /<\|tool_call_begin\|>\s*([\w.]+:\d+)\s*<\|tool_call_argument_begin\|>\s*(.*?)\s*<\|tool_call_end\|>/gs + /(?:<\|)?tool_call_begin(?:\|>)?\s*([\w.]+:\d+)\s*(?:<\|)?tool_call_argument_begin(?:\|>)?\s*(.*?)\s*(?:<\|)?tool_call_end(?:\|>)?/gs for (const section of toolCallSections) { let match