688 lines
23 KiB
JavaScript
688 lines
23 KiB
JavaScript
/**
|
|
* Format Converter
|
|
* Converts between Anthropic Messages API format and Google Generative AI format
|
|
*
|
|
* Based on patterns from:
|
|
* - https://github.com/NoeFabris/opencode-antigravity-auth
|
|
* - https://github.com/1rgs/claude-code-proxy
|
|
*/
|
|
|
|
import crypto from 'crypto';
|
|
import { MODEL_MAPPINGS } from './constants.js';
|
|
|
|
// Default thinking budget (16K tokens)
|
|
const DEFAULT_THINKING_BUDGET = 16000;
|
|
// Claude thinking models need larger max output tokens
|
|
const CLAUDE_THINKING_MAX_OUTPUT_TOKENS = 64000;
|
|
|
|
/**
|
|
* Map Anthropic model name to Antigravity model name
|
|
*/
|
|
export function mapModelName(anthropicModel) {
|
|
return MODEL_MAPPINGS[anthropicModel] || anthropicModel;
|
|
}
|
|
|
|
/**
|
|
* Check if a part is a thinking block
|
|
*/
|
|
function isThinkingPart(part) {
|
|
return part.type === 'thinking' ||
|
|
part.type === 'redacted_thinking' ||
|
|
part.thinking !== undefined ||
|
|
part.thought === true;
|
|
}
|
|
|
|
/**
|
|
* Check if a thinking part has a valid signature (>= 50 chars)
|
|
*/
|
|
function hasValidSignature(part) {
|
|
const signature = part.thought === true ? part.thoughtSignature : part.signature;
|
|
return typeof signature === 'string' && signature.length >= 50;
|
|
}
|
|
|
|
/**
|
|
* Sanitize a thinking part by keeping only allowed fields
|
|
*/
|
|
function sanitizeThinkingPart(part) {
|
|
// Gemini-style thought blocks: { thought: true, text, thoughtSignature }
|
|
if (part.thought === true) {
|
|
const sanitized = { thought: true };
|
|
if (part.text !== undefined) sanitized.text = part.text;
|
|
if (part.thoughtSignature !== undefined) sanitized.thoughtSignature = part.thoughtSignature;
|
|
return sanitized;
|
|
}
|
|
|
|
// Anthropic-style thinking blocks: { type: "thinking", thinking, signature }
|
|
if (part.type === 'thinking' || part.thinking !== undefined) {
|
|
const sanitized = { type: 'thinking' };
|
|
if (part.thinking !== undefined) sanitized.thinking = part.thinking;
|
|
if (part.signature !== undefined) sanitized.signature = part.signature;
|
|
return sanitized;
|
|
}
|
|
|
|
return part;
|
|
}
|
|
|
|
/**
|
|
* Filter content array, keeping only thinking blocks with valid signatures.
|
|
* Since signature_delta transmits signatures properly, cache is no longer needed.
|
|
*/
|
|
function filterContentArray(contentArray) {
|
|
const filtered = [];
|
|
|
|
for (const item of contentArray) {
|
|
if (!item || typeof item !== 'object') {
|
|
filtered.push(item);
|
|
continue;
|
|
}
|
|
|
|
if (!isThinkingPart(item)) {
|
|
filtered.push(item);
|
|
continue;
|
|
}
|
|
|
|
// Keep items with valid signatures
|
|
if (hasValidSignature(item)) {
|
|
filtered.push(sanitizeThinkingPart(item));
|
|
continue;
|
|
}
|
|
|
|
// Drop unsigned thinking blocks
|
|
console.log('[FormatConverter] Dropping unsigned thinking block');
|
|
}
|
|
|
|
return filtered;
|
|
}
|
|
|
|
/**
|
|
* Filter unsigned thinking blocks from contents (Gemini format)
|
|
*/
|
|
export function filterUnsignedThinkingBlocks(contents) {
|
|
return contents.map(content => {
|
|
if (!content || typeof content !== 'object') return content;
|
|
|
|
if (Array.isArray(content.parts)) {
|
|
return { ...content, parts: filterContentArray(content.parts) };
|
|
}
|
|
|
|
return content;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Remove trailing unsigned thinking blocks from assistant messages.
|
|
* Claude/Gemini APIs require that assistant messages don't end with unsigned thinking blocks.
|
|
* This function removes thinking blocks from the end of content arrays.
|
|
*/
|
|
export function removeTrailingThinkingBlocks(content) {
|
|
if (!Array.isArray(content)) return content;
|
|
if (content.length === 0) return content;
|
|
|
|
// Work backwards from the end, removing thinking blocks
|
|
let endIndex = content.length;
|
|
for (let i = content.length - 1; i >= 0; i--) {
|
|
const block = content[i];
|
|
if (!block || typeof block !== 'object') break;
|
|
|
|
// Check if it's a thinking block (any format)
|
|
const isThinking = isThinkingPart(block);
|
|
|
|
if (isThinking) {
|
|
// Check if it has a valid signature
|
|
if (!hasValidSignature(block)) {
|
|
endIndex = i;
|
|
} else {
|
|
break; // Stop at signed thinking block
|
|
}
|
|
} else {
|
|
break; // Stop at first non-thinking block
|
|
}
|
|
}
|
|
|
|
if (endIndex < content.length) {
|
|
console.log('[FormatConverter] Removed', content.length - endIndex, 'trailing unsigned thinking blocks');
|
|
return content.slice(0, endIndex);
|
|
}
|
|
|
|
return content;
|
|
}
|
|
|
|
/**
|
|
* Sanitize a thinking block by removing extra fields like cache_control.
|
|
* Only keeps: type, thinking, signature
|
|
*/
|
|
function sanitizeAnthropicThinkingBlock(block) {
|
|
if (!block || block.type !== 'thinking') return block;
|
|
|
|
const sanitized = { type: 'thinking' };
|
|
if (block.thinking !== undefined) sanitized.thinking = block.thinking;
|
|
if (block.signature !== undefined) sanitized.signature = block.signature;
|
|
return sanitized;
|
|
}
|
|
|
|
/**
|
|
* Filter thinking blocks: keep only those with valid signatures.
|
|
* Blocks without signatures are dropped (API requires signatures).
|
|
* Also sanitizes blocks to remove extra fields like cache_control.
|
|
*/
|
|
export function restoreThinkingSignatures(content) {
|
|
if (!Array.isArray(content)) return content;
|
|
|
|
const originalLength = content.length;
|
|
const filtered = [];
|
|
|
|
for (const block of content) {
|
|
if (!block || block.type !== 'thinking') {
|
|
filtered.push(block);
|
|
continue;
|
|
}
|
|
|
|
// Keep blocks with valid signatures (>= 50 chars), sanitized
|
|
if (block.signature && block.signature.length >= 50) {
|
|
filtered.push(sanitizeAnthropicThinkingBlock(block));
|
|
}
|
|
// Unsigned thinking blocks are dropped
|
|
}
|
|
|
|
if (filtered.length < originalLength) {
|
|
console.log(`[FormatConverter] Dropped ${originalLength - filtered.length} unsigned thinking block(s)`);
|
|
}
|
|
|
|
return filtered;
|
|
}
|
|
|
|
/**
|
|
* Reorder content so that:
|
|
* 1. Thinking blocks come first (required when thinking is enabled)
|
|
* 2. Text blocks come in the middle (filtering out empty/useless ones)
|
|
* 3. Tool_use blocks come at the end (required before tool_result)
|
|
*
|
|
* Claude API requires that when thinking is enabled, assistant messages must start with thinking.
|
|
*/
|
|
export function reorderAssistantContent(content) {
|
|
if (!Array.isArray(content)) return content;
|
|
if (content.length <= 1) return content;
|
|
|
|
const thinkingBlocks = [];
|
|
const textBlocks = [];
|
|
const toolUseBlocks = [];
|
|
let droppedEmptyBlocks = 0;
|
|
|
|
for (const block of content) {
|
|
if (!block) continue;
|
|
|
|
if (block.type === 'thinking') {
|
|
// Sanitize thinking blocks to remove cache_control and other extra fields
|
|
thinkingBlocks.push(sanitizeAnthropicThinkingBlock(block));
|
|
} else if (block.type === 'tool_use') {
|
|
toolUseBlocks.push(block);
|
|
} else if (block.type === 'text') {
|
|
// Only keep text blocks with meaningful content
|
|
if (block.text && block.text.trim().length > 0) {
|
|
textBlocks.push(block);
|
|
} else {
|
|
droppedEmptyBlocks++;
|
|
}
|
|
} else {
|
|
// Other block types go in the text position
|
|
textBlocks.push(block);
|
|
}
|
|
}
|
|
|
|
if (droppedEmptyBlocks > 0) {
|
|
console.log(`[FormatConverter] Dropped ${droppedEmptyBlocks} empty text block(s)`);
|
|
}
|
|
|
|
const reordered = [...thinkingBlocks, ...textBlocks, ...toolUseBlocks];
|
|
|
|
// Log only if actual reordering happened (not just filtering)
|
|
if (reordered.length === content.length) {
|
|
const originalOrder = content.map(b => b?.type || 'unknown').join(',');
|
|
const newOrder = reordered.map(b => b?.type || 'unknown').join(',');
|
|
if (originalOrder !== newOrder) {
|
|
console.log('[FormatConverter] Reordered assistant content');
|
|
}
|
|
}
|
|
|
|
return reordered;
|
|
}
|
|
|
|
/**
|
|
* Convert Anthropic message content to Google Generative AI parts
|
|
*/
|
|
function convertContentToParts(content, isClaudeModel = false) {
|
|
if (typeof content === 'string') {
|
|
return [{ text: content }];
|
|
}
|
|
|
|
if (!Array.isArray(content)) {
|
|
return [{ text: String(content) }];
|
|
}
|
|
|
|
const parts = [];
|
|
|
|
for (const block of content) {
|
|
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;
|
|
}
|
|
|
|
parts.push({ functionCall });
|
|
} else if (block.type === 'tool_result') {
|
|
// Convert tool_result to functionResponse (Google format)
|
|
let responseContent = block.content;
|
|
if (typeof responseContent === 'string') {
|
|
responseContent = { result: responseContent };
|
|
} else if (Array.isArray(responseContent)) {
|
|
const texts = responseContent
|
|
.filter(c => c.type === 'text')
|
|
.map(c => c.text)
|
|
.join('\n');
|
|
responseContent = { result: texts };
|
|
}
|
|
|
|
const functionResponse = {
|
|
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 });
|
|
} else if (block.type === 'thinking') {
|
|
// Handle thinking blocks - only those with valid signatures
|
|
if (block.signature && block.signature.length >= 50) {
|
|
// Convert to Gemini format with signature
|
|
parts.push({
|
|
text: block.thinking,
|
|
thought: true,
|
|
thoughtSignature: block.signature
|
|
});
|
|
}
|
|
// Unsigned thinking blocks are dropped upstream
|
|
}
|
|
}
|
|
|
|
return parts;
|
|
}
|
|
|
|
/**
|
|
* Convert Anthropic role to Google role
|
|
*/
|
|
function convertRole(role) {
|
|
if (role === 'assistant') return 'model';
|
|
if (role === 'user') return 'user';
|
|
return 'user'; // Default to user
|
|
}
|
|
|
|
/**
|
|
* Convert Anthropic Messages API request to the format expected by Cloud Code
|
|
*
|
|
* Uses Google Generative AI format, but for Claude models:
|
|
* - Keeps tool_result in Anthropic format (required by Claude API)
|
|
*
|
|
* @param {Object} anthropicRequest - Anthropic format request
|
|
* @returns {Object} Request body for Cloud Code API
|
|
*/
|
|
export function convertAnthropicToGoogle(anthropicRequest) {
|
|
const { messages, system, max_tokens, temperature, top_p, top_k, stop_sequences, tools, tool_choice, thinking } = anthropicRequest;
|
|
const modelName = anthropicRequest.model || '';
|
|
const isClaudeModel = modelName.toLowerCase().includes('claude');
|
|
const isClaudeThinkingModel = isClaudeModel && modelName.toLowerCase().includes('thinking');
|
|
|
|
|
|
const googleRequest = {
|
|
contents: [],
|
|
generationConfig: {}
|
|
};
|
|
|
|
// Handle system instruction
|
|
if (system) {
|
|
let systemParts = [];
|
|
if (typeof system === 'string') {
|
|
systemParts = [{ text: system }];
|
|
} else if (Array.isArray(system)) {
|
|
// Filter for text blocks as system prompts are usually text
|
|
// Anthropic supports text blocks in system prompts
|
|
systemParts = system
|
|
.filter(block => block.type === 'text')
|
|
.map(block => ({ text: block.text }));
|
|
}
|
|
|
|
if (systemParts.length > 0) {
|
|
googleRequest.systemInstruction = {
|
|
parts: systemParts
|
|
};
|
|
}
|
|
}
|
|
|
|
// Add interleaved thinking hint for Claude thinking models with tools
|
|
if (isClaudeThinkingModel && tools && tools.length > 0) {
|
|
const hint = 'Interleaved thinking is enabled. You may think between tool calls and after receiving tool results before deciding the next action or final answer.';
|
|
if (!googleRequest.systemInstruction) {
|
|
googleRequest.systemInstruction = { parts: [{ text: hint }] };
|
|
} else {
|
|
const lastPart = googleRequest.systemInstruction.parts[googleRequest.systemInstruction.parts.length - 1];
|
|
if (lastPart && lastPart.text) {
|
|
lastPart.text = `${lastPart.text}\n\n${hint}`;
|
|
} else {
|
|
googleRequest.systemInstruction.parts.push({ text: hint });
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert messages to contents, then filter unsigned thinking blocks
|
|
for (const msg of messages) {
|
|
let msgContent = msg.content;
|
|
|
|
// For assistant messages, process thinking blocks and reorder content
|
|
if ((msg.role === 'assistant' || msg.role === 'model') && Array.isArray(msgContent)) {
|
|
// First, try to restore signatures for unsigned thinking blocks from cache
|
|
msgContent = restoreThinkingSignatures(msgContent);
|
|
// Remove trailing unsigned thinking blocks
|
|
msgContent = removeTrailingThinkingBlocks(msgContent);
|
|
// Reorder: thinking first, then text, then tool_use
|
|
msgContent = reorderAssistantContent(msgContent);
|
|
}
|
|
|
|
const parts = convertContentToParts(msgContent, isClaudeModel);
|
|
const content = {
|
|
role: convertRole(msg.role),
|
|
parts: parts
|
|
};
|
|
googleRequest.contents.push(content);
|
|
}
|
|
|
|
// Filter unsigned thinking blocks for Claude models
|
|
if (isClaudeModel) {
|
|
googleRequest.contents = filterUnsignedThinkingBlocks(googleRequest.contents);
|
|
}
|
|
|
|
// Generation config
|
|
if (max_tokens) {
|
|
googleRequest.generationConfig.maxOutputTokens = max_tokens;
|
|
}
|
|
if (temperature !== undefined) {
|
|
googleRequest.generationConfig.temperature = temperature;
|
|
}
|
|
if (top_p !== undefined) {
|
|
googleRequest.generationConfig.topP = top_p;
|
|
}
|
|
if (top_k !== undefined) {
|
|
googleRequest.generationConfig.topK = top_k;
|
|
}
|
|
if (stop_sequences && stop_sequences.length > 0) {
|
|
googleRequest.generationConfig.stopSequences = stop_sequences;
|
|
}
|
|
|
|
// Enable thinking for Claude thinking models
|
|
if (isClaudeThinkingModel) {
|
|
// Get budget from request or use default
|
|
const thinkingBudget = thinking?.budget_tokens || DEFAULT_THINKING_BUDGET;
|
|
|
|
googleRequest.generationConfig.thinkingConfig = {
|
|
include_thoughts: true,
|
|
thinking_budget: thinkingBudget
|
|
};
|
|
|
|
// Ensure maxOutputTokens is large enough for thinking models
|
|
if (!googleRequest.generationConfig.maxOutputTokens ||
|
|
googleRequest.generationConfig.maxOutputTokens <= thinkingBudget) {
|
|
googleRequest.generationConfig.maxOutputTokens = CLAUDE_THINKING_MAX_OUTPUT_TOKENS;
|
|
}
|
|
|
|
console.log('[FormatConverter] Thinking enabled with budget:', thinkingBudget);
|
|
}
|
|
|
|
// Convert tools to Google format
|
|
if (tools && tools.length > 0) {
|
|
const functionDeclarations = tools.map((tool, idx) => {
|
|
// Extract name from various possible locations
|
|
const name = tool.name || tool.function?.name || tool.custom?.name || `tool-${idx}`;
|
|
|
|
// Extract description from various possible locations
|
|
const description = tool.description || tool.function?.description || tool.custom?.description || '';
|
|
|
|
// Extract schema from various possible locations
|
|
const schema = tool.input_schema
|
|
|| tool.function?.input_schema
|
|
|| tool.function?.parameters
|
|
|| tool.custom?.input_schema
|
|
|| tool.parameters
|
|
|| { type: 'object' };
|
|
|
|
return {
|
|
name: String(name).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64),
|
|
description: description,
|
|
parameters: sanitizeSchema(schema)
|
|
};
|
|
});
|
|
|
|
googleRequest.tools = [{ functionDeclarations }];
|
|
console.log('[FormatConverter] Tools:', JSON.stringify(googleRequest.tools).substring(0, 300));
|
|
}
|
|
|
|
return googleRequest;
|
|
}
|
|
|
|
/**
|
|
* Sanitize JSON Schema for Antigravity API compatibility.
|
|
* Uses allowlist approach - only permit known-safe JSON Schema features.
|
|
* Converts "const" to equivalent "enum" for compatibility.
|
|
* Generates placeholder schema for empty tool schemas.
|
|
*/
|
|
function sanitizeSchema(schema) {
|
|
if (!schema || typeof schema !== 'object') {
|
|
// Empty/missing schema - generate placeholder with reason property
|
|
return {
|
|
type: 'object',
|
|
properties: {
|
|
reason: {
|
|
type: 'string',
|
|
description: 'Reason for calling this tool'
|
|
}
|
|
},
|
|
required: ['reason']
|
|
};
|
|
}
|
|
|
|
// Allowlist of permitted JSON Schema fields
|
|
const ALLOWED_FIELDS = new Set([
|
|
'type',
|
|
'description',
|
|
'properties',
|
|
'required',
|
|
'items',
|
|
'enum',
|
|
'title'
|
|
]);
|
|
|
|
const sanitized = {};
|
|
|
|
for (const [key, value] of Object.entries(schema)) {
|
|
// Convert "const" to "enum" for compatibility
|
|
if (key === 'const') {
|
|
sanitized.enum = [value];
|
|
continue;
|
|
}
|
|
|
|
// Skip fields not in allowlist
|
|
if (!ALLOWED_FIELDS.has(key)) {
|
|
continue;
|
|
}
|
|
|
|
if (key === 'properties' && value && typeof value === 'object') {
|
|
sanitized.properties = {};
|
|
for (const [propKey, propValue] of Object.entries(value)) {
|
|
sanitized.properties[propKey] = sanitizeSchema(propValue);
|
|
}
|
|
} else if (key === 'items' && value && typeof value === 'object') {
|
|
if (Array.isArray(value)) {
|
|
sanitized.items = value.map(item => sanitizeSchema(item));
|
|
} else {
|
|
sanitized.items = sanitizeSchema(value);
|
|
}
|
|
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
sanitized[key] = sanitizeSchema(value);
|
|
} else {
|
|
sanitized[key] = value;
|
|
}
|
|
}
|
|
|
|
// Ensure we have at least a type
|
|
if (!sanitized.type) {
|
|
sanitized.type = 'object';
|
|
}
|
|
|
|
// If object type with no properties, add placeholder
|
|
if (sanitized.type === 'object' && (!sanitized.properties || Object.keys(sanitized.properties).length === 0)) {
|
|
sanitized.properties = {
|
|
reason: {
|
|
type: 'string',
|
|
description: 'Reason for calling this tool'
|
|
}
|
|
};
|
|
sanitized.required = ['reason'];
|
|
}
|
|
|
|
return sanitized;
|
|
}
|
|
|
|
/**
|
|
* Convert Google Generative AI response to Anthropic Messages API format
|
|
*
|
|
* @param {Object} googleResponse - Google format response (the inner response object)
|
|
* @param {string} model - The model name used
|
|
* @returns {Object} Anthropic format response
|
|
*/
|
|
export function convertGoogleToAnthropic(googleResponse, model) {
|
|
// Handle the response wrapper
|
|
const response = googleResponse.response || googleResponse;
|
|
|
|
|
|
const candidates = response.candidates || [];
|
|
const firstCandidate = candidates[0] || {};
|
|
const content = firstCandidate.content || {};
|
|
const parts = content.parts || [];
|
|
|
|
// Convert parts to Anthropic content blocks
|
|
const anthropicContent = [];
|
|
let hasToolCalls = false;
|
|
|
|
for (const part of parts) {
|
|
if (part.text !== undefined) {
|
|
// Handle thinking blocks
|
|
if (part.thought === true) {
|
|
const signature = part.thoughtSignature || '';
|
|
|
|
// Include thinking blocks in the response for Claude Code
|
|
anthropicContent.push({
|
|
type: 'thinking',
|
|
thinking: part.text,
|
|
signature: signature
|
|
});
|
|
} else {
|
|
anthropicContent.push({
|
|
type: 'text',
|
|
text: part.text
|
|
});
|
|
}
|
|
} else if (part.functionCall) {
|
|
// Convert functionCall to tool_use
|
|
// Use the id from the response if available, otherwise generate one
|
|
anthropicContent.push({
|
|
type: 'tool_use',
|
|
id: part.functionCall.id || `toolu_${crypto.randomBytes(12).toString('hex')}`,
|
|
name: part.functionCall.name,
|
|
input: part.functionCall.args || {}
|
|
});
|
|
hasToolCalls = true;
|
|
}
|
|
}
|
|
|
|
// Determine stop reason
|
|
const finishReason = firstCandidate.finishReason;
|
|
let stopReason = 'end_turn';
|
|
if (finishReason === 'STOP') {
|
|
stopReason = 'end_turn';
|
|
} else if (finishReason === 'MAX_TOKENS') {
|
|
stopReason = 'max_tokens';
|
|
} else if (finishReason === 'TOOL_USE' || hasToolCalls) {
|
|
stopReason = 'tool_use';
|
|
}
|
|
|
|
// Extract usage metadata
|
|
const usageMetadata = response.usageMetadata || {};
|
|
|
|
return {
|
|
id: `msg_${crypto.randomBytes(16).toString('hex')}`,
|
|
type: 'message',
|
|
role: 'assistant',
|
|
content: anthropicContent.length > 0 ? anthropicContent : [{ type: 'text', text: '' }],
|
|
model: model,
|
|
stop_reason: stopReason,
|
|
stop_sequence: null,
|
|
usage: {
|
|
input_tokens: usageMetadata.promptTokenCount || 0,
|
|
output_tokens: usageMetadata.candidatesTokenCount || 0
|
|
}
|
|
};
|
|
}
|
|
|
|
export default {
|
|
mapModelName,
|
|
convertAnthropicToGoogle,
|
|
convertGoogleToAnthropic
|
|
};
|