dsh-agy 0.4.4 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/lib/{accounts-DW-BL9c3.mjs → accounts-DtIsWCeF.mjs} +15 -11
- package/lib/{accounts-DW-BL9c3.mjs.map → accounts-DtIsWCeF.mjs.map} +1 -1
- package/lib/cli/index.mjs +6 -6
- package/lib/client.js +181 -23
- package/lib/{constants-CmA9wXNe.mjs → constants-AdsD4sKh.mjs} +3 -3
- package/lib/{constants-CmA9wXNe.mjs.map → constants-AdsD4sKh.mjs.map} +1 -1
- package/lib/{exchange-C-tndv7j.mjs → exchange-B_rHhU8K.mjs} +3 -3
- package/lib/{exchange-C-tndv7j.mjs.map → exchange-B_rHhU8K.mjs.map} +1 -1
- package/lib/{import-SxPDjH3u.mjs → import-BqyA9tzt.mjs} +4 -4
- package/lib/{import-SxPDjH3u.mjs.map → import-BqyA9tzt.mjs.map} +1 -1
- package/lib/index.mjs +2 -2
- package/lib/{models-CjB0rXtI.mjs → models-D_54Ro5z.mjs} +3 -3
- package/lib/{models-CjB0rXtI.mjs.map → models-D_54Ro5z.mjs.map} +1 -1
- package/lib/{parse-38V1zT1B.mjs → parse-CeJc7cHo.mjs} +141 -19
- package/lib/parse-CeJc7cHo.mjs.map +1 -0
- package/lib/{plugin-common-CKTYXxSc.mjs → plugin-common-BlGbNHxX.mjs} +426 -73
- package/lib/plugin-common-BlGbNHxX.mjs.map +1 -0
- package/lib/{proxy-BbuQX5U9.mjs → proxy-B7VnbAfO.mjs} +5 -2
- package/lib/proxy-B7VnbAfO.mjs.map +1 -0
- package/lib/{quota-summary-DEA3P3Zh.mjs → quota-summary-DarIZkmo.mjs} +3 -3
- package/lib/{quota-summary-DEA3P3Zh.mjs.map → quota-summary-DarIZkmo.mjs.map} +1 -1
- package/lib/{translate-BR1kuCaz.mjs → translate-vh3M5H_9.mjs} +51 -9
- package/lib/translate-vh3M5H_9.mjs.map +1 -0
- package/lib/web/plugin.d.mts +23 -1
- package/lib/web/plugin.d.mts.map +1 -1
- package/lib/web/plugin.mjs +86 -19
- package/lib/web/plugin.mjs.map +1 -1
- package/package.json +1 -1
- package/lib/parse-38V1zT1B.mjs.map +0 -1
- package/lib/plugin-common-CKTYXxSc.mjs.map +0 -1
- package/lib/proxy-BbuQX5U9.mjs.map +0 -1
- package/lib/translate-BR1kuCaz.mjs.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-common-CKTYXxSc.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/recent-store.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, UnmappedFinishReasonError } 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 rateLimited?: boolean\n /** Failure classification, when the record failed. */\n reason?: string\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\n/** How long discovered models stay cached before re-probing upstream (10 minutes). */\nexport const MODEL_LIST_CACHE_TTL_MS = 10 * 60 * 1000\n\nexport class AgyAdapter extends LlmAdapter {\n private readonly options: AgyAdapterOptions\n private cachedModels: { models: readonly LlmModelInfo[]; at: number } | null = null\n private inFlightModels: Promise<readonly LlmModelInfo[]> | null = null\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 * Cached in memory for `MODEL_LIST_CACHE_TTL_MS` with in-flight deduplication.\n * DSH 0.2.0-rc.1 calls `listModels` on every model and effort selection via\n * `requireModel`; without caching, each click paid a multi-second round-trip to\n * Google's `fetchAvailableModels` through the proxy, showing a loading spinner\n * on every interaction.\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(force = false): Promise<readonly LlmModelInfo[]> {\n const now = Date.now()\n if (!force && this.cachedModels && now - this.cachedModels.at < MODEL_LIST_CACHE_TTL_MS) {\n return this.cachedModels.models\n }\n if (this.inFlightModels) return this.inFlightModels\n\n const task = (async () => {\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 const models = await listAgyModels(session?.auth.access, session?.account.projectId, accountFetch(routing))\n this.cachedModels = { models, at: Date.now() }\n return models\n } catch (error) {\n if (error instanceof AgyPoolBlockedError || error instanceof AgyAuthError) {\n return catalogModelList()\n }\n if (this.cachedModels) return this.cachedModels.models\n throw error\n } finally {\n this.inFlightModels = null\n }\n })()\n\n this.inFlightModels = task\n return task\n }\n\n /**\n * Drop the cached model list.\n *\n * Model discovery is account-scoped (what `fetchAvailableModels` reports and\n * which proxy it rides depend on the account), but the cache is deliberately\n * keyed to NO account: a keyed read would have to call `getSession()` on\n * every hit, and that call can probe quota or refresh a token — the network\n * cost this cache exists to remove. Every event that can switch the account\n * discovery rides (activation, rotation) therefore drops the cache instead.\n */\n invalidateModelCache(): void {\n this.cachedModels = null\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, reason: 'network-error' }, 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 reason: classified.kind,\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 // An unmapped finishReason (SAFETY, MALFORMED_FUNCTION_CALL, ...) is a\n // CONTENT-level verdict: the request reached a healthy account and the\n // upstream chose to stop the response. Reporting network-error would\n // cool and rotate the account for a wall the next request may never\n // hit; request-error is a no-op at account level. DSH sees a terminal\n // UPSTREAM error either way (below).\n const unmappedFinish = error instanceof UnmappedFinishReasonError\n await this.options.reportFailure(unmappedFinish ? 'request-error' : '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, reason: unmappedFinish ? 'request-error' : 'network-error' }, 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 reason?: string\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.reason === undefined ? {} : { reason: result.reason }),\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 } from 'node:path'\nimport { migrateToAgyDir } from './store/paths.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 /**\n * Defaults to `$DSH_HOME/agy/agy-models.json`, migrating the legacy\n * `$DSH_HOME/agy-models.json` by one-shot rename (see `migrateToAgyDir`).\n */\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 ?? migrateToAgyDir('agy-models.json').file\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 } from 'node:path'\nimport { migrateToAgyDir } from './store/paths.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 /**\n * Defaults to `$DSH_HOME/agy/agy-thinking.json`, migrating the legacy\n * `$DSH_HOME/agy-thinking.json` by one-shot rename (see `migrateToAgyDir`).\n */\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 ?? migrateToAgyDir('agy-thinking.json').file\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/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 } from 'node:path'\nimport lockfile from 'proper-lockfile'\nimport { migrateToAgyDir } from './store/paths.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 * The failure classification this record carries ('rate-limit',\n * 'network-error', 'auth-failure', 'verification-required', 'project-error',\n * 'quota-exhausted'), when it failed. The counters ignore it: it exists for\n * the recent-activity ring, where \"why did it rotate\" is the whole question.\n */\n reason?: string\n}\n\n/** Cap of the in-memory recent-activity ring (see `UsageStats.recentRequests`). */\nexport const RECENT_MAX = 200\n\n/**\n * One recent record, as the UI's \"what just happened\" list shows it.\n *\n * Flattened from `UsageRecord` at capture time so the ring never aliases the\n * caller's object, and shaped for DISPLAY: pool events become `kind:\n * 'rotation'`, absent numbers become null. The ring answers \"just now\" — the\n * gap between the live in-flight line and the persisted ledger, whose records\n * collapse into counters the moment they are flushed.\n */\nexport interface RecentActivity {\n at: number\n account: string | null\n model: string | null\n kind: 'chat' | 'cli' | 'verify' | 'test' | 'rotation'\n ok: boolean\n rateLimited: boolean\n latencyMs: number | null\n ttftMs: number | null\n output: number | null\n /** Failure classification token, or null when the record succeeded. */\n reason: string | null\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. */\nexport function 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 /**\n * Defaults to `$DSH_HOME/agy/agy-stats.json`, migrating the legacy\n * `$DSH_HOME/agy-stats.json` by one-shot rename (see `migrateToAgyDir`).\n */\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 * Persistence half of the recent-activity ring (see `recent-store.ts`).\n * Called with each flattened record as it is captured; absent means the ring\n * stays per-process, as before.\n */\n persistRecent?: (entry: RecentActivity) => 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 private readonly persistRecent: ((entry: RecentActivity) => 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 // Explicit file (tests, callers with their own layout) is used as-is; the\n // default migrates the legacy home-directory file into `agy/` once.\n this.file = options.file ?? migrateToAgyDir('agy-stats.json').file\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 this.persistRecent = options.persistRecent\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.noteRecent(record)\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 /**\n * The in-memory recent-activity ring, persisted by an injected\n * `RecentActivityStore` when one is supplied.\n *\n * The ring itself stays in-memory and I/O-free — `noteRecent` only flattens —\n * while the OPTIONAL `persistRecent` seam hands each flattened record to\n * `recent-store.ts`, which owns the file, the triggers, the lock, and the\n * failure story. When no store is injected (tests, the CLI) the ring behaves\n * exactly as before: per-process, never persisted.\n *\n * An earlier revision recorded why persistence was rejected outright (file\n * growth, a versioned migration, a wider stored surface); `recent-store.ts`\n * answers each — a bounded list in its own file, no version field, emails\n * and model ids only — which is what turned this from a bad idea into a\n * spec.\n */\n private readonly recent: RecentActivity[] = []\n\n private noteRecent(record: UsageRecord): void {\n const entry: RecentActivity = {\n at: this.now(),\n account: record.account ?? null,\n model: record.model ?? null,\n kind: record.poolEvent === true ? 'rotation' : record.source,\n ok: record.ok === true,\n rateLimited: record.rateLimited === true,\n latencyMs: record.latencyMs ?? null,\n ttftMs: record.ttftMs ?? null,\n output: record.usage?.output ?? null,\n reason: record.ok === true ? null : record.reason ?? null,\n }\n this.recent.push(entry)\n if (this.recent.length > RECENT_MAX) {\n this.recent.splice(0, this.recent.length - RECENT_MAX)\n }\n // Fire-and-forget: the store never throws synchronously (its own flush\n // path is guarded), but a diagnostics hand-off must not break `record()`.\n try {\n this.persistRecent?.(entry)\n } catch {\n // Swallowed by design — same contract as `emitUsage` in session.ts.\n }\n }\n\n /**\n * Newest-first copy of the ring, for the `pool.recent` RPC.\n *\n * A copy, not the live array: the RPC layer may hold it across awaits while\n * `record()` keeps pushing.\n */\n recentRequests(): RecentActivity[] {\n return [...this.recent].reverse()\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 * Persisted recent-activity ring: `$DSH_HOME/agy/agy-recent.json`.\n *\n * The in-memory ring in `UsageStats` answers \"just now\" but dies with the\n * process; the ledger answers \"in aggregate\" only. This store is the bridge:\n * it persists the SAME flattened records to their own file so history survives\n * a restart, and cross-process history becomes visible (Desktop, a web-profile\n * server, and a CLI each merge into one file).\n *\n * Why a SEPARATE file rather than a `recent` field in `agy-stats.json`: the\n * ledger's document is versioned and migrated (`parseStatsDocument`); folding\n * an evicting list into it turns every prune into a document-shape question.\n * A flat list in its own file needs no version and no migration — a record we\n * cannot parse is simply dropped.\n *\n * Bounded by construction: at most `RECENT_MAX` (200) entries, oldest evicted,\n * so the file caps at roughly 40–60KB forever.\n *\n * Flush triggers, whichever comes first: 25 pending records, or 30s with\n * anything pending, plus `flushSync()` on process exit. No size trigger —\n * entries are fixed-shape, so size is a function of count and the count IS the\n * cap. Worst loss window on a hard crash: ≤30s of records.\n *\n * The merge under the lock re-reads the fresh on-disk list, appends this\n * process's batch, dedupes on the CAPTURE key (`at|account|model|kind|output`\n * — two writers can flush the same capture window, and a duplicated rotation\n * row is exactly the confusion this panel exists to end), sorts by `at`, and\n * truncates to `RECENT_MAX`. Atomic tmp+rename, 0600.\n *\n * Privacy surface is the ledger's: account emails and model ids only — never a\n * token, a proxy URL, or a project id.\n */\n\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport lockfile from 'proper-lockfile'\nimport { RECENT_MAX } from './stats.ts'\nimport type { RecentActivity } from './stats.ts'\nimport { migrateToAgyDir } from './store/paths.ts'\n\n/** Default filename inside the agy data dir. */\nexport const RECENT_FILE_NAME = 'agy-recent.json'\n\n/** Flush after this many pending records. */\nexport const RECENT_FLUSH_EVERY = 25\n\n/** Flush after this many ms with pending records. */\nexport const RECENT_FLUSH_INTERVAL_MS = 30_000\n\n/** Capture-key dedupe: the fields two processes writing the same capture share. */\nfunction captureKey(entry: RecentActivity): string {\n return `${entry.at}|${entry.account ?? ''}|${entry.model ?? ''}|${entry.kind}|${entry.output ?? ''}`\n}\n\n/**\n * Coerce one parsed JSON value into a `RecentActivity`, or null when it is not\n * one. No version field: a record this build cannot parse is dropped, which\n * for a bounded diagnostics list is the whole migration story.\n */\nexport function parseRecentActivity(raw: unknown): RecentActivity | null {\n if (typeof raw !== 'object' || raw === null) return null\n const r = raw as Record<string, unknown>\n const kinds = ['chat', 'cli', 'verify', 'test', 'rotation'] as const\n if (typeof r.at !== 'number' || !Number.isFinite(r.at)) return null\n if (!kinds.includes(r.kind as (typeof kinds)[number])) return null\n if (typeof r.ok !== 'boolean') return null\n const nullableString = (v: unknown): string | null => (typeof v === 'string' && v.length > 0 ? v : null)\n const nullableNumber = (v: unknown): number | null =>\n (typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : null)\n return {\n at: r.at,\n account: nullableString(r.account),\n model: nullableString(r.model),\n kind: r.kind as RecentActivity['kind'],\n ok: r.ok,\n rateLimited: r.rateLimited === true,\n latencyMs: nullableNumber(r.latencyMs),\n ttftMs: nullableNumber(r.ttftMs),\n output: nullableNumber(r.output),\n reason: nullableString(r.reason),\n }\n}\n\n/** Merge one batch into an on-disk list (pure; the caller owns the lock and the write). */\nexport function mergeRecentEntries(\n onDisk: RecentActivity[],\n batch: RecentActivity[],\n cap: number = RECENT_MAX,\n): RecentActivity[] {\n const byKey = new Map<string, RecentActivity>()\n for (const entry of [...onDisk, ...batch]) byKey.set(captureKey(entry), entry)\n return [...byKey.values()]\n .sort((a, b) => a.at - b.at)\n .slice(-cap)\n}\n\n/** Same file-lock seam shape as `StatsLock`: sync, no retries (see stats.ts). */\nexport interface RecentLock {\n withLock<T>(file: string, fn: () => T): T\n}\n\nexport const properRecentLock: RecentLock = {\n withLock<T>(file: string, fn: () => T): T {\n // Same parameter set as `properStatsLock`, for the same reasons: no\n // `realpath` (the file may be absent on first write), a short `stale` so a\n // SIGKILLed process's lock directory cannot black out the next half minute.\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 noopRecentLock: RecentLock = { withLock: (_file, fn) => fn() }\n\nexport interface RecentActivityStoreOptions {\n /** Defaults to `$DSH_HOME/agy/agy-recent.json` (migrating any legacy file). */\n file?: string\n lock?: RecentLock\n /** Injectable clock, for tests. */\n now?: () => number\n flushEvery?: number\n flushIntervalMs?: number\n /**\n * Called once per run of consecutive flush failures, with the first error —\n * the same soft-fail contract as the ledger's `onFlushError`. Absent means\n * failures are silent (acceptable for a diagnostics ring; the plugin entry\n * always supplies one).\n */\n onFlushError?: (error: unknown) => void\n}\n\n/**\n * Persists the recent-activity ring across processes and restarts.\n *\n * The plugin constructs it BEFORE any record can arrive, seeds the in-memory\n * ring from the file, and hands it to `UsageStats` as the persistence half;\n * `record()` in `UsageStats` stays I/O-free — it appends to this store's\n * pending list, and flushing happens on the triggers above.\n */\nexport class RecentActivityStore {\n private readonly file: string\n private readonly lock: RecentLock\n private readonly now: () => number\n private readonly flushEvery: number\n private readonly flushIntervalMs: number\n private readonly onFlushError: ((error: unknown) => void) | undefined\n\n /** The seeded ring plus this process's un-flushed captures. */\n private entries: RecentActivity[] = []\n private pending: RecentActivity[] = []\n private timer: ReturnType<typeof setTimeout> | undefined\n private flushFailedSince: number | undefined\n private lastFlushError: unknown\n\n constructor(options: RecentActivityStoreOptions = {}) {\n this.file = options.file ?? migrateToAgyDir(RECENT_FILE_NAME).file\n this.lock = options.lock ?? properRecentLock\n this.now = options.now ?? (() => Date.now())\n this.flushEvery = options.flushEvery ?? RECENT_FLUSH_EVERY\n this.flushIntervalMs = options.flushIntervalMs ?? RECENT_FLUSH_INTERVAL_MS\n this.onFlushError = options.onFlushError\n // Seed from disk BEFORE the first capture: `pool.recent` then shows the\n // other processes' history immediately. A corrupt file degrades to empty.\n try {\n const parsed = JSON.parse(readFileSync(this.file, 'utf8')) as unknown\n if (Array.isArray(parsed)) {\n for (const raw of parsed) {\n const entry = parseRecentActivity(raw)\n if (entry) this.entries.push(entry)\n }\n if (this.entries.length > RECENT_MAX) {\n this.entries.splice(0, this.entries.length - RECENT_MAX)\n }\n }\n } catch {\n // Missing or unreadable file: an empty ring is the correct start state.\n }\n }\n\n /** Path of the backing file. */\n get path(): string {\n return this.file\n }\n\n /** Whether the ring is currently failing to persist (a run of failures). */\n get flushFailing(): boolean {\n return this.flushFailedSince !== undefined\n }\n\n /** Number of records awaiting flush. */\n get pendingCount(): number {\n return this.pending.length\n }\n\n /**\n * Capture one flattened record and the I/O-free hot-path append.\n *\n * `UsageStats.record()` calls this after folding into its own ring; keeping\n * the write-behind state HERE (not in UsageStats) is what lets the file have\n * its own triggers, its own lock, and its own failure story without widening\n * the ledger's.\n */\n capture(entry: RecentActivity): void {\n this.entries.push(entry)\n if (this.entries.length > RECENT_MAX) {\n this.entries.splice(0, this.entries.length - RECENT_MAX)\n }\n this.pending.push(entry)\n if (this.flushEvery > 0 && this.pending.length >= this.flushEvery) {\n this.flush()\n return\n }\n this.armTimer()\n }\n\n /**\n * Newest-first copy of the ring, for the `pool.recent` RPC: the seeded\n * history plus everything this process captured since boot.\n */\n recentRequests(): RecentActivity[] {\n return [...this.entries].reverse()\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 diagnostics flush.\n this.timer.unref?.()\n }\n\n /**\n * Merge pending captures into the persisted file.\n *\n * Same merge discipline as the ledger: re-read under the lock, merge by\n * capture key (two processes replaying the same window must not duplicate\n * rows), sort by time, cap. A failed flush puts the batch back — a\n * diagnostics ring never breaks a generation — and reports once per run.\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 if (!existsSync(this.file)) {\n mkdirSync(dirname(this.file), { recursive: true })\n writeFileSync(this.file, '[]\\n', { mode: 0o600 })\n }\n this.lock.withLock(this.file, () => {\n const onDisk: RecentActivity[] = []\n try {\n const parsed = JSON.parse(readFileSync(this.file, 'utf8')) as unknown\n if (Array.isArray(parsed)) {\n for (const raw of parsed) {\n const entry = parseRecentActivity(raw)\n if (entry) onDisk.push(entry)\n }\n }\n } catch {\n // A corrupt file loses its history rather than this process's batch.\n }\n const merged = mergeRecentEntries(onDisk, batch)\n const tmp = `${this.file}.tmp`\n writeFileSync(tmp, JSON.stringify(merged, null, 2) + '\\n', { mode: 0o600 })\n renameSync(tmp, this.file)\n })\n this.lastFlushError = undefined\n this.flushFailedSince = undefined\n } catch (error) {\n this.pending = [...batch, ...this.pending]\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 this.armTimer()\n }\n }\n\n /** Flush if anything is pending; called on process exit. */\n flushSync(): void {\n this.flush()\n }\n\n /** The most recent flush failure, if any (diagnostics). */\n get flushError(): unknown {\n return this.lastFlushError\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 { RecentActivityStore } from './recent-store.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'\nimport { migrateToAgyDir } from './store/paths.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/** The runtime one plugin entry uses: store, sessions, adapter, and the caches. */\nexport interface AgyRuntime {\n store: AccountStore\n sessions: AgySessionManager\n adapter: AgyAdapter\n stats: UsageStats\n recentStore: RecentActivityStore\n modelVisibility: ModelVisibility\n thinkingBudget: ThinkingBudgetStore\n}\n\n/**\n * The process-wide runtime promise, shared by both entry points.\n *\n * Splitting it was a real defect, not a stylistic one: the ledger FILE merges\n * across instances, so counters looked fine, but every in-memory surface is\n * per instance — the web entry's ring and in-flight map could never see the\n * main plugin's chat traffic (same process, two rings), and `account.activate`\n * cleared affinity pins on the WRONG session manager while the serving one\n * kept its stale pin. Both entries call this in one process; one memo here is\n * what makes that \"shared runtime\" claim true. Separate profiles are separate\n * processes, so the memo never bridges compositions.\n */\nlet sharedRuntime: Promise<AgyRuntime> | undefined\n\n/** Build (once per process) and return the runtime both entries share. */\nexport async function createAgyRuntime(ctx: Context): Promise<AgyRuntime> {\n if (sharedRuntime === undefined) {\n sharedRuntime = buildAgyRuntime(ctx).catch((error) => {\n // A failed build must not poison every later activation: drop the memo\n // so the next entry (or a retried apply) builds fresh.\n sharedRuntime = undefined\n throw error\n })\n }\n return sharedRuntime\n}\n\n/** Test-only: drop the process memo so a test builds an isolated runtime. */\nexport function _resetAgyRuntimeForTest(): void {\n sharedRuntime = undefined\n}\n\nasync function buildAgyRuntime(ctx: Context): Promise<AgyRuntime> {\n const { codec } = await resolveCodec(ctx)\n const dshHome = resolveDshHome()\n // Data files live in `$DSH_HOME/agy/`; the constructor-side migrations below\n // each move their own legacy file once. One warn here covers the skew case\n // for the whole runtime: every store's result is checked, and the message is\n // the same shape, so callers see ONE diagnosis instead of five.\n const accountsMigrated = migrateToAgyDir('agy-accounts.json', dshHome)\n if (accountsMigrated.skew) {\n ctx.logger.warn(\n '[dsh-agy] legacy data file detected next to the agy folder — an older dsh-agy '\n + 'process may still be writing the pre-0.5 layout; its writes will not be seen '\n + 'until every dsh surface is restarted',\n )\n }\n const store = new JsonAccountStore({ file: accountsMigrated.file, codec })\n void warmVersionCache(store)\n // The persisted recent-activity ring, built FIRST so it seeds from disk\n // before any record can arrive and `pool.recent` shows the other processes'\n // history immediately. Same soft-fail reporting contract as the ledger.\n const recentStore = new RecentActivityStore({\n onFlushError: (error) => {\n ctx.logger.warn(\n `[dsh-agy] recent-activity file could not be written (${recentStore.path}): `\n + `${error instanceof Error ? error.message : String(error)} — entries are buffered and will retry`,\n )\n },\n })\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 // The ring's persistence half: every captured record is handed to the\n // store (write-behind there, still I/O-free here).\n persistRecent: (entry) => { recentStore.capture(entry) },\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 // The adapter's model-list cache is keyed to no account (see\n // `AgyAdapter.invalidateModelCache`), so every event that can switch the\n // account discovery rides must drop it. The adapter does not exist yet when\n // the session manager is constructed, hence the late-bound reference.\n let invalidateModelCache: () => void = () => {}\n const sessions = new AgySessionManager({\n store,\n recordUsage: (record) => { stats.record(record) },\n onRotate: () => { invalidateModelCache() },\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 invalidateModelCache = () => { adapter.invalidateModelCache() }\n // Warm up the model list in the background so the first model/effort selection\n // in DSH has zero latency and shows no loading spinner.\n void adapter.listAllModels().catch(() => {})\n // Persist the ledger and the recent ring on normal termination. `exit`\n // covers both a graceful shutdown and the CLI's explicit `process.exit`\n // 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/RecentActivityStore already bounds that loss, and these are\n // diagnostics.\n process.once('exit', () => {\n stats.flushSync()\n recentStore.flushSync()\n })\n return { store, sessions, adapter, stats, recentStore, modelVisibility, thinkingBudget }\n}\n\nexport { AGY_PROVIDER }\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"proxy-BbuQX5U9.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 { 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) ──\n/**\n * Decoded proxy credentials. `URL.username`/`password` are the percent-ENCODED\n * substrings, and undici decodes them only where it parses the URI itself: the\n * HTTP tunnel path builds `proxy-authorization` from decoded values, but the\n * SOCKS5 sub-path forwards `opts.username`/`opts.password` VERBATIM to the auth\n * subnegotiation, so an encoded `p%40ss` would reach the proxy literally and be\n * rejected. Decoding here is what makes both paths see the real credentials.\n */\nfunction decodedProxyAuth(proxyUrl: string): { username?: string; password?: string } {\n try {\n const u = new URL(proxyUrl)\n if (!u.username) return {}\n return {\n username: decodeURIComponent(u.username),\n ...(u.password ? { password: decodeURIComponent(u.password) } : {}),\n }\n } catch {\n return {}\n }\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 ...decodedProxyAuth(clean),\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 *\n * SOCKS5 exception (measured against undici 7.29): the socks sub-path drops\n * these options — the per-origin pool behind its Socks5ProxyAgent accepts only\n * pipelining/connections/connect — so on a socks5:// proxy both call classes\n * run at undici's defaults (~300s per-gap) and the streaming `bodyTimeout: 0`\n * guarantee above does NOT hold. There is no option channel to change that.\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\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 dispatcher = createHttpDispatcher(normalized, dispatcherOptsFor(streaming) as any)\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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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"translate-BR1kuCaz.mjs","names":[],"sources":["../src/runtime/signature-cache.ts","../src/adapter/dsh-view.ts","../src/adapter/multimodal.ts","../src/adapter/translate.ts"],"sourcesContent":["/**\n * Gemini thought-signature cache for Antigravity tool calls.\n *\n * The Antigravity backend requires every outbound `functionCall` part to carry\n * a `thoughtSignature` (400 \"Function call is missing a thought_signature\"\n * otherwise). Signatures arrive on the *response* side — as a sibling field of\n * functionCall parts, and on thought parts — and must be replayed on the next\n * turn for the same tool call id. When nothing is cached, the translator\n * injects the `skip_thought_signature_validator` sentinel (the established\n * bypass both reference implementations use by default).\n *\n * Process-local with TTL; a cold process falls back to the sentinel, which the\n * upstream accepts, so cross-process persistence is not required for\n * correctness.\n */\n\nexport const THOUGHT_SIGNATURE_SENTINEL = 'skip_thought_signature_validator'\n\ninterface CacheEntry {\n signature: string\n expiresAt: number\n}\n\nconst DEFAULT_TTL_MS = 60 * 60 * 1000\nconst MAX_ENTRIES = 2000\n\nconst store = new Map<string, CacheEntry>()\n\nfunction pruneExpired(now = Date.now()): void {\n if (store.size < MAX_ENTRIES / 2) return\n for (const [key, entry] of store) {\n if (entry.expiresAt <= now) store.delete(key)\n }\n}\n\n/** Store the signature observed for one tool call id (response side). */\nexport function setThoughtSignature(toolCallId: string, signature: string, ttlMs = DEFAULT_TTL_MS): void {\n if (!toolCallId || !signature) return\n pruneExpired()\n if (store.size >= MAX_ENTRIES) {\n // Evict the oldest entry to keep the cache bounded.\n let oldestKey: string | null = null\n let oldestExpiry = Infinity\n for (const [key, entry] of store) {\n if (entry.expiresAt < oldestExpiry) {\n oldestExpiry = entry.expiresAt\n oldestKey = key\n }\n }\n if (oldestKey) store.delete(oldestKey)\n }\n store.set(toolCallId, { signature, expiresAt: Date.now() + ttlMs })\n}\n\n/** Resolve the signature for one tool call id, or null when unknown/expired. */\nexport function getThoughtSignature(toolCallId: string): string | null {\n if (!toolCallId) return null\n const entry = store.get(toolCallId)\n if (!entry) return null\n if (entry.expiresAt <= Date.now()) {\n store.delete(toolCallId)\n return null\n }\n return entry.signature\n}\n","/**\n * The adapter's canonical view of a DSH message, normalized at the seam.\n *\n * WHY THIS FILE EXISTS. `@deepseek-ai/dsh-llm` changed its message vocabulary\n * between the release lines this plugin supports, and the change is not a\n * superset — the SAME tool result is described by a different shape:\n *\n * - 0.1.5: a tool result is a `tool-result` CONTENT BLOCK, carried inside a\n * `user` message (`ContentBlockMap['tool-result']`), and `Message['role']`\n * is `'system' | 'user' | 'assistant'`.\n * - 0.2.0: `ContentBlockMap` has NO `tool-result` entry; the result is\n * promoted to a first-class `tool`-ROLE message (`ToolResultMessage`) that\n * carries `toolCallId`/`isError` at message level, and `role` gains\n * `'developer'`. `GenerateOptions.messages` becomes `RequestMessage[]`\n * (`Message | RequestUserInput`), where `RequestUserInput` is an\n * identity-free user turn.\n *\n * Neither union is assignable to the other, so the adapter cannot switch on the\n * harness type directly: on 0.1.5 a `tool-result` case narrows fine, and on\n * 0.2.0 that same case is a type error against a union that no longer has it.\n *\n * The fix is a boundary, not a cast scattered through the translator: the raw\n * harness list is projected ONCE into the union below, which keeps BOTH\n * vocabularies as separate, explicitly handled cases. Downstream code then has\n * one vocabulary to reason about, and `pnpm run verify:dsh-llm-compat` pins\n * that `src/` still compiles against each supported line's published types.\n *\n * The projection is deliberately tolerant: an unreadable or unknown block is\n * dropped rather than guessed at, which matches the translator's existing\n * \"unknown block types are skipped\" policy for merge-extensible content.\n */\n\n/** A text part of a message. */\nexport interface AgyTextBlockView {\n type: 'text'\n text: string\n}\n\n/** A reasoning/thinking part of a message. */\nexport interface AgyReasoningBlockView {\n type: 'reasoning'\n text: string\n}\n\n/** An image reference; bytes are resolved separately through the attachment store. */\nexport interface AgyImageBlockView {\n type: 'image'\n attachment: { attachmentId: string }\n}\n\n/** A file reference; handled by the multimodal path, not by the translator. */\nexport interface AgyFileBlockView {\n type: 'file'\n attachment: { attachmentId: string }\n}\n\n/** A tool invocation requested by the model. */\nexport interface AgyToolCallBlockView {\n type: 'tool-call'\n id: string\n name: string\n arguments: unknown\n}\n\n/**\n * A tool result in the 0.1.5 shape: a content block inside a `user` message.\n *\n * Still modelled even on the 0.2.0 line, because the shape is what the\n * translator's shared `functionResponse` builder consumes — see\n * `normalizeMessages` for how a 0.2.0 `tool`-role message is kept distinct.\n */\nexport interface AgyToolResultBlockView {\n type: 'tool-result'\n toolCallId: string\n content: AgyBlockView[]\n isError?: boolean\n}\n\n/** One content block, in the union of both supported vocabularies. */\nexport type AgyBlockView =\n | AgyTextBlockView\n | AgyReasoningBlockView\n | AgyImageBlockView\n | AgyFileBlockView\n | AgyToolCallBlockView\n | AgyToolResultBlockView\n\n/**\n * One message.\n *\n * `role` stays `string` rather than a closed union: 0.2.0 introduced\n * `'developer'`, and the harness declares the role maps merge-extensible, so a\n * future role must degrade to \"no parts\" instead of failing a cast. Comparison\n * against the roles this adapter understands happens at each use site.\n */\nexport interface AgyMessageView {\n readonly role: string\n /** Durable identity; absent on 0.2.0's `RequestUserInput`. */\n readonly id?: string\n readonly content: AgyBlockView[]\n /**\n * 0.2.0 `ToolResultMessage` correlation, carried at MESSAGE level.\n *\n * Set only on a `tool`-role message; the 0.1.5 shape carries the same fact on\n * the `tool-result` block instead.\n */\n readonly toolCallId?: string\n /** 0.2.0 `ToolResultMessage`: whether the tool invocation failed. */\n readonly isError?: boolean\n}\n\nfunction asString(value: unknown): string | undefined {\n return typeof value === 'string' ? value : undefined\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n ? value as Record<string, unknown>\n : undefined\n}\n\nfunction normalizeBlocks(raw: unknown): AgyBlockView[] {\n if (!Array.isArray(raw)) return []\n const blocks: AgyBlockView[] = []\n for (const entry of raw) {\n const block = asRecord(entry)\n if (!block) continue\n switch (block.type) {\n case 'text':\n case 'reasoning': {\n const text = asString(block.text)\n if (text !== undefined) blocks.push({ type: block.type, text })\n break\n }\n case 'image':\n case 'file': {\n const attachment = asRecord(block.attachment)\n const attachmentId = attachment ? asString(attachment.attachmentId) : undefined\n if (attachmentId !== undefined) blocks.push({ type: block.type, attachment: { attachmentId } })\n break\n }\n case 'tool-call': {\n const id = asString(block.id)\n const name = asString(block.name)\n if (id !== undefined && name !== undefined) {\n blocks.push({ type: 'tool-call', id, name, arguments: block.arguments })\n }\n break\n }\n case 'tool-result': {\n const toolCallId = asString(block.toolCallId)\n if (toolCallId !== undefined) {\n blocks.push({\n type: 'tool-result',\n toolCallId,\n content: normalizeBlocks(block.content),\n ...(block.isError === true ? { isError: true } : {}),\n })\n }\n break\n }\n default:\n // Merge-extensible: tool-addition/tool-removal and any future block are\n // projected away here, exactly as the translator's `default` case\n // already skipped them on the wire.\n break\n }\n }\n return blocks\n}\n\n/**\n * Project a raw harness message list into {@link AgyMessageView}s.\n *\n * Both vocabularies survive the projection as their own shapes — a 0.1.5\n * `tool-result` block and a 0.2.0 `tool`-role message each keep the fields the\n * translator needs — so the wire shape is decided in ONE place (`toolResultPart`\n * in `translate.ts`) rather than duplicated per release line.\n *\n * @param raw - the harness's `GenerateOptions.messages` (or any list of them).\n * @returns one normalized view per readable message, in order.\n */\nexport function normalizeMessages(raw: readonly unknown[]): AgyMessageView[] {\n const messages: AgyMessageView[] = []\n for (const entry of raw) {\n const message = asRecord(entry)\n if (!message) continue\n const role = asString(message.role)\n if (role === undefined) continue\n const id = asString(message.id)\n const toolCallId = asString(message.toolCallId)\n messages.push({\n role,\n ...(id === undefined ? {} : { id }),\n content: normalizeBlocks(message.content),\n ...(toolCallId === undefined ? {} : { toolCallId }),\n ...(message.isError === true ? { isError: true } : {}),\n })\n }\n return messages\n}\n\n/**\n * Messages that project to a wire turn.\n *\n * `system` and `developer` are excluded because they are not conversation\n * turns: the system prompt becomes `systemInstruction` and a developer message\n * carries tool-addition/removal bookkeeping, which this channel expresses\n * through the tool declarations themselves.\n */\nexport function conversationMessages(messages: readonly AgyMessageView[]): AgyMessageView[] {\n return messages.filter((message) => message.role !== 'system' && message.role !== 'developer')\n}\n\n/**\n * The system prompt text carried by `system`-role messages, in order.\n *\n * `GenerateOptions.system` is documented as the one-shot channel and is\n * undefined for loop-built requests, whose derived history carries the prompt\n * as a leading `system`-role message on BOTH supported lines. Reading only\n * `options.system` therefore sent no system prompt at all for a real agent\n * turn, and mapping the message through the conversation path sent it as a USER\n * turn instead — this is the text that belongs in `systemInstruction`.\n */\nexport function systemTextFromMessages(messages: readonly AgyMessageView[]): string | undefined {\n const parts: string[] = []\n for (const message of messages) {\n if (message.role !== 'system') continue\n for (const block of message.content) {\n if (block.type === 'text' && block.text !== '') parts.push(block.text)\n }\n }\n return parts.length > 0 ? parts.join('\\n\\n') : undefined\n}\n","/**\n * Multimodal file resolution for Google Gemini on Antigravity.\n *\n * In DeepSeek Harness (DSH), non-image files (PDF, audio, video, etc.)\n * uploaded by users are converted by DSH's LLM runtime into deterministic\n * text handles:\n *\n * `[File \"${name}\" (${bytes} bytes, sha256:${digest}): verbatim read-only copy saved at \"${readonlyPath}\". ...]`\n *\n * Gemini models natively support these files as base64-encoded `inlineData`\n * parts alongside text. Claude models running behind the Antigravity Vertex\n * proxy reject non-image `inlineData` with 500, so injection is scoped strictly\n * to Gemini (non-Claude, vision-capable) models.\n */\n\nimport { readFile } from 'node:fs/promises'\nimport type { GenerateOptions } from '@deepseek-ai/dsh-llm'\nimport { catalogModel } from './catalog.ts'\nimport { normalizeMessages } from './dsh-view.ts'\n\n/**\n * MIME type mapping for Gemini supported multimodal formats.\n *\n * Covered formats (PDF observed live end-to-end; the rest follow Gemini's\n * documented multimodal support and are not yet live-verified):\n * - Document: .pdf -> 'application/pdf'\n * - Audio: .mp3 -> 'audio/mp3', .wav -> 'audio/wav', .m4a -> 'audio/m4a', .aac -> 'audio/aac', .ogg -> 'audio/ogg', .flac -> 'audio/flac'\n * - Video: .mp4 -> 'video/mp4', .mov -> 'video/quicktime', .webm -> 'video/webm'\n * - Extended image: .bmp -> 'image/bmp', .heic -> 'image/heic', .heif -> 'image/heif'\n */\nexport const GEMINI_MULTIMODAL_MIMES: Readonly<Record<string, string>> = Object.freeze({\n pdf: 'application/pdf',\n mp3: 'audio/mp3',\n wav: 'audio/wav',\n m4a: 'audio/m4a',\n aac: 'audio/aac',\n ogg: 'audio/ogg',\n flac: 'audio/flac',\n mp4: 'video/mp4',\n mov: 'video/quicktime',\n webm: 'video/webm',\n bmp: 'image/bmp',\n heic: 'image/heic',\n heif: 'image/heif',\n '.pdf': 'application/pdf',\n '.mp3': 'audio/mp3',\n '.wav': 'audio/wav',\n '.m4a': 'audio/m4a',\n '.aac': 'audio/aac',\n '.ogg': 'audio/ogg',\n '.flac': 'audio/flac',\n '.mp4': 'video/mp4',\n '.mov': 'video/quicktime',\n '.webm': 'video/webm',\n '.bmp': 'image/bmp',\n '.heic': 'image/heic',\n '.heif': 'image/heif',\n})\n\n/**\n * Regex matching DSH's deterministic read-only file handle blocks.\n * Capture groups:\n * 1: file name\n * 2: file size in bytes\n * 3: local read-only filesystem path\n */\nexport const DSH_FILE_HANDLE_REGEX =\n /\\[File \"([^\"]+)\" \\((\\d+) bytes, sha256:[a-f0-9]+\\): verbatim read-only copy saved at \"([^\"]+)\"\\./g\n\nexport const FILE_HANDLE_REGEX = DSH_FILE_HANDLE_REGEX\n\n/** Maximum file size for inlining (20MB). */\nexport const MAX_MULTIMODAL_FILE_BYTES = 20 * 1024 * 1024\nexport const MAX_MULTIMODAL_FILE_SIZE = MAX_MULTIMODAL_FILE_BYTES\n\nexport interface ExtractedFileHandle {\n name: string\n bytes: number\n readonlyPath: string\n}\n\n/** Extract file handle descriptors from text content. */\nexport function extractFileHandles(text: string): ExtractedFileHandle[] {\n const regex = new RegExp(DSH_FILE_HANDLE_REGEX.source, 'g')\n const results: ExtractedFileHandle[] = []\n for (const match of text.matchAll(regex)) {\n const [, name, bytesStr, readonlyPath] = match\n if (name && bytesStr && readonlyPath) {\n const bytes = parseInt(bytesStr, 10)\n if (!Number.isNaN(bytes)) {\n results.push({ name, bytes, readonlyPath })\n }\n }\n }\n return results\n}\n\n/** Resolve MIME type for a file name or path based on extension. */\nexport function getMultimodalMimeType(filenameOrPath: string): string | undefined {\n const dotIndex = filenameOrPath.lastIndexOf('.')\n if (dotIndex === -1) return undefined\n const ext = filenameOrPath.slice(dotIndex + 1).toLowerCase()\n return GEMINI_MULTIMODAL_MIMES[ext]\n}\n\n/** Whether a model id belongs to a Claude-branded model (Vertex-hosted). */\nexport function isClaudeModel(model: string): boolean {\n return model.startsWith('claude-') || model.includes('/claude')\n}\n\n/**\n * Check whether a model supports Gemini multimodal file inlineData.\n * Deny-by-default: Claude models are always excluded (Vertex 500 on\n * non-image inlineData), catalog models must be vision-capable, and ids\n * unknown to the catalog are only allowed when they carry the `gemini-`\n * prefix (so future tiered ids keep working without a catalog bump).\n */\nexport function supportsMultimodalFiles(model: string): boolean {\n if (isClaudeModel(model)) return false\n const meta = catalogModel(model)\n if (meta) return meta.supportsVision === true\n return model.startsWith('gemini-')\n}\n\nexport const isMultimodalSupported = supportsMultimodalFiles\nexport const isMultimodalModel = supportsMultimodalFiles\n\nexport interface AgyResolvedMultimodalFile {\n mimeType: string\n /** Pure base64 (no data: prefix). */\n data: string\n name?: string\n path?: string\n bytes?: number\n}\n\nexport interface ResolveMultimodalOptions {\n readFile?: (path: string) => Promise<Buffer | Uint8Array>\n}\n\n/**\n * Resolve multimodal files referenced in user messages.\n *\n * Reads local files up to 20MB into base64 strings. Silently ignores\n * missing/unreadable files or files exceeding the size limit so the original\n * text handle block remains intact in the prompt.\n */\nexport async function resolveMultimodalFiles(\n optionsOrMessages: GenerateOptions | readonly unknown[],\n modelOrOptions?: string | ResolveMultimodalOptions,\n extraOptions?: ResolveMultimodalOptions,\n): Promise<Map<string, AgyResolvedMultimodalFile[]>> {\n let raw: readonly unknown[]\n let model: string\n let customOptions: ResolveMultimodalOptions | undefined\n\n if ('messages' in optionsOrMessages) {\n raw = optionsOrMessages.messages ?? []\n model = optionsOrMessages.model ?? ''\n customOptions = typeof modelOrOptions === 'object' ? modelOrOptions : extraOptions\n } else {\n raw = optionsOrMessages\n if (typeof modelOrOptions === 'string') {\n model = modelOrOptions\n customOptions = extraOptions\n } else {\n model = ''\n customOptions = modelOrOptions\n }\n }\n\n // Normalized through the same boundary the translator uses, so this path sees\n // one vocabulary whichever dsh-llm line fed the request (see dsh-view.ts).\n const messages = normalizeMessages(raw)\n\n const result = new Map<string, AgyResolvedMultimodalFile[]>()\n if (!supportsMultimodalFiles(model)) {\n return result\n }\n\n const readFn = customOptions?.readFile ?? readFile\n\n for (let i = 0; i < messages.length; i++) {\n const message = messages[i]\n if (!message || message.role !== 'user') continue\n\n const resolvedForMessage: AgyResolvedMultimodalFile[] = []\n for (const block of message.content) {\n if (block.type !== 'text' || typeof block.text !== 'string') continue\n\n const handles = extractFileHandles(block.text)\n for (const handle of handles) {\n if (handle.bytes > MAX_MULTIMODAL_FILE_BYTES) continue\n\n const mimeType = getMultimodalMimeType(handle.name) ?? getMultimodalMimeType(handle.readonlyPath)\n if (!mimeType) continue\n\n try {\n const fileBuffer = await readFn(handle.readonlyPath)\n if (fileBuffer.length > MAX_MULTIMODAL_FILE_BYTES) continue\n resolvedForMessage.push({\n mimeType,\n data: Buffer.from(fileBuffer).toString('base64'),\n name: handle.name,\n path: handle.readonlyPath,\n bytes: fileBuffer.length,\n })\n } catch {\n // Error resilience: catch error silently and do not throw\n continue\n }\n }\n }\n\n if (resolvedForMessage.length > 0) {\n const key = message.id ?? `msg-${i}`\n result.set(key, resolvedForMessage)\n }\n }\n\n return result\n}\n","/**\n * Translate a DSH GenerateOptions into the Antigravity wrapped request.\n *\n * Envelope shape follows the actively-maintained OmniRoute wire format\n * (the archived opencode reference predates it): top-level `project`,\n * `requestId`, `model`, `userAgent`, `requestType`, with the Gemini-style\n * body under `request` (contents/systemInstruction/tools/generationConfig/\n * sessionId). `toolConfig` VALIDATED is attached when tools are present, and\n * Claude-path requests strip trailing model turns (Vertex rejects \"assistant\n * message prefill\").\n *\n * Thinking blocks are carried as-is on the Gemini path (Gemini `thought`\n * parts); no thought is ever re-signed — that signature dance was an artifact\n * of the reference plugin's interception architecture (see\n * docs/ARCHITECTURE.md). The Claude path is stricter and drops thought parts\n * entirely, because its validator demands a real thinking signature that only\n * the originating model can produce (docs/ANTIGRAVITY-API.md §3.3).\n */\n\nimport { createHash } from 'node:crypto'\nimport type { GenerateOptions, ToolSchema } from '@deepseek-ai/dsh-llm'\nimport { generateAntigravityRequestId } from '../runtime/identity.ts'\nimport { getThoughtSignature, THOUGHT_SIGNATURE_SENTINEL } from '../runtime/signature-cache.ts'\nimport { catalogModel, isLevelThinkingModel } from './catalog.ts'\nimport { isClaudeModel, supportsMultimodalFiles, type AgyResolvedMultimodalFile } from './multimodal.ts'\nimport {\n conversationMessages,\n normalizeMessages,\n systemTextFromMessages,\n type AgyBlockView,\n type AgyMessageView,\n} from './dsh-view.ts'\n\nexport { isClaudeModel, supportsMultimodalFiles }\nexport type { AgyResolvedMultimodalFile }\n\nexport type AgyPart =\n | { text: string }\n | { thought: true; text: string }\n | { thoughtSignature: string; functionCall: { id: string; name: string; args: unknown } }\n | { functionResponse: { id: string; name: string; response: unknown } }\n | { inlineData: { mimeType: string; data: string } }\n\n/** Image bytes pre-resolved from the durable attachment store, keyed by attachment id. */\nexport interface AgyResolvedImage {\n mediaType: string\n /** Pure base64 (no data: prefix). */\n data: string\n}\n\nexport interface AgyContent {\n role: 'user' | 'model'\n parts: AgyPart[]\n}\n\nexport interface AgyRequestBody {\n project?: string\n requestId?: string\n model: string\n userAgent?: string\n requestType?: 'agent'\n request: {\n contents: AgyContent[]\n systemInstruction?: { parts: Array<{ text: string }> }\n tools?: Array<{ functionDeclarations: Array<{ name: string; description: string; parameters: unknown }> }>\n toolConfig?: { functionCallingConfig: { mode: 'VALIDATED' } }\n generationConfig?: {\n temperature?: number\n maxOutputTokens?: number\n stopSequences?: string[]\n thinkingConfig?: { thinkingLevel?: string; includeThoughts?: boolean; thinkingBudget?: number }\n }\n sessionId?: string\n }\n}\n\n// isClaudeModel is imported from ./multimodal.ts and re-exported above\n\n/**\n * Vertex (the Antigravity Claude backend) rejects conversations ending on an\n * assistant/model turn (\"assistant message prefill\"); never strip to empty.\n */\nexport function stripTrailingModelTurn(contents: AgyContent[]): AgyContent[] {\n while (contents.length > 1 && contents[contents.length - 1]?.role === 'model') {\n contents.pop()\n }\n return contents\n}\n\n/**\n * The Antigravity backend parses tool `parameters` as a strict protobuf\n * schema and rejects ANY unknown keyword with 400 (verified empirically:\n * `$schema`, `propertyNames`, `pattern`, `minLength`, ... each fail in turn).\n * Denylisting is whack-a-mole, so keep only the keywords the upstream\n * accepts. Container shapes are handled distinctly: `properties` is a\n * name->schema map (keys preserved), `items`/`additionalProperties` are\n * nested schemas (additionalProperties also accepts a boolean — live-verified\n * against the Antigravity upstream), `required`/`enum` are plain arrays.\n *\n * Keyword VALUES are also constrained by the protobuf shape (verified\n * empirically): `type` must be a single enum string (union arrays like\n * `[\"string\",\"number\"]` are rejected) and every `enum` item must be a\n * non-empty string (booleans/numbers/empty strings are rejected). Values are\n * normalized to the nearest valid form instead of being dropped wholesale.\n */\n// Exported for the contract invariant test (tests/adapter.test.ts); not part of\n// the package public API (translate.ts is an internal module).\nexport const AGY_SCHEMA_ALLOWLIST = new Set([\n 'type', 'format', 'title', 'description', 'nullable',\n 'items', 'enum', 'default', 'properties', 'required', 'additionalProperties',\n])\nconst AGY_SCHEMA_MAP_KEYS = new Set(['properties'])\nconst AGY_SCHEMA_NESTED_KEYS = new Set(['items', 'additionalProperties'])\nconst AGY_SCHEMA_LIST_KEYS = new Set(['required', 'enum'])\n\nfunction sanitizeToolSchema(schema: unknown): unknown {\n if (!schema || typeof schema !== 'object') return schema\n if (Array.isArray(schema)) return schema.map((entry) => sanitizeToolSchema(entry))\n\n // Normalize union types: upstream `type` is a single enum string. Pick the\n // first non-null string type (`\"null\"` maps to the `nullable` keyword);\n // fall back to `string` when no usable type remains.\n let normalized = schema as Record<string, unknown>\n if (Array.isArray(normalized.type)) {\n const types = normalized.type.filter((t): t is string => typeof t === 'string' && t !== 'null')\n normalized = { ...normalized, type: types[0] ?? 'string' }\n }\n\n const result: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(normalized)) {\n if (!AGY_SCHEMA_ALLOWLIST.has(key)) continue\n if (AGY_SCHEMA_MAP_KEYS.has(key)) {\n const map: Record<string, unknown> = {}\n for (const [name, child] of Object.entries(value as Record<string, unknown>)) {\n map[name] = sanitizeToolSchema(child)\n }\n result[key] = map\n continue\n }\n if (AGY_SCHEMA_NESTED_KEYS.has(key)) {\n result[key] = sanitizeToolSchema(value)\n continue\n }\n if (AGY_SCHEMA_LIST_KEYS.has(key)) {\n // Upstream `enum` items must be non-empty strings; filter the rest\n // and omit an empty enum entirely (an empty array would be rejected too).\n if (key === 'enum' && Array.isArray(value)) {\n const filtered = value.filter((v): v is string => typeof v === 'string' && v.length > 0)\n if (filtered.length > 0) result[key] = filtered\n } else {\n result[key] = value\n }\n continue\n }\n result[key] = value\n }\n return result\n}\n\n/** Collect tool-call names by id so tool results can name their function. */\nfunction buildToolNameIndex(messages: readonly AgyMessageView[]): Map<string, string> {\n const index = new Map<string, string>()\n for (const message of messages) {\n for (const block of message.content) {\n if (block.type === 'tool-call') {\n index.set(block.id, block.name)\n }\n }\n }\n return index\n}\n\n/**\n * The single producer of a `functionResponse` part.\n *\n * Both supported dsh-llm vocabularies funnel through here: a 0.1.5\n * `tool-result` CONTENT BLOCK and a 0.2.0 `tool`-ROLE message describe the same\n * result, and this channel's wire shape must not depend on which one arrived.\n */\nfunction toolResultPart(\n toolCallId: string,\n isError: boolean,\n content: readonly AgyBlockView[],\n toolNames: Map<string, string>,\n): AgyPart {\n const name = toolNames.get(toolCallId) ?? toolCallId\n const text = content\n .filter((block): block is Extract<AgyBlockView, { type: 'text' }> => block.type === 'text')\n .map((block) => block.text)\n .join('\\n')\n return {\n functionResponse: {\n // The Anthropic-backed Claude path requires tool_result.tool_use_id\n // and 400s without it (\"messages.N.content.M.tool_result.tool_use_id:\n // Field required\"); the Gemini path accepts the id too, so it is\n // always carried rather than branched per family (live-verified).\n id: toolCallId,\n name,\n response: { result: text, is_error: isError },\n },\n }\n}\n\nfunction blockToParts(\n block: AgyBlockView,\n toolNames: Map<string, string>,\n images: Map<string, AgyResolvedImage>,\n /** Claude path: replayed thought blocks are rejected outright (see below). */\n dropThoughts = false,\n): AgyPart[] {\n switch (block.type) {\n case 'text':\n // An empty text part is rejected by the Anthropic-backed Claude path\n // (\"messages.N.content.M.text.text: Field required\") while the Gemini\n // path tolerates it. Upstream's own parts normalization drops empty\n // text, so drop it here for every family (live-verified). DSH emits\n // these as trailing zero-length blocks after a tool call.\n return block.text === '' ? [] : [{ text: block.text }]\n case 'reasoning':\n // Empty thought: same rejection class as `text` (\"thinking.thinking:\n // Field required\").\n if (block.text === '') return []\n // A replayed thought block cannot be sent to the Claude path at all: the\n // backend demands a thinking `signature`, and the\n // `skip_thought_signature_validator` sentinel that works for functionCall\n // parts is rejected here as an invalid signature (live-verified). Only\n // the model that produced the thought could re-sign it, so a thought that\n // came from another family (e.g. a mid-session switch from a Gemini\n // tiered model) has no valid form — drop it rather than 400. Gemini\n // accepts replayed thoughts, so this is Claude-only.\n if (dropThoughts) return []\n return [{ thought: true, text: block.text }]\n case 'tool-call': {\n // Upstream parses functionCall.args as google.protobuf.Struct and\n // rejects a raw string with 400. Guarantee an object: parse the string\n // form, fall back to {} when it is truncated/malformed (the failure\n // mode for long multi-turn histories).\n let args: unknown = {}\n if (typeof block.arguments === 'object' && block.arguments !== null && !Array.isArray(block.arguments)) {\n args = block.arguments\n } else if (typeof block.arguments === 'string') {\n try {\n const parsed: unknown = JSON.parse(block.arguments)\n if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {\n args = parsed\n }\n } catch {\n // truncated/malformed JSON -> empty object\n }\n }\n // Antigravity rejects functionCall parts without a thoughtSignature\n // (400). Replay the signature captured for this tool call id on the\n // previous turn; the sentinel is the established bypass when nothing is\n // cached (both reference implementations default to it).\n //\n // OPEN QUESTION, deliberately not changed yet: another implementation of\n // this client stamps the sentinel ONLY on the FIRST functionCall of a model\n // turn and leaves sibling calls unsigned (\"unsigned sibling functionCalls\n // preserve native parallel-call shape\"), and never synthesizes a bypass\n // signature anywhere else. This path stamps every call. Which is right is\n // unverified for THIS channel — the sentinel is live-verified to work here,\n // and a wrong change turns working parallel tool calls into 400s — so it\n // needs one real multi-tool turn measured before any edit.\n const signature = getThoughtSignature(block.id) ?? THOUGHT_SIGNATURE_SENTINEL\n return [{\n thoughtSignature: signature,\n functionCall: { id: block.id, name: block.name, args },\n }]\n }\n case 'tool-result':\n return [toolResultPart(\n block.toolCallId,\n block.isError === true,\n block.content,\n toolNames,\n )]\n case 'image': {\n // Bytes are pre-resolved by the adapter before translation; a missing\n // entry breaks that invariant and must fail loudly, never silently drop.\n const resolved = images.get(block.attachment.attachmentId)\n if (!resolved) {\n throw new Error(`agy translate: unresolved image attachment \"${block.attachment.attachmentId}\"`)\n }\n return [{ inlineData: { mimeType: resolved.mediaType, data: resolved.data } }]\n }\n default:\n return [] // unknown block types (merge-extensible) are skipped\n }\n}\n\nfunction messageToContent(\n message: AgyMessageView,\n toolNames: Map<string, string>,\n images: Map<string, AgyResolvedImage>,\n multimodalFiles?: Map<string, AgyResolvedMultimodalFile[]>,\n messageIndex?: number,\n dropThoughts = false,\n): AgyContent | null {\n // 0.2.0 promotes a tool result to a `tool`-ROLE message, with the correlation\n // (`toolCallId`/`isError`) on the message instead of on a `tool-result` block.\n // It projects to the same wire part, and a `functionResponse` always belongs\n // to a user-side turn on this channel.\n if (message.role === 'tool') {\n if (message.toolCallId === undefined) {\n // Uncorrelated: the Claude path 400s without an id, and inventing one\n // would mis-address a real call. Dropped like any other unreadable shape.\n return null\n }\n return {\n role: 'user',\n parts: [toolResultPart(message.toolCallId, message.isError === true, message.content, toolNames)],\n }\n }\n\n const parts = message.content.flatMap((block) =>\n // Non-user images are out of scope by policy (docs ANTIGRAVITY-API §3.2):\n // skip them like any other untranslatable block instead of tripping the\n // unresolved-map guard, which protects only the user-image invariant.\n block.type === 'image' && message.role !== 'user'\n ? []\n : blockToParts(block, toolNames, images, dropThoughts),\n )\n\n if (message.role === 'user' && multimodalFiles) {\n const files =\n (message.id ? multimodalFiles.get(message.id) : undefined) ??\n (multimodalFiles as Map<unknown, AgyResolvedMultimodalFile[]>).get(message) ??\n (messageIndex !== undefined ? multimodalFiles.get(`msg-${messageIndex}`) : undefined) ??\n (messageIndex !== undefined ? multimodalFiles.get(String(messageIndex)) : undefined)\n\n if (files) {\n for (const file of files) {\n parts.push({ inlineData: { mimeType: file.mimeType, data: file.data } })\n }\n }\n }\n\n if (parts.length === 0) return null\n const role = message.role === 'assistant' ? 'model' : 'user'\n return { role, parts }\n}\n\n/**\n * Whether a part makes its turn a tool-result turn (`functionResponse` family).\n * Every other part kind — text, media, thought, functionCall — belongs to the\n * ordinary-content family.\n */\nfunction isFrPart(part: AgyPart): boolean {\n return 'functionResponse' in part\n}\n\n/**\n * Merge adjacent same-role contents into single turns, functionResponse-family\n * aware.\n *\n * The harness emits a fragmented per-step vocabulary — prompt, runtime-context\n * snapshot, and injected `<system-reminder>` messages as separate `user`\n * messages, one message per tool result — and its own first-party serializer\n * coalesces adjacent same-role messages at the wire boundary. Forwarding the\n * fragments 1:1 instead opened every request with a run of `user` turns that\n * all belong to one logical prompt, and sent parallel tool results as separate\n * one-`functionResponse` turns where the official Gemini tooling groups them\n * into a single user turn.\n *\n * Family rule: a turn carrying `functionResponse` parts never merges with (or\n * receives) other part kinds, so a mixed content is segmented in part order.\n * The harness's serializer instead re-sorts tool results to the front of one\n * merged message, but the Gemini-family validator is measured to reject a user\n * content mixing `functionResponse` with text (400; langchainjs#11445, on the\n * model family), and this channel's tolerance for the mixed shape is\n * unmeasured. Separate adjacent turns keep every part while keeping each turn\n * unmixed; parts are never reordered.\n *\n * Known boundary loss (pinned in tests): a model turn whose parts all drop\n * (thoughts-only turn replayed on the Claude path) is filtered to null before\n * this point, so the user turns around it fuse into one content. A text\n * placeholder would fabricate a model utterance, and the acceptance of a\n * structural empty turn (`parts: []`) is unmeasured here — so the fusion\n * stands (issue #93).\n */\nexport function coalesceContents(contents: AgyContent[]): AgyContent[] {\n const result: AgyContent[] = []\n let lastFamily: boolean | undefined\n for (const content of contents) {\n for (const part of content.parts) {\n const family = isFrPart(part)\n const last = result[result.length - 1]\n if (last && lastFamily === family && last.role === content.role) {\n last.parts.push(part)\n } else {\n result.push({ role: content.role, parts: [part] })\n lastFamily = family\n }\n }\n }\n return result\n}\n\n/**\n * Builtin Gemini tools must not shadow functionDeclarations names (upstream\n * treats them as native tools; verified by OmniRoute's GEMINI_BUILTIN_TOOL_NAMES).\n */\nconst AGY_BUILTIN_TOOL_NAMES = new Set(['google_search', 'web_search', 'search_web', 'googleSearch'])\n\n/** Level-thinking: single id + selectable low/medium/high via thinkingLevel (catalog thinking:'level'). */\nconst LEVEL_THINKING_LEVELS = new Set(['low', 'medium', 'high'])\n\n/**\n * Claude-family output ceiling on the Antigravity channel (live-measured).\n *\n * The Claude models are served through the Gemini-style\n * `generationConfig.maxOutputTokens` field here, and this channel rejects the\n * Claude family above 64000: 64000 answers 200, 64001 answers 400\n * `INVALID_ARGUMENT: Request contains an invalid argument` (deterministic\n * across both Claude ids, with and without the full 87-tool payload). The\n * Gemini family accepts 65536 on the same endpoint, so the limit is\n * model-family-specific, not endpoint-wide.\n *\n * Do NOT reason about this number from Anthropic's public API limits. Agy may\n * front a self-hosted or otherwise gated Claude deployment whose capacity and\n * validation rules are its own; the only authority is what this channel\n * accepts, which is what the probe measures. The value here is that\n * measurement, nothing more.\n *\n * The catalog's `maxOutputTokens` is the harness-injected default\n * (`LlmResolvedModelInfo.defaultMaxTokens`), so a wrong value there makes every\n * Claude request fail. This clamp is the second line of defense: it also covers\n * an explicit `maxTokens` (agent preset / call config) and a dynamically\n * discovered Claude id absent from the pinned catalog.\n */\nexport const AGY_CLAUDE_MAX_OUTPUT_TOKENS = 64_000\n\n/** Upstream functionDeclarations names are `[a-zA-Z0-9_]` and ≤64 chars (OmniRoute-verified). */\nconst AGY_TOOL_NAME_MAX_LENGTH = 64\n\n/** Sanitize a tool name to the upstream charset/length; dedupe via a short hash. */\nfunction sanitizeToolName(name: string, seen: Set<string>): string {\n let candidate = name.replace(/[^a-zA-Z0-9_]/g, '_') || 'tool'\n if (candidate.length > AGY_TOOL_NAME_MAX_LENGTH || seen.has(candidate)) {\n const hash = createHash('sha256').update(candidate).digest('hex').slice(0, 8)\n const prefix = candidate.slice(0, AGY_TOOL_NAME_MAX_LENGTH - hash.length - 1)\n candidate = `${prefix}_${hash}`\n let i = 2\n while (seen.has(candidate)) candidate = `${prefix}_${i++}_${hash}`\n }\n seen.add(candidate)\n return candidate\n}\n\nfunction toolsToDeclarations(tools: ToolSchema[] | undefined): AgyRequestBody['request']['tools'] {\n if (!tools || tools.length === 0) return undefined\n const seenNames = new Set<string>()\n const declarations = []\n for (const tool of tools) {\n if (AGY_BUILTIN_TOOL_NAMES.has(tool.name)) continue\n declarations.push({\n name: sanitizeToolName(tool.name, seenNames),\n description: tool.description,\n parameters: sanitizeToolSchema(tool.parameters),\n })\n }\n if (declarations.length === 0) return undefined\n return [{ functionDeclarations: declarations }]\n}\n\n/** Build the wrapped Antigravity request body for one call. */\nexport function toAgyRequestBody(\n options: GenerateOptions,\n context: {\n projectId?: string\n sessionId?: string\n images?: Map<string, AgyResolvedImage>\n multimodalFiles?: Map<string, AgyResolvedMultimodalFile[]>\n /**\n * The request id, when the caller also stamps it on the wire\n * (`x-goog-request-id`). Generating it here as well produced TWO different\n * ids for one request — the body and the header disagreed, a shape no client\n * produces. Callers that send the header must pass the same value.\n */\n requestId?: string\n /**\n * A configured token budget for a reasoning level, or undefined to leave the\n * level's own budget to upstream.\n *\n * When this returns a number the budget REPLACES `thinkingLevel` rather than\n * joining it, because measurement shows the level wins when both are sent:\n * `{thinkingLevel:\"low\", thinkingBudget:16000}` spends what `low` alone\n * spends (~180 thoughts, against ~330 for the budget alone), and\n * `{thinkingLevel:\"high\", thinkingBudget:1000}` likewise tracks `high`\n * (~316 vs ~173). Sending both would therefore make a configured number\n * silently inert, which is worse than not offering the setting.\n */\n thinkingBudgetFor?: (level: string) => number | undefined\n /**\n * The configured Claude thinking budget, or undefined when unset.\n *\n * A single value, not a per-level map: the Claude family is id-bound (each\n * capability is its own model id with no level selector), so there is no\n * level to key by.\n */\n claudeBudgetFor?: () => number | undefined\n /**\n * The configured budget for the TIERED slot (the selector's \"Default\"\n * effort), or undefined when unset.\n *\n * That effort arrives with NO level id, so it cannot go through\n * `thinkingBudgetFor`. A value here sends a bare `thinkingBudget` with no\n * `thinkingLevel` — the tiered model's adaptive entry, given an explicit cap.\n */\n tieredBudgetFor?: () => number | undefined\n },\n): AgyRequestBody {\n const messages = normalizeMessages(options.messages)\n const toolNames = buildToolNameIndex(messages)\n const images = context.images ?? new Map<string, AgyResolvedImage>()\n const multimodalFiles = supportsMultimodalFiles(options.model) ? context.multimodalFiles : undefined\n const claude = isClaudeModel(options.model)\n let contents = coalesceContents(\n conversationMessages(messages)\n .map((message, index) => messageToContent(message, toolNames, images, multimodalFiles, index, claude))\n .filter((c): c is AgyContent => c !== null),\n )\n if (claude) {\n contents = stripTrailingModelTurn(contents)\n }\n\n // `options.system` is the one-shot channel and is undefined for a loop-built\n // request, whose derived history carries the prompt as a `system`-role message\n // (documented identically on both supported dsh-llm lines). Both sources feed\n // the system slot; routing the message through `contents` instead would put\n // the system prompt on the wire as a USER turn.\n const systemText = [options.system, systemTextFromMessages(messages)]\n .filter((text): text is string => typeof text === 'string' && text !== '')\n .join('\\n\\n')\n\n const tools = toolsToDeclarations(options.tools)\n const generationConfig: NonNullable<AgyRequestBody['request']['generationConfig']> = {}\n if (options.temperature !== undefined) generationConfig.temperature = options.temperature\n if (options.maxTokens !== undefined) {\n // Claude family: the platform rejects >64000 with 400, and the value the\n // harness injects comes from the catalog default, so clamp before the wire.\n generationConfig.maxOutputTokens = isClaudeModel(options.model)\n ? Math.min(options.maxTokens, AGY_CLAUDE_MAX_OUTPUT_TOKENS)\n : options.maxTokens\n }\n if (options.stop !== undefined && options.stop.length > 0) generationConfig.stopSequences = options.stop\n // Level-thinking: map the DSH reasoning effort to thinkingConfig.\n // Id-bound models (thinking !== 'level') never emit it, and a tiered model\n // emits NOTHING when no effort is requested — there is deliberately no default\n // level, because \"no level chosen\" is what lets the model allocate its own\n // thinking (see `LEVEL_REASONING` for why declaring a default broke that).\n // When purpose is 'session-title' or reasoning is off, thinkingBudget: 0 prevents\n // default thinking tokens from exhausting tight output caps (e.g. maxTokens: 64).\n const effort = options.reasoningEffort?.toLowerCase()\n if (isLevelThinkingModel(options.model)) {\n if (options.purpose === 'session-title' || effort === 'none' || effort === 'off') {\n generationConfig.thinkingConfig = { thinkingBudget: 0 }\n } else if (effort === undefined) {\n // The selector's \"Default\" effort: no level was chosen. A configured tiered\n // budget turns this into Max by sending a bare `thinkingBudget` (no\n // `thinkingLevel`, so the number alone decides). Unset sends nothing at all,\n // which leaves upstream's own adaptive allocation in charge.\n const tiered = context.tieredBudgetFor?.()\n if (tiered !== undefined) {\n generationConfig.thinkingConfig = { thinkingBudget: tiered, includeThoughts: true }\n }\n } else if (LEVEL_THINKING_LEVELS.has(effort)) {\n // A configured number for this level takes the place of the level token:\n // both together would let the level win (see `thinkingBudgetFor`).\n const configured = context.thinkingBudgetFor?.(effort)\n generationConfig.thinkingConfig = configured === undefined\n ? { thinkingLevel: effort, includeThoughts: true }\n : { thinkingBudget: configured, includeThoughts: true }\n }\n } else if (claude && options.purpose !== 'session-title') {\n // Claude thinking models are id-bound (no level selector), so their budget is\n // a single configured value rather than one per level. Measured constraints,\n // all of which must hold or the request is a 400:\n // - `max_tokens` must be STRICTLY greater than the budget. `budget=1024`\n // with `max_tokens=1024` is rejected, and so is a budget sent with no\n // `maxOutputTokens` at all.\n // - the floor is 1024 (not `-1`); the store validates that on save.\n // So a budget that does not leave room is DROPPED rather than forced through\n // by raising `maxTokens`: silently enlarging the caller's output cap would\n // change the request's cost and truncation behaviour, while omitting the\n // budget merely means this turn thinks with upstream's default. A 400 would\n // be worse than either.\n const claudeBudget = context.claudeBudgetFor?.()\n const outputCap = generationConfig.maxOutputTokens\n if (claudeBudget !== undefined && outputCap !== undefined && outputCap > claudeBudget) {\n generationConfig.thinkingConfig = { thinkingBudget: claudeBudget, includeThoughts: true }\n }\n }\n\n return {\n project: context.projectId || undefined,\n requestId: context.requestId ?? generateAntigravityRequestId(),\n model: options.model,\n userAgent: 'antigravity',\n requestType: 'agent',\n request: {\n contents,\n ...(systemText ? { systemInstruction: { parts: [{ text: systemText }] } } : {}),\n ...(tools ? { tools } : {}),\n ...(tools ? { toolConfig: { functionCallingConfig: { mode: 'VALIDATED' } } } : {}),\n ...(Object.keys(generationConfig).length > 0 ? { generationConfig } : {}),\n ...(context.sessionId ? { sessionId: context.sessionId } : {}),\n },\n }\n}\n"],"mappings":"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"}
|