@wardby/cli 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/.env.example +22 -3
  2. package/README.md +11 -9
  3. package/dist/cli-help.d.ts +1 -1
  4. package/dist/cli-help.js +1 -0
  5. package/dist/cli.js +68 -10
  6. package/dist/coding/base-commit.d.ts +6 -0
  7. package/dist/coding/base-commit.js +12 -0
  8. package/dist/coding/protocol.d.ts +17 -1
  9. package/dist/coding/protocol.js +17 -6
  10. package/dist/coding/provider.d.ts +6 -1
  11. package/dist/coding/provider.js +16 -9
  12. package/dist/config/providers.d.ts +25 -0
  13. package/dist/config/providers.js +71 -0
  14. package/dist/core/attribution.d.ts +101 -0
  15. package/dist/core/attribution.js +208 -0
  16. package/dist/core/budget-groups.d.ts +17 -10
  17. package/dist/core/budget-groups.js +15 -12
  18. package/dist/core/coding-queue.d.ts +3 -0
  19. package/dist/core/coding-queue.js +6 -2
  20. package/dist/core/coding-service-status.d.ts +11 -0
  21. package/dist/core/coding-service-status.js +17 -0
  22. package/dist/core/cost-report.d.ts +88 -0
  23. package/dist/core/cost-report.js +248 -0
  24. package/dist/core/dispatch.d.ts +42 -2
  25. package/dist/core/dispatch.js +144 -26
  26. package/dist/core/engine-native.js +17 -4
  27. package/dist/core/glob.d.ts +10 -0
  28. package/dist/core/glob.js +33 -0
  29. package/dist/core/host-events.d.ts +24 -1
  30. package/dist/core/host-events.js +341 -1
  31. package/dist/core/host-status.d.ts +19 -2
  32. package/dist/core/host-status.js +47 -30
  33. package/dist/core/issue-bridge.d.ts +60 -0
  34. package/dist/core/issue-bridge.js +189 -0
  35. package/dist/core/issue-dedupe.d.ts +70 -0
  36. package/dist/core/issue-dedupe.js +255 -0
  37. package/dist/core/issue-events.d.ts +42 -0
  38. package/dist/core/issue-events.js +155 -0
  39. package/dist/core/issue-status.d.ts +29 -0
  40. package/dist/core/issue-status.js +241 -0
  41. package/dist/core/issue-tracker-tools.d.ts +64 -0
  42. package/dist/core/issue-tracker-tools.js +850 -0
  43. package/dist/core/model-usage.d.ts +10 -0
  44. package/dist/core/model-usage.js +24 -0
  45. package/dist/core/reconciler.d.ts +8 -4
  46. package/dist/core/reconciler.js +15 -4
  47. package/dist/core/review-host-tools.js +10 -3
  48. package/dist/core/run-pricing.d.ts +61 -0
  49. package/dist/core/run-pricing.js +56 -0
  50. package/dist/core/runner.d.ts +5 -2
  51. package/dist/core/runner.js +178 -27
  52. package/dist/core/scheduler.d.ts +4 -1
  53. package/dist/core/scheduler.js +3 -2
  54. package/dist/core/self-defects.d.ts +80 -0
  55. package/dist/core/self-defects.js +180 -0
  56. package/dist/core/tool-names.js +3 -0
  57. package/dist/core/webhooks.d.ts +9 -1
  58. package/dist/core/webhooks.js +19 -1
  59. package/dist/env.js +6 -1
  60. package/dist/generated/prisma/browser.d.ts +66 -0
  61. package/dist/generated/prisma/client.d.ts +66 -0
  62. package/dist/generated/prisma/commonInputTypes.d.ts +122 -52
  63. package/dist/generated/prisma/enums.d.ts +7 -0
  64. package/dist/generated/prisma/enums.js +6 -0
  65. package/dist/generated/prisma/internal/class.d.ts +99 -0
  66. package/dist/generated/prisma/internal/class.js +4 -4
  67. package/dist/generated/prisma/internal/prismaNamespace.d.ts +826 -1
  68. package/dist/generated/prisma/internal/prismaNamespace.js +135 -2
  69. package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +142 -0
  70. package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +135 -2
  71. package/dist/generated/prisma/models/Agent.d.ts +389 -1
  72. package/dist/generated/prisma/models/AgentIssueProject.d.ts +1838 -0
  73. package/dist/generated/prisma/models/AgentIssueProject.js +1 -0
  74. package/dist/generated/prisma/models/AgentRepository.d.ts +1 -1
  75. package/dist/generated/prisma/models/AuthUser.d.ts +1 -1
  76. package/dist/generated/prisma/models/CodingProxySession.d.ts +73 -1
  77. package/dist/generated/prisma/models/CodingRun.d.ts +130 -1
  78. package/dist/generated/prisma/models/CodingRunServiceStatus.d.ts +1404 -0
  79. package/dist/generated/prisma/models/CodingRunServiceStatus.js +1 -0
  80. package/dist/generated/prisma/models/IssueFingerprint.d.ts +1183 -0
  81. package/dist/generated/prisma/models/IssueFingerprint.js +1 -0
  82. package/dist/generated/prisma/models/IssuePullRequest.d.ts +1255 -0
  83. package/dist/generated/prisma/models/IssuePullRequest.js +1 -0
  84. package/dist/generated/prisma/models/ModelCatalogEntry.d.ts +1322 -0
  85. package/dist/generated/prisma/models/ModelCatalogEntry.js +1 -0
  86. package/dist/generated/prisma/models/Run.d.ts +933 -1
  87. package/dist/generated/prisma/models/RunAttribution.d.ts +1259 -0
  88. package/dist/generated/prisma/models/RunAttribution.js +1 -0
  89. package/dist/generated/prisma/models/RunIssueStatus.d.ts +1199 -0
  90. package/dist/generated/prisma/models/RunIssueStatus.js +1 -0
  91. package/dist/generated/prisma/models/RunModelUsage.d.ts +1316 -0
  92. package/dist/generated/prisma/models/RunModelUsage.js +1 -0
  93. package/dist/generated/prisma/models/WorkItem.d.ts +1408 -0
  94. package/dist/generated/prisma/models/WorkItem.js +1 -0
  95. package/dist/generated/prisma/models.d.ts +9 -0
  96. package/dist/help-index.json +355 -16
  97. package/dist/import/neutral-schema.d.ts +16 -16
  98. package/dist/knowledge/check.d.ts +13 -0
  99. package/dist/knowledge/check.js +69 -0
  100. package/dist/knowledge/cli.d.ts +14 -0
  101. package/dist/knowledge/cli.js +67 -0
  102. package/dist/knowledge/concept.d.ts +54 -0
  103. package/dist/knowledge/concept.js +78 -0
  104. package/dist/knowledge/note.d.ts +11 -0
  105. package/dist/knowledge/note.js +39 -0
  106. package/dist/knowledge/relevance.d.ts +11 -0
  107. package/dist/knowledge/relevance.js +14 -0
  108. package/dist/knowledge/span-hash.d.ts +3 -0
  109. package/dist/knowledge/span-hash.js +16 -0
  110. package/dist/mcp/auth/access.d.ts +4 -2
  111. package/dist/mcp/auth/ownership.d.ts +9 -9
  112. package/dist/mcp/auth/resource-server.d.ts +3 -1
  113. package/dist/mcp/auth/resource-server.js +18 -3
  114. package/dist/mcp/auth/self-hosted/credentials.d.ts +3 -3
  115. package/dist/mcp/auth/self-hosted/session.d.ts +5 -5
  116. package/dist/mcp/context.d.ts +3 -0
  117. package/dist/mcp/host-events/deliveries.d.ts +9 -0
  118. package/dist/mcp/host-events/deliveries.js +17 -0
  119. package/dist/mcp/host-events/github-ingress.d.ts +4 -2
  120. package/dist/mcp/host-events/github-ingress.js +4 -13
  121. package/dist/mcp/host-events/jira-ingress.d.ts +29 -0
  122. package/dist/mcp/host-events/jira-ingress.js +92 -0
  123. package/dist/mcp/index.d.ts +2 -0
  124. package/dist/mcp/index.js +87 -9
  125. package/dist/mcp/server.js +5 -2
  126. package/dist/mcp/tools/agents.js +74 -3
  127. package/dist/mcp/tools/cost-report.d.ts +8 -0
  128. package/dist/mcp/tools/cost-report.js +60 -0
  129. package/dist/mcp/tools/issue-projects.d.ts +2 -0
  130. package/dist/mcp/tools/issue-projects.js +238 -0
  131. package/dist/mcp/tools/model-catalog.d.ts +22 -0
  132. package/dist/mcp/tools/model-catalog.js +423 -0
  133. package/dist/mcp/tools/repositories.js +2 -1
  134. package/dist/mcp/tools/tools.d.ts +2 -2
  135. package/dist/mcp/tools/trigger.js +33 -5
  136. package/dist/mcp/transport/streamable-http.d.ts +5 -0
  137. package/dist/mcp/transport/streamable-http.js +23 -1
  138. package/dist/mcp/webhooks/ingress.d.ts +2 -1
  139. package/dist/mcp/webhooks/ingress.js +9 -2
  140. package/dist/providers/auth/self-hosted.d.ts +8 -1
  141. package/dist/providers/auth/self-hosted.js +39 -2
  142. package/dist/providers/coding-proxy/memory-ledger.d.ts +1 -1
  143. package/dist/providers/coding-proxy/memory-ledger.js +10 -1
  144. package/dist/providers/coding-proxy/metering.d.ts +2 -1
  145. package/dist/providers/coding-proxy/metering.js +13 -2
  146. package/dist/providers/coding-proxy/prisma-ledger.js +59 -6
  147. package/dist/providers/coding-proxy/proxy.d.ts +12 -2
  148. package/dist/providers/coding-proxy/proxy.js +92 -30
  149. package/dist/providers/coding-proxy/types.d.ts +18 -1
  150. package/dist/providers/coding-proxy/types.js +12 -1
  151. package/dist/providers/engine/types.d.ts +19 -0
  152. package/dist/providers/executor/composition.js +9 -1
  153. package/dist/providers/executor/container.d.ts +30 -2
  154. package/dist/providers/executor/container.js +98 -17
  155. package/dist/providers/executor/dbos.d.ts +2 -0
  156. package/dist/providers/executor/dbos.js +7 -5
  157. package/dist/providers/executor/routing.d.ts +6 -0
  158. package/dist/providers/executor/routing.js +5 -0
  159. package/dist/providers/executor/types.d.ts +12 -0
  160. package/dist/providers/issue-tracker/adf.d.ts +31 -0
  161. package/dist/providers/issue-tracker/adf.js +181 -0
  162. package/dist/providers/issue-tracker/index.d.ts +5 -0
  163. package/dist/providers/issue-tracker/index.js +12 -0
  164. package/dist/providers/issue-tracker/jira-client.d.ts +41 -0
  165. package/dist/providers/issue-tracker/jira-client.js +151 -0
  166. package/dist/providers/issue-tracker/jira-events.d.ts +3 -0
  167. package/dist/providers/issue-tracker/jira-events.js +98 -0
  168. package/dist/providers/issue-tracker/jira.d.ts +116 -0
  169. package/dist/providers/issue-tracker/jira.js +502 -0
  170. package/dist/providers/issue-tracker/types.d.ts +269 -0
  171. package/dist/providers/issue-tracker/types.js +16 -0
  172. package/dist/providers/jobs/docker.d.ts +5 -1
  173. package/dist/providers/jobs/docker.js +61 -33
  174. package/dist/providers/jobs/kubernetes.d.ts +3 -0
  175. package/dist/providers/jobs/kubernetes.js +44 -4
  176. package/dist/providers/jobs/service-state.d.ts +22 -0
  177. package/dist/providers/jobs/service-state.js +17 -0
  178. package/dist/providers/llm/anthropic.d.ts +3 -3
  179. package/dist/providers/llm/anthropic.js +3 -9
  180. package/dist/providers/llm/bedrock.d.ts +3 -3
  181. package/dist/providers/llm/bedrock.js +3 -9
  182. package/dist/providers/llm/catalog-lookup.d.ts +10 -0
  183. package/dist/providers/llm/catalog-lookup.js +15 -0
  184. package/dist/providers/llm/catalog-shipped.d.ts +18 -0
  185. package/dist/providers/llm/catalog-shipped.js +197 -0
  186. package/dist/providers/llm/catalog-store.d.ts +58 -0
  187. package/dist/providers/llm/catalog-store.js +138 -0
  188. package/dist/providers/llm/catalog-types.d.ts +66 -0
  189. package/dist/providers/llm/catalog-types.js +64 -0
  190. package/dist/providers/llm/catalog.d.ts +61 -0
  191. package/dist/providers/llm/catalog.js +147 -0
  192. package/dist/providers/llm/claude-provider.d.ts +10 -14
  193. package/dist/providers/llm/claude-provider.js +11 -6
  194. package/dist/providers/llm/index.d.ts +9 -6
  195. package/dist/providers/llm/index.js +8 -5
  196. package/dist/providers/llm/openai.d.ts +14 -5
  197. package/dist/providers/llm/openai.js +24 -14
  198. package/dist/providers/llm/pricing-core.d.ts +5 -3
  199. package/dist/providers/llm/registration.js +8 -12
  200. package/dist/providers/llm/routing.d.ts +18 -17
  201. package/dist/providers/llm/routing.js +40 -24
  202. package/dist/providers/review-host/github-events.js +47 -1
  203. package/dist/providers/review-host/github.js +7 -6
  204. package/dist/providers/review-host/types.d.ts +25 -0
  205. package/dist/providers/vcs/git.js +2 -22
  206. package/dist/providers/vcs/github.d.ts +20 -0
  207. package/dist/providers/vcs/github.js +28 -2
  208. package/dist/providers/vcs/types.d.ts +6 -0
  209. package/dist/quickstart/index.d.ts +8 -0
  210. package/dist/quickstart/index.js +34 -34
  211. package/dist/serve.js +8 -2
  212. package/dist/viewer/api-schema.d.ts +2757 -0
  213. package/dist/viewer/api-schema.js +165 -0
  214. package/dist/viewer/build-schemas.d.ts +2 -0
  215. package/dist/viewer/build-schemas.js +18 -0
  216. package/dist/viewer/event-bus.d.ts +38 -0
  217. package/dist/viewer/event-bus.js +232 -0
  218. package/dist/viewer/graph.d.ts +40 -0
  219. package/dist/viewer/graph.js +243 -0
  220. package/dist/viewer/http.d.ts +30 -0
  221. package/dist/viewer/http.js +133 -0
  222. package/dist/viewer/run-detail.d.ts +4 -0
  223. package/dist/viewer/run-detail.js +61 -0
  224. package/dist/wardby-bin.js +5 -0
  225. package/docs/README.md +10 -0
  226. package/docs/agent-recipes.md +383 -0
  227. package/docs/code-review-agents.md +29 -2
  228. package/docs/coding-agent-setup.md +3 -0
  229. package/docs/coding-worker-isolation.md +39 -5
  230. package/docs/getting-started-gke.md +28 -11
  231. package/docs/getting-started-identity-provider.md +49 -38
  232. package/docs/getting-started.md +14 -0
  233. package/docs/jira-agents.md +649 -0
  234. package/docs/knowledge.md +387 -0
  235. package/docs/models.md +221 -0
  236. package/docs/security-deployment.md +19 -9
  237. package/docs/viewer-api.md +142 -0
  238. package/help/admin-viewer.md +39 -0
  239. package/help/agent-recipes.md +173 -0
  240. package/help/architecture-agent.md +189 -0
  241. package/help/builder-agent.md +80 -0
  242. package/help/code-review-agents.md +6 -0
  243. package/help/cost-attribution.md +67 -0
  244. package/help/creating-agents.md +22 -0
  245. package/help/deploy-gke.md +6 -0
  246. package/help/errors/model-unavailable.md +63 -0
  247. package/help/getting-started.md +1 -0
  248. package/help/github.md +18 -0
  249. package/help/identity-and-access.md +8 -3
  250. package/help/jira.md +135 -0
  251. package/help/knowledge.md +47 -0
  252. package/help/models.md +90 -0
  253. package/help/operating-agents.md +7 -1
  254. package/help/troubleshooting/budgets.md +6 -0
  255. package/package.json +5 -2
  256. package/prisma/migrations/20260930000000_jira_issue_projects/migration.sql +34 -0
  257. package/prisma/migrations/20261001000000_jira_phase2_allowlists/migration.sql +3 -0
  258. package/prisma/migrations/20261001010000_jira_link_types_allowlist/migration.sql +2 -0
  259. package/prisma/migrations/20261002000000_jira_coding_bridge/migration.sql +28 -0
  260. package/prisma/migrations/20261002010000_jira_issue_creation/migration.sql +25 -0
  261. package/prisma/migrations/20261003000000_issue_cost_attribution/migration.sql +56 -0
  262. package/prisma/migrations/20261003010000_coding_run_service_status/migration.sql +23 -0
  263. package/prisma/migrations/20261003020000_viewer_notify/migration.sql +54 -0
  264. package/prisma/migrations/20261003030000_viewer_notify_fixes/migration.sql +47 -0
  265. package/prisma/migrations/20261003040000_viewer_indexes/migration.sql +12 -0
  266. package/prisma/migrations/20261004000000_model_catalog/migration.sql +26 -0
  267. package/prisma/schema.prisma +258 -2
  268. package/dist/mcp/tools/models.d.ts +0 -8
  269. package/dist/mcp/tools/models.js +0 -15
  270. package/dist/providers/llm/pricing-anthropic.d.ts +0 -14
  271. package/dist/providers/llm/pricing-anthropic.js +0 -48
  272. package/dist/providers/llm/pricing-bedrock-claude.d.ts +0 -20
  273. package/dist/providers/llm/pricing-bedrock-claude.js +0 -46
  274. package/dist/providers/llm/pricing.d.ts +0 -30
  275. package/dist/providers/llm/pricing.js +0 -74
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The merged model catalog: the shipped catalog (code) overlaid with a
3
+ * deployment's ModelCatalogEntry rows (admins, via set_model). Immutable;
4
+ * CatalogStore swaps in a new one on each refresh. Pure — no I/O.
5
+ *
6
+ * A row replaces the shipped entry with the same (provider, modelId)
7
+ * completely; enabled=false removes the model; a row with no shipped match
8
+ * adds one. Routing is by model id alone, so one model id can belong to only
9
+ * one provider (ownerOf): a row that would claim an id another provider owns
10
+ * is skipped (set_model refuses it at write time; this is the backstop).
11
+ */
12
+ import { type CatalogEntry, type CatalogRow, type ResolvedCatalogEntry } from "./catalog-types.js";
13
+ export declare class ModelCatalog {
14
+ private readonly active;
15
+ private readonly disabled;
16
+ private readonly shipped;
17
+ constructor(active: ReadonlyMap<string, ResolvedCatalogEntry>, disabled: ReadonlyMap<string, ResolvedCatalogEntry>, shipped: ReadonlyMap<string, CatalogEntry>);
18
+ get(modelId: string): ResolvedCatalogEntry | undefined;
19
+ /** Throws ModelUnavailableError (not_in_catalog | disabled). */
20
+ require(modelId: string): ResolvedCatalogEntry;
21
+ entries(): ResolvedCatalogEntry[];
22
+ disabledEntries(): ResolvedCatalogEntry[];
23
+ shippedEntry(modelId: string): CatalogEntry | undefined;
24
+ }
25
+ /** What deciding a model id's owner needs from one of its ModelCatalogEntry rows. */
26
+ export interface CatalogClaim {
27
+ provider: string;
28
+ enabled: boolean;
29
+ createdAt: Date;
30
+ }
31
+ /**
32
+ * Orders claims on one model id: the earliest-created row first, ties broken
33
+ * by provider name. createdAt, never updatedAt: an owner editing its own row
34
+ * (set_model, disable_model) must not hand the id to a row claimed after it.
35
+ */
36
+ export declare function compareClaims(a: CatalogClaim, b: CatalogClaim): number;
37
+ /**
38
+ * The one ownership rule, shared by buildCatalog (the merge) and set_model
39
+ * (its write-time conflict check), so the two can never disagree. A shipped
40
+ * model id ALWAYS belongs to its shipped provider: any row for it under
41
+ * another provider is a dead orphan, however many there are and whenever
42
+ * they were written. A non-shipped id belongs to the provider of its
43
+ * earliest-created row (compareClaims) until reset_model clears every row.
44
+ * `enabled` is the owner's row's (a disabled row still reserves the id); a
45
+ * shipped provider with no row is the untouched, enabled shipped entry.
46
+ * Null only for an id that is neither shipped nor claimed by any row.
47
+ *
48
+ * Which rows count as claims is the caller's call: buildCatalog passes only
49
+ * rows that passed rowFromRecord (CatalogStore drops malformed ones), while
50
+ * set_model passes every raw row, malformed included, so a malformed row
51
+ * still reserves its id until reset_model clears it.
52
+ */
53
+ export declare function ownerOf(shippedEntry: Pick<CatalogEntry, "provider"> | undefined, claims: readonly CatalogClaim[]): {
54
+ provider: string;
55
+ enabled: boolean;
56
+ } | null;
57
+ export declare function buildCatalog(shipped: readonly CatalogEntry[], rows: readonly CatalogRow[], shippedVersion: string, onConflict?: (message: string) => void): ModelCatalog;
58
+ /** The shipped catalog with no overrides (tests, tools without a database, old proxy sessions). */
59
+ export declare function shippedCatalog(): ModelCatalog;
60
+ /** Validates one ModelCatalogEntry record; null (and the caller logs) if it is malformed. */
61
+ export declare function rowFromRecord(record: Record<string, unknown>): CatalogRow | null;
@@ -0,0 +1,147 @@
1
+ /**
2
+ * The merged model catalog: the shipped catalog (code) overlaid with a
3
+ * deployment's ModelCatalogEntry rows (admins, via set_model). Immutable;
4
+ * CatalogStore swaps in a new one on each refresh. Pure — no I/O.
5
+ *
6
+ * A row replaces the shipped entry with the same (provider, modelId)
7
+ * completely; enabled=false removes the model; a row with no shipped match
8
+ * adds one. Routing is by model id alone, so one model id can belong to only
9
+ * one provider (ownerOf): a row that would claim an id another provider owns
10
+ * is skipped (set_model refuses it at write time; this is the backstop).
11
+ */
12
+ import { MODEL_PROVIDERS, ModelUnavailableError, parseStoredEntry, sameEntry, } from "./catalog-types.js";
13
+ import { SHIPPED_CATALOG, SHIPPED_CATALOG_VERSION } from "./catalog-shipped.js";
14
+ export class ModelCatalog {
15
+ active;
16
+ disabled;
17
+ shipped;
18
+ constructor(active, disabled, shipped) {
19
+ this.active = active;
20
+ this.disabled = disabled;
21
+ this.shipped = shipped;
22
+ }
23
+ get(modelId) {
24
+ return this.active.get(modelId);
25
+ }
26
+ /** Throws ModelUnavailableError (not_in_catalog | disabled). */
27
+ require(modelId) {
28
+ const entry = this.active.get(modelId);
29
+ if (entry)
30
+ return entry;
31
+ throw new ModelUnavailableError(modelId, this.disabled.has(modelId) ? "disabled" : "not_in_catalog");
32
+ }
33
+ entries() {
34
+ return [...this.active.values()];
35
+ }
36
+ disabledEntries() {
37
+ return [...this.disabled.values()];
38
+ }
39
+ shippedEntry(modelId) {
40
+ return this.shipped.get(modelId);
41
+ }
42
+ }
43
+ /**
44
+ * Orders claims on one model id: the earliest-created row first, ties broken
45
+ * by provider name. createdAt, never updatedAt: an owner editing its own row
46
+ * (set_model, disable_model) must not hand the id to a row claimed after it.
47
+ */
48
+ export function compareClaims(a, b) {
49
+ return a.createdAt.getTime() - b.createdAt.getTime() || a.provider.localeCompare(b.provider);
50
+ }
51
+ /**
52
+ * The one ownership rule, shared by buildCatalog (the merge) and set_model
53
+ * (its write-time conflict check), so the two can never disagree. A shipped
54
+ * model id ALWAYS belongs to its shipped provider: any row for it under
55
+ * another provider is a dead orphan, however many there are and whenever
56
+ * they were written. A non-shipped id belongs to the provider of its
57
+ * earliest-created row (compareClaims) until reset_model clears every row.
58
+ * `enabled` is the owner's row's (a disabled row still reserves the id); a
59
+ * shipped provider with no row is the untouched, enabled shipped entry.
60
+ * Null only for an id that is neither shipped nor claimed by any row.
61
+ *
62
+ * Which rows count as claims is the caller's call: buildCatalog passes only
63
+ * rows that passed rowFromRecord (CatalogStore drops malformed ones), while
64
+ * set_model passes every raw row, malformed included, so a malformed row
65
+ * still reserves its id until reset_model clears it.
66
+ */
67
+ export function ownerOf(shippedEntry, claims) {
68
+ if (shippedEntry) {
69
+ const ownRow = claims.find((claim) => claim.provider === shippedEntry.provider);
70
+ return { provider: shippedEntry.provider, enabled: ownRow ? ownRow.enabled : true };
71
+ }
72
+ if (claims.length === 0)
73
+ return null;
74
+ const [first] = [...claims].sort(compareClaims);
75
+ return { provider: first.provider, enabled: first.enabled };
76
+ }
77
+ export function buildCatalog(shipped, rows, shippedVersion, onConflict = () => undefined) {
78
+ const shippedById = new Map(shipped.map((e) => [e.modelId, e]));
79
+ const active = new Map(shipped.map((e) => [e.modelId, { ...e, origin: "shipped", priceVersion: `shipped:${shippedVersion}` }]));
80
+ const disabled = new Map();
81
+ const sorted = [...rows].sort(compareClaims);
82
+ const rowsById = new Map();
83
+ for (const row of sorted)
84
+ rowsById.set(row.modelId, [...(rowsById.get(row.modelId) ?? []), row]);
85
+ for (const row of sorted) {
86
+ const shippedEntry = shippedById.get(row.modelId);
87
+ // ownerOf is never null here (the id has at least this row); the fallback only satisfies the type.
88
+ const owner = ownerOf(shippedEntry, rowsById.get(row.modelId) ?? [row])?.provider ?? row.provider;
89
+ if (owner !== row.provider) {
90
+ onConflict(`ModelCatalogEntry (${row.provider}, ${row.modelId}) skipped: model id already served by provider "${owner}".`);
91
+ continue;
92
+ }
93
+ const resolved = {
94
+ provider: row.provider,
95
+ modelId: row.modelId,
96
+ encoding: row.encoding,
97
+ inputPerMTok: row.inputPerMTok,
98
+ outputPerMTok: row.outputPerMTok,
99
+ cachedInputPerMTok: row.cachedInputPerMTok,
100
+ cacheWritePerMTok: row.cacheWritePerMTok,
101
+ efforts: [...row.efforts],
102
+ thinkingMode: row.thinkingMode,
103
+ origin: "override",
104
+ priceVersion: row.updatedAt.toISOString(),
105
+ ...(shippedEntry ? { shippedDiffers: !sameEntry(shippedEntry, row) } : {}),
106
+ sourceUrl: row.sourceUrl,
107
+ updatedBy: row.updatedBy,
108
+ updatedAt: row.updatedAt,
109
+ };
110
+ if (row.enabled) {
111
+ active.set(row.modelId, resolved);
112
+ }
113
+ else {
114
+ active.delete(row.modelId);
115
+ disabled.set(row.modelId, resolved);
116
+ }
117
+ }
118
+ return new ModelCatalog(active, disabled, shippedById);
119
+ }
120
+ let shippedOnly;
121
+ /** The shipped catalog with no overrides (tests, tools without a database, old proxy sessions). */
122
+ export function shippedCatalog() {
123
+ shippedOnly ??= buildCatalog(SHIPPED_CATALOG, [], SHIPPED_CATALOG_VERSION);
124
+ return shippedOnly;
125
+ }
126
+ /** Validates one ModelCatalogEntry record; null (and the caller logs) if it is malformed. */
127
+ export function rowFromRecord(record) {
128
+ const entry = parseStoredEntry(record);
129
+ if (!entry)
130
+ return null;
131
+ if (typeof record.enabled !== "boolean")
132
+ return null;
133
+ if (typeof record.sourceUrl !== "string" || typeof record.updatedBy !== "string")
134
+ return null;
135
+ if (!(record.createdAt instanceof Date) || !(record.updatedAt instanceof Date))
136
+ return null;
137
+ if (!MODEL_PROVIDERS.includes(entry.provider))
138
+ return null;
139
+ return {
140
+ ...entry,
141
+ enabled: record.enabled,
142
+ sourceUrl: record.sourceUrl,
143
+ updatedBy: record.updatedBy,
144
+ createdAt: record.createdAt,
145
+ updatedAt: record.updatedAt,
146
+ };
147
+ }
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * LlmProvider implementation shared by every Claude-protocol adapter (direct
3
3
  * Anthropic API, Bedrock-Claude, and any future Claude-protocol client).
