antigravity-proxy 0.1.1
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/.dockerignore +10 -0
- package/.env.example +2 -0
- package/Dockerfile +20 -0
- package/README.md +132 -0
- package/bun.lock +51 -0
- package/docker-compose.yml +11 -0
- package/package.json +22 -0
- package/reset-accounts.ts +17 -0
- package/screenshots/screenshot.png +0 -0
- package/src/api/quota.ts +187 -0
- package/src/auth/manager.ts +326 -0
- package/src/auth/oauth.ts +95 -0
- package/src/auth/storage.ts +39 -0
- package/src/auth/types.ts +73 -0
- package/src/config/manager.ts +141 -0
- package/src/config/types.ts +73 -0
- package/src/frontend/components/config-modal.html +109 -0
- package/src/frontend/components/header.html +55 -0
- package/src/frontend/components/main.html +64 -0
- package/src/frontend/css/styles.css +53 -0
- package/src/frontend/index.html +48 -0
- package/src/frontend/js/app.js +883 -0
- package/src/frontend/js/tailwind-config.js +40 -0
- package/src/scripts/check_quota_api.ts +70 -0
- package/src/scripts/check_sandbox_quota.ts +42 -0
- package/src/scripts/debug-accounts.ts +25 -0
- package/src/scripts/debug-quota-raw.ts +47 -0
- package/src/scripts/diagnose_claude_quota.ts +97 -0
- package/src/scripts/reset-accounts.ts +24 -0
- package/src/scripts/test-claude-cli.ts +55 -0
- package/src/scripts/test-request.ts +138 -0
- package/src/scripts/test-routing-logic.ts +40 -0
- package/src/scripts/test_claude_forced.ts +53 -0
- package/src/scripts/test_placeholder_model.ts +85 -0
- package/src/scripts/verify-claude.ts +51 -0
- package/src/server.ts +679 -0
- package/src/utils/cache.ts +18 -0
- package/src/utils/errors.ts +93 -0
- package/src/utils/headers.ts +172 -0
- package/src/utils/schema.ts +100 -0
- package/src/utils/transform.ts +532 -0
- package/tests/functional/gemini-functional.test.ts +122 -0
- package/tests/functional/models.test.ts +100 -0
- package/tests/unit/manager.test.ts +13 -0
- package/tests/unit/transform.test.ts +135 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { expect, test, describe, beforeAll } from "bun:test";
|
|
2
|
+
import { readFileSync, existsSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
|
|
6
|
+
const CONFIG_PATH = join(homedir(), ".config/opencode/opencode.json");
|
|
7
|
+
|
|
8
|
+
function parseConfig(text: string) {
|
|
9
|
+
const cleaned = text.replace(/,(\s*[\]}])/g, "$1");
|
|
10
|
+
return JSON.parse(cleaned);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("Antigravity Proxy Functional Tests", () => {
|
|
14
|
+
let config: any;
|
|
15
|
+
let provider: any;
|
|
16
|
+
let baseURL: string;
|
|
17
|
+
let headers: Record<string, string>;
|
|
18
|
+
let modelIds: string[] = [];
|
|
19
|
+
|
|
20
|
+
beforeAll(() => {
|
|
21
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
22
|
+
throw new Error(`Config file not found at ${CONFIG_PATH}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const content = readFileSync(CONFIG_PATH, "utf8");
|
|
27
|
+
config = parseConfig(content);
|
|
28
|
+
provider = config.provider?.["antigravity-proxy"];
|
|
29
|
+
|
|
30
|
+
if (!provider) {
|
|
31
|
+
throw new Error("Provider 'antigravity-proxy' not found in config");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
baseURL = provider.options?.baseURL;
|
|
35
|
+
if (!baseURL) {
|
|
36
|
+
throw new Error("baseURL not defined in provider options");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
headers = provider.options?.headers || {};
|
|
40
|
+
modelIds = Object.keys(provider.models || {});
|
|
41
|
+
} catch (e: any) {
|
|
42
|
+
throw new Error(`Failed to initialize test: ${e.message}`);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("Config should have models defined", () => {
|
|
47
|
+
expect(modelIds.length).toBeGreaterThan(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("Proxy should be reachable", async () => {
|
|
51
|
+
const response = await fetch(`${baseURL}/models`, {
|
|
52
|
+
headers: { ...headers }
|
|
53
|
+
});
|
|
54
|
+
expect(response.ok).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("Model Completions", () => {
|
|
58
|
+
test("All models should respond to a basic prompt", async () => {
|
|
59
|
+
console.log(`\n🚀 Testing ${modelIds.length} models...`);
|
|
60
|
+
|
|
61
|
+
const results = await Promise.all(modelIds.map(async (modelId) => {
|
|
62
|
+
try {
|
|
63
|
+
const response = await fetch(`${baseURL}/chat/completions`, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: {
|
|
66
|
+
"Content-Type": "application/json",
|
|
67
|
+
...headers
|
|
68
|
+
},
|
|
69
|
+
body: JSON.stringify({
|
|
70
|
+
model: modelId,
|
|
71
|
+
messages: [{ role: "user", content: "Say 'OK'" }],
|
|
72
|
+
max_tokens: 10,
|
|
73
|
+
stream: false
|
|
74
|
+
})
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (response.ok) {
|
|
78
|
+
const data = await response.json() as any;
|
|
79
|
+
return { modelId, success: true, status: response.status };
|
|
80
|
+
} else {
|
|
81
|
+
const error = await response.text();
|
|
82
|
+
return { modelId, success: false, status: response.status, error };
|
|
83
|
+
}
|
|
84
|
+
} catch (e: any) {
|
|
85
|
+
return { modelId, success: false, error: e.message };
|
|
86
|
+
}
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
const failures = results.filter(r => !r.success);
|
|
90
|
+
if (failures.length > 0) {
|
|
91
|
+
console.error("Failures detected:");
|
|
92
|
+
failures.forEach(f => {
|
|
93
|
+
console.error(`- ${f.modelId}: ${f.error || 'Status ' + f.status}`);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
expect(failures.length).toBe(0);
|
|
98
|
+
}, 120000);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { expect, test, describe } from "bun:test";
|
|
2
|
+
import { getFamilyName } from "../../src/auth/manager";
|
|
3
|
+
|
|
4
|
+
describe("Manager Utils", () => {
|
|
5
|
+
test("getFamilyName should correctly classify models", () => {
|
|
6
|
+
expect(getFamilyName("gemini-1.5-flash")).toBe("Gemini 3 Flash");
|
|
7
|
+
expect(getFamilyName("gemini-1.5-pro")).toBe("Gemini 3 Pro");
|
|
8
|
+
expect(getFamilyName("claude-3-5-sonnet")).toBe("Claude/GPT");
|
|
9
|
+
expect(getFamilyName("gpt-4o")).toBe("Claude/GPT");
|
|
10
|
+
expect(getFamilyName("gemini-2.5-flash")).toBe("Gemini 2.5");
|
|
11
|
+
expect(getFamilyName("unknown-model")).toBe("Other");
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
|
|
2
|
+
import { describe, expect, test } from "bun:test";
|
|
3
|
+
import { transformToGoogleBody, transformGoogleEventToOpenAI } from "../../src/utils/transform";
|
|
4
|
+
|
|
5
|
+
describe("Unit Tests: transformToGoogleBody", () => {
|
|
6
|
+
test("Basic message transformation", () => {
|
|
7
|
+
const openaiBody = {
|
|
8
|
+
model: "gpt-4o",
|
|
9
|
+
messages: [
|
|
10
|
+
{ role: "user", content: "Hello Gemini" }
|
|
11
|
+
],
|
|
12
|
+
temperature: 0.5
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const result = transformToGoogleBody(openaiBody, "test-project", false, "us-central1");
|
|
16
|
+
|
|
17
|
+
expect(result.project).toBe("test-project");
|
|
18
|
+
expect(result.model).toBe("gpt-4o"); // It passes through if no antigravity prefix
|
|
19
|
+
expect(result.request.contents).toHaveLength(1);
|
|
20
|
+
expect(result.request.contents[0].role).toBe("user");
|
|
21
|
+
expect(result.request.contents[0].parts[0].text).toBe("Hello Gemini");
|
|
22
|
+
expect(result.request.generationConfig.temperature).toBe(0.5);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("Antigravity model prefix removal", () => {
|
|
26
|
+
const openaiBody = {
|
|
27
|
+
model: "antigravity-gemini-2.0-flash",
|
|
28
|
+
messages: [{ role: "user", content: "Hi" }]
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const result = transformToGoogleBody(openaiBody, "p", false, "us-central1");
|
|
32
|
+
expect(result.model).toBe("gemini-2.0-flash");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("Thinking level extraction for CLI", () => {
|
|
36
|
+
const openaiBody = {
|
|
37
|
+
model: "gemini-3-flash-thinking-medium",
|
|
38
|
+
messages: [{ role: "user", content: "Hi" }]
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const result = transformToGoogleBody(openaiBody, "p", true, "us-central1"); // isCli = true
|
|
42
|
+
expect(result.model).toBe("gemini-3-flash-preview");
|
|
43
|
+
expect(result.request.generationConfig.thinkingConfig.thinkingLevel).toBe("medium");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("Multi-turn conversation", () => {
|
|
47
|
+
const openaiBody = {
|
|
48
|
+
model: "gemini-1.5-pro",
|
|
49
|
+
messages: [
|
|
50
|
+
{ role: "user", content: "Hello" },
|
|
51
|
+
{ role: "assistant", content: "Hi there!" },
|
|
52
|
+
{ role: "user", content: "How are you?" }
|
|
53
|
+
]
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const result = transformToGoogleBody(openaiBody, "p", false, "us-central1");
|
|
57
|
+
expect(result.request.contents).toHaveLength(3);
|
|
58
|
+
expect(result.request.contents[0].role).toBe("user");
|
|
59
|
+
expect(result.request.contents[1].role).toBe("model"); // OpenAI assistant -> Google model
|
|
60
|
+
expect(result.request.contents[2].role).toBe("user");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("Tool transformation", () => {
|
|
64
|
+
const openaiBody = {
|
|
65
|
+
model: "gemini-1.5-pro",
|
|
66
|
+
messages: [{ role: "user", content: "Check weather" }],
|
|
67
|
+
tools: [
|
|
68
|
+
{
|
|
69
|
+
type: "function",
|
|
70
|
+
function: {
|
|
71
|
+
name: "get_weather",
|
|
72
|
+
description: "Get weather",
|
|
73
|
+
parameters: {
|
|
74
|
+
type: "object",
|
|
75
|
+
properties: {
|
|
76
|
+
location: { type: "string" }
|
|
77
|
+
},
|
|
78
|
+
required: ["location"]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const result = transformToGoogleBody(openaiBody, "p", false, "us-central1");
|
|
86
|
+
expect(result.request.tools).toBeDefined();
|
|
87
|
+
expect(result.request.tools[0].functionDeclarations).toHaveLength(1);
|
|
88
|
+
expect(result.request.tools[0].functionDeclarations[0].name).toBe("get_weather");
|
|
89
|
+
expect(result.request.tools[0].functionDeclarations[0].parameters.properties.location).toBeDefined();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("Unit Tests: transformGoogleEventToOpenAI", () => {
|
|
94
|
+
test("Basic text response", () => {
|
|
95
|
+
const googleData = {
|
|
96
|
+
candidates: [{
|
|
97
|
+
content: {
|
|
98
|
+
parts: [{ text: "Hello world" }]
|
|
99
|
+
},
|
|
100
|
+
finishReason: "STOP"
|
|
101
|
+
}]
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const result = transformGoogleEventToOpenAI(googleData, "gemini-1.5-pro", "req-123");
|
|
105
|
+
expect(result).not.toBeNull();
|
|
106
|
+
expect(result.choices[0].delta.content).toBe("Hello world");
|
|
107
|
+
expect(result.choices[0].finish_reason).toBe("stop");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("Tool call response", () => {
|
|
111
|
+
const googleData = {
|
|
112
|
+
candidates: [{
|
|
113
|
+
content: {
|
|
114
|
+
parts: [{
|
|
115
|
+
functionCall: {
|
|
116
|
+
name: "get_weather",
|
|
117
|
+
args: { location: "London" }
|
|
118
|
+
}
|
|
119
|
+
}]
|
|
120
|
+
}
|
|
121
|
+
}]
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const result = transformGoogleEventToOpenAI(googleData, "gemini-1.5-pro");
|
|
125
|
+
expect(result.choices[0].delta.tool_calls).toHaveLength(1);
|
|
126
|
+
expect(result.choices[0].delta.tool_calls[0].function.name).toBe("get_weather");
|
|
127
|
+
expect(JSON.parse(result.choices[0].delta.tool_calls[0].function.arguments).location).toBe("London");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("Empty/Invalid response", () => {
|
|
131
|
+
const googleData = { candidates: [] };
|
|
132
|
+
const result = transformGoogleEventToOpenAI(googleData, "model");
|
|
133
|
+
expect(result).toBeNull();
|
|
134
|
+
});
|
|
135
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["ESNext"],
|
|
4
|
+
"module": "esnext",
|
|
5
|
+
"target": "esnext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"allowImportingTsExtensions": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"jsx": "react-jsx",
|
|
13
|
+
"allowSyntheticDefaultImports": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"allowJs": true,
|
|
16
|
+
"types": [
|
|
17
|
+
"bun-types"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
}
|