antigravity-claude-proxy 1.1.1 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cloudcode-client.js +4 -1
- package/src/constants.js +10 -0
- package/src/format/content-converter.js +151 -0
- package/src/format/index.js +20 -0
- package/src/format/request-converter.js +214 -0
- package/src/format/response-converter.js +104 -0
- package/src/format/schema-sanitizer.js +646 -0
- package/src/format/signature-cache.js +65 -0
- package/src/format/thinking-utils.js +481 -0
- package/src/format-converter.js +0 -828
package/package.json
CHANGED
package/src/cloudcode-client.js
CHANGED
|
@@ -22,7 +22,8 @@ import {
|
|
|
22
22
|
import {
|
|
23
23
|
convertAnthropicToGoogle,
|
|
24
24
|
convertGoogleToAnthropic
|
|
25
|
-
} from './format
|
|
25
|
+
} from './format/index.js';
|
|
26
|
+
import { cacheSignature } from './format/signature-cache.js';
|
|
26
27
|
import { formatDuration, sleep } from './utils/helpers.js';
|
|
27
28
|
import { isRateLimitError, isAuthError } from './errors.js';
|
|
28
29
|
|
|
@@ -848,6 +849,8 @@ async function* streamSSEResponse(response, originalModel) {
|
|
|
848
849
|
// Store the signature in the tool_use block for later retrieval
|
|
849
850
|
if (functionCallSignature && functionCallSignature.length >= MIN_SIGNATURE_LENGTH) {
|
|
850
851
|
toolUseBlock.thoughtSignature = functionCallSignature;
|
|
852
|
+
// Cache for future requests (Claude Code may strip this field)
|
|
853
|
+
cacheSignature(toolId, functionCallSignature);
|
|
851
854
|
}
|
|
852
855
|
|
|
853
856
|
yield {
|
package/src/constants.js
CHANGED
|
@@ -87,6 +87,14 @@ export const MIN_SIGNATURE_LENGTH = 50; // Minimum valid thinking signature leng
|
|
|
87
87
|
// Gemini-specific limits
|
|
88
88
|
export const GEMINI_MAX_OUTPUT_TOKENS = 16384;
|
|
89
89
|
|
|
90
|
+
// Gemini signature handling
|
|
91
|
+
// Sentinel value to skip thought signature validation when Claude Code strips the field
|
|
92
|
+
// See: https://ai.google.dev/gemini-api/docs/thought-signatures
|
|
93
|
+
export const GEMINI_SKIP_SIGNATURE = 'skip_thought_signature_validator';
|
|
94
|
+
|
|
95
|
+
// Cache TTL for Gemini thoughtSignatures (2 hours)
|
|
96
|
+
export const GEMINI_SIGNATURE_CACHE_TTL_MS = 2 * 60 * 60 * 1000;
|
|
97
|
+
|
|
90
98
|
/**
|
|
91
99
|
* Get the model family from model name (dynamic detection, no hardcoded list).
|
|
92
100
|
* @param {string} modelName - The model name from the request
|
|
@@ -152,6 +160,8 @@ export default {
|
|
|
152
160
|
MAX_WAIT_BEFORE_ERROR_MS,
|
|
153
161
|
MIN_SIGNATURE_LENGTH,
|
|
154
162
|
GEMINI_MAX_OUTPUT_TOKENS,
|
|
163
|
+
GEMINI_SKIP_SIGNATURE,
|
|
164
|
+
GEMINI_SIGNATURE_CACHE_TTL_MS,
|
|
155
165
|
getModelFamily,
|
|
156
166
|
isThinkingModel,
|
|
157
167
|
OAUTH_CONFIG,
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Converter
|
|
3
|
+
* Converts Anthropic message content to Google Generative AI parts format
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { MIN_SIGNATURE_LENGTH, GEMINI_SKIP_SIGNATURE } from '../constants.js';
|
|
7
|
+
import { getCachedSignature } from './signature-cache.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Convert Anthropic role to Google role
|
|
11
|
+
* @param {string} role - Anthropic role ('user', 'assistant')
|
|
12
|
+
* @returns {string} Google role ('user', 'model')
|
|
13
|
+
*/
|
|
14
|
+
export function convertRole(role) {
|
|
15
|
+
if (role === 'assistant') return 'model';
|
|
16
|
+
if (role === 'user') return 'user';
|
|
17
|
+
return 'user'; // Default to user
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Convert Anthropic message content to Google Generative AI parts
|
|
22
|
+
* @param {string|Array} content - Anthropic message content
|
|
23
|
+
* @param {boolean} isClaudeModel - Whether the model is a Claude model
|
|
24
|
+
* @param {boolean} isGeminiModel - Whether the model is a Gemini model
|
|
25
|
+
* @returns {Array} Google Generative AI parts array
|
|
26
|
+
*/
|
|
27
|
+
export function convertContentToParts(content, isClaudeModel = false, isGeminiModel = false) {
|
|
28
|
+
if (typeof content === 'string') {
|
|
29
|
+
return [{ text: content }];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!Array.isArray(content)) {
|
|
33
|
+
return [{ text: String(content) }];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const parts = [];
|
|
37
|
+
|
|
38
|
+
for (const block of content) {
|
|
39
|
+
if (block.type === 'text') {
|
|
40
|
+
// Skip empty text blocks - they cause API errors
|
|
41
|
+
if (block.text && block.text.trim()) {
|
|
42
|
+
parts.push({ text: block.text });
|
|
43
|
+
}
|
|
44
|
+
} else if (block.type === 'image') {
|
|
45
|
+
// Handle image content
|
|
46
|
+
if (block.source?.type === 'base64') {
|
|
47
|
+
// Base64-encoded image
|
|
48
|
+
parts.push({
|
|
49
|
+
inlineData: {
|
|
50
|
+
mimeType: block.source.media_type,
|
|
51
|
+
data: block.source.data
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
} else if (block.source?.type === 'url') {
|
|
55
|
+
// URL-referenced image
|
|
56
|
+
parts.push({
|
|
57
|
+
fileData: {
|
|
58
|
+
mimeType: block.source.media_type || 'image/jpeg',
|
|
59
|
+
fileUri: block.source.url
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
} else if (block.type === 'document') {
|
|
64
|
+
// Handle document content (e.g. PDF)
|
|
65
|
+
if (block.source?.type === 'base64') {
|
|
66
|
+
parts.push({
|
|
67
|
+
inlineData: {
|
|
68
|
+
mimeType: block.source.media_type,
|
|
69
|
+
data: block.source.data
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
} else if (block.source?.type === 'url') {
|
|
73
|
+
parts.push({
|
|
74
|
+
fileData: {
|
|
75
|
+
mimeType: block.source.media_type || 'application/pdf',
|
|
76
|
+
fileUri: block.source.url
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
} else if (block.type === 'tool_use') {
|
|
81
|
+
// Convert tool_use to functionCall (Google format)
|
|
82
|
+
// For Claude models, include the id field
|
|
83
|
+
const functionCall = {
|
|
84
|
+
name: block.name,
|
|
85
|
+
args: block.input || {}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
if (isClaudeModel && block.id) {
|
|
89
|
+
functionCall.id = block.id;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Build the part with functionCall
|
|
93
|
+
const part = { functionCall };
|
|
94
|
+
|
|
95
|
+
// For Gemini models, include thoughtSignature at the part level
|
|
96
|
+
// This is required by Gemini 3+ for tool calls to work correctly
|
|
97
|
+
if (isGeminiModel) {
|
|
98
|
+
// Priority: block.thoughtSignature > cache > GEMINI_SKIP_SIGNATURE
|
|
99
|
+
let signature = block.thoughtSignature;
|
|
100
|
+
|
|
101
|
+
if (!signature && block.id) {
|
|
102
|
+
signature = getCachedSignature(block.id);
|
|
103
|
+
if (signature) {
|
|
104
|
+
console.log('[ContentConverter] Restored signature from cache for:', block.id);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
part.thoughtSignature = signature || GEMINI_SKIP_SIGNATURE;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
parts.push(part);
|
|
112
|
+
} else if (block.type === 'tool_result') {
|
|
113
|
+
// Convert tool_result to functionResponse (Google format)
|
|
114
|
+
let responseContent = block.content;
|
|
115
|
+
if (typeof responseContent === 'string') {
|
|
116
|
+
responseContent = { result: responseContent };
|
|
117
|
+
} else if (Array.isArray(responseContent)) {
|
|
118
|
+
const texts = responseContent
|
|
119
|
+
.filter(c => c.type === 'text')
|
|
120
|
+
.map(c => c.text)
|
|
121
|
+
.join('\n');
|
|
122
|
+
responseContent = { result: texts };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const functionResponse = {
|
|
126
|
+
name: block.tool_use_id || 'unknown',
|
|
127
|
+
response: responseContent
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// For Claude models, the id field must match the tool_use_id
|
|
131
|
+
if (isClaudeModel && block.tool_use_id) {
|
|
132
|
+
functionResponse.id = block.tool_use_id;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
parts.push({ functionResponse });
|
|
136
|
+
} else if (block.type === 'thinking') {
|
|
137
|
+
// Handle thinking blocks - only those with valid signatures
|
|
138
|
+
if (block.signature && block.signature.length >= MIN_SIGNATURE_LENGTH) {
|
|
139
|
+
// Convert to Gemini format with signature
|
|
140
|
+
parts.push({
|
|
141
|
+
text: block.thinking,
|
|
142
|
+
thought: true,
|
|
143
|
+
thoughtSignature: block.signature
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
// Unsigned thinking blocks are dropped upstream
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return parts;
|
|
151
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format Converter Module
|
|
3
|
+
* Converts between Anthropic Messages API format and Google Generative AI format
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Re-export all from each module
|
|
7
|
+
export * from './request-converter.js';
|
|
8
|
+
export * from './response-converter.js';
|
|
9
|
+
export * from './content-converter.js';
|
|
10
|
+
export * from './schema-sanitizer.js';
|
|
11
|
+
export * from './thinking-utils.js';
|
|
12
|
+
|
|
13
|
+
// Default export for backward compatibility
|
|
14
|
+
import { convertAnthropicToGoogle } from './request-converter.js';
|
|
15
|
+
import { convertGoogleToAnthropic } from './response-converter.js';
|
|
16
|
+
|
|
17
|
+
export default {
|
|
18
|
+
convertAnthropicToGoogle,
|
|
19
|
+
convertGoogleToAnthropic
|
|
20
|
+
};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request Converter
|
|
3
|
+
* Converts Anthropic Messages API requests to Google Generative AI format
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
GEMINI_MAX_OUTPUT_TOKENS,
|
|
8
|
+
getModelFamily,
|
|
9
|
+
isThinkingModel
|
|
10
|
+
} from '../constants.js';
|
|
11
|
+
import { convertContentToParts, convertRole } from './content-converter.js';
|
|
12
|
+
import { sanitizeSchema, cleanSchemaForGemini } from './schema-sanitizer.js';
|
|
13
|
+
import {
|
|
14
|
+
restoreThinkingSignatures,
|
|
15
|
+
removeTrailingThinkingBlocks,
|
|
16
|
+
reorderAssistantContent,
|
|
17
|
+
filterUnsignedThinkingBlocks,
|
|
18
|
+
needsThinkingRecovery,
|
|
19
|
+
closeToolLoopForThinking
|
|
20
|
+
} from './thinking-utils.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Convert Anthropic Messages API request to the format expected by Cloud Code
|
|
24
|
+
*
|
|
25
|
+
* Uses Google Generative AI format, but for Claude models:
|
|
26
|
+
* - Keeps tool_result in Anthropic format (required by Claude API)
|
|
27
|
+
*
|
|
28
|
+
* @param {Object} anthropicRequest - Anthropic format request
|
|
29
|
+
* @returns {Object} Request body for Cloud Code API
|
|
30
|
+
*/
|
|
31
|
+
export function convertAnthropicToGoogle(anthropicRequest) {
|
|
32
|
+
const { messages, system, max_tokens, temperature, top_p, top_k, stop_sequences, tools, tool_choice, thinking } = anthropicRequest;
|
|
33
|
+
const modelName = anthropicRequest.model || '';
|
|
34
|
+
const modelFamily = getModelFamily(modelName);
|
|
35
|
+
const isClaudeModel = modelFamily === 'claude';
|
|
36
|
+
const isGeminiModel = modelFamily === 'gemini';
|
|
37
|
+
const isThinking = isThinkingModel(modelName);
|
|
38
|
+
|
|
39
|
+
const googleRequest = {
|
|
40
|
+
contents: [],
|
|
41
|
+
generationConfig: {}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Handle system instruction
|
|
45
|
+
if (system) {
|
|
46
|
+
let systemParts = [];
|
|
47
|
+
if (typeof system === 'string') {
|
|
48
|
+
systemParts = [{ text: system }];
|
|
49
|
+
} else if (Array.isArray(system)) {
|
|
50
|
+
// Filter for text blocks as system prompts are usually text
|
|
51
|
+
// Anthropic supports text blocks in system prompts
|
|
52
|
+
systemParts = system
|
|
53
|
+
.filter(block => block.type === 'text')
|
|
54
|
+
.map(block => ({ text: block.text }));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (systemParts.length > 0) {
|
|
58
|
+
googleRequest.systemInstruction = {
|
|
59
|
+
parts: systemParts
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Add interleaved thinking hint for Claude thinking models with tools
|
|
65
|
+
if (isClaudeModel && isThinking && tools && tools.length > 0) {
|
|
66
|
+
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.';
|
|
67
|
+
if (!googleRequest.systemInstruction) {
|
|
68
|
+
googleRequest.systemInstruction = { parts: [{ text: hint }] };
|
|
69
|
+
} else {
|
|
70
|
+
const lastPart = googleRequest.systemInstruction.parts[googleRequest.systemInstruction.parts.length - 1];
|
|
71
|
+
if (lastPart && lastPart.text) {
|
|
72
|
+
lastPart.text = `${lastPart.text}\n\n${hint}`;
|
|
73
|
+
} else {
|
|
74
|
+
googleRequest.systemInstruction.parts.push({ text: hint });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Apply thinking recovery for Gemini thinking models when needed
|
|
80
|
+
// This handles corrupted tool loops where thinking blocks are stripped
|
|
81
|
+
// Claude models handle this differently and don't need this recovery
|
|
82
|
+
let processedMessages = messages;
|
|
83
|
+
if (isGeminiModel && isThinking && needsThinkingRecovery(messages)) {
|
|
84
|
+
console.log('[RequestConverter] Applying thinking recovery for Gemini');
|
|
85
|
+
processedMessages = closeToolLoopForThinking(messages);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Convert messages to contents, then filter unsigned thinking blocks
|
|
89
|
+
for (let i = 0; i < processedMessages.length; i++) {
|
|
90
|
+
const msg = processedMessages[i];
|
|
91
|
+
let msgContent = msg.content;
|
|
92
|
+
|
|
93
|
+
// For assistant messages, process thinking blocks and reorder content
|
|
94
|
+
if ((msg.role === 'assistant' || msg.role === 'model') && Array.isArray(msgContent)) {
|
|
95
|
+
// First, try to restore signatures for unsigned thinking blocks from cache
|
|
96
|
+
msgContent = restoreThinkingSignatures(msgContent);
|
|
97
|
+
// Remove trailing unsigned thinking blocks
|
|
98
|
+
msgContent = removeTrailingThinkingBlocks(msgContent);
|
|
99
|
+
// Reorder: thinking first, then text, then tool_use
|
|
100
|
+
msgContent = reorderAssistantContent(msgContent);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const parts = convertContentToParts(msgContent, isClaudeModel, isGeminiModel);
|
|
104
|
+
|
|
105
|
+
// SAFETY: Google API requires at least one part per content message
|
|
106
|
+
// This happens when all thinking blocks are filtered out (unsigned)
|
|
107
|
+
if (parts.length === 0) {
|
|
108
|
+
console.log('[RequestConverter] WARNING: Empty parts array after filtering, adding placeholder');
|
|
109
|
+
parts.push({ text: '' });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const content = {
|
|
113
|
+
role: convertRole(msg.role),
|
|
114
|
+
parts: parts
|
|
115
|
+
};
|
|
116
|
+
googleRequest.contents.push(content);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Filter unsigned thinking blocks for Claude models
|
|
120
|
+
if (isClaudeModel) {
|
|
121
|
+
googleRequest.contents = filterUnsignedThinkingBlocks(googleRequest.contents);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Generation config
|
|
125
|
+
if (max_tokens) {
|
|
126
|
+
googleRequest.generationConfig.maxOutputTokens = max_tokens;
|
|
127
|
+
}
|
|
128
|
+
if (temperature !== undefined) {
|
|
129
|
+
googleRequest.generationConfig.temperature = temperature;
|
|
130
|
+
}
|
|
131
|
+
if (top_p !== undefined) {
|
|
132
|
+
googleRequest.generationConfig.topP = top_p;
|
|
133
|
+
}
|
|
134
|
+
if (top_k !== undefined) {
|
|
135
|
+
googleRequest.generationConfig.topK = top_k;
|
|
136
|
+
}
|
|
137
|
+
if (stop_sequences && stop_sequences.length > 0) {
|
|
138
|
+
googleRequest.generationConfig.stopSequences = stop_sequences;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Enable thinking for thinking models (Claude and Gemini 3+)
|
|
142
|
+
if (isThinking) {
|
|
143
|
+
if (isClaudeModel) {
|
|
144
|
+
// Claude thinking config
|
|
145
|
+
const thinkingConfig = {
|
|
146
|
+
include_thoughts: true
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Only set thinking_budget if explicitly provided
|
|
150
|
+
const thinkingBudget = thinking?.budget_tokens;
|
|
151
|
+
if (thinkingBudget) {
|
|
152
|
+
thinkingConfig.thinking_budget = thinkingBudget;
|
|
153
|
+
console.log('[RequestConverter] Claude thinking enabled with budget:', thinkingBudget);
|
|
154
|
+
} else {
|
|
155
|
+
console.log('[RequestConverter] Claude thinking enabled (no budget specified)');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
googleRequest.generationConfig.thinkingConfig = thinkingConfig;
|
|
159
|
+
} else if (isGeminiModel) {
|
|
160
|
+
// Gemini thinking config (uses camelCase)
|
|
161
|
+
const thinkingConfig = {
|
|
162
|
+
includeThoughts: true,
|
|
163
|
+
thinkingBudget: thinking?.budget_tokens || 16000
|
|
164
|
+
};
|
|
165
|
+
console.log('[RequestConverter] Gemini thinking enabled with budget:', thinkingConfig.thinkingBudget);
|
|
166
|
+
|
|
167
|
+
googleRequest.generationConfig.thinkingConfig = thinkingConfig;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Convert tools to Google format
|
|
172
|
+
if (tools && tools.length > 0) {
|
|
173
|
+
const functionDeclarations = tools.map((tool, idx) => {
|
|
174
|
+
// Extract name from various possible locations
|
|
175
|
+
const name = tool.name || tool.function?.name || tool.custom?.name || `tool-${idx}`;
|
|
176
|
+
|
|
177
|
+
// Extract description from various possible locations
|
|
178
|
+
const description = tool.description || tool.function?.description || tool.custom?.description || '';
|
|
179
|
+
|
|
180
|
+
// Extract schema from various possible locations
|
|
181
|
+
const schema = tool.input_schema
|
|
182
|
+
|| tool.function?.input_schema
|
|
183
|
+
|| tool.function?.parameters
|
|
184
|
+
|| tool.custom?.input_schema
|
|
185
|
+
|| tool.parameters
|
|
186
|
+
|| { type: 'object' };
|
|
187
|
+
|
|
188
|
+
// Sanitize schema for general compatibility
|
|
189
|
+
let parameters = sanitizeSchema(schema);
|
|
190
|
+
|
|
191
|
+
// For Gemini models, apply additional cleaning for VALIDATED mode
|
|
192
|
+
if (isGeminiModel) {
|
|
193
|
+
parameters = cleanSchemaForGemini(parameters);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
name: String(name).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64),
|
|
198
|
+
description: description,
|
|
199
|
+
parameters
|
|
200
|
+
};
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
googleRequest.tools = [{ functionDeclarations }];
|
|
204
|
+
console.log('[RequestConverter] Tools:', JSON.stringify(googleRequest.tools).substring(0, 300));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Cap max tokens for Gemini models
|
|
208
|
+
if (isGeminiModel && googleRequest.generationConfig.maxOutputTokens > GEMINI_MAX_OUTPUT_TOKENS) {
|
|
209
|
+
console.log(`[RequestConverter] Capping Gemini max_tokens from ${googleRequest.generationConfig.maxOutputTokens} to ${GEMINI_MAX_OUTPUT_TOKENS}`);
|
|
210
|
+
googleRequest.generationConfig.maxOutputTokens = GEMINI_MAX_OUTPUT_TOKENS;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return googleRequest;
|
|
214
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response Converter
|
|
3
|
+
* Converts Google Generative AI responses to Anthropic Messages API format
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import crypto from 'crypto';
|
|
7
|
+
import { MIN_SIGNATURE_LENGTH } from '../constants.js';
|
|
8
|
+
import { cacheSignature } from './signature-cache.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Convert Google Generative AI response to Anthropic Messages API format
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} googleResponse - Google format response (the inner response object)
|
|
14
|
+
* @param {string} model - The model name used
|
|
15
|
+
* @returns {Object} Anthropic format response
|
|
16
|
+
*/
|
|
17
|
+
export function convertGoogleToAnthropic(googleResponse, model) {
|
|
18
|
+
// Handle the response wrapper
|
|
19
|
+
const response = googleResponse.response || googleResponse;
|
|
20
|
+
|
|
21
|
+
const candidates = response.candidates || [];
|
|
22
|
+
const firstCandidate = candidates[0] || {};
|
|
23
|
+
const content = firstCandidate.content || {};
|
|
24
|
+
const parts = content.parts || [];
|
|
25
|
+
|
|
26
|
+
// Convert parts to Anthropic content blocks
|
|
27
|
+
const anthropicContent = [];
|
|
28
|
+
let hasToolCalls = false;
|
|
29
|
+
|
|
30
|
+
for (const part of parts) {
|
|
31
|
+
if (part.text !== undefined) {
|
|
32
|
+
// Handle thinking blocks
|
|
33
|
+
if (part.thought === true) {
|
|
34
|
+
const signature = part.thoughtSignature || '';
|
|
35
|
+
|
|
36
|
+
// Include thinking blocks in the response for Claude Code
|
|
37
|
+
anthropicContent.push({
|
|
38
|
+
type: 'thinking',
|
|
39
|
+
thinking: part.text,
|
|
40
|
+
signature: signature
|
|
41
|
+
});
|
|
42
|
+
} else {
|
|
43
|
+
anthropicContent.push({
|
|
44
|
+
type: 'text',
|
|
45
|
+
text: part.text
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
} else if (part.functionCall) {
|
|
49
|
+
// Convert functionCall to tool_use
|
|
50
|
+
// Use the id from the response if available, otherwise generate one
|
|
51
|
+
const toolId = part.functionCall.id || `toolu_${crypto.randomBytes(12).toString('hex')}`;
|
|
52
|
+
const toolUseBlock = {
|
|
53
|
+
type: 'tool_use',
|
|
54
|
+
id: toolId,
|
|
55
|
+
name: part.functionCall.name,
|
|
56
|
+
input: part.functionCall.args || {}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// For Gemini 3+, include thoughtSignature from the part level
|
|
60
|
+
if (part.thoughtSignature && part.thoughtSignature.length >= MIN_SIGNATURE_LENGTH) {
|
|
61
|
+
toolUseBlock.thoughtSignature = part.thoughtSignature;
|
|
62
|
+
// Cache for future requests (Claude Code may strip this field)
|
|
63
|
+
cacheSignature(toolId, part.thoughtSignature);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
anthropicContent.push(toolUseBlock);
|
|
67
|
+
hasToolCalls = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Determine stop reason
|
|
72
|
+
const finishReason = firstCandidate.finishReason;
|
|
73
|
+
let stopReason = 'end_turn';
|
|
74
|
+
if (finishReason === 'STOP') {
|
|
75
|
+
stopReason = 'end_turn';
|
|
76
|
+
} else if (finishReason === 'MAX_TOKENS') {
|
|
77
|
+
stopReason = 'max_tokens';
|
|
78
|
+
} else if (finishReason === 'TOOL_USE' || hasToolCalls) {
|
|
79
|
+
stopReason = 'tool_use';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Extract usage metadata
|
|
83
|
+
// Note: Antigravity's promptTokenCount is the TOTAL (includes cached),
|
|
84
|
+
// but Anthropic's input_tokens excludes cached. We subtract to match.
|
|
85
|
+
const usageMetadata = response.usageMetadata || {};
|
|
86
|
+
const promptTokens = usageMetadata.promptTokenCount || 0;
|
|
87
|
+
const cachedTokens = usageMetadata.cachedContentTokenCount || 0;
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
id: `msg_${crypto.randomBytes(16).toString('hex')}`,
|
|
91
|
+
type: 'message',
|
|
92
|
+
role: 'assistant',
|
|
93
|
+
content: anthropicContent.length > 0 ? anthropicContent : [{ type: 'text', text: '' }],
|
|
94
|
+
model: model,
|
|
95
|
+
stop_reason: stopReason,
|
|
96
|
+
stop_sequence: null,
|
|
97
|
+
usage: {
|
|
98
|
+
input_tokens: promptTokens - cachedTokens,
|
|
99
|
+
output_tokens: usageMetadata.candidatesTokenCount || 0,
|
|
100
|
+
cache_read_input_tokens: cachedTokens,
|
|
101
|
+
cache_creation_input_tokens: 0
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|