@kamel-ahmed/proxy-claude 1.0.0

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.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +622 -0
  3. package/bin/cli.js +124 -0
  4. package/package.json +80 -0
  5. package/public/app.js +228 -0
  6. package/public/css/src/input.css +523 -0
  7. package/public/css/style.css +1 -0
  8. package/public/favicon.svg +10 -0
  9. package/public/index.html +381 -0
  10. package/public/js/components/account-manager.js +245 -0
  11. package/public/js/components/claude-config.js +420 -0
  12. package/public/js/components/dashboard/charts.js +589 -0
  13. package/public/js/components/dashboard/filters.js +362 -0
  14. package/public/js/components/dashboard/stats.js +110 -0
  15. package/public/js/components/dashboard.js +236 -0
  16. package/public/js/components/logs-viewer.js +100 -0
  17. package/public/js/components/models.js +36 -0
  18. package/public/js/components/server-config.js +349 -0
  19. package/public/js/config/constants.js +102 -0
  20. package/public/js/data-store.js +386 -0
  21. package/public/js/settings-store.js +58 -0
  22. package/public/js/store.js +78 -0
  23. package/public/js/translations/en.js +351 -0
  24. package/public/js/translations/id.js +396 -0
  25. package/public/js/translations/pt.js +287 -0
  26. package/public/js/translations/tr.js +342 -0
  27. package/public/js/translations/zh.js +357 -0
  28. package/public/js/utils/account-actions.js +189 -0
  29. package/public/js/utils/error-handler.js +96 -0
  30. package/public/js/utils/model-config.js +42 -0
  31. package/public/js/utils/validators.js +77 -0
  32. package/public/js/utils.js +69 -0
  33. package/public/views/accounts.html +329 -0
  34. package/public/views/dashboard.html +484 -0
  35. package/public/views/logs.html +97 -0
  36. package/public/views/models.html +331 -0
  37. package/public/views/settings.html +1329 -0
  38. package/src/account-manager/credentials.js +243 -0
  39. package/src/account-manager/index.js +380 -0
  40. package/src/account-manager/onboarding.js +117 -0
  41. package/src/account-manager/rate-limits.js +237 -0
  42. package/src/account-manager/storage.js +136 -0
  43. package/src/account-manager/strategies/base-strategy.js +104 -0
  44. package/src/account-manager/strategies/hybrid-strategy.js +195 -0
  45. package/src/account-manager/strategies/index.js +79 -0
  46. package/src/account-manager/strategies/round-robin-strategy.js +76 -0
  47. package/src/account-manager/strategies/sticky-strategy.js +138 -0
  48. package/src/account-manager/strategies/trackers/health-tracker.js +162 -0
  49. package/src/account-manager/strategies/trackers/index.js +8 -0
  50. package/src/account-manager/strategies/trackers/token-bucket-tracker.js +121 -0
  51. package/src/auth/database.js +169 -0
  52. package/src/auth/oauth.js +419 -0
  53. package/src/auth/token-extractor.js +117 -0
  54. package/src/cli/accounts.js +512 -0
  55. package/src/cli/refresh.js +201 -0
  56. package/src/cli/setup.js +338 -0
  57. package/src/cloudcode/index.js +29 -0
  58. package/src/cloudcode/message-handler.js +386 -0
  59. package/src/cloudcode/model-api.js +248 -0
  60. package/src/cloudcode/rate-limit-parser.js +181 -0
  61. package/src/cloudcode/request-builder.js +93 -0
  62. package/src/cloudcode/session-manager.js +47 -0
  63. package/src/cloudcode/sse-parser.js +121 -0
  64. package/src/cloudcode/sse-streamer.js +293 -0
  65. package/src/cloudcode/streaming-handler.js +492 -0
  66. package/src/config.js +107 -0
  67. package/src/constants.js +278 -0
  68. package/src/errors.js +238 -0
  69. package/src/fallback-config.js +29 -0
  70. package/src/format/content-converter.js +193 -0
  71. package/src/format/index.js +20 -0
  72. package/src/format/request-converter.js +248 -0
  73. package/src/format/response-converter.js +120 -0
  74. package/src/format/schema-sanitizer.js +673 -0
  75. package/src/format/signature-cache.js +88 -0
  76. package/src/format/thinking-utils.js +558 -0
  77. package/src/index.js +146 -0
  78. package/src/modules/usage-stats.js +205 -0
  79. package/src/server.js +861 -0
  80. package/src/utils/claude-config.js +245 -0
  81. package/src/utils/helpers.js +51 -0
  82. package/src/utils/logger.js +142 -0
  83. package/src/utils/native-module-helper.js +162 -0
  84. package/src/webui/index.js +707 -0
