feat(gemini): persist and replay thoughtSignature with session namespace

- Add open-sse/services/thoughtSignatureStore.js managing LRU Map (2k) + SQLite kv table
- Store thoughtSignature with sessionId namespace and toolCallId fallback
- Replay cached signature by sessionId:tool_call_id to prevent multi-process collisions
- Normalize Antigravity sessionId to numeric int64 format

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
decolua
2026-09-03 17:55:52 +07:00
parent 4eda76e2ab
commit c08efdbe2b
7 changed files with 263 additions and 39 deletions

View File

@@ -2,6 +2,7 @@ import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { DEFAULT_THINKING_AG_SIGNATURE, DEFAULT_THINKING_GEMINI_CLI_SIGNATURE } from "../../config/defaultThinkingSignature.js";
import { openaiToClaudeRequestForAntigravity } from "./openai-to-claude.js";
import { getGeminiThoughtSignatureSync } from "../../services/thoughtSignatureStore.js";
function generateUUID() {
return crypto.randomUUID();
}
@@ -46,7 +47,7 @@ function normalizeGeminiContents(contents) {
}
// Core: Convert OpenAI request to Gemini format (base for all variants)
function openaiToGeminiBase(model, body, stream, signature = DEFAULT_THINKING_AG_SIGNATURE) {
function openaiToGeminiBase(model, body, stream, signature = DEFAULT_THINKING_AG_SIGNATURE, sessionId = null) {
const result = {
model: model,
contents: [],
@@ -133,18 +134,27 @@ function openaiToGeminiBase(model, body, stream, signature = DEFAULT_THINKING_AG
if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
const toolCallIds = [];
let firstFunctionCallSeen = false;
for (const tc of msg.tool_calls) {
if (tc.type !== OPENAI_BLOCK.FUNCTION) continue;
const args = tryParseJSON(tc.function?.arguments || "{}");
parts.push({
thoughtSignature: signature,
const cachedSig = tc.id ? getGeminiThoughtSignatureSync(tc.id, sessionId) : null;
// First call gets cached signature or fallback; sibling calls remain unsigned if no cached sig
const callSig = cachedSig || (!firstFunctionCallSeen ? signature : undefined);
firstFunctionCallSeen = true;
const part = {
functionCall: {
id: tc.id,
name: sanitizeGeminiFunctionName(tc.function.name),
args: args
}
});
};
if (callSig) {
part.thoughtSignature = callSig;
}
parts.push(part);
toolCallIds.push(tc.id);
}
@@ -232,13 +242,13 @@ function openaiToGeminiBase(model, body, stream, signature = DEFAULT_THINKING_AG
}
// OpenAI -> Gemini (standard API)
export function openaiToGeminiRequest(model, body, stream) {
return openaiToGeminiBase(model, body, stream);
export function openaiToGeminiRequest(model, body, stream, credentials = null) {
return openaiToGeminiBase(model, body, stream, DEFAULT_THINKING_AG_SIGNATURE, credentials?._clientSessionId);
}
// OpenAI -> Gemini CLI (Cloud Code Assist)
export function openaiToGeminiCLIRequest(model, body, stream) {
const gemini = openaiToGeminiBase(model, body, stream, DEFAULT_THINKING_GEMINI_CLI_SIGNATURE);
export function openaiToGeminiCLIRequest(model, body, stream, credentials = null) {
const gemini = openaiToGeminiBase(model, body, stream, DEFAULT_THINKING_GEMINI_CLI_SIGNATURE, credentials?._clientSessionId);
// Thinking is normalized centrally by applyThinking (thinkingUnified.js) after translation.
// Clean schema for tools
@@ -335,18 +345,26 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu
const parts = [];
if (Array.isArray(msg.content)) {
let firstToolUseSeen = false;
for (const block of msg.content) {
if (block.type === CLAUDE_BLOCK.TEXT) {
parts.push({ text: block.text });
} else if (block.type === CLAUDE_BLOCK.TOOL_USE) {
parts.push({
thoughtSignature: signature,
const cachedSig = block.id ? getGeminiThoughtSignatureSync(block.id, credentials?._clientSessionId) : null;
const callSig = cachedSig || (!firstToolUseSeen ? signature : undefined);
firstToolUseSeen = true;
const part = {
functionCall: {
id: block.id,
name: sanitizeGeminiFunctionName(block.name),
args: block.input || {}
}
});
};
if (callSig) {
part.thoughtSignature = callSig;
}
parts.push(part);
} else if (block.type === CLAUDE_BLOCK.TOOL_RESULT) {
let content = block.content;
if (Array.isArray(content)) {

View File

@@ -6,6 +6,7 @@ import { toOpenAIUsage } from "../concerns/usage.js";
import { reasoningDelta } from "../concerns/reasoning.js";
import { encodeDataUri } from "../concerns/image.js";
import { toOpenAIFinish } from "../concerns/finishReason.js";
import { storeGeminiThoughtSignature } from "../../services/thoughtSignatureStore.js";
// Build chunk meta for current gemini state
function chunkMeta(state) {
@@ -13,14 +14,18 @@ function chunkMeta(state) {
}
// Build a tool_call chunk from a gemini functionCall part (shared by sig/non-sig branches)
function emitFunctionCall(functionCall, state) {
function emitFunctionCall(functionCall, state, signature = null) {
const rawName = functionCall.name;
// Restore original tool name from mapping (AG cloaking)
const fcName = state.toolNameMap?.get(rawName) || rawName;
const fcArgs = functionCall.args || {};
const toolCallIndex = state.functionIndex++;
const callId = functionCall.id || `${fcName}-${Date.now()}-${toolCallIndex}`;
if (signature) {
storeGeminiThoughtSignature(callId, signature, state.sessionId);
}
const toolCall = {
id: `${fcName}-${Date.now()}-${toolCallIndex}`,
id: callId,
index: toolCallIndex,
type: OPENAI_BLOCK.FUNCTION,
function: { name: fcName, arguments: JSON.stringify(fcArgs) },
@@ -57,13 +62,21 @@ export function geminiToOpenAIResponse(chunk, state) {
if (content?.parts) {
for (const part of content.parts) {
const hasThoughtSig = part.thoughtSignature || part.thought_signature;
if (hasThoughtSig && typeof hasThoughtSig === "string") {
state.pendingThoughtSignature = hasThoughtSig;
}
const isThought = part.thought === true;
// Handle thought signature (thinking mode)
if (hasThoughtSig) {
const hasTextContent = part.text !== undefined && part.text !== "";
const hasFunctionCall = !!part.functionCall;
// Standalone thoughtSignature part (no text, no functionCall): keep pending for next functionCall
if (!hasTextContent && !hasFunctionCall) {
continue;
}
if (hasTextContent) {
results.push(buildChunk(
chunkMeta(state),
@@ -71,9 +84,10 @@ export function geminiToOpenAIResponse(chunk, state) {
null
));
}
if (hasFunctionCall) {
results.push(emitFunctionCall(part.functionCall, state));
results.push(emitFunctionCall(part.functionCall, state, hasThoughtSig));
state.pendingThoughtSignature = null;
}
continue;
}
@@ -92,7 +106,9 @@ export function geminiToOpenAIResponse(chunk, state) {
// Function call
if (part.functionCall) {
results.push(emitFunctionCall(part.functionCall, state));
const sig = state.pendingThoughtSignature || null;
results.push(emitFunctionCall(part.functionCall, state, sig));
state.pendingThoughtSignature = null;
}
// Inline data (images)