@desplega.ai/agent-swarm 1.166.0 → 1.168.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.
Files changed (221) hide show
  1. package/dist/acp-adapter-zhejfecx.js +34 -0
  2. package/dist/{actions-qz3m692w.js → actions-7jg840nr.js} +25 -25
  3. package/dist/{amp-adapter-j1xbmsqz.js → amp-adapter-ye6rnetm.js} +4 -4
  4. package/dist/{anthropic-messages-hst62ws9.js → anthropic-messages-13yn502s.js} +4 -4
  5. package/dist/{app-43vrdmv8.js → app-ffes2kyf.js} +10 -9
  6. package/dist/{assistant-ertffmka.js → assistant-fhm5smyz.js} +26 -26
  7. package/dist/{azure-openai-responses-mh8tvf78.js → azure-openai-responses-9st34pfz.js} +5 -5
  8. package/dist/{boot-reembed-998jtpdg.js → boot-reembed-20q3nzvg.js} +9 -8
  9. package/dist/{boot-reembed-s7k7c6v2.js → boot-reembed-jvfadhsk.js} +9 -8
  10. package/dist/{boot-scrub-sweep-nepsk4t1.js → boot-scrub-sweep-jtfajzve.js} +10 -9
  11. package/dist/{boot-seeds-vvzs1zhk.js → boot-seeds-9qt0j8j2.js} +12 -11
  12. package/dist/{claude-adapter-2zvmekrp.js → claude-adapter-a8znhthz.js} +24 -22
  13. package/dist/{claude-managed-adapter-s49wbk8t.js → claude-managed-adapter-tzwxy803.js} +4 -4
  14. package/dist/{claude-managed-setup-cvhqq8qn.js → claude-managed-setup-nrhfx5b0.js} +3 -3
  15. package/dist/{claude-sdk-session-kkbxtw5q.js → claude-sdk-session-7x7chdrs.js} +7608 -7596
  16. package/dist/{cli-zsh093hk.js → cli-014vns2y.js} +63 -1
  17. package/dist/{cli-8kgvmkak.js → cli-0jngabtb.js} +1 -1
  18. package/dist/{cli-qvfk83fy.js → cli-13xwcqvj.js} +2 -2
  19. package/dist/{cli-0nhdwxd5.js → cli-43jg9xdw.js} +3 -3
  20. package/dist/{cli-cpfmqz3n.js → cli-47zfzshm.js} +5 -5
  21. package/dist/{cli-zy7vwsne.js → cli-5qkcdv8c.js} +16 -9
  22. package/dist/{cli-cwxhbf43.js → cli-5t5my0h8.js} +85 -2
  23. package/dist/{cli-y59h2pk9.js → cli-69egj654.js} +1 -1
  24. package/dist/{cli-68r1j4sg.js → cli-6bpvv8h4.js} +3 -3
  25. package/dist/{cli-yshaazs1.js → cli-74ez9b66.js} +63 -20
  26. package/dist/{cli-32bca7nr.js → cli-7avy4v8w.js} +1 -1
  27. package/dist/{cli-f2n17zhp.js → cli-7mhy3ths.js} +21 -18
  28. package/dist/{cli-q38n4bq0.js → cli-95zs84qw.js} +3 -3
  29. package/dist/{cli-fn697bnt.js → cli-9q0bh411.js} +21 -8
  30. package/dist/{cli-rggdq8wq.js → cli-9q24yyvs.js} +3 -3
  31. package/dist/cli-a35hzdve.js +33 -0
  32. package/dist/{cli-3qn4r334.js → cli-aj1e0h0d.js} +20 -14
  33. package/dist/{cli-rsy933yz.js → cli-bcspch6h.js} +30 -19
  34. package/dist/{cli-x3x0qwky.js → cli-bxqez1p0.js} +51 -23
  35. package/dist/{cli-ng97fjq7.js → cli-cmxkbzzs.js} +33 -7
  36. package/dist/{cli-df56tqdw.js → cli-ee0402n1.js} +198 -70
  37. package/dist/{cli-mwegs4af.js → cli-ew0c3a1j.js} +3 -3
  38. package/dist/{cli-ysnyhws9.js → cli-ewhwpyrb.js} +54 -51
  39. package/dist/{cli-j2jbr8af.js → cli-fhj4y16f.js} +1 -1
  40. package/dist/{cli-8t229cmr.js → cli-fqq06kkb.js} +30 -17
  41. package/dist/{cli-jvj6a0cv.js → cli-g6gx2dn1.js} +54 -19
  42. package/dist/{cli-nexwh8zg.js → cli-g7k7zbcz.js} +12 -5
  43. package/dist/{cli-ecknp7p1.js → cli-j05x7pnb.js} +10 -10
  44. package/dist/{cli-gt8azyet.js → cli-jb931ba2.js} +1 -1
  45. package/dist/{cli-nvvfd8bq.js → cli-jbzbr36n.js} +4091 -3793
  46. package/dist/{cli-3ddybbbx.js → cli-jvmtxqtc.js} +26 -14
  47. package/dist/{cli-pkke6geq.js → cli-km4x66j0.js} +83 -65
  48. package/dist/{cli-b0f70z24.js → cli-mn8qkd8v.js} +1 -1
  49. package/dist/{cli-0qjcsxgz.js → cli-nctfv2yy.js} +99 -9
  50. package/dist/{cli-4r1sjfc4.js → cli-ng1pmdx6.js} +10 -10
  51. package/dist/cli-nkqjj42q.js +104 -0
  52. package/dist/{cli-aqcey741.js → cli-p5jqk8fr.js} +1 -1
  53. package/dist/{cli-66zh26sp.js → cli-pgehz8e1.js} +57 -17
  54. package/dist/cli-q1vnz8wv.js +96 -0
  55. package/dist/{cli-fj86xr3c.js → cli-qd97v6z2.js} +2 -2
  56. package/dist/{cli-p4ztb80h.js → cli-r7eeddkq.js} +13 -8
  57. package/dist/{cli-8ssjez7e.js → cli-rhy7z1rp.js} +22 -11
  58. package/dist/{cli-vzc73wet.js → cli-rr5z1j4p.js} +87 -69
  59. package/dist/{cli-4mevd1hf.js → cli-s9y81s7j.js} +5 -5
  60. package/dist/{cli-dw2q9f7g.js → cli-svah6b6w.js} +4 -4
  61. package/dist/{cli-h3x613ze.js → cli-tc5zec7v.js} +6 -6
  62. package/dist/{cli-ttbsse3f.js → cli-tmhsxhb8.js} +1 -1
  63. package/dist/{cli-feh3dkah.js → cli-v1w7pbd8.js} +2 -1
  64. package/dist/{cli-10zmp1p7.js → cli-vr5avem2.js} +27 -32
  65. package/dist/{cli-bzr0ftht.js → cli-wnb9b2g3.js} +19 -0
  66. package/dist/{cli-krqm5f11.js → cli-xezn9nkh.js} +16 -13
  67. package/dist/{cli-8kt0bmgx.js → cli-xf713zz0.js} +2 -2
  68. package/dist/{cli-6zcfw0x7.js → cli-y195hv1a.js} +1 -1
  69. package/dist/{cli-x7kpr72a.js → cli-ytmmyj7f.js} +2 -2
  70. package/dist/{cli-v2a8x45d.js → cli-z48ggfyq.js} +11 -6
  71. package/dist/{cli-qbksc292.js → cli-zy39d433.js} +2 -0
  72. package/dist/cli.js +28 -28
  73. package/dist/{cloudflare-workers-ai-system-one-tt484t61.js → cloudflare-workers-ai-system-one-sta45jf9.js} +7 -5
  74. package/dist/{codex-adapter-dw1wxqt8.js → codex-adapter-n63k33d3.js} +18 -18
  75. package/dist/{codex-login-terbdsqq.js → codex-login-g1cyt5zm.js} +1 -1
  76. package/dist/{codex-session-runner-3cgb8y1g.js → codex-session-runner-xkxfd81a.js} +18 -18
  77. package/dist/{commands-mhxam6z2.js → commands-q4mfhn51.js} +9 -8
  78. package/dist/{cursor-adapter-2qda3st7.js → cursor-adapter-v8mm6bp5.js} +5 -5
  79. package/dist/{db-sjks418k.js → db-1t0bj3pc.js} +9 -8
  80. package/dist/{dispatcher-1es759rn.js → dispatcher-k1fhavcf.js} +24 -24
  81. package/dist/{dsh-adapter-bek4cyw9.js → dsh-adapter-5423p70m.js} +5 -5
  82. package/dist/{easter-egg-3d-mtpnw06s.js → easter-egg-3d-66zxervv.js} +2 -2
  83. package/dist/{execute-s4ccjq3n.js → execute-h4jgevyw.js} +61 -11
  84. package/dist/{google-generative-ai-d6nphg3q.js → google-generative-ai-b8zxrpax.js} +5 -5
  85. package/dist/{google-vertex-cpshkref.js → google-vertex-nrdd697t.js} +5 -5
  86. package/dist/{grok-adapter-ar9cw9jg.js → grok-adapter-phrndeb8.js} +18 -8
  87. package/dist/{handlers-1gkbhktr.js → handlers-59ywqwzh.js} +27 -27
  88. package/dist/{hook-vxa3tcsy.js → hook-atj0swc2.js} +14 -14
  89. package/dist/{hook-ds2zxq9v.js → hook-mew1chq9.js} +42 -31
  90. package/dist/{http-3h21z9md.js → http-ef2fnn9v.js} +953 -98
  91. package/dist/{index-n99fycq7.js → index-48pb9erw.js} +24 -24
  92. package/dist/{index-bn6cy8ps.js → index-9fc2h7ww.js} +24 -24
  93. package/dist/{index-9b65j1m5.js → index-gr69c43q.js} +9 -8
  94. package/dist/{index-f8xnqrye.js → index-h26wjcfy.js} +43 -24
  95. package/dist/{index-dkfxpkmm.js → index-w2yhye23.js} +25 -25
  96. package/dist/{keepalive-3xspa9jw.js → keepalive-qhcqz56v.js} +11 -10
  97. package/dist/{lead-6qphc124.js → lead-hpk5r3b1.js} +47 -47
  98. package/dist/{llama-cpp-classify-tgj35k8d.js → llama-cpp-classify-6erb7wwv.js} +3 -1
  99. package/dist/{maintenance-0teem1bb.js → maintenance-nvkyp2xe.js} +9 -8
  100. package/dist/{mistral-conversations-8445ddjn.js → mistral-conversations-059y51zt.js} +4 -4
  101. package/dist/{node-9sg7ajby.js → node-qm9eb6z0.js} +1 -1
  102. package/dist/{oauth-refresh-sweep-sh9q25xp.js → oauth-refresh-sweep-ngskthwh.js} +10 -9
  103. package/dist/{onboard-54qmnqxe.js → onboard-yfnkqhxc.js} +2 -2
  104. package/dist/{openai-codex-responses-9ynm88zy.js → openai-codex-responses-jzxkta9b.js} +8 -7
  105. package/dist/{openai-completions-nhqwjmaw.js → openai-completions-663yncfq.js} +4 -4
  106. package/dist/openai-decisions-vj9nkpf8.js +162 -0
  107. package/dist/{openai-responses-xx1c4kgm.js → openai-responses-m3g43rrt.js} +5 -5
  108. package/dist/{opencode-adapter-wjfa2wvd.js → opencode-adapter-yjyqynzf.js} +7 -7
  109. package/dist/{openrouter-images-8yx3487g.js → openrouter-images-kr60n6b9.js} +1 -1
  110. package/dist/{otel-impl-ycqzp2a1.js → otel-impl-hhm64r6m.js} +1 -1
  111. package/dist/{pi-messages-vxdt8v7j.js → pi-messages-xszk613n.js} +1 -1
  112. package/dist/{pi-mono-adapter-e77e9zst.js → pi-mono-adapter-0eg8kw2c.js} +68 -51
  113. package/dist/{pricing-refresh-atnkngqs.js → pricing-refresh-35f91nx2.js} +11 -10
  114. package/dist/{rbac-roles-j78wdksm.js → rbac-roles-97w370qj.js} +10 -9
  115. package/dist/{rbac-roles-9pjphh9q.js → rbac-roles-z6sje3gy.js} +9 -8
  116. package/dist/{render-v2-pxjs49fx.js → render-v2-n4v66bj3.js} +10 -9
  117. package/dist/{runtime-v2t1r79e.js → runtime-h9ha1mq6.js} +4 -4
  118. package/dist/{setup-ymej0xtx.js → setup-m3hhyv5f.js} +2 -2
  119. package/dist/{typesafe-system-one-datfr82p.js → typesafe-system-one-e988vty7.js} +7 -5
  120. package/dist/{virtual-modules-wm8mxdxx.js → virtual-modules-ftr9kwe8.js} +11 -11
  121. package/dist/{work-objects-3nh4ymzb.js → work-objects-mxf2c842.js} +27 -27
  122. package/dist/{worker-qa9fkz5s.js → worker-9t07z1gq.js} +47 -47
  123. package/openapi.json +72 -1
  124. package/package.json +10 -10
  125. package/src/be/memory/providers/openai-embedding.ts +7 -2
  126. package/src/be/memory/raters/llm-summarizer.ts +7 -2
  127. package/src/be/modelsdev-cache.json +61 -0
  128. package/src/be/script-connections.ts +40 -6
  129. package/src/be/seed-pricing.ts +84 -53
  130. package/src/be/swarm-config-guard.ts +14 -0
  131. package/src/commands/provider-credentials.ts +28 -5
  132. package/src/commands/runner.ts +7 -12
  133. package/src/extensions/catalog.generated.json +17 -0
  134. package/src/heartbeat/heartbeat.ts +91 -19
  135. package/src/hooks/hook.ts +72 -27
  136. package/src/http/mcp.ts +5 -0
  137. package/src/http/onboarding.ts +2 -0
  138. package/src/http/script-connections.ts +35 -0
  139. package/src/providers/acp-targets.ts +10 -5
  140. package/src/providers/amp-adapter.ts +1 -1
  141. package/src/providers/claude-adapter.ts +32 -0
  142. package/src/providers/claude-background-keepalive.ts +100 -0
  143. package/src/providers/claude-sdk-session.ts +6 -0
  144. package/src/providers/codex-adapter.ts +30 -3
  145. package/src/providers/codex-oauth/auth-json.ts +21 -4
  146. package/src/providers/codex-oauth/types.ts +2 -0
  147. package/src/providers/dsh-adapter.ts +11 -2
  148. package/src/providers/grok-adapter.ts +18 -2
  149. package/src/providers/opencode-adapter.ts +21 -5
  150. package/src/providers/pi-mono-extension.ts +56 -25
  151. package/src/providers/pricing-sources.md +2 -0
  152. package/src/scheduler/scheduler.ts +82 -13
  153. package/src/scripts-runtime/executors/registry.ts +1 -1
  154. package/src/server.ts +63 -3
  155. package/src/tests/acp-adapter.test.ts +152 -12
  156. package/src/tests/ci-unit-test-report.test.ts +73 -0
  157. package/src/tests/claude-background-keepalive-wiring.test.ts +220 -0
  158. package/src/tests/claude-background-keepalive.fixture.ts +162 -0
  159. package/src/tests/claude-background-keepalive.test.ts +139 -0
  160. package/src/tests/claude-default-route.test.ts +75 -0
  161. package/src/tests/claude-hook-output.test.ts +233 -0
  162. package/src/tests/codex-adapter-pool-auth-revalidate.test.ts +4 -2
  163. package/src/tests/codex-adapter.test.ts +166 -26
  164. package/src/tests/codex-oauth-auth-timestamps.test.ts +76 -0
  165. package/src/tests/codex-oauth.test.ts +4 -1
  166. package/src/tests/connection-embedded-auth.test.ts +51 -0
  167. package/src/tests/context-window.test.ts +2 -2
  168. package/src/tests/credential-check.test.ts +51 -1
  169. package/src/tests/docs-index-footprint.test.js +3 -3
  170. package/src/tests/dsh-adapter.test.ts +24 -2
  171. package/src/tests/enabled-tools.test.ts +269 -0
  172. package/src/tests/entrypoint-codex-oauth-seed.test.ts +12 -0
  173. package/src/tests/extensions-catalog.test.ts +1 -0
  174. package/src/tests/extensions-example-slack-reply-gate.test.ts +756 -0
  175. package/src/tests/grok-adapter.test.ts +13 -0
  176. package/src/tests/haiku-5-5-pricing.test.ts +38 -0
  177. package/src/tests/heartbeat-checklist.test.ts +158 -1
  178. package/src/tests/onboarding.test.ts +44 -0
  179. package/src/tests/opencode-adapter.test.ts +66 -3
  180. package/src/tests/opencode-plugin.test.ts +37 -0
  181. package/src/tests/openrouter-attribution-call-sites.test.ts +204 -0
  182. package/src/tests/openrouter-base-url.test.ts +30 -39
  183. package/src/tests/pi-mono-extension.test.ts +161 -2
  184. package/src/tests/pricing-refresh.test.ts +70 -1
  185. package/src/tests/reasoning-effort.test.ts +3 -1
  186. package/src/tests/schedule-target-type.test.ts +136 -0
  187. package/src/tests/script-connections-http.test.ts +45 -0
  188. package/src/tests/script-connections-mcp.test.ts +41 -0
  189. package/src/tests/script-executor-registry.test.ts +18 -0
  190. package/src/tests/send-task-self-send.test.ts +107 -0
  191. package/src/tests/telemetry-trigger.test.ts +12 -1
  192. package/src/tests/workflow-llm.test.ts +14 -0
  193. package/src/tests/workflow-system-one-decision-providers.test.ts +33 -0
  194. package/src/tools/request-human-input.ts +7 -2
  195. package/src/tools/script-connections/tool.ts +35 -3
  196. package/src/tools/send-task.ts +9 -1
  197. package/src/utils/enabled-tools.ts +27 -0
  198. package/src/utils/internal-ai/complete-structured.ts +11 -3
  199. package/src/utils/openrouter-base-url.ts +23 -0
  200. package/src/workflows/executors/raw-llm.ts +1 -0
  201. package/src/workflows/executors/system-one-decision.ts +2 -0
  202. package/src/workflows/executors/validate.ts +1 -0
  203. package/src/workflows/executors/workflow-llm.ts +9 -2
  204. package/templates/extensions/README.md +1 -0
  205. package/templates/extensions/slack-reply-gate/README.md +98 -0
  206. package/templates/extensions/slack-reply-gate/hooks.ts +582 -0
  207. package/templates/extensions/slack-reply-gate/manifest.json +8 -0
  208. package/templates/skills/agent-browser/SKILL.md +2 -0
  209. package/templates/skills/agent-browser/content.md +2 -0
  210. package/templates/skills/code-quality/content.md +1 -0
  211. package/templates/skills/heartbeat-runbook/content.md +1 -1
  212. package/templates/skills/swarm-scripts/SKILL.md +10 -0
  213. package/templates/skills/swarm-scripts/content.md +10 -0
  214. package/templates/skills/work-on-task/SKILL.md +4 -0
  215. package/templates/skills/work-on-task/content.md +4 -0
  216. package/templates/skills/workflow-iterate/content.md +9 -0
  217. package/dist/acp-adapter-yzfxn8vj.js +0 -23
  218. package/dist/cli-3dpj7atn.js +0 -11
  219. package/dist/cli-gvcgmx69.js +0 -183
  220. package/dist/{cli-jhm6hzjn.js → cli-mxvnnv5q.js} +7 -7
  221. package/dist/{codex-hook-d2rkvf39.js → codex-hook-15dgc4cb.js} +1 -1
