From 72e6a945fe1311a0b9eea1033703a74dc6cdbc18 Mon Sep 17 00:00:00 2001 From: hsain9357 <90194836+hsain9357@users.noreply.github.com> Date: Mon, 6 Apr 2026 16:04:49 +0300 Subject: [PATCH] Fixed gemini error Function call is missing a thought_signature in functionCall parts (#426) * docs(docs): add agent guidance and repository instructions - Created `AGENTS.md` and `CLAUDE.md` to provide high-signal guidance for AI agents and developers working in the repository. - Outlined critical developer commands for building, testing, and running diagnostics using `bun`. - Documented the repository architecture, source entrypoints, and core service logic. - Defined framework-specific quirks, including module stubbing for internal modules and macro versioning. - Established style and workflow guidelines regarding telemetry, environment variables, and security scan requirements. * feat(api): support gemini thought signatures in openai shim - Added `isGeminiMode` utility to detect Gemini backends via `CLAUDE_CODE_USE_GEMINI` or `OPENAI_BASE_URL`. - Updated `convertMessages` to extract `thought_signature` from thinking blocks and inject them into tool calls. - Implemented a fallback mechanism that provides a `skip_thought_signature_validator` string to avoid 400 validation errors when a signature is missing. - Enhanced `openaiStreamToAnthropic` and `OpenAIShimMessages` to correctly preserve and pass through Gemini-specific metadata in `extra_content`. * refactor(api): improve gemini metadata handling and remove redundant docs - Updated `src/services/api/openaiShim.ts` to merge existing `google`-specific metadata within `extra_content` instead of overwriting it. - Simplified the `thought_signature` assignment logic to use a fallback value of `skip_thought_signature_validator` when no signature is provided. - Deleted `AGENTS.md` and `CLAUDE.md` files to eliminate redundant agent guidance documentation. * fix(api): propagate gemini thought signatures to all parallel tool calls - Removed the index constraint when assigning the `signature` from a `thinkingBlock` to tool calls in `openaiShim.ts`. - Ensured that the `thought_signature` is applied to every tool call in a parallel set, rather than just the first one. - Aligned the shim with Gemini API requirements, which mandate that the same signature must be present on every replayed function call part within an assistant turn. --- src/services/api/openaiShim.ts | 76 +++++++++++++++++++++++++++------- 1 file changed, 62 insertions(+), 14 deletions(-) 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 } + : {}), }) } }