@desplega.ai/agent-swarm 1.165.0 → 1.167.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acp-adapter-zpvy6321.js +34 -0
- package/dist/{actions-jna0b3wm.js → actions-fq2zrwtf.js} +30 -34
- package/dist/{amp-adapter-0eve81pz.js → amp-adapter-5gbk7501.js} +7 -7
- package/dist/{anthropic-messages-hst62ws9.js → anthropic-messages-13yn502s.js} +4 -4
- package/dist/{app-75hm5cfa.js → app-nctetn1e.js} +11 -11
- package/dist/{assistant-yzd51j33.js → assistant-0aq8gjvj.js} +31 -35
- package/dist/{azure-openai-responses-mh8tvf78.js → azure-openai-responses-9st34pfz.js} +5 -5
- package/dist/{boot-reembed-z158xq32.js → boot-reembed-1ze5853n.js} +14 -16
- package/dist/{boot-reembed-2ex2mcsh.js → boot-reembed-fmp5ww69.js} +15 -16
- package/dist/boot-scrub-sweep-cbp8xd4e.js +212 -0
- package/dist/{boot-seeds-2bmnh18p.js → boot-seeds-22tx8a53.js} +13 -12
- package/dist/{claude-adapter-jbj1mycz.js → claude-adapter-0jhgeh1d.js} +25 -25
- package/dist/{claude-managed-adapter-17ggz09t.js → claude-managed-adapter-tzwxy803.js} +5 -5
- package/dist/{claude-managed-setup-ptm1nzm2.js → claude-managed-setup-nrhfx5b0.js} +5 -5
- package/dist/{claude-sdk-session-9re1g7r2.js → claude-sdk-session-wh77rh83.js} +2632 -2614
- package/dist/{cli-zsh093hk.js → cli-014vns2y.js} +63 -1
- package/dist/{cli-qvfk83fy.js → cli-13xwcqvj.js} +2 -2
- package/dist/{cli-8gpbwy09.js → cli-1yrcfa5s.js} +5 -5
- package/dist/{cli-xtgtyafv.js → cli-577bxkf6.js} +212 -451
- package/dist/{cli-ma3zvh39.js → cli-5qkcdv8c.js} +6118 -5874
- package/dist/{cli-cwxhbf43.js → cli-5t5my0h8.js} +85 -2
- package/dist/{cli-zvckv750.js → cli-69egj654.js} +2 -2
- package/dist/{cli-9xb89xca.js → cli-6bpvv8h4.js} +9 -9
- package/dist/{cli-90v942q1.js → cli-6qxmjpem.js} +8 -8
- package/dist/{cli-9866bd07.js → cli-6vfz44sr.js} +1 -1
- package/dist/{cli-32bca7nr.js → cli-7avy4v8w.js} +1 -1
- package/dist/{cli-xb3c6aat.js → cli-7mhy3ths.js} +21 -18
- package/dist/{cli-q38n4bq0.js → cli-95zs84qw.js} +3 -3
- package/dist/cli-a35hzdve.js +33 -0
- package/dist/{cli-x3x0qwky.js → cli-bxqez1p0.js} +51 -23
- package/dist/{cli-prt9emct.js → cli-c2tpw9ck.js} +3 -3
- package/dist/{cli-0n4fasz0.js → cli-c8r99akr.js} +16 -16
- package/dist/{cli-5anb0ev3.js → cli-cexctgn5.js} +89 -26
- package/dist/{cli-bns5c8k9.js → cli-cf2vgn9d.js} +105 -91
- package/dist/{cli-ng97fjq7.js → cli-cmxkbzzs.js} +33 -7
- package/dist/{cli-nexwh8zg.js → cli-g7k7zbcz.js} +12 -5
- package/dist/{cli-mmzwwptd.js → cli-hf5s9pgf.js} +1 -1
- package/dist/{cli-nvvfd8bq.js → cli-jbzbr36n.js} +4091 -3793
- package/dist/{cli-zn2v3t8v.js → cli-jvmtxqtc.js} +27 -15
- package/dist/{cli-pkke6geq.js → cli-km4x66j0.js} +83 -65
- package/dist/cli-kw0jcfje.js +516 -0
- package/dist/{cli-28smfvbn.js → cli-m7vmeepg.js} +32 -19
- package/dist/{cli-hpryr6em.js → cli-mb75wpxf.js} +1 -1
- package/dist/{cli-qdv8kgz7.js → cli-mkdsy1nq.js} +14018 -3333
- package/dist/{cli-b0f70z24.js → cli-mn8qkd8v.js} +1 -1
- package/dist/{cli-420bdgw0.js → cli-mv2520nm.js} +1 -1
- package/dist/{cli-n9ehc28t.js → cli-mxry91sa.js} +31 -10
- package/dist/{cli-wfajtmf7.js → cli-mxvnnv5q.js} +596 -13
- package/dist/cli-nkqjj42q.js +104 -0
- package/dist/{cli-e7t4snpa.js → cli-p8rt8spp.js} +5 -5
- package/dist/{cli-gh5yqzpe.js → cli-pkj1w46r.js} +12 -7
- package/dist/cli-q1vnz8wv.js +96 -0
- package/dist/{cli-fj86xr3c.js → cli-qd97v6z2.js} +2 -2
- package/dist/{cli-ba66wyhy.js → cli-qzmzwegq.js} +60 -20
- package/dist/{cli-5b2fjjzh.js → cli-r7eeddkq.js} +14 -9
- package/dist/{cli-8ssjez7e.js → cli-rhy7z1rp.js} +22 -11
- package/dist/{cli-4mevd1hf.js → cli-s9y81s7j.js} +5 -5
- package/dist/{cli-59jn5twa.js → cli-tc5zec7v.js} +7 -7
- package/dist/{cli-ttbsse3f.js → cli-tmhsxhb8.js} +1 -1
- package/dist/{cli-k6xx3pve.js → cli-tx2zvn23.js} +4 -4
- package/dist/{cli-pjxnr1jn.js → cli-ty4bzkqr.js} +5 -5
- package/dist/{cli-nfygw434.js → cli-v1pwxreq.js} +55 -52
- package/dist/{cli-ftry7975.js → cli-v1w7pbd8.js} +3 -2
- package/dist/{cli-10zmp1p7.js → cli-vr5avem2.js} +27 -32
- package/dist/{cli-bzr0ftht.js → cli-wnb9b2g3.js} +19 -0
- package/dist/cli-wt7ccy4r.js +2654 -0
- package/dist/{cli-50eg13rj.js → cli-wydz3feg.js} +549 -321
- package/dist/{cli-krqm5f11.js → cli-xezn9nkh.js} +16 -13
- package/dist/{cli-8kt0bmgx.js → cli-xf713zz0.js} +2 -2
- package/dist/{cli-x7afk5wm.js → cli-xvc3q9br.js} +1 -1
- package/dist/{cli-9s3h836g.js → cli-xvdgasgy.js} +1 -1
- package/dist/{cli-j0pdrmwg.js → cli-y0kjpfm9.js} +3 -3
- package/dist/{cli-6zcfw0x7.js → cli-y195hv1a.js} +1 -1
- package/dist/{cli-amwhs8h2.js → cli-ykt8f7va.js} +22 -9
- package/dist/{cli-1kzfvd1z.js → cli-ymmc1zxg.js} +15 -11
- package/dist/{cli-xnzrzets.js → cli-ypgba1a8.js} +2 -2
- package/dist/{cli-bdv39kgj.js → cli-z1dqhc9y.js} +31 -20
- package/dist/{cli-fw6vdcq5.js → cli-zp67qg5x.js} +34 -1
- package/dist/{cli-qbksc292.js → cli-zy39d433.js} +2 -0
- package/dist/cli.js +36 -32
- package/dist/{cloudflare-workers-ai-system-one-tt484t61.js → cloudflare-workers-ai-system-one-sta45jf9.js} +7 -5
- package/dist/{codex-adapter-3qnxshje.js → codex-adapter-cgesnjkr.js} +20 -20
- package/dist/{codex-login-brkxhhdg.js → codex-login-g1cyt5zm.js} +2 -2
- package/dist/{codex-session-runner-612wnapp.js → codex-session-runner-8kd7ddhr.js} +20 -20
- package/dist/{commands-2ybfcxkj.js → commands-h6zaaxym.js} +11 -10
- package/dist/{cursor-adapter-p3jgdehv.js → cursor-adapter-v8mm6bp5.js} +7 -7
- package/dist/{db-f4ans0jj.js → db-qg3myn53.js} +13 -10
- package/dist/{devin-adapter-dhzt01ff.js → devin-adapter-bffjj5n4.js} +2 -2
- package/dist/{dispatcher-pk182apd.js → dispatcher-7vnwj9qw.js} +28 -32
- package/dist/{dsh-adapter-7twaeps1.js → dsh-adapter-tz6hfp5r.js} +7 -7
- package/dist/{e2b-y8t3jzyz.js → e2b-nd50eccg.js} +1 -1
- package/dist/{easter-egg-3d-mtpnw06s.js → easter-egg-3d-66zxervv.js} +2 -2
- package/dist/{event-bus-w7ac4n3c.js → event-bus-vnfthafg.js} +2 -2
- package/dist/{execute-s4ccjq3n.js → execute-h4jgevyw.js} +61 -11
- package/dist/{google-generative-ai-d6nphg3q.js → google-generative-ai-b8zxrpax.js} +5 -5
- package/dist/{google-vertex-cpshkref.js → google-vertex-nrdd697t.js} +5 -5
- package/dist/grok-adapter-jdt2dz2s.js +39 -0
- package/dist/{handlers-g2wnr4t9.js → handlers-2qtkpncr.js} +31 -35
- package/dist/{hook-f7jt9vnm.js → hook-51yfcgsv.js} +48 -39
- package/dist/{hook-bbdpnkb4.js → hook-qnbdgwvd.js} +15 -17
- package/dist/{http-w40q4w94.js → http-jsmtag08.js} +536 -231
- package/dist/{index-056j5kzn.js → index-2t9kesn1.js} +13 -12
- package/dist/{index-cj1r9q83.js → index-bt8wkv1w.js} +29 -33
- package/dist/{index-7n70jwbb.js → index-cx1sb9bq.js} +42 -35
- package/dist/{index-9bfssf54.js → index-ekywyv2v.js} +28 -32
- package/dist/{index-fdk1t4d6.js → index-n0rxa4ca.js} +11962 -9900
- package/dist/{index-k8xc083e.js → index-t4g06w93.js} +28 -32
- package/dist/{keepalive-1tj10tf4.js → keepalive-4aezx4bj.js} +16 -12
- package/dist/{lead-st0bg7v4.js → lead-r44bkpm5.js} +52 -56
- package/dist/{llama-cpp-classify-tgj35k8d.js → llama-cpp-classify-6erb7wwv.js} +3 -1
- package/dist/{maintenance-nz71zats.js → maintenance-grz5jrzh.js} +12 -13
- package/dist/{mistral-conversations-8445ddjn.js → mistral-conversations-059y51zt.js} +4 -4
- package/dist/{node-f0ykebbv.js → node-qm9eb6z0.js} +25 -7
- package/dist/{oauth-refresh-sweep-aefzwvvq.js → oauth-refresh-sweep-wm4ctjfj.js} +15 -11
- package/dist/{onboard-kgpr9mbn.js → onboard-v60zgqf0.js} +2 -2
- package/dist/{openai-codex-responses-9ynm88zy.js → openai-codex-responses-jzxkta9b.js} +8 -7
- package/dist/{openai-completions-nhqwjmaw.js → openai-completions-663yncfq.js} +4 -4
- package/dist/openai-decisions-vj9nkpf8.js +162 -0
- package/dist/{openai-responses-xx1c4kgm.js → openai-responses-m3g43rrt.js} +5 -5
- package/dist/{opencode-adapter-0c8e70dk.js → opencode-adapter-yjyqynzf.js} +8 -8
- package/dist/{openrouter-images-8yx3487g.js → openrouter-images-kr60n6b9.js} +1 -1
- package/dist/{otel-impl-x8zp4y5a.js → otel-impl-ghh4dyf8.js} +2 -2
- package/dist/{pi-messages-vxdt8v7j.js → pi-messages-xszk613n.js} +1 -1
- package/dist/{pi-mono-adapter-t280ah5c.js → pi-mono-adapter-0eg8kw2c.js} +69 -52
- package/dist/{pricing-refresh-9e0q7cwk.js → pricing-refresh-mjrqvnvw.js} +12 -11
- package/dist/{rbac-roles-790j3ga8.js → rbac-roles-axv4d74x.js} +11 -10
- package/dist/{rbac-roles-kcp6zge2.js → rbac-roles-fd5tskj9.js} +11 -10
- package/dist/{render-v2-tch4nefj.js → render-v2-k50j33qd.js} +11 -11
- package/dist/{runtime-v2t1r79e.js → runtime-h9ha1mq6.js} +4 -4
- package/dist/{setup-rcm303ns.js → setup-4syh0pd2.js} +2 -2
- package/dist/{typesafe-system-one-datfr82p.js → typesafe-system-one-e988vty7.js} +7 -5
- package/dist/{virtual-modules-wm8mxdxx.js → virtual-modules-ftr9kwe8.js} +11 -11
- package/dist/{work-objects-ctg7t70w.js → work-objects-jcdbg8rr.js} +32 -36
- package/dist/{worker-5dp0xm55.js → worker-x8j8c9sx.js} +52 -56
- package/dist/{x-v3yw66zs.js → x-ma4gnfgy.js} +2 -2
- package/openapi.json +5 -2
- package/package.json +15 -11
- package/src/be/automation-preflight.ts +9 -4
- package/src/be/boot-scrub-sweep.ts +302 -0
- package/src/be/crypto/key-bootstrap.ts +1 -1
- package/src/be/db/approvals.ts +15 -1
- package/src/be/db/codex-oauth-device-flows.ts +40 -0
- package/src/be/db/config-secrets.ts +31 -0
- package/src/be/db/runtime.ts +4 -0
- package/src/be/db/script-runs.ts +15 -0
- package/src/be/db/tasks/write.ts +1 -1
- package/src/be/db-queries/mcp-oauth.ts +5 -0
- package/src/be/db-queries/oauth.ts +77 -0
- package/src/be/db.ts +162 -59
- package/src/be/events.ts +17 -1
- package/src/be/memory/index-content.ts +2 -2
- package/src/be/memory/providers/openai-embedding.ts +7 -2
- package/src/be/memory/providers/sqlite-store.ts +63 -11
- package/src/be/memory/raters/llm-summarizer.ts +7 -2
- package/src/be/memory/types.ts +14 -0
- package/src/be/migrations/201_workflow_replay_columns.sql +10 -0
- package/src/be/migrations/202_codex_oauth_device_flows.sql +24 -0
- package/src/be/modelsdev-cache.json +61 -0
- package/src/be/scripts/db.ts +28 -4
- package/src/be/scripts/source-secrets.ts +37 -0
- package/src/be/scrub-json.ts +32 -0
- package/src/be/sealed-json.ts +59 -0
- package/src/be/secret-registry.ts +131 -0
- package/src/be/seed-pricing.ts +84 -53
- package/src/be/swarm-config-guard.ts +13 -0
- package/src/be/workflow-replay.ts +77 -0
- package/src/commands/provider-credentials.ts +28 -5
- package/src/commands/runner.ts +7 -12
- package/src/heartbeat/heartbeat.ts +91 -19
- package/src/hooks/hook.ts +72 -27
- package/src/http/codex-oauth-device.ts +8 -10
- package/src/http/db-query-bounded.ts +3 -0
- package/src/http/db-query-shared.ts +107 -15
- package/src/http/db-query.ts +10 -2
- package/src/http/index.ts +23 -6
- package/src/http/mcp.ts +5 -0
- package/src/http/onboarding.ts +2 -0
- package/src/http/script-runs.ts +9 -1
- package/src/http/scripts.ts +15 -0
- package/src/http/workflows.ts +3 -2
- package/src/oauth/mcp-wrapper.ts +1 -1
- package/src/providers/acp-adapter.ts +25 -11
- package/src/providers/acp-target-catalog.ts +35 -1
- package/src/providers/acp-targets.ts +78 -7
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/claude-adapter.ts +26 -0
- package/src/providers/codex-adapter.ts +30 -3
- package/src/providers/codex-oauth/auth-json.ts +21 -4
- package/src/providers/codex-oauth/types.ts +2 -0
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/providers/dsh-adapter.ts +10 -1
- package/src/providers/grok-adapter.ts +18 -2
- package/src/providers/opencode-adapter.ts +21 -5
- package/src/providers/pi-mono-extension.ts +56 -25
- package/src/providers/pricing-sources.md +2 -0
- package/src/script-workflows/supervisor.ts +11 -2
- package/src/scripts-runtime/eval-harness.ts +2 -12
- package/src/scripts-runtime/executors/quickjs-bundle.ts +152 -0
- package/src/scripts-runtime/executors/quickjs-prelude.ts +285 -0
- package/src/scripts-runtime/executors/quickjs-runner.ts +580 -0
- package/src/scripts-runtime/executors/quickjs-worker.ts +81 -0
- package/src/scripts-runtime/executors/quickjs.ts +196 -0
- package/src/scripts-runtime/executors/registry.ts +2 -0
- package/src/scripts-runtime/executors/sourcemap.ts +79 -0
- package/src/scripts-runtime/script-error-hints.ts +14 -0
- package/src/server.ts +66 -4
- package/src/tests/acp-adapter.test.ts +277 -12
- package/src/tests/boot-scrub-sweep.test.ts +390 -0
- package/src/tests/check-text-columns.test.ts +145 -0
- package/src/tests/ci-unit-test-report.test.ts +73 -0
- package/src/tests/claude-default-route.test.ts +75 -0
- package/src/tests/claude-hook-output.test.ts +233 -0
- package/src/tests/claude-sdk-process.fixture.ts +13 -2
- package/src/tests/codex-adapter-pool-auth-revalidate.test.ts +4 -2
- package/src/tests/codex-adapter.test.ts +166 -26
- package/src/tests/codex-oauth-auth-timestamps.test.ts +76 -0
- package/src/tests/codex-oauth-device.test.ts +23 -37
- package/src/tests/codex-oauth.test.ts +4 -1
- package/src/tests/context-window.test.ts +2 -2
- package/src/tests/credential-check.test.ts +51 -1
- package/src/tests/db-query-denylist.test.ts +202 -0
- package/src/tests/db-task-read-contract.test.ts +1 -0
- package/src/tests/db-task-write-contract.test.ts +1 -0
- package/src/tests/dsh-adapter.test.ts +23 -1
- package/src/tests/enabled-tools.test.ts +269 -0
- package/src/tests/entrypoint-acp-target-binary.test.ts +31 -1
- package/src/tests/entrypoint-codex-oauth-seed.test.ts +12 -0
- package/src/tests/events-scrub.test.ts +88 -0
- package/src/tests/fixtures/secret-corpus.ts +320 -0
- package/src/tests/grok-adapter.test.ts +13 -0
- package/src/tests/haiku-5-5-pricing.test.ts +38 -0
- package/src/tests/heartbeat-checklist.test.ts +158 -1
- package/src/tests/onboarding.test.ts +44 -0
- package/src/tests/opencode-adapter.test.ts +66 -3
- package/src/tests/opencode-plugin.test.ts +37 -0
- package/src/tests/openrouter-attribution-call-sites.test.ts +204 -0
- package/src/tests/openrouter-base-url.test.ts +30 -39
- package/src/tests/pi-mono-extension.test.ts +161 -2
- package/src/tests/pricing-refresh.test.ts +70 -1
- package/src/tests/reasoning-effort.test.ts +3 -1
- package/src/tests/script-connections.test.ts +5 -0
- package/src/tests/script-executor-conformance.test.ts +3 -0
- package/src/tests/script-executor-quickjs.test.ts +442 -0
- package/src/tests/script-executor-registry.test.ts +6 -1
- package/src/tests/script-runs-http.test.ts +88 -1
- package/src/tests/script-runs-scrub.test.ts +275 -0
- package/src/tests/scripts-http.test.ts +82 -1
- package/src/tests/scrubbed-text-12e.test.ts +289 -0
- package/src/tests/sealed-key-guard.test.ts +166 -0
- package/src/tests/secret-canary-sweep.test.ts +958 -0
- package/src/tests/secret-registry.test.ts +355 -0
- package/src/tests/secret-rules.test.ts +230 -0
- package/src/tests/secret-scrubber.test.ts +3 -2
- package/src/tests/telemetry-trigger.test.ts +12 -1
- package/src/tests/workflow-llm.test.ts +14 -0
- package/src/tests/workflow-replay.test.ts +518 -0
- package/src/tests/workflow-scrub.test.ts +242 -0
- package/src/tests/workflow-system-one-decision-providers.test.ts +33 -0
- package/src/tools/db-query.ts +1 -1
- package/src/tools/inject-learning.ts +1 -1
- package/src/tools/request-human-input.ts +7 -2
- package/src/tools/workflows/get-workflow-run.ts +3 -2
- package/src/types.ts +1 -1
- package/src/utils/enabled-tools.ts +27 -0
- package/src/utils/internal-ai/complete-structured.ts +11 -3
- package/src/utils/openrouter-base-url.ts +23 -0
- package/src/utils/secret-rules.generated.ts +2055 -0
- package/src/utils/secret-scrubber.ts +363 -29
- package/src/workflows/engine.ts +39 -4
- package/src/workflows/executors/raw-llm.ts +1 -0
- package/src/workflows/executors/system-one-decision.ts +2 -0
- package/src/workflows/executors/validate.ts +1 -0
- package/src/workflows/executors/workflow-llm.ts +9 -2
- package/src/workflows/recovery.ts +10 -1
- package/src/workflows/resume.ts +62 -3
- package/src/workflows/retry-poller.ts +3 -0
- package/templates/skills/agent-browser/SKILL.md +2 -0
- package/templates/skills/agent-browser/content.md +2 -0
- package/templates/skills/code-quality/content.md +1 -0
- package/templates/skills/heartbeat-runbook/content.md +1 -1
- package/templates/skills/work-on-task/SKILL.md +4 -0
- package/templates/skills/work-on-task/content.md +4 -0
- package/templates/skills/workflow-iterate/content.md +9 -0
- package/dist/acp-adapter-w6zws6me.js +0 -23
- package/dist/boot-scrub-logs-246yn076.js +0 -99
- package/dist/cli-3a4m1aws.js +0 -572
- package/dist/cli-3dpj7atn.js +0 -11
- package/dist/cli-a87b04k8.js +0 -951
- package/dist/cli-cvk6yzw5.js +0 -40
- package/dist/cli-gvcgmx69.js +0 -183
- package/dist/cli-h1spv8v4.js +0 -427
- package/dist/cli-jqt9hrnz.js +0 -168
- package/dist/cli-yhmwmv0g.js +0 -9281
- package/dist/grok-adapter-yt9pab3k.js +0 -29
- package/src/be/boot-scrub-logs.ts +0 -132
- package/dist/{codex-hook-d2rkvf39.js → codex-hook-15dgc4cb.js} +1 -1
|
@@ -1294,15 +1294,17 @@ describe("OpencodeAdapter — reasoning_effort wiring", () => {
|
|
|
1294
1294
|
expect(options).toEqual({ thinking: { type: "enabled", budgetTokens: 32768 } });
|
|
1295
1295
|
});
|
|
1296
1296
|
|
|
1297
|
-
test("undefined reasoningEffort leaves
|
|
1297
|
+
test("undefined reasoningEffort leaves model options unset", async () => {
|
|
1298
1298
|
const events: OpencodeEvent[] = [
|
|
1299
1299
|
{ type: "session.idle", properties: { sessionID: "sess-abc-123" } },
|
|
1300
1300
|
];
|
|
1301
1301
|
const cfg = testConfig({ model: "openrouter/google/gemini-3-flash-preview" });
|
|
1302
1302
|
await driveSession(events, cfg);
|
|
1303
1303
|
|
|
1304
|
-
const opts = lastCreateOpencodeConfig as {
|
|
1305
|
-
|
|
1304
|
+
const opts = lastCreateOpencodeConfig as {
|
|
1305
|
+
config?: { provider?: Record<string, { models?: unknown }> };
|
|
1306
|
+
};
|
|
1307
|
+
expect(opts.config?.provider?.openrouter?.models).toBeUndefined();
|
|
1306
1308
|
});
|
|
1307
1309
|
});
|
|
1308
1310
|
|
|
@@ -1496,3 +1498,64 @@ describe("OpencodeAdapter: event stream ends without session.idle", () => {
|
|
|
1496
1498
|
expect(closeServer).toHaveBeenCalledTimes(1);
|
|
1497
1499
|
});
|
|
1498
1500
|
});
|
|
1501
|
+
|
|
1502
|
+
describe("OpenRouter attribution config", () => {
|
|
1503
|
+
test("overrides harness attribution while preserving custom headers and options", async () => {
|
|
1504
|
+
const { applyOpenRouterBaseUrlOverride } = await import("../providers/opencode-adapter");
|
|
1505
|
+
const config = {
|
|
1506
|
+
provider: {
|
|
1507
|
+
openrouter: {
|
|
1508
|
+
options: {
|
|
1509
|
+
apiKey: "test",
|
|
1510
|
+
headers: {
|
|
1511
|
+
"X-Custom": "keep",
|
|
1512
|
+
"X-Title": "opencode",
|
|
1513
|
+
"HTTP-Referer": "https://opencode.ai",
|
|
1514
|
+
},
|
|
1515
|
+
},
|
|
1516
|
+
},
|
|
1517
|
+
},
|
|
1518
|
+
};
|
|
1519
|
+
applyOpenRouterBaseUrlOverride(config, {});
|
|
1520
|
+
expect(config.provider.openrouter.options).toEqual({
|
|
1521
|
+
apiKey: "test",
|
|
1522
|
+
headers: {
|
|
1523
|
+
"X-Custom": "keep",
|
|
1524
|
+
"HTTP-Referer": "https://agent-swarm.dev",
|
|
1525
|
+
"X-Title": "Agent Swarm",
|
|
1526
|
+
"X-OpenRouter-Title": "Agent Swarm",
|
|
1527
|
+
"X-OpenRouter-Categories": "personal-agent,cloud-agent",
|
|
1528
|
+
},
|
|
1529
|
+
});
|
|
1530
|
+
});
|
|
1531
|
+
test("gateway override preserves options without injecting attribution", async () => {
|
|
1532
|
+
const { applyOpenRouterBaseUrlOverride } = await import("../providers/opencode-adapter");
|
|
1533
|
+
const config = {
|
|
1534
|
+
provider: {
|
|
1535
|
+
openrouter: {
|
|
1536
|
+
models: {
|
|
1537
|
+
"google/gemini-3-flash-preview": { options: { reasoning: { effort: "low" } } },
|
|
1538
|
+
},
|
|
1539
|
+
options: { apiKey: "test", headers: { "X-Custom": "keep" } },
|
|
1540
|
+
},
|
|
1541
|
+
anthropic: { options: { apiKey: "test-other" } },
|
|
1542
|
+
},
|
|
1543
|
+
};
|
|
1544
|
+
applyOpenRouterBaseUrlOverride(config, { OPENROUTER_BASE_URL: "http://localhost:1234/v1" });
|
|
1545
|
+
expect(config.provider.openrouter.options).toEqual({
|
|
1546
|
+
apiKey: "test",
|
|
1547
|
+
headers: { "X-Custom": "keep" },
|
|
1548
|
+
baseURL: "http://localhost:1234/v1",
|
|
1549
|
+
});
|
|
1550
|
+
expect(config.provider.openrouter.models).toEqual({
|
|
1551
|
+
"google/gemini-3-flash-preview": { options: { reasoning: { effort: "low" } } },
|
|
1552
|
+
});
|
|
1553
|
+
expect(config.provider.anthropic).toEqual({ options: { apiKey: "test-other" } });
|
|
1554
|
+
});
|
|
1555
|
+
test("task opt-out leaves default provider config untouched", async () => {
|
|
1556
|
+
const { applyOpenRouterBaseUrlOverride } = await import("../providers/opencode-adapter");
|
|
1557
|
+
const config = {};
|
|
1558
|
+
applyOpenRouterBaseUrlOverride(config, { OPENROUTER_APP_ATTRIBUTION: "false" });
|
|
1559
|
+
expect(config).toEqual({});
|
|
1560
|
+
});
|
|
1561
|
+
});
|
|
@@ -21,11 +21,13 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
|
21
21
|
import type { Message, Part } from "@opencode-ai/sdk";
|
|
22
22
|
import {
|
|
23
23
|
flattenOpencodeTranscript,
|
|
24
|
+
OPENROUTER_APP_ATTRIBUTION_HEADERS as PLUGIN_OPENROUTER_APP_ATTRIBUTION_HEADERS,
|
|
24
25
|
runSummaryLlm,
|
|
25
26
|
type SummarizeSessionForOpencodeDeps,
|
|
26
27
|
type SwarmConfig,
|
|
27
28
|
summarizeSessionForOpencode,
|
|
28
29
|
} from "../../plugin/opencode-plugins/lib/summarize";
|
|
30
|
+
import { OPENROUTER_APP_ATTRIBUTION_HEADERS } from "../utils/openrouter-base-url";
|
|
29
31
|
|
|
30
32
|
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
31
33
|
|
|
@@ -514,6 +516,41 @@ describe("runSummaryLlm — OPENROUTER_BASE_URL gateway", () => {
|
|
|
514
516
|
|
|
515
517
|
afterEach(() => {
|
|
516
518
|
delete process.env.OPENROUTER_BASE_URL;
|
|
519
|
+
delete process.env.OPENROUTER_APP_ATTRIBUTION;
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
const okSummary = async () => ({
|
|
523
|
+
ok: true,
|
|
524
|
+
status: 200,
|
|
525
|
+
text: async () => "",
|
|
526
|
+
json: async () => summaryBody,
|
|
527
|
+
});
|
|
528
|
+
const sentHeaders = () => new Headers(fetchCalls[0]?.init?.headers);
|
|
529
|
+
|
|
530
|
+
test("the inlined attribution copy matches src/utils/openrouter-base-url.ts", () => {
|
|
531
|
+
expect(PLUGIN_OPENROUTER_APP_ATTRIBUTION_HEADERS).toEqual(OPENROUTER_APP_ATTRIBUTION_HEADERS);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
test("attributes direct openrouter.ai calls to Agent Swarm", async () => {
|
|
535
|
+
fetchHandler = okSummary;
|
|
536
|
+
expect(await runSummaryLlm(openrouterCred, "sys", "user")).not.toBeNull();
|
|
537
|
+
expect(sentHeaders().get("http-referer")).toBe("https://agent-swarm.dev");
|
|
538
|
+
expect(sentHeaders().get("x-openrouter-title")).toBe("Agent Swarm");
|
|
539
|
+
expect(sentHeaders().get("x-openrouter-categories")).toBe("personal-agent,cloud-agent");
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
test("a gateway or OPENROUTER_APP_ATTRIBUTION=false sends no attribution", async () => {
|
|
543
|
+
fetchHandler = okSummary;
|
|
544
|
+
process.env.OPENROUTER_BASE_URL = "https://control-plane.example/proxy/v1/";
|
|
545
|
+
await runSummaryLlm(openrouterCred, "sys", "user");
|
|
546
|
+
expect(sentHeaders().has("http-referer")).toBe(false);
|
|
547
|
+
|
|
548
|
+
fetchCalls.length = 0;
|
|
549
|
+
delete process.env.OPENROUTER_BASE_URL;
|
|
550
|
+
process.env.OPENROUTER_APP_ATTRIBUTION = "false";
|
|
551
|
+
await runSummaryLlm(openrouterCred, "sys", "user");
|
|
552
|
+
expect(fetchCalls[0]?.url).toBe("https://openrouter.ai/api/v1/chat/completions");
|
|
553
|
+
expect(sentHeaders().has("http-referer")).toBe(false);
|
|
517
554
|
});
|
|
518
555
|
|
|
519
556
|
test("defaults to openrouter.ai when the env is unset", async () => {
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { OpenAIEmbeddingProvider } from "../be/memory/providers/openai-embedding";
|
|
3
|
+
import { runMemoryRater } from "../be/memory/raters/llm-summarizer";
|
|
4
|
+
import { validateProviderCredentials } from "../commands/provider-credentials";
|
|
5
|
+
import { completeStructured } from "../utils/internal-ai/complete-structured";
|
|
6
|
+
import type { ExecutorDependencies, ExecutorInput } from "../workflows/executors/base";
|
|
7
|
+
import { executeRawLlm } from "../workflows/executors/raw-llm";
|
|
8
|
+
import { ValidateExecutor } from "../workflows/executors/validate";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* OpenRouter app attribution at each of our own HTTP call sites: present for
|
|
12
|
+
* openrouter.ai, absent for a gateway, absent with OPENROUTER_APP_ATTRIBUTION=false.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const ENV_KEYS = [
|
|
16
|
+
"OPENROUTER_API_KEY",
|
|
17
|
+
"OPENROUTER_BASE_URL",
|
|
18
|
+
"OPENROUTER_APP_ATTRIBUTION",
|
|
19
|
+
"ANTHROPIC_API_KEY",
|
|
20
|
+
"OPENAI_API_KEY",
|
|
21
|
+
"EMBEDDING_API_BASE_URL",
|
|
22
|
+
] as const;
|
|
23
|
+
const GATEWAY = "https://gateway.example.test/v1";
|
|
24
|
+
|
|
25
|
+
type Case = { name: string; env: Record<string, string>; attributed: boolean };
|
|
26
|
+
const CASES: Case[] = [
|
|
27
|
+
{ name: "openrouter.ai", env: {}, attributed: true },
|
|
28
|
+
{ name: "a gateway", env: { OPENROUTER_BASE_URL: GATEWAY }, attributed: false },
|
|
29
|
+
{ name: "the opt-out", env: { OPENROUTER_APP_ATTRIBUTION: "false" }, attributed: false },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
let saved: Record<string, string | undefined> = {};
|
|
33
|
+
let requests: { url: string; headers: Headers }[] = [];
|
|
34
|
+
const origFetch = globalThis.fetch;
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
saved = Object.fromEntries(ENV_KEYS.map((key) => [key, process.env[key]]));
|
|
38
|
+
for (const key of ENV_KEYS) delete process.env[key];
|
|
39
|
+
requests = [];
|
|
40
|
+
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
|
|
41
|
+
const request = new Request(input, init);
|
|
42
|
+
requests.push({ url: request.url, headers: request.headers });
|
|
43
|
+
return new Response("bad request", { status: 400 });
|
|
44
|
+
}) as typeof fetch;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
globalThis.fetch = origFetch;
|
|
49
|
+
for (const key of ENV_KEYS) {
|
|
50
|
+
if (saved[key] === undefined) delete process.env[key];
|
|
51
|
+
else process.env[key] = saved[key];
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function setEnv(env: Record<string, string>) {
|
|
56
|
+
for (const [key, value] of Object.entries(env)) process.env[key] = value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function expectAttribution(headers: Headers | undefined, attributed: boolean) {
|
|
60
|
+
expect(headers).toBeDefined();
|
|
61
|
+
if (attributed) {
|
|
62
|
+
expect(headers?.get("http-referer")).toBe("https://agent-swarm.dev");
|
|
63
|
+
expect(headers?.get("x-openrouter-title")).toBe("Agent Swarm");
|
|
64
|
+
expect(headers?.get("x-openrouter-categories")).toBe("personal-agent,cloud-agent");
|
|
65
|
+
} else {
|
|
66
|
+
expect(headers?.has("http-referer")).toBe(false);
|
|
67
|
+
expect(headers?.has("x-openrouter-title")).toBe(false);
|
|
68
|
+
expect(headers?.has("x-openrouter-categories")).toBe(false);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe("memory rater (llm-summarizer)", () => {
|
|
73
|
+
for (const c of CASES) {
|
|
74
|
+
test(`${c.name}`, async () => {
|
|
75
|
+
setEnv(c.env);
|
|
76
|
+
await runMemoryRater({ prompt: "p", apiKey: "example-or-key" });
|
|
77
|
+
expectAttribution(requests[0]?.headers, c.attributed);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("completeStructured (pi-ai path)", () => {
|
|
83
|
+
for (const c of CASES) {
|
|
84
|
+
test(`${c.name}`, async () => {
|
|
85
|
+
let headers: Headers | undefined;
|
|
86
|
+
await completeStructured({
|
|
87
|
+
zodSchema: { safeParse: () => ({ success: false, error: new Error("x") }) } as never,
|
|
88
|
+
toolSchema: { type: "object", properties: {} } as never,
|
|
89
|
+
toolName: "record",
|
|
90
|
+
toolDescription: "Record.",
|
|
91
|
+
systemPrompt: "sys",
|
|
92
|
+
userPrompt: "user",
|
|
93
|
+
retries: 1,
|
|
94
|
+
env: c.env,
|
|
95
|
+
_credentialOverride: {
|
|
96
|
+
kind: "openrouter",
|
|
97
|
+
apiKey: "example-or-key",
|
|
98
|
+
modelDefault: "openrouter/deepseek/deepseek-v4.1-flash",
|
|
99
|
+
},
|
|
100
|
+
_complete: async (_model, _context, options) => {
|
|
101
|
+
headers = new Headers((options?.headers ?? {}) as Record<string, string>);
|
|
102
|
+
throw new Error("stop");
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
expectAttribution(headers, c.attributed);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
test("a non-OpenRouter credential gets no attribution", async () => {
|
|
110
|
+
let headers: Headers | undefined;
|
|
111
|
+
await completeStructured({
|
|
112
|
+
zodSchema: { safeParse: () => ({ success: false, error: new Error("x") }) } as never,
|
|
113
|
+
toolSchema: { type: "object", properties: {} } as never,
|
|
114
|
+
toolName: "record",
|
|
115
|
+
toolDescription: "Record.",
|
|
116
|
+
systemPrompt: "sys",
|
|
117
|
+
userPrompt: "user",
|
|
118
|
+
retries: 1,
|
|
119
|
+
env: {},
|
|
120
|
+
_credentialOverride: {
|
|
121
|
+
kind: "anthropic",
|
|
122
|
+
apiKey: "example-ant-key",
|
|
123
|
+
modelDefault: "anthropic/claude-sonnet-4-5",
|
|
124
|
+
},
|
|
125
|
+
_complete: async (_model, _context, options) => {
|
|
126
|
+
headers = new Headers((options?.headers ?? {}) as Record<string, string>);
|
|
127
|
+
throw new Error("stop");
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
expectAttribution(headers, false);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("workflow raw-llm and validate nodes", () => {
|
|
135
|
+
const deps = { interpolate: (template: string) => template } as unknown as ExecutorDependencies;
|
|
136
|
+
const meta = { runId: "run", stepId: "step", nodeId: "node" } as unknown as ExecutorInput["meta"];
|
|
137
|
+
|
|
138
|
+
for (const c of CASES) {
|
|
139
|
+
test(`raw-llm: ${c.name}`, async () => {
|
|
140
|
+
setEnv({ OPENROUTER_API_KEY: "example-or-key", ...c.env });
|
|
141
|
+
expect((await executeRawLlm({ prompt: "hi" })).status).toBe("failed");
|
|
142
|
+
expect(
|
|
143
|
+
requests[0]?.url.startsWith(c.env.OPENROUTER_BASE_URL ?? "https://openrouter.ai/"),
|
|
144
|
+
).toBe(true);
|
|
145
|
+
expectAttribution(requests[0]?.headers, c.attributed);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test(`validate: ${c.name}`, async () => {
|
|
149
|
+
setEnv({ OPENROUTER_API_KEY: "example-or-key", ...c.env });
|
|
150
|
+
await new ValidateExecutor(deps).run({
|
|
151
|
+
config: { targetNodeId: "a", prompt: "Is it valid?" },
|
|
152
|
+
context: { a: "output" },
|
|
153
|
+
meta,
|
|
154
|
+
});
|
|
155
|
+
expectAttribution(requests[0]?.headers, c.attributed);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
test("an OpenAI credential gets no attribution", async () => {
|
|
160
|
+
setEnv({ OPENAI_API_KEY: "example-sk-openai" });
|
|
161
|
+
await executeRawLlm({ prompt: "hi" });
|
|
162
|
+
expect(requests[0]?.url.startsWith("https://api.openai.com/")).toBe(true);
|
|
163
|
+
expectAttribution(requests[0]?.headers, false);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("OpenRouter credential check (GET /models)", () => {
|
|
168
|
+
for (const c of CASES) {
|
|
169
|
+
test(`${c.name}`, async () => {
|
|
170
|
+
setEnv(c.env);
|
|
171
|
+
await validateProviderCredentials("grok", { OPENROUTER_API_KEY: "example-or-key" });
|
|
172
|
+
expect(requests[0]?.url.endsWith("/models")).toBe(true);
|
|
173
|
+
expectAttribution(requests[0]?.headers, c.attributed);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("OpenAI-compatible embedding provider", () => {
|
|
179
|
+
const embedCases: Case[] = [
|
|
180
|
+
{
|
|
181
|
+
name: "an openrouter.ai base URL",
|
|
182
|
+
env: { EMBEDDING_API_BASE_URL: "https://openrouter.ai/api/v1" },
|
|
183
|
+
attributed: true,
|
|
184
|
+
},
|
|
185
|
+
{ name: "the OpenAI default", env: {}, attributed: false },
|
|
186
|
+
{ name: "a gateway", env: { EMBEDDING_API_BASE_URL: GATEWAY }, attributed: false },
|
|
187
|
+
{
|
|
188
|
+
name: "the opt-out",
|
|
189
|
+
env: {
|
|
190
|
+
EMBEDDING_API_BASE_URL: "https://openrouter.ai/api/v1",
|
|
191
|
+
OPENROUTER_APP_ATTRIBUTION: "0",
|
|
192
|
+
},
|
|
193
|
+
attributed: false,
|
|
194
|
+
},
|
|
195
|
+
];
|
|
196
|
+
for (const c of embedCases) {
|
|
197
|
+
test(`${c.name}`, async () => {
|
|
198
|
+
setEnv(c.env);
|
|
199
|
+
const provider = new OpenAIEmbeddingProvider({ apiKey: "example-embed-key" });
|
|
200
|
+
expect(await provider.embed("hello")).toBeNull();
|
|
201
|
+
expectAttribution(requests[0]?.headers, c.attributed);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
});
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Covers the shared resolver (`src/utils/openrouter-base-url.ts`), the pi
|
|
5
5
|
* models.json override writer (`ensureOpenRouterModelsOverride`) including
|
|
6
|
-
* its composition through pi-coding-agent's ModelRuntime, and
|
|
7
|
-
* per-task config injection (`applyOpenRouterBaseUrlOverride`).
|
|
6
|
+
* its composition through pi-coding-agent's ModelRuntime, and attribution host gating.
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
9
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
@@ -12,10 +11,12 @@ import { mkdtempSync, rmSync } from "node:fs";
|
|
|
12
11
|
import { tmpdir } from "node:os";
|
|
13
12
|
import { join } from "node:path";
|
|
14
13
|
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import type { Config } from "@opencode-ai/sdk";
|
|
16
|
-
import { applyOpenRouterBaseUrlOverride } from "../providers/opencode-adapter";
|
|
17
14
|
import { ensureOpenRouterModelsOverride } from "../providers/pi-mono-adapter";
|
|
18
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_OPENROUTER_BASE_URL,
|
|
17
|
+
getOpenRouterAttributionHeaders,
|
|
18
|
+
getOpenRouterBaseUrl,
|
|
19
|
+
} from "../utils/openrouter-base-url";
|
|
19
20
|
|
|
20
21
|
const GATEWAY = "https://control-plane.example/proxy/v1";
|
|
21
22
|
|
|
@@ -220,40 +221,30 @@ describe("ensureOpenRouterModelsOverride (pi)", () => {
|
|
|
220
221
|
});
|
|
221
222
|
});
|
|
222
223
|
|
|
223
|
-
describe("
|
|
224
|
-
test(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
applyOpenRouterBaseUrlOverride(config, { OPENROUTER_BASE_URL: GATEWAY });
|
|
233
|
-
expect(config.provider?.openrouter?.options?.baseURL).toBe(GATEWAY);
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
test("preserves existing provider config (e.g. reasoning model options)", () => {
|
|
237
|
-
const config: Config = {
|
|
238
|
-
model: "openrouter/google/gemini-3-flash-preview",
|
|
239
|
-
provider: {
|
|
240
|
-
openrouter: {
|
|
241
|
-
models: {
|
|
242
|
-
"google/gemini-3-flash-preview": { options: { reasoning: { effort: "low" } } },
|
|
243
|
-
},
|
|
244
|
-
options: { apiKey: "sk-keep" },
|
|
245
|
-
},
|
|
246
|
-
anthropic: { options: { apiKey: "sk-ant" } },
|
|
247
|
-
},
|
|
248
|
-
};
|
|
249
|
-
applyOpenRouterBaseUrlOverride(config, { OPENROUTER_BASE_URL: GATEWAY });
|
|
250
|
-
expect(config.provider?.openrouter?.options).toEqual({
|
|
251
|
-
apiKey: "sk-keep",
|
|
252
|
-
baseURL: GATEWAY,
|
|
253
|
-
});
|
|
254
|
-
expect(config.provider?.openrouter?.models).toEqual({
|
|
255
|
-
"google/gemini-3-flash-preview": { options: { reasoning: { effort: "low" } } },
|
|
224
|
+
describe("getOpenRouterAttributionHeaders", () => {
|
|
225
|
+
test.each([
|
|
226
|
+
"https://openrouter.ai/api/v1",
|
|
227
|
+
"https://api.openrouter.ai/v1",
|
|
228
|
+
])("attributes direct OpenRouter host %s", (url) => {
|
|
229
|
+
expect(getOpenRouterAttributionHeaders(url, {})).toEqual({
|
|
230
|
+
"HTTP-Referer": "https://agent-swarm.dev",
|
|
231
|
+
"X-OpenRouter-Title": "Agent Swarm",
|
|
232
|
+
"X-OpenRouter-Categories": "personal-agent,cloud-agent",
|
|
256
233
|
});
|
|
257
|
-
|
|
234
|
+
});
|
|
235
|
+
test.each([
|
|
236
|
+
"http://localhost:1234/v1",
|
|
237
|
+
"https://evil-openrouter.ai",
|
|
238
|
+
"https://openrouter.ai.evil.com",
|
|
239
|
+
"invalid",
|
|
240
|
+
])("does not attribute gateway, lookalike or invalid URL %s", (url) => {
|
|
241
|
+
expect(getOpenRouterAttributionHeaders(url, {})).toEqual({});
|
|
242
|
+
});
|
|
243
|
+
test.each(["false", "FALSE", "FaLsE", "0"])("honors opt-out %s", (value) => {
|
|
244
|
+
expect(
|
|
245
|
+
getOpenRouterAttributionHeaders("https://openrouter.ai", {
|
|
246
|
+
OPENROUTER_APP_ATTRIBUTION: value,
|
|
247
|
+
}),
|
|
248
|
+
).toEqual({});
|
|
258
249
|
});
|
|
259
250
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Unit tests for `summarizeSessionForPi` in `src/providers/pi-mono-extension.ts
|
|
2
|
+
* Unit tests for `summarizeSessionForPi` in `src/providers/pi-mono-extension.ts`,
|
|
3
|
+
* plus the extension's advisory nudges (delivered with `pi.sendMessage`).
|
|
3
4
|
*
|
|
4
5
|
* Plan: thoughts/taras/plans/2026-05-10-fix-session-summarization-workers.md
|
|
5
6
|
* → Phase 1 § "Test coverage"
|
|
@@ -19,8 +20,14 @@
|
|
|
19
20
|
*/
|
|
20
21
|
|
|
21
22
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
23
|
+
import type { ExtensionAPI, ExtensionFactory } from "@earendil-works/pi-coding-agent";
|
|
24
|
+
import { clearToolHistory } from "../hooks/tool-loop-detection";
|
|
22
25
|
import type { SummarizeSessionForPiDeps, SwarmHooksConfig } from "../providers/pi-mono-extension";
|
|
23
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
createSwarmHooksExtension,
|
|
28
|
+
SWARM_NUDGE_CUSTOM_TYPE,
|
|
29
|
+
summarizeSessionForPi,
|
|
30
|
+
} from "../providers/pi-mono-extension";
|
|
24
31
|
|
|
25
32
|
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
26
33
|
|
|
@@ -345,3 +352,155 @@ describe("summarizeSessionForPi", () => {
|
|
|
345
352
|
expect(consoleErrors.length).toBe(0);
|
|
346
353
|
});
|
|
347
354
|
});
|
|
355
|
+
|
|
356
|
+
describe("pi provider header attribution", () => {
|
|
357
|
+
test("uses request model URL and task env, replacing pi attribution", async () => {
|
|
358
|
+
let handler:
|
|
359
|
+
| ((event: { headers: Record<string, string> }, ctx: { model: { baseUrl: string } }) => void)
|
|
360
|
+
| undefined;
|
|
361
|
+
const pi = {
|
|
362
|
+
on: (event: string, callback: unknown) => {
|
|
363
|
+
if (event === "before_provider_headers") handler = callback as NonNullable<typeof handler>;
|
|
364
|
+
},
|
|
365
|
+
} as unknown as ExtensionAPI;
|
|
366
|
+
const config = { ...makeConfig(), env: {} as Record<string, string | undefined> };
|
|
367
|
+
await createSwarmHooksExtension(config)(pi);
|
|
368
|
+
expect(handler).toBeDefined();
|
|
369
|
+
const headers = {
|
|
370
|
+
"HTTP-Referer": "https://pi.dev",
|
|
371
|
+
"X-OpenRouter-Title": "pi",
|
|
372
|
+
"X-Custom": "keep",
|
|
373
|
+
};
|
|
374
|
+
handler!({ headers }, { model: { baseUrl: "https://openrouter.ai/api" } });
|
|
375
|
+
expect(headers).toEqual({
|
|
376
|
+
"HTTP-Referer": "https://agent-swarm.dev",
|
|
377
|
+
"X-OpenRouter-Title": "Agent Swarm",
|
|
378
|
+
"X-OpenRouter-Categories": "personal-agent,cloud-agent",
|
|
379
|
+
"X-Custom": "keep",
|
|
380
|
+
});
|
|
381
|
+
config.env.OPENROUTER_APP_ATTRIBUTION = "false";
|
|
382
|
+
const optedOut = { "X-Custom": "keep" };
|
|
383
|
+
handler!({ headers: optedOut }, { model: { baseUrl: "https://openrouter.ai/api" } });
|
|
384
|
+
expect(optedOut).toEqual({ "X-Custom": "keep" });
|
|
385
|
+
config.env = {};
|
|
386
|
+
const gateway = {};
|
|
387
|
+
handler!({ headers: gateway }, { model: { baseUrl: "http://localhost:1234/v1" } });
|
|
388
|
+
expect(gateway).toEqual({});
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// ── advisory nudges reach the model via pi.sendMessage ───────────────────────
|
|
393
|
+
|
|
394
|
+
type Handler = (event: unknown, ctx: unknown) => Promise<unknown> | unknown;
|
|
395
|
+
type SentMessage = { customType: string; content: Array<{ type: string; text: string }> };
|
|
396
|
+
|
|
397
|
+
/** Minimal stand-in for pi's ExtensionAPI: records handlers and sent messages. */
|
|
398
|
+
function loadExtension(config: SwarmHooksConfig) {
|
|
399
|
+
const handlers = new Map<string, Handler>();
|
|
400
|
+
const sent: SentMessage[] = [];
|
|
401
|
+
const pi = {
|
|
402
|
+
on: (event: string, handler: Handler) => {
|
|
403
|
+
handlers.set(event, handler);
|
|
404
|
+
},
|
|
405
|
+
sendMessage: (message: SentMessage) => {
|
|
406
|
+
sent.push(message);
|
|
407
|
+
},
|
|
408
|
+
registerMcpServer: () => {},
|
|
409
|
+
};
|
|
410
|
+
createSwarmHooksExtension(config)(pi as unknown as Parameters<ExtensionFactory>[0]);
|
|
411
|
+
const ctx = { getContextUsage: () => undefined };
|
|
412
|
+
const fire = (event: string, payload: unknown = {}) => {
|
|
413
|
+
const handler = handlers.get(event);
|
|
414
|
+
if (!handler) throw new Error(`no handler for ${event}`);
|
|
415
|
+
return handler({ type: event, ...(payload as object) }, ctx);
|
|
416
|
+
};
|
|
417
|
+
const texts = () =>
|
|
418
|
+
sent.map((m) => {
|
|
419
|
+
expect(m.customType).toBe(SWARM_NUDGE_CUSTOM_TYPE);
|
|
420
|
+
return m.content.map((c) => c.text).join("");
|
|
421
|
+
});
|
|
422
|
+
return { fire, texts };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function jsonResponse(body: unknown): FetchHandlerResp {
|
|
426
|
+
return { ok: true, status: 200, text: async () => "", json: async () => body };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
describe("createSwarmHooksExtension nudges", () => {
|
|
430
|
+
test("loop warning on tool_call is sent to the model", async () => {
|
|
431
|
+
const config = { ...makeConfig(), taskId: `task-pi-loop-${crypto.randomUUID()}` };
|
|
432
|
+
const { fire, texts } = loadExtension(config);
|
|
433
|
+
try {
|
|
434
|
+
for (let i = 0; i < 8; i++) {
|
|
435
|
+
const result = await fire("tool_call", { toolName: "bash", input: { command: "ls" } });
|
|
436
|
+
expect(result).toBeUndefined();
|
|
437
|
+
}
|
|
438
|
+
const warnings = texts().filter((t) => t.startsWith("Warning: "));
|
|
439
|
+
expect(warnings.length).toBe(1);
|
|
440
|
+
expect(warnings[0]).toContain("bash");
|
|
441
|
+
} finally {
|
|
442
|
+
await clearToolHistory(config.taskId);
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test("lead send-task reminder on tool_result is sent to the model", async () => {
|
|
447
|
+
const { fire, texts } = loadExtension({ ...makeConfig(), isLead: true });
|
|
448
|
+
await fire("tool_result", { toolName: "mcp__agent-swarm__send-task", input: {} });
|
|
449
|
+
expect(texts()).toEqual([
|
|
450
|
+
"Task sent successfully. Monitor progress using the get-task-details tool periodically.",
|
|
451
|
+
]);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
test("lead concurrent context on session_start is sent to the model", async () => {
|
|
455
|
+
fetchHandler = async (url) =>
|
|
456
|
+
url.includes("/api/concurrent-context")
|
|
457
|
+
? jsonResponse({
|
|
458
|
+
processingInboxMessages: [
|
|
459
|
+
{ content: "deploy the thing", source: "slack", createdAt: "now" },
|
|
460
|
+
],
|
|
461
|
+
recentTaskDelegations: [],
|
|
462
|
+
activeSwarmTasks: [],
|
|
463
|
+
})
|
|
464
|
+
: jsonResponse({});
|
|
465
|
+
const { fire, texts } = loadExtension({ ...makeConfig(), isLead: true });
|
|
466
|
+
await fire("session_start");
|
|
467
|
+
expect(texts().length).toBe(1);
|
|
468
|
+
expect(texts()[0]).toContain("=== CONCURRENT SESSION AWARENESS ===");
|
|
469
|
+
expect(texts()[0]).toContain("deploy the thing");
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test("goal reminder is sent after compaction, not before every LLM call", async () => {
|
|
473
|
+
fetchHandler = async (url) =>
|
|
474
|
+
url.endsWith("/api/tasks/task-pi-1")
|
|
475
|
+
? jsonResponse({ id: "task-pi-1", task: "ship the fix", progress: "tests green" })
|
|
476
|
+
: jsonResponse({});
|
|
477
|
+
const { fire, texts } = loadExtension(makeConfig());
|
|
478
|
+
|
|
479
|
+
await fire("context", { messages: [] });
|
|
480
|
+
expect(texts()).toEqual([]);
|
|
481
|
+
|
|
482
|
+
await fire("session_compact");
|
|
483
|
+
expect(texts()).toEqual([
|
|
484
|
+
[
|
|
485
|
+
"=== GOAL REMINDER (injected before context compaction) ===",
|
|
486
|
+
"Task ID: task-pi-1",
|
|
487
|
+
"Task: ship the fix",
|
|
488
|
+
"Current Progress: tests green",
|
|
489
|
+
"=== Continue working on this task after compaction ===",
|
|
490
|
+
].join("\n"),
|
|
491
|
+
]);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test("cancelled task on input skips the turn and leaves the nudge in the session", async () => {
|
|
495
|
+
fetchHandler = async (url) =>
|
|
496
|
+
url.includes("/cancelled-tasks")
|
|
497
|
+
? jsonResponse({ cancelled: [{ id: "task-pi-1", failureReason: "lead stopped it" }] })
|
|
498
|
+
: jsonResponse({});
|
|
499
|
+
const { fire, texts } = loadExtension(makeConfig());
|
|
500
|
+
const result = await fire("input", { text: "go", source: "rpc" });
|
|
501
|
+
expect(result).toEqual({ action: "handled" });
|
|
502
|
+
expect(texts()).toEqual([
|
|
503
|
+
"🛑 TASK CANCELLED: lead stopped it. Stop working and use store-progress to acknowledge.",
|
|
504
|
+
]);
|
|
505
|
+
});
|
|
506
|
+
});
|