@desplega.ai/agent-swarm 1.173.0 → 2.0.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/README.md +1 -1
- package/dist/{acp-adapter-ccm9xrep.js → acp-adapter-sj2g0m84.js} +17 -16
- package/dist/{actions-sgeyv08k.js → actions-fegk44gr.js} +39 -34
- package/dist/{amp-adapter-gpxp3kc9.js → amp-adapter-ad8ft89x.js} +8 -7
- package/dist/{app-km9qnv2f.js → app-kfvgbvzy.js} +18 -14
- package/dist/{assistant-5r8tcvvd.js → assistant-z7zdy9ja.js} +40 -35
- package/dist/{boot-reembed-swwzdgd7.js → boot-reembed-5c2z0mbe.js} +10 -9
- package/dist/{boot-reembed-16rv9843.js → boot-reembed-j8xa181e.js} +10 -9
- package/dist/{boot-scrub-sweep-zkr6vkqb.js → boot-scrub-sweep-hnkn3rgt.js} +11 -10
- package/dist/{boot-seeds-d8sp49t2.js → boot-seeds-kwjq4d2z.js} +14 -13
- package/dist/{claude-adapter-yzzzh8jp.js → claude-adapter-9vmvpvgd.js} +19 -18
- package/dist/{claude-managed-adapter-xgxhqrt2.js → claude-managed-adapter-k9qae05j.js} +7 -5
- package/dist/{claude-managed-setup-bnmjarjc.js → claude-managed-setup-q54zx5q2.js} +6 -4
- package/dist/{claude-sdk-session-4whxere4.js → claude-sdk-session-9jye6cxr.js} +11881 -11472
- package/dist/{cli-94b48jhk.js → cli-03gedhng.js} +16 -14
- package/dist/{cli-1w0vj3gw.js → cli-1e4b6bzh.js} +7 -7
- package/dist/{cli-d603qf86.js → cli-29jc5q7w.js} +10 -8
- package/dist/cli-2m1ktfzz.js +17795 -0
- package/dist/{cli-x119kdba.js → cli-3dj8k4vy.js} +1 -1
- package/dist/{cli-srqq3gc5.js → cli-3fbmmawm.js} +5 -5
- package/dist/{cli-gmdajfw5.js → cli-4jxm2qtz.js} +4987 -1708
- package/dist/{cli-tadfgd0t.js → cli-5hkm7whv.js} +33 -4
- package/dist/{cli-42qhyvwx.js → cli-5t4f29ez.js} +3 -3
- package/dist/{cli-7a0sde2x.js → cli-6cagzc96.js} +9 -0
- package/dist/cli-6cvfxs8r.js +47 -0
- package/dist/{cli-0kxf81jj.js → cli-6jyj59nb.js} +2 -2
- package/dist/{cli-w1apjecb.js → cli-6razj6t4.js} +2 -2
- package/dist/{cli-anzdk0z4.js → cli-74ahz61x.js} +2 -2
- package/dist/{cli-m89t9jdf.js → cli-7x0g4sn1.js} +15416 -16024
- package/dist/{cli-yatxqynm.js → cli-9ha6hs2d.js} +1 -290
- package/dist/{cli-81r1wjyv.js → cli-9nfx44eh.js} +1 -1
- package/dist/cli-9traj3dn.js +638 -0
- package/dist/{cli-smcrrmhy.js → cli-agzebbd5.js} +9 -9
- package/dist/{cli-na2pxe4f.js → cli-apx87jne.js} +2 -2
- package/dist/{cli-rfz2ve55.js → cli-bzszjf2p.js} +2855 -862
- package/dist/{cli-c5km5znr.js → cli-c8r3kzzb.js} +1 -1
- package/dist/{cli-hsem16hb.js → cli-cgtksawx.js} +7 -4
- package/dist/{cli-db3m6zdt.js → cli-d2qr7aj4.js} +1 -1
- package/dist/{cli-rfs4vddv.js → cli-d5f9ed5c.js} +5 -5
- package/dist/cli-e3g5mbkz.js +776 -0
- package/dist/{cli-7jykv7ef.js → cli-e6mpjhed.js} +1 -1
- package/dist/{cli-7a93cv8c.js → cli-ee9gtz70.js} +98 -23
- package/dist/{cli-3nxdmjrc.js → cli-eejsgvm4.js} +4 -4
- package/dist/{cli-fmhj1ehj.js → cli-ef1hg1wv.js} +470 -51
- package/dist/cli-f527ce74.js +781 -0
- package/dist/{cli-ppy4nnnh.js → cli-f7kr7svz.js} +2 -2
- package/dist/cli-hjwmhcg1.js +10622 -0
- package/dist/{cli-2bw3w47j.js → cli-hnhtk6cj.js} +1 -1
- package/dist/{cli-ywm1azg9.js → cli-hnmhtzvh.js} +6 -4
- package/dist/{cli-h7k0xtry.js → cli-hvqbtna4.js} +4 -1256
- package/dist/{cli-s8z6x4b4.js → cli-j0pfptdp.js} +218 -102
- package/dist/cli-jta2eyzc.js +619 -0
- package/dist/{cli-5er5gjbm.js → cli-k382tshb.js} +10 -11
- package/dist/{cli-v3zhnxsb.js → cli-k3dp5tpf.js} +2 -2
- package/dist/{cli-wd0hz6ah.js → cli-kbrt8gqa.js} +9 -91
- package/dist/cli-kceah1eh.js +92 -0
- package/dist/{cli-ne7smj1b.js → cli-ke896fcf.js} +10 -10
- package/dist/{cli-xc7p7s3g.js → cli-kt6zqg0e.js} +6 -6
- package/dist/{cli-wrh08zjd.js → cli-m50t4309.js} +5 -18
- package/dist/cli-n2wq4j7m.js +6985 -0
- package/dist/{cli-bv2etw2y.js → cli-n3hzg89b.js} +46 -15
- package/dist/{cli-tmhsxhb8.js → cli-n5vwg842.js} +23 -6
- package/dist/{cli-qm1448f9.js → cli-ne236afy.js} +8 -8
- package/dist/{cli-d161g69q.js → cli-p1x85d2t.js} +50 -2
- package/dist/{cli-7pz3b7x5.js → cli-przpmdgr.js} +25 -0
- package/dist/{cli-0a7mm3zp.js → cli-q0ke1yww.js} +25 -21
- package/dist/{cli-e3pmyt39.js → cli-q50942rs.js} +1 -1
- package/dist/{cli-r44t9wqc.js → cli-r106725k.js} +2 -2
- package/dist/{cli-phcqp1b9.js → cli-r2pnvj8g.js} +2 -2
- package/dist/{cli-jpc4r224.js → cli-r62a35ea.js} +19 -3
- package/dist/{cli-cd0hv9nh.js → cli-ra0xzggj.js} +2 -2
- package/dist/cli-rhs9pz33.js +294 -0
- package/dist/{cli-r4fqfpwf.js → cli-s2sp957d.js} +50 -27
- package/dist/{cli-1x7da0de.js → cli-s4setq91.js} +8 -8
- package/dist/{cli-6thk4jz3.js → cli-se8w6wny.js} +6 -6
- package/dist/cli-skxvvpap.js +10140 -0
- package/dist/{cli-014vns2y.js → cli-stfs0k5s.js} +208 -525
- package/dist/{cli-0p3enk35.js → cli-tqaazemc.js} +9 -1
- package/dist/{cli-nwrdx840.js → cli-tr4sxp68.js} +3 -3
- package/dist/{cli-r0byd61h.js → cli-tsde1hdr.js} +15 -15
- package/dist/{cli-b07hr3bt.js → cli-txqb9s38.js} +2 -2
- package/dist/{cli-6r2f7edk.js → cli-v1yb0rqb.js} +6 -6
- package/dist/{cli-saqyage0.js → cli-vatyhshy.js} +179 -27
- package/dist/{cli-5wpw4rab.js → cli-vdach5a9.js} +5 -5
- package/dist/{cli-7avy4v8w.js → cli-w1q7h2r2.js} +5 -3
- package/dist/{cli-rcp5aqdw.js → cli-w7m81tb7.js} +1 -1
- package/dist/{cli-c3rk1v0x.js → cli-wayw93nt.js} +3 -3
- package/dist/cli-wmgrkscj.js +23254 -0
- package/dist/{cli-9xf7fdtp.js → cli-z5jjpaxw.js} +29 -6
- package/dist/{cli-451ks6aw.js → cli-zkpks3r3.js} +1 -1
- package/dist/cli.js +29 -45
- package/dist/{codex-adapter-r3dn4fp5.js → codex-adapter-y81sssk0.js} +15 -14
- package/dist/{codex-login-841kv5fk.js → codex-login-f2rcfpp9.js} +3 -2
- package/dist/{codex-session-runner-fwt793h4.js → codex-session-runner-8vx4j6a9.js} +15 -14
- package/dist/{commands-z8fq9qna.js → commands-kcpvpcrr.js} +11 -10
- package/dist/{controller-hq5ggpcp.js → controller-ywrmt6yd.js} +5 -4
- package/dist/{controllers-connect-3mf2btyj.js → controllers-connect-kwpvj3rf.js} +42 -37
- package/dist/{cursor-adapter-q8pgv5qa.js → cursor-adapter-cehv1qns.js} +7 -6
- package/dist/{db-yyhqqrbf.js → db-k8ankj20.js} +12 -11
- package/dist/{db-qfpq4bt7.js → db-ww5d7jq3.js} +28 -9
- package/dist/{devin-adapter-xgtj680j.js → devin-adapter-x1022qb1.js} +3 -2
- package/dist/{dispatcher-x0x7qfcq.js → dispatcher-e8gq5m68.js} +38 -33
- package/dist/{docker-f503k9p0.js → docker-ff6y1190.js} +3 -2
- package/dist/{dsh-adapter-34etvb5m.js → dsh-adapter-3epjm3af.js} +7 -6
- package/dist/{e2b-66xra4az.js → e2b-8ygwhg0f.js} +4 -3
- package/dist/{e2b-xwhyj8cw.js → e2b-swhnpw3n.js} +4 -3
- package/dist/{event-bus-vjtv82gm.js → event-bus-6axnngdj.js} +3 -2
- package/dist/{external-channel-h1fjecwe.js → external-channel-h3359cwn.js} +11 -10
- package/dist/{google-generative-ai-szdxbpqg.js → google-generative-ai-g8db7x0f.js} +4 -2
- package/dist/{google-vertex-qzw3p36d.js → google-vertex-r2692q0r.js} +4 -2
- package/dist/{grok-adapter-e7mr287e.js → grok-adapter-0d4x7gkx.js} +18 -17
- package/dist/{handlers-znb9n7d4.js → handlers-an1gzv21.js} +41 -36
- package/dist/{heartbeat-bm4qkn8d.js → heartbeat-6dakeb61.js} +43 -38
- package/dist/{hook-254r4mza.js → hook-6v6cv6d5.js} +11 -23
- package/dist/{hook-xaa3q80s.js → hook-sa5xye4y.js} +12 -11
- package/dist/{http-3dgrgp8y.js → http-r7f3eky0.js} +1499 -251
- package/dist/index-52j1aja4.js +3465 -0
- package/dist/{index-fx3jqq1e.js → index-7ep4gzdk.js} +38 -33
- package/dist/index-9gw58n4k.js +193 -0
- package/dist/index-9j87mcm8.js +56305 -0
- package/dist/index-bsmcresk.js +79424 -0
- package/dist/index-mh85qhec.js +475 -0
- package/dist/{index-g2byveez.js → index-rmpw9a8q.js} +38 -33
- package/dist/{index-2ybk7khy.js → index-s2wxgzr0.js} +468 -683
- package/dist/{index-bdsqa799.js → index-v1v6n7hg.js} +43 -38
- package/dist/index-we27kd31.js +12810 -0
- package/dist/{index-jd7sejm2.js → index-zk96aq1k.js} +10 -9
- package/dist/{keepalive-6ywhpja1.js → keepalive-be9vwy5h.js} +12 -11
- package/dist/{lead-cw45n21s.js → lead-0861cwrf.js} +61 -56
- package/dist/{maintenance-ss1a1ge9.js → maintenance-mzzgc8bn.js} +10 -9
- package/dist/{node-fkajaq96.js → node-mn1zs7an.js} +2 -1
- package/dist/{oauth-refresh-sweep-3c6f8vyy.js → oauth-refresh-sweep-cpdnfwdj.js} +11 -10
- package/dist/{onboard-8hzdhc1k.js → onboard-fh8qs4r3.js} +11 -3
- package/dist/{opencode-adapter-qbchbcfz.js → opencode-adapter-nm42wwj8.js} +18 -13
- package/dist/{otel-impl-e6mnvc4v.js → otel-impl-x6c1c9g4.js} +242 -868
- package/dist/{pi-mono-adapter-3c9g9snh.js → pi-mono-adapter-9n2ya240.js} +244 -106
- package/dist/{pricing-refresh-gedkjdw0.js → pricing-refresh-qb0rv0ap.js} +12 -11
- package/dist/{rbac-roles-dt4vh1yy.js → rbac-roles-1gzdwbh1.js} +11 -10
- package/dist/{rbac-roles-eszdg6pm.js → rbac-roles-3mraey52.js} +12 -11
- package/dist/{render-v2-907cy6sa.js → render-v2-agpa45zz.js} +18 -14
- package/dist/{resume-42stcaz0.js → resume-x0ydy9s9.js} +25 -21
- package/dist/{runtime-qfry88v2.js → runtime-sa2vr16g.js} +2 -2
- package/dist/{setup-adbc1c10.js → setup-as9ga0d6.js} +2 -2
- package/dist/{virtual-modules-rtmt494q.js → virtual-modules-9gkxqrap.js} +6 -5
- package/dist/{work-objects-wpj8ck95.js → work-objects-y5g1y7ev.js} +41 -36
- package/dist/{worker-bzdra9rf.js → worker-860c3e38.js} +61 -56
- package/dist/{x-qfmygqqv.js → x-m477fr0h.js} +3 -2
- package/openapi.json +523 -344
- package/package.json +10 -11
- package/src/be/config-refs.ts +60 -0
- package/src/be/db/page-versions.ts +15 -0
- package/src/be/db/runtime.ts +35 -8
- package/src/be/db/skill-name-resolution.ts +36 -0
- package/src/be/db/tasks/read.ts +230 -0
- package/src/be/db/tasks/write.ts +7 -0
- package/src/be/db.ts +52 -7
- package/src/be/migrations/215_swarm_config_export_to_env.sql +34 -0
- package/src/be/migrations/216_task_resolved_effort.sql +5 -0
- package/src/be/migrations/217_chat_sdk_state.sql +104 -0
- package/src/be/model-catalog-store.ts +5 -1
- package/src/be/model-tier-resolution.ts +12 -1
- package/src/be/model-validation.ts +5 -2
- package/src/be/models-catalog.ts +26 -1
- package/src/be/modelsdev-cache.json +198 -63
- package/src/be/modelsdev-cache.ts +9 -1
- package/src/be/script-credential-broker.ts +39 -2
- package/src/be/scripts/run-saved.ts +1 -0
- package/src/be/scripts/typecheck.ts +10 -8
- package/src/be/seed/runner.ts +13 -1
- package/src/be/seed/state-db.ts +5 -0
- package/src/be/seed/types.ts +6 -0
- package/src/be/seed-pricing.ts +19 -2
- package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
- package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
- package/src/be/seed-skills/bundled-files.generated.json +0 -18
- package/src/be/seed-skills/index.ts +104 -3
- package/src/be/swarm-config-guard.ts +52 -0
- package/src/cli.tsx +0 -16
- package/src/commands/onboard/non-interactive.ts +4 -2
- package/src/commands/provider-credentials.ts +52 -6
- package/src/commands/runner.ts +190 -54
- package/src/e2b/env.ts +5 -0
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +45 -0
- package/src/extensions/dispatcher.ts +7 -0
- package/src/extensions/post-bridge.ts +40 -0
- package/src/github/handlers.ts +11 -4
- package/src/heartbeat/templates.ts +8 -5
- package/src/hooks/hook.ts +0 -26
- package/src/http/all-routes.ts +0 -1
- package/src/http/config.ts +36 -17
- package/src/http/core.ts +4 -0
- package/src/http/index.ts +14 -2
- package/src/http/integrations.ts +88 -1
- package/src/http/mcp.ts +15 -5
- package/src/http/page-proxy.ts +2 -4
- package/src/http/pages-public.ts +3 -3
- package/src/http/pages.ts +3 -13
- package/src/http/poll.ts +28 -68
- package/src/http/requested-by.ts +54 -0
- package/src/http/scripts.ts +1 -0
- package/src/http/stats.ts +1 -30
- package/src/http/tasks.ts +61 -5
- package/src/http/utils.ts +14 -0
- package/src/http/webhooks.ts +159 -1
- package/src/http/x.ts +1 -0
- package/src/integrations/chat-sdk/adapters.ts +129 -0
- package/src/integrations/chat-sdk/attachments.ts +232 -0
- package/src/integrations/chat-sdk/background.ts +36 -0
- package/src/integrations/chat-sdk/commands.ts +194 -0
- package/src/integrations/chat-sdk/config.ts +198 -0
- package/src/integrations/chat-sdk/discord-api.ts +63 -0
- package/src/integrations/chat-sdk/discord-gateway.ts +185 -0
- package/src/integrations/chat-sdk/handlers.ts +28 -0
- package/src/integrations/chat-sdk/inbound.ts +426 -0
- package/src/integrations/chat-sdk/limits.ts +48 -0
- package/src/integrations/chat-sdk/logger.ts +81 -0
- package/src/integrations/chat-sdk/outbound.ts +123 -0
- package/src/integrations/chat-sdk/relay.ts +172 -0
- package/src/integrations/chat-sdk/router.ts +169 -0
- package/src/integrations/chat-sdk/runtime.ts +351 -0
- package/src/integrations/chat-sdk/schema.ts +245 -0
- package/src/integrations/chat-sdk/state.ts +243 -0
- package/src/integrations/chat-sdk/status.ts +244 -0
- package/src/integrations/chat-sdk/steering.ts +58 -0
- package/src/integrations/chat-sdk/teams-origin.ts +77 -0
- package/src/integrations/chat-sdk/telegram-api.ts +56 -0
- package/src/integrations/chat-sdk/templates.ts +16 -0
- package/src/integrations/chat-sdk/test-hooks.ts +37 -0
- package/src/integrations/chat-sdk/thread-buffer.ts +100 -0
- package/src/integrations/chat-sdk/typing.ts +65 -0
- package/src/pages/version.ts +4 -5
- package/src/prompts/base-prompt.ts +111 -45
- package/src/providers/acp-target-catalog.ts +6 -0
- package/src/providers/acp-targets.ts +55 -2
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/claude-adapter.ts +1 -1
- package/src/providers/claude-flag-settings.ts +17 -5
- package/src/providers/claude-managed-adapter.ts +1 -1
- package/src/providers/claude-session-events.ts +6 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/opencode-adapter.ts +243 -26
- package/src/providers/opencode-swarm-plugin.ts +31 -0
- package/src/providers/pi-mono-adapter.ts +398 -153
- package/src/providers/pricing-sources.md +33 -2
- package/src/providers/reasoning-effort.ts +27 -0
- package/src/providers/types.ts +6 -2
- package/src/rbac/legacy-policy.ts +8 -2
- package/src/rbac/permissions.ts +21 -0
- package/src/scheduler/scheduler.ts +4 -2
- package/src/scripts-runtime/loader.ts +19 -7
- package/src/scripts-runtime/sdk-allowlist.ts +8 -6
- package/src/scripts-runtime/swarm-sdk.ts +0 -11
- package/src/scripts-runtime/types/stdlib.d.ts +27 -15
- package/src/scripts-runtime/types/swarm-extension.d.ts +63 -1
- package/src/scripts-runtime/types/swarm-sdk.d.ts +27 -15
- package/src/server.ts +52 -18
- package/src/slack/event-dedup.ts +1 -1
- package/src/tasks/additive-buffer.ts +4 -2
- package/src/tasks/context-key.ts +60 -1
- package/src/tasks/model-pin.ts +67 -0
- package/src/tasks/worker-follow-up.ts +4 -31
- package/src/tests/acp-adapter.test.ts +42 -0
- package/src/tests/api-drain-worker-e2e.test.ts +1 -6
- package/src/tests/base-prompt.test.ts +58 -54
- package/src/tests/chat-sdk-commands.test.ts +191 -0
- package/src/tests/chat-sdk-config.test.ts +334 -0
- package/src/tests/chat-sdk-discord-api.test.ts +68 -0
- package/src/tests/chat-sdk-discord-gateway.test.ts +252 -0
- package/src/tests/chat-sdk-discord-webhook.test.ts +126 -0
- package/src/tests/chat-sdk-fields-sync.test.ts +50 -0
- package/src/tests/chat-sdk-harness.ts +146 -0
- package/src/tests/chat-sdk-inbound.test.ts +363 -0
- package/src/tests/chat-sdk-relay.test.ts +470 -0
- package/src/tests/chat-sdk-reload.test.ts +128 -0
- package/src/tests/chat-sdk-shutdown.test.ts +220 -0
- package/src/tests/chat-sdk-slack.test.ts +268 -0
- package/src/tests/chat-sdk-state.test.ts +163 -0
- package/src/tests/chat-sdk-status-route.test.ts +382 -0
- package/src/tests/chat-sdk-teams.test.ts +277 -0
- package/src/tests/chat-sdk-tools.test.ts +520 -0
- package/src/tests/chat-sdk-typing.test.ts +120 -0
- package/src/tests/chat-sdk-webhook-telegram.test.ts +200 -0
- package/src/tests/claude-adapter-binary.test.ts +9 -4
- package/src/tests/claude-flag-settings.test.ts +11 -3
- package/src/tests/claude-managed-adapter.test.ts +1 -1
- package/src/tests/claude-sdk-lifecycle.test.ts +9 -3
- package/src/tests/claude-thinking-tokens.test.ts +57 -0
- package/src/tests/claude-worker-parity.test.ts +2 -7
- package/src/tests/config-env-export-chat-sdk.test.ts +11 -0
- package/src/tests/config-refs.test.ts +66 -0
- package/src/tests/context-key.test.ts +19 -0
- package/src/tests/cors-allowlist.test.ts +25 -0
- package/src/tests/create-page-tool.test.ts +18 -0
- package/src/tests/credential-check.test.ts +139 -0
- package/src/tests/credentials.test.ts +21 -0
- package/src/tests/db-runtime.test.ts +61 -0
- package/src/tests/db-task-read-contract.test.ts +20 -0
- package/src/tests/db-task-write-contract.test.ts +10 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/enabled-capabilities.test.ts +20 -0
- package/src/tests/entrypoint-api-readiness.test.ts +14 -8
- package/src/tests/entrypoint-config-env-export.test.ts +5 -2
- package/src/tests/entrypoint-export-to-env.test.ts +78 -0
- package/src/tests/explicit-model-validation.test.ts +105 -1
- package/src/tests/extensions-example-route-chat-channel.test.ts +154 -0
- package/src/tests/extensions-post-bridge.test.ts +51 -0
- package/src/tests/fetch-resolved-env.test.ts +128 -0
- package/src/tests/fixtures/chat-sdk/telegram-private-text.json +10 -0
- package/src/tests/fixtures/extensions/route-chat-channel-to-agent/hooks.ts +27 -0
- package/src/tests/fixtures/extensions/route-chat-channel-to-agent/manifest.json +9 -0
- package/src/tests/fixtures/openai-compatible-registry-extension.child.ts +146 -0
- package/src/tests/fixtures/runner-legacy-resume-prompt.ts +122 -0
- package/src/tests/github-handlers.test.ts +35 -0
- package/src/tests/harness-provider-resolution.test.ts +12 -0
- package/src/tests/heartbeat-checklist.test.ts +3 -2
- package/src/tests/hook-runtime-close.test.ts +0 -2
- package/src/tests/http-api-integration.test.ts +0 -25
- package/src/tests/migration-046-budgets.test.ts +17 -16
- package/src/tests/migration-217-chat-sdk-state.test.ts +113 -0
- package/src/tests/model-tier-resolution.test.ts +93 -0
- package/src/tests/models-catalog.test.ts +42 -1
- package/src/tests/openai-compatible-providers.test.ts +134 -0
- package/src/tests/openai-compatible-registry-extension.test.ts +42 -0
- package/src/tests/opencode-adapter.test.ts +216 -2
- package/src/tests/opencode-helmcode-sdk.test.ts +201 -0
- package/src/tests/opencode-shell-env.test.ts +145 -0
- package/src/tests/openrouter-base-url.test.ts +101 -151
- package/src/tests/page-proxy.test.ts +1 -1
- package/src/tests/pages-browser-sdk.test.ts +76 -0
- package/src/tests/pages-feedback.test.ts +1 -1
- package/src/tests/paused-tasks-requested-by.test.ts +102 -0
- package/src/tests/pi-custom-provider-fake-server.test.ts +442 -0
- package/src/tests/pi-mono-adapter.test.ts +385 -5
- package/src/tests/pi-shell-env.test.ts +170 -0
- package/src/tests/prompt-template-remaining.test.ts +35 -0
- package/src/tests/rbac-engine.test.ts +11 -3
- package/src/tests/runner-exit-after-tasks.test.ts +0 -4
- package/src/tests/runner-legacy-resume-prompt.test.ts +64 -0
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/scripts-http.test.ts +201 -1
- package/src/tests/scripts-only-gating.test.ts +2 -3
- package/src/tests/scripts-runtime.test.ts +58 -40
- package/src/tests/secret-scrubber.test.ts +86 -0
- package/src/tests/seed-skills-bundled-files.test.ts +45 -53
- package/src/tests/seed-skills-retire.test.ts +182 -0
- package/src/tests/send-task-requester-gate.test.ts +58 -0
- package/src/tests/session-cost-recompute-unit.test.ts +46 -0
- package/src/tests/skill-get-name-resolution.test.ts +137 -0
- package/src/tests/sonnet-5-5-registry.test.ts +9 -1
- package/src/tests/swarm-config-export-to-env.test.ts +267 -0
- package/src/tests/swarm-config-reserved-keys.test.ts +16 -0
- package/src/tests/swarm-diff.test.ts +1 -1
- package/src/tests/task-model-effort-rerun.test.ts +164 -0
- package/src/tests/task-session-prompts.test.ts +132 -0
- package/src/tests/task-tool-manifest.test.ts +14 -0
- package/src/tests/task-tools-ctx.test.ts +2 -0
- package/src/tests/tool-annotations.test.ts +6 -6
- package/src/tests/ui-logs-parser.test.ts +43 -0
- package/src/tests/used-by-route.test.ts +6 -0
- package/src/tests/vercel-ai-gateway.test.ts +149 -0
- package/src/tests/workflow-swarm-script.test.ts +38 -0
- package/src/tests/workflow-wait-filter.test.ts +5 -3
- package/src/tools/chat-access.ts +41 -0
- package/src/tools/chat-open-dm.ts +59 -0
- package/src/tools/chat-post.ts +74 -0
- package/src/tools/chat-read-thread.ts +100 -0
- package/src/tools/chat-reply.ts +75 -0
- package/src/tools/create-page.ts +3 -13
- package/src/tools/model-catalog/index.ts +2 -1
- package/src/tools/register-chat-channel.ts +139 -0
- package/src/tools/send-task.ts +24 -0
- package/src/tools/skills/skill-get.ts +17 -9
- package/src/tools/swarm-config/get-config.ts +2 -1
- package/src/tools/swarm-config/list-config.ts +2 -1
- package/src/tools/swarm-config/set-config.ts +16 -3
- package/src/tools/tool-config.ts +8 -6
- package/src/types.ts +12 -2
- package/src/utils/config-env-export.ts +74 -0
- package/src/utils/credentials.test.ts +92 -0
- package/src/utils/credentials.ts +52 -4
- package/src/utils/harness-provider.ts +15 -6
- package/src/utils/runtime-model-catalog.ts +11 -1
- package/src/utils/secret-scrubber.ts +71 -2
- package/src/utils/task-tool-manifest.ts +19 -2
- package/src/workflows/executors/swarm-script.ts +1 -0
- package/templates/skills/agent-browser/SKILL.md +1 -1
- package/templates/skills/agent-browser/content.md +1 -1
- package/templates/skills/apps/SKILL.md +8 -1
- package/templates/skills/apps/content.md +8 -1
- package/templates/skills/chat-sdk/config.json +14 -0
- package/templates/skills/chat-sdk/content.md +84 -0
- package/templates/skills/composio-gmail/content.md +7 -0
- package/templates/skills/composio-google-docs/content.md +1 -1
- package/templates/skills/email-domain-health/config.json +14 -0
- package/templates/skills/email-domain-health/content.md +131 -0
- package/templates/skills/kv-storage/content.md +2 -2
- package/templates/skills/pages/content.md +8 -10
- package/templates/skills/swarm-extensions/content.md +2 -0
- package/templates/skills/swarm-scripts/SKILL.md +4 -2
- package/templates/skills/swarm-scripts/config.json +1 -1
- package/templates/skills/swarm-scripts/content.md +4 -2
- package/templates/skills/workflow-iterate/content.md +38 -0
- package/dist/artifact-eczsknc3.js +0 -3843
- package/dist/cli-bd957dgb.js +0 -153
- package/dist/cli-q51yprfq.js +0 -1104
- package/dist/cli-t1ze64h3.js +0 -811
- package/dist/cli-tcc4wd5d.js +0 -56471
- package/dist/index-9ch1n761.js +0 -1102
- package/src/artifact-sdk/index.ts +0 -2
- package/src/artifact-sdk/localtunnel.d.ts +0 -20
- package/src/artifact-sdk/port.ts +0 -12
- package/src/artifact-sdk/server.ts +0 -183
- package/src/artifact-sdk/tunnel.ts +0 -19
- package/src/commands/artifact.ts +0 -249
- package/src/http/ecosystem.ts +0 -77
- package/src/tests/artifact-commands.test.ts +0 -92
- package/src/tests/artifact-sdk.test.ts +0 -793
- package/src/tests/register-service-security.test.ts +0 -194
- package/src/tools/list-services.ts +0 -107
- package/src/tools/register-service.ts +0 -177
- package/src/tools/unregister-service.ts +0 -73
- package/src/tools/update-service-status.ts +0 -96
- package/templates/skills/artifacts/config.json +0 -14
- package/templates/skills/artifacts/content.md +0 -344
- package/templates/skills/artifacts/files/examples/approval-flow.ts +0 -34
- package/templates/skills/artifacts/files/examples/hono-dashboard.ts +0 -31
- package/templates/skills/artifacts/files/examples/multi-artifact.ts +0 -20
- package/templates/skills/artifacts/files/examples/static-report.sh +0 -17
- /package/src/{artifact-sdk → pages}/browser-sdk.ts +0 -0
- /package/src/{artifact-sdk → pages}/feedback-overlay.ts +0 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
baseUrlEnvVar,
|
|
4
|
+
OPENAI_COMPATIBLE_PROVIDERS,
|
|
5
|
+
openAICompatibleProviderIds,
|
|
6
|
+
providerBaseUrl,
|
|
7
|
+
providerForKeyVar,
|
|
8
|
+
providerForModel,
|
|
9
|
+
providerKeyFromEnv,
|
|
10
|
+
withRegistryCatalogSections,
|
|
11
|
+
} from "@desplega/model-catalog";
|
|
12
|
+
import { loadModelsDevCache } from "../be/modelsdev-cache";
|
|
13
|
+
import snapshot from "../be/modelsdev-cache.json";
|
|
14
|
+
import { normalizeProviderKeyAliases } from "../utils/credentials";
|
|
15
|
+
|
|
16
|
+
type SnapshotModel = {
|
|
17
|
+
name: string;
|
|
18
|
+
reasoning?: boolean;
|
|
19
|
+
tool_call?: boolean;
|
|
20
|
+
modalities?: { input?: string[] };
|
|
21
|
+
limit?: { context?: number; output?: number };
|
|
22
|
+
};
|
|
23
|
+
type SnapshotProvider = {
|
|
24
|
+
api?: string;
|
|
25
|
+
env?: string[];
|
|
26
|
+
npm?: string;
|
|
27
|
+
models: Record<string, SnapshotModel>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
describe("openai-compatible provider table", () => {
|
|
31
|
+
test("providerForModel routes by exact prefix only", () => {
|
|
32
|
+
expect(providerForModel("nan/glm5.3-flash")?.id).toBe("nan");
|
|
33
|
+
expect(providerForModel("helmcode/gemma4")?.id).toBe("helmcode");
|
|
34
|
+
expect(providerForModel("openrouter/nan/glm5.3")).toBeUndefined();
|
|
35
|
+
expect(providerForModel("glm5.3")).toBeUndefined();
|
|
36
|
+
expect(providerForModel("nanx/glm5.3")).toBeUndefined();
|
|
37
|
+
expect(providerForModel("toString/x")).toBeUndefined();
|
|
38
|
+
expect(providerForModel(undefined)).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("providerForKeyVar finds canonical and alias key vars", () => {
|
|
42
|
+
expect(providerForKeyVar("NAN_API_KEY")?.id).toBe("nan");
|
|
43
|
+
expect(providerForKeyVar("HELMCODE_API_NAN")?.id).toBe("nan");
|
|
44
|
+
expect(providerForKeyVar("HELMCODE_API_KEY")?.id).toBe("helmcode");
|
|
45
|
+
expect(providerForKeyVar("OPENROUTER_API_KEY")).toBeUndefined();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("<ID>_BASE_URL overrides the default base URL", () => {
|
|
49
|
+
const nan = OPENAI_COMPATIBLE_PROVIDERS.nan;
|
|
50
|
+
const helmcode = OPENAI_COMPATIBLE_PROVIDERS.helmcode;
|
|
51
|
+
expect(baseUrlEnvVar(nan)).toBe("NAN_BASE_URL");
|
|
52
|
+
expect(baseUrlEnvVar(helmcode)).toBe("HELMCODE_BASE_URL");
|
|
53
|
+
expect(providerBaseUrl(nan, {})).toBe("https://api.nan.builders/v1");
|
|
54
|
+
expect(providerBaseUrl(nan, { NAN_BASE_URL: " " })).toBe("https://api.nan.builders/v1");
|
|
55
|
+
expect(providerBaseUrl(helmcode, { HELMCODE_BASE_URL: "http://127.0.0.1:9/v1/" })).toBe(
|
|
56
|
+
"http://127.0.0.1:9/v1",
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("providerKeyFromEnv prefers the canonical var, then the alias", () => {
|
|
61
|
+
const nan = OPENAI_COMPATIBLE_PROVIDERS.nan;
|
|
62
|
+
expect(providerKeyFromEnv(nan, { HELMCODE_API_NAN: "alias" })).toBe("alias");
|
|
63
|
+
expect(providerKeyFromEnv(nan, { NAN_API_KEY: "canon", HELMCODE_API_NAN: "alias" })).toBe(
|
|
64
|
+
"canon",
|
|
65
|
+
);
|
|
66
|
+
expect(providerKeyFromEnv(nan, { NAN_API_KEY: " " })).toBeUndefined();
|
|
67
|
+
expect(providerKeyFromEnv(OPENAI_COMPATIBLE_PROVIDERS.helmcode, { NAN_API_KEY: "k" })).toBe(
|
|
68
|
+
undefined,
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("normalizeProviderKeyAliases copies an alias only into an empty canonical var", () => {
|
|
73
|
+
const aliasOnly: Record<string, string | undefined> = { HELMCODE_API_NAN: "alias" };
|
|
74
|
+
normalizeProviderKeyAliases(aliasOnly);
|
|
75
|
+
expect(aliasOnly.NAN_API_KEY).toBe("alias");
|
|
76
|
+
const both: Record<string, string | undefined> = {
|
|
77
|
+
NAN_API_KEY: "canon",
|
|
78
|
+
HELMCODE_API_NAN: "alias",
|
|
79
|
+
};
|
|
80
|
+
normalizeProviderKeyAliases(both);
|
|
81
|
+
expect(both.NAN_API_KEY).toBe("canon");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("registry-sourced sections are synthesized, never hand-written in the snapshot", () => {
|
|
85
|
+
const raw = snapshot as unknown as Record<string, unknown>;
|
|
86
|
+
for (const id of openAICompatibleProviderIds()) {
|
|
87
|
+
const entry = OPENAI_COMPATIBLE_PROVIDERS[id];
|
|
88
|
+
if (entry.catalog === "registry") expect(raw[id]).toBeUndefined();
|
|
89
|
+
else expect(raw[id]).toBeDefined();
|
|
90
|
+
}
|
|
91
|
+
// The server loader carries the synthesized sections.
|
|
92
|
+
expect(loadModelsDevCache()?.helmcode?.models?.["glm5.3"]?.limit?.context).toBe(1048576);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// The registry duplicates the models.dev sections of `catalog: "models.dev"`
|
|
96
|
+
// entries (the pi/opencode blocks need limits the worker cannot read from
|
|
97
|
+
// the catalog store); keep the two from drifting. Registry-sourced sections
|
|
98
|
+
// are built from the registry, so they agree by construction.
|
|
99
|
+
const merged = withRegistryCatalogSections(
|
|
100
|
+
snapshot as unknown as Record<string, SnapshotProvider | undefined>,
|
|
101
|
+
);
|
|
102
|
+
for (const id of openAICompatibleProviderIds()) {
|
|
103
|
+
test(`${id} matches its catalog section`, () => {
|
|
104
|
+
const entry = OPENAI_COMPATIBLE_PROVIDERS[id];
|
|
105
|
+
const section = merged[id];
|
|
106
|
+
expect(section).toBeDefined();
|
|
107
|
+
if (!section) return;
|
|
108
|
+
// OpenCode's built-in AI Gateway SDK has no OpenAI-compatible `api` URL.
|
|
109
|
+
if (entry.npm === "@ai-sdk/gateway") expect(section.api).toBeUndefined();
|
|
110
|
+
else expect(section.api).toBe(entry.baseUrl);
|
|
111
|
+
expect(section.env).toEqual([entry.envVar]);
|
|
112
|
+
expect(section.npm).toBe(entry.npm);
|
|
113
|
+
for (const model of entry.models) {
|
|
114
|
+
const snap = section.models[model.id];
|
|
115
|
+
expect(snap, `${id}/${model.id} missing from snapshot`).toBeDefined();
|
|
116
|
+
if (!snap) continue;
|
|
117
|
+
expect({
|
|
118
|
+
name: model.name,
|
|
119
|
+
contextWindow: model.contextWindow,
|
|
120
|
+
maxOutputTokens: model.maxOutputTokens,
|
|
121
|
+
reasoning: model.reasoning,
|
|
122
|
+
input: [...model.input],
|
|
123
|
+
}).toEqual({
|
|
124
|
+
name: snap.name,
|
|
125
|
+
contextWindow: snap.limit?.context,
|
|
126
|
+
maxOutputTokens: snap.limit?.output,
|
|
127
|
+
reasoning: snap.reasoning,
|
|
128
|
+
input: (snap.modalities?.input ?? []).filter((m) => m === "text" || m === "image"),
|
|
129
|
+
});
|
|
130
|
+
expect(snap.tool_call).toBe(true);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proof that adding an OpenAI-compatible provider takes one registry entry:
|
|
3
|
+
* a fake `testgw` entry must reach every derived surface
|
|
4
|
+
* (`fixtures/openai-compatible-registry-extension.child.ts`).
|
|
5
|
+
*
|
|
6
|
+
* The proof mocks the registry module, and its consumers cache derivations of
|
|
7
|
+
* the table at import (integration cards, picker constants) that no mock
|
|
8
|
+
* restore can undo. It runs in its own `bun test` child so this process, and
|
|
9
|
+
* every file sharing it in a single-process run, keeps the real table.
|
|
10
|
+
*/
|
|
11
|
+
import { expect, test } from "bun:test";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { modelGroupsForHarness } from "../../apps/ui/src/lib/agent-runtime-models";
|
|
14
|
+
import { INTEGRATIONS } from "../../apps/ui/src/lib/integrations-catalog";
|
|
15
|
+
import { CHILD_PROCESS_TEST_BUDGET_MS, expectChildOk, runChild } from "./test-proc";
|
|
16
|
+
|
|
17
|
+
const REPO_ROOT = join(import.meta.dir, "../..");
|
|
18
|
+
const CHILD = "./src/tests/fixtures/openai-compatible-registry-extension.child.ts";
|
|
19
|
+
|
|
20
|
+
test(
|
|
21
|
+
"a testgw registry entry reaches every derived surface (child process)",
|
|
22
|
+
async () => {
|
|
23
|
+
const result = expectChildOk(
|
|
24
|
+
await runChild([process.execPath, "test", CHILD], {
|
|
25
|
+
cwd: REPO_ROOT,
|
|
26
|
+
env: { ...process.env },
|
|
27
|
+
}),
|
|
28
|
+
"registry extension proof",
|
|
29
|
+
);
|
|
30
|
+
// bun test reports to stderr; 0 fail and every case ran.
|
|
31
|
+
const report = `${result.stdout}\n${result.stderr}`;
|
|
32
|
+
expect(report).toMatch(/\b6 pass\b/);
|
|
33
|
+
expect(report).toMatch(/\b0 fail\b/);
|
|
34
|
+
},
|
|
35
|
+
CHILD_PROCESS_TEST_BUDGET_MS,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
test("this process still sees the real registry only", () => {
|
|
39
|
+
expect(INTEGRATIONS.some((def) => def.id === "testgw")).toBe(false);
|
|
40
|
+
const providers = modelGroupsForHarness("pi", [], {}).map((g) => g.provider);
|
|
41
|
+
expect(providers).not.toContain("TestGW");
|
|
42
|
+
});
|
|
@@ -7,18 +7,20 @@
|
|
|
7
7
|
* config wiring, steering, abort, and stream-death handling.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
|
|
10
|
+
import { afterAll, afterEach, beforeAll, describe, expect, spyOn, test } from "bun:test";
|
|
11
11
|
import type { OpenCodeEvent } from "@opencode/sdk";
|
|
12
12
|
import { normalizeSessionLogs, type SessionLogRecord } from "../../apps/ui/src/logs-parser";
|
|
13
13
|
import {
|
|
14
14
|
_setOpencodeSdkLoaderForTests,
|
|
15
15
|
_setOpencodeSessionHooksForTests,
|
|
16
16
|
applyOpenRouterBaseUrlOverride,
|
|
17
|
+
applyProviderKeySplice,
|
|
17
18
|
OpencodeAdapter,
|
|
18
19
|
type OpencodeSession,
|
|
19
20
|
resolveOpencodeSdkSpecifier,
|
|
20
21
|
} from "../providers/opencode-adapter";
|
|
21
22
|
import type { ProviderEvent, ProviderSessionConfig } from "../providers/types";
|
|
23
|
+
import { clearVolatileSecretsForTesting } from "../utils/secret-scrubber";
|
|
22
24
|
|
|
23
25
|
const SESSION_ID = "ses_test";
|
|
24
26
|
|
|
@@ -128,13 +130,14 @@ function makeFakeHost(events: OpenCodeEvent[], opts: FakeHostOptions = {}) {
|
|
|
128
130
|
return host;
|
|
129
131
|
}
|
|
130
132
|
|
|
131
|
-
function installHost(host: FakeHost) {
|
|
133
|
+
function installHost(host: FakeHost, onCreate?: (options: Record<string, unknown>) => void) {
|
|
132
134
|
_setOpencodeSdkLoaderForTests(async () => ({
|
|
133
135
|
version: "2.0.28",
|
|
134
136
|
sdk: {
|
|
135
137
|
OpenCode: {
|
|
136
138
|
create: async (options: Record<string, unknown>) => {
|
|
137
139
|
host.createOptions = options;
|
|
140
|
+
onCreate?.(options);
|
|
138
141
|
// Set the plugins up the way OpenCode does, with a stub context.
|
|
139
142
|
const plugins = options.plugins as Array<{ setup: (ctx: unknown) => Promise<void> }>;
|
|
140
143
|
for (const plugin of plugins) {
|
|
@@ -143,6 +146,8 @@ function installHost(host: FakeHost) {
|
|
|
143
146
|
hook: async () => {},
|
|
144
147
|
list: async () => host.tools.map((id) => ({ id })),
|
|
145
148
|
},
|
|
149
|
+
// Registered when the session config carries a per-task env.
|
|
150
|
+
shell: { hook: async () => {} },
|
|
146
151
|
session: {
|
|
147
152
|
hook: async (
|
|
148
153
|
name: string,
|
|
@@ -841,3 +846,212 @@ describe("OpenRouter attribution config", () => {
|
|
|
841
846
|
expect(config).toEqual({});
|
|
842
847
|
});
|
|
843
848
|
});
|
|
849
|
+
|
|
850
|
+
describe("provider key splice (one picked key per task)", () => {
|
|
851
|
+
const PICKED = "sk-picked-member-0123456789";
|
|
852
|
+
const POOL = "sk-pool-member-aaaaaaaaaa,sk-pool-member-bbbbbbbbbb";
|
|
853
|
+
const SPLICE_VARS = [
|
|
854
|
+
"NAN_API_KEY",
|
|
855
|
+
"HELMCODE_API_NAN",
|
|
856
|
+
"HELMCODE_API_KEY",
|
|
857
|
+
"OPENROUTER_API_KEY",
|
|
858
|
+
"OPENROUTER_BASE_URL",
|
|
859
|
+
];
|
|
860
|
+
const saved: Record<string, string | undefined> = {};
|
|
861
|
+
beforeAll(() => {
|
|
862
|
+
for (const key of SPLICE_VARS) saved[key] = process.env[key];
|
|
863
|
+
});
|
|
864
|
+
afterEach(() => {
|
|
865
|
+
for (const key of SPLICE_VARS) {
|
|
866
|
+
if (saved[key] === undefined) delete process.env[key];
|
|
867
|
+
else process.env[key] = saved[key];
|
|
868
|
+
}
|
|
869
|
+
clearVolatileSecretsForTesting();
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
test("nan: env [] and the per-task key, even with a pool in process.env", async () => {
|
|
873
|
+
process.env.NAN_API_KEY = POOL;
|
|
874
|
+
const { host } = await runSession(
|
|
875
|
+
[succeeded],
|
|
876
|
+
testConfig({ model: "nan/glm5.3-flash", env: { NAN_API_KEY: PICKED } }),
|
|
877
|
+
);
|
|
878
|
+
const nan = configOf(host).provider.nan;
|
|
879
|
+
expect(nan.env).toEqual([]);
|
|
880
|
+
expect(nan.options.apiKey).toBe(PICKED);
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
test("nan: the HELMCODE_API_NAN alias is read when NAN_API_KEY is empty", () => {
|
|
884
|
+
const config = {};
|
|
885
|
+
const picked = applyProviderKeySplice(config, "nan/qwen3.6", { HELMCODE_API_NAN: PICKED });
|
|
886
|
+
expect(picked).toEqual({ key: PICKED, name: "HELMCODE_API_NAN" });
|
|
887
|
+
expect(config).toEqual({ provider: { nan: { env: [], options: { apiKey: PICKED } } } });
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
test("a pool that reaches the splice unpicked yields its first member", () => {
|
|
891
|
+
const config = {};
|
|
892
|
+
applyProviderKeySplice(config, "nan/qwen3.6", {
|
|
893
|
+
NAN_API_KEY: ` ${PICKED} , sk-other-0000000000`,
|
|
894
|
+
});
|
|
895
|
+
expect(config).toEqual({ provider: { nan: { env: [], options: { apiKey: PICKED } } } });
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
test("helmcode: full custom block with limits, reasoning options merged in", async () => {
|
|
899
|
+
const { host } = await runSession(
|
|
900
|
+
[succeeded],
|
|
901
|
+
testConfig({
|
|
902
|
+
model: "helmcode/glm5.3",
|
|
903
|
+
reasoningEffort: "high",
|
|
904
|
+
env: { HELMCODE_API_KEY: PICKED },
|
|
905
|
+
}),
|
|
906
|
+
);
|
|
907
|
+
const helmcode = configOf(host).provider.helmcode;
|
|
908
|
+
expect(helmcode).toMatchObject({
|
|
909
|
+
npm: "@ai-sdk/openai-compatible",
|
|
910
|
+
name: "Helmcode",
|
|
911
|
+
env: [],
|
|
912
|
+
options: { baseURL: "https://api.helmcode.com/v1", apiKey: PICKED },
|
|
913
|
+
});
|
|
914
|
+
expect(helmcode.models["glm5.3"]).toEqual({
|
|
915
|
+
name: "GLM-5.3",
|
|
916
|
+
tool_call: true,
|
|
917
|
+
reasoning: true,
|
|
918
|
+
limit: { context: 1048576, output: 32768 },
|
|
919
|
+
options: { reasoningEffort: "high" },
|
|
920
|
+
});
|
|
921
|
+
expect(Object.keys(helmcode.models)).toEqual([
|
|
922
|
+
"deepseek-v4-flash",
|
|
923
|
+
"glm5.3",
|
|
924
|
+
"glm5.3-flash",
|
|
925
|
+
"qwen3.6",
|
|
926
|
+
"gemma4",
|
|
927
|
+
]);
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
test("helmcode: a model missing from the table still gets an entry", () => {
|
|
931
|
+
const config: Record<string, any> = {};
|
|
932
|
+
applyProviderKeySplice(config, "helmcode/brand-new", { HELMCODE_API_KEY: PICKED });
|
|
933
|
+
expect(config.provider.helmcode.models["brand-new"]).toEqual({
|
|
934
|
+
name: "brand-new",
|
|
935
|
+
tool_call: true,
|
|
936
|
+
});
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
test("openrouter gateway: baseURL kept next to env [] and the picked key", async () => {
|
|
940
|
+
process.env.OPENROUTER_API_KEY = POOL;
|
|
941
|
+
const { host } = await runSession(
|
|
942
|
+
[succeeded],
|
|
943
|
+
testConfig({
|
|
944
|
+
model: "openrouter/deepseek/deepseek-v4-flash",
|
|
945
|
+
env: { OPENROUTER_API_KEY: PICKED, OPENROUTER_BASE_URL: "http://127.0.0.1:1234/v1" },
|
|
946
|
+
}),
|
|
947
|
+
);
|
|
948
|
+
expect(configOf(host).provider.openrouter).toEqual({
|
|
949
|
+
env: [],
|
|
950
|
+
options: { apiKey: PICKED, baseURL: "http://127.0.0.1:1234/v1" },
|
|
951
|
+
});
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
test("openrouter host: attribution headers kept next to env [] and the picked key", async () => {
|
|
955
|
+
const { host } = await runSession(
|
|
956
|
+
[succeeded],
|
|
957
|
+
testConfig({
|
|
958
|
+
model: "openrouter/deepseek/deepseek-v4-flash",
|
|
959
|
+
env: { OPENROUTER_API_KEY: PICKED },
|
|
960
|
+
}),
|
|
961
|
+
);
|
|
962
|
+
const openrouter = configOf(host).provider.openrouter;
|
|
963
|
+
expect(openrouter.env).toEqual([]);
|
|
964
|
+
expect(openrouter.options.apiKey).toBe(PICKED);
|
|
965
|
+
expect(openrouter.options.headers).toMatchObject({ "X-Title": "Agent Swarm" });
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
test("no splice without a key, or for providers outside the splice", () => {
|
|
969
|
+
for (const model of ["nan/qwen3.6", "helmcode/glm5.3", "openrouter/x/y"]) {
|
|
970
|
+
const config = {};
|
|
971
|
+
expect(applyProviderKeySplice(config, model, {})).toBeUndefined();
|
|
972
|
+
expect(config).toEqual({});
|
|
973
|
+
}
|
|
974
|
+
for (const model of ["anthropic/claude-sonnet-4-5", "openai/gpt-5", "glm5.3", "", undefined]) {
|
|
975
|
+
const config = {};
|
|
976
|
+
applyProviderKeySplice(config, model, {
|
|
977
|
+
ANTHROPIC_API_KEY: PICKED,
|
|
978
|
+
OPENAI_API_KEY: PICKED,
|
|
979
|
+
NAN_API_KEY: PICKED,
|
|
980
|
+
});
|
|
981
|
+
expect(config).toEqual({});
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
describe("spliced key never reaches logs, events, or errors", () => {
|
|
987
|
+
// Not in process.env: the key a dashboard save delivers to the per-task env only.
|
|
988
|
+
const SENTINEL = "sk-nan-sentinel-do-not-log-4f9c2e";
|
|
989
|
+
let captured: string[] = [];
|
|
990
|
+
let spies: Array<ReturnType<typeof spyOn>> = [];
|
|
991
|
+
beforeAll(() => {
|
|
992
|
+
delete process.env.NAN_API_KEY;
|
|
993
|
+
});
|
|
994
|
+
afterEach(() => {
|
|
995
|
+
for (const spy of spies) spy.mockRestore();
|
|
996
|
+
spies = [];
|
|
997
|
+
captured = [];
|
|
998
|
+
clearVolatileSecretsForTesting();
|
|
999
|
+
});
|
|
1000
|
+
function captureConsole() {
|
|
1001
|
+
for (const method of ["log", "info", "warn", "error", "debug"] as const) {
|
|
1002
|
+
spies.push(
|
|
1003
|
+
spyOn(console, method).mockImplementation((...args: unknown[]) => {
|
|
1004
|
+
captured.push(args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" "));
|
|
1005
|
+
}),
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
test("SDK log lines and a provider failure that echo the config are scrubbed", async () => {
|
|
1011
|
+
captureConsole();
|
|
1012
|
+
const host = makeFakeHost([
|
|
1013
|
+
ev("session.execution.failed", {
|
|
1014
|
+
sessionID: SESSION_ID,
|
|
1015
|
+
error: { message: `401 Invalid API key ${SENTINEL}` },
|
|
1016
|
+
}),
|
|
1017
|
+
]);
|
|
1018
|
+
installHost(host, (options) => {
|
|
1019
|
+
const content = (options.config as { content: string }).content;
|
|
1020
|
+
const log = options.log as { emit: (entry: { message: string }) => void };
|
|
1021
|
+
log.emit({ message: `config: ${content}` });
|
|
1022
|
+
});
|
|
1023
|
+
const session = (await new OpencodeAdapter().createSession(
|
|
1024
|
+
testConfig({ model: "nan/glm5.3-flash", env: { NAN_API_KEY: SENTINEL } }),
|
|
1025
|
+
)) as OpencodeSession;
|
|
1026
|
+
const emitted: ProviderEvent[] = [];
|
|
1027
|
+
session.onEvent((e) => emitted.push(e));
|
|
1028
|
+
const result = await session.waitForCompletion();
|
|
1029
|
+
|
|
1030
|
+
// Positive control: the key did reach the SDK config.
|
|
1031
|
+
expect(configOf(host).provider.nan.options.apiKey).toBe(SENTINEL);
|
|
1032
|
+
expect(captured.some((line) => line.includes("[opencode] config:"))).toBe(true);
|
|
1033
|
+
expect(result.isError).toBe(true);
|
|
1034
|
+
|
|
1035
|
+
for (const line of captured) expect(line).not.toContain(SENTINEL);
|
|
1036
|
+
expect(JSON.stringify(emitted)).not.toContain(SENTINEL);
|
|
1037
|
+
expect(result.failureReason ?? "").not.toContain(SENTINEL);
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
test("a host start failure that echoes the config is scrubbed before it propagates", async () => {
|
|
1041
|
+
captureConsole();
|
|
1042
|
+
const host = makeFakeHost([]);
|
|
1043
|
+
installHost(host, (options) => {
|
|
1044
|
+
const content = (options.config as { content: string }).content;
|
|
1045
|
+
throw new Error(`invalid config ${content}`);
|
|
1046
|
+
});
|
|
1047
|
+
const error = await new OpencodeAdapter()
|
|
1048
|
+
.createSession(testConfig({ model: "nan/glm5.3-flash", env: { NAN_API_KEY: SENTINEL } }))
|
|
1049
|
+
.then(
|
|
1050
|
+
() => undefined,
|
|
1051
|
+
(err: unknown) => err as Error,
|
|
1052
|
+
);
|
|
1053
|
+
expect(error?.message).toStartWith("opencode host failed to start: invalid config");
|
|
1054
|
+
expect(error?.message).not.toContain(SENTINEL);
|
|
1055
|
+
for (const line of captured) expect(line).not.toContain(SENTINEL);
|
|
1056
|
+
});
|
|
1057
|
+
});
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The provider key splice and the `helmcode` block, run through the real
|
|
3
|
+
* `@opencode/sdk` embedded host against a fake OpenAI-compatible server.
|
|
4
|
+
*
|
|
5
|
+
* `applyProviderKeySplice` builds the provider entry; the test only repoints
|
|
6
|
+
* `options.baseURL` at a local `Bun.serve({ port: 0 })` that answers
|
|
7
|
+
* `/v1/chat/completions`, then checks the request OpenCode sent: the picked
|
|
8
|
+
* pool member as the bearer (not the comma pool sitting in `process.env`) and
|
|
9
|
+
* the bare model id. The model catalog is bundled in the SDK, and no request
|
|
10
|
+
* leaves the machine. The control test pins why `env: []` is needed: without
|
|
11
|
+
* it, the pool in `process.env` beats `options.apiKey`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
15
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { OpenCode, type OpenCodeEvent } from "@opencode/sdk";
|
|
19
|
+
import { applyProviderKeySplice, type OpencodeConfig } from "../providers/opencode-adapter";
|
|
20
|
+
|
|
21
|
+
const TEST_TIMEOUT_MS = 60_000;
|
|
22
|
+
|
|
23
|
+
const PICKED = "sk-helmcode-picked-live-0123456789";
|
|
24
|
+
const POOL_IN_PROCESS_ENV = "sk-helmcode-wrong-aaaaaaaaaa,sk-helmcode-wrong-bbbbbbbbbb";
|
|
25
|
+
|
|
26
|
+
type SeenRequest = { path: string; authorization: string | null; model?: string };
|
|
27
|
+
|
|
28
|
+
let server: ReturnType<typeof Bun.serve>;
|
|
29
|
+
let directory: string;
|
|
30
|
+
const seen: SeenRequest[] = [];
|
|
31
|
+
let prevHelmcodeKey: string | undefined;
|
|
32
|
+
|
|
33
|
+
/** One streamed (or plain) assistant turn that says "pong". */
|
|
34
|
+
function completion(model: string, stream: boolean): Response {
|
|
35
|
+
const base = { id: "chatcmpl-test", created: 0, model };
|
|
36
|
+
const usage = { prompt_tokens: 5, completion_tokens: 1, total_tokens: 6 };
|
|
37
|
+
if (!stream) {
|
|
38
|
+
return Response.json({
|
|
39
|
+
...base,
|
|
40
|
+
object: "chat.completion",
|
|
41
|
+
choices: [
|
|
42
|
+
{
|
|
43
|
+
index: 0,
|
|
44
|
+
message: { role: "assistant", content: "pong" },
|
|
45
|
+
finish_reason: "stop",
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
usage,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const chunk = (body: Record<string, unknown>) =>
|
|
52
|
+
`data: ${JSON.stringify({ ...base, object: "chat.completion.chunk", ...body })}\n\n`;
|
|
53
|
+
const sse =
|
|
54
|
+
chunk({
|
|
55
|
+
choices: [{ index: 0, delta: { role: "assistant", content: "pong" }, finish_reason: null }],
|
|
56
|
+
}) +
|
|
57
|
+
chunk({ choices: [{ index: 0, delta: {}, finish_reason: "stop" }], usage }) +
|
|
58
|
+
"data: [DONE]\n\n";
|
|
59
|
+
return new Response(sse, { headers: { "content-type": "text/event-stream" } });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
beforeAll(() => {
|
|
63
|
+
directory = mkdtempSync(join(tmpdir(), "opencode-helmcode-sdk-"));
|
|
64
|
+
server = Bun.serve({
|
|
65
|
+
port: 0,
|
|
66
|
+
async fetch(req) {
|
|
67
|
+
const url = new URL(req.url);
|
|
68
|
+
const body = req.method === "POST" ? ((await req.json().catch(() => ({}))) as any) : {};
|
|
69
|
+
seen.push({
|
|
70
|
+
path: url.pathname,
|
|
71
|
+
authorization: req.headers.get("authorization"),
|
|
72
|
+
model: body?.model,
|
|
73
|
+
});
|
|
74
|
+
if (url.pathname.endsWith("/chat/completions")) {
|
|
75
|
+
return completion(String(body?.model ?? ""), body?.stream === true);
|
|
76
|
+
}
|
|
77
|
+
return new Response("not found", { status: 404 });
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
// The pool a production entrypoint exports raw; the splice must beat it.
|
|
81
|
+
prevHelmcodeKey = process.env.HELMCODE_API_KEY;
|
|
82
|
+
process.env.HELMCODE_API_KEY = POOL_IN_PROCESS_ENV;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
afterAll(() => {
|
|
86
|
+
server.stop(true);
|
|
87
|
+
rmSync(directory, { recursive: true, force: true });
|
|
88
|
+
if (prevHelmcodeKey === undefined) delete process.env.HELMCODE_API_KEY;
|
|
89
|
+
else process.env.HELMCODE_API_KEY = prevHelmcodeKey;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
async function runOnePrompt(config: OpencodeConfig): Promise<string> {
|
|
93
|
+
const host = await OpenCode.create({
|
|
94
|
+
config: { content: JSON.stringify(config) },
|
|
95
|
+
plugins: [],
|
|
96
|
+
log: { level: "error", emit: () => {} },
|
|
97
|
+
});
|
|
98
|
+
try {
|
|
99
|
+
const session = await host.sessions.create({ location: { directory } });
|
|
100
|
+
const done = (async () => {
|
|
101
|
+
for await (const event of host.events.subscribe() as AsyncIterable<OpenCodeEvent>) {
|
|
102
|
+
const data = (event as { data?: { sessionID?: string; error?: unknown } }).data;
|
|
103
|
+
if (data?.sessionID !== session.id) continue;
|
|
104
|
+
if (event.type === "session.execution.succeeded") return "succeeded";
|
|
105
|
+
if (event.type === "session.execution.failed") {
|
|
106
|
+
return `failed: ${JSON.stringify(data.error)}`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return "stream ended";
|
|
110
|
+
})();
|
|
111
|
+
await host.sessions.prompt({ sessionID: session.id, text: "say pong", delivery: "queue" });
|
|
112
|
+
return await done;
|
|
113
|
+
} finally {
|
|
114
|
+
await host.close();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
describe("provider key splice through the real OpenCode host", () => {
|
|
119
|
+
test(
|
|
120
|
+
"sends the picked key and the bare model id to the provider's baseURL",
|
|
121
|
+
async () => {
|
|
122
|
+
seen.length = 0;
|
|
123
|
+
const model = "helmcode/glm5.3";
|
|
124
|
+
const config: OpencodeConfig = { model };
|
|
125
|
+
const spliced = applyProviderKeySplice(config, model, { HELMCODE_API_KEY: PICKED });
|
|
126
|
+
expect(spliced?.name).toBe("HELMCODE_API_KEY");
|
|
127
|
+
const helmcode = config.provider?.helmcode;
|
|
128
|
+
if (!helmcode?.options) throw new Error("helmcode block missing");
|
|
129
|
+
helmcode.options.baseURL = `http://127.0.0.1:${server.port}/v1`;
|
|
130
|
+
|
|
131
|
+
const outcome = await runOnePrompt(config);
|
|
132
|
+
|
|
133
|
+
const chats = seen.filter((r) => r.path === "/v1/chat/completions");
|
|
134
|
+
expect(chats.length).toBeGreaterThan(0);
|
|
135
|
+
for (const chat of chats) {
|
|
136
|
+
expect(chat.authorization).toBe(`Bearer ${PICKED}`);
|
|
137
|
+
expect(chat.model).toBe("glm5.3");
|
|
138
|
+
}
|
|
139
|
+
expect(outcome).toBe("succeeded");
|
|
140
|
+
},
|
|
141
|
+
TEST_TIMEOUT_MS,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
test(
|
|
145
|
+
"nan (built-in provider): env [] plus the picked key beats a pool in process.env",
|
|
146
|
+
async () => {
|
|
147
|
+
const prevNan = process.env.NAN_API_KEY;
|
|
148
|
+
process.env.NAN_API_KEY = POOL_IN_PROCESS_ENV;
|
|
149
|
+
seen.length = 0;
|
|
150
|
+
try {
|
|
151
|
+
const model = "nan/glm5.3-flash";
|
|
152
|
+
const config: OpencodeConfig = { model };
|
|
153
|
+
const spliced = applyProviderKeySplice(config, model, { NAN_API_KEY: PICKED });
|
|
154
|
+
expect(spliced?.name).toBe("NAN_API_KEY");
|
|
155
|
+
const nan = config.provider?.nan;
|
|
156
|
+
if (!nan?.options) throw new Error("nan block missing");
|
|
157
|
+
nan.options.baseURL = `http://127.0.0.1:${server.port}/v1`;
|
|
158
|
+
|
|
159
|
+
const outcome = await runOnePrompt(config);
|
|
160
|
+
|
|
161
|
+
const chats = seen.filter((r) => r.path === "/v1/chat/completions");
|
|
162
|
+
expect(chats.length).toBeGreaterThan(0);
|
|
163
|
+
for (const chat of chats) {
|
|
164
|
+
expect(chat.authorization).toBe(`Bearer ${PICKED}`);
|
|
165
|
+
expect(chat.model).toBe("glm5.3-flash");
|
|
166
|
+
}
|
|
167
|
+
expect(outcome).toBe("succeeded");
|
|
168
|
+
} finally {
|
|
169
|
+
if (prevNan === undefined) delete process.env.NAN_API_KEY;
|
|
170
|
+
else process.env.NAN_API_KEY = prevNan;
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
TEST_TIMEOUT_MS,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
test(
|
|
177
|
+
"nan control: without the splice the pool in process.env reaches the provider",
|
|
178
|
+
async () => {
|
|
179
|
+
const prevNan = process.env.NAN_API_KEY;
|
|
180
|
+
process.env.NAN_API_KEY = POOL_IN_PROCESS_ENV;
|
|
181
|
+
seen.length = 0;
|
|
182
|
+
try {
|
|
183
|
+
const model = "nan/glm5.3-flash";
|
|
184
|
+
const config: OpencodeConfig = {
|
|
185
|
+
model,
|
|
186
|
+
provider: {
|
|
187
|
+
nan: { options: { apiKey: PICKED, baseURL: `http://127.0.0.1:${server.port}/v1` } },
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
await runOnePrompt(config);
|
|
191
|
+
const chats = seen.filter((r) => r.path === "/v1/chat/completions");
|
|
192
|
+
expect(chats.length).toBeGreaterThan(0);
|
|
193
|
+
expect(chats[0]?.authorization).not.toBe(`Bearer ${PICKED}`);
|
|
194
|
+
} finally {
|
|
195
|
+
if (prevNan === undefined) delete process.env.NAN_API_KEY;
|
|
196
|
+
else process.env.NAN_API_KEY = prevNan;
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
TEST_TIMEOUT_MS,
|
|
200
|
+
);
|
|
201
|
+
});
|