@@ -5,6 +5,7 @@ import { setupServer } from "msw/node";
55import { afterAll , afterEach , beforeAll , describe , expect , it } from "vitest" ;
66import { z } from "zod/v4" ;
77import { createWorkersAI } from "../src/index" ;
8+ import { toWorkersAIToolCallId } from "../src/utils" ;
89
910const TEST_ACCOUNT_ID = "test-account-id" ;
1011const TEST_API_KEY = "test-api-key" ;
@@ -206,7 +207,8 @@ describe("REST API - Streaming Text Tests", () => {
206207
207208 expect ( toolCalls ) . toHaveLength ( 1 ) ;
208209 expect ( toolCalls [ 0 ] . toolName ) . toBe ( "get_weather" ) ;
209- expect ( toolCalls [ 0 ] . toolCallId ) . toBe ( "call123" ) ;
210+ expect ( toolCalls [ 0 ] . toolCallId ) . not . toBe ( "call123" ) ;
211+ expect ( toWorkersAIToolCallId ( toolCalls [ 0 ] . toolCallId ) ) . toBe ( "call123" ) ;
210212 expect ( await result . finishReason ) . toBe ( "tool-calls" ) ;
211213 } ) ;
212214
@@ -263,7 +265,8 @@ describe("REST API - Streaming Text Tests", () => {
263265
264266 expect ( toolCalls ) . toHaveLength ( 1 ) ;
265267 expect ( toolCalls [ 0 ] . toolName ) . toBe ( "get_weather" ) ;
266- expect ( toolCalls [ 0 ] . toolCallId ) . toBe ( "chatcmpl-tool-abc" ) ;
268+ expect ( toolCalls [ 0 ] . toolCallId ) . not . toBe ( "chatcmpl-tool-abc" ) ;
269+ expect ( toWorkersAIToolCallId ( toolCalls [ 0 ] . toolCallId ) ) . toBe ( "chatcmpl-tool-abc" ) ;
267270 expect ( await result . finishReason ) . toBe ( "tool-calls" ) ;
268271 } ) ;
269272
@@ -496,7 +499,8 @@ describe("Binding - Streaming Text Tests", () => {
496499
497500 expect ( toolCalls ) . toHaveLength ( 1 ) ;
498501 expect ( toolCalls [ 0 ] . toolName ) . toBe ( "get_weather" ) ;
499- expect ( toolCalls [ 0 ] . toolCallId ) . toBe ( "call_abc" ) ;
502+ expect ( toolCalls [ 0 ] . toolCallId ) . not . toBe ( "call_abc" ) ;
503+ expect ( toWorkersAIToolCallId ( toolCalls [ 0 ] . toolCallId ) ) . toBe ( "call_abc" ) ;
500504 } ) ;
501505
502506 it ( "should handle streamed multiple tool calls via binding" , async ( ) => {
@@ -575,6 +579,98 @@ describe("Binding - Streaming Text Tests", () => {
575579 expect ( toolCalls [ 1 ] . toolName ) . toBe ( "get_temperature" ) ;
576580 } ) ;
577581
582+ it ( "should rewrite repeated Kimi-style tool call IDs across turns and restore originals in prompts" , async ( ) => {
583+ const capturedInputs : any [ ] = [ ] ;
584+ const workersai = createWorkersAI ( {
585+ binding : {
586+ run : async ( _modelName : string , inputs : any ) => {
587+ capturedInputs . push ( inputs ) ;
588+ return mockStream ( [
589+ {
590+ tool_calls : [
591+ {
592+ id : "functions.list_toolbox_tools:0" ,
593+ type : "function" ,
594+ index : 0 ,
595+ function : { name : "list_toolbox_tools" , arguments : "{}" } ,
596+ } ,
597+ ] ,
598+ } ,
599+ { finish_reason : "tool_calls" } ,
600+ "[DONE]" ,
601+ ] ) ;
602+ } ,
603+ } ,
604+ } ) ;
605+
606+ const model = workersai ( TEST_MODEL ) ;
607+ const tools = {
608+ list_toolbox_tools : {
609+ description : "List tools" ,
610+ inputSchema : z . object ( { } ) ,
611+ } ,
612+ } ;
613+
614+ const first = streamText ( {
615+ model,
616+ messages : [ { role : "user" , content : "first" } ] ,
617+ tools,
618+ } ) ;
619+ const firstToolCalls : any [ ] = [ ] ;
620+ for await ( const chunk of first . fullStream ) {
621+ if ( chunk . type === "tool-call" ) firstToolCalls . push ( chunk ) ;
622+ }
623+
624+ const second = streamText ( {
625+ model,
626+ messages : [
627+ { role : "user" , content : "first" } ,
628+ {
629+ role : "assistant" ,
630+ content : [
631+ {
632+ type : "tool-call" ,
633+ toolCallId : firstToolCalls [ 0 ] . toolCallId ,
634+ toolName : "list_toolbox_tools" ,
635+ input : { } ,
636+ } ,
637+ ] ,
638+ } ,
639+ {
640+ role : "tool" ,
641+ content : [
642+ {
643+ type : "tool-result" ,
644+ toolCallId : firstToolCalls [ 0 ] . toolCallId ,
645+ toolName : "list_toolbox_tools" ,
646+ output : { type : "text" , value : "[]" } ,
647+ } ,
648+ ] ,
649+ } ,
650+ { role : "user" , content : "second" } ,
651+ ] as any ,
652+ tools,
653+ } ) ;
654+ const secondToolCalls : any [ ] = [ ] ;
655+ for await ( const chunk of second . fullStream ) {
656+ if ( chunk . type === "tool-call" ) secondToolCalls . push ( chunk ) ;
657+ }
658+
659+ expect ( firstToolCalls ) . toHaveLength ( 1 ) ;
660+ expect ( secondToolCalls ) . toHaveLength ( 1 ) ;
661+ expect ( firstToolCalls [ 0 ] . toolCallId ) . not . toBe ( secondToolCalls [ 0 ] . toolCallId ) ;
662+ expect ( toWorkersAIToolCallId ( firstToolCalls [ 0 ] . toolCallId ) ) . toBe (
663+ "functions.list_toolbox_tools:0" ,
664+ ) ;
665+ expect ( toWorkersAIToolCallId ( secondToolCalls [ 0 ] . toolCallId ) ) . toBe (
666+ "functions.list_toolbox_tools:0" ,
667+ ) ;
668+ expect ( capturedInputs [ 1 ] . messages [ 1 ] . tool_calls [ 0 ] . id ) . toBe (
669+ "functions.list_toolbox_tools:0" ,
670+ ) ;
671+ expect ( capturedInputs [ 1 ] . messages [ 2 ] . tool_call_id ) . toBe ( "functions.list_toolbox_tools:0" ) ;
672+ } ) ;
673+
578674 it ( "should handle streamed OpenAI-format tool calls with reasoning via binding" , async ( ) => {
579675 const workersai = createWorkersAI ( {
580676 binding : {
@@ -649,7 +745,8 @@ describe("Binding - Streaming Text Tests", () => {
649745 expect ( reasoning ) . toBe ( "Let me check the weather." ) ;
650746 expect ( toolCalls ) . toHaveLength ( 1 ) ;
651747 expect ( toolCalls [ 0 ] . toolName ) . toBe ( "get_weather" ) ;
652- expect ( toolCalls [ 0 ] . toolCallId ) . toBe ( "chatcmpl-tool-abc" ) ;
748+ expect ( toolCalls [ 0 ] . toolCallId ) . not . toBe ( "chatcmpl-tool-abc" ) ;
749+ expect ( toWorkersAIToolCallId ( toolCalls [ 0 ] . toolCallId ) ) . toBe ( "chatcmpl-tool-abc" ) ;
653750 expect ( await result . finishReason ) . toBe ( "tool-calls" ) ;
654751 } ) ;
655752
@@ -1583,7 +1680,17 @@ describe("Incremental Tool Call Streaming", () => {
15831680 const toolCall = parts . find ( ( p ) => p . type === "tool-call" ) ;
15841681 expect ( toolCall ) . toBeDefined ( ) ;
15851682 expect ( toolCall . toolName ) . toBe ( "get_weather" ) ;
1586- expect ( toolCall . toolCallId ) . toBe ( "call1" ) ;
1683+ expect ( toolCall . toolCallId ) . not . toBe ( "call1" ) ;
1684+ expect ( toWorkersAIToolCallId ( toolCall . toolCallId ) ) . toBe ( "call1" ) ;
1685+
1686+ const toolEventIds = parts
1687+ . filter ( ( p ) =>
1688+ [ "tool-input-start" , "tool-input-delta" , "tool-input-end" , "tool-call" ] . includes (
1689+ p . type ,
1690+ ) ,
1691+ )
1692+ . map ( ( p ) => p . toolCallId ?? p . id ) ;
1693+ expect ( new Set ( toolEventIds ) ) . toEqual ( new Set ( [ toolCall . toolCallId ] ) ) ;
15871694
15881695 // The AI SDK assembles and parses the full arguments from incremental events
15891696 const args = toolCall . args ?? toolCall . input ;
@@ -2242,7 +2349,8 @@ describe("Eager tool-input-end streaming (issue #488)", () => {
22422349 "tool-input-end" ,
22432350 "tool-call" ,
22442351 ] ) ;
2245- expect ( toolCallData [ 0 ] . toolCallId ) . toBe ( "solo" ) ;
2352+ expect ( toolCallData [ 0 ] . toolCallId ) . not . toBe ( "solo" ) ;
2353+ expect ( toWorkersAIToolCallId ( toolCallData [ 0 ] . toolCallId ) ) . toBe ( "solo" ) ;
22462354 expect ( toolCallData [ 0 ] . toolName ) . toBe ( "get_weather" ) ;
22472355 } ) ;
22482356
@@ -2498,8 +2606,10 @@ describe("Eager tool-input-end streaming (issue #488)", () => {
24982606 "tool-call" ,
24992607 ] ) ;
25002608
2501- expect ( toolCalls [ 0 ] . toolCallId ) . toBe ( "call_1" ) ;
2502- expect ( toolCalls [ 1 ] . toolCallId ) . toBe ( "call_2" ) ;
2609+ expect ( toolCalls [ 0 ] . toolCallId ) . not . toBe ( "call_1" ) ;
2610+ expect ( toolCalls [ 1 ] . toolCallId ) . not . toBe ( "call_2" ) ;
2611+ expect ( toWorkersAIToolCallId ( toolCalls [ 0 ] . toolCallId ) ) . toBe ( "call_1" ) ;
2612+ expect ( toWorkersAIToolCallId ( toolCalls [ 1 ] . toolCallId ) ) . toBe ( "call_2" ) ;
25032613 } ) ;
25042614
25052615 it ( "should not double-close a tool call (finalization + new index)" , async ( ) => {
@@ -2639,8 +2749,10 @@ describe("Eager tool-input-end streaming (issue #488)", () => {
26392749 "tool-input-end" ,
26402750 "tool-call" ,
26412751 ] ) ;
2642- expect ( toolCalls [ 0 ] . toolCallId ) . toBe ( "call_1" ) ;
2643- expect ( toolCalls [ 1 ] . toolCallId ) . toBe ( "call_2" ) ;
2752+ expect ( toolCalls [ 0 ] . toolCallId ) . not . toBe ( "call_1" ) ;
2753+ expect ( toolCalls [ 1 ] . toolCallId ) . not . toBe ( "call_2" ) ;
2754+ expect ( toWorkersAIToolCallId ( toolCalls [ 0 ] . toolCallId ) ) . toBe ( "call_1" ) ;
2755+ expect ( toWorkersAIToolCallId ( toolCalls [ 1 ] . toolCallId ) ) . toBe ( "call_2" ) ;
26442756 } ) ;
26452757
26462758 it ( "should handle three sequential OpenAI-format tool calls with eager close" , async ( ) => {
@@ -2736,9 +2848,12 @@ describe("Eager tool-input-end streaming (issue #488)", () => {
27362848 }
27372849
27382850 expect ( toolCalls ) . toHaveLength ( 3 ) ;
2739- expect ( toolCalls [ 0 ] . toolCallId ) . toBe ( "call_1" ) ;
2740- expect ( toolCalls [ 1 ] . toolCallId ) . toBe ( "call_2" ) ;
2741- expect ( toolCalls [ 2 ] . toolCallId ) . toBe ( "call_3" ) ;
2851+ expect ( toolCalls [ 0 ] . toolCallId ) . not . toBe ( "call_1" ) ;
2852+ expect ( toolCalls [ 1 ] . toolCallId ) . not . toBe ( "call_2" ) ;
2853+ expect ( toolCalls [ 2 ] . toolCallId ) . not . toBe ( "call_3" ) ;
2854+ expect ( toWorkersAIToolCallId ( toolCalls [ 0 ] . toolCallId ) ) . toBe ( "call_1" ) ;
2855+ expect ( toWorkersAIToolCallId ( toolCalls [ 1 ] . toolCallId ) ) . toBe ( "call_2" ) ;
2856+ expect ( toWorkersAIToolCallId ( toolCalls [ 2 ] . toolCallId ) ) . toBe ( "call_3" ) ;
27422857
27432858 const toolEvents = events . filter ( ( e ) =>
27442859 [ "tool-input-start" , "tool-input-end" , "tool-call" ] . includes ( e ) ,
0 commit comments