From 53a5edda1cb5671307d82ebf154a3ff7c2ed958d Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Mon, 12 Jan 2026 11:20:05 -0500 Subject: [PATCH] fix: correct Gemini 3 thought signature injection format via OpenRouter Fix 'Thought signature is not valid' error when using Gemini 3 models via OpenRouter with native tool calling. The issue was caused by creating multiple reasoning_details entries (one per tool call), but OpenRouter/Gemini expects ONE entry per assistant turn with the first tool call's ID and index 0. Changes: - Create ONE reasoning.encrypted block per assistant message with tool calls - Use first tool call's ID instead of each tool call's individual ID - Always set index to 0 instead of incrementing indices This matches the format documented by OpenRouter (Toven, Nov 2025) and aligns with the native Gemini provider's behavior in gemini-format.ts. Closes ROO-494 --- src/api/providers/openrouter.ts | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 8f56cddc5c8..d435a05618e 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -220,7 +220,8 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH // even if you don't request them. This is not the default for // other providers (including Gemini), so we need to explicitly disable // them unless the user has explicitly configured reasoning. - // Note: Gemini 3 models use reasoning_details format and should not be excluded. + // Note: Gemini 3 models use reasoning_details format with thought signatures, + // but we handle this via skip_thought_signature_validator injection below. if ( (modelId === "google/gemini-2.5-pro-preview" || modelId === "google/gemini-2.5-pro") && typeof reasoning === "undefined" @@ -250,8 +251,13 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH const isNativeProtocol = toolProtocol === TOOL_PROTOCOL.NATIVE const isGemini = modelId.startsWith("google/gemini") - // For Gemini with native protocol: inject fake reasoning.encrypted blocks for tool calls - // This is required when switching from other models to Gemini to satisfy API validation + // For Gemini with native protocol: inject fake reasoning.encrypted block for tool calls + // This is required when switching from other models to Gemini to satisfy API validation. + // Per OpenRouter documentation (conversation with Toven, Nov 2025): + // - Create ONE reasoning_details entry per assistant message with tool calls + // - Set `id` to the FIRST tool call's ID from the tool_calls array + // - Set `data` to "skip_thought_signature_validator" to bypass signature validation + // - Set `index` to 0 if (isNativeProtocol && isGemini) { openAiMessages = openAiMessages.map((msg) => { if (msg.role === "assistant") { @@ -263,17 +269,19 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH const hasEncrypted = existingDetails?.some((d) => d.type === "reasoning.encrypted") ?? false if (!hasEncrypted) { - const fakeEncrypted = toolCalls.map((tc, idx) => ({ - id: tc.id, + // Create ONE fake encrypted block with the FIRST tool call's ID + // This is the documented format from OpenRouter for skipping thought signature validation + const fakeEncrypted = { type: "reasoning.encrypted", data: "skip_thought_signature_validator", + id: toolCalls[0].id, format: "google-gemini-v1", - index: (existingDetails?.length ?? 0) + idx, - })) + index: 0, + } return { ...msg, - reasoning_details: [...(existingDetails ?? []), ...fakeEncrypted], + reasoning_details: [...(existingDetails ?? []), fakeEncrypted], } } }