pi-antigravity-rotator 1.14.0 → 2.0.0
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/CHANGELOG.md +28 -0
- package/README.md +65 -7
- package/package.json +1 -1
- package/src/account-store.ts +71 -14
- package/src/cli.ts +8 -0
- package/src/compat.ts +82 -11
- package/src/dashboard.ts +276 -9
- package/src/doctor.ts +97 -0
- package/src/index.ts +21 -37
- package/src/paths.ts +4 -0
- package/src/proxy.ts +93 -71
- package/src/rate-limit-parser.ts +126 -0
- package/src/rotator.ts +469 -51
- package/src/storage.ts +56 -0
- package/src/types.ts +98 -17
- package/src/validators.ts +19 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
### Added
|
|
6
|
+
- **Hybrid Routing Policy**: Added optional `routingPolicy: "hybrid"` with weighted selection across timer priority, quota, tier, health, local token bucket state, and distance.
|
|
7
|
+
- **Routing Inspector**: Added a dashboard modal that explains the currently selected route, candidate scores, and why each account was excluded for a model.
|
|
8
|
+
- **Rate Limit Parser Module**: Extracted robust retry parsing into `src/rate-limit-parser.ts` with support for `Retry-After`, `x-ratelimit-reset`, `quotaResetDelay`, `quotaResetTimeStamp`, `retryDelay`, and duration strings.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **Token Bucket Guardrail**: Added optional per-account token buckets to slow repeated reuse of the same account without changing the default v2.0 routing behavior.
|
|
12
|
+
- **Attention Needed Coverage**: The dashboard now surfaces unroutable models and token-bucket exhaustion alongside existing security, cooldown, disabled, flagged, and error alerts.
|
|
13
|
+
- **Compat Hardening**: Added coverage for `cache_control` stripping, schema forwarding, missing-signature tool history, and empty SSE parsing.
|
|
14
|
+
|
|
15
|
+
## [2.0.0] - 2026-05-20
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **Admin Config APIs**: Added `GET /api/config`, `PUT /api/config`, `GET /api/config/export`, and `POST /api/config/import` for validated runtime config management.
|
|
19
|
+
- **Dashboard Config Editor**: Added an embedded JSON editor with load/save/import/export controls and hosted login access.
|
|
20
|
+
- **Docker Deployment**: Added `Dockerfile`, `docker-compose.yml`, and `.dockerignore` for headless deployments with persistent `/data`.
|
|
21
|
+
- **Doctor Command**: Added `pi-antigravity-rotator doctor` to validate config, inspect backups, and report missing admin auth.
|
|
22
|
+
- **Gemini-Compatible Discovery**: Added `/v1beta/models` and a minimal Gemini-style `generateContent` route family.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- **Version 2.0**: Bumped package version to `2.0.0` on branch `v2.0`.
|
|
26
|
+
- **Persistence Hardening**: Config, state, and token usage now write atomically with timestamped backups.
|
|
27
|
+
- **Routing Metadata**: Added optional account `tier` plus runtime `healthScore` as timer-first tie-breakers.
|
|
28
|
+
- **Security Visibility**: Startup logs, `/api/status`, and the dashboard now warn when `PI_ROTATOR_ADMIN_TOKEN` is missing.
|
|
29
|
+
|
|
30
|
+
### Migration
|
|
31
|
+
- Existing `accounts.json` stays compatible. New defaults are `bindHost: "0.0.0.0"`, `routingPolicy: "timer-first"`, and `accounts[].tier: "unknown"`.
|
|
32
|
+
|
|
5
33
|
## [1.14.0] - 2026-05-19
|
|
6
34
|
|
|
7
35
|
### Added
|
package/README.md
CHANGED
|
@@ -1,7 +1,40 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
1
3
|
# Pi Antigravity Rotator
|
|
2
4
|
|
|
3
5
|
Multi-account rotation proxy for Google Antigravity. Distributes API usage across multiple Google accounts with per-model routing, real-time quota tracking, automatic token management, and infringement detection.
|
|
4
6
|
|
|
7
|
+
> **⚠️ WARNING:** Using this proxy may put connected Google accounts at risk of Terms of Service enforcement, including restriction, suspension, or permanent bans. Use at your own risk.
|
|
8
|
+
|
|
9
|
+
<details>
|
|
10
|
+
<summary><strong>⚠️ Terms of Service Warning — Read Before Installing</strong></summary>
|
|
11
|
+
|
|
12
|
+
> [!CAUTION]
|
|
13
|
+
> This is an unofficial tool and is not endorsed by Google. Routing traffic through this proxy may violate Google's Terms of Service or trigger automated abuse or policy enforcement systems.
|
|
14
|
+
>
|
|
15
|
+
> **By using this proxy, you acknowledge:**
|
|
16
|
+
> - Your account may be restricted, suspended, shadow-banned, or permanently banned
|
|
17
|
+
> - Multi-account rotation and proxying can increase account risk compared to normal interactive usage
|
|
18
|
+
> - You assume all responsibility for the accounts and traffic routed through this tool
|
|
19
|
+
>
|
|
20
|
+
> **Recommendation:** Do not use your primary Google account. Prefer disposable or lower-risk accounts, and keep account exposure conservative.
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
## Support Me
|
|
25
|
+
|
|
26
|
+
If this tool has helped you optimize your API usage and save costs, consider supporting its development!
|
|
27
|
+
|
|
28
|
+
<a href="https://ko-fi.com/tuxevil" target="_blank"><img src="https://storage.ko-fi.com/cdn/kofi2.png?v=3" height="36" alt="Buy Me a Coffee at ko-fi.com" /></a>
|
|
29
|
+
|
|
30
|
+
## v2.0 Highlights
|
|
31
|
+
|
|
32
|
+
- Full dashboard config editor with import/export.
|
|
33
|
+
- Official Docker and compose deployment.
|
|
34
|
+
- `pi-antigravity-rotator doctor` for config/state validation.
|
|
35
|
+
- Optional account `tier` metadata and runtime `healthScore`.
|
|
36
|
+
- Strong security warnings when admin routes are open without `PI_ROTATOR_ADMIN_TOKEN`.
|
|
37
|
+
|
|
5
38
|
## Features
|
|
6
39
|
|
|
7
40
|
- **Per-model routing** -- Each model (Gemini Pro, Flash, Claude) routes to its own active account independently. Multiple agents using different models won't interfere with each other.
|
|
@@ -49,6 +82,14 @@ npm run login
|
|
|
49
82
|
npm start
|
|
50
83
|
```
|
|
51
84
|
|
|
85
|
+
### Option C: Docker
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
docker compose up -d
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The included compose file persists runtime data under `./docker-data` and sets `PI_ROTATOR_DIR=/data`.
|
|
92
|
+
|
|
52
93
|
## Adding Accounts
|
|
53
94
|
|
|
54
95
|
Run `npm run login` once per Google account:
|
|
@@ -89,6 +130,8 @@ If login fails at project discovery:
|
|
|
89
130
|
|
|
90
131
|
After starting the proxy, open `http://localhost:51200/dashboard` or `http://<your-server-ip>:51200/dashboard` from any machine on the same network (the proxy binds to `0.0.0.0`).
|
|
91
132
|
|
|
133
|
+
If `PI_ROTATOR_ADMIN_TOKEN` is unset, dashboard and `/api/*` access remains open for backwards compatibility. v2.0 now surfaces loud warnings about that state in startup logs, `/api/status`, and the dashboard itself.
|
|
134
|
+
|
|
92
135
|
The dashboard shows:
|
|
93
136
|
|
|
94
137
|
- **Top Status & Controls** -- Real-time routing state, uptime, requests, and PII masking toggle.
|
|
@@ -98,7 +141,8 @@ The dashboard shows:
|
|
|
98
141
|
- **Quota Forecast** -- Predictive modeling showing when each model's quota will run out based on the current requests/hour burn rate.
|
|
99
142
|
- **Searchable Request Log** -- Live feed of the last 200 requests with exact timestamps, models, masked accounts, status codes, and latency.
|
|
100
143
|
- **Account Cards** -- Sorted by total quota. Shows status (`active`, `ready`, `cooldown`, `flagged`, `disabled`), quota bars with timers, and precise error messages.
|
|
101
|
-
- **Operator Panels** -- "Attention Needed" summaries for quarantined accounts and a real-time event feed of rotator actions.
|
|
144
|
+
- **Operator Panels** -- "Attention Needed" summaries for quarantined accounts, unroutable models, token-bucket pressure, and a real-time event feed of rotator actions.
|
|
145
|
+
- **Routing Inspector** -- On-demand modal showing the active routing policy, candidate scores, local token bucket state, and rejection reasons per model.
|
|
102
146
|
|
|
103
147
|

