@@ -111,14 +111,24 @@ function processFunctionResponseArray(
111111 for ( const responseItem of responseArray ) {
112112 if ( "functionResponse" in responseItem ) {
113113 const functionName = responseItem . functionResponse . name
114- const toolCallId = pendingToolCalls . get ( functionName )
115- if ( toolCallId ) {
114+ // Find tool call ID by searching through the map
115+ let matchedToolCallId : string | undefined
116+ for ( const [
117+ toolCallId ,
118+ mappedFunctionName ,
119+ ] of pendingToolCalls . entries ( ) ) {
120+ if ( mappedFunctionName === functionName ) {
121+ matchedToolCallId = toolCallId
122+ break
123+ }
124+ }
125+ if ( matchedToolCallId ) {
116126 messages . push ( {
117127 role : "tool" ,
118- tool_call_id : toolCallId ,
128+ tool_call_id : matchedToolCallId ,
119129 content : JSON . stringify ( responseItem . functionResponse . response ) ,
120130 } )
121- pendingToolCalls . delete ( functionName )
131+ pendingToolCalls . delete ( matchedToolCallId )
122132 }
123133 }
124134 }
@@ -150,14 +160,21 @@ function processFunctionResponses(
150160) : void {
151161 for ( const funcResponse of functionResponses ) {
152162 const functionName = funcResponse . functionResponse . name
153- const toolCallId = pendingToolCalls . get ( functionName )
154- if ( toolCallId ) {
163+ // Find tool call ID by searching through the map
164+ let matchedToolCallId : string | undefined
165+ for ( const [ toolCallId , mappedFunctionName ] of pendingToolCalls . entries ( ) ) {
166+ if ( mappedFunctionName === functionName ) {
167+ matchedToolCallId = toolCallId
168+ break
169+ }
170+ }
171+ if ( matchedToolCallId ) {
155172 messages . push ( {
156173 role : "tool" ,
157- tool_call_id : toolCallId ,
174+ tool_call_id : matchedToolCallId ,
158175 content : JSON . stringify ( funcResponse . functionResponse . response ) ,
159176 } )
160- pendingToolCalls . delete ( functionName )
177+ pendingToolCalls . delete ( matchedToolCallId )
161178 }
162179 }
163180}
@@ -175,7 +192,8 @@ function processFunctionCalls(options: {
175192 const toolCalls = functionCalls . map ( ( call ) => {
176193 const toolCallId = generateToolCallId ( call . functionCall . name )
177194 // Remember this tool call for later matching with responses
178- pendingToolCalls . set ( call . functionCall . name , toolCallId )
195+ // Use tool_call_id as key to avoid duplicate function name overwrites
196+ pendingToolCalls . set ( toolCallId , call . functionCall . name )
179197
180198 return {
181199 id : toolCallId ,
@@ -207,6 +225,8 @@ function mergeConsecutiveSameRoleMessages(
207225 && lastMessage . role === message . role
208226 && ! lastMessage . tool_calls
209227 && ! message . tool_calls
228+ && ! ( lastMessage as { tool_call_id ?: string } ) . tool_call_id // Don't merge tool responses
229+ && ! ( message as { tool_call_id ?: string } ) . tool_call_id // Don't merge tool responses
210230 ) {
211231 // Merge with previous message of same role
212232 if (
@@ -247,7 +267,7 @@ function removeIncompleteAssistantMessages(messages: Array<Message>): void {
247267 }
248268}
249269
250- function translateGeminiContentsToOpenAI (
270+ export function translateGeminiContentsToOpenAI (
251271 contents : Array <
252272 | GeminiContent
253273 | Array < {
@@ -257,7 +277,7 @@ function translateGeminiContentsToOpenAI(
257277 systemInstruction ?: GeminiContent ,
258278) : Array < Message > {
259279 const messages : Array < Message > = [ ]
260- const pendingToolCalls = new Map < string , string > ( ) // function name -> tool_call_id
280+ const pendingToolCalls = new Map < string , string > ( ) // tool_call_id -> function_name
261281
262282 // Add system instruction first if present
263283 if ( systemInstruction ) {
@@ -309,8 +329,11 @@ function translateGeminiContentsToOpenAI(
309329 // Post-process: Remove incomplete assistant messages from cancelled tool calls
310330 removeIncompleteAssistantMessages ( messages )
311331
332+ // Post-process: Deduplicate tool responses (remove duplicate tool_call_ids)
333+ const matchedMessages = ensureToolCallResponseMatch ( messages )
334+
312335 // Post-process: Merge consecutive messages with same role (based on LiteLLM research)
313- return mergeConsecutiveSameRoleMessages ( messages )
336+ return mergeConsecutiveSameRoleMessages ( matchedMessages )
314337}
315338
316339function synthesizeToolsFromContents (
@@ -466,6 +489,32 @@ function translateGeminiToolConfigToOpenAI(
466489
467490// Response translation: OpenAI -> Gemini
468491
492+ // Helper function to deduplicate tool responses - remove duplicate tool_call_ids
493+ // The problem was our logic was CREATING duplicates instead of preventing them
494+
495+ function ensureToolCallResponseMatch ( messages : Array < Message > ) : Array < Message > {
496+ const result : Array < Message > = [ ]
497+ const seenToolCallIds = new Set < string > ( ) // Track processed tool_call_ids to avoid duplicates
498+
499+ for ( const message of messages ) {
500+ if ( message . role === "tool" && message . tool_call_id ) {
501+ const toolCallId = message . tool_call_id
502+
503+ // Only keep the FIRST response for each tool_call_id (deduplicate)
504+ if ( ! seenToolCallIds . has ( toolCallId ) ) {
505+ seenToolCallIds . add ( toolCallId )
506+ result . push ( message )
507+ }
508+ // Skip any duplicate responses for the same tool_call_id
509+ } else {
510+ // Keep all non-tool messages as-is
511+ result . push ( message )
512+ }
513+ }
514+
515+ return result
516+ }
517+
469518export function translateOpenAIToGemini (
470519 response : ChatCompletionResponse ,
471520) : GeminiResponse {
@@ -561,7 +610,12 @@ function processToolCalls(
561610 const parts : Array < GeminiPart > = [ ]
562611
563612 for ( const toolCall of toolCalls ) {
564- if ( ! toolCall . function ?. name ) {
613+ // Enhanced validation: check for empty/whitespace-only names
614+ if (
615+ ! toolCall . function ?. name
616+ || typeof toolCall . function . name !== "string"
617+ || toolCall . function . name . trim ( ) === ""
618+ ) {
565619 continue
566620 }
567621
@@ -723,6 +777,22 @@ export function translateOpenAIChunkToGemini(chunk: ChatCompletionChunk): {
723777 return null
724778 }
725779
780+ // Additional validation - if we only have function call parts with empty names,
781+ // skip this chunk entirely to prevent invalid tool call responses
782+ const hasOnlyEmptyToolCalls =
783+ parts . length > 0
784+ && parts . every ( ( part ) => {
785+ if ( "functionCall" in part ) {
786+ return ! part . functionCall . name || part . functionCall . name . trim ( ) === ""
787+ }
788+ return false
789+ } )
790+ && parts . some ( ( part ) => "functionCall" in part )
791+
792+ if ( hasOnlyEmptyToolCalls && ! choice . finish_reason ) {
793+ return null
794+ }
795+
726796 const shouldInclude = shouldIncludeFinishReason ( choice )
727797 const mappedFinishReason =
728798 shouldInclude ?
0 commit comments