dsh-agy 0.4.3 → 0.4.5
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/lib/{accounts-CB4blXpP.mjs → accounts-BOuz6_Sh.mjs} +3 -3
- package/lib/{accounts-CB4blXpP.mjs.map → accounts-BOuz6_Sh.mjs.map} +1 -1
- package/lib/cli/index.mjs +1 -1
- package/lib/index.mjs +2 -2
- package/lib/{parse-38V1zT1B.mjs → parse-D3InNyUx.mjs} +34 -12
- package/lib/parse-D3InNyUx.mjs.map +1 -0
- package/lib/{plugin-common-D2NWMsX6.mjs → plugin-common-Dwa1K13O.mjs} +164 -68
- package/lib/plugin-common-Dwa1K13O.mjs.map +1 -0
- package/lib/{translate-B9ZOauqw.mjs → translate-Cq6PGO4b.mjs} +101 -5
- package/lib/translate-Cq6PGO4b.mjs.map +1 -0
- package/lib/web/plugin.mjs +2 -2
- package/package.json +2 -1
- package/lib/parse-38V1zT1B.mjs.map +0 -1
- package/lib/plugin-common-D2NWMsX6.mjs.map +0 -1
- package/lib/translate-B9ZOauqw.mjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-common-Dwa1K13O.mjs","names":[],"sources":["../src/adapter/adapter.ts","../src/model-visibility.ts","../src/thinking-types.ts","../src/thinking-budget.ts","../src/usage-types.ts","../src/stats.ts","../src/recent-store.ts","../src/plugin-common.ts"],"sourcesContent":["/**\n * AgyAdapter: the DSH seam. A thin orchestrator over the deep modules —\n * account session resolution (shell-provided), request translation, SSE\n * parsing, failure classification, and rotation reporting. All wire details\n * live in translate.ts / parse.ts / models.ts.\n */\n\nimport {\n LlmAdapter,\n LlmError,\n ProviderRequestId,\n QUOTA_EXCEEDED_CODE,\n} from '@deepseek-ai/dsh-llm'\nimport type {\n GenerateOptions,\n LlmModelInfo,\n LlmProviderInfo,\n LlmResolvedModelInfo,\n StreamChunk,\n ToolSchema,\n} from '@deepseek-ai/dsh-llm'\n\nimport { AgyAuthError, AgyPoolBlockedError } from '../types.ts'\nimport type { AgyAccountSession, FailureKind, ManagedAccount, OAuthAuthDetails } from '../types.ts'\nimport type { RateLimitCategory } from '../runtime/classify.ts'\nimport { fetchAgyFirstOk } from '../oauth/constants.ts'\nimport {\n classifyFetchError,\n classifyHttpError,\n describeFetchError,\n isSessionAccumulationOverflow,\n} from '../runtime/classify.ts'\nimport { accountFetch } from '../proxy.ts'\nimport {\n bumpSessionGeneration,\n currentSessionGeneration,\n deriveAntigravitySessionId,\n generateAntigravityRequestId,\n} from '../runtime/identity.ts'\nimport { setThoughtSignature } from '../runtime/signature-cache.ts'\nimport { toAgyRequestBody } from './translate.ts'\nimport type { AgyResolvedImage } from './translate.ts'\nimport { resolveMultimodalFiles } from './multimodal.ts'\nimport { parseAgySse, UnmappedFinishReasonError } from './parse.ts'\nimport { AGY_PROVIDER, catalogModelList, listAgyModels, resolveAgyModel } from './models.ts'\n\nexport type { AgyAccountSession }\n\n/**\n * Structural view of the harness attachment service (ctx.attachments).\n * Deliberately not an import of @deepseek-ai/dsh-attachment: the CLI bundle\n * must stay free of harness runtime dependencies, and the real store\n * satisfies this shape.\n */\nexport interface AgyAttachmentStore {\n readImage(ref: {\n attachmentId: string\n mediaType: string\n }): Promise<{ ref: { mediaType: string }; data: Uint8Array }>\n}\n\n/** Collect image refs from user-message content only (spec scope: user images; tool-result nesting out of scope). */\nfunction collectImageRefs(options: GenerateOptions): Array<{ attachmentId: string; mediaType: string }> {\n const refs: Array<{ attachmentId: string; mediaType: string }> = []\n for (const message of options.messages) {\n if (message.role !== 'user') continue\n for (const block of message.content) {\n if (block.type === 'image') refs.push(block.attachment)\n }\n }\n return refs\n}\n\nexport interface AgyAdapterOptions {\n /**\n * Resolve the active account for a request (model-aware: family-scoped quota\n * ranking). `conversationKey` scopes account affinity, so concurrent\n * conversations hold independent pins.\n */\n getSession(model?: string, conversationKey?: string): Promise<AgyAccountSession | undefined>\n /** Report a classified upstream failure so the shell can cool/rotate/revoke. */\n reportFailure(\n kind: FailureKind,\n session: AgyAccountSession,\n info?: {\n retryAfterMs?: number\n status?: number\n rateLimitCategory?: RateLimitCategory\n /** Server-reported absolute reset time; drives precise cooldowns. */\n resetTime?: string\n /** Requested model id; drives family-scoped rate-limit bookkeeping. */\n model?: string\n /** Appeal link from a `verification-required` body; surfaced to the user. */\n verificationUrl?: string\n },\n ): Promise<void>\n /** Report a clean stream completion (resets the failure counter). */\n markSuccess?(session: AgyAccountSession): Promise<void>\n /**\n * Configured token budget for a reasoning level (see `thinking-budget.ts`).\n *\n * Supplied as a resolver rather than a snapshot so an edit in the settings UI\n * applies to the next request without rebuilding the adapter.\n *\n * @param level - the lowercased effort id (`low`/`medium`/`high`).\n * @returns the budget to send, or undefined to let the level stand alone.\n */\n thinkingBudgetFor?(level: string): number | undefined\n /**\n * Configured Claude thinking budget, or undefined when unset.\n *\n * Separate from `thinkingBudgetFor` because the Claude family is id-bound and\n * carries its own validation (floor 1024, and `max_tokens` must exceed it).\n */\n claudeBudgetFor?(): number | undefined\n /** Budget for the tiered slot (the selector's \"Default\" effort). */\n tieredBudgetFor?(): number | undefined\n /** Resolve the harness attachment store; undefined outside the harness (standalone CLI). */\n resolveAttachments?(): AgyAttachmentStore | undefined\n /**\n * In-flight accounting for per-account request fan-out. Takes the account\n * rather than a key so the session manager derives the identity itself —\n * `accountKey` (id-first) and `ledgerAccountKey` (email-first) differ, and\n * mixing them would count against an account nobody selects.\n *\n * `noteRequestSettled` MUST run on every path, including an abandoned stream:\n * the adapter calls it from a `finally`, which async generators run on\n * completion, error, and early consumer termination alike.\n */\n noteRequestStarted?(account: ManagedAccount): void\n noteRequestSettled?(account: ManagedAccount): void\n /**\n * Hidden-model lookup. Optional so an adapter stays constructible without the\n * settings layer (tests, standalone use); absent means nothing is hidden.\n */\n modelVisibility?: { disabledFor(provider: string): ReadonlySet<string> }\n /**\n * Record one request's usage. Optional by design: the CLI must be able to\n * build an adapter without the stats ledger, and a statistics failure must\n * never break a generation.\n */\n recordUsage?(record: {\n account?: string\n model?: string\n usage?: { input: number; output: number; cacheRead: number; cacheWrite: number }\n ok: boolean\n rateLimited?: boolean\n /** Failure classification, when the record failed. */\n reason?: string\n latencyMs?: number\n ttftMs?: number\n }): void\n}\n\nconst UPSTREAM_ERROR_CODE = 'UPSTREAM'\n/** First-class DSH retryable code: the default retry policy honors SERVER (5xx), not UPSTREAM. */\nconst SERVER_ERROR_CODE = 'SERVER'\n\n/** Abort-aware delay helper for transient stream retries. */\nasync function sleepWithSignal(ms: number, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) {\n throw new DOMException('aborted', 'AbortError')\n }\n await new Promise<void>((resolve, reject) => {\n // Both exits must detach the listener: `{ once: true }` covers the abort\n // firing, but a timer that simply wins would otherwise leave `onAbort` on the\n // request-scoped signal for the rest of the call.\n const onAbort = () => {\n clearTimeout(timer)\n reject(new DOMException('aborted', 'AbortError'))\n }\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort)\n resolve()\n }, ms)\n if (!signal) return\n signal.addEventListener('abort', onAbort, { once: true })\n })\n}\n\n/** Stable ledger key for an account: email when present, else the generated id. */\nfunction ledgerAccountKey(session: AgyAccountSession): string | undefined {\n return session.account.email ?? session.account.id\n}\n\n/**\n * Build the impersonation headers for one request.\n *\n * There is NO request-id header. `x-goog-request-id` used to be sent here and is\n * present in neither official binary; the id the backend correlates on is the\n * body's `requestId` field, which this request already carries. An invented\n * header is a shape no official client emits, which is precisely the anomaly the\n * impersonation set exists to avoid.\n *\n * `User-Agent` carries the Antigravity client string and NOTHING else. The\n * harness's `attributionHeaders()` is deliberately not merged in: it returns a\n * lowercase `user-agent` key, so spreading it alongside the camel-case\n * `session.impersonation` produced TWO distinct object properties that `fetch`\n * folded into one comma-joined value —\n *\n * `deepseek-harness/<v> (+url), antigravity/<v> <platform>`\n *\n * — a header no official client can emit, byte-identical for every dsh-agy\n * user, and therefore a stronger fingerprint than the one it was trying to\n * avoid. There is exactly one `User-Agent` field on the wire and the upstream\n * requires it to be the client identity, so this request cannot carry both.\n *\n * This adapter performs its own dispatch (see the `fetchAgyFirstOk` call in\n * `stream()`); nothing downstream re-adds the header.\n */\nexport function buildRequestHeaders(session: AgyAccountSession): Record<string, string> {\n return {\n authorization: `Bearer ${session.auth.access}`,\n 'content-type': 'application/json',\n accept: 'text/event-stream',\n // Exactly the two impersonation HEADERS, named individually rather than\n // spread. `session.impersonation.clientMetadata` is a body message and must\n // not become a header — and it is deliberately not added to this envelope\n // either: the only `metadata` field the official descriptor evidences is on\n // the control-plane calls (`LoadCodeAssistRequest`/`OnboardUserRequest`), so\n // on this generate request the identity carrier is the body's existing\n // `userAgent`/`requestType` pair.\n 'User-Agent': session.impersonation['User-Agent'],\n 'X-Goog-Api-Client': session.impersonation['X-Goog-Api-Client'],\n }\n}\n\n/** How long discovered models stay cached before re-probing upstream (10 minutes). */\nexport const MODEL_LIST_CACHE_TTL_MS = 10 * 60 * 1000\n\nexport class AgyAdapter extends LlmAdapter {\n private readonly options: AgyAdapterOptions\n private cachedModels: { models: readonly LlmModelInfo[]; at: number } | null = null\n private inFlightModels: Promise<readonly LlmModelInfo[]> | null = null\n\n constructor(options: AgyAdapterOptions) {\n super()\n this.options = options\n }\n\n override providerInfo(_provider: string): LlmProviderInfo {\n return { id: AGY_PROVIDER, name: 'Antigravity (agy)' }\n }\n\n /**\n * The catalog as DSH's model selector sees it: discovered models minus the\n * user's hidden set. Filtering here is what makes \"turn a model off\" hide it\n * from the picker — the selector reads this, and DSH itself is untouched.\n */\n override async listModels(_provider: string): Promise<readonly LlmModelInfo[]> {\n const all = await this.listAllModels()\n const hidden = this.options.modelVisibility?.disabledFor(AGY_PROVIDER)\n if (hidden === undefined || hidden.size === 0) return all\n return all.filter((model) => !hidden.has(model.id))\n }\n\n /**\n * The complete catalog, ignoring the user's hidden set.\n *\n * Cached in memory for `MODEL_LIST_CACHE_TTL_MS` with in-flight deduplication.\n * DSH 0.2.0-rc.1 calls `listModels` on every model and effort selection via\n * `requireModel`; without caching, each click paid a multi-second round-trip to\n * Google's `fetchAvailableModels` through the proxy, showing a loading spinner\n * on every interaction.\n *\n * The settings page lists models through this rather than `listModels`: if it\n * read the filtered list, a hidden model would vanish from the page along\n * with the switch that hides it, leaving no way to turn it back on without\n * hand-editing the file.\n */\n async listAllModels(force = false): Promise<readonly LlmModelInfo[]> {\n const now = Date.now()\n if (!force && this.cachedModels && now - this.cachedModels.at < MODEL_LIST_CACHE_TTL_MS) {\n return this.cachedModels.models\n }\n if (this.inFlightModels) return this.inFlightModels\n\n const task = (async () => {\n try {\n const session = await this.options.getSession()\n // Model discovery is account-scoped: route it through the account's proxy\n // (control-plane class, so the standard timeouts apply).\n const routing = { proxyUrl: session?.account.proxy }\n const models = await listAgyModels(session?.auth.access, session?.account.projectId, accountFetch(routing))\n this.cachedModels = { models, at: Date.now() }\n return models\n } catch (error) {\n if (error instanceof AgyPoolBlockedError || error instanceof AgyAuthError) {\n return catalogModelList()\n }\n if (this.cachedModels) return this.cachedModels.models\n throw error\n } finally {\n this.inFlightModels = null\n }\n })()\n\n this.inFlightModels = task\n return task\n }\n\n /**\n * Drop the cached model list.\n *\n * Model discovery is account-scoped (what `fetchAvailableModels` reports and\n * which proxy it rides depend on the account), but the cache is deliberately\n * keyed to NO account: a keyed read would have to call `getSession()` on\n * every hit, and that call can probe quota or refresh a token — the network\n * cost this cache exists to remove. Every event that can switch the account\n * discovery rides (activation, rotation) therefore drops the cache instead.\n */\n invalidateModelCache(): void {\n this.cachedModels = null\n }\n\n override async resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo> {\n return resolveAgyModel(provider, model)\n }\n\n // `prepareCall` is deliberately NOT overridden: the LlmAdapter base class\n // (dsh-llm 0.1.1-rc.2+) already binds the resolved model to the stream, which\n // is exactly what this adapter would do. The base implementation also\n // forwards the cancellation signal, which the previous hand-written copy\n // dropped. Only dynamic adapters — those needing different capabilities per\n // generation — should override it.\n\n /**\n * Pre-resolve every image attachment into base64 bytes before translation.\n * Image input hard-fails with UNSUPPORTED_CONTENT (terminal, never retried)\n * when the store is missing or a read fails — silently dropping images and\n * sending text-only is the exact failure mode this path exists to prevent.\n */\n private async resolveRequestImages(options: GenerateOptions): Promise<Map<string, AgyResolvedImage>> {\n const refs = collectImageRefs(options)\n const images = new Map<string, AgyResolvedImage>()\n if (refs.length === 0) return images\n const store = this.options.resolveAttachments?.()\n if (!store) {\n throw new LlmError(\n 'agy image input requires the durable attachment service (in-harness plugin only)',\n 'UNSUPPORTED_CONTENT',\n )\n }\n // Read every attachment concurrently (N images cost one round-trip, not N).\n // allSettled rather than all: more than one read may reject, and the\n // surfaced error must be deterministic (first failure in ref order) instead\n // of whichever concurrent read happened to reject first — and no rejection\n // may escape as unhandled.\n const settled = await Promise.allSettled(\n refs.map(async (ref) => {\n const stored = await store.readImage(ref)\n return {\n attachmentId: ref.attachmentId,\n image: {\n mediaType: stored.ref.mediaType,\n data: Buffer.from(stored.data).toString('base64'),\n },\n }\n }),\n )\n const failedIndex = settled.findIndex((outcome) => outcome.status === 'rejected')\n if (failedIndex !== -1) {\n const ref = refs[failedIndex]!\n const cause: unknown = (settled[failedIndex] as PromiseRejectedResult).reason\n throw new LlmError(\n `agy image attachment \"${ref.attachmentId}\" could not be loaded: ${cause instanceof Error ? cause.message : String(cause)}`,\n 'UNSUPPORTED_CONTENT',\n { cause: cause instanceof Error ? cause : undefined },\n )\n }\n for (const outcome of settled) {\n if (outcome.status === 'fulfilled') {\n images.set(outcome.value.attachmentId, outcome.value.image)\n }\n }\n return images\n }\n\n override async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {\n // The in-flight slot must be released on EVERY path — clean completion, a\n // thrown error, and a consumer that abandons the stream mid-turn. An async\n // generator's `finally` is the one hook covering all three, so the slot is\n // released here rather than at each exit inside `streamInner`.\n const holder: { account?: ManagedAccount } = {}\n try {\n yield* this.streamInner(options, holder)\n } finally {\n if (holder.account !== undefined) this.options.noteRequestSettled?.(holder.account)\n }\n }\n\n private async *streamInner(\n options: GenerateOptions,\n holder: { account?: ManagedAccount },\n ): AsyncIterable<StreamChunk> {\n // Spec D1 sequence: resolve images first — a locally-failing image request\n // must surface UNSUPPORTED_CONTENT (user story 8) instead of being masked\n // by account-pool errors, and must not touch pool state at all.\n const images = await this.resolveRequestImages(options)\n /**\n * The DSH agent loop stamps `options.sessionId` on every request it builds,\n * so it is the conversation identity — used both to scope account affinity\n * here and to derive the upstream `sessionId` below.\n */\n const conversationKey = options.sessionId === undefined ? undefined : String(options.sessionId)\n let session: AgyAccountSession | undefined\n try {\n session = await this.options.getSession(options.model, conversationKey)\n } catch (error) {\n if (error instanceof AgyAuthError) {\n if (error.kind === 'transport') {\n throw new LlmError(error.message, 'TRANSPORT', { cause: error })\n }\n if (error.kind === 'rate-limit') {\n throw new LlmError(error.message, 'RATE_LIMIT', {\n requestId: ProviderRequestId(generateAntigravityRequestId()),\n })\n }\n throw new LlmError(error.message, 'INVALID_CREDENTIAL', { cause: error })\n }\n if (error instanceof AgyPoolBlockedError) {\n if (error.kind === 'quota-exhausted') {\n throw new LlmError(error.message, QUOTA_EXCEEDED_CODE)\n }\n const delta = Math.ceil(error.blockedUntil - Date.now())\n const providerRetryAfterMs = Number.isFinite(delta) && delta > 0 ? delta : 1\n throw new LlmError(error.message, 'RATE_LIMIT', {\n providerRetryAfterMs,\n requestId: ProviderRequestId(generateAntigravityRequestId()),\n })\n }\n throw error\n }\n if (!session) {\n throw new LlmError(\n 'No agy account configured — run `dsh-agy login` to authenticate.',\n 'NO_CREDENTIAL',\n )\n }\n\n const multimodalFiles = await resolveMultimodalFiles(options)\n // The account is now fixed for this request; take the in-flight slot that the\n // `stream` wrapper releases (see its comment for why release lives there).\n holder.account = session.account\n this.options.noteRequestStarted?.(session.account)\n // `conversationKey` is resolved above and shared with account affinity.\n // Standalone CLI callers leave `options.sessionId` unset, so the upstream\n // session id degrades to the per-account value rather than inventing a\n // conversation.\n const conversationAccount = ledgerAccountKey(session)\n // 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 * `attemptStartedAt` is the clock of the outer attempt that owns this send,\n * so a transport failure is billed to that attempt rather than to the call.\n */\n const sendAttempt = async (attemptStartedAt: number): Promise<{ response: Response; bodyText?: string }> => {\n for (let attempt = 0; ; attempt++) {\n const generation = conversationKey !== undefined && conversationAccount !== undefined\n ? currentSessionGeneration(conversationAccount, conversationKey)\n : 0\n // One id per attempt, carried by the body. A resend under a bumped\n // session generation is a new upstream request, so it gets a new id.\n const requestId = generateAntigravityRequestId()\n const body = toAgyRequestBody(options, {\n projectId: session.account.projectId,\n sessionId:\n deriveAntigravitySessionId(session.account.email, conversationKey, generation) ?? undefined,\n requestId,\n ...(this.options.thinkingBudgetFor === undefined\n ? {}\n : { thinkingBudgetFor: this.options.thinkingBudgetFor }),\n ...(this.options.claudeBudgetFor === undefined\n ? {}\n : { claudeBudgetFor: this.options.claudeBudgetFor }),\n ...(this.options.tieredBudgetFor === undefined\n ? {}\n : { tieredBudgetFor: this.options.tieredBudgetFor }),\n ...(images.size > 0 ? { images } : {}),\n ...(multimodalFiles.size > 0 ? { multimodalFiles } : {}),\n })\n let response: Response\n try {\n response = await fetchAgyFirstOk(\n '/v1internal:streamGenerateContent?alt=sse',\n {\n method: 'POST',\n headers: buildRequestHeaders(session),\n body: JSON.stringify(body),\n signal: options.signal,\n },\n accountFetch(routing),\n routing,\n )\n } catch (error) {\n const classified = classifyFetchError(error, { proxyUrl: session.account.proxy })\n await this.options.reportFailure(classified.kind, session)\n // A transport failure consumed no tokens, but it is a real attempt\n // against this account's quota — record the request, not the usage.\n this.recordUsage(session, options.model, { ok: false, reason: 'network-error' }, attemptStartedAt)\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 /**\n * Maximum internal retries when the upstream ends a stream with\n * `MALFORMED_FUNCTION_CALL` before any user-facing text chunks have been\n * yielded to DSH. Gemini models occasionally glitch on tool-call JSON syntax;\n * re-sampling cleanly recovers the turn without crashing the conversation.\n *\n * This deliberately generalises the `sendAttempt` accumulation-wall resend\n * into the stream path: the invariant that one DSH call is one provider\n * attempt now has this second, documented exception (see AGENTS.md, \"the\n * adapter does not silent-retry\"). Both exceptions share the same shape —\n * the account is healthy and the request was accepted — and neither goes\n * through `reportFailure`, so the pool sees nothing.\n */\n const MAX_MALFORMED_RETRIES = 3\n\n for (let malformedAttempt = 0; ; malformedAttempt++) {\n const attemptStartedAt = Date.now()\n const { response, bodyText } = await sendAttempt(attemptStartedAt)\n\n if (!response.ok) {\n const classified = classifyHttpError(response.status, response.headers, bodyText)\n await this.options.reportFailure(classified.kind, session, {\n retryAfterMs: classified.retryAfterMs,\n status: response.status,\n rateLimitCategory: classified.rateLimitCategory,\n resetTime: classified.resetTime,\n model: options.model,\n verificationUrl: classified.verificationUrl,\n })\n this.recordUsage(session, options.model, {\n ok: false,\n rateLimited: classified.kind === 'rate-limit',\n reason: classified.kind,\n }, attemptStartedAt)\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 // Buffer stream chunks while reasoning or tool calls accumulate. If\n // `text-delta` arrives, commit the buffer and stream directly to preserve\n // real-time token streaming for user-facing responses. If\n // `MALFORMED_FUNCTION_CALL` arrives before any text deltas were committed,\n // the buffer is discarded and the attempt is cleanly retried.\n //\n // Cost, deliberately accepted: until the first `text-delta` this also holds\n // `reasoning-delta`, so a tool-call-only turn (reasoning + functionCall, no\n // text) delivers its thinking only at stream end. That is the price of\n // being able to discard the attempt at all — flushing reasoning would make\n // a thinking model's tool-call turn unretryable, which is the one case this\n // loop exists to recover.\n const bufferedChunks: StreamChunk[] = []\n /**\n * Thought signatures observed during this attempt, held back with the\n * chunks that carried them. `onToolSignature` fires as soon as the parser\n * sees a signature, i.e. BEFORE the attempt is known to be keepable, so\n * caching eagerly would let a discarded attempt write the process-global\n * signature cache (keyed by `toolCallId` — and issue #99 records those ids\n * colliding across distant turns, which would poison the very retry it\n * triggered).\n */\n const pendingSignatures: Array<readonly [string, string]> = []\n let yieldedDirect = false\n let usage: { input: number; output: number; cacheRead: number; cacheWrite: number } | undefined\n let ttftMs: number | undefined\n\n /**\n * Persist the signatures observed so far.\n *\n * Called at every point where the chunks that carried them have been\n * handed off — never on the discard path, so a thrown-away attempt leaves\n * no trace. The hand-off is the only sound anchor: a `functionCall` can\n * arrive AFTER the first `text-delta` (text-then-tool is a common shape),\n * so anchoring at stream end would drop the signature of an already\n * delivered call the moment the stream dies mid-body, silently degrading\n * the next round to the sentinel.\n */\n const commitSignatures = (): void => {\n for (const [toolCallId, signature] of pendingSignatures) {\n setThoughtSignature(toolCallId, signature)\n }\n pendingSignatures.length = 0\n }\n\n /** Hand the held chunks to the consumer. */\n const drainBuffer = (): StreamChunk[] => {\n const drained = bufferedChunks.slice()\n bufferedChunks.length = 0\n return drained\n }\n\n try {\n for await (const chunk of parseAgySse(response.body, {\n signal: options.signal,\n onToolSignature: (toolCallId, signature) => {\n pendingSignatures.push([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 }\n\n if (yieldedDirect) {\n // Past the hand-off, so this chunk leaves immediately: its signature\n // commits here rather than at stream end. A `functionCall` can arrive\n // after the first `text-delta`, so a mid-body death must not cost the\n // consumer a signature it was already delivered.\n if (pendingSignatures.length !== 0) commitSignatures()\n yield chunk\n } else if (chunk.type === 'text-delta') {\n yieldedDirect = true\n commitSignatures()\n // TTFT is when the CONSUMER first sees output, not when the parser\n // first produced it: a buffered turn holds every chunk until this\n // point, so measuring at parse time would under-report the user's\n // wait by the whole thinking period.\n ttftMs = Date.now() - attemptStartedAt\n for (const buffered of drainBuffer()) yield buffered\n yield chunk\n } else {\n bufferedChunks.push(chunk)\n }\n }\n\n // Commit whatever the turn left buffered (a tool-call or completed\n // reasoning turn never emits a text-delta, so it ends up here).\n if (ttftMs === undefined) ttftMs = Date.now() - attemptStartedAt\n commitSignatures()\n for (const buffered of drainBuffer()) yield buffered\n\n await this.options.markSuccess?.(session)\n this.recordUsage(session, options.model, { ok: true, usage, ttftMs }, attemptStartedAt)\n return\n } catch (error) {\n if (error instanceof DOMException && error.name === 'AbortError') {\n throw new LlmError('agy stream aborted', 'ABORTED', { cause: error })\n }\n\n // MALFORMED_FUNCTION_CALL is a transient model syntax fluke during tool calling.\n // If no text chunks were committed to DSH, discard the buffered chunks, record\n // this attempt's usage, and retry after a short linear wait. Across at most\n // 3 retries the whole schedule is 1.2s, so the shape buys nothing here;\n // what matters is that the first re-sample is not immediate.\n const isMalformed = error instanceof UnmappedFinishReasonError && error.reason === 'MALFORMED_FUNCTION_CALL'\n if (isMalformed && !yieldedDirect && malformedAttempt < MAX_MALFORMED_RETRIES) {\n // Bill the tokens this attempt really spent. `usage` arrives because\n // parseAgySse flushes its stashed totals before propagating an\n // unmapped finishReason; it stays undefined when the stream died too\n // early to report any.\n this.recordUsage(session, options.model, { ok: false, reason: 'request-error', usage }, attemptStartedAt)\n // Abandon the body. The streaming dispatcher runs with `bodyTimeout: 0`\n // (a reasoning pause must not be killed mid-turn), so nothing else\n // reclaims this connection: leaving it unread can pin one per discarded\n // attempt. `parseAgySse`'s finally has already released the reader\n // lock, so cancelling is safe here.\n await response.body?.cancel().catch(() => {})\n // Abort-aware, and converted here rather than in a shared helper: a\n // `catch` block never catches exceptions thrown by its own body, so\n // awaiting the sleep inside this handler would let an abort landing in\n // the backoff escape the generator as a raw `DOMException`. Every other\n // abort path in this adapter reports the typed `ABORTED` code, which is\n // what DSH's retry policy reads, so dropping it here would give the\n // same cancellation two different shapes.\n try {\n await sleepWithSignal(200 * (malformedAttempt + 1), options.signal)\n } catch (sleepError) {\n if (sleepError instanceof DOMException && sleepError.name === 'AbortError') {\n throw new LlmError('agy stream aborted', 'ABORTED', { cause: sleepError })\n }\n throw sleepError\n }\n continue\n }\n\n // An unmapped finishReason (SAFETY, unrecovered MALFORMED_FUNCTION_CALL, ...) is a\n // CONTENT-level verdict: the request reached a healthy account and the\n // upstream chose to stop the response. Reporting network-error would\n // cool and rotate the account for a wall the next request may never\n // hit; request-error is a no-op at account level. DSH sees a terminal\n // UPSTREAM error either way (below).\n const unmappedFinish = error instanceof UnmappedFinishReasonError\n await this.options.reportFailure(unmappedFinish ? 'request-error' : 'network-error', session)\n // `usage` is defined here exactly when the stream died with an unmapped\n // finishReason: `parseAgySse` flushes its stashed totals before\n // propagating that one error and no other. A mid-body socket death and\n // the EOF completeness guard produce none, so the value is `undefined`\n // there and passing it is a no-op — the retry path bills the same way.\n this.recordUsage(session, options.model, { ok: false, reason: unmappedFinish ? 'request-error' : 'network-error', usage }, attemptStartedAt)\n await response.body?.cancel().catch(() => {})\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 /**\n * Fold one attempt into the usage ledger.\n *\n * Statistics are diagnostics, never load-bearing: a ledger failure must not\n * fail a generation, so this swallows its own errors.\n */\n private recordUsage(\n session: AgyAccountSession,\n model: string | undefined,\n result: {\n ok: boolean\n rateLimited?: boolean\n reason?: string\n usage?: { input: number; output: number; cacheRead: number; cacheWrite: number }\n ttftMs?: number\n },\n startedAt: number,\n ): void {\n const record = this.options.recordUsage\n if (record === undefined) return\n try {\n record({\n ...(ledgerAccountKey(session) === undefined ? {} : { account: ledgerAccountKey(session) }),\n ...(model === undefined ? {} : { model }),\n ok: result.ok,\n ...(result.rateLimited === true ? { rateLimited: true } : {}),\n ...(result.reason === undefined ? {} : { reason: result.reason }),\n ...(result.usage === undefined ? {} : { usage: result.usage }),\n ...(result.ttftMs === undefined ? {} : { ttftMs: result.ttftMs }),\n latencyMs: Math.max(0, Date.now() - startedAt),\n })\n } catch {\n // Swallowed by design.\n }\n }\n}\n\nexport type { ToolSchema }\n","/**\n * Model visibility: a per-provider blacklist of model ids hidden from the DSH\n * model selector.\n *\n * Mechanism: DSH's selector reads `session.modelCatalog`, which calls\n * `ctx.llm.listModels(provider)` -> `AgyAdapter.listModels()`. Filtering inside\n * the adapter is therefore sufficient to hide a model from the picker without\n * touching DSH — no host patch, no new RPC.\n *\n * Two deliberate choices:\n *\n * - Blacklist, not whitelist. Only explicitly-disabled models are hidden, so a\n * model the server adds later still shows up. A whitelist would silently hide\n * every new model until the user re-enabled it.\n * - Persisted to agy's own file rather than a `ctx.settings` namespace. The\n * settings service requires a `@deepseek-ai/schemastery` schema, and the\n * standalone CLI must not import any `@deepseek-ai/*` package at runtime\n * (AGENTS.md invariant). agy already owns plain JSON state with the same\n * atomic-write pattern (`agy-accounts.json`), so this stays consistent and\n * lets the CLI read the same blacklist.\n *\n * Only `true` counts as disabled; an id maps to `true` or is absent. Enabling\n * deletes the key, so the file holds just the hidden set and cannot grow with\n * every toggle.\n */\n\nimport { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport { migrateToAgyDir } from './store/paths.ts'\n\nexport const MODELS_VERSION = 1\n\n/** `provider -> modelId -> true`. */\nexport type DisabledModelMap = Record<string, Record<string, true>>\n\nexport interface ModelVisibilityDocument {\n version: number\n disabled: DisabledModelMap\n}\n\nconst EMPTY_SET: ReadonlySet<string> = new Set<string>()\n\n/** Keep only well-formed `provider -> { modelId: true }` entries (tolerates hand edits). */\nexport function sanitizeDisabledModels(raw: unknown): DisabledModelMap {\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return {}\n const out: DisabledModelMap = {}\n for (const [provider, value] of Object.entries(raw as Record<string, unknown>)) {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) continue\n const perProvider: Record<string, true> = {}\n for (const [modelId, flag] of Object.entries(value as Record<string, unknown>)) {\n if (flag === true) perProvider[modelId] = true\n }\n if (Object.keys(perProvider).length > 0) out[provider] = perProvider\n }\n return out\n}\n\nexport function parseModelVisibility(text: string): ModelVisibilityDocument {\n let raw: unknown\n try {\n raw = JSON.parse(text)\n } catch {\n return { version: MODELS_VERSION, disabled: {} }\n }\n const disabled = typeof raw === 'object' && raw !== null\n ? sanitizeDisabledModels((raw as Record<string, unknown>).disabled)\n : {}\n return { version: MODELS_VERSION, disabled }\n}\n\nexport interface ModelVisibilityOptions {\n /**\n * Defaults to `$DSH_HOME/agy/agy-models.json`, migrating the legacy\n * `$DSH_HOME/agy-models.json` by one-shot rename (see `migrateToAgyDir`).\n */\n file?: string\n}\n\n/**\n * In-memory authoritative copy of the blacklist, backed by a JSON file.\n *\n * The adapter calls `disabledFor()` on every `listModels()`, so that read is a\n * pure in-memory lookup: a toggle takes effect on the next catalog refresh\n * without rebuilding the adapter or restarting the host. Writes go through the\n * file immediately (toggles are rare, user-initiated).\n *\n * `disabledFor()` also revalidates against the file's mtime and reloads when it\n * moved. Two instances of this class routinely coexist in ONE process — the\n * main plugin entry and the web entry each build their own runtime — and a\n * toggle is written by the web instance while the model selector reads the main\n * instance. Without the mtime check the selector would keep serving its stale\n * in-memory copy, so switching a model off appeared to do nothing until the\n * host restarted (the `llm/adapters-updated` notification fires, the picker\n * refreshes, and the model is still listed). The same check is what lets a CLI\n * toggle be seen by a running server.\n */\nexport class ModelVisibility {\n private readonly file: string\n private disabled: DisabledModelMap\n /** Raw text this instance last read or wrote; see `reloadIfChanged`. */\n private raw: string\n /** Reusable frozen empty set, so the common \"nothing disabled\" read allocates nothing. */\n private readonly sets = new Map<string, ReadonlySet<string>>()\n\n constructor(options: ModelVisibilityOptions = {}) {\n this.file = options.file ?? migrateToAgyDir('agy-models.json').file\n const initial = this.readWithRaw()\n this.disabled = initial.disabled\n this.raw = initial.raw\n }\n\n /** Path of the backing file. */\n get path(): string {\n return this.file\n }\n\n /** The exact bytes `write()` would persist, so `raw` can track what this instance wrote. */\n private static serialize(next: DisabledModelMap): string {\n const doc: ModelVisibilityDocument = { version: MODELS_VERSION, disabled: next }\n return JSON.stringify(doc, null, 2) + '\\n'\n }\n\n /**\n * Read the map together with the raw text it came from.\n *\n * The TEXT (not an mtime) is what detects another writer's change. An mtime\n * cannot: a toggle and a re-read can land in the same filesystem timestamp\n * tick, and Windows resolves that coarsely enough to have failed CI — two\n * distinct writes reported an identical mtime, so the second change was never\n * seen and a disabled model stayed selectable. Content comparison cannot miss\n * a change, whatever the clock resolution.\n */\n private readWithRaw(): { disabled: DisabledModelMap, raw: string } {\n try {\n const raw = readFileSync(this.file, 'utf8')\n return { disabled: parseModelVisibility(raw).disabled, raw }\n } catch {\n // A missing or unreadable file means \"nothing disabled\".\n return { disabled: {}, raw: '' }\n }\n }\n\n private write(next: DisabledModelMap): void {\n mkdirSync(dirname(this.file), { recursive: true })\n const tmp = `${this.file}.tmp`\n writeFileSync(tmp, ModelVisibility.serialize(next), { mode: 0o600 })\n renameSync(tmp, this.file)\n }\n\n /**\n * Disabled model ids for one provider. Cheap enough for every catalog refresh:\n * one `stat` plus an in-memory map lookup, and the stat is what keeps two\n * same-process instances (and a CLI writer) in agreement.\n */\n disabledFor(provider: string): ReadonlySet<string> {\n this.reloadIfChanged()\n const cacheKey = provider\n const cached = this.sets.get(cacheKey)\n if (cached !== undefined) return cached\n const perProvider = this.disabled[provider]\n if (perProvider === undefined) return EMPTY_SET\n const ids = Object.keys(perProvider)\n if (ids.length === 0) return EMPTY_SET\n const set: ReadonlySet<string> = new Set(ids)\n this.sets.set(cacheKey, set)\n return set\n }\n\n /** Whether one model is currently hidden. Also revalidates, for the same reason. */\n isDisabled(provider: string, modelId: string): boolean {\n this.reloadIfChanged()\n return this.disabled[provider]?.[modelId] === true\n }\n\n /** The raw map, for the settings UI. */\n all(): DisabledModelMap {\n const out: DisabledModelMap = {}\n for (const [provider, models] of Object.entries(this.disabled)) out[provider] = { ...models }\n return out\n }\n\n /**\n * Hide or show one model. Persists immediately.\n *\n * The edit is applied to a freshly re-read map, not to this instance's copy:\n * with two instances in one process (main plugin + web entry) and a CLI in\n * another, editing the stale copy would silently drop whatever the other\n * writer had toggled since this instance last read. Toggles are rare and\n * user-initiated, so a read-modify-write costs nothing here — unlike the\n * ledger, this is not a hot path.\n *\n * Not locked: a simultaneous toggle from two processes can still lose one of\n * the two updates. The write itself is atomic (tmp + rename), so the file is\n * never corrupt — losing one toggle under a genuine race is an acceptable\n * trade for not introducing a lock file into a settings toggle.\n */\n setDisabled(provider: string, modelId: string, disabled: boolean): void {\n if (modelId === '') throw new Error('setDisabled: modelId must not be empty')\n this.reload()\n const next: DisabledModelMap = {}\n for (const [key, models] of Object.entries(this.disabled)) next[key] = { ...models }\n const perProvider = { ...(next[provider] ?? {}) }\n if (disabled) perProvider[modelId] = true\n else delete perProvider[modelId]\n if (Object.keys(perProvider).length === 0) delete next[provider]\n else next[provider] = perProvider\n this.write(next)\n this.disabled = next\n this.raw = ModelVisibility.serialize(next)\n // Drop the memoized set for this provider so the next read reflects the write.\n this.sets.delete(provider)\n }\n\n /** Re-read the file (multi-process: another writer may have toggled a model). */\n reload(): void {\n const fresh = this.readWithRaw()\n this.disabled = fresh.disabled\n this.raw = fresh.raw\n this.sets.clear()\n }\n\n /**\n * Reload when the file's contents differ from what this instance last read.\n *\n * Reads rather than stats: `disabledFor()` runs once per catalog refresh, not\n * per request, and the call it feeds is a network request to the model\n * endpoint — one small local read is not the cost that matters here, while a\n * missed change is the bug this whole mechanism exists to prevent.\n */\n private reloadIfChanged(): void {\n let current: string\n try {\n current = readFileSync(this.file, 'utf8')\n } catch {\n // Missing/unreadable: treat as empty, matching `readWithRaw`.\n current = ''\n }\n if (current === this.raw) return\n this.reload()\n }\n}\n","/**\n * Thinking-budget vocabulary shared by the host store, the RPC wire contract,\n * and the browser UI.\n *\n * Split out for the same reason `usage-types.ts` exists: these types live on\n * their own, free of any `node:*` import, so the browser bundle can describe the\n * settings surface without pulling in the host's storage module. Putting them in\n * `thinking-budget.ts` instead made `rpc-contract.ts` (which the client\n * typechecks) reach `node:fs` through the transitive import — caught by\n * `tsconfig.client.json`, which includes the contract and excludes `src/store`.\n */\n\n/** The three levels a tiered model exposes, in display order. */\nexport const THINKING_LEVELS = ['low', 'medium', 'high'] as const\n\nexport type ThinkingLevel = typeof THINKING_LEVELS[number]\n\n/**\n * The accepted `thinkingBudget` interval, measured on this channel.\n *\n * Exported so the UI, the RPC validation, and the store's sanitizer share ONE\n * definition: a value outside it is a 400 from upstream naming exactly this\n * range, so rejecting it locally turns a per-request failure into a save-time\n * message.\n */\nexport const THINKING_BUDGET_MIN = -1\nexport const THINKING_BUDGET_MAX = 65_535\n\n/** One budget per level; an ABSENT key means \"send no budget\" (upstream decides). */\nexport type ThinkingBudgets = Partial<Record<ThinkingLevel, number>>\n\n/**\n * The Claude family's `thinkingBudget` bounds, measured separately from Gemini's.\n *\n * The two families do NOT share a contract, which is why one interval cannot\n * serve both:\n * - Claude's floor is **1024**, not `-1`. A budget of 1 or 512 is rejected with\n * `thinking.enabled.budget_tokens: Input should be greater than or equal to\n * 1024`; `-1` and `0` are accepted as special values.\n * - Claude additionally requires **`max_tokens` strictly greater than the\n * budget**: `budget=1024, max_tokens=1024` is a 400, and a budget sent with\n * no `maxOutputTokens` at all also fails. So the margin is at least one token.\n *\n * `CLAUDE_BUDGET_MAX` is therefore one below `AGY_CLAUDE_MAX_OUTPUT_TOKENS` — the\n * largest budget that can still leave room for a strictly greater `max_tokens`.\n */\nexport const CLAUDE_BUDGET_MIN = 1024\nexport const CLAUDE_BUDGET_MAX = 63_999\n\n/** Whether `value` may be sent as a Claude `thinkingBudget`. */\nexport function isValidClaudeBudget(value: unknown): value is number {\n if (typeof value !== 'number' || !Number.isInteger(value)) return false\n // `-1` (adaptive) and `0` are accepted in addition to the documented minimum;\n // both are measured rather than inferred.\n if (value === -1 || value === 0) return true\n return value >= CLAUDE_BUDGET_MIN && value <= CLAUDE_BUDGET_MAX\n}\n","/**\n * Thinking-budget configuration: a global Low/Medium/High -> token mapping.\n *\n * WHY A GLOBAL MAP RATHER THAN PER-MODEL. Only `*-tiered` models emit a\n * `thinkingConfig` at all (see `isLevelThinkingModel`), and today that is three\n * ids in one family — while the levels a user actually picks are already exposed\n * by DSH's own model selector (`reasoningEffort`). So the missing piece is not a\n * per-model switch but the VALUE behind each level: \"High\" currently means the\n * upstream's own High, with no way to say \"High = 16000 tokens\". One mapping of\n * three numbers gives that, and because it is keyed by level it applies to every\n * tiered model without a per-model row.\n *\n * Measured on this channel (docs/ANTIGRAVITY-API.md §3):\n * - accepted interval is [-1, 65535]; `-1` is adaptive and IS accepted\n * - `minThinkingBudget` is NOT a validation (a budget below it answers 200),\n * so it must never become a client-side clamp\n * - `0` reduces thinking but does NOT reliably disable it\n *\n * An EMPTY entry means \"do not send a budget for this level\" — the request keeps\n * sending `thinkingLevel`, which is the behaviour before this feature existed.\n * That is why the shipped defaults are all empty: the feature is opt-in and its\n * absence is a no-op.\n *\n * Persisted to agy's own JSON rather than `ctx.settings`, for the reason recorded\n * in `model-visibility.ts`: that service needs a `@deepseek-ai/schemastery`\n * schema, and the CLI must not import `@deepseek-ai/*` at runtime. Same\n * atomic-write and content-compare reload pattern, so a writer in one process\n * (the web entry) is seen by a reader in another (the main plugin).\n */\n\nimport { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport { migrateToAgyDir } from './store/paths.ts'\nimport {\n THINKING_BUDGET_MAX,\n THINKING_BUDGET_MIN,\n THINKING_LEVELS,\n CLAUDE_BUDGET_MAX,\n CLAUDE_BUDGET_MIN,\n isValidClaudeBudget,\n} from './thinking-types.ts'\nimport type { ThinkingBudgets, ThinkingLevel } from './thinking-types.ts'\n\nexport const THINKING_VERSION = 1\n\nexport {\n THINKING_LEVELS,\n THINKING_BUDGET_MIN,\n THINKING_BUDGET_MAX,\n} from './thinking-types.ts'\nexport type { ThinkingLevel, ThinkingBudgets } from './thinking-types.ts'\n\n/**\n * The persisted document.\n *\n * `budgets` is keyed by reasoning LEVEL and applies to the tiered (Gemini)\n * family, whose single id exposes low/medium/high. `claudeBudget` is a single\n * value because the Claude family is id-bound: each capability is its own model\n * id with no level selector, so there is nothing to key by. Keeping them in one\n * document but distinct fields is what lets each carry its own validation —\n * their accepted intervals genuinely differ (Claude's floor is 1024, not -1).\n */\nexport interface ThinkingDocument {\n version: number\n budgets: ThinkingBudgets\n /** Budget for Claude thinking models; absent means \"send no budget\". */\n claudeBudget?: number\n /**\n * Budget for the TIERED slot — the selector's \"Default\" effort.\n *\n * That effort carries no level id (`effort: void 0`), so it cannot be keyed in\n * `budgets`. A value here sends `thinkingBudget` WITHOUT `thinkingLevel`, which\n * is what makes \"Default\" configurable as a Max: the effort still decides\n * nothing, the number does. Absent means the request sends no thinkingConfig at\n * all, i.e. upstream's own adaptive allocation.\n */\n tieredBudget?: number\n}\n\n/** Whether `value` may be sent as a `thinkingBudget`. */\nexport function isValidThinkingBudget(value: unknown): value is number {\n return typeof value === 'number'\n && Number.isInteger(value)\n && value >= THINKING_BUDGET_MIN\n && value <= THINKING_BUDGET_MAX\n}\n\n/**\n * Keep only well-formed entries (tolerates hand edits).\n *\n * Rejects out-of-range and non-integer values rather than clamping them: a\n * clamped value would silently mean something the user did not ask for, and\n * upstream rejects it anyway, so dropping the entry falls back to the\n * `thinkingLevel` path — which is valid.\n */\nexport function sanitizeThinkingBudgets(raw: unknown): ThinkingBudgets {\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return {}\n const out: ThinkingBudgets = {}\n for (const level of THINKING_LEVELS) {\n const value = (raw as Record<string, unknown>)[level]\n if (isValidThinkingBudget(value)) out[level] = value\n }\n return out\n}\n\nexport function parseThinkingDocument(text: string): ThinkingDocument {\n let raw: unknown\n try {\n raw = JSON.parse(text)\n } catch {\n return { version: THINKING_VERSION, budgets: {} }\n }\n const record = typeof raw === 'object' && raw !== null ? raw as Record<string, unknown> : {}\n const budgets = sanitizeThinkingBudgets(record.budgets)\n const claude = record.claudeBudget\n const tiered = record.tieredBudget\n return {\n version: THINKING_VERSION,\n budgets,\n // Dropped when unusable rather than clamped, matching the level budgets: the\n // request then sends no budget, which is a valid state.\n ...(isValidClaudeBudget(claude) ? { claudeBudget: claude } : {}),\n ...(isValidThinkingBudget(tiered) ? { tieredBudget: tiered } : {}),\n }\n}\n\nexport interface ThinkingBudgetOptions {\n /**\n * Defaults to `$DSH_HOME/agy/agy-thinking.json`, migrating the legacy\n * `$DSH_HOME/agy-thinking.json` by one-shot rename (see `migrateToAgyDir`).\n */\n file?: string\n /**\n * Minimum gap between hot-path file revalidations. Defaults to\n * `DEFAULT_REVALIDATE_INTERVAL_MS`; `0` disables throttling (useful in tests\n * that need a cross-instance write visible immediately).\n */\n revalidateIntervalMs?: number\n}\n\n/**\n * How long a hot-path read may serve the in-memory copy before re-checking the\n * file. One second keeps a settings change effectively immediate to the user\n * while collapsing a per-request read into at most one per second.\n */\nexport const DEFAULT_REVALIDATE_INTERVAL_MS = 1_000\n\n/**\n * In-memory authoritative copy of the budgets, backed by a JSON file.\n *\n * `budgetFor` sits on the GENERATION HOT PATH (`toAgyRequestBody` calls it for\n * every request), so the cross-instance revalidation it needs is rate-limited:\n * see `revalidateIntervalMs`. Writes go to the file immediately, since edits are\n * rare and user-initiated, and they bypass the limit so a concurrent edit is\n * never overwritten.\n */\nexport class ThinkingBudgetStore {\n private readonly file: string\n private readonly revalidateIntervalMs: number\n private doc: ThinkingDocument\n /** Raw text this instance last read or wrote; see `reloadNow`. */\n private raw: string\n /** When `reloadNow` last ran, for the hot-path throttle. */\n private checkedAt: number\n\n constructor(options: ThinkingBudgetOptions = {}) {\n this.file = options.file ?? migrateToAgyDir('agy-thinking.json').file\n this.revalidateIntervalMs = options.revalidateIntervalMs ?? DEFAULT_REVALIDATE_INTERVAL_MS\n const initial = this.readWithRaw()\n this.doc = initial.doc\n this.raw = initial.raw\n this.checkedAt = Date.now()\n }\n\n /** Path of the backing file. */\n get path(): string {\n return this.file\n }\n\n private static serialize(doc: ThinkingDocument): string {\n return JSON.stringify(doc, null, 2) + '\\n'\n }\n\n /**\n * Read the document together with the raw text it came from.\n *\n * Content, not mtime: two writers can land in the same filesystem timestamp\n * tick (Windows resolves it coarsely enough to have failed CI in this repo),\n * and a missed change is exactly the bug this check exists to prevent.\n */\n private readWithRaw(): { doc: ThinkingDocument, raw: string } {\n try {\n const raw = readFileSync(this.file, 'utf8')\n return { doc: parseThinkingDocument(raw), raw }\n } catch {\n // A missing or unreadable file means \"nothing configured\".\n return { doc: { version: THINKING_VERSION, budgets: {} }, raw: '' }\n }\n }\n\n /**\n * Re-read the file if another writer changed it.\n *\n * Compares CONTENT, not mtime: a toggle and a re-read can land in the same\n * filesystem timestamp tick, and Windows resolves that coarsely enough to have\n * failed CI in this repo (`model-visibility.ts` records the incident). Content\n * comparison cannot miss a change, whatever the clock resolution.\n *\n * Unconditional — callers on the hot path use `reloadIfChanged` instead.\n */\n private reloadNow(): void {\n let current: string\n try {\n current = readFileSync(this.file, 'utf8')\n } catch {\n current = ''\n }\n this.checkedAt = Date.now()\n if (current === this.raw) return\n const fresh = this.readWithRaw()\n this.doc = fresh.doc\n this.raw = fresh.raw\n }\n\n /**\n * Hot-path revalidation: `reloadNow`, rate-limited.\n *\n * Without the limit every generation pays a blocking `readFileSync` plus a\n * UTF-8 decode — the module used to claim these reads were \"in-memory\", which\n * an unconditional read made false. The file is written only by `setBudget`\n * (a rare, user-initiated action), so the worst case here is that a settings\n * change takes up to `revalidateIntervalMs` to reach an already-running\n * adapter. That is the right trade for a setting; it is not right for the write\n * path, which is why `write` calls `reloadNow` directly.\n */\n private reloadIfChanged(): void {\n if (Date.now() - this.checkedAt < this.revalidateIntervalMs) return\n this.reloadNow()\n }\n\n /**\n * Write the whole document atomically.\n *\n * Always re-reads first, so a concurrent edit by the OTHER instance in this\n * process (main plugin + web entry) is merged rather than overwritten: each\n * caller passes a mutation, not a full replacement built from a stale copy.\n */\n private write(mutate: (doc: ThinkingDocument) => void): void {\n // Unconditional: building on a throttled-skipped stale copy would drop the\n // other instance's concurrent edit, which is the whole reason for re-reading.\n this.reloadNow()\n const next: ThinkingDocument = {\n version: THINKING_VERSION,\n budgets: { ...this.doc.budgets },\n ...(this.doc.claudeBudget === undefined ? {} : { claudeBudget: this.doc.claudeBudget }),\n ...(this.doc.tieredBudget === undefined ? {} : { tieredBudget: this.doc.tieredBudget }),\n }\n mutate(next)\n const text = ThinkingBudgetStore.serialize(next)\n mkdirSync(dirname(this.file), { recursive: true })\n const tmp = `${this.file}.tmp`\n writeFileSync(tmp, text, { mode: 0o600 })\n renameSync(tmp, this.file)\n this.doc = next\n this.raw = text\n }\n\n /** The configured budget for one level, or undefined when unset. */\n budgetFor(level: string | undefined): number | undefined {\n this.reloadIfChanged()\n if (level === undefined) return undefined\n const key = level.toLowerCase() as ThinkingLevel\n if (!THINKING_LEVELS.includes(key)) return undefined\n return this.doc.budgets[key]\n }\n\n /** The Claude-family budget, or undefined when unset. */\n claudeBudget(): number | undefined {\n this.reloadIfChanged()\n return this.doc.claudeBudget\n }\n\n /** The tiered-slot budget (the selector's Default effort), or undefined. */\n tieredBudget(): number | undefined {\n this.reloadIfChanged()\n return this.doc.tieredBudget\n }\n\n /** The raw map, for the settings UI. */\n all(): ThinkingBudgets {\n this.reloadIfChanged()\n return { ...this.doc.budgets }\n }\n\n /** The whole document, for the settings UI. */\n snapshot(): ThinkingDocument {\n this.reloadIfChanged()\n return {\n version: THINKING_VERSION,\n budgets: { ...this.doc.budgets },\n ...(this.doc.claudeBudget === undefined ? {} : { claudeBudget: this.doc.claudeBudget }),\n ...(this.doc.tieredBudget === undefined ? {} : { tieredBudget: this.doc.tieredBudget }),\n }\n }\n\n /** Replace one level's budget, or clear it when `value` is undefined. */\n setBudget(level: string, value: number | undefined): ThinkingBudgets {\n const key = level.toLowerCase() as ThinkingLevel\n if (!THINKING_LEVELS.includes(key)) throw new Error(`unknown thinking level: ${level}`)\n if (value !== undefined && !isValidThinkingBudget(value)) {\n throw new Error(`thinking budget must be an integer in [${THINKING_BUDGET_MIN}, ${THINKING_BUDGET_MAX}]`)\n }\n this.write((doc) => {\n if (value === undefined) delete doc.budgets[key]\n else doc.budgets[key] = value\n })\n return this.all()\n }\n\n /** Replace the tiered-slot budget, or clear it when `value` is undefined. */\n setTieredBudget(value: number | undefined): ThinkingDocument {\n if (value !== undefined && !isValidThinkingBudget(value)) {\n throw new Error(`thinking budget must be an integer in [${THINKING_BUDGET_MIN}, ${THINKING_BUDGET_MAX}]`)\n }\n this.write((doc) => {\n if (value === undefined) delete doc.tieredBudget\n else doc.tieredBudget = value\n })\n return this.snapshot()\n }\n\n /** Replace the Claude budget, or clear it when `value` is undefined. */\n setClaudeBudget(value: number | undefined): ThinkingDocument {\n if (value !== undefined && !isValidClaudeBudget(value)) {\n throw new Error(`Claude thinking budget must be -1, 0, or an integer in [${CLAUDE_BUDGET_MIN}, ${CLAUDE_BUDGET_MAX}]`)\n }\n this.write((doc) => {\n if (value === undefined) delete doc.claudeBudget\n else doc.claudeBudget = value\n })\n return this.snapshot()\n }\n}\n","/**\n * Usage vocabulary shared by the host ledger, the RPC wire contract, and the\n * browser UI.\n *\n * These types live on their own, free of any `node:*` import, so the browser\n * bundle can describe usage without pulling in the host's storage module. The\n * ledger (`stats.ts`) and the wire contract (`rpc-contract.ts`) both build on\n * this module rather than on each other.\n */\n\n/** Token counters, disjoint exactly like DSH's `TokenUsage`. */\nexport interface TokenBuckets {\n /** Uncached input tokens. */\n input: number\n output: number\n cacheRead: number\n cacheWrite: number\n}\n\n/** One scope's counters (an account, a model, a day, or the grand total). */\nexport interface UsageCounters extends TokenBuckets {\n /** Requests attempted, including failures. */\n requests: number\n /** Requests that returned a usable stream. */\n succeeded: number\n /** Classified upstream failures of any kind. */\n failed: number\n /** Rate-limit classifications (HTTP 429, and 403 carrying quota phrasing). */\n rateLimited: number\n /** Pool rotations triggered by a failure. */\n rotations: number\n /** Summed request wall time, and the number of requests that timed it. */\n latencyMs: number\n latencyN: number\n /** Summed time-to-first-token, and the number of requests that reported one. */\n ttftMs: number\n ttftN: number\n}\n\n/** Where a request came from; CLI/verify/test never appear in DSH's own ledger. */\nexport type UsageSource = 'chat' | 'cli' | 'verify' | 'test'\n\n/** Zeroed counters, for accumulation. */\nexport function zeroCounters(): UsageCounters {\n return {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n requests: 0,\n succeeded: 0,\n failed: 0,\n rateLimited: 0,\n rotations: 0,\n latencyMs: 0,\n latencyN: 0,\n ttftMs: 0,\n ttftN: 0,\n }\n}\n","/**\n * Cumulative agy usage statistics, persisted to `$DSH_HOME/agy/agy-stats.json`.\n *\n * Why agy keeps its own ledger instead of reading DSH's session projections:\n *\n * - DSH structurally has no account dimension (no `account` field anywhere in\n * `dsh-llm` or the session types), so \"which account burned this quota\" is\n * unanswerable from DSH's data.\n * - agy's own traffic bypasses DSH entirely — `dsh-agy login/verify/status`,\n * `testCall()`, `checkAccounts()`, and quota discovery all consume upstream\n * quota without producing a session event.\n * - DSH's cumulative `tokenUsage` projection has no model dimension and folds\n * by `(turn, step)`, so a mid-session model switch cannot be attributed.\n *\n * Counter semantics deliberately differ from DSH's: DSH replaces a step's\n * usage (a retried attempt counts once, for conversation cost), while this\n * ledger accumulates per attempt (every retry really did consume quota, which\n * is what an account-level view must show).\n *\n * Storage shape is cumulative with a rolling per-day window:\n * - `totals` never expires — it answers \"all time\".\n * - `days` keeps `DAY_WINDOW` buckets — it answers today / 7d / 30d.\n * A day rolling out of `days` therefore loses only granularity; its counts\n * already live in `totals`.\n *\n * Writes are throttled and merged, never read-modify-write per request:\n * callers accumulate in memory (zero I/O on the hot path) and `flush()` takes\n * the file lock, re-reads the fresh on-disk state, adds this process's delta,\n * and atomically replaces the file. A plain overwrite would lose the counts\n * another process (the Desktop app, a web-profile server, a CLI invocation)\n * wrote in the meantime.\n *\n * A ledger that cannot be written must fail soft and stay bounded: the pending\n * backlog is capped, immediate (count-based) flushing is suspended while writes\n * fail, and the first error of each failure run is reported through\n * `onFlushError`. Statistics are diagnostics — they never break a generation,\n * and they never grow without limit.\n */\n\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport lockfile from 'proper-lockfile'\nimport { migrateToAgyDir } from './store/paths.ts'\nimport { zeroCounters } from './usage-types.ts'\nimport type { TokenBuckets, UsageCounters, UsageSource } from './usage-types.ts'\n\nexport type { TokenBuckets, UsageCounters, UsageSource } from './usage-types.ts'\n\nexport const STATS_VERSION = 2\n\n/** Days of per-day buckets retained. Longer spans read `totals`. */\nexport const DAY_WINDOW = 30\n\n/** Per-account ledger. */\nexport interface AccountUsage {\n totals: UsageCounters\n /** Per-model totals for this account, keyed by provider model id. */\n models: Record<string, UsageCounters>\n /** Request counts by call site. */\n sources: Record<UsageSource, number>\n /** Last time this account recorded anything (Unix ms). */\n lastUsedAt: number\n}\n\n/**\n * One day's slice, broken down the same three ways the Usage tab reads it.\n *\n * Version 1 stored only the flat counters here, which was enough for the KPI\n * strip but not for the breakdown tables: those had to read the ALL-TIME\n * `accounts`/`models` maps, so selecting \"Today\" changed the headline figures\n * while the tables below still showed every request ever recorded — 30 requests\n * above a 164-request row on the same screen. Keeping the per-day breakdown is\n * what lets the whole page follow one range.\n *\n * `totals` stays the sum of the day's records; `models`/`accounts` are the same\n * counters partitioned by dimension. Every record contributes to `totals` and to\n * whichever dimensions it names.\n */\nexport interface DayBucket {\n totals: UsageCounters\n /** Per-model counters within this day. */\n models: Record<string, UsageCounters>\n /** Per-account counters within this day. */\n accounts: Record<string, UsageCounters>\n}\n\n/** The persisted document. */\nexport interface StatsDocument {\n version: number\n /** When this ledger began collecting (Unix ms). */\n since: number\n /** Grand totals across every account. */\n totals: UsageCounters\n /** Per-account ledgers, keyed by account email (falling back to id). */\n accounts: Record<string, AccountUsage>\n /** Rolling per-day buckets, `YYYY-MM-DD` -> that day's slice. */\n days: Record<string, DayBucket>\n}\n\n/** One recorded request, as the call sites observe it. */\nexport interface UsageRecord {\n /** Account key (email preferred); omitted when no account was resolved. */\n account?: string\n /** Provider model id. */\n model?: string\n source: UsageSource\n /** Token usage; absent for calls that failed before producing usage. */\n usage?: TokenBuckets\n ok: boolean\n rateLimited?: boolean\n rotated?: boolean\n latencyMs?: number\n ttftMs?: number\n /**\n * A pool-level event (a rotation) rather than a request.\n *\n * Rotations are decided inside the session manager, after the adapter has\n * already recorded the failing request. Emitting them as ordinary records\n * would count that request twice, so a pool event adds only to the pool\n * counters and leaves the request counters alone.\n */\n poolEvent?: boolean\n /**\n * The failure classification this record carries ('rate-limit',\n * 'network-error', 'auth-failure', 'verification-required', 'project-error',\n * 'quota-exhausted'), when it failed. The counters ignore it: it exists for\n * the recent-activity ring, where \"why did it rotate\" is the whole question.\n */\n reason?: string\n}\n\n/** Cap of the in-memory recent-activity ring (see `UsageStats.recentRequests`). */\nexport const RECENT_MAX = 200\n\n/**\n * One recent record, as the UI's \"what just happened\" list shows it.\n *\n * Flattened from `UsageRecord` at capture time so the ring never aliases the\n * caller's object, and shaped for DISPLAY: pool events become `kind:\n * 'rotation'`, absent numbers become null. The ring answers \"just now\" — the\n * gap between the live in-flight line and the persisted ledger, whose records\n * collapse into counters the moment they are flushed.\n */\nexport interface RecentActivity {\n at: number\n account: string | null\n model: string | null\n kind: 'chat' | 'cli' | 'verify' | 'test' | 'rotation'\n ok: boolean\n rateLimited: boolean\n latencyMs: number | null\n ttftMs: number | null\n output: number | null\n /** Failure classification token, or null when the record succeeded. */\n reason: string | null\n}\n\nfunction zeroAccount(now: number): AccountUsage {\n return {\n totals: zeroCounters(),\n models: {},\n sources: { chat: 0, cli: 0, verify: 0, test: 0 },\n lastUsedAt: now,\n }\n}\n\nfunction emptyDocument(now: number): StatsDocument {\n return { version: STATS_VERSION, since: now, totals: zeroCounters(), accounts: {}, days: {} }\n}\n\n/** Local-time day key. Local (not UTC) so \"today\" matches the user's clock. */\nexport function dayKey(time: number): string {\n const d = new Date(time)\n const month = String(d.getMonth() + 1).padStart(2, '0')\n const day = String(d.getDate()).padStart(2, '0')\n return `${d.getFullYear()}-${month}-${day}`\n}\n\n/** Add `add` into `into`, in place. */\nfunction addInto(into: UsageCounters, add: UsageCounters): void {\n into.input += add.input\n into.output += add.output\n into.cacheRead += add.cacheRead\n into.cacheWrite += add.cacheWrite\n into.requests += add.requests\n into.succeeded += add.succeeded\n into.failed += add.failed\n into.rateLimited += add.rateLimited\n into.rotations += add.rotations\n into.latencyMs += add.latencyMs\n into.latencyN += add.latencyN\n into.ttftMs += add.ttftMs\n into.ttftN += add.ttftN\n}\n\n/** Coerce arbitrary parsed JSON into a valid counter block (tolerates hand edits). */\nfunction sanitizeCounters(raw: unknown): UsageCounters {\n const out = zeroCounters()\n if (typeof raw !== 'object' || raw === null) return out\n for (const key of Object.keys(out) as Array<keyof UsageCounters>) {\n const value = (raw as Record<string, unknown>)[key]\n if (typeof value === 'number' && Number.isFinite(value) && value >= 0) out[key] = value\n }\n return out\n}\n\nfunction sanitizeAccount(raw: unknown, now: number): AccountUsage {\n const out = zeroAccount(now)\n if (typeof raw !== 'object' || raw === null) return out\n const record = raw as Record<string, unknown>\n out.totals = sanitizeCounters(record.totals)\n if (typeof record.lastUsedAt === 'number' && Number.isFinite(record.lastUsedAt)) {\n out.lastUsedAt = record.lastUsedAt\n }\n if (typeof record.models === 'object' && record.models !== null) {\n for (const [id, value] of Object.entries(record.models as Record<string, unknown>)) {\n out.models[id] = sanitizeCounters(value)\n }\n }\n if (typeof record.sources === 'object' && record.sources !== null) {\n for (const key of ['chat', 'cli', 'verify', 'test'] as const) {\n const value = (record.sources as Record<string, unknown>)[key]\n if (typeof value === 'number' && Number.isFinite(value) && value >= 0) out.sources[key] = value\n }\n }\n return out\n}\n\n/** Parse a persisted document defensively; a corrupt file degrades to empty. */\nexport function parseStatsDocument(text: string, now: number): StatsDocument {\n let raw: unknown\n try {\n raw = JSON.parse(text)\n } catch {\n return emptyDocument(now)\n }\n if (typeof raw !== 'object' || raw === null) return emptyDocument(now)\n const record = raw as Record<string, unknown>\n const doc = emptyDocument(now)\n if (typeof record.since === 'number' && Number.isFinite(record.since)) doc.since = record.since\n doc.totals = sanitizeCounters(record.totals)\n if (typeof record.accounts === 'object' && record.accounts !== null) {\n for (const [key, value] of Object.entries(record.accounts as Record<string, unknown>)) {\n doc.accounts[key] = sanitizeAccount(value, now)\n }\n }\n if (typeof record.days === 'object' && record.days !== null) {\n for (const [key, value] of Object.entries(record.days as Record<string, unknown>)) {\n // Only well-formed day keys survive, so a stray key can never grow the file.\n if (/^\\d{4}-\\d{2}-\\d{2}$/.test(key)) doc.days[key] = sanitizeDayBucket(value)\n }\n }\n return doc\n}\n\n/**\n * Coerce one day's stored value into a `DayBucket`.\n *\n * Accepts the version-1 shape too: that stored a bare counter block, so a\n * document written before the upgrade keeps its day counts as `totals` with no\n * breakdown. Losing the breakdown for those days is the correct trade — the\n * alternative (dropping the days) would erase real history, and recomputing a\n * dimension split that was never recorded is impossible. The all-time\n * `accounts`/`models` maps are unaffected, so only range-scoped breakdown tables\n * are thinner for pre-upgrade days.\n */\nfunction sanitizeDayBucket(raw: unknown): DayBucket {\n const out: DayBucket = { totals: zeroCounters(), models: {}, accounts: {} }\n if (typeof raw !== 'object' || raw === null) return out\n const record = raw as Record<string, unknown>\n if (typeof record.totals === 'object' && record.totals !== null) {\n out.totals = sanitizeCounters(record.totals)\n if (typeof record.models === 'object' && record.models !== null) {\n for (const [id, value] of Object.entries(record.models as Record<string, unknown>)) {\n out.models[id] = sanitizeCounters(value)\n }\n }\n if (typeof record.accounts === 'object' && record.accounts !== null) {\n for (const [key, value] of Object.entries(record.accounts as Record<string, unknown>)) {\n out.accounts[key] = sanitizeCounters(value)\n }\n }\n return out\n }\n // Version-1 day: a bare counter block.\n out.totals = sanitizeCounters(raw)\n return out\n}\n\n/** Drop day buckets older than the window; their counts already live in `totals`. */\nfunction pruneDays(doc: StatsDocument, now: number): void {\n const cutoff = dayKey(now - DAY_WINDOW * 86_400_000)\n for (const key of Object.keys(doc.days)) {\n if (key < cutoff) delete doc.days[key]\n }\n}\n\n/**\n * Sum the day buckets covering the last `days` days, inclusive of today.\n *\n * `days: 1` is \"today\" and `days: DAY_WINDOW` is the full retained window. The\n * window is deliberately read from the per-day buckets rather than `totals`:\n * `totals` is all-time and grows past every window.\n * @param doc - the ledger snapshot.\n * @param days - window length in days, at least 1.\n * @param now - current time (Unix ms).\n * @returns summed counters over the window.\n */\nexport function foldWindow(doc: StatsDocument, days: number, now: number): UsageCounters {\n const out = zeroCounters()\n const span = Math.max(1, Math.floor(days))\n const cutoff = dayKey(now - (span - 1) * 86_400_000)\n for (const [key, bucket] of Object.entries(doc.days)) {\n // String compare is a valid date order for `YYYY-MM-DD`.\n if (key >= cutoff) addInto(out, bucket.totals)\n }\n return out\n}\n\n/** One range's breakdown: totals plus the same counters split by model/account. */\nexport interface WindowBreakdown {\n totals: UsageCounters\n models: Array<{ model: string, counters: UsageCounters }>\n accounts: Array<{ account: string, counters: UsageCounters }>\n}\n\n/**\n * Fold the day buckets for a window into the three views the Usage tab renders.\n *\n * The breakdown tables read THIS rather than the all-time `accounts`/`models`\n * maps, so one range selection governs the whole page. Rows are sorted by\n * request count descending, matching the all-time view; the caller re-sorts if\n * it wants another order (the UI shows them as-is).\n *\n * @param doc - the ledger snapshot.\n * @param days - window length in days, at least 1.\n * @param now - current time (Unix ms).\n * @returns totals and their per-model / per-account partitions.\n */\nexport function foldWindowBreakdown(doc: StatsDocument, days: number, now: number): WindowBreakdown {\n const span = Math.max(1, Math.floor(days))\n const cutoff = dayKey(now - (span - 1) * 86_400_000)\n const totals = zeroCounters()\n const models = new Map<string, UsageCounters>()\n const accounts = new Map<string, UsageCounters>()\n for (const [key, bucket] of Object.entries(doc.days)) {\n if (key < cutoff) continue\n addInto(totals, bucket.totals)\n for (const [model, counters] of Object.entries(bucket.models)) {\n const current = models.get(model)\n if (current === undefined) models.set(model, { ...counters })\n else addInto(current, counters)\n }\n for (const [account, counters] of Object.entries(bucket.accounts)) {\n const current = accounts.get(account)\n if (current === undefined) accounts.set(account, { ...counters })\n else addInto(current, counters)\n }\n }\n const byRequests = (a: { counters: UsageCounters }, b: { counters: UsageCounters }): number =>\n b.counters.requests - a.counters.requests\n return {\n totals,\n models: [...models.entries()].map(([model, counters]) => ({ model, counters })).sort(byRequests),\n accounts: [...accounts.entries()].map(([account, counters]) => ({ account, counters })).sort(byRequests),\n }\n}\n\n/** Merge one record into a document, in place. Shared by the live store and tests. */\nexport function applyRecord(doc: StatsDocument, record: UsageRecord, now: number): void {\n const delta = zeroCounters()\n // A pool event carries no request of its own; it only moves pool counters.\n if (record.poolEvent !== true) {\n delta.requests = 1\n if (record.ok) delta.succeeded = 1\n else delta.failed = 1\n if (record.rateLimited === true) delta.rateLimited = 1\n }\n if (record.rotated === true) delta.rotations = 1\n if (record.usage !== undefined) {\n delta.input = record.usage.input\n delta.output = record.usage.output\n delta.cacheRead = record.usage.cacheRead\n delta.cacheWrite = record.usage.cacheWrite\n }\n if (typeof record.latencyMs === 'number' && Number.isFinite(record.latencyMs) && record.latencyMs >= 0) {\n delta.latencyMs = record.latencyMs\n delta.latencyN = 1\n }\n if (typeof record.ttftMs === 'number' && Number.isFinite(record.ttftMs) && record.ttftMs >= 0) {\n delta.ttftMs = record.ttftMs\n delta.ttftN = 1\n }\n\n addInto(doc.totals, delta)\n\n if (record.account !== undefined && record.account !== '') {\n const account = doc.accounts[record.account] ?? zeroAccount(now)\n addInto(account.totals, delta)\n // A pool event is not a call site occurrence, so it counts no source.\n if (record.poolEvent !== true) account.sources[record.source] += 1\n account.lastUsedAt = now\n if (record.model !== undefined && record.model !== '') {\n const model = account.models[record.model] ?? zeroCounters()\n addInto(model, delta)\n // A model with no traffic left is still a real model row; keep it.\n account.models[record.model] = model\n }\n doc.accounts[record.account] = account\n }\n\n const key = dayKey(now)\n const bucket = doc.days[key] ?? { totals: zeroCounters(), models: {}, accounts: {} }\n addInto(bucket.totals, delta)\n // The same delta is partitioned by the dimensions it names, so a range-scoped\n // breakdown table adds up to that range's headline figures exactly.\n if (record.model !== undefined && record.model !== '') {\n const model = bucket.models[record.model] ?? zeroCounters()\n addInto(model, delta)\n bucket.models[record.model] = model\n }\n if (record.account !== undefined && record.account !== '') {\n const account = bucket.accounts[record.account] ?? zeroCounters()\n addInto(account, delta)\n bucket.accounts[record.account] = account\n }\n doc.days[key] = bucket\n\n pruneDays(doc, now)\n}\n\n/**\n * Cross-process file lock seam (proper-lockfile behind a small interface for tests).\n *\n * The sync form is deliberate — `flush()` is called from `process.on('exit')`,\n * where async work cannot settle.\n *\n * The sync API accepts NO retry configuration: `proper-lockfile` throws\n * \"Cannot use retries with the sync api\" when `retries` is passed, which fails\n * every flush and silently drops every count. Contention therefore surfaces as\n * a thrown error instead of a retry, and `flush()` puts the batch back for the\n * next timer tick — a statistics flush that loses a race costs nothing, so\n * waiting is never worth blocking the event loop for.\n */\nexport interface StatsLock {\n withLock<T>(file: string, fn: () => T): T\n}\n\nexport const properStatsLock: StatsLock = {\n withLock<T>(file: string, fn: () => T): T {\n // `realpath: false` because the target may be absent on first write; the\n // caller pre-creates it, and lockSync otherwise throws ENOENT resolving it.\n //\n // `stale` is deliberately short (8s, the same order as `flushIntervalMs`).\n // proper-lockfile only removes its lock directory through signal-exit, so a\n // `SIGKILL`ed process leaves one behind; with the former 30s window every\n // flush failed for 30 seconds afterwards — a long blackout for a diagnostic\n // ledger whose next flush can simply try again. `update` must stay below\n // `stale` or a lock holder self-expires while it is still working.\n const release = lockfile.lockSync(file, { stale: 8_000, update: 4_000, realpath: false })\n try {\n return fn()\n } finally {\n release()\n }\n },\n}\n\nexport const noopStatsLock: StatsLock = { withLock: (_file, fn) => fn() }\n\nexport interface UsageStatsStoreOptions {\n /**\n * Defaults to `$DSH_HOME/agy/agy-stats.json`, migrating the legacy\n * `$DSH_HOME/agy-stats.json` by one-shot rename (see `migrateToAgyDir`).\n */\n file?: string\n lock?: StatsLock\n /** Injectable clock, for tests. */\n now?: () => number\n /**\n * Flush after this many pending records. `0` disables count-based flushing.\n * Defaults to 50 (DSH's own projection cache writes at 200 records / 5s).\n */\n flushEvery?: number\n /** Flush after this many ms with pending records. Defaults to 5000. */\n flushIntervalMs?: number\n /**\n * Hard ceiling on un-flushed records. Defaults to 500 (ten batches).\n *\n * A persistent write failure (read-only `$DSH_HOME`, ENOSPC) makes every\n * flush fail, so the pending list would otherwise grow without bound in a\n * long-lived process. Past the ceiling the OLDEST records are dropped: a\n * bounded diagnostic ledger beats an unbounded one, and the oldest counts are\n * the least useful ones.\n */\n maxPending?: number\n /**\n * Called once per run of consecutive flush failures, with the first error.\n *\n * Without this a ledger that cannot be written fails silently: counts simply\n * stop appearing, with nothing to correlate. Once per run (not per attempt)\n * because the timer retries every `flushIntervalMs` and a log per retry would\n * be noise.\n */\n onFlushError?: (error: unknown) => void\n /**\n * Persistence half of the recent-activity ring (see `recent-store.ts`).\n * Called with each flattened record as it is captured; absent means the ring\n * stays per-process, as before.\n */\n persistRecent?: (entry: RecentActivity) => void\n}\n\n/**\n * Accumulating usage ledger. `record()` is the hot path and performs no I/O:\n * it folds the record into an in-memory delta. `flush()` merges that delta into\n * the persisted document under the file lock.\n */\nexport class UsageStats {\n private readonly file: string\n private readonly lock: StatsLock\n private readonly now: () => number\n private readonly flushEvery: number\n private readonly flushIntervalMs: number\n private readonly maxPending: number\n private readonly onFlushError: ((error: unknown) => void) | undefined\n private readonly persistRecent: ((entry: RecentActivity) => void) | undefined\n\n /** This process's un-flushed records. */\n private pending: UsageRecord[] = []\n private timer: ReturnType<typeof setTimeout> | undefined\n private lastFlushError: unknown\n /**\n * Whether the current run of consecutive failures has already been reported.\n * Count-based flushing is also suspended while set: see `record()`.\n */\n private flushFailedSince: number | undefined\n\n constructor(options: UsageStatsStoreOptions = {}) {\n // Explicit file (tests, callers with their own layout) is used as-is; the\n // default migrates the legacy home-directory file into `agy/` once.\n this.file = options.file ?? migrateToAgyDir('agy-stats.json').file\n this.lock = options.lock ?? properStatsLock\n this.now = options.now ?? (() => Date.now())\n this.flushEvery = options.flushEvery ?? 50\n this.flushIntervalMs = options.flushIntervalMs ?? 5_000\n this.maxPending = options.maxPending ?? 500\n this.onFlushError = options.onFlushError\n this.persistRecent = options.persistRecent\n // Deliberately does NOT create the file: a read-only command (`status`)\n // builds a ledger and must not leave state behind. `flush()` pre-creates it\n // before locking, which is the first point a write actually needs it.\n }\n\n /** Path of the backing file. */\n get path(): string {\n return this.file\n }\n\n /**\n * Pre-create the ledger so `proper-lockfile` can lock it. The lock throws\n * ENOENT on a nonexistent target, so the empty document must exist (0600,\n * tmp+rename) before the first flush.\n */\n private ensureFile(now: number): void {\n if (existsSync(this.file)) return\n mkdirSync(dirname(this.file), { recursive: true })\n const tmp = `${this.file}.tmp-init`\n writeFileSync(tmp, JSON.stringify(emptyDocument(now), null, 2) + '\\n', { mode: 0o600 })\n renameSync(tmp, this.file)\n }\n\n private readFile(): StatsDocument {\n const now = this.now()\n try {\n return parseStatsDocument(readFileSync(this.file, 'utf8'), now)\n } catch {\n return emptyDocument(now)\n }\n }\n\n /** Atomic replace (tmp + rename), owner-only, so a reader never sees a partial file. */\n private writeFile(doc: StatsDocument): void {\n mkdirSync(dirname(this.file), { recursive: true })\n const tmp = `${this.file}.tmp`\n writeFileSync(tmp, JSON.stringify(doc, null, 2) + '\\n', { mode: 0o600 })\n renameSync(tmp, this.file)\n }\n\n /** Record one request. Hot path: no I/O. */\n record(record: UsageRecord): void {\n this.noteRecent(record)\n this.pending.push(record)\n // Cap the backlog: unbounded growth is the one failure mode a diagnostic\n // ledger must not have (see `maxPending`).\n if (this.maxPending > 0 && this.pending.length > this.maxPending) {\n this.pending.splice(0, this.pending.length - this.maxPending)\n }\n // Count-based flushing is a throughput optimisation, so it is suspended\n // while writes are failing. Without this a persistent failure made EVERY\n // `record()` attempt a full synchronous flush (stat + lock + read + write),\n // turning the generation hot path into I/O — the exact opposite of this\n // class's contract. The timer below keeps retrying instead.\n if (\n this.flushFailedSince === undefined\n && this.flushEvery > 0\n && this.pending.length >= this.flushEvery\n ) {\n this.flush()\n return\n }\n this.armTimer()\n }\n\n /**\n * The in-memory recent-activity ring, persisted by an injected\n * `RecentActivityStore` when one is supplied.\n *\n * The ring itself stays in-memory and I/O-free — `noteRecent` only flattens —\n * while the OPTIONAL `persistRecent` seam hands each flattened record to\n * `recent-store.ts`, which owns the file, the triggers, the lock, and the\n * failure story. When no store is injected (tests, the CLI) the ring behaves\n * exactly as before: per-process, never persisted.\n *\n * An earlier revision recorded why persistence was rejected outright (file\n * growth, a versioned migration, a wider stored surface); `recent-store.ts`\n * answers each — a bounded list in its own file, no version field, emails\n * and model ids only — which is what turned this from a bad idea into a\n * spec.\n */\n private readonly recent: RecentActivity[] = []\n\n private noteRecent(record: UsageRecord): void {\n const entry: RecentActivity = {\n at: this.now(),\n account: record.account ?? null,\n model: record.model ?? null,\n kind: record.poolEvent === true ? 'rotation' : record.source,\n ok: record.ok === true,\n rateLimited: record.rateLimited === true,\n latencyMs: record.latencyMs ?? null,\n ttftMs: record.ttftMs ?? null,\n output: record.usage?.output ?? null,\n reason: record.ok === true ? null : record.reason ?? null,\n }\n this.recent.push(entry)\n if (this.recent.length > RECENT_MAX) {\n this.recent.splice(0, this.recent.length - RECENT_MAX)\n }\n // Fire-and-forget: the store never throws synchronously (its own flush\n // path is guarded), but a diagnostics hand-off must not break `record()`.\n try {\n this.persistRecent?.(entry)\n } catch {\n // Swallowed by design — same contract as `emitUsage` in session.ts.\n }\n }\n\n /**\n * Newest-first copy of the ring, for the `pool.recent` RPC.\n *\n * A copy, not the live array: the RPC layer may hold it across awaits while\n * `record()` keeps pushing.\n */\n recentRequests(): RecentActivity[] {\n return [...this.recent].reverse()\n }\n\n private armTimer(): void {\n if (this.timer !== undefined || this.flushIntervalMs <= 0) return\n this.timer = setTimeout(() => {\n this.timer = undefined\n this.flush()\n }, this.flushIntervalMs)\n // Never hold the process open for a statistics flush.\n this.timer.unref?.()\n }\n\n /**\n * Merge pending records into the persisted document.\n *\n * Under the lock, the fresh on-disk state is re-read and this process's\n * records are replayed onto it. That is what keeps concurrently-running\n * writers (Desktop, a web-profile server, a CLI invocation) from clobbering\n * each other: a plain overwrite of the in-memory document would drop every\n * count another process added since this one loaded the file.\n */\n flush(): void {\n if (this.timer !== undefined) {\n clearTimeout(this.timer)\n this.timer = undefined\n }\n const batch = this.pending\n if (batch.length === 0) return\n this.pending = []\n const now = this.now()\n try {\n this.ensureFile(now)\n this.lock.withLock(this.file, () => {\n // Re-read under the lock and replay this batch onto the fresh state:\n // that is what makes concurrent writers additive instead of last-write-wins.\n const doc = this.readFile()\n for (const record of batch) applyRecord(doc, record, now)\n this.writeFile(doc)\n })\n this.lastFlushError = undefined\n this.flushFailedSince = undefined\n } catch (error) {\n // A failed flush must not lose the counts or break a generation: put the\n // batch back so the next flush retries it, and surface the reason once.\n this.pending = [...batch, ...this.pending]\n if (this.maxPending > 0 && this.pending.length > this.maxPending) {\n this.pending.splice(0, this.pending.length - this.maxPending)\n }\n this.lastFlushError = error\n if (this.flushFailedSince === undefined) {\n this.flushFailedSince = now\n try {\n this.onFlushError?.(error)\n } catch {\n // The reporter must never break a flush path.\n }\n }\n // Only the timer retries now; `record()` stops triggering immediate\n // flushes until one succeeds.\n this.armTimer()\n }\n }\n\n /** Flush if anything is pending; safe to call on turn end and process exit. */\n flushSync(): void {\n this.flush()\n }\n\n /**\n * A snapshot for reading (the Usage tab).\n *\n * Reads the file rather than an in-memory copy: the writer is not always this\n * process. A CLI `login`/`verify` runs in its own process, and the browser may\n * be served by a different plugin instance than the one that generated, so an\n * in-memory cache would show a stale or empty ledger. This process's own\n * un-flushed records are layered on top so the UI never lags behind its own\n * activity.\n *\n * Called once per UI open/refresh, never on the generation hot path, so the\n * read is cheap enough.\n */\n snapshot(): StatsDocument {\n const now = this.now()\n const doc = this.readFile()\n if (this.pending.length > 0) {\n for (const record of this.pending) applyRecord(doc, record, now)\n }\n return doc\n }\n\n /** The most recent flush failure, if any (diagnostics). */\n get flushError(): unknown {\n return this.lastFlushError\n }\n\n /** Number of records awaiting flush. */\n get pendingCount(): number {\n return this.pending.length\n }\n\n /** Whether the ledger is currently failing to persist (a run of failures). */\n get flushFailing(): boolean {\n return this.flushFailedSince !== undefined\n }\n}\n","/**\n * Persisted recent-activity ring: `$DSH_HOME/agy/agy-recent.json`.\n *\n * The in-memory ring in `UsageStats` answers \"just now\" but dies with the\n * process; the ledger answers \"in aggregate\" only. This store is the bridge:\n * it persists the SAME flattened records to their own file so history survives\n * a restart, and cross-process history becomes visible (Desktop, a web-profile\n * server, and a CLI each merge into one file).\n *\n * Why a SEPARATE file rather than a `recent` field in `agy-stats.json`: the\n * ledger's document is versioned and migrated (`parseStatsDocument`); folding\n * an evicting list into it turns every prune into a document-shape question.\n * A flat list in its own file needs no version and no migration — a record we\n * cannot parse is simply dropped.\n *\n * Bounded by construction: at most `RECENT_MAX` (200) entries, oldest evicted,\n * so the file caps at roughly 40–60KB forever.\n *\n * Flush triggers, whichever comes first: 25 pending records, or 30s with\n * anything pending, plus `flushSync()` on process exit. No size trigger —\n * entries are fixed-shape, so size is a function of count and the count IS the\n * cap. Worst loss window on a hard crash: ≤30s of records.\n *\n * The merge under the lock re-reads the fresh on-disk list, appends this\n * process's batch, dedupes on the CAPTURE key (`at|account|model|kind|output`\n * — two writers can flush the same capture window, and a duplicated rotation\n * row is exactly the confusion this panel exists to end), sorts by `at`, and\n * truncates to `RECENT_MAX`. Atomic tmp+rename, 0600.\n *\n * Privacy surface is the ledger's: account emails and model ids only — never a\n * token, a proxy URL, or a project id.\n */\n\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport lockfile from 'proper-lockfile'\nimport { RECENT_MAX } from './stats.ts'\nimport type { RecentActivity } from './stats.ts'\nimport { migrateToAgyDir } from './store/paths.ts'\n\n/** Default filename inside the agy data dir. */\nexport const RECENT_FILE_NAME = 'agy-recent.json'\n\n/** Flush after this many pending records. */\nexport const RECENT_FLUSH_EVERY = 25\n\n/** Flush after this many ms with pending records. */\nexport const RECENT_FLUSH_INTERVAL_MS = 30_000\n\n/** Capture-key dedupe: the fields two processes writing the same capture share. */\nfunction captureKey(entry: RecentActivity): string {\n return `${entry.at}|${entry.account ?? ''}|${entry.model ?? ''}|${entry.kind}|${entry.output ?? ''}`\n}\n\n/**\n * Coerce one parsed JSON value into a `RecentActivity`, or null when it is not\n * one. No version field: a record this build cannot parse is dropped, which\n * for a bounded diagnostics list is the whole migration story.\n */\nexport function parseRecentActivity(raw: unknown): RecentActivity | null {\n if (typeof raw !== 'object' || raw === null) return null\n const r = raw as Record<string, unknown>\n const kinds = ['chat', 'cli', 'verify', 'test', 'rotation'] as const\n if (typeof r.at !== 'number' || !Number.isFinite(r.at)) return null\n if (!kinds.includes(r.kind as (typeof kinds)[number])) return null\n if (typeof r.ok !== 'boolean') return null\n const nullableString = (v: unknown): string | null => (typeof v === 'string' && v.length > 0 ? v : null)\n const nullableNumber = (v: unknown): number | null =>\n (typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : null)\n return {\n at: r.at,\n account: nullableString(r.account),\n model: nullableString(r.model),\n kind: r.kind as RecentActivity['kind'],\n ok: r.ok,\n rateLimited: r.rateLimited === true,\n latencyMs: nullableNumber(r.latencyMs),\n ttftMs: nullableNumber(r.ttftMs),\n output: nullableNumber(r.output),\n reason: nullableString(r.reason),\n }\n}\n\n/** Merge one batch into an on-disk list (pure; the caller owns the lock and the write). */\nexport function mergeRecentEntries(\n onDisk: RecentActivity[],\n batch: RecentActivity[],\n cap: number = RECENT_MAX,\n): RecentActivity[] {\n const byKey = new Map<string, RecentActivity>()\n for (const entry of [...onDisk, ...batch]) byKey.set(captureKey(entry), entry)\n return [...byKey.values()]\n .sort((a, b) => a.at - b.at)\n .slice(-cap)\n}\n\n/** Same file-lock seam shape as `StatsLock`: sync, no retries (see stats.ts). */\nexport interface RecentLock {\n withLock<T>(file: string, fn: () => T): T\n}\n\nexport const properRecentLock: RecentLock = {\n withLock<T>(file: string, fn: () => T): T {\n // Same parameter set as `properStatsLock`, for the same reasons: no\n // `realpath` (the file may be absent on first write), a short `stale` so a\n // SIGKILLed process's lock directory cannot black out the next half minute.\n const release = lockfile.lockSync(file, { stale: 8_000, update: 4_000, realpath: false })\n try {\n return fn()\n } finally {\n release()\n }\n },\n}\n\nexport const noopRecentLock: RecentLock = { withLock: (_file, fn) => fn() }\n\nexport interface RecentActivityStoreOptions {\n /** Defaults to `$DSH_HOME/agy/agy-recent.json` (migrating any legacy file). */\n file?: string\n lock?: RecentLock\n /** Injectable clock, for tests. */\n now?: () => number\n flushEvery?: number\n flushIntervalMs?: number\n /**\n * Called once per run of consecutive flush failures, with the first error —\n * the same soft-fail contract as the ledger's `onFlushError`. Absent means\n * failures are silent (acceptable for a diagnostics ring; the plugin entry\n * always supplies one).\n */\n onFlushError?: (error: unknown) => void\n}\n\n/**\n * Persists the recent-activity ring across processes and restarts.\n *\n * The plugin constructs it BEFORE any record can arrive, seeds the in-memory\n * ring from the file, and hands it to `UsageStats` as the persistence half;\n * `record()` in `UsageStats` stays I/O-free — it appends to this store's\n * pending list, and flushing happens on the triggers above.\n */\nexport class RecentActivityStore {\n private readonly file: string\n private readonly lock: RecentLock\n private readonly now: () => number\n private readonly flushEvery: number\n private readonly flushIntervalMs: number\n private readonly onFlushError: ((error: unknown) => void) | undefined\n\n /** The seeded ring plus this process's un-flushed captures. */\n private entries: RecentActivity[] = []\n private pending: RecentActivity[] = []\n private timer: ReturnType<typeof setTimeout> | undefined\n private flushFailedSince: number | undefined\n private lastFlushError: unknown\n\n constructor(options: RecentActivityStoreOptions = {}) {\n this.file = options.file ?? migrateToAgyDir(RECENT_FILE_NAME).file\n this.lock = options.lock ?? properRecentLock\n this.now = options.now ?? (() => Date.now())\n this.flushEvery = options.flushEvery ?? RECENT_FLUSH_EVERY\n this.flushIntervalMs = options.flushIntervalMs ?? RECENT_FLUSH_INTERVAL_MS\n this.onFlushError = options.onFlushError\n // Seed from disk BEFORE the first capture: `pool.recent` then shows the\n // other processes' history immediately. A corrupt file degrades to empty.\n try {\n const parsed = JSON.parse(readFileSync(this.file, 'utf8')) as unknown\n if (Array.isArray(parsed)) {\n for (const raw of parsed) {\n const entry = parseRecentActivity(raw)\n if (entry) this.entries.push(entry)\n }\n if (this.entries.length > RECENT_MAX) {\n this.entries.splice(0, this.entries.length - RECENT_MAX)\n }\n }\n } catch {\n // Missing or unreadable file: an empty ring is the correct start state.\n }\n }\n\n /** Path of the backing file. */\n get path(): string {\n return this.file\n }\n\n /** Whether the ring is currently failing to persist (a run of failures). */\n get flushFailing(): boolean {\n return this.flushFailedSince !== undefined\n }\n\n /** Number of records awaiting flush. */\n get pendingCount(): number {\n return this.pending.length\n }\n\n /**\n * Capture one flattened record and the I/O-free hot-path append.\n *\n * `UsageStats.record()` calls this after folding into its own ring; keeping\n * the write-behind state HERE (not in UsageStats) is what lets the file have\n * its own triggers, its own lock, and its own failure story without widening\n * the ledger's.\n */\n capture(entry: RecentActivity): void {\n this.entries.push(entry)\n if (this.entries.length > RECENT_MAX) {\n this.entries.splice(0, this.entries.length - RECENT_MAX)\n }\n this.pending.push(entry)\n if (this.flushEvery > 0 && this.pending.length >= this.flushEvery) {\n this.flush()\n return\n }\n this.armTimer()\n }\n\n /**\n * Newest-first copy of the ring, for the `pool.recent` RPC: the seeded\n * history plus everything this process captured since boot.\n */\n recentRequests(): RecentActivity[] {\n return [...this.entries].reverse()\n }\n\n private armTimer(): void {\n if (this.timer !== undefined || this.flushIntervalMs <= 0) return\n this.timer = setTimeout(() => {\n this.timer = undefined\n this.flush()\n }, this.flushIntervalMs)\n // Never hold the process open for a diagnostics flush.\n this.timer.unref?.()\n }\n\n /**\n * Merge pending captures into the persisted file.\n *\n * Same merge discipline as the ledger: re-read under the lock, merge by\n * capture key (two processes replaying the same window must not duplicate\n * rows), sort by time, cap. A failed flush puts the batch back — a\n * diagnostics ring never breaks a generation — and reports once per run.\n */\n flush(): void {\n if (this.timer !== undefined) {\n clearTimeout(this.timer)\n this.timer = undefined\n }\n const batch = this.pending\n if (batch.length === 0) return\n this.pending = []\n const now = this.now()\n try {\n if (!existsSync(this.file)) {\n mkdirSync(dirname(this.file), { recursive: true })\n writeFileSync(this.file, '[]\\n', { mode: 0o600 })\n }\n this.lock.withLock(this.file, () => {\n const onDisk: RecentActivity[] = []\n try {\n const parsed = JSON.parse(readFileSync(this.file, 'utf8')) as unknown\n if (Array.isArray(parsed)) {\n for (const raw of parsed) {\n const entry = parseRecentActivity(raw)\n if (entry) onDisk.push(entry)\n }\n }\n } catch {\n // A corrupt file loses its history rather than this process's batch.\n }\n const merged = mergeRecentEntries(onDisk, batch)\n const tmp = `${this.file}.tmp`\n writeFileSync(tmp, JSON.stringify(merged, null, 2) + '\\n', { mode: 0o600 })\n renameSync(tmp, this.file)\n })\n this.lastFlushError = undefined\n this.flushFailedSince = undefined\n } catch (error) {\n this.pending = [...batch, ...this.pending]\n this.lastFlushError = error\n if (this.flushFailedSince === undefined) {\n this.flushFailedSince = now\n try {\n this.onFlushError?.(error)\n } catch {\n // The reporter must never break a flush path.\n }\n }\n this.armTimer()\n }\n }\n\n /** Flush if anything is pending; called on process exit. */\n flushSync(): void {\n this.flush()\n }\n\n /** The most recent flush failure, if any (diagnostics). */\n get flushError(): unknown {\n return this.lastFlushError\n }\n}\n","/**\n * Shared runtime construction for the in-harness plugin entries: master-key\n * codec resolution (credentials seam first, credentials document fallback),\n * account store, session manager, and adapter. Used by the main plugin\n * (adapter registration) and the web plugin (route registration) so both\n * entries operate on the same store.\n */\n\nimport { randomBytes } from 'node:crypto'\nimport type { Context } from '@deepseek-ai/cordis'\nimport { AgyAdapter } from './adapter/adapter.ts'\nimport type { AgyAttachmentStore } from './adapter/adapter.ts'\nimport { AGY_PROVIDER } from './adapter/models.ts'\nimport { ModelVisibility } from './model-visibility.ts'\nimport { ThinkingBudgetStore } from './thinking-budget.ts'\nimport { UsageStats } from './stats.ts'\nimport { RecentActivityStore } from './recent-store.ts'\nimport { probeFetch, proxiedFetch } from './proxy.ts'\nimport { pickProbeProxyUrl } from './runtime/rotation.ts'\nimport { resolveAntigravityVersion } from './runtime/version.ts'\nimport type { FetchLike } from './runtime/version.ts'\nimport { AgySessionManager } from './session.ts'\nimport { JsonAccountStore } from './store/accounts.ts'\nimport type { AccountStore } from './store/accounts.ts'\nimport {\n MASTER_KEY_REF,\n createAesGcmCodec,\n deriveKey,\n loadMasterKey,\n persistMasterKey,\n resolveDshHome,\n resolveMasterKeyCodec,\n} from './store/keyring.ts'\nimport type { SecretCodec } from './store/keyring.ts'\nimport { migrateToAgyDir } from './store/paths.ts'\n\nexport interface CredentialsSeam {\n resolve(ref: string): Promise<{ value: string } | undefined>\n set(ref: string, value: string): Promise<void>\n}\n\nfunction codecFrom(masterKey: string): SecretCodec {\n return createAesGcmCodec(deriveKey(masterKey))\n}\n\n/** Resolve or create the master key, preferring the credentials seam when available. */\nexport async function resolveCodec(ctx: Context): Promise<{ codec: SecretCodec; created: boolean }> {\n const dshHome = resolveDshHome()\n const credentials = ctx.get('credentials') as CredentialsSeam | undefined\n\n if (credentials) {\n const resolved = await credentials.resolve(MASTER_KEY_REF)\n if (resolved) return { codec: codecFrom(resolved.value), created: false }\n const fileKey = loadMasterKey(dshHome)\n if (fileKey) return { codec: codecFrom(fileKey), created: false }\n const fresh = randomBytes(32).toString('hex')\n try {\n await credentials.set(MASTER_KEY_REF, fresh)\n return { codec: codecFrom(fresh), created: true }\n } catch {\n // Read-only shadowing: persist to the credentials document directly.\n persistMasterKey(dshHome, fresh)\n return { codec: codecFrom(fresh), created: true }\n }\n }\n\n return resolveMasterKeyCodec(dshHome)\n}\n\n/**\n * Fire-and-forget version warm-up so fingerprint generation inside the\n * rate-limit path never waits on a cold feed.\n *\n * Routed through `pickProbeProxyUrl`: the release feeds are not account-scoped,\n * but the host IP is exactly what a per-account-proxy user asked to hide, and a\n * bare `proxiedFetch` sends this boot-time request directly. Never throws — an\n * unreadable store or a dead feed must not be the reason plugin boot fails.\n */\nasync function warmVersionCache(store: AccountStore): Promise<void> {\n let fetchImpl: FetchLike = proxiedFetch\n try {\n const storage = await store.load()\n const proxyUrl = pickProbeProxyUrl(storage.accounts, storage.activeIndex)\n fetchImpl = probeFetch(proxyUrl)\n } catch {\n // Fall through to the env/direct route.\n }\n await resolveAntigravityVersion(fetchImpl).catch(() => {})\n}\n\n/** The runtime one plugin entry uses: store, sessions, adapter, and the caches. */\nexport interface AgyRuntime {\n store: AccountStore\n sessions: AgySessionManager\n adapter: AgyAdapter\n stats: UsageStats\n recentStore: RecentActivityStore\n modelVisibility: ModelVisibility\n thinkingBudget: ThinkingBudgetStore\n}\n\n/**\n * The process-wide runtime promise, shared by both entry points.\n *\n * Splitting it was a real defect, not a stylistic one: the ledger FILE merges\n * across instances, so counters looked fine, but every in-memory surface is\n * per instance — the web entry's ring and in-flight map could never see the\n * main plugin's chat traffic (same process, two rings), and `account.activate`\n * cleared affinity pins on the WRONG session manager while the serving one\n * kept its stale pin. Both entries call this in one process; one memo here is\n * what makes that \"shared runtime\" claim true. Separate profiles are separate\n * processes, so the memo never bridges compositions.\n */\nlet sharedRuntime: Promise<AgyRuntime> | undefined\n\n/** Build (once per process) and return the runtime both entries share. */\nexport async function createAgyRuntime(ctx: Context): Promise<AgyRuntime> {\n if (sharedRuntime === undefined) {\n sharedRuntime = buildAgyRuntime(ctx).catch((error) => {\n // A failed build must not poison every later activation: drop the memo\n // so the next entry (or a retried apply) builds fresh.\n sharedRuntime = undefined\n throw error\n })\n }\n return sharedRuntime\n}\n\n/** Test-only: drop the process memo so a test builds an isolated runtime. */\nexport function _resetAgyRuntimeForTest(): void {\n sharedRuntime = undefined\n}\n\nasync function buildAgyRuntime(ctx: Context): Promise<AgyRuntime> {\n const { codec } = await resolveCodec(ctx)\n const dshHome = resolveDshHome()\n // Data files live in `$DSH_HOME/agy/`; the constructor-side migrations below\n // each move their own legacy file once. One warn here covers the skew case\n // for the whole runtime: every store's result is checked, and the message is\n // the same shape, so callers see ONE diagnosis instead of five.\n const accountsMigrated = migrateToAgyDir('agy-accounts.json', dshHome)\n if (accountsMigrated.skew) {\n ctx.logger.warn(\n '[dsh-agy] legacy data file detected next to the agy folder — an older dsh-agy '\n + 'process may still be writing the pre-0.5 layout; its writes will not be seen '\n + 'until every dsh surface is restarted',\n )\n }\n const store = new JsonAccountStore({ file: accountsMigrated.file, codec })\n void warmVersionCache(store)\n // The persisted recent-activity ring, built FIRST so it seeds from disk\n // before any record can arrive and `pool.recent` shows the other processes'\n // history immediately. Same soft-fail reporting contract as the ledger.\n const recentStore = new RecentActivityStore({\n onFlushError: (error) => {\n ctx.logger.warn(\n `[dsh-agy] recent-activity file could not be written (${recentStore.path}): `\n + `${error instanceof Error ? error.message : String(error)} — entries are buffered and will retry`,\n )\n },\n })\n // The ledger reports the first failure of a run, once. Without it a ledger\n // that cannot be written (read-only $DSH_HOME, ENOSPC) simply stops counting\n // with nothing anywhere to explain why — the failure mode this whole\n // soft-fail design otherwise hides.\n const stats = new UsageStats({\n // The ring's persistence half: every captured record is handed to the\n // store (write-behind there, still I/O-free here).\n persistRecent: (entry) => { recentStore.capture(entry) },\n onFlushError: (error) => {\n ctx.logger.warn(\n `[dsh-agy] usage ledger could not be written (${stats.path}): `\n + `${error instanceof Error ? error.message : String(error)} — counts are buffered and will retry`,\n )\n },\n })\n const modelVisibility = new ModelVisibility()\n const thinkingBudget = new ThinkingBudgetStore()\n // The adapter's model-list cache is keyed to no account (see\n // `AgyAdapter.invalidateModelCache`), so every event that can switch the\n // account discovery rides must drop it. The adapter does not exist yet when\n // the session manager is constructed, hence the late-bound reference.\n let invalidateModelCache: () => void = () => {}\n const sessions = new AgySessionManager({\n store,\n recordUsage: (record) => { stats.record(record) },\n onRotate: () => { invalidateModelCache() },\n })\n // Optional background health probe (DSH_AGY_HEALTH_INTERVAL_MS), off by default.\n const healthIntervalMs = Number(process.env.DSH_AGY_HEALTH_INTERVAL_MS ?? 0)\n if (Number.isFinite(healthIntervalMs) && healthIntervalMs > 0) {\n sessions.startHealthProbe(healthIntervalMs)\n }\n const adapter = new AgyAdapter({\n getSession: (model, conversationKey) => sessions.getSession(model, undefined, conversationKey),\n reportFailure: (kind, session, info) => sessions.reportFailure(kind, session, info),\n markSuccess: (session) => sessions.markSuccess(session),\n resolveAttachments: () => ctx.get('attachments') as AgyAttachmentStore | undefined,\n noteRequestStarted: (account) => sessions.noteRequestStarted(account),\n noteRequestSettled: (account) => sessions.noteRequestSettled(account),\n modelVisibility,\n thinkingBudgetFor: (level) => thinkingBudget.budgetFor(level),\n claudeBudgetFor: () => thinkingBudget.claudeBudget(),\n tieredBudgetFor: () => thinkingBudget.tieredBudget(),\n recordUsage: (record) => { stats.record({ ...record, source: 'chat' }) },\n })\n invalidateModelCache = () => { adapter.invalidateModelCache() }\n // Warm up the model list in the background so the first model/effort selection\n // in DSH has zero latency and shows no loading spinner.\n void adapter.listAllModels().catch(() => {})\n // Persist the ledger and the recent ring on normal termination. `exit`\n // covers both a graceful shutdown and the CLI's explicit `process.exit`\n // calls.\n //\n // Deliberately NO signal handlers: installing a SIGINT/SIGTERM listener\n // replaces Node's default terminate behavior, so it must flush and then\n // re-raise or Ctrl-C stops working — a real bug for the sake of at most one\n // throttle window (5s) of counts on an interrupted process. The timer in\n // UsageStats/RecentActivityStore already bounds that loss, and these are\n // diagnostics.\n process.once('exit', () => {\n stats.flushSync()\n recentStore.flushSync()\n })\n return { store, sessions, adapter, stats, recentStore, modelVisibility, thinkingBudget }\n}\n\nexport { AGY_PROVIDER }\n"],"mappings":"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"}
|
|
@@ -345,6 +345,7 @@ async function resolveMultimodalFiles(optionsOrMessages, modelOrOptions, extraOp
|
|
|
345
345
|
var translate_exports = /* @__PURE__ */ __exportAll({
|
|
346
346
|
AGY_CLAUDE_MAX_OUTPUT_TOKENS: () => AGY_CLAUDE_MAX_OUTPUT_TOKENS,
|
|
347
347
|
AGY_SCHEMA_ALLOWLIST: () => AGY_SCHEMA_ALLOWLIST,
|
|
348
|
+
coalesceContents: () => coalesceContents,
|
|
348
349
|
isClaudeModel: () => isClaudeModel,
|
|
349
350
|
stripTrailingModelTurn: () => stripTrailingModelTurn,
|
|
350
351
|
supportsMultimodalFiles: () => supportsMultimodalFiles,
|
|
@@ -425,13 +426,54 @@ function sanitizeToolSchema(schema) {
|
|
|
425
426
|
}
|
|
426
427
|
return result;
|
|
427
428
|
}
|
|
428
|
-
/**
|
|
429
|
+
/**
|
|
430
|
+
* Collect tool-call names by id so tool results can name their function.
|
|
431
|
+
*
|
|
432
|
+
* Names are PUSHED rather than set, so a repeated id keeps every occurrence in
|
|
433
|
+
* document order; resolution consumes them FIFO (see {@link resolveToolName}).
|
|
434
|
+
*/
|
|
429
435
|
function buildToolNameIndex(messages) {
|
|
430
436
|
const index = /* @__PURE__ */ new Map();
|
|
431
|
-
for (const message of messages) for (const block of message.content) if (block.type === "tool-call")
|
|
437
|
+
for (const message of messages) for (const block of message.content) if (block.type === "tool-call") {
|
|
438
|
+
const entry = index.get(block.id);
|
|
439
|
+
if (entry) entry.names.push(block.name);
|
|
440
|
+
else index.set(block.id, {
|
|
441
|
+
names: [block.name],
|
|
442
|
+
cursor: 0
|
|
443
|
+
});
|
|
444
|
+
}
|
|
432
445
|
return index;
|
|
433
446
|
}
|
|
434
447
|
/**
|
|
448
|
+
* The name one `functionResponse` must carry, or undefined when the id answers
|
|
449
|
+
* no recorded call (the caller then falls back to the raw id, as it always did).
|
|
450
|
+
*
|
|
451
|
+
* FIFO per id: the calls recorded under an id are consumed in the order the
|
|
452
|
+
* results arrive in, so each result meets its OWN call even when the id
|
|
453
|
+
* repeats. Distinct ids never interact — the two cases a single-valued index
|
|
454
|
+
* conflated (a repeated id across distant turns, and two calls sharing one id
|
|
455
|
+
* inside one assistant turn) are handled by the same queue.
|
|
456
|
+
*
|
|
457
|
+
* Drained (more results than calls — a call dropped from the history while its
|
|
458
|
+
* result survived) is the one case with nothing left to pair: guessing the LAST
|
|
459
|
+
* name recorded for the id beats the pre-fix behaviour in both directions. The
|
|
460
|
+
* old lookup answered every extra result with that same last name, which was
|
|
461
|
+
* right whenever the repeated calls were the same tool and wrong the moment
|
|
462
|
+
* they were not; answering with the raw id instead would be wrong EVERY time,
|
|
463
|
+
* because an id is not a function name and upstream rejects it outright. So the
|
|
464
|
+
* last name is kept, and only a never-recorded id falls through to the raw id.
|
|
465
|
+
*/
|
|
466
|
+
function resolveToolName(toolNames, toolCallId) {
|
|
467
|
+
const entry = toolNames.get(toolCallId);
|
|
468
|
+
if (entry === void 0) return void 0;
|
|
469
|
+
if (entry.cursor < entry.names.length) {
|
|
470
|
+
const name = entry.names[entry.cursor];
|
|
471
|
+
entry.cursor += 1;
|
|
472
|
+
return name;
|
|
473
|
+
}
|
|
474
|
+
return entry.names[entry.names.length - 1];
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
435
477
|
* The single producer of a `functionResponse` part.
|
|
436
478
|
*
|
|
437
479
|
* Both supported dsh-llm vocabularies funnel through here: a 0.1.5
|
|
@@ -441,7 +483,7 @@ function buildToolNameIndex(messages) {
|
|
|
441
483
|
function toolResultPart(toolCallId, isError, content, toolNames) {
|
|
442
484
|
return { functionResponse: {
|
|
443
485
|
id: toolCallId,
|
|
444
|
-
name: toolNames
|
|
486
|
+
name: resolveToolName(toolNames, toolCallId) ?? toolCallId,
|
|
445
487
|
response: {
|
|
446
488
|
result: content.filter((block) => block.type === "text").map((block) => block.text).join("\n"),
|
|
447
489
|
is_error: isError
|
|
@@ -509,6 +551,60 @@ function messageToContent(message, toolNames, images, multimodalFiles, messageIn
|
|
|
509
551
|
};
|
|
510
552
|
}
|
|
511
553
|
/**
|
|
554
|
+
* Whether a part makes its turn a tool-result turn (`functionResponse` family).
|
|
555
|
+
* Every other part kind — text, media, thought, functionCall — belongs to the
|
|
556
|
+
* ordinary-content family.
|
|
557
|
+
*/
|
|
558
|
+
function isFrPart(part) {
|
|
559
|
+
return "functionResponse" in part;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Merge adjacent same-role contents into single turns, functionResponse-family
|
|
563
|
+
* aware.
|
|
564
|
+
*
|
|
565
|
+
* The harness emits a fragmented per-step vocabulary — prompt, runtime-context
|
|
566
|
+
* snapshot, and injected `<system-reminder>` messages as separate `user`
|
|
567
|
+
* messages, one message per tool result — and its own first-party serializer
|
|
568
|
+
* coalesces adjacent same-role messages at the wire boundary. Forwarding the
|
|
569
|
+
* fragments 1:1 instead opened every request with a run of `user` turns that
|
|
570
|
+
* all belong to one logical prompt, and sent parallel tool results as separate
|
|
571
|
+
* one-`functionResponse` turns where the official Gemini tooling groups them
|
|
572
|
+
* into a single user turn.
|
|
573
|
+
*
|
|
574
|
+
* Family rule: a turn carrying `functionResponse` parts never merges with (or
|
|
575
|
+
* receives) other part kinds, so a mixed content is segmented in part order.
|
|
576
|
+
* The harness's serializer instead re-sorts tool results to the front of one
|
|
577
|
+
* merged message, but the Gemini-family validator is measured to reject a user
|
|
578
|
+
* content mixing `functionResponse` with text (400; langchainjs#11445, on the
|
|
579
|
+
* model family), and this channel's tolerance for the mixed shape is
|
|
580
|
+
* unmeasured. Separate adjacent turns keep every part while keeping each turn
|
|
581
|
+
* unmixed; parts are never reordered.
|
|
582
|
+
*
|
|
583
|
+
* Known boundary loss (pinned in tests): a model turn whose parts all drop
|
|
584
|
+
* (thoughts-only turn replayed on the Claude path) is filtered to null before
|
|
585
|
+
* this point, so the user turns around it fuse into one content. A text
|
|
586
|
+
* placeholder would fabricate a model utterance, and the acceptance of a
|
|
587
|
+
* structural empty turn (`parts: []`) is unmeasured here — so the fusion
|
|
588
|
+
* stands (issue #93).
|
|
589
|
+
*/
|
|
590
|
+
function coalesceContents(contents) {
|
|
591
|
+
const result = [];
|
|
592
|
+
let lastFamily;
|
|
593
|
+
for (const content of contents) for (const part of content.parts) {
|
|
594
|
+
const family = isFrPart(part);
|
|
595
|
+
const last = result[result.length - 1];
|
|
596
|
+
if (last && lastFamily === family && last.role === content.role) last.parts.push(part);
|
|
597
|
+
else {
|
|
598
|
+
result.push({
|
|
599
|
+
role: content.role,
|
|
600
|
+
parts: [part]
|
|
601
|
+
});
|
|
602
|
+
lastFamily = family;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return result;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
512
608
|
* Builtin Gemini tools must not shadow functionDeclarations names (upstream
|
|
513
609
|
* treats them as native tools; verified by OmniRoute's GEMINI_BUILTIN_TOOL_NAMES).
|
|
514
610
|
*/
|
|
@@ -585,7 +681,7 @@ function toAgyRequestBody(options, context) {
|
|
|
585
681
|
const images = context.images ?? /* @__PURE__ */ new Map();
|
|
586
682
|
const multimodalFiles = supportsMultimodalFiles(options.model) ? context.multimodalFiles : void 0;
|
|
587
683
|
const claude = isClaudeModel(options.model);
|
|
588
|
-
let contents = conversationMessages(messages).map((message, index) => messageToContent(message, toolNames, images, multimodalFiles, index, claude)).filter((c) => c !== null);
|
|
684
|
+
let contents = coalesceContents(conversationMessages(messages).map((message, index) => messageToContent(message, toolNames, images, multimodalFiles, index, claude)).filter((c) => c !== null));
|
|
589
685
|
if (claude) contents = stripTrailingModelTurn(contents);
|
|
590
686
|
const systemText = [options.system, systemTextFromMessages(messages)].filter((text) => typeof text === "string" && text !== "").join("\n\n");
|
|
591
687
|
const tools = toolsToDeclarations(options.tools);
|
|
@@ -639,4 +735,4 @@ function toAgyRequestBody(options, context) {
|
|
|
639
735
|
//#endregion
|
|
640
736
|
export { setThoughtSignature as i, translate_exports as n, resolveMultimodalFiles as r, toAgyRequestBody as t };
|
|
641
737
|
|
|
642
|
-
//# sourceMappingURL=translate-
|
|
738
|
+
//# sourceMappingURL=translate-Cq6PGO4b.mjs.map
|