|
|
104
148
|
|
|
@@ -220,6 +264,8 @@ export PI_ROTATOR_DIR=/path/to/config
|
|
|
220
264
|
export PI_ROTATOR_QUOTA_USER_AGENT="antigravity/1.107.0 darwin/arm64"
|
|
221
265
|
# Optional: require this token for dashboard/API access. If unset, legacy open access is preserved.
|
|
222
266
|
export PI_ROTATOR_ADMIN_TOKEN="change-me"
|
|
267
|
+
# Optional: bind the proxy to a safer local-only interface.
|
|
268
|
+
export PI_ROTATOR_BIND_HOST="127.0.0.1"
|
|
223
269
|
# Optional: max accepted proxy request body size in bytes. Default: 26214400 (25 MiB).
|
|
224
270
|
export PI_ROTATOR_MAX_BODY_BYTES=26214400
|
|
225
271
|
# Optional: log verbosity. One of debug, info, warn, error, silent. Default: info.
|
|
@@ -231,6 +277,24 @@ export PI_AI_ANTIGRAVITY_VERSION=1.107.0
|
|
|
231
277
|
pi-antigravity-rotator start --config-dir /path/to/config
|
|
232
278
|
```
|
|
233
279
|
|
|
280
|
+
New v2.0 config fields:
|
|
281
|
+
|
|
282
|
+
- `bindHost`: interface to bind on. Default: `0.0.0.0`.
|
|
283
|
+
- `routingPolicy`: current default is `timer-first`. Optional values now include `tier-first`, `quota-first`, and `hybrid`.
|
|
284
|
+
- `tokenBucketEnabled`: enables the local per-account request bucket used by `hybrid`. Default: `false`.
|
|
285
|
+
- `tokenBucketMaxTokens`: bucket capacity when enabled. Default: `50`.
|
|
286
|
+
- `tokenBucketRefillPerMinute`: refill speed when enabled. Default: `6`.
|
|
287
|
+
- `tokenBucketInitialTokens`: startup fill level when enabled. Default: `50`.
|
|
288
|
+
- `accounts[].tier`: optional `ultra`, `pro`, `free`, or `unknown`.
|
|
289
|
+
|
|
290
|
+
## Doctor
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
pi-antigravity-rotator doctor
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
This validates `accounts.json`, checks local state files, lists backups, and warns when admin auth is not configured.
|
|
297
|
+
|
|
234
298
|
`accounts.json` is created automatically by the login command.
|
|
235
299
|
Login now fails if Google does not return a project ID. No shared fallback.
|
|
236
300
|
|
|
@@ -463,9 +527,3 @@ export PI_ROTATOR_TELEMETRY=off
|
|
|
463
527
|
```
|
|
464
528
|
|
|
465
529
|
Or use any of: `PI_ROTATOR_TELEMETRY=false`, `PI_ROTATOR_TELEMETRY=0`.
|
|
466
|
-
|
|
467
|
-
## Support Me
|
|
468
|
-
|
|
469
|
-
If this tool has helped you optimize your API usage and save costs, consider supporting its development!
|
|
470
|
-
|
|
471
|
-
<a href="https://ko-fi.com/tuxevil" target="_blank"><img src="https://storage.ko-fi.com/cdn/kofi2.png?v=3" height="36" alt="Buy Me a Coffee at ko-fi.com" /></a>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-antigravity-rotator",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Multi-account rotation proxy for Google Antigravity with per-model routing, real-time quota tracking, and infringement detection",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/account-store.ts
CHANGED
|
@@ -1,24 +1,59 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { getAccountsPath } from "./paths.js";
|
|
5
5
|
import type { AccountConfig, Config } from "./types.js";
|
|
6
|
+
import { backupFile, readJsonFile, writeJsonFileAtomic } from "./storage.js";
|
|
7
|
+
import { formatValidationErrors, validateConfig } from "./validators.js";
|
|
6
8
|
|
|
7
9
|
const ACCOUNTS_FILE = getAccountsPath();
|
|
8
10
|
const PI_DIR = join(homedir(), ".pi", "agent");
|
|
9
11
|
const PI_MODELS_FILE = join(PI_DIR, "models.json");
|
|
10
12
|
const PI_AUTH_FILE = join(PI_DIR, "auth.json");
|
|
13
|
+
const TOKEN_USAGE_FILE = join(join(ACCOUNTS_FILE, ".."), "token-usage.json");
|
|
11
14
|
|
|
12
|
-
export function
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
// Corrupted, start fresh
|
|
18
|
-
}
|
|
19
|
-
}
|
|
15
|
+
export function getTokenUsagePath(): string {
|
|
16
|
+
return TOKEN_USAGE_FILE;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function applyConfigDefaults(config: Config): Config {
|
|
20
20
|
return {
|
|
21
|
+
proxyPort: config.proxyPort || 51200,
|
|
22
|
+
bindHost: config.bindHost || process.env.PI_ROTATOR_BIND_HOST || "0.0.0.0",
|
|
23
|
+
routingPolicy: config.routingPolicy || "timer-first",
|
|
24
|
+
requestsPerRotation: config.requestsPerRotation || 5,
|
|
25
|
+
rotateOnQuotaDrop: config.rotateOnQuotaDrop ?? 20,
|
|
26
|
+
quotaPollIntervalMs: config.quotaPollIntervalMs || 300000,
|
|
27
|
+
maxConcurrentRequestsPerAccount: config.maxConcurrentRequestsPerAccount ?? 1,
|
|
28
|
+
maxConcurrentRequestsPerProjectModel: config.maxConcurrentRequestsPerProjectModel ?? 1,
|
|
29
|
+
projectCircuitBreaker429Threshold: config.projectCircuitBreaker429Threshold ?? 3,
|
|
30
|
+
projectCircuitBreakerWindowMs: config.projectCircuitBreakerWindowMs ?? 10 * 60 * 1000,
|
|
31
|
+
projectCircuitBreakerCooldownMs: config.projectCircuitBreakerCooldownMs ?? 60 * 60 * 1000,
|
|
32
|
+
modelCircuitBreaker429Threshold: config.modelCircuitBreaker429Threshold ?? 3,
|
|
33
|
+
modelCircuitBreakerCooldownMs: config.modelCircuitBreakerCooldownMs ?? 6 * 60 * 60 * 1000,
|
|
34
|
+
dailyAccountSlowRequests: config.dailyAccountSlowRequests ?? 250,
|
|
35
|
+
dailyAccountStopRequests: config.dailyAccountStopRequests ?? 350,
|
|
36
|
+
dailyProjectSlowRequests: config.dailyProjectSlowRequests ?? 900,
|
|
37
|
+
dailyProjectStopRequests: config.dailyProjectStopRequests ?? 1200,
|
|
38
|
+
slowModeJitterMinMs: config.slowModeJitterMinMs ?? 8_000,
|
|
39
|
+
slowModeJitterMaxMs: config.slowModeJitterMaxMs ?? 25_000,
|
|
40
|
+
protectivePauseMs: config.protectivePauseMs ?? 21600000,
|
|
41
|
+
useRequestCountRotationWhenQuotaUnknownOnly: config.useRequestCountRotationWhenQuotaUnknownOnly ?? true,
|
|
42
|
+
tokenBucketEnabled: config.tokenBucketEnabled ?? false,
|
|
43
|
+
tokenBucketMaxTokens: config.tokenBucketMaxTokens ?? 50,
|
|
44
|
+
tokenBucketRefillPerMinute: config.tokenBucketRefillPerMinute ?? 6,
|
|
45
|
+
tokenBucketInitialTokens: config.tokenBucketInitialTokens ?? (config.tokenBucketMaxTokens ?? 50),
|
|
46
|
+
accounts: config.accounts.map((account) => ({
|
|
47
|
+
...account,
|
|
48
|
+
tier: account.tier || "unknown",
|
|
49
|
+
})),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getDefaultConfig(): Config {
|
|
54
|
+
return applyConfigDefaults({
|
|
21
55
|
proxyPort: 51200,
|
|
56
|
+
accounts: [],
|
|
22
57
|
requestsPerRotation: 5,
|
|
23
58
|
rotateOnQuotaDrop: 20,
|
|
24
59
|
quotaPollIntervalMs: 300000,
|
|
@@ -37,12 +72,34 @@ export function loadOrCreateAccountsConfig(): Config {
|
|
|
37
72
|
slowModeJitterMaxMs: 25_000,
|
|
38
73
|
protectivePauseMs: 21600000,
|
|
39
74
|
useRequestCountRotationWhenQuotaUnknownOnly: true,
|
|
40
|
-
|
|
41
|
-
|
|
75
|
+
tokenBucketEnabled: false,
|
|
76
|
+
tokenBucketMaxTokens: 50,
|
|
77
|
+
tokenBucketRefillPerMinute: 6,
|
|
78
|
+
tokenBucketInitialTokens: 50,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function loadConfigFromDisk(): Config {
|
|
83
|
+
const parsed = readJsonFile<unknown>(ACCOUNTS_FILE);
|
|
84
|
+
if (parsed === null) return getDefaultConfig();
|
|
85
|
+
const validation = validateConfig(parsed);
|
|
86
|
+
if (!validation.ok || !validation.value) {
|
|
87
|
+
throw new Error(formatValidationErrors(validation.errors));
|
|
88
|
+
}
|
|
89
|
+
return applyConfigDefaults(validation.value);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function loadOrCreateAccountsConfig(): Config {
|
|
93
|
+
try {
|
|
94
|
+
return loadConfigFromDisk();
|
|
95
|
+
} catch {
|
|
96
|
+
return getDefaultConfig();
|
|
97
|
+
}
|
|
42
98
|
}
|
|
43
99
|
|
|
44
100
|
export function saveAccountsConfig(config: Config): void {
|
|
45
|
-
|
|
101
|
+
backupFile(ACCOUNTS_FILE, "accounts");
|
|
102
|
+
writeJsonFileAtomic(ACCOUNTS_FILE, applyConfigDefaults(config));
|
|
46
103
|
}
|
|
47
104
|
|
|
48
105
|
export function addAccountToConfig(entry: AccountConfig): { isNew: boolean } {
|
|
@@ -83,7 +140,7 @@ export function ensurePiModelsConfig(): void {
|
|
|
83
140
|
providers["google-antigravity"] = antigravity;
|
|
84
141
|
models.providers = providers;
|
|
85
142
|
|
|
86
|
-
|
|
143
|
+
writeJsonFileAtomic(PI_MODELS_FILE, models);
|
|
87
144
|
console.log(` Updated ${PI_MODELS_FILE}`);
|
|
88
145
|
}
|
|
89
146
|
|
|
@@ -112,6 +169,6 @@ export function ensurePiAuthConfig(): void {
|
|
|
112
169
|
projectId: "proxy-managed",
|
|
113
170
|
};
|
|
114
171
|
|
|
115
|
-
|
|
172
|
+
writeJsonFileAtomic(PI_AUTH_FILE, auth);
|
|
116
173
|
console.log(` Updated ${PI_AUTH_FILE}`);
|
|
117
174
|
}
|
package/src/cli.ts
CHANGED
|
@@ -38,6 +38,13 @@ switch (command) {
|
|
|
38
38
|
}
|
|
39
39
|
break;
|
|
40
40
|
}
|
|
41
|
+
case "doctor": {
|
|
42
|
+
const { printDoctorReport, runDoctor } = await import("./doctor.js");
|
|
43
|
+
const result = runDoctor();
|
|
44
|
+
printDoctorReport(result);
|
|
45
|
+
process.exit(result.ok ? 0 : 1);
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
41
48
|
default:
|
|
42
49
|
console.log("Pi Antigravity Rotator");
|
|
43
50
|
console.log();
|
|
@@ -45,6 +52,7 @@ switch (command) {
|
|
|
45
52
|
console.log(" pi-antigravity-rotator start Start the proxy (default)");
|
|
46
53
|
console.log(" pi-antigravity-rotator login Add a new Google account");
|
|
47
54
|
console.log(" pi-antigravity-rotator status Show account status (JSON)");
|
|
55
|
+
console.log(" pi-antigravity-rotator doctor Validate config and local state");
|
|
48
56
|
console.log();
|
|
49
57
|
console.log("Options:");
|
|
50
58
|
console.log(" --config-dir <path> Config directory (default: ~/.pi-antigravity-rotator/)");
|
package/src/compat.ts
CHANGED
|
@@ -1190,20 +1190,21 @@ async function completeViaRotator(
|
|
|
1190
1190
|
}
|
|
1191
1191
|
|
|
1192
1192
|
|
|
1193
|
+
const MODEL_CATALOG = [
|
|
1194
|
+
{ id: "gemini-3.5-flash-low", family: "gemini-3.5-flash", ctx: 1048576, quotaPool: "gemini-3.5-flash", multimodal: true, tools: true },
|
|
1195
|
+
{ id: "gemini-3.5-flash-high", family: "gemini-3.5-flash", ctx: 1048576, quotaPool: "gemini-3.5-flash", multimodal: true, tools: true },
|
|
1196
|
+
{ id: "gemini-3-flash", family: "gemini-3.5-flash", ctx: 1048576, quotaPool: "gemini-3.5-flash", multimodal: true, tools: true },
|
|
1197
|
+
{ id: "gemini-3.1-pro-low", family: "gemini-3.1-pro", ctx: 1048576, quotaPool: "gemini-3.1-pro", multimodal: true, tools: true },
|
|
1198
|
+
{ id: "gemini-3.1-pro-high", family: "gemini-3.1-pro", ctx: 1048576, quotaPool: "gemini-3.1-pro", multimodal: true, tools: true },
|
|
1199
|
+
{ id: "claude-sonnet-4-6", family: "claude", ctx: 500000, quotaPool: "claude-opus-4-6-thinking", multimodal: true, tools: true },
|
|
1200
|
+
{ id: "claude-opus-4-6-thinking", family: "claude", ctx: 500000, quotaPool: "claude-opus-4-6-thinking", multimodal: true, tools: true },
|
|
1201
|
+
{ id: "gpt-oss-120b-medium", family: "gpt-oss", ctx: 131072, quotaPool: "claude-opus-4-6-thinking", multimodal: false, tools: true },
|
|
1202
|
+
] as const;
|
|
1203
|
+
|
|
1193
1204
|
export function serveOpenAIModels(res: ServerResponse): void {
|
|
1194
|
-
const models = [
|
|
1195
|
-
{ id: "gemini-3.5-flash-low", ctx: 1048576 },
|
|
1196
|
-
{ id: "gemini-3.5-flash-high", ctx: 1048576 },
|
|
1197
|
-
{ id: "gemini-3-flash", ctx: 1048576 },
|
|
1198
|
-
{ id: "gemini-3.1-pro-low", ctx: 1048576 },
|
|
1199
|
-
{ id: "gemini-3.1-pro-high", ctx: 1048576 },
|
|
1200
|
-
{ id: "claude-sonnet-4-6", ctx: 500000 },
|
|
1201
|
-
{ id: "claude-opus-4-6-thinking", ctx: 500000 },
|
|
1202
|
-
{ id: "gpt-oss-120b-medium", ctx: 131072 },
|
|
1203
|
-
];
|
|
1204
1205
|
writeJson(res, 200, {
|
|
1205
1206
|
object: "list",
|
|
1206
|
-
data:
|
|
1207
|
+
data: MODEL_CATALOG.map(({ id, ctx, family, quotaPool, multimodal, tools }) => ({
|
|
1207
1208
|
id,
|
|
1208
1209
|
object: "model",
|
|
1209
1210
|
created: 0,
|
|
@@ -1212,11 +1213,81 @@ export function serveOpenAIModels(res: ServerResponse): void {
|
|
|
1212
1213
|
max_model_len: ctx,
|
|
1213
1214
|
meta: {
|
|
1214
1215
|
context_length: ctx,
|
|
1216
|
+
family,
|
|
1217
|
+
quota_pool: quotaPool,
|
|
1218
|
+
multimodal,
|
|
1219
|
+
tool_calling: tools,
|
|
1215
1220
|
}
|
|
1216
1221
|
})),
|
|
1217
1222
|
});
|
|
1218
1223
|
}
|
|
1219
1224
|
|
|
1225
|
+
export function serveGeminiModels(res: ServerResponse): void {
|
|
1226
|
+
writeJson(res, 200, {
|
|
1227
|
+
models: MODEL_CATALOG.map(({ id, ctx, family, quotaPool, multimodal, tools }) => ({
|
|
1228
|
+
name: `models/${id}`,
|
|
1229
|
+
baseModelId: family,
|
|
1230
|
+
version: "v2.0",
|
|
1231
|
+
displayName: id,
|
|
1232
|
+
description: `Pi Antigravity Rotator Gemini-compatible model entry for ${id}`,
|
|
1233
|
+
inputTokenLimit: ctx,
|
|
1234
|
+
outputTokenLimit: ctx,
|
|
1235
|
+
supportedGenerationMethods: ["generateContent", "streamGenerateContent"],
|
|
1236
|
+
capabilities: {
|
|
1237
|
+
tools,
|
|
1238
|
+
multimodal,
|
|
1239
|
+
quotaPool,
|
|
1240
|
+
},
|
|
1241
|
+
})),
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
export async function handleGeminiGenerateContent(req: IncomingMessage, res: ServerResponse, rotator: AccountRotator): Promise<void> {
|
|
1246
|
+
let parsed: unknown;
|
|
1247
|
+
try {
|
|
1248
|
+
parsed = await readJsonBody(req);
|
|
1249
|
+
} catch (err) {
|
|
1250
|
+
if (err instanceof PayloadTooLargeError) return writeJson(res, 413, { error: { message: "Payload too large", status: "INVALID_ARGUMENT" } });
|
|
1251
|
+
return writeJson(res, 400, { error: { message: "Invalid JSON body", status: "INVALID_ARGUMENT" } });
|
|
1252
|
+
}
|
|
1253
|
+
if (!isRecord(parsed)) return writeJson(res, 400, { error: { message: "Body must be an object", status: "INVALID_ARGUMENT" } });
|
|
1254
|
+
|
|
1255
|
+
const pathname = new URL(req.url || "/", "http://localhost").pathname;
|
|
1256
|
+
const modelToken = pathname.match(/\/v1beta\/models\/(.+):(generateContent|streamGenerateContent)$/)?.[1];
|
|
1257
|
+
const model = modelToken ? decodeURIComponent(modelToken).replace(/^models\//, "") : null;
|
|
1258
|
+
if (!model) return writeJson(res, 400, { error: { message: "Model path is required", status: "INVALID_ARGUMENT" } });
|
|
1259
|
+
|
|
1260
|
+
const body: RequestBody = {
|
|
1261
|
+
model,
|
|
1262
|
+
project: "",
|
|
1263
|
+
request: {
|
|
1264
|
+
contents: Array.isArray(parsed.contents) ? parsed.contents : [],
|
|
1265
|
+
systemInstruction: parsed.systemInstruction,
|
|
1266
|
+
generationConfig: parsed.generationConfig,
|
|
1267
|
+
tools: parsed.tools,
|
|
1268
|
+
},
|
|
1269
|
+
};
|
|
1270
|
+
const result = await completeViaRotator(req, res, rotator, body, "none");
|
|
1271
|
+
if (result.status !== 200) {
|
|
1272
|
+
return writeJson(res, result.status, { error: { message: result.errorText || "Upstream error", status: "UPSTREAM_ERROR" } });
|
|
1273
|
+
}
|
|
1274
|
+
if (result.streamed) return;
|
|
1275
|
+
writeJson(res, 200, {
|
|
1276
|
+
candidates: [{
|
|
1277
|
+
content: {
|
|
1278
|
+
role: "model",
|
|
1279
|
+
parts: [{ text: result.completion.text }],
|
|
1280
|
+
},
|
|
1281
|
+
finishReason: "STOP",
|
|
1282
|
+
}],
|
|
1283
|
+
usageMetadata: {
|
|
1284
|
+
promptTokenCount: result.completion.inputTokens,
|
|
1285
|
+
candidatesTokenCount: result.completion.outputTokens,
|
|
1286
|
+
totalTokenCount: result.completion.inputTokens + result.completion.outputTokens,
|
|
1287
|
+
},
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1220
1291
|
export async function handleOpenAIChatCompletions(req: IncomingMessage, res: ServerResponse, rotator: AccountRotator): Promise<void> {
|
|
1221
1292
|
let parsed: unknown;
|
|
1222
1293
|
try {
|