Signature handling for fallback
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { MIN_SIGNATURE_LENGTH, GEMINI_SKIP_SIGNATURE } from '../constants.js';
|
||||
import { getCachedSignature } from './signature-cache.js';
|
||||
import { getCachedSignature, getCachedSignatureFamily } from './signature-cache.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
/**
|
||||
@@ -155,16 +155,31 @@ export function convertContentToParts(content, isClaudeModel = false, isGeminiMo
|
||||
// Add any images from the tool result as separate parts
|
||||
parts.push(...imageParts);
|
||||
} else if (block.type === 'thinking') {
|
||||
// Handle thinking blocks - only those with valid signatures
|
||||
// Handle thinking blocks with signature compatibility check
|
||||
if (block.signature && block.signature.length >= MIN_SIGNATURE_LENGTH) {
|
||||
// Convert to Gemini format with signature
|
||||
const signatureFamily = getCachedSignatureFamily(block.signature);
|
||||
const targetFamily = isClaudeModel ? 'claude' : isGeminiModel ? 'gemini' : null;
|
||||
|
||||
// Drop blocks with incompatible signatures for Gemini (cross-model switch)
|
||||
if (isGeminiModel && signatureFamily && targetFamily && signatureFamily !== targetFamily) {
|
||||
logger.debug(`[ContentConverter] Dropping incompatible ${signatureFamily} thinking for ${targetFamily} model`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Drop blocks with unknown signature origin for Gemini (cold cache - safe default)
|
||||
if (isGeminiModel && !signatureFamily && targetFamily) {
|
||||
logger.debug(`[ContentConverter] Dropping thinking with unknown signature origin`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Compatible - convert to Gemini format with signature
|
||||
parts.push({
|
||||
text: block.thinking,
|
||||
thought: true,
|
||||
thoughtSignature: block.signature
|
||||
});
|
||||
}
|
||||
// Unsigned thinking blocks are dropped upstream
|
||||
// Unsigned thinking blocks are dropped (existing behavior)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,12 +77,14 @@ export function convertAnthropicToGoogle(anthropicRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// Apply thinking recovery for Gemini thinking models when needed
|
||||
// This handles corrupted tool loops where thinking blocks are stripped
|
||||
// Claude models handle this differently and don't need this recovery
|
||||
// Apply thinking recovery for thinking models when needed
|
||||
// - Gemini: needs recovery for tool loops/interrupted tools (stripped thinking)
|
||||
// - Claude: needs recovery ONLY when cross-model (incompatible Gemini signatures will be dropped)
|
||||
let processedMessages = messages;
|
||||
if (isGeminiModel && isThinking && needsThinkingRecovery(messages)) {
|
||||
logger.debug('[RequestConverter] Applying thinking recovery for Gemini');
|
||||
const targetFamily = isClaudeModel ? 'claude' : isGeminiModel ? 'gemini' : null;
|
||||
|
||||
if (isThinking && targetFamily && needsThinkingRecovery(messages, targetFamily)) {
|
||||
logger.debug(`[RequestConverter] Applying thinking recovery for ${targetFamily}`);
|
||||
processedMessages = closeToolLoopForThinking(messages);
|
||||
}
|
||||
|
||||
@@ -107,7 +109,7 @@ export function convertAnthropicToGoogle(anthropicRequest) {
|
||||
// This happens when all thinking blocks are filtered out (unsigned)
|
||||
if (parts.length === 0) {
|
||||
logger.warn('[RequestConverter] WARNING: Empty parts array after filtering, adding placeholder');
|
||||
parts.push({ text: '' });
|
||||
parts.push({ text: '.' });
|
||||
}
|
||||
|
||||
const content = {
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import crypto from 'crypto';
|
||||
import { MIN_SIGNATURE_LENGTH } from '../constants.js';
|
||||
import { cacheSignature } from './signature-cache.js';
|
||||
import { MIN_SIGNATURE_LENGTH, getModelFamily } from '../constants.js';
|
||||
import { cacheSignature, cacheThinkingSignature } from './signature-cache.js';
|
||||
|
||||
/**
|
||||
* Convert Google Generative AI response to Anthropic Messages API format
|
||||
@@ -33,6 +33,12 @@ export function convertGoogleToAnthropic(googleResponse, model) {
|
||||
if (part.thought === true) {
|
||||
const signature = part.thoughtSignature || '';
|
||||
|
||||
// Cache thinking signature with model family for cross-model compatibility
|
||||
if (signature && signature.length >= MIN_SIGNATURE_LENGTH) {
|
||||
const modelFamily = getModelFamily(model);
|
||||
cacheThinkingSignature(signature, modelFamily);
|
||||
}
|
||||
|
||||
// Include thinking blocks in the response for Claude Code
|
||||
anthropicContent.push({
|
||||
type: 'thinking',
|
||||
|
||||
@@ -5,11 +5,15 @@
|
||||
* Gemini models require thoughtSignature on tool calls, but Claude Code
|
||||
* strips non-standard fields. This cache stores signatures by tool_use_id
|
||||
* so they can be restored in subsequent requests.
|
||||
*
|
||||
* Also caches thinking block signatures with model family for cross-model
|
||||
* compatibility checking.
|
||||
*/
|
||||
|
||||
import { GEMINI_SIGNATURE_CACHE_TTL_MS } from '../constants.js';
|
||||
import { GEMINI_SIGNATURE_CACHE_TTL_MS, MIN_SIGNATURE_LENGTH } from '../constants.js';
|
||||
|
||||
const signatureCache = new Map();
|
||||
const thinkingSignatureCache = new Map();
|
||||
|
||||
/**
|
||||
* Store a signature for a tool_use_id
|
||||
@@ -54,6 +58,11 @@ export function cleanupCache() {
|
||||
signatureCache.delete(key);
|
||||
}
|
||||
}
|
||||
for (const [key, entry] of thinkingSignatureCache) {
|
||||
if (now - entry.timestamp > GEMINI_SIGNATURE_CACHE_TTL_MS) {
|
||||
thinkingSignatureCache.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,3 +72,43 @@ export function cleanupCache() {
|
||||
export function getCacheSize() {
|
||||
return signatureCache.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache a thinking block signature with its model family
|
||||
* @param {string} signature - The thinking signature to cache
|
||||
* @param {string} modelFamily - The model family ('claude' or 'gemini')
|
||||
*/
|
||||
export function cacheThinkingSignature(signature, modelFamily) {
|
||||
if (!signature || signature.length < MIN_SIGNATURE_LENGTH) return;
|
||||
thinkingSignatureCache.set(signature, {
|
||||
modelFamily,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cached model family for a thinking signature
|
||||
* @param {string} signature - The signature to look up
|
||||
* @returns {string|null} 'claude', 'gemini', or null if not found/expired
|
||||
*/
|
||||
export function getCachedSignatureFamily(signature) {
|
||||
if (!signature) return null;
|
||||
const entry = thinkingSignatureCache.get(signature);
|
||||
if (!entry) return null;
|
||||
|
||||
// Check TTL
|
||||
if (Date.now() - entry.timestamp > GEMINI_SIGNATURE_CACHE_TTL_MS) {
|
||||
thinkingSignatureCache.delete(signature);
|
||||
return null;
|
||||
}
|
||||
|
||||
return entry.modelFamily;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current thinking signature cache size (for debugging)
|
||||
* @returns {number} Number of entries in the thinking signature cache
|
||||
*/
|
||||
export function getThinkingCacheSize() {
|
||||
return thinkingSignatureCache.size;
|
||||
}
|
||||
|
||||
@@ -386,16 +386,23 @@ export function analyzeConversationState(messages) {
|
||||
|
||||
/**
|
||||
* Check if conversation needs thinking recovery.
|
||||
* Returns true when:
|
||||
* 1. We're in a tool loop but have no valid thinking blocks, OR
|
||||
* 2. We have an interrupted tool with no valid thinking blocks
|
||||
*
|
||||
* For Gemini: recovery needed when (tool loop OR interrupted tool) AND no valid thinking
|
||||
* For Claude: recovery needed when no valid compatible thinking (cross-model detection)
|
||||
*
|
||||
* @param {Array<Object>} messages - Array of messages
|
||||
* @param {string} targetFamily - Target model family ('claude' or 'gemini')
|
||||
* @returns {boolean} True if thinking recovery is needed
|
||||
*/
|
||||
export function needsThinkingRecovery(messages) {
|
||||
const state = analyzeConversationState(messages);
|
||||
// Need recovery if (tool loop OR interrupted tool) AND no thinking
|
||||
export function needsThinkingRecovery(messages, targetFamily = null) {
|
||||
const state = analyzeConversationState(messages, targetFamily);
|
||||
|
||||
if (targetFamily === 'claude') {
|
||||
// Claude: only check if thinking is valid/compatible
|
||||
return !state.turnHasThinking;
|
||||
}
|
||||
|
||||
// Gemini (default): check tool loop/interrupted AND no thinking
|
||||
return (state.inToolLoop || state.interruptedTool) && !state.turnHasThinking;
|
||||
}
|
||||
|
||||
@@ -414,9 +421,9 @@ function stripAllThinkingBlocks(messages) {
|
||||
const filtered = content.filter(block => !isThinkingPart(block));
|
||||
|
||||
if (msg.content) {
|
||||
return { ...msg, content: filtered.length > 0 ? filtered : [{ type: 'text', text: '' }] };
|
||||
return { ...msg, content: filtered.length > 0 ? filtered : [{ type: 'text', text: '.' }] };
|
||||
} else if (msg.parts) {
|
||||
return { ...msg, parts: filtered.length > 0 ? filtered : [{ text: '' }] };
|
||||
return { ...msg, parts: filtered.length > 0 ? filtered : [{ text: '.' }] };
|
||||
}
|
||||
return msg;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user