dsh-agy 0.2.11 → 0.3.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.
Files changed (46) hide show
  1. package/README.md +67 -11
  2. package/cordis.patch.yml +4 -2
  3. package/lib/{accounts-C0Oj3tQA.mjs → accounts-CkaHhb5H.mjs} +1155 -646
  4. package/lib/accounts-CkaHhb5H.mjs.map +1 -0
  5. package/lib/{catalog-bfGIyoba.mjs → catalog-BgdDVJEB.mjs} +17 -2
  6. package/lib/{catalog-bfGIyoba.mjs.map → catalog-BgdDVJEB.mjs.map} +1 -1
  7. package/lib/cli/index.d.mts +10 -1
  8. package/lib/cli/index.d.mts.map +1 -1
  9. package/lib/cli/index.mjs +21 -8
  10. package/lib/cli/index.mjs.map +1 -1
  11. package/lib/client.js +2168 -14
  12. package/lib/{constants-BFKYjOmh.mjs → constants-Db4tmyfb.mjs} +79 -14
  13. package/lib/constants-Db4tmyfb.mjs.map +1 -0
  14. package/lib/{exchange-dQm2mjJ5.mjs → exchange-yZPIZumk.mjs} +27 -12
  15. package/lib/exchange-yZPIZumk.mjs.map +1 -0
  16. package/lib/identity-Ci8EGc1F.mjs +96 -0
  17. package/lib/identity-Ci8EGc1F.mjs.map +1 -0
  18. package/lib/{import-D7R0GpRr.mjs → import-BaFULQnH.mjs} +11 -7
  19. package/lib/import-BaFULQnH.mjs.map +1 -0
  20. package/lib/index.mjs +2 -2
  21. package/lib/{models-ClGTQBLY.mjs → models-BGuwl50d.mjs} +35 -22
  22. package/lib/models-BGuwl50d.mjs.map +1 -0
  23. package/lib/plugin-common-B1MG00JT.mjs +1313 -0
  24. package/lib/plugin-common-B1MG00JT.mjs.map +1 -0
  25. package/lib/{proxy-DLvX8yxv.mjs → proxy-DfaL73mL.mjs} +52 -3
  26. package/lib/proxy-DfaL73mL.mjs.map +1 -0
  27. package/lib/quota-summary-5CypVffd.mjs +127 -0
  28. package/lib/quota-summary-5CypVffd.mjs.map +1 -0
  29. package/lib/{translate-pdeVGFpT.mjs → translate-BQu8HLvB.mjs} +57 -26
  30. package/lib/translate-BQu8HLvB.mjs.map +1 -0
  31. package/lib/web/plugin.d.mts +1 -0
  32. package/lib/web/plugin.d.mts.map +1 -1
  33. package/lib/web/plugin.mjs +651 -1701
  34. package/lib/web/plugin.mjs.map +1 -1
  35. package/package.json +17 -4
  36. package/lib/accounts-C0Oj3tQA.mjs.map +0 -1
  37. package/lib/constants-BFKYjOmh.mjs.map +0 -1
  38. package/lib/exchange-dQm2mjJ5.mjs.map +0 -1
  39. package/lib/identity-DsftHTJC.mjs +0 -35
  40. package/lib/identity-DsftHTJC.mjs.map +0 -1
  41. package/lib/import-D7R0GpRr.mjs.map +0 -1
  42. package/lib/models-ClGTQBLY.mjs.map +0 -1
  43. package/lib/plugin-common-DtF_PyxH.mjs +0 -254
  44. package/lib/plugin-common-DtF_PyxH.mjs.map +0 -1
  45. package/lib/proxy-DLvX8yxv.mjs.map +0 -1
  46. package/lib/translate-pdeVGFpT.mjs.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-common-B1MG00JT.mjs","names":[],"sources":["../src/adapter/adapter.ts","../src/model-visibility.ts","../src/thinking-types.ts","../src/thinking-budget.ts","../src/usage-types.ts","../src/stats.ts","../src/plugin-common.ts"],"sourcesContent":["/**\n * AgyAdapter: the DSH seam. A thin orchestrator over the deep modules —\n * account session resolution (shell-provided), request translation, SSE\n * parsing, failure classification, and rotation reporting. All wire details\n * live in translate.ts / parse.ts / models.ts.\n */\n\nimport {\n LlmAdapter,\n LlmError,\n ProviderRequestId,\n QUOTA_EXCEEDED_CODE,\n} from '@deepseek-ai/dsh-llm'\nimport type {\n GenerateOptions,\n LlmModelInfo,\n LlmProviderInfo,\n LlmResolvedModelInfo,\n StreamChunk,\n ToolSchema,\n} from '@deepseek-ai/dsh-llm'\n\nimport { AgyAuthError, AgyPoolBlockedError } from '../types.ts'\nimport type { AgyAccountSession, FailureKind, ManagedAccount, OAuthAuthDetails } from '../types.ts'\nimport type { RateLimitCategory } from '../runtime/classify.ts'\nimport { fetchAgyFirstOk } from '../oauth/constants.ts'\nimport {\n classifyFetchError,\n classifyHttpError,\n describeFetchError,\n isSessionAccumulationOverflow,\n} from '../runtime/classify.ts'\nimport { accountFetch } from '../proxy.ts'\nimport {\n bumpSessionGeneration,\n currentSessionGeneration,\n deriveAntigravitySessionId,\n generateAntigravityRequestId,\n} from '../runtime/identity.ts'\nimport { setThoughtSignature } from '../runtime/signature-cache.ts'\nimport { toAgyRequestBody } from './translate.ts'\nimport type { AgyResolvedImage } from './translate.ts'\nimport { resolveMultimodalFiles } from './multimodal.ts'\nimport { parseAgySse } from './parse.ts'\nimport { AGY_PROVIDER, catalogModelList, listAgyModels, resolveAgyModel } from './models.ts'\n\nexport type { AgyAccountSession }\n\n/**\n * Structural view of the harness attachment service (ctx.attachments).\n * Deliberately not an import of @deepseek-ai/dsh-attachment: the CLI bundle\n * must stay free of harness runtime dependencies, and the real store\n * satisfies this shape.\n */\nexport interface AgyAttachmentStore {\n readImage(ref: {\n attachmentId: string\n mediaType: string\n }): Promise<{ ref: { mediaType: string }; data: Uint8Array }>\n}\n\n/** Collect image refs from user-message content only (spec scope: user images; tool-result nesting out of scope). */\nfunction collectImageRefs(options: GenerateOptions): Array<{ attachmentId: string; mediaType: string }> {\n const refs: Array<{ attachmentId: string; mediaType: string }> = []\n for (const message of options.messages) {\n if (message.role !== 'user') continue\n for (const block of message.content) {\n if (block.type === 'image') refs.push(block.attachment)\n }\n }\n return refs\n}\n\nexport interface AgyAdapterOptions {\n /**\n * Resolve the active account for a request (model-aware: family-scoped quota\n * ranking). `conversationKey` scopes account affinity, so concurrent\n * conversations hold independent pins.\n */\n getSession(model?: string, conversationKey?: string): Promise<AgyAccountSession | undefined>\n /** Report a classified upstream failure so the shell can cool/rotate/revoke. */\n reportFailure(\n kind: FailureKind,\n session: AgyAccountSession,\n info?: {\n retryAfterMs?: number\n status?: number\n rateLimitCategory?: RateLimitCategory\n /** Server-reported absolute reset time; drives precise cooldowns. */\n resetTime?: string\n /** Requested model id; drives family-scoped rate-limit bookkeeping. */\n model?: string\n /** Appeal link from a `verification-required` body; surfaced to the user. */\n verificationUrl?: string\n },\n ): Promise<void>\n /** Report a clean stream completion (resets the failure counter). */\n markSuccess?(session: AgyAccountSession): Promise<void>\n /**\n * Configured token budget for a reasoning level (see `thinking-budget.ts`).\n *\n * Supplied as a resolver rather than a snapshot so an edit in the settings UI\n * applies to the next request without rebuilding the adapter.\n *\n * @param level - the lowercased effort id (`low`/`medium`/`high`).\n * @returns the budget to send, or undefined to let the level stand alone.\n */\n thinkingBudgetFor?(level: string): number | undefined\n /**\n * Configured Claude thinking budget, or undefined when unset.\n *\n * Separate from `thinkingBudgetFor` because the Claude family is id-bound and\n * carries its own validation (floor 1024, and `max_tokens` must exceed it).\n */\n claudeBudgetFor?(): number | undefined\n /** Budget for the tiered slot (the selector's \"Default\" effort). */\n tieredBudgetFor?(): number | undefined\n /** Resolve the harness attachment store; undefined outside the harness (standalone CLI). */\n resolveAttachments?(): AgyAttachmentStore | undefined\n /**\n * In-flight accounting for per-account request fan-out. Takes the account\n * rather than a key so the session manager derives the identity itself —\n * `accountKey` (id-first) and `ledgerAccountKey` (email-first) differ, and\n * mixing them would count against an account nobody selects.\n *\n * `noteRequestSettled` MUST run on every path, including an abandoned stream:\n * the adapter calls it from a `finally`, which async generators run on\n * completion, error, and early consumer termination alike.\n */\n noteRequestStarted?(account: ManagedAccount): void\n noteRequestSettled?(account: ManagedAccount): void\n /**\n * Hidden-model lookup. Optional so an adapter stays constructible without the\n * settings layer (tests, standalone use); absent means nothing is hidden.\n */\n modelVisibility?: { disabledFor(provider: string): ReadonlySet<string> }\n /**\n * Record one request's usage. Optional by design: the CLI must be able to\n * build an adapter without the stats ledger, and a statistics failure must\n * never break a generation.\n */\n recordUsage?(record: {\n account?: string\n model?: string\n usage?: { input: number; output: number; cacheRead: number; cacheWrite: number }\n ok: boolean\n latencyMs?: number\n ttftMs?: number\n }): void\n}\n\nconst UPSTREAM_ERROR_CODE = 'UPSTREAM'\n/** First-class DSH retryable code: the default retry policy honors SERVER (5xx), not UPSTREAM. */\nconst SERVER_ERROR_CODE = 'SERVER'\n\n/** Stable ledger key for an account: email when present, else the generated id. */\nfunction ledgerAccountKey(session: AgyAccountSession): string | undefined {\n return session.account.email ?? session.account.id\n}\n\n/**\n * Build the impersonation headers for one request.\n *\n * There is NO request-id header. `x-goog-request-id` used to be sent here and is\n * present in neither official binary; the id the backend correlates on is the\n * body's `requestId` field, which this request already carries. An invented\n * header is a shape no official client emits, which is precisely the anomaly the\n * impersonation set exists to avoid.\n *\n * `User-Agent` carries the Antigravity client string and NOTHING else. The\n * harness's `attributionHeaders()` is deliberately not merged in: it returns a\n * lowercase `user-agent` key, so spreading it alongside the camel-case\n * `session.impersonation` produced TWO distinct object properties that `fetch`\n * folded into one comma-joined value —\n *\n * `deepseek-harness/<v> (+url), antigravity/<v> <platform>`\n *\n * — a header no official client can emit, byte-identical for every dsh-agy\n * user, and therefore a stronger fingerprint than the one it was trying to\n * avoid. There is exactly one `User-Agent` field on the wire and the upstream\n * requires it to be the client identity, so this request cannot carry both.\n *\n * This adapter performs its own dispatch (see the `fetchAgyFirstOk` call in\n * `stream()`); nothing downstream re-adds the header.\n */\nexport function buildRequestHeaders(session: AgyAccountSession): Record<string, string> {\n return {\n authorization: `Bearer ${session.auth.access}`,\n 'content-type': 'application/json',\n accept: 'text/event-stream',\n // Exactly the two impersonation HEADERS, named individually rather than\n // spread. `session.impersonation.clientMetadata` is a body message and must\n // not become a header — and it is deliberately not added to this envelope\n // either: the only `metadata` field the official descriptor evidences is on\n // the control-plane calls (`LoadCodeAssistRequest`/`OnboardUserRequest`), so\n // on this generate request the identity carrier is the body's existing\n // `userAgent`/`requestType` pair.\n 'User-Agent': session.impersonation['User-Agent'],\n 'X-Goog-Api-Client': session.impersonation['X-Goog-Api-Client'],\n }\n}\n\nexport class AgyAdapter extends LlmAdapter {\n private readonly options: AgyAdapterOptions\n\n constructor(options: AgyAdapterOptions) {\n super()\n this.options = options\n }\n\n override providerInfo(_provider: string): LlmProviderInfo {\n return { id: AGY_PROVIDER, name: 'Antigravity (agy)' }\n }\n\n /**\n * The catalog as DSH's model selector sees it: discovered models minus the\n * user's hidden set. Filtering here is what makes \"turn a model off\" hide it\n * from the picker — the selector reads this, and DSH itself is untouched.\n */\n override async listModels(_provider: string): Promise<readonly LlmModelInfo[]> {\n const all = await this.listAllModels()\n const hidden = this.options.modelVisibility?.disabledFor(AGY_PROVIDER)\n if (hidden === undefined || hidden.size === 0) return all\n return all.filter((model) => !hidden.has(model.id))\n }\n\n /**\n * The complete catalog, ignoring the user's hidden set.\n *\n * The settings page lists models through this rather than `listModels`: if it\n * read the filtered list, a hidden model would vanish from the page along\n * with the switch that hides it, leaving no way to turn it back on without\n * hand-editing the file.\n */\n async listAllModels(): Promise<readonly LlmModelInfo[]> {\n try {\n const session = await this.options.getSession()\n // Model discovery is account-scoped: route it through the account's proxy\n // (control-plane class, so the standard timeouts apply).\n const routing = { proxyUrl: session?.account.proxy }\n return await listAgyModels(session?.auth.access, session?.account.projectId, accountFetch(routing))\n } catch (error) {\n if (error instanceof AgyPoolBlockedError || error instanceof AgyAuthError) {\n return catalogModelList()\n }\n throw error\n }\n }\n\n override async resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo> {\n return resolveAgyModel(provider, model)\n }\n\n // `prepareCall` is deliberately NOT overridden: the LlmAdapter base class\n // (dsh-llm 0.1.1-rc.2+) already binds the resolved model to the stream, which\n // is exactly what this adapter would do. The base implementation also\n // forwards the cancellation signal, which the previous hand-written copy\n // dropped. Only dynamic adapters — those needing different capabilities per\n // generation — should override it.\n\n /**\n * Pre-resolve every image attachment into base64 bytes before translation.\n * Image input hard-fails with UNSUPPORTED_CONTENT (terminal, never retried)\n * when the store is missing or a read fails — silently dropping images and\n * sending text-only is the exact failure mode this path exists to prevent.\n */\n private async resolveRequestImages(options: GenerateOptions): Promise<Map<string, AgyResolvedImage>> {\n const refs = collectImageRefs(options)\n const images = new Map<string, AgyResolvedImage>()\n if (refs.length === 0) return images\n const store = this.options.resolveAttachments?.()\n if (!store) {\n throw new LlmError(\n 'agy image input requires the durable attachment service (in-harness plugin only)',\n 'UNSUPPORTED_CONTENT',\n )\n }\n // Read every attachment concurrently (N images cost one round-trip, not N).\n // allSettled rather than all: more than one read may reject, and the\n // surfaced error must be deterministic (first failure in ref order) instead\n // of whichever concurrent read happened to reject first — and no rejection\n // may escape as unhandled.\n const settled = await Promise.allSettled(\n refs.map(async (ref) => {\n const stored = await store.readImage(ref)\n return {\n attachmentId: ref.attachmentId,\n image: {\n mediaType: stored.ref.mediaType,\n data: Buffer.from(stored.data).toString('base64'),\n },\n }\n }),\n )\n const failedIndex = settled.findIndex((outcome) => outcome.status === 'rejected')\n if (failedIndex !== -1) {\n const ref = refs[failedIndex]!\n const cause: unknown = (settled[failedIndex] as PromiseRejectedResult).reason\n throw new LlmError(\n `agy image attachment \"${ref.attachmentId}\" could not be loaded: ${cause instanceof Error ? cause.message : String(cause)}`,\n 'UNSUPPORTED_CONTENT',\n { cause: cause instanceof Error ? cause : undefined },\n )\n }\n for (const outcome of settled) {\n if (outcome.status === 'fulfilled') {\n images.set(outcome.value.attachmentId, outcome.value.image)\n }\n }\n return images\n }\n\n override async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {\n // The in-flight slot must be released on EVERY path — clean completion, a\n // thrown error, and a consumer that abandons the stream mid-turn. An async\n // generator's `finally` is the one hook covering all three, so the slot is\n // released here rather than at each exit inside `streamInner`.\n const holder: { account?: ManagedAccount } = {}\n try {\n yield* this.streamInner(options, holder)\n } finally {\n if (holder.account !== undefined) this.options.noteRequestSettled?.(holder.account)\n }\n }\n\n private async *streamInner(\n options: GenerateOptions,\n holder: { account?: ManagedAccount },\n ): AsyncIterable<StreamChunk> {\n // Spec D1 sequence: resolve images first — a locally-failing image request\n // must surface UNSUPPORTED_CONTENT (user story 8) instead of being masked\n // by account-pool errors, and must not touch pool state at all.\n const images = await this.resolveRequestImages(options)\n /**\n * The DSH agent loop stamps `options.sessionId` on every request it builds,\n * so it is the conversation identity — used both to scope account affinity\n * here and to derive the upstream `sessionId` below.\n */\n const conversationKey = options.sessionId === undefined ? undefined : String(options.sessionId)\n let session: AgyAccountSession | undefined\n try {\n session = await this.options.getSession(options.model, conversationKey)\n } catch (error) {\n if (error instanceof AgyAuthError) {\n if (error.kind === 'transport') {\n throw new LlmError(error.message, 'TRANSPORT', { cause: error })\n }\n if (error.kind === 'rate-limit') {\n throw new LlmError(error.message, 'RATE_LIMIT', {\n requestId: ProviderRequestId(generateAntigravityRequestId()),\n })\n }\n throw new LlmError(error.message, 'INVALID_CREDENTIAL', { cause: error })\n }\n if (error instanceof AgyPoolBlockedError) {\n if (error.kind === 'quota-exhausted') {\n throw new LlmError(error.message, QUOTA_EXCEEDED_CODE)\n }\n const delta = Math.ceil(error.blockedUntil - Date.now())\n const providerRetryAfterMs = Number.isFinite(delta) && delta > 0 ? delta : 1\n throw new LlmError(error.message, 'RATE_LIMIT', {\n providerRetryAfterMs,\n requestId: ProviderRequestId(generateAntigravityRequestId()),\n })\n }\n throw error\n }\n if (!session) {\n throw new LlmError(\n 'No agy account configured — run `dsh-agy login` to authenticate.',\n 'NO_CREDENTIAL',\n )\n }\n\n const multimodalFiles = await resolveMultimodalFiles(options)\n // The account is now fixed for this request; take the in-flight slot that the\n // `stream` wrapper releases (see its comment for why release lives there).\n holder.account = session.account\n this.options.noteRequestStarted?.(session.account)\n // `conversationKey` is resolved above and shared with account affinity.\n // Standalone CLI callers leave `options.sessionId` unset, so the upstream\n // session id degrades to the per-account value rather than inventing a\n // conversation.\n const conversationAccount = ledgerAccountKey(session)\n /** Wall-clock origin for this attempt's latency figures. */\n const startedAt = Date.now()\n // Streaming dispatch: the account proxy MUST carry the generation request\n // (it carried only the control-plane calls before, so a proxied account\n // silently generated from the host's real IP), and the streaming\n // dispatcher drops the per-gap body timeout a reasoning pause would trip.\n const routing = { proxyUrl: session.account.proxy, streaming: true }\n\n /**\n * Send one request, resending at most once when the upstream reports the\n * per-`sessionId` accumulation wall.\n *\n * The upstream accumulates a conversation's input server-side per\n * `sessionId`; once that passes 1M tokens every request reusing the id fails\n * with a 400 until the upstream session expires. Bumping the generation\n * names a fresh upstream session and recovers the conversation. This is not\n * an account fault, so it does not go through `reportFailure`: the account\n * stays healthy and only the derived id changes.\n */\n const sendAttempt = async (): Promise<{ response: Response; bodyText?: string }> => {\n for (let attempt = 0; ; attempt++) {\n const generation = conversationKey !== undefined && conversationAccount !== undefined\n ? currentSessionGeneration(conversationAccount, conversationKey)\n : 0\n // One id per attempt, carried by the body. A resend under a bumped\n // session generation is a new upstream request, so it gets a new id.\n const requestId = generateAntigravityRequestId()\n const body = toAgyRequestBody(options, {\n projectId: session.account.projectId,\n sessionId:\n deriveAntigravitySessionId(session.account.email, conversationKey, generation) ?? undefined,\n requestId,\n ...(this.options.thinkingBudgetFor === undefined\n ? {}\n : { thinkingBudgetFor: this.options.thinkingBudgetFor }),\n ...(this.options.claudeBudgetFor === undefined\n ? {}\n : { claudeBudgetFor: this.options.claudeBudgetFor }),\n ...(this.options.tieredBudgetFor === undefined\n ? {}\n : { tieredBudgetFor: this.options.tieredBudgetFor }),\n ...(images.size > 0 ? { images } : {}),\n ...(multimodalFiles.size > 0 ? { multimodalFiles } : {}),\n })\n let response: Response\n try {\n response = await fetchAgyFirstOk(\n '/v1internal:streamGenerateContent?alt=sse',\n {\n method: 'POST',\n headers: buildRequestHeaders(session),\n body: JSON.stringify(body),\n signal: options.signal,\n },\n accountFetch(routing),\n routing,\n )\n } catch (error) {\n const classified = classifyFetchError(error, { proxyUrl: session.account.proxy })\n await this.options.reportFailure(classified.kind, session)\n // A transport failure consumed no tokens, but it is a real attempt\n // against this account's quota — record the request, not the usage.\n this.recordUsage(session, options.model, { ok: false }, startedAt)\n throw new LlmError(classified.message ?? 'agy fetch failed', 'TRANSPORT', { cause: error })\n }\n if (response.ok) return { response }\n const bodyText = await response.text().catch(() => undefined)\n if (\n attempt === 0\n && conversationKey !== undefined\n && conversationAccount !== undefined\n && isSessionAccumulationOverflow(response.status, bodyText)\n ) {\n bumpSessionGeneration(conversationAccount, conversationKey)\n continue\n }\n return { response, bodyText }\n }\n }\n\n const { response, bodyText } = await sendAttempt()\n\n if (!response.ok) {\n const classified = classifyHttpError(response.status, response.headers, bodyText)\n await this.options.reportFailure(classified.kind, session, {\n retryAfterMs: classified.retryAfterMs,\n status: response.status,\n rateLimitCategory: classified.rateLimitCategory,\n resetTime: classified.resetTime,\n model: options.model,\n verificationUrl: classified.verificationUrl,\n })\n this.recordUsage(session, options.model, {\n ok: false,\n rateLimited: classified.kind === 'rate-limit',\n }, startedAt)\n if (classified.kind === 'rate-limit') {\n // soft/rate limits are retryable by the harness (RATE_LIMIT + delay);\n // daily quota exhaustion is terminal (QUOTA, 24h cooldown already set).\n if (classified.rateLimitCategory === 'quota_exhausted') {\n throw new LlmError(\n `agy daily quota exhausted (${response.status}): ${classified.message ?? ''}`,\n QUOTA_EXCEEDED_CODE,\n )\n }\n throw new LlmError(\n `agy rate-limited (${response.status}): ${classified.message ?? ''}`,\n 'RATE_LIMIT',\n {\n providerRetryAfterMs: classified.retryAfterMs ?? undefined,\n requestId: ProviderRequestId(generateAntigravityRequestId()),\n },\n )\n }\n if (classified.kind === 'verification-required') {\n // Recoverable, so deliberately NOT INVALID_CREDENTIAL: the account is\n // parked for a timed window, not disabled, and the pool moves on. The\n // appeal link goes in the message because a message is the only channel\n // DSH surfaces to the user.\n //\n // Deliberately NO `providerRetryAfterMs`: the park IS the cooldown, and a\n // delay above DSH's `maxDelayMs` makes its `normal` retry mode give up\n // outright (`llm-retry`: `providerRetryAfterMs > maxDelayMs` -> `next()`),\n // turning a recoverable challenge into a failed turn. With no delay DSH\n // backs off locally and retries, and that retry lands on another account\n // because this one is already parked.\n const appeal = classified.verificationUrl ? ` Verify at: ${classified.verificationUrl}` : ''\n throw new LlmError(\n `agy account needs verification (${response.status}): ${classified.message ?? ''}${appeal}`,\n 'RATE_LIMIT',\n {\n requestId: ProviderRequestId(generateAntigravityRequestId()),\n },\n )\n }\n if (classified.kind === 'auth-failure') {\n throw new LlmError(\n `agy authentication failed (${response.status}) — run \\`dsh-agy login\\``,\n 'INVALID_CREDENTIAL',\n )\n }\n // 5xx upstream failures (e.g. 503 \"No capacity available\") are transient:\n // the DSH retry policy honors SERVER but treats UPSTREAM as terminal, so\n // classifying 5xx as UPSTREAM kills the turn with zero retries. Non-5xx\n // transient/request errors (404, generic 400, other 4xx) stay terminal.\n if (classified.status !== undefined && classified.status >= 500) {\n throw new LlmError(\n `agy upstream error (${response.status}): ${classified.message ?? ''}`,\n SERVER_ERROR_CODE,\n {\n providerRetryAfterMs: classified.retryAfterMs ?? undefined,\n requestId: ProviderRequestId(generateAntigravityRequestId()),\n },\n )\n }\n throw new LlmError(\n `agy upstream error (${response.status}): ${classified.message ?? ''}`,\n UPSTREAM_ERROR_CODE,\n )\n }\n\n if (!response.body) {\n throw new LlmError('agy stream returned no body', UPSTREAM_ERROR_CODE)\n }\n\n try {\n // The usage chunk is the ledger's only token source, so the stream is\n // consumed here rather than piped: every chunk still reaches DSH\n // unchanged, but the terminal `usage` chunk is also folded into this\n // account's record. Upstream repeats `usageMetadata` on every SSE event\n // (cumulative); parse.ts already reduces that to one final chunk.\n let usage: { input: number; output: number; cacheRead: number; cacheWrite: number } | undefined\n let ttftMs: number | undefined\n for await (const chunk of parseAgySse(response.body, {\n signal: options.signal,\n onToolSignature: (toolCallId, signature) => {\n setThoughtSignature(toolCallId, signature)\n },\n })) {\n if (chunk.type === 'usage') {\n usage = {\n input: chunk.usage.inputTokens,\n output: chunk.usage.outputTokens,\n cacheRead: chunk.usage.cacheReadTokens ?? 0,\n cacheWrite: chunk.usage.cacheWriteTokens ?? 0,\n }\n } else if (ttftMs === undefined && (chunk.type === 'text-delta' || chunk.type === 'reasoning-delta')) {\n // First model-authored output: the honest end of \"time to first token\".\n ttftMs = Date.now() - startedAt\n }\n yield chunk\n }\n await this.options.markSuccess?.(session)\n this.recordUsage(session, options.model, { ok: true, usage, ttftMs }, startedAt)\n } catch (error) {\n if (error instanceof DOMException && error.name === 'AbortError') {\n throw new LlmError('agy stream aborted', 'ABORTED', { cause: error })\n }\n await this.options.reportFailure('network-error', session)\n // A stream that died mid-body may already have delivered billable\n // content, so the attempt is recorded even though no usage chunk arrived.\n this.recordUsage(session, options.model, { ok: false }, startedAt)\n // Deliberately UPSTREAM (terminal), not TRANSPORT: content may already\n // have been emitted, and DSH's retry policy honours TRANSPORT, so retrying\n // here would replay a partially-delivered turn. The account-level report\n // above already absorbs the transient case by cooling/rotating. The cause\n // code is still surfaced so the socket failure is legible in session events.\n throw new LlmError(\n error instanceof Error ? describeFetchError(error) : 'agy stream parse failed',\n UPSTREAM_ERROR_CODE,\n { cause: error },\n )\n }\n }\n\n /**\n * Fold one attempt into the usage ledger.\n *\n * Statistics are diagnostics, never load-bearing: a ledger failure must not\n * fail a generation, so this swallows its own errors.\n */\n private recordUsage(\n session: AgyAccountSession,\n model: string | undefined,\n result: {\n ok: boolean\n rateLimited?: boolean\n usage?: { input: number; output: number; cacheRead: number; cacheWrite: number }\n ttftMs?: number\n },\n startedAt: number,\n ): void {\n const record = this.options.recordUsage\n if (record === undefined) return\n try {\n record({\n ...(ledgerAccountKey(session) === undefined ? {} : { account: ledgerAccountKey(session) }),\n ...(model === undefined ? {} : { model }),\n ok: result.ok,\n ...(result.rateLimited === true ? { rateLimited: true } : {}),\n ...(result.usage === undefined ? {} : { usage: result.usage }),\n ...(result.ttftMs === undefined ? {} : { ttftMs: result.ttftMs }),\n latencyMs: Math.max(0, Date.now() - startedAt),\n })\n } catch {\n // Swallowed by design.\n }\n }\n}\n\nexport type { ToolSchema }\n","/**\n * Model visibility: a per-provider blacklist of model ids hidden from the DSH\n * model selector.\n *\n * Mechanism: DSH's selector reads `session.modelCatalog`, which calls\n * `ctx.llm.listModels(provider)` -> `AgyAdapter.listModels()`. Filtering inside\n * the adapter is therefore sufficient to hide a model from the picker without\n * touching DSH — no host patch, no new RPC.\n *\n * Two deliberate choices:\n *\n * - Blacklist, not whitelist. Only explicitly-disabled models are hidden, so a\n * model the server adds later still shows up. A whitelist would silently hide\n * every new model until the user re-enabled it.\n * - Persisted to agy's own file rather than a `ctx.settings` namespace. The\n * settings service requires a `@deepseek-ai/schemastery` schema, and the\n * standalone CLI must not import any `@deepseek-ai/*` package at runtime\n * (AGENTS.md invariant). agy already owns plain JSON state with the same\n * atomic-write pattern (`agy-accounts.json`), so this stays consistent and\n * lets the CLI read the same blacklist.\n *\n * Only `true` counts as disabled; an id maps to `true` or is absent. Enabling\n * deletes the key, so the file holds just the hidden set and cannot grow with\n * every toggle.\n */\n\nimport { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { resolveDshHome } from './store/keyring.ts'\n\nexport const MODELS_VERSION = 1\n\n/** `provider -> modelId -> true`. */\nexport type DisabledModelMap = Record<string, Record<string, true>>\n\nexport interface ModelVisibilityDocument {\n version: number\n disabled: DisabledModelMap\n}\n\nconst EMPTY_SET: ReadonlySet<string> = new Set<string>()\n\n/** Keep only well-formed `provider -> { modelId: true }` entries (tolerates hand edits). */\nexport function sanitizeDisabledModels(raw: unknown): DisabledModelMap {\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return {}\n const out: DisabledModelMap = {}\n for (const [provider, value] of Object.entries(raw as Record<string, unknown>)) {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) continue\n const perProvider: Record<string, true> = {}\n for (const [modelId, flag] of Object.entries(value as Record<string, unknown>)) {\n if (flag === true) perProvider[modelId] = true\n }\n if (Object.keys(perProvider).length > 0) out[provider] = perProvider\n }\n return out\n}\n\nexport function parseModelVisibility(text: string): ModelVisibilityDocument {\n let raw: unknown\n try {\n raw = JSON.parse(text)\n } catch {\n return { version: MODELS_VERSION, disabled: {} }\n }\n const disabled = typeof raw === 'object' && raw !== null\n ? sanitizeDisabledModels((raw as Record<string, unknown>).disabled)\n : {}\n return { version: MODELS_VERSION, disabled }\n}\n\nexport interface ModelVisibilityOptions {\n /** Defaults to `$DSH_HOME/agy-models.json`. */\n file?: string\n}\n\n/**\n * In-memory authoritative copy of the blacklist, backed by a JSON file.\n *\n * The adapter calls `disabledFor()` on every `listModels()`, so that read is a\n * pure in-memory lookup: a toggle takes effect on the next catalog refresh\n * without rebuilding the adapter or restarting the host. Writes go through the\n * file immediately (toggles are rare, user-initiated).\n *\n * `disabledFor()` also revalidates against the file's mtime and reloads when it\n * moved. Two instances of this class routinely coexist in ONE process — the\n * main plugin entry and the web entry each build their own runtime — and a\n * toggle is written by the web instance while the model selector reads the main\n * instance. Without the mtime check the selector would keep serving its stale\n * in-memory copy, so switching a model off appeared to do nothing until the\n * host restarted (the `llm/adapters-updated` notification fires, the picker\n * refreshes, and the model is still listed). The same check is what lets a CLI\n * toggle be seen by a running server.\n */\nexport class ModelVisibility {\n private readonly file: string\n private disabled: DisabledModelMap\n /** Raw text this instance last read or wrote; see `reloadIfChanged`. */\n private raw: string\n /** Reusable frozen empty set, so the common \"nothing disabled\" read allocates nothing. */\n private readonly sets = new Map<string, ReadonlySet<string>>()\n\n constructor(options: ModelVisibilityOptions = {}) {\n this.file = options.file ?? join(resolveDshHome(), 'agy-models.json')\n const initial = this.readWithRaw()\n this.disabled = initial.disabled\n this.raw = initial.raw\n }\n\n /** Path of the backing file. */\n get path(): string {\n return this.file\n }\n\n /** The exact bytes `write()` would persist, so `raw` can track what this instance wrote. */\n private static serialize(next: DisabledModelMap): string {\n const doc: ModelVisibilityDocument = { version: MODELS_VERSION, disabled: next }\n return JSON.stringify(doc, null, 2) + '\\n'\n }\n\n /**\n * Read the map together with the raw text it came from.\n *\n * The TEXT (not an mtime) is what detects another writer's change. An mtime\n * cannot: a toggle and a re-read can land in the same filesystem timestamp\n * tick, and Windows resolves that coarsely enough to have failed CI — two\n * distinct writes reported an identical mtime, so the second change was never\n * seen and a disabled model stayed selectable. Content comparison cannot miss\n * a change, whatever the clock resolution.\n */\n private readWithRaw(): { disabled: DisabledModelMap, raw: string } {\n try {\n const raw = readFileSync(this.file, 'utf8')\n return { disabled: parseModelVisibility(raw).disabled, raw }\n } catch {\n // A missing or unreadable file means \"nothing disabled\".\n return { disabled: {}, raw: '' }\n }\n }\n\n private write(next: DisabledModelMap): void {\n mkdirSync(dirname(this.file), { recursive: true })\n const tmp = `${this.file}.tmp`\n writeFileSync(tmp, ModelVisibility.serialize(next), { mode: 0o600 })\n renameSync(tmp, this.file)\n }\n\n /**\n * Disabled model ids for one provider. Cheap enough for every catalog refresh:\n * one `stat` plus an in-memory map lookup, and the stat is what keeps two\n * same-process instances (and a CLI writer) in agreement.\n */\n disabledFor(provider: string): ReadonlySet<string> {\n this.reloadIfChanged()\n const cacheKey = provider\n const cached = this.sets.get(cacheKey)\n if (cached !== undefined) return cached\n const perProvider = this.disabled[provider]\n if (perProvider === undefined) return EMPTY_SET\n const ids = Object.keys(perProvider)\n if (ids.length === 0) return EMPTY_SET\n const set: ReadonlySet<string> = new Set(ids)\n this.sets.set(cacheKey, set)\n return set\n }\n\n /** Whether one model is currently hidden. Also revalidates, for the same reason. */\n isDisabled(provider: string, modelId: string): boolean {\n this.reloadIfChanged()\n return this.disabled[provider]?.[modelId] === true\n }\n\n /** The raw map, for the settings UI. */\n all(): DisabledModelMap {\n const out: DisabledModelMap = {}\n for (const [provider, models] of Object.entries(this.disabled)) out[provider] = { ...models }\n return out\n }\n\n /**\n * Hide or show one model. Persists immediately.\n *\n * The edit is applied to a freshly re-read map, not to this instance's copy:\n * with two instances in one process (main plugin + web entry) and a CLI in\n * another, editing the stale copy would silently drop whatever the other\n * writer had toggled since this instance last read. Toggles are rare and\n * user-initiated, so a read-modify-write costs nothing here — unlike the\n * ledger, this is not a hot path.\n *\n * Not locked: a simultaneous toggle from two processes can still lose one of\n * the two updates. The write itself is atomic (tmp + rename), so the file is\n * never corrupt — losing one toggle under a genuine race is an acceptable\n * trade for not introducing a lock file into a settings toggle.\n */\n setDisabled(provider: string, modelId: string, disabled: boolean): void {\n if (modelId === '') throw new Error('setDisabled: modelId must not be empty')\n this.reload()\n const next: DisabledModelMap = {}\n for (const [key, models] of Object.entries(this.disabled)) next[key] = { ...models }\n const perProvider = { ...(next[provider] ?? {}) }\n if (disabled) perProvider[modelId] = true\n else delete perProvider[modelId]\n if (Object.keys(perProvider).length === 0) delete next[provider]\n else next[provider] = perProvider\n this.write(next)\n this.disabled = next\n this.raw = ModelVisibility.serialize(next)\n // Drop the memoized set for this provider so the next read reflects the write.\n this.sets.delete(provider)\n }\n\n /** Re-read the file (multi-process: another writer may have toggled a model). */\n reload(): void {\n const fresh = this.readWithRaw()\n this.disabled = fresh.disabled\n this.raw = fresh.raw\n this.sets.clear()\n }\n\n /**\n * Reload when the file's contents differ from what this instance last read.\n *\n * Reads rather than stats: `disabledFor()` runs once per catalog refresh, not\n * per request, and the call it feeds is a network request to the model\n * endpoint — one small local read is not the cost that matters here, while a\n * missed change is the bug this whole mechanism exists to prevent.\n */\n private reloadIfChanged(): void {\n let current: string\n try {\n current = readFileSync(this.file, 'utf8')\n } catch {\n // Missing/unreadable: treat as empty, matching `readWithRaw`.\n current = ''\n }\n if (current === this.raw) return\n this.reload()\n }\n}\n","/**\n * Thinking-budget vocabulary shared by the host store, the RPC wire contract,\n * and the browser UI.\n *\n * Split out for the same reason `usage-types.ts` exists: these types live on\n * their own, free of any `node:*` import, so the browser bundle can describe the\n * settings surface without pulling in the host's storage module. Putting them in\n * `thinking-budget.ts` instead made `rpc-contract.ts` (which the client\n * typechecks) reach `node:fs` through the transitive import — caught by\n * `tsconfig.client.json`, which includes the contract and excludes `src/store`.\n */\n\n/** The three levels a tiered model exposes, in display order. */\nexport const THINKING_LEVELS = ['low', 'medium', 'high'] as const\n\nexport type ThinkingLevel = typeof THINKING_LEVELS[number]\n\n/**\n * The accepted `thinkingBudget` interval, measured on this channel.\n *\n * Exported so the UI, the RPC validation, and the store's sanitizer share ONE\n * definition: a value outside it is a 400 from upstream naming exactly this\n * range, so rejecting it locally turns a per-request failure into a save-time\n * message.\n */\nexport const THINKING_BUDGET_MIN = -1\nexport const THINKING_BUDGET_MAX = 65_535\n\n/** One budget per level; an ABSENT key means \"send no budget\" (upstream decides). */\nexport type ThinkingBudgets = Partial<Record<ThinkingLevel, number>>\n\n/**\n * The Claude family's `thinkingBudget` bounds, measured separately from Gemini's.\n *\n * The two families do NOT share a contract, which is why one interval cannot\n * serve both:\n * - Claude's floor is **1024**, not `-1`. A budget of 1 or 512 is rejected with\n * `thinking.enabled.budget_tokens: Input should be greater than or equal to\n * 1024`; `-1` and `0` are accepted as special values.\n * - Claude additionally requires **`max_tokens` strictly greater than the\n * budget**: `budget=1024, max_tokens=1024` is a 400, and a budget sent with\n * no `maxOutputTokens` at all also fails. So the margin is at least one token.\n *\n * `CLAUDE_BUDGET_MAX` is therefore one below `AGY_CLAUDE_MAX_OUTPUT_TOKENS` — the\n * largest budget that can still leave room for a strictly greater `max_tokens`.\n */\nexport const CLAUDE_BUDGET_MIN = 1024\nexport const CLAUDE_BUDGET_MAX = 63_999\n\n/** Whether `value` may be sent as a Claude `thinkingBudget`. */\nexport function isValidClaudeBudget(value: unknown): value is number {\n if (typeof value !== 'number' || !Number.isInteger(value)) return false\n // `-1` (adaptive) and `0` are accepted in addition to the documented minimum;\n // both are measured rather than inferred.\n if (value === -1 || value === 0) return true\n return value >= CLAUDE_BUDGET_MIN && value <= CLAUDE_BUDGET_MAX\n}\n","/**\n * Thinking-budget configuration: a global Low/Medium/High -> token mapping.\n *\n * WHY A GLOBAL MAP RATHER THAN PER-MODEL. Only `*-tiered` models emit a\n * `thinkingConfig` at all (see `isLevelThinkingModel`), and today that is three\n * ids in one family — while the levels a user actually picks are already exposed\n * by DSH's own model selector (`reasoningEffort`). So the missing piece is not a\n * per-model switch but the VALUE behind each level: \"High\" currently means the\n * upstream's own High, with no way to say \"High = 16000 tokens\". One mapping of\n * three numbers gives that, and because it is keyed by level it applies to every\n * tiered model without a per-model row.\n *\n * Measured on this channel (docs/ANTIGRAVITY-API.md §3):\n * - accepted interval is [-1, 65535]; `-1` is adaptive and IS accepted\n * - `minThinkingBudget` is NOT a validation (a budget below it answers 200),\n * so it must never become a client-side clamp\n * - `0` reduces thinking but does NOT reliably disable it\n *\n * An EMPTY entry means \"do not send a budget for this level\" — the request keeps\n * sending `thinkingLevel`, which is the behaviour before this feature existed.\n * That is why the shipped defaults are all empty: the feature is opt-in and its\n * absence is a no-op.\n *\n * Persisted to agy's own JSON rather than `ctx.settings`, for the reason recorded\n * in `model-visibility.ts`: that service needs a `@deepseek-ai/schemastery`\n * schema, and the CLI must not import `@deepseek-ai/*` at runtime. Same\n * atomic-write and content-compare reload pattern, so a writer in one process\n * (the web entry) is seen by a reader in another (the main plugin).\n */\n\nimport { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { resolveDshHome } from './store/keyring.ts'\nimport {\n THINKING_BUDGET_MAX,\n THINKING_BUDGET_MIN,\n THINKING_LEVELS,\n CLAUDE_BUDGET_MAX,\n CLAUDE_BUDGET_MIN,\n isValidClaudeBudget,\n} from './thinking-types.ts'\nimport type { ThinkingBudgets, ThinkingLevel } from './thinking-types.ts'\n\nexport const THINKING_VERSION = 1\n\nexport {\n THINKING_LEVELS,\n THINKING_BUDGET_MIN,\n THINKING_BUDGET_MAX,\n} from './thinking-types.ts'\nexport type { ThinkingLevel, ThinkingBudgets } from './thinking-types.ts'\n\n/**\n * The persisted document.\n *\n * `budgets` is keyed by reasoning LEVEL and applies to the tiered (Gemini)\n * family, whose single id exposes low/medium/high. `claudeBudget` is a single\n * value because the Claude family is id-bound: each capability is its own model\n * id with no level selector, so there is nothing to key by. Keeping them in one\n * document but distinct fields is what lets each carry its own validation —\n * their accepted intervals genuinely differ (Claude's floor is 1024, not -1).\n */\nexport interface ThinkingDocument {\n version: number\n budgets: ThinkingBudgets\n /** Budget for Claude thinking models; absent means \"send no budget\". */\n claudeBudget?: number\n /**\n * Budget for the TIERED slot — the selector's \"Default\" effort.\n *\n * That effort carries no level id (`effort: void 0`), so it cannot be keyed in\n * `budgets`. A value here sends `thinkingBudget` WITHOUT `thinkingLevel`, which\n * is what makes \"Default\" configurable as a Max: the effort still decides\n * nothing, the number does. Absent means the request sends no thinkingConfig at\n * all, i.e. upstream's own adaptive allocation.\n */\n tieredBudget?: number\n}\n\n/** Whether `value` may be sent as a `thinkingBudget`. */\nexport function isValidThinkingBudget(value: unknown): value is number {\n return typeof value === 'number'\n && Number.isInteger(value)\n && value >= THINKING_BUDGET_MIN\n && value <= THINKING_BUDGET_MAX\n}\n\n/**\n * Keep only well-formed entries (tolerates hand edits).\n *\n * Rejects out-of-range and non-integer values rather than clamping them: a\n * clamped value would silently mean something the user did not ask for, and\n * upstream rejects it anyway, so dropping the entry falls back to the\n * `thinkingLevel` path — which is valid.\n */\nexport function sanitizeThinkingBudgets(raw: unknown): ThinkingBudgets {\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return {}\n const out: ThinkingBudgets = {}\n for (const level of THINKING_LEVELS) {\n const value = (raw as Record<string, unknown>)[level]\n if (isValidThinkingBudget(value)) out[level] = value\n }\n return out\n}\n\nexport function parseThinkingDocument(text: string): ThinkingDocument {\n let raw: unknown\n try {\n raw = JSON.parse(text)\n } catch {\n return { version: THINKING_VERSION, budgets: {} }\n }\n const record = typeof raw === 'object' && raw !== null ? raw as Record<string, unknown> : {}\n const budgets = sanitizeThinkingBudgets(record.budgets)\n const claude = record.claudeBudget\n const tiered = record.tieredBudget\n return {\n version: THINKING_VERSION,\n budgets,\n // Dropped when unusable rather than clamped, matching the level budgets: the\n // request then sends no budget, which is a valid state.\n ...(isValidClaudeBudget(claude) ? { claudeBudget: claude } : {}),\n ...(isValidThinkingBudget(tiered) ? { tieredBudget: tiered } : {}),\n }\n}\n\nexport interface ThinkingBudgetOptions {\n /** Defaults to `$DSH_HOME/agy-thinking.json`. */\n file?: string\n /**\n * Minimum gap between hot-path file revalidations. Defaults to\n * `DEFAULT_REVALIDATE_INTERVAL_MS`; `0` disables throttling (useful in tests\n * that need a cross-instance write visible immediately).\n */\n revalidateIntervalMs?: number\n}\n\n/**\n * How long a hot-path read may serve the in-memory copy before re-checking the\n * file. One second keeps a settings change effectively immediate to the user\n * while collapsing a per-request read into at most one per second.\n */\nexport const DEFAULT_REVALIDATE_INTERVAL_MS = 1_000\n\n/**\n * In-memory authoritative copy of the budgets, backed by a JSON file.\n *\n * `budgetFor` sits on the GENERATION HOT PATH (`toAgyRequestBody` calls it for\n * every request), so the cross-instance revalidation it needs is rate-limited:\n * see `revalidateIntervalMs`. Writes go to the file immediately, since edits are\n * rare and user-initiated, and they bypass the limit so a concurrent edit is\n * never overwritten.\n */\nexport class ThinkingBudgetStore {\n private readonly file: string\n private readonly revalidateIntervalMs: number\n private doc: ThinkingDocument\n /** Raw text this instance last read or wrote; see `reloadNow`. */\n private raw: string\n /** When `reloadNow` last ran, for the hot-path throttle. */\n private checkedAt: number\n\n constructor(options: ThinkingBudgetOptions = {}) {\n this.file = options.file ?? join(resolveDshHome(), 'agy-thinking.json')\n this.revalidateIntervalMs = options.revalidateIntervalMs ?? DEFAULT_REVALIDATE_INTERVAL_MS\n const initial = this.readWithRaw()\n this.doc = initial.doc\n this.raw = initial.raw\n this.checkedAt = Date.now()\n }\n\n /** Path of the backing file. */\n get path(): string {\n return this.file\n }\n\n private static serialize(doc: ThinkingDocument): string {\n return JSON.stringify(doc, null, 2) + '\\n'\n }\n\n /**\n * Read the document together with the raw text it came from.\n *\n * Content, not mtime: two writers can land in the same filesystem timestamp\n * tick (Windows resolves it coarsely enough to have failed CI in this repo),\n * and a missed change is exactly the bug this check exists to prevent.\n */\n private readWithRaw(): { doc: ThinkingDocument, raw: string } {\n try {\n const raw = readFileSync(this.file, 'utf8')\n return { doc: parseThinkingDocument(raw), raw }\n } catch {\n // A missing or unreadable file means \"nothing configured\".\n return { doc: { version: THINKING_VERSION, budgets: {} }, raw: '' }\n }\n }\n\n /**\n * Re-read the file if another writer changed it.\n *\n * Compares CONTENT, not mtime: a toggle and a re-read can land in the same\n * filesystem timestamp tick, and Windows resolves that coarsely enough to have\n * failed CI in this repo (`model-visibility.ts` records the incident). Content\n * comparison cannot miss a change, whatever the clock resolution.\n *\n * Unconditional — callers on the hot path use `reloadIfChanged` instead.\n */\n private reloadNow(): void {\n let current: string\n try {\n current = readFileSync(this.file, 'utf8')\n } catch {\n current = ''\n }\n this.checkedAt = Date.now()\n if (current === this.raw) return\n const fresh = this.readWithRaw()\n this.doc = fresh.doc\n this.raw = fresh.raw\n }\n\n /**\n * Hot-path revalidation: `reloadNow`, rate-limited.\n *\n * Without the limit every generation pays a blocking `readFileSync` plus a\n * UTF-8 decode — the module used to claim these reads were \"in-memory\", which\n * an unconditional read made false. The file is written only by `setBudget`\n * (a rare, user-initiated action), so the worst case here is that a settings\n * change takes up to `revalidateIntervalMs` to reach an already-running\n * adapter. That is the right trade for a setting; it is not right for the write\n * path, which is why `write` calls `reloadNow` directly.\n */\n private reloadIfChanged(): void {\n if (Date.now() - this.checkedAt < this.revalidateIntervalMs) return\n this.reloadNow()\n }\n\n /**\n * Write the whole document atomically.\n *\n * Always re-reads first, so a concurrent edit by the OTHER instance in this\n * process (main plugin + web entry) is merged rather than overwritten: each\n * caller passes a mutation, not a full replacement built from a stale copy.\n */\n private write(mutate: (doc: ThinkingDocument) => void): void {\n // Unconditional: building on a throttled-skipped stale copy would drop the\n // other instance's concurrent edit, which is the whole reason for re-reading.\n this.reloadNow()\n const next: ThinkingDocument = {\n version: THINKING_VERSION,\n budgets: { ...this.doc.budgets },\n ...(this.doc.claudeBudget === undefined ? {} : { claudeBudget: this.doc.claudeBudget }),\n ...(this.doc.tieredBudget === undefined ? {} : { tieredBudget: this.doc.tieredBudget }),\n }\n mutate(next)\n const text = ThinkingBudgetStore.serialize(next)\n mkdirSync(dirname(this.file), { recursive: true })\n const tmp = `${this.file}.tmp`\n writeFileSync(tmp, text, { mode: 0o600 })\n renameSync(tmp, this.file)\n this.doc = next\n this.raw = text\n }\n\n /** The configured budget for one level, or undefined when unset. */\n budgetFor(level: string | undefined): number | undefined {\n this.reloadIfChanged()\n if (level === undefined) return undefined\n const key = level.toLowerCase() as ThinkingLevel\n if (!THINKING_LEVELS.includes(key)) return undefined\n return this.doc.budgets[key]\n }\n\n /** The Claude-family budget, or undefined when unset. */\n claudeBudget(): number | undefined {\n this.reloadIfChanged()\n return this.doc.claudeBudget\n }\n\n /** The tiered-slot budget (the selector's Default effort), or undefined. */\n tieredBudget(): number | undefined {\n this.reloadIfChanged()\n return this.doc.tieredBudget\n }\n\n /** The raw map, for the settings UI. */\n all(): ThinkingBudgets {\n this.reloadIfChanged()\n return { ...this.doc.budgets }\n }\n\n /** The whole document, for the settings UI. */\n snapshot(): ThinkingDocument {\n this.reloadIfChanged()\n return {\n version: THINKING_VERSION,\n budgets: { ...this.doc.budgets },\n ...(this.doc.claudeBudget === undefined ? {} : { claudeBudget: this.doc.claudeBudget }),\n ...(this.doc.tieredBudget === undefined ? {} : { tieredBudget: this.doc.tieredBudget }),\n }\n }\n\n /** Replace one level's budget, or clear it when `value` is undefined. */\n setBudget(level: string, value: number | undefined): ThinkingBudgets {\n const key = level.toLowerCase() as ThinkingLevel\n if (!THINKING_LEVELS.includes(key)) throw new Error(`unknown thinking level: ${level}`)\n if (value !== undefined && !isValidThinkingBudget(value)) {\n throw new Error(`thinking budget must be an integer in [${THINKING_BUDGET_MIN}, ${THINKING_BUDGET_MAX}]`)\n }\n this.write((doc) => {\n if (value === undefined) delete doc.budgets[key]\n else doc.budgets[key] = value\n })\n return this.all()\n }\n\n /** Replace the tiered-slot budget, or clear it when `value` is undefined. */\n setTieredBudget(value: number | undefined): ThinkingDocument {\n if (value !== undefined && !isValidThinkingBudget(value)) {\n throw new Error(`thinking budget must be an integer in [${THINKING_BUDGET_MIN}, ${THINKING_BUDGET_MAX}]`)\n }\n this.write((doc) => {\n if (value === undefined) delete doc.tieredBudget\n else doc.tieredBudget = value\n })\n return this.snapshot()\n }\n\n /** Replace the Claude budget, or clear it when `value` is undefined. */\n setClaudeBudget(value: number | undefined): ThinkingDocument {\n if (value !== undefined && !isValidClaudeBudget(value)) {\n throw new Error(`Claude thinking budget must be -1, 0, or an integer in [${CLAUDE_BUDGET_MIN}, ${CLAUDE_BUDGET_MAX}]`)\n }\n this.write((doc) => {\n if (value === undefined) delete doc.claudeBudget\n else doc.claudeBudget = value\n })\n return this.snapshot()\n }\n}\n","/**\n * Usage vocabulary shared by the host ledger, the RPC wire contract, and the\n * browser UI.\n *\n * These types live on their own, free of any `node:*` import, so the browser\n * bundle can describe usage without pulling in the host's storage module. The\n * ledger (`stats.ts`) and the wire contract (`rpc-contract.ts`) both build on\n * this module rather than on each other.\n */\n\n/** Token counters, disjoint exactly like DSH's `TokenUsage`. */\nexport interface TokenBuckets {\n /** Uncached input tokens. */\n input: number\n output: number\n cacheRead: number\n cacheWrite: number\n}\n\n/** One scope's counters (an account, a model, a day, or the grand total). */\nexport interface UsageCounters extends TokenBuckets {\n /** Requests attempted, including failures. */\n requests: number\n /** Requests that returned a usable stream. */\n succeeded: number\n /** Classified upstream failures of any kind. */\n failed: number\n /** Rate-limit classifications (HTTP 429, and 403 carrying quota phrasing). */\n rateLimited: number\n /** Pool rotations triggered by a failure. */\n rotations: number\n /** Summed request wall time, and the number of requests that timed it. */\n latencyMs: number\n latencyN: number\n /** Summed time-to-first-token, and the number of requests that reported one. */\n ttftMs: number\n ttftN: number\n}\n\n/** Where a request came from; CLI/verify/test never appear in DSH's own ledger. */\nexport type UsageSource = 'chat' | 'cli' | 'verify' | 'test'\n\n/** Zeroed counters, for accumulation. */\nexport function zeroCounters(): UsageCounters {\n return {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n requests: 0,\n succeeded: 0,\n failed: 0,\n rateLimited: 0,\n rotations: 0,\n latencyMs: 0,\n latencyN: 0,\n ttftMs: 0,\n ttftN: 0,\n }\n}\n","/**\n * Cumulative agy usage statistics, persisted to `$DSH_HOME/agy-stats.json`.\n *\n * Why agy keeps its own ledger instead of reading DSH's session projections:\n *\n * - DSH structurally has no account dimension (no `account` field anywhere in\n * `dsh-llm` or the session types), so \"which account burned this quota\" is\n * unanswerable from DSH's data.\n * - agy's own traffic bypasses DSH entirely — `dsh-agy login/verify/status`,\n * `testCall()`, `checkAccounts()`, and quota discovery all consume upstream\n * quota without producing a session event.\n * - DSH's cumulative `tokenUsage` projection has no model dimension and folds\n * by `(turn, step)`, so a mid-session model switch cannot be attributed.\n *\n * Counter semantics deliberately differ from DSH's: DSH replaces a step's\n * usage (a retried attempt counts once, for conversation cost), while this\n * ledger accumulates per attempt (every retry really did consume quota, which\n * is what an account-level view must show).\n *\n * Storage shape is cumulative with a rolling per-day window:\n * - `totals` never expires — it answers \"all time\".\n * - `days` keeps `DAY_WINDOW` buckets — it answers today / 7d / 30d.\n * A day rolling out of `days` therefore loses only granularity; its counts\n * already live in `totals`.\n *\n * Writes are throttled and merged, never read-modify-write per request:\n * callers accumulate in memory (zero I/O on the hot path) and `flush()` takes\n * the file lock, re-reads the fresh on-disk state, adds this process's delta,\n * and atomically replaces the file. A plain overwrite would lose the counts\n * another process (the Desktop app, a web-profile server, a CLI invocation)\n * wrote in the meantime.\n *\n * A ledger that cannot be written must fail soft and stay bounded: the pending\n * backlog is capped, immediate (count-based) flushing is suspended while writes\n * fail, and the first error of each failure run is reported through\n * `onFlushError`. Statistics are diagnostics — they never break a generation,\n * and they never grow without limit.\n */\n\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport lockfile from 'proper-lockfile'\nimport { resolveDshHome } from './store/keyring.ts'\nimport { zeroCounters } from './usage-types.ts'\nimport type { TokenBuckets, UsageCounters, UsageSource } from './usage-types.ts'\n\nexport type { TokenBuckets, UsageCounters, UsageSource } from './usage-types.ts'\n\nexport const STATS_VERSION = 2\n\n/** Days of per-day buckets retained. Longer spans read `totals`. */\nexport const DAY_WINDOW = 30\n\n/** Per-account ledger. */\nexport interface AccountUsage {\n totals: UsageCounters\n /** Per-model totals for this account, keyed by provider model id. */\n models: Record<string, UsageCounters>\n /** Request counts by call site. */\n sources: Record<UsageSource, number>\n /** Last time this account recorded anything (Unix ms). */\n lastUsedAt: number\n}\n\n/**\n * One day's slice, broken down the same three ways the Usage tab reads it.\n *\n * Version 1 stored only the flat counters here, which was enough for the KPI\n * strip but not for the breakdown tables: those had to read the ALL-TIME\n * `accounts`/`models` maps, so selecting \"Today\" changed the headline figures\n * while the tables below still showed every request ever recorded — 30 requests\n * above a 164-request row on the same screen. Keeping the per-day breakdown is\n * what lets the whole page follow one range.\n *\n * `totals` stays the sum of the day's records; `models`/`accounts` are the same\n * counters partitioned by dimension. Every record contributes to `totals` and to\n * whichever dimensions it names.\n */\nexport interface DayBucket {\n totals: UsageCounters\n /** Per-model counters within this day. */\n models: Record<string, UsageCounters>\n /** Per-account counters within this day. */\n accounts: Record<string, UsageCounters>\n}\n\n/** The persisted document. */\nexport interface StatsDocument {\n version: number\n /** When this ledger began collecting (Unix ms). */\n since: number\n /** Grand totals across every account. */\n totals: UsageCounters\n /** Per-account ledgers, keyed by account email (falling back to id). */\n accounts: Record<string, AccountUsage>\n /** Rolling per-day buckets, `YYYY-MM-DD` -> that day's slice. */\n days: Record<string, DayBucket>\n}\n\n/** One recorded request, as the call sites observe it. */\nexport interface UsageRecord {\n /** Account key (email preferred); omitted when no account was resolved. */\n account?: string\n /** Provider model id. */\n model?: string\n source: UsageSource\n /** Token usage; absent for calls that failed before producing usage. */\n usage?: TokenBuckets\n ok: boolean\n rateLimited?: boolean\n rotated?: boolean\n latencyMs?: number\n ttftMs?: number\n /**\n * A pool-level event (a rotation) rather than a request.\n *\n * Rotations are decided inside the session manager, after the adapter has\n * already recorded the failing request. Emitting them as ordinary records\n * would count that request twice, so a pool event adds only to the pool\n * counters and leaves the request counters alone.\n */\n poolEvent?: boolean\n}\n\nfunction zeroAccount(now: number): AccountUsage {\n return {\n totals: zeroCounters(),\n models: {},\n sources: { chat: 0, cli: 0, verify: 0, test: 0 },\n lastUsedAt: now,\n }\n}\n\nfunction emptyDocument(now: number): StatsDocument {\n return { version: STATS_VERSION, since: now, totals: zeroCounters(), accounts: {}, days: {} }\n}\n\n/** Local-time day key. Local (not UTC) so \"today\" matches the user's clock. */\nfunction dayKey(time: number): string {\n const d = new Date(time)\n const month = String(d.getMonth() + 1).padStart(2, '0')\n const day = String(d.getDate()).padStart(2, '0')\n return `${d.getFullYear()}-${month}-${day}`\n}\n\n/** Add `add` into `into`, in place. */\nfunction addInto(into: UsageCounters, add: UsageCounters): void {\n into.input += add.input\n into.output += add.output\n into.cacheRead += add.cacheRead\n into.cacheWrite += add.cacheWrite\n into.requests += add.requests\n into.succeeded += add.succeeded\n into.failed += add.failed\n into.rateLimited += add.rateLimited\n into.rotations += add.rotations\n into.latencyMs += add.latencyMs\n into.latencyN += add.latencyN\n into.ttftMs += add.ttftMs\n into.ttftN += add.ttftN\n}\n\n/** Coerce arbitrary parsed JSON into a valid counter block (tolerates hand edits). */\nfunction sanitizeCounters(raw: unknown): UsageCounters {\n const out = zeroCounters()\n if (typeof raw !== 'object' || raw === null) return out\n for (const key of Object.keys(out) as Array<keyof UsageCounters>) {\n const value = (raw as Record<string, unknown>)[key]\n if (typeof value === 'number' && Number.isFinite(value) && value >= 0) out[key] = value\n }\n return out\n}\n\nfunction sanitizeAccount(raw: unknown, now: number): AccountUsage {\n const out = zeroAccount(now)\n if (typeof raw !== 'object' || raw === null) return out\n const record = raw as Record<string, unknown>\n out.totals = sanitizeCounters(record.totals)\n if (typeof record.lastUsedAt === 'number' && Number.isFinite(record.lastUsedAt)) {\n out.lastUsedAt = record.lastUsedAt\n }\n if (typeof record.models === 'object' && record.models !== null) {\n for (const [id, value] of Object.entries(record.models as Record<string, unknown>)) {\n out.models[id] = sanitizeCounters(value)\n }\n }\n if (typeof record.sources === 'object' && record.sources !== null) {\n for (const key of ['chat', 'cli', 'verify', 'test'] as const) {\n const value = (record.sources as Record<string, unknown>)[key]\n if (typeof value === 'number' && Number.isFinite(value) && value >= 0) out.sources[key] = value\n }\n }\n return out\n}\n\n/** Parse a persisted document defensively; a corrupt file degrades to empty. */\nexport function parseStatsDocument(text: string, now: number): StatsDocument {\n let raw: unknown\n try {\n raw = JSON.parse(text)\n } catch {\n return emptyDocument(now)\n }\n if (typeof raw !== 'object' || raw === null) return emptyDocument(now)\n const record = raw as Record<string, unknown>\n const doc = emptyDocument(now)\n if (typeof record.since === 'number' && Number.isFinite(record.since)) doc.since = record.since\n doc.totals = sanitizeCounters(record.totals)\n if (typeof record.accounts === 'object' && record.accounts !== null) {\n for (const [key, value] of Object.entries(record.accounts as Record<string, unknown>)) {\n doc.accounts[key] = sanitizeAccount(value, now)\n }\n }\n if (typeof record.days === 'object' && record.days !== null) {\n for (const [key, value] of Object.entries(record.days as Record<string, unknown>)) {\n // Only well-formed day keys survive, so a stray key can never grow the file.\n if (/^\\d{4}-\\d{2}-\\d{2}$/.test(key)) doc.days[key] = sanitizeDayBucket(value)\n }\n }\n return doc\n}\n\n/**\n * Coerce one day's stored value into a `DayBucket`.\n *\n * Accepts the version-1 shape too: that stored a bare counter block, so a\n * document written before the upgrade keeps its day counts as `totals` with no\n * breakdown. Losing the breakdown for those days is the correct trade — the\n * alternative (dropping the days) would erase real history, and recomputing a\n * dimension split that was never recorded is impossible. The all-time\n * `accounts`/`models` maps are unaffected, so only range-scoped breakdown tables\n * are thinner for pre-upgrade days.\n */\nfunction sanitizeDayBucket(raw: unknown): DayBucket {\n const out: DayBucket = { totals: zeroCounters(), models: {}, accounts: {} }\n if (typeof raw !== 'object' || raw === null) return out\n const record = raw as Record<string, unknown>\n if (typeof record.totals === 'object' && record.totals !== null) {\n out.totals = sanitizeCounters(record.totals)\n if (typeof record.models === 'object' && record.models !== null) {\n for (const [id, value] of Object.entries(record.models as Record<string, unknown>)) {\n out.models[id] = sanitizeCounters(value)\n }\n }\n if (typeof record.accounts === 'object' && record.accounts !== null) {\n for (const [key, value] of Object.entries(record.accounts as Record<string, unknown>)) {\n out.accounts[key] = sanitizeCounters(value)\n }\n }\n return out\n }\n // Version-1 day: a bare counter block.\n out.totals = sanitizeCounters(raw)\n return out\n}\n\n/** Drop day buckets older than the window; their counts already live in `totals`. */\nfunction pruneDays(doc: StatsDocument, now: number): void {\n const cutoff = dayKey(now - DAY_WINDOW * 86_400_000)\n for (const key of Object.keys(doc.days)) {\n if (key < cutoff) delete doc.days[key]\n }\n}\n\n/**\n * Sum the day buckets covering the last `days` days, inclusive of today.\n *\n * `days: 1` is \"today\" and `days: DAY_WINDOW` is the full retained window. The\n * window is deliberately read from the per-day buckets rather than `totals`:\n * `totals` is all-time and grows past every window.\n * @param doc - the ledger snapshot.\n * @param days - window length in days, at least 1.\n * @param now - current time (Unix ms).\n * @returns summed counters over the window.\n */\nexport function foldWindow(doc: StatsDocument, days: number, now: number): UsageCounters {\n const out = zeroCounters()\n const span = Math.max(1, Math.floor(days))\n const cutoff = dayKey(now - (span - 1) * 86_400_000)\n for (const [key, bucket] of Object.entries(doc.days)) {\n // String compare is a valid date order for `YYYY-MM-DD`.\n if (key >= cutoff) addInto(out, bucket.totals)\n }\n return out\n}\n\n/** One range's breakdown: totals plus the same counters split by model/account. */\nexport interface WindowBreakdown {\n totals: UsageCounters\n models: Array<{ model: string, counters: UsageCounters }>\n accounts: Array<{ account: string, counters: UsageCounters }>\n}\n\n/**\n * Fold the day buckets for a window into the three views the Usage tab renders.\n *\n * The breakdown tables read THIS rather than the all-time `accounts`/`models`\n * maps, so one range selection governs the whole page. Rows are sorted by\n * request count descending, matching the all-time view; the caller re-sorts if\n * it wants another order (the UI shows them as-is).\n *\n * @param doc - the ledger snapshot.\n * @param days - window length in days, at least 1.\n * @param now - current time (Unix ms).\n * @returns totals and their per-model / per-account partitions.\n */\nexport function foldWindowBreakdown(doc: StatsDocument, days: number, now: number): WindowBreakdown {\n const span = Math.max(1, Math.floor(days))\n const cutoff = dayKey(now - (span - 1) * 86_400_000)\n const totals = zeroCounters()\n const models = new Map<string, UsageCounters>()\n const accounts = new Map<string, UsageCounters>()\n for (const [key, bucket] of Object.entries(doc.days)) {\n if (key < cutoff) continue\n addInto(totals, bucket.totals)\n for (const [model, counters] of Object.entries(bucket.models)) {\n const current = models.get(model)\n if (current === undefined) models.set(model, { ...counters })\n else addInto(current, counters)\n }\n for (const [account, counters] of Object.entries(bucket.accounts)) {\n const current = accounts.get(account)\n if (current === undefined) accounts.set(account, { ...counters })\n else addInto(current, counters)\n }\n }\n const byRequests = (a: { counters: UsageCounters }, b: { counters: UsageCounters }): number =>\n b.counters.requests - a.counters.requests\n return {\n totals,\n models: [...models.entries()].map(([model, counters]) => ({ model, counters })).sort(byRequests),\n accounts: [...accounts.entries()].map(([account, counters]) => ({ account, counters })).sort(byRequests),\n }\n}\n\n/** Merge one record into a document, in place. Shared by the live store and tests. */\nexport function applyRecord(doc: StatsDocument, record: UsageRecord, now: number): void {\n const delta = zeroCounters()\n // A pool event carries no request of its own; it only moves pool counters.\n if (record.poolEvent !== true) {\n delta.requests = 1\n if (record.ok) delta.succeeded = 1\n else delta.failed = 1\n if (record.rateLimited === true) delta.rateLimited = 1\n }\n if (record.rotated === true) delta.rotations = 1\n if (record.usage !== undefined) {\n delta.input = record.usage.input\n delta.output = record.usage.output\n delta.cacheRead = record.usage.cacheRead\n delta.cacheWrite = record.usage.cacheWrite\n }\n if (typeof record.latencyMs === 'number' && Number.isFinite(record.latencyMs) && record.latencyMs >= 0) {\n delta.latencyMs = record.latencyMs\n delta.latencyN = 1\n }\n if (typeof record.ttftMs === 'number' && Number.isFinite(record.ttftMs) && record.ttftMs >= 0) {\n delta.ttftMs = record.ttftMs\n delta.ttftN = 1\n }\n\n addInto(doc.totals, delta)\n\n if (record.account !== undefined && record.account !== '') {\n const account = doc.accounts[record.account] ?? zeroAccount(now)\n addInto(account.totals, delta)\n // A pool event is not a call site occurrence, so it counts no source.\n if (record.poolEvent !== true) account.sources[record.source] += 1\n account.lastUsedAt = now\n if (record.model !== undefined && record.model !== '') {\n const model = account.models[record.model] ?? zeroCounters()\n addInto(model, delta)\n // A model with no traffic left is still a real model row; keep it.\n account.models[record.model] = model\n }\n doc.accounts[record.account] = account\n }\n\n const key = dayKey(now)\n const bucket = doc.days[key] ?? { totals: zeroCounters(), models: {}, accounts: {} }\n addInto(bucket.totals, delta)\n // The same delta is partitioned by the dimensions it names, so a range-scoped\n // breakdown table adds up to that range's headline figures exactly.\n if (record.model !== undefined && record.model !== '') {\n const model = bucket.models[record.model] ?? zeroCounters()\n addInto(model, delta)\n bucket.models[record.model] = model\n }\n if (record.account !== undefined && record.account !== '') {\n const account = bucket.accounts[record.account] ?? zeroCounters()\n addInto(account, delta)\n bucket.accounts[record.account] = account\n }\n doc.days[key] = bucket\n\n pruneDays(doc, now)\n}\n\n/**\n * Cross-process file lock seam (proper-lockfile behind a small interface for tests).\n *\n * The sync form is deliberate — `flush()` is called from `process.on('exit')`,\n * where async work cannot settle.\n *\n * The sync API accepts NO retry configuration: `proper-lockfile` throws\n * \"Cannot use retries with the sync api\" when `retries` is passed, which fails\n * every flush and silently drops every count. Contention therefore surfaces as\n * a thrown error instead of a retry, and `flush()` puts the batch back for the\n * next timer tick — a statistics flush that loses a race costs nothing, so\n * waiting is never worth blocking the event loop for.\n */\nexport interface StatsLock {\n withLock<T>(file: string, fn: () => T): T\n}\n\nexport const properStatsLock: StatsLock = {\n withLock<T>(file: string, fn: () => T): T {\n // `realpath: false` because the target may be absent on first write; the\n // caller pre-creates it, and lockSync otherwise throws ENOENT resolving it.\n //\n // `stale` is deliberately short (8s, the same order as `flushIntervalMs`).\n // proper-lockfile only removes its lock directory through signal-exit, so a\n // `SIGKILL`ed process leaves one behind; with the former 30s window every\n // flush failed for 30 seconds afterwards — a long blackout for a diagnostic\n // ledger whose next flush can simply try again. `update` must stay below\n // `stale` or a lock holder self-expires while it is still working.\n const release = lockfile.lockSync(file, { stale: 8_000, update: 4_000, realpath: false })\n try {\n return fn()\n } finally {\n release()\n }\n },\n}\n\nexport const noopStatsLock: StatsLock = { withLock: (_file, fn) => fn() }\n\nexport interface UsageStatsStoreOptions {\n /** Defaults to `$DSH_HOME/agy-stats.json`. */\n file?: string\n lock?: StatsLock\n /** Injectable clock, for tests. */\n now?: () => number\n /**\n * Flush after this many pending records. `0` disables count-based flushing.\n * Defaults to 50 (DSH's own projection cache writes at 200 records / 5s).\n */\n flushEvery?: number\n /** Flush after this many ms with pending records. Defaults to 5000. */\n flushIntervalMs?: number\n /**\n * Hard ceiling on un-flushed records. Defaults to 500 (ten batches).\n *\n * A persistent write failure (read-only `$DSH_HOME`, ENOSPC) makes every\n * flush fail, so the pending list would otherwise grow without bound in a\n * long-lived process. Past the ceiling the OLDEST records are dropped: a\n * bounded diagnostic ledger beats an unbounded one, and the oldest counts are\n * the least useful ones.\n */\n maxPending?: number\n /**\n * Called once per run of consecutive flush failures, with the first error.\n *\n * Without this a ledger that cannot be written fails silently: counts simply\n * stop appearing, with nothing to correlate. Once per run (not per attempt)\n * because the timer retries every `flushIntervalMs` and a log per retry would\n * be noise.\n */\n onFlushError?: (error: unknown) => void\n}\n\n/**\n * Accumulating usage ledger. `record()` is the hot path and performs no I/O:\n * it folds the record into an in-memory delta. `flush()` merges that delta into\n * the persisted document under the file lock.\n */\nexport class UsageStats {\n private readonly file: string\n private readonly lock: StatsLock\n private readonly now: () => number\n private readonly flushEvery: number\n private readonly flushIntervalMs: number\n private readonly maxPending: number\n private readonly onFlushError: ((error: unknown) => void) | undefined\n\n /** This process's un-flushed records. */\n private pending: UsageRecord[] = []\n private timer: ReturnType<typeof setTimeout> | undefined\n private lastFlushError: unknown\n /**\n * Whether the current run of consecutive failures has already been reported.\n * Count-based flushing is also suspended while set: see `record()`.\n */\n private flushFailedSince: number | undefined\n\n constructor(options: UsageStatsStoreOptions = {}) {\n this.file = options.file ?? join(resolveDshHome(), 'agy-stats.json')\n this.lock = options.lock ?? properStatsLock\n this.now = options.now ?? (() => Date.now())\n this.flushEvery = options.flushEvery ?? 50\n this.flushIntervalMs = options.flushIntervalMs ?? 5_000\n this.maxPending = options.maxPending ?? 500\n this.onFlushError = options.onFlushError\n // Deliberately does NOT create the file: a read-only command (`status`)\n // builds a ledger and must not leave state behind. `flush()` pre-creates it\n // before locking, which is the first point a write actually needs it.\n }\n\n /** Path of the backing file. */\n get path(): string {\n return this.file\n }\n\n /**\n * Pre-create the ledger so `proper-lockfile` can lock it. The lock throws\n * ENOENT on a nonexistent target, so the empty document must exist (0600,\n * tmp+rename) before the first flush.\n */\n private ensureFile(now: number): void {\n if (existsSync(this.file)) return\n mkdirSync(dirname(this.file), { recursive: true })\n const tmp = `${this.file}.tmp-init`\n writeFileSync(tmp, JSON.stringify(emptyDocument(now), null, 2) + '\\n', { mode: 0o600 })\n renameSync(tmp, this.file)\n }\n\n private readFile(): StatsDocument {\n const now = this.now()\n try {\n return parseStatsDocument(readFileSync(this.file, 'utf8'), now)\n } catch {\n return emptyDocument(now)\n }\n }\n\n /** Atomic replace (tmp + rename), owner-only, so a reader never sees a partial file. */\n private writeFile(doc: StatsDocument): void {\n mkdirSync(dirname(this.file), { recursive: true })\n const tmp = `${this.file}.tmp`\n writeFileSync(tmp, JSON.stringify(doc, null, 2) + '\\n', { mode: 0o600 })\n renameSync(tmp, this.file)\n }\n\n /** Record one request. Hot path: no I/O. */\n record(record: UsageRecord): void {\n this.pending.push(record)\n // Cap the backlog: unbounded growth is the one failure mode a diagnostic\n // ledger must not have (see `maxPending`).\n if (this.maxPending > 0 && this.pending.length > this.maxPending) {\n this.pending.splice(0, this.pending.length - this.maxPending)\n }\n // Count-based flushing is a throughput optimisation, so it is suspended\n // while writes are failing. Without this a persistent failure made EVERY\n // `record()` attempt a full synchronous flush (stat + lock + read + write),\n // turning the generation hot path into I/O — the exact opposite of this\n // class's contract. The timer below keeps retrying instead.\n if (\n this.flushFailedSince === undefined\n && this.flushEvery > 0\n && this.pending.length >= this.flushEvery\n ) {\n this.flush()\n return\n }\n this.armTimer()\n }\n\n private armTimer(): void {\n if (this.timer !== undefined || this.flushIntervalMs <= 0) return\n this.timer = setTimeout(() => {\n this.timer = undefined\n this.flush()\n }, this.flushIntervalMs)\n // Never hold the process open for a statistics flush.\n this.timer.unref?.()\n }\n\n /**\n * Merge pending records into the persisted document.\n *\n * Under the lock, the fresh on-disk state is re-read and this process's\n * records are replayed onto it. That is what keeps concurrently-running\n * writers (Desktop, a web-profile server, a CLI invocation) from clobbering\n * each other: a plain overwrite of the in-memory document would drop every\n * count another process added since this one loaded the file.\n */\n flush(): void {\n if (this.timer !== undefined) {\n clearTimeout(this.timer)\n this.timer = undefined\n }\n const batch = this.pending\n if (batch.length === 0) return\n this.pending = []\n const now = this.now()\n try {\n this.ensureFile(now)\n this.lock.withLock(this.file, () => {\n // Re-read under the lock and replay this batch onto the fresh state:\n // that is what makes concurrent writers additive instead of last-write-wins.\n const doc = this.readFile()\n for (const record of batch) applyRecord(doc, record, now)\n this.writeFile(doc)\n })\n this.lastFlushError = undefined\n this.flushFailedSince = undefined\n } catch (error) {\n // A failed flush must not lose the counts or break a generation: put the\n // batch back so the next flush retries it, and surface the reason once.\n this.pending = [...batch, ...this.pending]\n if (this.maxPending > 0 && this.pending.length > this.maxPending) {\n this.pending.splice(0, this.pending.length - this.maxPending)\n }\n this.lastFlushError = error\n if (this.flushFailedSince === undefined) {\n this.flushFailedSince = now\n try {\n this.onFlushError?.(error)\n } catch {\n // The reporter must never break a flush path.\n }\n }\n // Only the timer retries now; `record()` stops triggering immediate\n // flushes until one succeeds.\n this.armTimer()\n }\n }\n\n /** Flush if anything is pending; safe to call on turn end and process exit. */\n flushSync(): void {\n this.flush()\n }\n\n /**\n * A snapshot for reading (the Usage tab).\n *\n * Reads the file rather than an in-memory copy: the writer is not always this\n * process. A CLI `login`/`verify` runs in its own process, and the browser may\n * be served by a different plugin instance than the one that generated, so an\n * in-memory cache would show a stale or empty ledger. This process's own\n * un-flushed records are layered on top so the UI never lags behind its own\n * activity.\n *\n * Called once per UI open/refresh, never on the generation hot path, so the\n * read is cheap enough.\n */\n snapshot(): StatsDocument {\n const now = this.now()\n const doc = this.readFile()\n if (this.pending.length > 0) {\n for (const record of this.pending) applyRecord(doc, record, now)\n }\n return doc\n }\n\n /** The most recent flush failure, if any (diagnostics). */\n get flushError(): unknown {\n return this.lastFlushError\n }\n\n /** Number of records awaiting flush. */\n get pendingCount(): number {\n return this.pending.length\n }\n\n /** Whether the ledger is currently failing to persist (a run of failures). */\n get flushFailing(): boolean {\n return this.flushFailedSince !== undefined\n }\n}\n","/**\n * Shared runtime construction for the in-harness plugin entries: master-key\n * codec resolution (credentials seam first, credentials document fallback),\n * account store, session manager, and adapter. Used by the main plugin\n * (adapter registration) and the web plugin (route registration) so both\n * entries operate on the same store.\n */\n\nimport { randomBytes } from 'node:crypto'\nimport type { Context } from '@deepseek-ai/cordis'\nimport { AgyAdapter } from './adapter/adapter.ts'\nimport type { AgyAttachmentStore } from './adapter/adapter.ts'\nimport { AGY_PROVIDER } from './adapter/models.ts'\nimport { ModelVisibility } from './model-visibility.ts'\nimport { ThinkingBudgetStore } from './thinking-budget.ts'\nimport { UsageStats } from './stats.ts'\nimport { probeFetch, proxiedFetch } from './proxy.ts'\nimport { pickProbeProxyUrl } from './runtime/rotation.ts'\nimport { resolveAntigravityVersion } from './runtime/version.ts'\nimport type { FetchLike } from './runtime/version.ts'\nimport { AgySessionManager } from './session.ts'\nimport { JsonAccountStore } from './store/accounts.ts'\nimport type { AccountStore } from './store/accounts.ts'\nimport {\n MASTER_KEY_REF,\n createAesGcmCodec,\n deriveKey,\n loadMasterKey,\n persistMasterKey,\n resolveDshHome,\n resolveMasterKeyCodec,\n} from './store/keyring.ts'\nimport type { SecretCodec } from './store/keyring.ts'\n\nexport interface CredentialsSeam {\n resolve(ref: string): Promise<{ value: string } | undefined>\n set(ref: string, value: string): Promise<void>\n}\n\nfunction codecFrom(masterKey: string): SecretCodec {\n return createAesGcmCodec(deriveKey(masterKey))\n}\n\n/** Resolve or create the master key, preferring the credentials seam when available. */\nexport async function resolveCodec(ctx: Context): Promise<{ codec: SecretCodec; created: boolean }> {\n const dshHome = resolveDshHome()\n const credentials = ctx.get('credentials') as CredentialsSeam | undefined\n\n if (credentials) {\n const resolved = await credentials.resolve(MASTER_KEY_REF)\n if (resolved) return { codec: codecFrom(resolved.value), created: false }\n const fileKey = loadMasterKey(dshHome)\n if (fileKey) return { codec: codecFrom(fileKey), created: false }\n const fresh = randomBytes(32).toString('hex')\n try {\n await credentials.set(MASTER_KEY_REF, fresh)\n return { codec: codecFrom(fresh), created: true }\n } catch {\n // Read-only shadowing: persist to the credentials document directly.\n persistMasterKey(dshHome, fresh)\n return { codec: codecFrom(fresh), created: true }\n }\n }\n\n return resolveMasterKeyCodec(dshHome)\n}\n\n/**\n * Fire-and-forget version warm-up so fingerprint generation inside the\n * rate-limit path never waits on a cold feed.\n *\n * Routed through `pickProbeProxyUrl`: the release feeds are not account-scoped,\n * but the host IP is exactly what a per-account-proxy user asked to hide, and a\n * bare `proxiedFetch` sends this boot-time request directly. Never throws — an\n * unreadable store or a dead feed must not be the reason plugin boot fails.\n */\nasync function warmVersionCache(store: AccountStore): Promise<void> {\n let fetchImpl: FetchLike = proxiedFetch\n try {\n const storage = await store.load()\n const proxyUrl = pickProbeProxyUrl(storage.accounts, storage.activeIndex)\n fetchImpl = probeFetch(proxyUrl)\n } catch {\n // Fall through to the env/direct route.\n }\n await resolveAntigravityVersion(fetchImpl).catch(() => {})\n}\n\n/** Build the store, session manager, and adapter for one plugin entry. */\nexport async function createAgyRuntime(ctx: Context): Promise<{\n store: AccountStore\n sessions: AgySessionManager\n adapter: AgyAdapter\n stats: UsageStats\n modelVisibility: ModelVisibility\n thinkingBudget: ThinkingBudgetStore\n}> {\n const { codec } = await resolveCodec(ctx)\n const dshHome = resolveDshHome()\n const store = new JsonAccountStore({ file: `${dshHome}/agy-accounts.json`, codec })\n void warmVersionCache(store)\n // The ledger reports the first failure of a run, once. Without it a ledger\n // that cannot be written (read-only $DSH_HOME, ENOSPC) simply stops counting\n // with nothing anywhere to explain why — the failure mode this whole\n // soft-fail design otherwise hides.\n const stats = new UsageStats({\n onFlushError: (error) => {\n ctx.logger.warn(\n `[dsh-agy] usage ledger could not be written (${stats.path}): `\n + `${error instanceof Error ? error.message : String(error)} — counts are buffered and will retry`,\n )\n },\n })\n const modelVisibility = new ModelVisibility()\n const thinkingBudget = new ThinkingBudgetStore()\n const sessions = new AgySessionManager({\n store,\n recordUsage: (record) => { stats.record(record) },\n })\n // Optional background health probe (DSH_AGY_HEALTH_INTERVAL_MS), off by default.\n const healthIntervalMs = Number(process.env.DSH_AGY_HEALTH_INTERVAL_MS ?? 0)\n if (Number.isFinite(healthIntervalMs) && healthIntervalMs > 0) {\n sessions.startHealthProbe(healthIntervalMs)\n }\n const adapter = new AgyAdapter({\n getSession: (model, conversationKey) => sessions.getSession(model, undefined, conversationKey),\n reportFailure: (kind, session, info) => sessions.reportFailure(kind, session, info),\n markSuccess: (session) => sessions.markSuccess(session),\n resolveAttachments: () => ctx.get('attachments') as AgyAttachmentStore | undefined,\n noteRequestStarted: (account) => sessions.noteRequestStarted(account),\n noteRequestSettled: (account) => sessions.noteRequestSettled(account),\n modelVisibility,\n thinkingBudgetFor: (level) => thinkingBudget.budgetFor(level),\n claudeBudgetFor: () => thinkingBudget.claudeBudget(),\n tieredBudgetFor: () => thinkingBudget.tieredBudget(),\n recordUsage: (record) => { stats.record({ ...record, source: 'chat' }) },\n })\n // Persist the ledger on normal termination. `exit` covers both a graceful\n // shutdown and the CLI's explicit `process.exit` calls.\n //\n // Deliberately NO signal handlers: installing a SIGINT/SIGTERM listener\n // replaces Node's default terminate behavior, so it must flush and then\n // re-raise or Ctrl-C stops working — a real bug for the sake of at most one\n // throttle window (5s) of counts on an interrupted process. The timer in\n // UsageStats already bounds that loss, and statistics are diagnostics.\n process.once('exit', () => { stats.flushSync() })\n return { store, sessions, adapter, stats, modelVisibility, thinkingBudget }\n}\n\nexport { AGY_PROVIDER }\n"],"mappings":"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"}
@@ -1,6 +1,40 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
2
  import { EnvHttpProxyAgent, ProxyAgent } from "undici";
3
3
  import { createConnection } from "node:net";
4
+ //#region src/redact.ts
5
+ /**
6
+ * Credential redaction for text that may reach logs, the GUI, or a terminal.
7
+ *
8
+ * A leaf module on purpose: `runtime/classify.ts` and `proxy.ts` both need it,
9
+ * and `classify.ts` already imports `proxy.ts`, so importing redaction from
10
+ * either one into the other would close an import cycle.
11
+ */
12
+ /**
13
+ * Strip proxy credentials from URL-like text so `user:pass` never reaches logs
14
+ * or the GUI. Splits at the LAST `@` of the authority, which is the real
15
+ * userinfo delimiter (RFC 3986: a host cannot contain a raw `@`), so a password
16
+ * that itself contains `@` is redacted whole rather than truncated at the first
17
+ * one — the failure mode of the character-class regex this replaced.
18
+ *
19
+ * Two passes, because a rejected proxy URL is exactly the input that does not
20
+ * parse: the first handles `scheme://user:pass@host`, the second catches a
21
+ * scheme-less `user:pass@host` that {@link import('./proxy.ts').normalizeProxyUrl}
22
+ * rejects before ever building a `URL`. Both split at the LAST `@`, so a password
23
+ * containing `@` is redacted whole in either shape (truncating at the first `@`
24
+ * left the password's tail in the message). The second requires a `:` inside the
25
+ * userinfo, so a bare address (`someone@example.com`) is left alone.
26
+ *
27
+ * Over-redaction is the intended bias: an error message losing a detail costs
28
+ * nothing, while an unredacted one can put a live proxy password in a screenshot.
29
+ */
30
+ function redactCredentials(text) {
31
+ return text.replace(/([a-z][a-z0-9+.-]*:\/\/)([^\s/?#]+)/gi, (match, scheme, authority) => {
32
+ const at = authority.lastIndexOf("@");
33
+ if (at === -1) return match;
34
+ return `${scheme}<REDACTED>@${authority.slice(at + 1)}`;
35
+ }).replace(/(^|[\s(]|\/\/)([^\s/@:]+:(?!\/\/)[^\s]*)@/g, (_match, lead) => `${lead}<REDACTED>@`);
36
+ }
37
+ //#endregion
4
38
  //#region src/proxy.ts
5
39
  /**
6
40
  * Proxy-aware fetch: per-account proxy (http/https/socks5) + env fallback.
@@ -16,6 +50,7 @@ var proxy_exports = /* @__PURE__ */ __exportAll({
16
50
  isProxyReachable: () => isProxyReachable,
17
51
  isProxyUnreachableError: () => isProxyUnreachableError,
18
52
  normalizeProxyUrl: () => normalizeProxyUrl,
53
+ probeFetch: () => probeFetch,
19
54
  proxiedFetch: () => proxiedFetch,
20
55
  proxyUrlForLogs: () => proxyUrlForLogs,
21
56
  tagProxyUnreachable: () => tagProxyUnreachable
@@ -77,10 +112,11 @@ function proxyUrlForLogs(proxyUrl) {
77
112
  const fam = proxyUrl.match(/\?family=(ipv4|ipv6)$/);
78
113
  const base = fam ? proxyUrl.slice(0, -fam[0].length) : proxyUrl;
79
114
  const u = new URL(base);
115
+ if (!u.hostname) return redactCredentials(proxyUrl);
80
116
  const port = u.port || defaultPort(u.protocol);
81
117
  return `${u.protocol}//${u.hostname}:${port}`;
82
118
  } catch {
83
- return proxyUrl;
119
+ return redactCredentials(proxyUrl);
84
120
  }
85
121
  }
86
122
  const PROXY_UNREACHABLE_CODES = /* @__PURE__ */ new Set([
@@ -315,7 +351,20 @@ function accountFetch(routing) {
315
351
  ...routing?.streaming ? { streaming: true } : {}
316
352
  }));
317
353
  }
354
+ /**
355
+ * Fetch for a POOL-LEVEL probe: a request that belongs to no single account
356
+ * (the Antigravity release feeds) but still egresses the host.
357
+ *
358
+ * `undefined` means the env/direct route, which is also where an unproxied
359
+ * account's traffic goes. Given a URL it routes there, so a per-account-proxy
360
+ * user does not leak the real IP on a boot-time or failure-path probe. Both
361
+ * probe sites (plugin boot, the rate-limit fingerprint path) go through here so
362
+ * the routing rule cannot drift between them.
363
+ */
364
+ function probeFetch(proxyUrl) {
365
+ return proxyUrl === void 0 || proxyUrl === "" ? proxiedFetch : accountFetch({ proxyUrl });
366
+ }
318
367
  //#endregion
319
- export { proxiedFetch as a, normalizeProxyUrl as i, isProxyReachable as n, proxyUrlForLogs as o, isProxyUnreachableError as r, proxy_exports as s, accountFetch as t };
368
+ export { probeFetch as a, proxy_exports as c, normalizeProxyUrl as i, redactCredentials as l, isProxyReachable as n, proxiedFetch as o, isProxyUnreachableError as r, proxyUrlForLogs as s, accountFetch as t };
320
369
 
321
- //# sourceMappingURL=proxy-DLvX8yxv.mjs.map
370
+ //# sourceMappingURL=proxy-DfaL73mL.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy-DfaL73mL.mjs","names":[],"sources":["../src/redact.ts","../src/proxy.ts"],"sourcesContent":["/**\n * Credential redaction for text that may reach logs, the GUI, or a terminal.\n *\n * A leaf module on purpose: `runtime/classify.ts` and `proxy.ts` both need it,\n * and `classify.ts` already imports `proxy.ts`, so importing redaction from\n * either one into the other would close an import cycle.\n */\n\n/**\n * Strip proxy credentials from URL-like text so `user:pass` never reaches logs\n * or the GUI. Splits at the LAST `@` of the authority, which is the real\n * userinfo delimiter (RFC 3986: a host cannot contain a raw `@`), so a password\n * that itself contains `@` is redacted whole rather than truncated at the first\n * one — the failure mode of the character-class regex this replaced.\n *\n * Two passes, because a rejected proxy URL is exactly the input that does not\n * parse: the first handles `scheme://user:pass@host`, the second catches a\n * scheme-less `user:pass@host` that {@link import('./proxy.ts').normalizeProxyUrl}\n * rejects before ever building a `URL`. Both split at the LAST `@`, so a password\n * containing `@` is redacted whole in either shape (truncating at the first `@`\n * left the password's tail in the message). The second requires a `:` inside the\n * userinfo, so a bare address (`someone@example.com`) is left alone.\n *\n * Over-redaction is the intended bias: an error message losing a detail costs\n * nothing, while an unredacted one can put a live proxy password in a screenshot.\n */\nexport function redactCredentials(text: string): string {\n return text\n .replace(/([a-z][a-z0-9+.-]*:\\/\\/)([^\\s/?#]+)/gi, (match, scheme: string, authority: string) => {\n const at = authority.lastIndexOf('@')\n if (at === -1) return match\n return `${scheme}<REDACTED>@${authority.slice(at + 1)}`\n })\n // The password class must allow `@` and `:` (bounded only by whitespace):\n // excluding them makes the engine stop at the FIRST `@` or `:`, which is what\n // truncated a password containing either one. Greedy matching plus\n // backtracking lands on the last `@` of the token, matching pass 1's rule.\n //\n // The `(?!//)` guard and the `//` anchor keep this pass off a string pass 1\n // already rewrote: without them `http://<REDACTED>@` re-matched with `http` as\n // the user and `//<REDACTED>` as the password, so the message lost the scheme\n // it was trying to report. `//user:pass@host` (protocol-relative) still needs\n // the `//` anchor to be caught at all.\n .replace(/(^|[\\s(]|\\/\\/)([^\\s/@:]+:(?!\\/\\/)[^\\s]*)@/g, (_match, lead: string) => `${lead}<REDACTED>@`)\n}\n","/**\n * Proxy-aware fetch: per-account proxy (http/https/socks5) + env fallback.\n * - account.proxy present => per-account dispatcher (fail-closed, not affected by NO_PROXY)\n * - otherwise => EnvHttpProxyAgent (honours HTTP_PROXY/HTTPS_PROXY/NO_PROXY)\n * Applied per-request via dispatcher option so the host's global dispatcher stays untouched.\n */\n\nimport { EnvHttpProxyAgent, ProxyAgent } from 'undici'\nimport { createConnection } from 'node:net'\nimport { createRequire } from 'node:module'\nimport { isProxyRouted } from './types.ts'\nimport type { AccountRouting } from './types.ts'\nimport { redactCredentials } from './redact.ts'\n\nconst envAgent = new EnvHttpProxyAgent()\n\n/**\n * Streaming variant of the env agent. A generation stream may legitimately stay\n * silent for minutes mid-turn (reasoning), and undici's `bodyTimeout` is a\n * per-gap inactivity timer — the 30s control-plane value kills such a turn.\n * Only generation uses this agent; every other call keeps the short bound.\n */\nconst envStreamingAgent = new EnvHttpProxyAgent({ bodyTimeout: 0 })\n\n/** The env proxy agent (exported for tests). */\nexport const proxyAgent = envAgent\n\n/** The streaming env proxy agent (exported for tests). */\nexport const proxyStreamingAgent = envStreamingAgent\n\n// ── Dispatcher cache, keyed `normalizedProxyUrl|stream` ──\n// Entries live for the process, so the cache size tracks the number of DISTINCT\n// proxy URLs in use rather than the account count. (It previously claimed to be\n// \"bounded by MAX_ACCOUNTS=10\" — a constant nothing enforced.)\nconst dispatcherCache = new Map<string, any>()\n\n// ── Proxy URL normalization ──\nconst SUPPORTED = new Set(['http:', 'https:', 'socks5:', 'socks5h:'])\n\nfunction defaultPort(protocol: string): string {\n if (protocol === 'https:') return '443'\n if (protocol === 'socks5:' || protocol === 'socks5h:') return '1080'\n return '8080'\n}\n\nexport function normalizeProxyUrl(proxyUrl: string): string {\n let raw = proxyUrl.trim()\n if (!raw) throw new Error('[proxy] empty proxy URL')\n // alias socks:// -> socks5://\n if (raw.toLowerCase().startsWith('socks://')) raw = 'socks5://' + raw.slice('socks://'.length)\n // strip family marker before parse, re-append after\n const familyMatch = raw.match(/\\?family=(ipv4|ipv6)$/)\n const familySuffix = familyMatch ? familyMatch[0] : ''\n const baseRaw = familySuffix ? raw.slice(0, -familySuffix.length) : raw\n\n let parsed: URL\n try {\n parsed = new URL(baseRaw)\n } catch {\n throw new Error(`[proxy] invalid proxy URL: ${proxyUrlForLogs(proxyUrl)}`)\n }\n // normalize socks5h -> socks5 (remote DNS, same agent)\n let protocol = parsed.protocol.toLowerCase()\n if (protocol === 'socks5h:') protocol = 'socks5:'\n if (!SUPPORTED.has(protocol) && protocol !== 'socks5:') {\n // allow socks5h already normalized\n if (!SUPPORTED.has(parsed.protocol.toLowerCase())) {\n throw new Error(`[proxy] unsupported protocol: ${parsed.protocol}`)\n }\n }\n // ensure protocol is one of http/https/socks5\n if (!['http:', 'https:', 'socks5:'].includes(protocol)) {\n throw new Error(`[proxy] unsupported protocol: ${parsed.protocol}`)\n }\n if (!parsed.hostname) throw new Error('[proxy] missing host')\n\n let port = parsed.port\n if (!port) port = defaultPort(protocol)\n const portNum = Number(port)\n if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) {\n throw new Error('[proxy] invalid port')\n }\n // Build auth from the DECODED credentials. `URL.username`/`password` return the\n // percent-ENCODED substrings (not decoded, despite what the older comment here\n // claimed), so encoding them again double-encodes: `p@ss` became `p%2540ss`,\n // which the proxy decodes to the literal `p%40ss` and rejects. Decoding first\n // also makes this function idempotent, which matters because a stored proxy URL\n // is normalized again on every request.\n const auth = parsed.username\n ? `${encodeURIComponent(decodeURIComponent(parsed.username))}${parsed.password ? `:${encodeURIComponent(decodeURIComponent(parsed.password))}` : ''}@`\n : ''\n const normalizedBase = `${protocol}//${auth}${parsed.hostname}:${port}`\n return familySuffix ? `${normalizedBase}${familySuffix}` : normalizedBase\n}\n\nexport function proxyUrlForLogs(proxyUrl: string): string {\n try {\n // handle family suffix\n const fam = proxyUrl.match(/\\?family=(ipv4|ipv6)$/)\n const base = fam ? proxyUrl.slice(0, -fam[0].length) : proxyUrl\n const u = new URL(base)\n // A scheme-less `user:pass@host` parses \"successfully\" as the non-special\n // scheme `user:` with an empty host, which would render as `user://:8080` —\n // a misleading string in the very error message meant to explain the input.\n // Fall through to the redacting fallback instead of reporting a host that\n // was never in the input.\n if (!u.hostname) return redactCredentials(proxyUrl)\n const port = u.port || defaultPort(u.protocol)\n return `${u.protocol}//${u.hostname}:${port}`\n } catch {\n // Unparseable input is exactly the case that still holds credentials: this\n // value is embedded in `normalizeProxyUrl`'s error message, which reaches the\n // GUI and stderr. Returning the raw input leaked `user:pass` there, so the\n // fallback redacts instead of echoing.\n return redactCredentials(proxyUrl)\n }\n}\n\n// ── Proxy unreachable detection (mirrors OmniRoute proxyFetch.ts:30) ──\nconst PROXY_UNREACHABLE_CODES = new Set([\n 'ECONNREFUSED',\n 'ECONNRESET',\n 'ETIMEDOUT',\n 'ENETUNREACH',\n 'EHOSTUNREACH',\n 'EPIPE',\n 'UND_ERR_CONNECT_TIMEOUT',\n 'UND_ERR_SOCKET',\n])\n\nfunction isLoopbackUrl(url: string | URL): boolean {\n try {\n const u = typeof url === 'string' ? new URL(url as string) : (url as URL)\n const h = u.hostname.toLowerCase()\n return h === 'localhost' || h === '127.0.0.1' || h === '::1' || h === '::ffff:127.0.0.1'\n } catch {\n return false\n }\n}\n\nexport function isProxyUnreachableError(err: unknown): boolean {\n const seen = new Set<unknown>()\n let cur: unknown = err\n for (let depth = 0; cur && depth < 5 && !seen.has(cur); depth++) {\n seen.add(cur)\n if (cur && typeof cur === 'object') {\n const code = (cur as { code?: unknown }).code\n if (typeof code === 'string' && PROXY_UNREACHABLE_CODES.has(code)) return true\n const errorCode = (cur as { errorCode?: unknown }).errorCode\n if (errorCode === 'proxy_unreachable' || errorCode === 'PROXY_UNREACHABLE') return true\n const statusCode = (cur as { statusCode?: unknown }).statusCode\n if (statusCode === 503) {\n const m = (cur as { message?: unknown }).message\n if (typeof m === 'string' && m.includes('Proxy unreachable')) return true\n }\n const msg = (cur as { message?: unknown }).message\n if (typeof msg === 'string') {\n if (msg.includes('proxy_unreachable') || msg.includes('PROXY_UNREACHABLE')) return true\n for (const c of PROXY_UNREACHABLE_CODES) {\n if (msg.includes(c)) return true\n }\n }\n }\n cur = (cur as { cause?: unknown })?.cause\n // Also handle AggregateError.errors (undici Happy Eyeballs)\n if (!cur && (err as { errors?: unknown })?.errors && depth === 0) {\n const errs = (err as { errors?: unknown[] }).errors\n if (Array.isArray(errs)) {\n for (const e of errs) if (isProxyUnreachableError(e)) return true\n }\n }\n }\n return false\n}\n\nexport function tagProxyUnreachable<T>(err: T): T {\n if (isProxyUnreachableError(err)) {\n const e = err as unknown as Error & { code?: string; errorCode?: string }\n e.code = 'PROXY_UNREACHABLE'\n e.errorCode = 'proxy_unreachable'\n }\n return err\n}\n\n// ── Fast-fail TCP reachability (2s, 30s healthy / 2s unhealthy cache) ──\nconst FAST_FAIL_TIMEOUT_MS = 2000\nconst HEALTHY_TTL_MS = 30_000\nconst UNHEALTHY_TTL_MS = 2000\ntype HealthEntry = { healthy: boolean; at: number; ttl: number }\nconst healthCache = new Map<string, HealthEntry>()\nconst healthInflight = new Map<string, Promise<boolean>>()\n\nfunction tcpCheck(host: string, port: number, timeoutMs: number): Promise<boolean> {\n return new Promise((resolve) => {\n const socket = createConnection({ host, port }, () => {\n socket.destroy()\n resolve(true)\n })\n socket.setTimeout(timeoutMs)\n socket.on('error', () => resolve(false))\n socket.on('timeout', () => {\n socket.destroy()\n resolve(false)\n })\n })\n}\n\nexport async function isProxyReachable(proxyUrl: string, timeoutMs = FAST_FAIL_TIMEOUT_MS): Promise<boolean> {\n const key = normalizeProxyUrl(proxyUrl)\n const cached = healthCache.get(key)\n if (cached && Date.now() - cached.at < cached.ttl) return cached.healthy\n const existing = healthInflight.get(key)\n if (existing) return existing\n let url: URL\n try {\n const base = key.replace(/\\?family=(ipv4|ipv6)$/, '')\n url = new URL(base)\n } catch {\n healthCache.set(key, { healthy: false, at: Date.now(), ttl: UNHEALTHY_TTL_MS })\n return false\n }\n const host = url.hostname.replace(/^\\[/, '').replace(/\\]$/, '')\n const port = Number(url.port || defaultPort(url.protocol))\n const probe = tcpCheck(host, port, timeoutMs).then((healthy) => {\n healthCache.set(key, { healthy, at: Date.now(), ttl: healthy ? HEALTHY_TTL_MS : UNHEALTHY_TTL_MS })\n return healthy\n })\n healthInflight.set(key, probe)\n try {\n return await probe\n } finally {\n if (healthInflight.get(key) === probe) healthInflight.delete(key)\n }\n}\n\nexport function _clearProxyHealthCacheForTest(): void {\n healthCache.clear()\n healthInflight.clear()\n}\n\n// ── Dispatcher creation (cached) ──\nasync function createSocksDispatcher(proxyUrl: string, dispatcherOpts: Record<string, unknown>): Promise<any> {\n // @ts-ignore — optional dep, only required for socks5\n const { SocksProxyAgent } = await import('socks-proxy-agent')\n // socks-proxy-agent is compatible with undici dispatcher interface via fetch\n return new (SocksProxyAgent as any)(proxyUrl) as any\n}\n\nfunction createHttpDispatcher(proxyUrl: string, dispatcherOpts: Record<string, unknown>): any {\n const clean = proxyUrl.replace(/\\?family=(ipv4|ipv6)$/, '')\n return new ProxyAgent({\n uri: clean,\n // tunnel all (http+https) via CONNECT, same as OmniRoute\n proxyTunnel: true,\n ...dispatcherOpts,\n } as any)\n}\n\n/**\n * Dispatcher options. `bodyTimeout` is undici's per-gap inactivity timer, not a\n * total transfer budget: it fires when NO bytes arrive for that long. The\n * control-plane calls want the short bound, but a generation stream may\n * legitimately stay silent for minutes mid-turn (reasoning), so streaming runs\n * with the timer disabled — the caller's own AbortSignal bounds it instead.\n *\n * `keepAliveTimeout`/`keepAliveMaxTimeout` deliberately stay at 1ms and are NOT\n * loosened for streaming: undici applies them only when `pipelining` is\n * non-zero, and `pipelining: 0` forces every connection to reset on completion,\n * so they are inert here. They are kept at the conservative value spec #8 asks\n * for rather than being replaced by a setting that would never take effect.\n */\nconst DISPATCHER_OPTS = {\n headersTimeout: 30_000,\n bodyTimeout: 30_000,\n connectTimeout: 10_000,\n keepAliveTimeout: 1,\n keepAliveMaxTimeout: 1,\n pipelining: 0,\n} as const\n\n/** Streaming dispatcher options: same bounds, minus the body inactivity timer. */\nconst STREAMING_DISPATCHER_OPTS = { ...DISPATCHER_OPTS, bodyTimeout: 0 } as const\n\n/** Shape shared by both dispatcher option sets. */\nexport type DispatcherOpts = { -readonly [K in keyof typeof DISPATCHER_OPTS]: number }\n\n/** Resolved dispatcher options for a call class (exported as the source of truth). */\nexport function dispatcherOptsFor(streaming: boolean): DispatcherOpts {\n return streaming ? STREAMING_DISPATCHER_OPTS : DISPATCHER_OPTS\n}\n\nfunction dispatcherCacheKey(normalized: string, streaming: boolean): string {\n return streaming ? `${normalized}|stream` : normalized\n}\n\n/**\n * Synchronous dispatcher for a proxy URL (control-plane class only).\n *\n * Streaming must NOT use this: it needs `bodyTimeout: 0`, and the socks path\n * already requires the async variant. Kept sync for callers that cannot await.\n */\nexport function dispatcherFor(proxyUrl?: string): any | undefined {\n if (!proxyUrl) return envAgent as any\n const normalized = normalizeProxyUrl(proxyUrl)\n const cached = dispatcherCache.get(dispatcherCacheKey(normalized, false))\n if (cached) return cached\n const famClean = normalized.replace(/\\?family=(ipv4|ipv6)$/, '')\n let dispatcher: any\n if (famClean.startsWith('socks5:')) {\n // SOCKS needs SocksProxyAgent. Try sync require for cached case; otherwise prefer async path (proxiedFetch uses dispatcherForAsync).\n try {\n const rq = createRequire(import.meta.url)\n const mod = rq('socks-proxy-agent') as { SocksProxyAgent?: new (u: string) => unknown }\n const Cls = mod.SocksProxyAgent\n if (Cls) {\n dispatcher = new (Cls as unknown as new (u: string) => unknown)(normalized) as unknown\n } else {\n throw new Error('no SocksProxyAgent')\n }\n } catch {\n throw new Error('[proxy] socks dispatcher requires async creation — use dispatcherForAsync or proxiedFetch with proxyUrl')\n }\n } else {\n dispatcher = createHttpDispatcher(normalized, DISPATCHER_OPTS as any)\n }\n dispatcherCache.set(dispatcherCacheKey(normalized, false), dispatcher)\n return dispatcher\n}\n\nexport async function dispatcherForAsync(\n proxyUrl?: string,\n options: { streaming?: boolean } = {},\n): Promise<any | undefined> {\n const streaming = options.streaming === true\n if (!proxyUrl) return (streaming ? envStreamingAgent : envAgent) as any\n const normalized = normalizeProxyUrl(proxyUrl)\n const key = dispatcherCacheKey(normalized, streaming)\n const cached = dispatcherCache.get(key)\n if (cached) return cached\n const famClean = normalized.replace(/\\?family=(ipv4|ipv6)$/, '')\n let dispatcher: any\n if (famClean.startsWith('socks5:')) {\n dispatcher = await createSocksDispatcher(normalized, dispatcherOptsFor(streaming) as any)\n } else {\n dispatcher = createHttpDispatcher(normalized, dispatcherOptsFor(streaming) as any)\n }\n dispatcherCache.set(key, dispatcher)\n return dispatcher\n}\n\nexport function _clearDispatcherCacheForTest(): void {\n dispatcherCache.clear()\n}\n\nfunction getTargetUrlString(input: Parameters<typeof fetch>[0]): string {\n if (typeof input === 'string') return input\n try {\n if (input instanceof URL) return input.toString()\n if (typeof (input as Request).url === 'string') return (input as Request).url\n } catch {\n // ignore\n }\n return ''\n}\n\n/** Options accepted by proxiedFetch; `streaming` selects the long-silence dispatcher. */\nexport interface ProxiedFetchOptions {\n proxyUrl?: string\n /**\n * True for a generation stream (long model silences are normal). Selects a\n * dispatcher without the per-gap body inactivity timer.\n */\n streaming?: boolean\n}\n\n/** fetch() that respects per-account proxyUrl (when given) otherwise env. */\nexport const proxiedFetch = async (\n input: Parameters<typeof fetch>[0],\n init?: Parameters<typeof fetch>[1] & { proxyUrl?: string },\n opts?: ProxiedFetchOptions,\n): Promise<Response> => {\n const proxyUrl = (opts as any)?.proxyUrl ?? (init as any)?.proxyUrl\n const streaming = opts?.streaming === true\n // Normalize init without proxyUrl leakage\n const cleanInit = { ...init } as any\n if (cleanInit.proxyUrl) delete cleanInit.proxyUrl\n if (!proxyUrl) {\n return fetch(input, { ...cleanInit, dispatcher: (streaming ? envStreamingAgent : envAgent) as any })\n }\n // Loopback bypass: per-account proxy must never intercept OAuth loopback callback (spec 1)\n const targetStr = getTargetUrlString(input)\n if (targetStr && isLoopbackUrl(targetStr)) {\n return fetch(input, cleanInit as RequestInit)\n }\n // Fast-fail pre-check (skip for env path)\n try {\n const reachable = await isProxyReachable(proxyUrl)\n if (!reachable) {\n const err = new Error(`[Proxy Fast-Fail] Proxy unreachable: ${proxyUrlForLogs(proxyUrl)}`) as Error & { code?: string; errorCode?: string; statusCode?: number }\n err.code = 'PROXY_UNREACHABLE'\n err.errorCode = 'proxy_unreachable'\n ;(err as any).statusCode = 503\n throw err\n }\n } catch (e) {\n if ((e as any)?.errorCode === 'proxy_unreachable') throw e\n // normalization failure -> throw\n if (e instanceof Error && e.message.startsWith('[proxy]')) throw e\n }\n\n let dispatcher: any\n try {\n dispatcher = await dispatcherForAsync(proxyUrl, { streaming })\n } catch (e) {\n throw tagProxyUnreachable(e)\n }\n try {\n return await fetch(input, { ...cleanInit, dispatcher })\n } catch (err) {\n // Tag proxy unreachable so caller can classify\n throw tagProxyUnreachable(err)\n }\n}\n\n// Keep named export for tests that assert instanceof\nexport { envAgent as _envAgentForTest }\n\n/**\n * Bind one account's routing to a fetch implementation.\n *\n * The single entry point every account-scoped call uses (generation, test call,\n * model/quota discovery, project healing, import enrichment). Threading a bare\n * `proxyUrl` at each site is what let six call sites drift and silently route a\n * proxied account over the host's real IP, so the invariant is enforced here\n * instead of by review (AGENTS.md \"Proxy Routing\").\n */\nexport function accountFetch(routing: AccountRouting | undefined): typeof fetch {\n return ((input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) =>\n proxiedFetch(input, init, {\n ...(routing?.proxyUrl ? { proxyUrl: routing.proxyUrl } : {}),\n ...(routing?.streaming ? { streaming: true } : {}),\n })) as typeof fetch\n}\n\n/**\n * Fetch for a POOL-LEVEL probe: a request that belongs to no single account\n * (the Antigravity release feeds) but still egresses the host.\n *\n * `undefined` means the env/direct route, which is also where an unproxied\n * account's traffic goes. Given a URL it routes there, so a per-account-proxy\n * user does not leak the real IP on a boot-time or failure-path probe. Both\n * probe sites (plugin boot, the rate-limit fingerprint path) go through here so\n * the routing rule cannot drift between them.\n */\nexport function probeFetch(proxyUrl: string | undefined): typeof fetch {\n return proxyUrl === undefined || proxyUrl === '' ? proxiedFetch : accountFetch({ proxyUrl })\n}\n\n/**\n * Wrap a fetch implementation with a TOTAL wall-clock budget.\n *\n * The dispatcher's `bodyTimeout`/`headersTimeout` are per-GAP timers, not a\n * total budget, and `fetchAvailableModels` tries four endpoints in series — so\n * a slow network can hold a caller for minutes (worst case ~4 x (10s connect +\n * 30s headers)). Anything gating a user-visible RPC needs a real ceiling.\n *\n * Composed with `AbortSignal.any` rather than replacing the caller's signal, so\n * an explicit abort still wins.\n *\n * @param fetchImpl - the routed fetch to wrap.\n * @param ms - total budget in milliseconds.\n * @returns a fetch that fails once the budget expires.\n */\nexport function withTotalTimeout(fetchImpl: typeof fetch, ms: number): typeof fetch {\n return ((input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => {\n const budget = AbortSignal.timeout(ms)\n const signal = init?.signal ? AbortSignal.any([init.signal, budget]) : budget\n return fetchImpl(input, { ...init, signal })\n }) as typeof fetch\n}\n\n/** Whether these requests are pinned to an explicit per-account proxy. */\nexport { isProxyRouted }\n"],"mappings":"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"}
@@ -0,0 +1,127 @@
1
+ import { h as getAgyBootstrapUserAgent, r as AGY_ENDPOINT_FALLBACKS } from "./constants-Db4tmyfb.mjs";
2
+ import { o as proxiedFetch } from "./proxy-DfaL73mL.mjs";
3
+ //#region src/adapter/quota-summary.ts
4
+ /**
5
+ * `v1internal:retrieveUserQuotaSummary` — the grouped 5-hour / weekly quota
6
+ * windows, as distinct from `fetchAvailableModels`' per-model `quotaInfo`.
7
+ *
8
+ * WHY THIS ENDPOINT EXISTS HERE AT ALL. `fetchAvailableModels` reports only
9
+ * `remainingFraction` + `resetTime` per model, with NO window field: measured
10
+ * across every id, the only keys under `quotaInfo` are those two. So a "5-hour
11
+ * vs weekly" view is not derivable from it, and the summary endpoint is the only
12
+ * source for both windows. Recorded in docs/ANTIGRAVITY-API.md §3, which
13
+ * previously stated the opposite ("no retrieveUserQuota path").
14
+ *
15
+ * The shapes differ meaningfully and must not be merged:
16
+ * - `fetchAvailableModels` -> per MODEL (used for ranking/rotation)
17
+ * - `retrieveUserQuotaSummary` -> per GROUP (Gemini, Claude+GPT), 2 windows each
18
+ * The group split is upstream's own, so it is carried through verbatim rather
19
+ * than re-derived from model-id prefixes: `3p-*` covers Claude AND GPT, which no
20
+ * prefix rule in `modelFamilyOf` would group together.
21
+ *
22
+ * Measured response (live account):
23
+ * { groups: [ { displayName, buckets: [
24
+ * { bucketId: "gemini-5h", window: "5h", remainingFraction, resetTime },
25
+ * { bucketId: "gemini-weekly", window: "weekly", ... } ] } ] }
26
+ *
27
+ * The identity used is the same bootstrap User-Agent as `fetchAvailableModels`
28
+ * (that pair is what answers 200 here), so nothing new is impersonated.
29
+ */
30
+ const QUOTA_SUMMARY_PATH = "/v1internal:retrieveUserQuotaSummary";
31
+ /**
32
+ * Sort rank for an upstream window token: shorter windows first.
33
+ *
34
+ * An explicit table so the order states the intended duration ordering rather
35
+ * than inferring it from string length. Unknown tokens rank last (and tie-break
36
+ * alphabetically) so a window added upstream later is still shown, just after
37
+ * the ones we understand.
38
+ */
39
+ function windowRank(window) {
40
+ return {
41
+ "5h": 0,
42
+ daily: 1,
43
+ weekly: 2,
44
+ monthly: 3
45
+ }[window] ?? Number.MAX_SAFE_INTEGER;
46
+ }
47
+ /** Clamp to 0..1; a non-number is "unknown" rather than zero. */
48
+ function fractionOf(raw) {
49
+ if (typeof raw !== "number" || !Number.isFinite(raw)) return null;
50
+ return Math.max(0, Math.min(1, raw));
51
+ }
52
+ /** A non-empty string, else null. */
53
+ function stringOf(raw) {
54
+ return typeof raw === "string" && raw !== "" ? raw : null;
55
+ }
56
+ /**
57
+ * Parse a summary payload into groups.
58
+ *
59
+ * Defensive like the rest of the wire parsing: a group with no usable window is
60
+ * DROPPED rather than rendered as an empty card, because a group row with no
61
+ * windows tells the user nothing and would push a real group off screen. Buckets
62
+ * are ordered with the shorter window first so a group reads "immediate pressure,
63
+ * then the weekly budget" consistently regardless of upstream order.
64
+ */
65
+ function parseQuotaSummary(raw) {
66
+ const data = raw;
67
+ if (typeof data !== "object" || data === null) return [];
68
+ const groups = [];
69
+ for (const group of Array.isArray(data.groups) ? data.groups : []) {
70
+ if (typeof group !== "object" || group === null) continue;
71
+ const windows = [];
72
+ for (const bucket of Array.isArray(group.buckets) ? group.buckets : []) {
73
+ if (typeof bucket !== "object" || bucket === null) continue;
74
+ const bucketId = stringOf(bucket.bucketId);
75
+ const window = stringOf(bucket.window);
76
+ if (bucketId === null || window === null) continue;
77
+ windows.push({
78
+ bucketId,
79
+ window,
80
+ remainingFraction: fractionOf(bucket.remainingFraction),
81
+ resetTime: stringOf(bucket.resetTime)
82
+ });
83
+ }
84
+ if (windows.length === 0) continue;
85
+ windows.sort((a, b) => windowRank(a.window) - windowRank(b.window) || a.window.localeCompare(b.window));
86
+ groups.push({
87
+ name: stringOf(group.displayName) ?? "",
88
+ windows
89
+ });
90
+ }
91
+ return groups;
92
+ }
93
+ /**
94
+ * Fetch and parse the grouped windows for one account.
95
+ *
96
+ * Mirrors `fetchAvailableModels`: the endpoint fallback chain is walked here, and
97
+ * the caller injects only the routed+time-bounded fetch, so the account's proxy
98
+ * is honoured (omitting it would go direct and leak the account's real IP) and
99
+ * one shared budget bounds both calls.
100
+ *
101
+ * @param accessToken - a live access token for the account.
102
+ * @param projectId - the account's Cloud Code project id, when known.
103
+ * @param fetchImpl - routed, time-bounded fetch implementation.
104
+ * @returns the parsed groups; empty when no endpoint answers (never throws, so a
105
+ * summary failure cannot discard the ranking data fetched alongside it).
106
+ */
107
+ async function fetchQuotaSummary(accessToken, projectId, fetchImpl = proxiedFetch) {
108
+ const body = JSON.stringify(projectId ? { project: projectId } : {});
109
+ for (const baseEndpoint of AGY_ENDPOINT_FALLBACKS) try {
110
+ const response = await fetchImpl(`${baseEndpoint}${QUOTA_SUMMARY_PATH}`, {
111
+ method: "POST",
112
+ headers: {
113
+ Authorization: `Bearer ${accessToken}`,
114
+ "Content-Type": "application/json",
115
+ "User-Agent": getAgyBootstrapUserAgent()
116
+ },
117
+ body
118
+ });
119
+ if (!response.ok) continue;
120
+ return parseQuotaSummary(await response.json());
121
+ } catch {}
122
+ return [];
123
+ }
124
+ //#endregion
125
+ export { fetchQuotaSummary };
126
+
127
+ //# sourceMappingURL=quota-summary-5CypVffd.mjs.map
@@ -0,0 +1 @@
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"}