gravityyy-proxyyy / src /format /content-converter.js
bardd's picture
Fix gemini-3.1-pro-high by routing to pro-low with thinkingLevel high.
4badc3b
Raw
History Blame Contribute Delete
8.26 kB
/**
* Content Converter
* Converts Anthropic message content to Google Generative AI parts format
*/
import { MIN_SIGNATURE_LENGTH, GEMINI_SKIP_SIGNATURE } from '../constants.js';
import { getCachedSignature, getCachedSignatureFamily } from './signature-cache.js';
import { logger } from '../utils/logger.js';
/**
* Convert Anthropic role to Google role
* @param {string} role - Anthropic role ('user', 'assistant')
* @returns {string} Google role ('user', 'model')
*/
export function convertRole(role) {
if (role === 'assistant') return 'model';
if (role === 'user') return 'user';
return 'user'; // Default to user
}
/**
* Convert Anthropic message content to Google Generative AI parts
* @param {string|Array} content - Anthropic message content
* @param {boolean} isClaudeModel - Whether the model is a Claude model
* @param {boolean} isGeminiModel - Whether the model is a Gemini model
* @returns {Array} Google Generative AI parts array
*/
export function convertContentToParts(content, isClaudeModel = false, isGeminiModel = false) {
if (typeof content === 'string') {
return [{ text: content }];
}
if (!Array.isArray(content)) {
return [{ text: String(content) }];
}
const parts = [];
const deferredInlineData = []; // Collect inlineData to add at the end (Issue #91)
for (const block of content) {
if (!block) continue;
if (block.type === 'text') {
// Skip empty text blocks - they cause API errors
if (block.text && block.text.trim()) {
parts.push({ text: block.text });
}
} else if (block.type === 'image') {
// Handle image content
if (block.source?.type === 'base64') {
// Base64-encoded image
parts.push({
inlineData: {
mimeType: block.source.media_type,
data: block.source.data
}
});
} else if (block.source?.type === 'url') {
// URL-referenced image
parts.push({
fileData: {
mimeType: block.source.media_type || 'image/jpeg',
fileUri: block.source.url
}
});
}
} else if (block.type === 'document') {
// Handle document content (e.g. PDF)
if (block.source?.type === 'base64') {
parts.push({
inlineData: {
mimeType: block.source.media_type,
data: block.source.data
}
});
} else if (block.source?.type === 'url') {
parts.push({
fileData: {
mimeType: block.source.media_type || 'application/pdf',
fileUri: block.source.url
}
});
}
} else if (block.type === 'tool_use') {
// Convert tool_use to functionCall (Google format)
// For Claude models, include the id field
const functionCall = {
name: block.name,
args: block.input || {}
};
if (isClaudeModel && block.id) {
functionCall.id = block.id;
}
// Build the part with functionCall
const part = { functionCall };
// For Gemini models, include thoughtSignature at the part level
// This is required by Gemini 3+ for tool calls to work correctly
if (isGeminiModel) {
// Priority: block.thoughtSignature > cache > GEMINI_SKIP_SIGNATURE
let signature = block.thoughtSignature;
if (!signature && block.id) {
signature = getCachedSignature(block.id);
if (signature) {
logger.debug(`[ContentConverter] Restored signature from cache for: ${block.id}`);
}
}
part.thoughtSignature = signature || GEMINI_SKIP_SIGNATURE;
}
parts.push(part);
} else if (block.type === 'tool_result') {
// Convert tool_result to functionResponse (Google format)
let responseContent = block.content;
let imageParts = [];
if (typeof responseContent === 'string') {
responseContent = { result: responseContent };
} else if (Array.isArray(responseContent)) {
// Extract images from tool results first (e.g., from Read tool reading image files)
for (const item of responseContent) {
if (item.type === 'image' && item.source?.type === 'base64') {
imageParts.push({
inlineData: {
mimeType: item.source.media_type,
data: item.source.data
}
});
}
}
// Extract text content
const texts = responseContent
.filter(c => c.type === 'text')
.map(c => c.text)
.join('\n');
responseContent = { result: texts || (imageParts.length > 0 ? 'Image attached' : '') };
}
const functionResponse = {
name: block.name || block.tool_use_id || 'unknown',
response: responseContent
};
// For Claude models, the id field must match the tool_use_id
if (isClaudeModel && block.tool_use_id) {
functionResponse.id = block.tool_use_id;
}
parts.push({ functionResponse });
// Defer images from the tool result to end of parts array (Issue #91)
// This ensures all functionResponse parts are consecutive
deferredInlineData.push(...imageParts);
} else if (block.type === 'thinking') {
// Handle thinking blocks with signature compatibility check
if (block.signature && block.signature.length >= MIN_SIGNATURE_LENGTH) {
const signatureFamily = getCachedSignatureFamily(block.signature);
const targetFamily = isClaudeModel ? 'claude' : isGeminiModel ? 'gemini' : null;
// For Claude: drop unknown or non-Claude signatures.
// This prevents resumed sessions from forwarding stale signatures that
// fail upstream validation with "Invalid `signature` in `thinking` block".
if (isClaudeModel && (!signatureFamily || signatureFamily !== 'claude')) {
logger.debug('[ContentConverter] Dropping untrusted thinking signature for Claude model');
continue;
}
// 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 (existing behavior)
}
}
// Add deferred inlineData at the end (Issue #91)
// This ensures functionResponse parts are consecutive, which Claude's API requires
parts.push(...deferredInlineData);
return parts;
}