@@ -662,6 +662,93 @@ describe('resource completions', () => {
662662 expect ( listener . functionCallResults ) . toEqual ( [ `it's raining` ] ) ;
663663 await listener . sanityCheck ( { ignoredMessages : new Set ( [ injectedMessage ] ) } ) ;
664664 } ) ;
665+ test ( 'generates unique IDs for parallel tool calls with empty IDs' , async ( ) => {
666+ const { fetch, handleRequest } = mockChatCompletionFetch ( ) ;
667+
668+ const openai = new OpenAI ( { apiKey : 'something1234' , baseURL : 'http://127.0.0.1:4010' , fetch } ) ;
669+ const runner = openai . chat . completions . runTools ( {
670+ messages : [ { role : 'user' , content : 'echo both values' } ] ,
671+ model : 'gpt-3.5-turbo' ,
672+ tools : [
673+ {
674+ type : 'function' ,
675+ function : {
676+ function : function echo ( value : string ) {
677+ return value ;
678+ } ,
679+ parameters : { } ,
680+ description : 'echoes a value' ,
681+ } ,
682+ } ,
683+ ] ,
684+ } ) ;
685+
686+ await handleRequest ( async ( ) => ( {
687+ id : '1' ,
688+ choices : [
689+ {
690+ index : 0 ,
691+ finish_reason : 'tool_calls' ,
692+ logprobs : null ,
693+ message : {
694+ role : 'assistant' ,
695+ content : null ,
696+ refusal : null ,
697+ tool_calls : [
698+ {
699+ type : 'function' ,
700+ id : '' ,
701+ function : { arguments : 'first' , name : 'echo' } ,
702+ } ,
703+ {
704+ type : 'function' ,
705+ id : '' ,
706+ function : { arguments : 'second' , name : 'echo' } ,
707+ } ,
708+ ] ,
709+ } ,
710+ } ,
711+ ] ,
712+ created : Math . floor ( Date . now ( ) / 1000 ) ,
713+ model : 'gpt-3.5-turbo' ,
714+ object : 'chat.completion' ,
715+ } ) ) ;
716+
717+ await handleRequest ( async ( request ) => {
718+ const assistantMessage = request . messages [ 1 ] ;
719+ if ( assistantMessage ?. role !== 'assistant' || ! assistantMessage . tool_calls ) {
720+ throw new Error ( 'expected an assistant message with tool calls' ) ;
721+ }
722+
723+ const generatedIDs = assistantMessage . tool_calls . map ( ( toolCall ) => toolCall . id ) ;
724+ expect ( generatedIDs ) . toHaveLength ( 2 ) ;
725+ expect ( generatedIDs . every ( ( id ) => id . startsWith ( 'call_' ) ) ) . toBe ( true ) ;
726+ expect ( new Set ( generatedIDs ) . size ) . toBe ( 2 ) ;
727+
728+ const toolMessages = request . messages . slice ( 2 ) ;
729+ expect (
730+ toolMessages . map ( ( message ) => ( message . role === 'tool' ? message . tool_call_id : undefined ) ) ,
731+ ) . toEqual ( generatedIDs ) ;
732+
733+ return {
734+ id : '2' ,
735+ choices : [
736+ {
737+ index : 0 ,
738+ finish_reason : 'stop' ,
739+ logprobs : null ,
740+ message : { role : 'assistant' , content : 'done' , refusal : null } ,
741+ } ,
742+ ] ,
743+ created : Math . floor ( Date . now ( ) / 1000 ) ,
744+ model : 'gpt-3.5-turbo' ,
745+ object : 'chat.completion' ,
746+ } ;
747+ } ) ;
748+
749+ await runner . done ( ) ;
750+ await expect ( runner . finalFunctionToolCallResult ( ) ) . resolves . toBe ( 'second' ) ;
751+ } ) ;
665752 test ( 'runs tool calls concurrently and preserves their result order' , async ( ) => {
666753 const { fetch, handleRequest } = mockChatCompletionFetch ( ) ;
667754
@@ -1827,6 +1914,69 @@ describe('resource completions', () => {
18271914 expect ( listener . eventFunctionCallResults ) . toEqual ( [ `it's raining` ] ) ;
18281915 await listener . sanityCheck ( { ignoredMessages : new Set ( [ injectedMessage ] ) } ) ;
18291916 } ) ;
1917+ test ( 'generates an ID for streamed tool calls with an empty ID' , async ( ) => {
1918+ const { fetch, handleRequest } = mockStreamingChatCompletionFetch ( ) ;
1919+
1920+ const openai = new OpenAI ( { apiKey : 'something1234' , baseURL : 'http://127.0.0.1:4010' , fetch } ) ;
1921+ const runner = openai . chat . completions . runTools ( {
1922+ stream : true ,
1923+ messages : [ { role : 'user' , content : 'tell me what the weather is like' } ] ,
1924+ model : 'gpt-3.5-turbo' ,
1925+ tools : [
1926+ {
1927+ type : 'function' ,
1928+ function : {
1929+ function : function getWeather ( ) {
1930+ return `it's raining` ;
1931+ } ,
1932+ parameters : { } ,
1933+ description : 'gets the weather' ,
1934+ } ,
1935+ } ,
1936+ ] ,
1937+ } ) ;
1938+
1939+ await Promise . all ( [
1940+ handleRequest ( async function * ( ) : AsyncIterable < OpenAI . Chat . ChatCompletionChunk > {
1941+ for ( const choice of functionCallDeltas ( '' , { id : '' , name : 'getWeather' } ) ) {
1942+ yield {
1943+ id : '1' ,
1944+ choices : [ choice ] ,
1945+ created : Math . floor ( Date . now ( ) / 1000 ) ,
1946+ model : 'gpt-3.5-turbo' ,
1947+ object : 'chat.completion.chunk' ,
1948+ } ;
1949+ }
1950+ } ) ,
1951+ handleRequest ( async function * ( request ) : AsyncIterable < OpenAI . Chat . ChatCompletionChunk > {
1952+ const assistantMessage = request . messages [ 1 ] ;
1953+ const toolMessage = request . messages [ 2 ] ;
1954+ if ( assistantMessage ?. role !== 'assistant' || ! assistantMessage . tool_calls ?. [ 0 ] ) {
1955+ throw new Error ( 'expected an assistant message with a tool call' ) ;
1956+ }
1957+ if ( toolMessage ?. role !== 'tool' ) {
1958+ throw new Error ( 'expected a tool result message' ) ;
1959+ }
1960+
1961+ const generatedID = assistantMessage . tool_calls [ 0 ] . id ;
1962+ expect ( generatedID ) . toMatch ( / ^ c a l l _ / ) ;
1963+ expect ( toolMessage . tool_call_id ) . toBe ( generatedID ) ;
1964+
1965+ for ( const choice of contentChoiceDeltas ( `it's raining` ) ) {
1966+ yield {
1967+ id : '2' ,
1968+ choices : [ choice ] ,
1969+ created : Math . floor ( Date . now ( ) / 1000 ) ,
1970+ model : 'gpt-3.5-turbo' ,
1971+ object : 'chat.completion.chunk' ,
1972+ } ;
1973+ }
1974+ } ) ,
1975+ runner . done ( ) ,
1976+ ] ) ;
1977+
1978+ await expect ( runner . finalFunctionToolCallResult ( ) ) . resolves . toBe ( `it's raining` ) ;
1979+ } ) ;
18301980 test ( 'flow with abort' , async ( ) => {
18311981 const { fetch, handleRequest } = mockStreamingChatCompletionFetch ( ) ;
18321982
0 commit comments