@desplega.ai/agent-swarm 1.172.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-fpecx1g1.js → acp-adapter-sj2g0m84.js} +18 -17
- package/dist/{actions-pw87a1a8.js → actions-fegk44gr.js} +65 -35
- package/dist/{amp-adapter-8jem2bc0.js → amp-adapter-ad8ft89x.js} +8 -7
- package/dist/{anthropic-messages-8ee2f22q.js → anthropic-messages-r6czkjv1.js} +7 -7
- package/dist/{app-wmf0m843.js → app-kfvgbvzy.js} +19 -13
- package/dist/{assistant-emp1e108.js → assistant-z7zdy9ja.js} +65 -38
- package/dist/{boot-reembed-1de4f4aj.js → boot-reembed-5c2z0mbe.js} +11 -9
- package/dist/{boot-reembed-4ymn9atn.js → boot-reembed-j8xa181e.js} +11 -9
- package/dist/{boot-scrub-sweep-adhk1s8c.js → boot-scrub-sweep-hnkn3rgt.js} +12 -10
- package/dist/{boot-seeds-8q637aew.js → boot-seeds-kwjq4d2z.js} +15 -13
- package/dist/{claude-adapter-t26cv8a4.js → claude-adapter-9vmvpvgd.js} +20 -19
- 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-gsfsh662.js → claude-sdk-session-9jye6cxr.js} +12698 -11995
- package/dist/{cli-k46r9hmk.js → cli-03gedhng.js} +16 -14
- package/dist/{cli-kb4ehzx5.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-8vs9trd4.js → cli-3dj8k4vy.js} +1 -1
- package/dist/{cli-zxm9p3x4.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-6jb2rf77.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-pj6x9qce.js → cli-6jyj59nb.js} +2 -2
- package/dist/{cli-388egyf6.js → cli-6razj6t4.js} +2 -2
- package/dist/{cli-qsztpfry.js → cli-74ahz61x.js} +2 -2
- package/dist/{cli-4nz8p1p5.js → cli-7x0g4sn1.js} +15474 -16082
- 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-bf7485pm.js → cli-apx87jne.js} +2 -2
- package/dist/{cli-ztrv9y21.js → cli-bzszjf2p.js} +3689 -1081
- 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-1hgat6pa.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-j6kpj9az.js → cli-ee9gtz70.js} +1553 -68
- package/dist/{cli-e0a9sk00.js → cli-eejsgvm4.js} +4 -4
- package/dist/{cli-tgpfnm6r.js → cli-ef1hg1wv.js} +518 -59
- 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-4fnb6884.js → cli-hvqbtna4.js} +7 -1259
- package/dist/{cli-pstq9888.js → cli-j0pfptdp.js} +220 -103
- package/dist/cli-jta2eyzc.js +619 -0
- package/dist/{cli-esnpehf5.js → cli-k382tshb.js} +12 -13
- package/dist/cli-k3dp5tpf.js +263 -0
- package/dist/{cli-wd0hz6ah.js → cli-kbrt8gqa.js} +9 -91
- package/dist/cli-kceah1eh.js +92 -0
- package/dist/{cli-prrt9fa0.js → cli-ke896fcf.js} +10 -10
- package/dist/{cli-nv4zd0b9.js → cli-kt6zqg0e.js} +6 -6
- package/dist/{cli-g6t9texm.js → cli-m50t4309.js} +5 -18
- package/dist/cli-n2wq4j7m.js +6985 -0
- package/dist/{cli-c2pq2rjb.js → cli-n3hzg89b.js} +46 -15
- package/dist/{cli-tmhsxhb8.js → cli-n5vwg842.js} +23 -6
- package/dist/{cli-5rv1j9ga.js → cli-ne236afy.js} +53 -7
- package/dist/cli-p0sg24zp.js +16 -0
- package/dist/{cli-d161g69q.js → cli-p1x85d2t.js} +50 -2
- package/dist/{cli-7pz3b7x5.js → cli-przpmdgr.js} +25 -0
- package/dist/{cli-g4f5pj26.js → cli-q0ke1yww.js} +111 -25
- package/dist/{cli-e3pmyt39.js → cli-q50942rs.js} +1 -1
- package/dist/{cli-r44t9wqc.js → cli-r106725k.js} +2 -2
- package/dist/{cli-gj81e47t.js → cli-r2pnvj8g.js} +2 -2
- package/dist/{cli-r8smj8b8.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-nyhb1r6x.js → cli-s2sp957d.js} +52 -29
- package/dist/{cli-1x7da0de.js → cli-s4setq91.js} +8 -8
- package/dist/{cli-xczpdp9e.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-cf69s3ew.js → cli-tr4sxp68.js} +3 -3
- package/dist/{cli-8hzfb6xz.js → cli-tsde1hdr.js} +15 -15
- package/dist/{cli-cceskcvt.js → cli-txqb9s38.js} +8 -8
- package/dist/{cli-aspcaef3.js → cli-v1yb0rqb.js} +6 -6
- package/dist/{cli-j1pmc07z.js → cli-vatyhshy.js} +268 -40
- package/dist/{cli-04emfe2q.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-hwwvy36w.js → cli-wayw93nt.js} +3 -3
- package/dist/cli-wmgrkscj.js +23254 -0
- package/dist/{cli-gp1rtyvk.js → cli-z5jjpaxw.js} +29 -6
- package/dist/{cli-451ks6aw.js → cli-zkpks3r3.js} +1 -1
- package/dist/cli.js +32 -48
- package/dist/{codex-adapter-m4tgdysy.js → codex-adapter-y81sssk0.js} +16 -15
- package/dist/{codex-login-841kv5fk.js → codex-login-f2rcfpp9.js} +3 -2
- package/dist/{codex-session-runner-ymwzm6ea.js → codex-session-runner-8vx4j6a9.js} +16 -15
- package/dist/{commands-n0hshcvs.js → commands-kcpvpcrr.js} +21 -10
- package/dist/{controller-hq5ggpcp.js → controller-ywrmt6yd.js} +5 -4
- package/dist/{controllers-connect-y07cqcvy.js → controllers-connect-kwpvj3rf.js} +45 -38
- package/dist/{cursor-adapter-q8pgv5qa.js → cursor-adapter-cehv1qns.js} +7 -6
- package/dist/{db-hkqvrdcb.js → db-k8ankj20.js} +13 -11
- package/dist/{db-4w7tmt1g.js → db-ww5d7jq3.js} +29 -9
- package/dist/{devin-adapter-xgtj680j.js → devin-adapter-x1022qb1.js} +3 -2
- package/dist/{dispatcher-5sd69y14.js → dispatcher-e8gq5m68.js} +41 -34
- package/dist/{docker-f503k9p0.js → docker-ff6y1190.js} +3 -2
- package/dist/{dsh-adapter-2h8m4cab.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-h3359cwn.js +52 -0
- package/dist/{google-generative-ai-7pavrbv7.js → google-generative-ai-g8db7x0f.js} +5 -3
- package/dist/{google-vertex-3f1jk41f.js → google-vertex-r2692q0r.js} +5 -3
- package/dist/{grok-adapter-8m9yqzbq.js → grok-adapter-0d4x7gkx.js} +19 -18
- package/dist/{handlers-s20xr10r.js → handlers-an1gzv21.js} +44 -37
- package/dist/{heartbeat-3k31q4g2.js → heartbeat-6dakeb61.js} +46 -39
- package/dist/{hook-gdn6a2fn.js → hook-6v6cv6d5.js} +12 -24
- package/dist/{hook-ygwerrdh.js → hook-sa5xye4y.js} +13 -12
- package/dist/{http-jdteeg81.js → http-r7f3eky0.js} +1598 -279
- package/dist/index-52j1aja4.js +3465 -0
- package/dist/{index-e0snym4t.js → index-7ep4gzdk.js} +41 -34
- 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-vm3bm9t7.js → index-rmpw9a8q.js} +41 -34
- package/dist/{index-6ksewd5y.js → index-s2wxgzr0.js} +1015 -732
- package/dist/{index-tsszt7g9.js → index-v1v6n7hg.js} +46 -39
- package/dist/index-we27kd31.js +12810 -0
- package/dist/{index-ffqzqgwb.js → index-zk96aq1k.js} +11 -9
- package/dist/{keepalive-jekc896t.js → keepalive-be9vwy5h.js} +13 -11
- package/dist/{lead-4k9s9szx.js → lead-0861cwrf.js} +64 -57
- package/dist/{maintenance-shazch4n.js → maintenance-mzzgc8bn.js} +11 -9
- package/dist/node-c9we95xf.js +1 -1
- package/dist/{node-fkajaq96.js → node-mn1zs7an.js} +2 -1
- package/dist/{oauth-refresh-sweep-5gb36kn8.js → oauth-refresh-sweep-cpdnfwdj.js} +12 -10
- package/dist/{onboard-ydrbnvww.js → onboard-fh8qs4r3.js} +11 -3
- package/dist/{opencode-adapter-5zh0he20.js → opencode-adapter-nm42wwj8.js} +19 -14
- package/dist/{otel-impl-6pf5cv86.js → otel-impl-x6c1c9g4.js} +248 -869
- package/dist/{pi-mono-adapter-ef9wwx9m.js → pi-mono-adapter-9n2ya240.js} +245 -107
- package/dist/{pricing-refresh-s52nsa5q.js → pricing-refresh-qb0rv0ap.js} +13 -11
- package/dist/{rbac-roles-aq3msh4e.js → rbac-roles-1gzdwbh1.js} +12 -10
- package/dist/{rbac-roles-q5gb5e8v.js → rbac-roles-3mraey52.js} +13 -11
- package/dist/{render-v2-pd1rg54q.js → render-v2-agpa45zz.js} +19 -13
- package/dist/{resume-gw4a2c6v.js → resume-x0ydy9s9.js} +27 -21
- package/dist/{runtime-qfry88v2.js → runtime-sa2vr16g.js} +2 -2
- package/dist/{setup-jda1ksdh.js → setup-as9ga0d6.js} +2 -2
- package/dist/{virtual-modules-b3ga2m65.js → virtual-modules-9gkxqrap.js} +7 -6
- package/dist/{work-objects-tb7ycye8.js → work-objects-y5g1y7ev.js} +49 -37
- package/dist/{worker-njjccz5d.js → worker-860c3e38.js} +64 -57
- package/dist/{x-qfmygqqv.js → x-m477fr0h.js} +3 -2
- package/openapi.json +951 -356
- package/package.json +12 -13
- package/src/apps/catalog.generated.json +1309 -53
- package/src/apps/page-validator.ts +93 -31
- package/src/azure-devops/api.ts +94 -18
- package/src/azure-devops/auth.ts +32 -0
- package/src/azure-devops/boards-sync.ts +708 -0
- package/src/azure-devops/handlers.ts +52 -8
- package/src/azure-devops/html.ts +26 -0
- package/src/azure-devops/index.ts +15 -0
- package/src/azure-devops/templates.ts +86 -0
- package/src/azure-devops/types.ts +65 -1
- 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 +74 -13
- package/src/be/migrations/213_script_connections_sql.sql +79 -0
- 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-connections.ts +47 -8
- 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 +10 -18
- package/src/be/seed-skills/index.ts +107 -3
- package/src/be/sql-connections.ts +118 -0
- package/src/be/swarm-config-guard.ts +53 -0
- package/src/cli.tsx +0 -16
- package/src/commands/onboard/non-interactive.ts +4 -2
- package/src/commands/profile-sync.ts +5 -3
- package/src/commands/provider-credentials.ts +52 -6
- package/src/commands/runner.ts +196 -56
- 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-oauth.ts +6 -13
- package/src/http/mcp.ts +15 -5
- package/src/http/page-proxy.ts +2 -4
- package/src/http/pages-public.ts +6 -5
- package/src/http/pages.ts +5 -14
- package/src/http/poll.ts +28 -68
- package/src/http/requested-by.ts +54 -0
- package/src/http/route-def.ts +7 -6
- package/src/http/script-connection-proxy.ts +51 -4
- package/src/http/script-connections.ts +5 -2
- package/src/http/scripts.ts +1 -0
- package/src/http/session-cost-recompute.ts +12 -2
- package/src/http/session-data.ts +4 -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 +183 -2
- 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/jira/adf.ts +3 -1
- package/src/kv-reserved-namespaces.ts +2 -0
- package/src/oauth/mcp-wrapper.ts +10 -6
- package/src/otel-impl.ts +2 -1
- 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 +56 -3
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/claude-adapter.ts +27 -2
- package/src/providers/claude-flag-settings.ts +133 -0
- package/src/providers/claude-managed-adapter.ts +1 -1
- package/src/providers/claude-sdk-session.ts +7 -0
- package/src/providers/claude-session-events.ts +6 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/opencode-adapter.ts +268 -37
- package/src/providers/opencode-billing.ts +86 -0
- package/src/providers/opencode-swarm-plugin.ts +51 -0
- package/src/providers/pi-mono-adapter.ts +398 -153
- package/src/providers/pricing-sources.md +37 -3
- package/src/providers/reasoning-effort.ts +27 -0
- package/src/providers/types.ts +11 -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/api-client.ts +15 -1
- package/src/scripts-runtime/api-types.ts +9 -1
- package/src/scripts-runtime/ctx.ts +1 -1
- package/src/scripts-runtime/loader.ts +19 -7
- package/src/scripts-runtime/mcp-client.ts +30 -19
- 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/actions.ts +29 -1
- package/src/slack/app.ts +14 -2
- package/src/slack/assistant.ts +36 -2
- package/src/slack/channel-join.ts +3 -16
- package/src/slack/commands.ts +9 -1
- package/src/slack/event-dedup.ts +1 -1
- package/src/slack/external-channel-egress.ts +113 -0
- package/src/slack/external-channel.ts +309 -0
- package/src/slack/handlers.ts +61 -0
- package/src/slack/render-v2.ts +40 -47
- package/src/slack/work-objects.ts +5 -0
- package/src/tasks/additive-buffer.ts +4 -2
- package/src/tasks/context-key.ts +94 -2
- package/src/tasks/model-pin.ts +67 -0
- package/src/tasks/worker-follow-up.ts +4 -31
- package/src/telemetry.ts +2 -1
- package/src/tests/acp-adapter.test.ts +42 -0
- package/src/tests/api-drain-worker-e2e.test.ts +1 -6
- package/src/tests/apps-catalog-feedback.test.ts +310 -0
- package/src/tests/apps-catalog-inputs.test.ts +70 -0
- package/src/tests/apps-catalog-table.test.ts +113 -0
- package/src/tests/azure-devops-boards-sync.test.ts +584 -0
- package/src/tests/azure-devops-html.test.ts +13 -0
- package/src/tests/azure-devops-webhook-malformed.test.ts +4 -0
- 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 +144 -0
- package/src/tests/claude-flag-settings.test.ts +114 -0
- package/src/tests/claude-managed-adapter.test.ts +1 -1
- package/src/tests/claude-sdk-lifecycle.test.ts +148 -1
- package/src/tests/claude-sdk-process.fixture.ts +7 -0
- 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 +51 -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/deferred-task-wake.test.ts +52 -4
- 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/azure-boards/workitem.commented.json +89 -0
- package/src/tests/fixtures/azure-boards/workitem.created.json +81 -0
- package/src/tests/fixtures/azure-boards/workitem.deleted.json +70 -0
- package/src/tests/fixtures/azure-boards/workitem.restored.json +82 -0
- package/src/tests/fixtures/azure-boards/workitem.updated.json +130 -0
- package/src/tests/fixtures/catalog-feedback-definition.json.txt +860 -0
- package/src/tests/fixtures/catalog-inputs-definition.json.txt +910 -0
- package/src/tests/fixtures/catalog-table-definition.json.txt +468 -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-identity-refresh.ts +3 -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/node-env-runtime.test.ts +98 -0
- 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 +348 -3
- package/src/tests/opencode-billing.test.ts +28 -0
- 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-identity-refresh.test.ts +3 -0
- package/src/tests/runner-legacy-resume-prompt.test.ts +64 -0
- package/src/tests/script-connections-sql.test.ts +675 -0
- package/src/tests/script-connections.test.ts +7 -1
- 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 +94 -0
- package/src/tests/skill-get-name-resolution.test.ts +137 -0
- package/src/tests/slack-channel-join.test.ts +4 -1
- package/src/tests/slack-external-channel-config.test.ts +65 -0
- package/src/tests/slack-external-channel-egress.test.ts +237 -0
- package/src/tests/slack-external-channel-guard.test.ts +280 -0
- package/src/tests/slack-external-channel-tools.test.ts +173 -0
- package/src/tests/slack-external-channel.test.ts +370 -0
- package/src/tests/slack-render-v2.test.ts +86 -10
- package/src/tests/sonnet-5-5-registry.test.ts +9 -1
- package/src/tests/spec-base-url.test.ts +16 -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/vendored-openapi.test.ts +16 -1
- 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/defer-task.ts +17 -0
- 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/slack-post.ts +6 -0
- package/src/tools/slack-read.ts +8 -0
- package/src/tools/slack-reply.ts +6 -0
- package/src/tools/slack-start-thread.ts +6 -0
- package/src/tools/slack-update.ts +6 -0
- package/src/tools/slack-upload-file.ts +6 -0
- 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/node-env.ts +24 -0
- 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 +749 -0
- package/templates/skills/apps/content.md +60 -22
- 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/qa-loom/config.json +14 -0
- package/templates/skills/qa-loom/content.md +187 -0
- package/templates/skills/qa-loom/files/build-loom.mjs +221 -0
- package/templates/skills/qa-loom/files/run-qa.sh +57 -0
- package/templates/skills/slack-interaction/content.md +4 -0
- 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-9xsmdpdt.js +0 -56377
- package/dist/cli-bd957dgb.js +0 -153
- package/dist/cli-q51yprfq.js +0 -1104
- package/dist/cli-t1ze64h3.js +0 -811
- 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/dist/{azure-openai-responses-ap1gwxgr.js → azure-openai-responses-ygfreq4t.js} +5 -5
- package/dist/{cli-5rqrj7q0.js → cli-pw7ebjdt.js} +0 -0
- package/dist/{openai-codex-responses-33qhkkdy.js → openai-codex-responses-3bk0grnr.js} +4 -4
- package/dist/{openai-completions-s88xm3ns.js → openai-completions-5d4jpakc.js} +9 -9
- package/dist/{openai-responses-7xd2f2e2.js → openai-responses-krb51f59.js} +5 -5
- package/dist/{pi-messages-xszk613n.js → pi-messages-x9gzynm1.js} +3 -3
- /package/src/{artifact-sdk → pages}/browser-sdk.ts +0 -0
- /package/src/{artifact-sdk → pages}/feedback-overlay.ts +0 -0
|
@@ -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
|
|
|
@@ -63,6 +65,8 @@ type FakeHostOptions = {
|
|
|
63
65
|
mcpStatuses?: Array<McpStatus | null>;
|
|
64
66
|
/** Registered tool ids the plugin's tool lister returns. */
|
|
65
67
|
tools?: string[];
|
|
68
|
+
/** Model responses handed to the plugin's `http.response` hook at startup. */
|
|
69
|
+
responses?: Response[];
|
|
66
70
|
};
|
|
67
71
|
|
|
68
72
|
/**
|
|
@@ -83,6 +87,11 @@ function makeFakeHost(events: OpenCodeEvent[], opts: FakeHostOptions = {}) {
|
|
|
83
87
|
interrupts: [] as unknown[],
|
|
84
88
|
closeCalls: 0,
|
|
85
89
|
mcpListCalls: 0,
|
|
90
|
+
/** Session hooks the plugins registered, with their options. */
|
|
91
|
+
sessionHooks: {} as Record<
|
|
92
|
+
string,
|
|
93
|
+
{ fn: (event: any) => unknown; options?: { providerID?: string } }
|
|
94
|
+
>,
|
|
86
95
|
mcp: {
|
|
87
96
|
list: async () => {
|
|
88
97
|
const status = statuses[Math.min(host.mcpListCalls, statuses.length - 1)];
|
|
@@ -91,6 +100,7 @@ function makeFakeHost(events: OpenCodeEvent[], opts: FakeHostOptions = {}) {
|
|
|
91
100
|
},
|
|
92
101
|
},
|
|
93
102
|
tools: opts.tools ?? ["read", "swarm_store-progress"],
|
|
103
|
+
responses: opts.responses ?? [],
|
|
94
104
|
sessions: {
|
|
95
105
|
create: async (input: unknown) => {
|
|
96
106
|
host.created.push(input);
|
|
@@ -120,13 +130,14 @@ function makeFakeHost(events: OpenCodeEvent[], opts: FakeHostOptions = {}) {
|
|
|
120
130
|
return host;
|
|
121
131
|
}
|
|
122
132
|
|
|
123
|
-
function installHost(host: FakeHost) {
|
|
133
|
+
function installHost(host: FakeHost, onCreate?: (options: Record<string, unknown>) => void) {
|
|
124
134
|
_setOpencodeSdkLoaderForTests(async () => ({
|
|
125
135
|
version: "2.0.28",
|
|
126
136
|
sdk: {
|
|
127
137
|
OpenCode: {
|
|
128
138
|
create: async (options: Record<string, unknown>) => {
|
|
129
139
|
host.createOptions = options;
|
|
140
|
+
onCreate?.(options);
|
|
130
141
|
// Set the plugins up the way OpenCode does, with a stub context.
|
|
131
142
|
const plugins = options.plugins as Array<{ setup: (ctx: unknown) => Promise<void> }>;
|
|
132
143
|
for (const plugin of plugins) {
|
|
@@ -135,9 +146,22 @@ function installHost(host: FakeHost) {
|
|
|
135
146
|
hook: async () => {},
|
|
136
147
|
list: async () => host.tools.map((id) => ({ id })),
|
|
137
148
|
},
|
|
138
|
-
|
|
149
|
+
// Registered when the session config carries a per-task env.
|
|
150
|
+
shell: { hook: async () => {} },
|
|
151
|
+
session: {
|
|
152
|
+
hook: async (
|
|
153
|
+
name: string,
|
|
154
|
+
fn: (event: unknown) => unknown,
|
|
155
|
+
options?: { providerID?: string },
|
|
156
|
+
) => {
|
|
157
|
+
host.sessionHooks[name] = { fn, options };
|
|
158
|
+
},
|
|
159
|
+
},
|
|
139
160
|
});
|
|
140
161
|
}
|
|
162
|
+
for (const response of host.responses) {
|
|
163
|
+
await host.sessionHooks["http.response"]?.fn({ response });
|
|
164
|
+
}
|
|
141
165
|
return host;
|
|
142
166
|
},
|
|
143
167
|
},
|
|
@@ -445,6 +469,118 @@ describe("OpencodeSession — event mapping", () => {
|
|
|
445
469
|
});
|
|
446
470
|
});
|
|
447
471
|
|
|
472
|
+
describe("OpencodeSession — cost of an OpenRouter session", () => {
|
|
473
|
+
// Task 06c031dd (opencode 2.0.28, openrouter/deepseek/deepseek-v4.1-flash),
|
|
474
|
+
// verbatim from session_logs: a title call, then three agent steps.
|
|
475
|
+
// `session.usage.updated` is cumulative; `output` excludes reasoning.
|
|
476
|
+
const deepseek = testConfig({ model: "openrouter/deepseek/deepseek-v4.1-flash" });
|
|
477
|
+
const usage = (cost: number, input: number, output: number, reasoning: number, read: number) =>
|
|
478
|
+
ev("session.usage.updated", {
|
|
479
|
+
sessionID: SESSION_ID,
|
|
480
|
+
cost,
|
|
481
|
+
tokens: { input, output, reasoning, cache: { read, write: 0 } },
|
|
482
|
+
});
|
|
483
|
+
const step = (cost: number, input: number, output: number, reasoning: number, read: number) =>
|
|
484
|
+
ev("session.step.ended", {
|
|
485
|
+
sessionID: SESSION_ID,
|
|
486
|
+
finish: "tool-calls",
|
|
487
|
+
cost,
|
|
488
|
+
tokens: { input, output, reasoning, cache: { read, write: 0 } },
|
|
489
|
+
});
|
|
490
|
+
const v2Events = [
|
|
491
|
+
usage(0.00010640000000000001, 1019, 9, 0, 0),
|
|
492
|
+
step(0.00023894900000000002, 51770, 53, 26, 0),
|
|
493
|
+
usage(0.000345349, 52789, 62, 26, 0),
|
|
494
|
+
step(0.0005280831000000001, 12963, 102, 7, 51840),
|
|
495
|
+
usage(0.0008734321000000001, 65752, 164, 33, 51840),
|
|
496
|
+
step(0.0005287785, 165, 15, 0, 64896),
|
|
497
|
+
usage(0.0014022106, 65917, 179, 33, 116736),
|
|
498
|
+
succeeded,
|
|
499
|
+
];
|
|
500
|
+
|
|
501
|
+
// OpenRouter's final SSE chunk for deepseek/deepseek-v4.1-flash, served by
|
|
502
|
+
// Together (live probe, 2026-10-14): `usage.cost` is the billed USD.
|
|
503
|
+
const openRouterSse = (cost: number, cached: number) =>
|
|
504
|
+
new Response(
|
|
505
|
+
[
|
|
506
|
+
": OPENROUTER PROCESSING",
|
|
507
|
+
"",
|
|
508
|
+
`data: ${JSON.stringify({ id: "gen-1", choices: [{ index: 0, delta: { content: "pong" } }] })}`,
|
|
509
|
+
"",
|
|
510
|
+
`data: ${JSON.stringify({
|
|
511
|
+
id: "gen-1",
|
|
512
|
+
provider: "Together",
|
|
513
|
+
model: "deepseek/deepseek-v4.1-flash",
|
|
514
|
+
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
|
515
|
+
usage: {
|
|
516
|
+
prompt_tokens: 26545,
|
|
517
|
+
completion_tokens: 15,
|
|
518
|
+
total_tokens: 26560,
|
|
519
|
+
cost,
|
|
520
|
+
is_byok: false,
|
|
521
|
+
prompt_tokens_details: { cached_tokens: cached, cache_write_tokens: 0 },
|
|
522
|
+
completion_tokens_details: { reasoning_tokens: 11 },
|
|
523
|
+
},
|
|
524
|
+
})}`,
|
|
525
|
+
"",
|
|
526
|
+
"data: [DONE]",
|
|
527
|
+
"",
|
|
528
|
+
].join("\n"),
|
|
529
|
+
{ headers: { "content-type": "text/event-stream" } },
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
test("reports v2 tokens with reasoning billed as output", async () => {
|
|
533
|
+
const { result } = await runSession(v2Events, deepseek);
|
|
534
|
+
expect(result.cost).toMatchObject({
|
|
535
|
+
totalCostUsd: 0.0014022106,
|
|
536
|
+
inputTokens: 65917,
|
|
537
|
+
outputTokens: 212,
|
|
538
|
+
reasoningOutputTokens: 33,
|
|
539
|
+
cacheReadTokens: 116736,
|
|
540
|
+
cacheWriteTokens: 0,
|
|
541
|
+
numTurns: 3,
|
|
542
|
+
});
|
|
543
|
+
expect(result.cost?.harnessCostBilled).toBeUndefined();
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
test("reports OpenRouter's billed cost when every response carried one", async () => {
|
|
547
|
+
const { result } = await runSession(v2Events, deepseek, {
|
|
548
|
+
responses: [
|
|
549
|
+
openRouterSse(0.0079815, 0),
|
|
550
|
+
openRouterSse(0.000191676, 26496),
|
|
551
|
+
new Response('{"error":{"message":"rate limited"}}', { status: 429 }),
|
|
552
|
+
],
|
|
553
|
+
});
|
|
554
|
+
expect(result.cost?.harnessCostBilled).toBe(true);
|
|
555
|
+
expect(result.cost?.totalCostUsd).toBeCloseTo(0.0079815 + 0.000191676, 12);
|
|
556
|
+
expect(result.cost?.inputTokens).toBe(65917);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
test("falls back to OpenCode's estimate when a response has no billed cost", async () => {
|
|
560
|
+
const { result } = await runSession(v2Events, deepseek, {
|
|
561
|
+
responses: [openRouterSse(0.0079815, 0), new Response("data: [DONE]\n\n")],
|
|
562
|
+
});
|
|
563
|
+
expect(result.cost?.harnessCostBilled).toBeUndefined();
|
|
564
|
+
expect(result.cost?.totalCostUsd).toBe(0.0014022106);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
test("hooks only OpenRouter responses, and only for an OpenRouter model", async () => {
|
|
568
|
+
const openrouter = await runSession([succeeded], deepseek);
|
|
569
|
+
expect(openrouter.host.sessionHooks["http.response"]?.options).toEqual({
|
|
570
|
+
providerID: "openrouter",
|
|
571
|
+
});
|
|
572
|
+
const direct = await runSession([succeeded], testConfig({ model: "anthropic/claude-haiku" }));
|
|
573
|
+
expect(direct.host.sessionHooks["http.response"]).toBeUndefined();
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test("skips title generation", async () => {
|
|
577
|
+
const { host } = await runSession([succeeded]);
|
|
578
|
+
const event: { result?: string } = {};
|
|
579
|
+
await host.sessionHooks.title?.fn(event);
|
|
580
|
+
expect(event.result).toBe("swarm task task-1");
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
|
|
448
584
|
describe("OpencodeSession — persisted rows render in the dashboard", () => {
|
|
449
585
|
test("text, tools and errors render once each, in order", async () => {
|
|
450
586
|
const text = (ordinal: number, value: string, messageID = "msg_1") => [
|
|
@@ -710,3 +846,212 @@ describe("OpenRouter attribution config", () => {
|
|
|
710
846
|
expect(config).toEqual({});
|
|
711
847
|
});
|
|
712
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,28 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { OpenRouterBilling, parseOpenRouterBilledCost } from "../providers/opencode-billing";
|
|
3
|
+
|
|
4
|
+
describe("parseOpenRouterBilledCost", () => {
|
|
5
|
+
test("reads usage.cost from a non-streamed JSON body", () => {
|
|
6
|
+
expect(parseOpenRouterBilledCost('{"id":"gen-1","usage":{"cost":0.0042}}')).toBe(0.0042);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("a BYOK cost is only OpenRouter's fee, so it is not the bill", () => {
|
|
10
|
+
expect(parseOpenRouterBilledCost('{"usage":{"cost":0.0001,"is_byok":true}}')).toBeNull();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("a stream without usage has no billed cost", () => {
|
|
14
|
+
expect(parseOpenRouterBilledCost('data: {"choices":[]}\n\ndata: [DONE]\n\n')).toBeNull();
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("OpenRouterBilling", () => {
|
|
19
|
+
test("no observed request reports nothing", async () => {
|
|
20
|
+
expect(await new OpenRouterBilling().settle(10)).toBeNull();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("a body still streaming at the deadline leaves the bill incomplete", async () => {
|
|
24
|
+
const billing = new OpenRouterBilling();
|
|
25
|
+
billing.observe(new Response(new ReadableStream({ start() {} })));
|
|
26
|
+
expect(await billing.settle(10)).toEqual({ costUsd: 0, complete: false });
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -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
|
+
});
|