diff --git a/src/services/api/openaiShim.ts b/src/services/api/openaiShim.ts index 8c2e07e3..962b87d8 100644 --- a/src/services/api/openaiShim.ts +++ b/src/services/api/openaiShim.ts @@ -197,6 +197,14 @@ function convertContentBlocks( return parts } +function isGeminiMode(): boolean { + return ( + isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI) || + (process.env.OPENAI_BASE_URL?.includes('generativelanguage.googleapis.com') ?? + false) + ) +} + function convertMessages( messages: Array<{ role: string; message?: { role?: string; content?: unknown }; content?: unknown }>, system: unknown, @@ -248,6 +256,7 @@ function convertMessages( // Check for tool_use blocks if (Array.isArray(content)) { const toolUses = content.filter((b: { type?: string }) => b.type === 'tool_use') + const thinkingBlock = content.find((b: { type?: string }) => b.type === 'thinking') const textContent = content.filter( (b: { type?: string }) => b.type !== 'tool_use' && b.type !== 'thinking', ) @@ -267,18 +276,46 @@ function convertMessages( name?: string input?: unknown extra_content?: Record - }) => ({ - id: tu.id ?? `call_${crypto.randomUUID().replace(/-/g, '')}`, - type: 'function' as const, - function: { - name: tu.name ?? 'unknown', - arguments: - typeof tu.input === 'string' - ? tu.input - : JSON.stringify(tu.input ?? {}), - }, - ...(tu.extra_content ? { extra_content: tu.extra_content } : {}), - }), + signature?: string + }, index) => { + const toolCall: NonNullable[number] = { + id: tu.id ?? `call_${crypto.randomUUID().replace(/-/g, '')}`, + type: 'function' as const, + function: { + name: tu.name ?? 'unknown', + arguments: + typeof tu.input === 'string' + ? tu.input + : JSON.stringify(tu.input ?? {}), + }, + } + + // Preserve existing extra_content if present + if (tu.extra_content) { + toolCall.extra_content = { ...tu.extra_content } + } + + // Handle Gemini thought_signature + if (isGeminiMode()) { + // If the model provided a signature in the tool_use block itself (e.g. from a previous Turn/Step) + // Use thinkingBlock.signature for ALL tool calls in the same assistant turn if available. + // The API requires the same signature on every replayed function call part in a parallel set. + const signature = tu.signature ?? (thinkingBlock as any)?.signature + + // Merge into existing google-specific metadata if present + const existingGoogle = (toolCall.extra_content?.google as Record) ?? {} + + toolCall.extra_content = { + ...toolCall.extra_content, + google: { + ...existingGoogle, + thought_signature: signature ?? "skip_thought_signature_validator" + } + } + } + + return toolCall + }, ) } @@ -397,7 +434,7 @@ function normalizeSchemaForOpenAI( function convertTools( tools: Array<{ name: string; description?: string; input_schema?: Record }>, ): OpenAITool[] { - const isGemini = isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI) + const isGemini = isGeminiMode() return tools .filter(t => t.name !== 'ToolSearchTool') // Not relevant for OpenAI @@ -593,6 +630,13 @@ async function* openaiStreamToAnthropic( name: tc.function.name, input: {}, ...(tc.extra_content ? { extra_content: tc.extra_content } : {}), + // Extract Gemini signature from extra_content + ...((tc.extra_content?.google as any)?.thought_signature + ? { + signature: (tc.extra_content.google as any) + .thought_signature, + } + : {}), }, } contentBlockIndex++ @@ -930,7 +974,7 @@ class OpenAIShimMessages { ...(options?.headers ?? {}), } - const isGemini = isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI) + const isGemini = isGeminiMode() const apiKey = this.providerOverride?.apiKey ?? process.env.OPENAI_API_KEY ?? '' // Detect Azure endpoints by hostname (not raw URL) to prevent bypass via @@ -1099,6 +1143,10 @@ class OpenAIShimMessages { name: tc.function.name, input, ...(tc.extra_content ? { extra_content: tc.extra_content } : {}), + // Extract Gemini signature from extra_content + ...((tc.extra_content?.google as any)?.thought_signature + ? { signature: (tc.extra_content.google as any).thought_signature } + : {}), }) } }