@@ -25,6 +25,7 @@ import {
25
25
  buildGrokConfigToml,
26
26
  checkGrokCredentials,
27
27
  GrokAdapter,
28
+ grokOpenRouterModel,
28
29
  installedClaudePluginNames,
29
30
  } from "../providers/grok-adapter";
30
31
  import type { ProviderEvent, ProviderSessionConfig } from "../providers/types";
@@ -487,6 +488,7 @@ describe("GrokAdapter", () => {
487
488
  'base_url = "https://openrouter.ai/api/v1"',
488
489
  'env_key = "OPENROUTER_API_KEY"',
489
490
  'api_backend = "chat_completions"',
491
+ 'extra_headers = { "HTTP-Referer" = "https://agent-swarm.dev", "X-OpenRouter-Title" = "Agent Swarm", "X-OpenRouter-Categories" = "personal-agent,cloud-agent" }',
490
492
  ].join("\n"),
491
493
  );
492
494
  } finally {
@@ -495,6 +497,17 @@ describe("GrokAdapter", () => {
495
497
  }
496
498
  });
497
499
 
500
+ test("OpenRouter app attribution rides only on direct openrouter.ai models", () => {
501
+ const toml = (env: Record<string, string>) =>
502
+ buildGrokConfigToml([], grokOpenRouterModel("openrouter/acme/thing", env));
503
+ expect(toml({})).toContain('extra_headers = { "HTTP-Referer" = "https://agent-swarm.dev"');
504
+ expect(toml({ OPENROUTER_BASE_URL: "https://gateway.example/v1" })).not.toContain(
505
+ "extra_headers",
506
+ );
507
+ expect(toml({ OPENROUTER_APP_ATTRIBUTION: "false" })).not.toContain("extra_headers");
508
+ expect(buildGrokConfigToml([], null)).not.toContain("extra_headers");
509
+ });
510
+
498
511
  test("an xai/-qualified model runs as the bare xAI id", async () => {
499
512
  const dir = makeTempDir();
500
513
  const grok = writeFakeGrok(dir, "ok");
@@ -0,0 +1,38 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { loadModelsDevCache } from "../be/modelsdev-cache";
3
+ import { buildModelsDevSeedRows } from "../be/seed-pricing";
4
+
5
+ function ratesFor(provider: string, model: string) {
6
+ const cache = loadModelsDevCache();
7
+ expect(cache).not.toBeNull();
8
+ return Object.fromEntries(
9
+ buildModelsDevSeedRows(cache!)
10
+ .filter((row) => row.provider === provider && row.model === model)
11
+ .map((row) => [row.tokenClass, row.pricePerMillionUsd]),
12
+ );
13
+ }
14
+
15
+ describe("Haiku 5.5 pricing seed", () => {
16
+ test("seeds Anthropic rates for the claude harnesses, including the 1h cache write", () => {
17
+ for (const provider of ["claude", "claude-managed"]) {
18
+ expect(ratesFor(provider, "claude-haiku-5-5")).toEqual({
19
+ input: 0.1,
20
+ output: 0.5,
21
+ cached_input: 0.01,
22
+ cache_write: 0.125,
23
+ cache_write_1h: 0.2,
24
+ });
25
+ }
26
+ });
27
+
28
+ test("seeds the same rates without a 1h class for amp and cursor", () => {
29
+ for (const provider of ["amp", "cursor"]) {
30
+ expect(ratesFor(provider, "claude-haiku-5-5")).toEqual({
31
+ input: 0.1,
32
+ output: 0.5,
33
+ cached_input: 0.01,
34
+ cache_write: 0.125,
35
+ });
36
+ }
37
+ });
38
+ });
@@ -1,4 +1,13 @@
1
- import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
1
+ import {
2
+ afterAll,
3
+ afterEach,
4
+ beforeAll,
5
+ beforeEach,
6
+ describe,
7
+ expect,
8
+ spyOn,
9
+ test,
10
+ } from "bun:test";
2
11
  import { unlink } from "node:fs/promises";
3
12
  import {
4
13
  closeDb,
@@ -13,6 +22,7 @@ import {
13
22
  checkHeartbeatChecklist,
14
23
  createBootTriageTask,
15
24
  gatherSystemStatus,
25
+ getBootTriageFindings,
16
26
  isEffectivelyEmpty,
17
27
  runRebootSweep,
18
28
  } from "../heartbeat/heartbeat";
@@ -343,6 +353,14 @@ describe("Heartbeat Checklist", () => {
343
353
  // ==========================================================================
344
354
 
345
355
  describe("createBootTriageTask", () => {
356
+ // These cases cover the task body; the clean-boot skip has its own block.
357
+ beforeEach(() => {
358
+ process.env.HEARTBEAT_BOOT_TRIAGE_ALWAYS = "true";
359
+ });
360
+ afterEach(() => {
361
+ delete process.env.HEARTBEAT_BOOT_TRIAGE_ALWAYS;
362
+ });
363
+
346
364
  test("skips when no lead agent registered", async () => {
347
365
  await createAgent({ name: "worker", isLead: false, status: "idle" });
348
366
 
@@ -505,6 +523,145 @@ describe("Heartbeat Checklist", () => {
505
523
  });
506
524
  });
507
525
 
526
+ // ==========================================================================
527
+ // createBootTriageTask() — clean-boot skip
528
+ // ==========================================================================
529
+
530
+ describe("createBootTriageTask clean-boot skip", () => {
531
+ const bootTriageCount = async () =>
532
+ (
533
+ await getDbClient().get<{ n: number }>(
534
+ "SELECT COUNT(*) AS n FROM agent_tasks WHERE taskType = 'boot-triage'",
535
+ )
536
+ )?.n ?? 0;
537
+
538
+ beforeEach(async () => {
539
+ delete process.env.HEARTBEAT_BOOT_TRIAGE_ALWAYS;
540
+ // Reset the module-level reboot findings left by earlier tests.
541
+ await runRebootSweep();
542
+ });
543
+
544
+ afterEach(() => {
545
+ delete process.env.HEARTBEAT_BOOT_TRIAGE_ALWAYS;
546
+ });
547
+
548
+ test("clean boot creates no Lead task", async () => {
549
+ await createAgent({ name: "lead", isLead: true, status: "idle" });
550
+ await createAgent({ name: "worker", isLead: false, status: "idle" });
551
+
552
+ expect(await getBootTriageFindings()).toEqual({
553
+ rebootInterrupted: 0,
554
+ stalled: 0,
555
+ orphaned: 0,
556
+ supersededWithoutResume: 0,
557
+ });
558
+ await createBootTriageTask();
559
+
560
+ expect(await bootTriageCount()).toBe(0);
561
+ });
562
+
563
+ test("a seeded orphan creates exactly one Lead task", async () => {
564
+ await createAgent({ name: "lead", isLead: true, status: "idle" });
565
+ const offline = await createAgent({ name: "gone", isLead: false, status: "offline" });
566
+ await createTaskExtended("Orphaned work", { agentId: offline.id });
567
+
568
+ expect((await getBootTriageFindings()).orphaned).toBe(1);
569
+ await createBootTriageTask();
570
+ await createBootTriageTask();
571
+
572
+ expect(await bootTriageCount()).toBe(1);
573
+ });
574
+
575
+ test("reboot-interrupted work creates the Lead task", async () => {
576
+ await createAgent({ name: "lead", isLead: true, status: "idle" });
577
+ const agent = await createAgent({ name: "dead-worker", isLead: false, status: "busy" });
578
+ const task = await createTaskExtended("Interrupted work", { agentId: agent.id });
579
+ await startTask(task.id);
580
+ await getDbClient().run("UPDATE agent_tasks SET lastUpdatedAt = ? WHERE id = ?", [
581
+ new Date(Date.now() - 1000).toISOString(),
582
+ task.id,
583
+ ]);
584
+ await runRebootSweep();
585
+
586
+ expect((await getBootTriageFindings()).rebootInterrupted).toBe(1);
587
+ await createBootTriageTask();
588
+
589
+ expect(await bootTriageCount()).toBe(1);
590
+ });
591
+
592
+ test("superseded task without a resume creates the Lead task", async () => {
593
+ await createAgent({ name: "lead", isLead: true, status: "idle" });
594
+ const worker = await createAgent({ name: "worker", isLead: false, status: "idle" });
595
+ const task = await createTaskExtended("Lost resume", { agentId: worker.id });
596
+ await getDbClient().run(
597
+ "UPDATE agent_tasks SET status = 'superseded', finishedAt = ? WHERE id = ?",
598
+ [new Date(Date.now() - 5 * 60 * 1000).toISOString(), task.id],
599
+ );
600
+
601
+ expect((await getBootTriageFindings()).supersededWithoutResume).toBe(1);
602
+ await createBootTriageTask();
603
+
604
+ expect(await bootTriageCount()).toBe(1);
605
+ });
606
+
607
+ test("stalled-only work creates the Lead task", async () => {
608
+ await createAgent({ name: "lead", isLead: true, status: "idle" });
609
+ const agent = await createAgent({ name: "stuck-worker", isLead: false, status: "busy" });
610
+ const task = await createTaskExtended("Stalled work", { agentId: agent.id });
611
+ await startTask(task.id);
612
+ await getDbClient().run("UPDATE agent_tasks SET lastUpdatedAt = ? WHERE id = ?", [
613
+ new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
614
+ task.id,
615
+ ]);
616
+
617
+ expect(await getBootTriageFindings()).toEqual({
618
+ rebootInterrupted: 0,
619
+ stalled: 1,
620
+ orphaned: 0,
621
+ supersededWithoutResume: 0,
622
+ });
623
+ await createBootTriageTask();
624
+
625
+ expect(await bootTriageCount()).toBe(1);
626
+ });
627
+
628
+ test("a persistent reader error still creates exactly one Lead task", async () => {
629
+ await createAgent({ name: "lead", isLead: true, status: "idle" });
630
+ const client = getDbClient();
631
+ const query = client.query.bind(client);
632
+ const failStalled = spyOn(client, "query").mockImplementation((sql, params) => {
633
+ if (sql.includes("status = 'in_progress' AND lastUpdatedAt < ?")) {
634
+ throw new Error("stalled read unavailable");
635
+ }
636
+ return query(sql, params);
637
+ });
638
+ const errors = spyOn(console, "error").mockImplementation(() => {});
639
+ try {
640
+ await createBootTriageTask();
641
+ await createBootTriageTask();
642
+ } finally {
643
+ failStalled.mockRestore();
644
+ errors.mockRestore();
645
+ }
646
+
647
+ expect(await bootTriageCount()).toBe(1);
648
+ const task = await getDbClient().get<{ task: string }>(
649
+ "SELECT task FROM agent_tasks WHERE taskType = 'boot-triage'",
650
+ );
651
+ expect(task?.task).toContain("System status unavailable");
652
+ expect(task?.task).toContain("stalled read unavailable");
653
+ });
654
+
655
+ test("HEARTBEAT_BOOT_TRIAGE_ALWAYS creates the task on a clean boot", async () => {
656
+ await createAgent({ name: "lead", isLead: true, status: "idle" });
657
+ process.env.HEARTBEAT_BOOT_TRIAGE_ALWAYS = "true";
658
+
659
+ await createBootTriageTask();
660
+
661
+ expect(await bootTriageCount()).toBe(1);
662
+ });
663
+ });
664
+
508
665
  // ==========================================================================
509
666
  // gatherSystemStatus() — boot triage sections
510
667
  // ==========================================================================
@@ -580,6 +580,50 @@ describe("onboarding memory probe", () => {
580
580
  });
581
581
  });
582
582
 
583
+ test("only an openrouter.ai endpoint gets OpenRouter app attribution", async () => {
584
+ const referers: Record<string, string | null> = {};
585
+ const fake = Bun.serve({
586
+ hostname: "127.0.0.1",
587
+ port: 0,
588
+ fetch(req) {
589
+ referers.local = req.headers.get("http-referer");
590
+ return embeddingResponse(512);
591
+ },
592
+ });
593
+ const origFetch = globalThis.fetch;
594
+ // The probe builds a fresh OpenAI client per call, which picks up this stub.
595
+ globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
596
+ const req = new Request(input, init);
597
+ if (new URL(req.url).hostname !== "openrouter.ai") return origFetch(input, init);
598
+ referers[process.env.OPENROUTER_APP_ATTRIBUTION ? "optedOut" : "openrouter"] =
599
+ req.headers.get("http-referer");
600
+ return embeddingResponse(512);
601
+ }) as typeof fetch;
602
+ const probe = (body: Record<string, unknown>) =>
603
+ request("/api/onboarding/memory", {
604
+ method: "POST",
605
+ body: { model: "test-embedding-model", apiKey: "example-key", ...body },
606
+ });
607
+ try {
608
+ expect((await probe({ preset: "openrouter" })).status).toBe(200);
609
+ process.env.OPENROUTER_APP_ATTRIBUTION = "false";
610
+ expect((await probe({ preset: "openrouter" })).status).toBe(200);
611
+ delete process.env.OPENROUTER_APP_ATTRIBUTION;
612
+ expect(
613
+ (await probe({ preset: "custom", baseUrl: `http://127.0.0.1:${fake.port}/v1` })).status,
614
+ ).toBe(200);
615
+ } finally {
616
+ globalThis.fetch = origFetch;
617
+ delete process.env.OPENROUTER_APP_ATTRIBUTION;
618
+ fake.stop(true);
619
+ }
620
+ expect(referers).toEqual({
621
+ openrouter: "https://agent-swarm.dev",
622
+ optedOut: null,
623
+ local: null,
624
+ });
625
+ });
626
+
583
627
  test("classifies a local 401 embeddings response as auth", async () => {
584
628
  const fake = startEmbeddingServer(() =>
585
629
  Response.json(
@@ -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 config.provider unset", async () => {
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 { config?: { provider?: unknown } };
1305
- expect(opts.config?.provider).toBeUndefined();
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
+ });