mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-10 17:22:17 +03:00
fix(sse): preserve Gemini thought_signature on Claude Desktop tool turns
Claude→Gemini direct translators dropped thoughtSignature, so Gemini 3 tool follow-ups returned 400. Store on the response path, re-attach (or context-fallback) on the request path, and thread signatureNamespace. Closes #8979
This commit is contained in:
@@ -258,11 +258,16 @@ export function translateRequest(
|
||||
if (directTranslator && sourceFormat !== FORMATS.OPENAI && targetFormat !== FORMATS.OPENAI) {
|
||||
// Thread the routed provider id so target translators can apply provider-specific
|
||||
// quirks (e.g. Vertex rejects function_call.id — #3440).
|
||||
// Also thread signatureNamespace so Claude→Gemini can re-attach cached
|
||||
// thoughtSignature on tool-use history (#8979 / #2504 parity with the hub path).
|
||||
const hasNs = options?.signatureNamespace != null;
|
||||
const hasProvider = provider != null;
|
||||
const directCredentials =
|
||||
provider != null
|
||||
hasNs || hasProvider
|
||||
? {
|
||||
...(credentials && typeof credentials === "object" ? credentials : {}),
|
||||
_provider: provider,
|
||||
...(hasProvider ? { _provider: provider } : {}),
|
||||
...(hasNs ? { _signatureNamespace: options.signatureNamespace } : {}),
|
||||
}
|
||||
: credentials;
|
||||
result = directTranslator(model, result, stream, directCredentials);
|
||||
|
||||
@@ -5,10 +5,14 @@ import {
|
||||
tryParseJSON,
|
||||
cleanJSONSchemaForAntigravity,
|
||||
} from "../helpers/geminiHelper.ts";
|
||||
import { DEFAULT_THINKING_GEMINI_SIGNATURE } from "../../config/defaultThinkingSignature.ts";
|
||||
import { buildGeminiTools, sanitizeGeminiToolName } from "../helpers/geminiToolsSanitizer.ts";
|
||||
import { capMaxOutputTokens, capThinkingBudget } from "../../../src/lib/modelCapabilities.ts";
|
||||
import { getModelSpec } from "../../../src/shared/constants/modelSpecs.ts";
|
||||
import {
|
||||
buildGeminiThoughtSignatureKey,
|
||||
resolveGeminiThoughtSignature,
|
||||
} from "../../services/geminiThoughtSignatureStore.ts";
|
||||
import { buildHistoricalToolResultContext } from "./openai-to-gemini/helpers.ts";
|
||||
|
||||
/**
|
||||
* Direct Claude → Gemini request translator.
|
||||
@@ -26,6 +30,16 @@ export function claudeToGeminiRequest(model, body, stream, credentials = null) {
|
||||
// is scoped to the routed vertex provider only (threaded via credentials._provider).
|
||||
const provider = credentials && typeof credentials === "object" ? credentials._provider : null;
|
||||
const stripFunctionCallId = provider === "vertex" || provider === "vertex-partner";
|
||||
// Thread the signature namespace so a thinking model's thoughtSignature (cached on the
|
||||
// Gemini→Claude response turn under `<connectionId>:<toolUseId>`) is found and
|
||||
// re-attached on the follow-up Claude→Gemini request. Without this, Claude Desktop
|
||||
// combo turns hit HTTP 400 "missing thought_signature" (#8979 / #2504 parity).
|
||||
const signatureNamespace =
|
||||
credentials &&
|
||||
typeof credentials === "object" &&
|
||||
typeof credentials._signatureNamespace === "string"
|
||||
? credentials._signatureNamespace
|
||||
: null;
|
||||
const result: {
|
||||
model: string;
|
||||
contents: Array<Record<string, unknown>>;
|
||||
@@ -81,14 +95,30 @@ export function claudeToGeminiRequest(model, body, stream, credentials = null) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Build tool_use name lookup (for tool_result matching) ──────
|
||||
const toolUseNames = {};
|
||||
// ── Build tool_use name lookup + resolve thought signatures ────
|
||||
// Standard Gemini rejects signature-less native functionCall parts with
|
||||
// HTTP 400 (#8979). Match the OPENAI→GEMINI "context" policy (#3688): only
|
||||
// emit native functionCall/functionResponse when a real signature is
|
||||
// available; otherwise represent history as context text.
|
||||
const toolUseNames: Record<string, string> = {};
|
||||
const resolvedSignatures = new Map<string, string>();
|
||||
if (body.messages && Array.isArray(body.messages)) {
|
||||
for (const msg of body.messages) {
|
||||
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
||||
for (const block of msg.content) {
|
||||
if (block.type === "tool_use" && block.id && block.name) {
|
||||
toolUseNames[block.id] = sanitizeToolName(block.name);
|
||||
const clientSignature =
|
||||
(typeof block.thoughtSignature === "string" && block.thoughtSignature) ||
|
||||
(typeof block.thought_signature === "string" && block.thought_signature) ||
|
||||
null;
|
||||
const resolved = resolveGeminiThoughtSignature(
|
||||
buildGeminiThoughtSignatureKey(signatureNamespace, block.id),
|
||||
clientSignature
|
||||
);
|
||||
if (typeof resolved === "string" && resolved.length > 0) {
|
||||
resolvedSignatures.set(block.id, resolved);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,6 +129,7 @@ export function claudeToGeminiRequest(model, body, stream, credentials = null) {
|
||||
if (body.messages && Array.isArray(body.messages)) {
|
||||
for (const msg of body.messages) {
|
||||
const parts = [];
|
||||
let shouldUseEmbeddedSignature = true;
|
||||
|
||||
if (Array.isArray(msg.content)) {
|
||||
for (const block of msg.content) {
|
||||
@@ -114,8 +145,25 @@ export function claudeToGeminiRequest(model, body, stream, credentials = null) {
|
||||
}
|
||||
break;
|
||||
|
||||
case "tool_use":
|
||||
case "tool_use": {
|
||||
const signatureForToolCall = resolvedSignatures.get(block.id);
|
||||
// Signature-less historical tool_use → omit native functionCall
|
||||
// (context mode). Matching tool_result becomes context text below.
|
||||
if (!signatureForToolCall) {
|
||||
break;
|
||||
}
|
||||
|
||||
const embeddedThoughtSignature = shouldUseEmbeddedSignature
|
||||
? signatureForToolCall
|
||||
: undefined;
|
||||
if (embeddedThoughtSignature) {
|
||||
shouldUseEmbeddedSignature = false;
|
||||
}
|
||||
|
||||
parts.push({
|
||||
...(embeddedThoughtSignature
|
||||
? { thoughtSignature: embeddedThoughtSignature }
|
||||
: {}),
|
||||
functionCall: {
|
||||
...(stripFunctionCallId ? {} : { id: block.id }),
|
||||
name: sanitizeToolName(block.name),
|
||||
@@ -123,6 +171,7 @@ export function claudeToGeminiRequest(model, body, stream, credentials = null) {
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "tool_result": {
|
||||
let content = block.content;
|
||||
@@ -137,10 +186,24 @@ export function claudeToGeminiRequest(model, body, stream, credentials = null) {
|
||||
} else if (typeof parsedContent !== "object") {
|
||||
parsedContent = { result: parsedContent };
|
||||
}
|
||||
|
||||
const toolUseId = block.tool_use_id;
|
||||
const name = toolUseNames[toolUseId] || "unknown";
|
||||
|
||||
// Signature-less history: represent as context text so Gemini 3+
|
||||
// does not reject a native functionResponse without a matching
|
||||
// signed functionCall (#8979 / #3688).
|
||||
if (!resolvedSignatures.has(toolUseId)) {
|
||||
parts.push({
|
||||
text: buildHistoricalToolResultContext(name, content),
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
parts.push({
|
||||
functionResponse: {
|
||||
...(stripFunctionCallId ? {} : { id: block.tool_use_id }),
|
||||
name: toolUseNames[block.tool_use_id] || "unknown",
|
||||
...(stripFunctionCallId ? {} : { id: toolUseId }),
|
||||
name,
|
||||
response: { result: parsedContent },
|
||||
},
|
||||
});
|
||||
@@ -167,14 +230,6 @@ export function claudeToGeminiRequest(model, body, stream, credentials = null) {
|
||||
if (parts.length > 0) {
|
||||
// Map Claude roles to Gemini roles
|
||||
const geminiRole = msg.role === "assistant" ? "model" : "user";
|
||||
|
||||
// Gemini 3+ expects the signature on all functionCall parts in a tool-call
|
||||
// batch. If there is no real signature, we don't inject a fake one because
|
||||
// Gemini API strictly validates it and returns 400.
|
||||
if (geminiRole === "model") {
|
||||
// No operation needed since we no longer inject fake signatures.
|
||||
}
|
||||
|
||||
result.contents.push({ role: geminiRole, parts });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@ import { register } from "../registry.ts";
|
||||
import { FORMATS } from "../formats.ts";
|
||||
import { isAbortFinishReason } from "../../utils/finishReason.ts";
|
||||
import { REVERSE_MAP } from "../../services/claudeCodeToolRemapper.ts";
|
||||
import {
|
||||
buildGeminiThoughtSignatureKey,
|
||||
storeGeminiThoughtSignature,
|
||||
} from "../../services/geminiThoughtSignatureStore.ts";
|
||||
|
||||
function normalizeToolName(name: string): string {
|
||||
return REVERSE_MAP[name] ?? name;
|
||||
@@ -56,6 +60,12 @@ export function geminiToClaudeResponse(chunk, state) {
|
||||
const hasThoughtSig = part.thoughtSignature || part.thought_signature;
|
||||
const isThought = part.thought === true;
|
||||
|
||||
// Capture thoughtSignature so the next functionCall (or same-part call)
|
||||
// can persist it for Claude→Gemini follow-up turns (#8979 / #2504 parity).
|
||||
if (typeof hasThoughtSig === "string" && hasThoughtSig.length > 0) {
|
||||
state.pendingThoughtSignature = hasThoughtSig;
|
||||
}
|
||||
|
||||
// Thinking content → thinking block (always open+close per chunk)
|
||||
if (isThought && part.text) {
|
||||
// Close any open text block first
|
||||
@@ -78,6 +88,17 @@ export function geminiToClaudeResponse(chunk, state) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Standalone thoughtSignature part (no text / no functionCall): keep
|
||||
// pending and wait for the following functionCall — do not emit to Claude.
|
||||
if (
|
||||
typeof hasThoughtSig === "string" &&
|
||||
hasThoughtSig.length > 0 &&
|
||||
(part.text === undefined || part.text === "") &&
|
||||
!part.functionCall
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Function call → tool_use block
|
||||
if (part.functionCall) {
|
||||
// Close any open text block first
|
||||
@@ -91,6 +112,22 @@ export function geminiToClaudeResponse(chunk, state) {
|
||||
const idx = state.contentBlockIndex++;
|
||||
const toolId = fc.id || `toolu_${Date.now()}_${idx}`;
|
||||
|
||||
const signatureForToolCall =
|
||||
(typeof hasThoughtSig === "string" && hasThoughtSig.length > 0
|
||||
? hasThoughtSig
|
||||
: null) ||
|
||||
(typeof state.pendingThoughtSignature === "string" &&
|
||||
state.pendingThoughtSignature.length > 0
|
||||
? state.pendingThoughtSignature
|
||||
: null);
|
||||
if (signatureForToolCall) {
|
||||
storeGeminiThoughtSignature(
|
||||
buildGeminiThoughtSignatureKey(state.signatureNamespace, toolId),
|
||||
signatureForToolCall
|
||||
);
|
||||
state.pendingThoughtSignature = null;
|
||||
}
|
||||
|
||||
results.push({
|
||||
type: "content_block_start",
|
||||
index: idx,
|
||||
|
||||
260
tests/unit/claude-gemini-thought-signature-8979.test.ts
Normal file
260
tests/unit/claude-gemini-thought-signature-8979.test.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
/**
|
||||
* #8979: Claude Desktop → Gemini tool calls fail with
|
||||
* "Function call is missing a thought_signature in functionCall parts".
|
||||
*
|
||||
* Claude clients hit the direct CLAUDE↔GEMINI translators (not the OpenAI hub
|
||||
* path that already stores/reattaches signatures via #2504 / #3688). The direct
|
||||
* path must:
|
||||
* 1) Persist thoughtSignature from Gemini functionCall parts (response)
|
||||
* 2) Re-attach it on the follow-up Claude→Gemini request (tool_use history)
|
||||
* 3) Fall back to context text when no signature is available (avoid 400)
|
||||
* 4) Thread credentials._signatureNamespace through translateRequest's direct path
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { claudeToGeminiRequest } =
|
||||
await import("../../open-sse/translator/request/claude-to-gemini.ts");
|
||||
const { geminiToClaudeResponse } =
|
||||
await import("../../open-sse/translator/response/gemini-to-claude.ts");
|
||||
const { translateRequest } = await import("../../open-sse/translator/index.ts");
|
||||
const { FORMATS } = await import("../../open-sse/translator/formats.ts");
|
||||
const {
|
||||
buildGeminiThoughtSignatureKey,
|
||||
storeGeminiThoughtSignature,
|
||||
getGeminiThoughtSignature,
|
||||
clearGeminiThoughtSignatures,
|
||||
} = await import("../../open-sse/services/geminiThoughtSignatureStore.ts");
|
||||
|
||||
test.beforeEach(() => {
|
||||
clearGeminiThoughtSignatures();
|
||||
});
|
||||
|
||||
test("gemini→claude stores thoughtSignature keyed by connection + tool_use id (#8979)", () => {
|
||||
const ns = "conn-8979-store";
|
||||
const toolId = "toolu_8979_read";
|
||||
const signature = "SIG_8979_FROM_GEMINI";
|
||||
const state = { signatureNamespace: ns };
|
||||
|
||||
geminiToClaudeResponse(
|
||||
{
|
||||
responseId: "resp-8979",
|
||||
modelVersion: "gemini-3.6-flash",
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
parts: [
|
||||
{
|
||||
thoughtSignature: signature,
|
||||
functionCall: {
|
||||
id: toolId,
|
||||
name: "default_api_read",
|
||||
args: { path: "/tmp/a" },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
finishReason: "STOP",
|
||||
},
|
||||
],
|
||||
},
|
||||
state
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
getGeminiThoughtSignature(buildGeminiThoughtSignatureKey(ns, toolId)),
|
||||
signature,
|
||||
"direct Gemini→Claude path must persist thoughtSignature for the follow-up turn"
|
||||
);
|
||||
});
|
||||
|
||||
test("gemini→claude stores thoughtSignature from a preceding standalone part (#8979)", () => {
|
||||
const ns = "conn-8979-pending";
|
||||
const toolId = "toolu_8979_pending";
|
||||
const signature = "SIG_8979_PENDING_PART";
|
||||
const state = { signatureNamespace: ns };
|
||||
|
||||
geminiToClaudeResponse(
|
||||
{
|
||||
responseId: "resp-8979b",
|
||||
modelVersion: "gemini-3.6-flash",
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
parts: [
|
||||
{ thoughtSignature: signature },
|
||||
{
|
||||
functionCall: {
|
||||
id: toolId,
|
||||
name: "read_file",
|
||||
args: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
state
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
getGeminiThoughtSignature(buildGeminiThoughtSignatureKey(ns, toolId)),
|
||||
signature,
|
||||
"standalone thoughtSignature part must be bound to the following functionCall"
|
||||
);
|
||||
});
|
||||
|
||||
test("claude→gemini re-attaches cached thoughtSignature on tool_use history (#8979)", () => {
|
||||
const ns = "conn-8979-reattach";
|
||||
const toolId = "toolu_8979_reattach";
|
||||
const signature = "SIG_8979_REATTACH";
|
||||
storeGeminiThoughtSignature(buildGeminiThoughtSignatureKey(ns, toolId), signature);
|
||||
|
||||
const result = claudeToGeminiRequest(
|
||||
"gemini-3.6-flash",
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: toolId,
|
||||
name: "default_api:read",
|
||||
input: { path: "/tmp/a" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: toolId,
|
||||
content: "file contents",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
false,
|
||||
{ _signatureNamespace: ns }
|
||||
) as {
|
||||
contents: Array<{ role: string; parts: Array<Record<string, unknown>> }>;
|
||||
};
|
||||
|
||||
const modelTurn = result.contents.find(
|
||||
(c) => c.role === "model" && c.parts?.some((p) => p.functionCall)
|
||||
);
|
||||
assert.ok(modelTurn, "expected model turn with functionCall");
|
||||
const fcPart = modelTurn.parts.find((p) => p.functionCall) as {
|
||||
thoughtSignature?: string;
|
||||
functionCall: { name: string };
|
||||
};
|
||||
assert.equal(
|
||||
fcPart.thoughtSignature,
|
||||
signature,
|
||||
"cached thoughtSignature must be re-attached to functionCall"
|
||||
);
|
||||
assert.ok(
|
||||
result.contents.some((c) => c.parts?.some((p) => p.functionResponse)),
|
||||
"signed tool_result must remain a native functionResponse"
|
||||
);
|
||||
});
|
||||
|
||||
test("claude→gemini omits unsigned functionCall and uses context fallback (#8979 / #3688 parity)", () => {
|
||||
const result = claudeToGeminiRequest(
|
||||
"gemini-3.6-flash",
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "toolu_8979_unsigned",
|
||||
name: "default_api:read",
|
||||
input: { path: "/tmp/a" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "toolu_8979_unsigned",
|
||||
content: "file contents",
|
||||
},
|
||||
],
|
||||
},
|
||||
{ role: "user", content: "summarize the file" },
|
||||
],
|
||||
},
|
||||
false,
|
||||
{ _signatureNamespace: "conn-8979-unsigned" }
|
||||
) as {
|
||||
contents: Array<{ role: string; parts: Array<Record<string, unknown>> }>;
|
||||
};
|
||||
|
||||
const modelWithFc = result.contents.find(
|
||||
(c) => c.role === "model" && c.parts?.some((p) => p.functionCall)
|
||||
);
|
||||
assert.equal(
|
||||
modelWithFc,
|
||||
undefined,
|
||||
"must NOT emit signature-less functionCall (triggers Gemini HTTP 400)"
|
||||
);
|
||||
|
||||
const body = JSON.stringify(result);
|
||||
assert.ok(
|
||||
body.includes("previous_tool_result_context"),
|
||||
"signature-less tool_result must become context text"
|
||||
);
|
||||
assert.ok(body.includes("file contents"), "context block must keep tool result content");
|
||||
});
|
||||
|
||||
test("translateRequest threads signatureNamespace on direct CLAUDE→GEMINI path (#8979)", () => {
|
||||
const ns = "conn-8979-direct-ns";
|
||||
const toolId = "toolu_8979_direct";
|
||||
const signature = "SIG_8979_DIRECT_NS";
|
||||
storeGeminiThoughtSignature(buildGeminiThoughtSignatureKey(ns, toolId), signature);
|
||||
|
||||
const result = translateRequest(
|
||||
FORMATS.CLAUDE,
|
||||
FORMATS.GEMINI,
|
||||
"gemini-3.6-flash",
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: toolId,
|
||||
name: "read_file",
|
||||
input: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "tool_result", tool_use_id: toolId, content: "ok" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
false,
|
||||
null,
|
||||
"gemini",
|
||||
null,
|
||||
{ signatureNamespace: ns }
|
||||
) as { contents: Array<{ parts: Array<Record<string, unknown>> }> };
|
||||
|
||||
const body = JSON.stringify(result);
|
||||
assert.ok(
|
||||
body.includes(signature),
|
||||
"direct CLAUDE→GEMINI translateRequest must thread _signatureNamespace so cache lookup succeeds"
|
||||
);
|
||||
});
|
||||
@@ -5,6 +5,15 @@ const { claudeToGeminiRequest } =
|
||||
await import("../../open-sse/translator/request/claude-to-gemini.ts");
|
||||
const { DEFAULT_SAFETY_SETTINGS } =
|
||||
await import("../../open-sse/translator/helpers/geminiHelper.ts");
|
||||
const {
|
||||
buildGeminiThoughtSignatureKey,
|
||||
storeGeminiThoughtSignature,
|
||||
clearGeminiThoughtSignatures,
|
||||
} = await import("../../open-sse/services/geminiThoughtSignatureStore.ts");
|
||||
|
||||
test.beforeEach(() => {
|
||||
clearGeminiThoughtSignatures();
|
||||
});
|
||||
|
||||
type UnknownRecord = Record<string, unknown>;
|
||||
|
||||
@@ -34,6 +43,10 @@ function getFunctionResponse(part: unknown) {
|
||||
}
|
||||
|
||||
test("Claude -> Gemini maps system, thinking, tool use, tool result and tools", () => {
|
||||
// Native functionCall requires a cached thoughtSignature (#8979 / #3688).
|
||||
const ns = "conn-claude-gemini-map";
|
||||
storeGeminiThoughtSignature(buildGeminiThoughtSignatureKey(ns, "tu_1"), "SIG_MAP_WEATHER");
|
||||
|
||||
const result = claudeToGeminiRequest(
|
||||
"gemini-2.5-pro",
|
||||
{
|
||||
@@ -72,7 +85,8 @@ test("Claude -> Gemini maps system, thinking, tool use, tool result and tools",
|
||||
top_p: 0.8,
|
||||
thinking: { type: "enabled", budget_tokens: 512 },
|
||||
},
|
||||
false
|
||||
false,
|
||||
{ _signatureNamespace: ns }
|
||||
);
|
||||
|
||||
assert.deepEqual(result.systemInstruction, {
|
||||
@@ -82,6 +96,7 @@ test("Claude -> Gemini maps system, thinking, tool use, tool result and tools",
|
||||
assert.equal(result.contents[0].role, "model");
|
||||
assert.deepEqual(result.contents[0].parts[0] as any, { thought: true, text: "need tool" });
|
||||
assert.deepEqual(result.contents[0].parts[1] as any, {
|
||||
thoughtSignature: "SIG_MAP_WEATHER",
|
||||
functionCall: { id: "tu_1", name: "weather", args: { city: "Tokyo" } },
|
||||
});
|
||||
assert.deepEqual(result.contents[1].parts[0] as any, {
|
||||
@@ -162,7 +177,10 @@ test("Claude -> Gemini converts text and base64 images to Gemini parts", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("Claude -> Gemini injects a fallback thoughtSignature on tool-call batches without thinking", () => {
|
||||
test("Claude -> Gemini omits unsigned functionCall instead of injecting a fake thoughtSignature (#8979)", () => {
|
||||
// After #1410 / #8979: never inject a fake signature. Without a cached
|
||||
// thoughtSignature, native functionCall parts are omitted (context mode)
|
||||
// so Gemini 3+ does not return HTTP 400.
|
||||
const result = claudeToGeminiRequest(
|
||||
"gemini-2.5-flash",
|
||||
{
|
||||
@@ -176,15 +194,20 @@ test("Claude -> Gemini injects a fallback thoughtSignature on tool-call batches
|
||||
false
|
||||
);
|
||||
|
||||
assert.equal(result.contents.length, 1);
|
||||
assert.equal(result.contents[0].role, "model");
|
||||
assert.equal((result.contents[0].parts[0] as any).functionCall.name, "read_file");
|
||||
assert.equal((result.contents[0].parts[0] as any).thoughtSignature, undefined);
|
||||
assert.equal(result.contents.length, 0);
|
||||
assert.equal(
|
||||
JSON.stringify(result).includes('"functionCall"'),
|
||||
false,
|
||||
"signature-less tool_use must not become a native functionCall"
|
||||
);
|
||||
});
|
||||
|
||||
test("Claude -> Gemini sanitizes long tool names and exposes a restore map", () => {
|
||||
const longToolName =
|
||||
"mcp__filesystem__read_multiple_files_with_validation_and_metadata_bundle_v2";
|
||||
const ns = "conn-claude-gemini-long";
|
||||
storeGeminiThoughtSignature(buildGeminiThoughtSignatureKey(ns, "tu_long_1"), "SIG_LONG_TOOL");
|
||||
|
||||
const result = claudeToGeminiRequest(
|
||||
"gemini-2.5-pro",
|
||||
{
|
||||
@@ -214,7 +237,8 @@ test("Claude -> Gemini sanitizes long tool names and exposes a restore map", ()
|
||||
},
|
||||
],
|
||||
},
|
||||
false
|
||||
false,
|
||||
{ _signatureNamespace: ns }
|
||||
);
|
||||
|
||||
const sanitizedToolName = (result as any).tools[0].functionDeclarations[0].name as string;
|
||||
|
||||
Reference in New Issue
Block a user