@@ -0,0 +1,193 @@
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, getCachedSignatureFamily } from './signature-cache.js';
8
+ import { logger } from '../utils/logger.js';
9
+
10
+ /**
11
+ * Convert Anthropic role to Google role
12
+ * @param {string} role - Anthropic role ('user', 'assistant')
13
+ * @returns {string} Google role ('user', 'model')
14
+ */
15
+ export function convertRole(role) {
16
+ if (role === 'assistant') return 'model';
17
+ if (role === 'user') return 'user';
18
+ return 'user'; // Default to user
19
+ }
20
+
21
+ /**
22
+ * Convert Anthropic message content to Google Generative AI parts
23
+ * @param {string|Array} content - Anthropic message content
24
+ * @param {boolean} isClaudeModel - Whether the model is a Claude model
25
+ * @param {boolean} isGeminiModel - Whether the model is a Gemini model
26
+ * @returns {Array} Google Generative AI parts array
27
+ */
28
+ export function convertContentToParts(content, isClaudeModel = false, isGeminiModel = false) {
29
+ if (typeof content === 'string') {
30
+ return [{ text: content }];
31
+ }
32
+
33
+ if (!Array.isArray(content)) {
34
+ return [{ text: String(content) }];
35
+ }
36
+
37
+ const parts = [];
38
+ const deferredInlineData = []; // Collect inlineData to add at the end (Issue #91)
39
+
40
+ for (const block of content) {
41
+ if (!block) continue;
42
+
43
+ if (block.type === 'text') {
44
+ // Skip empty text blocks - they cause API errors
45
+ if (block.text && block.text.trim()) {
46
+ parts.push({ text: block.text });
47
+ }
48
+ } else if (block.type === 'image') {
49
+ // Handle image content
50
+ if (block.source?.type === 'base64') {
51
+ // Base64-encoded image
52
+ parts.push({
53
+ inlineData: {
54
+ mimeType: block.source.media_type,
55
+ data: block.source.data
56
+ }
57
+ });
58
+ } else if (block.source?.type === 'url') {
59
+ // URL-referenced image
60
+ parts.push({
61
+ fileData: {
62
+ mimeType: block.source.media_type || 'image/jpeg',
63
+ fileUri: block.source.url
64
+ }
65
+ });
66
+ }
67
+ } else if (block.type === 'document') {
68
+ // Handle document content (e.g. PDF)
69
+ if (block.source?.type === 'base64') {
70
+ parts.push({
71
+ inlineData: {
72
+ mimeType: block.source.media_type,
73
+ data: block.source.data
74
+ }
75
+ });
76
+ } else if (block.source?.type === 'url') {
77
+ parts.push({
78
+ fileData: {
79
+ mimeType: block.source.media_type || 'application/pdf',
80
+ fileUri: block.source.url
81
+ }
82
+ });
83
+ }
84
+ } else if (block.type === 'tool_use') {
85
+ // Convert tool_use to functionCall (Google format)
86
+ // For Claude models, include the id field
87
+ const functionCall = {
88
+ name: block.name,
89
+ args: block.input || {}
90
+ };
91
+
92
+ if (isClaudeModel && block.id) {
93
+ functionCall.id = block.id;
94
+ }
95
+
96
+ // Build the part with functionCall
97
+ const part = { functionCall };
98
+
99
+ // For Gemini models, include thoughtSignature at the part level
100
+ // This is required by Gemini 3+ for tool calls to work correctly
101
+ if (isGeminiModel) {
102
+ // Priority: block.thoughtSignature > cache > GEMINI_SKIP_SIGNATURE
103
+ let signature = block.thoughtSignature;
104
+
105
+ if (!signature && block.id) {
106
+ signature = getCachedSignature(block.id);
107
+ if (signature) {
108
+ logger.debug(`[ContentConverter] Restored signature from cache for: ${block.id}`);
109
+ }
110
+ }
111
+
112
+ part.thoughtSignature = signature || GEMINI_SKIP_SIGNATURE;
113
+ }
114
+
115
+ parts.push(part);
116
+ } else if (block.type === 'tool_result') {
117
+ // Convert tool_result to functionResponse (Google format)
118
+ let responseContent = block.content;
119
+ let imageParts = [];
120
+
121
+ if (typeof responseContent === 'string') {
122
+ responseContent = { result: responseContent };
123
+ } else if (Array.isArray(responseContent)) {
124
+ // Extract images from tool results first (e.g., from Read tool reading image files)
125
+ for (const item of responseContent) {
126
+ if (item.type === 'image' && item.source?.type === 'base64') {
127
+ imageParts.push({
128
+ inlineData: {
129
+ mimeType: item.source.media_type,
130
+ data: item.source.data
131
+ }
132
+ });
133
+ }
134
+ }
135
+
136
+ // Extract text content
137
+ const texts = responseContent
138
+ .filter(c => c.type === 'text')
139
+ .map(c => c.text)
140
+ .join('\n');
141
+ responseContent = { result: texts || (imageParts.length > 0 ? 'Image attached' : '') };
142
+ }
143
+
144
+ const functionResponse = {
145
+ name: block.tool_use_id || 'unknown',
146
+ response: responseContent
147
+ };
148
+
149
+ // For Claude models, the id field must match the tool_use_id
150
+ if (isClaudeModel && block.tool_use_id) {
151
+ functionResponse.id = block.tool_use_id;
152
+ }
153
+
154
+ parts.push({ functionResponse });
155
+
156
+ // Defer images from the tool result to end of parts array (Issue #91)
157
+ // This ensures all functionResponse parts are consecutive
158
+ deferredInlineData.push(...imageParts);
159
+ } else if (block.type === 'thinking') {
160
+ // Handle thinking blocks with signature compatibility check
161
+ if (block.signature && block.signature.length >= MIN_SIGNATURE_LENGTH) {
162
+ const signatureFamily = getCachedSignatureFamily(block.signature);
163
+ const targetFamily = isClaudeModel ? 'claude' : isGeminiModel ? 'gemini' : null;
164
+
165
+ // Drop blocks with incompatible signatures for Gemini (cross-model switch)
166
+ if (isGeminiModel && signatureFamily && targetFamily && signatureFamily !== targetFamily) {
167
+ logger.debug(`[ContentConverter] Dropping incompatible ${signatureFamily} thinking for ${targetFamily} model`);
168
+ continue;
169
+ }
170
+
171
+ // Drop blocks with unknown signature origin for Gemini (cold cache - safe default)
172
+ if (isGeminiModel && !signatureFamily && targetFamily) {
173
+ logger.debug(`[ContentConverter] Dropping thinking with unknown signature origin`);
174
+ continue;
175
+ }
176
+
177
+ // Compatible - convert to Gemini format with signature
178
+ parts.push({
179
+ text: block.thinking,
180
+ thought: true,
181
+ thoughtSignature: block.signature
182
+ });
183
+ }
184
+ // Unsigned thinking blocks are dropped (existing behavior)
185
+ }
186
+ }
187
+
188
+ // Add deferred inlineData at the end (Issue #91)
189
+ // This ensures functionResponse parts are consecutive, which Claude's API requires
190
+ parts.push(...deferredInlineData);
191
+
192
+ return parts;
193
+ }
@@ -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,248 @@
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, cleanSchema } from './schema-sanitizer.js';
13
+ import {
14
+ restoreThinkingSignatures,
15
+ removeTrailingThinkingBlocks,
16
+ reorderAssistantContent,
17
+ filterUnsignedThinkingBlocks,
18
+ hasGeminiHistory,
19
+ hasUnsignedThinkingBlocks,
20
+ needsThinkingRecovery,
21
+ closeToolLoopForThinking
22
+ } from './thinking-utils.js';
23
+ import { logger } from '../utils/logger.js';
24
+
25
+ /**
26
+ * Convert Anthropic Messages API request to the format expected by Cloud Code
27
+ *
28
+ * Uses Google Generative AI format, but for Claude models:
29
+ * - Keeps tool_result in Anthropic format (required by Claude API)
30
+ *
31
+ * @param {Object} anthropicRequest - Anthropic format request
32
+ * @returns {Object} Request body for Cloud Code API
33
+ */
34
+ export function convertAnthropicToGoogle(anthropicRequest) {
35
+ const { messages, system, max_tokens, temperature, top_p, top_k, stop_sequences, tools, tool_choice, thinking } = anthropicRequest;
36
+ const modelName = anthropicRequest.model || '';
37
+ const modelFamily = getModelFamily(modelName);
38
+ const isClaudeModel = modelFamily === 'claude';
39
+ const isGeminiModel = modelFamily === 'gemini';
40
+ const isThinking = isThinkingModel(modelName);
41
+
42
+ const googleRequest = {
43
+ contents: [],
44
+ generationConfig: {}
45
+ };
46
+
47
+ // Handle system instruction
48
+ if (system) {
49
+ let systemParts = [];
50
+ if (typeof system === 'string') {
51
+ systemParts = [{ text: system }];
52
+ } else if (Array.isArray(system)) {
53
+ // Filter for text blocks as system prompts are usually text
54
+ // Anthropic supports text blocks in system prompts
55
+ systemParts = system
56
+ .filter(block => block.type === 'text')
57
+ .map(block => ({ text: block.text }));
58
+ }
59
+
60
+ if (systemParts.length > 0) {
61
+ googleRequest.systemInstruction = {
62
+ parts: systemParts
63
+ };
64
+ }
65
+ }
66
+
67
+ // Add interleaved thinking hint for Claude thinking models with tools
68
+ if (isClaudeModel && isThinking && tools && tools.length > 0) {
69
+ 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.';
70
+ if (!googleRequest.systemInstruction) {
71
+ googleRequest.systemInstruction = { parts: [{ text: hint }] };
72
+ } else {
73
+ const lastPart = googleRequest.systemInstruction.parts[googleRequest.systemInstruction.parts.length - 1];
74
+ if (lastPart && lastPart.text) {
75
+ lastPart.text = `${lastPart.text}\n\n${hint}`;
76
+ } else {
77
+ googleRequest.systemInstruction.parts.push({ text: hint });
78
+ }
79
+ }
80
+ }
81
+
82
+ // Apply thinking recovery for Gemini thinking models when needed
83
+ // Gemini needs recovery for tool loops/interrupted tools (stripped thinking)
84
+ let processedMessages = messages;
85
+
86
+ if (isGeminiModel && isThinking && needsThinkingRecovery(messages)) {
87
+ logger.debug('[RequestConverter] Applying thinking recovery for Gemini');
88
+ processedMessages = closeToolLoopForThinking(messages, 'gemini');
89
+ }
90
+
91
+ // For Claude: apply recovery for cross-model (Gemini→Claude) or unsigned thinking blocks
92
+ // Unsigned thinking blocks occur when Claude Code strips signatures it doesn't understand
93
+ const needsClaudeRecovery = hasGeminiHistory(messages) || hasUnsignedThinkingBlocks(messages);
94
+ if (isClaudeModel && isThinking && needsClaudeRecovery && needsThinkingRecovery(messages)) {
95
+ logger.debug('[RequestConverter] Applying thinking recovery for Claude');
96
+ processedMessages = closeToolLoopForThinking(messages, 'claude');
97
+ }
98
+
99
+ // Convert messages to contents, then filter unsigned thinking blocks
100
+ for (const msg of processedMessages) {
101
+ let msgContent = msg.content;
102
+
103
+ // For assistant messages, process thinking blocks and reorder content
104
+ if ((msg.role === 'assistant' || msg.role === 'model') && Array.isArray(msgContent)) {
105
+ // First, try to restore signatures for unsigned thinking blocks from cache
106
+ msgContent = restoreThinkingSignatures(msgContent);
107
+ // Remove trailing unsigned thinking blocks
108
+ msgContent = removeTrailingThinkingBlocks(msgContent);
109
+ // Reorder: thinking first, then text, then tool_use
110
+ msgContent = reorderAssistantContent(msgContent);
111
+ }
112
+
113
+ const parts = convertContentToParts(msgContent, isClaudeModel, isGeminiModel);
114
+
115
+ // SAFETY: Google API requires at least one part per content message
116
+ // This happens when all thinking blocks are filtered out (unsigned)
117
+ if (parts.length === 0) {
118
+ // Use '.' instead of '' because claude models reject empty text parts.
119
+ // A single period is invisible in practice but satisfies the API requirement.
120
+ logger.warn('[RequestConverter] WARNING: Empty parts array after filtering, adding placeholder');
121
+ parts.push({ text: '.' });
122
+ }
123
+
124
+ const content = {
125
+ role: convertRole(msg.role),
126
+ parts: parts
127
+ };
128
+ googleRequest.contents.push(content);
129
+ }
130
+
131
+ // Filter unsigned thinking blocks for Claude models
132
+ if (isClaudeModel) {
133
+ googleRequest.contents = filterUnsignedThinkingBlocks(googleRequest.contents);
134
+ }
135
+
136
+ // Generation config
137
+ if (max_tokens) {
138
+ googleRequest.generationConfig.maxOutputTokens = max_tokens;
139
+ }
140
+ if (temperature !== undefined) {
141
+ googleRequest.generationConfig.temperature = temperature;
142
+ }
143
+ if (top_p !== undefined) {
144
+ googleRequest.generationConfig.topP = top_p;
145
+ }
146
+ if (top_k !== undefined) {
147
+ googleRequest.generationConfig.topK = top_k;
148
+ }
149
+ if (stop_sequences && stop_sequences.length > 0) {
150
+ googleRequest.generationConfig.stopSequences = stop_sequences;
151
+ }
152
+
153
+ // Enable thinking for thinking models (Claude and Gemini 3+)
154
+ if (isThinking) {
155
+ if (isClaudeModel) {
156
+ // Claude thinking config
157
+ const thinkingConfig = {
158
+ include_thoughts: true
159
+ };
160
+
161
+ // Only set thinking_budget if explicitly provided
162
+ const thinkingBudget = thinking?.budget_tokens;
163
+ if (thinkingBudget) {
164
+ thinkingConfig.thinking_budget = thinkingBudget;
165
+ logger.debug(`[RequestConverter] Claude thinking enabled with budget: ${thinkingBudget}`);
166
+
167
+ // Validate max_tokens > thinking_budget as required by the API
168
+ const currentMaxTokens = googleRequest.generationConfig.maxOutputTokens;
169
+ if (currentMaxTokens && currentMaxTokens <= thinkingBudget) {
170
+ // Bump max_tokens to allow for some response content
171
+ // Default to budget + 8192 (standard output buffer)
172
+ const adjustedMaxTokens = thinkingBudget + 8192;
173
+ logger.warn(`[RequestConverter] max_tokens (${currentMaxTokens}) <= thinking_budget (${thinkingBudget}). Adjusting to ${adjustedMaxTokens} to satisfy API requirements`);
174
+ googleRequest.generationConfig.maxOutputTokens = adjustedMaxTokens;
175
+ }
176
+ } else {
177
+ logger.debug('[RequestConverter] Claude thinking enabled (no budget specified)');
178
+ }
179
+
180
+ googleRequest.generationConfig.thinkingConfig = thinkingConfig;
181
+ } else if (isGeminiModel) {
182
+ // Gemini thinking config (uses camelCase)
183
+ const thinkingConfig = {
184
+ includeThoughts: true,
185
+ thinkingBudget: thinking?.budget_tokens || 16000
186
+ };
187
+ logger.debug(`[RequestConverter] Gemini thinking enabled with budget: ${thinkingConfig.thinkingBudget}`);
188
+
189
+
190
+ googleRequest.generationConfig.thinkingConfig = thinkingConfig;
191
+ }
192
+ }
193
+
194
+ // Convert tools to Google format
195
+ if (tools && tools.length > 0) {
196
+ const functionDeclarations = tools.map((tool, idx) => {
197
+ // Extract name from various possible locations
198
+ const name = tool.name || tool.function?.name || tool.custom?.name || `tool-${idx}`;
199
+
200
+ // Extract description from various possible locations
201
+ const description = tool.description || tool.function?.description || tool.custom?.description || '';
202
+
203
+ // Extract schema from various possible locations
204
+ const schema = tool.input_schema
205
+ || tool.function?.input_schema
206
+ || tool.function?.parameters
207
+ || tool.custom?.input_schema
208
+ || tool.parameters
209
+ || { type: 'object' };
210
+
211
+ // Sanitize schema for general compatibility
212
+ let parameters = sanitizeSchema(schema);
213
+
214
+ // Apply Google-format cleaning for ALL models since they all go through
215
+ // Cloud Code API which validates schemas using Google's protobuf format.
216
+ // This fixes issue #82: /compact command fails with schema transformation error
217
+ // "Proto field is not repeating, cannot start list" for Claude models.
218
+ parameters = cleanSchema(parameters);
219
+
220
+ return {
221
+ name: String(name).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64),
222
+ description: description,
223
+ parameters
224
+ };
225
+ });
226
+
227
+ googleRequest.tools = [{ functionDeclarations }];
228
+ logger.debug(`[RequestConverter] Tools: ${JSON.stringify(googleRequest.tools).substring(0, 300)}`);
229
+
230
+ // For Claude models, set functionCallingConfig.mode = "VALIDATED"
231
+ // This ensures strict parameter validation (matches opencode-antigravity-auth)
232
+ if (isClaudeModel) {
233
+ googleRequest.toolConfig = {
234
+ functionCallingConfig: {
235
+ mode: 'VALIDATED'
236
+ }
237
+ };
238
+ }
239
+ }
240
+
241
+ // Cap max tokens for Gemini models
242
+ if (isGeminiModel && googleRequest.generationConfig.maxOutputTokens > GEMINI_MAX_OUTPUT_TOKENS) {
243
+ logger.debug(`[RequestConverter] Capping Gemini max_tokens from ${googleRequest.generationConfig.maxOutputTokens} to ${GEMINI_MAX_OUTPUT_TOKENS}`);
244
+ googleRequest.generationConfig.maxOutputTokens = GEMINI_MAX_OUTPUT_TOKENS;
245
+ }
246
+
247
+ return googleRequest;
248
+ }
@@ -0,0 +1,120 @@
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, getModelFamily } from '../constants.js';
8
+ import { cacheSignature, cacheThinkingSignature } 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
+ // Cache thinking signature with model family for cross-model compatibility
37
+ if (signature && signature.length >= MIN_SIGNATURE_LENGTH) {
38
+ const modelFamily = getModelFamily(model);
39
+ cacheThinkingSignature(signature, modelFamily);
40
+ }
41
+
42
+ // Include thinking blocks in the response for Claude Code
43
+ anthropicContent.push({
44
+ type: 'thinking',
45
+ thinking: part.text,
46
+ signature: signature
47
+ });
48
+ } else {
49
+ anthropicContent.push({
50
+ type: 'text',
51
+ text: part.text
52
+ });
53
+ }
54
+ } else if (part.functionCall) {
55
+ // Convert functionCall to tool_use
56
+ // Use the id from the response if available, otherwise generate one
57
+ const toolId = part.functionCall.id || `toolu_${crypto.randomBytes(12).toString('hex')}`;
58
+ const toolUseBlock = {
59
+ type: 'tool_use',
60
+ id: toolId,
61
+ name: part.functionCall.name,
62
+ input: part.functionCall.args || {}
63
+ };
64
+
65
+ // For Gemini 3+, include thoughtSignature from the part level
66
+ if (part.thoughtSignature && part.thoughtSignature.length >= MIN_SIGNATURE_LENGTH) {
67
+ toolUseBlock.thoughtSignature = part.thoughtSignature;
68
+ // Cache for future requests (Claude Code may strip this field)
69
+ cacheSignature(toolId, part.thoughtSignature);
70
+ }
71
+
72
+ anthropicContent.push(toolUseBlock);
73
+ hasToolCalls = true;
74
+ } else if (part.inlineData) {
75
+ // Handle image content from Google format
76
+ anthropicContent.push({
77
+ type: 'image',
78
+ source: {
79
+ type: 'base64',
80
+ media_type: part.inlineData.mimeType,
81
+ data: part.inlineData.data
82
+ }
83
+ });
84
+ }
85
+ }
86
+
87
+ // Determine stop reason
88
+ const finishReason = firstCandidate.finishReason;
89
+ let stopReason = 'end_turn';
90
+ if (finishReason === 'STOP') {
91
+ stopReason = 'end_turn';
92
+ } else if (finishReason === 'MAX_TOKENS') {
93
+ stopReason = 'max_tokens';
94
+ } else if (finishReason === 'TOOL_USE' || hasToolCalls) {
95
+ stopReason = 'tool_use';
96
+ }
97
+
98
+ // Extract usage metadata
99
+ // Note: Antigravity's promptTokenCount is the TOTAL (includes cached),
100
+ // but Anthropic's input_tokens excludes cached. We subtract to match.
101
+ const usageMetadata = response.usageMetadata || {};
102
+ const promptTokens = usageMetadata.promptTokenCount || 0;
103
+ const cachedTokens = usageMetadata.cachedContentTokenCount || 0;
104
+
105
+ return {
106
+ id: `msg_${crypto.randomBytes(16).toString('hex')}`,
107
+ type: 'message',
108
+ role: 'assistant',
109
+ content: anthropicContent.length > 0 ? anthropicContent : [{ type: 'text', text: '' }],
110
+ model: model,
111
+ stop_reason: stopReason,
112
+ stop_sequence: null,
113
+ usage: {
114
+ input_tokens: promptTokens - cachedTokens,
115
+ output_tokens: usageMetadata.candidatesTokenCount || 0,
116
+ cache_read_input_tokens: cachedTokens,
117
+ cache_creation_input_tokens: 0
118
+ }
119
+ };
120
+ }