nexus-agents 2.72.1 → 2.74.0

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 (112) hide show
  1. package/dist/{adaptive-memory-UPE76IP6.js → adaptive-memory-OJY3IVNM.js} +3 -3
  2. package/dist/{child-mcp-config-5HRJGLCR.js → child-mcp-config-KMCKKPNY.js} +2 -2
  3. package/dist/{chunk-TF3GROMO.js → chunk-2SVS5WRV.js} +2 -2
  4. package/dist/{chunk-QECRZ3YA.js → chunk-3ESCBV47.js} +2 -2
  5. package/dist/{chunk-QL4HCYRD.js → chunk-3SZBDLFX.js} +3 -3
  6. package/dist/{chunk-VTVKC4FS.js → chunk-44MVIL3F.js} +4 -4
  7. package/dist/{chunk-BVETPIOQ.js → chunk-4K6L7RKC.js} +3 -3
  8. package/dist/chunk-4K6L7RKC.js.map +1 -0
  9. package/dist/{chunk-C3JGKBL2.js → chunk-65R32U3M.js} +4 -4
  10. package/dist/{chunk-3FIDMWFC.js → chunk-BK54O5J5.js} +2 -2
  11. package/dist/chunk-BK54O5J5.js.map +1 -0
  12. package/dist/{chunk-J4VR2WNI.js → chunk-BYKS53GW.js} +1086 -1709
  13. package/dist/chunk-BYKS53GW.js.map +1 -0
  14. package/dist/{chunk-L6N2S3UB.js → chunk-CCODJRS6.js} +2 -2
  15. package/dist/{chunk-DA5UDQYW.js → chunk-DGELGUZF.js} +2 -2
  16. package/dist/{chunk-2KB63QGE.js → chunk-EQHYXT56.js} +2 -2
  17. package/dist/{chunk-VPC3YNFR.js → chunk-G5VE2DZS.js} +2 -2
  18. package/dist/{chunk-2MD5MWCK.js → chunk-GS3GW7C7.js} +2 -2
  19. package/dist/{chunk-ES6GFP35.js → chunk-NC2LECY6.js} +26 -9
  20. package/dist/chunk-NC2LECY6.js.map +1 -0
  21. package/dist/{chunk-5MHIWRKB.js → chunk-NQR7QAZX.js} +21 -2
  22. package/dist/chunk-NQR7QAZX.js.map +1 -0
  23. package/dist/{chunk-5WQ3SRSE.js → chunk-PAFH336F.js} +2 -2
  24. package/dist/{chunk-O4KUCF5S.js → chunk-Q3RFPJYK.js} +1325 -441
  25. package/dist/chunk-Q3RFPJYK.js.map +1 -0
  26. package/dist/{chunk-P5OFZWDW.js → chunk-Q6JDV36D.js} +13 -6
  27. package/dist/chunk-Q6JDV36D.js.map +1 -0
  28. package/dist/{chunk-A35XORXU.js → chunk-Q7FTNKPO.js} +2 -2
  29. package/dist/chunk-QON7LR7J.js +153 -0
  30. package/dist/chunk-QON7LR7J.js.map +1 -0
  31. package/dist/{chunk-53K3KEKT.js → chunk-T5VPZZYX.js} +1564 -848
  32. package/dist/chunk-T5VPZZYX.js.map +1 -0
  33. package/dist/{chunk-TQFRPFMG.js → chunk-TBRNRW2Q.js} +2 -2
  34. package/dist/{chunk-345KMHWH.js → chunk-UWJKMBPL.js} +6 -6
  35. package/dist/{chunk-V7ATY4BG.js → chunk-VJD5LANR.js} +14 -16
  36. package/dist/chunk-VJD5LANR.js.map +1 -0
  37. package/dist/{chunk-YOREAPF6.js → chunk-WCCTIRSB.js} +11 -10
  38. package/dist/chunk-WCCTIRSB.js.map +1 -0
  39. package/dist/{cli-circuit-breaker-GFF2RLBZ.js → cli-circuit-breaker-STXIH563.js} +4 -4
  40. package/dist/cli.d.ts +5 -2
  41. package/dist/cli.js +138 -106
  42. package/dist/cli.js.map +1 -1
  43. package/dist/{composite-router-33F3F74I.js → composite-router-7AHZN3VI.js} +2 -2
  44. package/dist/{consensus-vote-5V4KVHBE.js → consensus-vote-XY55C7WQ.js} +11 -10
  45. package/dist/consensus-vote-types-CoGbAEjf.d.ts +195 -0
  46. package/dist/{doctor-deep-AHDTNURD.js → doctor-deep-ONHJTGR4.js} +3 -3
  47. package/dist/{expert-bridge-DMDHHDEU.js → expert-bridge-3AWQHR65.js} +3 -3
  48. package/dist/{factory-FVD7PZ6S.js → factory-E5NMAMZC.js} +5 -5
  49. package/dist/{factory-VQS3HJ7V.js → factory-HWHQ44BB.js} +4 -4
  50. package/dist/index.d.ts +4089 -3693
  51. package/dist/index.js +85 -73
  52. package/dist/index.js.map +1 -1
  53. package/dist/{init-opencode-EIOIPVWL.js → init-opencode-Z7OQ5RCB.js} +5 -5
  54. package/dist/{issue-triage-HJUJWGAD.js → issue-triage-UWBHMQHC.js} +4 -4
  55. package/dist/{mobimem-BOJFXQ7B.js → mobimem-G4UXJTCV.js} +2 -2
  56. package/dist/{registry-command-NCWUJKAF.js → registry-command-HYWVRAHE.js} +5 -6
  57. package/dist/registry-command-HYWVRAHE.js.map +1 -0
  58. package/dist/{repo-security-plan-3J45VAD6.js → repo-security-plan-W35CXK3T.js} +3 -3
  59. package/dist/{research-helpers-synthesize-UGQHZZJN.js → research-helpers-synthesize-GUQORWL4.js} +3 -3
  60. package/dist/{routing-memory-NO7QEH7T.js → routing-memory-VOJBOX3X.js} +2 -2
  61. package/dist/{session-memory-DOXLEWEU.js → session-memory-B6LQMF4N.js} +3 -3
  62. package/dist/{setup-command-BWUFMZ7U.js → setup-command-CTC5YNA4.js} +9 -8
  63. package/dist/{setup-config-E3JZYSLR.js → setup-config-53MHJA7S.js} +3 -3
  64. package/dist/{setup-custom-api-DHJ5DRH2.js → setup-custom-api-VD5W754A.js} +4 -4
  65. package/dist/{weather-report-FNN4OX3N.js → weather-report-APASTJDQ.js} +2 -2
  66. package/package.json +1 -1
  67. package/dist/chunk-3FIDMWFC.js.map +0 -1
  68. package/dist/chunk-53K3KEKT.js.map +0 -1
  69. package/dist/chunk-5MHIWRKB.js.map +0 -1
  70. package/dist/chunk-BVETPIOQ.js.map +0 -1
  71. package/dist/chunk-ES6GFP35.js.map +0 -1
  72. package/dist/chunk-J4VR2WNI.js.map +0 -1
  73. package/dist/chunk-O4KUCF5S.js.map +0 -1
  74. package/dist/chunk-P5OFZWDW.js.map +0 -1
  75. package/dist/chunk-V7ATY4BG.js.map +0 -1
  76. package/dist/chunk-YOREAPF6.js.map +0 -1
  77. package/dist/model-capabilities-types-B57GZryc.d.ts +0 -18
  78. package/dist/registry-command-NCWUJKAF.js.map +0 -1
  79. /package/dist/{adaptive-memory-UPE76IP6.js.map → adaptive-memory-OJY3IVNM.js.map} +0 -0
  80. /package/dist/{child-mcp-config-5HRJGLCR.js.map → child-mcp-config-KMCKKPNY.js.map} +0 -0
  81. /package/dist/{chunk-TF3GROMO.js.map → chunk-2SVS5WRV.js.map} +0 -0
  82. /package/dist/{chunk-QECRZ3YA.js.map → chunk-3ESCBV47.js.map} +0 -0
  83. /package/dist/{chunk-QL4HCYRD.js.map → chunk-3SZBDLFX.js.map} +0 -0
  84. /package/dist/{chunk-VTVKC4FS.js.map → chunk-44MVIL3F.js.map} +0 -0
  85. /package/dist/{chunk-C3JGKBL2.js.map → chunk-65R32U3M.js.map} +0 -0
  86. /package/dist/{chunk-L6N2S3UB.js.map → chunk-CCODJRS6.js.map} +0 -0
  87. /package/dist/{chunk-DA5UDQYW.js.map → chunk-DGELGUZF.js.map} +0 -0
  88. /package/dist/{chunk-2KB63QGE.js.map → chunk-EQHYXT56.js.map} +0 -0
  89. /package/dist/{chunk-VPC3YNFR.js.map → chunk-G5VE2DZS.js.map} +0 -0
  90. /package/dist/{chunk-2MD5MWCK.js.map → chunk-GS3GW7C7.js.map} +0 -0
  91. /package/dist/{chunk-5WQ3SRSE.js.map → chunk-PAFH336F.js.map} +0 -0
  92. /package/dist/{chunk-A35XORXU.js.map → chunk-Q7FTNKPO.js.map} +0 -0
  93. /package/dist/{chunk-TQFRPFMG.js.map → chunk-TBRNRW2Q.js.map} +0 -0
  94. /package/dist/{chunk-345KMHWH.js.map → chunk-UWJKMBPL.js.map} +0 -0
  95. /package/dist/{cli-circuit-breaker-GFF2RLBZ.js.map → cli-circuit-breaker-STXIH563.js.map} +0 -0
  96. /package/dist/{composite-router-33F3F74I.js.map → composite-router-7AHZN3VI.js.map} +0 -0
  97. /package/dist/{consensus-vote-5V4KVHBE.js.map → consensus-vote-XY55C7WQ.js.map} +0 -0
  98. /package/dist/{doctor-deep-AHDTNURD.js.map → doctor-deep-ONHJTGR4.js.map} +0 -0
  99. /package/dist/{expert-bridge-DMDHHDEU.js.map → expert-bridge-3AWQHR65.js.map} +0 -0
  100. /package/dist/{factory-FVD7PZ6S.js.map → factory-E5NMAMZC.js.map} +0 -0
  101. /package/dist/{factory-VQS3HJ7V.js.map → factory-HWHQ44BB.js.map} +0 -0
  102. /package/dist/{init-opencode-EIOIPVWL.js.map → init-opencode-Z7OQ5RCB.js.map} +0 -0
  103. /package/dist/{issue-triage-HJUJWGAD.js.map → issue-triage-UWBHMQHC.js.map} +0 -0
  104. /package/dist/{mobimem-BOJFXQ7B.js.map → mobimem-G4UXJTCV.js.map} +0 -0
  105. /package/dist/{repo-security-plan-3J45VAD6.js.map → repo-security-plan-W35CXK3T.js.map} +0 -0
  106. /package/dist/{research-helpers-synthesize-UGQHZZJN.js.map → research-helpers-synthesize-GUQORWL4.js.map} +0 -0
  107. /package/dist/{routing-memory-NO7QEH7T.js.map → routing-memory-VOJBOX3X.js.map} +0 -0
  108. /package/dist/{session-memory-DOXLEWEU.js.map → session-memory-B6LQMF4N.js.map} +0 -0
  109. /package/dist/{setup-command-BWUFMZ7U.js.map → setup-command-CTC5YNA4.js.map} +0 -0
  110. /package/dist/{setup-config-E3JZYSLR.js.map → setup-config-53MHJA7S.js.map} +0 -0
  111. /package/dist/{setup-custom-api-DHJ5DRH2.js.map → setup-custom-api-VD5W754A.js.map} +0 -0
  112. /package/dist/{weather-report-FNN4OX3N.js.map → weather-report-APASTJDQ.js.map} +0 -0
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  OpenAIAdapter
3
- } from "./chunk-BVETPIOQ.js";
3
+ } from "./chunk-4K6L7RKC.js";
4
4
  import {
5
5
  ConfigError,
6
6
  createLogger,
7
7
  err,
8
8
  getErrorMessage,
9
- getModelCapabilities,
10
9
  getTimeProvider,
10
+ lookupInTreeCapability,
11
11
  ok
12
- } from "./chunk-O4KUCF5S.js";
12
+ } from "./chunk-Q3RFPJYK.js";
13
13
  import {
14
14
  getNexusDataDir
15
15
  } from "./chunk-GOT7OAL5.js";
