@plurnk/plurnk-providers 1.18.0 → 1.19.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 (67) hide show
  1. package/.env.defaults +8 -4
  2. package/README.md +4 -16
  3. package/SPEC.md +41 -11
  4. package/dist/AiSdkProvider.d.ts +2 -4
  5. package/dist/AiSdkProvider.d.ts.map +1 -1
  6. package/dist/AiSdkProvider.js +7 -17
  7. package/dist/AiSdkProvider.js.map +1 -1
  8. package/dist/AiSdkRequestBody.d.ts +1 -4
  9. package/dist/AiSdkRequestBody.d.ts.map +1 -1
  10. package/dist/AiSdkRequestBody.js +10 -48
  11. package/dist/AiSdkRequestBody.js.map +1 -1
  12. package/dist/Mock.d.ts +2 -2
  13. package/dist/Mock.d.ts.map +1 -1
  14. package/dist/Mock.js +3 -3
  15. package/dist/Mock.js.map +1 -1
  16. package/dist/ProviderRegistry.js +2 -2
  17. package/dist/ProviderRegistry.js.map +1 -1
  18. package/dist/accounting.d.ts +0 -1
  19. package/dist/accounting.d.ts.map +1 -1
  20. package/dist/accounting.js +1 -7
  21. package/dist/accounting.js.map +1 -1
  22. package/dist/aiSdkTransport.d.ts.map +1 -1
  23. package/dist/aiSdkTransport.js +79 -7
  24. package/dist/aiSdkTransport.js.map +1 -1
  25. package/dist/catalogProvider.d.ts.map +1 -1
  26. package/dist/catalogProvider.js +13 -3
  27. package/dist/catalogProvider.js.map +1 -1
  28. package/dist/compatibleProvider.d.ts +1 -1
  29. package/dist/compatibleProvider.d.ts.map +1 -1
  30. package/dist/compatibleProvider.js +13 -25
  31. package/dist/compatibleProvider.js.map +1 -1
  32. package/dist/env.d.ts.map +1 -1
  33. package/dist/env.js +1 -0
  34. package/dist/env.js.map +1 -1
  35. package/dist/index.d.ts +1 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +1 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/types.d.ts +0 -6
  40. package/dist/types.d.ts.map +1 -1
  41. package/package.json +7 -7
  42. package/src/AiSdkProvider.test.ts +97 -126
  43. package/src/AiSdkProvider.ts +8 -22
  44. package/src/AiSdkRequestBody.ts +10 -42
  45. package/src/Mock.test.ts +11 -3
  46. package/src/Mock.ts +3 -3
  47. package/src/ProviderRegistry.test.ts +0 -1
  48. package/src/ProviderRegistry.ts +1 -1
  49. package/src/accounting.test.ts +0 -11
  50. package/src/accounting.ts +0 -8
  51. package/src/aiSdkTransport.test.ts +63 -1
  52. package/src/aiSdkTransport.ts +82 -7
  53. package/src/boundaries.test.ts +1 -1
  54. package/src/capacity.test.ts +2 -2
  55. package/src/catalogProvider.test.ts +46 -1
  56. package/src/catalogProvider.ts +12 -3
  57. package/src/compatibleProvider.test.ts +6 -6
  58. package/src/compatibleProvider.ts +13 -28
  59. package/src/cost.test.ts +3 -3
  60. package/src/discover.test.ts +11 -2
  61. package/src/env.test.ts +3 -0
  62. package/src/env.ts +2 -1
  63. package/src/errors.test.ts +2 -2
  64. package/src/index.ts +0 -1
  65. package/src/inputModalities.test.ts +2 -1
  66. package/src/sdkModels.test.ts +1 -1
  67. package/src/types.ts +8 -35
@@ -272,7 +272,7 @@ test("(#482) conformance judges the grant: tolerated overflow is accepted, past-
272
272
  );
273
273
  });
274
274
 
