dsh-agy 0.3.0 → 0.3.2
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/README.md +29 -5
- package/lib/{accounts-CkaHhb5H.mjs → accounts-CKWybAfc.mjs} +236 -35
- package/lib/accounts-CKWybAfc.mjs.map +1 -0
- package/lib/cli/index.mjs +5 -5
- package/lib/client.js +18 -2
- package/lib/{constants-Db4tmyfb.mjs → constants-BpyRirvk.mjs} +38 -2
- package/lib/constants-BpyRirvk.mjs.map +1 -0
- package/lib/{exchange-yZPIZumk.mjs → exchange-LzEyxve1.mjs} +2 -2
- package/lib/{exchange-yZPIZumk.mjs.map → exchange-LzEyxve1.mjs.map} +1 -1
- package/lib/{import-BaFULQnH.mjs → import-CotRHNDk.mjs} +3 -3
- package/lib/{import-BaFULQnH.mjs.map → import-CotRHNDk.mjs.map} +1 -1
- package/lib/index.mjs +2 -2
- package/lib/{models-BGuwl50d.mjs → models-Qn0-sh6X.mjs} +2 -2
- package/lib/{models-BGuwl50d.mjs.map → models-Qn0-sh6X.mjs.map} +1 -1
- package/lib/{plugin-common-B1MG00JT.mjs → plugin-common-B5dyfnMH.mjs} +4 -4
- package/lib/{plugin-common-B1MG00JT.mjs.map → plugin-common-B5dyfnMH.mjs.map} +1 -1
- package/lib/{quota-summary-5CypVffd.mjs → quota-summary-adID82I2.mjs} +9 -12
- package/lib/quota-summary-adID82I2.mjs.map +1 -0
- package/lib/web/plugin.mjs +6 -6
- package/package.json +6 -2
- package/lib/accounts-CkaHhb5H.mjs.map +0 -1
- package/lib/constants-Db4tmyfb.mjs.map +0 -1
- package/lib/quota-summary-5CypVffd.mjs.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"constants-Db4tmyfb.mjs","names":[],"sources":["../src/types.ts","../src/oauth/constants.ts"],"sourcesContent":["/** Shared domain types for dsh-agy. */\n\n/**\n * Device fingerprint persisted per account (rate-limit mitigation).\n *\n * Mirrors `google.internal.cloud.code.v1internal.ClientMetadata`, read out of the\n * installed official CLI's own descriptor (`docs/official-identity.json`). The\n * message has eight fields; only the three below are populated, because the rest\n * (`pluginVersion`, `updateChannel`, `duetProject`, `pluginType`, `ideName`) have\n * no captured value and a wrong value is a worse anomaly than an absent one.\n *\n * This is the BODY message (`metadata`), not a header. Nothing here is sent as a\n * `Client-Metadata` header: neither official binary contains that header name.\n */\nexport interface ClientMetadata {\n ideType: string\n /** Client version for the claimed product line (the CLI's, not the IDE's). */\n ideVersion?: string\n /**\n * `ClientMetadata.platform` enum NAME (`DARWIN_ARM64`), which is a DIFFERENT\n * vocabulary from the UA's `darwin/arm64` token — both exist, and conflating\n * them is how `\"MACOS\"` came to be sent and rejected.\n */\n platform?: string\n}\n\nexport interface Fingerprint {\n deviceId: string\n sessionToken: string\n userAgent: string\n apiClient: string\n clientMetadata: ClientMetadata\n createdAt: number\n}\n\nexport interface FingerprintVersion {\n fingerprint: Fingerprint\n timestamp: number\n reason: 'initial' | 'regenerated' | 'restored'\n}\n\nexport type CooldownReason =\n | 'auth-failure'\n | 'network-error'\n | 'project-error'\n | 'quota-exhausted'\n | 'validation-required'\n\n/** Per-account quota cache keyed by model id. */\nexport interface CachedQuota {\n remainingFraction?: number\n resetTime?: string\n modelCount?: number\n}\n\n/**\n * One window of one `QuotaGroup`, as `retrieveUserQuotaSummary` reports it.\n *\n * Declared here (the dependency-free leaf) because it is PERSISTED on the\n * account; the parser that produces it lives in `adapter/quota-summary.ts`, which\n * imports this module rather than the reverse.\n */\nexport interface QuotaWindow {\n /** Upstream's own bucket id, e.g. `gemini-5h`, `3p-weekly`. Kept verbatim. */\n bucketId: string\n /** Upstream's window token: `5h` or `weekly` today. */\n window: string\n /** 0..1, or null when upstream omitted the fraction (unknown, not empty). */\n remainingFraction: number | null\n /** RFC3339 reset moment, or null when upstream omitted it. */\n resetTime: string | null\n}\n\n/** One group of models sharing a 5-hour and a weekly window. */\nexport interface QuotaGroup {\n /** Upstream's group label, e.g. `Gemini Models`. */\n name: string\n windows: QuotaWindow[]\n}\n\n/**\n * The grouped 5-hour / weekly windows, cached per account.\n *\n * Deliberately SEPARATE from `cachedQuota`: that map is per-model and feeds the\n * rotation/ranking path (`familyQuotaFor`, `isFamilyDrained`), while this is\n * per-GROUP and display-only. Merging them would put two different shapes under\n * one key and let a display refresh influence scheduling.\n */\nexport interface CachedLimits {\n groups: QuotaGroup[]\n /** When this snapshot was taken (Unix ms). */\n updatedAt: number\n}\n\n/** One account in the pool. `refresh` is the packed `refreshToken|projectId|managedProjectId` string. */\nexport interface ManagedAccount {\n id?: string\n email?: string\n refresh: string\n projectId?: string\n managedProjectId?: string\n clientId?: string\n addedAt: number\n lastUsed: number\n enabled?: boolean\n rateLimitResetTimes?: Record<string, number>\n coolingDownUntil?: number\n cooldownReason?: CooldownReason\n /**\n * When the CURRENT cooldown began (Unix ms).\n *\n * Separate from `coolingDownUntil`, which is its END: the duration is a\n * backoff computed from the consecutive-failure count, so the start cannot be\n * recovered from the end. Persisted because the reason is only useful with an\n * age attached — \"network-error\" alone cannot distinguish a blip from seconds\n * ago from one that has been sitting there for days. Cleared together with\n * `cooldownReason` when the window expires.\n */\n cooldownSetAt?: number\n verificationRequired?: boolean\n verificationRequiredAt?: number\n verificationRequiredReason?: string\n verificationUrl?: string\n fingerprint?: Fingerprint\n fingerprintHistory?: FingerprintVersion[]\n cachedQuota?: Record<string, CachedQuota>\n cachedQuotaUpdatedAt?: number\n /** Grouped 5h/weekly windows, display-only (see `CachedLimits`). */\n cachedLimits?: CachedLimits\n /** Per-account proxy URL (e.g. http://user:pass@host:8080 or socks5://host:1080). Undefined = follow env. */\n proxy?: string\n}\n\nexport interface AccountStorageV1 {\n version: 1\n accounts: Array<{\n email?: string\n refreshToken: string\n projectId?: string\n managedProjectId?: string\n addedAt: number\n lastUsed: number\n isRateLimited?: boolean\n rateLimitResetTime?: number\n lastSwitchReason?: 'rate-limit' | 'initial' | 'rotation'\n }>\n activeIndex: number\n}\n\nexport interface AccountStorageV2 {\n version: 2\n accounts: Array<{\n email?: string\n refreshToken: string\n projectId?: string\n managedProjectId?: string\n addedAt: number\n lastUsed: number\n lastSwitchReason?: 'rate-limit' | 'initial' | 'rotation'\n rateLimitResetTimes?: Record<string, number>\n }>\n activeIndex: number\n}\n\nexport interface AccountStorageV3 {\n version: 3\n accounts: ManagedAccount[]\n activeIndex: number\n}\n\nexport interface AccountStorageV4 {\n version: 4\n accounts: ManagedAccount[]\n activeIndex: number\n}\n\nexport type AccountStorage = AccountStorageV1 | AccountStorageV2 | AccountStorageV3 | AccountStorageV4\n\n/**\n * How one account-scoped request is routed — and therefore how its transport\n * failures must be read. `proxyUrl` is the single source of truth: fail-closed\n * applies exactly when it is set, so routing and failure classification can\n * never disagree (splitting them into two arguments let them drift).\n */\nexport interface AccountRouting {\n /** Per-account proxy; unset means the env/direct route. */\n proxyUrl?: string\n /**\n * Generation stream: long model silences are normal, so the per-gap body\n * inactivity timer must be disabled (AGENTS.md \"Proxy Routing\").\n */\n streaming?: boolean\n}\n\n/** Whether a request ran through an explicit per-account proxy. */\nexport function isProxyRouted(routing: AccountRouting | undefined): boolean {\n return Boolean(routing?.proxyUrl)\n}\n\n/** Parsed halves of the packed refresh string. */\nexport interface RefreshParts {\n refreshToken?: string\n projectId?: string\n managedProjectId?: string\n}\n\n/** OAuth token view of an account used by the refresh path. */\nexport interface OAuthAuthDetails {\n access: string\n expires: number\n refresh: string\n}\n\n/** Result of the OAuth token exchange. */\nexport interface TokenExchangeSuccess {\n type: 'success'\n refresh: string\n access: string\n expires: number\n email?: string\n projectId: string\n tier?: string\n clientId?: string\n}\n\nexport interface TokenExchangeFailure {\n type: 'failed'\n error: string\n}\n\nexport type TokenExchangeResult = TokenExchangeSuccess | TokenExchangeFailure\n\nexport interface AgyAccountSession {\n auth: OAuthAuthDetails\n account: ManagedAccount\n index: number\n /**\n * Impersonation headers for this request.\n *\n * `clientMetadata` rides alongside rather than inside: it is a BODY message\n * (`metadata` on the control-plane calls), because the `Client-Metadata` header\n * this used to be is present in neither official binary.\n */\n impersonation: {\n 'User-Agent': string\n 'X-Goog-Api-Client': string\n clientMetadata: ClientMetadata\n }\n}\n\n/** Authentication failure while resolving an account session. */\nexport type AgyAuthErrorKind = 'transport' | 'rate-limit' | 'invalid-credential'\n\n/**\n * Host-independent authentication error. The adapter maps `kind` to the DSH\n * error protocol without coupling the session or CLI layers to dsh-llm.\n */\nexport class AgyAuthError extends Error {\n readonly kind: AgyAuthErrorKind\n\n constructor(kind: AgyAuthErrorKind, message: string, options?: ErrorOptions) {\n super(message, options)\n this.name = 'AgyAuthError'\n this.kind = kind\n }\n}\n\n/** Why an enabled account pool cannot currently serve one model family. */\nexport type PoolBlockedKind = 'retryable' | 'quota-exhausted'\n\n/**\n * Enabled accounts exist, but every candidate is temporarily blocked. Kept\n * independent of dsh-llm so CLI and web entry points do not gain a host import.\n */\nexport class AgyPoolBlockedError extends Error {\n readonly kind: PoolBlockedKind\n readonly blockedUntil: number\n\n constructor(kind: PoolBlockedKind, blockedUntil: number) {\n super(kind === 'quota-exhausted'\n ? 'All agy accounts have exhausted quota for the requested model family.'\n : 'All agy accounts are temporarily blocked for the requested model family.')\n this.name = 'AgyPoolBlockedError'\n this.kind = kind\n this.blockedUntil = blockedUntil\n }\n}\n\n/** Classified upstream failure kinds consumed by the rotation state machine. */\nexport type FailureKind =\n | 'rate-limit'\n | 'auth-failure'\n /**\n * Upstream asked for account verification (`VALIDATION_REQUIRED`) rather than\n * rejecting the credential. Deliberately distinct from `auth-failure` because\n * it is RECOVERABLE: the account is temporarily walled, not dead, and the user\n * can act on the returned URL. Treating it as `auth-failure` permanently\n * disabled a healthy account on a signal that meant \"come back after\n * verifying\", with no automatic way back.\n */\n | 'verification-required'\n | 'network-error'\n | 'project-error'\n | 'request-error'\n | 'transient'\n | 'proxy-unreachable'\n\n/**\n * Rotation state machine decision for one failed attempt.\n *\n * `backoffMs` is **advisory**, and for `retry`/`rotate` no caller consumes it.\n * It is not the mechanism that paces the pool: the account-level `cool` paths\n * write it into `coolingDownUntil` themselves, `rotate` blocks the failed\n * account through `rateLimitResetTimes`, and the delay before the retry of a\n * single request belongs to DSH's retry policy (`providerRetryAfterMs`, else its\n * own exponential `localDelay`). Do NOT \"wire it up\" by feeding it into\n * `providerRetryAfterMs`: a tier above DSH's `maxDelayMs` makes the normal retry\n * mode give up entirely, turning a recoverable 5xx into a failed turn.\n */\nexport type RotationAction =\n | { action: 'retry'; backoffMs: number }\n | { action: 'cool'; backoffMs: number }\n | { action: 'rotate'; backoffMs: number }\n | { action: 'revoke' }\n /** Permanent request-construction error: no state change, surface as-is. */\n | { action: 'noop' }\n","/**\n * Antigravity (agy) OAuth and API constants.\n *\n * The client id/secret below are the public Google consumer-OAuth credentials\n * shipped inside the Antigravity desktop product and its `agy` CLI; they are\n * embedded in many public tools (see NOTICE.md). They are not secrets owned by\n * this project.\n */\n\nimport { proxiedFetch } from '../proxy.ts'\nimport { isProxyRouted } from '../types.ts'\nimport type { AccountRouting } from '../types.ts'\n\nexport const AGY_CLIENT_ID =\n '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com'\n\nexport const AGY_CLIENT_SECRET = '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'\n\n/**\n * Effective OAuth client credentials: AGY_CLIENT_ID / AGY_CLIENT_SECRET env\n * overrides win when set (BYO OAuth app escape hatch, mirrors pi-antigravity);\n * otherwise the embedded public Antigravity credentials are used.\n */\nexport function resolveAgyClientCredentials(overrideClientId?: string): { clientId: string; clientSecret: string } {\n if (overrideClientId) {\n if (overrideClientId === AGY_CLIENT_ID) {\n return { clientId: AGY_CLIENT_ID, clientSecret: AGY_CLIENT_SECRET }\n }\n return {\n clientId: overrideClientId,\n clientSecret: process.env.AGY_CLIENT_SECRET || AGY_CLIENT_SECRET,\n }\n }\n return {\n clientId: process.env.AGY_CLIENT_ID || AGY_CLIENT_ID,\n clientSecret: process.env.AGY_CLIENT_SECRET || AGY_CLIENT_SECRET,\n }\n}\n\n/** Required scopes. `openid` must NOT be added: it routes Google into the hanging\n * `firstparty/nativeapp` consent for this client (verified by OmniRoute). */\nexport const AGY_SCOPES: readonly string[] = [\n 'https://www.googleapis.com/auth/cloud-platform',\n 'https://www.googleapis.com/auth/userinfo.email',\n 'https://www.googleapis.com/auth/userinfo.profile',\n 'https://www.googleapis.com/auth/cclog',\n 'https://www.googleapis.com/auth/experimentsandconfigs',\n]\n\nexport const OAUTH_AUTHORIZE_URL = 'https://accounts.google.com/o/oauth2/v2/auth'\nexport const OAUTH_TOKEN_URL = 'https://oauth2.googleapis.com/token'\nexport const OAUTH_USERINFO_URL = 'https://www.googleapis.com/oauth2/v1/userinfo'\n\n/** Default loopback callback used by the standalone CLI listener (fixed port, like opencode). */\nexport const AGY_DEFAULT_REDIRECT_URI = 'http://localhost:51121/oauth-callback'\n\n/**\n * Antigravity API endpoints. The daily runtime host (no .sandbox suffix) is the\n * live endpoint for consumer OAuth accounts — cloudcode-pa.googleapis.com\n * answers RESOURCE_EXHAUSTED for them (verified by live probe), while the\n * daily host answers 200. Order matters: first reachable non-429/403/503 wins.\n */\nexport const AGY_ENDPOINT_DAILY = 'https://daily-cloudcode-pa.googleapis.com'\nexport const AGY_ENDPOINT_PROD = 'https://cloudcode-pa.googleapis.com'\nexport const AGY_ENDPOINT_DAILY_SANDBOX = 'https://daily-cloudcode-pa.sandbox.googleapis.com'\nexport const AGY_ENDPOINT_AUTOPUSH = 'https://autopush-cloudcode-pa.sandbox.googleapis.com'\n\n/** Runtime/bootstrap endpoint fallback order (daily first, mirroring OmniRoute). */\nexport const AGY_ENDPOINT_FALLBACKS: readonly string[] = [\n AGY_ENDPOINT_DAILY,\n AGY_ENDPOINT_PROD,\n AGY_ENDPOINT_DAILY_SANDBOX,\n AGY_ENDPOINT_AUTOPUSH,\n]\n\n/**\n * Statuses that mean \"this endpoint is not usable for this attempt\"; skip to the\n * next one in the chain. 429/403 = rate/quota/entitlement wall, 503 = capacity\n * rejection (e.g. \"No capacity available for model ...\"). A 503 skipped here\n * still reaches the caller when every endpoint fails, where the classifier marks\n * it transient and the adapter surfaces it as a retryable SERVER error.\n */\nexport const AGY_ENDPOINT_SKIP_STATUSES = new Set([429, 403, 503])\n\n/** Routing of the requests being tried (see {@link AccountRouting}). */\nexport type AgyFallbackOptions = Pick<AccountRouting, 'proxyUrl'>\n/**\n * Try each runtime endpoint in order, skipping unusable ones (429/403/503/network).\n * Returns the first other response (2xx or a real error like 400/401); when\n * every endpoint is unusable, returns the last skipped response so the caller's\n * classifier can still produce a meaningful error (a returned 503 becomes a\n * retryable SERVER failure rather than being swallowed here).\n *\n * When every endpoint fails at the network level, the LAST error is rethrown so\n * its cause survives (a generic \"all endpoints failed\" hid DNS/TLS/timeout).\n */\nexport async function fetchAgyFirstOk(\n urlPath: string,\n init: RequestInit,\n fetchImpl: typeof fetch = proxiedFetch,\n options: AgyFallbackOptions = {},\n): Promise<Response> {\n let lastSkipped: Response | null = null\n let lastNetworkError: unknown\n for (const baseEndpoint of AGY_ENDPOINT_FALLBACKS) {\n try {\n const response = await fetchImpl(`${baseEndpoint}${urlPath}`, init)\n if (!AGY_ENDPOINT_SKIP_STATUSES.has(response.status)) return response\n lastSkipped = response\n } catch (error) {\n lastNetworkError = error\n // Fail-closed only while an explicit per-account proxy is in effect: the\n // request must not be retried on another endpoint or go direct.\n if (isProxyRouted(options)) {\n const { isProxyUnreachableError } = await import('../proxy.ts')\n if (isProxyUnreachableError(error)) throw error\n }\n // network error — try the next endpoint\n }\n }\n if (lastSkipped) return lastSkipped\n if (lastNetworkError !== undefined) throw lastNetworkError\n throw new Error('all agy endpoints failed')\n}\n\n/**\n * Pinned Antigravity version, used only until the runtime resolver has published\n * a fresh one (see {@link setResolvedAgyVersion}).\n *\n * A stale version string is the most detectable fingerprint anomaly, so this\n * value is a cold-start floor, not the version anything should normally send.\n * It tracks the release feed's newest entry; refresh it when the feed moves.\n *\n * NAMESPACE: this is the **Antigravity CLI** line (`google-antigravity/antigravity-cli`),\n * not the IDE or hub line, because that is the product this client claims to be\n * (`docs/official-identity.json`). The three lines are separate version\n * namespaces — IDE `2.0.0`, hub `2.15.1`, CLI `1.2.9` — so comparing them\n * numerically is meaningless and mixing them advertises a version that does not\n * exist for the product we name.\n */\nexport const AGY_VERSION_FALLBACK = '1.2.9'\n\n/**\n * `ClientMetadata.platform` value: the proto enum NAME, as protobuf-JSON emits it.\n *\n * The official enumeration is `PLATFORM_UNSPECIFIED | DARWIN_AMD64 | DARWIN_ARM64\n * | LINUX_AMD64 | LINUX_ARM64 | WINDOWS_AMD64` (read from the installed official\n * CLI's own descriptor). The earlier `\"MACOS\"` that this backend rejected with\n * `INVALID_ARGUMENT` was an invalid *value*, not a forbidden field — a\n * distinction that mattered, because it had been read as \"send `ideType` only\".\n */\nexport const AGY_PLATFORM_ENUM = 'DARWIN_ARM64'\n\n/**\n * Newest Antigravity version the runtime resolver has observed, or undefined\n * before its first success.\n *\n * Deliberately lives in this module — the dependency leaf — rather than in\n * `runtime/version.ts`. `oauth/` must not import `runtime/` (AGENTS.md), and the\n * edge already runs the other way: `runtime/version.ts` imports THIS file. So the\n * resolver publishes here and every User-Agent builder reads here.\n *\n * This shape is the fix for a real defect: the version used to be a function\n * parameter, and all six bootstrap call sites silently took the default, pinning\n * every control-plane request to the stale fallback while the data plane used the\n * resolved version — one process presenting two different clients. Reading a\n * shared value makes that drift unrepresentable rather than merely discouraged.\n */\nlet resolvedAgyVersion: string | undefined\n\n/** Publish a freshly resolved version for every User-Agent builder. */\nexport function setResolvedAgyVersion(version: string | undefined): void {\n resolvedAgyVersion = version\n}\n\n/** Version a User-Agent should advertise: the resolved one, else the pinned floor. */\nexport function currentAgyVersion(): string {\n return resolvedAgyVersion ?? AGY_VERSION_FALLBACK\n}\n\n/** Electron-style UA used for bootstrap calls (loadCodeAssist/onboardUser). */\nexport function getAgyBootstrapUserAgent(version = currentAgyVersion()): string {\n return `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Antigravity/${version} Chrome/138.0.7204.235 Electron/37.3.1 Safari/537.36`\n}\n\n/**\n * Short-form client User-Agent: `antigravity/<version> <platform>`.\n *\n * Used where the full Electron string would be inappropriate but the request\n * must still not self-identify as this tool (e.g. the version feeds, which the\n * official client ecosystem also polls).\n */\nexport function antigravityUserAgent(version = currentAgyVersion(), platform = 'darwin/arm64'): string {\n return `antigravity/${version} ${platform}`\n}\n\n/**\n * `ClientMetadata.ideType` value.\n *\n * `ANTIGRAVITY` is retained rather than switched to the enumeration's newer\n * `GEMINI_CLI`: the CLI's own `IdeType` is injected through its auth provider and\n * has not been captured, so the established working value is kept and the\n * alternative recorded (`docs/official-identity.json`) rather than guessed at.\n */\nexport const AGY_IDE_TYPE = 'ANTIGRAVITY'\n"],"mappings":";;;;AAmMA,SAAgB,cAAc,SAA8C;CAC1E,OAAO,QAAQ,SAAS,QAAQ;AAClC;;;;;AA4DA,IAAa,eAAb,cAAkC,MAAM;CACtC;CAEA,YAAY,MAAwB,SAAiB,SAAwB;EAC3E,MAAM,SAAS,OAAO;EACtB,KAAK,OAAO;EACZ,KAAK,OAAO;CACd;AACF;;;;;AASA,IAAa,sBAAb,cAAyC,MAAM;CAC7C;CACA;CAEA,YAAY,MAAuB,cAAsB;EACvD,MAAM,SAAS,oBACX,0EACA,0EAA0E;EAC9E,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,KAAK,eAAe;CACtB;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACjRA,MAAa,gBACX;AAEF,MAAa,oBAAoB;;;;;;AAOjC,SAAgB,4BAA4B,kBAAuE;CACjH,IAAI,kBAAkB;EACpB,IAAI,qBAAA,6EACF,OAAO;GAAE,UAAU;GAAe,cAAc;EAAkB;EAEpE,OAAO;GACL,UAAU;GACV,cAAc,QAAQ,IAAI,qBAAA;EAC5B;CACF;CACA,OAAO;EACL,UAAU,QAAQ,IAAI,iBAAA;EACtB,cAAc,QAAQ,IAAI,qBAAA;CAC5B;AACF;;;AAIA,MAAa,aAAgC;CAC3C;CACA;CACA;CACA;CACA;AACF;AAEA,MAAa,sBAAsB;AACnC,MAAa,kBAAkB;AAC/B,MAAa,qBAAqB;;AAGlC,MAAa,2BAA2B;;;;;;;AAQxC,MAAa,qBAAqB;AAClC,MAAa,oBAAoB;AACjC,MAAa,6BAA6B;AAC1C,MAAa,wBAAwB;;AAGrC,MAAa,yBAA4C;CACvD;CACA;CACA;CACA;AACF;;;;;;;;AASA,MAAa,6CAA6B,IAAI,IAAI;CAAC;CAAK;CAAK;AAAG,CAAC;;;;;;;;;;;AAcjE,eAAsB,gBACpB,SACA,MACA,YAA0B,cAC1B,UAA8B,CAAC,GACZ;CACnB,IAAI,cAA+B;CACnC,IAAI;CACJ,KAAK,MAAM,gBAAgB,wBACzB,IAAI;EACF,MAAM,WAAW,MAAM,UAAU,GAAG,eAAe,WAAW,IAAI;EAClE,IAAI,CAAC,2BAA2B,IAAI,SAAS,MAAM,GAAG,OAAO;EAC7D,cAAc;CAChB,SAAS,OAAO;EACd,mBAAmB;EAGnB,IAAI,cAAc,OAAO,GAAG;GAC1B,MAAM,EAAE,4BAA4B,MAAM,OAAO,uBAAc,CAAA,MAAA,MAAA,EAAA,CAAA;GAC/D,IAAI,wBAAwB,KAAK,GAAG,MAAM;EAC5C;CAEF;CAEF,IAAI,aAAa,OAAO;CACxB,IAAI,qBAAqB,KAAA,GAAW,MAAM;CAC1C,MAAM,IAAI,MAAM,0BAA0B;AAC5C;;;;;;;;;;;;;;;;AAiBA,MAAa,uBAAuB;;;;;;;;;;AAWpC,MAAa,oBAAoB;;;;;;;;;;;;;;;;AAiBjC,IAAI;;AAGJ,SAAgB,sBAAsB,SAAmC;CACvE,qBAAqB;AACvB;;AAGA,SAAgB,oBAA4B;CAC1C,OAAO,sBAAA;AACT;;AAGA,SAAgB,yBAAyB,UAAU,kBAAkB,GAAW;CAC9E,OAAO,gGAAgG,QAAQ;AACjH;;;;;;;;AASA,SAAgB,qBAAqB,UAAU,kBAAkB,GAAG,WAAW,gBAAwB;CACrG,OAAO,eAAe,QAAQ,GAAG;AACnC;;;;;;;;;AAUA,MAAa,eAAe"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"quota-summary-5CypVffd.mjs","names":[],"sources":["../src/adapter/quota-summary.ts"],"sourcesContent":["/**\n * `v1internal:retrieveUserQuotaSummary` — the grouped 5-hour / weekly quota\n * windows, as distinct from `fetchAvailableModels`' per-model `quotaInfo`.\n *\n * WHY THIS ENDPOINT EXISTS HERE AT ALL. `fetchAvailableModels` reports only\n * `remainingFraction` + `resetTime` per model, with NO window field: measured\n * across every id, the only keys under `quotaInfo` are those two. So a \"5-hour\n * vs weekly\" view is not derivable from it, and the summary endpoint is the only\n * source for both windows. Recorded in docs/ANTIGRAVITY-API.md §3, which\n * previously stated the opposite (\"no retrieveUserQuota path\").\n *\n * The shapes differ meaningfully and must not be merged:\n * - `fetchAvailableModels` -> per MODEL (used for ranking/rotation)\n * - `retrieveUserQuotaSummary` -> per GROUP (Gemini, Claude+GPT), 2 windows each\n * The group split is upstream's own, so it is carried through verbatim rather\n * than re-derived from model-id prefixes: `3p-*` covers Claude AND GPT, which no\n * prefix rule in `modelFamilyOf` would group together.\n *\n * Measured response (live account):\n * { groups: [ { displayName, buckets: [\n * { bucketId: \"gemini-5h\", window: \"5h\", remainingFraction, resetTime },\n * { bucketId: \"gemini-weekly\", window: \"weekly\", ... } ] } ] }\n *\n * The identity used is the same bootstrap User-Agent as `fetchAvailableModels`\n * (that pair is what answers 200 here), so nothing new is impersonated.\n */\n\nimport { AGY_ENDPOINT_FALLBACKS, getAgyBootstrapUserAgent } from '../oauth/constants.ts'\nimport { proxiedFetch } from '../proxy.ts'\nimport type { QuotaGroup, QuotaWindow } from '../types.ts'\n\nexport type { QuotaGroup, QuotaWindow } from '../types.ts'\n\nexport const QUOTA_SUMMARY_PATH = '/v1internal:retrieveUserQuotaSummary'\n\n/** The raw response, structurally. */\ninterface RawQuotaSummary {\n groups?: Array<{\n displayName?: unknown\n buckets?: Array<{\n bucketId?: unknown\n window?: unknown\n remainingFraction?: unknown\n resetTime?: unknown\n }>\n }>\n}\n\n/**\n * Sort rank for an upstream window token: shorter windows first.\n *\n * An explicit table so the order states the intended duration ordering rather\n * than inferring it from string length. Unknown tokens rank last (and tie-break\n * alphabetically) so a window added upstream later is still shown, just after\n * the ones we understand.\n */\nfunction windowRank(window: string): number {\n const known: Record<string, number> = { '5h': 0, daily: 1, weekly: 2, monthly: 3 }\n return known[window] ?? Number.MAX_SAFE_INTEGER\n}\n\n/** Clamp to 0..1; a non-number is \"unknown\" rather than zero. */\nfunction fractionOf(raw: unknown): number | null {\n if (typeof raw !== 'number' || !Number.isFinite(raw)) return null\n return Math.max(0, Math.min(1, raw))\n}\n\n/** A non-empty string, else null. */\nfunction stringOf(raw: unknown): string | null {\n return typeof raw === 'string' && raw !== '' ? raw : null\n}\n\n/**\n * Parse a summary payload into groups.\n *\n * Defensive like the rest of the wire parsing: a group with no usable window is\n * DROPPED rather than rendered as an empty card, because a group row with no\n * windows tells the user nothing and would push a real group off screen. Buckets\n * are ordered with the shorter window first so a group reads \"immediate pressure,\n * then the weekly budget\" consistently regardless of upstream order.\n */\nexport function parseQuotaSummary(raw: unknown): QuotaGroup[] {\n const data = raw as RawQuotaSummary | null\n if (typeof data !== 'object' || data === null) return []\n const groups: QuotaGroup[] = []\n for (const group of Array.isArray(data.groups) ? data.groups : []) {\n if (typeof group !== 'object' || group === null) continue\n const windows: QuotaWindow[] = []\n for (const bucket of Array.isArray(group.buckets) ? group.buckets : []) {\n if (typeof bucket !== 'object' || bucket === null) continue\n const bucketId = stringOf(bucket.bucketId)\n const window = stringOf(bucket.window)\n if (bucketId === null || window === null) continue\n windows.push({\n bucketId,\n window,\n remainingFraction: fractionOf(bucket.remainingFraction),\n resetTime: stringOf(bucket.resetTime),\n })\n }\n if (windows.length === 0) continue\n // Shortest window first, by an explicit table rather than by token length.\n // Length happens to order today's vocabulary correctly (`5h` < `weekly`), and\n // would even survive `daily`/`monthly`, but it ranks by a coincidence of\n // spelling instead of by the duration the token names — `daily` (5 chars)\n // would sort BEFORE `weekly` (6) for the wrong reason. An unknown token sorts\n // last, so a window upstream adds later appears after the known ones instead\n // of landing somewhere arbitrary.\n windows.sort((a, b) => windowRank(a.window) - windowRank(b.window) || a.window.localeCompare(b.window))\n groups.push({ name: stringOf(group.displayName) ?? '', windows })\n }\n return groups\n}\n\n/**\n * Fetch and parse the grouped windows for one account.\n *\n * Mirrors `fetchAvailableModels`: the endpoint fallback chain is walked here, and\n * the caller injects only the routed+time-bounded fetch, so the account's proxy\n * is honoured (omitting it would go direct and leak the account's real IP) and\n * one shared budget bounds both calls.\n *\n * @param accessToken - a live access token for the account.\n * @param projectId - the account's Cloud Code project id, when known.\n * @param fetchImpl - routed, time-bounded fetch implementation.\n * @returns the parsed groups; empty when no endpoint answers (never throws, so a\n * summary failure cannot discard the ranking data fetched alongside it).\n */\nexport async function fetchQuotaSummary(\n accessToken: string,\n projectId: string | undefined,\n fetchImpl: typeof fetch = proxiedFetch,\n): Promise<QuotaGroup[]> {\n const body = JSON.stringify(projectId ? { project: projectId } : {})\n for (const baseEndpoint of AGY_ENDPOINT_FALLBACKS) {\n try {\n const response = await fetchImpl(`${baseEndpoint}${QUOTA_SUMMARY_PATH}`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n 'User-Agent': getAgyBootstrapUserAgent(),\n },\n body,\n })\n if (!response.ok) continue\n return parseQuotaSummary(await response.json())\n } catch {\n // Try the next endpoint; the caller treats an empty result as \"unknown\".\n }\n }\n return []\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,MAAa,qBAAqB;;;;;;;;;AAuBlC,SAAS,WAAW,QAAwB;CAE1C,OAAO;EADiC,MAAM;EAAG,OAAO;EAAG,QAAQ;EAAG,SAAS;CACpE,EAAE,WAAW,OAAO;AACjC;;AAGA,SAAS,WAAW,KAA6B;CAC/C,IAAI,OAAO,QAAQ,YAAY,CAAC,OAAO,SAAS,GAAG,GAAG,OAAO;CAC7D,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,GAAG,CAAC;AACrC;;AAGA,SAAS,SAAS,KAA6B;CAC7C,OAAO,OAAO,QAAQ,YAAY,QAAQ,KAAK,MAAM;AACvD;;;;;;;;;;AAWA,SAAgB,kBAAkB,KAA4B;CAC5D,MAAM,OAAO;CACb,IAAI,OAAO,SAAS,YAAY,SAAS,MAAM,OAAO,CAAC;CACvD,MAAM,SAAuB,CAAC;CAC9B,KAAK,MAAM,SAAS,MAAM,QAAQ,KAAK,MAAM,IAAI,KAAK,SAAS,CAAC,GAAG;EACjE,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM;EACjD,MAAM,UAAyB,CAAC;EAChC,KAAK,MAAM,UAAU,MAAM,QAAQ,MAAM,OAAO,IAAI,MAAM,UAAU,CAAC,GAAG;GACtE,IAAI,OAAO,WAAW,YAAY,WAAW,MAAM;GACnD,MAAM,WAAW,SAAS,OAAO,QAAQ;GACzC,MAAM,SAAS,SAAS,OAAO,MAAM;GACrC,IAAI,aAAa,QAAQ,WAAW,MAAM;GAC1C,QAAQ,KAAK;IACX;IACA;IACA,mBAAmB,WAAW,OAAO,iBAAiB;IACtD,WAAW,SAAS,OAAO,SAAS;GACtC,CAAC;EACH;EACA,IAAI,QAAQ,WAAW,GAAG;EAQ1B,QAAQ,MAAM,GAAG,MAAM,WAAW,EAAE,MAAM,IAAI,WAAW,EAAE,MAAM,KAAK,EAAE,OAAO,cAAc,EAAE,MAAM,CAAC;EACtG,OAAO,KAAK;GAAE,MAAM,SAAS,MAAM,WAAW,KAAK;GAAI;EAAQ,CAAC;CAClE;CACA,OAAO;AACT;;;;;;;;;;;;;;;AAgBA,eAAsB,kBACpB,aACA,WACA,YAA0B,cACH;CACvB,MAAM,OAAO,KAAK,UAAU,YAAY,EAAE,SAAS,UAAU,IAAI,CAAC,CAAC;CACnE,KAAK,MAAM,gBAAgB,wBACzB,IAAI;EACF,MAAM,WAAW,MAAM,UAAU,GAAG,eAAe,sBAAsB;GACvE,QAAQ;GACR,SAAS;IACP,eAAe,UAAU;IACzB,gBAAgB;IAChB,cAAc,yBAAyB;GACzC;GACA;EACF,CAAC;EACD,IAAI,CAAC,SAAS,IAAI;EAClB,OAAO,kBAAkB,MAAM,SAAS,KAAK,CAAC;CAChD,QAAQ,CAER;CAEF,OAAO,CAAC;AACV"}
|