dsh-agy 0.3.2 → 0.4.1
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 +21 -10
- package/lib/{accounts-CKWybAfc.mjs → accounts-BjdDqUq1.mjs} +425 -76
- package/lib/accounts-BjdDqUq1.mjs.map +1 -0
- package/lib/cli/index.d.mts.map +1 -1
- package/lib/cli/index.mjs +4 -2
- package/lib/cli/index.mjs.map +1 -1
- package/lib/client.js +406 -36
- package/lib/index.mjs +2 -2
- package/lib/{plugin-common-B5dyfnMH.mjs → plugin-common-B7OSfnyl.mjs} +354 -24
- package/lib/plugin-common-B7OSfnyl.mjs.map +1 -0
- package/lib/{translate-BQu8HLvB.mjs → translate-YhGDUuVa.mjs} +157 -23
- package/lib/translate-YhGDUuVa.mjs.map +1 -0
- package/lib/web/plugin.d.mts +28 -1
- package/lib/web/plugin.d.mts.map +1 -1
- package/lib/web/plugin.mjs +63 -25
- package/lib/web/plugin.mjs.map +1 -1
- package/package.json +4 -3
- package/lib/accounts-CKWybAfc.mjs.map +0 -1
- package/lib/plugin-common-B5dyfnMH.mjs.map +0 -1
- package/lib/translate-BQu8HLvB.mjs.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-common-B5dyfnMH.mjs","names":[],"sources":["../src/adapter/adapter.ts","../src/model-visibility.ts","../src/thinking-types.ts","../src/thinking-budget.ts","../src/usage-types.ts","../src/stats.ts","../src/plugin-common.ts"],"sourcesContent":["/**\n * AgyAdapter: the DSH seam. A thin orchestrator over the deep modules —\n * account session resolution (shell-provided), request translation, SSE\n * parsing, failure classification, and rotation reporting. All wire details\n * live in translate.ts / parse.ts / models.ts.\n */\n\nimport {\n LlmAdapter,\n LlmError,\n ProviderRequestId,\n QUOTA_EXCEEDED_CODE,\n} from '@deepseek-ai/dsh-llm'\nimport type {\n GenerateOptions,\n LlmModelInfo,\n LlmProviderInfo,\n LlmResolvedModelInfo,\n StreamChunk,\n ToolSchema,\n} from '@deepseek-ai/dsh-llm'\n\nimport { AgyAuthError, AgyPoolBlockedError } from '../types.ts'\nimport type { AgyAccountSession, FailureKind, ManagedAccount, OAuthAuthDetails } from '../types.ts'\nimport type { RateLimitCategory } from '../runtime/classify.ts'\nimport { fetchAgyFirstOk } from '../oauth/constants.ts'\nimport {\n classifyFetchError,\n classifyHttpError,\n describeFetchError,\n isSessionAccumulationOverflow,\n} from '../runtime/classify.ts'\nimport { accountFetch } from '../proxy.ts'\nimport {\n bumpSessionGeneration,\n currentSessionGeneration,\n deriveAntigravitySessionId,\n generateAntigravityRequestId,\n} from '../runtime/identity.ts'\nimport { setThoughtSignature } from '../runtime/signature-cache.ts'\nimport { toAgyRequestBody } from './translate.ts'\nimport type { AgyResolvedImage } from './translate.ts'\nimport { resolveMultimodalFiles } from './multimodal.ts'\nimport { parseAgySse } from './parse.ts'\nimport { AGY_PROVIDER, catalogModelList, listAgyModels, resolveAgyModel } from './models.ts'\n\nexport type { AgyAccountSession }\n\n/**\n * Structural view of the harness attachment service (ctx.attachments).\n * Deliberately not an import of @deepseek-ai/dsh-attachment: the CLI bundle\n * must stay free of harness runtime dependencies, and the real store\n * satisfies this shape.\n */\nexport interface AgyAttachmentStore {\n readImage(ref: {\n attachmentId: string\n mediaType: string\n }): Promise<{ ref: { mediaType: string }; data: Uint8Array }>\n}\n\n/** Collect image refs from user-message content only (spec scope: user images; tool-result nesting out of scope). */\nfunction collectImageRefs(options: GenerateOptions): Array<{ attachmentId: string; mediaType: string }> {\n const refs: Array<{ attachmentId: string; mediaType: string }> = []\n for (const message of options.messages) {\n if (message.role !== 'user') continue\n for (const block of message.content) {\n if (block.type === 'image') refs.push(block.attachment)\n }\n }\n return refs\n}\n\nexport interface AgyAdapterOptions {\n /**\n * Resolve the active account for a request (model-aware: family-scoped quota\n * ranking). `conversationKey` scopes account affinity, so concurrent\n * conversations hold independent pins.\n */\n getSession(model?: string, conversationKey?: string): Promise<AgyAccountSession | undefined>\n /** Report a classified upstream failure so the shell can cool/rotate/revoke. */\n reportFailure(\n kind: FailureKind,\n session: AgyAccountSession,\n info?: {\n retryAfterMs?: number\n status?: number\n rateLimitCategory?: RateLimitCategory\n /** Server-reported absolute reset time; drives precise cooldowns. */\n resetTime?: string\n /** Requested model id; drives family-scoped rate-limit bookkeeping. */\n model?: string\n /** Appeal link from a `verification-required` body; surfaced to the user. */\n verificationUrl?: string\n },\n ): Promise<void>\n /** Report a clean stream completion (resets the failure counter). */\n markSuccess?(session: AgyAccountSession): Promise<void>\n /**\n * Configured token budget for a reasoning level (see `thinking-budget.ts`).\n *\n * Supplied as a resolver rather than a snapshot so an edit in the settings UI\n * applies to the next request without rebuilding the adapter.\n *\n * @param level - the lowercased effort id (`low`/`medium`/`high`).\n * @returns the budget to send, or undefined to let the level stand alone.\n */\n thinkingBudgetFor?(level: string): number | undefined\n /**\n * Configured Claude thinking budget, or undefined when unset.\n *\n * Separate from `thinkingBudgetFor` because the Claude family is id-bound and\n * carries its own validation (floor 1024, and `max_tokens` must exceed it).\n */\n claudeBudgetFor?(): number | undefined\n /** Budget for the tiered slot (the selector's \"Default\" effort). */\n tieredBudgetFor?(): number | undefined\n /** Resolve the harness attachment store; undefined outside the harness (standalone CLI). */\n resolveAttachments?(): AgyAttachmentStore | undefined\n /**\n * In-flight accounting for per-account request fan-out. Takes the account\n * rather than a key so the session manager derives the identity itself —\n * `accountKey` (id-first) and `ledgerAccountKey` (email-first) differ, and\n * mixing them would count against an account nobody selects.\n *\n * `noteRequestSettled` MUST run on every path, including an abandoned stream:\n * the adapter calls it from a `finally`, which async generators run on\n * completion, error, and early consumer termination alike.\n */\n noteRequestStarted?(account: ManagedAccount): void\n noteRequestSettled?(account: ManagedAccount): void\n /**\n * Hidden-model lookup. Optional so an adapter stays constructible without the\n * settings layer (tests, standalone use); absent means nothing is hidden.\n */\n modelVisibility?: { disabledFor(provider: string): ReadonlySet<string> }\n /**\n * Record one request's usage. Optional by design: the CLI must be able to\n * build an adapter without the stats ledger, and a statistics failure must\n * never break a generation.\n */\n recordUsage?(record: {\n account?: string\n model?: string\n usage?: { input: number; output: number; cacheRead: number; cacheWrite: number }\n ok: boolean\n latencyMs?: number\n ttftMs?: number\n }): void\n}\n\nconst UPSTREAM_ERROR_CODE = 'UPSTREAM'\n/** First-class DSH retryable code: the default retry policy honors SERVER (5xx), not UPSTREAM. */\nconst SERVER_ERROR_CODE = 'SERVER'\n\n/** Stable ledger key for an account: email when present, else the generated id. */\nfunction ledgerAccountKey(session: AgyAccountSession): string | undefined {\n return session.account.email ?? session.account.id\n}\n\n/**\n * Build the impersonation headers for one request.\n *\n * There is NO request-id header. `x-goog-request-id` used to be sent here and is\n * present in neither official binary; the id the backend correlates on is the\n * body's `requestId` field, which this request already carries. An invented\n * header is a shape no official client emits, which is precisely the anomaly the\n * impersonation set exists to avoid.\n *\n * `User-Agent` carries the Antigravity client string and NOTHING else. The\n * harness's `attributionHeaders()` is deliberately not merged in: it returns a\n * lowercase `user-agent` key, so spreading it alongside the camel-case\n * `session.impersonation` produced TWO distinct object properties that `fetch`\n * folded into one comma-joined value —\n *\n * `deepseek-harness/<v> (+url), antigravity/<v> <platform>`\n *\n * — a header no official client can emit, byte-identical for every dsh-agy\n * user, and therefore a stronger fingerprint than the one it was trying to\n * avoid. There is exactly one `User-Agent` field on the wire and the upstream\n * requires it to be the client identity, so this request cannot carry both.\n *\n * This adapter performs its own dispatch (see the `fetchAgyFirstOk` call in\n * `stream()`); nothing downstream re-adds the header.\n */\nexport function buildRequestHeaders(session: AgyAccountSession): Record<string, string> {\n return {\n authorization: `Bearer ${session.auth.access}`,\n 'content-type': 'application/json',\n accept: 'text/event-stream',\n // Exactly the two impersonation HEADERS, named individually rather than\n // spread. `session.impersonation.clientMetadata` is a body message and must\n // not become a header — and it is deliberately not added to this envelope\n // either: the only `metadata` field the official descriptor evidences is on\n // the control-plane calls (`LoadCodeAssistRequest`/`OnboardUserRequest`), so\n // on this generate request the identity carrier is the body's existing\n // `userAgent`/`requestType` pair.\n 'User-Agent': session.impersonation['User-Agent'],\n 'X-Goog-Api-Client': session.impersonation['X-Goog-Api-Client'],\n }\n}\n\nexport class AgyAdapter extends LlmAdapter {\n private readonly options: AgyAdapterOptions\n\n constructor(options: AgyAdapterOptions) {\n super()\n this.options = options\n }\n\n override providerInfo(_provider: string): LlmProviderInfo {\n return { id: AGY_PROVIDER, name: 'Antigravity (agy)' }\n }\n\n /**\n * The catalog as DSH's model selector sees it: discovered models minus the\n * user's hidden set. Filtering here is what makes \"turn a model off\" hide it\n * from the picker — the selector reads this, and DSH itself is untouched.\n */\n override async listModels(_provider: string): Promise<readonly LlmModelInfo[]> {\n const all = await this.listAllModels()\n const hidden = this.options.modelVisibility?.disabledFor(AGY_PROVIDER)\n if (hidden === undefined || hidden.size === 0) return all\n return all.filter((model) => !hidden.has(model.id))\n }\n\n /**\n * The complete catalog, ignoring the user's hidden set.\n *\n * The settings page lists models through this rather than `listModels`: if it\n * read the filtered list, a hidden model would vanish from the page along\n * with the switch that hides it, leaving no way to turn it back on without\n * hand-editing the file.\n */\n async listAllModels(): Promise<readonly LlmModelInfo[]> {\n try {\n const session = await this.options.getSession()\n // Model discovery is account-scoped: route it through the account's proxy\n // (control-plane class, so the standard timeouts apply).\n const routing = { proxyUrl: session?.account.proxy }\n return await listAgyModels(session?.auth.access, session?.account.projectId, accountFetch(routing))\n } catch (error) {\n if (error instanceof AgyPoolBlockedError || error instanceof AgyAuthError) {\n return catalogModelList()\n }\n throw error\n }\n }\n\n override async resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo> {\n return resolveAgyModel(provider, model)\n }\n\n // `prepareCall` is deliberately NOT overridden: the LlmAdapter base class\n // (dsh-llm 0.1.1-rc.2+) already binds the resolved model to the stream, which\n // is exactly what this adapter would do. The base implementation also\n // forwards the cancellation signal, which the previous hand-written copy\n // dropped. Only dynamic adapters — those needing different capabilities per\n // generation — should override it.\n\n /**\n * Pre-resolve every image attachment into base64 bytes before translation.\n * Image input hard-fails with UNSUPPORTED_CONTENT (terminal, never retried)\n * when the store is missing or a read fails — silently dropping images and\n * sending text-only is the exact failure mode this path exists to prevent.\n */\n private async resolveRequestImages(options: GenerateOptions): Promise<Map<string, AgyResolvedImage>> {\n const refs = collectImageRefs(options)\n const images = new Map<string, AgyResolvedImage>()\n if (refs.length === 0) return images\n const store = this.options.resolveAttachments?.()\n if (!store) {\n throw new LlmError(\n 'agy image input requires the durable attachment service (in-harness plugin only)',\n 'UNSUPPORTED_CONTENT',\n )\n }\n // Read every attachment concurrently (N images cost one round-trip, not N).\n // allSettled rather than all: more than one read may reject, and the\n // surfaced error must be deterministic (first failure in ref order) instead\n // of whichever concurrent read happened to reject first — and no rejection\n // may escape as unhandled.\n const settled = await Promise.allSettled(\n refs.map(async (ref) => {\n const stored = await store.readImage(ref)\n return {\n attachmentId: ref.attachmentId,\n image: {\n mediaType: stored.ref.mediaType,\n data: Buffer.from(stored.data).toString('base64'),\n },\n }\n }),\n )\n const failedIndex = settled.findIndex((outcome) => outcome.status === 'rejected')\n if (failedIndex !== -1) {\n const ref = refs[failedIndex]!\n const cause: unknown = (settled[failedIndex] as PromiseRejectedResult).reason\n throw new LlmError(\n `agy image attachment \"${ref.attachmentId}\" could not be loaded: ${cause instanceof Error ? cause.message : String(cause)}`,\n 'UNSUPPORTED_CONTENT',\n { cause: cause instanceof Error ? cause : undefined },\n )\n }\n for (const outcome of settled) {\n if (outcome.status === 'fulfilled') {\n images.set(outcome.value.attachmentId, outcome.value.image)\n }\n }\n return images\n }\n\n override async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {\n // The in-flight slot must be released on EVERY path — clean completion, a\n // thrown error, and a consumer that abandons the stream mid-turn. An async\n // generator's `finally` is the one hook covering all three, so the slot is\n // released here rather than at each exit inside `streamInner`.\n const holder: { account?: ManagedAccount } = {}\n try {\n yield* this.streamInner(options, holder)\n } finally {\n if (holder.account !== undefined) this.options.noteRequestSettled?.(holder.account)\n }\n }\n\n private async *streamInner(\n options: GenerateOptions,\n holder: { account?: ManagedAccount },\n ): AsyncIterable<StreamChunk> {\n // Spec D1 sequence: resolve images first — a locally-failing image request\n // must surface UNSUPPORTED_CONTENT (user story 8) instead of being masked\n // by account-pool errors, and must not touch pool state at all.\n const images = await this.resolveRequestImages(options)\n /**\n * The DSH agent loop stamps `options.sessionId` on every request it builds,\n * so it is the conversation identity — used both to scope account affinity\n * here and to derive the upstream `sessionId` below.\n */\n const conversationKey = options.sessionId === undefined ? undefined : String(options.sessionId)\n let session: AgyAccountSession | undefined\n try {\n session = await this.options.getSession(options.model, conversationKey)\n } catch (error) {\n if (error instanceof AgyAuthError) {\n if (error.kind === 'transport') {\n throw new LlmError(error.message, 'TRANSPORT', { cause: error })\n }\n if (error.kind === 'rate-limit') {\n throw new LlmError(error.message, 'RATE_LIMIT', {\n requestId: ProviderRequestId(generateAntigravityRequestId()),\n })\n }\n throw new LlmError(error.message, 'INVALID_CREDENTIAL', { cause: error })\n }\n if (error instanceof AgyPoolBlockedError) {\n if (error.kind === 'quota-exhausted') {\n throw new LlmError(error.message, QUOTA_EXCEEDED_CODE)\n }\n const delta = Math.ceil(error.blockedUntil - Date.now())\n const providerRetryAfterMs = Number.isFinite(delta) && delta > 0 ? delta : 1\n throw new LlmError(error.message, 'RATE_LIMIT', {\n providerRetryAfterMs,\n requestId: ProviderRequestId(generateAntigravityRequestId()),\n })\n }\n throw error\n }\n if (!session) {\n throw new LlmError(\n 'No agy account configured — run `dsh-agy login` to authenticate.',\n 'NO_CREDENTIAL',\n )\n }\n\n const multimodalFiles = await resolveMultimodalFiles(options)\n // The account is now fixed for this request; take the in-flight slot that the\n // `stream` wrapper releases (see its comment for why release lives there).\n holder.account = session.account\n this.options.noteRequestStarted?.(session.account)\n // `conversationKey` is resolved above and shared with account affinity.\n // Standalone CLI callers leave `options.sessionId` unset, so the upstream\n // session id degrades to the per-account value rather than inventing a\n // conversation.\n const conversationAccount = ledgerAccountKey(session)\n /** Wall-clock origin for this attempt's latency figures. */\n const startedAt = Date.now()\n // Streaming dispatch: the account proxy MUST carry the generation request\n // (it carried only the control-plane calls before, so a proxied account\n // silently generated from the host's real IP), and the streaming\n // dispatcher drops the per-gap body timeout a reasoning pause would trip.\n const routing = { proxyUrl: session.account.proxy, streaming: true }\n\n /**\n * Send one request, resending at most once when the upstream reports the\n * per-`sessionId` accumulation wall.\n *\n * The upstream accumulates a conversation's input server-side per\n * `sessionId`; once that passes 1M tokens every request reusing the id fails\n * with a 400 until the upstream session expires. Bumping the generation\n * names a fresh upstream session and recovers the conversation. This is not\n * an account fault, so it does not go through `reportFailure`: the account\n * stays healthy and only the derived id changes.\n */\n const sendAttempt = async (): Promise<{ response: Response; bodyText?: string }> => {\n for (let attempt = 0; ; attempt++) {\n const generation = conversationKey !== undefined && conversationAccount !== undefined\n ? currentSessionGeneration(conversationAccount, conversationKey)\n : 0\n // One id per attempt, carried by the body. A resend under a bumped\n // session generation is a new upstream request, so it gets a new id.\n const requestId = generateAntigravityRequestId()\n const body = toAgyRequestBody(options, {\n projectId: session.account.projectId,\n sessionId:\n deriveAntigravitySessionId(session.account.email, conversationKey, generation) ?? undefined,\n requestId,\n ...(this.options.thinkingBudgetFor === undefined\n ? {}\n : { thinkingBudgetFor: this.options.thinkingBudgetFor }),\n ...(this.options.claudeBudgetFor === undefined\n ? {}\n : { claudeBudgetFor: this.options.claudeBudgetFor }),\n ...(this.options.tieredBudgetFor === undefined\n ? {}\n : { tieredBudgetFor: this.options.tieredBudgetFor }),\n ...(images.size > 0 ? { images } : {}),\n ...(multimodalFiles.size > 0 ? { multimodalFiles } : {}),\n })\n let response: Response\n try {\n response = await fetchAgyFirstOk(\n '/v1internal:streamGenerateContent?alt=sse',\n {\n method: 'POST',\n headers: buildRequestHeaders(session),\n body: JSON.stringify(body),\n signal: options.signal,\n },\n accountFetch(routing),\n routing,\n )\n } catch (error) {\n const classified = classifyFetchError(error, { proxyUrl: session.account.proxy })\n await this.options.reportFailure(classified.kind, session)\n // A transport failure consumed no tokens, but it is a real attempt\n // against this account's quota — record the request, not the usage.\n this.recordUsage(session, options.model, { ok: false }, startedAt)\n throw new LlmError(classified.message ?? 'agy fetch failed', 'TRANSPORT', { cause: error })\n }\n if (response.ok) return { response }\n const bodyText = await response.text().catch(() => undefined)\n if (\n attempt === 0\n && conversationKey !== undefined\n && conversationAccount !== undefined\n && isSessionAccumulationOverflow(response.status, bodyText)\n ) {\n bumpSessionGeneration(conversationAccount, conversationKey)\n continue\n }\n return { response, bodyText }\n }\n }\n\n const { response, bodyText } = await sendAttempt()\n\n if (!response.ok) {\n const classified = classifyHttpError(response.status, response.headers, bodyText)\n await this.options.reportFailure(classified.kind, session, {\n retryAfterMs: classified.retryAfterMs,\n status: response.status,\n rateLimitCategory: classified.rateLimitCategory,\n resetTime: classified.resetTime,\n model: options.model,\n verificationUrl: classified.verificationUrl,\n })\n this.recordUsage(session, options.model, {\n ok: false,\n rateLimited: classified.kind === 'rate-limit',\n }, startedAt)\n if (classified.kind === 'rate-limit') {\n // soft/rate limits are retryable by the harness (RATE_LIMIT + delay);\n // daily quota exhaustion is terminal (QUOTA, 24h cooldown already set).\n if (classified.rateLimitCategory === 'quota_exhausted') {\n throw new LlmError(\n `agy daily quota exhausted (${response.status}): ${classified.message ?? ''}`,\n QUOTA_EXCEEDED_CODE,\n )\n }\n throw new LlmError(\n `agy rate-limited (${response.status}): ${classified.message ?? ''}`,\n 'RATE_LIMIT',\n {\n providerRetryAfterMs: classified.retryAfterMs ?? undefined,\n requestId: ProviderRequestId(generateAntigravityRequestId()),\n },\n )\n }\n if (classified.kind === 'verification-required') {\n // Recoverable, so deliberately NOT INVALID_CREDENTIAL: the account is\n // parked for a timed window, not disabled, and the pool moves on. The\n // appeal link goes in the message because a message is the only channel\n // DSH surfaces to the user.\n //\n // Deliberately NO `providerRetryAfterMs`: the park IS the cooldown, and a\n // delay above DSH's `maxDelayMs` makes its `normal` retry mode give up\n // outright (`llm-retry`: `providerRetryAfterMs > maxDelayMs` -> `next()`),\n // turning a recoverable challenge into a failed turn. With no delay DSH\n // backs off locally and retries, and that retry lands on another account\n // because this one is already parked.\n const appeal = classified.verificationUrl ? ` Verify at: ${classified.verificationUrl}` : ''\n throw new LlmError(\n `agy account needs verification (${response.status}): ${classified.message ?? ''}${appeal}`,\n 'RATE_LIMIT',\n {\n requestId: ProviderRequestId(generateAntigravityRequestId()),\n },\n )\n }\n if (classified.kind === 'auth-failure') {\n throw new LlmError(\n `agy authentication failed (${response.status}) — run \\`dsh-agy login\\``,\n 'INVALID_CREDENTIAL',\n )\n }\n // 5xx upstream failures (e.g. 503 \"No capacity available\") are transient:\n // the DSH retry policy honors SERVER but treats UPSTREAM as terminal, so\n // classifying 5xx as UPSTREAM kills the turn with zero retries. Non-5xx\n // transient/request errors (404, generic 400, other 4xx) stay terminal.\n if (classified.status !== undefined && classified.status >= 500) {\n throw new LlmError(\n `agy upstream error (${response.status}): ${classified.message ?? ''}`,\n SERVER_ERROR_CODE,\n {\n providerRetryAfterMs: classified.retryAfterMs ?? undefined,\n requestId: ProviderRequestId(generateAntigravityRequestId()),\n },\n )\n }\n throw new LlmError(\n `agy upstream error (${response.status}): ${classified.message ?? ''}`,\n UPSTREAM_ERROR_CODE,\n )\n }\n\n if (!response.body) {\n throw new LlmError('agy stream returned no body', UPSTREAM_ERROR_CODE)\n }\n\n try {\n // The usage chunk is the ledger's only token source, so the stream is\n // consumed here rather than piped: every chunk still reaches DSH\n // unchanged, but the terminal `usage` chunk is also folded into this\n // account's record. Upstream repeats `usageMetadata` on every SSE event\n // (cumulative); parse.ts already reduces that to one final chunk.\n let usage: { input: number; output: number; cacheRead: number; cacheWrite: number } | undefined\n let ttftMs: number | undefined\n for await (const chunk of parseAgySse(response.body, {\n signal: options.signal,\n onToolSignature: (toolCallId, signature) => {\n setThoughtSignature(toolCallId, signature)\n },\n })) {\n if (chunk.type === 'usage') {\n usage = {\n input: chunk.usage.inputTokens,\n output: chunk.usage.outputTokens,\n cacheRead: chunk.usage.cacheReadTokens ?? 0,\n cacheWrite: chunk.usage.cacheWriteTokens ?? 0,\n }\n } else if (ttftMs === undefined && (chunk.type === 'text-delta' || chunk.type === 'reasoning-delta')) {\n // First model-authored output: the honest end of \"time to first token\".\n ttftMs = Date.now() - startedAt\n }\n yield chunk\n }\n await this.options.markSuccess?.(session)\n this.recordUsage(session, options.model, { ok: true, usage, ttftMs }, startedAt)\n } catch (error) {\n if (error instanceof DOMException && error.name === 'AbortError') {\n throw new LlmError('agy stream aborted', 'ABORTED', { cause: error })\n }\n await this.options.reportFailure('network-error', session)\n // A stream that died mid-body may already have delivered billable\n // content, so the attempt is recorded even though no usage chunk arrived.\n this.recordUsage(session, options.model, { ok: false }, startedAt)\n // Deliberately UPSTREAM (terminal), not TRANSPORT: content may already\n // have been emitted, and DSH's retry policy honours TRANSPORT, so retrying\n // here would replay a partially-delivered turn. The account-level report\n // above already absorbs the transient case by cooling/rotating. The cause\n // code is still surfaced so the socket failure is legible in session events.\n throw new LlmError(\n error instanceof Error ? describeFetchError(error) : 'agy stream parse failed',\n UPSTREAM_ERROR_CODE,\n { cause: error },\n )\n }\n }\n\n /**\n * Fold one attempt into the usage ledger.\n *\n * Statistics are diagnostics, never load-bearing: a ledger failure must not\n * fail a generation, so this swallows its own errors.\n */\n private recordUsage(\n session: AgyAccountSession,\n model: string | undefined,\n result: {\n ok: boolean\n rateLimited?: boolean\n usage?: { input: number; output: number; cacheRead: number; cacheWrite: number }\n ttftMs?: number\n },\n startedAt: number,\n ): void {\n const record = this.options.recordUsage\n if (record === undefined) return\n try {\n record({\n ...(ledgerAccountKey(session) === undefined ? {} : { account: ledgerAccountKey(session) }),\n ...(model === undefined ? {} : { model }),\n ok: result.ok,\n ...(result.rateLimited === true ? { rateLimited: true } : {}),\n ...(result.usage === undefined ? {} : { usage: result.usage }),\n ...(result.ttftMs === undefined ? {} : { ttftMs: result.ttftMs }),\n latencyMs: Math.max(0, Date.now() - startedAt),\n })\n } catch {\n // Swallowed by design.\n }\n }\n}\n\nexport type { ToolSchema }\n","/**\n * Model visibility: a per-provider blacklist of model ids hidden from the DSH\n * model selector.\n *\n * Mechanism: DSH's selector reads `session.modelCatalog`, which calls\n * `ctx.llm.listModels(provider)` -> `AgyAdapter.listModels()`. Filtering inside\n * the adapter is therefore sufficient to hide a model from the picker without\n * touching DSH — no host patch, no new RPC.\n *\n * Two deliberate choices:\n *\n * - Blacklist, not whitelist. Only explicitly-disabled models are hidden, so a\n * model the server adds later still shows up. A whitelist would silently hide\n * every new model until the user re-enabled it.\n * - Persisted to agy's own file rather than a `ctx.settings` namespace. The\n * settings service requires a `@deepseek-ai/schemastery` schema, and the\n * standalone CLI must not import any `@deepseek-ai/*` package at runtime\n * (AGENTS.md invariant). agy already owns plain JSON state with the same\n * atomic-write pattern (`agy-accounts.json`), so this stays consistent and\n * lets the CLI read the same blacklist.\n *\n * Only `true` counts as disabled; an id maps to `true` or is absent. Enabling\n * deletes the key, so the file holds just the hidden set and cannot grow with\n * every toggle.\n */\n\nimport { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { resolveDshHome } from './store/keyring.ts'\n\nexport const MODELS_VERSION = 1\n\n/** `provider -> modelId -> true`. */\nexport type DisabledModelMap = Record<string, Record<string, true>>\n\nexport interface ModelVisibilityDocument {\n version: number\n disabled: DisabledModelMap\n}\n\nconst EMPTY_SET: ReadonlySet<string> = new Set<string>()\n\n/** Keep only well-formed `provider -> { modelId: true }` entries (tolerates hand edits). */\nexport function sanitizeDisabledModels(raw: unknown): DisabledModelMap {\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return {}\n const out: DisabledModelMap = {}\n for (const [provider, value] of Object.entries(raw as Record<string, unknown>)) {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) continue\n const perProvider: Record<string, true> = {}\n for (const [modelId, flag] of Object.entries(value as Record<string, unknown>)) {\n if (flag === true) perProvider[modelId] = true\n }\n if (Object.keys(perProvider).length > 0) out[provider] = perProvider\n }\n return out\n}\n\nexport function parseModelVisibility(text: string): ModelVisibilityDocument {\n let raw: unknown\n try {\n raw = JSON.parse(text)\n } catch {\n return { version: MODELS_VERSION, disabled: {} }\n }\n const disabled = typeof raw === 'object' && raw !== null\n ? sanitizeDisabledModels((raw as Record<string, unknown>).disabled)\n : {}\n return { version: MODELS_VERSION, disabled }\n}\n\nexport interface ModelVisibilityOptions {\n /** Defaults to `$DSH_HOME/agy-models.json`. */\n file?: string\n}\n\n/**\n * In-memory authoritative copy of the blacklist, backed by a JSON file.\n *\n * The adapter calls `disabledFor()` on every `listModels()`, so that read is a\n * pure in-memory lookup: a toggle takes effect on the next catalog refresh\n * without rebuilding the adapter or restarting the host. Writes go through the\n * file immediately (toggles are rare, user-initiated).\n *\n * `disabledFor()` also revalidates against the file's mtime and reloads when it\n * moved. Two instances of this class routinely coexist in ONE process — the\n * main plugin entry and the web entry each build their own runtime — and a\n * toggle is written by the web instance while the model selector reads the main\n * instance. Without the mtime check the selector would keep serving its stale\n * in-memory copy, so switching a model off appeared to do nothing until the\n * host restarted (the `llm/adapters-updated` notification fires, the picker\n * refreshes, and the model is still listed). The same check is what lets a CLI\n * toggle be seen by a running server.\n */\nexport class ModelVisibility {\n private readonly file: string\n private disabled: DisabledModelMap\n /** Raw text this instance last read or wrote; see `reloadIfChanged`. */\n private raw: string\n /** Reusable frozen empty set, so the common \"nothing disabled\" read allocates nothing. */\n private readonly sets = new Map<string, ReadonlySet<string>>()\n\n constructor(options: ModelVisibilityOptions = {}) {\n this.file = options.file ?? join(resolveDshHome(), 'agy-models.json')\n const initial = this.readWithRaw()\n this.disabled = initial.disabled\n this.raw = initial.raw\n }\n\n /** Path of the backing file. */\n get path(): string {\n return this.file\n }\n\n /** The exact bytes `write()` would persist, so `raw` can track what this instance wrote. */\n private static serialize(next: DisabledModelMap): string {\n const doc: ModelVisibilityDocument = { version: MODELS_VERSION, disabled: next }\n return JSON.stringify(doc, null, 2) + '\\n'\n }\n\n /**\n * Read the map together with the raw text it came from.\n *\n * The TEXT (not an mtime) is what detects another writer's change. An mtime\n * cannot: a toggle and a re-read can land in the same filesystem timestamp\n * tick, and Windows resolves that coarsely enough to have failed CI — two\n * distinct writes reported an identical mtime, so the second change was never\n * seen and a disabled model stayed selectable. Content comparison cannot miss\n * a change, whatever the clock resolution.\n */\n private readWithRaw(): { disabled: DisabledModelMap, raw: string } {\n try {\n const raw = readFileSync(this.file, 'utf8')\n return { disabled: parseModelVisibility(raw).disabled, raw }\n } catch {\n // A missing or unreadable file means \"nothing disabled\".\n return { disabled: {}, raw: '' }\n }\n }\n\n private write(next: DisabledModelMap): void {\n mkdirSync(dirname(this.file), { recursive: true })\n const tmp = `${this.file}.tmp`\n writeFileSync(tmp, ModelVisibility.serialize(next), { mode: 0o600 })\n renameSync(tmp, this.file)\n }\n\n /**\n * Disabled model ids for one provider. Cheap enough for every catalog refresh:\n * one `stat` plus an in-memory map lookup, and the stat is what keeps two\n * same-process instances (and a CLI writer) in agreement.\n */\n disabledFor(provider: string): ReadonlySet<string> {\n this.reloadIfChanged()\n const cacheKey = provider\n const cached = this.sets.get(cacheKey)\n if (cached !== undefined) return cached\n const perProvider = this.disabled[provider]\n if (perProvider === undefined) return EMPTY_SET\n const ids = Object.keys(perProvider)\n if (ids.length === 0) return EMPTY_SET\n const set: ReadonlySet<string> = new Set(ids)\n this.sets.set(cacheKey, set)\n return set\n }\n\n /** Whether one model is currently hidden. Also revalidates, for the same reason. */\n isDisabled(provider: string, modelId: string): boolean {\n this.reloadIfChanged()\n return this.disabled[provider]?.[modelId] === true\n }\n\n /** The raw map, for the settings UI. */\n all(): DisabledModelMap {\n const out: DisabledModelMap = {}\n for (const [provider, models] of Object.entries(this.disabled)) out[provider] = { ...models }\n return out\n }\n\n /**\n * Hide or show one model. Persists immediately.\n *\n * The edit is applied to a freshly re-read map, not to this instance's copy:\n * with two instances in one process (main plugin + web entry) and a CLI in\n * another, editing the stale copy would silently drop whatever the other\n * writer had toggled since this instance last read. Toggles are rare and\n * user-initiated, so a read-modify-write costs nothing here — unlike the\n * ledger, this is not a hot path.\n *\n * Not locked: a simultaneous toggle from two processes can still lose one of\n * the two updates. The write itself is atomic (tmp + rename), so the file is\n * never corrupt — losing one toggle under a genuine race is an acceptable\n * trade for not introducing a lock file into a settings toggle.\n */\n setDisabled(provider: string, modelId: string, disabled: boolean): void {\n if (modelId === '') throw new Error('setDisabled: modelId must not be empty')\n this.reload()\n const next: DisabledModelMap = {}\n for (const [key, models] of Object.entries(this.disabled)) next[key] = { ...models }\n const perProvider = { ...(next[provider] ?? {}) }\n if (disabled) perProvider[modelId] = true\n else delete perProvider[modelId]\n if (Object.keys(perProvider).length === 0) delete next[provider]\n else next[provider] = perProvider\n this.write(next)\n this.disabled = next\n this.raw = ModelVisibility.serialize(next)\n // Drop the memoized set for this provider so the next read reflects the write.\n this.sets.delete(provider)\n }\n\n /** Re-read the file (multi-process: another writer may have toggled a model). */\n reload(): void {\n const fresh = this.readWithRaw()\n this.disabled = fresh.disabled\n this.raw = fresh.raw\n this.sets.clear()\n }\n\n /**\n * Reload when the file's contents differ from what this instance last read.\n *\n * Reads rather than stats: `disabledFor()` runs once per catalog refresh, not\n * per request, and the call it feeds is a network request to the model\n * endpoint — one small local read is not the cost that matters here, while a\n * missed change is the bug this whole mechanism exists to prevent.\n */\n private reloadIfChanged(): void {\n let current: string\n try {\n current = readFileSync(this.file, 'utf8')\n } catch {\n // Missing/unreadable: treat as empty, matching `readWithRaw`.\n current = ''\n }\n if (current === this.raw) return\n this.reload()\n }\n}\n","/**\n * Thinking-budget vocabulary shared by the host store, the RPC wire contract,\n * and the browser UI.\n *\n * Split out for the same reason `usage-types.ts` exists: these types live on\n * their own, free of any `node:*` import, so the browser bundle can describe the\n * settings surface without pulling in the host's storage module. Putting them in\n * `thinking-budget.ts` instead made `rpc-contract.ts` (which the client\n * typechecks) reach `node:fs` through the transitive import — caught by\n * `tsconfig.client.json`, which includes the contract and excludes `src/store`.\n */\n\n/** The three levels a tiered model exposes, in display order. */\nexport const THINKING_LEVELS = ['low', 'medium', 'high'] as const\n\nexport type ThinkingLevel = typeof THINKING_LEVELS[number]\n\n/**\n * The accepted `thinkingBudget` interval, measured on this channel.\n *\n * Exported so the UI, the RPC validation, and the store's sanitizer share ONE\n * definition: a value outside it is a 400 from upstream naming exactly this\n * range, so rejecting it locally turns a per-request failure into a save-time\n * message.\n */\nexport const THINKING_BUDGET_MIN = -1\nexport const THINKING_BUDGET_MAX = 65_535\n\n/** One budget per level; an ABSENT key means \"send no budget\" (upstream decides). */\nexport type ThinkingBudgets = Partial<Record<ThinkingLevel, number>>\n\n/**\n * The Claude family's `thinkingBudget` bounds, measured separately from Gemini's.\n *\n * The two families do NOT share a contract, which is why one interval cannot\n * serve both:\n * - Claude's floor is **1024**, not `-1`. A budget of 1 or 512 is rejected with\n * `thinking.enabled.budget_tokens: Input should be greater than or equal to\n * 1024`; `-1` and `0` are accepted as special values.\n * - Claude additionally requires **`max_tokens` strictly greater than the\n * budget**: `budget=1024, max_tokens=1024` is a 400, and a budget sent with\n * no `maxOutputTokens` at all also fails. So the margin is at least one token.\n *\n * `CLAUDE_BUDGET_MAX` is therefore one below `AGY_CLAUDE_MAX_OUTPUT_TOKENS` — the\n * largest budget that can still leave room for a strictly greater `max_tokens`.\n */\nexport const CLAUDE_BUDGET_MIN = 1024\nexport const CLAUDE_BUDGET_MAX = 63_999\n\n/** Whether `value` may be sent as a Claude `thinkingBudget`. */\nexport function isValidClaudeBudget(value: unknown): value is number {\n if (typeof value !== 'number' || !Number.isInteger(value)) return false\n // `-1` (adaptive) and `0` are accepted in addition to the documented minimum;\n // both are measured rather than inferred.\n if (value === -1 || value === 0) return true\n return value >= CLAUDE_BUDGET_MIN && value <= CLAUDE_BUDGET_MAX\n}\n","/**\n * Thinking-budget configuration: a global Low/Medium/High -> token mapping.\n *\n * WHY A GLOBAL MAP RATHER THAN PER-MODEL. Only `*-tiered` models emit a\n * `thinkingConfig` at all (see `isLevelThinkingModel`), and today that is three\n * ids in one family — while the levels a user actually picks are already exposed\n * by DSH's own model selector (`reasoningEffort`). So the missing piece is not a\n * per-model switch but the VALUE behind each level: \"High\" currently means the\n * upstream's own High, with no way to say \"High = 16000 tokens\". One mapping of\n * three numbers gives that, and because it is keyed by level it applies to every\n * tiered model without a per-model row.\n *\n * Measured on this channel (docs/ANTIGRAVITY-API.md §3):\n * - accepted interval is [-1, 65535]; `-1` is adaptive and IS accepted\n * - `minThinkingBudget` is NOT a validation (a budget below it answers 200),\n * so it must never become a client-side clamp\n * - `0` reduces thinking but does NOT reliably disable it\n *\n * An EMPTY entry means \"do not send a budget for this level\" — the request keeps\n * sending `thinkingLevel`, which is the behaviour before this feature existed.\n * That is why the shipped defaults are all empty: the feature is opt-in and its\n * absence is a no-op.\n *\n * Persisted to agy's own JSON rather than `ctx.settings`, for the reason recorded\n * in `model-visibility.ts`: that service needs a `@deepseek-ai/schemastery`\n * schema, and the CLI must not import `@deepseek-ai/*` at runtime. Same\n * atomic-write and content-compare reload pattern, so a writer in one process\n * (the web entry) is seen by a reader in another (the main plugin).\n */\n\nimport { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { resolveDshHome } from './store/keyring.ts'\nimport {\n THINKING_BUDGET_MAX,\n THINKING_BUDGET_MIN,\n THINKING_LEVELS,\n CLAUDE_BUDGET_MAX,\n CLAUDE_BUDGET_MIN,\n isValidClaudeBudget,\n} from './thinking-types.ts'\nimport type { ThinkingBudgets, ThinkingLevel } from './thinking-types.ts'\n\nexport const THINKING_VERSION = 1\n\nexport {\n THINKING_LEVELS,\n THINKING_BUDGET_MIN,\n THINKING_BUDGET_MAX,\n} from './thinking-types.ts'\nexport type { ThinkingLevel, ThinkingBudgets } from './thinking-types.ts'\n\n/**\n * The persisted document.\n *\n * `budgets` is keyed by reasoning LEVEL and applies to the tiered (Gemini)\n * family, whose single id exposes low/medium/high. `claudeBudget` is a single\n * value because the Claude family is id-bound: each capability is its own model\n * id with no level selector, so there is nothing to key by. Keeping them in one\n * document but distinct fields is what lets each carry its own validation —\n * their accepted intervals genuinely differ (Claude's floor is 1024, not -1).\n */\nexport interface ThinkingDocument {\n version: number\n budgets: ThinkingBudgets\n /** Budget for Claude thinking models; absent means \"send no budget\". */\n claudeBudget?: number\n /**\n * Budget for the TIERED slot — the selector's \"Default\" effort.\n *\n * That effort carries no level id (`effort: void 0`), so it cannot be keyed in\n * `budgets`. A value here sends `thinkingBudget` WITHOUT `thinkingLevel`, which\n * is what makes \"Default\" configurable as a Max: the effort still decides\n * nothing, the number does. Absent means the request sends no thinkingConfig at\n * all, i.e. upstream's own adaptive allocation.\n */\n tieredBudget?: number\n}\n\n/** Whether `value` may be sent as a `thinkingBudget`. */\nexport function isValidThinkingBudget(value: unknown): value is number {\n return typeof value === 'number'\n && Number.isInteger(value)\n && value >= THINKING_BUDGET_MIN\n && value <= THINKING_BUDGET_MAX\n}\n\n/**\n * Keep only well-formed entries (tolerates hand edits).\n *\n * Rejects out-of-range and non-integer values rather than clamping them: a\n * clamped value would silently mean something the user did not ask for, and\n * upstream rejects it anyway, so dropping the entry falls back to the\n * `thinkingLevel` path — which is valid.\n */\nexport function sanitizeThinkingBudgets(raw: unknown): ThinkingBudgets {\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return {}\n const out: ThinkingBudgets = {}\n for (const level of THINKING_LEVELS) {\n const value = (raw as Record<string, unknown>)[level]\n if (isValidThinkingBudget(value)) out[level] = value\n }\n return out\n}\n\nexport function parseThinkingDocument(text: string): ThinkingDocument {\n let raw: unknown\n try {\n raw = JSON.parse(text)\n } catch {\n return { version: THINKING_VERSION, budgets: {} }\n }\n const record = typeof raw === 'object' && raw !== null ? raw as Record<string, unknown> : {}\n const budgets = sanitizeThinkingBudgets(record.budgets)\n const claude = record.claudeBudget\n const tiered = record.tieredBudget\n return {\n version: THINKING_VERSION,\n budgets,\n // Dropped when unusable rather than clamped, matching the level budgets: the\n // request then sends no budget, which is a valid state.\n ...(isValidClaudeBudget(claude) ? { claudeBudget: claude } : {}),\n ...(isValidThinkingBudget(tiered) ? { tieredBudget: tiered } : {}),\n }\n}\n\nexport interface ThinkingBudgetOptions {\n /** Defaults to `$DSH_HOME/agy-thinking.json`. */\n file?: string\n /**\n * Minimum gap between hot-path file revalidations. Defaults to\n * `DEFAULT_REVALIDATE_INTERVAL_MS`; `0` disables throttling (useful in tests\n * that need a cross-instance write visible immediately).\n */\n revalidateIntervalMs?: number\n}\n\n/**\n * How long a hot-path read may serve the in-memory copy before re-checking the\n * file. One second keeps a settings change effectively immediate to the user\n * while collapsing a per-request read into at most one per second.\n */\nexport const DEFAULT_REVALIDATE_INTERVAL_MS = 1_000\n\n/**\n * In-memory authoritative copy of the budgets, backed by a JSON file.\n *\n * `budgetFor` sits on the GENERATION HOT PATH (`toAgyRequestBody` calls it for\n * every request), so the cross-instance revalidation it needs is rate-limited:\n * see `revalidateIntervalMs`. Writes go to the file immediately, since edits are\n * rare and user-initiated, and they bypass the limit so a concurrent edit is\n * never overwritten.\n */\nexport class ThinkingBudgetStore {\n private readonly file: string\n private readonly revalidateIntervalMs: number\n private doc: ThinkingDocument\n /** Raw text this instance last read or wrote; see `reloadNow`. */\n private raw: string\n /** When `reloadNow` last ran, for the hot-path throttle. */\n private checkedAt: number\n\n constructor(options: ThinkingBudgetOptions = {}) {\n this.file = options.file ?? join(resolveDshHome(), 'agy-thinking.json')\n this.revalidateIntervalMs = options.revalidateIntervalMs ?? DEFAULT_REVALIDATE_INTERVAL_MS\n const initial = this.readWithRaw()\n this.doc = initial.doc\n this.raw = initial.raw\n this.checkedAt = Date.now()\n }\n\n /** Path of the backing file. */\n get path(): string {\n return this.file\n }\n\n private static serialize(doc: ThinkingDocument): string {\n return JSON.stringify(doc, null, 2) + '\\n'\n }\n\n /**\n * Read the document together with the raw text it came from.\n *\n * Content, not mtime: two writers can land in the same filesystem timestamp\n * tick (Windows resolves it coarsely enough to have failed CI in this repo),\n * and a missed change is exactly the bug this check exists to prevent.\n */\n private readWithRaw(): { doc: ThinkingDocument, raw: string } {\n try {\n const raw = readFileSync(this.file, 'utf8')\n return { doc: parseThinkingDocument(raw), raw }\n } catch {\n // A missing or unreadable file means \"nothing configured\".\n return { doc: { version: THINKING_VERSION, budgets: {} }, raw: '' }\n }\n }\n\n /**\n * Re-read the file if another writer changed it.\n *\n * Compares CONTENT, not mtime: a toggle and a re-read can land in the same\n * filesystem timestamp tick, and Windows resolves that coarsely enough to have\n * failed CI in this repo (`model-visibility.ts` records the incident). Content\n * comparison cannot miss a change, whatever the clock resolution.\n *\n * Unconditional — callers on the hot path use `reloadIfChanged` instead.\n */\n private reloadNow(): void {\n let current: string\n try {\n current = readFileSync(this.file, 'utf8')\n } catch {\n current = ''\n }\n this.checkedAt = Date.now()\n if (current === this.raw) return\n const fresh = this.readWithRaw()\n this.doc = fresh.doc\n this.raw = fresh.raw\n }\n\n /**\n * Hot-path revalidation: `reloadNow`, rate-limited.\n *\n * Without the limit every generation pays a blocking `readFileSync` plus a\n * UTF-8 decode — the module used to claim these reads were \"in-memory\", which\n * an unconditional read made false. The file is written only by `setBudget`\n * (a rare, user-initiated action), so the worst case here is that a settings\n * change takes up to `revalidateIntervalMs` to reach an already-running\n * adapter. That is the right trade for a setting; it is not right for the write\n * path, which is why `write` calls `reloadNow` directly.\n */\n private reloadIfChanged(): void {\n if (Date.now() - this.checkedAt < this.revalidateIntervalMs) return\n this.reloadNow()\n }\n\n /**\n * Write the whole document atomically.\n *\n * Always re-reads first, so a concurrent edit by the OTHER instance in this\n * process (main plugin + web entry) is merged rather than overwritten: each\n * caller passes a mutation, not a full replacement built from a stale copy.\n */\n private write(mutate: (doc: ThinkingDocument) => void): void {\n // Unconditional: building on a throttled-skipped stale copy would drop the\n // other instance's concurrent edit, which is the whole reason for re-reading.\n this.reloadNow()\n const next: ThinkingDocument = {\n version: THINKING_VERSION,\n budgets: { ...this.doc.budgets },\n ...(this.doc.claudeBudget === undefined ? {} : { claudeBudget: this.doc.claudeBudget }),\n ...(this.doc.tieredBudget === undefined ? {} : { tieredBudget: this.doc.tieredBudget }),\n }\n mutate(next)\n const text = ThinkingBudgetStore.serialize(next)\n mkdirSync(dirname(this.file), { recursive: true })\n const tmp = `${this.file}.tmp`\n writeFileSync(tmp, text, { mode: 0o600 })\n renameSync(tmp, this.file)\n this.doc = next\n this.raw = text\n }\n\n /** The configured budget for one level, or undefined when unset. */\n budgetFor(level: string | undefined): number | undefined {\n this.reloadIfChanged()\n if (level === undefined) return undefined\n const key = level.toLowerCase() as ThinkingLevel\n if (!THINKING_LEVELS.includes(key)) return undefined\n return this.doc.budgets[key]\n }\n\n /** The Claude-family budget, or undefined when unset. */\n claudeBudget(): number | undefined {\n this.reloadIfChanged()\n return this.doc.claudeBudget\n }\n\n /** The tiered-slot budget (the selector's Default effort), or undefined. */\n tieredBudget(): number | undefined {\n this.reloadIfChanged()\n return this.doc.tieredBudget\n }\n\n /** The raw map, for the settings UI. */\n all(): ThinkingBudgets {\n this.reloadIfChanged()\n return { ...this.doc.budgets }\n }\n\n /** The whole document, for the settings UI. */\n snapshot(): ThinkingDocument {\n this.reloadIfChanged()\n return {\n version: THINKING_VERSION,\n budgets: { ...this.doc.budgets },\n ...(this.doc.claudeBudget === undefined ? {} : { claudeBudget: this.doc.claudeBudget }),\n ...(this.doc.tieredBudget === undefined ? {} : { tieredBudget: this.doc.tieredBudget }),\n }\n }\n\n /** Replace one level's budget, or clear it when `value` is undefined. */\n setBudget(level: string, value: number | undefined): ThinkingBudgets {\n const key = level.toLowerCase() as ThinkingLevel\n if (!THINKING_LEVELS.includes(key)) throw new Error(`unknown thinking level: ${level}`)\n if (value !== undefined && !isValidThinkingBudget(value)) {\n throw new Error(`thinking budget must be an integer in [${THINKING_BUDGET_MIN}, ${THINKING_BUDGET_MAX}]`)\n }\n this.write((doc) => {\n if (value === undefined) delete doc.budgets[key]\n else doc.budgets[key] = value\n })\n return this.all()\n }\n\n /** Replace the tiered-slot budget, or clear it when `value` is undefined. */\n setTieredBudget(value: number | undefined): ThinkingDocument {\n if (value !== undefined && !isValidThinkingBudget(value)) {\n throw new Error(`thinking budget must be an integer in [${THINKING_BUDGET_MIN}, ${THINKING_BUDGET_MAX}]`)\n }\n this.write((doc) => {\n if (value === undefined) delete doc.tieredBudget\n else doc.tieredBudget = value\n })\n return this.snapshot()\n }\n\n /** Replace the Claude budget, or clear it when `value` is undefined. */\n setClaudeBudget(value: number | undefined): ThinkingDocument {\n if (value !== undefined && !isValidClaudeBudget(value)) {\n throw new Error(`Claude thinking budget must be -1, 0, or an integer in [${CLAUDE_BUDGET_MIN}, ${CLAUDE_BUDGET_MAX}]`)\n }\n this.write((doc) => {\n if (value === undefined) delete doc.claudeBudget\n else doc.claudeBudget = value\n })\n return this.snapshot()\n }\n}\n","/**\n * Usage vocabulary shared by the host ledger, the RPC wire contract, and the\n * browser UI.\n *\n * These types live on their own, free of any `node:*` import, so the browser\n * bundle can describe usage without pulling in the host's storage module. The\n * ledger (`stats.ts`) and the wire contract (`rpc-contract.ts`) both build on\n * this module rather than on each other.\n */\n\n/** Token counters, disjoint exactly like DSH's `TokenUsage`. */\nexport interface TokenBuckets {\n /** Uncached input tokens. */\n input: number\n output: number\n cacheRead: number\n cacheWrite: number\n}\n\n/** One scope's counters (an account, a model, a day, or the grand total). */\nexport interface UsageCounters extends TokenBuckets {\n /** Requests attempted, including failures. */\n requests: number\n /** Requests that returned a usable stream. */\n succeeded: number\n /** Classified upstream failures of any kind. */\n failed: number\n /** Rate-limit classifications (HTTP 429, and 403 carrying quota phrasing). */\n rateLimited: number\n /** Pool rotations triggered by a failure. */\n rotations: number\n /** Summed request wall time, and the number of requests that timed it. */\n latencyMs: number\n latencyN: number\n /** Summed time-to-first-token, and the number of requests that reported one. */\n ttftMs: number\n ttftN: number\n}\n\n/** Where a request came from; CLI/verify/test never appear in DSH's own ledger. */\nexport type UsageSource = 'chat' | 'cli' | 'verify' | 'test'\n\n/** Zeroed counters, for accumulation. */\nexport function zeroCounters(): UsageCounters {\n return {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n requests: 0,\n succeeded: 0,\n failed: 0,\n rateLimited: 0,\n rotations: 0,\n latencyMs: 0,\n latencyN: 0,\n ttftMs: 0,\n ttftN: 0,\n }\n}\n","/**\n * Cumulative agy usage statistics, persisted to `$DSH_HOME/agy-stats.json`.\n *\n * Why agy keeps its own ledger instead of reading DSH's session projections:\n *\n * - DSH structurally has no account dimension (no `account` field anywhere in\n * `dsh-llm` or the session types), so \"which account burned this quota\" is\n * unanswerable from DSH's data.\n * - agy's own traffic bypasses DSH entirely — `dsh-agy login/verify/status`,\n * `testCall()`, `checkAccounts()`, and quota discovery all consume upstream\n * quota without producing a session event.\n * - DSH's cumulative `tokenUsage` projection has no model dimension and folds\n * by `(turn, step)`, so a mid-session model switch cannot be attributed.\n *\n * Counter semantics deliberately differ from DSH's: DSH replaces a step's\n * usage (a retried attempt counts once, for conversation cost), while this\n * ledger accumulates per attempt (every retry really did consume quota, which\n * is what an account-level view must show).\n *\n * Storage shape is cumulative with a rolling per-day window:\n * - `totals` never expires — it answers \"all time\".\n * - `days` keeps `DAY_WINDOW` buckets — it answers today / 7d / 30d.\n * A day rolling out of `days` therefore loses only granularity; its counts\n * already live in `totals`.\n *\n * Writes are throttled and merged, never read-modify-write per request:\n * callers accumulate in memory (zero I/O on the hot path) and `flush()` takes\n * the file lock, re-reads the fresh on-disk state, adds this process's delta,\n * and atomically replaces the file. A plain overwrite would lose the counts\n * another process (the Desktop app, a web-profile server, a CLI invocation)\n * wrote in the meantime.\n *\n * A ledger that cannot be written must fail soft and stay bounded: the pending\n * backlog is capped, immediate (count-based) flushing is suspended while writes\n * fail, and the first error of each failure run is reported through\n * `onFlushError`. Statistics are diagnostics — they never break a generation,\n * and they never grow without limit.\n */\n\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport lockfile from 'proper-lockfile'\nimport { resolveDshHome } from './store/keyring.ts'\nimport { zeroCounters } from './usage-types.ts'\nimport type { TokenBuckets, UsageCounters, UsageSource } from './usage-types.ts'\n\nexport type { TokenBuckets, UsageCounters, UsageSource } from './usage-types.ts'\n\nexport const STATS_VERSION = 2\n\n/** Days of per-day buckets retained. Longer spans read `totals`. */\nexport const DAY_WINDOW = 30\n\n/** Per-account ledger. */\nexport interface AccountUsage {\n totals: UsageCounters\n /** Per-model totals for this account, keyed by provider model id. */\n models: Record<string, UsageCounters>\n /** Request counts by call site. */\n sources: Record<UsageSource, number>\n /** Last time this account recorded anything (Unix ms). */\n lastUsedAt: number\n}\n\n/**\n * One day's slice, broken down the same three ways the Usage tab reads it.\n *\n * Version 1 stored only the flat counters here, which was enough for the KPI\n * strip but not for the breakdown tables: those had to read the ALL-TIME\n * `accounts`/`models` maps, so selecting \"Today\" changed the headline figures\n * while the tables below still showed every request ever recorded — 30 requests\n * above a 164-request row on the same screen. Keeping the per-day breakdown is\n * what lets the whole page follow one range.\n *\n * `totals` stays the sum of the day's records; `models`/`accounts` are the same\n * counters partitioned by dimension. Every record contributes to `totals` and to\n * whichever dimensions it names.\n */\nexport interface DayBucket {\n totals: UsageCounters\n /** Per-model counters within this day. */\n models: Record<string, UsageCounters>\n /** Per-account counters within this day. */\n accounts: Record<string, UsageCounters>\n}\n\n/** The persisted document. */\nexport interface StatsDocument {\n version: number\n /** When this ledger began collecting (Unix ms). */\n since: number\n /** Grand totals across every account. */\n totals: UsageCounters\n /** Per-account ledgers, keyed by account email (falling back to id). */\n accounts: Record<string, AccountUsage>\n /** Rolling per-day buckets, `YYYY-MM-DD` -> that day's slice. */\n days: Record<string, DayBucket>\n}\n\n/** One recorded request, as the call sites observe it. */\nexport interface UsageRecord {\n /** Account key (email preferred); omitted when no account was resolved. */\n account?: string\n /** Provider model id. */\n model?: string\n source: UsageSource\n /** Token usage; absent for calls that failed before producing usage. */\n usage?: TokenBuckets\n ok: boolean\n rateLimited?: boolean\n rotated?: boolean\n latencyMs?: number\n ttftMs?: number\n /**\n * A pool-level event (a rotation) rather than a request.\n *\n * Rotations are decided inside the session manager, after the adapter has\n * already recorded the failing request. Emitting them as ordinary records\n * would count that request twice, so a pool event adds only to the pool\n * counters and leaves the request counters alone.\n */\n poolEvent?: boolean\n}\n\nfunction zeroAccount(now: number): AccountUsage {\n return {\n totals: zeroCounters(),\n models: {},\n sources: { chat: 0, cli: 0, verify: 0, test: 0 },\n lastUsedAt: now,\n }\n}\n\nfunction emptyDocument(now: number): StatsDocument {\n return { version: STATS_VERSION, since: now, totals: zeroCounters(), accounts: {}, days: {} }\n}\n\n/** Local-time day key. Local (not UTC) so \"today\" matches the user's clock. */\nfunction dayKey(time: number): string {\n const d = new Date(time)\n const month = String(d.getMonth() + 1).padStart(2, '0')\n const day = String(d.getDate()).padStart(2, '0')\n return `${d.getFullYear()}-${month}-${day}`\n}\n\n/** Add `add` into `into`, in place. */\nfunction addInto(into: UsageCounters, add: UsageCounters): void {\n into.input += add.input\n into.output += add.output\n into.cacheRead += add.cacheRead\n into.cacheWrite += add.cacheWrite\n into.requests += add.requests\n into.succeeded += add.succeeded\n into.failed += add.failed\n into.rateLimited += add.rateLimited\n into.rotations += add.rotations\n into.latencyMs += add.latencyMs\n into.latencyN += add.latencyN\n into.ttftMs += add.ttftMs\n into.ttftN += add.ttftN\n}\n\n/** Coerce arbitrary parsed JSON into a valid counter block (tolerates hand edits). */\nfunction sanitizeCounters(raw: unknown): UsageCounters {\n const out = zeroCounters()\n if (typeof raw !== 'object' || raw === null) return out\n for (const key of Object.keys(out) as Array<keyof UsageCounters>) {\n const value = (raw as Record<string, unknown>)[key]\n if (typeof value === 'number' && Number.isFinite(value) && value >= 0) out[key] = value\n }\n return out\n}\n\nfunction sanitizeAccount(raw: unknown, now: number): AccountUsage {\n const out = zeroAccount(now)\n if (typeof raw !== 'object' || raw === null) return out\n const record = raw as Record<string, unknown>\n out.totals = sanitizeCounters(record.totals)\n if (typeof record.lastUsedAt === 'number' && Number.isFinite(record.lastUsedAt)) {\n out.lastUsedAt = record.lastUsedAt\n }\n if (typeof record.models === 'object' && record.models !== null) {\n for (const [id, value] of Object.entries(record.models as Record<string, unknown>)) {\n out.models[id] = sanitizeCounters(value)\n }\n }\n if (typeof record.sources === 'object' && record.sources !== null) {\n for (const key of ['chat', 'cli', 'verify', 'test'] as const) {\n const value = (record.sources as Record<string, unknown>)[key]\n if (typeof value === 'number' && Number.isFinite(value) && value >= 0) out.sources[key] = value\n }\n }\n return out\n}\n\n/** Parse a persisted document defensively; a corrupt file degrades to empty. */\nexport function parseStatsDocument(text: string, now: number): StatsDocument {\n let raw: unknown\n try {\n raw = JSON.parse(text)\n } catch {\n return emptyDocument(now)\n }\n if (typeof raw !== 'object' || raw === null) return emptyDocument(now)\n const record = raw as Record<string, unknown>\n const doc = emptyDocument(now)\n if (typeof record.since === 'number' && Number.isFinite(record.since)) doc.since = record.since\n doc.totals = sanitizeCounters(record.totals)\n if (typeof record.accounts === 'object' && record.accounts !== null) {\n for (const [key, value] of Object.entries(record.accounts as Record<string, unknown>)) {\n doc.accounts[key] = sanitizeAccount(value, now)\n }\n }\n if (typeof record.days === 'object' && record.days !== null) {\n for (const [key, value] of Object.entries(record.days as Record<string, unknown>)) {\n // Only well-formed day keys survive, so a stray key can never grow the file.\n if (/^\\d{4}-\\d{2}-\\d{2}$/.test(key)) doc.days[key] = sanitizeDayBucket(value)\n }\n }\n return doc\n}\n\n/**\n * Coerce one day's stored value into a `DayBucket`.\n *\n * Accepts the version-1 shape too: that stored a bare counter block, so a\n * document written before the upgrade keeps its day counts as `totals` with no\n * breakdown. Losing the breakdown for those days is the correct trade — the\n * alternative (dropping the days) would erase real history, and recomputing a\n * dimension split that was never recorded is impossible. The all-time\n * `accounts`/`models` maps are unaffected, so only range-scoped breakdown tables\n * are thinner for pre-upgrade days.\n */\nfunction sanitizeDayBucket(raw: unknown): DayBucket {\n const out: DayBucket = { totals: zeroCounters(), models: {}, accounts: {} }\n if (typeof raw !== 'object' || raw === null) return out\n const record = raw as Record<string, unknown>\n if (typeof record.totals === 'object' && record.totals !== null) {\n out.totals = sanitizeCounters(record.totals)\n if (typeof record.models === 'object' && record.models !== null) {\n for (const [id, value] of Object.entries(record.models as Record<string, unknown>)) {\n out.models[id] = sanitizeCounters(value)\n }\n }\n if (typeof record.accounts === 'object' && record.accounts !== null) {\n for (const [key, value] of Object.entries(record.accounts as Record<string, unknown>)) {\n out.accounts[key] = sanitizeCounters(value)\n }\n }\n return out\n }\n // Version-1 day: a bare counter block.\n out.totals = sanitizeCounters(raw)\n return out\n}\n\n/** Drop day buckets older than the window; their counts already live in `totals`. */\nfunction pruneDays(doc: StatsDocument, now: number): void {\n const cutoff = dayKey(now - DAY_WINDOW * 86_400_000)\n for (const key of Object.keys(doc.days)) {\n if (key < cutoff) delete doc.days[key]\n }\n}\n\n/**\n * Sum the day buckets covering the last `days` days, inclusive of today.\n *\n * `days: 1` is \"today\" and `days: DAY_WINDOW` is the full retained window. The\n * window is deliberately read from the per-day buckets rather than `totals`:\n * `totals` is all-time and grows past every window.\n * @param doc - the ledger snapshot.\n * @param days - window length in days, at least 1.\n * @param now - current time (Unix ms).\n * @returns summed counters over the window.\n */\nexport function foldWindow(doc: StatsDocument, days: number, now: number): UsageCounters {\n const out = zeroCounters()\n const span = Math.max(1, Math.floor(days))\n const cutoff = dayKey(now - (span - 1) * 86_400_000)\n for (const [key, bucket] of Object.entries(doc.days)) {\n // String compare is a valid date order for `YYYY-MM-DD`.\n if (key >= cutoff) addInto(out, bucket.totals)\n }\n return out\n}\n\n/** One range's breakdown: totals plus the same counters split by model/account. */\nexport interface WindowBreakdown {\n totals: UsageCounters\n models: Array<{ model: string, counters: UsageCounters }>\n accounts: Array<{ account: string, counters: UsageCounters }>\n}\n\n/**\n * Fold the day buckets for a window into the three views the Usage tab renders.\n *\n * The breakdown tables read THIS rather than the all-time `accounts`/`models`\n * maps, so one range selection governs the whole page. Rows are sorted by\n * request count descending, matching the all-time view; the caller re-sorts if\n * it wants another order (the UI shows them as-is).\n *\n * @param doc - the ledger snapshot.\n * @param days - window length in days, at least 1.\n * @param now - current time (Unix ms).\n * @returns totals and their per-model / per-account partitions.\n */\nexport function foldWindowBreakdown(doc: StatsDocument, days: number, now: number): WindowBreakdown {\n const span = Math.max(1, Math.floor(days))\n const cutoff = dayKey(now - (span - 1) * 86_400_000)\n const totals = zeroCounters()\n const models = new Map<string, UsageCounters>()\n const accounts = new Map<string, UsageCounters>()\n for (const [key, bucket] of Object.entries(doc.days)) {\n if (key < cutoff) continue\n addInto(totals, bucket.totals)\n for (const [model, counters] of Object.entries(bucket.models)) {\n const current = models.get(model)\n if (current === undefined) models.set(model, { ...counters })\n else addInto(current, counters)\n }\n for (const [account, counters] of Object.entries(bucket.accounts)) {\n const current = accounts.get(account)\n if (current === undefined) accounts.set(account, { ...counters })\n else addInto(current, counters)\n }\n }\n const byRequests = (a: { counters: UsageCounters }, b: { counters: UsageCounters }): number =>\n b.counters.requests - a.counters.requests\n return {\n totals,\n models: [...models.entries()].map(([model, counters]) => ({ model, counters })).sort(byRequests),\n accounts: [...accounts.entries()].map(([account, counters]) => ({ account, counters })).sort(byRequests),\n }\n}\n\n/** Merge one record into a document, in place. Shared by the live store and tests. */\nexport function applyRecord(doc: StatsDocument, record: UsageRecord, now: number): void {\n const delta = zeroCounters()\n // A pool event carries no request of its own; it only moves pool counters.\n if (record.poolEvent !== true) {\n delta.requests = 1\n if (record.ok) delta.succeeded = 1\n else delta.failed = 1\n if (record.rateLimited === true) delta.rateLimited = 1\n }\n if (record.rotated === true) delta.rotations = 1\n if (record.usage !== undefined) {\n delta.input = record.usage.input\n delta.output = record.usage.output\n delta.cacheRead = record.usage.cacheRead\n delta.cacheWrite = record.usage.cacheWrite\n }\n if (typeof record.latencyMs === 'number' && Number.isFinite(record.latencyMs) && record.latencyMs >= 0) {\n delta.latencyMs = record.latencyMs\n delta.latencyN = 1\n }\n if (typeof record.ttftMs === 'number' && Number.isFinite(record.ttftMs) && record.ttftMs >= 0) {\n delta.ttftMs = record.ttftMs\n delta.ttftN = 1\n }\n\n addInto(doc.totals, delta)\n\n if (record.account !== undefined && record.account !== '') {\n const account = doc.accounts[record.account] ?? zeroAccount(now)\n addInto(account.totals, delta)\n // A pool event is not a call site occurrence, so it counts no source.\n if (record.poolEvent !== true) account.sources[record.source] += 1\n account.lastUsedAt = now\n if (record.model !== undefined && record.model !== '') {\n const model = account.models[record.model] ?? zeroCounters()\n addInto(model, delta)\n // A model with no traffic left is still a real model row; keep it.\n account.models[record.model] = model\n }\n doc.accounts[record.account] = account\n }\n\n const key = dayKey(now)\n const bucket = doc.days[key] ?? { totals: zeroCounters(), models: {}, accounts: {} }\n addInto(bucket.totals, delta)\n // The same delta is partitioned by the dimensions it names, so a range-scoped\n // breakdown table adds up to that range's headline figures exactly.\n if (record.model !== undefined && record.model !== '') {\n const model = bucket.models[record.model] ?? zeroCounters()\n addInto(model, delta)\n bucket.models[record.model] = model\n }\n if (record.account !== undefined && record.account !== '') {\n const account = bucket.accounts[record.account] ?? zeroCounters()\n addInto(account, delta)\n bucket.accounts[record.account] = account\n }\n doc.days[key] = bucket\n\n pruneDays(doc, now)\n}\n\n/**\n * Cross-process file lock seam (proper-lockfile behind a small interface for tests).\n *\n * The sync form is deliberate — `flush()` is called from `process.on('exit')`,\n * where async work cannot settle.\n *\n * The sync API accepts NO retry configuration: `proper-lockfile` throws\n * \"Cannot use retries with the sync api\" when `retries` is passed, which fails\n * every flush and silently drops every count. Contention therefore surfaces as\n * a thrown error instead of a retry, and `flush()` puts the batch back for the\n * next timer tick — a statistics flush that loses a race costs nothing, so\n * waiting is never worth blocking the event loop for.\n */\nexport interface StatsLock {\n withLock<T>(file: string, fn: () => T): T\n}\n\nexport const properStatsLock: StatsLock = {\n withLock<T>(file: string, fn: () => T): T {\n // `realpath: false` because the target may be absent on first write; the\n // caller pre-creates it, and lockSync otherwise throws ENOENT resolving it.\n //\n // `stale` is deliberately short (8s, the same order as `flushIntervalMs`).\n // proper-lockfile only removes its lock directory through signal-exit, so a\n // `SIGKILL`ed process leaves one behind; with the former 30s window every\n // flush failed for 30 seconds afterwards — a long blackout for a diagnostic\n // ledger whose next flush can simply try again. `update` must stay below\n // `stale` or a lock holder self-expires while it is still working.\n const release = lockfile.lockSync(file, { stale: 8_000, update: 4_000, realpath: false })\n try {\n return fn()\n } finally {\n release()\n }\n },\n}\n\nexport const noopStatsLock: StatsLock = { withLock: (_file, fn) => fn() }\n\nexport interface UsageStatsStoreOptions {\n /** Defaults to `$DSH_HOME/agy-stats.json`. */\n file?: string\n lock?: StatsLock\n /** Injectable clock, for tests. */\n now?: () => number\n /**\n * Flush after this many pending records. `0` disables count-based flushing.\n * Defaults to 50 (DSH's own projection cache writes at 200 records / 5s).\n */\n flushEvery?: number\n /** Flush after this many ms with pending records. Defaults to 5000. */\n flushIntervalMs?: number\n /**\n * Hard ceiling on un-flushed records. Defaults to 500 (ten batches).\n *\n * A persistent write failure (read-only `$DSH_HOME`, ENOSPC) makes every\n * flush fail, so the pending list would otherwise grow without bound in a\n * long-lived process. Past the ceiling the OLDEST records are dropped: a\n * bounded diagnostic ledger beats an unbounded one, and the oldest counts are\n * the least useful ones.\n */\n maxPending?: number\n /**\n * Called once per run of consecutive flush failures, with the first error.\n *\n * Without this a ledger that cannot be written fails silently: counts simply\n * stop appearing, with nothing to correlate. Once per run (not per attempt)\n * because the timer retries every `flushIntervalMs` and a log per retry would\n * be noise.\n */\n onFlushError?: (error: unknown) => void\n}\n\n/**\n * Accumulating usage ledger. `record()` is the hot path and performs no I/O:\n * it folds the record into an in-memory delta. `flush()` merges that delta into\n * the persisted document under the file lock.\n */\nexport class UsageStats {\n private readonly file: string\n private readonly lock: StatsLock\n private readonly now: () => number\n private readonly flushEvery: number\n private readonly flushIntervalMs: number\n private readonly maxPending: number\n private readonly onFlushError: ((error: unknown) => void) | undefined\n\n /** This process's un-flushed records. */\n private pending: UsageRecord[] = []\n private timer: ReturnType<typeof setTimeout> | undefined\n private lastFlushError: unknown\n /**\n * Whether the current run of consecutive failures has already been reported.\n * Count-based flushing is also suspended while set: see `record()`.\n */\n private flushFailedSince: number | undefined\n\n constructor(options: UsageStatsStoreOptions = {}) {\n this.file = options.file ?? join(resolveDshHome(), 'agy-stats.json')\n this.lock = options.lock ?? properStatsLock\n this.now = options.now ?? (() => Date.now())\n this.flushEvery = options.flushEvery ?? 50\n this.flushIntervalMs = options.flushIntervalMs ?? 5_000\n this.maxPending = options.maxPending ?? 500\n this.onFlushError = options.onFlushError\n // Deliberately does NOT create the file: a read-only command (`status`)\n // builds a ledger and must not leave state behind. `flush()` pre-creates it\n // before locking, which is the first point a write actually needs it.\n }\n\n /** Path of the backing file. */\n get path(): string {\n return this.file\n }\n\n /**\n * Pre-create the ledger so `proper-lockfile` can lock it. The lock throws\n * ENOENT on a nonexistent target, so the empty document must exist (0600,\n * tmp+rename) before the first flush.\n */\n private ensureFile(now: number): void {\n if (existsSync(this.file)) return\n mkdirSync(dirname(this.file), { recursive: true })\n const tmp = `${this.file}.tmp-init`\n writeFileSync(tmp, JSON.stringify(emptyDocument(now), null, 2) + '\\n', { mode: 0o600 })\n renameSync(tmp, this.file)\n }\n\n private readFile(): StatsDocument {\n const now = this.now()\n try {\n return parseStatsDocument(readFileSync(this.file, 'utf8'), now)\n } catch {\n return emptyDocument(now)\n }\n }\n\n /** Atomic replace (tmp + rename), owner-only, so a reader never sees a partial file. */\n private writeFile(doc: StatsDocument): void {\n mkdirSync(dirname(this.file), { recursive: true })\n const tmp = `${this.file}.tmp`\n writeFileSync(tmp, JSON.stringify(doc, null, 2) + '\\n', { mode: 0o600 })\n renameSync(tmp, this.file)\n }\n\n /** Record one request. Hot path: no I/O. */\n record(record: UsageRecord): void {\n this.pending.push(record)\n // Cap the backlog: unbounded growth is the one failure mode a diagnostic\n // ledger must not have (see `maxPending`).\n if (this.maxPending > 0 && this.pending.length > this.maxPending) {\n this.pending.splice(0, this.pending.length - this.maxPending)\n }\n // Count-based flushing is a throughput optimisation, so it is suspended\n // while writes are failing. Without this a persistent failure made EVERY\n // `record()` attempt a full synchronous flush (stat + lock + read + write),\n // turning the generation hot path into I/O — the exact opposite of this\n // class's contract. The timer below keeps retrying instead.\n if (\n this.flushFailedSince === undefined\n && this.flushEvery > 0\n && this.pending.length >= this.flushEvery\n ) {\n this.flush()\n return\n }\n this.armTimer()\n }\n\n private armTimer(): void {\n if (this.timer !== undefined || this.flushIntervalMs <= 0) return\n this.timer = setTimeout(() => {\n this.timer = undefined\n this.flush()\n }, this.flushIntervalMs)\n // Never hold the process open for a statistics flush.\n this.timer.unref?.()\n }\n\n /**\n * Merge pending records into the persisted document.\n *\n * Under the lock, the fresh on-disk state is re-read and this process's\n * records are replayed onto it. That is what keeps concurrently-running\n * writers (Desktop, a web-profile server, a CLI invocation) from clobbering\n * each other: a plain overwrite of the in-memory document would drop every\n * count another process added since this one loaded the file.\n */\n flush(): void {\n if (this.timer !== undefined) {\n clearTimeout(this.timer)\n this.timer = undefined\n }\n const batch = this.pending\n if (batch.length === 0) return\n this.pending = []\n const now = this.now()\n try {\n this.ensureFile(now)\n this.lock.withLock(this.file, () => {\n // Re-read under the lock and replay this batch onto the fresh state:\n // that is what makes concurrent writers additive instead of last-write-wins.\n const doc = this.readFile()\n for (const record of batch) applyRecord(doc, record, now)\n this.writeFile(doc)\n })\n this.lastFlushError = undefined\n this.flushFailedSince = undefined\n } catch (error) {\n // A failed flush must not lose the counts or break a generation: put the\n // batch back so the next flush retries it, and surface the reason once.\n this.pending = [...batch, ...this.pending]\n if (this.maxPending > 0 && this.pending.length > this.maxPending) {\n this.pending.splice(0, this.pending.length - this.maxPending)\n }\n this.lastFlushError = error\n if (this.flushFailedSince === undefined) {\n this.flushFailedSince = now\n try {\n this.onFlushError?.(error)\n } catch {\n // The reporter must never break a flush path.\n }\n }\n // Only the timer retries now; `record()` stops triggering immediate\n // flushes until one succeeds.\n this.armTimer()\n }\n }\n\n /** Flush if anything is pending; safe to call on turn end and process exit. */\n flushSync(): void {\n this.flush()\n }\n\n /**\n * A snapshot for reading (the Usage tab).\n *\n * Reads the file rather than an in-memory copy: the writer is not always this\n * process. A CLI `login`/`verify` runs in its own process, and the browser may\n * be served by a different plugin instance than the one that generated, so an\n * in-memory cache would show a stale or empty ledger. This process's own\n * un-flushed records are layered on top so the UI never lags behind its own\n * activity.\n *\n * Called once per UI open/refresh, never on the generation hot path, so the\n * read is cheap enough.\n */\n snapshot(): StatsDocument {\n const now = this.now()\n const doc = this.readFile()\n if (this.pending.length > 0) {\n for (const record of this.pending) applyRecord(doc, record, now)\n }\n return doc\n }\n\n /** The most recent flush failure, if any (diagnostics). */\n get flushError(): unknown {\n return this.lastFlushError\n }\n\n /** Number of records awaiting flush. */\n get pendingCount(): number {\n return this.pending.length\n }\n\n /** Whether the ledger is currently failing to persist (a run of failures). */\n get flushFailing(): boolean {\n return this.flushFailedSince !== undefined\n }\n}\n","/**\n * Shared runtime construction for the in-harness plugin entries: master-key\n * codec resolution (credentials seam first, credentials document fallback),\n * account store, session manager, and adapter. Used by the main plugin\n * (adapter registration) and the web plugin (route registration) so both\n * entries operate on the same store.\n */\n\nimport { randomBytes } from 'node:crypto'\nimport type { Context } from '@deepseek-ai/cordis'\nimport { AgyAdapter } from './adapter/adapter.ts'\nimport type { AgyAttachmentStore } from './adapter/adapter.ts'\nimport { AGY_PROVIDER } from './adapter/models.ts'\nimport { ModelVisibility } from './model-visibility.ts'\nimport { ThinkingBudgetStore } from './thinking-budget.ts'\nimport { UsageStats } from './stats.ts'\nimport { probeFetch, proxiedFetch } from './proxy.ts'\nimport { pickProbeProxyUrl } from './runtime/rotation.ts'\nimport { resolveAntigravityVersion } from './runtime/version.ts'\nimport type { FetchLike } from './runtime/version.ts'\nimport { AgySessionManager } from './session.ts'\nimport { JsonAccountStore } from './store/accounts.ts'\nimport type { AccountStore } from './store/accounts.ts'\nimport {\n MASTER_KEY_REF,\n createAesGcmCodec,\n deriveKey,\n loadMasterKey,\n persistMasterKey,\n resolveDshHome,\n resolveMasterKeyCodec,\n} from './store/keyring.ts'\nimport type { SecretCodec } from './store/keyring.ts'\n\nexport interface CredentialsSeam {\n resolve(ref: string): Promise<{ value: string } | undefined>\n set(ref: string, value: string): Promise<void>\n}\n\nfunction codecFrom(masterKey: string): SecretCodec {\n return createAesGcmCodec(deriveKey(masterKey))\n}\n\n/** Resolve or create the master key, preferring the credentials seam when available. */\nexport async function resolveCodec(ctx: Context): Promise<{ codec: SecretCodec; created: boolean }> {\n const dshHome = resolveDshHome()\n const credentials = ctx.get('credentials') as CredentialsSeam | undefined\n\n if (credentials) {\n const resolved = await credentials.resolve(MASTER_KEY_REF)\n if (resolved) return { codec: codecFrom(resolved.value), created: false }\n const fileKey = loadMasterKey(dshHome)\n if (fileKey) return { codec: codecFrom(fileKey), created: false }\n const fresh = randomBytes(32).toString('hex')\n try {\n await credentials.set(MASTER_KEY_REF, fresh)\n return { codec: codecFrom(fresh), created: true }\n } catch {\n // Read-only shadowing: persist to the credentials document directly.\n persistMasterKey(dshHome, fresh)\n return { codec: codecFrom(fresh), created: true }\n }\n }\n\n return resolveMasterKeyCodec(dshHome)\n}\n\n/**\n * Fire-and-forget version warm-up so fingerprint generation inside the\n * rate-limit path never waits on a cold feed.\n *\n * Routed through `pickProbeProxyUrl`: the release feeds are not account-scoped,\n * but the host IP is exactly what a per-account-proxy user asked to hide, and a\n * bare `proxiedFetch` sends this boot-time request directly. Never throws — an\n * unreadable store or a dead feed must not be the reason plugin boot fails.\n */\nasync function warmVersionCache(store: AccountStore): Promise<void> {\n let fetchImpl: FetchLike = proxiedFetch\n try {\n const storage = await store.load()\n const proxyUrl = pickProbeProxyUrl(storage.accounts, storage.activeIndex)\n fetchImpl = probeFetch(proxyUrl)\n } catch {\n // Fall through to the env/direct route.\n }\n await resolveAntigravityVersion(fetchImpl).catch(() => {})\n}\n\n/** Build the store, session manager, and adapter for one plugin entry. */\nexport async function createAgyRuntime(ctx: Context): Promise<{\n store: AccountStore\n sessions: AgySessionManager\n adapter: AgyAdapter\n stats: UsageStats\n modelVisibility: ModelVisibility\n thinkingBudget: ThinkingBudgetStore\n}> {\n const { codec } = await resolveCodec(ctx)\n const dshHome = resolveDshHome()\n const store = new JsonAccountStore({ file: `${dshHome}/agy-accounts.json`, codec })\n void warmVersionCache(store)\n // The ledger reports the first failure of a run, once. Without it a ledger\n // that cannot be written (read-only $DSH_HOME, ENOSPC) simply stops counting\n // with nothing anywhere to explain why — the failure mode this whole\n // soft-fail design otherwise hides.\n const stats = new UsageStats({\n onFlushError: (error) => {\n ctx.logger.warn(\n `[dsh-agy] usage ledger could not be written (${stats.path}): `\n + `${error instanceof Error ? error.message : String(error)} — counts are buffered and will retry`,\n )\n },\n })\n const modelVisibility = new ModelVisibility()\n const thinkingBudget = new ThinkingBudgetStore()\n const sessions = new AgySessionManager({\n store,\n recordUsage: (record) => { stats.record(record) },\n })\n // Optional background health probe (DSH_AGY_HEALTH_INTERVAL_MS), off by default.\n const healthIntervalMs = Number(process.env.DSH_AGY_HEALTH_INTERVAL_MS ?? 0)\n if (Number.isFinite(healthIntervalMs) && healthIntervalMs > 0) {\n sessions.startHealthProbe(healthIntervalMs)\n }\n const adapter = new AgyAdapter({\n getSession: (model, conversationKey) => sessions.getSession(model, undefined, conversationKey),\n reportFailure: (kind, session, info) => sessions.reportFailure(kind, session, info),\n markSuccess: (session) => sessions.markSuccess(session),\n resolveAttachments: () => ctx.get('attachments') as AgyAttachmentStore | undefined,\n noteRequestStarted: (account) => sessions.noteRequestStarted(account),\n noteRequestSettled: (account) => sessions.noteRequestSettled(account),\n modelVisibility,\n thinkingBudgetFor: (level) => thinkingBudget.budgetFor(level),\n claudeBudgetFor: () => thinkingBudget.claudeBudget(),\n tieredBudgetFor: () => thinkingBudget.tieredBudget(),\n recordUsage: (record) => { stats.record({ ...record, source: 'chat' }) },\n })\n // Persist the ledger on normal termination. `exit` covers both a graceful\n // shutdown and the CLI's explicit `process.exit` calls.\n //\n // Deliberately NO signal handlers: installing a SIGINT/SIGTERM listener\n // replaces Node's default terminate behavior, so it must flush and then\n // re-raise or Ctrl-C stops working — a real bug for the sake of at most one\n // throttle window (5s) of counts on an interrupted process. The timer in\n // UsageStats already bounds that loss, and statistics are diagnostics.\n process.once('exit', () => { stats.flushSync() })\n return { store, sessions, adapter, stats, modelVisibility, thinkingBudget }\n}\n\nexport { AGY_PROVIDER }\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"translate-BQu8HLvB.mjs","names":[],"sources":["../src/runtime/signature-cache.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 * 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, Message } from '@deepseek-ai/dsh-llm'\nimport { catalogModel } from './catalog.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 Message[],\n modelOrOptions?: string | ResolveMultimodalOptions,\n extraOptions?: ResolveMultimodalOptions,\n): Promise<Map<string, AgyResolvedMultimodalFile[]>> {\n let messages: readonly Message[]\n let model: string\n let customOptions: ResolveMultimodalOptions | undefined\n\n if ('messages' in optionsOrMessages) {\n messages = optionsOrMessages.messages ?? []\n model = optionsOrMessages.model ?? ''\n customOptions = typeof modelOrOptions === 'object' ? modelOrOptions : extraOptions\n } else {\n messages = optionsOrMessages\n if (typeof modelOrOptions === 'string') {\n model = modelOrOptions\n customOptions = extraOptions\n } else {\n model = ''\n customOptions = modelOrOptions\n }\n }\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 { ContentBlock, GenerateOptions, Message, 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'\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 * string (booleans/numbers are rejected). Values are normalized to the\n * 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 strings; filter the rest and omit an\n // 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')\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-result blocks can name their function. */\nfunction buildToolNameIndex(messages: readonly Message[]): 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\nfunction blockToParts(\n block: ContentBlock,\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 const name = toolNames.get(block.toolCallId) ?? block.toolCallId\n const text = block.content\n .filter((b): b is Extract<ContentBlock, { type: 'text' }> => b.type === 'text')\n .map((b) => b.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: block.toolCallId,\n name,\n response: { result: text, is_error: block.isError === true },\n },\n }]\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: Message,\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 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 * 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 toolNames = buildToolNameIndex(options.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 = options.messages\n .map((message, index) => messageToContent(message, toolNames, images, multimodalFiles, index, claude))\n .filter((c): c is AgyContent => c !== null)\n if (claude) {\n contents = stripTrailingModelTurn(contents)\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 ...(options.system ? { systemInstruction: { parts: [{ text: options.system }] } } : {}),\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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"}
|