275
- test("request-observer open failures preserve the durability cause and issue no provider I/O", async () => {
275
+ test("{§provider-request-observer} request-observer open failures preserve the durability cause and issue no provider I/O", async () => {
276
276
  const root = new Error("durable request open failed");
277
277
  let calls = 0;
278
278
  const provider = testProvider({
@@ -299,7 +299,7 @@ test("request-observer open failures preserve the durability cause and issue no
299
299
  assert.equal(calls, 0);
300
300
  });
301
301
 
302
- test("request-observer settlement failures preserve the durability cause without retrying I/O", async () => {
302
+ test("{§provider-request-observer} request-observer settlement failures preserve the durability cause without retrying I/O", async () => {
303
303
  const root = new Error("durable request settlement failed");
304
304
  let calls = 0;
305
305
  const provider = testProvider({
@@ -533,7 +533,7 @@ test("only the trailing eos_token is stripped; a quoted one mid-body survives",
533
533
  assert.equal(res.assistant.content, "quotes <eos> in the body"); // only the tail goes
534
534
  });
535
535
 
536
- test("identity getters and default prompt estimate", async () => {
536
+ test("{§provider-prompt-measurement} identity getters and default prompt estimate", async () => {
537
537
  const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 1000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
538
538
  assert.equal(p.model, "m");
539
539
  assert.equal(p.contextWindow, null); // default
@@ -578,7 +578,7 @@ test("injected prompt measurement preserves provenance and request cost estimati
578
578
  });
579
579
  });
580
580
 
581
- test("an exact request overflow rejects before observer or provider I/O", async () => {
581
+ test("{§provider-capacity-failure} an exact request overflow rejects before observer or provider I/O", async () => {
582
582
  const calls = installFetch([{ choices: [{ delta: { content: "unreachable" } }] }]);
583
583
  let observed = false;
584
584
  const p = testProvider({
@@ -672,7 +672,7 @@ test("an unsupported fixed reasoning policy fails before provider I/O", () => {
672
672
  );
673
673
  });
674
674
 
675
- test("native SDK warnings survive as source-attributed provider Notices", async () => {
675
+ test("{§provider-sdk-warning} native SDK warnings survive as source-attributed provider Notices", async () => {
676
676
  const usage = {
677
677
  inputTokens: { total: 1, noCache: 1, cacheRead: 0, cacheWrite: 0 },
678
678
  outputTokens: { total: 1, text: 1, reasoning: 0 },
@@ -1251,7 +1251,7 @@ test("{§provider-tagged-reasoning} verbatim, non-leading, and structured-reason
1251
1251
  });
1252
1252
 
1253
1253
  test("{§provider-tagged-reasoning} grammar evidence retains the exact pre-projection tagged sentence", async () => {
1254
- const content = "<think>🧠reason</think>```SEND\ndone\n```\n```TASK\n[{\"content\":\"Task completed.\",\"status\":\"completed\"}]\n```";
1254
+ const content = "<think>🧠reason</think>````SEND\ndone\n````";
1255
1255
  const config = {
1256
1256
  ...injectedBase,
1257
1257
  contextWindow: 640,
@@ -1269,7 +1269,7 @@ test("{§provider-tagged-reasoning} grammar evidence retains the exact pre-proje
1269
1269
  });
1270
1270
 
1271
1271
  assert.equal(response.assistant.reasoning, "🧠reason");
1272
- assert.equal(response.assistant.content, "```SEND\ndone\n```\n```TASK\n[{\"content\":\"Task completed.\",\"status\":\"completed\"}]\n```");
1272
+ assert.equal(response.assistant.content, "````SEND\ndone\n````");
1273
1273
  assert.deepEqual(response.grammarEvidence, {
1274
1274
  input: content,
1275
1275
  contentStart: [..."<think>🧠reason</think>"].length,
@@ -1857,81 +1857,6 @@ test("meta: passes backend fields through without reinterpreting monetary values
1857
1857
  assert.equal(res.meta?.system_fingerprint, "fp_abc");
1858
1858
  });
1859
1859
 
1860
- // — first-party telemetry headers ({§provider-request-authority}) —
1861
-
1862
- const headerVal = (init: RequestInit, name: string): string | undefined =>
1863
- new Headers(init.headers).get(name) ?? undefined;
1864
-
1865
- test("firstPartyMetadata: attributions + client ride as Plurnk-* headers", async () => {
1866
- const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, firstPartyMetadata: true });
1867
- const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1868
- await p.generate({ workerId: "r", messages: [], attributions: ["@acme/x@1.2.0", "@foo/y@0.3.1"], client: "plurnk.nvim/1.4.0" });
1869
- assert.equal(headerVal(calls[0].init, "Plurnk-Attribution"), '["@acme/x@1.2.0","@foo/y@0.3.1"]');
1870
- assert.equal(headerVal(calls[0].init, "Plurnk-Client"), "plurnk.nvim/1.4.0");
1871
- });
1872
-
1873
- test("Plurnk-Call-Kind carries the caller's emission or bare output contract under the first-party gate", async () => {
1874
- const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, firstPartyMetadata: true });
1875
- const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1876
- await p.generate({ workerId: "emission", messages: [], callKind: "emission" });
1877
- await p.generate({ workerId: "bare", messages: [], callKind: "bare" });
1878
- assert.equal(headerVal(calls[0].init, "Plurnk-Call-Kind"), "emission");
1879
- assert.equal(headerVal(calls[1].init, "Plurnk-Call-Kind"), "bare");
1880
- });
1881
-
1882
- test("generate rejects an unknown call kind before provider I/O", async () => {
1883
- const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, firstPartyMetadata: true });
1884
- const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1885
- await assert.rejects(
1886
- p.generate({ workerId: "invalid", messages: [], callKind: "unknown" as never }),
1887
- /unsupported callKind "unknown"/,
1888
- );
1889
- assert.equal(calls.length, 0);
1890
- });
1891
-
1892
- test("Plurnk-Worker-Primary: the lineage root rides under the gate; emitted even when it equals workerId", async () => {
1893
- const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, firstPartyMetadata: true });
1894
- let calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1895
- await p.generate({ workerId: "w-child", primaryWorkerId: "w-root", messages: [] });
1896
- assert.equal(headerVal(calls[0].init, "Plurnk-Worker-Primary"), "w-root"); // a descendant: Primary != Worker-Id
1897
- mock.restoreAll();
1898
-
1899
- // the primary worker's own turn: Primary == Worker-Id, still stamped (never skipped on equality)
1900
- calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1901
- await p.generate({ workerId: "w-root", primaryWorkerId: "w-root", messages: [] });
1902
- assert.equal(headerVal(calls[0].init, "Plurnk-Worker-Primary"), "w-root");
1903
- mock.restoreAll();
1904
-
1905
- // absent when the consumer supplies none — the provider never invents a primary
1906
- calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1907
- await p.generate({ workerId: "w-root", messages: [] });
1908
- assert.equal(headerVal(calls[0].init, "Plurnk-Worker-Primary"), undefined);
1909
- });
1910
-
1911
- test("Plurnk-Worker-Primary is structurally dropped when firstPartyMetadata is off", async () => {
1912
- const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
1913
- const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1914
- await p.generate({ workerId: "w-child", primaryWorkerId: "w-root", messages: [] });
1915
- assert.equal(headerVal(calls[0].init, "Plurnk-Worker-Primary"), undefined); // never reaches a third-party backend
1916
- });
1917
-
1918
- test("firstPartyMetadata off (default): the headers are structurally dropped even when values are passed", async () => {
1919
- const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
1920
- const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1921
- await p.generate({ workerId: "r", messages: [], attributions: ["@acme/x@1.2.0"], client: "plurnk-cli/2.0.0", callKind: "bare" });
1922
- assert.equal(headerVal(calls[0].init, "Plurnk-Attribution"), undefined); // never leaks to a non-first-party backend
1923
- assert.equal(headerVal(calls[0].init, "Plurnk-Client"), undefined);
1924
- assert.equal(headerVal(calls[0].init, "Plurnk-Call-Kind"), undefined);
1925
- });
1926
-
1927
- test("firstPartyMetadata on but empty values: no header emitted", async () => {
1928
- const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, firstPartyMetadata: true });
1929
- const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1930
- await p.generate({ workerId: "r", messages: [], attributions: [], client: "" });
1931
- assert.equal(headerVal(calls[0].init, "Plurnk-Attribution"), undefined);
1932
- assert.equal(headerVal(calls[0].init, "Plurnk-Client"), undefined);
1933
- });
1934
-
1935
1860
  test("grammar transport: no grammar passed sends no grammar field, but the penalty rides", async () => {
1936
1861
  const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, grammarStyle: "llamacpp" });
1937
1862
  const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
@@ -2048,7 +1973,7 @@ test("streaming:false: a non-ok response rejects as a classified ProviderError (
2048
1973
  });
2049
1974
  });
2050
1975
 
2051
- test("generate fail-hards on a missing or empty workerId", async () => {
1976
+ test("{§provider-cache-identity} generate fail-hards on a missing or empty workerId", async () => {
2052
1977
  const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
2053
1978
  installFetch([{ choices: [{ delta: { content: "x" } }] }]);
2054
1979
  await assert.rejects(() => p.generate({ workerId: "", messages: [] }), /workerId is required/);
@@ -2096,6 +2021,58 @@ test("{§provider-input-modalities} compatible transports serialize image files
2096
2021
  }]);
2097
2022
  });
2098
2023
 
2024
+ for (const [mediaType, format] of [["audio/wav", "wav"], ["audio/mpeg", "mp3"]]) {
2025
+ test(`{§provider-input-modalities} ${mediaType} reaches the compatible wire as native input_audio`, async () => {
2026
+ const provider = testProvider({
2027
+ model: "audio-model", url: "https://example.test/v1/chat/completions", fetchTimeoutMs: 5000,
2028
+ temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0,
2029
+ });
2030
+ const calls = installFetch([{ choices: [{ delta: { content: "heard" } }] }]);
2031
+ const bytes = new Uint8Array([82, 73, 70, 70]);
2032
+ await provider.generate({ workerId: "audio", messages: [{ role: "user", content: [
2033
+ { type: "text", text: "listen" }, { type: "file", data: bytes, mediaType: mediaType! },
2034
+ ] }] });
2035
+ assert.deepEqual(JSON.parse(String(calls[0]?.init.body)).messages, [{ role: "user", content: [
2036
+ { type: "text", text: "listen" },
2037
+ { type: "input_audio", input_audio: { data: Buffer.from(bytes).toString("base64"), format } },
2038
+ ] }]);
2039
+ });
2040
+ }
2041
+
2042
+ test("{§provider-input-modalities} Google serializes native audio through its own SDK adapter", async () => {
2043
+ const { createGoogleGenerativeAI } = await import("@ai-sdk/google");
2044
+ const google = createGoogleGenerativeAI({ apiKey: "fixture", baseURL: "https://example.test/v1beta" });
2045
+ const calls = installFetchJson({ candidates: [{ content: { role: "model", parts: [{ text: "heard" }] }, finishReason: "STOP" }] });
2046
+ const provider = testProvider({
2047
+ model: "audio-fixture", languageModel: google("audio-fixture"), fetchTimeoutMs: 5000,
2048
+ temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0,
2049
+ streaming: false,
2050
+ });
2051
+ const bytes = new Uint8Array([82, 73, 70, 70]);
2052
+ await provider.generate({ workerId: "audio", messages: [{ role: "user", content: [
2053
+ { type: "text", text: "listen" }, { type: "file", data: bytes, mediaType: "audio/wav" },
2054
+ ] }] });
2055
+ assert.deepEqual(JSON.parse(String(calls[0]?.init.body)).contents, [{ role: "user", parts: [
2056
+ { text: "listen" }, { inlineData: { mimeType: "audio/wav", data: Buffer.from(bytes).toString("base64") } },
2057
+ ] }]);
2058
+ });
2059
+
2060
+ test("{§provider-input-modalities} an unsupported audio codec fails visibly instead of dropping its part", async () => {
2061
+ const provider = testProvider({
2062
+ model: "audio-model", url: "https://example.test/v1/chat/completions", fetchTimeoutMs: 5000,
2063
+ temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0,
2064
+ });
2065
+ const calls = installFetch([]);
2066
+ await assert.rejects(provider.generate({ workerId: "audio", messages: [{ role: "user", content: [
2067
+ { type: "file", data: new Uint8Array([79, 103, 103, 83]), mediaType: "audio/ogg" },
2068
+ ] }] }), (error: unknown) => {
2069
+ assert.ok(error instanceof ProviderError);
2070
+ assert.match(error.message, /audio\/ogg/u);
2071
+ return true;
2072
+ });
2073
+ assert.equal(calls.length, 0, "no text-only substitute is sent to the provider");
2074
+ });
2075
+
2099
2076
  test("generate wraps an HTTP failure as a ProviderError carrying Problem Details", async () => {
2100
2077
  const { ProviderError } = await import("./errors.ts");
2101
2078
  const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, source: "provider:test" });
@@ -2572,40 +2549,6 @@ test("caller sampling cannot forge logprobs (reserved keys): the env flag is the
2572
2549
  assert.equal("top_logprobs" in body, false);
2573
2550
  mock.restoreAll();
2574
2551
  });
2575
-
2576
- // — turn coordinate headers ({§lifecycle-terms}): same gate as every first-party signal —
2577
-
2578
- test("workspaceId/loop/turn ride as Plurnk-Workspace-Id/Loop/Turn under the first-party gate", async () => {
2579
- const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, firstPartyMetadata: true });
2580
- const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
2581
- await p.generate({ workerId: "r", messages: [], workspaceId: "s-9", loop: 3, turn: 41 });
2582
- const headers = new Headers(calls[0].init.headers);
2583
- assert.equal(headers.get("plurnk-workspace-id"), "s-9");
2584
- assert.equal(headers.get("plurnk-loop"), "3");
2585
- assert.equal(headers.get("plurnk-turn"), "41");
2586
- });
2587
-
2588
- test("third-party providers structurally DROP the coordinate (gate off by default)", async () => {
2589
- const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
2590
- const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
2591
- await p.generate({ workerId: "r", messages: [], workspaceId: "s-9", loop: 3, turn: 41 });
2592
- const headers = new Headers(calls[0].init.headers);
2593
- assert.equal(headers.has("plurnk-workspace-id"), false);
2594
- assert.equal(headers.has("plurnk-loop"), false);
2595
- assert.equal(headers.has("plurnk-turn"), false);
2596
- });
2597
-
2598
- test("coordinates are 1-based — 0/absent/empty emit no header", async () => {
2599
- const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, firstPartyMetadata: true });
2600
- const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
2601
- await p.generate({ workerId: "r", messages: [], workspaceId: "", loop: 0, turn: 0 });
2602
- const headers = new Headers(calls[0].init.headers);
2603
- assert.equal(headers.has("plurnk-workspace-id"), false);
2604
- assert.equal(headers.has("plurnk-loop"), false);
2605
- assert.equal(headers.has("plurnk-turn"), false);
2606
- assert.equal(headers.has("plurnk-strikes"), false);
2607
- });
2608
-
2609
2552
  // -- {§provider-generation-envelope} --
2610
2553
 
2611
2554
  test("the adapter exposes independent model limits and the resolved generation envelope", () => {
@@ -2626,15 +2569,6 @@ test("the adapter exposes independent model limits and the resolved generation e
2626
2569
  assert.equal(unknown.outputBudget, null);
2627
2570
  });
2628
2571
 
2629
- test("router-owned tuning: tuningFloors:false drops the temperature/penalty floors, caller sampling still rides", async () => {
2630
- const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, frequencyPenalty: 0.4, reasoning: { mode: "off", budget: null }, retryAttempts: 0, tuningFloors: false });
2631
- const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
2632
- await p.generate({ workerId: "r", messages: [], sampling: { temperature: 0.9 } });
2633
- const body = JSON.parse(calls[0].init.body as string);
2634
- assert.equal(body.temperature, 0.9); // caller intent passes verbatim
2635
- assert.equal("frequency_penalty" in body, false); // the floor is suppressed; the router owns tuning
2636
- });
2637
-
2638
2572
  // -- {§provider-cache-affinity} / {§provider-cache-write-policy} --
2639
2573
 
2640
2574
  test("a compatible route's declared body affinity is managed by workerId", async () => {
@@ -2988,3 +2922,40 @@ test("a response whose usage counters contradict each other is delivered with un
2988
2922
  const raw = response.assistantRaw as { usageRefusal?: { reason: string; usage: unknown } };
2989
2923
  assert.deepEqual(raw.usageRefusal, { reason: "provider usage.outputTokenDetails.textTokens must be a non-negative safe integer", usage }, "the durable response carries the refused counters");
2990
2924
  });
2925
+
2926
+ test("{§provider-connectivity} a stream still producing content outlives the attempt deadline; stream-idle governs it instead", async () => {
2927
+ const encoder = new TextEncoder();
2928
+ const chunk = (content: string, finish: string | null = null) => `data: ${JSON.stringify({
2929
+ id: "long", object: "chat.completion.chunk", created: 1, model: "m",
2930
+ choices: [{ index: 0, delta: { content }, finish_reason: finish }],
2931
+ })}\n\n`;
2932
+ mock.method(globalThis, "fetch", async () => new Response(new ReadableStream({
2933
+ async start(controller) {
2934
+ for (let index = 0; index < 8; index += 1) {
2935
+ controller.enqueue(encoder.encode(chunk(`part${index} `)));
2936
+ await new Promise((resolve) => setTimeout(resolve, 25));
2937
+ }
2938
+ controller.enqueue(encoder.encode(chunk("", "stop")));
2939
+ controller.enqueue(encoder.encode("data: [DONE]\n\n"));
2940
+ controller.close();
2941
+ },
2942
+ }), { headers: { "content-type": "text/event-stream" } }));
2943
+ try {
2944
+ const p = testProvider({
2945
+ model: "m",
2946
+ url: "http://x/v1/chat/completions",
2947
+ fetchTimeoutMs: 60,
2948
+ streamIdleTimeoutMs: 1_000,
2949
+ temperature: 0.2,
2950
+ repeatPenalty: 1.15,
2951
+ reasoning: { mode: "off", budget: null },
2952
+ retryAttempts: 1,
2953
+ source: "provider:test",
2954
+ operationTimeoutMs: 5_000,
2955
+ });
2956
+ const response = await p.generate({ workerId: "long", messages: [] });
2957
+ assert.equal(response.assistant.content, "part0 part1 part2 part3 part4 part5 part6 part7 ", "about 200 ms of streaming against a 60 ms attempt deadline completes");
2958
+ } finally {
2959
+ mock.restoreAll();
2960
+ }
2961
+ });
@@ -54,7 +54,7 @@ const raceAgainstDeadline = async <T>(work: PromiseLike<T>, signal: AbortSignal)
54
54
 
55
55
  // Backend wire spellings for the resolved reasoning intent. The switch beside each
56
56
  // mapping retains any backend-specific omission/explicit-disable constraint.
57
- export type ReasoningStyle = "none" | "think" | "include_reasoning" | "effort" | "effort_explicit" | "effort_required" | "thinking_effort" | "template" | "anthropic";
57
+ export type ReasoningStyle = "none" | "think" | "include_reasoning" | "effort" | "effort_explicit" | "effort_required" | "thinking_effort" | "thinking_config" | "template" | "anthropic";
58
58
 
59
59
  export type NativeReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh";
60
60
  export type CompatibleReasoningEffort = NativeReasoningEffort | "max";
@@ -126,7 +126,6 @@ export type AiSdkProviderConfig = {
126
126
  // a fixed deployment choice and therefore wins on every request.
127
127
  serviceTier?: string;
128
128
  streaming?: boolean; // SSE transport (default true); false → one non-streamed JSON
129
- firstPartyMetadata?: boolean; // forward per-turn attributions + client as Plurnk-* headers (plurnk only); default false
130
129
  apiKeyRejectedMessage?: string; // friendly hint when a present key is 401/403-rejected (distinct from unset); default undefined
131
130
  eosText?: string; // server-reported eos_token, stripped from the content tail (--special renders it as text); default undefined
132
131
  // Slot affinity wiring is provider-internal, never consumer-facing.
@@ -199,10 +198,6 @@ export type AiSdkProviderConfig = {
199
198
  // the standard factory always supplies them. Resolved against contextWindow
200
199
  // at read time (getters), so a probe that lands after config assembly still
201
200
  // derives correctly.
202
- // The plurnk.ai router owns tuning — false suppresses the
203
- // client-side temperature/penalty FLOORS on this provider (caller `sampling`
204
- // still passes through verbatim). Default true (floors ride).
205
- tuningFloors?: boolean;
206
201
  };
207
202
 
208
203
  class ProviderRequestObserverError extends Error {
@@ -296,13 +291,11 @@ export default class AiSdkProvider implements Provider {
296
291
  #reasoningResponseProviderOptions: AiSdkProviderOptions | undefined;
297
292
  #serviceTier: string | undefined;
298
293
  #streaming: boolean;
299
- #firstPartyMetadata: boolean;
300
294
  #supportsSlotPinning: boolean;
301
295
  #slotCount: number | null;
302
296
  #retryAttempts: number;
303
297
  #errorDetailLimit: number | undefined;
304
298
  #topLogprobs: number | null;
305
- #tuningFloors: boolean;
306
299
  #rawBody: boolean;
307
300
  #servedModel: string | undefined;
308
301
  #requiresOutputBudget: boolean | undefined;
@@ -418,14 +411,12 @@ export default class AiSdkProvider implements Provider {
418
411
  }
419
412
  this.#serviceTier = config.serviceTier;
420
413
  this.#streaming = config.streaming ?? true;
421
- this.#firstPartyMetadata = config.firstPartyMetadata ?? false;
422
414
  this.#apiKeyRejectedMessage = config.apiKeyRejectedMessage;
423
415
  this.#eosText = config.eosText;
424
416
  this.#hasApiKey = "Authorization" in this.#headers;
425
417
  this.#supportsSlotPinning = config.supportsSlotPinning ?? false;
426
418
  this.#slotCount = config.slotCount ?? null;
427
419
  this.#topLogprobs = config.topLogprobs ?? null;
428
- this.#tuningFloors = config.tuningFloors ?? true;
429
420
  this.#rawBody = config.rawBody ?? false;
430
421
  this.#servedModel = config.servedModel;
431
422
  this.#requiresOutputBudget = config.requiresOutputBudget;
@@ -477,7 +468,7 @@ export default class AiSdkProvider implements Provider {
477
468
  return tokens;
478
469
  };
479
470
  }
480
- this.#requestBody = new AiSdkRequestBody({ reasoningBudget: this.#reasoningBudget, additiveReasoningProvider: this.#additiveReasoningProvider, reasoning: this.#reasoning, reasoningToggle: this.#reasoningToggle, compatibleAdaptiveReasoning: this.#compatibleAdaptiveReasoning, compatibleOffReasoning: this.#compatibleOffReasoning, adaptiveReasoningProviderOptions: this.#adaptiveReasoningProviderOptions, repeatPenalty: this.#repeatPenalty, frequencyPenalty: this.#frequencyPenalty, dryMultiplier: this.#dryMultiplier, dryBase: this.#dryBase, dryAllowedLength: this.#dryAllowedLength, repeatLastN: this.#repeatLastN, reasoningStyle: this.#reasoningStyle, source: this.#source, grammarStyle: this.#grammarStyle, cacheAffinity: this.#cacheAffinity, reasoningResponseProviderOptions: this.#reasoningResponseProviderOptions, firstPartyMetadata: this.#firstPartyMetadata, supportsSlotPinning: this.#supportsSlotPinning, slotCount: this.#slotCount });
471
+ this.#requestBody = new AiSdkRequestBody({ reasoningBudget: this.#reasoningBudget, additiveReasoningProvider: this.#additiveReasoningProvider, reasoning: this.#reasoning, reasoningToggle: this.#reasoningToggle, compatibleAdaptiveReasoning: this.#compatibleAdaptiveReasoning, compatibleOffReasoning: this.#compatibleOffReasoning, adaptiveReasoningProviderOptions: this.#adaptiveReasoningProviderOptions, repeatPenalty: this.#repeatPenalty, frequencyPenalty: this.#frequencyPenalty, dryMultiplier: this.#dryMultiplier, dryBase: this.#dryBase, dryAllowedLength: this.#dryAllowedLength, repeatLastN: this.#repeatLastN, reasoningStyle: this.#reasoningStyle, source: this.#source, grammarStyle: this.#grammarStyle, cacheAffinity: this.#cacheAffinity, reasoningResponseProviderOptions: this.#reasoningResponseProviderOptions, supportsSlotPinning: this.#supportsSlotPinning, slotCount: this.#slotCount });
481
472
  }
482
473
 
483
474
  get contextWindow(): number | null { return this.#contextWindow; }
@@ -613,7 +604,7 @@ export default class AiSdkProvider implements Provider {
613
604
  });
614
605
  }
615
606
 
616
- async generate({ messages, workerId, primaryWorkerId, signal, grammar, maxOutputTokens, attributions, client, strikes, workspaceId, loop, turn, sampling, observeRequest, observeReasoning, callKind }: ProviderGenerateArgs): Promise<ProviderResponse> {
607
+ async generate({ messages, workerId, signal, grammar, maxOutputTokens, sampling, observeRequest, observeReasoning, callKind }: ProviderGenerateArgs): Promise<ProviderResponse> {
617
608
  // {§provider-interface} The worker identity is required.
618
609
  if (workerId === undefined || workerId.length === 0) throw new Error("generate: workerId is required — the worker's stable, opaque identity");
619
610
  if (callKind !== undefined && callKind !== "emission" && callKind !== "bare") {
@@ -655,9 +646,8 @@ export default class AiSdkProvider implements Provider {
655
646
  // paths and the name promises every request) < the caller's `sampling`
656
647
  // < the managed fields, which always win.
657
648
  const body: Record<string, unknown> = {
658
- // Floors are suppressed on router-owned-tuning providers (plurnk) —
659
- // the router's per-model tuning must not be overridden by client floors.
660
- ...(this.#tuningFloors ? { ...(this.#temperature !== null ? { temperature: this.#temperature } : {}), ...this.#requestBody.repetitionPenaltyBody() } : {}),
649
+ ...(this.#temperature !== null ? { temperature: this.#temperature } : {}),
650
+ ...this.#requestBody.repetitionPenaltyBody(),
661
651
  ...this.#requestBody.samplingBody(sampling),
662
652
  ...(this.#serviceTier !== undefined ? { service_tier: this.#serviceTier } : {}),
663
653
  ...this.#requestBody.reasoningBody(preserveGrammarSentence, capacity.reasoningBudget),
@@ -672,13 +662,11 @@ export default class AiSdkProvider implements Provider {
672
662
  : {}),
673
663
  };
674
664
 
675
- // Per-request headers = static auth/routing + any first-party telemetry.
676
- const metaHeaders = this.#requestBody.metadataHeaders(attributions, client, strikes, workerId, primaryWorkerId, workspaceId, loop, turn, callKind);
665
+ // Per-request headers = static auth/routing only.
677
666
  const headers = new Headers(this.#headers);
678
667
  if (this.#cacheAffinity?.target === "header") {
679
668
  headers.set(this.#cacheAffinity.name, workerId);
680
669
  }
681
- for (const [name, value] of Object.entries(metaHeaders)) headers.set(name, value);
682
670
  const requestHeaders = Object.fromEntries(headers.entries());
683
671
  const accounting: ProviderRequestAccounting[] = [];
684
672
  const operationTimeout = this.#operationTimeoutMs > 0
@@ -803,15 +791,13 @@ export default class AiSdkProvider implements Provider {
803
791
  streaming: this.#streaming,
804
792
  captureRawBody: this.#rawBody,
805
793
  ...observers,
806
- temperature: this.#tuningFloors
807
- ? (typeof sampling?.temperature === "number" ? sampling.temperature : this.#temperature ?? undefined)
808
- : typeof sampling?.temperature === "number" ? sampling.temperature : undefined,
794
+ temperature: typeof sampling?.temperature === "number" ? sampling.temperature : this.#temperature ?? undefined,
809
795
  topP: typeof sampling?.top_p === "number" ? sampling.top_p : undefined,
810
796
  topK: typeof sampling?.top_k === "number" ? sampling.top_k : undefined,
811
797
  presencePenalty: typeof sampling?.presence_penalty === "number" ? sampling.presence_penalty : undefined,
812
798
  frequencyPenalty: typeof sampling?.frequency_penalty === "number"
813
799
  ? sampling.frequency_penalty
814
- : this.#tuningFloors && this.#frequencyPenalty > 0 ? this.#frequencyPenalty : undefined,
800
+ : this.#frequencyPenalty > 0 ? this.#frequencyPenalty : undefined,
815
801
  stopSequences: typeof sampling?.stop === "string"
816
802
  ? [sampling.stop]
817
803
  : Array.isArray(sampling?.stop) && sampling.stop.every((value) => typeof value === "string")
@@ -1,5 +1,5 @@
1
1
  // The provider-specific request body and headers one generate call sends: reasoning, grammar, sampling, repetition, slots, metadata. Split out of AiSdkProvider; every knob it reads is injected.
2
- import type { ProviderCallKind, ReasoningPolicy } from "./types.ts";
2
+ import type { ReasoningPolicy } from "./types.ts";
3
3
  import type { JSONValue } from "ai";
4
4
  import { type Reasoning } from "./env.ts";
5
5
  import { fixedEffort } from "./reasoning-effort.ts";
@@ -80,13 +80,12 @@ export default class AiSdkRequestBody {
80
80
  readonly #grammarStyle: GrammarStyle;
81
81
  readonly #cacheAffinity: CacheAffinity | undefined;
82
82
  readonly #reasoningResponseProviderOptions: AiSdkProviderOptions | undefined;
83
- readonly #firstPartyMetadata: boolean;
84
83
  readonly #supportsSlotPinning: boolean;
85
84
  readonly #slotCount: number | null;
86
85
  #runSlots = new Map<string, number>();
87
86
  #nextSlot = 0;
88
87
 
89
- constructor({ reasoningBudget, additiveReasoningProvider, reasoning, reasoningToggle, compatibleAdaptiveReasoning, compatibleOffReasoning, adaptiveReasoningProviderOptions, repeatPenalty, frequencyPenalty, dryMultiplier, dryBase, dryAllowedLength, repeatLastN, reasoningStyle, source, grammarStyle, cacheAffinity, reasoningResponseProviderOptions, firstPartyMetadata, supportsSlotPinning, slotCount }: {
88
+ constructor({ reasoningBudget, additiveReasoningProvider, reasoning, reasoningToggle, compatibleAdaptiveReasoning, compatibleOffReasoning, adaptiveReasoningProviderOptions, repeatPenalty, frequencyPenalty, dryMultiplier, dryBase, dryAllowedLength, repeatLastN, reasoningStyle, source, grammarStyle, cacheAffinity, reasoningResponseProviderOptions, supportsSlotPinning, slotCount }: {
90
89
  reasoningBudget: number | null;
91
90
  additiveReasoningProvider: "anthropic" | "bedrock" | undefined;
92
91
  reasoning: Reasoning;
@@ -105,7 +104,6 @@ export default class AiSdkRequestBody {
105
104
  grammarStyle: GrammarStyle;
106
105
  cacheAffinity: CacheAffinity | undefined;
107
106
  reasoningResponseProviderOptions: AiSdkProviderOptions | undefined;
108
- firstPartyMetadata: boolean;
109
107
  supportsSlotPinning: boolean;
110
108
  slotCount: number | null;
111
109
  }) {
@@ -127,7 +125,6 @@ export default class AiSdkRequestBody {
127
125
  this.#grammarStyle = grammarStyle;
128
126
  this.#cacheAffinity = cacheAffinity;
129
127
  this.#reasoningResponseProviderOptions = reasoningResponseProviderOptions;
130
- this.#firstPartyMetadata = firstPartyMetadata;
131
128
  this.#supportsSlotPinning = supportsSlotPinning;
132
129
  this.#slotCount = slotCount;
133
130
  }
@@ -204,6 +201,14 @@ export default class AiSdkRequestBody {
204
201
  thinking: { type: "enabled" },
205
202
  reasoning_effort: fixedEffort(mode),
206
203
  };
204
+ // {§google-reasoning-request} — Gemini's OpenAI-compatible extension. Readable
205
+ // thoughts ride on every reasoning request; Gemini refuses `reasoning_effort` beside a
206
+ // thinking_config, so the level travels inside it. Gemini cannot turn reasoning off.
207
+ case "thinking_config": {
208
+ if (mode === "off") throw new TypeError(`${this.#source}: thinking_config reasoning has no off projection`);
209
+ const level = mode === "adaptive" ? {} : { thinking_level: fixedEffort(mode) };
210
+ return { extra_body: { google: { thinking_config: { include_thoughts: true, ...level } } } };
211
+ }
207
212
  // Anthropic-compatible native dynamic or manual budget mode.
208
213
  case "anthropic": return mode === "off"
209
214
  ? { thinking: { type: "disabled" } }
@@ -294,43 +299,6 @@ export default class AiSdkRequestBody {
294
299
  }
295
300
 
296
301
 
297
- // First-party telemetry headers ({§provider-request-authority} {§provider-call-kind}): forwarded only when the spec
298
- // opted in (the plurnk endpoint). The gate is here, not at the call site, so
299
- // attributions/client/strikes can never reach a third-party backend even if
300
- // the consumer passes them to the wrong provider. Empty values emit no header
301
- // — EXCEPT strikes, where 0 is a real value (clean streak) distinct from
302
- // absent (consumer didn't report); contract {§strikes-first-party-metadata}. Strikes
303
- // ride HTTP headers only — the packet never carries them (the model must
304
- // never see strike state; engine accounting is not a metric to game).
305
- metadataHeaders(attributions: string[] | undefined, client: string | undefined, strikes: number | undefined, workerId: string, primaryWorkerId: string | undefined, workspaceId: string | undefined, loop: number | undefined, turn: number | undefined, callKind: ProviderCallKind | undefined): Record<string, string> {
306
- if (!this.#firstPartyMetadata) return {};
307
- const h: Record<string, string> = {};
308
- if (attributions !== undefined && attributions.length > 0) h["Plurnk-Attribution"] = JSON.stringify(attributions);
309
- if (client !== undefined && client.length > 0) h["Plurnk-Client"] = client;
310
- if (strikes !== undefined && Number.isInteger(strikes) && strikes >= 0) h["Plurnk-Strikes"] = String(strikes);
311
- // Worker identity: the opaque workerId
312
- // the consumer already supplies, forwarded so the endpoint can key
313
- // per-worker affinity/telemetry — same gate as every first-party signal.
314
- h["Plurnk-Worker-Id"] = workerId;
315
- // Root worker of the lineage ({§worker-primary}): the no-parent ancestor of this turn's
316
- // worker tree. The consumer classifies primary-vs-spawned by equality
317
- // (primaryWorkerId == workerId ⇒ the primary/root worker). The provider
318
- // EMITS what the consumer supplies and never invents a primary; the
319
- // consumer's contract is to stamp it EVERY turn (including the primary's
320
- // own, where it equals workerId). Absence is the consumer's violation for
321
- // the endpoint to surface, not a provider default.
322
- if (primaryWorkerId !== undefined && primaryWorkerId.length > 0) h["Plurnk-Worker-Primary"] = primaryWorkerId;
323
- // Turn coordinate ({§lifecycle-terms}): workspace/loop/turn, the
324
- // daemon-side sequence the endpoint can never scrape from the wire.
325
- // Coordinates are 1-based — 0 is not a real value, so no strikes-style
326
- // zero exception; absent/empty/0 emits no header.
327
- if (workspaceId !== undefined && workspaceId.length > 0) h["Plurnk-Workspace-Id"] = workspaceId;
328
- if (loop !== undefined && Number.isInteger(loop) && loop >= 1) h["Plurnk-Loop"] = String(loop);
329
- if (turn !== undefined && Number.isInteger(turn) && turn >= 1) h["Plurnk-Turn"] = String(turn);
330
- if (callKind !== undefined) h["Plurnk-Call-Kind"] = callKind;
331
- return h;
332
- }
333
-
334
302
 
335
303
  requestProviderOptions(
336
304
  workerId: string,
package/src/Mock.test.ts CHANGED
@@ -8,10 +8,18 @@ import { ProviderError } from "./errors.ts";
8
8
  const build = (responses: MockResponse[] = [{ assistant: { content: "hi", reasoning: null } }]) =>
9
9
  new Mock({ contextWindow: 100000, responses });
10
10
 
11
+ // The suite runs on this package's own panel, which ships an output budget. "No budget" is therefore
12
+ // something a test states — the panel's explicit empty value — never an ambient absence of the key.
13
+ const withoutOutputBudget = <T>(body: () => T): T => {
14
+ const panel = process.env.PLURNK_PROVIDERS_OUTPUT_BUDGET;
15
+ process.env.PLURNK_PROVIDERS_OUTPUT_BUDGET = "";
16
+ try { return body(); } finally { process.env.PLURNK_PROVIDERS_OUTPUT_BUDGET = panel; }
17
+ };
18
+
11
19
  // — Identity ({§provider-interface}) —
12
20
 
13
21
  test("Mock: contextWindow and model are stable across reads", () => {
14
- const m = build();
22
+ const m = withoutOutputBudget(() => build());
15
23
  assert.equal(m.contextWindow, 100000);
16
24
  assert.equal(m.inputCapacity, null);
17
25
  assert.equal(m.contextWindow, 100000);
@@ -193,8 +201,8 @@ test("Mock resolves percentage and absolute generation budgets against its windo
193
201
  }
194
202
  });
195
203
 
196
- test("no generation-budget env leaves a bare Mock unbounded", () => {
197
- const m = new Mock({ contextWindow: 49152, responses: [] });
204
+ test("an empty generation budget leaves a bare Mock unbounded", () => {
205
+ const m = withoutOutputBudget(() => new Mock({ contextWindow: 49152, responses: [] }));
198
206
  assert.equal(m.outputBudget, null);
199
207
  assert.equal(m.reasoningBudget, null);
200
208
  assert.equal(m.inputCapacity, null);
package/src/Mock.ts CHANGED
@@ -41,7 +41,7 @@ export type MockResponse = {
41
41
  export type MockReturnedAssistant = ProviderAssistant & { ops?: unknown[] };
42
42
  export type MockReturnedResponse = ProviderResponse & { assistant: MockReturnedAssistant };
43
43
 
44
- const DEFAULT_USAGE: ProviderUsage = {
44
+ const MOCK_USAGE: ProviderUsage = {
45
45
  inputTokens: 0,
46
46
  outputTokens: 0,
47
47
  totalTokens: 0,
@@ -154,7 +154,7 @@ export default class Mock implements Provider {
154
154
  }
155
155
  const a = next.assistant;
156
156
  const usage: ProviderUsage = {
157
- ...DEFAULT_USAGE,
157
+ ...MOCK_USAGE,
158
158
  ...next.usage,
159
159
  };
160
160
  const requestAccounting: ProviderRequestAccounting = validateProviderRequestAccounting({
@@ -196,4 +196,4 @@ export default class Mock implements Provider {
196
196
  get remaining(): number { return this.#queue.length; }
197
197
  }
198
198
 
199
- export { DEFAULT_USAGE as mockDefaultUsage };
199
+ export { MOCK_USAGE as mockDefaultUsage };
@@ -73,7 +73,6 @@ test("instantiateProvider: a selected plugin composes its static and runtime att
73
73
  const context: PluginAttributionContext = {
74
74
  workspaceId: "workspace",
75
75
  workerId: "worker",
76
- primaryWorkerId: "primary",
77
76
  loop: 3,
78
77
  turn: 2,
79
78
  attempt: 1,
@@ -60,7 +60,7 @@ export const instantiateProvider = async (
60
60
  const catalog = catalogProviderFromEnv(name, env, model, baseUrl);
61
61
  if (catalog !== null) return catalog;
62
62
  if (name === "ollama") return ollamaProviderFromEnv(env, model, baseUrl === undefined ? undefined : { baseUrl });
63
- if (name === "openai" || name === "plurnk") return compatibleProviderFromEnv(name, env, model, baseUrl);
63
+ if (name === "openai") return compatibleProviderFromEnv(env, model, baseUrl);
64
64
  const { registry, skipped, packageAttributions = new Map(), grammarStyles = new Map() } = await providerPackages(discoverFn, env);
65
65
  const specifier = registry.get(name);
66
66
  if (specifier === undefined) {
@@ -2,7 +2,6 @@ import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import {
4
4
  aggregateProviderAccounting,
5
- plurnkCostNormalizer,
6
5
  providerCostNormalizer,
7
6
  } from "./accounting.ts";
8
7
 
@@ -48,16 +47,6 @@ test("DeepInfra's documented response estimate remains estimated", () => {
48
47
  });
49
48
  });
50
49
 
51
- test("first-party charged evidence is validated at its adapter boundary", () => {
52
- const charged = {
53
- kind: "charged",
54
- amount: { amount: "0.01", currency: "USD" },
55
- source: "plurnk endpoint",
56
- } as const;
57
- assert.deepEqual(plurnkCostNormalizer(evidence({ charge: charged })), charged);
58
- assert.equal(plurnkCostNormalizer(evidence({})), undefined);
59
- });
60
-
61
50
  test("response cost normalization is an explicit adapter capability", () => {
62
51
  assert.equal(providerCostNormalizer("@ai-sdk/anthropic"), undefined);
63
52
  assert.equal(providerCostNormalizer("@ai-sdk/xai")!(evidence({ usage: {} })), undefined);