@desplega.ai/agent-swarm 1.158.0 → 1.159.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.
- package/dist/{acp-adapter-xaq4n6pc.js → acp-adapter-qhch0n2v.js} +1 -1
- package/dist/{actions-p9hz5yat.js → actions-dtsh1mfb.js} +33 -29
- package/dist/{anthropic-messages-51pz26qe.js → anthropic-messages-f2h06k64.js} +8 -3
- package/dist/{app-g26wdcz6.js → app-y1b9srva.js} +10 -9
- package/dist/{assistant-wydfqkb3.js → assistant-31hwy5xj.js} +34 -30
- package/dist/{azure-openai-responses-8gksqtmz.js → azure-openai-responses-0k1mfax5.js} +12 -13
- package/dist/{boot-reembed-se8zmdcp.js → boot-reembed-48d2qpkt.js} +11 -11
- package/dist/{boot-reembed-g5ckg9yb.js → boot-reembed-w0bryhkr.js} +12 -12
- package/dist/{boot-scrub-logs-wfxqan61.js → boot-scrub-logs-t65wbrn7.js} +9 -8
- package/dist/{boot-seeds-s3q01xj3.js → boot-seeds-h7c32hvm.js} +12 -11
- package/dist/{claude-adapter-tv2x4f36.js → claude-adapter-1t6611ne.js} +26 -21
- package/dist/{claude-managed-adapter-41b2s7fd.js → claude-managed-adapter-werp659j.js} +4 -2
- package/dist/{claude-managed-setup-60g8aazx.js → claude-managed-setup-y6rdgbc1.js} +11 -2
- package/dist/claude-sdk-session-zhkyk8yw.js +29319 -0
- package/dist/{cli-vj6h9kc2.js → cli-0m7wct0d.js} +59 -1
- package/dist/{cli-wscdvcqz.js → cli-0n2e4x49.js} +1 -1
- package/dist/cli-0xqde53b.js +176 -0
- package/dist/{cli-xberxa1r.js → cli-10ym38qc.js} +2 -2
- package/dist/{cli-5b6b62tz.js → cli-13hw4z91.js} +1 -1
- package/dist/{cli-rqz8qbw7.js → cli-20035trh.js} +50 -18
- package/dist/{cli-k0g1sjzm.js → cli-31nkw3g3.js} +3 -3
- package/dist/cli-3gvrr8vj.js +1422 -0
- package/dist/{cli-c07r9a2m.js → cli-48527fpw.js} +9 -8
- package/dist/{cli-24k806sc.js → cli-4aj25szq.js} +2 -2
- package/dist/{cli-7gtvvpbh.js → cli-4tvgz99m.js} +7 -7
- package/dist/cli-503ya656.js +455 -0
- package/dist/{cli-cw9zck8j.js → cli-5f2hq128.js} +8 -4
- package/dist/{cli-s8eet254.js → cli-68bmme5p.js} +11078 -27833
- package/dist/{cli-ftfvggzx.js → cli-79aq9cgb.js} +794 -563
- package/dist/cli-7cxw8b60.js +14910 -0
- package/dist/cli-7kyk9jr3.js +22 -0
- package/dist/cli-7zttzetj.js +324 -0
- package/dist/{cli-y95wp3tg.js → cli-96qd5psa.js} +1 -1
- package/dist/{cli-g8djp6qe.js → cli-9yg65c1j.js} +22 -0
- package/dist/{cli-3gmcwfyx.js → cli-ardpzhjm.js} +5 -5
- package/dist/{cli-7s3syk22.js → cli-b6thtzac.js} +5 -5
- package/dist/{cli-00w2c66p.js → cli-bgv70kym.js} +13 -2
- package/dist/{cli-j55n64b6.js → cli-bv0a38v9.js} +163 -44
- package/dist/cli-ca2w7zv7.js +115 -0
- package/dist/cli-cpvsaqsw.js +333 -0
- package/dist/cli-cygyqj3b.js +754 -0
- package/dist/{cli-x24pshtq.js → cli-dvjpkkjd.js} +6 -6
- package/dist/{cli-5d10vd2m.js → cli-e0ndxhtk.js} +4 -4
- package/dist/{cli-4198qc9v.js → cli-e4xnc0qp.js} +15 -3
- package/dist/{cli-rjehvrts.js → cli-eg7cdg68.js} +8 -8
- package/dist/cli-ejpygnqz.js +3815 -0
- package/dist/{cli-fgqxwrpv.js → cli-fjtp3463.js} +1631 -127
- package/dist/{cli-zt1227k1.js → cli-fkqhbg8g.js} +5 -3
- package/dist/{cli-47m6dkd3.js → cli-g9df2m37.js} +409 -168
- package/dist/cli-gzyh0j0g.js +8455 -0
- package/dist/cli-hgqfttfc.js +183 -0
- package/dist/cli-hj65wsts.js +17755 -0
- package/dist/cli-j9z8mfx3.js +476 -0
- package/dist/{cli-aehm1rbq.js → cli-kc3dp84d.js} +1135 -61
- package/dist/{cli-h38x0qhg.js → cli-kdnehe0t.js} +4 -3
- package/dist/{cli-20cj37vt.js → cli-m4excttz.js} +136 -2216
- package/dist/{cli-8qmme6vd.js → cli-mg24hkt4.js} +2 -2
- package/dist/{cli-xhaeg2nc.js → cli-mzkqpxee.js} +1 -1
- package/dist/{cli-q67y4wzm.js → cli-phsaj4xq.js} +24 -2
- package/dist/{cli-pw9rj5t9.js → cli-pqetb83f.js} +1 -176
- package/dist/{cli-rj19hc34.js → cli-qjxy52w7.js} +1 -1
- package/dist/{cli-53sz8gkz.js → cli-qv4jv8bx.js} +1 -1
- package/dist/cli-qvnnx4ct.js +1051 -0
- package/dist/cli-rb9m6rpe.js +4980 -0
- package/dist/{cli-tt9k8r35.js → cli-rfezr056.js} +476 -909
- package/dist/{cli-ggm92wcn.js → cli-rgjprct3.js} +1 -0
- package/dist/{cli-9httdpyq.js → cli-rrqmqr1s.js} +1 -1
- package/dist/cli-rykfmgwg.js +102 -0
- package/dist/{cli-eetrvv91.js → cli-t3p77qk1.js} +1 -1
- package/dist/{cli-68w09sjw.js → cli-vn0qv9cj.js} +25 -88
- package/dist/{cli-987hg0aa.js → cli-vwbbaecp.js} +2 -2
- package/dist/cli-wd9hz40d.js +69 -0
- package/dist/{cli-j57qkrpq.js → cli-wgjx0q33.js} +13 -13
- package/dist/cli-xgpf00sk.js +2149 -0
- package/dist/{cli-mcnhgnn1.js → cli-ykk5jaax.js} +184 -26
- package/dist/{cli-aky8tctm.js → cli-z9x0sh1t.js} +3 -3
- package/dist/{cli-1hkrch3k.js → cli-zw65mab6.js} +5 -5
- package/dist/cli.js +38 -29
- package/dist/cloudflare-workers-ai-system-one-1sg586qv.js +46 -0
- package/dist/{codex-adapter-cnm8wk11.js → codex-adapter-jqfqqa21.js} +21 -14
- package/dist/{codex-hook-9hptmdhj.js → codex-hook-5eys3dya.js} +2 -2
- package/dist/{codex-session-runner-gfg547zj.js → codex-session-runner-zd341dsm.js} +21 -14
- package/dist/{commands-e3bn0p75.js → commands-vfsysz4s.js} +9 -8
- package/dist/{db-03v9kzr2.js → db-s3znhvbn.js} +15 -10
- package/dist/{dispatcher-nrmg8yht.js → dispatcher-5xn0c7rf.js} +32 -28
- package/dist/{dsh-adapter-k03nfqa6.js → dsh-adapter-894mgfvs.js} +3 -3
- package/dist/{e2b-93x1k0j5.js → e2b-ez07znam.js} +4 -3
- package/dist/execute-71ysgv35.js +965 -0
- package/dist/{google-generative-ai-bxmv50nc.js → google-generative-ai-1nfcdphp.js} +5 -4
- package/dist/{google-vertex-809af514.js → google-vertex-1tqcd7b1.js} +5 -4
- package/dist/{handlers-aqcedbyb.js → handlers-5w4jwycr.js} +35 -31
- package/dist/{hook-7vkskkre.js → hook-853bxrr6.js} +16 -13
- package/dist/{hook-ybvmrmhq.js → hook-z0hbxj85.js} +16 -13
- package/dist/{http-2eqa8t24.js → http-akc3wy3m.js} +600 -123
- package/dist/{index-f6cryxwe.js → index-4a5njnm1.js} +2 -59
- package/dist/{index-a3vn96n8.js → index-5sz16ksh.js} +187 -47
- package/dist/{index-c6zsv1bd.js → index-bjmjh9h4.js} +11 -11
- package/dist/{index-chgdkq36.js → index-cd151qkq.js} +32 -28
- package/dist/{index-x71bvq0p.js → index-k661dzzr.js} +33 -29
- package/dist/{index-1yks3qcr.js → index-ttpbhnjg.js} +32 -28
- package/dist/{keepalive-vtkf5n99.js → keepalive-8p9rpvz5.js} +11 -10
- package/dist/{lead-h3sp357x.js → lead-jp69z5h3.js} +49 -45
- package/dist/llama-cpp-classify-t2tccams.js +335 -0
- package/dist/{maintenance-jzeqtk3q.js → maintenance-fwx4ev0k.js} +12 -12
- package/dist/{mistral-conversations-zx0kn554.js → mistral-conversations-bhvwqcnk.js} +14 -17
- package/dist/{oauth-refresh-sweep-qp3dzmdp.js → oauth-refresh-sweep-cwcq1ctx.js} +10 -9
- package/dist/{onboard-tmsp0qbz.js → onboard-2b4q0t84.js} +6 -5
- package/dist/{openai-codex-responses-4vjqzvjp.js → openai-codex-responses-nekthgks.js} +21 -8
- package/dist/{openai-completions-9hjrqbw8.js → openai-completions-hxtzwj0s.js} +8 -11
- package/dist/{openai-responses-7b1pn0xt.js → openai-responses-w4r118w7.js} +22 -17
- package/dist/{opencode-adapter-x6x2da1z.js → opencode-adapter-zz7xvjj6.js} +6 -4
- package/dist/{openrouter-images-9f9ek4rw.js → openrouter-images-b6sqqjgn.js} +4 -6
- package/dist/{otel-impl-9mz73q65.js → otel-impl-g8rs4vwr.js} +1 -1
- package/dist/{pi-messages-txpjj39q.js → pi-messages-vxdt8v7j.js} +2 -1
- package/dist/{pi-mono-adapter-m6dvv0dz.js → pi-mono-adapter-n0me28wt.js} +29 -17
- package/dist/pricing-refresh-0v77385v.js +39 -0
- package/dist/{rbac-roles-c9wqz83x.js → rbac-roles-krjyazbj.js} +10 -9
- package/dist/{render-v2-gryrh9h8.js → render-v2-3wjsp9aw.js} +10 -9
- package/dist/runtime-ktjysacx.js +24 -0
- package/dist/{setup-q9720bt6.js → setup-yeqryke6.js} +6 -5
- package/dist/typesafe-system-one-96960jxp.js +27 -0
- package/dist/{virtual-modules-3htpgn8x.js → virtual-modules-dzdeq1sf.js} +21 -13
- package/dist/{work-objects-b76x7deb.js → work-objects-3pakpkw0.js} +35 -31
- package/dist/{worker-6vrxbv41.js → worker-85testgj.js} +49 -45
- package/openapi.json +863 -14
- package/package.json +9 -8
- package/src/be/db/agents.ts +20 -0
- package/src/be/db/tasks/human-free.ts +442 -0
- package/src/be/db/tasks/read.ts +56 -1
- package/src/be/db/tasks/write.ts +107 -11
- package/src/be/db.ts +89 -165
- package/src/be/harness-model-support.ts +182 -0
- package/src/be/human-free-drain.ts +67 -0
- package/src/be/migrations/182_task_human_free_flag.sql +63 -0
- package/src/be/migrations/183_human_free_reclassify_queue.sql +25 -0
- package/src/be/migrations/184_model_catalog.sql +120 -0
- package/src/be/model-catalog-store.ts +644 -0
- package/src/be/model-tier-keys.ts +31 -0
- package/src/be/model-tier-resolution.ts +489 -0
- package/src/be/model-validation.ts +112 -0
- package/src/be/models-catalog.ts +61 -7
- package/src/be/modelsdev-cache.json +82 -0
- package/src/be/modelsdev-cache.ts +2 -0
- package/src/be/pricing-refresh.ts +154 -12
- package/src/be/scripts/typecheck.ts +15 -0
- package/src/be/seed-scripts/catalog/task-failure-audit.ts +61 -9
- package/src/be/swarm-config-guard.ts +6 -0
- package/src/cli.tsx +10 -0
- package/src/commands/claude-managed-setup.ts +3 -1
- package/src/commands/runner.ts +76 -2
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/heartbeat/heartbeat.ts +64 -15
- package/src/heartbeat/templates.ts +2 -2
- package/src/http/agents.ts +49 -1
- package/src/http/index.ts +8 -0
- package/src/http/models-catalog.ts +357 -8
- package/src/http/poll.ts +45 -2
- package/src/http/schedules.ts +36 -6
- package/src/http/session-data.ts +2 -5
- package/src/http/stats.ts +6 -8
- package/src/http/tasks.ts +26 -1
- package/src/http/usage-cache.ts +56 -15
- package/src/http/workflows.ts +35 -8
- package/src/prompts/session-templates.ts +1 -0
- package/src/providers/claude-managed-models.ts +47 -161
- package/src/providers/codex-adapter.ts +4 -4
- package/src/providers/codex-models.ts +65 -198
- package/src/providers/reasoning-effort.ts +44 -140
- package/src/rbac/legacy-policy.ts +25 -0
- package/src/rbac/permissions.ts +10 -0
- package/src/scripts-runtime/sdk-allowlist.ts +4 -0
- package/src/scripts-runtime/types/stdlib.d.ts +15 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +3 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +15 -0
- package/src/server.ts +6 -0
- package/src/slack/message-text.ts +56 -2
- package/src/slack/thread-buffer.ts +10 -2
- package/src/tests/agents-list-model-display.test.ts +4 -6
- package/src/tests/base-prompt.test.ts +3 -0
- package/src/tests/claude-adapter.test.ts +10 -0
- package/src/tests/claude-managed-adapter.test.ts +12 -18
- package/src/tests/codex-adapter.test.ts +2 -1
- package/src/tests/codex-gpt6-release.test.ts +66 -5
- package/src/tests/context-window.test.ts +7 -5
- package/src/tests/db-extraction-contract.test.ts +1 -0
- package/src/tests/db-task-read-contract.test.ts +5 -1
- package/src/tests/db-task-write-contract.test.ts +4 -1
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/explicit-model-validation.test.ts +332 -0
- package/src/tests/harness-model-support.test.ts +374 -0
- package/src/tests/heartbeat-checklist-config.test.ts +71 -0
- package/src/tests/heartbeat-checklist.test.ts +4 -2
- package/src/tests/heartbeat-orphan-resume.test.ts +48 -0
- package/src/tests/human-free-flag.test.ts +279 -0
- package/src/tests/human-free-oracle.ts +57 -0
- package/src/tests/human-free-reclassify.test.ts +669 -0
- package/src/tests/migration-046-budgets.test.ts +3 -3
- package/src/tests/model-catalog-migration.test.ts +134 -0
- package/src/tests/model-catalog-table.test.ts +249 -0
- package/src/tests/model-catalog-write-access.test.ts +387 -0
- package/src/tests/model-tier-resolution.test.ts +519 -0
- package/src/tests/models-catalog.test.ts +1 -1
- package/src/tests/multi-runtime-enabled-stats.test.ts +65 -1
- package/src/tests/package-publish.test.ts +137 -36
- package/src/tests/pricing-routes.test.ts +2 -2
- package/src/tests/rbac-engine.test.ts +42 -2
- package/src/tests/reasoning-effort.test.ts +109 -0
- package/src/tests/refresh-modelsdev-carryforward.test.ts +5 -5
- package/src/tests/runtime-model-catalog.test.ts +86 -0
- package/src/tests/script-connections.test.ts +5 -0
- package/src/tests/seed-scripts.test.ts +89 -0
- package/src/tests/session-costs.test.ts +14 -11
- package/src/tests/slack-message-text.test.ts +101 -0
- package/src/tests/slack-thread-buffer-context.test.ts +93 -0
- package/src/tests/task-cascade-fail.test.ts +148 -2
- package/src/tests/tool-annotations.test.ts +1 -1
- package/src/tests/usage-cache.test.ts +166 -0
- package/src/tests/workflow-executors.test.ts +5 -2
- package/src/tests/workflow-system-one-decision-review.test.ts +1021 -0
- package/src/tests/workflow-system-one-decision-save-warnings.test.ts +365 -0
- package/src/tests/workflow-system-one-decision.test.ts +2063 -0
- package/src/tools/model-catalog/index.ts +137 -0
- package/src/tools/schedules/create-schedule.ts +16 -1
- package/src/tools/schedules/patch-schedule.ts +17 -0
- package/src/tools/schedules/update-schedule.ts +17 -0
- package/src/tools/send-task.ts +14 -0
- package/src/tools/task-action.ts +12 -0
- package/src/tools/tool-config.ts +3 -1
- package/src/tools/workflows/create-workflow.ts +9 -2
- package/src/tools/workflows/patch-workflow-node.ts +17 -7
- package/src/tools/workflows/patch-workflow.ts +9 -2
- package/src/tools/workflows/update-workflow.ts +9 -2
- package/src/types.ts +30 -1
- package/src/utils/context-window.ts +47 -53
- package/src/utils/harness-cli-version.ts +115 -0
- package/src/utils/harness-model-error.ts +33 -0
- package/src/utils/runtime-model-catalog.ts +137 -0
- package/src/workflows/approval-resolution.ts +39 -0
- package/src/workflows/definition.ts +7 -0
- package/src/workflows/engine.ts +69 -1
- package/src/workflows/executors/agent-task.ts +9 -0
- package/src/workflows/executors/base.ts +53 -0
- package/src/workflows/executors/human-in-the-loop.ts +8 -8
- package/src/workflows/executors/index.ts +1 -0
- package/src/workflows/executors/registry.ts +2 -0
- package/src/workflows/executors/system-one-decision.ts +1232 -0
- package/src/workflows/executors/system-one-providers.ts +301 -0
- package/src/workflows/executors/workflow-llm.ts +13 -1
- package/src/workflows/readiness.ts +86 -0
- package/src/workflows/recovery.ts +12 -12
- package/src/workflows/resume.ts +23 -9
- package/templates/schedules/daily-blocker-digest/content.md +4 -5
- package/templates/skills/agent-browser/SKILL.md +6 -5
- package/templates/skills/agent-browser/content.md +6 -5
- package/templates/skills/heartbeat-runbook/config.json +1 -1
- package/templates/skills/heartbeat-runbook/content.md +2 -2
- package/templates/skills/swarm-scripts/SKILL.md +1 -1
- package/templates/skills/swarm-scripts/content.md +1 -1
- package/templates/skills/workflow-iterate/content.md +81 -1
- package/templates/workflows/claude-code-changelog-watch/content.md +2 -0
- package/templates/workflows/pr-review-status-sweep/content.md +2 -0
- package/dist/claude-sdk-session-t442qz6f.js +0 -39108
- package/dist/cli-0xjkj1j7.js +0 -61
- package/dist/cli-65hjmev5.js +0 -1141
- package/dist/cli-aw1jwp1r.js +0 -14075
- package/dist/cli-h967nb6r.js +0 -6603
- package/dist/cli-vapyk8sw.js +0 -20
- package/dist/cli-ws6rqycm.js +0 -247
- package/dist/pricing-refresh-v1cbr9te.js +0 -27
- package/src/providers/modelsdev-reasoning.json +0 -6453
- package/dist/{codex-login-x3jk4etn.js → codex-login-he9vrn90.js} +3 -3
- package/dist/{index-f24115ge.js → index-2wsdsnmm.js} +3 -3
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent model catalog (migration 184).
|
|
3
|
+
*
|
|
4
|
+
* - `model_catalog`: models.dev rows for CATALOG_PROVIDER_IDS, rewritten on
|
|
5
|
+
* every full fetch by `src/be/pricing-refresh.ts`.
|
|
6
|
+
* - `model_catalog_overlay`: hand-verified facts (overlay wins per non-null
|
|
7
|
+
* field; overlay-only models are served too). Rows flagged
|
|
8
|
+
* `expiresWhenUpstreamMatches` are deleted once upstream agrees.
|
|
9
|
+
* - `model_catalog_meta`: single-row ETag / Last-Modified / fetch times.
|
|
10
|
+
*
|
|
11
|
+
* The in-memory projection in `src/be/models-catalog.ts` is rebuilt from here
|
|
12
|
+
* by `reloadModelsCatalog()` and invalidated on every refresh / overlay write.
|
|
13
|
+
* See runbooks/model-catalog.md.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { type RuntimeCatalog, setRuntimeModelCatalog } from "../utils/runtime-model-catalog";
|
|
17
|
+
import {
|
|
18
|
+
getActivePricingRow,
|
|
19
|
+
getDbClient,
|
|
20
|
+
type InsertPricingRowInput,
|
|
21
|
+
insertPricingRow,
|
|
22
|
+
} from "./db";
|
|
23
|
+
import {
|
|
24
|
+
CATALOG_PROVIDER_IDS,
|
|
25
|
+
type CatalogModel,
|
|
26
|
+
type CatalogProviderId,
|
|
27
|
+
type CatalogReasoningOption,
|
|
28
|
+
getModelsCatalog,
|
|
29
|
+
invalidateModelsCatalog,
|
|
30
|
+
type ModelsCatalog,
|
|
31
|
+
type ModelsCatalogResult,
|
|
32
|
+
setLiveModelsCatalog,
|
|
33
|
+
setSnapshotOverlayCatalog,
|
|
34
|
+
snapshotModelsCatalog,
|
|
35
|
+
} from "./models-catalog";
|
|
36
|
+
import type { ModelsDevCache } from "./modelsdev-cache";
|
|
37
|
+
import { buildModelsDevSeedRows } from "./seed-pricing";
|
|
38
|
+
|
|
39
|
+
export interface ModelCatalogPricing {
|
|
40
|
+
input?: number;
|
|
41
|
+
output?: number;
|
|
42
|
+
cache_read?: number;
|
|
43
|
+
cache_write?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Fact columns shared by the upstream table and the overlay. */
|
|
47
|
+
export interface ModelCatalogFacts {
|
|
48
|
+
name?: string | null;
|
|
49
|
+
family?: string | null;
|
|
50
|
+
releaseDate?: string | null;
|
|
51
|
+
contextWindow?: number | null;
|
|
52
|
+
maxOutput?: number | null;
|
|
53
|
+
reasoning?: boolean | null;
|
|
54
|
+
reasoningOptions?: CatalogReasoningOption[] | null;
|
|
55
|
+
pricing?: ModelCatalogPricing | null;
|
|
56
|
+
status?: string | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ModelCatalogEntry extends ModelCatalogFacts {
|
|
60
|
+
provider: string;
|
|
61
|
+
modelId: string;
|
|
62
|
+
providerName?: string | null;
|
|
63
|
+
checkedAt: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ModelCatalogOverlayInput extends ModelCatalogFacts {
|
|
67
|
+
provider: string;
|
|
68
|
+
modelId: string;
|
|
69
|
+
reason: string;
|
|
70
|
+
verifiedBy?: string | null;
|
|
71
|
+
expiresWhenUpstreamMatches?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ModelCatalogOverlayEntry extends ModelCatalogOverlayInput {
|
|
75
|
+
expiresWhenUpstreamMatches: boolean;
|
|
76
|
+
createdAt: number;
|
|
77
|
+
updatedAt: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ModelCatalogMeta {
|
|
81
|
+
etag: string | null;
|
|
82
|
+
lastModified: string | null;
|
|
83
|
+
lastFetchAt: number | null;
|
|
84
|
+
lastCheckedAt: number | null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface FactRow {
|
|
88
|
+
provider: string;
|
|
89
|
+
modelId: string;
|
|
90
|
+
name: string | null;
|
|
91
|
+
family: string | null;
|
|
92
|
+
releaseDate: string | null;
|
|
93
|
+
contextWindow: number | null;
|
|
94
|
+
maxOutput: number | null;
|
|
95
|
+
reasoning: number | null;
|
|
96
|
+
reasoningOptions: string | null;
|
|
97
|
+
pricing: string | null;
|
|
98
|
+
status: string | null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface CatalogRow extends FactRow {
|
|
102
|
+
providerName: string | null;
|
|
103
|
+
checkedAt: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface OverlayRow extends FactRow {
|
|
107
|
+
reason: string;
|
|
108
|
+
verifiedBy: string | null;
|
|
109
|
+
expiresWhenUpstreamMatches: number;
|
|
110
|
+
createdAt: number;
|
|
111
|
+
updatedAt: number;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const FACT_KEYS = [
|
|
115
|
+
"name",
|
|
116
|
+
"family",
|
|
117
|
+
"releaseDate",
|
|
118
|
+
"contextWindow",
|
|
119
|
+
"maxOutput",
|
|
120
|
+
"reasoning",
|
|
121
|
+
"reasoningOptions",
|
|
122
|
+
"pricing",
|
|
123
|
+
"status",
|
|
124
|
+
] as const;
|
|
125
|
+
|
|
126
|
+
function parseJson<T>(value: string | null): T | null {
|
|
127
|
+
if (value === null) return null;
|
|
128
|
+
try {
|
|
129
|
+
return JSON.parse(value) as T;
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function rowToFacts(row: FactRow): ModelCatalogFacts {
|
|
136
|
+
return {
|
|
137
|
+
name: row.name,
|
|
138
|
+
family: row.family,
|
|
139
|
+
releaseDate: row.releaseDate,
|
|
140
|
+
contextWindow: row.contextWindow,
|
|
141
|
+
maxOutput: row.maxOutput,
|
|
142
|
+
reasoning: row.reasoning === null ? null : row.reasoning === 1,
|
|
143
|
+
reasoningOptions: parseJson<CatalogReasoningOption[]>(row.reasoningOptions),
|
|
144
|
+
pricing: parseJson<ModelCatalogPricing>(row.pricing),
|
|
145
|
+
status: row.status,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function factParams(facts: ModelCatalogFacts): (string | number | null)[] {
|
|
150
|
+
return [
|
|
151
|
+
facts.name ?? null,
|
|
152
|
+
facts.family ?? null,
|
|
153
|
+
facts.releaseDate ?? null,
|
|
154
|
+
facts.contextWindow ?? null,
|
|
155
|
+
facts.maxOutput ?? null,
|
|
156
|
+
facts.reasoning === undefined || facts.reasoning === null ? null : facts.reasoning ? 1 : 0,
|
|
157
|
+
facts.reasoningOptions ? JSON.stringify(facts.reasoningOptions) : null,
|
|
158
|
+
facts.pricing ? JSON.stringify(facts.pricing) : null,
|
|
159
|
+
facts.status ?? null,
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ─── models.dev projection ───────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
/** Loose view of a raw models.dev model — fields beyond the typed cache subset. */
|
|
166
|
+
interface RawModelsDevModel {
|
|
167
|
+
id?: string;
|
|
168
|
+
name?: string;
|
|
169
|
+
family?: string;
|
|
170
|
+
release_date?: string;
|
|
171
|
+
status?: string;
|
|
172
|
+
reasoning?: boolean;
|
|
173
|
+
reasoning_options?: { type?: unknown; values?: unknown }[];
|
|
174
|
+
limit?: { context?: number; output?: number };
|
|
175
|
+
cost?: { input?: number; output?: number; cache_read?: number; cache_write?: number };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function pickNumber(value: unknown): number | undefined {
|
|
179
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Project a models.dev payload into `model_catalog` rows for CATALOG_PROVIDER_IDS. */
|
|
183
|
+
export function buildCatalogEntries(cache: ModelsDevCache, checkedAt: number): ModelCatalogEntry[] {
|
|
184
|
+
const entries: ModelCatalogEntry[] = [];
|
|
185
|
+
for (const providerId of CATALOG_PROVIDER_IDS) {
|
|
186
|
+
const provider = cache[providerId];
|
|
187
|
+
if (!provider?.models) continue;
|
|
188
|
+
for (const [modelKey, rawModel] of Object.entries(provider.models)) {
|
|
189
|
+
const model = rawModel as RawModelsDevModel;
|
|
190
|
+
const reasoningOptions = Array.isArray(model.reasoning_options)
|
|
191
|
+
? model.reasoning_options
|
|
192
|
+
.filter((o): o is { type: string; values?: unknown } => typeof o?.type === "string")
|
|
193
|
+
.map((o) => ({
|
|
194
|
+
type: o.type,
|
|
195
|
+
...(Array.isArray(o.values) ? { values: o.values as string[] } : {}),
|
|
196
|
+
}))
|
|
197
|
+
: [];
|
|
198
|
+
const pricing: ModelCatalogPricing = {};
|
|
199
|
+
for (const key of ["input", "output", "cache_read", "cache_write"] as const) {
|
|
200
|
+
const value = pickNumber(model.cost?.[key]);
|
|
201
|
+
if (value !== undefined) pricing[key] = value;
|
|
202
|
+
}
|
|
203
|
+
entries.push({
|
|
204
|
+
provider: providerId,
|
|
205
|
+
modelId: modelKey,
|
|
206
|
+
providerName: provider.name ?? null,
|
|
207
|
+
name: model.name ?? null,
|
|
208
|
+
family: model.family ?? null,
|
|
209
|
+
releaseDate: model.release_date ?? null,
|
|
210
|
+
contextWindow: pickNumber(model.limit?.context) ?? null,
|
|
211
|
+
maxOutput: pickNumber(model.limit?.output) ?? null,
|
|
212
|
+
reasoning: typeof model.reasoning === "boolean" ? model.reasoning : null,
|
|
213
|
+
reasoningOptions: reasoningOptions.length > 0 ? reasoningOptions : null,
|
|
214
|
+
pricing: Object.keys(pricing).length > 0 ? pricing : null,
|
|
215
|
+
status: model.status ?? null,
|
|
216
|
+
checkedAt,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return entries;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ─── Table access ────────────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Replace the upstream catalog with `entries`. Returns the "provider/modelId"
|
|
227
|
+
* keys that were not in the table before.
|
|
228
|
+
*/
|
|
229
|
+
export async function replaceModelCatalog(entries: ModelCatalogEntry[]): Promise<string[]> {
|
|
230
|
+
return await getDbClient().transaction(async (tx) => {
|
|
231
|
+
const before = await tx.query<{ provider: string; modelId: string }>(
|
|
232
|
+
"SELECT provider, modelId FROM model_catalog",
|
|
233
|
+
);
|
|
234
|
+
const known = new Set(before.map((r) => `${r.provider}/${r.modelId}`));
|
|
235
|
+
await tx.run("DELETE FROM model_catalog");
|
|
236
|
+
const added: string[] = [];
|
|
237
|
+
for (const entry of entries) {
|
|
238
|
+
await tx.run(
|
|
239
|
+
`INSERT INTO model_catalog
|
|
240
|
+
(provider, modelId, name, family, releaseDate, contextWindow, maxOutput, reasoning,
|
|
241
|
+
reasoningOptions, pricing, status, providerName, checkedAt)
|
|
242
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
243
|
+
[
|
|
244
|
+
entry.provider,
|
|
245
|
+
entry.modelId,
|
|
246
|
+
...factParams(entry),
|
|
247
|
+
entry.providerName ?? null,
|
|
248
|
+
entry.checkedAt,
|
|
249
|
+
],
|
|
250
|
+
);
|
|
251
|
+
const key = `${entry.provider}/${entry.modelId}`;
|
|
252
|
+
// An empty table before means first population, not "new models".
|
|
253
|
+
if (known.size > 0 && !known.has(key)) added.push(key);
|
|
254
|
+
}
|
|
255
|
+
return added;
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function touchModelCatalogCheckedAt(now: number): Promise<void> {
|
|
260
|
+
await getDbClient().run("UPDATE model_catalog SET checkedAt = ?", [now]);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export async function listModelCatalog(): Promise<ModelCatalogEntry[]> {
|
|
264
|
+
const rows = await getDbClient().query<CatalogRow>(
|
|
265
|
+
"SELECT * FROM model_catalog ORDER BY provider, modelId",
|
|
266
|
+
);
|
|
267
|
+
return rows.map((row) => ({
|
|
268
|
+
provider: row.provider,
|
|
269
|
+
modelId: row.modelId,
|
|
270
|
+
providerName: row.providerName,
|
|
271
|
+
checkedAt: row.checkedAt,
|
|
272
|
+
...rowToFacts(row),
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export async function countModelCatalog(): Promise<number> {
|
|
277
|
+
const row = await getDbClient().get<{ n: number }>("SELECT COUNT(*) AS n FROM model_catalog");
|
|
278
|
+
return row?.n ?? 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export async function getModelCatalogMeta(): Promise<ModelCatalogMeta> {
|
|
282
|
+
const row = await getDbClient().get<ModelCatalogMeta>(
|
|
283
|
+
"SELECT etag, lastModified, lastFetchAt, lastCheckedAt FROM model_catalog_meta WHERE id = 1",
|
|
284
|
+
);
|
|
285
|
+
return row ?? { etag: null, lastModified: null, lastFetchAt: null, lastCheckedAt: null };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export async function updateModelCatalogMeta(patch: Partial<ModelCatalogMeta>): Promise<void> {
|
|
289
|
+
const current = await getModelCatalogMeta();
|
|
290
|
+
const next = { ...current, ...patch };
|
|
291
|
+
await getDbClient().run(
|
|
292
|
+
`INSERT INTO model_catalog_meta (id, etag, lastModified, lastFetchAt, lastCheckedAt)
|
|
293
|
+
VALUES (1, ?, ?, ?, ?)
|
|
294
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
295
|
+
etag = excluded.etag,
|
|
296
|
+
lastModified = excluded.lastModified,
|
|
297
|
+
lastFetchAt = excluded.lastFetchAt,
|
|
298
|
+
lastCheckedAt = excluded.lastCheckedAt`,
|
|
299
|
+
[next.etag, next.lastModified, next.lastFetchAt, next.lastCheckedAt],
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ─── Overlay ─────────────────────────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
function overlayRowToEntry(row: OverlayRow): ModelCatalogOverlayEntry {
|
|
306
|
+
return {
|
|
307
|
+
provider: row.provider,
|
|
308
|
+
modelId: row.modelId,
|
|
309
|
+
reason: row.reason,
|
|
310
|
+
verifiedBy: row.verifiedBy,
|
|
311
|
+
expiresWhenUpstreamMatches: row.expiresWhenUpstreamMatches === 1,
|
|
312
|
+
createdAt: row.createdAt,
|
|
313
|
+
updatedAt: row.updatedAt,
|
|
314
|
+
...rowToFacts(row),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export async function listModelCatalogOverlay(): Promise<ModelCatalogOverlayEntry[]> {
|
|
319
|
+
const rows = await getDbClient().query<OverlayRow>(
|
|
320
|
+
"SELECT * FROM model_catalog_overlay ORDER BY provider, modelId",
|
|
321
|
+
);
|
|
322
|
+
return rows.map(overlayRowToEntry);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Upsert one overlay row, add pricing-table rows for any of its prices that
|
|
327
|
+
* have no active pricing row yet, and invalidate the in-memory catalog.
|
|
328
|
+
*/
|
|
329
|
+
export async function upsertModelCatalogOverlay(
|
|
330
|
+
input: ModelCatalogOverlayInput,
|
|
331
|
+
opts: { userId?: string | null; now?: number } = {},
|
|
332
|
+
): Promise<ModelCatalogOverlayEntry> {
|
|
333
|
+
const now = opts.now ?? Date.now();
|
|
334
|
+
const userId = opts.userId ?? null;
|
|
335
|
+
await getDbClient().run(
|
|
336
|
+
`INSERT INTO model_catalog_overlay
|
|
337
|
+
(provider, modelId, name, family, releaseDate, contextWindow, maxOutput, reasoning,
|
|
338
|
+
reasoningOptions, pricing, status, reason, verifiedBy, expiresWhenUpstreamMatches,
|
|
339
|
+
createdAt, updatedAt, created_by, updated_by)
|
|
340
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
341
|
+
ON CONFLICT(provider, modelId) DO UPDATE SET
|
|
342
|
+
name = excluded.name,
|
|
343
|
+
family = excluded.family,
|
|
344
|
+
releaseDate = excluded.releaseDate,
|
|
345
|
+
contextWindow = excluded.contextWindow,
|
|
346
|
+
maxOutput = excluded.maxOutput,
|
|
347
|
+
reasoning = excluded.reasoning,
|
|
348
|
+
reasoningOptions = excluded.reasoningOptions,
|
|
349
|
+
pricing = excluded.pricing,
|
|
350
|
+
status = excluded.status,
|
|
351
|
+
reason = excluded.reason,
|
|
352
|
+
verifiedBy = excluded.verifiedBy,
|
|
353
|
+
expiresWhenUpstreamMatches = excluded.expiresWhenUpstreamMatches,
|
|
354
|
+
updatedAt = excluded.updatedAt,
|
|
355
|
+
updated_by = excluded.updated_by`,
|
|
356
|
+
[
|
|
357
|
+
input.provider,
|
|
358
|
+
input.modelId,
|
|
359
|
+
...factParams(input),
|
|
360
|
+
input.reason,
|
|
361
|
+
input.verifiedBy ?? null,
|
|
362
|
+
input.expiresWhenUpstreamMatches === false ? 0 : 1,
|
|
363
|
+
now,
|
|
364
|
+
now,
|
|
365
|
+
userId,
|
|
366
|
+
userId,
|
|
367
|
+
],
|
|
368
|
+
);
|
|
369
|
+
await applyOverlayPricingRows(now);
|
|
370
|
+
markModelsCatalogStale();
|
|
371
|
+
const row = await getDbClient().get<OverlayRow>(
|
|
372
|
+
"SELECT * FROM model_catalog_overlay WHERE provider = ? AND modelId = ?",
|
|
373
|
+
[input.provider, input.modelId],
|
|
374
|
+
);
|
|
375
|
+
if (!row) throw new Error("overlay row vanished after upsert");
|
|
376
|
+
return overlayRowToEntry(row);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export async function deleteModelCatalogOverlay(
|
|
380
|
+
provider: string,
|
|
381
|
+
modelId: string,
|
|
382
|
+
): Promise<boolean> {
|
|
383
|
+
const result = await getDbClient().run(
|
|
384
|
+
"DELETE FROM model_catalog_overlay WHERE provider = ? AND modelId = ?",
|
|
385
|
+
[provider, modelId],
|
|
386
|
+
);
|
|
387
|
+
markModelsCatalogStale();
|
|
388
|
+
return result.changes > 0;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function factEquals(key: (typeof FACT_KEYS)[number], overlay: unknown, upstream: unknown): boolean {
|
|
392
|
+
if (key === "pricing") {
|
|
393
|
+
const o = overlay as ModelCatalogPricing;
|
|
394
|
+
const u = (upstream ?? {}) as ModelCatalogPricing;
|
|
395
|
+
return (Object.keys(o) as (keyof ModelCatalogPricing)[]).every(
|
|
396
|
+
(k) => o[k] === undefined || o[k] === null || o[k] === u[k],
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
if (key === "reasoningOptions") return JSON.stringify(overlay) === JSON.stringify(upstream);
|
|
400
|
+
return overlay === upstream;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Delete overlay rows flagged `expiresWhenUpstreamMatches` whose non-null facts
|
|
405
|
+
* all equal the upstream row. Returns the expired "provider/modelId" keys.
|
|
406
|
+
*/
|
|
407
|
+
export async function expireMatchedOverlays(): Promise<string[]> {
|
|
408
|
+
const overlays = await listModelCatalogOverlay();
|
|
409
|
+
if (overlays.length === 0) return [];
|
|
410
|
+
const upstream = new Map(
|
|
411
|
+
(await listModelCatalog()).map((e) => [`${e.provider}/${e.modelId}`, e]),
|
|
412
|
+
);
|
|
413
|
+
const expired: string[] = [];
|
|
414
|
+
for (const overlay of overlays) {
|
|
415
|
+
if (!overlay.expiresWhenUpstreamMatches) continue;
|
|
416
|
+
const key = `${overlay.provider}/${overlay.modelId}`;
|
|
417
|
+
const up = upstream.get(key);
|
|
418
|
+
if (!up) continue;
|
|
419
|
+
const matches = FACT_KEYS.every((fact) => {
|
|
420
|
+
const value = overlay[fact];
|
|
421
|
+
if (value === null || value === undefined) return true;
|
|
422
|
+
return factEquals(fact, value, up[fact]);
|
|
423
|
+
});
|
|
424
|
+
if (!matches) continue;
|
|
425
|
+
await deleteModelCatalogOverlay(overlay.provider, overlay.modelId);
|
|
426
|
+
expired.push(key);
|
|
427
|
+
}
|
|
428
|
+
return expired;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Overlay prices → pricing rows, via the same models.dev projection the
|
|
433
|
+
* refresh uses. A (provider, model, tokenClass) triple with NO active pricing
|
|
434
|
+
* row is written, so upstream prices are never overridden and a brand-new
|
|
435
|
+
* model still gets priced by the server-side cost recompute. For an
|
|
436
|
+
* overlay-only model (no upstream catalog row) the overlay is the only price
|
|
437
|
+
* source, so a corrected overlay price supersedes its earlier row.
|
|
438
|
+
*/
|
|
439
|
+
export async function applyOverlayPricingRows(now = Date.now()): Promise<number> {
|
|
440
|
+
const overlays = await listModelCatalogOverlay();
|
|
441
|
+
const upstream = new Set((await listModelCatalog()).map((e) => `${e.provider}/${e.modelId}`));
|
|
442
|
+
const synthetic: ModelsDevCache = {};
|
|
443
|
+
const overlayOnly: ModelsDevCache = {};
|
|
444
|
+
for (const overlay of overlays) {
|
|
445
|
+
if (!overlay.pricing || Object.keys(overlay.pricing).length === 0) continue;
|
|
446
|
+
for (const target of upstream.has(`${overlay.provider}/${overlay.modelId}`)
|
|
447
|
+
? [synthetic]
|
|
448
|
+
: [synthetic, overlayOnly]) {
|
|
449
|
+
target[overlay.provider] ??= { models: {} };
|
|
450
|
+
const models = target[overlay.provider]?.models;
|
|
451
|
+
if (models) models[overlay.modelId] = { cost: overlay.pricing };
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (Object.keys(synthetic).length === 0) return 0;
|
|
455
|
+
const rows = buildModelsDevSeedRows(synthetic);
|
|
456
|
+
const tripleKey = (r: { provider: string; model: string; tokenClass: string }) =>
|
|
457
|
+
`${r.provider}|${r.model}|${r.tokenClass}`;
|
|
458
|
+
// The projection also emits rows for an empty provider block (shortname
|
|
459
|
+
// fallbacks); those are not owned by any overlay-only model.
|
|
460
|
+
const emptyBlocks: ModelsDevCache = Object.fromEntries(
|
|
461
|
+
Object.keys(overlayOnly).map((provider) => [provider, { models: {} }]),
|
|
462
|
+
);
|
|
463
|
+
const fallbackKeys = new Set(buildModelsDevSeedRows(emptyBlocks).map(tripleKey));
|
|
464
|
+
const overlayOwned = new Set(
|
|
465
|
+
buildModelsDevSeedRows(overlayOnly)
|
|
466
|
+
.map(tripleKey)
|
|
467
|
+
.filter((key) => !fallbackKeys.has(key)),
|
|
468
|
+
);
|
|
469
|
+
return await getDbClient().transaction(async () => {
|
|
470
|
+
let inserted = 0;
|
|
471
|
+
for (const row of rows) {
|
|
472
|
+
const existing = await getActivePricingRow(row.provider, row.model, row.tokenClass, now);
|
|
473
|
+
if (existing) {
|
|
474
|
+
if (!overlayOwned.has(tripleKey(row))) continue;
|
|
475
|
+
if (existing.pricePerMillionUsd === row.pricePerMillionUsd) continue;
|
|
476
|
+
if (existing.effectiveFrom === now) {
|
|
477
|
+
await getDbClient().run(
|
|
478
|
+
"UPDATE pricing SET price_per_million_usd = ?, lastUpdatedAt = ? WHERE provider = ? AND model = ? AND token_class = ? AND effective_from = ?",
|
|
479
|
+
[row.pricePerMillionUsd, now, row.provider, row.model, row.tokenClass, now],
|
|
480
|
+
);
|
|
481
|
+
inserted += 1;
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const input: InsertPricingRowInput = { ...row, effectiveFrom: now };
|
|
486
|
+
await insertPricingRow(input);
|
|
487
|
+
inserted += 1;
|
|
488
|
+
}
|
|
489
|
+
return inserted;
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ─── Read path ───────────────────────────────────────────────────────────────
|
|
494
|
+
|
|
495
|
+
function toCatalogModel(modelId: string, facts: ModelCatalogFacts): CatalogModel {
|
|
496
|
+
const cost =
|
|
497
|
+
facts.pricing && (facts.pricing.input !== undefined || facts.pricing.output !== undefined)
|
|
498
|
+
? {
|
|
499
|
+
input: facts.pricing.input,
|
|
500
|
+
output: facts.pricing.output,
|
|
501
|
+
...(facts.pricing.cache_read !== undefined
|
|
502
|
+
? { cache_read: facts.pricing.cache_read }
|
|
503
|
+
: {}),
|
|
504
|
+
...(facts.pricing.cache_write !== undefined
|
|
505
|
+
? { cache_write: facts.pricing.cache_write }
|
|
506
|
+
: {}),
|
|
507
|
+
}
|
|
508
|
+
: undefined;
|
|
509
|
+
const reasoning =
|
|
510
|
+
facts.reasoning ?? (facts.reasoningOptions && facts.reasoningOptions.length > 0 ? true : null);
|
|
511
|
+
return {
|
|
512
|
+
id: modelId,
|
|
513
|
+
...(facts.name ? { name: facts.name } : {}),
|
|
514
|
+
...(cost ? { cost } : {}),
|
|
515
|
+
...(facts.contextWindow != null ? { limit: { context: facts.contextWindow } } : {}),
|
|
516
|
+
...(facts.releaseDate ? { release_date: facts.releaseDate } : {}),
|
|
517
|
+
...(facts.status ? { status: facts.status } : {}),
|
|
518
|
+
...(reasoning != null ? { reasoning } : {}),
|
|
519
|
+
...(facts.reasoningOptions && facts.reasoningOptions.length > 0
|
|
520
|
+
? { reasoning_options: facts.reasoningOptions }
|
|
521
|
+
: {}),
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function mergeFacts(base: ModelCatalogFacts, overlay: ModelCatalogFacts): ModelCatalogFacts {
|
|
526
|
+
const merged: ModelCatalogFacts = { ...base };
|
|
527
|
+
for (const key of FACT_KEYS) {
|
|
528
|
+
const value = overlay[key];
|
|
529
|
+
if (value === null || value === undefined) continue;
|
|
530
|
+
if (key === "pricing") {
|
|
531
|
+
merged.pricing = { ...(base.pricing ?? {}), ...(value as ModelCatalogPricing) };
|
|
532
|
+
} else {
|
|
533
|
+
(merged as Record<string, unknown>)[key] = value;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return merged;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function applyOverlays(
|
|
540
|
+
catalog: ModelsCatalog,
|
|
541
|
+
baseFacts: Map<string, ModelCatalogFacts>,
|
|
542
|
+
overlays: ModelCatalogOverlayEntry[],
|
|
543
|
+
): void {
|
|
544
|
+
for (const overlay of overlays) {
|
|
545
|
+
const providerId = overlay.provider as CatalogProviderId;
|
|
546
|
+
const key = `${overlay.provider}/${overlay.modelId}`;
|
|
547
|
+
const base = baseFacts.get(key) ?? {};
|
|
548
|
+
catalog[providerId] ??= { id: providerId, models: {} };
|
|
549
|
+
catalog[providerId].models[overlay.modelId] = toCatalogModel(
|
|
550
|
+
overlay.modelId,
|
|
551
|
+
mergeFacts(base, overlay),
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function catalogModelToFacts(model: CatalogModel): ModelCatalogFacts {
|
|
557
|
+
return {
|
|
558
|
+
name: model.name ?? null,
|
|
559
|
+
contextWindow: model.limit?.context ?? null,
|
|
560
|
+
releaseDate: model.release_date ?? null,
|
|
561
|
+
status: model.status ?? null,
|
|
562
|
+
reasoning: model.reasoning ?? null,
|
|
563
|
+
reasoningOptions: model.reasoning_options ?? null,
|
|
564
|
+
pricing: model.cost ? { ...model.cost } : null,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
let catalogLoaded = false;
|
|
569
|
+
let catalogGeneration = 0;
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Bumped by every reload and stale mark, i.e. after each refresh or overlay
|
|
573
|
+
* write. Caches derived from the catalog tables (tier resolution) compare it
|
|
574
|
+
* to drop entries that predate a write, instead of waiting out their TTL.
|
|
575
|
+
*/
|
|
576
|
+
export function getCatalogGeneration(): number {
|
|
577
|
+
return catalogGeneration;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Rebuild the in-memory catalog from `model_catalog` + overlay. Empty table →
|
|
582
|
+
* vendored snapshot (with overlay rows layered on). Returns the served result.
|
|
583
|
+
*/
|
|
584
|
+
export async function reloadModelsCatalog(): Promise<ModelsCatalogResult> {
|
|
585
|
+
catalogGeneration++;
|
|
586
|
+
invalidateModelsCatalog();
|
|
587
|
+
const [entries, overlays, meta] = await Promise.all([
|
|
588
|
+
listModelCatalog(),
|
|
589
|
+
listModelCatalogOverlay(),
|
|
590
|
+
getModelCatalogMeta(),
|
|
591
|
+
]);
|
|
592
|
+
|
|
593
|
+
if (entries.length === 0) {
|
|
594
|
+
if (overlays.length > 0) {
|
|
595
|
+
const snapshot = snapshotModelsCatalog();
|
|
596
|
+
const baseFacts = new Map<string, ModelCatalogFacts>();
|
|
597
|
+
for (const [providerId, provider] of Object.entries(snapshot)) {
|
|
598
|
+
for (const [modelId, model] of Object.entries(provider?.models ?? {})) {
|
|
599
|
+
baseFacts.set(`${providerId}/${modelId}`, catalogModelToFacts(model));
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
applyOverlays(snapshot, baseFacts, overlays);
|
|
603
|
+
setSnapshotOverlayCatalog(snapshot);
|
|
604
|
+
}
|
|
605
|
+
catalogLoaded = true;
|
|
606
|
+
return publishRuntimeCatalog(getModelsCatalog());
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const catalog: ModelsCatalog = {};
|
|
610
|
+
const baseFacts = new Map<string, ModelCatalogFacts>();
|
|
611
|
+
for (const entry of entries) {
|
|
612
|
+
const providerId = entry.provider as CatalogProviderId;
|
|
613
|
+
catalog[providerId] ??= {
|
|
614
|
+
id: providerId,
|
|
615
|
+
...(entry.providerName ? { name: entry.providerName } : {}),
|
|
616
|
+
models: {},
|
|
617
|
+
};
|
|
618
|
+
catalog[providerId].models[entry.modelId] = toCatalogModel(entry.modelId, entry);
|
|
619
|
+
baseFacts.set(`${entry.provider}/${entry.modelId}`, entry);
|
|
620
|
+
}
|
|
621
|
+
applyOverlays(catalog, baseFacts, overlays);
|
|
622
|
+
setLiveModelsCatalog(catalog, meta.lastFetchAt);
|
|
623
|
+
catalogLoaded = true;
|
|
624
|
+
return publishRuntimeCatalog(getModelsCatalog());
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** Hand the served projection to in-process consumers (adapters, context math). */
|
|
628
|
+
function publishRuntimeCatalog(result: ModelsCatalogResult): ModelsCatalogResult {
|
|
629
|
+
setRuntimeModelCatalog(result.providers as RuntimeCatalog, Date.now(), result.source);
|
|
630
|
+
return result;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/** Mark the cached projection stale; the next `loadModelsCatalog()` rebuilds it. */
|
|
634
|
+
export function markModelsCatalogStale(): void {
|
|
635
|
+
catalogLoaded = false;
|
|
636
|
+
catalogGeneration++;
|
|
637
|
+
invalidateModelsCatalog();
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/** Async read path used by the HTTP route: rebuilds from the DB when stale. */
|
|
641
|
+
export async function loadModelsCatalog(): Promise<ModelsCatalogResult> {
|
|
642
|
+
if (!catalogLoaded) return await reloadModelsCatalog();
|
|
643
|
+
return getModelsCatalog();
|
|
644
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for the MODEL_TIER_<PROVIDER>_<TIER> swarm_config keys (no DB),
|
|
3
|
+
* shared by the config validator and claim-time resolution.
|
|
4
|
+
*/
|
|
5
|
+
import { isAlias, parseAlias } from "@desplega/model-catalog";
|
|
6
|
+
import { MODEL_TIERS, type ModelTier, type ProviderName, ProviderNameSchema } from "../types";
|
|
7
|
+
|
|
8
|
+
/** `MODEL_TIER_<PROVIDER>_<TIER>`; provider dashes become underscores. */
|
|
9
|
+
export function tierConfigKey(provider: ProviderName, tier: ModelTier): string {
|
|
10
|
+
return `MODEL_TIER_${provider.toUpperCase().replace(/-/g, "_")}_${tier.toUpperCase()}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const TIER_CONFIG_KEY_RE = new RegExp(
|
|
14
|
+
`^MODEL_TIER_(${ProviderNameSchema.options
|
|
15
|
+
.map((p) => p.toUpperCase().replace(/-/g, "_"))
|
|
16
|
+
.join("|")})_(${MODEL_TIERS.map((t) => t.toUpperCase()).join("|")})$`,
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export function isTierConfigKey(key: string): boolean {
|
|
20
|
+
return TIER_CONFIG_KEY_RE.test(key.toUpperCase());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Validator for MODEL_TIER_<PROVIDER>_<TIER> values; null = ok. */
|
|
24
|
+
export function validateTierConfigValue(key: string, value: unknown): string | null {
|
|
25
|
+
const message = `Invalid ${key} (expected a model id, a CLI alias such as "opus", or latest:<anthropic|openai|openrouter>/<target>[@stable|@any])`;
|
|
26
|
+
if (typeof value !== "string") return message;
|
|
27
|
+
const trimmed = value.trim();
|
|
28
|
+
if (trimmed.length === 0 || /\s/.test(trimmed) || trimmed.length > 200) return message;
|
|
29
|
+
if (isAlias(trimmed) && !parseAlias(trimmed)) return message;
|
|
30
|
+
return null;
|
|
31
|
+
}
|