From ff29f220e77b7f4128cd90742f684bd955e53dcc Mon Sep 17 00:00:00 2001 From: Evan Jacobson Date: Wed, 11 Feb 2026 19:36:27 -0700 Subject: [PATCH 1/4] Fix minimax tool call ID issue thru coercion --- src/api/providers/__tests__/minimax.spec.ts | 84 +++++++++++++++++++++ src/api/providers/minimax.ts | 7 +- 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/src/api/providers/__tests__/minimax.spec.ts b/src/api/providers/__tests__/minimax.spec.ts index 96865e689a7..08f514b7741 100644 --- a/src/api/providers/__tests__/minimax.spec.ts +++ b/src/api/providers/__tests__/minimax.spec.ts @@ -377,6 +377,90 @@ describe("MiniMaxHandler", () => { arguments: undefined, }) }) + + it("should coerce numeric tool_use id to string in tool_call_partial (fixes AI_InvalidResponseDataError)", async () => { + // MiniMax API may return content_block.id as a number; downstream expects string. + mockCreate.mockResolvedValueOnce({ + [Symbol.asyncIterator]: () => ({ + next: vitest + .fn() + .mockResolvedValueOnce({ + done: false, + value: { + type: "content_block_start", + index: 0, + content_block: { + type: "tool_use", + id: 42 as unknown as string, + name: "read_file", + input: {}, + }, + }, + }) + .mockResolvedValueOnce({ + done: false, + value: { + type: "content_block_stop", + index: 0, + }, + }) + .mockResolvedValueOnce({ done: true }), + }), + }) + + const stream = handler.createMessage("system prompt", []) + const firstChunk = await stream.next() + + expect(firstChunk.done).toBe(false) + expect(firstChunk.value).toMatchObject({ + type: "tool_call_partial", + index: 0, + id: "42", + name: "read_file", + }) + expect(typeof (firstChunk.value as { id?: string }).id).toBe("string") + }) + + it("should yield undefined id when tool_use has no id", async () => { + mockCreate.mockResolvedValueOnce({ + [Symbol.asyncIterator]: () => ({ + next: vitest + .fn() + .mockResolvedValueOnce({ + done: false, + value: { + type: "content_block_start", + index: 0, + content_block: { + type: "tool_use", + id: undefined, + name: "run_terminal_cmd", + input: {}, + }, + }, + }) + .mockResolvedValueOnce({ + done: false, + value: { + type: "content_block_stop", + index: 0, + }, + }) + .mockResolvedValueOnce({ done: true }), + }), + }) + + const stream = handler.createMessage("system prompt", []) + const firstChunk = await stream.next() + + expect(firstChunk.done).toBe(false) + expect(firstChunk.value).toMatchObject({ + type: "tool_call_partial", + index: 0, + name: "run_terminal_cmd", + }) + expect((firstChunk.value as { id?: string }).id).toBeUndefined() + }) }) describe("Model Configuration", () => { diff --git a/src/api/providers/minimax.ts b/src/api/providers/minimax.ts index 303f5f4b296..a8fb5725523 100644 --- a/src/api/providers/minimax.ts +++ b/src/api/providers/minimax.ts @@ -189,11 +189,14 @@ export class MiniMaxHandler extends BaseProvider implements SingleCompletionHand yield { type: "text", text: chunk.content_block.text } break case "tool_use": { - // Emit initial tool call partial with id and name + // Emit initial tool call partial with id and name. + // MiniMax API may return content_block.id as a number; coerce to string. + const rawId = chunk.content_block.id + const id = rawId != null ? String(rawId) : undefined yield { type: "tool_call_partial", index: chunk.index, - id: chunk.content_block.id, + id, name: chunk.content_block.name, arguments: undefined, } From 3fdd482e20c23aba0c26d574a799772566f13e9a Mon Sep 17 00:00:00 2001 From: Evan Jacobson Date: Wed, 11 Feb 2026 19:49:50 -0700 Subject: [PATCH 2/4] =?UTF-8?q?Move=20the=20coercion=20to=20the=20shared?= =?UTF-8?q?=20level=20=E2=80=94=20avoids=20'whack-a-mole'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/providers/__tests__/minimax.spec.ts | 84 ------------------- src/api/providers/minimax.ts | 7 +- .../assistant-message/NativeToolCallParser.ts | 5 +- .../__tests__/NativeToolCallParser.spec.ts | 45 ++++++++++ 4 files changed, 51 insertions(+), 90 deletions(-) diff --git a/src/api/providers/__tests__/minimax.spec.ts b/src/api/providers/__tests__/minimax.spec.ts index 08f514b7741..96865e689a7 100644 --- a/src/api/providers/__tests__/minimax.spec.ts +++ b/src/api/providers/__tests__/minimax.spec.ts @@ -377,90 +377,6 @@ describe("MiniMaxHandler", () => { arguments: undefined, }) }) - - it("should coerce numeric tool_use id to string in tool_call_partial (fixes AI_InvalidResponseDataError)", async () => { - // MiniMax API may return content_block.id as a number; downstream expects string. - mockCreate.mockResolvedValueOnce({ - [Symbol.asyncIterator]: () => ({ - next: vitest - .fn() - .mockResolvedValueOnce({ - done: false, - value: { - type: "content_block_start", - index: 0, - content_block: { - type: "tool_use", - id: 42 as unknown as string, - name: "read_file", - input: {}, - }, - }, - }) - .mockResolvedValueOnce({ - done: false, - value: { - type: "content_block_stop", - index: 0, - }, - }) - .mockResolvedValueOnce({ done: true }), - }), - }) - - const stream = handler.createMessage("system prompt", []) - const firstChunk = await stream.next() - - expect(firstChunk.done).toBe(false) - expect(firstChunk.value).toMatchObject({ - type: "tool_call_partial", - index: 0, - id: "42", - name: "read_file", - }) - expect(typeof (firstChunk.value as { id?: string }).id).toBe("string") - }) - - it("should yield undefined id when tool_use has no id", async () => { - mockCreate.mockResolvedValueOnce({ - [Symbol.asyncIterator]: () => ({ - next: vitest - .fn() - .mockResolvedValueOnce({ - done: false, - value: { - type: "content_block_start", - index: 0, - content_block: { - type: "tool_use", - id: undefined, - name: "run_terminal_cmd", - input: {}, - }, - }, - }) - .mockResolvedValueOnce({ - done: false, - value: { - type: "content_block_stop", - index: 0, - }, - }) - .mockResolvedValueOnce({ done: true }), - }), - }) - - const stream = handler.createMessage("system prompt", []) - const firstChunk = await stream.next() - - expect(firstChunk.done).toBe(false) - expect(firstChunk.value).toMatchObject({ - type: "tool_call_partial", - index: 0, - name: "run_terminal_cmd", - }) - expect((firstChunk.value as { id?: string }).id).toBeUndefined() - }) }) describe("Model Configuration", () => { diff --git a/src/api/providers/minimax.ts b/src/api/providers/minimax.ts index a8fb5725523..303f5f4b296 100644 --- a/src/api/providers/minimax.ts +++ b/src/api/providers/minimax.ts @@ -189,14 +189,11 @@ export class MiniMaxHandler extends BaseProvider implements SingleCompletionHand yield { type: "text", text: chunk.content_block.text } break case "tool_use": { - // Emit initial tool call partial with id and name. - // MiniMax API may return content_block.id as a number; coerce to string. - const rawId = chunk.content_block.id - const id = rawId != null ? String(rawId) : undefined + // Emit initial tool call partial with id and name yield { type: "tool_call_partial", index: chunk.index, - id, + id: chunk.content_block.id, name: chunk.content_block.name, arguments: undefined, } diff --git a/src/core/assistant-message/NativeToolCallParser.ts b/src/core/assistant-message/NativeToolCallParser.ts index fd80c4e84ee..66ca4e37efc 100644 --- a/src/core/assistant-message/NativeToolCallParser.ts +++ b/src/core/assistant-message/NativeToolCallParser.ts @@ -87,7 +87,10 @@ export class NativeToolCallParser { arguments?: string }): ToolCallStreamEvent[] { const events: ToolCallStreamEvent[] = [] - const { index, id, name, arguments: args } = chunk + const { index, id: rawId, name, arguments: args } = chunk + + // Some providers (e.g. MiniMax) return tool call id as a number; coerce to string. + const id = rawId != null ? String(rawId) : undefined let tracked = this.rawChunkTracker.get(index) diff --git a/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts b/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts index 0e81671cc15..02b648be51f 100644 --- a/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts +++ b/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts @@ -238,4 +238,49 @@ describe("NativeToolCallParser", () => { }) }) }) + + describe("processRawChunk", () => { + it("should coerce numeric tool call id to string", () => { + const events = NativeToolCallParser.processRawChunk({ + index: 0, + id: 42 as unknown as string, + name: "read_file", + arguments: '{"path":"test.ts"}', + }) + + expect(events).toHaveLength(2) // start + delta + expect(events[0]).toMatchObject({ + type: "tool_call_start", + id: "42", + name: "read_file", + }) + expect(typeof events[0].id).toBe("string") + }) + + it("should leave undefined id as undefined", () => { + const events = NativeToolCallParser.processRawChunk({ + index: 0, + id: undefined, + name: "read_file", + }) + + // No id means no tracking is initialized, so no events emitted + expect(events).toHaveLength(0) + }) + + it("should pass through string id unchanged", () => { + const events = NativeToolCallParser.processRawChunk({ + index: 0, + id: "call_abc123", + name: "read_file", + }) + + expect(events).toHaveLength(1) + expect(events[0]).toMatchObject({ + type: "tool_call_start", + id: "call_abc123", + name: "read_file", + }) + }) + }) }) From e33a2ef4ae339deb52f4cfe8b3acd68ca4103345 Mon Sep 17 00:00:00 2001 From: Evan Jacobson Date: Wed, 11 Feb 2026 22:34:06 -0700 Subject: [PATCH 3/4] changeset --- .changeset/thin-forks-draw.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/thin-forks-draw.md diff --git a/.changeset/thin-forks-draw.md b/.changeset/thin-forks-draw.md new file mode 100644 index 00000000000..87a9623491b --- /dev/null +++ b/.changeset/thin-forks-draw.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Fix tool use failure for providers returning numeric tool call IDs (e.g. MiniMax) by coercing ID to string in the shared stream parser From 27f971558b21b4777ba559a22d4c0feefc76d849 Mon Sep 17 00:00:00 2001 From: Kevin van Dijk Date: Fri, 13 Feb 2026 11:15:34 +0100 Subject: [PATCH 4/4] Add markers --- src/core/assistant-message/NativeToolCallParser.ts | 3 ++- .../assistant-message/__tests__/NativeToolCallParser.spec.ts | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/assistant-message/NativeToolCallParser.ts b/src/core/assistant-message/NativeToolCallParser.ts index 66ca4e37efc..c1bd66facd7 100644 --- a/src/core/assistant-message/NativeToolCallParser.ts +++ b/src/core/assistant-message/NativeToolCallParser.ts @@ -87,10 +87,11 @@ export class NativeToolCallParser { arguments?: string }): ToolCallStreamEvent[] { const events: ToolCallStreamEvent[] = [] + // kilocode_change start: Some providers (e.g. MiniMax) return tool call id as a number; coerce to string. const { index, id: rawId, name, arguments: args } = chunk - // Some providers (e.g. MiniMax) return tool call id as a number; coerce to string. const id = rawId != null ? String(rawId) : undefined + // kilocode_change end let tracked = this.rawChunkTracker.get(index) diff --git a/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts b/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts index 02b648be51f..1627b1005fb 100644 --- a/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts +++ b/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts @@ -239,6 +239,7 @@ describe("NativeToolCallParser", () => { }) }) + // kilocode_change start describe("processRawChunk", () => { it("should coerce numeric tool call id to string", () => { const events = NativeToolCallParser.processRawChunk({ @@ -283,4 +284,5 @@ describe("NativeToolCallParser", () => { }) }) }) + // kilocode_change end })