zidane 6.2.0 → 6.2.7

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 (108) hide show
  1. package/dist/{acp-0JO-UBEv.js → acp-B_8CI9-1.js} +4 -4
  2. package/dist/{acp-0JO-UBEv.js.map → acp-B_8CI9-1.js.map} +1 -1
  3. package/dist/acp-cli.js +7 -7
  4. package/dist/acp.d.ts +1 -1
  5. package/dist/acp.js +1 -1
  6. package/dist/{agent-25moybsj.js → agent-DPO6GKX5.js} +87 -69
  7. package/dist/agent-DPO6GKX5.js.map +1 -0
  8. package/dist/agent.d.ts +1 -1
  9. package/dist/agent.js +2 -2
  10. package/dist/{anthropic-CV_OSYVD.js → anthropic-J2tBjcSQ.js} +49 -27
  11. package/dist/anthropic-J2tBjcSQ.js.map +1 -0
  12. package/dist/{auth-D5FfJtLf.js → auth-Cexpxjfa.js} +7 -5
  13. package/dist/auth-Cexpxjfa.js.map +1 -0
  14. package/dist/{cache-telemetry-BKAwsNxm.js → cache-telemetry-KJ7B-_Gh.js} +8 -3
  15. package/dist/cache-telemetry-KJ7B-_Gh.js.map +1 -0
  16. package/dist/chat/pure.d.ts +3 -3
  17. package/dist/chat/pure.js +1 -1
  18. package/dist/chat.d.ts +11 -4
  19. package/dist/chat.d.ts.map +1 -1
  20. package/dist/chat.js +5 -5
  21. package/dist/contexts/daytona.d.ts +1 -1
  22. package/dist/contexts/e2b.d.ts +1 -1
  23. package/dist/eval.d.ts +1 -1
  24. package/dist/eval.js +1 -1
  25. package/dist/extensions.d.ts +1 -1
  26. package/dist/extensions.js +1 -1
  27. package/dist/headless.d.ts +1 -1
  28. package/dist/headless.js +3 -3
  29. package/dist/{index-mUT8TI9j.d.ts → index-CNyYEOy0.d.ts} +110 -36
  30. package/dist/index-CNyYEOy0.d.ts.map +1 -0
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.js +9 -9
  33. package/dist/{inline-autocomplete-w_O3SVtN.js → inline-autocomplete-BAwBcZtD.js} +12 -7
  34. package/dist/{inline-autocomplete-w_O3SVtN.js.map → inline-autocomplete-BAwBcZtD.js.map} +1 -1
  35. package/dist/mcp.d.ts +1 -1
  36. package/dist/{messages-Ce4yRjd3.js → messages-DEPX32kc.js} +2 -2
  37. package/dist/{messages-Ce4yRjd3.js.map → messages-DEPX32kc.js.map} +1 -1
  38. package/dist/openai-BcmYm0t4.js +579 -0
  39. package/dist/openai-BcmYm0t4.js.map +1 -0
  40. package/dist/{openai-compat-TA7aihJ-.js → openai-compat-LGAsBush.js} +25 -1
  41. package/dist/openai-compat-LGAsBush.js.map +1 -0
  42. package/dist/output/stream-json.d.ts +1 -1
  43. package/dist/output/terminal.d.ts +1 -1
  44. package/dist/presets.d.ts +1 -1
  45. package/dist/presets.js +1 -1
  46. package/dist/providers/anthropic.d.ts +1 -1
  47. package/dist/providers/anthropic.js +1 -1
  48. package/dist/providers/arcee.d.ts +1 -1
  49. package/dist/providers/arcee.js +1 -1
  50. package/dist/providers/baseten.d.ts +1 -1
  51. package/dist/providers/baseten.js +1 -1
  52. package/dist/providers/cerebras.d.ts +1 -1
  53. package/dist/providers/cerebras.js +1 -1
  54. package/dist/providers/local.d.ts +1 -1
  55. package/dist/providers/local.js +1 -1
  56. package/dist/providers/openai-compat.d.ts +1 -1
  57. package/dist/providers/openai-compat.js +1 -1
  58. package/dist/providers/openai.d.ts +1 -1
  59. package/dist/providers/openai.js +1 -503
  60. package/dist/providers/openrouter.d.ts +1 -1
  61. package/dist/providers/openrouter.js +1 -1
  62. package/dist/providers/xai.d.ts +1 -1
  63. package/dist/providers/xai.js +1 -1
  64. package/dist/{providers-Dln69sQ_.js → providers-BM6VuE_i.js} +3 -3
  65. package/dist/{providers-Dln69sQ_.js.map → providers-BM6VuE_i.js.map} +1 -1
  66. package/dist/providers.d.ts +1 -1
  67. package/dist/providers.js +5 -5
  68. package/dist/restate.d.ts +1 -1
  69. package/dist/session/sqlite.d.ts +1 -1
  70. package/dist/{session-D7JSEMME.js → session-Dc1v4Otm.js} +2 -2
  71. package/dist/{session-D7JSEMME.js.map → session-Dc1v4Otm.js.map} +1 -1
  72. package/dist/session.d.ts +1 -1
  73. package/dist/session.js +2 -2
  74. package/dist/skills.d.ts +1 -1
  75. package/dist/{tool-formatters-DOcJz_J9.d.ts → tool-formatters-B7oWiy5m.d.ts} +2 -2
  76. package/dist/tool-formatters-B7oWiy5m.d.ts.map +1 -0
  77. package/dist/tools/fetch-url.d.ts +1 -1
  78. package/dist/tools/web-search.d.ts +1 -1
  79. package/dist/tools.d.ts +1 -1
  80. package/dist/tools.js +1 -1
  81. package/dist/{transcript-anchors-CzMMNrhJ.d.ts → transcript-anchors-BUZ915pM.d.ts} +2 -2
  82. package/dist/{transcript-anchors-CzMMNrhJ.d.ts.map → transcript-anchors-BUZ915pM.d.ts.map} +1 -1
  83. package/dist/{transcript-anchors-CO-WnuYQ.js → transcript-anchors-DtOaKu4d.js} +12 -4
  84. package/dist/{transcript-anchors-CO-WnuYQ.js.map → transcript-anchors-DtOaKu4d.js.map} +1 -1
  85. package/dist/tui.d.ts +17 -14
  86. package/dist/tui.d.ts.map +1 -1
  87. package/dist/tui.js +207 -33
  88. package/dist/tui.js.map +1 -1
  89. package/dist/{turn-operations-CrCrJnvo.d.ts → turn-operations-2Ou_DkbC.d.ts} +3 -3
  90. package/dist/{turn-operations-CrCrJnvo.d.ts.map → turn-operations-2Ou_DkbC.d.ts.map} +1 -1
  91. package/dist/{turn-operations-D9p-y9EC.js → turn-operations-CXjRaltC.js} +72 -2
  92. package/dist/{turn-operations-D9p-y9EC.js.map → turn-operations-CXjRaltC.js.map} +1 -1
  93. package/dist/types-BIarq1qE.js.map +1 -1
  94. package/dist/types.d.ts +1 -1
  95. package/dist/{xai-kGdhWmds.js → xai-BYAyEPiJ.js} +2 -2
  96. package/dist/{xai-kGdhWmds.js.map → xai-BYAyEPiJ.js.map} +1 -1
  97. package/docs/ARCHITECTURE.md +1 -1
  98. package/docs/EXTENSIONS.md +6 -5
  99. package/package.json +2 -1
  100. package/scripts/eval.ts +21 -3
  101. package/dist/agent-25moybsj.js.map +0 -1
  102. package/dist/anthropic-CV_OSYVD.js.map +0 -1
  103. package/dist/auth-D5FfJtLf.js.map +0 -1
  104. package/dist/cache-telemetry-BKAwsNxm.js.map +0 -1
  105. package/dist/index-mUT8TI9j.d.ts.map +0 -1
  106. package/dist/openai-compat-TA7aihJ-.js.map +0 -1
  107. package/dist/providers/openai.js.map +0 -1
  108. package/dist/tool-formatters-DOcJz_J9.d.ts.map +0 -1
