antigravity-claude-proxy 1.0.8 → 1.0.9

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/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  <a href="https://buymeacoffee.com/badrinarayanans" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a>
8
8
 
9
- A proxy server that exposes an **Anthropic-compatible API** backed by **Antigravity's Cloud Code**, letting you use Claude models like sonnet and opus with **Claude Code CLI**.
9
+ A proxy server that exposes an **Anthropic-compatible API** backed by **Antigravity's Cloud Code**, letting you use Claude and Gemini models with **Claude Code CLI**.
10
10
 
11
11
  ![Antigravity Claude Proxy Banner](images/banner.png)
12
12
 
@@ -145,7 +145,23 @@ Add this configuration:
145
145
  "ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-4-5-thinking",
146
146
  "ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4-5-thinking",
147
147
  "ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-sonnet-4-5",
148
- "CLAUDE_CODE_SUBAGENT_MODEL": "claude-opus-4-5-thinking"
148
+ "CLAUDE_CODE_SUBAGENT_MODEL": "claude-sonnet-4-5"
149
+ }
150
+ }
151
+ ```
152
+
153
+ Or to use Gemini models:
154
+
155
+ ```json
156
+ {
157
+ "env": {
158
+ "ANTHROPIC_AUTH_TOKEN": "test",
159
+ "ANTHROPIC_BASE_URL": "http://localhost:8080",
160
+ "ANTHROPIC_MODEL": "gemini-3-pro-high",
161
+ "ANTHROPIC_DEFAULT_OPUS_MODEL": "gemini-3-pro-high",
162
+ "ANTHROPIC_DEFAULT_SONNET_MODEL": "gemini-3-flash",
163
+ "ANTHROPIC_DEFAULT_HAIKU_MODEL": "gemini-3-flash",
164
+ "CLAUDE_CODE_SUBAGENT_MODEL": "gemini-2.5-flash-lite"
149
165
  }
150
166
  }
