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
package/.dockerignore
ADDED
package/.env.example
ADDED
package/Dockerfile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
FROM oven/bun:alpine AS builder
|
|
2
|
+
WORKDIR /app
|
|
3
|
+
|
|
4
|
+
COPY package.json bun.lock ./
|
|
5
|
+
RUN bun install --frozen-lockfile --production
|
|
6
|
+
|
|
7
|
+
FROM oven/bun:alpine
|
|
8
|
+
WORKDIR /app
|
|
9
|
+
|
|
10
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
11
|
+
COPY src ./src
|
|
12
|
+
COPY package.json ./
|
|
13
|
+
|
|
14
|
+
# Create a data directory for persistence and set permissions
|
|
15
|
+
RUN mkdir -p /app/data && chown -R bun:bun /app/data
|
|
16
|
+
|
|
17
|
+
EXPOSE 3000
|
|
18
|
+
|
|
19
|
+
USER bun
|
|
20
|
+
CMD ["bun", "run", "src/server.ts"]
|
package/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Antigravity Proxy
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Antigravity Proxy is a high-performance, Bun-native gateway that exposes Google's internal Gemini and CloudCode APIs through an **OpenAI-compatible interface**. It enables seamless integration between advanced models (like Claude 3.5 Sonnet, Gemini 3, and GPT-equivalent models) and CLI agents (such as **OpenCode** or **Claude Code**), as well as any application supporting the OpenAI API standard.
|
|
6
|
+
|
|
7
|
+
This project is strongly inspired by [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth).
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **OpenAI API Compatibility**: Full support for `v1/chat/completions` with streaming (SSE).
|
|
12
|
+
- **Multi-Agent Support**: Specifically designed to work with **Claude Code**, **OpenCode**, and other agentic frameworks.
|
|
13
|
+
- **Account Rotation & Health Scoring**: Automatically rotates multiple Google accounts, penalizing those with errors and favoring healthy ones.
|
|
14
|
+
- **Quota Management**: Real-time monitoring and automatic cooldowns (backoff) on `429 Too Many Requests` errors.
|
|
15
|
+
- **Dual-Pool Routing**:
|
|
16
|
+
- **CLI Pool**: Routes to production endpoints (Gemini 2.5/3 Flash & Pro).
|
|
17
|
+
- **Sandbox Pool**: Accesses internal/experimental models (Claude 3.5 Sonnet, Thinking models, GPT-equivalent).
|
|
18
|
+
- **Integrated Dashboard**: Manage accounts, monitor health, and view real-time logs via a built-in web interface.
|
|
19
|
+
- **Automatic Project Discovery**: Auto-detects Google Cloud Project IDs via Cloud SDK impersonation.
|
|
20
|
+
|
|
21
|
+
## Deployment Options
|
|
22
|
+
|
|
23
|
+
### Bunx (Recommended)
|
|
24
|
+
You can run the proxy instantly using `bunx`:
|
|
25
|
+
```bash
|
|
26
|
+
bunx antigravity-proxy
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Docker Hub
|
|
30
|
+
```bash
|
|
31
|
+
docker run -d -p 3000:3000 -e BASE_URL=http://localhost:3000 --name antigravity-proxy frieserpaldi/antigravity-proxy:0.1.0
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Local Execution (Bun)
|
|
35
|
+
Requirements: Bun (v1.0.0 or higher).
|
|
36
|
+
```bash
|
|
37
|
+
bun install
|
|
38
|
+
bun run start
|
|
39
|
+
```
|
|
40
|
+
The server starts on port 3000.
|
|
41
|
+
|
|
42
|
+
## Integration Guides
|
|
43
|
+
|
|
44
|
+
### Claude Code Configuration
|
|
45
|
+
To use **Claude Code** with Antigravity Proxy, point the API base URL to your local instance and specify a model from the Sandbox Pool:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Point Claude Code to the proxy
|
|
49
|
+
export CLAUDE_CODE_API_BASE="http://localhost:3000/v1"
|
|
50
|
+
|
|
51
|
+
# Run Claude specifying an Antigravity model
|
|
52
|
+
claude --model antigravity-claude-sonnet-4-5
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### OpenCode Configuration
|
|
56
|
+
Add the following provider to your `~/.config/opencode/opencode.json` under the `"provider"` key:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
"provider": {
|
|
60
|
+
"antigravity-proxy": {
|
|
61
|
+
"npm": "@ai-sdk/openai-compatible",
|
|
62
|
+
"name": "Antigravity Proxy",
|
|
63
|
+
"options": {
|
|
64
|
+
"baseURL": "http://localhost:3000/v1"
|
|
65
|
+
},
|
|
66
|
+
"models": {
|
|
67
|
+
"antigravity-gemini-3-pro-low": {
|
|
68
|
+
"name": "Gemini 3 Pro Low (Antigravity)",
|
|
69
|
+
"limit": { "context": 1048576, "output": 65535 }
|
|
70
|
+
},
|
|
71
|
+
"antigravity-gemini-3-pro-high": {
|
|
72
|
+
"name": "Gemini 3 Pro High (Antigravity)",
|
|
73
|
+
"limit": { "context": 1048576, "output": 65535 }
|
|
74
|
+
},
|
|
75
|
+
"antigravity-gemini-3-flash": {
|
|
76
|
+
"name": "Gemini 3 Flash (Antigravity)",
|
|
77
|
+
"limit": { "context": 1048576, "output": 65536 }
|
|
78
|
+
},
|
|
79
|
+
"antigravity-claude-sonnet-4-5": {
|
|
80
|
+
"name": "Claude Sonnet 4.5 (Antigravity)",
|
|
81
|
+
"limit": { "context": 200000, "output": 64000 }
|
|
82
|
+
},
|
|
83
|
+
"antigravity-claude-sonnet-4-5-thinking-low": {
|
|
84
|
+
"name": "Claude Sonnet 4.5 Think Low (Antigravity)",
|
|
85
|
+
"limit": { "context": 200000, "output": 64000 }
|
|
86
|
+
},
|
|
87
|
+
"antigravity-claude-sonnet-4-5-thinking-medium": {
|
|
88
|
+
"name": "Claude Sonnet 4.5 Think Medium (Antigravity)",
|
|
89
|
+
"limit": { "context": 200000, "output": 64000 }
|
|
90
|
+
},
|
|
91
|
+
"antigravity-claude-sonnet-4-5-thinking-high": {
|
|
92
|
+
"name": "Claude Sonnet 4.5 Think High (Antigravity)",
|
|
93
|
+
"limit": { "context": 200000, "output": 64000 }
|
|
94
|
+
},
|
|
95
|
+
"antigravity-claude-opus-4-5-thinking-low": {
|
|
96
|
+
"name": "Claude Opus 4.5 Think Low (Antigravity)",
|
|
97
|
+
"limit": { "context": 200000, "output": 64000 }
|
|
98
|
+
},
|
|
99
|
+
"antigravity-claude-opus-4-5-thinking-medium": {
|
|
100
|
+
"name": "Claude Opus 4.5 Think Medium (Antigravity)",
|
|
101
|
+
"limit": { "context": 200000, "output": 64000 }
|
|
102
|
+
},
|
|
103
|
+
"antigravity-claude-opus-4-5-thinking-high": {
|
|
104
|
+
"name": "Claude Opus 4.5 Think High (Antigravity)",
|
|
105
|
+
"limit": { "context": 200000, "output": 64000 }
|
|
106
|
+
},
|
|
107
|
+
"gemini-2.5-flash": {
|
|
108
|
+
"name": "Gemini 2.5 Flash (CLI)",
|
|
109
|
+
"limit": { "context": 1048576, "output": 65536 }
|
|
110
|
+
},
|
|
111
|
+
"gemini-2.5-pro": {
|
|
112
|
+
"name": "Gemini 2.5 Pro (CLI)",
|
|
113
|
+
"limit": { "context": 1048576, "output": 65536 }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## How It Works
|
|
121
|
+
|
|
122
|
+
Antigravity Proxy acts as a sophisticated bridge that translates OpenAI-formatted requests into Google's internal RPC protocols. It manages the complexities of authentication, session handling, and response streaming, allowing you to use high-tier models with your favorite tools.
|
|
123
|
+
|
|
124
|
+
### Account Selection Strategy
|
|
125
|
+
- **Hybrid (Default)**: Ranks accounts based on `(Health Score × 2) + (Idle Time × 0.1)`.
|
|
126
|
+
- **Sticky**: Keeps a client session tied to the same account for consistency.
|
|
127
|
+
- **Round-Robin**: Cycles through all available accounts evenly.
|
|
128
|
+
|
|
129
|
+
## Security Notes
|
|
130
|
+
- **Safety Filters**: Controlled via `SAFETY_THRESHOLD` (default: `BLOCK_NONE`).
|
|
131
|
+
- **Credentials**: OAuth tokens are stored locally in `antigravity-accounts.json`. Do not share or commit this file.
|
|
132
|
+
|
package/bun.lock
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "antigravity-llm-proxy",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"bun": "^1.0.0",
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@types/bun": "latest",
|
|
12
|
+
"typescript": "^5.9.3",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
"packages": {
|
|
17
|
+
"@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Mh78f4B+vNTOhFpI7RWHRWDqSKTnFXj/MauRx7I/GmNwEfw56sUx98gWRwXyF4lkW+9VNU+33wuw6E+M22W66w=="],
|
|
18
|
+
|
|
19
|
+
"@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-dFfKdSVz6Ois5zjEJboUC7igcYAVd+c//ajotd0L6WUQAKQrHMVq/+6LjOj/0zjC6VPFNGWzeF8erymNo1y0Jw=="],
|
|
20
|
+
|
|
21
|
+
"@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-bUND1aQoTCfIL+idALT7FWtuX59ltOIRo954c7p/JkESbSIJ01jY06BSNVbkGk8RQM19v/7qiqZZqi4NyO4Utw=="],
|
|
22
|
+
|
|
23
|
+
"@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-m03OtzEs+/RkWtk6tBf8yw0GW4P8ajfzTXnTt984tQBgkMubGQYUyUnFasWgr3mD2820LhkVjhYeBf1rkz/biQ=="],
|
|
24
|
+
|
|
25
|
+
"@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-QDxrROdUnC1d/uoilXtUeFHaLhYdRN7dRIzw/Iqj/vrrhnkA6VS+HYoCWtyyVvci/K+JrPmDwxOWlSRpmV4INA=="],
|
|
26
|
+
|
|
27
|
+
"@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-uttKQ/eIRVGc4uBtLRqmQqXGf57/dmQaF0AEd37RQNRRRd1P/VYnFMiMcVaot3HJ6IFjHjGtcPO9ekT49LxBYQ=="],
|
|
28
|
+
|
|
29
|
+
"@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-Jlb/AcrIFU3QDeR3EL4UVT1CIKqnLJDgbU+R0k/+NaSWMrBEpZV+gJJT5L1cmEKTNhU/d+c7hudxkjtqA7XXqA=="],
|
|
30
|
+
|
|
31
|
+
"@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-aK8fvkCosrHRG3CNdVqMom1C8Rj3XkqZp0ZFSBXgaXlKP22RkxlEE9tS7OmSq9yVgEk6euTB3dW4NFo/jlXqeg=="],
|
|
32
|
+
|
|
33
|
+
"@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-lySQQ7zJJsoa5hQH+PE5bQyQaTI8G2Erszhu4iQuDtsocwy3zSxjB6TxGWTd4HmetPl9aRvg3nb2KR8RVAd7ug=="],
|
|
34
|
+
|
|
35
|
+
"@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.7", "", { "os": "win32", "cpu": "x64" }, "sha512-3QdIGdSn3fkssCq/vPjtPLAQxo+eMUzcwJedn1c5mXDy1AoisjhoxhWnbVl8+uk+wt9N6JUPdISoe0N4OdwXfg=="],
|
|
36
|
+
|
|
37
|
+
"@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.7", "", { "os": "win32", "cpu": "x64" }, "sha512-wMgELfW5vFceh4qEOYb5iV5TjrjjnBJzE383ixA3kqGKzaubksSxNc11eZhS0ptcJ5a0UjN5hfbMh6sYoh+cRQ=="],
|
|
38
|
+
|
|
39
|
+
"@types/bun": ["@types/bun@1.3.7", "", { "dependencies": { "bun-types": "1.3.7" } }, "sha512-lmNuMda+Z9b7tmhA0tohwy8ZWFSnmQm1UDWXtH5r9F7wZCfkeO3Jx7wKQ1EOiKq43yHts7ky6r8SDJQWRNupkA=="],
|
|
40
|
+
|
|
41
|
+
"@types/node": ["@types/node@25.1.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA=="],
|
|
42
|
+
|
|
43
|
+
"bun": ["bun@1.3.7", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.7", "@oven/bun-darwin-x64": "1.3.7", "@oven/bun-darwin-x64-baseline": "1.3.7", "@oven/bun-linux-aarch64": "1.3.7", "@oven/bun-linux-aarch64-musl": "1.3.7", "@oven/bun-linux-x64": "1.3.7", "@oven/bun-linux-x64-baseline": "1.3.7", "@oven/bun-linux-x64-musl": "1.3.7", "@oven/bun-linux-x64-musl-baseline": "1.3.7", "@oven/bun-windows-x64": "1.3.7", "@oven/bun-windows-x64-baseline": "1.3.7" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-ha86NG8WiAXYR7eQw/9S+7V7Lo8KfD36XutWJNS1VndzaipWS0QIen5n3K9MT3PpP/sdGmmHjhkrU0sCM2lGGQ=="],
|
|
44
|
+
|
|
45
|
+
"bun-types": ["bun-types@1.3.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-qyschsA03Qz+gou+apt6HNl6HnI+sJJLL4wLDke4iugsE6584CMupOtTY1n+2YC9nGVrEKUlTs99jjRLKgWnjQ=="],
|
|
46
|
+
|
|
47
|
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
48
|
+
|
|
49
|
+
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
|
50
|
+
}
|
|
51
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "antigravity-proxy",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"module": "index.ts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "bun run src/server.ts",
|
|
8
|
+
"dev": "bun run --watch src/server.ts",
|
|
9
|
+
"lint:check": "bun x tsc --noEmit --skipLibCheck",
|
|
10
|
+
"test": "bun test tests/unit",
|
|
11
|
+
"test:functional": "bun test tests/functional",
|
|
12
|
+
"reset-accounts": "bun run src/scripts/reset-accounts.ts",
|
|
13
|
+
"lint:html": "bun -e \"const c=await Bun.file('src/frontend/index.html').text(); try { new Function(c.match(/<script>([\\s\\S]*?)<\\/script>/)[1]) } catch(e) { console.error(e); process.exit(1) }\""
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"bun": "^1.0.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/bun": "latest",
|
|
20
|
+
"typescript": "^5.9.3"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
|
|
3
|
+
const path = './antigravity-accounts.json';
|
|
4
|
+
const data = JSON.parse(readFileSync(path, 'utf8'));
|
|
5
|
+
|
|
6
|
+
for (const acc of data.accounts) {
|
|
7
|
+
acc.healthScore = 100;
|
|
8
|
+
acc.consecutiveFailures = 0;
|
|
9
|
+
acc.cooldowns = {};
|
|
10
|
+
acc.modelScores = {};
|
|
11
|
+
acc.history = [];
|
|
12
|
+
acc.quota = [];
|
|
13
|
+
delete acc.challenge;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
writeFileSync(path, JSON.stringify(data, null, 2));
|
|
17
|
+
console.log("Reset all accounts successfully.");
|
|
Binary file
|
package/src/api/quota.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { getImpersonationHeaders, generateFingerprint } from "../utils/headers";
|
|
2
|
+
import { type AntigravityAccount } from "../auth/types";
|
|
3
|
+
import { getAccounts, saveAccounts } from "../auth/manager";
|
|
4
|
+
import { refreshAccessToken } from "../auth/oauth";
|
|
5
|
+
|
|
6
|
+
export async function fetchQuota(account: AntigravityAccount, retry = true): Promise<AntigravityAccount['quota'] | null> {
|
|
7
|
+
if (!account.projectId || !account.accessToken) return null;
|
|
8
|
+
|
|
9
|
+
if (!account.fingerprint || !account.fingerprint.clientMetadata?.sqmId) {
|
|
10
|
+
account.fingerprint = generateFingerprint(account.email);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const res = await fetch(`https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels`, {
|
|
15
|
+
method: "POST",
|
|
16
|
+
headers: {
|
|
17
|
+
...getImpersonationHeaders(account.accessToken, account.fingerprint),
|
|
18
|
+
"User-Agent": "antigravity",
|
|
19
|
+
},
|
|
20
|
+
body: JSON.stringify({
|
|
21
|
+
project: account.projectId
|
|
22
|
+
})
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (res.status === 401 && retry) {
|
|
26
|
+
console.log(`Quota fetch 401 for ${account.email}, refreshing token...`);
|
|
27
|
+
try {
|
|
28
|
+
const tokens = await refreshAccessToken(account.refreshToken);
|
|
29
|
+
account.accessToken = tokens.access_token;
|
|
30
|
+
account.expiresAt = Date.now() + (tokens.expires_in * 1000);
|
|
31
|
+
await saveAccounts(getAccounts());
|
|
32
|
+
return fetchQuota(account, false); // Retry once
|
|
33
|
+
} catch (e) {
|
|
34
|
+
console.error(`Failed to refresh token for ${account.email} during quota fetch`, e);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
console.error(`Quota fetch failed for ${account.email}: ${res.status}`);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return parseQuotaResponse(await res.json());
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.error(`Error fetching quota for ${account.email}`, e);
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getNextMidnightPT(): string {
|
|
52
|
+
const now = new Date();
|
|
53
|
+
const ptDateStr = now.toLocaleString("en-US", { timeZone: "America/Los_Angeles" });
|
|
54
|
+
const ptDate = new Date(ptDateStr);
|
|
55
|
+
|
|
56
|
+
const midnightPT = new Date(ptDate);
|
|
57
|
+
midnightPT.setHours(24, 0, 0, 0);
|
|
58
|
+
|
|
59
|
+
const diffMs = midnightPT.getTime() - ptDate.getTime();
|
|
60
|
+
return new Date(now.getTime() + diffMs).toISOString();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const supportedModelsCache: Set<string> = new Set();
|
|
64
|
+
|
|
65
|
+
function parseQuotaResponse(data: any): AntigravityAccount['quota'] | null {
|
|
66
|
+
// Handle both array and map formats
|
|
67
|
+
let rawModels = data.availableModels || data.models || [];
|
|
68
|
+
if (!Array.isArray(rawModels) && typeof rawModels === 'object') {
|
|
69
|
+
rawModels = Object.values(rawModels);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const groups = new Map<string, any>();
|
|
73
|
+
|
|
74
|
+
for (const m of rawModels) {
|
|
75
|
+
if (!m.quotaInfo) continue;
|
|
76
|
+
|
|
77
|
+
const label = m.displayMetadata?.label || m.displayName || m.model?.name || "Unknown";
|
|
78
|
+
const lowerLabel = label.toLowerCase();
|
|
79
|
+
|
|
80
|
+
// Skip unknown or placeholder models
|
|
81
|
+
if (label === "Unknown" || lowerLabel === "unknown") continue;
|
|
82
|
+
|
|
83
|
+
// Cache supported model ID/Name
|
|
84
|
+
if (m.model?.name) {
|
|
85
|
+
const modelName = m.model.name.replace("models/", "");
|
|
86
|
+
supportedModelsCache.add(modelName);
|
|
87
|
+
} else if (label !== "Unknown") {
|
|
88
|
+
supportedModelsCache.add(label);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const allowedPatterns = [
|
|
92
|
+
"Claude",
|
|
93
|
+
"Anthropic",
|
|
94
|
+
"GPT",
|
|
95
|
+
"Gemini",
|
|
96
|
+
"chat",
|
|
97
|
+
"tab_flash",
|
|
98
|
+
"MODEL_PLACEHOLDER"
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
const isAllowed = allowedPatterns.some(pattern => label.includes(pattern));
|
|
102
|
+
|
|
103
|
+
if (!isAllowed) continue;
|
|
104
|
+
|
|
105
|
+
const remainingFraction = m.quotaInfo.remainingFraction ?? 0;
|
|
106
|
+
|
|
107
|
+
const limitName = m.quotaInfo.limitName || label;
|
|
108
|
+
|
|
109
|
+
if (groups.has(limitName)) {
|
|
110
|
+
const group = groups.get(limitName);
|
|
111
|
+
// Append label if not already present
|
|
112
|
+
if (!group.labels.includes(label)) {
|
|
113
|
+
group.labels.push(label);
|
|
114
|
+
group.labels.sort();
|
|
115
|
+
group.groupName = group.labels.join(" / ");
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
let resetTime = m.quotaInfo.quotaResetTime ||
|
|
119
|
+
m.quotaResetTime ||
|
|
120
|
+
m.quotaInfo.resetTime ||
|
|
121
|
+
m.resetTime ||
|
|
122
|
+
m.quotaInfo.nextResetTime ||
|
|
123
|
+
m.nextResetTime ||
|
|
124
|
+
m.quotaInfo.quota_reset_time ||
|
|
125
|
+
m.quota_reset_time;
|
|
126
|
+
|
|
127
|
+
if (typeof resetTime === 'number') {
|
|
128
|
+
if (resetTime < 10000000000) resetTime *= 1000;
|
|
129
|
+
resetTime = new Date(resetTime).toISOString();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (typeof resetTime === 'string' && resetTime.endsWith('s') && /^\d+/.test(resetTime)) {
|
|
133
|
+
const seconds = parseInt(resetTime, 10);
|
|
134
|
+
if (!isNaN(seconds)) {
|
|
135
|
+
resetTime = new Date(Date.now() + seconds * 1000).toISOString();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!resetTime || Number.isNaN(new Date(resetTime).getTime())) {
|
|
140
|
+
resetTime = getNextMidnightPT();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const diffMs = Math.max(0, new Date(resetTime).getTime() - Date.now());
|
|
144
|
+
const hours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
145
|
+
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
146
|
+
const resetIn = `${hours}h ${minutes}m`;
|
|
147
|
+
const pct = Math.round(remainingFraction * 100);
|
|
148
|
+
const quotaLeft = `${pct}%`;
|
|
149
|
+
|
|
150
|
+
groups.set(limitName, {
|
|
151
|
+
groupName: label,
|
|
152
|
+
labels: [label],
|
|
153
|
+
limit: m.quotaInfo.quotaLimit || "Unknown",
|
|
154
|
+
usage: m.quotaInfo.quotaUsage || "Unknown",
|
|
155
|
+
limitName: limitName,
|
|
156
|
+
remainingFraction: remainingFraction,
|
|
157
|
+
resetTime: resetTime,
|
|
158
|
+
quotaLeft,
|
|
159
|
+
resetIn
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const results = Array.from(groups.values()).map(g => {
|
|
165
|
+
const { labels, ...rest } = g;
|
|
166
|
+
return rest;
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Sort by name for consistency
|
|
170
|
+
results.sort((a, b) => a.groupName.localeCompare(b.groupName));
|
|
171
|
+
|
|
172
|
+
return results.length > 0 ? results : null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function refreshAllQuotas() {
|
|
176
|
+
const accounts = getAccounts();
|
|
177
|
+
|
|
178
|
+
await Promise.all(accounts.map(async (acc) => {
|
|
179
|
+
if (acc.projectId) {
|
|
180
|
+
const quota = await fetchQuota(acc);
|
|
181
|
+
if (quota) {
|
|
182
|
+
acc.quota = quota;
|
|
183
|
+
await saveAccounts(getAccounts());
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}));
|
|
187
|
+
}
|