From 8f2fa57280cbe584bcce29fc1472e1b394c34813 Mon Sep 17 00:00:00 2001 From: Aaron Sewall Date: Wed, 28 Jan 2026 03:45:19 -0500 Subject: [PATCH 1/6] feat: add HTTP/HTTPS proxy support for all API traffic - Add fetchWithProxy() utility using https-proxy-agent - Thread proxyUrl through all fetch call sites (token refresh, project context, quota, search) - Save proxy URL from ANTIGRAVITY_LOGIN_PROXY env during login - Store proxyUrl per-account in antigravity-accounts.json - Add docs/PROXY.md with configuration guide - Update README with proxy quick start section Supports: http://host:port, http://user:pass@host:port, https://host:port Note: SOCKS5 proxies are NOT supported --- README.md | 38 +++++++++++ docs/PROXY.md | 140 +++++++++++++++++++++++++++++++++++++++ package-lock.json | 39 ++++++----- package.json | 1 + src/antigravity/oauth.ts | 17 +++-- src/plugin.ts | 20 ++++-- src/plugin/accounts.ts | 1 + src/plugin/project.ts | 14 ++-- src/plugin/proxy.test.ts | 131 ++++++++++++++++++++++++++++++++++++ src/plugin/proxy.ts | 65 ++++++++++++++++++ src/plugin/quota.ts | 9 ++- src/plugin/search.ts | 6 +- src/plugin/storage.ts | 1 + src/plugin/token.ts | 28 +++++--- 14 files changed, 465 insertions(+), 45 deletions(-) create mode 100644 docs/PROXY.md create mode 100644 src/plugin/proxy.test.ts create mode 100644 src/plugin/proxy.ts diff --git a/README.md b/README.md index aeabd9a1..bfc1329a 100644 --- a/README.md +++ b/README.md @@ -238,6 +238,44 @@ For details on load balancing, dual quota pools, and account storage, see [docs/ --- +## Proxy Configuration + +Use HTTP/HTTPS proxies for all API traffic (OAuth, token refresh, API calls). Useful for corporate environments or anti-detection setups. + +### Quick Start + +```bash +# Login with proxy - automatically saves to account +ANTIGRAVITY_LOGIN_PROXY=http://proxy.example.com:8080 opencode auth login + +# All future API calls from this account use the proxy +opencode run "Hello" --model=google/antigravity-claude-sonnet-4-5 +``` + +### How It Works + +1. **OAuth Login**: Set `ANTIGRAVITY_LOGIN_PROXY` during `opencode auth login` +2. **Auto-Save**: Proxy URL is saved to `~/.config/opencode/antigravity-accounts.json` +3. **All Traffic**: All API calls (token refresh, project discovery, API requests) use the proxy + +### Supported Formats + +- `http://proxy.example.com:8080` - Unauthenticated +- `http://user:pass@proxy.example.com:8080` - Authenticated +- `https://proxy.example.com:443` - TLS proxy + +**Note**: SOCKS5 proxies are NOT supported. Use HTTP/HTTPS only. + +### Security Warning + +⚠️ Proxy credentials are stored in **plaintext** in `antigravity-accounts.json` (same as OAuth tokens). Use filesystem permissions to protect this file. + +### Advanced: Manual Configuration + +See [EXAMPLE_PROXY_CONFIG.md](EXAMPLE_PROXY_CONFIG.md) for manual JSON editing and troubleshooting. + +--- + ## Troubleshoot > **Quick Reset**: Most issues can be resolved by deleting `~/.config/opencode/antigravity-accounts.json` and running `opencode auth login` again. diff --git a/docs/PROXY.md b/docs/PROXY.md new file mode 100644 index 00000000..cef6df45 --- /dev/null +++ b/docs/PROXY.md @@ -0,0 +1,140 @@ +# Proxy Configuration Example + +## OAuth Login Proxy (Recommended) + +**New in v1.3.4**: Use the `ANTIGRAVITY_LOGIN_PROXY` environment variable to configure proxy during account login. The proxy URL will be saved to the account automatically. + +```bash +# Login with proxy +ANTIGRAVITY_LOGIN_PROXY=http://proxy.example.com:8080 opencode auth login + +# Login with authenticated proxy +ANTIGRAVITY_LOGIN_PROXY=http://user:pass@proxy.example.com:8080 opencode auth login +``` + +The proxy URL is saved to `~/.config/opencode/antigravity-accounts.json` and used for: +- All OAuth token refreshes +- Project discovery API calls +- Gemini/Claude API requests +- Google Search tool requests +- Quota check requests + +**Benefits:** +- No manual JSON editing required +- Proxy is automatically associated with the account +- All future API calls from this account use the configured proxy + +## Manual Proxy Configuration (Alternative) + +Alternatively, you can manually edit your `~/.config/opencode/antigravity-accounts.json` file and add `proxyUrl` fields to each account: + +```json +{ + "version": 3, + "accounts": [ + { + "email": "user1@gmail.com", + "refreshToken": "1//0abc...", + "projectId": "my-project-1", + "proxyUrl": "http://user1:password1@proxy1.example.com:8080", + "addedAt": 1704067200000, + "lastUsed": 1704153600000 + }, + { + "email": "user2@gmail.com", + "refreshToken": "1//0def...", + "projectId": "my-project-2", + "proxyUrl": "http://user2:password2@proxy2.example.com:8080", + "addedAt": 1704067300000, + "lastUsed": 1704153700000 + }, + { + "email": "user3@gmail.com", + "refreshToken": "1//0ghi...", + "projectId": "my-project-3", + "proxyUrl": "https://proxy3.example.com:443", + "addedAt": 1704067400000, + "lastUsed": 1704153800000 + } + ], + "activeIndex": 0, + "activeIndexByFamily": { + "claude": 0, + "gemini": 1 + } +} +``` + +## Supported Proxy Formats + +- **HTTP**: `http://[user:pass@]host:port` +- **HTTPS**: `https://[user:pass@]host:port` + +**Note**: SOCKS5 proxies are NOT currently supported. Use HTTP/HTTPS proxies only. + +## Anti-Detection Features + +1. **Hard Fail**: If proxy fails, request fails immediately - NO direct fallback +2. **All Traffic**: Token refresh, project discovery, and API calls all use same proxy +3. **Per-Account Isolation**: Each account uses its own proxy → unique IP per account +4. **Connection Pooling**: Proxy connections are cached and reused for performance + +## Important Notes + +- **Credentials**: Proxy passwords stored in plaintext (same security level as OAuth tokens) +- **Backward Compatible**: Accounts without `proxyUrl` work unchanged (direct connection) +- **Restart Required**: Changes to `antigravity-accounts.json` require OpenCode restart +- **Error Handling**: Failed proxy connections mark account "cooling down" for 30 seconds + +## Testing Your Proxies + +Before adding proxies to all accounts, test one account first: + +```bash +# Test proxy during login +ANTIGRAVITY_LOGIN_PROXY=http://localhost:8080 opencode auth login + +# Watch your proxy logs - you should see: +# - POST https://oauth2.googleapis.com/token +# - GET https://www.googleapis.com/oauth2/v1/userinfo +# - POST to Antigravity loadCodeAssist endpoints + +# Make a test request +opencode run "Hello" --model=google/antigravity-claude-sonnet-4-5 + +# Proxy logs should show API traffic +``` + +## Troubleshooting + +### Proxy Connection Failed + +``` +Error: Failed to create proxy agent for http://proxy:8080: connect ECONNREFUSED +``` + +**Solutions:** +- Check proxy URL format: `http://host:port` (not `https://` unless TLS-enabled proxy) +- Test proxy with curl: `curl -x http://proxy:8080 https://google.com` +- Ensure proxy is running and accessible +- Check firewall rules + +### Invalid Proxy URL Format + +``` +Error: Invalid proxy URL format: http://***:***@:invalid +``` + +**Solutions:** +- Ensure URL format is correct: `http://user:pass@host:port` +- Host must be valid hostname or IP address +- Port must be numeric + +### Account Cooldown After Proxy Failures + +If proxy fails 5 times consecutively, the account enters a 30-second cooldown to prevent cascading failures. + +**Solutions:** +- Fix proxy configuration +- Wait 30 seconds for cooldown to expire +- Check proxy logs for error details diff --git a/package-lock.json b/package-lock.json index 8d50b23c..0a0a993b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,22 @@ { "name": "opencode-antigravity-auth", - "version": "1.3.0", + "version": "1.3.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "opencode-antigravity-auth", - "version": "1.3.0", + "version": "1.3.4", "license": "MIT", "dependencies": { "@openauthjs/openauth": "^0.4.3", + "@opencode-ai/plugin": "^0.15.30", "proper-lockfile": "^4.1.2", + "undici": "^6.23.0", "xdg-basedir": "^5.1.0", "zod": "^4.0.0" }, "devDependencies": { - "@opencode-ai/plugin": "^0.15.30", "@types/node": "^24.10.1", "@types/proper-lockfile": "^4.1.4", "@vitest/coverage-v8": "^3.0.0", @@ -632,7 +633,6 @@ "version": "0.15.31", "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-0.15.31.tgz", "integrity": "sha512-htKKCq9Htljf7vX5ANLDB7bU7TeJYrl8LP2CQUtCAguKUpVvpj5tiZ+edlCdhGFEqlpSp+pkiTEY5LCv1muowg==", - "dev": true, "dependencies": { "@opencode-ai/sdk": "0.15.31", "zod": "4.1.8" @@ -642,7 +642,6 @@ "version": "4.1.8", "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.8.tgz", "integrity": "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -651,14 +650,14 @@ "node_modules/@opencode-ai/sdk": { "version": "0.15.31", "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-0.15.31.tgz", - "integrity": "sha512-95HWBiNKQnwsubkR2E7QhBD/CH9yteZGrviWar0aKHWu8/RjWw9m7Znbv8DI+y6i2dMwBBcGQ8LJ7x0abzys4A==", - "dev": true + "integrity": "sha512-95HWBiNKQnwsubkR2E7QhBD/CH9yteZGrviWar0aKHWu8/RjWw9m7Znbv8DI+y6i2dMwBBcGQ8LJ7x0abzys4A==" }, "node_modules/@oslojs/asn1": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@oslojs/asn1/-/asn1-1.0.0.tgz", "integrity": "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA==", "license": "MIT", + "peer": true, "dependencies": { "@oslojs/binary": "1.0.0" } @@ -667,13 +666,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/@oslojs/binary/-/binary-1.0.0.tgz", "integrity": "sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@oslojs/crypto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@oslojs/crypto/-/crypto-1.0.1.tgz", "integrity": "sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ==", "license": "MIT", + "peer": true, "dependencies": { "@oslojs/asn1": "1.0.0", "@oslojs/binary": "1.0.0" @@ -683,13 +684,15 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@oslojs/jwt": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@oslojs/jwt/-/jwt-0.2.0.tgz", "integrity": "sha512-bLE7BtHrURedCn4Mco3ma9L4Y1GR2SMBuIvjWr7rmQ4/W/4Jy70TIAgZ+0nIlk0xHz1vNP8x8DCns45Sb2XRbg==", "license": "MIT", + "peer": true, "dependencies": { "@oslojs/encoding": "0.4.1" } @@ -698,7 +701,8 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-0.4.1.tgz", "integrity": "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", @@ -1063,7 +1067,6 @@ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1225,7 +1228,6 @@ "integrity": "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/utils": "3.2.4", "fflate": "^0.8.2", @@ -1922,7 +1924,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2352,6 +2353,15 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", + "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -2365,7 +2375,6 @@ "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -2464,7 +2473,6 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -2680,7 +2688,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 507127c9..0e2ad202 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "@opencode-ai/plugin": "^0.15.30", "@openauthjs/openauth": "^0.4.3", "proper-lockfile": "^4.1.2", + "undici": "^6.23.0", "xdg-basedir": "^5.1.0", "zod": "^4.0.0" } diff --git a/src/antigravity/oauth.ts b/src/antigravity/oauth.ts index 04847e01..02356a69 100644 --- a/src/antigravity/oauth.ts +++ b/src/antigravity/oauth.ts @@ -12,6 +12,7 @@ import { } from "../constants"; import { createLogger } from "../plugin/logger"; import { calculateTokenExpiry } from "../plugin/auth"; +import { fetchWithProxy } from "../plugin/proxy"; const log = createLogger("oauth"); @@ -119,10 +120,14 @@ async function fetchWithTimeout( url: string, options: RequestInit, timeoutMs = FETCH_TIMEOUT_MS, + proxyUrl?: string, ): Promise { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); try { + if (proxyUrl) { + return await fetchWithProxy(url, { ...options, signal: controller.signal }, proxyUrl); + } return await fetch(url, { ...options, signal: controller.signal }); } finally { clearTimeout(timeout); @@ -130,6 +135,7 @@ async function fetchWithTimeout( } async function fetchProjectID(accessToken: string): Promise { + const proxyUrl = process.env.ANTIGRAVITY_LOGIN_PROXY; const errors: string[] = []; const loadHeaders: Record = { Authorization: `Bearer ${accessToken}`, @@ -156,7 +162,7 @@ async function fetchProjectID(accessToken: string): Promise { pluginType: "GEMINI", }, }), - }); + }, undefined, proxyUrl); if (!response.ok) { const message = await response.text().catch(() => ""); @@ -203,11 +209,12 @@ export async function exchangeAntigravity( code: string, state: string, ): Promise { + const proxyUrl = process.env.ANTIGRAVITY_LOGIN_PROXY; try { const { verifier, projectId } = decodeState(state); const startTime = Date.now(); - const tokenResponse = await fetch("https://oauth2.googleapis.com/token", { + const tokenResponse = await fetchWithTimeout("https://oauth2.googleapis.com/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", @@ -224,7 +231,7 @@ export async function exchangeAntigravity( redirect_uri: ANTIGRAVITY_REDIRECT_URI, code_verifier: verifier, }), - }); + }, undefined, proxyUrl); if (!tokenResponse.ok) { const errorText = await tokenResponse.text(); @@ -233,7 +240,7 @@ export async function exchangeAntigravity( const tokenPayload = (await tokenResponse.json()) as AntigravityTokenResponse; - const userInfoResponse = await fetch( + const userInfoResponse = await fetchWithTimeout( "https://www.googleapis.com/oauth2/v1/userinfo?alt=json", { headers: { @@ -242,6 +249,8 @@ export async function exchangeAntigravity( "X-Goog-Api-Client": GEMINI_CLI_HEADERS["X-Goog-Api-Client"], }, }, + undefined, + proxyUrl ); const userInfo = userInfoResponse.ok diff --git a/src/plugin.ts b/src/plugin.ts index 4e5d50ab..6a3988f3 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -24,6 +24,7 @@ import { prepareAntigravityRequest, transformAntigravityResponse, } from "./plugin/request"; +import { fetchWithProxy } from "./plugin/proxy"; import { resolveModelWithTier } from "./plugin/transform/model-resolver"; import { isEmptyResponseBody, @@ -32,7 +33,7 @@ import { import { EmptyResponseError } from "./plugin/errors"; import { AntigravityTokenRefreshError, refreshAccessToken } from "./plugin/token"; import { startOAuthListener, type OAuthListener } from "./plugin/server"; -import { clearAccounts, loadAccounts, saveAccounts } from "./plugin/storage"; +import { clearAccounts, loadAccounts, saveAccounts, type AccountMetadataV3 } from "./plugin/storage"; import { AccountManager, type ModelFamily, parseRateLimitReason, calculateBackoffMs } from "./plugin/accounts"; import { createAutoUpdateCheckerHook } from "./hooks/auto-update-checker"; import { loadConfig, initRuntimeConfig, type AntigravityConfig } from "./plugin/config"; @@ -291,6 +292,7 @@ async function persistAccountPool( results: Array>, replaceAll: boolean = false, ): Promise { + const proxyUrl = process.env.ANTIGRAVITY_LOGIN_PROXY; if (results.length === 0) { return; } @@ -340,6 +342,7 @@ async function persistAccountPool( refreshToken: parts.refreshToken, projectId: parts.projectId, managedProjectId: parts.managedProjectId, + proxyUrl, addedAt: now, lastUsed: now, enabled: true, @@ -361,6 +364,7 @@ async function persistAccountPool( refreshToken: parts.refreshToken, projectId: parts.projectId ?? existing.projectId, managedProjectId: parts.managedProjectId ?? existing.managedProjectId, + proxyUrl: proxyUrl ?? existing.proxyUrl, lastUsed: now, }; @@ -857,6 +861,10 @@ export const createAntigravityPlugin = (providerId: string) => async ( return "Error: No valid access token available. Please run `opencode auth login` to re-authenticate."; } + const accountManager = await AccountManager.loadFromDisk(auth); + const accounts = accountManager.getAccounts(); + const account = accounts.find(a => a.parts.refreshToken === parts.refreshToken); + return executeSearch( { query: args.query, @@ -866,6 +874,7 @@ export const createAntigravityPlugin = (providerId: string) => async ( accessToken, projectId, ctx.abort, + account?.proxyUrl, ); }, }); @@ -1124,7 +1133,7 @@ export const createAntigravityPlugin = (providerId: string) => async ( if (accessTokenExpired(authRecord)) { try { - const refreshed = await refreshAccessToken(authRecord, client, providerId); + const refreshed = await refreshAccessToken(authRecord, client, providerId, account.proxyUrl); if (!refreshed) { const { failures, shouldCooldown, cooldownMs } = trackAccountFailure(account.index); getHealthTracker().recordFailure(account.index); @@ -1198,7 +1207,7 @@ export const createAntigravityPlugin = (providerId: string) => async ( let projectContext: ProjectContextResult; try { - projectContext = await ensureProjectContext(authRecord); + projectContext = await ensureProjectContext(authRecord, account.proxyUrl); resetAccountFailureState(account.index); } catch (error) { const { failures, shouldCooldown, cooldownMs } = trackAccountFailure(account.index); @@ -1265,7 +1274,7 @@ export const createAntigravityPlugin = (providerId: string) => async ( try { pushDebug("thinking-warmup: start"); - const warmupResponse = await fetch(warmupUrl, warmupInit); + const warmupResponse = await fetchWithProxy(warmupUrl, warmupInit, account.proxyUrl); const transformed = await transformAntigravityResponse( warmupResponse, true, @@ -1388,7 +1397,7 @@ export const createAntigravityPlugin = (providerId: string) => async ( tokenConsumed = getTokenTracker().consume(account.index); } - const response = await fetch(prepared.request, prepared.init); + const response = await fetchWithProxy(prepared.request, prepared.init, account.proxyUrl); pushDebug(`status=${response.status} ${response.statusText}`); @@ -2135,6 +2144,7 @@ export const createAntigravityPlugin = (providerId: string) => async ( refreshToken: parts.refreshToken, projectId: parts.projectId ?? updatedAccounts[refreshAccountIndex]?.projectId, managedProjectId: parts.managedProjectId ?? updatedAccounts[refreshAccountIndex]?.managedProjectId, + proxyUrl: process.env.ANTIGRAVITY_LOGIN_PROXY ?? updatedAccounts[refreshAccountIndex]?.proxyUrl, addedAt: updatedAccounts[refreshAccountIndex]?.addedAt ?? Date.now(), lastUsed: Date.now(), }; diff --git a/src/plugin/accounts.ts b/src/plugin/accounts.ts index 638ea74c..3afe709f 100644 --- a/src/plugin/accounts.ts +++ b/src/plugin/accounts.ts @@ -140,6 +140,7 @@ export interface ManagedAccount { fingerprint?: import("./fingerprint").Fingerprint; /** History of previous fingerprints for this account */ fingerprintHistory?: FingerprintVersion[]; + proxyUrl?: string; } function nowMs(): number { diff --git a/src/plugin/project.ts b/src/plugin/project.ts index 092229e1..8454f0e0 100644 --- a/src/plugin/project.ts +++ b/src/plugin/project.ts @@ -6,6 +6,7 @@ import { } from "../constants"; import { formatRefreshParts, parseRefreshParts } from "./auth"; import { createLogger } from "./logger"; +import { fetchWithProxy } from "./proxy"; import type { OAuthAuthDetails, ProjectContextResult } from "./types"; const log = createLogger("project"); @@ -121,6 +122,7 @@ export function invalidateProjectContextCache(refresh?: string): void { export async function loadManagedProject( accessToken: string, projectId?: string, + proxyUrl?: string, ): Promise { const metadata = buildMetadata(projectId); const requestBody: Record = { metadata }; @@ -139,13 +141,14 @@ export async function loadManagedProject( for (const baseEndpoint of loadEndpoints) { try { - const response = await fetch( + const response = await fetchWithProxy( `${baseEndpoint}/v1internal:loadCodeAssist`, { method: "POST", headers: loadHeaders, body: JSON.stringify(requestBody), }, + proxyUrl, ); if (!response.ok) { @@ -170,6 +173,7 @@ export async function onboardManagedProject( accessToken: string, tierId: string, projectId?: string, + proxyUrl?: string, attempts = 10, delayMs = 5000, ): Promise { @@ -182,7 +186,7 @@ export async function onboardManagedProject( for (const baseEndpoint of ANTIGRAVITY_ENDPOINT_FALLBACKS) { for (let attempt = 0; attempt < attempts; attempt += 1) { try { - const response = await fetch( + const response = await fetchWithProxy( `${baseEndpoint}/v1internal:onboardUser`, { method: "POST", @@ -193,6 +197,7 @@ export async function onboardManagedProject( }, body: JSON.stringify(requestBody), }, + proxyUrl, ); if (!response.ok) { @@ -222,7 +227,7 @@ export async function onboardManagedProject( /** * Resolves an effective project ID for the current auth state, caching results per refresh token. */ -export async function ensureProjectContext(auth: OAuthAuthDetails): Promise { +export async function ensureProjectContext(auth: OAuthAuthDetails, proxyUrl?: string): Promise { const accessToken = auth.access; if (!accessToken) { return { auth, effectiveProjectId: "" }; @@ -261,7 +266,7 @@ export async function ensureProjectContext(auth: OAuthAuthDetails): Promise { + const ProxyAgentMock = vi.fn().mockImplementation((options) => ({ + uri: options.uri, + options, + })); + return { + ProxyAgent: ProxyAgentMock, + fetch: vi.fn().mockResolvedValue({ ok: true, json: async () => ({}) } as Response), + }; +}); + +describe('proxy.ts', () => { + beforeEach(() => { + vi.clearAllMocks(); + // We can't easily clear the internal agentCache Map in proxy.ts + // because it's not exported. But we can test around it or + // use different URLs for different tests. + }); + + describe('getProxyAgent', () => { + it('returns undefined when proxyUrl is empty or undefined', () => { + expect(getProxyAgent()).toBeUndefined(); + expect(getProxyAgent('')).toBeUndefined(); + expect(getProxyAgent(' ')).toBeUndefined(); + }); + + it('returns a ProxyAgent instance for a valid URL', () => { + const proxyUrl = 'http://proxy.example.com:8080'; + const agent = getProxyAgent(proxyUrl); + + expect(agent).toBeDefined(); + expect(ProxyAgent).toHaveBeenCalledWith(expect.objectContaining({ + uri: proxyUrl, + connect: { timeout: 30000 } + })); + }); + + it('throws an error with sanitized credentials for a malformed URL', () => { + const malformedUrlWithCreds = 'http://user:pass@malformed-url'; + // URL constructor will throw for things like 'not a url' + // But 'http://user:pass@malformed-url' might actually be valid for URL. + // Let's try something definitely invalid. + const invalidUrl = 'http://user:pass@:invalid'; + + expect(() => getProxyAgent(invalidUrl)).toThrow('Invalid proxy URL format: http://***:***@:invalid'); + }); + + it('caches the ProxyAgent instance for the same URL', () => { + const proxyUrl = 'http://cache.example.com'; + const agent1 = getProxyAgent(proxyUrl); + const agent2 = getProxyAgent(proxyUrl); + + expect(agent1).toBe(agent2); + expect(ProxyAgent).toHaveBeenCalledTimes(1); + }); + + it('returns different instances for different URLs', () => { + const proxyUrl1 = 'http://proxy1.example.com'; + const proxyUrl2 = 'http://proxy2.example.com'; + + const agent1 = getProxyAgent(proxyUrl1); + const agent2 = getProxyAgent(proxyUrl2); + + expect(agent1).not.toBe(agent2); + expect(ProxyAgent).toHaveBeenCalledTimes(2); + }); + + it('throws error when ProxyAgent creation fails', () => { + // Setup ProxyAgent to throw once + (ProxyAgent as any).mockImplementationOnce(() => { + throw new Error('Creation failed'); + }); + + const proxyUrl = 'http://fail.example.com'; + expect(() => getProxyAgent(proxyUrl)).toThrow('Failed to create proxy agent for http://fail.example.com: Creation failed'); + }); + + it('sanitizes credentials when ProxyAgent creation fails', () => { + (ProxyAgent as any).mockImplementationOnce(() => { + throw new Error('Access denied'); + }); + + const proxyUrl = 'http://user:pass@fail.example.com'; + expect(() => getProxyAgent(proxyUrl)).toThrow('Failed to create proxy agent for http://***:***@fail.example.com: Access denied'); + }); + }); + + describe('fetchWithProxy', () => { + it('uses global fetch when no proxy is provided', async () => { + const globalFetch = vi.fn().mockResolvedValue({ ok: true } as Response); + vi.stubGlobal('fetch', globalFetch); + + await fetchWithProxy('https://api.example.com'); + + expect(globalFetch).toHaveBeenCalledWith('https://api.example.com', undefined); + expect(undiciFetch).not.toHaveBeenCalled(); + }); + + it('uses undici fetch with ProxyAgent when proxy is provided', async () => { + const proxyUrl = 'http://proxy.example.com'; + const targetUrl = 'https://api.example.com'; + + await fetchWithProxy(targetUrl, { method: 'POST' }, proxyUrl); + + expect(undiciFetch).toHaveBeenCalledWith(targetUrl, expect.objectContaining({ + method: 'POST', + dispatcher: expect.any(Object) + })); + }); + + it('handles Request object input in fetchWithProxy', async () => { + const proxyUrl = 'http://proxy.example.com'; + const request = new Request('https://api.example.com', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ key: 'value' }) + }); + + await fetchWithProxy(request, {}, proxyUrl); + + // New behavior: URL is extracted, init is passed through (not merged from Request) + expect(undiciFetch).toHaveBeenCalledWith('https://api.example.com/', expect.objectContaining({ + dispatcher: expect.any(Object) + })); + }); + }); +}); diff --git a/src/plugin/proxy.ts b/src/plugin/proxy.ts new file mode 100644 index 00000000..867c8ea3 --- /dev/null +++ b/src/plugin/proxy.ts @@ -0,0 +1,65 @@ +import { ProxyAgent } from 'undici'; + +const agentCache = new Map(); +const SUPPORTED_PROTOCOLS = new Set(['http:', 'https:']); + +function sanitizeCredentials(url: string): string { + return url.replace(/:\/\/[^@]+@/, '://***:***@'); +} + +export function getProxyAgent(proxyUrl?: string): ProxyAgent | undefined { + if (!proxyUrl?.trim()) return undefined; + + const normalizedUrl = proxyUrl.trim(); + + let parsed: URL; + try { + parsed = new URL(normalizedUrl); + } catch { + throw new Error(`Invalid proxy URL format: ${sanitizeCredentials(normalizedUrl)}`); + } + + if (!SUPPORTED_PROTOCOLS.has(parsed.protocol)) { + throw new Error(`Unsupported proxy protocol: ${parsed.protocol} (only http: and https: supported)`); + } + + let agent = agentCache.get(normalizedUrl); + + if (!agent) { + try { + agent = new ProxyAgent({ + uri: normalizedUrl, + connect: { timeout: 30000 }, + }); + agentCache.set(normalizedUrl, agent); + } catch (error) { + const rawMessage = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to create proxy agent for ${sanitizeCredentials(normalizedUrl)}: ${sanitizeCredentials(rawMessage)}`); + } + } + + return agent; +} + +export async function fetchWithProxy( + input: RequestInfo | URL, + init?: RequestInit, + proxyUrl?: string, +): Promise { + const agent = getProxyAgent(proxyUrl); + + if (!agent) { + return fetch(input, init); + } + + const { fetch: undiciFetch } = await import('undici'); + + const url = typeof input === 'string' + ? input + : input instanceof URL + ? input.href + : input.url; + + // @ts-ignore - undici.fetch dispatcher property not in standard RequestInit + return undiciFetch(url, { ...init, dispatcher: agent }) as unknown as Promise; +} diff --git a/src/plugin/quota.ts b/src/plugin/quota.ts index e1d97a9f..90bd2984 100644 --- a/src/plugin/quota.ts +++ b/src/plugin/quota.ts @@ -4,6 +4,7 @@ import { ANTIGRAVITY_PROVIDER_ID, } from "../constants"; import { accessTokenExpired, formatRefreshParts, parseRefreshParts } from "./auth"; +import { fetchWithProxy } from "./proxy"; import { ensureProjectContext } from "./project"; import { refreshAccessToken } from "./token"; import { getModelFamily } from "./transform/model-resolver"; @@ -147,11 +148,11 @@ function aggregateQuota(models?: Record): Quot return { groups, modelCount: totalCount }; } -async function fetchWithTimeout(url: string, options: RequestInit, timeoutMs = FETCH_TIMEOUT_MS): Promise { +async function fetchWithTimeout(url: string, options: RequestInit, timeoutMs = FETCH_TIMEOUT_MS, proxyUrl?: string): Promise { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); try { - return await fetch(url, { ...options, signal: controller.signal }); + return await fetchWithProxy(url, { ...options, signal: controller.signal }, proxyUrl); } finally { clearTimeout(timeout); } @@ -160,6 +161,7 @@ async function fetchWithTimeout(url: string, options: RequestInit, timeoutMs = F async function fetchAvailableModels( accessToken: string, projectId: string, + proxyUrl?: string, ): Promise { const endpoint = ANTIGRAVITY_ENDPOINT_PROD; const quotaUserAgent = ANTIGRAVITY_HEADERS["User-Agent"] || "antigravity/windows/amd64"; @@ -174,7 +176,7 @@ async function fetchAvailableModels( "User-Agent": quotaUserAgent, }, body: JSON.stringify(body), - }); + }, FETCH_TIMEOUT_MS, proxyUrl); if (response.ok) { return (await response.json()) as FetchAvailableModelsResponse; @@ -240,6 +242,7 @@ export async function checkAccountsQuota( const response = await fetchAvailableModels( auth.access ?? "", projectContext.effectiveProjectId, + account.proxyUrl, ); quotaResult = aggregateQuota(response.models); } catch (error) { diff --git a/src/plugin/search.ts b/src/plugin/search.ts index 4b6e9979..5f0e7de8 100644 --- a/src/plugin/search.ts +++ b/src/plugin/search.ts @@ -14,6 +14,7 @@ import { SEARCH_SYSTEM_INSTRUCTION, } from "../constants"; import { createLogger } from "./logger"; +import { fetchWithProxy } from "./proxy"; const log = createLogger("search"); @@ -225,6 +226,7 @@ export async function executeSearch( accessToken: string, projectId: string, abortSignal?: AbortSignal, + proxyUrl?: string, ): Promise { const { query, urls, thinking = true } = args; @@ -283,7 +285,7 @@ export async function executeSearch( }); try { - const response = await fetch(url, { + const response = await fetchWithProxy(url, { method: "POST", headers: { ...ANTIGRAVITY_HEADERS, @@ -292,7 +294,7 @@ export async function executeSearch( }, body: JSON.stringify(wrappedBody), signal: abortSignal ?? AbortSignal.timeout(SEARCH_TIMEOUT_MS), - }); + }, proxyUrl); if (!response.ok) { const errorText = await response.text(); diff --git a/src/plugin/storage.ts b/src/plugin/storage.ts index e10931c5..4d8670b3 100644 --- a/src/plugin/storage.ts +++ b/src/plugin/storage.ts @@ -189,6 +189,7 @@ export interface AccountMetadataV3 { cooldownReason?: CooldownReason; /** Per-account device fingerprint for rate limit mitigation */ fingerprint?: import("./fingerprint").Fingerprint; + proxyUrl?: string; // URL like "http://user:pass@host:port" (SOCKS5 not supported) } export interface AccountStorageV3 { diff --git a/src/plugin/token.ts b/src/plugin/token.ts index b2b1c89a..cedfdc5a 100644 --- a/src/plugin/token.ts +++ b/src/plugin/token.ts @@ -3,6 +3,7 @@ import { formatRefreshParts, parseRefreshParts, calculateTokenExpiry } from "./a import { clearCachedAuth, storeCachedAuth } from "./cache"; import { createLogger } from "./logger"; import { invalidateProjectContextCache } from "./project"; +import { fetchWithProxy } from "./proxy"; import type { OAuthAuthDetails, PluginClient, RefreshParts } from "./types"; const log = createLogger("token"); @@ -86,6 +87,7 @@ export async function refreshAccessToken( auth: OAuthAuthDetails, client: PluginClient, providerId: string, + proxyUrl?: string, ): Promise { const parts = parseRefreshParts(auth.refresh); if (!parts.refreshToken) { @@ -94,18 +96,22 @@ export async function refreshAccessToken( try { const startTime = Date.now(); - const response = await fetch("https://oauth2.googleapis.com/token", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", + const response = await fetchWithProxy( + "https://oauth2.googleapis.com/token", + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + grant_type: "refresh_token", + refresh_token: parts.refreshToken, + client_id: ANTIGRAVITY_CLIENT_ID, + client_secret: ANTIGRAVITY_CLIENT_SECRET, + }), }, - body: new URLSearchParams({ - grant_type: "refresh_token", - refresh_token: parts.refreshToken, - client_id: ANTIGRAVITY_CLIENT_ID, - client_secret: ANTIGRAVITY_CLIENT_SECRET, - }), - }); + proxyUrl, + ); if (!response.ok) { let errorText: string | undefined; From af579b497b3a89f4d0f07612e6b3a633e367f3df Mon Sep 17 00:00:00 2001 From: Aaron Sewall Date: Wed, 28 Jan 2026 13:46:16 -0500 Subject: [PATCH 2/6] fix: persist and hydrate proxyUrl in AccountManager proxyUrl was missing from saveToDisk() mapping and constructor hydration, causing proxy config to be lost on restart and all API calls to bypass proxy. --- src/plugin/accounts.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plugin/accounts.ts b/src/plugin/accounts.ts index 3afe709f..d8a0ba20 100644 --- a/src/plugin/accounts.ts +++ b/src/plugin/accounts.ts @@ -287,6 +287,7 @@ export class AccountManager { touchedForQuota: {}, // Use stored fingerprint or generate new one for rate limit mitigation fingerprint: acc.fingerprint ?? generateFingerprint(), + proxyUrl: acc.proxyUrl, }; }) .filter((a): a is ManagedAccount => a !== null); @@ -772,6 +773,7 @@ export class AccountManager { cooldownReason: a.cooldownReason, fingerprint: a.fingerprint, fingerprintHistory: a.fingerprintHistory?.length ? a.fingerprintHistory : undefined, + proxyUrl: a.proxyUrl, })), activeIndex: claudeIndex, activeIndexByFamily: { From 9543bc4b5d8f1f14d8f7fcdc904be5144a955d46 Mon Sep 17 00:00:00 2001 From: Aaron Sewall Date: Wed, 28 Jan 2026 13:54:16 -0500 Subject: [PATCH 3/6] Update README.md Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Signed-off-by: Aaron Sewall --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bfc1329a..a685f14b 100644 --- a/README.md +++ b/README.md @@ -272,7 +272,7 @@ opencode run "Hello" --model=google/antigravity-claude-sonnet-4-5 ### Advanced: Manual Configuration -See [EXAMPLE_PROXY_CONFIG.md](EXAMPLE_PROXY_CONFIG.md) for manual JSON editing and troubleshooting. +See [docs/PROXY.md](docs/PROXY.md) for manual JSON editing and troubleshooting. --- From bad0b495d33e337226adc6d61f32d8ac24fbee6c Mon Sep 17 00:00:00 2001 From: Aaron Sewall Date: Wed, 28 Jan 2026 13:57:10 -0500 Subject: [PATCH 4/6] fix: narrow fetchWithProxy signature to string | URL Request objects were accepted but properties (method, headers, body) were silently lost. All call sites pass strings, so narrowing the type prevents future misuse. Removed test for Request input since it's no longer valid. --- src/plugin.ts | 2 +- src/plugin/proxy.test.ts | 16 ---------------- src/plugin/proxy.ts | 8 ++------ 3 files changed, 3 insertions(+), 23 deletions(-) diff --git a/src/plugin.ts b/src/plugin.ts index 6a3988f3..db7894a9 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1397,7 +1397,7 @@ export const createAntigravityPlugin = (providerId: string) => async ( tokenConsumed = getTokenTracker().consume(account.index); } - const response = await fetchWithProxy(prepared.request, prepared.init, account.proxyUrl); + const response = await fetchWithProxy(resolvedUrl, prepared.init, account.proxyUrl); pushDebug(`status=${response.status} ${response.statusText}`); diff --git a/src/plugin/proxy.test.ts b/src/plugin/proxy.test.ts index 851c09b0..60e570b0 100644 --- a/src/plugin/proxy.test.ts +++ b/src/plugin/proxy.test.ts @@ -111,21 +111,5 @@ describe('proxy.ts', () => { dispatcher: expect.any(Object) })); }); - - it('handles Request object input in fetchWithProxy', async () => { - const proxyUrl = 'http://proxy.example.com'; - const request = new Request('https://api.example.com', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ key: 'value' }) - }); - - await fetchWithProxy(request, {}, proxyUrl); - - // New behavior: URL is extracted, init is passed through (not merged from Request) - expect(undiciFetch).toHaveBeenCalledWith('https://api.example.com/', expect.objectContaining({ - dispatcher: expect.any(Object) - })); - }); }); }); diff --git a/src/plugin/proxy.ts b/src/plugin/proxy.ts index 867c8ea3..4bddef93 100644 --- a/src/plugin/proxy.ts +++ b/src/plugin/proxy.ts @@ -42,7 +42,7 @@ export function getProxyAgent(proxyUrl?: string): ProxyAgent | undefined { } export async function fetchWithProxy( - input: RequestInfo | URL, + input: string | URL, init?: RequestInit, proxyUrl?: string, ): Promise { @@ -54,11 +54,7 @@ export async function fetchWithProxy( const { fetch: undiciFetch } = await import('undici'); - const url = typeof input === 'string' - ? input - : input instanceof URL - ? input.href - : input.url; + const url = typeof input === 'string' ? input : input.href; // @ts-ignore - undici.fetch dispatcher property not in standard RequestInit return undiciFetch(url, { ...init, dispatcher: agent }) as unknown as Promise; From a6c21d1adff88b4605e011cc9069814f682cd93b Mon Sep 17 00:00:00 2001 From: Aaron Sewall Date: Thu, 29 Jan 2026 22:45:29 -0500 Subject: [PATCH 5/6] Update docs/PROXY.md Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Signed-off-by: Aaron Sewall --- docs/PROXY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/PROXY.md b/docs/PROXY.md index cef6df45..4d023f7e 100644 --- a/docs/PROXY.md +++ b/docs/PROXY.md @@ -100,7 +100,7 @@ ANTIGRAVITY_LOGIN_PROXY=http://localhost:8080 opencode auth login # - POST to Antigravity loadCodeAssist endpoints # Make a test request -opencode run "Hello" --model=google/antigravity-claude-sonnet-4-5 +opencode run "Hello" --model=google/claude-sonnet-4-5 # Proxy logs should show API traffic ``` From 561fb1e0d4189fdab3ea8db10648143a7921aaa4 Mon Sep 17 00:00:00 2001 From: Aaron Sewall Date: Sat, 31 Jan 2026 00:04:17 -0500 Subject: [PATCH 6/6] fix(proxy): address code review feedback on proxy handling - Add MAX_AGENT_CACHE_SIZE (50) with LRU eviction to prevent unbounded growth - Add disposeProxyAgent() for explicit cleanup when accounts are removed - Fix proxyUrl clearing logic: use explicit undefined check instead of ?? (allows removing proxy by setting empty string, not treated as falsy) - Pass account.proxyUrl to refreshAccessToken() and ensureProjectContext() in quota checks to ensure proxy is used for all auth operations --- src/plugin.ts | 2 +- src/plugin/proxy.ts | 33 +++++++++++++++++++++++++++++++++ src/plugin/quota.ts | 4 ++-- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/plugin.ts b/src/plugin.ts index ecf1e296..c8d977b9 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -420,7 +420,7 @@ async function persistAccountPool( refreshToken: parts.refreshToken, projectId: parts.projectId ?? existing.projectId, managedProjectId: parts.managedProjectId ?? existing.managedProjectId, - proxyUrl: proxyUrl ?? existing.proxyUrl, + proxyUrl: proxyUrl !== undefined ? proxyUrl : existing.proxyUrl, lastUsed: now, }; diff --git a/src/plugin/proxy.ts b/src/plugin/proxy.ts index 4bddef93..a2bba590 100644 --- a/src/plugin/proxy.ts +++ b/src/plugin/proxy.ts @@ -1,5 +1,6 @@ import { ProxyAgent } from 'undici'; +const MAX_AGENT_CACHE_SIZE = 50; // Prevent unbounded growth const agentCache = new Map(); const SUPPORTED_PROTOCOLS = new Set(['http:', 'https:']); @@ -7,6 +8,37 @@ function sanitizeCredentials(url: string): string { return url.replace(/:\/\/[^@]+@/, '://***:***@'); } +/** + * Dispose a proxy agent and remove it from cache. + * Call this when an account is removed or its proxy URL changes. + */ +export function disposeProxyAgent(proxyUrl: string): void { + const normalizedUrl = proxyUrl.trim(); + const agent = agentCache.get(normalizedUrl); + if (agent) { + agent.close().catch(() => {}); + agentCache.delete(normalizedUrl); + } +} + +/** + * Clear oldest entries if cache exceeds max size (LRU eviction). + */ +function evictOldestIfNeeded(): void { + if (agentCache.size <= MAX_AGENT_CACHE_SIZE) return; + + const entriesToDelete = agentCache.size - MAX_AGENT_CACHE_SIZE; + const keys = Array.from(agentCache.keys()); + for (let i = 0; i < entriesToDelete; i++) { + const key = keys[i]; + if (key) { + const agent = agentCache.get(key); + agent?.close().catch(() => {}); + agentCache.delete(key); + } + } +} + export function getProxyAgent(proxyUrl?: string): ProxyAgent | undefined { if (!proxyUrl?.trim()) return undefined; @@ -32,6 +64,7 @@ export function getProxyAgent(proxyUrl?: string): ProxyAgent | undefined { connect: { timeout: 30000 }, }); agentCache.set(normalizedUrl, agent); + evictOldestIfNeeded(); } catch (error) { const rawMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to create proxy agent for ${sanitizeCredentials(normalizedUrl)}: ${sanitizeCredentials(rawMessage)}`); diff --git a/src/plugin/quota.ts b/src/plugin/quota.ts index 6dffc1e3..fbbd64c6 100644 --- a/src/plugin/quota.ts +++ b/src/plugin/quota.ts @@ -326,14 +326,14 @@ export async function checkAccountsQuota( try { if (accessTokenExpired(auth)) { - const refreshed = await refreshAccessToken(auth, client, providerId); + const refreshed = await refreshAccessToken(auth, client, providerId, account.proxyUrl); if (!refreshed) { throw new Error("Token refresh failed"); } auth = refreshed; } - const projectContext = await ensureProjectContext(auth); + const projectContext = await ensureProjectContext(auth, account.proxyUrl); auth = projectContext.auth; const updatedAccount = applyAccountUpdates(account, auth);