From dc65499c49b561cf8affa01be0b03eee10fdac21 Mon Sep 17 00:00:00 2001 From: Badri Narayanan S Date: Sat, 3 Jan 2026 23:17:38 +0530 Subject: [PATCH] Preserve valid thinking blocks during recovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of stripping all thinking blocks during thinking recovery, now only strips invalid or incompatible blocks. Uses signature cache to validate family compatibility for cross-model fallback scenarios. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/format/request-converter.js | 2 +- src/format/thinking-utils.js | 38 +++++++++++++++++++++++++++------ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/format/request-converter.js b/src/format/request-converter.js index 7343439..fa2d920 100644 --- a/src/format/request-converter.js +++ b/src/format/request-converter.js @@ -85,7 +85,7 @@ export function convertAnthropicToGoogle(anthropicRequest) { if (isThinking && targetFamily && needsThinkingRecovery(messages, targetFamily)) { logger.debug(`[RequestConverter] Applying thinking recovery for ${targetFamily}`); - processedMessages = closeToolLoopForThinking(messages); + processedMessages = closeToolLoopForThinking(messages, targetFamily); } // Convert messages to contents, then filter unsigned thinking blocks diff --git a/src/format/thinking-utils.js b/src/format/thinking-utils.js index af9b3de..9017a8c 100644 --- a/src/format/thinking-utils.js +++ b/src/format/thinking-utils.js @@ -4,6 +4,7 @@ */ import { MIN_SIGNATURE_LENGTH } from '../constants.js'; +import { getCachedSignatureFamily } from './signature-cache.js'; import { logger } from '../utils/logger.js'; /** @@ -407,18 +408,40 @@ export function needsThinkingRecovery(messages, targetFamily = null) { } /** - * Strip all thinking blocks from messages. + * Strip invalid or incompatible thinking blocks from messages. * Used before injecting synthetic messages for recovery. + * Keeps valid thinking blocks to preserve context from previous turns. * * @param {Array} messages - Array of messages - * @returns {Array} Messages with all thinking blocks removed + * @param {string} targetFamily - Target model family ('claude' or 'gemini') + * @returns {Array} Messages with invalid thinking blocks removed */ -function stripAllThinkingBlocks(messages) { +function stripInvalidThinkingBlocks(messages, targetFamily = null) { return messages.map(msg => { const content = msg.content || msg.parts; if (!Array.isArray(content)) return msg; - const filtered = content.filter(block => !isThinkingPart(block)); + const filtered = content.filter(block => { + // Keep non-thinking blocks + if (!isThinkingPart(block)) return true; + + // Check generic validity (has signature of sufficient length) + if (!hasValidSignature(block)) return false; + + // Check family compatibility if targetFamily is provided + if (targetFamily) { + const signature = block.thought === true ? block.thoughtSignature : block.signature; + const signatureFamily = getCachedSignatureFamily(signature); + + // Strict validation: If we don't know the family (cache miss) or it doesn't match, + // we drop it. We don't assume validity for unknown signatures. + if (signatureFamily !== targetFamily) { + return false; + } + } + + return true; + }); if (msg.content) { return { ...msg, content: filtered.length > 0 ? filtered : [{ type: 'text', text: '.' }] }; @@ -439,16 +462,17 @@ function stripAllThinkingBlocks(messages) { * loop and allow the model to continue. * * @param {Array} messages - Array of messages + * @param {string} targetFamily - Target model family ('claude' or 'gemini') * @returns {Array} Modified messages with synthetic messages injected */ -export function closeToolLoopForThinking(messages) { +export function closeToolLoopForThinking(messages, targetFamily = null) { const state = analyzeConversationState(messages); // Handle neither tool loop nor interrupted tool if (!state.inToolLoop && !state.interruptedTool) return messages; - // Strip all thinking blocks - let modified = stripAllThinkingBlocks(messages); + // Strip only invalid/incompatible thinking blocks (keep valid ones) + let modified = stripInvalidThinkingBlocks(messages, targetFamily); if (state.interruptedTool) { // For interrupted tools: just strip thinking and add a synthetic assistant message