@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,134 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
3
|
+
import { runMigrations } from "../be/migrations/runner";
|
|
4
|
+
|
|
5
|
+
const DB_PATH = "./test-model-catalog-migration.sqlite";
|
|
6
|
+
|
|
7
|
+
async function removeDb(): Promise<void> {
|
|
8
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
9
|
+
try {
|
|
10
|
+
await Bun.file(DB_PATH + suffix).delete();
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
afterEach(removeDb);
|
|
18
|
+
|
|
19
|
+
function columnNames(db: Database, table: string): string[] {
|
|
20
|
+
return db
|
|
21
|
+
.query<{ name: string }, []>(`PRAGMA table_info(${table})`)
|
|
22
|
+
.all()
|
|
23
|
+
.map((column) => column.name);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function tableExists(db: Database, table: string): boolean {
|
|
27
|
+
return (
|
|
28
|
+
db
|
|
29
|
+
.query<{ name: string }, [string]>(
|
|
30
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name = ?",
|
|
31
|
+
)
|
|
32
|
+
.get(table) !== null
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("migration 184 model catalog, tier resolution and harness support", () => {
|
|
37
|
+
test("upgrades a pre-184 database in place and keeps existing agent and task rows", async () => {
|
|
38
|
+
await removeDb();
|
|
39
|
+
const db = new Database(DB_PATH, { create: true });
|
|
40
|
+
try {
|
|
41
|
+
runMigrations(db);
|
|
42
|
+
|
|
43
|
+
// Roll the database back to the state before migration 184.
|
|
44
|
+
for (const table of [
|
|
45
|
+
"model_catalog",
|
|
46
|
+
"model_catalog_overlay",
|
|
47
|
+
"model_catalog_meta",
|
|
48
|
+
"model_alias_resolutions",
|
|
49
|
+
"harness_model_support",
|
|
50
|
+
]) {
|
|
51
|
+
db.run(`DROP TABLE ${table}`);
|
|
52
|
+
}
|
|
53
|
+
for (const column of ["resolvedModel", "modelSource", "modelAlias"]) {
|
|
54
|
+
db.run(`ALTER TABLE agent_tasks DROP COLUMN ${column}`);
|
|
55
|
+
}
|
|
56
|
+
for (const column of ["modelTierOverrides", "harnessCliVersion"]) {
|
|
57
|
+
db.run(`ALTER TABLE agents DROP COLUMN ${column}`);
|
|
58
|
+
}
|
|
59
|
+
db.run("DELETE FROM _migrations WHERE version = 184");
|
|
60
|
+
|
|
61
|
+
db.run(
|
|
62
|
+
`INSERT INTO agents (id, name, status, createdAt, lastUpdatedAt)
|
|
63
|
+
VALUES ('agent-1', 'worker', 'idle', '2026-09-01', '2026-09-01')`,
|
|
64
|
+
);
|
|
65
|
+
db.run(
|
|
66
|
+
`INSERT INTO agent_tasks (id, task, status, createdAt, lastUpdatedAt)
|
|
67
|
+
VALUES ('task-1', 'existing task', 'pending', '2026-09-01', '2026-09-01')`,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
runMigrations(db);
|
|
71
|
+
|
|
72
|
+
expect(
|
|
73
|
+
db
|
|
74
|
+
.query<{ version: number }, []>("SELECT version FROM _migrations WHERE version = 184")
|
|
75
|
+
.get(),
|
|
76
|
+
).not.toBeNull();
|
|
77
|
+
for (const table of [
|
|
78
|
+
"model_catalog",
|
|
79
|
+
"model_catalog_overlay",
|
|
80
|
+
"model_catalog_meta",
|
|
81
|
+
"model_alias_resolutions",
|
|
82
|
+
"harness_model_support",
|
|
83
|
+
]) {
|
|
84
|
+
expect(tableExists(db, table)).toBe(true);
|
|
85
|
+
}
|
|
86
|
+
expect(columnNames(db, "agent_tasks")).toEqual(
|
|
87
|
+
expect.arrayContaining(["resolvedModel", "modelSource", "modelAlias"]),
|
|
88
|
+
);
|
|
89
|
+
expect(columnNames(db, "agents")).toEqual(
|
|
90
|
+
expect.arrayContaining(["modelTierOverrides", "harnessCliVersion"]),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const task = db
|
|
94
|
+
.query<Record<string, unknown>, []>(
|
|
95
|
+
"SELECT task, resolvedModel, modelSource, modelAlias FROM agent_tasks WHERE id = 'task-1'",
|
|
96
|
+
)
|
|
97
|
+
.get();
|
|
98
|
+
expect(task).toEqual({
|
|
99
|
+
task: "existing task",
|
|
100
|
+
resolvedModel: null,
|
|
101
|
+
modelSource: null,
|
|
102
|
+
modelAlias: null,
|
|
103
|
+
});
|
|
104
|
+
const agent = db
|
|
105
|
+
.query<Record<string, unknown>, []>(
|
|
106
|
+
"SELECT name, modelTierOverrides, harnessCliVersion FROM agents WHERE id = 'agent-1'",
|
|
107
|
+
)
|
|
108
|
+
.get();
|
|
109
|
+
expect(agent).toEqual({ name: "worker", modelTierOverrides: null, harnessCliVersion: null });
|
|
110
|
+
} finally {
|
|
111
|
+
db.close();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("harness_model_support only accepts ok, unsupported and unknown", async () => {
|
|
116
|
+
await removeDb();
|
|
117
|
+
const db = new Database(DB_PATH, { create: true });
|
|
118
|
+
try {
|
|
119
|
+
runMigrations(db);
|
|
120
|
+
const insert = (status: string) =>
|
|
121
|
+
db.run(
|
|
122
|
+
`INSERT INTO harness_model_support (harness, cliVersion, modelId, status, checkedAt)
|
|
123
|
+
VALUES ('claude', '2.1.0', ?, ?, 1)`,
|
|
124
|
+
[`m-${status}`, status],
|
|
125
|
+
);
|
|
126
|
+
expect(() => insert("ok")).not.toThrow();
|
|
127
|
+
expect(() => insert("unsupported")).not.toThrow();
|
|
128
|
+
expect(() => insert("unknown")).not.toThrow();
|
|
129
|
+
expect(() => insert("maybe")).toThrow(/CHECK/);
|
|
130
|
+
} finally {
|
|
131
|
+
db.close();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import { closeDb, getActivePricingRow, getDbClient, initDb } from "../be/db";
|
|
4
|
+
import {
|
|
5
|
+
listModelCatalogOverlay,
|
|
6
|
+
loadModelsCatalog,
|
|
7
|
+
markModelsCatalogStale,
|
|
8
|
+
upsertModelCatalogOverlay,
|
|
9
|
+
} from "../be/model-catalog-store";
|
|
10
|
+
import { getModelsCatalog, resetModelsCatalogForTests } from "../be/models-catalog";
|
|
11
|
+
import type { ModelsDevCache } from "../be/modelsdev-cache";
|
|
12
|
+
import { MODEL_CATALOG_FRESH_MS, refreshModelCatalog } from "../be/pricing-refresh";
|
|
13
|
+
|
|
14
|
+
const TEST_DB_PATH = "./test-model-catalog-table.sqlite";
|
|
15
|
+
|
|
16
|
+
async function removeDbFiles(path: string): Promise<void> {
|
|
17
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
18
|
+
try {
|
|
19
|
+
await unlink(path + suffix);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function payload(extraModels: Record<string, unknown> = {}): ModelsDevCache {
|
|
27
|
+
return {
|
|
28
|
+
openai: {
|
|
29
|
+
name: "OpenAI",
|
|
30
|
+
models: {
|
|
31
|
+
"gpt-base": {
|
|
32
|
+
name: "GPT Base",
|
|
33
|
+
cost: { input: 1, output: 4 },
|
|
34
|
+
limit: { context: 100_000 },
|
|
35
|
+
},
|
|
36
|
+
...extraModels,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
} as ModelsDevCache;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function ok(cache: ModelsDevCache, etag: string): typeof fetch {
|
|
43
|
+
return (async () =>
|
|
44
|
+
new Response(JSON.stringify(cache), {
|
|
45
|
+
status: 200,
|
|
46
|
+
headers: { "content-type": "application/json", etag },
|
|
47
|
+
})) as unknown as typeof fetch;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
beforeAll(async () => {
|
|
51
|
+
await removeDbFiles(TEST_DB_PATH);
|
|
52
|
+
initDb(TEST_DB_PATH);
|
|
53
|
+
// Module state may carry over from other files sharing this process.
|
|
54
|
+
resetModelsCatalogForTests();
|
|
55
|
+
markModelsCatalogStale();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterAll(async () => {
|
|
59
|
+
closeDb();
|
|
60
|
+
await removeDbFiles(TEST_DB_PATH);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
afterEach(async () => {
|
|
64
|
+
const client = getDbClient();
|
|
65
|
+
await client.run("DELETE FROM model_catalog");
|
|
66
|
+
await client.run("DELETE FROM model_catalog_overlay");
|
|
67
|
+
await client.run("DELETE FROM model_catalog_meta");
|
|
68
|
+
await client.run("DELETE FROM pricing WHERE model LIKE 'gpt-%'");
|
|
69
|
+
resetModelsCatalogForTests();
|
|
70
|
+
markModelsCatalogStale();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("persistent model catalog", () => {
|
|
74
|
+
test("empty table serves the vendored snapshot", async () => {
|
|
75
|
+
const result = await loadModelsCatalog();
|
|
76
|
+
expect(result.source).toBe("snapshot");
|
|
77
|
+
expect(result.updatedAt).toBeNull();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("forced refresh adds a new model to the catalog and pricing without restart", async () => {
|
|
81
|
+
await refreshModelCatalog({ force: true, now: 1_000, fetchImpl: ok(payload(), '"v1"') });
|
|
82
|
+
|
|
83
|
+
const result = await refreshModelCatalog({
|
|
84
|
+
force: true,
|
|
85
|
+
now: 2_000,
|
|
86
|
+
fetchImpl: ok(
|
|
87
|
+
payload({ "gpt-brand-new": { name: "Brand New", cost: { input: 3, output: 9 } } }),
|
|
88
|
+
'"v2"',
|
|
89
|
+
),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(result.status).toBe("updated");
|
|
93
|
+
expect(result.added).toEqual(["openai/gpt-brand-new"]);
|
|
94
|
+
expect(result.models).toBe(2);
|
|
95
|
+
const catalog = await loadModelsCatalog();
|
|
96
|
+
expect(catalog.source).toBe("live");
|
|
97
|
+
expect(catalog.providers.openai?.models["gpt-brand-new"]?.cost?.input).toBe(3);
|
|
98
|
+
const price = await getActivePricingRow("codex", "gpt-brand-new", "input", 2_000);
|
|
99
|
+
expect(price?.pricePerMillionUsd).toBe(3);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("304 keeps the catalog and bumps checkedAt", async () => {
|
|
103
|
+
await refreshModelCatalog({ force: true, now: 1_000, fetchImpl: ok(payload(), '"v1"') });
|
|
104
|
+
let sent: string | null = null;
|
|
105
|
+
const result = await refreshModelCatalog({
|
|
106
|
+
force: true,
|
|
107
|
+
now: 5_000,
|
|
108
|
+
fetchImpl: (async (_url: unknown, init?: RequestInit) => {
|
|
109
|
+
sent = new Headers(init?.headers).get("if-none-match");
|
|
110
|
+
return new Response(null, { status: 304 });
|
|
111
|
+
}) as unknown as typeof fetch,
|
|
112
|
+
});
|
|
113
|
+
expect(sent).toBe('"v1"');
|
|
114
|
+
expect(result.status).toBe("not-modified");
|
|
115
|
+
expect(result.models).toBe(1);
|
|
116
|
+
expect(result.checkedAt).toBe(5_000);
|
|
117
|
+
const row = await getDbClient().get<{ checkedAt: number }>(
|
|
118
|
+
"SELECT checkedAt FROM model_catalog WHERE modelId = 'gpt-base'",
|
|
119
|
+
);
|
|
120
|
+
expect(row?.checkedAt).toBe(5_000);
|
|
121
|
+
expect(getModelsCatalog().providers.openai?.models["gpt-base"]).toBeDefined();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("unforced refresh within 4h skips the network", async () => {
|
|
125
|
+
await refreshModelCatalog({ now: 1_000, fetchImpl: ok(payload(), '"v1"') });
|
|
126
|
+
let called = false;
|
|
127
|
+
const fetchImpl = (async () => {
|
|
128
|
+
called = true;
|
|
129
|
+
return new Response(null, { status: 304 });
|
|
130
|
+
}) as unknown as typeof fetch;
|
|
131
|
+
const skipped = await refreshModelCatalog({ now: 1_000 + 60_000, fetchImpl });
|
|
132
|
+
expect(skipped.status).toBe("skipped-fresh");
|
|
133
|
+
expect(called).toBe(false);
|
|
134
|
+
|
|
135
|
+
const stale = await refreshModelCatalog({ now: 1_000 + MODEL_CATALOG_FRESH_MS + 1, fetchImpl });
|
|
136
|
+
expect(stale.status).toBe("not-modified");
|
|
137
|
+
expect(called).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("overlay-only model appears in the catalog and gets a pricing row", async () => {
|
|
141
|
+
await refreshModelCatalog({ force: true, now: 1_000, fetchImpl: ok(payload(), '"v1"') });
|
|
142
|
+
await upsertModelCatalogOverlay(
|
|
143
|
+
{
|
|
144
|
+
provider: "openai",
|
|
145
|
+
modelId: "gpt-overlay-only",
|
|
146
|
+
name: "Overlay Only",
|
|
147
|
+
contextWindow: 400_000,
|
|
148
|
+
pricing: { input: 7, output: 21 },
|
|
149
|
+
reason: "launch blog",
|
|
150
|
+
},
|
|
151
|
+
{ now: 2_000 },
|
|
152
|
+
);
|
|
153
|
+
const catalog = await loadModelsCatalog();
|
|
154
|
+
const model = catalog.providers.openai?.models["gpt-overlay-only"];
|
|
155
|
+
expect(model?.name).toBe("Overlay Only");
|
|
156
|
+
expect(model?.limit?.context).toBe(400_000);
|
|
157
|
+
const price = await getActivePricingRow("codex", "gpt-overlay-only", "output", 2_000);
|
|
158
|
+
expect(price?.pricePerMillionUsd).toBe(21);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("a corrected overlay-only price supersedes the earlier pricing row", async () => {
|
|
162
|
+
await refreshModelCatalog({ force: true, now: 1_000, fetchImpl: ok(payload(), '"v1"') });
|
|
163
|
+
const overlay = {
|
|
164
|
+
provider: "openai",
|
|
165
|
+
modelId: "gpt-overlay-only",
|
|
166
|
+
pricing: { input: 7, output: 21 },
|
|
167
|
+
reason: "launch blog",
|
|
168
|
+
};
|
|
169
|
+
await upsertModelCatalogOverlay(overlay, { now: 2_000 });
|
|
170
|
+
await upsertModelCatalogOverlay(
|
|
171
|
+
{ ...overlay, pricing: { input: 7, output: 28 } },
|
|
172
|
+
{ now: 3_000 },
|
|
173
|
+
);
|
|
174
|
+
expect(
|
|
175
|
+
(await getActivePricingRow("codex", "gpt-overlay-only", "output", 2_500))?.pricePerMillionUsd,
|
|
176
|
+
).toBe(21);
|
|
177
|
+
expect(
|
|
178
|
+
(await getActivePricingRow("codex", "gpt-overlay-only", "output", 3_000))?.pricePerMillionUsd,
|
|
179
|
+
).toBe(28);
|
|
180
|
+
expect(
|
|
181
|
+
(await getActivePricingRow("codex", "gpt-overlay-only", "input", 3_000))?.effectiveFrom,
|
|
182
|
+
).toBe(2_000);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("an overlay price never overrides an upstream-priced model", async () => {
|
|
186
|
+
await refreshModelCatalog({ force: true, now: 1_000, fetchImpl: ok(payload(), '"v1"') });
|
|
187
|
+
const before = await getActivePricingRow("codex", "gpt-base", "output", 1_000);
|
|
188
|
+
expect(before).not.toBeNull();
|
|
189
|
+
await upsertModelCatalogOverlay(
|
|
190
|
+
{
|
|
191
|
+
provider: "openai",
|
|
192
|
+
modelId: "gpt-base",
|
|
193
|
+
pricing: { input: 99, output: 999 },
|
|
194
|
+
reason: "upstream wrong",
|
|
195
|
+
},
|
|
196
|
+
{ now: 2_000 },
|
|
197
|
+
);
|
|
198
|
+
const after = await getActivePricingRow("codex", "gpt-base", "output", 2_000);
|
|
199
|
+
expect(after?.pricePerMillionUsd).toBe(before?.pricePerMillionUsd);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("overlay wins per field over upstream", async () => {
|
|
203
|
+
await refreshModelCatalog({ force: true, now: 1_000, fetchImpl: ok(payload(), '"v1"') });
|
|
204
|
+
await upsertModelCatalogOverlay({
|
|
205
|
+
provider: "openai",
|
|
206
|
+
modelId: "gpt-base",
|
|
207
|
+
contextWindow: 200_000,
|
|
208
|
+
reason: "upstream wrong",
|
|
209
|
+
});
|
|
210
|
+
const model = (await loadModelsCatalog()).providers.openai?.models["gpt-base"];
|
|
211
|
+
expect(model?.limit?.context).toBe(200_000);
|
|
212
|
+
expect(model?.name).toBe("GPT Base");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("overlay auto-expires once upstream matches", async () => {
|
|
216
|
+
await refreshModelCatalog({ force: true, now: 1_000, fetchImpl: ok(payload(), '"v1"') });
|
|
217
|
+
await upsertModelCatalogOverlay({
|
|
218
|
+
provider: "openai",
|
|
219
|
+
modelId: "gpt-late",
|
|
220
|
+
name: "Late",
|
|
221
|
+
pricing: { input: 2 },
|
|
222
|
+
reason: "not on models.dev yet",
|
|
223
|
+
});
|
|
224
|
+
await upsertModelCatalogOverlay({
|
|
225
|
+
provider: "openai",
|
|
226
|
+
modelId: "gpt-pinned",
|
|
227
|
+
name: "Pinned",
|
|
228
|
+
reason: "keep",
|
|
229
|
+
expiresWhenUpstreamMatches: false,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
await refreshModelCatalog({
|
|
233
|
+
force: true,
|
|
234
|
+
now: 2_000,
|
|
235
|
+
fetchImpl: ok(
|
|
236
|
+
payload({
|
|
237
|
+
"gpt-late": { name: "Late", cost: { input: 2, output: 8 } },
|
|
238
|
+
"gpt-pinned": { name: "Pinned" },
|
|
239
|
+
}),
|
|
240
|
+
'"v2"',
|
|
241
|
+
),
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const remaining = (await listModelCatalogOverlay()).map((o) => o.modelId);
|
|
245
|
+
expect(remaining).toEqual(["gpt-pinned"]);
|
|
246
|
+
const late = (await loadModelsCatalog()).providers.openai?.models["gpt-late"];
|
|
247
|
+
expect(late?.cost?.output).toBe(8);
|
|
248
|
+
});
|
|
249
|
+
});
|