zidane 6.4.0 → 6.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +1 -1
  2. package/dist/{acp-DzUgjzNa.js → acp-Ba4OzG71.js} +6 -6
  3. package/dist/{acp-DzUgjzNa.js.map → acp-Ba4OzG71.js.map} +1 -1
  4. package/dist/acp-cli.js +6 -6
  5. package/dist/acp.d.ts +1 -1
  6. package/dist/acp.js +1 -1
  7. package/dist/{agent-COHX5SpM.js → agent-BMJKrD84.js} +26 -12
  8. package/dist/{agent-COHX5SpM.js.map → agent-BMJKrD84.js.map} +1 -1
  9. package/dist/agent.d.ts +1 -1
  10. package/dist/agent.js +1 -1
  11. package/dist/{anthropic-Bw39RApb.js → anthropic-BJhMEi7x.js} +15 -10
  12. package/dist/anthropic-BJhMEi7x.js.map +1 -0
  13. package/dist/{auth-XS60l6Q5.js → auth-C2Hmb0tt.js} +106 -13
  14. package/dist/auth-C2Hmb0tt.js.map +1 -0
  15. package/dist/chat/pure.d.ts +3 -3
  16. package/dist/chat.d.ts +16 -7
  17. package/dist/chat.d.ts.map +1 -1
  18. package/dist/chat.js +5 -5
  19. package/dist/{context-transport-Nb9CVjLJ.js → context-transport-CwJZxs56.js} +3 -3
  20. package/dist/{context-transport-Nb9CVjLJ.js.map → context-transport-CwJZxs56.js.map} +1 -1
  21. package/dist/contexts/daytona.d.ts +1 -1
  22. package/dist/contexts/e2b.d.ts +1 -1
  23. package/dist/{errors-DJUxZg9b.js → errors-BwBxu3zD.js} +27 -2
  24. package/dist/errors-BwBxu3zD.js.map +1 -0
  25. package/dist/errors.d.ts +1 -1
  26. package/dist/errors.js +1 -1
  27. package/dist/eval.d.ts +1 -1
  28. package/dist/eval.js +1 -1
  29. package/dist/extensions.d.ts +1 -1
  30. package/dist/extensions.js +1 -1
  31. package/dist/{fetch-url-_5WKWpbQ.js → fetch-url-B7aSg6Bp.js} +2 -2
  32. package/dist/{fetch-url-_5WKWpbQ.js.map → fetch-url-B7aSg6Bp.js.map} +1 -1
  33. package/dist/headless.d.ts +1 -1
  34. package/dist/headless.js +3 -3
  35. package/dist/{index-1Dr4Yqw1.d.ts → index-Gy9wi80M.d.ts} +189 -8
  36. package/dist/index-Gy9wi80M.d.ts.map +1 -0
  37. package/dist/index.d.ts +3 -3
  38. package/dist/index.js +14 -14
  39. package/dist/{inline-autocomplete-DC2cxprN.js → inline-autocomplete-D0MPvBaS.js} +103 -17
  40. package/dist/inline-autocomplete-D0MPvBaS.js.map +1 -0
  41. package/dist/{login-CPU73khP.js → login-llWTV13a.js} +3 -3
  42. package/dist/{login-CPU73khP.js.map → login-llWTV13a.js.map} +1 -1
  43. package/dist/{mcp-BNg6hUeX.js → mcp-IaEpSauj.js} +4 -4
  44. package/dist/{mcp-BNg6hUeX.js.map → mcp-IaEpSauj.js.map} +1 -1
  45. package/dist/mcp.d.ts +1 -1
  46. package/dist/mcp.js +1 -1
  47. package/dist/{messages-DIxD2UCr.js → messages-D1EryrDs.js} +2 -2
  48. package/dist/{messages-DIxD2UCr.js.map → messages-D1EryrDs.js.map} +1 -1
  49. package/dist/{oauth-CuFPhsHY.js → oauth-bkEOybqm.js} +2 -2
  50. package/dist/{oauth-CuFPhsHY.js.map → oauth-bkEOybqm.js.map} +1 -1
  51. package/dist/{openai-compat-BTbSyQbm.js → openai-compat-0MJq4-g9.js} +192 -19
  52. package/dist/openai-compat-0MJq4-g9.js.map +1 -0
  53. package/dist/output/stream-json.d.ts +1 -1
  54. package/dist/output/terminal.d.ts +1 -1
  55. package/dist/presets.d.ts +1 -1
  56. package/dist/presets.js +1 -1
  57. package/dist/providers/anthropic.d.ts +1 -1
  58. package/dist/providers/anthropic.js +1 -1
  59. package/dist/providers/arcee.d.ts +1 -1
  60. package/dist/providers/arcee.js +1 -1
  61. package/dist/providers/baseten.d.ts +1 -1
  62. package/dist/providers/baseten.js +1 -1
  63. package/dist/providers/cerebras.d.ts +1 -1
  64. package/dist/providers/cerebras.js +1 -1
  65. package/dist/providers/local.d.ts +1 -1
  66. package/dist/providers/local.js +1 -1
  67. package/dist/providers/openai-compat.d.ts +1 -1
  68. package/dist/providers/openai-compat.js +1 -1
  69. package/dist/providers/openai.d.ts +1 -1
  70. package/dist/providers/openai.js +15 -14
  71. package/dist/providers/openai.js.map +1 -1
  72. package/dist/providers/openrouter.d.ts +1 -1
  73. package/dist/providers/openrouter.js +1 -1
  74. package/dist/providers/xai.d.ts +1 -1
  75. package/dist/providers/xai.js +1 -1
  76. package/dist/{providers-BsH1MnhC.js → providers-BSrJx0Tn.js} +4 -4
  77. package/dist/{providers-BsH1MnhC.js.map → providers-BSrJx0Tn.js.map} +1 -1
  78. package/dist/providers.d.ts +2 -2
  79. package/dist/providers.js +6 -6
  80. package/dist/{resolve-BPTA1RpX.js → resolve-CZJxds85.js} +2 -2
  81. package/dist/{resolve-BPTA1RpX.js.map → resolve-CZJxds85.js.map} +1 -1
  82. package/dist/restate.d.ts +1 -1
  83. package/dist/session/sqlite.d.ts +1 -1
  84. package/dist/session/sqlite.js +1 -1
  85. package/dist/{session-DY2LWai4.js → session-B40flcj7.js} +2 -2
  86. package/dist/{session-DY2LWai4.js.map → session-B40flcj7.js.map} +1 -1
  87. package/dist/session.d.ts +1 -1
  88. package/dist/session.js +2 -2
  89. package/dist/{skills-C7eIjWYm.js → skills-Dd6XCi4s.js} +3 -3
  90. package/dist/{skills-C7eIjWYm.js.map → skills-Dd6XCi4s.js.map} +1 -1
  91. package/dist/skills.d.ts +1 -1
  92. package/dist/skills.js +2 -2
  93. package/dist/{stdio-DfWu1rNB.js → stdio-D-02FOVT.js} +2 -2
  94. package/dist/{stdio-DfWu1rNB.js.map → stdio-D-02FOVT.js.map} +1 -1
  95. package/dist/{stdio-loader-mfN5JYns.js → stdio-loader-Byqw5KjJ.js} +2 -2
  96. package/dist/{stdio-loader-mfN5JYns.js.map → stdio-loader-Byqw5KjJ.js.map} +1 -1
  97. package/dist/{timeout-8vSIRjzl.d.ts → timeout-DEaqslLO.d.ts} +25 -2
  98. package/dist/timeout-DEaqslLO.d.ts.map +1 -0
  99. package/dist/{tool-formatters-BaHq343O.d.ts → tool-formatters-CU0memLh.d.ts} +2 -2
  100. package/dist/{tool-formatters-BaHq343O.d.ts.map → tool-formatters-CU0memLh.d.ts.map} +1 -1
  101. package/dist/tools/fetch-url.d.ts +1 -1
  102. package/dist/tools/fetch-url.js +1 -1
  103. package/dist/tools/web-search.d.ts +1 -1
  104. package/dist/tools/web-search.js +2 -2
  105. package/dist/tools.d.ts +1 -1
  106. package/dist/tools.js +1 -1
  107. package/dist/{transcript-anchors-BquAWoGN.d.ts → transcript-anchors-4IdT10xC.d.ts} +2 -2
  108. package/dist/{transcript-anchors-BquAWoGN.d.ts.map → transcript-anchors-4IdT10xC.d.ts.map} +1 -1
  109. package/dist/{transcript-anchors-CWzlwIOr.js → transcript-anchors-CJ4gJn1T.js} +7 -7
  110. package/dist/{transcript-anchors-CWzlwIOr.js.map → transcript-anchors-CJ4gJn1T.js.map} +1 -1
  111. package/dist/tui.d.ts +24 -3
  112. package/dist/tui.d.ts.map +1 -1
  113. package/dist/tui.js +51 -18
  114. package/dist/tui.js.map +1 -1
  115. package/dist/{turn-operations-OQlIx13T.d.ts → turn-operations-B08dE8kH.d.ts} +3 -3
  116. package/dist/{turn-operations-OQlIx13T.d.ts.map → turn-operations-B08dE8kH.d.ts.map} +1 -1
  117. package/dist/types-BIarq1qE.js.map +1 -1
  118. package/dist/types.d.ts +2 -2
  119. package/dist/types.js +1 -1
  120. package/dist/{xai-DnZfHvTz.js → xai-CpPbCGWO.js} +3 -3
  121. package/dist/{xai-DnZfHvTz.js.map → xai-CpPbCGWO.js.map} +1 -1
  122. package/docs/ARCHITECTURE.md +2 -0
  123. package/docs/CHAT.md +1 -1
  124. package/docs/TUI.md +1 -1
  125. package/package.json +8 -8
  126. package/dist/anthropic-Bw39RApb.js.map +0 -1
  127. package/dist/auth-XS60l6Q5.js.map +0 -1
  128. package/dist/errors-DJUxZg9b.js.map +0 -1
  129. package/dist/index-1Dr4Yqw1.d.ts.map +0 -1
  130. package/dist/inline-autocomplete-DC2cxprN.js.map +0 -1
  131. package/dist/openai-compat-BTbSyQbm.js.map +0 -1
  132. package/dist/timeout-8vSIRjzl.d.ts.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"oauth-CuFPhsHY.js","names":[],"sources":["../src/providers/oauth-registry.ts","../src/providers/oauth.ts"],"sourcesContent":["/**\n * Local OAuth provider registry.\n *\n * pi-ai 0.80.8 removed its global OAuth registry (`registerOAuthProvider`,\n * `getOAuthApiKey`, `OAuthProviderInterface`) and the public low-level\n * refresh helpers in favor of canonical `Provider.auth.oauth` objects\n * resolved through a `Models` collection + `CredentialStore`. zidane's\n * credential architecture (shared `.credentials.json`, cross-process refresh\n * locks in `./oauth.ts`, themed login flows in `src/chat/oauth-page/`)\n * predates that and owns storage itself, so this module reproduces the\n * removed registry surface locally with the same semantics.\n *\n * Nothing protocol-level is duplicated here: token refresh for the built-in\n * providers delegates to pi-ai's canonical `auth.oauth.refresh` flows via the\n * public `providers/*` subpaths (lazily imported, so processes that never\n * authenticate don't load provider code).\n */\n\nimport type { OAuthAuth } from '@earendil-works/pi-ai'\nimport type { OAuthCredentials, OAuthLoginCallbacks } from '@earendil-works/pi-ai/oauth'\nimport { errorMessage } from '../errors'\n\n/**\n * Provider-owned OAuth flow. Mirrors the interface pi-ai exported from\n * `@earendil-works/pi-ai/oauth` up to 0.80.6 — kept as a local type so\n * zidane's descriptor seam (`ProviderDescriptor.oauthProvider`) and provider\n * extensions (e.g. `extensions/bolt`) stay source-compatible across the\n * upstream removal.\n */\nexport interface OAuthProviderInterface {\n readonly id: string\n readonly name: string\n\n /** Run the login flow, return credentials to persist. */\n login: (callbacks: OAuthLoginCallbacks) => Promise<OAuthCredentials>\n\n /** Whether login uses a local callback server and supports manual code input. */\n usesCallbackServer?: boolean\n\n /** Refresh expired credentials, return updated credentials to persist. */\n refreshToken: (credentials: OAuthCredentials) => Promise<OAuthCredentials>\n\n /** Convert credentials to an API key string for the provider. */\n getApiKey: (credentials: OAuthCredentials) => string\n}\n\n/**\n * Run a canonical pi-ai `OAuthAuth.refresh` from a bare refresh token.\n * pi-ai's built-in refresh implementations only read `credential.refresh`,\n * so the placeholder access/expires fields are never observed.\n *\n * The canonical result is an `OAuthCredential` tagged `type: 'oauth'`. Strip\n * the tag before returning: refresh results get merged into stored\n * `.credentials.json` entries and `onOAuthRefresh` hook payloads (see\n * `resolveCredentialSource` in `./oauth.ts`), whose documented shape carries\n * zidane's own `kind` tag — letting pi-ai's `type` leak through would\n * silently drift the on-disk shape.\n */\nasync function refreshViaCanonicalFlow(\n providerId: string,\n oauth: OAuthAuth | undefined,\n refreshToken: string,\n): Promise<OAuthCredentials> {\n if (!oauth)\n throw new Error(`pi-ai provider ${providerId} exposes no OAuth flow`)\n const { type: _type, ...credentials }\n = await oauth.refresh({ type: 'oauth', access: '', refresh: refreshToken, expires: 0 })\n return credentials\n}\n\n/**\n * Refresh an Anthropic (Claude Pro/Max) OAuth token. Drop-in replacement for\n * the `refreshAnthropicToken` pi-ai exported until 0.80.6; delegates to the\n * canonical flow on `anthropicProvider().auth.oauth`.\n */\nexport async function refreshAnthropicToken(refreshToken: string): Promise<OAuthCredentials> {\n const { anthropicProvider } = await import('@earendil-works/pi-ai/providers/anthropic')\n return refreshViaCanonicalFlow('anthropic', anthropicProvider().auth.oauth, refreshToken)\n}\n\n/**\n * Refresh an OpenAI Codex (ChatGPT) OAuth token. Drop-in replacement for the\n * removed `refreshOpenAICodexToken`; the canonical flow still derives\n * `accountId` from the refreshed access token's JWT claims, so the returned\n * credentials keep the extra field older callers persisted.\n */\nexport async function refreshOpenAICodexToken(refreshToken: string): Promise<OAuthCredentials> {\n const { openaiCodexProvider } = await import('@earendil-works/pi-ai/providers/openai-codex')\n return refreshViaCanonicalFlow('openai-codex', openaiCodexProvider().auth.oauth, refreshToken)\n}\n\n/**\n * Built-in registry seeds. pi-ai's removed registry pre-seeded Anthropic and\n * OpenAI Codex, and zidane's lazy token refresh (`getOAuthApiKey` inside\n * `resolveOAuthApiKey`) relied on that — so the local registry seeds the same\n * ids. Login is intentionally unsupported here: interactive login always goes\n * through `ProviderDescriptor.oauthProvider` (the themed flows in\n * `src/chat/oauth-page/`), never through the registry.\n */\nfunction builtinProvider(\n id: string,\n name: string,\n refreshToken: (token: string) => Promise<OAuthCredentials>,\n): OAuthProviderInterface {\n return {\n id,\n name,\n login: async () => {\n throw new Error(`OAuth login for ${id} must run through a provider descriptor flow, not the registry`)\n },\n refreshToken: credentials => refreshToken(credentials.refresh),\n getApiKey: credentials => credentials.access,\n }\n}\n\n/**\n * The registry map is anchored on a process-wide `globalThis` slot rather\n * than module state. The pi-ai registry this replaces lived in pi-ai's own\n * module, so every copy of zidane code in a process shared one map — and two\n * copies is a real configuration, not a hypothetical: a host running from\n * `src` while a disk-loaded extension resolves `'zidane'` to `dist` through\n * ordinary node_modules resolution (see `import()` in\n * `src/extensions/discovery.ts`). The extension registers its flow at setup;\n * `resolveOAuthApiKey` looks it up only once a stored token expires — with\n * per-module maps that fails as a deferred \"Unknown OAuth provider\", which is\n * nasty to debug. `Symbol.for` restores the process-wide sharing. Whichever\n * copy touches the slot first seeds the built-ins; the seeds are pure\n * delegations to pi-ai's canonical flows, so first-copy-wins is harmless.\n */\nconst REGISTRY_SLOT = Symbol.for('zidane.oauth-provider-registry')\n\nfunction oauthProviderRegistry(): Map<string, OAuthProviderInterface> {\n const slots = globalThis as { [key: symbol]: Map<string, OAuthProviderInterface> | undefined }\n slots[REGISTRY_SLOT] ??= new Map(\n [\n builtinProvider('anthropic', 'Anthropic (Claude Pro/Max)', refreshAnthropicToken),\n builtinProvider('openai-codex', 'OpenAI (ChatGPT Plus/Pro)', refreshOpenAICodexToken),\n ].map(provider => [provider.id, provider]),\n )\n return slots[REGISTRY_SLOT]\n}\n\n/** Register (or replace) an OAuth provider by id. */\nexport function registerOAuthProvider(provider: OAuthProviderInterface): void {\n oauthProviderRegistry().set(provider.id, provider)\n}\n\n/** Get a registered OAuth provider by id. */\nexport function getOAuthProvider(id: string): OAuthProviderInterface | undefined {\n return oauthProviderRegistry().get(id)\n}\n\n/**\n * Get an API key for a provider from OAuth credentials, refreshing expired\n * tokens through the registered flow. Same contract as the pi-ai original\n * (`null` when no credentials are stored for the provider, throws on unknown\n * provider or refresh failure), except refresh failures keep the underlying\n * reason — the original discarded it, leaving invalid_grant (needs re-login)\n * indistinguishable from a transient network error.\n */\nexport async function getOAuthApiKey(\n providerId: string,\n credentials: Record<string, OAuthCredentials>,\n): Promise<{ newCredentials: OAuthCredentials, apiKey: string } | null> {\n const provider = oauthProviderRegistry().get(providerId)\n if (!provider)\n throw new Error(`Unknown OAuth provider: ${providerId}`)\n\n let creds = credentials[providerId]\n if (!creds)\n return null\n\n if (Date.now() >= creds.expires) {\n try {\n creds = await provider.refreshToken(creds)\n }\n catch (err) {\n throw new Error(`Failed to refresh OAuth token for ${providerId}: ${errorMessage(err)}`, { cause: err })\n }\n }\n\n return { newCredentials: creds, apiKey: provider.getApiKey(creds) }\n}\n","import type { OAuthCredentials } from '@earendil-works/pi-ai/oauth'\nimport type { StreamCallbacks } from '.'\nimport type { OAuthRefreshHookContext } from '../types'\nimport { Buffer } from 'node:buffer'\nimport { existsSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { resolve } from 'node:path'\nimport { writeFileAtomic } from '../atomic-write'\nimport { errorMessage } from '../errors'\nimport { getOAuthApiKey, registerOAuthProvider } from './oauth-registry'\n\ntype GetOAuthApiKey = typeof getOAuthApiKey\n\n/**\n * Resolve the creds-file path at call time rather than module-load time, so\n * env changes between import and first OAuth use (test suites, multi-project\n * CLIs) are honored.\n *\n * Order:\n * 1. `ZIDANE_CREDENTIALS_PATH` — explicit override. The TUI sets this at\n * launch to point at `<storageDir>/credentials.json` so the harness\n * providers find the same file the TUI manages.\n * 2. `~/.credentials.json` — home-anchored default.\n *\n * The historic `cwd/.credentials.json` fallback was deliberately dropped:\n * launching zidane inside an untrusted checkout would let that repo plant a\n * `.credentials.json` and become the credential source (and receive rotated\n * refresh tokens on the next refresh persist). Same trust model as the TUI's\n * legacy-file migration in `chat/credentials.ts`, which only honors the\n * home-anchored location. The only \"trusted\" cwd under that model is `$HOME`\n * itself — where the cwd-relative path resolves to `~/.credentials.json`\n * anyway, so anchoring at home preserves the legitimate legacy case.\n */\nexport function credentialsFilePath(): string {\n return process.env.ZIDANE_CREDENTIALS_PATH ?? resolve(homedir(), '.credentials.json')\n}\n\n/**\n * POSIX mode for credentials on disk. Read+write for the owner only — refresh\n * tokens and API keys leak if the file is world-readable on a shared box.\n * Ignored on Windows.\n */\nconst CREDENTIALS_FILE_MODE = 0o600\n\n/**\n * In-process mutex table keyed by provider id. Concurrent `resolveOAuthApiKey`\n * calls for the same provider (typical when a host fans out multiple streams\n * at once) coalesce onto a single refresh. Without this, N concurrent calls\n * each hit `getOAuthApiKey`, producing N writes to `.credentials.json` with\n * last-write-wins semantics — and, in worst-case interleaving, a corrupted\n * file.\n */\nconst refreshLocks = new Map<string, Promise<string>>()\n\n/**\n * Register a custom OAuth flow with the local registry (`./oauth-registry`)\n * so lazy refresh through {@link resolveOAuthApiKey} can resolve `providerId`.\n *\n * Public because provider extensions need it: without registration,\n * `getOAuthApiKey('<id>', …)` throws `Unknown OAuth provider` the first time a\n * stored token expires. Idempotent per id (the registry is a keyed map). Kept\n * async for API stability — it awaited a dynamic pi-ai import before pi-ai\n * 0.80.8 removed its global registry and this moved in-repo.\n */\nexport async function registerCustomOAuthProvider(\n provider: import('./oauth-registry').OAuthProviderInterface,\n): Promise<void> {\n registerOAuthProvider(provider)\n}\n\nexport interface OAuthParams {\n apiKey?: string\n access?: string\n refresh?: string\n expires?: number\n}\n\n/**\n * Pull a complete `{ access, refresh, expires }` triple out of provider params\n * when (and only when) all three OAuth fields are present and well-typed.\n * Returns `undefined` for the static-api-key path. Shared by the OAuth-capable\n * providers so the runtime-credential guard can't drift between them.\n */\n/**\n * Whether a credential string is an OAuth *access token* rather than a real,\n * static API key. Anthropic OAuth tokens are prefixed `sk-ant-oat`; OpenAI\n * Codex OAuth access tokens are JWTs (`eyJ...`). Used to decide whether an\n * env-provided value can be refreshed (OAuth) or must be sent verbatim (API key).\n */\nexport function isOAuthAccessToken(value: string): boolean {\n return value.startsWith('sk-ant-oat') || isJwtLikeAccessToken(value)\n}\n\nfunction isJwtLikeAccessToken(value: string): boolean {\n const parts = value.split('.')\n if (parts.length !== 3 || !parts[0]?.startsWith('eyJ'))\n return false\n try {\n const header = JSON.parse(Buffer.from(parts[0], 'base64url').toString('utf8')) as unknown\n return Boolean(header && typeof header === 'object' && 'alg' in header)\n }\n catch {\n return false\n }\n}\n\nexport function extractRuntimeCredentials(\n params?: OAuthParams,\n): { access: string, refresh: string, expires: number } | undefined {\n if (\n typeof params?.access === 'string'\n && typeof params.refresh === 'string'\n && typeof params.expires === 'number'\n ) {\n return { access: params.access, refresh: params.refresh, expires: params.expires }\n }\n return undefined\n}\n\nexport interface ResolveOAuthApiKeyOptions<TParams extends OAuthParams = OAuthParams> {\n provider: string\n providerId: string\n params?: TParams\n envKey?: string\n extraCredentialKeys?: (keyof TParams & string)[]\n missingError: string\n refreshError: (reason: string) => string\n readCredentials?: () => Record<string, OAuthCredentials | undefined>\n writeCredentials?: (credentials: Record<string, OAuthCredentials | undefined>) => void\n getOAuthApiKey?: GetOAuthApiKey\n}\n\n/**\n * Read the shared credentials file.\n *\n * Returns `{}` when the file is missing OR when the file exists but is corrupted\n * / not valid JSON. Corrupt-file tolerance is important: the refresh-then-persist\n * path below writes atomically, but older builds wrote in place and could have\n * truncated the file on a crash. Treating corruption as \"no stored creds\" lets\n * users re-auth without manually deleting the file.\n *\n * Supports two on-disk shapes:\n * 1. **Legacy** (untagged): `{ \"anthropic\": { access, refresh, expires, ... } }`\n * — written by older `bun run auth`.\n * 2. **New** (kind-tagged): `{ \"anthropic\": { kind: 'oauth', access, refresh, expires, ... } }`\n * — written by the TUI. Entries with `kind: 'apikey'` are skipped — those\n * reach providers via env vars set at TUI launch (see `applyApiKeyEnv`).\n */\nexport function readOAuthCredentials(): Record<string, OAuthCredentials | undefined> {\n const path = credentialsFilePath()\n if (!existsSync(path))\n return {}\n\n try {\n const raw = readFileSync(path, 'utf-8')\n const parsed = JSON.parse(raw)\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))\n return {}\n const result: Record<string, OAuthCredentials | undefined> = {}\n for (const [key, value] of Object.entries(parsed)) {\n if (!value || typeof value !== 'object' || Array.isArray(value))\n continue\n const v = value as Record<string, unknown>\n // New schema: explicit `kind` tag.\n if (v.kind === 'apikey')\n continue\n // Both new (`kind: 'oauth'`) and legacy (untagged) entries have `access`;\n // the harmless `kind` field is allowed by `OAuthCredentials`'s\n // `[key: string]: unknown` index signature. Validate the required\n // fields explicitly so the assignment doesn't need a double-cast.\n if (\n typeof v.access === 'string'\n && typeof v.refresh === 'string'\n && typeof v.expires === 'number'\n ) {\n result[key] = { ...v, access: v.access, refresh: v.refresh, expires: v.expires }\n }\n }\n return result\n }\n catch {\n return {}\n }\n}\n\n/**\n * Cross-process lockfile parameters for the credentials read-merge-write.\n * A holder normally completes in well under a millisecond (read + JSON merge\n * + atomic write of a tiny file), so the stale threshold is generous and the\n * total wait short — on timeout we proceed without the lock rather than fail\n * the refresh (degrades to the historic last-write-wins behavior).\n */\nconst CREDENTIALS_LOCK_STALE_MS = 5000\nconst CREDENTIALS_LOCK_RETRY_MS = 20\nconst CREDENTIALS_LOCK_MAX_WAIT_MS = 2000\n\n/** Synchronous sleep without spinning — works on Bun and Node main threads. */\nfunction sleepSync(ms: number): void {\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)\n}\n\n/**\n * Best-effort exclusive lock via `O_EXCL` create of `<file>.lock`. Returns\n * `true` when the lock was acquired (caller must release), `false` on\n * timeout (caller proceeds unlocked — see threshold rationale above). Locks\n * older than {@link CREDENTIALS_LOCK_STALE_MS} are treated as leftovers from\n * a crashed process and broken.\n */\nfunction acquireCredentialsLock(lockPath: string): boolean {\n const deadline = Date.now() + CREDENTIALS_LOCK_MAX_WAIT_MS\n while (true) {\n try {\n writeFileSync(lockPath, String(process.pid), { flag: 'wx', mode: CREDENTIALS_FILE_MODE })\n return true\n }\n catch (err) {\n // Only EEXIST means \"lock held\" — anything else (EPERM, ENOENT for a\n // missing parent dir, read-only fs) won't resolve by waiting, so bail\n // immediately and let the subsequent write surface the real error.\n if ((err as NodeJS.ErrnoException).code !== 'EEXIST')\n return false\n try {\n const age = Date.now() - statSync(lockPath).mtimeMs\n if (age > CREDENTIALS_LOCK_STALE_MS) {\n unlinkSync(lockPath)\n continue\n }\n }\n catch { /* lock vanished between create-fail and stat — retry */ }\n if (Date.now() >= deadline)\n return false\n sleepSync(CREDENTIALS_LOCK_RETRY_MS)\n }\n }\n}\n\nfunction releaseCredentialsLock(lockPath: string): void {\n try {\n unlinkSync(lockPath)\n }\n catch { /* already gone */ }\n}\n\n/**\n * Cross-process lock parameters for the REFRESH critical section (read →\n * check expiry → refresh → persist), held per provider around file-backed\n * resolves in {@link resolveOAuthApiKey}.\n *\n * This is a different beast from the write lock above: the holder performs a\n * network call, so it can legitimately hold the lock for seconds. Hence its\n * own generous thresholds — and async waiting, because blocking the event\n * loop for a peer's refresh round-trip is not acceptable. The stakes are\n * higher too: providers with single-use rotating refresh tokens (Bolt) treat\n * a replayed token as theft and revoke the WHOLE grant, so two processes\n * refreshing the same provider concurrently don't just race a write — they\n * self-revoke. A process that loses the race re-reads under the lock, finds\n * the winner's fresh token, and skips its own refresh entirely.\n *\n * On timeout we proceed without the lock (degrades to the pre-lock racy\n * behavior) rather than fail a resolve that would probably have succeeded.\n */\nconst REFRESH_LOCK_STALE_MS = 60_000\nconst REFRESH_LOCK_RETRY_MS = 50\nconst REFRESH_LOCK_MAX_WAIT_MS = 30_000\n\nfunction refreshLockPath(providerId: string): string {\n return `${credentialsFilePath()}.refresh-${providerId.replace(/[^\\w.-]/g, '_')}.lock`\n}\n\n/** Async sibling of {@link acquireCredentialsLock} with refresh-scale thresholds. */\nasync function acquireRefreshLock(lockPath: string): Promise<boolean> {\n const deadline = Date.now() + REFRESH_LOCK_MAX_WAIT_MS\n while (true) {\n try {\n writeFileSync(lockPath, String(process.pid), { flag: 'wx', mode: CREDENTIALS_FILE_MODE })\n return true\n }\n catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'EEXIST')\n return false\n try {\n const age = Date.now() - statSync(lockPath).mtimeMs\n if (age > REFRESH_LOCK_STALE_MS) {\n unlinkSync(lockPath)\n continue\n }\n }\n catch { /* lock vanished between create-fail and stat — retry */ }\n if (Date.now() >= deadline)\n return false\n await new Promise(resolve => setTimeout(resolve, REFRESH_LOCK_RETRY_MS))\n }\n }\n}\n\n/**\n * Atomically write `.credentials.json` with mode 0o600.\n *\n * Writes to a sibling temp file first, then renames. `rename(2)` is atomic on\n * the same filesystem, so readers either see the old file or the new one —\n * never a half-written one. This matters when a host has multiple processes\n * pointed at the same creds file.\n *\n * Merges the incoming OAuth entries on top of the raw file contents so that\n * entries this layer doesn't manage (e.g. `kind: 'apikey'` from the TUI\n * wizard) survive an OAuth refresh cycle. Without this merge,\n * `readOAuthCredentials` filters those entries out, and a naive overwrite\n * would delete them.\n *\n * The read-merge-write runs under a best-effort cross-process lockfile\n * (`<file>.lock`) so two processes refreshing different providers at the\n * same moment can't drop each other's rotated refresh tokens. On lock\n * timeout the write proceeds unlocked (last-write-wins, the pre-lock\n * behavior) rather than failing the refresh.\n */\nexport function writeOAuthCredentials(credentials: Record<string, OAuthCredentials | undefined>) {\n const path = credentialsFilePath()\n const lockPath = `${path}.lock`\n const locked = acquireCredentialsLock(lockPath)\n\n try {\n let existing: Record<string, unknown> = {}\n try {\n if (existsSync(path)) {\n const raw = readFileSync(path, 'utf-8')\n const parsed = JSON.parse(raw)\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))\n existing = parsed as Record<string, unknown>\n }\n }\n catch { /* corrupt file — start fresh */ }\n\n const merged = { ...existing, ...credentials }\n // Remove keys explicitly set to undefined (provider was deleted).\n for (const [key, value] of Object.entries(merged)) {\n if (value === undefined)\n delete merged[key]\n }\n\n writeFileAtomic(path, JSON.stringify(merged, null, 2), { mode: CREDENTIALS_FILE_MODE })\n }\n finally {\n if (locked)\n releaseCredentialsLock(lockPath)\n }\n}\n\nexport function credentialsFromParams<TParams extends OAuthParams>(params?: TParams, extraKeys: (keyof TParams & string)[] = []): OAuthCredentials | undefined {\n if (typeof params?.access !== 'string' || typeof params.refresh !== 'string' || typeof params.expires !== 'number')\n return undefined\n\n const extras = Object.fromEntries(\n extraKeys\n .map(key => [key, params[key]])\n .filter(([, value]) => value !== undefined),\n )\n\n return {\n access: params.access,\n refresh: params.refresh,\n expires: params.expires,\n ...extras,\n }\n}\n\nexport async function resolveOAuthApiKey<TParams extends OAuthParams>(\n options: ResolveOAuthApiKeyOptions<TParams>,\n callbacks?: Pick<StreamCallbacks, 'onOAuthRefresh'>,\n): Promise<string> {\n if (typeof options.params?.apiKey === 'string')\n return options.params.apiKey\n\n const paramsCredentials = credentialsFromParams(options.params, options.extraCredentialKeys)\n if (paramsCredentials) {\n return await withRefreshLock(\n `params:${options.providerId}`,\n () => resolveCredentialSource('params', paramsCredentials),\n )\n }\n\n if (typeof options.params?.access === 'string')\n return options.params.access\n\n const readCredentials = options.readCredentials ?? readOAuthCredentials\n const writeCredentials = options.writeCredentials ?? writeOAuthCredentials\n // The cross-process refresh lock only means something when we're actually\n // backed by the shared credentials file; injected stores (tests, embedders)\n // have no cross-process story to protect and must not touch real lock files.\n const usingDefaultStore = !options.readCredentials && !options.writeCredentials\n\n // Env key handling. A real API key (e.g. `sk-ant-api…`) is used as-is. But an\n // OAuth *access token* (`sk-ant-oat…`) pasted into the API-key env var can't\n // be refreshed and will 401 once expired — so if refreshable OAuth creds for\n // this provider exist on disk, prefer those (they self-heal via refresh).\n const envValue = options.envKey ? process.env[options.envKey] : undefined\n if (envValue) {\n if (!isOAuthAccessToken(envValue))\n return envValue\n if (!readCredentials()[options.providerId])\n return envValue // no refreshable creds — fall back to the raw token\n return await withRefreshLock(`file:${options.providerId}`, () => withCrossProcessRefreshLock(async () => {\n // Re-read under the locks — a concurrent resolve (in this process or\n // another) may have rotated the refresh token between the pre-check\n // above and lock acquisition, and refreshing with the stale token would\n // invalidate the new one.\n const allCredentials = readCredentials()\n const stored = allCredentials[options.providerId]\n if (!stored)\n return envValue\n return await resolveCredentialSource('file', stored, allCredentials, writeCredentials)\n }))\n }\n\n return await withRefreshLock(`file:${options.providerId}`, () => withCrossProcessRefreshLock(async () => {\n // Read under the locks so we're always operating on the latest file state\n // and never interleave read/refresh/write against another concurrent\n // resolve — in-process (withRefreshLock) or cross-process (the lockfile).\n const allCredentials = readCredentials()\n const storedCredentials = allCredentials[options.providerId]\n if (!storedCredentials)\n throw new Error(options.missingError)\n\n return await resolveCredentialSource('file', storedCredentials, allCredentials, writeCredentials)\n }))\n\n /**\n * Hold the per-provider cross-process lockfile across the WHOLE file-backed\n * critical section, not just the persist. Guarding only the write (as\n * `writeOAuthCredentials` does) still lets two processes both spend a\n * single-use rotating refresh token — which self-revokes providers with\n * replay-detecting rotation. See the {@link REFRESH_LOCK_STALE_MS} note.\n */\n async function withCrossProcessRefreshLock(fn: () => Promise<string>): Promise<string> {\n if (!usingDefaultStore)\n return fn()\n const lockPath = refreshLockPath(options.providerId)\n const locked = await acquireRefreshLock(lockPath)\n try {\n return await fn()\n }\n finally {\n if (locked)\n releaseCredentialsLock(lockPath)\n }\n }\n\n async function resolveCredentialSource(\n source: OAuthRefreshHookContext['source'],\n current: OAuthCredentials,\n allCredentials?: Record<string, OAuthCredentials | undefined>,\n persistCredentials?: (credentials: Record<string, OAuthCredentials | undefined>) => void,\n ): Promise<string> {\n // Only the refresh call itself maps to `refreshError` — a throwing\n // persist or onOAuthRefresh hook after a SUCCESSFUL refresh must not be\n // misreported as a refresh failure, so those run outside the try.\n let result: Awaited<ReturnType<GetOAuthApiKey>>\n try {\n const refreshOAuthApiKey = options.getOAuthApiKey ?? getOAuthApiKey\n result = await refreshOAuthApiKey(options.providerId, { [options.providerId]: current } as Record<string, OAuthCredentials>)\n }\n catch (err) {\n throw new Error(options.refreshError(errorMessage(err)))\n }\n if (!result)\n throw new Error(options.refreshError(options.missingError))\n\n if (result.newCredentials !== current) {\n // pi-ai's refresh returns only `{ access, refresh, expires }`. Merge it\n // onto the current credential so the persisted entry keeps its on-disk\n // `kind: 'oauth'` tag and any provider extras (e.g. openai-codex\n // `accountId`). A bare overwrite strips `kind`, and the TUI's auth-state\n // detection keys on `kind === 'oauth'` — so without this the provider\n // reads as signed out after every token refresh, forcing a daily re-login.\n const refreshed = { ...current, ...result.newCredentials }\n if (source === 'file' && allCredentials && persistCredentials) {\n allCredentials[options.providerId] = refreshed\n persistCredentials(allCredentials)\n }\n\n await callbacks?.onOAuthRefresh?.({\n provider: options.provider,\n providerId: options.providerId,\n source,\n previousCredentials: { ...current },\n credentials: { ...refreshed },\n })\n }\n\n return result.apiKey\n }\n}\n\n/**\n * Coalesce concurrent refresh calls onto a single in-flight promise per key.\n * The key combines the source (`params` vs `file`) and provider id, so a\n * per-agent param-only refresh does not starve a separate file-backed one.\n */\nasync function withRefreshLock(key: string, fn: () => Promise<string>): Promise<string> {\n const existing = refreshLocks.get(key)\n if (existing)\n return existing\n\n const task = (async () => {\n try {\n return await fn()\n }\n finally {\n refreshLocks.delete(key)\n }\n })()\n refreshLocks.set(key, task)\n return task\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"oauth-bkEOybqm.js","names":[],"sources":["../src/providers/oauth-registry.ts","../src/providers/oauth.ts"],"sourcesContent":["/**\n * Local OAuth provider registry.\n *\n * pi-ai 0.80.8 removed its global OAuth registry (`registerOAuthProvider`,\n * `getOAuthApiKey`, `OAuthProviderInterface`) and the public low-level\n * refresh helpers in favor of canonical `Provider.auth.oauth` objects\n * resolved through a `Models` collection + `CredentialStore`. zidane's\n * credential architecture (shared `.credentials.json`, cross-process refresh\n * locks in `./oauth.ts`, themed login flows in `src/chat/oauth-page/`)\n * predates that and owns storage itself, so this module reproduces the\n * removed registry surface locally with the same semantics.\n *\n * Nothing protocol-level is duplicated here: token refresh for the built-in\n * providers delegates to pi-ai's canonical `auth.oauth.refresh` flows via the\n * public `providers/*` subpaths (lazily imported, so processes that never\n * authenticate don't load provider code).\n */\n\nimport type { OAuthAuth } from '@earendil-works/pi-ai'\nimport type { OAuthCredentials, OAuthLoginCallbacks } from '@earendil-works/pi-ai/oauth'\nimport { errorMessage } from '../errors'\n\n/**\n * Provider-owned OAuth flow. Mirrors the interface pi-ai exported from\n * `@earendil-works/pi-ai/oauth` up to 0.80.6 — kept as a local type so\n * zidane's descriptor seam (`ProviderDescriptor.oauthProvider`) and provider\n * extensions (e.g. `extensions/bolt`) stay source-compatible across the\n * upstream removal.\n */\nexport interface OAuthProviderInterface {\n readonly id: string\n readonly name: string\n\n /** Run the login flow, return credentials to persist. */\n login: (callbacks: OAuthLoginCallbacks) => Promise<OAuthCredentials>\n\n /** Whether login uses a local callback server and supports manual code input. */\n usesCallbackServer?: boolean\n\n /** Refresh expired credentials, return updated credentials to persist. */\n refreshToken: (credentials: OAuthCredentials) => Promise<OAuthCredentials>\n\n /** Convert credentials to an API key string for the provider. */\n getApiKey: (credentials: OAuthCredentials) => string\n}\n\n/**\n * Run a canonical pi-ai `OAuthAuth.refresh` from a bare refresh token.\n * pi-ai's built-in refresh implementations only read `credential.refresh`,\n * so the placeholder access/expires fields are never observed.\n *\n * The canonical result is an `OAuthCredential` tagged `type: 'oauth'`. Strip\n * the tag before returning: refresh results get merged into stored\n * `.credentials.json` entries and `onOAuthRefresh` hook payloads (see\n * `resolveCredentialSource` in `./oauth.ts`), whose documented shape carries\n * zidane's own `kind` tag — letting pi-ai's `type` leak through would\n * silently drift the on-disk shape.\n */\nasync function refreshViaCanonicalFlow(\n providerId: string,\n oauth: OAuthAuth | undefined,\n refreshToken: string,\n): Promise<OAuthCredentials> {\n if (!oauth)\n throw new Error(`pi-ai provider ${providerId} exposes no OAuth flow`)\n const { type: _type, ...credentials }\n = await oauth.refresh({ type: 'oauth', access: '', refresh: refreshToken, expires: 0 })\n return credentials\n}\n\n/**\n * Refresh an Anthropic (Claude Pro/Max) OAuth token. Drop-in replacement for\n * the `refreshAnthropicToken` pi-ai exported until 0.80.6; delegates to the\n * canonical flow on `anthropicProvider().auth.oauth`.\n */\nexport async function refreshAnthropicToken(refreshToken: string): Promise<OAuthCredentials> {\n const { anthropicProvider } = await import('@earendil-works/pi-ai/providers/anthropic')\n return refreshViaCanonicalFlow('anthropic', anthropicProvider().auth.oauth, refreshToken)\n}\n\n/**\n * Refresh an OpenAI Codex (ChatGPT) OAuth token. Drop-in replacement for the\n * removed `refreshOpenAICodexToken`; the canonical flow still derives\n * `accountId` from the refreshed access token's JWT claims, so the returned\n * credentials keep the extra field older callers persisted.\n */\nexport async function refreshOpenAICodexToken(refreshToken: string): Promise<OAuthCredentials> {\n const { openaiCodexProvider } = await import('@earendil-works/pi-ai/providers/openai-codex')\n return refreshViaCanonicalFlow('openai-codex', openaiCodexProvider().auth.oauth, refreshToken)\n}\n\n/**\n * Built-in registry seeds. pi-ai's removed registry pre-seeded Anthropic and\n * OpenAI Codex, and zidane's lazy token refresh (`getOAuthApiKey` inside\n * `resolveOAuthApiKey`) relied on that — so the local registry seeds the same\n * ids. Login is intentionally unsupported here: interactive login always goes\n * through `ProviderDescriptor.oauthProvider` (the themed flows in\n * `src/chat/oauth-page/`), never through the registry.\n */\nfunction builtinProvider(\n id: string,\n name: string,\n refreshToken: (token: string) => Promise<OAuthCredentials>,\n): OAuthProviderInterface {\n return {\n id,\n name,\n login: async () => {\n throw new Error(`OAuth login for ${id} must run through a provider descriptor flow, not the registry`)\n },\n refreshToken: credentials => refreshToken(credentials.refresh),\n getApiKey: credentials => credentials.access,\n }\n}\n\n/**\n * The registry map is anchored on a process-wide `globalThis` slot rather\n * than module state. The pi-ai registry this replaces lived in pi-ai's own\n * module, so every copy of zidane code in a process shared one map — and two\n * copies is a real configuration, not a hypothetical: a host running from\n * `src` while a disk-loaded extension resolves `'zidane'` to `dist` through\n * ordinary node_modules resolution (see `import()` in\n * `src/extensions/discovery.ts`). The extension registers its flow at setup;\n * `resolveOAuthApiKey` looks it up only once a stored token expires — with\n * per-module maps that fails as a deferred \"Unknown OAuth provider\", which is\n * nasty to debug. `Symbol.for` restores the process-wide sharing. Whichever\n * copy touches the slot first seeds the built-ins; the seeds are pure\n * delegations to pi-ai's canonical flows, so first-copy-wins is harmless.\n */\nconst REGISTRY_SLOT = Symbol.for('zidane.oauth-provider-registry')\n\nfunction oauthProviderRegistry(): Map<string, OAuthProviderInterface> {\n const slots = globalThis as { [key: symbol]: Map<string, OAuthProviderInterface> | undefined }\n slots[REGISTRY_SLOT] ??= new Map(\n [\n builtinProvider('anthropic', 'Anthropic (Claude Pro/Max)', refreshAnthropicToken),\n builtinProvider('openai-codex', 'OpenAI (ChatGPT Plus/Pro)', refreshOpenAICodexToken),\n ].map(provider => [provider.id, provider]),\n )\n return slots[REGISTRY_SLOT]\n}\n\n/** Register (or replace) an OAuth provider by id. */\nexport function registerOAuthProvider(provider: OAuthProviderInterface): void {\n oauthProviderRegistry().set(provider.id, provider)\n}\n\n/** Get a registered OAuth provider by id. */\nexport function getOAuthProvider(id: string): OAuthProviderInterface | undefined {\n return oauthProviderRegistry().get(id)\n}\n\n/**\n * Get an API key for a provider from OAuth credentials, refreshing expired\n * tokens through the registered flow. Same contract as the pi-ai original\n * (`null` when no credentials are stored for the provider, throws on unknown\n * provider or refresh failure), except refresh failures keep the underlying\n * reason — the original discarded it, leaving invalid_grant (needs re-login)\n * indistinguishable from a transient network error.\n */\nexport async function getOAuthApiKey(\n providerId: string,\n credentials: Record<string, OAuthCredentials>,\n): Promise<{ newCredentials: OAuthCredentials, apiKey: string } | null> {\n const provider = oauthProviderRegistry().get(providerId)\n if (!provider)\n throw new Error(`Unknown OAuth provider: ${providerId}`)\n\n let creds = credentials[providerId]\n if (!creds)\n return null\n\n if (Date.now() >= creds.expires) {\n try {\n creds = await provider.refreshToken(creds)\n }\n catch (err) {\n throw new Error(`Failed to refresh OAuth token for ${providerId}: ${errorMessage(err)}`, { cause: err })\n }\n }\n\n return { newCredentials: creds, apiKey: provider.getApiKey(creds) }\n}\n","import type { OAuthCredentials } from '@earendil-works/pi-ai/oauth'\nimport type { StreamCallbacks } from '.'\nimport type { OAuthRefreshHookContext } from '../types'\nimport { Buffer } from 'node:buffer'\nimport { existsSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { resolve } from 'node:path'\nimport { writeFileAtomic } from '../atomic-write'\nimport { errorMessage } from '../errors'\nimport { getOAuthApiKey, registerOAuthProvider } from './oauth-registry'\n\ntype GetOAuthApiKey = typeof getOAuthApiKey\n\n/**\n * Resolve the creds-file path at call time rather than module-load time, so\n * env changes between import and first OAuth use (test suites, multi-project\n * CLIs) are honored.\n *\n * Order:\n * 1. `ZIDANE_CREDENTIALS_PATH` — explicit override. The TUI sets this at\n * launch to point at `<storageDir>/credentials.json` so the harness\n * providers find the same file the TUI manages.\n * 2. `~/.credentials.json` — home-anchored default.\n *\n * The historic `cwd/.credentials.json` fallback was deliberately dropped:\n * launching zidane inside an untrusted checkout would let that repo plant a\n * `.credentials.json` and become the credential source (and receive rotated\n * refresh tokens on the next refresh persist). Same trust model as the TUI's\n * legacy-file migration in `chat/credentials.ts`, which only honors the\n * home-anchored location. The only \"trusted\" cwd under that model is `$HOME`\n * itself — where the cwd-relative path resolves to `~/.credentials.json`\n * anyway, so anchoring at home preserves the legitimate legacy case.\n */\nexport function credentialsFilePath(): string {\n return process.env.ZIDANE_CREDENTIALS_PATH ?? resolve(homedir(), '.credentials.json')\n}\n\n/**\n * POSIX mode for credentials on disk. Read+write for the owner only — refresh\n * tokens and API keys leak if the file is world-readable on a shared box.\n * Ignored on Windows.\n */\nconst CREDENTIALS_FILE_MODE = 0o600\n\n/**\n * In-process mutex table keyed by provider id. Concurrent `resolveOAuthApiKey`\n * calls for the same provider (typical when a host fans out multiple streams\n * at once) coalesce onto a single refresh. Without this, N concurrent calls\n * each hit `getOAuthApiKey`, producing N writes to `.credentials.json` with\n * last-write-wins semantics — and, in worst-case interleaving, a corrupted\n * file.\n */\nconst refreshLocks = new Map<string, Promise<string>>()\n\n/**\n * Register a custom OAuth flow with the local registry (`./oauth-registry`)\n * so lazy refresh through {@link resolveOAuthApiKey} can resolve `providerId`.\n *\n * Public because provider extensions need it: without registration,\n * `getOAuthApiKey('<id>', …)` throws `Unknown OAuth provider` the first time a\n * stored token expires. Idempotent per id (the registry is a keyed map). Kept\n * async for API stability — it awaited a dynamic pi-ai import before pi-ai\n * 0.80.8 removed its global registry and this moved in-repo.\n */\nexport async function registerCustomOAuthProvider(\n provider: import('./oauth-registry').OAuthProviderInterface,\n): Promise<void> {\n registerOAuthProvider(provider)\n}\n\nexport interface OAuthParams {\n apiKey?: string\n access?: string\n refresh?: string\n expires?: number\n}\n\n/**\n * Pull a complete `{ access, refresh, expires }` triple out of provider params\n * when (and only when) all three OAuth fields are present and well-typed.\n * Returns `undefined` for the static-api-key path. Shared by the OAuth-capable\n * providers so the runtime-credential guard can't drift between them.\n */\n/**\n * Whether a credential string is an OAuth *access token* rather than a real,\n * static API key. Anthropic OAuth tokens are prefixed `sk-ant-oat`; OpenAI\n * Codex OAuth access tokens are JWTs (`eyJ...`). Used to decide whether an\n * env-provided value can be refreshed (OAuth) or must be sent verbatim (API key).\n */\nexport function isOAuthAccessToken(value: string): boolean {\n return value.startsWith('sk-ant-oat') || isJwtLikeAccessToken(value)\n}\n\nfunction isJwtLikeAccessToken(value: string): boolean {\n const parts = value.split('.')\n if (parts.length !== 3 || !parts[0]?.startsWith('eyJ'))\n return false\n try {\n const header = JSON.parse(Buffer.from(parts[0], 'base64url').toString('utf8')) as unknown\n return Boolean(header && typeof header === 'object' && 'alg' in header)\n }\n catch {\n return false\n }\n}\n\nexport function extractRuntimeCredentials(\n params?: OAuthParams,\n): { access: string, refresh: string, expires: number } | undefined {\n if (\n typeof params?.access === 'string'\n && typeof params.refresh === 'string'\n && typeof params.expires === 'number'\n ) {\n return { access: params.access, refresh: params.refresh, expires: params.expires }\n }\n return undefined\n}\n\nexport interface ResolveOAuthApiKeyOptions<TParams extends OAuthParams = OAuthParams> {\n provider: string\n providerId: string\n params?: TParams\n envKey?: string\n extraCredentialKeys?: (keyof TParams & string)[]\n missingError: string\n refreshError: (reason: string) => string\n readCredentials?: () => Record<string, OAuthCredentials | undefined>\n writeCredentials?: (credentials: Record<string, OAuthCredentials | undefined>) => void\n getOAuthApiKey?: GetOAuthApiKey\n}\n\n/**\n * Read the shared credentials file.\n *\n * Returns `{}` when the file is missing OR when the file exists but is corrupted\n * / not valid JSON. Corrupt-file tolerance is important: the refresh-then-persist\n * path below writes atomically, but older builds wrote in place and could have\n * truncated the file on a crash. Treating corruption as \"no stored creds\" lets\n * users re-auth without manually deleting the file.\n *\n * Supports two on-disk shapes:\n * 1. **Legacy** (untagged): `{ \"anthropic\": { access, refresh, expires, ... } }`\n * — written by older `bun run auth`.\n * 2. **New** (kind-tagged): `{ \"anthropic\": { kind: 'oauth', access, refresh, expires, ... } }`\n * — written by the TUI. Entries with `kind: 'apikey'` are skipped — those\n * reach providers via env vars set at TUI launch (see `applyApiKeyEnv`).\n */\nexport function readOAuthCredentials(): Record<string, OAuthCredentials | undefined> {\n const path = credentialsFilePath()\n if (!existsSync(path))\n return {}\n\n try {\n const raw = readFileSync(path, 'utf-8')\n const parsed = JSON.parse(raw)\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))\n return {}\n const result: Record<string, OAuthCredentials | undefined> = {}\n for (const [key, value] of Object.entries(parsed)) {\n if (!value || typeof value !== 'object' || Array.isArray(value))\n continue\n const v = value as Record<string, unknown>\n // New schema: explicit `kind` tag.\n if (v.kind === 'apikey')\n continue\n // Both new (`kind: 'oauth'`) and legacy (untagged) entries have `access`;\n // the harmless `kind` field is allowed by `OAuthCredentials`'s\n // `[key: string]: unknown` index signature. Validate the required\n // fields explicitly so the assignment doesn't need a double-cast.\n if (\n typeof v.access === 'string'\n && typeof v.refresh === 'string'\n && typeof v.expires === 'number'\n ) {\n result[key] = { ...v, access: v.access, refresh: v.refresh, expires: v.expires }\n }\n }\n return result\n }\n catch {\n return {}\n }\n}\n\n/**\n * Cross-process lockfile parameters for the credentials read-merge-write.\n * A holder normally completes in well under a millisecond (read + JSON merge\n * + atomic write of a tiny file), so the stale threshold is generous and the\n * total wait short — on timeout we proceed without the lock rather than fail\n * the refresh (degrades to the historic last-write-wins behavior).\n */\nconst CREDENTIALS_LOCK_STALE_MS = 5000\nconst CREDENTIALS_LOCK_RETRY_MS = 20\nconst CREDENTIALS_LOCK_MAX_WAIT_MS = 2000\n\n/** Synchronous sleep without spinning — works on Bun and Node main threads. */\nfunction sleepSync(ms: number): void {\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)\n}\n\n/**\n * Best-effort exclusive lock via `O_EXCL` create of `<file>.lock`. Returns\n * `true` when the lock was acquired (caller must release), `false` on\n * timeout (caller proceeds unlocked — see threshold rationale above). Locks\n * older than {@link CREDENTIALS_LOCK_STALE_MS} are treated as leftovers from\n * a crashed process and broken.\n */\nfunction acquireCredentialsLock(lockPath: string): boolean {\n const deadline = Date.now() + CREDENTIALS_LOCK_MAX_WAIT_MS\n while (true) {\n try {\n writeFileSync(lockPath, String(process.pid), { flag: 'wx', mode: CREDENTIALS_FILE_MODE })\n return true\n }\n catch (err) {\n // Only EEXIST means \"lock held\" — anything else (EPERM, ENOENT for a\n // missing parent dir, read-only fs) won't resolve by waiting, so bail\n // immediately and let the subsequent write surface the real error.\n if ((err as NodeJS.ErrnoException).code !== 'EEXIST')\n return false\n try {\n const age = Date.now() - statSync(lockPath).mtimeMs\n if (age > CREDENTIALS_LOCK_STALE_MS) {\n unlinkSync(lockPath)\n continue\n }\n }\n catch { /* lock vanished between create-fail and stat — retry */ }\n if (Date.now() >= deadline)\n return false\n sleepSync(CREDENTIALS_LOCK_RETRY_MS)\n }\n }\n}\n\nfunction releaseCredentialsLock(lockPath: string): void {\n try {\n unlinkSync(lockPath)\n }\n catch { /* already gone */ }\n}\n\n/**\n * Cross-process lock parameters for the REFRESH critical section (read →\n * check expiry → refresh → persist), held per provider around file-backed\n * resolves in {@link resolveOAuthApiKey}.\n *\n * This is a different beast from the write lock above: the holder performs a\n * network call, so it can legitimately hold the lock for seconds. Hence its\n * own generous thresholds — and async waiting, because blocking the event\n * loop for a peer's refresh round-trip is not acceptable. The stakes are\n * higher too: providers with single-use rotating refresh tokens (Bolt) treat\n * a replayed token as theft and revoke the WHOLE grant, so two processes\n * refreshing the same provider concurrently don't just race a write — they\n * self-revoke. A process that loses the race re-reads under the lock, finds\n * the winner's fresh token, and skips its own refresh entirely.\n *\n * On timeout we proceed without the lock (degrades to the pre-lock racy\n * behavior) rather than fail a resolve that would probably have succeeded.\n */\nconst REFRESH_LOCK_STALE_MS = 60_000\nconst REFRESH_LOCK_RETRY_MS = 50\nconst REFRESH_LOCK_MAX_WAIT_MS = 30_000\n\nfunction refreshLockPath(providerId: string): string {\n return `${credentialsFilePath()}.refresh-${providerId.replace(/[^\\w.-]/g, '_')}.lock`\n}\n\n/** Async sibling of {@link acquireCredentialsLock} with refresh-scale thresholds. */\nasync function acquireRefreshLock(lockPath: string): Promise<boolean> {\n const deadline = Date.now() + REFRESH_LOCK_MAX_WAIT_MS\n while (true) {\n try {\n writeFileSync(lockPath, String(process.pid), { flag: 'wx', mode: CREDENTIALS_FILE_MODE })\n return true\n }\n catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'EEXIST')\n return false\n try {\n const age = Date.now() - statSync(lockPath).mtimeMs\n if (age > REFRESH_LOCK_STALE_MS) {\n unlinkSync(lockPath)\n continue\n }\n }\n catch { /* lock vanished between create-fail and stat — retry */ }\n if (Date.now() >= deadline)\n return false\n await new Promise(resolve => setTimeout(resolve, REFRESH_LOCK_RETRY_MS))\n }\n }\n}\n\n/**\n * Atomically write `.credentials.json` with mode 0o600.\n *\n * Writes to a sibling temp file first, then renames. `rename(2)` is atomic on\n * the same filesystem, so readers either see the old file or the new one —\n * never a half-written one. This matters when a host has multiple processes\n * pointed at the same creds file.\n *\n * Merges the incoming OAuth entries on top of the raw file contents so that\n * entries this layer doesn't manage (e.g. `kind: 'apikey'` from the TUI\n * wizard) survive an OAuth refresh cycle. Without this merge,\n * `readOAuthCredentials` filters those entries out, and a naive overwrite\n * would delete them.\n *\n * The read-merge-write runs under a best-effort cross-process lockfile\n * (`<file>.lock`) so two processes refreshing different providers at the\n * same moment can't drop each other's rotated refresh tokens. On lock\n * timeout the write proceeds unlocked (last-write-wins, the pre-lock\n * behavior) rather than failing the refresh.\n */\nexport function writeOAuthCredentials(credentials: Record<string, OAuthCredentials | undefined>) {\n const path = credentialsFilePath()\n const lockPath = `${path}.lock`\n const locked = acquireCredentialsLock(lockPath)\n\n try {\n let existing: Record<string, unknown> = {}\n try {\n if (existsSync(path)) {\n const raw = readFileSync(path, 'utf-8')\n const parsed = JSON.parse(raw)\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))\n existing = parsed as Record<string, unknown>\n }\n }\n catch { /* corrupt file — start fresh */ }\n\n const merged = { ...existing, ...credentials }\n // Remove keys explicitly set to undefined (provider was deleted).\n for (const [key, value] of Object.entries(merged)) {\n if (value === undefined)\n delete merged[key]\n }\n\n writeFileAtomic(path, JSON.stringify(merged, null, 2), { mode: CREDENTIALS_FILE_MODE })\n }\n finally {\n if (locked)\n releaseCredentialsLock(lockPath)\n }\n}\n\nexport function credentialsFromParams<TParams extends OAuthParams>(params?: TParams, extraKeys: (keyof TParams & string)[] = []): OAuthCredentials | undefined {\n if (typeof params?.access !== 'string' || typeof params.refresh !== 'string' || typeof params.expires !== 'number')\n return undefined\n\n const extras = Object.fromEntries(\n extraKeys\n .map(key => [key, params[key]])\n .filter(([, value]) => value !== undefined),\n )\n\n return {\n access: params.access,\n refresh: params.refresh,\n expires: params.expires,\n ...extras,\n }\n}\n\nexport async function resolveOAuthApiKey<TParams extends OAuthParams>(\n options: ResolveOAuthApiKeyOptions<TParams>,\n callbacks?: Pick<StreamCallbacks, 'onOAuthRefresh'>,\n): Promise<string> {\n if (typeof options.params?.apiKey === 'string')\n return options.params.apiKey\n\n const paramsCredentials = credentialsFromParams(options.params, options.extraCredentialKeys)\n if (paramsCredentials) {\n return await withRefreshLock(\n `params:${options.providerId}`,\n () => resolveCredentialSource('params', paramsCredentials),\n )\n }\n\n if (typeof options.params?.access === 'string')\n return options.params.access\n\n const readCredentials = options.readCredentials ?? readOAuthCredentials\n const writeCredentials = options.writeCredentials ?? writeOAuthCredentials\n // The cross-process refresh lock only means something when we're actually\n // backed by the shared credentials file; injected stores (tests, embedders)\n // have no cross-process story to protect and must not touch real lock files.\n const usingDefaultStore = !options.readCredentials && !options.writeCredentials\n\n // Env key handling. A real API key (e.g. `sk-ant-api…`) is used as-is. But an\n // OAuth *access token* (`sk-ant-oat…`) pasted into the API-key env var can't\n // be refreshed and will 401 once expired — so if refreshable OAuth creds for\n // this provider exist on disk, prefer those (they self-heal via refresh).\n const envValue = options.envKey ? process.env[options.envKey] : undefined\n if (envValue) {\n if (!isOAuthAccessToken(envValue))\n return envValue\n if (!readCredentials()[options.providerId])\n return envValue // no refreshable creds — fall back to the raw token\n return await withRefreshLock(`file:${options.providerId}`, () => withCrossProcessRefreshLock(async () => {\n // Re-read under the locks — a concurrent resolve (in this process or\n // another) may have rotated the refresh token between the pre-check\n // above and lock acquisition, and refreshing with the stale token would\n // invalidate the new one.\n const allCredentials = readCredentials()\n const stored = allCredentials[options.providerId]\n if (!stored)\n return envValue\n return await resolveCredentialSource('file', stored, allCredentials, writeCredentials)\n }))\n }\n\n return await withRefreshLock(`file:${options.providerId}`, () => withCrossProcessRefreshLock(async () => {\n // Read under the locks so we're always operating on the latest file state\n // and never interleave read/refresh/write against another concurrent\n // resolve — in-process (withRefreshLock) or cross-process (the lockfile).\n const allCredentials = readCredentials()\n const storedCredentials = allCredentials[options.providerId]\n if (!storedCredentials)\n throw new Error(options.missingError)\n\n return await resolveCredentialSource('file', storedCredentials, allCredentials, writeCredentials)\n }))\n\n /**\n * Hold the per-provider cross-process lockfile across the WHOLE file-backed\n * critical section, not just the persist. Guarding only the write (as\n * `writeOAuthCredentials` does) still lets two processes both spend a\n * single-use rotating refresh token — which self-revokes providers with\n * replay-detecting rotation. See the {@link REFRESH_LOCK_STALE_MS} note.\n */\n async function withCrossProcessRefreshLock(fn: () => Promise<string>): Promise<string> {\n if (!usingDefaultStore)\n return fn()\n const lockPath = refreshLockPath(options.providerId)\n const locked = await acquireRefreshLock(lockPath)\n try {\n return await fn()\n }\n finally {\n if (locked)\n releaseCredentialsLock(lockPath)\n }\n }\n\n async function resolveCredentialSource(\n source: OAuthRefreshHookContext['source'],\n current: OAuthCredentials,\n allCredentials?: Record<string, OAuthCredentials | undefined>,\n persistCredentials?: (credentials: Record<string, OAuthCredentials | undefined>) => void,\n ): Promise<string> {\n // Only the refresh call itself maps to `refreshError` — a throwing\n // persist or onOAuthRefresh hook after a SUCCESSFUL refresh must not be\n // misreported as a refresh failure, so those run outside the try.\n let result: Awaited<ReturnType<GetOAuthApiKey>>\n try {\n const refreshOAuthApiKey = options.getOAuthApiKey ?? getOAuthApiKey\n result = await refreshOAuthApiKey(options.providerId, { [options.providerId]: current } as Record<string, OAuthCredentials>)\n }\n catch (err) {\n throw new Error(options.refreshError(errorMessage(err)))\n }\n if (!result)\n throw new Error(options.refreshError(options.missingError))\n\n if (result.newCredentials !== current) {\n // pi-ai's refresh returns only `{ access, refresh, expires }`. Merge it\n // onto the current credential so the persisted entry keeps its on-disk\n // `kind: 'oauth'` tag and any provider extras (e.g. openai-codex\n // `accountId`). A bare overwrite strips `kind`, and the TUI's auth-state\n // detection keys on `kind === 'oauth'` — so without this the provider\n // reads as signed out after every token refresh, forcing a daily re-login.\n const refreshed = { ...current, ...result.newCredentials }\n if (source === 'file' && allCredentials && persistCredentials) {\n allCredentials[options.providerId] = refreshed\n persistCredentials(allCredentials)\n }\n\n await callbacks?.onOAuthRefresh?.({\n provider: options.provider,\n providerId: options.providerId,\n source,\n previousCredentials: { ...current },\n credentials: { ...refreshed },\n })\n }\n\n return result.apiKey\n }\n}\n\n/**\n * Coalesce concurrent refresh calls onto a single in-flight promise per key.\n * The key combines the source (`params` vs `file`) and provider id, so a\n * per-agent param-only refresh does not starve a separate file-backed one.\n */\nasync function withRefreshLock(key: string, fn: () => Promise<string>): Promise<string> {\n const existing = refreshLocks.get(key)\n if (existing)\n return existing\n\n const task = (async () => {\n try {\n return await fn()\n }\n finally {\n refreshLocks.delete(key)\n }\n })()\n refreshLocks.set(key, task)\n return task\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { a as rewriteMessagesToWire, n as buildAliasMaps, o as toCanonicalName, r as buildSanitizedWireAliases, s as toWireName } from "./aliasing-lVFbm81n.js";
2
2
  import { i as documentBlockMarker, n as assertResolvedMediaBlock } from "./types-BIarq1qE.js";
3
3
  import { n as estimateTokens } from "./utils-ngQzYzZD.js";
4
- import { c as classifyErrorPrelude, d as isRetryableHttpStatus, f as matchesContextExceeded, p as matchesToolPairingError } from "./errors-DJUxZg9b.js";
4
+ import { f as isRetryableHttpStatus, l as classifyErrorPrelude, m as matchesToolPairingError, p as matchesContextExceeded } from "./errors-BwBxu3zD.js";
5
5
  import { t as audioFormatFromMediaType } from "./media-sniff-UKijEaiQ.js";
6
6
  import { renderSystemForWire, splitSystemPrompt } from "./prompt.js";
7
7
  import { createHash } from "node:crypto";
@@ -124,6 +124,154 @@ function buildPromptMessage(provider, parts) {
124
124
  return defaultPromptMessage(parts);
125
125
  }
126
126
  //#endregion
127
+ //#region src/providers/model-catalog.ts
128
+ const holder = { current: void 0 };
129
+ /**
130
+ * Normalization cache. Per-call catalogs arrive on every turn; without this a
131
+ * table form would allocate a fresh closure per lookup. Weak so a discarded
132
+ * catalog doesn't pin memory.
133
+ */
134
+ const normalized = /* @__PURE__ */ new WeakMap();
135
+ /**
136
+ * Install (or clear, with `null`) the process-wide catalog. Latest call wins.
137
+ *
138
+ * Prefer `behavior.modelCatalog` when you can: it needs no global state and
139
+ * scopes to one agent or one run. Use this for a deployment-wide correction,
140
+ * or when the host's model picker must see the override too.
141
+ */
142
+ function setModelCatalog(catalog) {
143
+ holder.current = catalog ? normalize(catalog) : void 0;
144
+ }
145
+ /** The process-wide catalog, normalized to source form. */
146
+ function getModelCatalog() {
147
+ return holder.current;
148
+ }
149
+ function normalize(catalog) {
150
+ const cached = normalized.get(catalog);
151
+ if (cached) return cached;
152
+ const source = isSource(catalog) ? catalog : (() => {
153
+ const table = catalog;
154
+ return {
155
+ table,
156
+ get: (provider, modelId) => table[`${provider}:${modelId}`]
157
+ };
158
+ })();
159
+ normalized.set(catalog, source);
160
+ return source;
161
+ }
162
+ /** A `ModelCatalog` is a source when it declares either accessor. */
163
+ function isSource(catalog) {
164
+ const candidate = catalog;
165
+ return typeof candidate.get === "function" || typeof candidate.list === "function";
166
+ }
167
+ /**
168
+ * Resolve one model through the catalog chain, WITHOUT consulting the bundled
169
+ * registry — callers own that fallback because each reader has a different
170
+ * bundled shape (pi-ai `Model`, `CodexModel`, `ModelInfo`).
171
+ *
172
+ * `perCall` (an agent's / run's `behavior.modelCatalog`) shadows the global.
173
+ */
174
+ function lookupModel(provider, modelId, perCall) {
175
+ for (const source of sourcesFor(perCall)) {
176
+ const entry = source.get?.(provider, modelId);
177
+ if (entry) return entry;
178
+ }
179
+ }
180
+ /**
181
+ * Authoritative model list for a provider, or `undefined` when no catalog
182
+ * claims ownership (the caller then uses the bundled list).
183
+ *
184
+ * Only the function form can claim ownership; table entries are overlaid via
185
+ * {@link listModelOverlay} instead.
186
+ */
187
+ function listModels(provider, perCall) {
188
+ for (const source of sourcesFor(perCall)) {
189
+ const list = source.list?.(provider);
190
+ if (list) return list;
191
+ }
192
+ }
193
+ /**
194
+ * Table-form entries for one provider, to merge on top of a bundled list
195
+ * (these win on id collision). Empty when no table-form catalog is active.
196
+ */
197
+ function listModelOverlay(provider, perCall) {
198
+ const prefix = `${provider}:`;
199
+ const out = [];
200
+ const seen = /* @__PURE__ */ new Set();
201
+ for (const source of sourcesFor(perCall)) {
202
+ if (!source.table) continue;
203
+ for (const [key, entry] of Object.entries(source.table)) {
204
+ if (!key.startsWith(prefix)) continue;
205
+ const id = entry.id ?? key.slice(prefix.length);
206
+ if (seen.has(id)) continue;
207
+ seen.add(id);
208
+ out.push(entry.id ? entry : {
209
+ ...entry,
210
+ id
211
+ });
212
+ }
213
+ }
214
+ return out;
215
+ }
216
+ /**
217
+ * View of `catalog` that resolves a model under several provider ids, in
218
+ * order — for providers whose metadata and pricing live under DIFFERENT
219
+ * registry keys.
220
+ *
221
+ * The Codex provider is the case: model records come from `openai-codex`
222
+ * while cost estimation runs under `openai`. Without this, one catalog entry
223
+ * fixes the ceiling or the price but not both, depending on which key the
224
+ * consumer guessed — and neither key is guessable.
225
+ *
226
+ * Returns `undefined` for an absent catalog so callers can pass it straight
227
+ * through.
228
+ */
229
+ function aliasCatalogProviders(catalog, providers) {
230
+ if (!catalog) return void 0;
231
+ return { get: (_provider, modelId) => {
232
+ for (const id of providers) {
233
+ const entry = lookupModel(id, modelId, catalog);
234
+ if (entry) return entry;
235
+ }
236
+ } };
237
+ }
238
+ /**
239
+ * Overlay a catalog entry onto a bundled model record (pi-ai `Model`, the
240
+ * chat layer's `ModelInfo`, …). Only fields the entry actually defines are
241
+ * replaced, so a one-field override corrects that field and inherits the rest.
242
+ *
243
+ * `cost` merges per-rate rather than wholesale, so `{ cost: { output: 30 } }`
244
+ * keeps the bundled input/cache rates instead of zeroing them.
245
+ *
246
+ * Kept here rather than at each reader so the merge semantics can't drift
247
+ * between the wire path and the host layer.
248
+ */
249
+ function mergeCatalogEntry(base, entry) {
250
+ if (!entry) return base;
251
+ const patch = {};
252
+ if (entry.id !== void 0) patch.id = entry.id;
253
+ if (entry.name !== void 0) patch.name = entry.name;
254
+ if (entry.maxTokens !== void 0) patch.maxTokens = entry.maxTokens;
255
+ if (entry.contextWindow !== void 0) patch.contextWindow = entry.contextWindow;
256
+ if (entry.reasoning !== void 0) patch.reasoning = entry.reasoning;
257
+ if (entry.input !== void 0) patch.input = entry.input;
258
+ if (entry.cost !== void 0) patch.cost = {
259
+ ...base.cost,
260
+ ...entry.cost
261
+ };
262
+ return {
263
+ ...base,
264
+ ...patch
265
+ };
266
+ }
267
+ /** Per-call source first (it shadows), then the process-wide one. */
268
+ function sourcesFor(perCall) {
269
+ const out = [];
270
+ if (perCall) out.push(normalize(perCall));
271
+ if (holder.current) out.push(holder.current);
272
+ return out;
273
+ }
274
+ //#endregion
127
275
  //#region src/providers/cost.ts
128
276
  let getModelPromise;
129
277
  function loadGetModel() {
@@ -134,6 +282,11 @@ function loadGetModel() {
134
282
  * Per-MTok rates for models not yet in pi-ai's bundled registry. Keyed by
135
283
  * `provider:modelId`. Consulted only when pi-ai's `getModel` misses, so a
136
284
  * later dep bump that bundles the model transparently takes over.
285
+ *
286
+ * MAINTAINER-side stopgap only. Consumers with custom / gateway / private
287
+ * models should install a catalog instead of sending PRs against this table:
288
+ * `behavior.modelCatalog` per agent, or `setModelCatalog()` process-wide
289
+ * (see `./model-catalog`) — both outrank the registry AND this table.
137
290
  */
138
291
  const EXTRA_COST_RATES = {
139
292
  "anthropic:claude-opus-5": {
@@ -238,33 +391,53 @@ const EXTRA_COST_RATES = {
238
391
  * rate miss (unknown provider/model, or no published cache-write rate) —
239
392
  * better than fabricating a $0. An optional `multiplier` scales the base
240
393
  * rates for premium modes (e.g. Anthropic fast mode, 1h-TTL writes at 2x).
394
+ *
395
+ * `catalog` is caller-supplied because this helper has no run context to read
396
+ * `behavior.modelCatalog` from: cache-break telemetry is computed by the host
397
+ * that owns the `cache:break` hook, not by the loop. A host using a model
398
+ * catalog must pass the same one here, or a re-bill estimate for an
399
+ * override-only model comes back `undefined` while the per-turn cost is
400
+ * priced correctly.
241
401
  */
242
- async function estimateRebilledCostUsd(provider, modelId, rebilledTokens, multiplier) {
243
- if (!modelId || rebilledTokens <= 0) return void 0;
402
+ /**
403
+ * Rates for one model: catalog over pi-ai registry over local table.
404
+ *
405
+ * A catalog entry wins because it's an explicit claim about a model we may not
406
+ * know (gateway alias, private deployment, post-pin release) — but it merges
407
+ * PER-RATE over whatever the registry knows, matching `mergeCatalogEntry` and
408
+ * the documented contract. Replacing the whole rate object would zero the rates
409
+ * a partial entry doesn't mention, so `{ cost: { output: 30 } }` would bill
410
+ * input and cache traffic at $0 — and the picker (which merges per-rate) would
411
+ * disagree with the invoice.
412
+ */
413
+ async function resolveRates(provider, modelId, catalog) {
414
+ const override = lookupModel(provider, modelId, catalog)?.cost;
244
415
  const getModel = await loadGetModel();
245
- let model;
416
+ let base;
246
417
  if (getModel) try {
247
- model = getModel(provider, modelId);
418
+ base = getModel(provider, modelId)?.cost;
248
419
  } catch {
249
- model = void 0;
420
+ base = void 0;
250
421
  }
251
- const rates = model?.cost ?? EXTRA_COST_RATES[`${provider}:${modelId}`];
422
+ base ??= EXTRA_COST_RATES[`${provider}:${modelId}`];
423
+ if (!override) return base;
424
+ return base ? {
425
+ ...base,
426
+ ...override
427
+ } : override;
428
+ }
429
+ async function estimateRebilledCostUsd(provider, modelId, rebilledTokens, multiplier, catalog) {
430
+ if (!modelId || rebilledTokens <= 0) return void 0;
431
+ const rates = await resolveRates(provider, modelId, catalog);
252
432
  if (!rates || rates.cacheWrite === void 0) return void 0;
253
433
  const write = rates.cacheWrite * (multiplier?.cacheWrite ?? 1);
254
434
  const read = (rates.cacheRead ?? 0) * (multiplier?.cacheRead ?? 1);
255
435
  return Math.max(0, (write - read) * rebilledTokens / 1e6);
256
436
  }
257
- async function fillEstimatedCost(usage, provider, multiplier) {
437
+ async function fillEstimatedCost(usage, provider, multiplier, catalog) {
258
438
  if (usage.cost !== void 0) return usage;
259
439
  if (!usage.modelId) return usage;
260
- const getModel = await loadGetModel();
261
- let model;
262
- if (getModel) try {
263
- model = getModel(provider, usage.modelId);
264
- } catch {
265
- model = void 0;
266
- }
267
- const rates = model?.cost ?? EXTRA_COST_RATES[`${provider}:${usage.modelId}`];
440
+ const rates = await resolveRates(provider, usage.modelId, catalog);
268
441
  if (!rates) return usage;
269
442
  const mul = multiplier ?? {};
270
443
  const total = ((rates.input ?? 0) * (mul.input ?? 1) * (usage.input ?? 0) + (rates.output ?? 0) * (mul.output ?? 1) * (usage.output ?? 0) + (rates.cacheRead ?? 0) * (mul.cacheRead ?? 1) * (usage.cacheRead ?? 0) + (rates.cacheWrite ?? 0) * (mul.cacheWrite ?? 1) * (usage.cacheCreation ?? 0)) / 1e6;
@@ -1838,7 +2011,7 @@ function openaiCompat(params) {
1838
2011
  ...usage.cost !== void 0 ? { cost: usage.cost } : {},
1839
2012
  ...finishReason ? { finishReason } : {},
1840
2013
  modelId
1841
- }, name);
2014
+ }, name, void 0, options.modelCatalog);
1842
2015
  const toolCalls = result.toolCalls.map((call) => ({
1843
2016
  ...call,
1844
2017
  name: nameBoundary.fromWire(call.name)
@@ -1854,6 +2027,6 @@ function openaiCompat(params) {
1854
2027
  };
1855
2028
  }
1856
2029
  //#endregion
1857
- export { estimateRebilledCostUsd as C, defaultPromptMessage as D, canonicalizePrompt as E, unsupportedMediaError as O, resolvePromptCacheKey as S, buildPromptMessage as T, userMessage as _, applyOAICacheBreakpoints as a, sanitizeToolSpecs as b, buildAssistantContent as c, formatTools as d, mapOAIFinishReason as f, toolResultsMessage as g, toOAIMessages as h, TOOL_RESULTS_TAG as i, classifyOpenAICompatError as l, openaiCompat as m, OpenAICompatHttpError as n, applyOAIToolCacheBreakpoint as o, openAICompatPromptMessage as p, OpenAICompatStreamError as r, assistantMessage as s, ASSISTANT_TOOL_CALLS_TAG as t, consumeSSE as u, createToolNameBoundary as v, fillEstimatedCost as w, derivePromptCacheKey as x, sanitizeToolSchema as y };
2030
+ export { mergeCatalogEntry as A, estimateRebilledCostUsd as C, listModelOverlay as D, getModelCatalog as E, unsupportedMediaError as F, buildPromptMessage as M, canonicalizePrompt as N, listModels as O, defaultPromptMessage as P, resolvePromptCacheKey as S, aliasCatalogProviders as T, userMessage as _, applyOAICacheBreakpoints as a, sanitizeToolSpecs as b, buildAssistantContent as c, formatTools as d, mapOAIFinishReason as f, toolResultsMessage as g, toOAIMessages as h, TOOL_RESULTS_TAG as i, setModelCatalog as j, lookupModel as k, classifyOpenAICompatError as l, openaiCompat as m, OpenAICompatHttpError as n, applyOAIToolCacheBreakpoint as o, openAICompatPromptMessage as p, OpenAICompatStreamError as r, assistantMessage as s, ASSISTANT_TOOL_CALLS_TAG as t, consumeSSE as u, createToolNameBoundary as v, fillEstimatedCost as w, derivePromptCacheKey as x, sanitizeToolSchema as y };
1858
2031
 
1859
- //# sourceMappingURL=openai-compat-BTbSyQbm.js.map
2032
+ //# sourceMappingURL=openai-compat-0MJq4-g9.js.map