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 +31 -3
- package/package.json +1 -1
- package/src/cloudcode-client.js +51 -28
- package/src/constants.js +37 -42
- package/src/format-converter.js +136 -39
- package/src/server.js +27 -3
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
|
|
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
|

|
|
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-
|
|
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
|
-
[](https://www.star-history.com/#badri-s2001/antigravity-claude-proxy&type=date&legend=top-left)
|
|
357
|
+
[](https://www.star-history.com/#badri-s2001/antigravity-claude-proxy&type=date&legend=top-left)
|
package/package.json
CHANGED
package/src/cloudcode-client.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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 =
|
|
277
|
-
const
|
|
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 =
|
|
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,
|
|
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 (
|
|
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 =
|
|
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
|
-
* @
|
|
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:
|
|
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
|
};
|
package/src/format-converter.js
CHANGED
|
@@ -9,19 +9,19 @@
|
|
|
9
9
|
|
|
10
10
|
import crypto from 'crypto';
|
|
11
11
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
MIN_SIGNATURE_LENGTH,
|
|
13
|
+
GEMINI_MAX_OUTPUT_TOKENS,
|
|
14
|
+
getModelFamily,
|
|
15
|
+
isThinkingModel
|
|
15
16
|
} from './constants.js';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
415
|
-
const
|
|
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 (
|
|
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
|
|
503
|
-
if (
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
//
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
520
|
-
} else {
|
|
521
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
772
|
+
const toolId = part.functionCall.id || `toolu_${crypto.randomBytes(12).toString('hex')}`;
|
|
773
|
+
const toolUseBlock = {
|
|
683
774
|
type: 'tool_use',
|
|
684
|
-
id:
|
|
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).
|
|
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
|
-
|
|
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
|
/**
|