151
167
  ```
@@ -164,6 +180,8 @@ claude
164
180
 
165
181
  ## Available Models
166
182
 
183
+ ### Claude Models
184
+
167
185
  | Model ID | Description |
168
186
  |----------|-------------|
169
187
  | `claude-sonnet-4-5-thinking` | Claude Sonnet 4.5 with extended thinking |
@@ -174,6 +192,16 @@ Standard Anthropic model names are automatically mapped:
174
192
  - `claude-sonnet-4-5-20250514` → `claude-sonnet-4-5-thinking`
175
193
  - `claude-opus-4-5-20250514` → `claude-opus-4-5-thinking`
176
194
 
195
+ ### Gemini Models
196
+
197
+ | Model ID | Description |
198
+ |----------|-------------|
199
+ | `gemini-3-flash` | Gemini 3 Flash with thinking |
200
+ | `gemini-3-pro-low` | Gemini 3 Pro Low with thinking |
201
+ | `gemini-3-pro-high` | Gemini 3 Pro High with thinking |
202
+
203
+ Gemini models include full thinking support with `thoughtSignature` handling for multi-turn conversations.
204
+
177
205
  ---
178
206
 
179
207
  ## Multi-Account Load Balancing
@@ -326,4 +354,4 @@ MIT
326
354
 
327
355
  ## Star History
328
356
 
329
- [![Star History Chart](https://api.star-history.com/svg?repos=badri-s2001/antigravity-claude-proxy&type=date&legend=top-left)](https://www.star-history.com/#badri-s2001/antigravity-claude-proxy&type=date&legend=top-left)
357
+ [![Star History Chart](https://api.star-history.com/svg?repos=badri-s2001/antigravity-claude-proxy&type=date&legend=top-left&cache-control=no-cache)](https://www.star-history.com/#badri-s2001/antigravity-claude-proxy&type=date&legend=top-left)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antigravity-claude-proxy",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "Proxy server to use Antigravity's Claude models with Claude Code CLI",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -13,13 +13,13 @@ import crypto from 'crypto';
13
13
  import {
14
14
  ANTIGRAVITY_ENDPOINT_FALLBACKS,
15
15
  ANTIGRAVITY_HEADERS,
16
- AVAILABLE_MODELS,
17
16
  MAX_RETRIES,
18
17
  MAX_WAIT_BEFORE_ERROR_MS,
19
- MIN_SIGNATURE_LENGTH
18
+ MIN_SIGNATURE_LENGTH,
19
+ getModelFamily,
20
+ isThinkingModel
20
21
  } from './constants.js';
21
22
  import {
22
- mapModelName,
23
23
  convertAnthropicToGoogle,
24
24
  convertGoogleToAnthropic
25
25
  } from './format-converter.js';
@@ -219,7 +219,7 @@ function parseResetTime(responseOrError, errorText = '') {
219
219
  * Build the wrapped request body for Cloud Code API
220
220
  */
221
221
  function buildCloudCodeRequest(anthropicRequest, projectId) {
222
- const model = mapModelName(anthropicRequest.model);
222
+ const model = anthropicRequest.model;
223
223
  const googleRequest = convertAnthropicToGoogle(anthropicRequest);
224
224
 
225
225
  // Use stable session ID derived from first user message for cache continuity
@@ -246,9 +246,10 @@ function buildHeaders(token, model, accept = 'application/json') {
246
246
  ...ANTIGRAVITY_HEADERS
247
247
  };
248
248
 
249
- // Add interleaved thinking header for Claude thinking models
250
- const isThinkingModel = model.toLowerCase().includes('thinking');
251
- if (isThinkingModel) {
249
+ const modelFamily = getModelFamily(model);
250
+
251
+ // Add interleaved thinking header only for Claude thinking models
252
+ if (modelFamily === 'claude' && isThinkingModel(model)) {
252
253
  headers['anthropic-beta'] = 'interleaved-thinking-2025-05-14';
253
254
  }
254
255
 
@@ -273,8 +274,8 @@ function buildHeaders(token, model, accept = 'application/json') {
273
274
  * @throws {Error} If max retries exceeded or no accounts available
274
275
  */
275
276
  export async function sendMessage(anthropicRequest, accountManager) {
276
- const model = mapModelName(anthropicRequest.model);
277
- const isThinkingModel = model.toLowerCase().includes('thinking');
277
+ const model = anthropicRequest.model;
278
+ const isThinking = isThinkingModel(model);
278
279
 
279
280
  // Retry loop with account failover
280
281
  // Ensure we try at least as many times as there are accounts to cycle through everyone
@@ -332,13 +333,13 @@ export async function sendMessage(anthropicRequest, accountManager) {
332
333
  let lastError = null;
333
334
  for (const endpoint of ANTIGRAVITY_ENDPOINT_FALLBACKS) {
334
335
  try {
335
- const url = isThinkingModel
336
+ const url = isThinking
336
337
  ? `${endpoint}/v1internal:streamGenerateContent?alt=sse`
337
338
  : `${endpoint}/v1internal:generateContent`;
338
339
 
339
340
  const response = await fetch(url, {
340
341
  method: 'POST',
341
- headers: buildHeaders(token, model, isThinkingModel ? 'text/event-stream' : 'application/json'),
342
+ headers: buildHeaders(token, model, isThinking ? 'text/event-stream' : 'application/json'),
342
343
  body: JSON.stringify(payload)
343
344
  });
344
345
 
@@ -372,7 +373,7 @@ export async function sendMessage(anthropicRequest, accountManager) {
372
373
  }
373
374
 
374
375
  // For thinking models, parse SSE and accumulate all parts
375
- if (isThinkingModel) {
376
+ if (isThinking) {
376
377
  return await parseThinkingSSEResponse(response, anthropicRequest.model);
377
378
  }
378
379
 
@@ -537,7 +538,7 @@ async function parseThinkingSSEResponse(response, originalModel) {
537
538
  * @throws {Error} If max retries exceeded or no accounts available
538
539
  */
539
540
  export async function* sendMessageStream(anthropicRequest, accountManager) {
540
- const model = mapModelName(anthropicRequest.model);
541
+ const model = anthropicRequest.model;
541
542
 
542
543
  // Retry loop with account failover
543
544
  // Ensure we try at least as many times as there are accounts to cycle through everyone
@@ -814,6 +815,10 @@ async function* streamSSEResponse(response, originalModel) {
814
815
 
815
816
  } else if (part.functionCall) {
816
817
  // Handle tool use
818
+ // For Gemini 3+, capture thoughtSignature from the functionCall part
819
+ // The signature is a sibling to functionCall, not inside it
820
+ const functionCallSignature = part.thoughtSignature || '';
821
+
817
822
  if (currentBlockType === 'thinking' && currentThinkingSignature) {
818
823
  yield {
819
824
  type: 'content_block_delta',
@@ -831,15 +836,24 @@ async function* streamSSEResponse(response, originalModel) {
831
836
 
832
837
  const toolId = part.functionCall.id || `toolu_${crypto.randomBytes(12).toString('hex')}`;
833
838
 
839
+ // For Gemini, include the thoughtSignature in the tool_use block
840
+ // so it can be sent back in subsequent requests
841
+ const toolUseBlock = {
842
+ type: 'tool_use',
843
+ id: toolId,
844
+ name: part.functionCall.name,
845
+ input: {}
846
+ };
847
+
848
+ // Store the signature in the tool_use block for later retrieval
849
+ if (functionCallSignature && functionCallSignature.length >= MIN_SIGNATURE_LENGTH) {
850
+ toolUseBlock.thoughtSignature = functionCallSignature;
851
+ }
852
+
834
853
  yield {
835
854
  type: 'content_block_start',
836
855
  index: blockIndex,
837
- content_block: {
838
- type: 'tool_use',
839
- id: toolId,
840
- name: part.functionCall.name,
841
- input: {}
842
- }
856
+ content_block: toolUseBlock
843
857
  };
844
858
 
845
859
  yield {
@@ -931,19 +945,28 @@ async function* streamSSEResponse(response, originalModel) {
931
945
 
932
946
  /**
933
947
  * List available models in Anthropic API format
948
+ * Fetches models dynamically from the Cloud Code API
934
949
  *
935
- * @returns {{object: string, data: Array<{id: string, object: string, created: number, owned_by: string, description: string}>}} List of available models
950
+ * @param {string} token - OAuth access token
951
+ * @returns {Promise<{object: string, data: Array<{id: string, object: string, created: number, owned_by: string, description: string}>}>} List of available models
936
952
  */
937
- export function listModels() {
953
+ export async function listModels(token) {
954
+ const data = await fetchAvailableModels(token);
955
+ if (!data || !data.models) {
956
+ return { object: 'list', data: [] };
957
+ }
958
+
959
+ const modelList = Object.entries(data.models).map(([modelId, modelData]) => ({
960
+ id: modelId,
961
+ object: 'model',
962
+ created: Math.floor(Date.now() / 1000),
963
+ owned_by: 'anthropic',
964
+ description: modelData.displayName || modelId
965
+ }));
966
+
938
967
  return {
939
968
  object: 'list',
940
- data: AVAILABLE_MODELS.map(m => ({
941
- id: m.id,
942
- object: 'model',
943
- created: Math.floor(Date.now() / 1000),
944
- owned_by: 'anthropic',
945
- description: m.description
946
- }))
969
+ data: modelList
947
970
  };
948
971
  }
949
972
 
package/src/constants.js CHANGED
@@ -56,44 +56,6 @@ export const ANTIGRAVITY_HEADERS = {
56
56
  })
57
57
  };
58
58
 
59
- // Model name mappings: Anthropic format → Antigravity format
60
- export const MODEL_MAPPINGS = {
61
- // Claude models
62
- 'claude-3-opus-20240229': 'claude-opus-4-5-thinking',
63
- 'claude-3-5-opus-20240229': 'claude-opus-4-5-thinking',
64
- 'claude-3-5-sonnet-20241022': 'claude-sonnet-4-5',
65
- 'claude-3-5-sonnet-20240620': 'claude-sonnet-4-5',
66
- 'claude-3-sonnet-20240229': 'claude-sonnet-4-5',
67
- 'claude-sonnet-4-5': 'claude-sonnet-4-5',
68
- 'claude-sonnet-4-5-thinking': 'claude-sonnet-4-5-thinking',
69
- 'claude-opus-4-5-thinking': 'claude-opus-4-5-thinking'
70
- };
71
-
72
- // Available models exposed by this proxy
73
- export const AVAILABLE_MODELS = [
74
- {
75
- id: 'claude-sonnet-4-5',
76
- name: 'Claude Sonnet 4.5 (Antigravity)',
77
- description: 'Claude Sonnet 4.5 via Antigravity Cloud Code',
78
- context: 200000,
79
- output: 64000
80
- },
81
- {
82
- id: 'claude-sonnet-4-5-thinking',
83
- name: 'Claude Sonnet 4.5 Thinking (Antigravity)',
84
- description: 'Claude Sonnet 4.5 with extended thinking via Antigravity',
85
- context: 200000,
86
- output: 64000
87
- },
88
- {
89
- id: 'claude-opus-4-5-thinking',
90
- name: 'Claude Opus 4.5 Thinking (Antigravity)',
91
- description: 'Claude Opus 4.5 with extended thinking via Antigravity',
92
- context: 200000,
93
- output: 64000
94
- }
95
- ];
96
-
97
59
  // Default project ID if none can be discovered
98
60
  export const DEFAULT_PROJECT_ID = 'rising-fact-p41fc';
99
61
 
@@ -120,9 +82,42 @@ export const MAX_ACCOUNTS = 10; // Maximum number of accounts allowed
120
82
  export const MAX_WAIT_BEFORE_ERROR_MS = 120000; // 2 minutes - throw error if wait exceeds this
121
83
 
122
84
  // Thinking model constants
123
- export const CLAUDE_THINKING_MAX_OUTPUT_TOKENS = 64000; // Max output tokens for thinking models
124
85
  export const MIN_SIGNATURE_LENGTH = 50; // Minimum valid thinking signature length
125
86
 
87
+ // Gemini-specific limits
88
+ export const GEMINI_MAX_OUTPUT_TOKENS = 16384;
89
+
90
+ /**
91
+ * Get the model family from model name (dynamic detection, no hardcoded list).
92
+ * @param {string} modelName - The model name from the request
93
+ * @returns {'claude' | 'gemini' | 'unknown'} The model family
94
+ */
95
+ export function getModelFamily(modelName) {
96
+ const lower = (modelName || '').toLowerCase();
97
+ if (lower.includes('claude')) return 'claude';
98
+ if (lower.includes('gemini')) return 'gemini';
99
+ return 'unknown';
100
+ }
101
+
102
+ /**
103
+ * Check if a model supports thinking/reasoning output.
104
+ * @param {string} modelName - The model name from the request
105
+ * @returns {boolean} True if the model supports thinking blocks
106
+ */
107
+ export function isThinkingModel(modelName) {
108
+ const lower = (modelName || '').toLowerCase();
109
+ // Claude thinking models have "thinking" in the name
110
+ if (lower.includes('claude') && lower.includes('thinking')) return true;
111
+ // Gemini thinking models: explicit "thinking" in name, OR gemini version 3+
112
+ if (lower.includes('gemini')) {
113
+ if (lower.includes('thinking')) return true;
114
+ // Check for gemini-3 or higher (e.g., gemini-3, gemini-3.5, gemini-4, etc.)
115
+ const versionMatch = lower.match(/gemini-(\d+)/);
116
+ if (versionMatch && parseInt(versionMatch[1], 10) >= 3) return true;
117
+ }
118
+ return false;
119
+ }
120
+
126
121
  // Google OAuth configuration (from opencode-antigravity-auth)
127
122
  export const OAUTH_CONFIG = {
128
123
  clientId: '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com',
@@ -144,8 +139,6 @@ export const OAUTH_REDIRECT_URI = `http://localhost:${OAUTH_CONFIG.callbackPort}
144
139
  export default {
145
140
  ANTIGRAVITY_ENDPOINT_FALLBACKS,
146
141
  ANTIGRAVITY_HEADERS,
147
- MODEL_MAPPINGS,
148
- AVAILABLE_MODELS,
149
142
  DEFAULT_PROJECT_ID,
150
143
  TOKEN_REFRESH_INTERVAL_MS,
151
144
  REQUEST_BODY_LIMIT,
@@ -157,8 +150,10 @@ export default {
157
150
  MAX_RETRIES,
158
151
  MAX_ACCOUNTS,
159
152
  MAX_WAIT_BEFORE_ERROR_MS,
160
- CLAUDE_THINKING_MAX_OUTPUT_TOKENS,
161
153
  MIN_SIGNATURE_LENGTH,
154
+ GEMINI_MAX_OUTPUT_TOKENS,
155
+ getModelFamily,
156
+ isThinkingModel,
162
157
  OAUTH_CONFIG,
163
158
  OAUTH_REDIRECT_URI
164
159
  };
@@ -9,19 +9,19 @@
9
9
 
10
10
  import crypto from 'crypto';
11
11
  import {
12
- MODEL_MAPPINGS,
13
- CLAUDE_THINKING_MAX_OUTPUT_TOKENS,
14
- MIN_SIGNATURE_LENGTH
12
+ MIN_SIGNATURE_LENGTH,
13
+ GEMINI_MAX_OUTPUT_TOKENS,
14
+ getModelFamily,
15
+ isThinkingModel
15
16
  } from './constants.js';
16
17
 
17
18
  /**
18
- * Map Anthropic model name to Antigravity model name
19
- * @param {string} anthropicModel - Anthropic format model name (e.g., 'claude-3-5-sonnet-20241022')
20
- * @returns {string} Antigravity format model name (e.g., 'claude-sonnet-4-5')
19
+ * Sentinel value to skip thought signature validation for Gemini models.
20
+ * Per Google documentation, this value can be used when Claude Code strips
21
+ * the thoughtSignature field from tool_use blocks in multi-turn requests.
22
+ * See: https://ai.google.dev/gemini-api/docs/thought-signatures
21
23
  */
22
- export function mapModelName(anthropicModel) {
23
- return MODEL_MAPPINGS[anthropicModel] || anthropicModel;
24
- }
24
+ const GEMINI_SKIP_SIGNATURE = 'skip_thought_signature_validator';
25
25
 
26
26
  /**
27
27
  * Check if a part is a thinking block
@@ -283,7 +283,7 @@ export function reorderAssistantContent(content) {
283
283
  /**
284
284
  * Convert Anthropic message content to Google Generative AI parts
285
285
  */
286
- function convertContentToParts(content, isClaudeModel = false) {
286
+ function convertContentToParts(content, isClaudeModel = false, isGeminiModel = false) {
287
287
  if (typeof content === 'string') {
288
288
  return [{ text: content }];
289
289
  }
@@ -348,7 +348,19 @@ function convertContentToParts(content, isClaudeModel = false) {
348
348
  functionCall.id = block.id;
349
349
  }
350
350
 
351
- parts.push({ functionCall });
351
+ // Build the part with functionCall
352
+ const part = { functionCall };
353
+
354
+ // For Gemini models, include thoughtSignature at the part level
355
+ // This is required by Gemini 3+ for tool calls to work correctly
356
+ if (isGeminiModel) {
357
+ // Use thoughtSignature from the block if Claude Code preserved it
358
+ // Otherwise, use the sentinel value to skip validation (Claude Code strips non-standard fields)
359
+ // See: https://ai.google.dev/gemini-api/docs/thought-signatures
360
+ part.thoughtSignature = block.thoughtSignature || GEMINI_SKIP_SIGNATURE;
361
+ }
362
+
363
+ parts.push(part);
352
364
  } else if (block.type === 'tool_result') {
353
365
  // Convert tool_result to functionResponse (Google format)
354
366
  let responseContent = block.content;
@@ -411,8 +423,10 @@ function convertRole(role) {
411
423
  export function convertAnthropicToGoogle(anthropicRequest) {
412
424
  const { messages, system, max_tokens, temperature, top_p, top_k, stop_sequences, tools, tool_choice, thinking } = anthropicRequest;
413
425
  const modelName = anthropicRequest.model || '';
414
- const isClaudeModel = modelName.toLowerCase().includes('claude');
415
- const isClaudeThinkingModel = isClaudeModel && modelName.toLowerCase().includes('thinking');
426
+ const modelFamily = getModelFamily(modelName);
427
+ const isClaudeModel = modelFamily === 'claude';
428
+ const isGeminiModel = modelFamily === 'gemini';
429
+ const isThinking = isThinkingModel(modelName);
416
430
 
417
431
  const googleRequest = {
418
432
  contents: [],
@@ -440,7 +454,7 @@ export function convertAnthropicToGoogle(anthropicRequest) {
440
454
  }
441
455
 
442
456
  // Add interleaved thinking hint for Claude thinking models with tools
443
- if (isClaudeThinkingModel && tools && tools.length > 0) {
457
+ if (isClaudeModel && isThinking && tools && tools.length > 0) {
444
458
  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.';
445
459
  if (!googleRequest.systemInstruction) {
446
460
  googleRequest.systemInstruction = { parts: [{ text: hint }] };
@@ -469,7 +483,7 @@ export function convertAnthropicToGoogle(anthropicRequest) {
469
483
  msgContent = reorderAssistantContent(msgContent);
470
484
  }
471
485
 
472
- const parts = convertContentToParts(msgContent, isClaudeModel);
486
+ const parts = convertContentToParts(msgContent, isClaudeModel, isGeminiModel);
473
487
  const content = {
474
488
  role: convertRole(msg.role),
475
489
  parts: parts
@@ -499,29 +513,34 @@ export function convertAnthropicToGoogle(anthropicRequest) {
499
513
  googleRequest.generationConfig.stopSequences = stop_sequences;
500
514
  }
501
515
 
502
- // Enable thinking for Claude thinking models
503
- if (isClaudeThinkingModel) {
504
- const thinkingConfig = {
505
- include_thoughts: true
506
- };
507
-
508
- // Only set thinking_budget if explicitly provided
509
- const thinkingBudget = thinking?.budget_tokens;
510
- if (thinkingBudget) {
511
- thinkingConfig.thinking_budget = thinkingBudget;
516
+ // Enable thinking for thinking models (Claude and Gemini 3+)
517
+ if (isThinking) {
518
+ if (isClaudeModel) {
519
+ // Claude thinking config
520
+ const thinkingConfig = {
521
+ include_thoughts: true
522
+ };
512
523
 
513
- // Ensure maxOutputTokens is large enough when budget is specified
514
- if (!googleRequest.generationConfig.maxOutputTokens ||
515
- googleRequest.generationConfig.maxOutputTokens <= thinkingBudget) {
516
- googleRequest.generationConfig.maxOutputTokens = CLAUDE_THINKING_MAX_OUTPUT_TOKENS;
524
+ // Only set thinking_budget if explicitly provided
525
+ const thinkingBudget = thinking?.budget_tokens;
526
+ if (thinkingBudget) {
527
+ thinkingConfig.thinking_budget = thinkingBudget;
528
+ console.log('[FormatConverter] Claude thinking enabled with budget:', thinkingBudget);
529
+ } else {
530
+ console.log('[FormatConverter] Claude thinking enabled (no budget specified)');
517
531
  }
518
532
 
519
- console.log('[FormatConverter] Thinking enabled with budget:', thinkingBudget);
520
- } else {
521
- console.log('[FormatConverter] Thinking enabled (no budget specified)');
522
- }
533
+ googleRequest.generationConfig.thinkingConfig = thinkingConfig;
534
+ } else if (isGeminiModel) {
535
+ // Gemini thinking config (uses camelCase)
536
+ const thinkingConfig = {
537
+ includeThoughts: true,
538
+ thinkingBudget: thinking?.budget_tokens || 16000
539
+ };
540
+ console.log('[FormatConverter] Gemini thinking enabled with budget:', thinkingConfig.thinkingBudget);
523
541
 
524
- googleRequest.generationConfig.thinkingConfig = thinkingConfig;
542
+ googleRequest.generationConfig.thinkingConfig = thinkingConfig;
543
+ }
525
544
  }
526
545
 
527
546
  // Convert tools to Google format
@@ -541,10 +560,18 @@ export function convertAnthropicToGoogle(anthropicRequest) {
541
560
  || tool.parameters
542
561
  || { type: 'object' };
543
562
 
563
+ // Sanitize schema for general compatibility
564
+ let parameters = sanitizeSchema(schema);
565
+
566
+ // For Gemini models, apply additional cleaning for VALIDATED mode
567
+ if (isGeminiModel) {
568
+ parameters = cleanSchemaForGemini(parameters);
569
+ }
570
+
544
571
  return {
545
572
  name: String(name).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64),
546
573
  description: description,
547
- parameters: sanitizeSchema(schema)
574
+ parameters
548
575
  };
549
576
  });
550
577
 
@@ -552,6 +579,12 @@ export function convertAnthropicToGoogle(anthropicRequest) {
552
579
  console.log('[FormatConverter] Tools:', JSON.stringify(googleRequest.tools).substring(0, 300));
553
580
  }
554
581
 
582
+ // Cap max tokens for Gemini models
583
+ if (isGeminiModel && googleRequest.generationConfig.maxOutputTokens > GEMINI_MAX_OUTPUT_TOKENS) {
584
+ console.log(`[FormatConverter] Capping Gemini max_tokens from ${googleRequest.generationConfig.maxOutputTokens} to ${GEMINI_MAX_OUTPUT_TOKENS}`);
585
+ googleRequest.generationConfig.maxOutputTokens = GEMINI_MAX_OUTPUT_TOKENS;
586
+ }
587
+
555
588
  return googleRequest;
556
589
  }
557
590
 
@@ -638,6 +671,63 @@ function sanitizeSchema(schema) {
638
671
  return sanitized;
639
672
  }
640
673
 
674
+ /**
675
+ * Cleans JSON schema for Gemini API compatibility.
676
+ * Removes unsupported fields that cause VALIDATED mode errors.
677
+ *
678
+ * Gemini's VALIDATED mode rejects schemas with certain JSON Schema keywords
679
+ * that are not supported by the Gemini API.
680
+ *
681
+ * @param {Object} schema - The JSON schema to clean
682
+ * @returns {Object} Cleaned schema safe for Gemini API
683
+ */
684
+ function cleanSchemaForGemini(schema) {
685
+ if (!schema || typeof schema !== 'object') return schema;
686
+ if (Array.isArray(schema)) return schema.map(cleanSchemaForGemini);
687
+
688
+ const result = { ...schema };
689
+
690
+ // Remove unsupported keywords that cause VALIDATED mode errors
691
+ const unsupported = [
692
+ 'additionalProperties', 'default', '$schema', '$defs',
693
+ 'definitions', '$ref', '$id', '$comment', 'title',
694
+ 'minLength', 'maxLength', 'pattern', 'format',
695
+ 'minItems', 'maxItems', 'examples'
696
+ ];
697
+
698
+ for (const key of unsupported) {
699
+ delete result[key];
700
+ }
701
+
702
+ // Check for unsupported 'format' in string types
703
+ if (result.type === 'string' && result.format) {
704
+ const allowed = ['enum', 'date-time'];
705
+ if (!allowed.includes(result.format)) {
706
+ delete result.format;
707
+ }
708
+ }
709
+
710
+ // Recursively clean nested schemas
711
+ for (const [key, value] of Object.entries(result)) {
712
+ if (typeof value === 'object' && value !== null) {
713
+ result[key] = cleanSchemaForGemini(value);
714
+ }
715
+ }
716
+
717
+ // Validate that required array only contains properties that exist
718
+ // Gemini's VALIDATED mode requires this
719
+ if (result.required && Array.isArray(result.required) && result.properties) {
720
+ const definedProps = new Set(Object.keys(result.properties));
721
+ result.required = result.required.filter(prop => definedProps.has(prop));
722
+ // If required is now empty, remove it
723
+ if (result.required.length === 0) {
724
+ delete result.required;
725
+ }
726
+ }
727
+
728
+ return result;
729
+ }
730
+
641
731
  /**
642
732
  * Convert Google Generative AI response to Anthropic Messages API format
643
733
  *
@@ -679,12 +769,20 @@ export function convertGoogleToAnthropic(googleResponse, model) {
679
769
  } else if (part.functionCall) {
680
770
  // Convert functionCall to tool_use
681
771
  // Use the id from the response if available, otherwise generate one
682
- anthropicContent.push({
772
+ const toolId = part.functionCall.id || `toolu_${crypto.randomBytes(12).toString('hex')}`;
773
+ const toolUseBlock = {
683
774
  type: 'tool_use',
684
- id: part.functionCall.id || `toolu_${crypto.randomBytes(12).toString('hex')}`,
775
+ id: toolId,
685
776
  name: part.functionCall.name,
686
777
  input: part.functionCall.args || {}
687
- });
778
+ };
779
+
780
+ // For Gemini 3+, include thoughtSignature from the part level
781
+ if (part.thoughtSignature && part.thoughtSignature.length >= MIN_SIGNATURE_LENGTH) {
782
+ toolUseBlock.thoughtSignature = part.thoughtSignature;
783
+ }
784
+
785
+ anthropicContent.push(toolUseBlock);
688
786
  hasToolCalls = true;
689
787
  }
690
788
  }
@@ -725,7 +823,6 @@ export function convertGoogleToAnthropic(googleResponse, model) {
725
823
  }
726
824
 
727
825
  export default {
728
- mapModelName,
729
826
  convertAnthropicToGoogle,
730
827
  convertGoogleToAnthropic
731
828
  };
package/src/server.js CHANGED
@@ -193,7 +193,7 @@ app.get('/account-limits', async (req, res) => {
193
193
  }
194
194
  }
195
195
 
196
- const sortedModels = Array.from(allModelIds).filter(m => m.includes('claude')).sort();
196
+ const sortedModels = Array.from(allModelIds).sort();
197
197
 
198
198
  // Return ASCII table format
199
199
  if (format === 'table') {
@@ -352,8 +352,32 @@ app.post('/refresh-token', async (req, res) => {
352
352
  /**
353
353
  * List models endpoint (OpenAI-compatible format)
354
354
  */
355
- app.get('/v1/models', (req, res) => {
356
- res.json(listModels());
355
+ app.get('/v1/models', async (req, res) => {
356
+ try {
357
+ await ensureInitialized();
358
+ const account = accountManager.pickNext();
359
+ if (!account) {
360
+ return res.status(503).json({
361
+ type: 'error',
362
+ error: {
363
+ type: 'api_error',
364
+ message: 'No accounts available'
365
+ }
366
+ });
367
+ }
368
+ const token = await accountManager.getTokenForAccount(account);
369
+ const models = await listModels(token);
370
+ res.json(models);
371
+ } catch (error) {
372
+ console.error('[API] Error listing models:', error);
373
+ res.status(500).json({
374
+ type: 'error',
375
+ error: {
376
+ type: 'api_error',
377
+ message: error.message
378
+ }
379
+ });
380
+ }
357
381
  });
358
382
 
359
383
  /**