4
- * Imports no SDK — the client and pricing module are injected, so this file
4
+ * Imports no SDK — the client and catalog lookup are injected, so this file
5
5
  * stays exactly what each concrete adapter (anthropic.ts, bedrock.ts) has in
6
6
  * common, and nothing more.
7
7
  */
8
- import type { LlmEffort, LlmMessage, LlmProvider, LlmRequest, LlmStreamEvent, LlmToolDef } from "./types.js";
9
- import type { ModelPricing, UsageTokens } from "./pricing-core.js";
8
+ import type { LlmMessage, LlmRequest, LlmStreamEvent, LlmToolDef } from "./types.js";
9
+ import { type CatalogLookup } from "./catalog-lookup.js";
10
+ import type { CatalogEntry } from "./catalog-types.js";
11
+ import type { CatalogLlmAdapter } from "./routing.js";
10
12
  /**
11
13
  * Structural shape every Claude-protocol SDK client satisfies. Loose on
12
14
  * purpose — params/return are untyped here, mirroring the cast anthropic.ts
@@ -20,17 +22,11 @@ export interface ClaudeMessagesClient {
20
22
  }): unknown;
21
23
  };
22
24
  }
23
- export interface ClaudePricingModule {
24
- /** Must throw on an unknown model — fail-closed, never price at zero. */
25
- getPricing(model: string): ModelPricing;
26
- priceUsd(model: string, usage: UsageTokens): number;
27
- /** Effort levels the model accepts; empty = never send an effort. */
28
- supportedEfforts(model: string): readonly LlmEffort[];
29
- }
30
- export declare class ClaudeLlmProvider implements LlmProvider {
31
- private readonly client;
32
- private readonly pricing;
33
- constructor(client: ClaudeMessagesClient, pricing: ClaudePricingModule);
25
+ export declare class ClaudeLlmProvider implements CatalogLlmAdapter {
26
+ protected readonly client: ClaudeMessagesClient;
27
+ private readonly lookup;
28
+ constructor(client: ClaudeMessagesClient, lookup?: CatalogLookup);
29
+ withEntry(entry: CatalogEntry): ClaudeLlmProvider;
34
30
  stream(req: LlmRequest, signal?: AbortSignal): AsyncIterable<LlmStreamEvent>;
35
31
  countTokens(model: string, messages: LlmMessage[], tools?: LlmToolDef[]): Promise<number>;
36
32
  priceUsd(model: string, usage: {
@@ -1,4 +1,6 @@
1
1
  import { toClaudeRequest, withCacheBreakpoints, mapClaudeStream, estimateClaudeTokens, } from "./claude-messages.js";
2
+ import { computeCost } from "./pricing-core.js";
3
+ import { currentLookup, pinnedLookup } from "./catalog-lookup.js";
2
4
  // Anthropic's own guidance: don't lowball max_tokens — hitting the cap
3
5
  // truncates output mid-thought with no error, silently handing engine-native.ts
4
6
  // (which never sets LlmRequest.maxTokens, and never checks stopReason) a
@@ -11,25 +13,28 @@ const DEFAULT_MAX_TOKENS = 16000;
11
13
  const CLAUDE_TOKEN_INFLATION = 1.2;
12
14
  export class ClaudeLlmProvider {
13
15
  client;
14
- pricing;
15
- constructor(client, pricing) {
16
+ lookup;
17
+ constructor(client, lookup = currentLookup) {
16
18
  this.client = client;
17
- this.pricing = pricing;
19
+ this.lookup = lookup;
20
+ }
21
+ withEntry(entry) {
22
+ return new ClaudeLlmProvider(this.client, pinnedLookup(entry));
18
23
  }
19
24
  async *stream(req, signal) {
20
25
  // Defence in depth behind config-time validation: a level the model
21
26
  // doesn't accept would fail the whole call, so it is dropped here.
22
27
  const { effort, ...rest } = req;
23
- const accepted = effort && this.pricing.supportedEfforts(req.model).includes(effort);
28
+ const accepted = effort && this.lookup(req.model).efforts.includes(effort);
24
29
  const claudeReq = withCacheBreakpoints(toClaudeRequest(accepted ? { ...rest, effort } : rest, DEFAULT_MAX_TOKENS));
25
30
  const raw = this.client.messages.stream({ model: req.model, ...claudeReq }, { signal });
26
31
  yield* mapClaudeStream(raw, (usage) => this.priceUsd(req.model, usage));
27
32
  }
28
33
  async countTokens(model, messages, tools) {
29
- this.pricing.getPricing(model); // fail-closed on unknown model
34
+ this.lookup(model); // fail-closed on a model the catalog does not serve
30
35
  return Math.ceil(estimateClaudeTokens(messages, tools) * CLAUDE_TOKEN_INFLATION);
31
36
  }
32
37
  priceUsd(model, usage) {
33
- return this.pricing.priceUsd(model, usage);
38
+ return computeCost(this.lookup(model), usage);
34
39
  }
35
40
  }
@@ -1,10 +1,13 @@
1
1
  export * from "./types.js";
2
- export * from "./pricing.js";
2
+ export * from "./pricing-core.js";
3
3
  export * from "./openai.js";
4
- export { AnthropicLlmProvider, anthropicCredentialsPresent, anthropicSupportedModels } from "./anthropic.js";
5
- export { BedrockClaudeLlmProvider, bedrockCredentialsPresent, bedrockClaudeSupportedModels } from "./bedrock.js";
6
- export { ClaudeLlmProvider, type ClaudeMessagesClient, type ClaudePricingModule } from "./claude-provider.js";
4
+ export { AnthropicLlmProvider, anthropicCredentialsPresent } from "./anthropic.js";
5
+ export { BedrockClaudeLlmProvider, bedrockCredentialsPresent } from "./bedrock.js";
6
+ export { ClaudeLlmProvider, type ClaudeMessagesClient } from "./claude-provider.js";
7
7
  export { openaiCredentialsPresent } from "./openai.js";
8
- export { supportedModels as openaiSupportedModels } from "./pricing.js";
9
- export { RoutingLlmProvider, modelAcceptsEffort, modelSupportedEfforts, type LlmRegistration } from "./routing.js";
8
+ export { RoutingLlmProvider, modelAcceptsEffort, modelSupportedEfforts, type CatalogLlmAdapter, type LlmRegistration, } from "./routing.js";
10
9
  export { resolveLlmRegistrations, type LlmRegistrationResult } from "./registration.js";
10
+ export * from "./catalog-types.js";
11
+ export { ModelCatalog, buildCatalog, shippedCatalog } from "./catalog.js";
12
+ export { CatalogStore, currentModelCatalog, installModelCatalog, refreshIntervalMs } from "./catalog-store.js";
13
+ export { SHIPPED_CATALOG, SHIPPED_CATALOG_VERSION } from "./catalog-shipped.js";
@@ -1,10 +1,13 @@
1
1
  export * from "./types.js";
2
- export * from "./pricing.js";
2
+ export * from "./pricing-core.js";
3
3
  export * from "./openai.js";
4
- export { AnthropicLlmProvider, anthropicCredentialsPresent, anthropicSupportedModels } from "./anthropic.js";
5
- export { BedrockClaudeLlmProvider, bedrockCredentialsPresent, bedrockClaudeSupportedModels } from "./bedrock.js";
4
+ export { AnthropicLlmProvider, anthropicCredentialsPresent } from "./anthropic.js";
5
+ export { BedrockClaudeLlmProvider, bedrockCredentialsPresent } from "./bedrock.js";
6
6
  export { ClaudeLlmProvider } from "./claude-provider.js";
7
7
  export { openaiCredentialsPresent } from "./openai.js";
8
- export { supportedModels as openaiSupportedModels } from "./pricing.js";
9
- export { RoutingLlmProvider, modelAcceptsEffort, modelSupportedEfforts } from "./routing.js";
8
+ export { RoutingLlmProvider, modelAcceptsEffort, modelSupportedEfforts, } from "./routing.js";
10
9
  export { resolveLlmRegistrations } from "./registration.js";
10
+ export * from "./catalog-types.js";
11
+ export { ModelCatalog, buildCatalog, shippedCatalog } from "./catalog.js";
12
+ export { CatalogStore, currentModelCatalog, installModelCatalog, refreshIntervalMs } from "./catalog-store.js";
13
+ export { SHIPPED_CATALOG, SHIPPED_CATALOG_VERSION } from "./catalog-shipped.js";
@@ -4,14 +4,23 @@
4
4
  * never imports it.
5
5
  */
6
6
  import OpenAI from "openai";
7
- import type { LlmMessage, LlmProvider, LlmRequest, LlmStreamEvent, LlmToolDef } from "./types.js";
7
+ import type { LlmMessage, LlmRequest, LlmStreamEvent, LlmToolDef } from "./types.js";
8
+ import { type CatalogLookup } from "./catalog-lookup.js";
9
+ import type { CatalogEntry } from "./catalog-types.js";
10
+ import type { CatalogLlmAdapter } from "./routing.js";
8
11
  /** Whether the OpenAI adapter has credentials to run (used by the router's enable-by-credential wiring). */
9
12
  export declare function openaiCredentialsPresent(env?: NodeJS.ProcessEnv): boolean;
10
- export declare function estimateTokens(model: string, messages: LlmMessage[], tools?: LlmToolDef[]): number;
11
- export declare class OpenAiLlmProvider implements LlmProvider {
13
+ export declare function estimateTokens(model: string, messages: LlmMessage[], tools?: LlmToolDef[], lookup?: CatalogLookup): number;
14
+ export declare class OpenAiLlmProvider implements CatalogLlmAdapter {
15
+ private readonly lookup;
12
16
  private readonly client;
13
- /** `client` is an injection point for tests — a real adapter never passes it. */
14
- constructor(apiKey?: string, client?: OpenAI);
17
+ /**
18
+ * `client` is an injection point for tests and withEntry — a real adapter
19
+ * never passes it. `lookup` is a parameter property so it is set before the
20
+ * early return below.
21
+ */
22
+ constructor(apiKey?: string, client?: OpenAI, lookup?: CatalogLookup);
23
+ withEntry(entry: CatalogEntry): OpenAiLlmProvider;
15
24
  stream(req: LlmRequest, signal?: AbortSignal): AsyncIterable<LlmStreamEvent>;
16
25
  countTokens(model: string, messages: LlmMessage[], tools?: LlmToolDef[]): Promise<number>;
17
26
  priceUsd(model: string, usage: {
@@ -6,7 +6,8 @@
6
6
  import OpenAI from "openai";
7
7
  import { encode as encodeCl100kBase } from "gpt-tokenizer/encoding/cl100k_base";
8
8
  import { encode as encodeO200kBase } from "gpt-tokenizer/encoding/o200k_base";
9
- import { getModelPricing, priceUsd as priceUsdFromTable } from "./pricing.js";
9
+ import { computeCost } from "./pricing-core.js";
10
+ import { currentLookup, pinnedLookup } from "./catalog-lookup.js";
10
11
  /** Whether the OpenAI adapter has credentials to run (used by the router's enable-by-credential wiring). */
11
12
  export function openaiCredentialsPresent(env = process.env) {
12
13
  return Boolean(env.OPENAI_API_KEY);
@@ -22,10 +23,10 @@ const TOKENS_PRIMING_REPLY = 3;
22
23
  // gpt-tokenizer's package default is cl100k_base, but every gpt-4o-and-later
23
24
  // model (all of Phase 1's roster) actually uses o200k_base — using the
24
25
  // wrong table skews the pre-flight token count, which is the budget
25
- // guardrail's input. The encoding lives on the pricing table (pricing.ts)
26
- // so a model can't be run without also declaring which tokenizer counts it.
27
- function encodeForModel(model, text) {
28
- const { encoding } = getModelPricing(model);
26
+ // guardrail's input. The encoding lives on the catalog entry, so a model
27
+ // can't be run without also declaring which tokenizer counts it.
28
+ function encodeForModel(model, text, lookup) {
29
+ const { encoding } = lookup(model);
29
30
  return encoding === "o200k_base" ? encodeO200kBase(text) : encodeCl100kBase(text);
30
31
  }
31
32
  /** Same shape sent to the API in `stream()` — kept as one function so the estimate can never drift from what's actually serialized. */
@@ -44,25 +45,31 @@ function toOpenAiTools(tools) {
44
45
  // is still an *approximation* (not OpenAI's undocumented exact tool-schema
45
46
  // tokenization), not a byte-for-byte match — calibration logging
46
47
  // (budget.ts's checkTokenCalibration) is what surfaces any remaining drift.
47
- export function estimateTokens(model, messages, tools) {
48
+ export function estimateTokens(model, messages, tools, lookup = currentLookup) {
48
49
  let total = TOKENS_PRIMING_REPLY;
49
50
  for (const message of messages) {
50
51
  total += TOKENS_PER_MESSAGE;
51
- total += encodeForModel(model, message.content).length;
52
- total += encodeForModel(model, message.role).length;
52
+ total += encodeForModel(model, message.content, lookup).length;
53
+ total += encodeForModel(model, message.role, lookup).length;
53
54
  if (message.name) {
54
- total += encodeForModel(model, message.name).length + TOKENS_PER_NAME;
55
+ total += encodeForModel(model, message.name, lookup).length + TOKENS_PER_NAME;
55
56
  }
56
57
  }
57
58
  if (tools && tools.length > 0) {
58
- total += encodeForModel(model, JSON.stringify(toOpenAiTools(tools))).length;
59
+ total += encodeForModel(model, JSON.stringify(toOpenAiTools(tools)), lookup).length;
59
60
  }
60
61
  return total;
61
62
  }
62
63
  export class OpenAiLlmProvider {
64
+ lookup;
63
65
  client;
64
- /** `client` is an injection point for tests — a real adapter never passes it. */
65
- constructor(apiKey = process.env.OPENAI_API_KEY ?? "", client) {
66
+ /**
67
+ * `client` is an injection point for tests and withEntry — a real adapter
68
+ * never passes it. `lookup` is a parameter property so it is set before the
69
+ * early return below.
70
+ */
71
+ constructor(apiKey = process.env.OPENAI_API_KEY ?? "", client, lookup = currentLookup) {
72
+ this.lookup = lookup;
66
73
  if (client) {
67
74
  this.client = client;
68
75
  return;
@@ -72,6 +79,9 @@ export class OpenAiLlmProvider {
72
79
  }
73
80
  this.client = new OpenAI({ apiKey });
74
81
  }
82
+ withEntry(entry) {
83
+ return new OpenAiLlmProvider("", this.client, pinnedLookup(entry));
84
+ }
75
85
  async *stream(req, signal) {
76
86
  const stream = await this.client.chat.completions.create({
77
87
  model: req.model,
@@ -148,9 +158,9 @@ export class OpenAiLlmProvider {
148
158
  }
149
159
  }
150
160
  async countTokens(model, messages, tools) {
151
- return estimateTokens(model, messages, tools);
161
+ return estimateTokens(model, messages, tools, this.lookup);
152
162
  }
153
163
  priceUsd(model, usage) {
154
- return priceUsdFromTable(model, usage);
164
+ return computeCost(this.lookup(model), usage);
155
165
  }
156
166
  }
@@ -1,7 +1,9 @@
1
1
  /**
2
- * Shared pricing types + cost math, provider-agnostic. Each LLM adapter
3
- * owns its own model table but prices through this one `computeCost`, so the
4
- * cache-read/cache-write accounting can never diverge between providers.
2
+ * Shared pricing types + cost math, provider-agnostic. Rates come from the
3
+ * model catalog (docs/models.md: the shipped catalog plus this deployment's
4
+ * set_model overrides), not from a per-adapter table; every provider prices
5
+ * through this one `computeCost`, so the cache-read/cache-write accounting
6
+ * can never diverge between providers.
5
7
  */
6
8
  export type TokenizerEncoding = "cl100k_base" | "o200k_base";
7
9
  export interface ModelPricing {
@@ -10,30 +10,26 @@
10
10
  * OpenAI, direct Anthropic, and Bedrock-Claude can all be active in one
11
11
  * deployment at once. An agent's `model` field (routed by RoutingLlmProvider)
12
12
  * picks which one handles its calls; there is no exclusive global mode.
13
+ * A registration names only its provider — models come from the catalog.
13
14
  */
14
15
  import { OpenAiLlmProvider, openaiCredentialsPresent } from "./openai.js";
15
- import { supportedModels as openaiSupportedModels } from "./pricing.js";
16
- import { AnthropicLlmProvider, anthropicCredentialsPresent, anthropicSupportedModels } from "./anthropic.js";
17
- import { BedrockClaudeLlmProvider, bedrockCredentialsPresent, bedrockClaudeSupportedModels } from "./bedrock.js";
16
+ import { AnthropicLlmProvider, anthropicCredentialsPresent } from "./anthropic.js";
17
+ import { BedrockClaudeLlmProvider, bedrockCredentialsPresent } from "./bedrock.js";
18
18
  export function resolveLlmRegistrations(env = process.env) {
19
19
  const registrations = [];
20
20
  if (openaiCredentialsPresent(env)) {
21
- registrations.push({ provider: new OpenAiLlmProvider(env.OPENAI_API_KEY), models: openaiSupportedModels() });
21
+ registrations.push({ provider: "openai", adapter: new OpenAiLlmProvider(env.OPENAI_API_KEY) });
22
22
  }
23
23
  if (anthropicCredentialsPresent(env)) {
24
- registrations.push({
25
- provider: new AnthropicLlmProvider(env.ANTHROPIC_API_KEY),
26
- models: anthropicSupportedModels(),
27
- });
24
+ registrations.push({ provider: "anthropic", adapter: new AnthropicLlmProvider(env.ANTHROPIC_API_KEY) });
28
25
  }
29
26
  if (bedrockCredentialsPresent(env)) {
30
27
  registrations.push({
31
- provider: new BedrockClaudeLlmProvider(env.BEDROCK_REGION ?? env.AWS_REGION),
32
- models: bedrockClaudeSupportedModels(),
28
+ provider: "bedrock-claude",
29
+ adapter: new BedrockClaudeLlmProvider(env.BEDROCK_REGION ?? env.AWS_REGION),
33
30
  });
34
31
  }
35
- if (registrations.length === 0) {
32
+ if (registrations.length === 0)
36
33
  return { kind: "no-credentials" };
37
- }
38
34
  return { kind: "registrations", registrations };
39
35
  }
@@ -1,27 +1,28 @@
1
- /**
2
- * Model->provider routing. Becomes the single ProviderRegistry.llm and
3
- * dispatches each call to the adapter that owns `model`, so per-agent model
4
- * selection (Agent.model) selects the provider too — GPT and Claude agents
5
- * run in one deployment. Unknown model / duplicate registration fail closed.
6
- */
1
+ import type { ModelCatalog } from "./catalog.js";
2
+ import { type CatalogEntry, type ModelProvider, type ResolvedCatalogEntry } from "./catalog-types.js";
7
3
  import type { LlmEffort, LlmMessage, LlmProvider, LlmRequest, LlmStreamEvent, LlmToolDef } from "./types.js";
8
- /**
9
- * Effort levels `model` accepts, whichever provider owns it — for config-time
10
- * validation, so it needs no credentials. Each provider's own table is the
11
- * source of truth. OpenAI models accept none: the Chat Completions
12
- * `reasoning_effort` parameter is documented for o-series models only, and
13
- * none is on the roster.
14
- */
4
+ /** Effort levels `model` accepts, from the current catalog; no credentials needed (config-time validation). */
15
5
  export declare function modelSupportedEfforts(model: string): readonly LlmEffort[];
16
6
  export declare function modelAcceptsEffort(model: string, effort: LlmEffort): boolean;
7
+ /** An adapter that can be pinned to one run's stored catalog entry. */
8
+ export interface CatalogLlmAdapter extends LlmProvider {
9
+ withEntry(entry: CatalogEntry): LlmProvider;
10
+ }
17
11
  export interface LlmRegistration {
18
- provider: LlmProvider;
19
- models: string[];
12
+ provider: ModelProvider;
13
+ adapter: CatalogLlmAdapter;
20
14
  }
21
15
  export declare class RoutingLlmProvider implements LlmProvider {
22
- private readonly byModel;
23
- constructor(registrations: LlmRegistration[]);
16
+ private readonly catalog;
17
+ private readonly byProvider;
18
+ constructor(registrations: LlmRegistration[], catalog?: () => ModelCatalog);
19
+ hasProvider(provider: ModelProvider): boolean;
20
+ /** Model ids this deployment can run right now. */
24
21
  listModels(): string[];
22
+ /** The current catalog entry for a runnable model; throws ModelUnavailableError otherwise. */
23
+ entryFor(model: string): ResolvedCatalogEntry;
24
+ /** A provider bound to one run's stored entry: prices and shapes requests from it, never the live catalog. */
25
+ forRun(entry: CatalogEntry): LlmProvider;
25
26
  private resolve;
26
27
  stream(req: LlmRequest, signal?: AbortSignal): AsyncIterable<LlmStreamEvent>;
27
28
  countTokens(model: string, messages: LlmMessage[], tools?: LlmToolDef[]): Promise<number>;
@@ -1,42 +1,58 @@
1
- import { anthropicSupportedEfforts } from "./pricing-anthropic.js";
2
- import { bedrockClaudeSupportedEfforts } from "./pricing-bedrock-claude.js";
3
1
  /**
4
- * Effort levels `model` accepts, whichever provider owns it — for config-time
5
- * validation, so it needs no credentials. Each provider's own table is the
6
- * source of truth. OpenAI models accept none: the Chat Completions
7
- * `reasoning_effort` parameter is documented for o-series models only, and
8
- * none is on the roster.
2
+ * Model->provider routing over the model catalog. Becomes the single
3
+ * ProviderRegistry.llm: each call's model is looked up in the current
4
+ * catalog, and the adapter registered for that entry's provider handles it.
5
+ * A model is routable when it is in the catalog AND its provider has
6
+ * credentials (registration.ts). Unknown model / unconfigured provider /
7
+ * duplicate registration all fail closed.
9
8
  */
9
+ import { currentModelCatalog } from "./catalog-store.js";
10
+ import { ModelUnavailableError, } from "./catalog-types.js";
11
+ /** Effort levels `model` accepts, from the current catalog; no credentials needed (config-time validation). */
10
12
  export function modelSupportedEfforts(model) {
11
- const direct = anthropicSupportedEfforts(model);
12
- if (direct.length > 0)
13
- return direct;
14
- return bedrockClaudeSupportedEfforts(model);
13
+ return currentModelCatalog().get(model)?.efforts ?? [];
15
14
  }
16
15
  export function modelAcceptsEffort(model, effort) {
17
16
  return modelSupportedEfforts(model).includes(effort);
18
17
  }
19
18
  export class RoutingLlmProvider {
20
- byModel = new Map();
21
- constructor(registrations) {
19
+ catalog;
20
+ byProvider = new Map();
21
+ constructor(registrations, catalog = currentModelCatalog) {
22
+ this.catalog = catalog;
22
23
  for (const reg of registrations) {
23
- for (const model of reg.models) {
24
- if (this.byModel.has(model)) {
25
- throw new Error(`Model "${model}" is registered by more than one LLM provider — check the routing configuration.`);
26
- }
27
- this.byModel.set(model, reg.provider);
24
+ if (this.byProvider.has(reg.provider)) {
25
+ throw new Error(`LLM provider "${reg.provider}" is registered more than once — check the routing configuration.`);
28
26
  }
27
+ this.byProvider.set(reg.provider, reg.adapter);
29
28
  }
30
29
  }
30
+ hasProvider(provider) {
31
+ return this.byProvider.has(provider);
32
+ }
33
+ /** Model ids this deployment can run right now. */
31
34
  listModels() {
32
- return [...this.byModel.keys()];
35
+ return this.catalog()
36
+ .entries()
37
+ .filter((e) => this.byProvider.has(e.provider))
38
+ .map((e) => e.modelId);
39
+ }
40
+ /** The current catalog entry for a runnable model; throws ModelUnavailableError otherwise. */
41
+ entryFor(model) {
42
+ const entry = this.catalog().require(model);
43
+ if (!this.byProvider.has(entry.provider))
44
+ throw new ModelUnavailableError(model, "provider_not_configured");
45
+ return entry;
46
+ }
47
+ /** A provider bound to one run's stored entry: prices and shapes requests from it, never the live catalog. */
48
+ forRun(entry) {
49
+ const adapter = this.byProvider.get(entry.provider);
50
+ if (!adapter)
51
+ throw new ModelUnavailableError(entry.modelId, "provider_not_configured");
52
+ return adapter.withEntry(entry);
33
53
  }
34
54
  resolve(model) {
35
- const provider = this.byModel.get(model);
36
- if (!provider) {
37
- throw new Error(`No LLM provider is registered for model "${model}". Known models: ${[...this.byModel.keys()].join(", ") || "(none)"}.`);
38
- }
39
- return provider;
55
+ return this.byProvider.get(this.entryFor(model).provider);
40
56
  }
41
57
  stream(req, signal) {
42
58
  return this.resolve(req.model).stream(req, signal);