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.
Files changed (46) hide show
  1. package/.dockerignore +10 -0
  2. package/.env.example +2 -0
  3. package/Dockerfile +20 -0
  4. package/README.md +132 -0
  5. package/bun.lock +51 -0
  6. package/docker-compose.yml +11 -0
  7. package/package.json +22 -0
  8. package/reset-accounts.ts +17 -0
  9. package/screenshots/screenshot.png +0 -0
  10. package/src/api/quota.ts +187 -0
  11. package/src/auth/manager.ts +326 -0
  12. package/src/auth/oauth.ts +95 -0
  13. package/src/auth/storage.ts +39 -0
  14. package/src/auth/types.ts +73 -0
  15. package/src/config/manager.ts +141 -0
  16. package/src/config/types.ts +73 -0
  17. package/src/frontend/components/config-modal.html +109 -0
  18. package/src/frontend/components/header.html +55 -0
  19. package/src/frontend/components/main.html +64 -0
  20. package/src/frontend/css/styles.css +53 -0
  21. package/src/frontend/index.html +48 -0
  22. package/src/frontend/js/app.js +883 -0
  23. package/src/frontend/js/tailwind-config.js +40 -0
  24. package/src/scripts/check_quota_api.ts +70 -0
  25. package/src/scripts/check_sandbox_quota.ts +42 -0
  26. package/src/scripts/debug-accounts.ts +25 -0
  27. package/src/scripts/debug-quota-raw.ts +47 -0
  28. package/src/scripts/diagnose_claude_quota.ts +97 -0
  29. package/src/scripts/reset-accounts.ts +24 -0
  30. package/src/scripts/test-claude-cli.ts +55 -0
  31. package/src/scripts/test-request.ts +138 -0
  32. package/src/scripts/test-routing-logic.ts +40 -0
  33. package/src/scripts/test_claude_forced.ts +53 -0
  34. package/src/scripts/test_placeholder_model.ts +85 -0
  35. package/src/scripts/verify-claude.ts +51 -0
  36. package/src/server.ts +679 -0
  37. package/src/utils/cache.ts +18 -0
  38. package/src/utils/errors.ts +93 -0
  39. package/src/utils/headers.ts +172 -0
  40. package/src/utils/schema.ts +100 -0
  41. package/src/utils/transform.ts +532 -0
  42. package/tests/functional/gemini-functional.test.ts +122 -0
  43. package/tests/functional/models.test.ts +100 -0
  44. package/tests/unit/manager.test.ts +13 -0
  45. package/tests/unit/transform.test.ts +135 -0
  46. 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
+ }