@@ -1 +0,0 @@
1
- {"version":3,"file":"anthropic-CV_OSYVD.js","names":[],"sources":["../src/chat/anthropic-models.ts","../src/providers/anthropic.ts"],"sourcesContent":["/**\n * Anthropic models not (yet) bundled in `@earendil-works/pi-ai`'s registry.\n *\n * pi-ai ships a static price/metadata registry that lags new Anthropic\n * releases by a dep bump. Until the bump lands, list freshly-shipped models\n * here so they show in the picker, expose the reasoning knob, and get an\n * accurate cost estimate. Entries are merged ahead of pi-ai's list in\n * {@link modelsForDescriptor} and de-duped by id, so once pi-ai catches up\n * the bundled entry simply wins.\n */\n\nimport type { ModelInfo, ModelOption } from './providers'\n\n/**\n * Anthropic \"fast mode\" — `speed: \"fast\"` on the Messages API. ~2.5× output\n * tokens/sec at a per-model price multiplier. Supported on Opus 4.6 / 4.7 / 4.8\n * only; the API errors if sent on any other model, so it's attached just to\n * those entries (see {@link FAST_MODE_OPTIONS}).\n *\n * The multiplier scales ALL rate components (input, output, and both cache\n * rates) uniformly: per Anthropic, \"prompt caching multipliers apply on top of\n * fast mode pricing\" across the full context window. Opus 4.8 is 2× ($5/$25 →\n * $10/$50); Opus 4.6 / 4.7 are a steeper 6× ($5/$25 → $30/$150).\n * See https://platform.claude.com/docs/en/build-with-claude/fast-mode.\n */\nexport const FAST_MODE_OPTION: ModelOption = {\n id: 'fast',\n label: 'Fast mode',\n description: '~2.5× faster output at premium pricing',\n costMultiplier: { input: 2, output: 2, cacheRead: 2, cacheWrite: 2 },\n}\n\nconst FAST_MODE_OPTION_LEGACY: ModelOption = {\n ...FAST_MODE_OPTION,\n description: '~2.5× faster output at 6× pricing',\n costMultiplier: { input: 6, output: 6, cacheRead: 6, cacheWrite: 6 },\n}\n\n/** Model ids that accept `speed: \"fast\"`, mapped to their fast-mode option. */\nexport const FAST_MODE_OPTIONS: Readonly<Record<string, ModelOption>> = {\n 'claude-opus-4-8': FAST_MODE_OPTION,\n 'claude-opus-4-7': FAST_MODE_OPTION_LEGACY,\n 'claude-opus-4-6': FAST_MODE_OPTION_LEGACY,\n}\n\n/**\n * The price multiplier to apply to a model's base rates when fast mode is\n * active, or `undefined` when the model has no fast-mode pricing (i.e. doesn't\n * support it). Mirrors {@link FAST_MODE_OPTIONS} — kept as a separate accessor\n * so the cost layer can stay decoupled from the picker-facing option objects.\n */\nexport function fastModeCostMultiplier(modelId: string): ModelOption['costMultiplier'] {\n return FAST_MODE_OPTIONS[modelId]?.costMultiplier\n}\n\n/**\n * Claude Fable 5 — GA 2026-06-09. Anthropic's most capable widely released\n * model: $10/$50 per MTok, 1M context, 128k output, adaptive thinking\n * (always on), vision. No fast-mode support (Opus 4.6/4.7/4.8 only).\n *\n * Claude Opus 4.8 — shipped 2026-05-28. Same price/context/output as Opus 4.7\n * ($5/$25 per MTok, 1M context, 128k output), adaptive thinking, vision.\n * See https://www.anthropic.com/news/claude-opus-4-8.\n *\n * Claude Mythos 5 is deliberately absent: it's invitation-only (Project\n * Glasswing), so it would be a dead picker entry for virtually all users.\n *\n * See https://platform.claude.com/docs/en/about-claude/models/overview.\n */\nexport const ANTHROPIC_EXTRA_MODELS: readonly ModelInfo[] = [\n {\n id: 'claude-fable-5',\n name: 'Claude Fable 5',\n reasoning: true,\n input: ['text', 'image'],\n cost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },\n contextWindow: 1_000_000,\n maxTokens: 128_000,\n provider: 'anthropic',\n },\n {\n id: 'claude-opus-4-8',\n name: 'Claude Opus 4.8',\n reasoning: true,\n input: ['text', 'image'],\n cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },\n contextWindow: 1_000_000,\n maxTokens: 128_000,\n provider: 'anthropic',\n options: [FAST_MODE_OPTION],\n },\n]\n","// Type-only imports — erased at build time. Safe when the peer dep is not installed\n// at consume time; the types only need to resolve during Zidane's own build\n// (where `@anthropic-ai/sdk` is present as a devDependency).\nimport type Anthropic from '@anthropic-ai/sdk'\nimport type { Model } from '@anthropic-ai/sdk/resources'\nimport type { Provider, StreamCallbacks, ToolResult, ToolSpec, TurnResult } from '.'\nimport type { ClassifiedError } from '../errors'\nimport type { PromptPart, SessionContentBlock, SessionMessage, ThinkingLevel, TurnFinishReason, TurnUsage } from '../types'\nimport type { OAuthParams } from './oauth'\nimport { fastModeCostMultiplier } from '../chat/anthropic-models'\nimport { classifyErrorPrelude, isRetryableHttpStatus, matchesContextExceeded, matchesToolPairingError } from '../errors'\nimport { lazyAsync } from '../lazy'\nimport { unsupportedMediaError } from '../prompt'\nimport { fromAnthropic, toAnthropic } from '../session/messages'\nimport { renderSystemForWire, splitSystemPrompt } from '../system-prompt'\nimport { fillEstimatedCost } from './cost'\nimport { extractRuntimeCredentials, readOAuthCredentials, resolveOAuthApiKey } from './oauth'\nimport { sanitizeToolSpecs } from './schema-sanitize'\n\ntype AnthropicCtor = typeof Anthropic\ntype AnthropicInstance = InstanceType<AnthropicCtor>\n\n// Lazy-loaded SDK class. `@anthropic-ai/sdk` is an OPTIONAL peer dependency: the\n// module is imported only when the anthropic provider opens a connection.\nconst loadAnthropicSdk = lazyAsync(async (): Promise<AnthropicCtor> => {\n try {\n const mod = await import('@anthropic-ai/sdk')\n return mod.default as unknown as AnthropicCtor\n }\n catch (err) {\n throw new Error(\n 'The `anthropic` provider requires the `@anthropic-ai/sdk` package, which is an optional peer dependency. '\n + 'Install it with your package manager (e.g. `bun add @anthropic-ai/sdk`).',\n err instanceof Error ? { cause: err } : undefined,\n )\n }\n})\n\n/**\n * Server-side context-management config — the body of `context_management` on\n * the Messages API. Typed loosely (Record-of-unknown) so we don't pin a specific\n * SDK schema version: the v0.90 SDK does not yet type this field, but the wire\n * format is stable behind the `context-management-2025-06-27` beta.\n *\n * See: https://docs.anthropic.com/en/docs/build-with-claude/context-management\n */\nexport interface AnthropicContextManagement {\n edits?: Array<Record<string, unknown>>\n [key: string]: unknown\n}\n\nexport interface AnthropicParams extends OAuthParams {\n defaultModel?: string\n /**\n * Optional override for the Anthropic SDK base URL. Honored end-to-end — headers and\n * routing pass through to the forwarded host. Useful for proxies (e.g. corporate\n * gateways, internal router).\n */\n baseURL?: string\n /**\n * Additional `anthropic-beta` flags to opt into. Merged with the OAuth-path\n * defaults (`claude-code-20250219`, `oauth-2025-04-20`); duplicates are\n * de-duped. Examples:\n *\n * - `'context-management-2025-06-27'` — server-side context compaction\n * (token-accurate; pair with {@link AnthropicParams.contextManagement}).\n * - `'token-efficient-tools-2026-03-28'` — terser tool_use wire format.\n * - `'interleaved-thinking-2025-05-14'` — think between tool calls within\n * one turn.\n * - `'redact-thinking-2026-02-12'` — replace large thinking blocks with\n * stubs server-side.\n * - `'prompt-caching-scope-2026-01-05'` — extended prompt-cache scope.\n *\n * Honored on both the OAuth and API-key paths.\n */\n extraBetas?: readonly string[]\n /**\n * Server-side context-management directive. Sent on the request body as\n * `context_management`. Requires the `context-management-2025-06-27` beta —\n * add it to {@link AnthropicParams.extraBetas}.\n *\n * Typed loosely so future Anthropic schema additions land without an SDK\n * bump. A typical compaction edit:\n *\n * ```ts\n * contextManagement: {\n * edits: [{\n * type: 'clear_tool_uses_20250919',\n * trigger: { type: 'input_tokens', value: 180_000 },\n * clear_at_least: { type: 'input_tokens', value: 140_000 },\n * clear_tool_inputs: ['Read', 'Bash', 'Grep'],\n * }],\n * }\n * ```\n */\n contextManagement?: AnthropicContextManagement\n /**\n * Generic pass-through for fields on the Messages API request body that the\n * SDK does not yet type. Spread into the request before the typed fields,\n * so explicit options ({@link AnthropicParams.contextManagement} and the\n * built-in fields like `model` / `tools` / `messages`) win on collision.\n *\n * Forward-compat escape hatch for new Anthropic betas — when a future flag\n * ships before zidane has a dedicated typed knob, set it here without\n * waiting on a release. Most fields will still need the matching beta in\n * {@link AnthropicParams.extraBetas}.\n */\n extraBodyParams?: Record<string, unknown>\n /**\n * Extra HTTP headers sent on every request, merged into the SDK's\n * `defaultHeaders`. Honored on both the OAuth and API-key paths.\n *\n * Use for routing/attribution headers a proxy or gateway in front of the\n * `baseURL` consumes (e.g. a target-provider selector). Reserved zidane\n * headers (`anthropic-beta`, `user-agent`, `x-app`, and the OAuth browser\n * flag) take precedence on collision.\n *\n * May be a **function** resolved per request (awaited) instead of a static\n * map — the seam for gateways minting short-lived/signed auth tokens that a\n * long, suspended, or replayed durable run outlives. The resolver runs on\n * every `stream()` / `countTokens()` call, so it can return a freshly-minted\n * token each turn (e.g. `{ authorization: \\`Bearer ${await mintWorkToken()}\\` }`)\n * without rebuilding the provider.\n */\n extraHeaders?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>)\n}\n\n/** Beta flags sent unconditionally on the OAuth path (Claude Code parity). */\nconst OAUTH_DEFAULT_BETAS = ['claude-code-20250219', 'oauth-2025-04-20'] as const\n\n/**\n * Beta flag that enables the `speed: \"fast\"` request field (fast mode — up to\n * 2.5× output tokens/sec on Opus 4.6/4.7/4.8 at premium pricing). Sent as an\n * `anthropic-beta` header only on requests that opt into fast mode; absent\n * otherwise so standard requests keep a byte-stable header for prompt caching.\n *\n * See: https://platform.claude.com/docs/en/build-with-claude/fast-mode\n */\nconst FAST_MODE_BETA = 'fast-mode-2026-02-01'\nconst API_KEY_RESERVED_EXTRA_HEADER_NAMES = new Set(['anthropic-beta'])\nconst OAUTH_RESERVED_EXTRA_HEADER_NAMES = new Set([\n 'anthropic-beta',\n 'anthropic-dangerous-direct-browser-access',\n 'user-agent',\n 'x-app',\n])\n\nfunction filterReservedExtraHeaders(\n extraHeaders: Record<string, string> | undefined,\n reservedNames: ReadonlySet<string>,\n): Record<string, string> {\n const out: Record<string, string> = {}\n for (const [name, value] of Object.entries(extraHeaders ?? {})) {\n if (!reservedNames.has(name.toLowerCase()))\n out[name] = value\n }\n return out\n}\n\n/**\n * Build the `anthropic-beta` header value — OAuth defaults plus caller-supplied\n * extras, de-duped while preserving order. Returns `undefined` when no betas\n * apply (non-OAuth, no extras).\n */\nexport function resolveAnthropicBetas(\n isOAuth: boolean,\n extraBetas?: readonly string[],\n): string | undefined {\n const seen = new Set<string>()\n const out: string[] = []\n if (isOAuth) {\n for (const b of OAUTH_DEFAULT_BETAS) {\n if (!seen.has(b)) { seen.add(b); out.push(b) }\n }\n }\n if (extraBetas) {\n for (const b of extraBetas) {\n if (typeof b === 'string' && b.length > 0 && !seen.has(b)) {\n seen.add(b)\n out.push(b)\n }\n }\n }\n return out.length > 0 ? out.join(',') : undefined\n}\n\nfunction getConfiguredApiKey(anthropicParams?: AnthropicParams): string {\n if (anthropicParams?.apiKey)\n return anthropicParams.apiKey\n\n if (anthropicParams?.access)\n return anthropicParams.access\n\n if (process.env.ANTHROPIC_API_KEY)\n return process.env.ANTHROPIC_API_KEY\n\n // `readOAuthCredentials` tolerates a missing or corrupted file and returns `{}`\n // in both cases, so stale/half-written creds never crash agent construction.\n const access = readOAuthCredentials().anthropic?.access\n if (typeof access === 'string' && access.length > 0)\n return access\n\n throw new Error('No API key found. Run `bun run auth` first.')\n}\n\n/**\n * Resolve {@link AnthropicParams.extraHeaders} to a concrete map. A function\n * form is invoked (and awaited) per call so gateway auth tokens can refresh\n * each turn; a static map passes through.\n */\nasync function resolveExtraHeaders(\n value: AnthropicParams['extraHeaders'],\n): Promise<Record<string, string> | undefined> {\n return typeof value === 'function' ? value() : value\n}\n\nfunction createClient(\n SDK: AnthropicCtor,\n apiKey: string,\n isOAuth: boolean,\n baseURL?: string,\n extraBetas?: readonly string[],\n extraHeaders?: Record<string, string>,\n): AnthropicInstance {\n const base = baseURL ? { baseURL } : {}\n const betaHeader = resolveAnthropicBetas(isOAuth, extraBetas)\n if (isOAuth) {\n const callerHeaders = filterReservedExtraHeaders(extraHeaders, OAUTH_RESERVED_EXTRA_HEADER_NAMES)\n const defaultHeaders: Record<string, string> = {\n ...callerHeaders,\n 'anthropic-dangerous-direct-browser-access': 'true',\n 'user-agent': 'zidane/2.0.0',\n 'x-app': 'cli',\n }\n if (betaHeader)\n defaultHeaders['anthropic-beta'] = betaHeader\n return new SDK({\n apiKey: null,\n authToken: apiKey,\n dangerouslyAllowBrowser: true,\n defaultHeaders,\n ...base,\n })\n }\n // API-key path — only emit the beta header when the caller actually opted in.\n const defaultHeaders = filterReservedExtraHeaders(extraHeaders, API_KEY_RESERVED_EXTRA_HEADER_NAMES)\n if (betaHeader)\n defaultHeaders['anthropic-beta'] = betaHeader\n return new SDK({\n apiKey,\n ...(Object.keys(defaultHeaders).length > 0 ? { defaultHeaders } : {}),\n ...base,\n })\n}\n\ntype BudgetedThinkingLevel = Exclude<ThinkingLevel, 'off' | 'adaptive'>\n\n/**\n * Map `ThinkingLevel` budgeted tiers to Anthropic's `output_config.effort`\n * enum. `minimal` collapses to `low` (the closest equivalent — Anthropic does\n * not have a sub-`low` tier).\n */\nconst EFFORT_FOR_LEVEL: Record<BudgetedThinkingLevel, 'low' | 'medium' | 'high' | 'xhigh' | 'max'> = {\n minimal: 'low',\n low: 'low',\n medium: 'medium',\n high: 'high',\n xhigh: 'xhigh',\n max: 'max',\n}\n\n/**\n * Description of the Anthropic-side thinking configuration derived from a\n * ThinkingLevel. `null` means no thinking-related field should be set on the\n * request.\n *\n * Two shapes:\n * - `adaptive` — `thinking.type='adaptive'`. The model self-budgets per turn.\n * When `effort` is set, also emits `output_config.effort` to hint at the\n * level of reasoning the caller wants. This is the post-Claude-4.6 default\n * for level-based control and avoids the `thinking.type='enabled'`\n * deprecation warning. When `maxTokensCap` is set, the request builder\n * reduces `max_tokens` to `min(max_tokens, maxTokensCap)` — adaptive thinks\n * and replies inside one envelope, so capping the envelope soft-bounds the\n * thinking too.\n * - `enabled` — explicit-budget path: `thinking.type='enabled'` with an\n * explicit `budget_tokens`. `maxTokensBump` is added to the request's\n * `max_tokens` so the budget sits on top of the response cap. The only\n * way to pin a precise token budget; Anthropic emits a deprecation\n * warning for this shape on opus 4.6+, so we only route here when the\n * caller explicitly sets `behavior.thinkingBudget`.\n *\n * In both shapes Anthropic requires `temperature` to be `1`.\n */\nexport type AnthropicThinkingPlan\n = | { kind: 'enabled', budgetTokens: number, maxTokensBump: number }\n | { kind: 'adaptive', effort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max', maxTokensCap?: number }\n\n/**\n * Decide how to translate a `ThinkingLevel` into Anthropic's request shape.\n * Pure / synchronous — exported so tests can assert routing without standing\n * up the SDK.\n *\n * Routing rules:\n * - `'off'` → `null` (no thinking field, no effort hint).\n * - `'adaptive'` → adaptive thinking with no effort hint (model decides\n * everything). When `customBudget` is set, it is carried as `maxTokensCap`\n * so the request builder caps `max_tokens` accordingly — adaptive has no\n * native budget knob, but capping the response envelope soft-bounds the\n * thinking that lives inside it.\n * - Budgeted levels → adaptive thinking with an `effort` hint, unless\n * `customBudget` is provided.\n * - Any level + `customBudget` → explicit-budget `enabled` path. The caller\n * has opted into precise budget control and accepts the Anthropic\n * deprecation warning that comes with it on opus 4.6+. `'adaptive'` is the\n * sole exception: it never falls back to enabled, since adaptive is the\n * current Anthropic API surface for self-budgeted thinking.\n */\nexport function planAnthropicThinking(\n level: ThinkingLevel,\n customBudget?: number,\n): AnthropicThinkingPlan | null {\n if (level === 'off')\n return null\n if (level === 'adaptive') {\n if (typeof customBudget === 'number' && customBudget > 0)\n return { kind: 'adaptive', maxTokensCap: customBudget }\n return { kind: 'adaptive' }\n }\n if (customBudget !== undefined) {\n return { kind: 'enabled', budgetTokens: customBudget, maxTokensBump: customBudget }\n }\n return { kind: 'adaptive', effort: EFFORT_FOR_LEVEL[level] }\n}\n\n/**\n * Map Anthropic's native `stop_reason` to the zidane `TurnFinishReason` union.\n *\n * `pause_turn` and `model_context_window_exceeded` are 4.6+ stop reasons that\n * pre-Z21 collapsed to `'other'` and silently terminated the run. They now\n * map to `'pause'` and `'length'` respectively, and the surrounding caller\n * adjusts the `done` flag so the loop can recover.\n */\nfunction mapStopReason(stopReason: string | null | undefined): TurnFinishReason | undefined {\n if (!stopReason)\n return undefined\n switch (stopReason) {\n case 'end_turn':\n case 'stop_sequence':\n return 'stop'\n case 'tool_use':\n return 'tool-calls'\n case 'max_tokens':\n case 'model_context_window_exceeded':\n // 4.6+: response bumped against the model's context window mid-stream.\n // Treat as a length-class stop so consumers can prune + retry; the\n // partial assistant response is preserved.\n return 'length'\n case 'refusal':\n return 'content-filter'\n // 4.6+: server-side mid-turn pause for long thinking. The loop\n // continues with a synthetic continue message rather than terminating.\n case 'pause_turn':\n return 'pause'\n default:\n return 'other'\n }\n}\n\n/**\n * Conservative per-family output ceiling, used ONLY as a fallback when pi-ai's\n * model registry doesn't know the model (or pi-ai is externalized) — see\n * {@link resolveAnthropicMaxOutput}, which is the primary path. These are\n * non-beta floors we can always send without a 400; the registry reports the\n * real (often much higher, e.g. Opus 4.x = 128K native) max when available.\n */\nexport function defaultAnthropicMaxTokens(modelId: string): number {\n const id = modelId.toLowerCase()\n if (id.includes('haiku'))\n return 8192\n if (id.includes('sonnet'))\n return 64000\n // opus + anything unrecognized → 32000: a safe floor when the registry is\n // unavailable. The registry lifts this to the model's true max when present.\n return 32000\n}\n\n/**\n * Lazily-loaded pi-ai model-registry getter (cached; keeps the multi-MB\n * registry out of the static bundle, mirroring `cost.ts`). Returns `null` when\n * pi-ai is externalized, in which case callers fall back to the family map.\n */\nconst loadPiModelGetter = lazyAsync(\n async (): Promise<((provider: string, model: string) => { maxTokens?: number } | undefined) | null> => {\n try {\n const mod = await import('@earendil-works/pi-ai/providers/all')\n return mod.getBuiltinModel as unknown as (provider: string, model: string) => { maxTokens?: number } | undefined\n }\n catch {\n return null\n }\n },\n)\n\n/**\n * Resolve a model's max output tokens. Prefers pi-ai's bundled registry — the\n * authoritative source the SDK itself uses to build requests, so e.g. Opus 4.x\n * reports its full native 128K (no extended-output beta required) — and falls\n * back to {@link defaultAnthropicMaxTokens} only when the registry lacks the\n * model. Used as BOTH the default (when the caller leaves `maxTokens` unset)\n * and a hard clamp, since a `max_tokens` above the model's max would 400.\n */\nexport async function resolveAnthropicMaxOutput(modelId: string): Promise<number> {\n const getModel = await loadPiModelGetter()\n const registryMax = getModel?.('anthropic', modelId)?.maxTokens\n return typeof registryMax === 'number' && registryMax > 0\n ? registryMax\n : defaultAnthropicMaxTokens(modelId)\n}\n\nconst EPHEMERAL: Anthropic.CacheControlEphemeral = { type: 'ephemeral' }\n\n/**\n * Mutate an Anthropic request in place to add cache breakpoints on the three\n * stable prefixes:\n * 1. System prompt — `cache_control` on the static prefix only (see below).\n * 2. Tool definitions — last tool.\n * 3. Conversation — last content block of the last message.\n *\n * Each breakpoint tells Anthropic to cache the prefix ending at that block;\n * subsequent turns reuse the cached prefix and pay only for the delta. Safe\n * no-op when any prefix is empty (no tools, empty system, etc.).\n *\n * System-prompt boundary handling (`SYSTEM_PROMPT_BOUNDARY`):\n *\n * - When `originalSystem` is provided AND carries the marker, the system\n * block is rewritten as a 2-block array: static (cached) then dynamic\n * (uncached). The caller is expected to have already stripped the marker\n * from `params.system` via {@link renderSystemForWire} — this helper\n * only re-splits to apply `cache_control` cleanly.\n * - When `originalSystem` is omitted or marker-free, the existing single-\n * block treatment applies: `cache_control` on the whole system string.\n * - Array system prompts: existing semantics — `cache_control` lands on the\n * last block. Callers building their own multi-block layout own the\n * breakpoint placement.\n */\nexport function applyAnthropicCacheBreakpoints(\n params: Anthropic.MessageCreateParamsStreaming,\n originalSystem?: string,\n): void {\n if (typeof params.system === 'string') {\n if (params.system.length > 0) {\n // `originalSystem` is the authoritative source for the boundary split\n // — `params.system` may have been pre-rendered by `renderSystemForWire`.\n // Fall back to `params.system` itself when no original was supplied\n // (or an empty original) so an external caller that embedded a marker\n // directly doesn't leak it into the cached block on the wire.\n const splitSource = originalSystem && originalSystem.length > 0\n ? originalSystem\n : params.system\n const parts = splitSystemPrompt(splitSource)\n if (parts.dynamic.length > 0) {\n const blocks: Anthropic.TextBlockParam[] = []\n if (parts.static.length > 0)\n blocks.push({ type: 'text', text: parts.static, cache_control: EPHEMERAL })\n blocks.push({ type: 'text', text: parts.dynamic })\n params.system = blocks\n }\n else {\n // No marker — wrap the (already marker-free) string in a single\n // cached block. `parts.static` equals `splitSource` in this branch.\n params.system = [{ type: 'text', text: parts.static, cache_control: EPHEMERAL }]\n }\n }\n }\n else if (Array.isArray(params.system) && params.system.length > 0) {\n const lastIdx = params.system.length - 1\n params.system = params.system.map((block, i) =>\n i === lastIdx ? { ...block, cache_control: EPHEMERAL } : block,\n )\n }\n\n if (params.tools && params.tools.length > 0) {\n const lastIdx = params.tools.length - 1\n params.tools = params.tools.map((tool, i) =>\n i === lastIdx ? { ...tool, cache_control: EPHEMERAL } : tool,\n ) as Anthropic.ToolUnion[]\n }\n\n if (params.messages.length === 0)\n return\n const lastMsgIdx = params.messages.length - 1\n const lastMsg = params.messages[lastMsgIdx]\n if (typeof lastMsg.content === 'string') {\n if (lastMsg.content.length === 0)\n return\n params.messages[lastMsgIdx] = {\n ...lastMsg,\n content: [{ type: 'text', text: lastMsg.content, cache_control: EPHEMERAL }],\n }\n return\n }\n if (!Array.isArray(lastMsg.content) || lastMsg.content.length === 0)\n return\n const blocks = lastMsg.content\n // `ThinkingBlockParam` and `RedactedThinkingBlockParam` do not accept `cache_control`\n // in the SDK types — walk backward past any trailing thinking blocks to find the\n // nearest cache-eligible block. Returns -1 when every block is a thinking variant,\n // which skips this breakpoint; system + tools still carry the cache prefix.\n let targetIdx = blocks.length - 1\n while (targetIdx >= 0 && isThinkingBlock(blocks[targetIdx]))\n targetIdx -= 1\n if (targetIdx < 0)\n return\n const nextBlocks = blocks.slice()\n nextBlocks[targetIdx] = { ...nextBlocks[targetIdx], cache_control: EPHEMERAL } as typeof nextBlocks[number]\n params.messages[lastMsgIdx] = { ...lastMsg, content: nextBlocks }\n}\n\nfunction isThinkingBlock(block: { type: string }): boolean {\n return block.type === 'thinking' || block.type === 'redacted_thinking'\n}\n\n/**\n * Duck-type check for an Anthropic SDK `APIError` — avoids a runtime dependency\n * on `@anthropic-ai/sdk` so `classifyError` stays usable even when the optional\n * peer dep isn't loaded (e.g. host code calling it on an unrelated provider's\n * error).\n *\n * Anthropic's APIError shape: `.status: number` + `.error` (parsed body, object\n * or null). Plain `Error`s don't have `.status` + `.error`.\n */\nfunction looksLikeAnthropicApiError(err: unknown): boolean {\n if (!err || typeof err !== 'object')\n return false\n const e = err as { status?: unknown, error?: unknown }\n return typeof e.status === 'number' && 'error' in e\n}\n\n/**\n * Anthropic SSE `error` event types that warrant a retry. `overloaded_error`\n * is the headline case (capacity event mid-stream); `api_error` and\n * `timeout_error` are both server-side transient failures. `invalid_request_error`,\n * `authentication_error`, `permission_error`, `not_found_error`, `billing_error`\n * are terminal — retrying them would just hammer a bad request.\n *\n * `rate_limit_error` here would be redundant: the SDK's pre-stream retry\n * handles 429, and a mid-stream `rate_limit_error` is exceedingly rare. We\n * include it anyway for symmetry with the HTTP-level retry policy.\n */\nconst SSE_RETRYABLE_ERROR_TYPES = new Set<string>([\n 'overloaded_error',\n 'api_error',\n 'timeout_error',\n 'rate_limit_error',\n])\n\n/**\n * Try to parse an Anthropic SSE `error` event payload out of a thrown error's\n * message. The SDK's `MessageStream` surfaces mid-stream error events by\n * throwing an `AnthropicError` whose `.message` contains the raw JSON shape:\n *\n * {\"type\":\"error\",\"error\":{\"type\":\"overloaded_error\",\"message\":\"Overloaded\"},\"request_id\":\"req_...\"}\n *\n * Returns the inner `{ type, message }` or `null` when the message isn't\n * the canonical SSE error shape. Tolerant: silently returns `null` for\n * non-string input, non-JSON content, or any shape mismatch.\n */\nfunction matchSseErrorEvent(message: unknown): { type: string, message: string } | null {\n if (typeof message !== 'string' || message.length === 0)\n return null\n // Cheap pre-check to avoid JSON.parse on every error message.\n if (!message.includes('\"type\":\"error\"') && !message.includes('\"type\": \"error\"'))\n return null\n // The message may be the JSON itself or have it embedded (some SDK paths\n // prefix a human prelude). Scan for the first `{` and try from there.\n const start = message.indexOf('{')\n if (start < 0)\n return null\n try {\n const parsed = JSON.parse(message.slice(start)) as unknown\n if (!parsed || typeof parsed !== 'object')\n return null\n const outer = parsed as { type?: unknown, error?: unknown }\n if (outer.type !== 'error' || !outer.error || typeof outer.error !== 'object')\n return null\n const inner = outer.error as { type?: unknown, message?: unknown }\n if (typeof inner.type !== 'string')\n return null\n return {\n type: inner.type,\n message: typeof inner.message === 'string' ? inner.message : inner.type,\n }\n }\n catch {\n return null\n }\n}\n\n/**\n * Classify an Anthropic SDK / HTTP error for typed-error wrapping.\n *\n * - `prompt is too long` (400 invalid_request_error) → `context_exceeded`.\n * - `'tool_use' ids were found without 'tool_result' blocks` /\n * `'tool_result must be preceded by a tool_call'` (400) →\n * `tool_pairing_corruption`. The local repair pass should have caught\n * this — the typed error tells consumers their pre-send pipeline is\n * shipping orphaned blocks somehow (custom `context:transform` hook,\n * bypassed loop, mid-stream session edit) so they can patch the root\n * cause instead of chasing 400s.\n * - Any other Anthropic `APIError`-shaped value → `provider_error` with the\n * native status/type code.\n * - Unknown errors → `null` (loop wraps in `AgentProviderError` generically).\n *\n * Anthropic's wire error shape is `{ type: 'error', error: { type, message } }` — the\n * SDK stores the parsed body on `err.error`. We walk both levels so callers see the\n * most specific `providerCode` we can find.\n */\nexport function classifyAnthropicError(err: unknown): ClassifiedError | null {\n const prelude = classifyErrorPrelude(err)\n if (prelude === 'not-object')\n return null\n if (prelude === 'aborted')\n return { kind: 'aborted' }\n\n interface InnerError { type?: string, message?: string }\n interface AnthError {\n name?: string\n message?: string\n status?: number\n error?: InnerError & { error?: InnerError }\n }\n const anyErr = err as AnthError\n\n // Mid-stream SSE error event — the SDK's `MessageStream` surfaces these by\n // throwing an `AnthropicError` (plain Error subclass, no `.status`) whose\n // `.message` carries the raw SSE error JSON, e.g.\n // `{\"type\":\"error\",\"error\":{\"type\":\"overloaded_error\",\"message\":\"Overloaded\"},\"request_id\":\"...\"}`.\n // The pre-stream HTTP retry inside the SDK can't help here — the response\n // already opened with a 200, then the server emitted `event: error`\n // mid-stream. Classify these so the loop-level retry kicks in.\n //\n // Guarded on `!looksLikeAnthropicApiError(err)` so we don't shadow the\n // structured `APIError` branch below (which also stringifies its body into\n // `.message` and would otherwise match this same pattern).\n if (!looksLikeAnthropicApiError(err)) {\n const sseError = matchSseErrorEvent(anyErr.message)\n if (sseError) {\n return {\n kind: 'provider_error',\n providerCode: sseError.type,\n message: sseError.message,\n retryable: SSE_RETRYABLE_ERROR_TYPES.has(sseError.type),\n }\n }\n return null\n }\n\n // Prefer the inner error when present (Anthropic's nested shape).\n const innerType = anyErr.error?.error?.type\n const outerType = anyErr.error?.type\n const nativeType = innerType && innerType !== 'error' ? innerType : outerType\n const message = anyErr.error?.error?.message\n ?? anyErr.error?.message\n ?? anyErr.message\n ?? ''\n\n if (matchesContextExceeded(message)) {\n return {\n kind: 'context_exceeded',\n providerCode: nativeType ?? 'invalid_request_error',\n message,\n }\n }\n\n if (matchesToolPairingError(message)) {\n return {\n kind: 'tool_pairing_corruption',\n providerCode: nativeType ?? 'invalid_request_error',\n message,\n }\n }\n\n const status = anyErr.status\n const retryable = typeof status === 'number'\n ? isRetryableHttpStatus(status)\n : undefined\n\n return {\n kind: 'provider_error',\n providerCode: nativeType ?? (status ? String(status) : undefined),\n message,\n ...(retryable !== undefined ? { retryable } : {}),\n }\n}\n\nfunction shouldFallbackFromFastMode(err: unknown): boolean {\n const classified = classifyAnthropicError(err)\n if (classified?.kind === 'provider_error' && classified.retryable === true)\n return true\n\n const message = err instanceof Error ? err.message : String(err)\n return /\\b(?:fast|speed)\\b/i.test(message)\n && /capacity|overload|rate|unavailable|unsupported|not supported|not permitted|extra inputs|invalid/i.test(message)\n}\n\n/**\n * Build a user `SessionMessage` from multimodal prompt parts.\n *\n * - Text parts → text blocks.\n * - Image parts → base64 image blocks.\n * - Document parts → canonical document blocks. `toAnthropic` emits native\n * document blocks only for base64 PDFs; text/non-PDF documents fall back to\n * attachment-tagged text to avoid invalid Anthropic source media types.\n *\n * The format mirrors `defaultPromptMessage`'s output on shape — Anthropic-specific\n * handling differs only in that `promptMessage` being present tells the agent loop\n * to route PromptPart[] through this function rather than throwing on base64 docs.\n */\nfunction anthropicPromptMessage(parts: PromptPart[]): SessionMessage {\n const content: SessionContentBlock[] = []\n\n for (const part of parts) {\n if (part.type === 'text') {\n if (part.text.length > 0)\n content.push({ type: 'text', text: part.text })\n continue\n }\n\n if (part.type === 'image') {\n content.push({ type: 'image', mediaType: part.mediaType, data: part.data })\n continue\n }\n\n if (part.type === 'audio')\n throw unsupportedMediaError('audio', 'anthropic')\n if (part.type === 'video')\n throw unsupportedMediaError('video', 'anthropic')\n\n // document\n content.push({\n type: 'document',\n mediaType: part.mediaType,\n data: part.data,\n encoding: part.encoding,\n ...(part.name ? { name: part.name } : {}),\n })\n }\n\n return { role: 'user', content }\n}\n\nexport function anthropic(\n anthropicParams?: AnthropicParams,\n): Provider {\n const configuredApiKey = getConfiguredApiKey(anthropicParams)\n // Factory-time snapshot — used only for the static `meta.isOAuth` flag.\n // Keys resolve per call (env / file creds can change between construction\n // and a turn), so `countTokens` / `stream` re-derive OAuth-ness from the\n // freshly resolved key instead of trusting this.\n const isOAuth = configuredApiKey.includes('sk-ant-oat')\n const defaultModel = anthropicParams?.defaultModel || 'claude-opus-4-8'\n let runtimeCredentials = extractRuntimeCredentials(anthropicParams)\n\n return {\n name: 'anthropic',\n meta: {\n defaultModel,\n isOAuth,\n // True when server-side context management is configured: the API may\n // clear old tool results mid-conversation without the client knowing.\n // The loop reads this to disable the read_file dedup, whose client-side\n // hash can't tell that a server-cleared body left the wire — otherwise\n // the \"unchanged\" replay stub would strand the model on missing content.\n clearsContextServerSide: Boolean(anthropicParams?.contextManagement),\n capabilities: {\n vision: true,\n imageInToolResult: true,\n documents: true,\n // Anthropic Messages API has no audio/video input content block.\n audio: false,\n video: false,\n // Anthropic Messages API supports `web_search_20250305` as a\n // server-side tool — the model issues searches and consumes\n // results without a client-visible tool_use round-trip. The\n // canonical `web_search` `ToolDef` ships as a cross-provider\n // fallback; when this capability is set, `formatTools` rewrites\n // it to the server-tool wire shape and drops the function-tool\n // version so the model uses the native path.\n nativeWebSearch: { maxUses: 5 },\n },\n },\n\n formatTools(tools: ToolSpec[]): Anthropic.ToolUnion[] {\n // Sanitize before forwarding — Anthropic 400s on the strict shapes\n // (root `$ref`, root `oneOf|anyOf|allOf`, `type: ['object', 'null']`,\n // missing `properties`, `nullable: true`) that show up in MCP-server\n // schemas. The strict `anthropic` profile coerces those without\n // changing semantics for valid schemas. Warnings are de-duped to\n // `console.warn` so the same bad schema doesn't spam the log every\n // turn.\n //\n // `web_search` is special-cased: when present, it's emitted as the\n // server-tool `web_search_20250305` block and removed from the\n // function-tools list. The model invokes it without a client-side\n // round-trip; Anthropic injects the search results into the\n // assistant message directly. See the `nativeWebSearch` capability\n // declared above.\n const nativeWebSearch = this.meta.capabilities?.nativeWebSearch\n // `hasWebSearch` is the existence check; `fnTools` is the filtered\n // list. Computing existence via `tools.some` (rather than comparing\n // pre/post-filter lengths) makes the rewrite intent obvious at a\n // glance.\n const hasWebSearch = !!nativeWebSearch && tools.some(t => t.name === 'web_search')\n const fnTools = hasWebSearch ? tools.filter(t => t.name !== 'web_search') : tools\n const sanitized = sanitizeToolSpecs(fnTools, { profile: 'anthropic' })\n const out: Anthropic.ToolUnion[] = sanitized.map(t => ({\n name: t.name,\n description: t.description,\n input_schema: t.inputSchema as Anthropic.Tool['input_schema'],\n }))\n if (hasWebSearch && nativeWebSearch) {\n // `max_uses` is optional — omit when the capability doesn't pin a value\n // so Anthropic's server-side default applies.\n const block: Anthropic.WebSearchTool20250305 = {\n type: 'web_search_20250305',\n name: 'web_search',\n }\n if (typeof nativeWebSearch.maxUses === 'number')\n block.max_uses = nativeWebSearch.maxUses\n out.push(block)\n }\n return out\n },\n\n userMessage(content: string): SessionMessage {\n return { role: 'user', content: [{ type: 'text', text: content }] }\n },\n\n assistantMessage(content: string): SessionMessage {\n return { role: 'assistant', content: [{ type: 'text', text: content }] }\n },\n\n toolResultsMessage(results: ToolResult[]): SessionMessage {\n return {\n role: 'user',\n content: results.map(r => ({\n type: 'tool_result' as const,\n callId: r.id,\n output: r.content,\n ...(r.isError ? { isError: true as const } : {}),\n ...(r.metadata ? { metadata: r.metadata } : {}),\n })),\n }\n },\n\n promptMessage: anthropicPromptMessage,\n\n classifyError: classifyAnthropicError,\n\n async countTokens(payload, signal): Promise<number | null> {\n try {\n const SDK = await loadAnthropicSdk()\n const apiKey = await resolveOAuthApiKey(\n {\n provider: 'anthropic',\n providerId: 'anthropic',\n params: runtimeCredentials ? { ...anthropicParams, ...runtimeCredentials } : anthropicParams,\n envKey: 'ANTHROPIC_API_KEY',\n missingError: 'No API key found. Run `bun run auth` first.',\n refreshError: reason => `Anthropic OAuth token refresh failed. ${reason}`,\n },\n )\n // Derive OAuth-ness from the freshly resolved key — auth mode can\n // change between factory construction and this call (env var set,\n // creds file rotated), and a stale flag would skew the count's\n // system-prompt folding relative to what `stream()` actually sends.\n const callIsOAuth = apiKey.includes('sk-ant-oat')\n const client = createClient(\n SDK,\n apiKey,\n callIsOAuth,\n anthropicParams?.baseURL,\n anthropicParams?.extraBetas,\n await resolveExtraHeaders(anthropicParams?.extraHeaders),\n )\n\n // Mirror `stream()`'s wire-payload build so the count matches what is\n // actually sent: OAuth folds the real system prompt into a synthetic\n // user/assistant pair and sends the canonical Claude Code prompt as\n // the system block.\n const wireSystem = renderSystemForWire(payload.system)\n const system = callIsOAuth\n ? `You are Claude Code, Anthropic's official CLI for Claude.`\n : wireSystem\n const messages: SessionMessage[] = callIsOAuth && payload.system\n ? [\n { role: 'user', content: [{ type: 'text' as const, text: wireSystem }] },\n { role: 'assistant', content: [{ type: 'text' as const, text: 'Understood. I will proceed with these instructions above the rest of my system prompt.' }] },\n ...payload.messages,\n ]\n : [...payload.messages]\n\n const res = await client.messages.countTokens(\n {\n model: payload.model as Model,\n system,\n tools: payload.tools as Anthropic.Tool[],\n messages: messages.map(m => toAnthropic(m)) as Anthropic.MessageParam[],\n },\n signal ? { signal } : undefined,\n )\n return res.input_tokens\n }\n catch (err) {\n // Unreachable under the active auth / network — caller falls back to\n // the heuristic. Surface the cause when debugging so a silent auth /\n // scope rejection (e.g. OAuth lacking the count endpoint) is visible.\n if (process.env.ZIDANE_DEBUG_COUNT)\n console.error('[anthropic.countTokens] failed:', err)\n return null\n }\n },\n\n async stream(options, callbacks: StreamCallbacks): Promise<TurnResult> {\n const SDK = await loadAnthropicSdk()\n const apiKey = await resolveOAuthApiKey(\n {\n provider: 'anthropic',\n providerId: 'anthropic',\n params: runtimeCredentials ? { ...anthropicParams, ...runtimeCredentials } : anthropicParams,\n envKey: 'ANTHROPIC_API_KEY',\n missingError: 'No API key found. Run `bun run auth` first.',\n refreshError: reason => `Anthropic OAuth token refresh failed. Run \\`bun run auth --anthropic\\` again. ${reason}`,\n },\n {\n ...callbacks,\n async onOAuthRefresh(ctx) {\n if (ctx.source === 'params') {\n runtimeCredentials = {\n access: ctx.credentials.access,\n refresh: ctx.credentials.refresh,\n expires: ctx.credentials.expires,\n }\n }\n await callbacks.onOAuthRefresh?.(ctx)\n },\n },\n )\n // Fast mode (`speed: \"fast\"`) is gated behind the `fast-mode-2026-02-01`\n // beta — without the header Anthropic rejects the body field with\n // `speed: Extra inputs are not permitted`. Inject it here (not in the\n // static default betas) so the header is present exactly when the request\n // will carry `speed: \"fast\"`, keeping non-fast requests byte-stable for\n // the prompt cache.\n const requestBetas = options.modelOptions?.fast\n ? [...(anthropicParams?.extraBetas ?? []), FAST_MODE_BETA]\n : anthropicParams?.extraBetas\n // Per-call OAuth detection — see the matching note in `countTokens`.\n const callIsOAuth = apiKey.includes('sk-ant-oat')\n const client = createClient(\n SDK,\n apiKey,\n callIsOAuth,\n anthropicParams?.baseURL,\n requestBetas,\n await resolveExtraHeaders(anthropicParams?.extraHeaders),\n )\n\n // System prompt injection is handled by agent.ts — we just pass it through.\n // For OAuth, the Claude Code API requires the system block to start with\n // the canonical Claude Code prompt — we prepend it to the real system prompt.\n // For OAuth, the CC API requires the system block to start with the canonical\n // Claude Code prompt. The real system prompt is injected as user+assistant messages.\n //\n // The boundary marker (`SYSTEM_PROMPT_BOUNDARY`) is structural metadata\n // for `applyAnthropicCacheBreakpoints`; strip it before the bytes hit\n // the wire so it never reaches the model. The original (un-rendered)\n // string is preserved as `options.system` and forwarded to the cache\n // helper below for an accurate static/dynamic split.\n const wireSystem = renderSystemForWire(options.system)\n const system = callIsOAuth\n ? `You are Claude Code, Anthropic's official CLI for Claude.`\n : wireSystem\n const messages: SessionMessage[] = callIsOAuth && options.system\n ? [\n { role: 'user', content: [{ type: 'text' as const, text: wireSystem }] },\n { role: 'assistant', content: [{ type: 'text' as const, text: 'Understood. I will proceed with these instructions above the rest of my system prompt.' }] },\n ...options.messages,\n ]\n : [...options.messages]\n const thinking = options.thinking ?? 'off'\n\n const modelId = options.model as Model\n // The model's max output, sourced from pi-ai's registry (Opus 4.x = 128K\n // native; conservative family fallback when the registry misses). Used as\n // BOTH the default (when `maxTokens` is unset — the field is required) AND\n // a hard clamp, since a value above the model's max would 400.\n const modelMaxOutput = await resolveAnthropicMaxOutput(modelId)\n\n // `context_management` is an untyped beta in SDK v0.90; widen `params` with\n // an optional field so we don't have to double-cast through `unknown` to\n // attach it below.\n const params: Anthropic.MessageCreateParamsStreaming & {\n context_management?: AnthropicContextManagement\n speed?: 'fast' | 'standard'\n } = {\n // Forward-compat escape hatch for un-typed beta fields. Spread first so\n // the typed core (model / max_tokens / system / tools / messages /\n // stream) and the explicit `context_management` below override on\n // collision — explicit always wins.\n ...((anthropicParams?.extraBodyParams ?? {}) as Record<string, unknown>),\n model: modelId,\n // Required field — default to the model's full max output when unset.\n // The ceiling (incl. a caller cap above the model max) is enforced once,\n // after the thinking plan, since the `enabled` path adds budget on top.\n max_tokens: options.maxTokens ?? modelMaxOutput,\n system,\n tools: options.tools as Anthropic.ToolUnion[],\n messages: messages.map(m => toAnthropic(m)) as Anthropic.MessageParam[],\n stream: true,\n }\n\n // Server-side context management (beta `context-management-2025-06-27`).\n // Skipped when the caller didn't pass a config — leaving the field off\n // the wire keeps requests valid on accounts that don't have the beta enabled.\n if (anthropicParams?.contextManagement)\n params.context_management = anthropicParams.contextManagement\n\n // Prompt caching — inject `cache_control: { type: 'ephemeral' }` breakpoints\n // on the three largest stable prefixes: system prompt, tool definitions, and\n // the last message's final content block. Three of Anthropic's four allowed\n // breakpoints; leaves headroom for a future thinking-block breakpoint.\n //\n // On the API-key path, `params.system` was derived from\n // `renderSystemForWire(options.system)` so the cache helper can split\n // the original on the `SYSTEM_PROMPT_BOUNDARY` marker and apply\n // `cache_control` to the static prefix only — per-turn churn in the\n // dynamic suffix no longer invalidates the cached doctrine above.\n //\n // On the OAuth path, `params.system` is the canonical Claude Code\n // prompt (`\"You are Claude Code...\"`) — `options.system` lives in the\n // injected user message instead, so we do NOT forward it here. Passing\n // it would replace the CC prompt with the user's doctrine. The user\n // message is still naturally cached via the last-message breakpoint\n // applied below; per-turn churn there bypasses the boundary feature on\n // OAuth, but parity with the pre-feature behavior is preserved.\n //\n // Skipped when `options.cache === false` (opt-out) or on any shape that would\n // produce an invalid request (empty system string, zero tools, empty message list).\n if (options.cache !== false)\n applyAnthropicCacheBreakpoints(params, callIsOAuth ? undefined : options.system)\n\n // Enable thinking / extended thinking when requested. The default path\n // for level-based control is `thinking.type='adaptive'` plus an\n // `output_config.effort` hint — Anthropic deprecated the explicit\n // `thinking.type='enabled' + budget_tokens` shape on opus 4.6+ and\n // recommends adaptive instead. Callers who need a precise token budget\n // can opt into the explicit-budget path by setting\n // `behavior.thinkingBudget`, accepting Anthropic's deprecation\n // warning in exchange. Either shape requires `temperature=1`.\n //\n // `display: 'summarized'` is set explicitly on both shapes — Opus 4.7\n // and Claude Mythos Preview silently flipped the default to\n // `'omitted'` (signature-only, no text), which makes the SDK fire\n // `thinking` deltas with empty `thinking` content and breaks every\n // host that surfaces traces live. Older models still default to\n // `'summarized'` so this is a no-op there.\n const plan = planAnthropicThinking(thinking, options.thinkingBudget)\n if (plan) {\n if (plan.kind === 'enabled') {\n params.thinking = { type: 'enabled', budget_tokens: plan.budgetTokens, display: 'summarized' }\n params.max_tokens = plan.maxTokensBump + params.max_tokens\n }\n else {\n params.thinking = { type: 'adaptive', display: 'summarized' }\n if (plan.effort)\n params.output_config = { effort: plan.effort }\n // Adaptive has no native budget knob — soft-cap the response envelope\n // (`max_tokens`) so unbounded thinking can't run away. Reduce only;\n // never raise above the caller's request.\n if (typeof plan.maxTokensCap === 'number' && plan.maxTokensCap > 0)\n params.max_tokens = Math.min(params.max_tokens, plan.maxTokensCap)\n }\n params.temperature = 1\n }\n\n // The `enabled` thinking path adds the budget ON TOP of `max_tokens`,\n // which — now that the base defaults to the model's full max — can push\n // the envelope past the model's hard ceiling → 400. Clamp the final value\n // so the budget shares the envelope rather than overflowing it.\n params.max_tokens = Math.min(params.max_tokens, modelMaxOutput)\n\n // Fast mode (`speed: \"fast\"`) — ~2.5× output throughput at premium\n // pricing. Supported only on Opus 4.6 / 4.7 / 4.8; the model-options\n // surface only offers it on those, and Anthropic 400s a request with\n // `speed` on an unsupported model. `speed` is an untyped beta field in\n // SDK v0.90 (widened on `params` above).\n if (options.modelOptions?.fast)\n params.speed = 'fast'\n\n // Tool choice forcing\n if (options.toolChoice) {\n if (options.toolChoice.type === 'tool' && options.toolChoice.name)\n params.tool_choice = { type: 'tool', name: options.toolChoice.name }\n else if (options.toolChoice.type === 'required')\n params.tool_choice = { type: 'any' }\n else\n params.tool_choice = { type: 'auto' }\n }\n\n const runStream = async (\n requestParams: typeof params,\n fastRequested: boolean,\n ): Promise<TurnResult> => {\n const s = client.messages.stream(requestParams, {\n signal: options.signal,\n })\n\n let text = ''\n let observedOutput = false\n\n s.on('text', (delta) => {\n observedOutput = true\n text += delta\n callbacks.onText(delta)\n })\n\n if (callbacks.onThinking) {\n s.on('thinking', (delta) => {\n observedOutput = true\n callbacks.onThinking!(delta)\n })\n }\n\n // Liveness signal for tool-only turns: `inputJson` fires as a\n // `tool_use` block's arguments stream in. Without it, a turn that\n // emits only tool calls (no text/thinking) never trips the loop's\n // \"first response observed\" gate and the first-response watchdog can\n // abort an actively-progressing stream. (B2)\n if (callbacks.onToolCallDelta) {\n s.on('inputJson', () => {\n observedOutput = true\n callbacks.onToolCallDelta!()\n })\n }\n\n // Server-tool blocks (e.g. `web_search_20250305`) execute on Anthropic's\n // side and arrive in the assistant content stream — not via our tool\n // loop. The SDK fires `contentBlock` once per block as it closes, so\n // listen for the two we care about and dispatch generic callbacks.\n // Other block types (text, thinking, tool_use, …) have their own paths\n // above / in `finalMessage()` — ignored here to avoid double-emission.\n if (callbacks.onServerToolUse || callbacks.onServerToolResult) {\n s.on('contentBlock', (block) => {\n observedOutput = true\n if (block.type === 'server_tool_use') {\n callbacks.onServerToolUse?.({\n id: block.id,\n name: block.name,\n input: (block.input as Record<string, unknown> | undefined) ?? {},\n })\n }\n else if (block.type === 'web_search_tool_result') {\n callbacks.onServerToolResult?.({\n toolUseId: block.tool_use_id,\n toolName: 'web_search',\n content: block.content,\n })\n }\n })\n }\n\n let response: Anthropic.Message\n try {\n response = await s.finalMessage()\n }\n catch (err) {\n if (fastRequested && !observedOutput && shouldFallbackFromFastMode(err)) {\n const standardParams = { ...requestParams }\n delete standardParams.speed\n return await runStream(standardParams, false)\n }\n throw err\n }\n\n const toolCalls = response.content\n .filter((b): b is Anthropic.ToolUseBlock => b.type === 'tool_use')\n .map(b => ({ id: b.id, name: b.name, input: b.input as Record<string, unknown> }))\n\n const finishReason = mapStopReason(response.stop_reason)\n\n // `pause_turn` is *not* terminal — the loop has to inject a continue\n // message and run another turn. Without this branch, the previous\n // `toolCalls.length === 0` shortcut would mark the run done at the\n // pause and silently drop the work in flight.\n const isPause = response.stop_reason === 'pause_turn'\n\n const rawUsage: TurnUsage = {\n input: response.usage.input_tokens,\n output: response.usage.output_tokens,\n cacheCreation: response.usage.cache_creation_input_tokens ?? undefined,\n cacheRead: response.usage.cache_read_input_tokens ?? undefined,\n ...(finishReason ? { finishReason } : {}),\n modelId: response.model ?? (options.model as string),\n }\n // OAuth = subscription traffic (Claude Pro/Max via a Claude Code\n // account): usage is included in the flat plan, not billed per\n // token. Estimating API-list prices for it produces large dollar\n // figures the user never pays — so `cost` stays absent and cost\n // surfaces (footer counter, usage-by-model rows) show tokens\n // without a price, matching Claude Code's own /cost behavior.\n // API-key turns estimate as before; a mixed session (OAuth primary\n // with an API-key fallback, or vice versa) prices each turn by how\n // it was actually served.\n const usage = callIsOAuth\n ? rawUsage\n : await fillEstimatedCost(rawUsage, 'anthropic', fastRequested\n ? fastModeCostMultiplier(response.model ?? (options.model as string))\n : undefined)\n return {\n assistantMessage: fromAnthropic({ role: 'assistant', content: response.content }),\n text,\n toolCalls,\n done: !isPause && (response.stop_reason === 'end_turn' || toolCalls.length === 0),\n usage,\n }\n }\n\n return await runStream(params, options.modelOptions?.fast === true)\n },\n }\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"auth-D5FfJtLf.js","names":["CLIENT_ID","AUTHORIZE_URL","TOKEN_URL","CALLBACK_PORT","CALLBACK_PATH","PROVIDER_NAME","parseAuthorizationInput","exchangeAuthorizationCode","getModels","getModel"],"sources":["../src/chat/oauth-page/server.ts","../src/chat/oauth-page/anthropic.ts","../src/chat/oauth-page/openai-codex.ts","../src/chat/oauth-page/render.ts","../src/chat/oauth-page/index.ts","../src/chat/providers.ts","../src/chat/credentials.ts","../src/chat/auth.ts"],"sourcesContent":["/**\n * Local HTTP callback server primitive shared by the Anthropic + Codex\n * login flows. Owns three concerns:\n *\n * 1. Bind a loopback listener (configurable port + host via\n * `PI_OAUTH_CALLBACK_HOST`, matching pi-ai's semantics).\n * 2. Parse the redirect query string into `{ code, state }` or surface\n * a structured error.\n * 3. Render the result page through the caller-supplied renderer.\n *\n * Each provider clones the lifecycle wrapper (`waitForCode` / `cancelWait`\n * / `close`) so the higher-level login code reads the same on either\n * side. The only thing the server itself decides is the HTML.\n */\n\nimport type { IncomingMessage, Server, ServerResponse } from 'node:http'\nimport type { OAuthCallbackPageRenderer } from './render'\nimport { createServer } from 'node:http'\n\nconst CALLBACK_HOST = process.env.PI_OAUTH_CALLBACK_HOST || '127.0.0.1'\n\nexport interface CallbackServerOptions {\n /** Loopback port. Must match the `redirect_uri` registered with the provider. */\n port: number\n /** Path component of the redirect URI (e.g. `/callback`, `/auth/callback`). */\n path: string\n /**\n * Expected `state` value. The server rejects any callback whose `state`\n * doesn't match, exactly. For Anthropic this is the PKCE verifier; for\n * Codex it's a random 16-byte hex string.\n */\n expectedState: string\n /** Display name passed through to {@link OAuthCallbackPageRenderer}. */\n providerName: string\n /** HTML renderer for both success + error pages. */\n renderPage: OAuthCallbackPageRenderer\n /** Success-page body shown after a clean redirect. */\n successMessage: string\n /**\n * How to react when `server.listen` itself fails (typically `EADDRINUSE`\n * when another instance is mid-flow). `\"reject\"` propagates the error to\n * the awaiting `start` caller; `\"resolveWithStub\"` returns a degraded\n * handle whose `waitForCode` immediately resolves to `null`, so the\n * caller can fall back to manual paste. Anthropic uses `\"reject\"`,\n * Codex uses `\"resolveWithStub\"` to match pi-ai's per-provider behavior.\n */\n onListenError?: 'reject' | 'resolveWithStub'\n}\n\nexport interface CallbackServerHandle {\n /** Fully-qualified redirect URI to advertise to the provider. */\n redirectUri: string\n /**\n * Resolves when the redirect lands (with `{ code, state? }`) or when\n * {@link cancelWait} is invoked (with `null`). Never throws.\n */\n waitForCode: () => Promise<{ code: string, state?: string } | null>\n /** Unblock `waitForCode` early (e.g. manual paste won the race). */\n cancelWait: () => void\n /** Close the listener. Idempotent. */\n close: () => void\n}\n\ninterface PendingResolution {\n settle: (value: { code: string, state?: string } | null) => void\n}\n\nfunction buildRequestHandler(opts: CallbackServerOptions, pending: PendingResolution) {\n return (req: IncomingMessage, res: ServerResponse) => {\n try {\n const url = new URL(req.url || '', 'http://localhost')\n\n if (url.pathname !== opts.path) {\n respondError(res, 404, opts, 'Callback route not found.')\n return\n }\n\n const error = url.searchParams.get('error')\n if (error) {\n respondError(res, 400, opts, `${opts.providerName} authentication did not complete.`, `Error: ${error}`)\n return\n }\n\n const code = url.searchParams.get('code')\n const state = url.searchParams.get('state')\n\n if (!code || !state) {\n respondError(res, 400, opts, 'Missing code or state parameter.')\n return\n }\n\n if (state !== opts.expectedState) {\n respondError(res, 400, opts, 'State mismatch.')\n return\n }\n\n res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })\n res.end(opts.renderPage({\n kind: 'success',\n provider: opts.providerName,\n message: opts.successMessage,\n }))\n pending.settle({ code, state })\n }\n catch {\n // We intentionally swallow the actual error here: surfacing a stack\n // to the user's browser would leak internals, and the login flow\n // is about to fall back to manual paste anyway.\n respondError(res, 500, opts, 'Internal error while processing the callback.')\n }\n }\n}\n\nfunction respondError(\n res: ServerResponse,\n status: number,\n opts: CallbackServerOptions,\n message: string,\n details?: string,\n) {\n res.writeHead(status, { 'Content-Type': 'text/html; charset=utf-8' })\n res.end(opts.renderPage({\n kind: 'error',\n provider: opts.providerName,\n message,\n details,\n }))\n}\n\nfunction buildStubHandle(redirectUri: string, server: Server | null): CallbackServerHandle {\n return {\n redirectUri,\n waitForCode: async () => null,\n cancelWait: () => {},\n close: () => {\n try { server?.close() }\n catch { /* already closed */ }\n },\n }\n}\n\n/**\n * Start the loopback HTTP callback server. The returned handle is the\n * caller's contract — they `await waitForCode()`, race it against manual\n * paste, then `close()` in a `finally`.\n */\nexport async function startCallbackServer(opts: CallbackServerOptions): Promise<CallbackServerHandle> {\n const onListenError = opts.onListenError ?? 'reject'\n const redirectUri = `http://localhost:${opts.port}${opts.path}`\n\n return new Promise<CallbackServerHandle>((resolve, reject) => {\n let settled = false\n const pending: PendingResolution = {\n settle: () => {},\n }\n const waitPromise = new Promise<{ code: string, state?: string } | null>((resolveWait) => {\n pending.settle = (value) => {\n if (settled)\n return\n settled = true\n resolveWait(value)\n }\n })\n\n const server = createServer(buildRequestHandler(opts, pending))\n\n server.on('error', (err) => {\n if (onListenError === 'resolveWithStub') {\n pending.settle(null)\n resolve(buildStubHandle(redirectUri, server))\n return\n }\n reject(err)\n })\n\n server.listen(opts.port, CALLBACK_HOST, () => {\n resolve({\n redirectUri,\n waitForCode: () => waitPromise,\n cancelWait: () => pending.settle(null),\n close: () => {\n try { server.close() }\n catch { /* already closed */ }\n },\n })\n })\n })\n}\n","/**\n * Anthropic OAuth flow with a customisable callback page.\n *\n * Mirrors `loginAnthropic` from `@earendil-works/pi-ai/oauth` 1:1 except:\n *\n * 1. The HTTP callback server's response HTML routes through\n * {@link OAuthCallbackPageRenderer} instead of pi-ai's baked-in page.\n * 2. The lifecycle is delegated to {@link startCallbackServer} so the\n * Codex sibling and any future loopback provider share one HTTP impl.\n *\n * Refresh + `getApiKey` delegate straight to pi-ai's exports — those don't\n * touch HTML and there's no upside to forking them.\n *\n * Endpoints / client id / port / scopes match pi-ai `0.79.10`. Bump in\n * lockstep on the next `@earendil-works/pi-ai` upgrade, or delete this\n * file once pi-ai exposes a `renderCallbackPage` callback upstream.\n */\n\nimport type {\n OAuthCredentials,\n OAuthLoginCallbacks,\n OAuthProviderInterface,\n} from '@earendil-works/pi-ai/oauth'\nimport type { OAuthCallbackPageRenderer } from './render'\nimport { refreshAnthropicToken } from '@earendil-works/pi-ai/oauth'\nimport { generatePkce } from './pkce'\nimport { startCallbackServer } from './server'\n\n// Pulled verbatim from pi-ai 0.79.10 — `dist/utils/oauth/anthropic.js`.\nconst CLIENT_ID = atob('OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl')\nconst AUTHORIZE_URL = 'https://claude.ai/oauth/authorize'\nconst TOKEN_URL = 'https://platform.claude.com/v1/oauth/token'\nconst CALLBACK_PORT = 53692\nconst CALLBACK_PATH = '/callback'\nconst SCOPES = 'org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload'\nconst PROVIDER_NAME = 'Anthropic'\n\n/**\n * Parse what the user pasted into the manual-code prompt. Accepts:\n * - the bare authorization code\n * - the full `redirect_uri?code=...&state=...` URL\n * - the `code#state` shorthand Anthropic surfaces on the page\n * - a raw query string with `code=...&state=...`\n *\n * Matches pi-ai's parse table so an existing user's muscle memory still works.\n */\nfunction parseAuthorizationInput(input: string): { code?: string, state?: string } {\n const value = input.trim()\n if (!value)\n return {}\n\n try {\n const url = new URL(value)\n return {\n code: url.searchParams.get('code') ?? undefined,\n state: url.searchParams.get('state') ?? undefined,\n }\n }\n catch {\n // fall through to non-URL parsing below\n }\n\n if (value.includes('#')) {\n const [code, state] = value.split('#', 2)\n return { code, state }\n }\n if (value.includes('code=')) {\n const params = new URLSearchParams(value)\n return {\n code: params.get('code') ?? undefined,\n state: params.get('state') ?? undefined,\n }\n }\n return { code: value }\n}\n\nasync function postJson(url: string, body: Record<string, unknown>): Promise<string> {\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(30_000),\n })\n const responseBody = await response.text()\n if (!response.ok)\n throw new Error(`HTTP request failed. status=${response.status}; url=${url}; body=${responseBody}`)\n return responseBody\n}\n\nasync function exchangeAuthorizationCode(\n code: string,\n state: string,\n verifier: string,\n redirectUri: string,\n): Promise<OAuthCredentials> {\n const responseBody = await postJson(TOKEN_URL, {\n grant_type: 'authorization_code',\n client_id: CLIENT_ID,\n code,\n state,\n redirect_uri: redirectUri,\n code_verifier: verifier,\n })\n\n const tokenData = JSON.parse(responseBody) as {\n refresh_token: string\n access_token: string\n expires_in: number\n }\n\n return {\n refresh: tokenData.refresh_token,\n access: tokenData.access_token,\n // -5min so a refresh fires before the upstream expiry; matches pi-ai.\n expires: Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000,\n }\n}\n\nexport interface LoginAnthropicWithPageOptions extends Pick<\n OAuthLoginCallbacks,\n 'onAuth' | 'onPrompt' | 'onProgress' | 'onManualCodeInput'\n> {\n renderPage: OAuthCallbackPageRenderer\n}\n\nexport async function loginAnthropicWithCustomPage(\n options: LoginAnthropicWithPageOptions,\n): Promise<OAuthCredentials> {\n const { verifier, challenge } = await generatePkce()\n const server = await startCallbackServer({\n port: CALLBACK_PORT,\n path: CALLBACK_PATH,\n expectedState: verifier,\n providerName: PROVIDER_NAME,\n renderPage: options.renderPage,\n successMessage: 'Anthropic authentication completed. You can close this window.',\n onListenError: 'reject',\n })\n\n let code: string | undefined\n let state: string | undefined\n\n try {\n const authParams = new URLSearchParams({\n code: 'true',\n client_id: CLIENT_ID,\n response_type: 'code',\n redirect_uri: server.redirectUri,\n scope: SCOPES,\n code_challenge: challenge,\n code_challenge_method: 'S256',\n state: verifier,\n })\n\n options.onAuth({\n url: `${AUTHORIZE_URL}?${authParams.toString()}`,\n instructions: 'Complete login in your browser. If the browser is on another machine, paste the final redirect URL here.',\n })\n\n if (options.onManualCodeInput) {\n let manualInput: string | undefined\n let manualError: Error | undefined\n const manualPromise = options\n .onManualCodeInput()\n .then((input) => {\n manualInput = input\n server.cancelWait()\n })\n .catch((err) => {\n manualError = err instanceof Error ? err : new Error(String(err))\n server.cancelWait()\n })\n\n const result = await server.waitForCode()\n if (manualError)\n throw manualError\n\n if (result?.code) {\n code = result.code\n state = result.state\n }\n else if (manualInput) {\n const parsed = parseAuthorizationInput(manualInput)\n if (parsed.state && parsed.state !== verifier)\n throw new Error('OAuth state mismatch')\n code = parsed.code\n state = parsed.state ?? verifier\n }\n\n if (!code) {\n await manualPromise\n if (manualError)\n throw manualError\n if (manualInput) {\n const parsed = parseAuthorizationInput(manualInput)\n if (parsed.state && parsed.state !== verifier)\n throw new Error('OAuth state mismatch')\n code = parsed.code\n state = parsed.state ?? verifier\n }\n }\n }\n else {\n const result = await server.waitForCode()\n if (result?.code) {\n code = result.code\n state = result.state\n }\n }\n\n if (!code) {\n const input = await options.onPrompt({\n message: 'Paste the authorization code or full redirect URL:',\n placeholder: server.redirectUri,\n })\n const parsed = parseAuthorizationInput(input)\n if (parsed.state && parsed.state !== verifier)\n throw new Error('OAuth state mismatch')\n code = parsed.code\n state = parsed.state ?? verifier\n }\n\n if (!code)\n throw new Error('Missing authorization code')\n if (!state)\n throw new Error('Missing OAuth state')\n\n options.onProgress?.('Exchanging authorization code for tokens...')\n return await exchangeAuthorizationCode(code, state, verifier, server.redirectUri)\n }\n finally {\n server.close()\n }\n}\n\n/**\n * Build an `OAuthProviderInterface` that behaves identically to pi-ai's\n * `anthropicOAuthProvider` except for the callback page HTML. Drop this\n * onto `ProviderDescriptor.oauthProvider` to override.\n */\nexport function createAnthropicOAuthProviderWithCustomPage(\n renderPage: OAuthCallbackPageRenderer,\n): OAuthProviderInterface {\n return {\n id: 'anthropic',\n name: 'Anthropic (Claude Pro/Max)',\n usesCallbackServer: true,\n async login(callbacks: OAuthLoginCallbacks) {\n return loginAnthropicWithCustomPage({\n renderPage,\n onAuth: callbacks.onAuth,\n onPrompt: callbacks.onPrompt,\n onProgress: callbacks.onProgress,\n onManualCodeInput: callbacks.onManualCodeInput,\n })\n },\n async refreshToken(credentials: OAuthCredentials) {\n return refreshAnthropicToken(credentials.refresh)\n },\n getApiKey(credentials: OAuthCredentials) {\n return credentials.access\n },\n }\n}\n","/**\n * OpenAI Codex (ChatGPT Plus/Pro) OAuth flow with a customisable callback\n * page. 1:1 mirror of pi-ai `0.79.10`'s `loginOpenAICodex` except the HTTP\n * callback server routes through {@link OAuthCallbackPageRenderer}.\n *\n * Refresh delegates to pi-ai's exported `refreshOpenAICodexToken` —\n * Codex's refresh path involves JWT decoding for `accountId`, and we don't\n * want to duplicate it. The HTML override is purely about the landing page.\n */\n\nimport type {\n OAuthCredentials,\n OAuthLoginCallbacks,\n OAuthProviderInterface,\n} from '@earendil-works/pi-ai/oauth'\nimport type { OAuthCallbackPageRenderer } from './render'\nimport { randomBytes } from 'node:crypto'\nimport { refreshOpenAICodexToken } from '@earendil-works/pi-ai/oauth'\nimport { generatePkce } from './pkce'\nimport { startCallbackServer } from './server'\n\n// Pulled verbatim from pi-ai 0.79.10 — `dist/utils/oauth/openai-codex.js`.\nconst CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann'\nconst AUTHORIZE_URL = 'https://auth.openai.com/oauth/authorize'\nconst TOKEN_URL = 'https://auth.openai.com/oauth/token'\nconst CALLBACK_PORT = 1455\nconst CALLBACK_PATH = '/auth/callback'\nconst SCOPE = 'openid profile email offline_access'\nconst JWT_CLAIM_PATH = 'https://api.openai.com/auth'\nconst PROVIDER_NAME = 'OpenAI'\n\nfunction createState(): string {\n return randomBytes(16).toString('hex')\n}\n\nfunction parseAuthorizationInput(input: string): { code?: string, state?: string } {\n const value = input.trim()\n if (!value)\n return {}\n\n try {\n const url = new URL(value)\n return {\n code: url.searchParams.get('code') ?? undefined,\n state: url.searchParams.get('state') ?? undefined,\n }\n }\n catch {\n // fall through\n }\n\n if (value.includes('#')) {\n const [code, state] = value.split('#', 2)\n return { code, state }\n }\n if (value.includes('code=')) {\n const params = new URLSearchParams(value)\n return {\n code: params.get('code') ?? undefined,\n state: params.get('state') ?? undefined,\n }\n }\n return { code: value }\n}\n\ninterface JwtPayload {\n [key: string]: unknown\n [JWT_CLAIM_PATH]?: { chatgpt_account_id?: string }\n}\n\nfunction decodeJwt(token: string): JwtPayload | null {\n try {\n const parts = token.split('.')\n if (parts.length !== 3)\n return null\n const payload = parts[1] ?? ''\n const decoded = atob(payload)\n return JSON.parse(decoded) as JwtPayload\n }\n catch {\n return null\n }\n}\n\nfunction getAccountId(accessToken: string): string | null {\n const payload = decodeJwt(accessToken)\n const claim = payload?.[JWT_CLAIM_PATH] as { chatgpt_account_id?: string } | undefined\n const accountId = claim?.chatgpt_account_id\n return typeof accountId === 'string' && accountId.length > 0 ? accountId : null\n}\n\ninterface TokenExchangeSuccess {\n type: 'success'\n access: string\n refresh: string\n expires: number\n}\n\ninterface TokenExchangeFailure {\n type: 'failed'\n message: string\n}\n\nasync function exchangeAuthorizationCode(\n code: string,\n verifier: string,\n redirectUri: string,\n): Promise<TokenExchangeSuccess | TokenExchangeFailure> {\n const response = await fetch(TOKEN_URL, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n client_id: CLIENT_ID,\n code,\n code_verifier: verifier,\n redirect_uri: redirectUri,\n }),\n })\n\n if (!response.ok) {\n const text = await response.text().catch(() => '')\n return {\n type: 'failed',\n message: `OpenAI Codex token exchange failed (${response.status}): ${text || response.statusText}`,\n }\n }\n\n const json = await response.json() as {\n access_token?: string\n refresh_token?: string\n expires_in?: number\n }\n if (!json.access_token || !json.refresh_token || typeof json.expires_in !== 'number') {\n return {\n type: 'failed',\n message: `OpenAI Codex token exchange response missing fields: ${JSON.stringify(json)}`,\n }\n }\n return {\n type: 'success',\n access: json.access_token,\n refresh: json.refresh_token,\n expires: Date.now() + json.expires_in * 1000,\n }\n}\n\nexport interface LoginOpenAICodexWithPageOptions extends Pick<\n OAuthLoginCallbacks,\n 'onAuth' | 'onPrompt' | 'onProgress' | 'onManualCodeInput'\n> {\n renderPage: OAuthCallbackPageRenderer\n /** OAuth `originator` parameter. Defaults to `\"pi\"` to stay observable upstream. */\n originator?: string\n}\n\nexport async function loginOpenAICodexWithCustomPage(\n options: LoginOpenAICodexWithPageOptions,\n): Promise<OAuthCredentials> {\n const { verifier, challenge } = await generatePkce()\n const state = createState()\n const originator = options.originator ?? 'pi'\n\n const server = await startCallbackServer({\n port: CALLBACK_PORT,\n path: CALLBACK_PATH,\n expectedState: state,\n providerName: PROVIDER_NAME,\n renderPage: options.renderPage,\n successMessage: 'OpenAI authentication completed. You can close this window.',\n // Matches pi-ai: Codex falls back to manual paste on port collision.\n onListenError: 'resolveWithStub',\n })\n\n const authUrl = new URL(AUTHORIZE_URL)\n authUrl.searchParams.set('response_type', 'code')\n authUrl.searchParams.set('client_id', CLIENT_ID)\n authUrl.searchParams.set('redirect_uri', server.redirectUri)\n authUrl.searchParams.set('scope', SCOPE)\n authUrl.searchParams.set('code_challenge', challenge)\n authUrl.searchParams.set('code_challenge_method', 'S256')\n authUrl.searchParams.set('state', state)\n authUrl.searchParams.set('id_token_add_organizations', 'true')\n authUrl.searchParams.set('codex_cli_simplified_flow', 'true')\n authUrl.searchParams.set('originator', originator)\n\n options.onAuth({\n url: authUrl.toString(),\n instructions: 'A browser window should open. Complete login to finish.',\n })\n\n let code: string | undefined\n\n try {\n if (options.onManualCodeInput) {\n let manualCode: string | undefined\n let manualError: Error | undefined\n const manualPromise = options\n .onManualCodeInput()\n .then((input) => {\n manualCode = input\n server.cancelWait()\n })\n .catch((err) => {\n manualError = err instanceof Error ? err : new Error(String(err))\n server.cancelWait()\n })\n\n const result = await server.waitForCode()\n if (manualError)\n throw manualError\n\n if (result?.code) {\n code = result.code\n }\n else if (manualCode) {\n const parsed = parseAuthorizationInput(manualCode)\n if (parsed.state && parsed.state !== state)\n throw new Error('State mismatch')\n code = parsed.code\n }\n\n if (!code) {\n await manualPromise\n if (manualError)\n throw manualError\n if (manualCode) {\n const parsed = parseAuthorizationInput(manualCode)\n if (parsed.state && parsed.state !== state)\n throw new Error('State mismatch')\n code = parsed.code\n }\n }\n }\n else {\n const result = await server.waitForCode()\n if (result?.code)\n code = result.code\n }\n\n if (!code) {\n const input = await options.onPrompt({\n message: 'Paste the authorization code (or full redirect URL):',\n })\n const parsed = parseAuthorizationInput(input)\n if (parsed.state && parsed.state !== state)\n throw new Error('State mismatch')\n code = parsed.code\n }\n\n if (!code)\n throw new Error('Missing authorization code')\n\n const tokenResult = await exchangeAuthorizationCode(code, verifier, server.redirectUri)\n if (tokenResult.type !== 'success')\n throw new Error(tokenResult.message)\n\n const accountId = getAccountId(tokenResult.access)\n if (!accountId)\n throw new Error('Failed to extract accountId from token')\n\n return {\n access: tokenResult.access,\n refresh: tokenResult.refresh,\n expires: tokenResult.expires,\n accountId,\n }\n }\n finally {\n server.close()\n }\n}\n\n/**\n * Build an `OAuthProviderInterface` that behaves identically to pi-ai's\n * `openaiCodexOAuthProvider` except for the callback page HTML.\n */\nexport function createOpenAICodexOAuthProviderWithCustomPage(\n renderPage: OAuthCallbackPageRenderer,\n): OAuthProviderInterface {\n return {\n id: 'openai-codex',\n name: 'ChatGPT Plus/Pro (Codex Subscription)',\n usesCallbackServer: true,\n async login(callbacks: OAuthLoginCallbacks) {\n return loginOpenAICodexWithCustomPage({\n renderPage,\n onAuth: callbacks.onAuth,\n onPrompt: callbacks.onPrompt,\n onProgress: callbacks.onProgress,\n onManualCodeInput: callbacks.onManualCodeInput,\n })\n },\n async refreshToken(credentials: OAuthCredentials) {\n return refreshOpenAICodexToken(credentials.refresh)\n },\n getApiKey(credentials: OAuthCredentials) {\n return credentials.access\n },\n }\n}\n","/**\n * HTML rendered by the local OAuth callback server when the browser hits\n * `redirect_uri`. This is the page the user sees the moment authentication\n * finishes — by default pi-ai bakes in its own dark page; this module lets\n * a host swap it for something branded without forking the package.\n *\n * Renderer is a single pure function — `(page) => string` — so a host can\n * inline a literal template, pull from a theme registry, or proxy a static\n * asset built at compile time. Anything that returns a `string`.\n */\n\n/**\n * Outcome rendered on the callback page. `kind` drives status code on the\n * underlying HTTP response (`200` for success, `4xx` for error); the rest\n * is content. `provider` is the human-readable name of the OAuth provider\n * (e.g. `\"Anthropic\"`, `\"OpenAI Codex\"`) so a renderer can swap copy or\n * logos per provider.\n */\nexport interface OAuthCallbackPage {\n kind: 'success' | 'error'\n /** Headline message — usually one sentence. Safe to include user-visible details. */\n message: string\n /** Optional secondary details (e.g. provider error code). Rendered in mono. */\n details?: string\n /** Provider display name passed through from the login function. */\n provider: string\n}\n\nexport type OAuthCallbackPageRenderer = (page: OAuthCallbackPage) => string\n\nfunction escapeHtml(value: string): string {\n return value\n .replaceAll('&', '&amp;')\n .replaceAll('<', '&lt;')\n .replaceAll('>', '&gt;')\n .replaceAll('\"', '&quot;')\n .replaceAll('\\'', '&#39;')\n}\n\n/**\n * Default zidane-themed page. Visually neutral but distinguishable from\n * pi-ai's stock page — dark background, mono headings, no logo (host can\n * pass a custom renderer to add one).\n */\nexport const renderDefaultCallbackPage: OAuthCallbackPageRenderer = (page) => {\n const title = page.kind === 'success'\n ? `Signed in to ${page.provider}`\n : `Could not sign in to ${page.provider}`\n const heading = escapeHtml(title)\n const message = escapeHtml(page.message)\n const details = page.details ? escapeHtml(page.details) : undefined\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>${heading}</title>\n <style>\n :root {\n --text: #f4f4f5;\n --text-dim: #a1a1aa;\n --accent: ${page.kind === 'success' ? '#22d3ee' : '#f87171'};\n --page-bg: #0a0a0a;\n --panel-bg: #131316;\n --border: #27272a;\n --font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;\n }\n * { box-sizing: border-box; }\n html { color-scheme: dark; }\n body {\n margin: 0;\n min-height: 100vh;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n background: var(--page-bg);\n color: var(--text);\n font-family: var(--font-sans);\n }\n main {\n width: 100%;\n max-width: 520px;\n padding: 32px;\n background: var(--panel-bg);\n border: 1px solid var(--border);\n border-radius: 12px;\n }\n .label {\n font-family: var(--font-mono);\n font-size: 12px;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--accent);\n margin-bottom: 12px;\n }\n h1 {\n margin: 0 0 12px;\n font-size: 22px;\n font-weight: 600;\n letter-spacing: -0.01em;\n color: var(--text);\n }\n p {\n margin: 0;\n line-height: 1.6;\n color: var(--text-dim);\n font-size: 14px;\n }\n .details {\n margin-top: 18px;\n padding: 12px 14px;\n background: var(--page-bg);\n border: 1px solid var(--border);\n border-radius: 8px;\n font-family: var(--font-mono);\n font-size: 12px;\n color: var(--text-dim);\n white-space: pre-wrap;\n word-break: break-word;\n }\n </style>\n</head>\n<body>\n <main>\n <div class=\"label\">zidane · OAuth · ${escapeHtml(page.provider)}</div>\n <h1>${heading}</h1>\n <p>${message}</p>\n ${details ? `<div class=\"details\">${details}</div>` : ''}\n </main>\n</body>\n</html>`\n}\n\n/**\n * Tiny helper kept private to the module so renderer authors can reuse\n * the same escaping rules as the default theme. Re-exported in case a\n * host wants to embed user-supplied messages safely.\n */\nexport const escapeHtmlForCallbackPage: (value: string) => string = escapeHtml\n","/**\n * Custom OAuth callback page — drop-in replacements for pi-ai's built-in\n * Anthropic + OpenAI Codex providers that route the post-redirect HTML\n * through your own renderer.\n *\n * Why this module exists\n * ----------------------\n * `@earendil-works/pi-ai` (as of `0.79.10`) bakes its callback page into\n * `loginAnthropic` / `loginOpenAICodex` with no override. The upstream\n * fix is a tiny `renderCallbackPage` hook on `OAuthLoginCallbacks`; until\n * that lands this module forks the two flows in one self-contained place\n * — everything else (token refresh, `getApiKey`, env handling) delegates\n * back to pi-ai unchanged.\n *\n * How to use\n * ----------\n *\n * ```ts\n * import { createCustomCallbackOAuthProviders, renderDefaultCallbackPage } from './chat/oauth-page'\n * import { BUILTIN_PROVIDERS } from './chat/providers'\n *\n * const { anthropic, openaiCodex } = createCustomCallbackOAuthProviders(renderDefaultCallbackPage)\n *\n * const providers = {\n * ...BUILTIN_PROVIDERS,\n * anthropic: { ...BUILTIN_PROVIDERS.anthropic, oauthProvider: anthropic },\n * openai: { ...BUILTIN_PROVIDERS.openai, oauthProvider: openaiCodex },\n * }\n * ```\n *\n * Or override per provider with `createAnthropicOAuthProviderWithCustomPage`\n * / `createOpenAICodexOAuthProviderWithCustomPage` directly.\n *\n * Deleting this module\n * --------------------\n * When pi-ai exposes `renderCallbackPage` on `OAuthLoginCallbacks`:\n *\n * 1. Delete this folder.\n * 2. Surface the upstream callback through `OAuthFlowOptions` in\n * `src/chat/oauth.ts` (one optional pass-through field).\n * 3. Re-point any descriptors that imported from here to pi-ai's\n * `anthropicOAuthProvider` / `openaiCodexOAuthProvider` directly.\n *\n * Pinned to pi-ai `0.79.10`. Re-verify endpoints + ports on every pi-ai\n * bump (auth URLs, client IDs, callback ports are duplicated verbatim\n * here; see `anthropic.ts` and `openai-codex.ts` for the source-of-truth\n * pointers).\n */\n\nimport type { OAuthProviderInterface } from '@earendil-works/pi-ai/oauth'\nimport type { OAuthCallbackPageRenderer } from './render'\nimport { registerOAuthProvider } from '@earendil-works/pi-ai/oauth'\nimport { createAnthropicOAuthProviderWithCustomPage } from './anthropic'\nimport { createCursorOAuthProvider } from './cursor'\nimport { createOpenAICodexOAuthProviderWithCustomPage } from './openai-codex'\nimport { createXaiOAuthProvider } from './xai'\n\nexport type {\n LoginAnthropicWithPageOptions,\n} from './anthropic'\nexport {\n createAnthropicOAuthProviderWithCustomPage,\n loginAnthropicWithCustomPage,\n} from './anthropic'\nexport {\n createCursorOAuthProvider,\n CURSOR_OAUTH_PROVIDER_ID,\n loginCursor,\n refreshCursorToken,\n} from './cursor'\nexport type {\n LoginOpenAICodexWithPageOptions,\n} from './openai-codex'\nexport {\n createOpenAICodexOAuthProviderWithCustomPage,\n loginOpenAICodexWithCustomPage,\n} from './openai-codex'\nexport type {\n OAuthCallbackPage,\n OAuthCallbackPageRenderer,\n} from './render'\nexport {\n escapeHtmlForCallbackPage,\n renderDefaultCallbackPage,\n} from './render'\nexport {\n createXaiOAuthProvider,\n loginXai,\n refreshXaiToken,\n XAI_OAUTH_PROVIDER_ID,\n xaiBaseUrl,\n} from './xai'\nexport type { XaiOAuthCredentials } from './xai'\n\nexport interface CustomCallbackOAuthProviders {\n anthropic: OAuthProviderInterface\n openaiCodex: OAuthProviderInterface\n}\n\n/**\n * Bundle helper — returns both Anthropic + OpenAI Codex providers wired\n * with the same renderer. Hosts that want different renderers per provider\n * should call the individual `create…WithCustomPage` factories instead.\n */\nexport function createCustomCallbackOAuthProviders(\n renderPage: OAuthCallbackPageRenderer,\n): CustomCallbackOAuthProviders {\n return {\n anthropic: createAnthropicOAuthProviderWithCustomPage(renderPage),\n openaiCodex: createOpenAICodexOAuthProviderWithCustomPage(renderPage),\n }\n}\n\nlet cursorRegistered = false\n\n/**\n * Register Cursor with pi-ai's OAuth registry.\n *\n * Unlike Anthropic / Codex, Cursor is **not** built into pi-ai, so\n * `getOAuthApiKey('cursor', …)` (used by `resolveOAuthApiKey` for lazy\n * token refresh) would throw \"Unknown OAuth provider\" until we register it.\n * Call once at startup from both the CLI auth path and the TUI. Idempotent.\n */\nexport function registerCursorOAuthProvider(): OAuthProviderInterface {\n const provider = createCursorOAuthProvider()\n if (!cursorRegistered) {\n registerOAuthProvider(provider)\n cursorRegistered = true\n }\n return provider\n}\n\nlet xaiRegistered = false\n\n/**\n * Register xAI Grok with pi-ai's OAuth registry.\n *\n * pi-ai ships an xAI *API-key* provider, but no OAuth one — so\n * `getOAuthApiKey('xai-oauth', …)` (lazy refresh in `resolveOAuthApiKey`)\n * would throw \"Unknown OAuth provider\" until we register our flow. The\n * callback page routes through the supplied renderer so the post-redirect\n * HTML matches zidane's theme. Call once at startup; idempotent.\n */\nexport function registerXaiOAuthProvider(\n renderPage: OAuthCallbackPageRenderer,\n): OAuthProviderInterface {\n const provider = createXaiOAuthProvider(renderPage)\n if (!xaiRegistered) {\n registerOAuthProvider(provider)\n xaiRegistered = true\n }\n return provider\n}\n","/**\n * Provider registry — the customization seam for the TUI.\n *\n * A {@link ProviderDescriptor} carries everything the TUI needs to know about\n * a provider in one place: how to instantiate it, what to show in the UI,\n * how to detect credentials, how to OAuth (if applicable), and where to look\n * up its models. Hosts can ship their own descriptors instead of the built-in\n * four — see {@link BUILTIN_PROVIDERS}.\n */\n\nimport type { OAuthProviderInterface } from '@earendil-works/pi-ai/oauth'\nimport type { Provider } from '../providers'\nimport { getBuiltinModel as getModel, getBuiltinModels as getModels } from '@earendil-works/pi-ai/providers/all'\nimport { anthropic, arcee, baseten, cerebras, cursor, local, openai, openrouter, xai } from '../providers'\nimport { ANTHROPIC_EXTRA_MODELS, FAST_MODE_OPTIONS } from './anthropic-models'\nimport { createCustomCallbackOAuthProviders, registerCursorOAuthProvider, registerXaiOAuthProvider, renderDefaultCallbackPage } from './oauth-page'\n\n/**\n * pi-ai's stock Anthropic + Codex OAuth providers bake their own callback\n * HTML; we route both through `renderDefaultCallbackPage` so the post-redirect\n * page matches zidane's theme. The override lives in `src/chat/oauth-page/`\n * — see that folder's `index.ts` for the deletion path once pi-ai exposes\n * a `renderCallbackPage` hook upstream.\n */\nconst { anthropic: anthropicOAuthProvider, openaiCodex: openaiCodexOAuthProvider }\n = createCustomCallbackOAuthProviders(renderDefaultCallbackPage)\n\n/**\n * Cursor isn't built into pi-ai, so registering it makes lazy token refresh\n * (`getOAuthApiKey('cursor', …)` inside `resolveOAuthApiKey`) resolve to our\n * Cursor flow. The poll-based login has no callback server, so no themed page.\n */\nconst cursorOAuthProvider = registerCursorOAuthProvider()\n\n/**\n * xAI Grok isn't built into pi-ai's OAuth registry (only its API-key provider\n * is), so register our loopback-PKCE flow — themed via `renderDefaultCallbackPage`\n * — so lazy token refresh resolves through it.\n */\nconst xaiOAuthProvider = registerXaiOAuthProvider(renderDefaultCallbackPage)\n\n/**\n * Structural model metadata — compatible with pi-ai's `Model` interface but\n * not coupled to it, so hosts can pass either pi-ai-shaped objects or a\n * custom registry of their own.\n *\n * Deliberately a structural duplicate of pi-ai's `Model` rather than a\n * re-export: keeps the public API stable when pi-ai bumps shape, and lets\n * hosts implement their own registry without depending on pi-ai's types.\n *\n * Lives here (rather than `./config`) because `ProviderDescriptor.models`\n * needs it — pushing it to `config.tsx` created an import cycle.\n */\nexport interface ModelInfo {\n id: string\n name?: string\n contextWindow: number\n maxTokens?: number\n reasoning?: boolean\n input?: readonly ('text' | 'image')[]\n cost?: { input: number, output: number, cacheRead?: number, cacheWrite?: number }\n provider?: string\n /**\n * Model-specific toggleable knobs surfaced in the UI (e.g. Anthropic's\n * \"fast mode\"). Each option is an independent boolean the user flips on the\n * active model; enabled options are collected into `StreamOptions.modelOptions`\n * keyed by {@link ModelOption.id} and applied to the wire by the provider.\n * Omitted / empty → the model has no custom options and the picker is hidden.\n */\n options?: readonly ModelOption[]\n}\n\n/**\n * A single toggleable model option. Declarative metadata only — the provider\n * owns the actual request-body translation (keyed on {@link id}). Kept generic\n * (not \"fast mode\"-specific) so new per-model knobs land without a type change.\n */\nexport interface ModelOption {\n /** Stable identifier; the key under which the toggle persists + reaches the provider. */\n id: string\n /** Short label shown in the picker (e.g. \"Fast mode\"). */\n label: string\n /** One-line description of the trade-off (shown in the picker row). */\n description?: string\n /**\n * Optional cost multipliers applied to the model's base rates while the\n * option is enabled. `{ input: 2, output: 2 }` doubles both. Used by the\n * cost estimator so the footer reflects premium-priced modes (fast mode).\n */\n costMultiplier?: { input?: number, output?: number, cacheRead?: number, cacheWrite?: number }\n}\n\n/**\n * Free-form configuration field that a provider needs the user to supply\n * alongside (or instead of) an API key — e.g. a self-hosted base URL, an\n * Azure deployment name, a default model id.\n *\n * The wizard prompts for each field in order. Submitted values are persisted\n * into the apikey credential (as `customFields[key]`) and mirrored into\n * `process.env[envVar]` so the provider's factory can read them via the same\n * env-var convention as `envKey`.\n *\n * Descriptors without an `envKey` use `customFields` as the sole credential\n * mechanism — the wizard skips its API-key step in that case.\n */\nexport interface CustomField {\n /** Storage key inside the credential's `customFields` record. */\n key: string\n /** Human-readable label shown in the wizard step's title. */\n label: string\n /** Env var name to mirror this field into at TUI launch + on submit. */\n envVar: string\n /** Optional placeholder shown in the wizard input. */\n placeholder?: string\n /** Optional helper copy rendered above the input. */\n hint?: string\n /** When true, an empty submission is rejected by the wizard. Default: false. */\n required?: boolean\n}\n\nexport interface ProviderDescriptor {\n /**\n * Unique identifier. Used as the registry key, persisted in `state.json`\n * as the resumed provider, and (by default) as the credential-file key.\n */\n key: string\n /** Display name shown in the TUI's provider picker and labels. */\n label: string\n /**\n * Factory that builds a fresh `Provider` instance. Called on every session\n * activation. Some factories (e.g. zidane's built-in `anthropic`) eagerly\n * resolve credentials at construction time and throw when none are\n * configured — set {@link defaultModel} on the descriptor to avoid the TUI\n * calling the factory before the user has picked + authed a provider.\n */\n factory: () => Provider\n /**\n * Default model id to seed the picker with. When omitted, the TUI falls\n * back to `descriptor.factory().meta.defaultModel`, which constructs the\n * provider — fine for lazy-credential factories, but breaks for factories\n * that throw on missing credentials before the wizard runs. Setting this\n * eagerly lets the TUI render the auth wizard without ever instantiating\n * the provider.\n */\n defaultModel?: string\n /**\n * Env var checked when detecting whether the user has an API key\n * configured for this provider. Optional — set to `undefined` when the\n * provider only supports OAuth (or only credentials via a custom path).\n */\n envKey?: string\n /**\n * Key under which credentials live in `credentials.json`. Defaults to\n * `key`. The only built-in that overrides this is OpenAI Codex\n * (`openai` → `openai-codex`) to stay compatible with the harness\n * provider's existing lookup.\n */\n credentialFileKey?: string\n /** Placeholder shown in the wizard's API-key input. */\n apiKeyPlaceholder?: string\n /**\n * pi-ai (or compat) OAuth provider. When present, the wizard offers an\n * OAuth option in addition to API key.\n */\n oauthProvider?: OAuthProviderInterface\n /**\n * Optional copy appended to the wizard's \"OAuth\" method description.\n * Use to communicate why a user might pick OAuth (e.g. subscription tier,\n * org-wide SSO). Shown after `browser-based sign-in`. Skip the leading\n * space — the wizard adds it. Example: `'Claude Pro/Max subscription'`.\n */\n oauthHint?: string\n /**\n * pi-ai provider id used to look up models in pi-ai's built-in registry.\n * Defaults to `key`. Only Codex differs (`openai` → `openai-codex`).\n */\n piProviderId?: string\n /**\n * Back-compat aliases for persisted model ids whose upstream slug changed.\n * Used for metadata lookups only; aliases are not surfaced as picker rows.\n */\n modelAliases?: Readonly<Record<string, string>>\n /**\n * Override the model list returned for this provider's picker. When set,\n * skips pi-ai's registry entirely. Useful for hosts maintaining their\n * own model catalogue or for custom providers pi-ai doesn't know about.\n */\n models?: readonly ModelInfo[]\n /**\n * Additional free-form fields the wizard should collect from the user\n * (e.g. base URL for a self-hosted endpoint, default model slug for a\n * provider with no fixed catalogue). Each field is persisted in the\n * apikey credential's `customFields` map and mirrored into the matching\n * `envVar` at TUI launch.\n *\n * When `envKey` is also set, the wizard collects custom fields **before**\n * the API key. When `envKey` is omitted, the wizard skips the API-key\n * step entirely — `customFields` becomes the sole credential mechanism\n * (used by the `local` provider, which authenticates via baseURL only).\n */\n customFields?: readonly CustomField[]\n /**\n * Extra models merged AHEAD of pi-ai's registry list (de-duped by id, pi-ai\n * wins on collision). Unlike {@link models} this augments rather than\n * replaces — used to surface freshly-shipped models pi-ai hasn't bundled\n * yet without losing the rest of the registry. Ignored when {@link models}\n * is set.\n */\n extraModels?: readonly ModelInfo[]\n /**\n * Resolve the custom {@link ModelOption}s a given model id supports. Used to\n * attach options (e.g. fast mode) to models that come from pi-ai's registry —\n * which {@link extraModels} can't reach because pi-ai wins the id collision.\n * Returns `undefined` / `[]` for models with no options. Models that already\n * carry `options` (e.g. via {@link extraModels}) keep theirs.\n */\n optionsFor?: (modelId: string) => readonly ModelOption[] | undefined\n /**\n * Env var holding a user-supplied context window (in tokens) for providers\n * that have no model registry to advertise one — chiefly the `local`\n * provider, whose runtime (Ollama, vLLM, …) doesn't expose its loaded\n * model's window over the OpenAI-compat API. When set, {@link getContextWindow}\n * falls back to resolving this env var (via {@link resolveContextWindow})\n * after the registry lookup comes up empty. The value is either a single\n * window (`\"32768\"`, applied to every model) or a per-model map\n * (`\"llama3.1:8b=32768, qwen=128k, 64k\"`, with an optional bare fallback).\n * Pair it with a {@link CustomField} that mirrors the user's value into the\n * same `envVar` so the footer's context indicator and auto-compaction work\n * for local models.\n */\n contextWindowEnvVar?: string\n}\n\n/** Convenience accessor — returns `credentialFileKey ?? key`. */\nexport function credKeyOf(desc: ProviderDescriptor): string {\n return desc.credentialFileKey ?? desc.key\n}\n\n/** Convenience accessor — returns `piProviderId ?? key`. */\nexport function piIdOf(desc: ProviderDescriptor): string {\n return desc.piProviderId ?? desc.key\n}\n\n// ---------------------------------------------------------------------------\n// Built-in descriptors\n// ---------------------------------------------------------------------------\n\nexport const anthropicDescriptor: ProviderDescriptor = {\n key: 'anthropic',\n label: 'Anthropic',\n factory: anthropic,\n defaultModel: 'claude-opus-4-8',\n envKey: 'ANTHROPIC_API_KEY',\n apiKeyPlaceholder: 'sk-ant-…',\n oauthProvider: anthropicOAuthProvider,\n oauthHint: 'Claude Pro/Max subscription',\n extraModels: ANTHROPIC_EXTRA_MODELS,\n optionsFor: id => FAST_MODE_OPTIONS[id] ? [FAST_MODE_OPTIONS[id]] : undefined,\n}\n\nexport const openaiDescriptor: ProviderDescriptor = {\n key: 'openai',\n label: 'OpenAI',\n factory: openai,\n defaultModel: 'gpt-5.5',\n envKey: 'OPENAI_CODEX_API_KEY',\n credentialFileKey: 'openai-codex',\n piProviderId: 'openai-codex',\n apiKeyPlaceholder: 'sk-… or eyJ… (Codex)',\n oauthProvider: openaiCodexOAuthProvider,\n optionsFor: id => id === 'gpt-5.5'\n ? [{\n id: 'fast',\n label: 'Fast mode',\n description: 'Priority tier at 2.5× pricing',\n costMultiplier: { input: 2.5, output: 2.5, cacheRead: 2.5, cacheWrite: 2.5 },\n }]\n : undefined,\n}\n\nexport const openrouterDescriptor: ProviderDescriptor = {\n key: 'openrouter',\n label: 'OpenRouter',\n factory: openrouter,\n defaultModel: 'anthropic/claude-sonnet-4.6',\n envKey: 'OPENROUTER_API_KEY',\n apiKeyPlaceholder: 'sk-or-…',\n // Metadata-side mirror of the OpenRouter provider's wire alias (see\n // `MODEL_ALIASES` in `src/providers/openrouter.ts`). Keeps context-window /\n // reasoning lookups resolving for sessions persisted with the old slug.\n modelAliases: {\n 'anthropic/claude-sonnet-4-6': 'anthropic/claude-sonnet-4.6',\n },\n}\n\nexport const cerebrasDescriptor: ProviderDescriptor = {\n key: 'cerebras',\n label: 'Cerebras',\n factory: cerebras,\n defaultModel: 'zai-glm-4.7',\n envKey: 'CEREBRAS_API_KEY',\n apiKeyPlaceholder: 'csk-…',\n}\n\n/**\n * xAI Grok. Supports BOTH a static API key (`XAI_API_KEY`) and the\n * SuperGrok / X Premium+ OAuth subscription, so the wizard offers both methods.\n * Models come from pi-ai's `xai` registry (OpenAI-compatible endpoint), shared\n * by both auth modes.\n */\nexport const xaiDescriptor: ProviderDescriptor = {\n key: 'xai',\n label: 'xAI Grok',\n factory: xai,\n defaultModel: 'grok-4.3',\n envKey: 'XAI_API_KEY',\n apiKeyPlaceholder: 'xai-…',\n oauthProvider: xaiOAuthProvider,\n oauthHint: 'SuperGrok / X Premium+ subscription',\n // pi-ai stores the OAuth credential under the provider id, distinct from the\n // `xai` descriptor key so the API-key and OAuth credentials never collide.\n credentialFileKey: 'xai-oauth',\n piProviderId: 'xai',\n}\n\n/**\n * Arcee Trinity models (Platform API slugs). pi-ai has no Arcee registry, so we\n * ship a curated list. `reasoning: true` surfaces the effort/thinking UI — the\n * `arcee()` factory round-trips Trinity's `reasoning_content` across turns so the\n * model resumes its chain-of-thought through tool calls.\n *\n * Costs/context per Arcee's published Trinity catalogue.\n */\nconst ARCEE_MODELS: readonly ModelInfo[] = [\n { id: 'trinity-large-thinking', name: 'Trinity Large (Thinking)', contextWindow: 256_000, reasoning: true, input: ['text'], cost: { input: 0.25, output: 0.8 } },\n { id: 'trinity-large-preview', name: 'Trinity Large (Preview)', contextWindow: 128_000, input: ['text'], cost: { input: 0.45, output: 0.15 } },\n { id: 'trinity-mini', name: 'Trinity Mini', contextWindow: 128_000, input: ['text'], cost: { input: 0.045, output: 0.15 } },\n]\n\nexport const arceeDescriptor: ProviderDescriptor = {\n key: 'arcee',\n label: 'Arcee',\n factory: arcee,\n defaultModel: 'trinity-large-thinking',\n envKey: 'ARCEE_API_KEY',\n apiKeyPlaceholder: 'arcee-…',\n models: ARCEE_MODELS,\n}\n\n/**\n * Baseten Model APIs catalogue. pi-ai has no Baseten registry, so we ship a\n * curated list mirroring https://docs.baseten.co/inference/model-apis/overview\n * (context windows) and Baseten's published per-MTok rates. Models without a\n * `cost` (GLM 5.1, Nemotron Ultra) have no published rate yet — the footer\n * cost indicator hides for those. `/v1/models` is the live source of truth.\n *\n * All listed models support reasoning (`reasoning_content`); the `baseten()`\n * factory maps the thinking level to `reasoning_effort` / `chat_template_args`\n * per model.\n */\nconst BASETEN_MODELS: readonly ModelInfo[] = [\n { id: 'zai-org/GLM-5.2', name: 'GLM 5.2', contextWindow: 1_000_000, reasoning: true, input: ['text'] },\n { id: 'zai-org/GLM-5.1', name: 'GLM 5.1', contextWindow: 200_000, reasoning: true, input: ['text'] },\n { id: 'zai-org/GLM-5', name: 'GLM 5', contextWindow: 200_000, reasoning: true, input: ['text'], cost: { input: 0.95, output: 3.15, cacheRead: 0.2 } },\n { id: 'zai-org/GLM-4.7', name: 'GLM 4.7', contextWindow: 200_000, reasoning: true, input: ['text'], cost: { input: 0.6, output: 2.2, cacheRead: 0.12 } },\n { id: 'moonshotai/Kimi-K2.6', name: 'Kimi K2.6', contextWindow: 256_000, reasoning: true, input: ['text', 'image'], cost: { input: 1, output: 3.9, cacheRead: 0.2 } },\n { id: 'moonshotai/Kimi-K2.5', name: 'Kimi K2.5', contextWindow: 256_000, reasoning: true, input: ['text', 'image'], cost: { input: 0.6, output: 3, cacheRead: 0.12 } },\n { id: 'deepseek-ai/DeepSeek-V4-Pro', name: 'DeepSeek V4 Pro', contextWindow: 131_000, reasoning: true, input: ['text'], cost: { input: 1.74, output: 3.48, cacheRead: 0.145 } },\n { id: 'nvidia/Nemotron-120B-A12B', name: 'Nemotron Super', contextWindow: 200_000, reasoning: true, input: ['text'], cost: { input: 0.3, output: 0.75, cacheRead: 0.06 } },\n { id: 'nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B', name: 'Nemotron Ultra', contextWindow: 200_000, reasoning: true, input: ['text'] },\n { id: 'openai/gpt-oss-120b', name: 'GPT-OSS 120B', contextWindow: 128_000, reasoning: true, input: ['text'], cost: { input: 0.1, output: 0.5 } },\n]\n\nexport const basetenDescriptor: ProviderDescriptor = {\n key: 'baseten',\n label: 'Baseten',\n factory: baseten,\n defaultModel: 'zai-org/GLM-5.1',\n envKey: 'BASETEN_API_KEY',\n apiKeyPlaceholder: 'baseten-api-key…',\n models: BASETEN_MODELS,\n}\n\n/**\n * Conservative context-window assumption for a user-configured local model.\n * Local runtimes expose no reliable per-model metadata over the OpenAI-compat\n * `/v1/models` endpoint (it returns ids, not context sizes), so we pick a\n * floor that fits the smallest mainstream OSS models (Llama 3.x 8B, Qwen2.5)\n * without over-promising. The footer's context indicator and auto-compaction\n * lean on this — under-estimating is the safe direction (compact early rather\n * than overflow the server's real window).\n *\n * Users running larger-context models (e.g. a 128K Qwen) can raise it via the\n * `LOCAL_LLM_CONTEXT_WINDOW` env var / customField, which supports a single\n * value or a per-model map (resolved by {@link resolveContextWindow}).\n */\nconst LOCAL_DEFAULT_CONTEXT_WINDOW = 8_192\n\n/**\n * Local OpenAI-compatible LLM (Ollama, vLLM, LM Studio, Lemonade, llama.cpp).\n *\n * No fixed base URL or model catalogue — both come from the user via\n * `customFields`. No `envKey`, so the wizard skips the API-key prompt and\n * treats the customFields-only credential as the auth signal (see\n * `applyApiKeyEnv` + `detectAuth`).\n *\n * Vision / cache / reasoning are off by default in the factory — flip them\n * by passing a custom descriptor that calls `local({ capabilities })`.\n *\n * `models` is a getter, not a static list: there's no fixed catalogue to ship,\n * but once the user has configured a default model (mirrored into\n * `LOCAL_LLM_DEFAULT_MODEL` by `applyApiKeyEnv`), we surface it as a\n * single-entry list so the model picker + footer aren't empty. Resolved at\n * access time because the env var is populated at TUI launch, after this\n * module loads. Empty list when unconfigured — the picker hides, the user\n * types a slug at the prompt as before.\n */\nexport const localDescriptor: ProviderDescriptor = {\n key: 'local',\n label: 'Local LLM',\n factory: local,\n get models(): readonly ModelInfo[] {\n const configured = process.env.LOCAL_LLM_DEFAULT_MODEL?.trim()\n if (!configured)\n return []\n // Resolve the window from the user's `LOCAL_LLM_CONTEXT_WINDOW` (single\n // value or per-model map), falling back to the conservative default. Uses\n // the same parser as `getContextWindow`'s `contextWindowEnvVar` path so\n // both surfaces agree on the number.\n const resolved = resolveContextWindow(process.env.LOCAL_LLM_CONTEXT_WINDOW, configured)\n return [{\n id: configured,\n name: configured,\n contextWindow: resolved ?? LOCAL_DEFAULT_CONTEXT_WINDOW,\n input: ['text'],\n }]\n },\n customFields: [\n {\n key: 'baseURL',\n label: 'Base URL',\n envVar: 'LOCAL_LLM_BASE_URL',\n placeholder: 'http://localhost:11434/v1',\n hint: 'Where your local LLM server is reachable. Examples: Ollama → http://localhost:11434/v1, vLLM → http://localhost:8000/v1, LM Studio → http://localhost:1234/v1.',\n required: true,\n },\n {\n key: 'apiKey',\n label: 'API key',\n envVar: 'LOCAL_LLM_API_KEY',\n placeholder: 'leave blank if your server is unauthenticated',\n },\n {\n key: 'model',\n label: 'Default model',\n envVar: 'LOCAL_LLM_DEFAULT_MODEL',\n placeholder: 'e.g. llama3.1:8b, qwen2.5-coder, mistral-small',\n hint: 'Model id your server exposes. Leave blank to pick later from the model picker.',\n },\n {\n key: 'contextWindow',\n label: 'Context window (tokens)',\n envVar: 'LOCAL_LLM_CONTEXT_WINDOW',\n placeholder: 'e.g. 32768 · or per-model: llama3.1:8b=32768, qwen2.5-coder=128k, 64k',\n hint: 'Context length(s) for your local model(s) — local runtimes don\\'t advertise this, so set it here to enable the context indicator and auto-compaction. Use a single value (32768, 128k) for all models, or a comma-separated map of `model=window` with an optional bare value as the fallback. Press',\n },\n ],\n contextWindowEnvVar: 'LOCAL_LLM_CONTEXT_WINDOW',\n}\n\n/**\n * Cursor models. pi-ai has no Cursor registry, so we ship a small curated\n * list (Cursor proxies these). Context windows are nominal — Cursor inference\n * isn't wired yet (the provider's `stream()` throws), so these only drive the\n * picker once the protobuf transport lands.\n */\nconst CURSOR_MODELS: readonly ModelInfo[] = [\n { id: 'claude-4.6-sonnet', name: 'Claude Sonnet 4.6', contextWindow: 200_000, reasoning: true, input: ['text', 'image'] },\n { id: 'claude-4.8-opus', name: 'Claude Opus 4.8', contextWindow: 200_000, reasoning: true, input: ['text', 'image'] },\n { id: 'gpt-5.5', name: 'GPT-5.5', contextWindow: 272_000, reasoning: true, input: ['text', 'image'] },\n { id: 'composer-2', name: 'Composer 2', contextWindow: 200_000, input: ['text'] },\n { id: 'composer-2.5', name: 'Composer 2.5', contextWindow: 200_000, input: ['text'] },\n]\n\nexport const cursorDescriptor: ProviderDescriptor = {\n key: 'cursor',\n label: 'Cursor',\n factory: cursor,\n defaultModel: 'claude-4.6-sonnet',\n // OAuth-only: no API-key env var.\n envKey: undefined,\n oauthProvider: cursorOAuthProvider,\n oauthHint: 'Cursor subscription',\n models: CURSOR_MODELS,\n}\n\n/**\n * Default provider registry. Passed verbatim when `runTui` is invoked without\n * an explicit `providers` option. Hosts that want to override per-provider\n * metadata can spread this and replace specific entries:\n *\n * ```ts\n * runTui({ providers: { ...BUILTIN_PROVIDERS, anthropic: myOwnAnthropicDescriptor } })\n * ```\n *\n * `cursor` is intentionally NOT registered here: OAuth works, but inference\n * isn't wired (`cursor.stream()` throws — Cursor speaks a protobuf/HTTP-2\n * agent protocol, not OpenAI-compat). Shipping it in the picker only lets users\n * select a model that errors every turn. The descriptor stays exported so a\n * host can opt in (`{ ...BUILTIN_PROVIDERS, cursor: cursorDescriptor }`) once\n * the transport lands — re-add it here at that point.\n */\nexport const BUILTIN_PROVIDERS: Readonly<Record<string, ProviderDescriptor>> = {\n anthropic: anthropicDescriptor,\n openai: openaiDescriptor,\n openrouter: openrouterDescriptor,\n cerebras: cerebrasDescriptor,\n xai: xaiDescriptor,\n arcee: arceeDescriptor,\n baseten: basetenDescriptor,\n local: localDescriptor,\n}\n\n// ---------------------------------------------------------------------------\n// Model registry helper\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve the model list for a given provider. Honors `descriptor.models`\n * when set; otherwise queries pi-ai via `descriptor.piProviderId`. Returns\n * `[]` for descriptors with no known mapping (custom providers without a\n * model list) — callers should hide the model picker in that case.\n */\nexport function modelsForDescriptor(descriptor: ProviderDescriptor): readonly ModelInfo[] {\n if (descriptor.models)\n return descriptor.models\n let bundled: readonly ModelInfo[]\n try {\n bundled = getModels(piIdOf(descriptor) as never) as readonly ModelInfo[]\n }\n catch {\n bundled = []\n }\n if (descriptor.extraModels?.length) {\n // Merge extras ahead of the bundled list, de-duped by id (bundled wins, so a\n // model that lands in pi-ai later transparently supersedes the local entry).\n const bundledIds = new Set(bundled.map(m => m.id))\n const extras = descriptor.extraModels.filter(m => !bundledIds.has(m.id))\n bundled = [...extras, ...bundled]\n }\n return descriptor.optionsFor ? bundled.map(m => decorateOptions(m, descriptor)) : bundled\n}\n\n/**\n * Attach descriptor-resolved {@link ModelOption}s to a model that doesn't\n * already declare its own. Pure / non-mutating — returns the input untouched\n * when the model has options already or the descriptor resolves none.\n */\nfunction decorateOptions(model: ModelInfo, descriptor: ProviderDescriptor): ModelInfo {\n if (model.options?.length || !descriptor.optionsFor)\n return model\n const options = descriptor.optionsFor(model.id)\n return options?.length ? { ...model, options } : model\n}\n\n/**\n * Resolve a single model's metadata via the descriptor's model source.\n * Mirrors {@link modelsForDescriptor} routing: descriptor's own list wins,\n * pi-ai's registry is the fallback. Returns `null` when the model isn't\n * known.\n */\nexport function getModelInfo(descriptor: ProviderDescriptor, modelId: string): ModelInfo | null {\n const lookupId = descriptor.modelAliases?.[modelId] ?? modelId\n if (descriptor.models)\n return descriptor.models.find(m => m.id === lookupId) ?? null\n try {\n const bundled = getModel(piIdOf(descriptor) as never, lookupId as never) as ModelInfo | undefined\n if (bundled)\n return decorateOptions(bundled, descriptor)\n }\n catch {\n // fall through to extras\n }\n const extra = descriptor.extraModels?.find(m => m.id === lookupId)\n return extra ? decorateOptions(extra, descriptor) : null\n}\n\n/**\n * Parse a user-supplied context-window string into a token count.\n *\n * Accepts a plain integer (`\"32768\"`), or a number with a `k`/`m` suffix\n * (×1_000 / ×1_000_000, case-insensitive, optional space) — so `\"128k\"`\n * yields `128_000`, matching how the footer renders windows (`fmtTokens`).\n * Fractional values are floored (`\"1.5k\"` → `1500`). Returns `null` for\n * empty, malformed, zero, or negative input so callers fall through to\n * \"window unknown\" rather than a bogus ceiling.\n */\nexport function parseContextWindow(raw: string | undefined | null): number | null {\n if (raw == null)\n return null\n const trimmed = raw.trim()\n if (trimmed.length === 0)\n return null\n const match = /^(\\d+(?:\\.\\d+)?)\\s*([km])?$/i.exec(trimmed)\n if (!match)\n return null\n const suffix = match[2]?.toLowerCase()\n const mult = suffix === 'k' ? 1_000 : suffix === 'm' ? 1_000_000 : 1\n const value = Math.floor(Number.parseFloat(match[1]) * mult)\n return value >= 1 ? value : null\n}\n\n/**\n * Resolve a per-model context window from a user-supplied spec string.\n *\n * A spec is a comma-separated list of entries. An entry is either:\n * - `model=window` — a per-model override (split on the **first** `=`, so\n * model ids containing `:` or `/` survive), or\n * - a bare `window` — the fallback applied to any model not named above.\n *\n * Windows are parsed by {@link parseContextWindow} (plain int or k/m suffix).\n * Whitespace around entries and the `=` is ignored; malformed entries are\n * skipped rather than poisoning the whole spec; a later duplicate key wins.\n *\n * Lookup order for `modelId`: exact map entry → bare fallback → `null`. A\n * lone bare value (`\"32768\"`) therefore behaves as a single global window,\n * keeping the simple single-model config working.\n *\n * @example resolveContextWindow('llama3.1:8b=32768, qwen=128k, 64k', 'qwen') // 128000\n */\nexport function resolveContextWindow(spec: string | undefined | null, modelId: string): number | null {\n if (spec == null)\n return null\n const overrides = new Map<string, number>()\n let fallback: number | null = null\n for (const entry of spec.split(',')) {\n const eq = entry.indexOf('=')\n if (eq === -1) {\n const bare = parseContextWindow(entry)\n if (bare != null)\n fallback = bare\n continue\n }\n const key = entry.slice(0, eq).trim()\n const window = parseContextWindow(entry.slice(eq + 1))\n if (key.length > 0 && window != null)\n overrides.set(key, window)\n }\n return overrides.get(modelId) ?? fallback\n}\n\n/**\n * Look up the model's max context window via the descriptor's model source.\n * Falls back to {@link ProviderDescriptor.contextWindowEnvVar} (parsed via\n * {@link resolveContextWindow}, which supports a per-model map) when the\n * registry has nothing — this is how local LLMs, whose runtimes don't\n * advertise a window, get one. Returns `null` when neither source resolves a\n * value (custom slugs, providers without a registry or a configured window);\n * callers should hide the context indicator and skip auto-compaction then.\n */\nexport function getContextWindow(descriptor: ProviderDescriptor, modelId: string): number | null {\n const fromRegistry = getModelInfo(descriptor, modelId)?.contextWindow\n if (fromRegistry != null)\n return fromRegistry\n if (descriptor.contextWindowEnvVar)\n return resolveContextWindow(process.env[descriptor.contextWindowEnvVar], modelId)\n return null\n}\n\n// `OUTPUT_RESERVE_TOKENS` + `effectiveContextWindow` moved to the core\n// `src/compact/policy.ts` so the agent loop (which can't import `src/chat/`)\n// shares the same window math as the chat footer + auto-compact trigger.\n// Re-exported here so existing `chat/providers` importers keep working.\nexport { effectiveContextWindow, OUTPUT_RESERVE_TOKENS } from '../compact/policy'\n\n/**\n * Whether the given model exposes a reasoning / extended-thinking knob.\n * Drives the TUI's effort picker visibility — the `ctrl+n` shortcut and\n * the bottom-bar `effortName` segment only surface when this is `true`.\n * Returns `false` for unknown models (no registry entry → no advertised\n * capability).\n */\nexport function modelSupportsReasoning(descriptor: ProviderDescriptor, modelId: string): boolean {\n return getModelInfo(descriptor, modelId)?.reasoning === true\n}\n\n/**\n * Custom {@link ModelOption}s the given model supports (e.g. fast mode), or `[]`\n * when none. Drives the TUI/GUI options picker visibility — surfaces only when\n * non-empty.\n */\nexport function modelOptionsFor(descriptor: ProviderDescriptor, modelId: string): readonly ModelOption[] {\n return getModelInfo(descriptor, modelId)?.options ?? []\n}\n\n/**\n * Normalize a model-options blob to an enabled-only `{ id: true }` map.\n *\n * Single source of truth shared by every surface that reads or persists model\n * options — the TUI run path, the GUI engine reader, and the GUI IPC handlers.\n * Tolerant of arbitrary input (persisted JSON metadata may be anything): a\n * non-object → `{}`, and only entries strictly equal to `true` survive. This\n * keeps the persisted shape minimal (no `{ fast: false }` noise) and means\n * callers never forward a disabled option to `agent.run`.\n */\nexport function enabledModelOptions(raw: unknown): Record<string, boolean> {\n if (!raw || typeof raw !== 'object')\n return {}\n const out: Record<string, boolean> = {}\n for (const [id, on] of Object.entries(raw as Record<string, unknown>)) {\n if (on === true)\n out[id] = true\n }\n return out\n}\n\n/**\n * Resolve a model's remembered options for a (re)pick: keep only options the\n * model still declares AND that are enabled, dropping any that no longer apply\n * (e.g. switching away from a model that supported `fast`). Returns `undefined`\n * when nothing applies so callers can omit the field entirely.\n *\n * Shared by the TUI pick path and the launch-time resume path so \"which options\n * survive a model switch / relaunch\" is decided in exactly one place.\n */\nexport function restoreModelOptions(\n descriptor: ProviderDescriptor,\n modelId: string,\n remembered: Record<string, Record<string, boolean>> | undefined,\n): Record<string, boolean> | undefined {\n const validIds = new Set(modelOptionsFor(descriptor, modelId).map(o => o.id))\n if (validIds.size === 0)\n return undefined\n const enabled = enabledModelOptions(remembered?.[modelId])\n const filtered: Record<string, boolean> = {}\n for (const id of Object.keys(enabled)) {\n if (validIds.has(id))\n filtered[id] = true\n }\n return Object.keys(filtered).length > 0 ? filtered : undefined\n}\n","/**\n * Credential storage for the TUI.\n *\n * Lives at `<dataDir>/credentials.json` (default `~/.zidane/credentials.json`,\n * overridable via `ZIDANE_STORAGE_DIR`). Owner-only (0o600) so refresh tokens\n * and API keys don't leak on shared boxes.\n *\n * Two credential kinds:\n * - `apikey` — a plain API key entered by the user (e.g. via the setup wizard).\n * - `oauth` — tokens obtained via a provider's OAuth flow (refresh-aware).\n *\n * **Schema note.** File keys come from {@link ProviderDescriptor.credentialFileKey}\n * (falling back to `descriptor.key`). They must match what the harness providers\n * look up at runtime — for built-ins this is `anthropic`, `openai-codex`,\n * `openrouter`, `cerebras`. Hosts that ship custom providers control the file\n * key via the descriptor.\n *\n * The legacy `cwd/.credentials.json` (written by `bun run auth` in older\n * versions) is migrated on first read if the new file doesn't exist yet. The\n * legacy file is left in place — we don't delete user data behind their back.\n */\n\nimport type { ProviderDescriptor } from './providers'\nimport { existsSync, readFileSync, realpathSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { dirname, resolve } from 'node:path'\nimport { writeFileAtomic } from '../atomic-write'\nimport { credKeyOf } from './providers'\n\n/** POSIX mode for the credentials file. Ignored on Windows. */\nconst FILE_MODE = 0o600\n\nexport interface ApiKeyCredential {\n kind: 'apikey'\n /**\n * Bearer API key. May be empty when the descriptor has no `envKey` and\n * authenticates entirely via {@link customFields} (e.g. the `local`\n * provider, where the auth signal is the configured base URL).\n */\n value: string\n /**\n * Free-form fields collected by the wizard from the descriptor's\n * `customFields` definitions. Keys match the descriptor field's `key`.\n * Mirrored into `process.env[field.envVar]` by `applyApiKeyEnv` so the\n * provider's factory can read them via the existing env-var convention.\n */\n customFields?: Record<string, string>\n}\nexport interface OAuthCredential {\n kind: 'oauth'\n access: string\n refresh?: string\n expires?: number\n /** Provider-specific extras (e.g. OpenAI Codex `accountId`). */\n [extra: string]: unknown\n}\nexport type ProviderCredential = ApiKeyCredential | OAuthCredential\n\n/** Top-level shape of `credentials.json` — keys are credential-file keys. */\nexport type CredentialsFile = Record<string, ProviderCredential>\n\n/**\n * Resolve the credentials file path given the resolved TUI data directory\n * (typically `~/.zidane`, i.e. `config.paths.dir`).\n *\n * Matches the convention used elsewhere in the TUI (sessions.db, state.json)\n * so a single `ZIDANE_STORAGE_DIR` override moves the entire data root.\n */\nexport function credentialsPath(dataDir: string): string {\n return resolve(dataDir, 'credentials.json')\n}\n\n/**\n * Read credentials from disk.\n *\n * Returns `{}` when the file is missing or corrupt (last-ditch tolerance —\n * a hand-edit gone wrong shouldn't lock the user out of re-authing). On first\n * call with no file present, attempts a migration from `cwd/.credentials.json`\n * (the legacy location used by `bun run auth`).\n */\nexport function readCredentials(dataDir: string): CredentialsFile {\n const path = credentialsPath(dataDir)\n\n if (!existsSync(path)) {\n const migrated = migrateLegacyFile(path)\n if (migrated)\n return migrated\n return {}\n }\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 return normalizeCredentials(parsed as Record<string, unknown>)\n }\n catch {\n return {}\n }\n}\n\n/**\n * Re-tag legacy / refresh-stripped entries so the discriminated union holds.\n *\n * An untagged object with a string `access` is an OAuth credential — either the\n * pre-TUI `bun run auth` shape, or one whose `kind` was dropped when a token\n * refresh was persisted (a provider refresh returns no `kind`). Without\n * re-tagging, {@link detectAuth} and {@link applyApiKeyEnv} stop recognizing it\n * and force a spurious re-login. Mirrors the provider-side tolerance in\n * `readOAuthCredentials`. Entries with an explicit `kind` and anything we can't\n * classify pass through untouched.\n */\nfunction normalizeCredentials(parsed: Record<string, unknown>): CredentialsFile {\n const result: CredentialsFile = {}\n for (const [key, value] of Object.entries(parsed)) {\n // Only re-tag entries with no explicit `kind`; `isOAuthLegacy` (shared with\n // the legacy-file migration) is the \"object carrying an access token\" guard.\n const untagged = !!value && typeof value === 'object' && (value as Record<string, unknown>).kind === undefined\n if (untagged && isOAuthLegacy(value))\n result[key] = { ...value, kind: 'oauth' }\n else\n result[key] = value as ProviderCredential\n }\n return result\n}\n\n/** Read a single provider's credential (translating via the descriptor). */\nexport function readProviderCredential(dataDir: string, descriptor: ProviderDescriptor): ProviderCredential | undefined {\n return readCredentials(dataDir)[credKeyOf(descriptor)]\n}\n\n/**\n * Write credentials atomically (write-then-rename) with mode 0o600.\n *\n * Atomic on the same filesystem — readers either see the previous file or the\n * new one, never a half-written intermediate. Creates the parent dir if needed\n * (first launch on a fresh machine: `~/.zidane/` may not exist yet).\n */\nexport function writeCredentials(dataDir: string, creds: CredentialsFile): void {\n writeFileAtomic(\n credentialsPath(dataDir),\n `${JSON.stringify(creds, null, 2)}\\n`,\n { ensureDir: true, mode: FILE_MODE },\n )\n}\n\nexport function setProviderCredential(\n dataDir: string,\n descriptor: ProviderDescriptor,\n cred: ProviderCredential,\n): void {\n const all = readCredentials(dataDir)\n all[credKeyOf(descriptor)] = cred\n writeCredentials(dataDir, all)\n}\n\nexport function removeProviderCredential(dataDir: string, descriptor: ProviderDescriptor): void {\n const all = readCredentials(dataDir)\n const fileKey = credKeyOf(descriptor)\n if (!(fileKey in all))\n return\n delete all[fileKey]\n writeCredentials(dataDir, all)\n}\n\n/**\n * Reconcile `process.env` with the stored credentials so the harness providers\n * pick up the wizard's output via their existing env-var resolution. Called\n * once at TUI launch after the credentials file has been resolved.\n *\n * **Precedence: stored credential > ambient env.** When the user has run the\n * auth wizard, that's an explicit, recent action — it MUST win over whatever\n * ambient value is already in `process.env`. Otherwise an upgrade reliably\n * breaks for anyone whose cwd happens to contain a stale `.env`: Bun\n * auto-loads `.env` from cwd into `process.env` before our code runs, the\n * legacy `bun run auth` used to upsert tokens into `cwd/.env`, and even a\n * one-off `export ANTHROPIC_API_KEY=…` from months ago in the user's shell rc\n * can outlive its usefulness. None of those should override a key the user\n * just typed into the wizard.\n *\n * Three cases per registered provider with an `envKey`:\n *\n * 1. Stored `kind: 'apikey'` → overwrite env with the stored value. Wizard\n * wins. A debug log fires when this clobbers a different ambient value.\n * 2. Stored `kind: 'oauth'` → DELETE env, so {@link resolveOAuthApiKey}\n * falls through to the file-read + refresh path. A stale OAuth token\n * in env would otherwise short-circuit refresh and get rejected.\n * 3. No stored entry → leave env alone. Env-only setups (no wizard run yet,\n * hosts driving zidane purely from secrets management) keep working.\n *\n * To opt out and force ambient env to win again, the user signs the provider\n * out via the wizard (which deletes the stored entry → case 3 above).\n *\n * Descriptors without an `envKey` (OAuth-only providers, custom providers\n * that bypass env-var resolution) are skipped silently.\n */\nexport function applyApiKeyEnv(\n dataDir: string,\n registry: Readonly<Record<string, ProviderDescriptor>>,\n): void {\n const creds = readCredentials(dataDir)\n for (const descriptor of Object.values(registry)) {\n const cred = creds[credKeyOf(descriptor)]\n\n // Mirror customFields → env regardless of whether the descriptor has an\n // envKey. Same \"stored beats ambient\" semantics as the API-key path.\n if (cred?.kind === 'apikey' && descriptor.customFields) {\n const stored = cred.customFields ?? {}\n for (const field of descriptor.customFields) {\n const value = stored[field.key]\n if (typeof value === 'string' && value.length > 0) {\n const ambient = process.env[field.envVar]\n if (ambient && ambient !== value && process.env.ZIDANE_DEBUG) {\n process.stderr.write(\n `[zidane/chat] applyApiKeyEnv: overriding ambient \\`${field.envVar}\\` `\n + `with stored value from credentials.json (provider=${descriptor.key}, field=${field.key}).\\n`,\n )\n }\n process.env[field.envVar] = value\n }\n }\n }\n\n if (!descriptor.envKey)\n continue\n const envKey = descriptor.envKey\n const ambient = process.env[envKey]\n\n if (cred?.kind === 'apikey' && cred.value) {\n if (ambient && ambient !== cred.value && process.env.ZIDANE_DEBUG) {\n process.stderr.write(\n `[zidane/chat] applyApiKeyEnv: overriding ambient \\`${envKey}\\` `\n + `with stored API key from credentials.json (provider=${descriptor.key}). `\n + `Sign out via the auth wizard if the ambient value was intended to win.\\n`,\n )\n }\n process.env[envKey] = cred.value\n continue\n }\n\n if (cred?.kind === 'oauth' && cred.access) {\n if (ambient !== undefined && process.env.ZIDANE_DEBUG) {\n process.stderr.write(\n `[zidane/chat] applyApiKeyEnv: clearing ambient \\`${envKey}\\` because `\n + `credentials.json has stored OAuth for ${descriptor.key} — refresh path needs the file.\\n`,\n )\n }\n delete process.env[envKey]\n continue\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Legacy migration\n// ---------------------------------------------------------------------------\n\n/**\n * `bun run auth` (pre-TUI) wrote `cwd/.credentials.json` with an entry per\n * provider mapping directly to an OAuthCredentials payload, e.g.:\n *\n * {\n * \"anthropic\": { \"access\": \"...\", \"refresh\": \"...\", \"expires\": 123 },\n * \"openai-codex\": { \"access\": \"...\", \"refresh\": \"...\", \"expires\": 123, \"accountId\": \"...\" }\n * }\n *\n * We don't delete the legacy file — it might still be used by a host that\n * imports the harness directly. We just copy its contents into the new\n * location under the kind-tagged shape so the TUI picks them up.\n *\n * Migration is provider-agnostic: any top-level entry with an `access` field\n * is preserved verbatim (extras included), under the same key. The TUI's\n * detection then looks them up via the matching descriptor's `credentialFileKey`.\n *\n * Returns the migrated credentials when the migration ran, or `null` when\n * there's no legacy file to migrate.\n */\nfunction migrateLegacyFile(targetPath: string): CredentialsFile | null {\n const legacyPath = resolve(process.cwd(), '.credentials.json')\n // Only trust a legacy file that sits directly in the user's home dir (where\n // `bun run auth` was typically run) or next to the migration target. A\n // cwd-relative read from an arbitrary checkout would let an untrusted repo\n // plant a `.credentials.json` that gets persisted as the USER-LEVEL\n // credential file. Compared via realpath so symlinked dirs (e.g. macOS\n // `/var` → `/private/var`) don't defeat the check.\n const real = (p: string): string => {\n try {\n return realpathSync(p)\n }\n catch {\n return p\n }\n }\n const legacyDir = real(dirname(legacyPath))\n if (legacyDir !== real(homedir()) && legacyDir !== real(dirname(resolve(targetPath))))\n return null\n if (!existsSync(legacyPath))\n return null\n\n let legacy: Record<string, unknown>\n try {\n legacy = JSON.parse(readFileSync(legacyPath, 'utf-8'))\n }\n catch {\n return null\n }\n if (!legacy || typeof legacy !== 'object' || Array.isArray(legacy))\n return null\n\n const migrated: CredentialsFile = {}\n for (const [fileKey, value] of Object.entries(legacy)) {\n if (!isOAuthLegacy(value))\n continue\n const { access, refresh, expires, ...extras } = value\n migrated[fileKey] = {\n kind: 'oauth',\n access,\n ...(typeof refresh === 'string' ? { refresh } : {}),\n ...(typeof expires === 'number' ? { expires } : {}),\n ...extras,\n }\n }\n\n if (Object.keys(migrated).length === 0)\n return null\n\n // Persist immediately so subsequent reads use the new file directly.\n writeFileAtomic(\n targetPath,\n `${JSON.stringify(migrated, null, 2)}\\n`,\n { ensureDir: true, mode: FILE_MODE },\n )\n\n return migrated\n}\n\nfunction isOAuthLegacy(value: unknown): value is { access: string, refresh?: string, expires?: number, [extra: string]: unknown } {\n return (\n typeof value === 'object'\n && value !== null\n && 'access' in value\n && typeof (value as { access: unknown }).access === 'string'\n )\n}\n","import type { ProviderCredential } from './credentials'\nimport type { ProviderDescriptor } from './providers'\nimport { readCredentials } from './credentials'\nimport { credKeyOf } from './providers'\n\n/**\n * Provider identifier as known to the TUI. Built-in keys are `anthropic`,\n * `openai`, `openrouter`, `cerebras`, but hosts can register additional\n * keys via {@link ProviderDescriptor} — so the type is open.\n */\nexport type ProviderKey = string\n\nexport interface AuthMethod {\n source: 'env' | 'oauth' | 'apikey'\n /** Human-readable detail (env var name, expiry timestamp, …). */\n detail: string\n}\n\nexport interface ProviderAuth {\n key: ProviderKey\n label: string\n /** True when at least one credential source is present. */\n available: boolean\n methods: AuthMethod[]\n}\n\n/**\n * Detect available auth for every registered provider.\n *\n * Resolution order per provider (a method appears in `methods` for each\n * layer that has a credential — the agent itself resolves them in the same\n * order via its provider factories):\n *\n * 1. `kind: 'apikey'` from `credentials.json` (injected into env at TUI launch)\n * 2. explicit env var (descriptor's `envKey`)\n * 3. `kind: 'oauth'` from `credentials.json` (or legacy `cwd/.credentials.json`)\n *\n * Pure read — never refreshes or rewrites the credentials file.\n */\nexport function detectAuth(\n dataDir: string,\n registry: Readonly<Record<string, ProviderDescriptor>>,\n env: Record<string, string | undefined> = process.env,\n): ProviderAuth[] {\n const creds = readCredentials(dataDir)\n\n return Object.values(registry).map((descriptor) => {\n const methods: AuthMethod[] = []\n const fileEntry = creds[credKeyOf(descriptor)] as ProviderCredential | undefined\n\n // 1. Stored API key\n if (fileEntry?.kind === 'apikey' && fileEntry.value)\n methods.push({ source: 'apikey', detail: 'credentials.json' })\n\n // 1b. customFields-only credential (descriptor has no `envKey` and is\n // authenticated entirely by configuration — e.g. the `local` provider,\n // where the auth signal is the configured base URL). The credential\n // is \"present\" when every required field has a stored value.\n if (\n fileEntry?.kind === 'apikey'\n && !fileEntry.value\n && descriptor.customFields\n && descriptor.customFields.length > 0\n ) {\n const stored = fileEntry.customFields ?? {}\n const required = descriptor.customFields.filter(f => f.required)\n const allRequiredPresent = required.every(\n f => typeof stored[f.key] === 'string' && (stored[f.key] as string).length > 0,\n )\n if (allRequiredPresent) {\n methods.push({ source: 'apikey', detail: 'credentials.json (configured)' })\n }\n }\n\n // 1c. Env-only custom field configuration. This lets local/OpenAI-compatible\n // endpoints participate in auth/model detection when configured through\n // `LOCAL_LLM_BASE_URL` without first writing credentials.json.\n if (descriptor.customFields && descriptor.customFields.length > 0) {\n const required = descriptor.customFields.filter(f => f.required)\n const allRequiredPresent = required.length > 0 && required.every(\n f => typeof env[f.envVar] === 'string' && (env[f.envVar] as string).length > 0,\n )\n\n if (allRequiredPresent) {\n methods.push({ source: 'env', detail: required.map(f => f.envVar).join(', ') })\n }\n }\n\n // 2. Env var (may be set by `applyApiKeyEnv` from a stored key, or by the\n // user's shell. Either way, treated as the same observable signal.)\n if (descriptor.envKey && env[descriptor.envKey])\n methods.push({ source: 'env', detail: descriptor.envKey })\n\n // 3. Stored OAuth\n if (fileEntry?.kind === 'oauth' && fileEntry.access) {\n const detail = typeof fileEntry.expires === 'number'\n ? `oauth · expires ${new Date(fileEntry.expires).toLocaleString()}`\n : 'oauth · credentials.json'\n methods.push({ source: 'oauth', detail })\n }\n\n return {\n key: descriptor.key,\n label: descriptor.label,\n available: methods.length > 0,\n methods,\n }\n })\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"cache-telemetry-BKAwsNxm.js","names":[],"sources":["../src/cache-telemetry.ts"],"sourcesContent":["/**\n * Per-dimension cache-break detection.\n *\n * The agent surfaces `cacheRead` / `cacheCreation` in {@link TurnUsage}, but\n * doesn't answer the load-bearing diagnostic question: *which* dimension\n * busted the cache? When `cacheCreation > 0` on turn N+1 with no obvious\n * reason, you want to know whether the system prompt, the tool list, the\n * thinking config, or the model id changed — without re-reading the loop\n * source.\n *\n * This module hashes each cache-affecting dimension at `turn:before`, then\n * on `turn:after` checks whether the latest turn read its share of the\n * cached prefix. If not, it diffs the previous turn's dimension hashes\n * against the current ones and logs the responsible dimension(s).\n *\n * Wire contract:\n *\n * - Install via {@link installCacheBreakLogger} on the agent's hook bus.\n * Returns an uninstall fn. Idempotent — calling install twice on the\n * same bus leaves two listeners; callers manage lifecycle.\n * - Output sink defaults to `console.error`; pass `log` to redirect into a\n * {@link Logger}, span attribute writer, or test buffer.\n * - Heuristic only: a missing cache read on turn 1 is expected (nothing\n * cached yet); the logger only reports from turn 2 onward.\n * - Pure helper {@link diffCacheDimensions} is exported for tests so the\n * diff logic can be exercised without standing up the loop.\n *\n * The dimensions tracked are the inputs the provider treats as part of the\n * cache key — `system`, `tools`, `model`, thinking config. Tools are hashed\n * after `provider.formatTools`, so MCP catalogues showing up mid-run also\n * register as a break. Messages are NOT tracked: the conversation grows by\n * design, and the last-message breakpoint is meant to advance every turn.\n */\n\nimport type { Hookable } from 'hookable'\nimport type { AgentHooks } from './agent'\nimport type { StreamOptions } from './providers'\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Snapshot of per-request dimensions that contribute to the provider-side\n * prompt cache key. Hashed at `turn:before` and stashed for diffing on the\n * next request.\n *\n * Hashes are FNV-1a 32-bit (cheap, no allocations on the hot path,\n * collision rate good enough for diff detection — we're not signing\n * anything). For verbose output the helper also captures byte lengths so\n * `system: 1024 → 1031` reads like a real diff line.\n */\nexport interface CacheDimensionSnapshot {\n /** Hash of `options.system` (with boundary marker preserved). */\n systemHash: number\n /** Byte length of `options.system`. */\n systemBytes: number\n /** Hash of the JSON-serialized `options.tools` array. */\n toolsHash: number\n /** Number of tools in the array. */\n toolsCount: number\n /** Model id. */\n model: string\n /** Thinking level — `'off'` when absent. */\n thinking: string\n /** Exact thinking budget — `0` when absent. */\n thinkingBudget: number\n}\n\n/** Result of {@link diffCacheDimensions}. */\nexport interface CacheDimensionDiff {\n /** Dimension names whose hashes differ between the two snapshots. */\n changed: readonly CacheDimensionName[]\n /** Per-dimension details — only filled for changed dimensions. */\n details: Readonly<Partial<Record<CacheDimensionName, string>>>\n}\n\n/** Cache-affecting dimension names. */\nexport type CacheDimensionName\n = | 'system'\n | 'tools'\n | 'model'\n | 'thinking'\n | 'thinkingBudget'\n\n// ---------------------------------------------------------------------------\n// Configuration\n// ---------------------------------------------------------------------------\n\nexport interface CacheBreakLoggerOptions {\n /**\n * Sink for log lines. Defaults to `console.error`. Use a {@link Logger} or\n * a test buffer here when you want the output to land somewhere other than\n * stderr.\n */\n log?: (line: string) => void\n /**\n * When `true`, emit a log line on EVERY turn (not just cache breaks) with\n * the per-dimension hashes. Useful for offline correlation; off by default\n * to keep the logger zero-noise on happy paths.\n */\n verbose?: boolean\n /**\n * Threshold for \"this turn missed cache\". The logger reports when\n * `cacheRead < expectedReadAtLeast` AND the dimensions differ from the\n * previous turn. Defaults to `1` — any cache read at all means the\n * provider matched some prefix, so a strict \"did we get any cache?\" check\n * is the right signal. Raise to detect partial-prefix breaks (e.g. the\n * tools cache hit but the system prompt didn't, which lowers `cacheRead`\n * without zeroing it).\n */\n expectedReadAtLeast?: number\n}\n\n// ---------------------------------------------------------------------------\n// Hashing\n// ---------------------------------------------------------------------------\n\nconst FNV_OFFSET = 0x811C9DC5\nconst FNV_PRIME = 0x01000193\n\n/**\n * FNV-1a-style 32-bit hash over UTF-16 code units. Pure, allocation-free,\n * ~10 ns per KB. Cryptographic strength is irrelevant here — we only need\n * change detection, and this has effectively zero collision risk for the\n * small number of dimensions tracked.\n *\n * Feeds the FULL char code into the mix (same as read-state's\n * `hashContent`) — masking to the low byte would collapse code points that\n * share a low byte, so a non-ASCII system-prompt change could hash\n * identically and silently hide a cache-dimension drift.\n *\n * Exported for tests; callers shouldn't typically import directly.\n */\nexport function fnv1a32(s: string): number {\n let h = FNV_OFFSET\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i)\n // Multiply by FNV_PRIME mod 2^32, using `Math.imul` to keep within\n // 32-bit signed range (avoid >>> 0 ping-pong on every iteration).\n h = Math.imul(h, FNV_PRIME)\n }\n // Map signed → unsigned 32-bit so the printed hex is stable.\n return h >>> 0\n}\n\n/**\n * Build a {@link CacheDimensionSnapshot} from a `turn:before` payload.\n *\n * Pure — exported so hosts can compute snapshots outside the logger (e.g.\n * stamping them onto custom telemetry spans). The hash is deterministic on\n * byte-identical inputs.\n */\nexport function snapshotCacheDimensions(options: StreamOptions): CacheDimensionSnapshot {\n const toolsJson = JSON.stringify(options.tools ?? [])\n return {\n systemHash: fnv1a32(options.system),\n systemBytes: options.system.length,\n toolsHash: fnv1a32(toolsJson),\n toolsCount: Array.isArray(options.tools) ? options.tools.length : 0,\n model: options.model,\n thinking: options.thinking ?? 'off',\n thinkingBudget: options.thinkingBudget ?? 0,\n }\n}\n\n// ---------------------------------------------------------------------------\n// Diffing\n// ---------------------------------------------------------------------------\n\n/**\n * Compare two snapshots and return the list of dimensions whose bytes\n * differ. The `details` map carries a short human-readable description per\n * changed dimension (e.g. `system: 1024 → 1031 bytes`).\n *\n * Pure / no I/O. Use this in tests to assert the diff logic; the logger\n * itself just formats and emits the result.\n */\nexport function diffCacheDimensions(\n prev: CacheDimensionSnapshot,\n curr: CacheDimensionSnapshot,\n): CacheDimensionDiff {\n const changed: CacheDimensionName[] = []\n const details: Partial<Record<CacheDimensionName, string>> = {}\n\n if (prev.systemHash !== curr.systemHash) {\n changed.push('system')\n details.system = `${prev.systemBytes} → ${curr.systemBytes} bytes (hash ${hex(prev.systemHash)} → ${hex(curr.systemHash)})`\n }\n if (prev.toolsHash !== curr.toolsHash) {\n changed.push('tools')\n details.tools = `${prev.toolsCount} → ${curr.toolsCount} tools (hash ${hex(prev.toolsHash)} → ${hex(curr.toolsHash)})`\n }\n if (prev.model !== curr.model) {\n changed.push('model')\n details.model = `${prev.model} → ${curr.model}`\n }\n if (prev.thinking !== curr.thinking) {\n changed.push('thinking')\n details.thinking = `${prev.thinking} → ${curr.thinking}`\n }\n if (prev.thinkingBudget !== curr.thinkingBudget) {\n changed.push('thinkingBudget')\n details.thinkingBudget = `${prev.thinkingBudget} → ${curr.thinkingBudget}`\n }\n\n return { changed, details }\n}\n\nfunction hex(n: number): string {\n return `0x${n.toString(16).padStart(8, '0')}`\n}\n\n// ---------------------------------------------------------------------------\n// Hook installer\n// ---------------------------------------------------------------------------\n\n/**\n * Install the cache-break logger on an agent's hook bus.\n *\n * Subscribes to `turn:before` (snapshot) + `turn:after` (compare with prior\n * snapshot, log breaks). Returns an `uninstall` fn that detaches both\n * listeners. Idempotent w.r.t. agent lifecycle — installs/uninstalls cleanly\n * across runs.\n *\n * Recommended setup:\n *\n * ```ts\n * const uninstall = installCacheBreakLogger(agent.hooks, {\n * log: line => logger.debug(line),\n * verbose: !!process.env.ZIDANE_DEBUG?.includes('cache'),\n * })\n * // ...\n * uninstall()\n * ```\n *\n * Performance: hashing runs on each `turn:before` and is bounded by the\n * input size (system + tools serialization). Disabled callers pay nothing\n * (don't install).\n */\nexport function installCacheBreakLogger(\n hooks: Hookable<AgentHooks>,\n options: CacheBreakLoggerOptions = {},\n): () => void {\n const log = options.log ?? ((line: string) => console.error(line))\n const verbose = options.verbose === true\n const expectedReadAtLeast = options.expectedReadAtLeast ?? 1\n\n // Per-bus latest snapshot. Keyed implicitly by the bus instance —\n // installing on a fresh bus starts from empty. Across runs on the same\n // bus, the snapshot persists, which is what we want: the first turn of\n // run N+1 should hit cache if it shares a prefix with the last turn of\n // run N. The detector reports correctly across that boundary.\n let prev: CacheDimensionSnapshot | undefined\n let turnIndex = 0\n let pending: { snapshot: CacheDimensionSnapshot, turnIndex: number, turnId: string } | undefined\n\n function onTurnBefore(ctx: { turn: number, turnId: string, options: StreamOptions }): void {\n const curr = snapshotCacheDimensions(ctx.options)\n turnIndex += 1\n\n if (verbose) {\n log(formatVerboseLine({\n turn: turnIndex,\n turnId: ctx.turnId,\n snapshot: curr,\n }))\n }\n\n // Stash for the matching `turn:after`. We can't compute the diff here —\n // the cache-read result lands on `turn:after.usage`.\n pending = { snapshot: curr, turnIndex, turnId: ctx.turnId }\n }\n\n function onTurnAfter(ctx: { turn: number, turnId: string, usage: { cacheRead?: number, cacheCreation?: number } }): void {\n if (!pending || pending.turnId !== ctx.turnId)\n return\n const { snapshot: curr, turnIndex: tIdx } = pending\n pending = undefined\n\n // Skip break detection on providers that don't report `cacheRead` —\n // treating undefined as 0 would log a false break every turn on Codex,\n // Cerebras, or any host without a cache_read field. We can only diagnose\n // breaks when the provider tells us how much cache was actually used.\n if (prev && tIdx > 1 && typeof ctx.usage.cacheRead === 'number') {\n const cacheRead = ctx.usage.cacheRead\n if (cacheRead < expectedReadAtLeast) {\n const diff = diffCacheDimensions(prev, curr)\n if (diff.changed.length > 0) {\n log(formatBreakLine({\n turn: tIdx,\n turnId: ctx.turnId,\n cacheRead,\n cacheCreation: ctx.usage.cacheCreation ?? 0,\n diff,\n }))\n }\n }\n }\n\n prev = curr\n }\n\n // Hookable's `.hook()` returns its own unregister fn — keep both so\n // `uninstall()` is symmetric with the install path and survives a\n // future hookable rename of `removeHook`.\n const unregisterBefore = hooks.hook('turn:before', onTurnBefore)\n const unregisterAfter = hooks.hook('turn:after', onTurnAfter)\n\n return () => {\n unregisterBefore()\n unregisterAfter()\n }\n}\n\n// ---------------------------------------------------------------------------\n// Formatting\n// ---------------------------------------------------------------------------\n\nfunction formatBreakLine(input: {\n turn: number\n turnId: string\n cacheRead: number\n cacheCreation: number\n diff: CacheDimensionDiff\n}): string {\n const names = input.diff.changed.join(', ')\n const details = input.diff.changed\n .map(d => ` ${d}: ${input.diff.details[d]}`)\n .join('\\n')\n return [\n `[zidane:cache-break] turn=${input.turn} turnId=${input.turnId} cacheRead=${input.cacheRead} cacheCreation=${input.cacheCreation}`,\n ` changed: ${names}`,\n details,\n ].join('\\n')\n}\n\nfunction formatVerboseLine(input: {\n turn: number\n turnId: string\n snapshot: CacheDimensionSnapshot\n}): string {\n const s = input.snapshot\n return `[zidane:cache] turn=${input.turn} turnId=${input.turnId} system=${hex(s.systemHash)}(${s.systemBytes}b) tools=${hex(s.toolsHash)}(${s.toolsCount}) model=${s.model} thinking=${s.thinking}/${s.thinkingBudget}`\n}\n"],"mappings":"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"}