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], } } }