@@ -90,7 +90,7 @@ import OpenAI from "openai";
90
90
  import { appendFileSync, existsSync, mkdirSync, readFileSync as readFileSync2, readdirSync } from "fs";
91
91
  import { dirname, join } from "path";
92
92
  function computeCostUSD(modelId, inputTokens, outputTokens) {
93
- const cap = getModelCapabilities(modelId);
93
+ const cap = lookupInTreeCapability(modelId);
94
94
  if (cap === void 0) return 0;
95
95
  const inputPer1M = cap.pricing?.inputPer1M ?? 0;
96
96
  const outputPer1M = cap.pricing?.outputPer1M ?? 0;
@@ -241,7 +241,7 @@ function createOpenAICompatAdapter(modelId, config) {
241
241
  return withUsageRecording(inner);
242
242
  }
243
243
  function withUsageRecording(inner) {
244
- return {
244
+ const wrapped = {
245
245
  providerId: inner.providerId,
246
246
  modelId: inner.modelId,
247
247
  capabilities: inner.capabilities,
@@ -283,6 +283,13 @@ function withUsageRecording(inner) {
283
283
  return result;
284
284
  }
285
285
  };
286
+ attachListModels(wrapped, inner);
287
+ return wrapped;
288
+ }
289
+ function attachListModels(wrapped, inner) {
290
+ const list = inner.listModels?.bind(inner);
291
+ if (list === void 0) return;
292
+ wrapped.listModels = () => list();
286
293
  }
287
294
  async function buildOpenAICompatAdapters() {
288
295
  const config = readOpenAICompatEnv();
@@ -300,4 +307,4 @@ export {
300
307
  discoverModels,
301
308
  buildOpenAICompatAdapters
302
309
  };
303
- //# sourceMappingURL=chunk-P5OFZWDW.js.map
310
+ //# sourceMappingURL=chunk-Q6JDV36D.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config/opencode-bridge.ts","../src/adapters/openai-compat-adapter.ts","../src/learning/usage-log.ts"],"sourcesContent":["/**\n * opencode.json gateway-config bridge (#2503, child 3 of epic #2500).\n *\n * When nexus-agents runs as an MCP loaded by OpenCode (typically inside a\n * Docker sandbox), OpenCode's `opencode.json` already declares the\n * OpenAI-compatible gateway the harness uses. Reading it from there saves\n * the operator from re-declaring the same `baseURL` + `apiKey` as\n * NEXUS_OPENAI_COMPAT_* env vars.\n *\n * Scope (per direction discussion 2026-05-09):\n * - **Only** reads `providers.openai-compat.options.{baseURL, apiKey}`.\n * Does NOT read OpenCode's MCP config, model list, logging, or other\n * settings. nexus-agents' own behaviour stays driven by\n * `nexus-agents.yaml` + `NEXUS_*` env vars.\n * - Resolves OpenCode's `{env:VAR}` interpolation in the apiKey field.\n * - Returns `null` on any failure path (missing file, parse error,\n * missing fields, unset interpolated env var) — caller falls back to\n * env-var precedence; we don't throw.\n *\n * Precedence (applied in `openai-compat-adapter.ts`):\n * 1. `NEXUS_OPENAI_COMPAT_URL` + `NEXUS_OPENAI_COMPAT_KEY` env vars (if both set)\n * 2. `NEXUS_OPENCODE_CONFIG` → `opencode.json` → `providers.openai-compat`\n * 3. Unconfigured → `null`, no adapter registered\n *\n * Sanitised logging: never log the resolved apiKey. Only the baseURL is\n * logged on success.\n *\n * @module config/opencode-bridge\n */\n\nimport { readFileSync } from 'node:fs';\n\nimport { createLogger } from '../core/index.js';\n\nconst logger = createLogger({ component: 'opencode-bridge' });\n\nexport interface OpencodeGatewayConfig {\n readonly baseURL: string;\n readonly apiKey: string;\n}\n\n/**\n * Read `providers.openai-compat.options.{baseURL, apiKey}` from the given\n * opencode.json path. Returns `null` on any failure — the caller falls\n * back to env-var precedence.\n */\nexport function readOpencodeGateway(path: string): OpencodeGatewayConfig | null {\n const raw = readFileOrNull(path);\n if (raw === null) return null;\n\n const parsed = parseJsonOrNull(raw, path);\n if (parsed === null) return null;\n\n const options = extractOpenAICompatOptions(parsed);\n if (options === null) {\n logger.debug('opencode.json has no providers.openai-compat.options block', { path });\n return null;\n }\n\n const baseURL = typeof options.baseURL === 'string' ? options.baseURL.trim() : '';\n const apiKeyRaw = typeof options.apiKey === 'string' ? options.apiKey.trim() : '';\n if (baseURL === '' || apiKeyRaw === '') {\n logger.warn('opencode.json providers.openai-compat missing baseURL or apiKey', { path });\n return null;\n }\n\n const apiKey = resolveEnvInterpolation(apiKeyRaw);\n if (apiKey === null) {\n logger.warn(\n 'opencode.json apiKey references an env var that is not set; gateway not configured',\n { path }\n );\n return null;\n }\n\n logger.info('Gateway config sourced from opencode.json', { baseURL, path });\n return { baseURL, apiKey };\n}\n\nfunction readFileOrNull(path: string): string | null {\n try {\n return readFileSync(path, 'utf8');\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n logger.debug('Could not read opencode.json', { path, error: msg });\n return null;\n }\n}\n\nfunction parseJsonOrNull(raw: string, path: string): unknown {\n try {\n return JSON.parse(raw);\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n logger.warn('opencode.json is not valid JSON; ignoring', { path, error: msg });\n return null;\n }\n}\n\n/**\n * Pull `providers.openai-compat.options` out of the parsed JSON without\n * coupling to a Zod schema. OpenCode's config has many keys we don't\n * care about; we only navigate to the one we need.\n */\nfunction extractOpenAICompatOptions(\n parsed: unknown\n): { baseURL?: unknown; apiKey?: unknown } | null {\n if (typeof parsed !== 'object' || parsed === null) return null;\n const root = parsed as { providers?: unknown };\n const providers = root.providers;\n if (typeof providers !== 'object' || providers === null) return null;\n const provider = (providers as Record<string, unknown>)['openai-compat'];\n if (typeof provider !== 'object' || provider === null) return null;\n const options = (provider as { options?: unknown }).options;\n if (typeof options !== 'object' || options === null) return null;\n return options;\n}\n\n/**\n * Resolve OpenCode's `{env:VAR}` interpolation. Only the literal pattern\n * is supported (no shell expansion, no defaults). Returns the string as-is\n * when no interpolation is present; returns `null` when the referenced\n * env var is not set.\n */\nfunction resolveEnvInterpolation(value: string): string | null {\n const match = /^\\{env:([A-Z0-9_]+)\\}$/.exec(value);\n if (match === null) return value;\n const envName = match[1];\n if (envName === undefined) return null;\n const resolved = process.env[envName];\n if (resolved === undefined || resolved === '') return null;\n return resolved;\n}\n","/**\n * OpenAI-compatible gateway adapter — talk to any HTTP gateway that exposes\n * the OpenAI Chat Completions API. The gateway may itself be a multi-model\n * router (Bedrock/Vertex/Azure proxy, OpenRouter, vLLM, etc.). nexus-agents\n * sees one adapter, the gateway exposes N models, and the existing routing\n * pipeline picks among them.\n *\n * Source: Issue #2468 (epic #2467 child).\n *\n * Configuration precedence (#2503, child 3 of epic #2500):\n * 1. NEXUS_OPENAI_COMPAT_URL + NEXUS_OPENAI_COMPAT_KEY env vars (both required)\n * 2. NEXUS_OPENCODE_CONFIG path → opencode.json → providers.openai-compat\n * 3. Unconfigured → adapter not built\n *\n * Models are discovered via GET {base}/v1/models at first use. Each model\n * the gateway exposes can be selected by ID; the adapter wraps the existing\n * `OpenAIAdapter` for the actual chat-completions request, so streaming +\n * tool use + the full IModelAdapter contract come for free.\n */\n\nimport OpenAI from 'openai';\n\nimport type {\n Result,\n CompletionRequest,\n CompletionResponse,\n ModelError,\n ModelMetadata,\n IModelAdapter,\n} from '../core/index.js';\nimport { ok, err, ConfigError, getErrorMessage, getTimeProvider } from '../core/index.js';\nimport { OpenAIAdapter } from './openai-adapter.js';\nimport { recordUsageEvent, computeCostUSD } from '../learning/usage-log.js';\nimport { readOpencodeGateway } from '../config/opencode-bridge.js';\n\nexport interface OpenAICompatConfig {\n /** Gateway base URL — must reach `/v1/models` and `/v1/chat/completions`. */\n readonly baseUrl: string;\n /** API key the gateway expects. */\n readonly apiKey: string;\n}\n\nexport interface DiscoveredModel {\n readonly id: string;\n /** Unix epoch seconds when the model was created (per OpenAI API). */\n readonly created?: number;\n /** Owning organization or upstream provider name. */\n readonly ownedBy?: string;\n}\n\n/**\n * Read the gateway config with the precedence chain documented in the\n * module docstring: env vars > opencode.json > unconfigured.\n *\n * The env-var path (#2468) wins when both `NEXUS_OPENAI_COMPAT_URL` and\n * `NEXUS_OPENAI_COMPAT_KEY` are set. Otherwise, when `NEXUS_OPENCODE_CONFIG`\n * names a path, the opencode.json bridge tries to source the gateway from\n * `providers.openai-compat.options.{baseURL, apiKey}` (#2503). Returns\n * `null` when neither path yields a config — caller treats unset gateway\n * as \"no adapter from this source.\"\n */\nexport function readOpenAICompatEnv(): OpenAICompatConfig | null {\n const fromEnv = readGatewayFromEnv();\n if (fromEnv !== null) return fromEnv;\n return readGatewayFromOpencode();\n}\n\nfunction readGatewayFromEnv(): OpenAICompatConfig | null {\n const envUrl = process.env['NEXUS_OPENAI_COMPAT_URL']?.trim();\n const envKey = process.env['NEXUS_OPENAI_COMPAT_KEY']?.trim();\n if (envUrl === undefined || envUrl === '') return null;\n if (envKey === undefined || envKey === '') return null;\n return { baseUrl: envUrl, apiKey: envKey };\n}\n\nfunction readGatewayFromOpencode(): OpenAICompatConfig | null {\n const opencodePath = process.env['NEXUS_OPENCODE_CONFIG']?.trim();\n if (opencodePath === undefined || opencodePath === '') return null;\n const fromFile = readOpencodeGateway(opencodePath);\n if (fromFile === null) return null;\n return { baseUrl: fromFile.baseURL, apiKey: fromFile.apiKey };\n}\n\n/**\n * Discover available models by calling `GET {baseUrl}/v1/models`. Uses the\n * official `openai` SDK's `client.models.list()` so we benefit from its\n * pagination + retry handling. The list is the strongly authoritative\n * source: nexus-agents won't try to dispatch to a model the gateway doesn't\n * expose.\n */\nexport async function discoverModels(\n config: OpenAICompatConfig\n): Promise<Result<readonly DiscoveredModel[], ConfigError>> {\n try {\n const client = new OpenAI({ baseURL: config.baseUrl, apiKey: config.apiKey });\n const list = await client.models.list();\n const models: readonly DiscoveredModel[] = list.data.map((m) => ({\n id: m.id,\n created: m.created,\n ownedBy: m.owned_by,\n }));\n return ok(models);\n } catch (e: unknown) {\n return err(\n new ConfigError(\n `Failed to discover models from ${config.baseUrl}: ${getErrorMessage(e)}. ` +\n `Verify NEXUS_OPENAI_COMPAT_URL and NEXUS_OPENAI_COMPAT_KEY, then retry.`\n )\n );\n }\n}\n\n/**\n * Create an OpenAIAdapter pointed at the gateway for a specific model ID,\n * wrapped with usage recording so every completion appends a UsageEvent\n * to the JSONL log consumed by `nexus-agents usage`.\n *\n * The wrapper is transparent — same IModelAdapter contract, same fields,\n * same error handling. Recording is best-effort (telemetry never fails\n * the user's call).\n *\n * When invoked via MCP, the host harness's model identifier is passed\n * through verbatim — nexus-agents doesn't second-guess what the host is\n * already routing.\n */\nexport function createOpenAICompatAdapter(\n modelId: string,\n config: OpenAICompatConfig\n): IModelAdapter {\n const inner = new OpenAIAdapter({ modelId, apiKey: config.apiKey, baseUrl: config.baseUrl });\n return withUsageRecording(inner);\n}\n\n/**\n * Wrap any IModelAdapter so that successful + failed `complete()` calls\n * append a UsageEvent to the on-disk usage log. Stream calls aren't yet\n * instrumented (a future PR can add streaming-aware recording).\n *\n * The returned object preserves the IModelAdapter contract identically;\n * downstream code can't tell the difference except that one extra JSONL\n * line gets written per call.\n */\nfunction withUsageRecording(inner: IModelAdapter): IModelAdapter {\n const wrapped: IModelAdapter = {\n providerId: inner.providerId,\n modelId: inner.modelId,\n capabilities: inner.capabilities,\n countTokens: (text) => inner.countTokens(text),\n validateConfig: () => inner.validateConfig(),\n stream: (request) => inner.stream(request),\n async complete(request: CompletionRequest): Promise<Result<CompletionResponse, ModelError>> {\n const start = getTimeProvider().now();\n const result = await inner.complete(request);\n const latencyMs = getTimeProvider().now() - start;\n try {\n if (result.ok) {\n const u = result.value.usage;\n recordUsageEvent({\n timestamp: new Date().toISOString(),\n modelId: inner.modelId,\n providerId: inner.providerId,\n inputTokens: u.inputTokens,\n outputTokens: u.outputTokens,\n usdCost: computeCostUSD(inner.modelId, u.inputTokens, u.outputTokens),\n latencyMs,\n success: true,\n });\n } else {\n recordUsageEvent({\n timestamp: new Date().toISOString(),\n modelId: inner.modelId,\n providerId: inner.providerId,\n inputTokens: 0,\n outputTokens: 0,\n usdCost: 0,\n latencyMs,\n success: false,\n errorCode: result.error.code,\n });\n }\n } catch {\n // Telemetry must not break user calls.\n }\n return result;\n },\n };\n attachListModels(wrapped, inner);\n return wrapped;\n}\n\n/**\n * (#2540) Forward `listModels` through the wrapper when the inner adapter\n * exposes one. Only attach when defined so the wrapper's `listModels?:`\n * hint stays accurate for the resolver. The inner reference is captured\n * by closure so the forwarded call binds `this` to the inner adapter.\n */\nfunction attachListModels(wrapped: IModelAdapter, inner: IModelAdapter): void {\n const list = inner.listModels?.bind(inner);\n if (list === undefined) return;\n wrapped.listModels = (): Promise<readonly ModelMetadata[]> => list();\n}\n\n/**\n * Convenience: read env, discover, return adapter instances for every\n * discovered model. Returns `null` (not an error) when env vars aren't set\n * — the caller treats unset gateway as \"no adapter from this source.\"\n *\n * Use case: the unified registry / factory calls this at startup; if the\n * operator has configured a gateway, every discovered model becomes a\n * dispatch target alongside the existing claude/codex/gemini/opencode\n * adapter slots.\n */\nexport async function buildOpenAICompatAdapters(): Promise<Result<\n readonly IModelAdapter[],\n ConfigError\n> | null> {\n const config = readOpenAICompatEnv();\n if (config === null) return null;\n const discovered = await discoverModels(config);\n if (!discovered.ok) return discovered;\n return ok(discovered.value.map((m) => createOpenAICompatAdapter(m.id, config)));\n}\n","/**\n * usage-log — append-only per-call usage events with cost.\n *\n * Source: Issue #2469 (epic #2467 child).\n *\n * For operators running against metered API gateways, per-call cost +\n * tokens + latency is the data they need to manage spend. This module\n * provides three things:\n *\n * 1. `recordUsageEvent(event)` — append a per-call record to a JSONL\n * log under <NEXUS_DATA_DIR>/usage/usage-YYYY-MM.jsonl.\n * 2. `loadUsageEvents({...})` — read events for a window, filtered\n * by model / category.\n * 3. `computeCostUSD(modelId, inputTokens, outputTokens)` — compute\n * cost from `config/model-capabilities.ts` pricing.\n *\n * The `usage` CLI command (cli/usage-command.ts) consumes this for the\n * operator dashboard. Existing OutcomeStore is intentionally untouched\n * — its schema is for routing/learning signals, not billing.\n */\n\nimport { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\n\nimport { getNexusDataDir } from '../config/nexus-data-dir.js';\nimport { lookupInTreeCapability } from '../config/model-config-helpers.js';\n\nexport interface UsageEvent {\n /** ISO 8601 timestamp of the call. */\n readonly timestamp: string;\n /** Model identifier (e.g., 'claude-sonnet-4', 'gpt-4o'). */\n readonly modelId: string;\n /** Provider/adapter (e.g., 'anthropic', 'openai', 'openai-compat'). */\n readonly providerId: string;\n /** Token counts. */\n readonly inputTokens: number;\n readonly outputTokens: number;\n /** Cost in USD. Computed at write time from pricing in model-capabilities. */\n readonly usdCost: number;\n /** Wall-clock latency in milliseconds. */\n readonly latencyMs: number;\n /** Whether the call succeeded. */\n readonly success: boolean;\n /**\n * Optional task category — populated when the call was made on behalf of\n * a routed task (so aggregation can roll up by category).\n */\n readonly category?: string;\n /** Optional failure code when success === false. */\n readonly errorCode?: string;\n}\n\n/**\n * Compute cost in USD given a model and token counts. Returns 0 when the\n * model has no pricing data (e.g., free local model, gateway-routed model\n * we don't have rates for). Operators with custom gateways can extend\n * `config/model-capabilities.ts` to add their pricing.\n */\nexport function computeCostUSD(modelId: string, inputTokens: number, outputTokens: number): number {\n const cap = lookupInTreeCapability(modelId);\n if (cap === undefined) return 0;\n const inputPer1M = cap.pricing?.inputPer1M ?? 0;\n const outputPer1M = cap.pricing?.outputPer1M ?? 0;\n // Multiply token counts by per-million rate then divide. Use Math.round\n // at micro-USD precision so JSONL files don't drift to floating-point\n // noise on small calls.\n const microUsd = Math.round(\n inputTokens * inputPer1M + outputTokens * outputPer1M // micro-USD per million scaled\n );\n return microUsd / 1_000_000;\n}\n\n/** Resolve the active usage log path for the current month. */\nexport function getUsageLogPath(date: Date = new Date()): string {\n const year = date.getUTCFullYear();\n const month = String(date.getUTCMonth() + 1).padStart(2, '0');\n return join(getNexusDataDir(), 'usage', `usage-${String(year)}-${month}.jsonl`);\n}\n\n/**\n * Append a usage event to the current month's log. Best-effort — failures\n * are silent (we don't want to fail a successful model call because we\n * couldn't write a log line).\n */\nexport function recordUsageEvent(event: UsageEvent): void {\n try {\n const path = getUsageLogPath(new Date(event.timestamp));\n mkdirSync(dirname(path), { recursive: true });\n appendFileSync(path, `${JSON.stringify(event)}\\n`, 'utf-8');\n } catch {\n // Intentionally silent — telemetry must not break user calls.\n }\n}\n\nexport interface LoadUsageOptions {\n /** Restrict to events at or after this ISO timestamp. */\n readonly sinceIso?: string;\n /** Restrict to events before this ISO timestamp. */\n readonly untilIso?: string;\n /** Only events for this model. */\n readonly modelId?: string;\n /** Only events for this category. */\n readonly category?: string;\n}\n\nfunction listUsageFiles(dir: string): readonly string[] {\n if (!existsSync(dir)) return [];\n try {\n return readdirSync(dir).filter((f) => f.startsWith('usage-') && f.endsWith('.jsonl'));\n } catch {\n return [];\n }\n}\n\ninterface LoadFilter {\n readonly sinceMs: number;\n readonly untilMs: number;\n readonly modelId: string | undefined;\n readonly category: string | undefined;\n}\n\nfunction eventMatches(parsed: UsageEvent, f: LoadFilter): boolean {\n const ts = Date.parse(parsed.timestamp);\n if (ts < f.sinceMs || ts >= f.untilMs) return false;\n if (f.modelId !== undefined && parsed.modelId !== f.modelId) return false;\n if (f.category !== undefined && parsed.category !== f.category) return false;\n return true;\n}\n\nfunction parseFileLines(filePath: string, filter: LoadFilter): readonly UsageEvent[] {\n let content: string;\n try {\n content = readFileSync(filePath, 'utf-8');\n } catch {\n return [];\n }\n const out: UsageEvent[] = [];\n for (const line of content.split('\\n')) {\n if (line.trim() === '') continue;\n try {\n const parsed = JSON.parse(line) as UsageEvent;\n if (eventMatches(parsed, filter)) out.push(parsed);\n } catch {\n // Skip malformed line; keep reading.\n continue;\n }\n }\n return out;\n}\n\n/**\n * Load all usage events from disk that match the filter. Reads every\n * monthly log file under the data dir; for sub-second filtering at scale\n * a future PR can index by month, but linear scan is fine at the\n * \"operator dashboard\" scale this command targets.\n */\nexport function loadUsageEvents(opts: LoadUsageOptions = {}): readonly UsageEvent[] {\n const dir = join(getNexusDataDir(), 'usage');\n const files = listUsageFiles(dir);\n if (files.length === 0) return [];\n const filter: LoadFilter = {\n sinceMs: opts.sinceIso !== undefined ? Date.parse(opts.sinceIso) : Number.NEGATIVE_INFINITY,\n untilMs: opts.untilIso !== undefined ? Date.parse(opts.untilIso) : Number.POSITIVE_INFINITY,\n modelId: opts.modelId,\n category: opts.category,\n };\n const events: UsageEvent[] = [];\n for (const f of files) {\n events.push(...parseFileLines(join(dir, f), filter));\n }\n return events;\n}\n\nexport interface ModelRollup {\n readonly modelId: string;\n readonly providerId: string;\n readonly callCount: number;\n readonly successCount: number;\n readonly successRate: number;\n readonly totalInputTokens: number;\n readonly totalOutputTokens: number;\n readonly totalUsdCost: number;\n readonly avgLatencyMs: number;\n readonly costPerSuccessUsd: number;\n}\n\n/**\n * Aggregate events into per-model rollups. Sorted by total cost descending\n * — the model burning the most money at top. Useful for \"where is my spend\n * going?\" investigations.\n */\nexport function rollupByModel(events: readonly UsageEvent[]): readonly ModelRollup[] {\n const groups = new Map<string, UsageEvent[]>();\n for (const e of events) {\n const arr = groups.get(e.modelId);\n if (arr === undefined) groups.set(e.modelId, [e]);\n else arr.push(e);\n }\n const rollups: ModelRollup[] = [];\n for (const [modelId, group] of groups) {\n const callCount = group.length;\n const successCount = group.filter((e) => e.success).length;\n const totalInputTokens = group.reduce((s, e) => s + e.inputTokens, 0);\n const totalOutputTokens = group.reduce((s, e) => s + e.outputTokens, 0);\n const totalUsdCost = group.reduce((s, e) => s + e.usdCost, 0);\n const totalLatency = group.reduce((s, e) => s + e.latencyMs, 0);\n const successRate = callCount === 0 ? 0 : successCount / callCount;\n const avgLatencyMs = callCount === 0 ? 0 : totalLatency / callCount;\n const costPerSuccessUsd = successCount === 0 ? totalUsdCost : totalUsdCost / successCount;\n rollups.push({\n modelId,\n providerId: group[0]?.providerId ?? 'unknown',\n callCount,\n successCount,\n successRate,\n totalInputTokens,\n totalOutputTokens,\n totalUsdCost,\n avgLatencyMs,\n costPerSuccessUsd,\n });\n }\n return rollups.sort((a, b) => b.totalUsdCost - a.totalUsdCost);\n}\n"],"mappings":"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","names":["err","readFileSync","readFileSync"]}
@@ -7,7 +7,7 @@ import {
7
7
  err,
8
8
  getErrorMessage,
9
9
  ok
10
- } from "./chunk-O4KUCF5S.js";
10
+ } from "./chunk-Q3RFPJYK.js";
11
11
 
12
12
  // src/cli/research-helpers-io.ts
13
13
  import * as fs2 from "fs/promises";
@@ -981,4 +981,4 @@ export {
981
981
  normalizeTopicToCanonical,
982
982
  synthesizeResearch
983
983
  };
984
- //# sourceMappingURL=chunk-A35XORXU.js.map
984
+ //# sourceMappingURL=chunk-Q7FTNKPO.js.map
@@ -0,0 +1,153 @@
1
+ import {
2
+ createLogger
3
+ } from "./chunk-Q3RFPJYK.js";
4
+
5
+ // src/config/available-models-cache.ts
6
+ var logger = createLogger({ component: "available-models-cache" });
7
+ var DEFAULT_TTL_MS = 5 * 60 * 1e3;
8
+ var DEFAULT_STALE_TTL_MS = 25 * 60 * 1e3;
9
+ var AvailableModelsCache = class {
10
+ sources;
11
+ ttlMs;
12
+ staleTtlMs;
13
+ now;
14
+ states;
15
+ constructor(options) {
16
+ this.sources = [...options.sources];
17
+ this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
18
+ this.staleTtlMs = options.staleTtlMs ?? DEFAULT_STALE_TTL_MS;
19
+ this.now = options.now ?? Date.now;
20
+ this.states = /* @__PURE__ */ new Map();
21
+ for (const s of this.sources) {
22
+ this.states.set(s.name, { value: null, fetchedAt: 0, inFlight: null });
23
+ }
24
+ }
25
+ /**
26
+ * Register a source after construction. Used by adapter factories that
27
+ * wire themselves into the default cache lazily as they're built.
28
+ * Duplicate names are ignored (first registration wins, on the
29
+ * assumption that the same factory might run twice in a test session).
30
+ */
31
+ addSource(source) {
32
+ if (this.states.has(source.name)) return;
33
+ this.sources.push(source);
34
+ this.states.set(source.name, { value: null, fetchedAt: 0, inFlight: null });
35
+ }
36
+ /**
37
+ * Remove a previously-registered source. Used by tests + by factories
38
+ * that want to drop a probe when an adapter is being disposed.
39
+ */
40
+ removeSource(name) {
41
+ this.sources = this.sources.filter((s) => s.name !== name);
42
+ this.states.delete(name);
43
+ }
44
+ /**
45
+ * Returns the union of every source's available models. Stale-while-
46
+ * revalidate: serve fresh-or-stale immediately, refresh stale entries
47
+ * in the background. First call (no cache yet) blocks on every source.
48
+ */
49
+ async getAll() {
50
+ const probes = this.sources.map((s) => this.probeOne(s));
51
+ const results = await Promise.all(probes);
52
+ const out = [];
53
+ for (const r of results) {
54
+ for (const m of r) out.push(m);
55
+ }
56
+ return out;
57
+ }
58
+ /** Models reported by one named source (subset of getAll). */
59
+ async byProvider(sourceName) {
60
+ const all = await this.getAll();
61
+ return all.filter((m) => m.source === sourceName);
62
+ }
63
+ /** True iff some source currently reports the given model id. */
64
+ async has(modelId) {
65
+ const all = await this.getAll();
66
+ return all.some((m) => m.id === modelId);
67
+ }
68
+ /** Force a synchronous refresh of every source — used after a 404. */
69
+ async refresh() {
70
+ for (const s of this.sources) {
71
+ const state = this.states.get(s.name);
72
+ if (state === void 0) continue;
73
+ state.fetchedAt = 0;
74
+ state.value = null;
75
+ }
76
+ return this.getAll();
77
+ }
78
+ /**
79
+ * Probe one source. Returns:
80
+ * - cached value if still fresh
81
+ * - cached value AND kicks a background refresh if stale-but-not-expired
82
+ * - blocks on the source if no cache or fully expired
83
+ *
84
+ * Source errors yield an empty list (logged) so one bad source can't
85
+ * poison the union.
86
+ */
87
+ async probeOne(source) {
88
+ const state = this.getState(source.name);
89
+ const age = this.now() - state.fetchedAt;
90
+ if (state.value !== null && age < this.ttlMs) {
91
+ return state.value;
92
+ }
93
+ if (state.value !== null && age < this.staleTtlMs) {
94
+ state.inFlight ??= this.fetchSource(source).finally(() => {
95
+ state.inFlight = null;
96
+ });
97
+ return state.value;
98
+ }
99
+ if (state.inFlight !== null) return state.inFlight;
100
+ state.inFlight = this.fetchSource(source).finally(() => {
101
+ state.inFlight = null;
102
+ });
103
+ return state.inFlight;
104
+ }
105
+ async fetchSource(source) {
106
+ const state = this.getState(source.name);
107
+ try {
108
+ const raw = await source.listModels();
109
+ const value = raw.map((m) => normaliseEntry(m, source));
110
+ state.value = value;
111
+ state.fetchedAt = this.now();
112
+ return value;
113
+ } catch (e) {
114
+ logger.warn("AvailableModelsCache source probe failed", {
115
+ source: source.name,
116
+ error: e instanceof Error ? e.message : String(e)
117
+ });
118
+ return state.value ?? [];
119
+ }
120
+ }
121
+ getState(name) {
122
+ const state = this.states.get(name);
123
+ if (state === void 0) {
124
+ throw new Error(`AvailableModelsCache: unknown source "${name}"`);
125
+ }
126
+ return state;
127
+ }
128
+ };
129
+ function normaliseEntry(raw, source) {
130
+ const slash = raw.id.indexOf("/");
131
+ if (slash > 0 && slash < raw.id.length - 1) {
132
+ return { id: raw.id, source: source.name, provider: raw.id.slice(0, slash) };
133
+ }
134
+ if (source.providerHint !== void 0) {
135
+ return { id: raw.id, source: source.name, provider: source.providerHint };
136
+ }
137
+ return { id: raw.id, source: source.name };
138
+ }
139
+ var defaultCache = null;
140
+ function getDefaultAvailableModelsCache() {
141
+ defaultCache ??= new AvailableModelsCache({ sources: [] });
142
+ return defaultCache;
143
+ }
144
+ function setDefaultAvailableModelsCache(cache) {
145
+ defaultCache = cache;
146
+ }
147
+
148
+ export {
149
+ AvailableModelsCache,
150
+ getDefaultAvailableModelsCache,
151
+ setDefaultAvailableModelsCache
152
+ };
153
+ //# sourceMappingURL=chunk-QON7LR7J.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config/available-models-cache.ts"],"sourcesContent":["/**\n * AvailableModelsCache (#2540 PR 6 of 8).\n *\n * Central, harness-driven view of what models the runtime can actually\n * dispatch to right now. PR 5 added `listModels()` on direct-API adapters\n * (Anthropic, Google, OpenAI gateway) and on the OpenCode CLI adapter;\n * this cache stitches those probes together into one queryable surface.\n *\n * Design invariants:\n *\n * 1. Sources are the source of truth — if a harness drops a model,\n * the registry never decides it's still routable. The\n * `ModelRegistry` (PR 1) answers \"how should this model behave\"\n * while this cache answers \"is this model routable at all\".\n *\n * 2. Stale-while-revalidate: serve the most recent successful result\n * while a refresh runs in the background, so callers never block on\n * a slow `models.list` round-trip.\n *\n * 3. One bad source must not poison the others. A failing `listModels`\n * logs and is excluded from the next snapshot; remaining sources\n * remain queryable.\n *\n * 4. No persistence. Process-local cache only — operators restart and\n * get a fresh probe. Persistence belongs in PR 7 if it's needed.\n */\n\nimport { createLogger } from '../core/logger.js';\n\nconst logger = createLogger({ component: 'available-models-cache' });\n\n/** Default freshness TTL: 5 minutes — matches per-adapter listModels TTL. */\nconst DEFAULT_TTL_MS = 5 * 60 * 1000;\n/** Default stale TTL: 25 minutes — beyond this, callers wait on a refresh. */\nconst DEFAULT_STALE_TTL_MS = 25 * 60 * 1000;\n\n/**\n * One adapter (CLI or API) that can be asked what models it currently has.\n * Exists so this cache doesn't have to know about IModelAdapter vs\n * ICliAdapter — both can adapt themselves to this minimal surface.\n */\nexport interface AvailableModelsSource {\n /** Stable, human-readable identifier — e.g. `claude`, `gateway-openrouter`. */\n readonly name: string;\n /**\n * Optional vendor-family hint — `anthropic` / `openai` / `google` / etc.\n * Used to pre-tag entries when the source's own model ids don't carry\n * a `provider/` prefix (Anthropic API, Google API).\n */\n readonly providerHint?: string;\n /** Probe the underlying surface for currently available models. */\n listModels(): Promise<readonly { id: string }[]>;\n}\n\n/** One row in the cache: model id + which source first reported it. */\nexport interface AvailableModel {\n readonly id: string;\n readonly source: string;\n /** `provider/` prefix when the source uses one; otherwise the providerHint. */\n readonly provider?: string;\n}\n\nexport interface AvailableModelsCacheOptions {\n readonly sources: readonly AvailableModelsSource[];\n /** Freshness TTL (ms). Defaults to 5 minutes. */\n readonly ttlMs?: number;\n /** Beyond this, callers block on the next refresh. Defaults to 25 minutes. */\n readonly staleTtlMs?: number;\n /** Override `Date.now` for tests. */\n readonly now?: () => number;\n}\n\ninterface SourceState {\n value: readonly AvailableModel[] | null;\n fetchedAt: number;\n inFlight: Promise<readonly AvailableModel[]> | null;\n}\n\nexport class AvailableModelsCache {\n private sources: AvailableModelsSource[];\n private readonly ttlMs: number;\n private readonly staleTtlMs: number;\n private readonly now: () => number;\n private readonly states: Map<string, SourceState>;\n\n constructor(options: AvailableModelsCacheOptions) {\n this.sources = [...options.sources];\n this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;\n this.staleTtlMs = options.staleTtlMs ?? DEFAULT_STALE_TTL_MS;\n this.now = options.now ?? Date.now;\n this.states = new Map();\n for (const s of this.sources) {\n this.states.set(s.name, { value: null, fetchedAt: 0, inFlight: null });\n }\n }\n\n /**\n * Register a source after construction. Used by adapter factories that\n * wire themselves into the default cache lazily as they're built.\n * Duplicate names are ignored (first registration wins, on the\n * assumption that the same factory might run twice in a test session).\n */\n addSource(source: AvailableModelsSource): void {\n if (this.states.has(source.name)) return;\n this.sources.push(source);\n this.states.set(source.name, { value: null, fetchedAt: 0, inFlight: null });\n }\n\n /**\n * Remove a previously-registered source. Used by tests + by factories\n * that want to drop a probe when an adapter is being disposed.\n */\n removeSource(name: string): void {\n this.sources = this.sources.filter((s) => s.name !== name);\n this.states.delete(name);\n }\n\n /**\n * Returns the union of every source's available models. Stale-while-\n * revalidate: serve fresh-or-stale immediately, refresh stale entries\n * in the background. First call (no cache yet) blocks on every source.\n */\n async getAll(): Promise<readonly AvailableModel[]> {\n const probes = this.sources.map((s) => this.probeOne(s));\n const results = await Promise.all(probes);\n const out: AvailableModel[] = [];\n for (const r of results) {\n for (const m of r) out.push(m);\n }\n return out;\n }\n\n /** Models reported by one named source (subset of getAll). */\n async byProvider(sourceName: string): Promise<readonly AvailableModel[]> {\n const all = await this.getAll();\n return all.filter((m) => m.source === sourceName);\n }\n\n /** True iff some source currently reports the given model id. */\n async has(modelId: string): Promise<boolean> {\n const all = await this.getAll();\n return all.some((m) => m.id === modelId);\n }\n\n /** Force a synchronous refresh of every source — used after a 404. */\n async refresh(): Promise<readonly AvailableModel[]> {\n for (const s of this.sources) {\n const state = this.states.get(s.name);\n if (state === undefined) continue;\n state.fetchedAt = 0;\n state.value = null;\n }\n return this.getAll();\n }\n\n /**\n * Probe one source. Returns:\n * - cached value if still fresh\n * - cached value AND kicks a background refresh if stale-but-not-expired\n * - blocks on the source if no cache or fully expired\n *\n * Source errors yield an empty list (logged) so one bad source can't\n * poison the union.\n */\n private async probeOne(source: AvailableModelsSource): Promise<readonly AvailableModel[]> {\n const state = this.getState(source.name);\n const age = this.now() - state.fetchedAt;\n\n if (state.value !== null && age < this.ttlMs) {\n return state.value;\n }\n if (state.value !== null && age < this.staleTtlMs) {\n state.inFlight ??= this.fetchSource(source).finally(() => {\n state.inFlight = null;\n });\n return state.value;\n }\n if (state.inFlight !== null) return state.inFlight;\n state.inFlight = this.fetchSource(source).finally(() => {\n state.inFlight = null;\n });\n return state.inFlight;\n }\n\n private async fetchSource(source: AvailableModelsSource): Promise<readonly AvailableModel[]> {\n const state = this.getState(source.name);\n try {\n const raw = await source.listModels();\n const value = raw.map((m) => normaliseEntry(m, source));\n state.value = value;\n state.fetchedAt = this.now();\n return value;\n } catch (e: unknown) {\n logger.warn('AvailableModelsCache source probe failed', {\n source: source.name,\n error: e instanceof Error ? e.message : String(e),\n });\n // Keep the stale value if we have one; otherwise empty.\n return state.value ?? [];\n }\n }\n\n private getState(name: string): SourceState {\n const state = this.states.get(name);\n // Constructor seeds an entry for every configured source, and the public\n // surface only routes through `this.sources`, so a missing state would\n // be a programming error — surface it loudly rather than masking it.\n if (state === undefined) {\n throw new Error(`AvailableModelsCache: unknown source \"${name}\"`);\n }\n return state;\n }\n}\n\nfunction normaliseEntry(raw: { id: string }, source: AvailableModelsSource): AvailableModel {\n const slash = raw.id.indexOf('/');\n if (slash > 0 && slash < raw.id.length - 1) {\n return { id: raw.id, source: source.name, provider: raw.id.slice(0, slash) };\n }\n if (source.providerHint !== undefined) {\n return { id: raw.id, source: source.name, provider: source.providerHint };\n }\n return { id: raw.id, source: source.name };\n}\n\n// ============================================================================\n// Default (process-singleton) instance — used by adapter factories that\n// auto-wire `withModelNotFoundFallback`. Operators that need a different\n// cache (different TTLs, custom sources) can override via\n// `setDefaultAvailableModelsCache` at startup.\n// ============================================================================\n\nlet defaultCache: AvailableModelsCache | null = null;\n\n/**\n * Get (or lazily construct) the process-default cache. Starts with no\n * sources — adapter factories register themselves via `addSource` on\n * construction. Until at least one source is added, the cache returns\n * empty snapshots and `withModelNotFoundFallback` degrades to surfacing\n * the original error (safe).\n */\nexport function getDefaultAvailableModelsCache(): AvailableModelsCache {\n defaultCache ??= new AvailableModelsCache({ sources: [] });\n return defaultCache;\n}\n\n/**\n * Override the default cache. Useful for tests and for operators that\n * want a pre-populated cache wired at startup.\n */\nexport function setDefaultAvailableModelsCache(cache: AvailableModelsCache | null): void {\n defaultCache = cache;\n}\n"],"mappings":"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","names":[]}