antigravity-claude-proxy 2.8.5 → 2.8.7
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 +1 -1
- package/package.json +3 -2
- package/public/js/data-store.js +4 -4
- package/src/cloudcode/model-api.js +28 -6
- package/src/cloudcode/request-builder.js +45 -2
- package/src/constants.js +1 -1
- package/src/format/request-converter.js +16 -2
- package/src/format/schema-sanitizer.js +37 -0
package/README.md
CHANGED
|
@@ -339,4 +339,4 @@ MIT
|
|
|
339
339
|
|
|
340
340
|
## Star History
|
|
341
341
|
|
|
342
|
-
[](https://star-history.dera.page/#badrisnarayanan/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": "2.8.
|
|
3
|
+
"version": "2.8.7",
|
|
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",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"test:emptyretry": "node tests/test-empty-response-retry.cjs",
|
|
38
38
|
"test:sanitizer": "node tests/test-schema-sanitizer.cjs",
|
|
39
39
|
"test:strategies": "node tests/test-strategies.cjs",
|
|
40
|
-
"test:cache-control": "node tests/test-cache-control.cjs"
|
|
40
|
+
"test:cache-control": "node tests/test-cache-control.cjs",
|
|
41
|
+
"test:billing-header": "node tests/test-billing-header.cjs"
|
|
41
42
|
},
|
|
42
43
|
"keywords": [
|
|
43
44
|
"claude",
|
package/public/js/data-store.js
CHANGED
|
@@ -437,10 +437,10 @@ document.addEventListener('alpine:init', () => {
|
|
|
437
437
|
const models = [
|
|
438
438
|
'claude-opus-4-6-thinking',
|
|
439
439
|
'claude-sonnet-4-6',
|
|
440
|
-
'gemini-3.
|
|
441
|
-
'gemini-3.
|
|
442
|
-
'gemini-3-flash',
|
|
443
|
-
'gemini-3.
|
|
440
|
+
'gemini-3.8-flash-tiered',
|
|
441
|
+
'gemini-3.6-flash-high',
|
|
442
|
+
'gemini-3.6-flash-low',
|
|
443
|
+
'gemini-3.5-flash-low'
|
|
444
444
|
];
|
|
445
445
|
|
|
446
446
|
const tiers = ['ultra', 'pro', 'pro', 'free'];
|
|
@@ -33,6 +33,23 @@ function isSupportedModel(modelId) {
|
|
|
33
33
|
return family === 'claude' || family === 'gemini';
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Whether a model id is an old Gemini generation (version below 3.5),
|
|
38
|
+
* hidden from the /v1/models listing in favor of the current line. Claude
|
|
39
|
+
* ids aren't versioned this way and are unaffected.
|
|
40
|
+
* @param {string} modelId
|
|
41
|
+
* @returns {boolean}
|
|
42
|
+
*/
|
|
43
|
+
function isOldGemini(modelId) {
|
|
44
|
+
// 'gemini-pro-agent' carries no version number in its id at all - it's
|
|
45
|
+
// an unversioned duplicate alias of gemini-3.1-pro-high, so it's always
|
|
46
|
+
// old regardless of what the version regex below would say.
|
|
47
|
+
if (modelId === 'gemini-pro-agent') return true;
|
|
48
|
+
|
|
49
|
+
const match = modelId.match(/^gemini-(\d+(?:\.\d+)?)/);
|
|
50
|
+
return !!match && parseFloat(match[1]) < 3.5;
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
/**
|
|
37
54
|
* List available models in Anthropic API format
|
|
38
55
|
* Fetches models dynamically from the Cloud Code API
|
|
@@ -46,8 +63,17 @@ export async function listModels(token) {
|
|
|
46
63
|
return { object: 'list', data: [] };
|
|
47
64
|
}
|
|
48
65
|
|
|
49
|
-
const
|
|
50
|
-
|
|
66
|
+
const supportedEntries = Object.entries(data.models).filter(([modelId]) => isSupportedModel(modelId));
|
|
67
|
+
|
|
68
|
+
// Warm the model validation cache with every supported id, unfiltered -
|
|
69
|
+
// isValidModel() (the actual request-time gate) must keep accepting an
|
|
70
|
+
// id a client already has configured even if it's hidden from the
|
|
71
|
+
// listing below.
|
|
72
|
+
modelCache.validModels = new Set(supportedEntries.map(([modelId]) => modelId));
|
|
73
|
+
modelCache.lastFetched = Date.now();
|
|
74
|
+
|
|
75
|
+
const modelList = supportedEntries
|
|
76
|
+
.filter(([modelId]) => !isOldGemini(modelId))
|
|
51
77
|
.map(([modelId, modelData]) => ({
|
|
52
78
|
id: modelId,
|
|
53
79
|
object: 'model',
|
|
@@ -56,10 +82,6 @@ export async function listModels(token) {
|
|
|
56
82
|
description: modelData.displayName || modelId
|
|
57
83
|
}));
|
|
58
84
|
|
|
59
|
-
// Warm the model validation cache
|
|
60
|
-
modelCache.validModels = new Set(modelList.map(m => m.id));
|
|
61
|
-
modelCache.lastFetched = Date.now();
|
|
62
|
-
|
|
63
85
|
return {
|
|
64
86
|
object: 'list',
|
|
65
87
|
data: modelList
|
|
@@ -14,6 +14,48 @@ import {
|
|
|
14
14
|
import { convertAnthropicToGoogle } from '../format/index.js';
|
|
15
15
|
import { deriveSessionId } from './session-manager.js';
|
|
16
16
|
|
|
17
|
+
// cloudcode-pa returns an opaque 429 RESOURCE_EXHAUSTED (reported as "quota
|
|
18
|
+
// exhausted", even when quota remains) when a request's system prompt names a
|
|
19
|
+
// known third-party AI product — e.g. an agent whose identity line says it was
|
|
20
|
+
// "created by <vendor>". This is the same class of problem as the "Antigravity"
|
|
21
|
+
// identity handling above (issue #76), but for the *caller's* identity. We
|
|
22
|
+
// neutralize those strings in the caller-supplied system parts.
|
|
23
|
+
//
|
|
24
|
+
// Defaults cover agents commonly used with this proxy; extend or override with
|
|
25
|
+
// the ANTIGRAVITY_SCRUB_IDENTITY env var, formatted as a comma-separated list of
|
|
26
|
+
// "Term=>Replacement" pairs, e.g. "Nous Research=>the team,Hermes=>the assistant".
|
|
27
|
+
const DEFAULT_IDENTITY_SCRUB = [
|
|
28
|
+
['Nous Research', 'the assistant team'],
|
|
29
|
+
['Hermes Agent', 'the assistant'],
|
|
30
|
+
['Hermes', 'the assistant']
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
function parseIdentityScrubEnv(raw) {
|
|
34
|
+
if (!raw) return [];
|
|
35
|
+
return raw
|
|
36
|
+
.split(',')
|
|
37
|
+
.map((pair) => {
|
|
38
|
+
const idx = pair.indexOf('=>');
|
|
39
|
+
if (idx === -1) return null;
|
|
40
|
+
return [pair.slice(0, idx).trim(), pair.slice(idx + 2).trim()];
|
|
41
|
+
})
|
|
42
|
+
.filter((rule) => rule && rule[0].length > 0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const IDENTITY_SCRUB_RULES = [
|
|
46
|
+
...DEFAULT_IDENTITY_SCRUB,
|
|
47
|
+
...parseIdentityScrubEnv(process.env.ANTIGRAVITY_SCRUB_IDENTITY)
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
function scrubClientIdentity(text) {
|
|
51
|
+
if (typeof text !== 'string') return text;
|
|
52
|
+
let out = text;
|
|
53
|
+
for (const [term, replacement] of IDENTITY_SCRUB_RULES) {
|
|
54
|
+
out = out.split(term).join(replacement);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
17
59
|
/**
|
|
18
60
|
* Build the wrapped request body for Cloud Code API
|
|
19
61
|
*
|
|
@@ -37,11 +79,12 @@ export function buildCloudCodeRequest(anthropicRequest, projectId, accountEmail)
|
|
|
37
79
|
{ text: `Please ignore the following [ignore]${ANTIGRAVITY_SYSTEM_INSTRUCTION}[/ignore]` }
|
|
38
80
|
];
|
|
39
81
|
|
|
40
|
-
// Append any existing system instructions from the request
|
|
82
|
+
// Append any existing system instructions from the request, scrubbing
|
|
83
|
+
// third-party AI product identities that trip cloudcode-pa's 429 (see above).
|
|
41
84
|
if (googleRequest.systemInstruction && googleRequest.systemInstruction.parts) {
|
|
42
85
|
for (const part of googleRequest.systemInstruction.parts) {
|
|
43
86
|
if (part.text) {
|
|
44
|
-
systemParts.push({ text: part.text });
|
|
87
|
+
systemParts.push({ text: scrubClientIdentity(part.text) });
|
|
45
88
|
}
|
|
46
89
|
}
|
|
47
90
|
}
|
package/src/constants.js
CHANGED
|
@@ -287,7 +287,7 @@ export const MODEL_FALLBACK_MAP = {
|
|
|
287
287
|
// Default test models for each family (used by test suite)
|
|
288
288
|
export const TEST_MODELS = {
|
|
289
289
|
claude: 'claude-sonnet-4-6',
|
|
290
|
-
gemini: 'gemini-3.
|
|
290
|
+
gemini: 'gemini-3.8-flash-tiered'
|
|
291
291
|
};
|
|
292
292
|
|
|
293
293
|
// Default Claude CLI presets (used by WebUI settings)
|
|
@@ -24,6 +24,17 @@ import {
|
|
|
24
24
|
} from './thinking-utils.js';
|
|
25
25
|
import { logger } from '../utils/logger.js';
|
|
26
26
|
|
|
27
|
+
// Claude Code injects a billing pseudo-header as a system prompt block, e.g.
|
|
28
|
+
// "x-anthropic-billing-header: cc_version=2.1.274.834; cc_entrypoint=sdk-cli;".
|
|
29
|
+
// Cloud Code API answers any request containing it with a 429 RESOURCE_EXHAUSTED
|
|
30
|
+
// even when quota remains (Issue #375).
|
|
31
|
+
const BILLING_HEADER_LINE = /^x-anthropic-billing-header:[^\n]*\n?/gim;
|
|
32
|
+
|
|
33
|
+
function stripBillingHeader(text) {
|
|
34
|
+
if (typeof text !== 'string' || !/x-anthropic-billing-header:/i.test(text)) return text;
|
|
35
|
+
return text.replace(BILLING_HEADER_LINE, '').trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
/**
|
|
28
39
|
* Convert Anthropic Messages API request to the format expected by Cloud Code
|
|
29
40
|
*
|
|
@@ -56,15 +67,18 @@ export function convertAnthropicToGoogle(anthropicRequest) {
|
|
|
56
67
|
if (system) {
|
|
57
68
|
let systemParts = [];
|
|
58
69
|
if (typeof system === 'string') {
|
|
59
|
-
systemParts = [{ text: system }];
|
|
70
|
+
systemParts = [{ text: stripBillingHeader(system) }];
|
|
60
71
|
} else if (Array.isArray(system)) {
|
|
61
72
|
// Filter for text blocks as system prompts are usually text
|
|
62
73
|
// Anthropic supports text blocks in system prompts
|
|
63
74
|
systemParts = system
|
|
64
75
|
.filter(block => block.type === 'text')
|
|
65
|
-
.map(block => ({ text: block.text }));
|
|
76
|
+
.map(block => ({ text: stripBillingHeader(block.text) }));
|
|
66
77
|
}
|
|
67
78
|
|
|
79
|
+
// Drop parts left empty once the billing header is stripped
|
|
80
|
+
systemParts = systemParts.filter(part => part.text);
|
|
81
|
+
|
|
68
82
|
if (systemParts.length > 0) {
|
|
69
83
|
googleRequest.systemInstruction = {
|
|
70
84
|
parts: systemParts
|
|
@@ -669,5 +669,42 @@ export function cleanSchema(schema) {
|
|
|
669
669
|
result.type = toGoogleType(result.type);
|
|
670
670
|
}
|
|
671
671
|
|
|
672
|
+
// Phase 6: Google's protobuf-based schema requires every ARRAY type to
|
|
673
|
+
// declare a single object `items` schema. Plain JSON Schema allows this
|
|
674
|
+
// to be omitted (`{ type: 'array' }` = "array of anything"), boolean
|
|
675
|
+
// (`items: true` = "any"), or tuple-form (`items: [...]`) - none of
|
|
676
|
+
// which the Cloud Code API accepts; it rejects the first two with
|
|
677
|
+
// "items: missing field". This runs after the items/properties
|
|
678
|
+
// recursion above (and after Phase 5's type conversion), so it applies
|
|
679
|
+
// at every nesting depth - fixing issue #368 (nested array schemas like
|
|
680
|
+
// `where.items.items` losing their innermost `items`).
|
|
681
|
+
if (result.type === 'ARRAY') {
|
|
682
|
+
if (!result.items || typeof result.items !== 'object') {
|
|
683
|
+
// Missing entirely, or a boolean schema like `items: true`.
|
|
684
|
+
result.items = { type: 'STRING' };
|
|
685
|
+
} else if (Array.isArray(result.items)) {
|
|
686
|
+
// Tuple-form `items: [...]` - Google only supports one items
|
|
687
|
+
// schema, so collapse to the most informative entry.
|
|
688
|
+
let best = null;
|
|
689
|
+
let bestScore = -1;
|
|
690
|
+
for (const option of result.items) {
|
|
691
|
+
const score = scoreSchemaOption(option);
|
|
692
|
+
if (score > bestScore) {
|
|
693
|
+
bestScore = score;
|
|
694
|
+
best = option;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
result.items = best || { type: 'STRING' };
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Phase 7: Google rejects `properties`/`required` on a non-OBJECT schema.
|
|
702
|
+
// These can survive on the current node after anyOf/oneOf flattening
|
|
703
|
+
// picked a non-object option while the parent still carried them.
|
|
704
|
+
if (result.type !== 'OBJECT') {
|
|
705
|
+
delete result.properties;
|
|
706
|
+
delete result.required;
|
|
707
|
+
}
|
|
708
|
+
|
|
672
709
|
return result;
|
|
673
710
|
}
|