@plurnk/plurnk-providers 1.5.0 → 1.6.1

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 (105) hide show
  1. package/.env.defaults +41 -34
  2. package/README.md +15 -0
  3. package/SPEC.md +242 -89
  4. package/dist/AiSdkProvider.d.ts +33 -33
  5. package/dist/AiSdkProvider.d.ts.map +1 -1
  6. package/dist/AiSdkProvider.js +442 -133
  7. package/dist/AiSdkProvider.js.map +1 -1
  8. package/dist/Mock.d.ts +10 -11
  9. package/dist/Mock.d.ts.map +1 -1
  10. package/dist/Mock.js +87 -25
  11. package/dist/Mock.js.map +1 -1
  12. package/dist/Pool.d.ts +9 -24
  13. package/dist/Pool.d.ts.map +1 -1
  14. package/dist/Pool.js +86 -25
  15. package/dist/Pool.js.map +1 -1
  16. package/dist/accounting.d.ts +5 -2
  17. package/dist/accounting.d.ts.map +1 -1
  18. package/dist/accounting.js +100 -16
  19. package/dist/accounting.js.map +1 -1
  20. package/dist/accountingPublic.d.ts +5 -0
  21. package/dist/accountingPublic.d.ts.map +1 -0
  22. package/dist/accountingPublic.js +3 -0
  23. package/dist/accountingPublic.js.map +1 -0
  24. package/dist/aiSdkTransport.d.ts +9 -2
  25. package/dist/aiSdkTransport.d.ts.map +1 -1
  26. package/dist/aiSdkTransport.js +160 -62
  27. package/dist/aiSdkTransport.js.map +1 -1
  28. package/dist/capacity.d.ts +26 -0
  29. package/dist/capacity.d.ts.map +1 -0
  30. package/dist/capacity.js +90 -0
  31. package/dist/capacity.js.map +1 -0
  32. package/dist/catalogProvider.d.ts +8 -3
  33. package/dist/catalogProvider.d.ts.map +1 -1
  34. package/dist/catalogProvider.js +45 -41
  35. package/dist/catalogProvider.js.map +1 -1
  36. package/dist/compatibleProvider.d.ts.map +1 -1
  37. package/dist/compatibleProvider.js +26 -12
  38. package/dist/compatibleProvider.js.map +1 -1
  39. package/dist/cost.d.ts +10 -10
  40. package/dist/cost.d.ts.map +1 -1
  41. package/dist/cost.js +90 -42
  42. package/dist/cost.js.map +1 -1
  43. package/dist/env.d.ts +13 -11
  44. package/dist/env.d.ts.map +1 -1
  45. package/dist/env.js +83 -46
  46. package/dist/env.js.map +1 -1
  47. package/dist/errors.d.ts +17 -3
  48. package/dist/errors.d.ts.map +1 -1
  49. package/dist/errors.js +91 -8
  50. package/dist/errors.js.map +1 -1
  51. package/dist/index.d.ts +7 -6
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +5 -3
  54. package/dist/index.js.map +1 -1
  55. package/dist/ollama.js +3 -3
  56. package/dist/ollama.js.map +1 -1
  57. package/dist/promptTokens.d.ts.map +1 -1
  58. package/dist/promptTokens.js +7 -4
  59. package/dist/promptTokens.js.map +1 -1
  60. package/dist/sdkModels.d.ts +7 -2
  61. package/dist/sdkModels.d.ts.map +1 -1
  62. package/dist/sdkModels.js +43 -13
  63. package/dist/sdkModels.js.map +1 -1
  64. package/dist/types.d.ts +55 -33
  65. package/dist/types.d.ts.map +1 -1
  66. package/dist/usage.d.ts +22 -5
  67. package/dist/usage.d.ts.map +1 -1
  68. package/dist/usage.js +169 -83
  69. package/dist/usage.js.map +1 -1
  70. package/package.json +18 -7
  71. package/src/AiSdkProvider.test.ts +964 -206
  72. package/src/AiSdkProvider.ts +545 -155
  73. package/src/Mock.test.ts +69 -30
  74. package/src/Mock.ts +99 -29
  75. package/src/Pool.test.ts +90 -19
  76. package/src/Pool.ts +96 -27
  77. package/src/ProviderRegistry.test.ts +16 -11
  78. package/src/accounting.test.ts +58 -22
  79. package/src/accounting.ts +119 -18
  80. package/src/accountingPublic.ts +9 -0
  81. package/src/aiSdkTransport.test.ts +42 -49
  82. package/src/aiSdkTransport.ts +174 -62
  83. package/src/boundaries.test.ts +2 -0
  84. package/src/capacity.test.ts +92 -0
  85. package/src/capacity.ts +140 -0
  86. package/src/catalogProvider.test.ts +339 -30
  87. package/src/catalogProvider.ts +65 -47
  88. package/src/compatibleProvider.test.ts +7 -5
  89. package/src/compatibleProvider.ts +29 -13
  90. package/src/cost.test.ts +86 -36
  91. package/src/cost.ts +111 -50
  92. package/src/defaults.test.ts +13 -3
  93. package/src/env.test.ts +103 -25
  94. package/src/env.ts +153 -65
  95. package/src/errors.test.ts +80 -2
  96. package/src/errors.ts +107 -8
  97. package/src/index.ts +26 -7
  98. package/src/ollama.test.ts +5 -3
  99. package/src/ollama.ts +3 -3
  100. package/src/promptTokens.ts +8 -5
  101. package/src/sdkModels.test.ts +77 -8
  102. package/src/sdkModels.ts +51 -15
  103. package/src/types.ts +112 -51
  104. package/src/usage.test.ts +112 -116
  105. package/src/usage.ts +214 -93
@@ -1,10 +1,26 @@
1
1
  import test, { mock } from "node:test";
2
2
  import { strict as assert } from "node:assert";
3
- import AiSdkProvider, { effortFromBudget } from "./AiSdkProvider.ts";
3
+ import AiSdkProvider, { effortFromBudget, type AiSdkProviderConfig } from "./AiSdkProvider.ts";
4
4
  import { ProviderError } from "./errors.ts";
5
- import { authoritativeChargeNormalizer } from "./accounting.ts";
5
+ import { providerCostNormalizer } from "./accounting.ts";
6
6
  import type { LanguageModel } from "ai";
7
7
 
8
+ type TestProviderConfig = Omit<AiSdkProviderConfig, "operationTimeoutMs" | "firstContentTimeoutMs">
9
+ & Partial<Pick<AiSdkProviderConfig, "operationTimeoutMs" | "firstContentTimeoutMs">>;
10
+
11
+ const testProvider = (config: TestProviderConfig): AiSdkProvider => {
12
+ const {
13
+ operationTimeoutMs = config.fetchTimeoutMs,
14
+ firstContentTimeoutMs = 0,
15
+ ...rest
16
+ } = config;
17
+ return new AiSdkProvider({
18
+ ...rest,
19
+ operationTimeoutMs,
20
+ firstContentTimeoutMs,
21
+ });
22
+ };
23
+
8
24
  // Build a fake fetch returning a one-chunk SSE stream, capturing the request
9
25
  // so tests can assert what the spine sent on the wire.
10
26
  const sseStream = (chunks: unknown[]) => {
@@ -61,6 +77,31 @@ const installFetchJson = (payload: unknown) => {
61
77
  return calls;
62
78
  };
63
79
 
80
+ const settledCharge = {
81
+ kind: "charged",
82
+ amount: { amount: "0.00000042", currency: "XMR" },
83
+ usdEquivalent: "0.000071",
84
+ source: "plurnk endpoint settlement",
85
+ } as const;
86
+
87
+ const billedErrorBody = {
88
+ status: 422,
89
+ error: {
90
+ message: "non-conforming emission rejected",
91
+ type: "grammar_invalid",
92
+ },
93
+ usage: {
94
+ prompt_tokens: 8,
95
+ completion_tokens: 3,
96
+ reasoning_tokens: 0,
97
+ prompt_tokens_details: { cached_tokens: 2 },
98
+ total_tokens: 11,
99
+ },
100
+ charge: settledCharge,
101
+ };
102
+
103
+ const directCost = ({ charge }: { charge?: unknown }) => charge as typeof settledCharge | undefined;
104
+
64
105
  const jsonChoice = { model: "m", choices: [{ message: { content: "x" }, finish_reason: "stop" }], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } };
65
106
 
66
107
  const injectedBase = {
@@ -90,9 +131,9 @@ test("per-instance fetch owns streaming and buffered requests", async () => {
90
131
  }), { status: 200, headers: { "Content-Type": "application/json" } });
91
132
  };
92
133
 
93
- const streamed = await new AiSdkProvider({ ...injectedBase, fetch: streamingFetch, rawBody: true })
134
+ const streamed = await testProvider({ ...injectedBase, fetch: streamingFetch, rawBody: true })
94
135
  .generate({ workerId: "stream", messages: [{ role: "user", content: "hello" }] });
95
- const buffered = await new AiSdkProvider({ ...injectedBase, fetch: bufferedFetch, streaming: false })
136
+ const buffered = await testProvider({ ...injectedBase, fetch: bufferedFetch, streaming: false })
96
137
  .generate({ workerId: "buffer", messages: [{ role: "user", content: "hello" }] });
97
138
 
98
139
  assert.equal(streamed.assistant.content, "streamed");
@@ -116,12 +157,17 @@ test("caller cancellation and provider timeout reach an injected fetch", async (
116
157
  });
117
158
  };
118
159
  const caller = new AbortController();
119
- const callerProvider = new AiSdkProvider({ ...injectedBase, fetch: pendingFetch });
160
+ const callerProvider = testProvider({ ...injectedBase, fetch: pendingFetch });
120
161
  const callerRequest = callerProvider.generate({ workerId: "cancel", messages: [], signal: caller.signal });
121
162
  caller.abort(new Error("operator cancelled"));
122
163
  await assert.rejects(callerRequest, /operator cancelled/);
123
164
 
124
- const timeoutProvider = new AiSdkProvider({ ...injectedBase, fetch: pendingFetch, fetchTimeoutMs: 1 });
165
+ const timeoutProvider = testProvider({
166
+ ...injectedBase,
167
+ fetch: pendingFetch,
168
+ fetchTimeoutMs: 1,
169
+ operationTimeoutMs: 100,
170
+ });
125
171
  await assert.rejects(
126
172
  timeoutProvider.generate({ workerId: "timeout", messages: [] }),
127
173
  (error: ProviderError) => error.kind === "network_failure",
@@ -143,7 +189,7 @@ test("per-instance fetch owns tokenization and retry attempts", async () => {
143
189
  { choices: [], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } },
144
190
  ]), { status: 200 });
145
191
  };
146
- const provider = new AiSdkProvider({
192
+ const provider = testProvider({
147
193
  ...injectedBase,
148
194
  fetch: providerFetch,
149
195
  retryAttempts: 1,
@@ -159,6 +205,60 @@ test("per-instance fetch owns tokenization and retry attempts", async () => {
159
205
  ]);
160
206
  });
161
207
 
208
+ test("request-observer open failures preserve the durability cause and issue no provider I/O", async () => {
209
+ const root = new Error("durable request open failed");
210
+ let calls = 0;
211
+ const provider = testProvider({
212
+ ...injectedBase,
213
+ retryAttempts: 3,
214
+ fetch: async () => {
215
+ calls++;
216
+ return new Response(JSON.stringify(jsonChoice), {
217
+ status: 200,
218
+ headers: { "Content-Type": "application/json" },
219
+ });
220
+ },
221
+ streaming: false,
222
+ });
223
+
224
+ await assert.rejects(
225
+ provider.generate({
226
+ workerId: "observer-open",
227
+ messages: [],
228
+ observeRequest: async () => { throw root; },
229
+ }),
230
+ (error: unknown) => error === root,
231
+ );
232
+ assert.equal(calls, 0);
233
+ });
234
+
235
+ test("request-observer settlement failures preserve the durability cause without retrying I/O", async () => {
236
+ const root = new Error("durable request settlement failed");
237
+ let calls = 0;
238
+ const provider = testProvider({
239
+ ...injectedBase,
240
+ retryAttempts: 3,
241
+ fetch: async () => {
242
+ calls++;
243
+ return new Response(JSON.stringify(jsonChoice), {
244
+ status: 200,
245
+ headers: { "Content-Type": "application/json" },
246
+ });
247
+ },
248
+ streaming: false,
249
+ });
250
+
251
+ await assert.rejects(
252
+ provider.generate({
253
+ workerId: "observer-settle",
254
+ messages: [],
255
+ observeRequest: async () => async () => { throw root; },
256
+ }),
257
+ (error: unknown) => error === root,
258
+ );
259
+ assert.equal(calls, 1);
260
+ });
261
+
162
262
  // Sequenced fetch mock for retry tests: each entry is one HTTP response. A 200
163
263
  // streams its chunks; any other status returns that error (with an optional
164
264
  // retry-after header). The last entry repeats once the script runs out.
@@ -206,8 +306,13 @@ test("effortFromBudget: maps budget to tiers", () => {
206
306
 
207
307
  test("a 524 Cloudflare edge timeout fails fast - not retried despite retryAttempts", async () => {
208
308
  const calls = installFetchScript([{ status: 524, retryAfter: 120 }]);
209
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 1000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 3 });
210
- await assert.rejects(p.generate({ workerId: "r", messages: [] }));
309
+ 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: 3 });
310
+ await assert.rejects(
311
+ p.generate({ workerId: "r", messages: [] }),
312
+ (error: ProviderError) => error.kind === "network_failure"
313
+ && error.status === 524
314
+ && error.problem.retryable === false,
315
+ );
211
316
  await flush();
212
317
  assert.equal(calls.length, 1); // edge code: one attempt, no retry despite retryAttempts: 3
213
318
  mock.restoreAll();
@@ -216,7 +321,7 @@ test("a 524 Cloudflare edge timeout fails fast - not retried despite retryAttemp
216
321
  test("a 422 grammar_invalid is a failed exchange, not transport replay policy", async () => {
217
322
  const body = JSON.stringify({ error: { message: "non-conforming emission rejected: ...", type: "grammar_invalid" } });
218
323
  const calls = installFetchScript([{ status: 422, body }]);
219
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 1000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 2 });
324
+ 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: 2 });
220
325
  await assert.rejects(
221
326
  p.generate({ workerId: "r", messages: [] }),
222
327
  (e: unknown) => e instanceof ProviderError && e.kind === "grammar_invalid",
@@ -231,7 +336,7 @@ test("an SSE error frame is a failed exchange, not an empty completion", async (
231
336
  status: 422,
232
337
  error: { message: "non-conforming emission rejected", type: "grammar_invalid" },
233
338
  }]);
234
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 1000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
339
+ 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 });
235
340
  await assert.rejects(
236
341
  p.generate({ workerId: "r", messages: [] }),
237
342
  (e: unknown) => e instanceof ProviderError && e.kind === "grammar_invalid",
@@ -239,29 +344,138 @@ test("an SSE error frame is a failed exchange, not an empty completion", async (
239
344
  assert.equal(calls.length, 1);
240
345
  });
241
346
 
347
+ test("a buffered classified error retains normalized usage and settled charge", async () => {
348
+ const calls = installFetchScript([{ status: 422, body: JSON.stringify(billedErrorBody) }]);
349
+ const p = testProvider({
350
+ ...injectedBase,
351
+ streaming: false,
352
+ normalizeCost: directCost,
353
+ });
354
+ await assert.rejects(
355
+ p.generate({ workerId: "billed-json-error", messages: [] }),
356
+ (error: unknown) => {
357
+ assert.ok(error instanceof ProviderError);
358
+ assert.equal(error.kind, "grammar_invalid");
359
+ assert.deepEqual(error.accounting, [{
360
+ provider: "provider",
361
+ model: "m",
362
+ outcome: "error",
363
+ status: 422,
364
+ usage: {
365
+ inputTokens: 8,
366
+ outputTokens: 3,
367
+ totalTokens: 11,
368
+ inputTokenDetails: { cacheReadTokens: 2 },
369
+ outputTokenDetails: { textTokens: 3, reasoningTokens: 0 },
370
+ },
371
+ cost: settledCharge,
372
+ }]);
373
+ assert.equal(error.attempt, undefined, "accounting evidence does not fabricate an assistant response");
374
+ return true;
375
+ },
376
+ );
377
+ assert.equal(calls.length, 1);
378
+ });
379
+
380
+ test("an SSE classified error retains the same normalized usage and settled charge", async () => {
381
+ const calls = installFetch([billedErrorBody]);
382
+ const p = testProvider({
383
+ ...injectedBase,
384
+ normalizeCost: directCost,
385
+ });
386
+ await assert.rejects(
387
+ p.generate({ workerId: "billed-sse-error", messages: [] }),
388
+ (error: unknown) => {
389
+ assert.ok(error instanceof ProviderError);
390
+ assert.equal(error.kind, "grammar_invalid");
391
+ assert.deepEqual(error.accounting, [{
392
+ provider: "provider",
393
+ model: "m",
394
+ outcome: "error",
395
+ status: 422,
396
+ usage: {
397
+ inputTokens: 8,
398
+ outputTokens: 3,
399
+ totalTokens: 11,
400
+ inputTokenDetails: { cacheReadTokens: 2 },
401
+ outputTokenDetails: { textTokens: 3, reasoningTokens: 0 },
402
+ },
403
+ cost: settledCharge,
404
+ }]);
405
+ assert.equal(error.attempt, undefined);
406
+ return true;
407
+ },
408
+ );
409
+ assert.equal(calls.length, 1);
410
+ });
411
+
412
+ test("a successful response normalizes direct charge without duplicating it as metadata", async () => {
413
+ installFetchJson({ ...jsonChoice, charge: settledCharge });
414
+ const p = testProvider({
415
+ ...injectedBase,
416
+ streaming: false,
417
+ normalizeCost: directCost,
418
+ });
419
+ const response = await p.generate({ workerId: "billed-json-success", messages: [] });
420
+ assert.deepEqual(response.accounting[0]?.cost, settledCharge);
421
+ assert.equal(response.meta?.charge, undefined);
422
+ });
423
+
424
+ test("malformed monetary evidence closes the physical request before surfacing the normalization failure", async () => {
425
+ const root = new TypeError("direct charge is malformed");
426
+ const settled: unknown[] = [];
427
+ const calls = installFetchJson({ ...jsonChoice, charge: { malformed: true } });
428
+ const provider = testProvider({
429
+ ...injectedBase,
430
+ retryAttempts: 3,
431
+ streaming: false,
432
+ normalizeCost: () => { throw root; },
433
+ });
434
+
435
+ await assert.rejects(
436
+ provider.generate({
437
+ workerId: "malformed-charge",
438
+ messages: [],
439
+ observeRequest: async () => async (accounting) => { settled.push(accounting); },
440
+ }),
441
+ (error: unknown) => error === root,
442
+ );
443
+ assert.equal(calls.length, 1);
444
+ assert.deepEqual(settled, [{
445
+ provider: "provider",
446
+ model: "m",
447
+ outcome: "response",
448
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
449
+ cost: {
450
+ kind: "unknown",
451
+ reason: "provider request accounting could not be normalized after physical I/O",
452
+ },
453
+ }]);
454
+ });
455
+
242
456
  test("a trailing eos_token (--special EOG leak) is stripped from content", async () => {
243
457
  installFetchJson({ model: "m", choices: [{ message: { content: "the answer<eos>" }, finish_reason: "stop" }], usage: { prompt_tokens: 1, completion_tokens: 3, total_tokens: 4 } });
244
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, streaming: false, eosText: "<eos>" });
458
+ 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, streaming: false, eosText: "<eos>" });
245
459
  const res = await p.generate({ workerId: "r", messages: [] });
246
460
  assert.equal(res.assistant.content, "the answer"); // trailing <eos> gone; packet + verdict see clean bytes
247
461
  });
248
462
 
249
463
  test("without a probed eos_token the content passes through untouched", async () => {
250
464
  installFetchJson({ model: "m", choices: [{ message: { content: "keeps <eos> literally" }, finish_reason: "stop" }], usage: { prompt_tokens: 1, completion_tokens: 3, total_tokens: 4 } });
251
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, streaming: false });
465
+ 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, streaming: false });
252
466
  const res = await p.generate({ workerId: "r", messages: [] });
253
467
  assert.equal(res.assistant.content, "keeps <eos> literally"); // no eosText (a cloud backend) -> no strip
254
468
  });
255
469
 
256
470
  test("only the trailing eos_token is stripped; a quoted one mid-body survives", async () => {
257
471
  installFetchJson({ model: "m", choices: [{ message: { content: "quotes <eos> in the body<eos>" }, finish_reason: "stop" }], usage: { prompt_tokens: 1, completion_tokens: 5, total_tokens: 6 } });
258
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, streaming: false, eosText: "<eos>" });
472
+ 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, streaming: false, eosText: "<eos>" });
259
473
  const res = await p.generate({ workerId: "r", messages: [] });
260
474
  assert.equal(res.assistant.content, "quotes <eos> in the body"); // only the tail goes
261
475
  });
262
476
 
263
477
  test("identity getters and default prompt estimate", async () => {
264
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 1000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
478
+ 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 });
265
479
  assert.equal(p.model, "m");
266
480
  assert.equal(p.contextWindow, null); // default
267
481
  assert.deepEqual(
@@ -274,39 +488,116 @@ test("identity getters and default prompt estimate", async () => {
274
488
  },
275
489
  "chars/2 is explicitly an estimate; high-token-density Unicode prevents an upper-bound claim",
276
490
  );
277
- assert.equal(p.calculateCost({ prompt: 9, completion: 9, reasoning: 0, cached: 0, total: 18 }), 0); // current unknown-rate sentinel
278
491
  });
279
492
 
280
- test("injected prompt measurement preserves provenance and calculateCost is used", async () => {
493
+ test("injected prompt measurement preserves provenance and request cost estimation stays internal", async () => {
281
494
  const seen: string[] = [];
282
- const p = new AiSdkProvider({
495
+ installFetchJson(jsonChoice);
496
+ const p = testProvider({
283
497
  model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 1000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0,
284
498
  countPromptTokens: (messages) => {
285
499
  seen.push(...messages.map(({ content }) => content));
286
500
  return { kind: "upper_bound", tokens: 7, source: "test:proven-bound" };
287
501
  },
288
- calculateCost: (u) => u.total * 2,
502
+ streaming: false,
503
+ estimateCost: (usage) => ({
504
+ kind: "estimated",
505
+ amount: { amount: String((usage?.totalTokens ?? 0) * 2), currency: "USD" },
506
+ source: "test estimator",
507
+ }),
289
508
  });
290
509
  assert.deepEqual(
291
510
  await p.countPromptTokens([{ role: "system", content: "system" }, { role: "user", content: "user" }]),
292
511
  { kind: "upper_bound", tokens: 7, source: "test:proven-bound" },
293
512
  );
294
513
  assert.deepEqual(seen, ["system", "user"]);
295
- assert.equal(p.calculateCost({ prompt: 1, completion: 1, reasoning: 0, cached: 0, total: 5 }), 10);
514
+ const response = await p.generate({ workerId: "accounted", messages: [] });
515
+ assert.deepEqual(response.accounting[0]?.cost, {
516
+ kind: "estimated",
517
+ amount: { amount: "4", currency: "USD" },
518
+ source: "test estimator",
519
+ });
520
+ });
521
+
522
+ test("an exact request overflow rejects before observer or provider I/O", async () => {
523
+ const calls = installFetch([{ choices: [{ delta: { content: "unreachable" } }] }]);
524
+ let observed = false;
525
+ const p = testProvider({
526
+ model: "m",
527
+ url: "http://x/v1/chat/completions",
528
+ contextWindow: 100,
529
+ outputBudget: 40,
530
+ countPromptTokens: () => ({ kind: "exact", tokens: 61, source: "test:exact" }),
531
+ fetchTimeoutMs: 1000,
532
+ temperature: 0.2,
533
+ repeatPenalty: 1.15,
534
+ reasoning: { mode: "off", budget: null },
535
+ retryAttempts: 0,
536
+ });
537
+ await assert.rejects(
538
+ () => p.generate({
539
+ workerId: "capacity",
540
+ messages: [{ role: "user", content: "payload" }],
541
+ observeRequest: async () => {
542
+ observed = true;
543
+ return async () => {};
544
+ },
545
+ }),
546
+ (error: unknown) => {
547
+ assert.ok(error instanceof ProviderError);
548
+ assert.equal(error.kind, "capacity_exceeded");
549
+ assert.equal(error.problem.capacityStage, "preflight");
550
+ assert.equal(error.capacity?.decision, "reject");
551
+ assert.deepEqual(error.accounting, []);
552
+ return true;
553
+ },
554
+ );
555
+ assert.equal(observed, false);
556
+ assert.equal(calls.length, 0);
557
+ });
558
+
559
+ test("an estimate above the known envelope defers to the provider", async () => {
560
+ const calls = installFetch([{ choices: [{ delta: { content: "ok" }, finish_reason: "stop" }] }]);
561
+ const p = testProvider({
562
+ model: "m",
563
+ url: "http://x/v1/chat/completions",
564
+ contextWindow: 100,
565
+ outputBudget: 40,
566
+ countPromptTokens: () => ({
567
+ kind: "estimate",
568
+ tokens: 61,
569
+ source: "test:estimate",
570
+ detail: "fixture has no serving tokenizer",
571
+ }),
572
+ fetchTimeoutMs: 1000,
573
+ temperature: 0.2,
574
+ repeatPenalty: 1.15,
575
+ reasoning: { mode: "off", budget: null },
576
+ retryAttempts: 0,
577
+ });
578
+ const response = await p.generate({ workerId: "capacity", messages: [] });
579
+ assert.equal(response.capacity.decision, "defer");
580
+ assert.equal(calls.length, 1);
581
+ assert.equal(JSON.parse(calls[0].init.body as string).max_tokens, 40);
296
582
  });
297
583
 
298
584
  test("generate maps a streamed response into ProviderResponse", async () => {
299
- const p = new AiSdkProvider({ model: "req-model", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
585
+ const p = testProvider({ model: "req-model", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
300
586
  installFetch([
301
587
  { model: "wire-model", choices: [{ delta: { content: "hel" } }] },
302
588
  { choices: [{ delta: { content: "lo" }, finish_reason: "stop" }] },
303
589
  { usage: { prompt_tokens: 3, completion_tokens: 2, total_tokens: 5, cached_tokens: 1 } },
304
590
  ]);
305
- const { assistant, assistantRaw } = await p.generate({ workerId: "r", messages: [{ role: "user", content: "hi" }] });
591
+ const { assistant, assistantRaw, accounting } = await p.generate({ workerId: "r", messages: [{ role: "user", content: "hi" }] });
306
592
  assert.equal(assistant.content, "hello");
307
593
  assert.equal(assistant.model, "wire-model"); // wire-reported wins
308
594
  assert.equal(assistant.finishReason, "stop");
309
- assert.deepEqual(assistant.usage, { prompt: 3, completion: 2, reasoning: 0, cached: 1, total: 5 });
595
+ assert.deepEqual(accounting[0]?.usage, {
596
+ inputTokens: 3,
597
+ outputTokens: 2,
598
+ totalTokens: 5,
599
+ inputTokenDetails: { cacheReadTokens: 1 },
600
+ });
310
601
  assert.equal(assistant.reasoning, null); // none emitted
311
602
  assert.notEqual(assistantRaw, undefined);
312
603
  });
@@ -318,9 +609,8 @@ test("native SDK accounting metadata becomes a normalized charge in buffered and
318
609
  };
319
610
  const providerMetadata = { openrouter: { usage: { cost: 0.00154935 } } };
320
611
  const charge = {
321
- kind: "authoritative",
612
+ kind: "charged",
322
613
  amount: { amount: "0.00154935", currency: "USD" },
323
- usdEquivalent: "0.00154935",
324
614
  source: "OpenRouter response usage.cost",
325
615
  };
326
616
  const languageModel = {
@@ -364,23 +654,91 @@ test("native SDK accounting metadata becomes a normalized charge in buffered and
364
654
  repeatPenalty: 1.15,
365
655
  reasoning: { mode: "off" as const, budget: null },
366
656
  retryAttempts: 0,
367
- normalizeCharge: authoritativeChargeNormalizer("@openrouter/ai-sdk-provider"),
657
+ normalizeCost: providerCostNormalizer("@openrouter/ai-sdk-provider"),
368
658
  };
369
659
 
370
660
  await t.test("buffered", async () => {
371
- const response = await new AiSdkProvider({ ...config, streaming: false })
661
+ const response = await testProvider({ ...config, streaming: false })
372
662
  .generate({ workerId: "buffered", messages: [] });
373
- assert.deepEqual(response.charge, charge);
663
+ assert.deepEqual(response.accounting[0]?.cost, charge);
374
664
  });
375
665
  await t.test("streamed", async () => {
376
- const response = await new AiSdkProvider(config)
666
+ const response = await testProvider(config)
377
667
  .generate({ workerId: "streamed", messages: [] });
378
- assert.deepEqual(response.charge, charge);
668
+ assert.deepEqual(response.accounting[0]?.cost, charge);
669
+ });
670
+ });
671
+
672
+ test("native SDK providers share the first-content retry contract", async () => {
673
+ let calls = 0;
674
+ const usage = {
675
+ inputTokens: { total: 1, noCache: 1, cacheRead: 0, cacheWrite: 0 },
676
+ outputTokens: { total: 1, text: 1, reasoning: 0 },
677
+ };
678
+ const languageModel = {
679
+ specificationVersion: "v4",
680
+ provider: "native.test",
681
+ modelId: "native-timeout",
682
+ supportedUrls: {},
683
+ doGenerate: async () => { throw new Error("buffered generation is not under test"); },
684
+ doStream: async ({ abortSignal }: { abortSignal?: AbortSignal }) => {
685
+ calls++;
686
+ if (calls > 1) {
687
+ return {
688
+ stream: new ReadableStream({
689
+ start(controller) {
690
+ controller.enqueue({ type: "stream-start", warnings: [] });
691
+ controller.enqueue({ type: "response-metadata", id: "native-retry", modelId: "native-timeout" });
692
+ controller.enqueue({ type: "text-start", id: "text-1" });
693
+ controller.enqueue({ type: "text-delta", id: "text-1", delta: "recovered" });
694
+ controller.enqueue({ type: "text-end", id: "text-1" });
695
+ controller.enqueue({
696
+ type: "finish",
697
+ finishReason: { unified: "stop", raw: "completed" },
698
+ usage,
699
+ });
700
+ controller.close();
701
+ },
702
+ }),
703
+ response: {},
704
+ };
705
+ }
706
+ return {
707
+ stream: new ReadableStream({
708
+ start(controller) {
709
+ controller.enqueue({ type: "stream-start", warnings: [] });
710
+ const timer = setTimeout(() => controller.close(), 100);
711
+ abortSignal?.addEventListener("abort", () => {
712
+ clearTimeout(timer);
713
+ controller.error(abortSignal.reason);
714
+ }, { once: true });
715
+ },
716
+ }),
717
+ response: {},
718
+ };
719
+ },
720
+ } as unknown as LanguageModel;
721
+ const provider = testProvider({
722
+ model: "native-timeout",
723
+ languageModel,
724
+ fetchTimeoutMs: 5_000,
725
+ operationTimeoutMs: 5_000,
726
+ firstContentTimeoutMs: 10,
727
+ temperature: 0.2,
728
+ repeatPenalty: 1.15,
729
+ reasoning: { mode: "off", budget: null },
730
+ retryAttempts: 1,
731
+ source: "provider:test-native",
379
732
  });
733
+
734
+ const result = await provider.generate({ workerId: "native-retry", messages: [] });
735
+ assert.equal(result.assistant.content, "recovered");
736
+ assert.equal(calls, 2);
737
+ assert.deepEqual(result.accounting.map(({ outcome }) => outcome), ["error", "response"]);
380
738
  });
381
739
 
382
740
  test("compatible xAI wire usage becomes an exact tick charge without raw-body capture", async () => {
383
- const p = new AiSdkProvider({
741
+ const p = testProvider({
384
742
  model: "grok-test",
385
743
  url: "http://x/v1/chat/completions",
386
744
  fetchTimeoutMs: 5_000,
@@ -389,7 +747,7 @@ test("compatible xAI wire usage becomes an exact tick charge without raw-body ca
389
747
  reasoning: { mode: "off", budget: null },
390
748
  retryAttempts: 0,
391
749
  streaming: false,
392
- normalizeCharge: authoritativeChargeNormalizer("@ai-sdk/xai"),
750
+ normalizeCost: providerCostNormalizer("@ai-sdk/xai"),
393
751
  });
394
752
  installFetchJson({
395
753
  id: "response-1",
@@ -403,8 +761,8 @@ test("compatible xAI wire usage becomes an exact tick charge without raw-body ca
403
761
  },
404
762
  });
405
763
  const response = await p.generate({ workerId: "xai", messages: [] });
406
- assert.deepEqual(response.charge, {
407
- kind: "authoritative",
764
+ assert.deepEqual(response.accounting[0]?.cost, {
765
+ kind: "charged",
408
766
  amount: { amount: "15493500", currency: "USDTICK" },
409
767
  usdEquivalent: "0.00154935",
410
768
  source: "xAI response usage.cost_in_usd_ticks",
@@ -413,7 +771,7 @@ test("compatible xAI wire usage becomes an exact tick charge without raw-body ca
413
771
  });
414
772
 
415
773
  test("streamed xAI final usage retains its exact tick charge", async () => {
416
- const p = new AiSdkProvider({
774
+ const p = testProvider({
417
775
  model: "grok-test",
418
776
  url: "http://x/v1/chat/completions",
419
777
  fetchTimeoutMs: 5_000,
@@ -421,7 +779,7 @@ test("streamed xAI final usage retains its exact tick charge", async () => {
421
779
  repeatPenalty: 1.15,
422
780
  reasoning: { mode: "off", budget: null },
423
781
  retryAttempts: 0,
424
- normalizeCharge: authoritativeChargeNormalizer("@ai-sdk/xai"),
782
+ normalizeCost: providerCostNormalizer("@ai-sdk/xai"),
425
783
  });
426
784
  installFetch([
427
785
  { choices: [{ delta: { content: "ok" }, finish_reason: "stop" }] },
@@ -436,8 +794,8 @@ test("streamed xAI final usage retains its exact tick charge", async () => {
436
794
  },
437
795
  ]);
438
796
  const response = await p.generate({ workerId: "xai", messages: [] });
439
- assert.deepEqual(response.charge, {
440
- kind: "authoritative",
797
+ assert.deepEqual(response.accounting[0]?.cost, {
798
+ kind: "charged",
441
799
  amount: { amount: "15493500", currency: "USDTICK" },
442
800
  usdEquivalent: "0.00154935",
443
801
  source: "xAI response usage.cost_in_usd_ticks",
@@ -452,7 +810,7 @@ test("generate surfaces and normalizes an out-of-set finish_reason", async () =>
452
810
  ...(typeof options === "object" && options.code !== undefined ? { code: options.code } : {}),
453
811
  });
454
812
  });
455
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
813
+ 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 });
456
814
  installFetch([{ choices: [{ delta: { content: "x" }, finish_reason: "function_call" }] }]);
457
815
  const { assistant } = await p.generate({ workerId: "r", messages: [] });
458
816
  assert.equal(assistant.finishReason, null);
@@ -470,7 +828,7 @@ test("#161: a streamed resource interruption is a failed exchange with complete
470
828
  usage: { prompt_tokens: 7, completion_tokens: 5, total_tokens: 12 },
471
829
  },
472
830
  ]);
473
- const provider = new AiSdkProvider({
831
+ const provider = testProvider({
474
832
  ...injectedBase,
475
833
  retryAttempts: 2,
476
834
  rawBody: true,
@@ -489,15 +847,13 @@ test("#161: a streamed resource interruption is a failed exchange with complete
489
847
  assert.equal(error.attempt?.assistant.content, "partial answer");
490
848
  assert.equal(error.attempt?.assistant.reasoning, "partial thought");
491
849
  assert.equal(error.attempt?.assistant.finishReason, "resource_interrupted");
492
- assert.deepEqual(error.attempt?.assistant.usage, {
493
- prompt: 7,
494
- completion: 2,
495
- reasoning: 3,
496
- cached: 0,
497
- total: 12,
850
+ assert.deepEqual(error.accounting[0]?.usage, {
851
+ inputTokens: 7,
852
+ outputTokens: 5,
853
+ totalTokens: 12,
498
854
  });
499
855
  assert.equal(
500
- (error.attempt?.assistantRaw as { rawFinishReason?: string }).rawFinishReason,
856
+ (error.attempt?.assistantRaw as { rawFinishReason?: string } | undefined)?.rawFinishReason,
501
857
  "insufficient_system_resource",
502
858
  );
503
859
  assert.ok(Array.isArray(error.attempt?.rawBody));
@@ -517,7 +873,7 @@ test("#161: a buffered resource interruption preserves the successful wire respo
517
873
  usage: { prompt_tokens: 7, completion_tokens: 5, total_tokens: 12 },
518
874
  };
519
875
  const calls = installFetchJson(wire);
520
- const provider = new AiSdkProvider({
876
+ const provider = testProvider({
521
877
  ...injectedBase,
522
878
  streaming: false,
523
879
  retryAttempts: 2,
@@ -534,7 +890,7 @@ test("#161: a buffered resource interruption preserves the successful wire respo
534
890
  assert.equal(error.attempt?.assistant.finishReason, "resource_interrupted");
535
891
  assert.deepEqual(error.attempt?.rawBody, wire);
536
892
  assert.equal(
537
- (error.attempt?.assistantRaw as { rawFinishReason?: string }).rawFinishReason,
893
+ (error.attempt?.assistantRaw as { rawFinishReason?: string } | undefined)?.rawFinishReason,
538
894
  "insufficient_system_resource",
539
895
  );
540
896
  return true;
@@ -546,37 +902,37 @@ test("#161: a buffered resource interruption preserves the successful wire respo
546
902
  test("generate translates a backend cap synonym to canonical length", async () => {
547
903
  // gemini shouts MAX_TOKENS, anthropic says max_tokens -- both must reach core as
548
904
  // "length" so its truncation check (=== "length") is a cross-backend invariant.
549
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
905
+ 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 });
550
906
  installFetch([{ choices: [{ delta: { content: "x" }, finish_reason: "MAX_TOKENS" }] }]);
551
907
  const { assistant } = await p.generate({ workerId: "r", messages: [] });
552
908
  assert.equal(assistant.finishReason, "length");
553
909
  });
554
910
 
555
911
  test("generate translates end_turn to canonical stop", async () => {
556
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
912
+ 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 });
557
913
  installFetch([{ choices: [{ delta: { content: "x" }, finish_reason: "end_turn" }] }]);
558
914
  const { assistant } = await p.generate({ workerId: "r", messages: [] });
559
915
  assert.equal(assistant.finishReason, "stop");
560
916
  });
561
917
 
562
918
  test("generate translates xAI completed to canonical stop", async () => {
563
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
919
+ 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 });
564
920
  installFetch([{ choices: [{ delta: { content: "x" }, finish_reason: "completed" }] }]);
565
921
  const { assistant } = await p.generate({ workerId: "r", messages: [] });
566
922
  assert.equal(assistant.finishReason, "stop");
567
923
  });
568
924
 
569
925
  test("generate aggregates reasoning deltas under multiple field names", async () => {
570
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
926
+ 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 });
571
927
  installFetch([{ choices: [{ delta: { reasoning_content: "be", thinking: "cause" } }] }]);
572
928
  const { assistant } = await p.generate({ workerId: "r", messages: [] });
573
929
  assert.equal(assistant.reasoning, "because");
574
930
  assert.equal("reasoningEncrypted" in assistant, false); // open reasoning only -> field absent
575
931
  });
576
932
 
577
- test("{§provider-tagged-reasoning} explicit think-tags projects one streamed leading envelope and reclassifies usage", async () => {
933
+ test("{§provider-tagged-reasoning} explicit think-tags project content without estimating token attribution", async () => {
578
934
  const config = { ...injectedBase, reasoningResponseStyle: "think-tags" as const, rawBody: true };
579
- const p = new AiSdkProvider(config);
935
+ const p = testProvider(config);
580
936
  installFetch([
581
937
  { choices: [{ delta: { content: "<thi" } }] },
582
938
  { choices: [{ delta: { content: "nk>12345</th" } }] },
@@ -588,12 +944,10 @@ test("{§provider-tagged-reasoning} explicit think-tags projects one streamed le
588
944
 
589
945
  assert.equal(response.assistant.reasoning, "12345");
590
946
  assert.equal(response.assistant.content, "abcde");
591
- assert.deepEqual(response.assistant.usage, {
592
- prompt: 3,
593
- completion: 5,
594
- reasoning: 5,
595
- cached: 0,
596
- total: 13,
947
+ assert.deepEqual(response.accounting[0]?.usage, {
948
+ inputTokens: 3,
949
+ outputTokens: 10,
950
+ totalTokens: 13,
597
951
  });
598
952
  assert.deepEqual(
599
953
  ((response.assistantRaw as { content: string; reasoning: string }).content),
@@ -611,12 +965,12 @@ test("{§provider-tagged-reasoning} explicit think-tags projects one buffered le
611
965
  usage: { prompt_tokens: 3, completion_tokens: 10, total_tokens: 13 },
612
966
  });
613
967
  const config = { ...injectedBase, streaming: false, reasoningResponseStyle: "think-tags" as const };
614
- const response = await new AiSdkProvider(config).generate({ workerId: "tagged-buffer", messages: [] });
968
+ const response = await testProvider(config).generate({ workerId: "tagged-buffer", messages: [] });
615
969
 
616
970
  assert.equal(response.assistant.reasoning, "12345");
617
971
  assert.equal(response.assistant.content, "abcde");
618
- assert.equal(response.assistant.usage.completion, 5);
619
- assert.equal(response.assistant.usage.reasoning, 5);
972
+ assert.equal(response.accounting[0]?.usage?.outputTokens, 10);
973
+ assert.equal(response.accounting[0]?.usage?.outputTokenDetails, undefined);
620
974
  });
621
975
 
622
976
  test("{§provider-tagged-reasoning} tagged text does not overwrite itemized reasoning usage", async () => {
@@ -631,12 +985,14 @@ test("{§provider-tagged-reasoning} tagged text does not overwrite itemized reas
631
985
  },
632
986
  });
633
987
  const config = { ...injectedBase, streaming: false, reasoningResponseStyle: "think-tags" as const };
634
- const response = await new AiSdkProvider(config).generate({ workerId: "tagged-itemized", messages: [] });
988
+ const response = await testProvider(config).generate({ workerId: "tagged-itemized", messages: [] });
635
989
 
636
990
  assert.equal(response.assistant.reasoning, "12345");
637
991
  assert.equal(response.assistant.content, "abcde");
638
- assert.equal(response.assistant.usage.completion, 7);
639
- assert.equal(response.assistant.usage.reasoning, 3);
992
+ assert.deepEqual(response.accounting[0]?.usage?.outputTokenDetails, {
993
+ textTokens: 7,
994
+ reasoningTokens: 3,
995
+ });
640
996
  });
641
997
 
642
998
  test("{§provider-tagged-reasoning} an unclosed capped envelope is wholly reasoning in streamed and buffered responses", async () => {
@@ -645,15 +1001,13 @@ test("{§provider-tagged-reasoning} an unclosed capped envelope is wholly reason
645
1001
  { choices: [{ delta: { content: "<think>unfinished" }, finish_reason: "length" }] },
646
1002
  { usage: { prompt_tokens: 3, completion_tokens: 8, total_tokens: 11 } },
647
1003
  ]);
648
- const streamed = await new AiSdkProvider(config).generate({ workerId: "tagged-capped-stream", messages: [] });
1004
+ const streamed = await testProvider(config).generate({ workerId: "tagged-capped-stream", messages: [] });
649
1005
  assert.equal(streamed.assistant.reasoning, "unfinished");
650
1006
  assert.equal(streamed.assistant.content, "");
651
- assert.deepEqual(streamed.assistant.usage, {
652
- prompt: 3,
653
- completion: 0,
654
- reasoning: 8,
655
- cached: 0,
656
- total: 11,
1007
+ assert.deepEqual(streamed.accounting[0]?.usage, {
1008
+ inputTokens: 3,
1009
+ outputTokens: 8,
1010
+ totalTokens: 11,
657
1011
  });
658
1012
 
659
1013
  mock.restoreAll();
@@ -663,11 +1017,11 @@ test("{§provider-tagged-reasoning} an unclosed capped envelope is wholly reason
663
1017
  usage: { prompt_tokens: 3, completion_tokens: 8, total_tokens: 11 },
664
1018
  });
665
1019
  const bufferedConfig = { ...config, streaming: false };
666
- const buffered = await new AiSdkProvider(bufferedConfig).generate({ workerId: "tagged-capped-buffer", messages: [] });
1020
+ const buffered = await testProvider(bufferedConfig).generate({ workerId: "tagged-capped-buffer", messages: [] });
667
1021
  assert.equal(buffered.assistant.reasoning, "unfinished");
668
1022
  assert.equal(buffered.assistant.content, "");
669
- assert.equal(buffered.assistant.usage.completion, 0);
670
- assert.equal(buffered.assistant.usage.reasoning, 8);
1023
+ assert.equal(buffered.accounting[0]?.usage?.outputTokens, 8);
1024
+ assert.equal(buffered.accounting[0]?.usage?.outputTokenDetails, undefined);
671
1025
  });
672
1026
 
673
1027
  test("{§provider-tagged-reasoning} verbatim, non-leading, and structured-reasoning controls preserve literal tags", async () => {
@@ -676,11 +1030,11 @@ test("{§provider-tagged-reasoning} verbatim, non-leading, and structured-reason
676
1030
  choices: [{ message: { content: "<think>literal</think>answer" }, finish_reason: "stop" }],
677
1031
  usage: { prompt_tokens: 1, completion_tokens: 4, total_tokens: 5 },
678
1032
  });
679
- const verbatim = await new AiSdkProvider({ ...injectedBase, streaming: false })
1033
+ const verbatim = await testProvider({ ...injectedBase, streaming: false })
680
1034
  .generate({ workerId: "verbatim", messages: [] });
681
1035
  assert.equal(verbatim.assistant.content, "<think>literal</think>answer");
682
1036
  assert.equal(verbatim.assistant.reasoning, null);
683
- assert.equal(verbatim.assistant.usage.completion, 4);
1037
+ assert.equal(verbatim.accounting[0]?.usage?.outputTokens, 4);
684
1038
 
685
1039
  mock.restoreAll();
686
1040
  installFetchJson({
@@ -689,7 +1043,7 @@ test("{§provider-tagged-reasoning} verbatim, non-leading, and structured-reason
689
1043
  usage: { prompt_tokens: 1, completion_tokens: 5, total_tokens: 6 },
690
1044
  });
691
1045
  const taggedConfig = { ...injectedBase, streaming: false, reasoningResponseStyle: "think-tags" as const };
692
- const nonLeading = await new AiSdkProvider(taggedConfig)
1046
+ const nonLeading = await testProvider(taggedConfig)
693
1047
  .generate({ workerId: "non-leading", messages: [] });
694
1048
  assert.equal(nonLeading.assistant.content, "show <think>literal</think> exactly");
695
1049
  assert.equal(nonLeading.assistant.reasoning, null);
@@ -703,14 +1057,14 @@ test("{§provider-tagged-reasoning} verbatim, non-leading, and structured-reason
703
1057
  }, finish_reason: "stop" }],
704
1058
  usage: { prompt_tokens: 1, completion_tokens: 7, total_tokens: 8 },
705
1059
  });
706
- const structured = await new AiSdkProvider(taggedConfig)
1060
+ const structured = await testProvider(taggedConfig)
707
1061
  .generate({ workerId: "structured", messages: [] });
708
1062
  assert.equal(structured.assistant.content, "<think>literal visible bytes</think>");
709
1063
  assert.equal(structured.assistant.reasoning, "structured reasoning");
710
1064
  });
711
1065
 
712
1066
  test("{§provider-tagged-reasoning} grammar evidence retains the exact pre-projection tagged sentence", async () => {
713
- const content = "<think>🧠reason</think><<PLAN::PLAN\n<<SEND[200]:done:SEND";
1067
+ const content = "<think>🧠reason</think># PLAN0\n\n## SEND0 [200]\ndone";
714
1068
  const config = {
715
1069
  ...injectedBase,
716
1070
  contextWindow: 640,
@@ -721,14 +1075,14 @@ test("{§provider-tagged-reasoning} grammar evidence retains the exact pre-proje
721
1075
  };
722
1076
  installFetch([{ choices: [{ delta: { content }, finish_reason: "stop" }] }]);
723
1077
 
724
- const response = await new AiSdkProvider(config).generate({
1078
+ const response = await testProvider(config).generate({
725
1079
  workerId: "tagged-grammar",
726
1080
  messages: [],
727
1081
  grammar: `root ::= ${JSON.stringify(content)}`,
728
1082
  });
729
1083
 
730
1084
  assert.equal(response.assistant.reasoning, "🧠reason");
731
- assert.equal(response.assistant.content, "<<PLAN::PLAN\n<<SEND[200]:done:SEND");
1085
+ assert.equal(response.assistant.content, "# PLAN0\n\n## SEND0 [200]\ndone");
732
1086
  assert.deepEqual(response.grammarEvidence, {
733
1087
  input: content,
734
1088
  contentStart: [..."<think>🧠reason</think>"].length,
@@ -745,7 +1099,7 @@ test("encrypted reasoning (non-streamed): encrypted entries normalize and text e
745
1099
  { type: "reasoning.text", text: "never surfaced here" },
746
1100
  ],
747
1101
  }, finish_reason: "stop" }], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } });
748
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, streaming: false });
1102
+ 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, streaming: false });
749
1103
  const { assistant } = await p.generate({ workerId: "r", messages: [] });
750
1104
  // Wire detail ID is preserved; the assistant-message location supports the
751
1105
  // derived classification but supplies no downstream client entity ID.
@@ -759,7 +1113,7 @@ test("distinct encrypted-reasoning wire ids stay distinct items", async () => {
759
1113
  { type: "reasoning.encrypted", data: "AAA", format: "openai-responses-v1", id: "rs_1" },
760
1114
  { type: "reasoning.encrypted", data: "BBB", format: "openai-responses-v1", id: "rs_2" },
761
1115
  ] }, finish_reason: "stop" }], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } });
762
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, streaming: false });
1116
+ 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, streaming: false });
763
1117
  const { assistant } = await p.generate({ workerId: "r", messages: [] });
764
1118
  assert.equal(assistant.reasoningEncrypted?.length, 2);
765
1119
  assert.deepEqual(assistant.reasoningEncrypted?.map((i) => i.id), ["rs_1", "rs_2"]);
@@ -769,7 +1123,7 @@ test("assistant-message location classifies encrypted reasoning without inventin
769
1123
  installFetchJson({ model: "m", choices: [{ message: { content: "ok", reasoning_details: [
770
1124
  { type: "reasoning.encrypted", data: "OPAQUE", format: "openai-responses-v1", id: null, index: 0 },
771
1125
  ] }, finish_reason: "stop" }], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } });
772
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, streaming: false });
1126
+ 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, streaming: false });
773
1127
  const { assistant } = await p.generate({ workerId: "r", messages: [] });
774
1128
  assert.deepEqual(assistant.reasoningEncrypted, [{
775
1129
  id: null,
@@ -779,7 +1133,7 @@ test("assistant-message location classifies encrypted reasoning without inventin
779
1133
  });
780
1134
 
781
1135
  test("encrypted reasoning (streamed): chunked blob concatenates per entry index", async () => {
782
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
1136
+ 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 });
783
1137
  installFetch([
784
1138
  { choices: [{ delta: { reasoning_details: [{ type: "reasoning.encrypted", data: "gAAAA", format: "openai-responses-v1", id: "rs_1", index: 0 }] } }] },
785
1139
  { choices: [{ delta: { reasoning_details: [{ type: "reasoning.encrypted", data: "BqXYZ", id: "rs_1", index: 0 }] } }] },
@@ -790,32 +1144,35 @@ test("encrypted reasoning (streamed): chunked blob concatenates per entry index"
790
1144
  assert.equal(assistant.content, "4");
791
1145
  });
792
1146
 
793
- test("reasoningStyle 'think' gates on budget != 0 (magnitude irrelevant for native)", async () => {
794
- const on = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "adaptive", budget: null }, retryAttempts: 0, reasoningStyle: "think" });
1147
+ test("reasoningStyle 'think' follows activation (magnitude is irrelevant to the boolean wire control)", async () => {
1148
+ const on = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "adaptive", budget: null }, retryAttempts: 0, reasoningStyle: "think" });
795
1149
  let calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
796
1150
  await on.generate({ workerId: "r", messages: [] });
797
1151
  assert.equal(JSON.parse(calls[0].init.body as string).think, true);
798
1152
 
799
1153
  mock.restoreAll();
800
- const off = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, reasoningStyle: "think" });
1154
+ const off = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, reasoningStyle: "think" });
801
1155
  calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
802
1156
  await off.generate({ workerId: "r", messages: [] });
803
1157
  assert.equal("think" in JSON.parse(calls[0].init.body as string), false);
804
1158
  });
805
1159
 
806
- test("reasoningStyle 'effort' sends a reasoning_effort tier from the budget", async () => {
807
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "on", budget: 5000 }, retryAttempts: 0, reasoningStyle: "effort" });
808
- const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
809
- await p.generate({ workerId: "r", messages: [] });
810
- assert.equal(JSON.parse(calls[0].init.body as string).reasoning_effort, "high");
1160
+ test("reasoningStyle 'effort' enables at the portable default without inventing a budget", async () => {
1161
+ for (const [budget, expected] of [[null, "medium"], [5000, "high"]] as const) {
1162
+ const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", ...(budget === null ? {} : { outputBudget: budget + 1, reasoningBudget: budget }), fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "on", budget }, retryAttempts: 0, reasoningStyle: "effort" });
1163
+ const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1164
+ await p.generate({ workerId: "r", messages: [] });
1165
+ assert.equal(JSON.parse(calls[0].init.body as string).reasoning_effort, expected);
1166
+ mock.restoreAll();
1167
+ }
811
1168
  });
812
1169
 
813
1170
  test("reasoningStyle 'effort_explicit': off SENDS none, adaptive OMITS, on sends the tier", async () => {
814
1171
  // expected === null → the field must be ABSENT from the wire body. Fireworks
815
1172
  // 400s reasoning_effort='adaptive' for non-MiniMax models (wire-verified,
816
1173
  // Adaptive = the backend's own default posture = omission.
817
- for (const [reasoning, expected] of [[{ mode: "off", budget: null }, "none"], [{ mode: "adaptive", budget: null }, null], [{ mode: "on", budget: 5000 }, "high"]] as Array<[{ mode: "off" | "adaptive" | "on"; budget: number | null }, string | null]>) {
818
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning, retryAttempts: 0, reasoningStyle: "effort_explicit" });
1174
+ for (const [reasoning, expected] of [[{ mode: "off", budget: null }, "none"], [{ mode: "adaptive", budget: null }, null], [{ mode: "on", budget: null }, "medium"], [{ mode: "on", budget: 5000 }, "high"]] as Array<[{ mode: "off" | "adaptive" | "on"; budget: number | null }, string | null]>) {
1175
+ const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", ...(reasoning.budget === null ? {} : { outputBudget: reasoning.budget + 1, reasoningBudget: reasoning.budget }), fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning, retryAttempts: 0, reasoningStyle: "effort_explicit" });
819
1176
  const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
820
1177
  await p.generate({ workerId: "r", messages: [] });
821
1178
  const body = JSON.parse(calls[0].init.body as string);
@@ -829,15 +1186,17 @@ test("{§deepseek-reasoning-request} #157: thinking_effort maps the complete Dee
829
1186
  const cases = [
830
1187
  [{ mode: "off", budget: null }, { thinking: { type: "disabled" } }],
831
1188
  [{ mode: "adaptive", budget: null }, {}],
1189
+ [{ mode: "on", budget: null }, { thinking: { type: "enabled" } }],
832
1190
  [{ mode: "on", budget: 5000 }, { thinking: { type: "enabled" }, reasoning_effort: "high" }],
833
1191
  ] as const;
834
1192
  for (const [reasoning, expected] of cases) {
835
- const p = new AiSdkProvider({
1193
+ const p = testProvider({
836
1194
  model: "m",
837
1195
  url: "http://x/v1/chat/completions",
838
1196
  fetchTimeoutMs: 5000,
839
1197
  temperature: 0.2,
840
1198
  repeatPenalty: 1.15,
1199
+ ...(reasoning.budget === null ? {} : { outputBudget: reasoning.budget + 1, reasoningBudget: reasoning.budget }),
841
1200
  reasoning,
842
1201
  retryAttempts: 0,
843
1202
  reasoningStyle: "thinking_effort",
@@ -858,7 +1217,7 @@ test("{§deepseek-reasoning-request} #157: thinking_effort maps the complete Dee
858
1217
  });
859
1218
 
860
1219
  test("the family temperature default rides every request; caller sampling overrides it", async () => {
861
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
1220
+ 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 });
862
1221
  let calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
863
1222
  await p.generate({ workerId: "r", messages: [] });
864
1223
  assert.equal(JSON.parse(calls[0].init.body as string).temperature, 0.2);
@@ -877,7 +1236,7 @@ test("the family temperature default rides every request; caller sampling overri
877
1236
  test("DRY + repeat_last_n ride the llamacpp path when set; unset leaves the box default; never on cloud", async () => {
878
1237
  const base = { model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off" as const, budget: null }, retryAttempts: 0 };
879
1238
  // set + llamacpp -> the loop-breakers ride the wire
880
- const p = new AiSdkProvider({ ...base, grammarStyle: "llamacpp", dryMultiplier: 0.8, dryBase: 1.75, dryAllowedLength: 2, repeatLastN: 512 });
1239
+ const p = testProvider({ ...base, grammarStyle: "llamacpp", dryMultiplier: 0.8, dryBase: 1.75, dryAllowedLength: 2, repeatLastN: 512 });
881
1240
  let calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
882
1241
  await p.generate({ workerId: "r", messages: [] });
883
1242
  let body = JSON.parse(calls[0].init.body as string);
@@ -888,7 +1247,7 @@ test("DRY + repeat_last_n ride the llamacpp path when set; unset leaves the box
888
1247
  assert.equal(body.repeat_penalty, 1.15); // repeat_penalty always rides the llamacpp path
889
1248
  mock.restoreAll();
890
1249
  // unset -> no dry_*/repeat_last_n on the wire (box keeps its own defaults)
891
- const p2 = new AiSdkProvider({ ...base, grammarStyle: "llamacpp" });
1250
+ const p2 = testProvider({ ...base, grammarStyle: "llamacpp" });
892
1251
  calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
893
1252
  await p2.generate({ workerId: "r", messages: [] });
894
1253
  body = JSON.parse(calls[0].init.body as string);
@@ -896,7 +1255,7 @@ test("DRY + repeat_last_n ride the llamacpp path when set; unset leaves the box
896
1255
  assert.equal("repeat_last_n" in body, false);
897
1256
  mock.restoreAll();
898
1257
  // DRY is a llama.cpp sampler: a cloud ("none") provider never emits it, even if configured
899
- const p3 = new AiSdkProvider({ ...base, grammarStyle: "none", dryMultiplier: 0.8, repeatLastN: 512 });
1258
+ const p3 = testProvider({ ...base, grammarStyle: "none", dryMultiplier: 0.8, repeatLastN: 512 });
900
1259
  calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
901
1260
  await p3.generate({ workerId: "r", messages: [] });
902
1261
  body = JSON.parse(calls[0].init.body as string);
@@ -906,7 +1265,7 @@ test("DRY + repeat_last_n ride the llamacpp path when set; unset leaves the box
906
1265
  });
907
1266
 
908
1267
  test("llamacpp grammar path: temperature default + the managed repeat-penalty floor", async () => {
909
- const p = new AiSdkProvider({ 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" });
1268
+ 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" });
910
1269
  const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
911
1270
  await p.generate({ workerId: "r", messages: [], grammar: 'root ::= "x"' });
912
1271
  const body = JSON.parse(calls[0].init.body as string);
@@ -916,13 +1275,13 @@ test("llamacpp grammar path: temperature default + the managed repeat-penalty fl
916
1275
 
917
1276
  test("the repeat penalty rides every request rail-off, keyed per backend", async () => {
918
1277
  // llama.cpp with NO grammar carries its key too (unconstrained local is guarded)
919
- const llama = new AiSdkProvider({ 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" });
1278
+ const llama = 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" });
920
1279
  let calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
921
1280
  await llama.generate({ workerId: "r", messages: [] });
922
1281
  assert.equal(JSON.parse(calls[0].init.body as string).repeat_penalty, 1.15);
923
1282
  mock.restoreAll();
924
1283
  // A `none`-style cloud backend with a frequency penalty gets frequency_penalty.
925
- const cloud = new AiSdkProvider({ 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 });
1284
+ const cloud = 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 });
926
1285
  calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
927
1286
  await cloud.generate({ workerId: "r", messages: [] });
928
1287
  const cloudBody = JSON.parse(calls[0].init.body as string);
@@ -931,19 +1290,19 @@ test("the repeat penalty rides every request rail-off, keyed per backend", async
931
1290
  assert.equal("repeat_penalty" in cloudBody, false);
932
1291
  mock.restoreAll();
933
1292
  // frequencyPenalty unset (default 0) opts out cleanly - sends nothing (an out-of-date plugin runs unguarded, never breaks)
934
- const bare = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
1293
+ const bare = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
935
1294
  calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
936
1295
  await bare.generate({ workerId: "r", messages: [] });
937
1296
  assert.equal("frequency_penalty" in JSON.parse(calls[0].init.body as string), false);
938
1297
  });
939
1298
 
940
1299
  test("sampling passthrough forwards caller params; managed + reserved keys win", async () => {
941
- const p = new AiSdkProvider({ model: "managed-model", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
1300
+ const p = testProvider({ model: "managed-model", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
942
1301
  const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
943
1302
  await p.generate({
944
1303
  workerId: "r",
945
1304
  messages: [{ role: "user", content: "hi" }],
946
- maxTokens: 100,
1305
+ maxOutputTokens: 100,
947
1306
  sampling: {
948
1307
  temperature: 0.2, top_p: 0.9, top_k: 40, stop: ["\n"], // real sampling → passthrough
949
1308
  model: "hijack", response_format: { type: "grammar", grammar: "x" }, id_slot: 7, // reserved → stripped
@@ -961,12 +1320,12 @@ test("sampling passthrough forwards caller params; managed + reserved keys win",
961
1320
  });
962
1321
 
963
1322
  test("sampling passthrough guards contract invariants: n/tools/caps stripped, platform knobs pass", async () => {
964
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
1323
+ 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 });
965
1324
  const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
966
1325
  await p.generate({
967
1326
  workerId: "r",
968
1327
  messages: [{ role: "user", content: "hi" }],
969
- maxTokens: 100,
1328
+ maxOutputTokens: 100,
970
1329
  sampling: {
971
1330
  n: 3, // breaks choices[0] atomicity -> stripped
972
1331
  tools: [{ type: "function" }], tool_choice: "auto", // tools-in-body doctrine -> stripped
@@ -986,7 +1345,7 @@ test("sampling passthrough guards contract invariants: n/tools/caps stripped, pl
986
1345
  });
987
1346
 
988
1347
  test("template reasoning returns the exact pre-projection grammar sentence ({§gbnf-response-observation})", async () => {
989
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", contextWindow: 640, reasoningReserve: { tokens: 64 }, completionReserve: { tokens: 160 }, fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "adaptive", budget: null }, retryAttempts: 0, reasoningStyle: "template", grammarStyle: "llamacpp" });
1348
+ const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", contextWindow: 640, outputBudget: 224, reasoningBudget: 64, fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "adaptive", budget: 64 }, retryAttempts: 0, reasoningStyle: "template", grammarStyle: "llamacpp" });
990
1349
  const grammarInput = "<|channel>thought\ncon🙂sider<channel|>x";
991
1350
  const calls = installFetch([{ choices: [{ delta: { content: grammarInput } }] }]);
992
1351
  const res = await p.generate({ workerId: "r", messages: [], grammar: `root ::= ${JSON.stringify(grammarInput)}` });
@@ -1005,8 +1364,24 @@ test("template reasoning returns the exact pre-projection grammar sentence ({§g
1005
1364
  assert.equal(res.meta?.railsVerdict, undefined, "the provider represents evidence but does not grade itself");
1006
1365
  });
1007
1366
 
1367
+ test("template reasoning projects a leading think envelope without losing grammar evidence", async () => {
1368
+ const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", contextWindow: 640, outputBudget: 224, reasoningBudget: 64, fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "adaptive", budget: 64 }, retryAttempts: 0, reasoningStyle: "template", grammarStyle: "llamacpp" });
1369
+ const input = "<think>\ncon🙂sider</think>x";
1370
+ const calls = installFetch([{ choices: [{ delta: { content: input } }] }]);
1371
+ const res = await p.generate({ workerId: "r", messages: [], grammar: `root ::= ${JSON.stringify(input)}` });
1372
+ const body = JSON.parse(calls[0].init.body as string);
1373
+ assert.equal(body.reasoning_format, "none");
1374
+ assert.equal(res.assistant.reasoning, "con🙂sider");
1375
+ assert.equal(res.assistant.content, "x");
1376
+ assert.deepEqual(res.grammarEvidence, {
1377
+ input,
1378
+ contentStart: [..."<think>\ncon🙂sider</think>"].length,
1379
+ transported: true,
1380
+ });
1381
+ });
1382
+
1008
1383
  test("a verbatim template response remains exact evidence when it has no channel envelope", async () => {
1009
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", contextWindow: 640, reasoningReserve: { tokens: 64 }, completionReserve: { tokens: 160 }, fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "adaptive", budget: null }, retryAttempts: 0, reasoningStyle: "template", grammarStyle: "llamacpp" });
1384
+ const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", contextWindow: 640, outputBudget: 224, reasoningBudget: 64, fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "adaptive", budget: 64 }, retryAttempts: 0, reasoningStyle: "template", grammarStyle: "llamacpp" });
1010
1385
  const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1011
1386
  const res = await p.generate({ workerId: "r", messages: [], grammar: 'root ::= "x"' });
1012
1387
  const body = JSON.parse(calls[0].init.body as string);
@@ -1015,7 +1390,7 @@ test("a verbatim template response remains exact evidence when it has no channel
1015
1390
  });
1016
1391
 
1017
1392
  test("a template grammar preserves exact evidence when reasoning is disabled", async () => {
1018
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", contextWindow: 640, reasoningReserve: { tokens: 64 }, completionReserve: { tokens: 160 }, fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, reasoningStyle: "template", grammarStyle: "llamacpp" });
1393
+ const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", contextWindow: 640, outputBudget: 224, fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, reasoningStyle: "template", grammarStyle: "llamacpp" });
1019
1394
  const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1020
1395
  const res = await p.generate({ workerId: "r", messages: [], grammar: 'root ::= "x"' });
1021
1396
  const body = JSON.parse(calls[0].init.body as string);
@@ -1025,14 +1400,14 @@ test("a template grammar preserves exact evidence when reasoning is disabled", a
1025
1400
  });
1026
1401
 
1027
1402
  test("an unexpectedly projected template response cannot claim pre-projection evidence", async () => {
1028
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", contextWindow: 640, reasoningReserve: { tokens: 64 }, completionReserve: { tokens: 160 }, fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "adaptive", budget: null }, retryAttempts: 0, reasoningStyle: "template", grammarStyle: "llamacpp" });
1403
+ const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", contextWindow: 640, outputBudget: 224, reasoningBudget: 64, fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "adaptive", budget: 64 }, retryAttempts: 0, reasoningStyle: "template", grammarStyle: "llamacpp" });
1029
1404
  installFetch([{ choices: [{ delta: { reasoning_content: "reason", content: "x" } }] }]);
1030
1405
  const res = await p.generate({ workerId: "r", messages: [], grammar: 'root ::= "x"' });
1031
1406
  assert.equal(res.grammarEvidence, undefined);
1032
1407
  });
1033
1408
 
1034
1409
  test("template reasoning preserves an empty grammar-required channel as exact evidence", async () => {
1035
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", contextWindow: 640, reasoningReserve: { tokens: 64 }, completionReserve: { tokens: 160 }, fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "adaptive", budget: null }, retryAttempts: 0, reasoningStyle: "template", grammarStyle: "llamacpp" });
1410
+ const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", contextWindow: 640, outputBudget: 224, reasoningBudget: 64, fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "adaptive", budget: 64 }, retryAttempts: 0, reasoningStyle: "template", grammarStyle: "llamacpp" });
1036
1411
  const input = "<|channel>thought\n<channel|>x";
1037
1412
  const calls = installFetch([{ choices: [{ delta: { content: input } }] }]);
1038
1413
  const res = await p.generate({ workerId: "r", messages: [], grammar: `root ::= ${JSON.stringify(input)}` });
@@ -1063,16 +1438,16 @@ test("channel-escape detector: billed completion tokens vastly beyond visible ch
1063
1438
  }
1064
1439
  return new Response(sseStream(chunks), { status: 200 });
1065
1440
  };
1066
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetch, tokenizeUrl: "http://x/tokenize", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "adaptive", budget: null }, retryAttempts: 0, reasoningStyle: "template", grammarStyle: "llamacpp" });
1441
+ const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetch, tokenizeUrl: "http://x/tokenize", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "adaptive", budget: null }, retryAttempts: 0, reasoningStyle: "template", grammarStyle: "llamacpp" });
1067
1442
  const res = await p.generate({ workerId: "r", messages: [], grammar: 'root ::= "x"' });
1068
1443
  const escape = res.notices?.find((e) => e.message.includes("escaped the grammar"));
1069
1444
  assert.ok(escape, "escape notice attached");
1070
1445
  assert.equal(escape!.kind, "grammar_unenforced");
1071
- assert.match(escape!.message ?? "", /5000 completion tokens billed/);
1446
+ assert.match(escape!.message ?? "", /5000 output tokens billed/);
1072
1447
  });
1073
1448
 
1074
1449
  test("channel-escape state is absent without a transported grammar", async () => {
1075
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "adaptive", budget: null }, retryAttempts: 0, reasoningStyle: "template", grammarStyle: "llamacpp" });
1450
+ const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "adaptive", budget: null }, retryAttempts: 0, reasoningStyle: "template", grammarStyle: "llamacpp" });
1076
1451
  installFetch([
1077
1452
  { choices: [{ delta: { content: "x" }, finish_reason: "length" }] },
1078
1453
  { usage: { prompt_tokens: 10, completion_tokens: 5000, total_tokens: 5010 } },
@@ -1083,7 +1458,7 @@ test("channel-escape state is absent without a transported grammar", async () =>
1083
1458
  });
1084
1459
 
1085
1460
  test("reasoningStyle 'template' sends llama-server activation, parser, and response-wide allowance", async () => {
1086
- const on = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", contextWindow: 640, reasoningReserve: { percent: 0.1 }, completionReserve: { percent: 0.25 }, fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "adaptive", budget: null }, retryAttempts: 0, reasoningStyle: "template" });
1461
+ const on = testProvider({ model: "m", url: "http://x/v1/chat/completions", contextWindow: 640, outputBudget: 224, reasoningBudget: 64, fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "adaptive", budget: 64 }, retryAttempts: 0, reasoningStyle: "template" });
1087
1462
  let calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1088
1463
  await on.generate({ workerId: "r", messages: [] });
1089
1464
  let body = JSON.parse(calls[0].init.body as string);
@@ -1092,7 +1467,7 @@ test("reasoningStyle 'template' sends llama-server activation, parser, and respo
1092
1467
  assert.equal(body.thinking_budget_tokens, 64);
1093
1468
 
1094
1469
  mock.restoreAll();
1095
- const off = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", contextWindow: 640, reasoningReserve: { percent: 0.1 }, completionReserve: { percent: 0.25 }, fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, reasoningStyle: "template" });
1470
+ const off = testProvider({ model: "m", url: "http://x/v1/chat/completions", contextWindow: 640, outputBudget: 224, fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, reasoningStyle: "template" });
1096
1471
  calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1097
1472
  await off.generate({ workerId: "r", messages: [] });
1098
1473
  body = JSON.parse(calls[0].init.body as string);
@@ -1101,35 +1476,56 @@ test("reasoningStyle 'template' sends llama-server activation, parser, and respo
1101
1476
  assert.equal(body.thinking_budget_tokens, 0);
1102
1477
  });
1103
1478
 
1104
- test("reasoningStyle 'template' explicit budget tightens the reserve and cannot exceed it", async () => {
1105
- const base = { model: "m", url: "http://x/v1/chat/completions", contextWindow: 640, reasoningReserve: { tokens: 64 } as const, completionReserve: { tokens: 160 } as const, fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, retryAttempts: 0, reasoningStyle: "template" as const };
1106
- const p = new AiSdkProvider({ ...base, reasoning: { mode: "on", budget: 32 } });
1479
+ test("reasoningStyle 'template' carries the explicit reasoning subset and rejects an additive envelope", async () => {
1480
+ const base = { model: "m", url: "http://x/v1/chat/completions", contextWindow: 640, outputBudget: 224, fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, retryAttempts: 0, reasoningStyle: "template" as const };
1481
+ const p = testProvider({ ...base, reasoningBudget: 32, reasoning: { mode: "on", budget: 32 } });
1107
1482
  const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1108
1483
  await p.generate({ workerId: "r", messages: [], sampling: { thinking_budget_tokens: 999, reasoning_format: "none" } });
1109
1484
  const body = JSON.parse(calls[0].init.body as string);
1110
1485
  assert.equal(body.thinking_budget_tokens, 32);
1111
1486
  assert.equal(body.reasoning_format, "auto");
1112
1487
  assert.throws(
1113
- () => new AiSdkProvider({ ...base, reasoning: { mode: "on", budget: 65 } }),
1114
- /REASONING_BUDGET \(65\) exceeds the resolved PLURNK_PROVIDERS_REASONING_RESERVE \(64\)/,
1488
+ () => testProvider({ ...base, reasoningBudget: 224, reasoning: { mode: "on", budget: 224 } }),
1489
+ /reasoningBudget must be smaller than the total outputBudget/,
1115
1490
  );
1116
1491
  });
1117
1492
 
1118
- test("budget 0 suppresses effort and include_reasoning", async () => {
1119
- const effort = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, reasoningStyle: "effort" });
1493
+ test("reasoningStyle 'template' explicit activation uses the configured reasoning subset", async () => {
1494
+ const p = testProvider({
1495
+ model: "m",
1496
+ url: "http://x/v1/chat/completions",
1497
+ contextWindow: 640,
1498
+ outputBudget: 224,
1499
+ reasoningBudget: 64,
1500
+ fetchTimeoutMs: 5000,
1501
+ temperature: 0.2,
1502
+ repeatPenalty: 1.15,
1503
+ reasoning: { mode: "on", budget: 64 },
1504
+ retryAttempts: 0,
1505
+ reasoningStyle: "template",
1506
+ });
1507
+ const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1508
+ await p.generate({ workerId: "r", messages: [] });
1509
+ const body = JSON.parse(calls[0].init.body as string);
1510
+ assert.deepEqual(body.chat_template_kwargs, { enable_thinking: true });
1511
+ assert.equal(body.thinking_budget_tokens, 64);
1512
+ });
1513
+
1514
+ test("reasoning off suppresses effort and include_reasoning controls", async () => {
1515
+ const effort = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, reasoningStyle: "effort" });
1120
1516
  let calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1121
1517
  await effort.generate({ workerId: "r", messages: [] });
1122
1518
  assert.equal("reasoning_effort" in JSON.parse(calls[0].init.body as string), false);
1123
1519
 
1124
1520
  mock.restoreAll();
1125
- const relay = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, reasoningStyle: "include_reasoning" });
1521
+ const relay = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, reasoningStyle: "include_reasoning" });
1126
1522
  calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1127
1523
  await relay.generate({ workerId: "r", messages: [] });
1128
1524
  assert.equal("include_reasoning" in JSON.parse(calls[0].init.body as string), false);
1129
1525
  });
1130
1526
 
1131
1527
  test("reasoningStyle 'include_reasoning' sets the relay passthrough toggle", async () => {
1132
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "adaptive", budget: null }, retryAttempts: 0, reasoningStyle: "include_reasoning" });
1528
+ const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "adaptive", budget: null }, retryAttempts: 0, reasoningStyle: "include_reasoning" });
1133
1529
  const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1134
1530
  await p.generate({ workerId: "r", messages: [] });
1135
1531
  assert.equal(JSON.parse(calls[0].init.body as string).include_reasoning, true);
@@ -1138,7 +1534,7 @@ test("reasoningStyle 'include_reasoning' sets the relay passthrough toggle", asy
1138
1534
  // — grammar-constrained sampling —
1139
1535
 
1140
1536
  test("grammar transport 'llamacpp': top-level grammar + the repeat-penalty floor", async () => {
1141
- const p = new AiSdkProvider({ 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" });
1537
+ 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" });
1142
1538
  const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1143
1539
  await p.generate({ workerId: "r", messages: [], grammar: 'root ::= "x"' });
1144
1540
  const body = JSON.parse(calls[0].init.body as string);
@@ -1148,7 +1544,7 @@ test("grammar transport 'llamacpp': top-level grammar + the repeat-penalty floor
1148
1544
  });
1149
1545
 
1150
1546
  test("grammar transport 'none' (default): the grammar is never sent — no silent unconstrained", async () => {
1151
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
1547
+ 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 });
1152
1548
  const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1153
1549
  await p.generate({ workerId: "r", messages: [], grammar: "root ::= statement" });
1154
1550
  const body = JSON.parse(calls[0].init.body as string);
@@ -1158,7 +1554,7 @@ test("grammar transport 'none' (default): the grammar is never sent — no silen
1158
1554
 
1159
1555
  // — exact pre-projection grammar evidence ({§gbnf-response-observation}) —
1160
1556
 
1161
- const grammarProvider = () => new AiSdkProvider({ 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", source: "provider:test" });
1557
+ const grammarProvider = () => 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", source: "provider:test" });
1162
1558
  const streamingContent = (content: string) => installFetch([{ choices: [{ delta: { content }, finish_reason: "stop" }] }]);
1163
1559
 
1164
1560
  test("an unsplit grammar response carries the exact observed sentence", async () => {
@@ -1192,7 +1588,7 @@ test("empty unsplit content remains exact grammar evidence", async () => {
1192
1588
  });
1193
1589
 
1194
1590
  test("grammarStyle 'none' produces no grammar observation", async () => {
1195
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 }); // grammarStyle defaults to "none"
1591
+ 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 defaults to "none"
1196
1592
  streamingContent("anything goes");
1197
1593
  const res = await p.generate({ workerId: "r", messages: [], grammar: 'root ::= "ok"' });
1198
1594
  assert.equal(res.assistant.content, "anything goes");
@@ -1211,7 +1607,7 @@ test("provider evidence does not depend on the local validator understanding the
1211
1607
  // — PLURNK_PROVIDERS_GBNF_DEBUG: validate the grammar, withhold it, and preserve the observation —
1212
1608
 
1213
1609
  test("gbnfDebug marks an unconstrained observation as not transported", async () => {
1214
- const p = new AiSdkProvider({ 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", gbnfDebug: true, source: "provider:test" });
1610
+ 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", gbnfDebug: true, source: "provider:test" });
1215
1611
  const calls = installFetch([{ choices: [{ delta: { content: "ok" }, finish_reason: "stop" }] }]);
1216
1612
  const res = await p.generate({ workerId: "r", messages: [], grammar: 'root ::= "ok"' });
1217
1613
  const body = JSON.parse(calls[0].init.body as string);
@@ -1223,7 +1619,7 @@ test("gbnfDebug marks an unconstrained observation as not transported", async ()
1223
1619
  });
1224
1620
 
1225
1621
  test("gbnfDebug preserves conflicting bytes without a provider verdict", async () => {
1226
- const p = new AiSdkProvider({ 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", gbnfDebug: true, source: "provider:test" });
1622
+ 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", gbnfDebug: true, source: "provider:test" });
1227
1623
  const calls = installFetch([{ choices: [{ delta: { content: "xon-conforming output" }, finish_reason: "stop" }] }]);
1228
1624
  const res = await p.generate({ workerId: "r", messages: [], grammar: 'root ::= "ok"' });
1229
1625
  assert.equal(res.assistant.content, "xon-conforming output");
@@ -1234,7 +1630,7 @@ test("gbnfDebug preserves conflicting bytes without a provider verdict", async (
1234
1630
  });
1235
1631
 
1236
1632
  test("gbnfDebug: an INVALID grammar throws before any wire call — it never reaches the model", async () => {
1237
- const p = new AiSdkProvider({ 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", gbnfDebug: true });
1633
+ 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", gbnfDebug: true });
1238
1634
  const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1239
1635
  await assert.rejects(
1240
1636
  () => p.generate({ workerId: "r", messages: [], grammar: 'foo ::= "a"' }), // no `root` rule → invalid GBNF
@@ -1246,7 +1642,7 @@ test("gbnfDebug: an INVALID grammar throws before any wire call — it never rea
1246
1642
  // — meta bag: verbatim provider metadata —
1247
1643
 
1248
1644
  test("meta: passes backend fields through without reinterpreting monetary values", async () => {
1249
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, streaming: false });
1645
+ 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, streaming: false });
1250
1646
  const balance = { amount: "0.0000042", currency: "XMR" };
1251
1647
  installFetchJson({ ...jsonChoice, balance, system_fingerprint: "fp_abc" });
1252
1648
  const res = await p.generate({ workerId: "r", messages: [] });
@@ -1260,15 +1656,34 @@ const headerVal = (init: RequestInit, name: string): string | undefined =>
1260
1656
  new Headers(init.headers).get(name) ?? undefined;
1261
1657
 
1262
1658
  test("firstPartyMetadata: attributions + client ride as Plurnk-* headers", async () => {
1263
- const p = new AiSdkProvider({ 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 });
1659
+ 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 });
1264
1660
  const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1265
1661
  await p.generate({ workerId: "r", messages: [], attributions: ["@acme/x@1.2.0", "@foo/y@0.3.1"], client: "plurnk.nvim/1.4.0" });
1266
1662
  assert.equal(headerVal(calls[0].init, "Plurnk-Attribution"), '["@acme/x@1.2.0","@foo/y@0.3.1"]');
1267
1663
  assert.equal(headerVal(calls[0].init, "Plurnk-Client"), "plurnk.nvim/1.4.0");
1268
1664
  });
1269
1665
 
1666
+ test("Plurnk-Call-Kind carries the caller's emission or bare output contract under the first-party gate", async () => {
1667
+ 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 });
1668
+ const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1669
+ await p.generate({ workerId: "emission", messages: [], callKind: "emission" });
1670
+ await p.generate({ workerId: "bare", messages: [], callKind: "bare" });
1671
+ assert.equal(headerVal(calls[0].init, "Plurnk-Call-Kind"), "emission");
1672
+ assert.equal(headerVal(calls[1].init, "Plurnk-Call-Kind"), "bare");
1673
+ });
1674
+
1675
+ test("generate rejects an unknown call kind before provider I/O", async () => {
1676
+ 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 });
1677
+ const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1678
+ await assert.rejects(
1679
+ p.generate({ workerId: "invalid", messages: [], callKind: "unknown" as never }),
1680
+ /unsupported callKind "unknown"/,
1681
+ );
1682
+ assert.equal(calls.length, 0);
1683
+ });
1684
+
1270
1685
  test("Plurnk-Worker-Primary: the lineage root rides under the gate; emitted even when it equals workerId", async () => {
1271
- const p = new AiSdkProvider({ 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 });
1686
+ 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 });
1272
1687
  let calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1273
1688
  await p.generate({ workerId: "w-child", primaryWorkerId: "w-root", messages: [] });
1274
1689
  assert.equal(headerVal(calls[0].init, "Plurnk-Worker-Primary"), "w-root"); // a descendant: Primary != Worker-Id
@@ -1287,22 +1702,23 @@ test("Plurnk-Worker-Primary: the lineage root rides under the gate; emitted even
1287
1702
  });
1288
1703
 
1289
1704
  test("Plurnk-Worker-Primary is structurally dropped when firstPartyMetadata is off", async () => {
1290
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
1705
+ 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 });
1291
1706
  const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1292
1707
  await p.generate({ workerId: "w-child", primaryWorkerId: "w-root", messages: [] });
1293
1708
  assert.equal(headerVal(calls[0].init, "Plurnk-Worker-Primary"), undefined); // never reaches a third-party backend
1294
1709
  });
1295
1710
 
1296
1711
  test("firstPartyMetadata off (default): the headers are structurally dropped even when values are passed", async () => {
1297
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
1712
+ 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 });
1298
1713
  const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1299
- await p.generate({ workerId: "r", messages: [], attributions: ["@acme/x@1.2.0"], client: "plurnk-cli/2.0.0" });
1714
+ await p.generate({ workerId: "r", messages: [], attributions: ["@acme/x@1.2.0"], client: "plurnk-cli/2.0.0", callKind: "bare" });
1300
1715
  assert.equal(headerVal(calls[0].init, "Plurnk-Attribution"), undefined); // never leaks to a non-first-party backend
1301
1716
  assert.equal(headerVal(calls[0].init, "Plurnk-Client"), undefined);
1717
+ assert.equal(headerVal(calls[0].init, "Plurnk-Call-Kind"), undefined);
1302
1718
  });
1303
1719
 
1304
1720
  test("firstPartyMetadata on but empty values: no header emitted", async () => {
1305
- const p = new AiSdkProvider({ 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 });
1721
+ 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 });
1306
1722
  const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1307
1723
  await p.generate({ workerId: "r", messages: [], attributions: [], client: "" });
1308
1724
  assert.equal(headerVal(calls[0].init, "Plurnk-Attribution"), undefined);
@@ -1310,7 +1726,7 @@ test("firstPartyMetadata on but empty values: no header emitted", async () => {
1310
1726
  });
1311
1727
 
1312
1728
  test("grammar transport: no grammar passed sends no grammar field, but the penalty rides", async () => {
1313
- const p = new AiSdkProvider({ 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" });
1729
+ 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" });
1314
1730
  const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1315
1731
  await p.generate({ workerId: "r", messages: [] });
1316
1732
  const body = JSON.parse(calls[0].init.body as string);
@@ -1318,10 +1734,10 @@ test("grammar transport: no grammar passed sends no grammar field, but the penal
1318
1734
  assert.equal(body.repeat_penalty, 1.15); // penalty is not grammar-gated
1319
1735
  });
1320
1736
 
1321
- test("maxTokens transports as max_tokens; absent → no wire field (server default)", async () => {
1322
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
1737
+ test("maxOutputTokens tightens the total output budget; absent leaves an unconfigured provider uncapped", async () => {
1738
+ 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 });
1323
1739
  let calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1324
- await p.generate({ workerId: "r", messages: [], maxTokens: 2048 });
1740
+ await p.generate({ workerId: "r", messages: [], maxOutputTokens: 2048 });
1325
1741
  assert.equal(JSON.parse(calls[0].init.body as string).max_tokens, 2048);
1326
1742
 
1327
1743
  mock.restoreAll();
@@ -1330,8 +1746,54 @@ test("maxTokens transports as max_tokens; absent → no wire field (server defau
1330
1746
  assert.equal("max_tokens" in JSON.parse(calls[0].init.body as string), false);
1331
1747
  });
1332
1748
 
1749
+ test("{§provider-output-budget-conformance} reported output beyond the total budget fails once with complete evidence", async () => {
1750
+ const p = testProvider({
1751
+ model: "m",
1752
+ url: "http://x/v1/chat/completions",
1753
+ fetchTimeoutMs: 5000,
1754
+ temperature: 0.2,
1755
+ repeatPenalty: 1.15,
1756
+ reasoning: { mode: "adaptive", budget: null },
1757
+ retryAttempts: 2,
1758
+ });
1759
+ const calls = installFetch([
1760
+ { choices: [{ delta: { reasoning_content: "consider" } }] },
1761
+ { choices: [{ delta: { content: "x" }, finish_reason: "stop" }] },
1762
+ {
1763
+ choices: [],
1764
+ usage: {
1765
+ prompt_tokens: 5,
1766
+ completion_tokens: 17,
1767
+ total_tokens: 22,
1768
+ completion_tokens_details: { reasoning_tokens: 16 },
1769
+ },
1770
+ },
1771
+ ]);
1772
+
1773
+ await assert.rejects(
1774
+ p.generate({ workerId: "r", messages: [], maxOutputTokens: 16 }),
1775
+ (error: unknown) => {
1776
+ assert.ok(error instanceof ProviderError);
1777
+ assert.equal(error.kind, "invalid_response");
1778
+ assert.equal(error.status, 502);
1779
+ assert.equal(error.problem.stage, "provider-response");
1780
+ assert.equal(error.problem.retryable, false);
1781
+ assert.equal(error.problem.outputBudget, 16);
1782
+ assert.equal(error.problem.reportedOutputTokens, 17);
1783
+ assert.equal(error.attempt?.assistant.content, "x");
1784
+ assert.equal(error.attempt?.assistant.reasoning, "consider");
1785
+ assert.equal(error.attempt?.assistant.finishReason, "stop");
1786
+ assert.equal(error.attempt?.capacity.outputBudget, 16);
1787
+ assert.equal(error.accounting[0]?.outcome, "response");
1788
+ assert.equal(error.accounting[0]?.usage?.outputTokens, 17);
1789
+ return true;
1790
+ },
1791
+ );
1792
+ assert.equal(calls.length, 1, "a paid provider contract violation is never replayed");
1793
+ });
1794
+
1333
1795
  test("slot affinity is internal: sticky per workerId, distinct workers spread across slots", async () => {
1334
- const pinning = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, supportsSlotPinning: true, slotCount: 2 });
1796
+ const pinning = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, supportsSlotPinning: true, slotCount: 2 });
1335
1797
  const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1336
1798
  await pinning.generate({ workerId: "run-A", messages: [] });
1337
1799
  await pinning.generate({ workerId: "run-B", messages: [] });
@@ -1342,20 +1804,20 @@ test("slot affinity is internal: sticky per workerId, distinct workers spread ac
1342
1804
  });
1343
1805
 
1344
1806
  test("slot affinity: no pinning backend or unknown slotCount → no id_slot ever", async () => {
1345
- const cloud = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 }); // default: no pinning
1807
+ const cloud = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 }); // default: no pinning
1346
1808
  let calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1347
1809
  await cloud.generate({ workerId: "run-A", messages: [] });
1348
1810
  assert.equal("id_slot" in JSON.parse(calls[0].init.body as string), false);
1349
1811
 
1350
1812
  mock.restoreAll();
1351
- const noCount = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, supportsSlotPinning: true }); // slotCount null
1813
+ const noCount = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, supportsSlotPinning: true }); // slotCount null
1352
1814
  calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1353
1815
  await noCount.generate({ workerId: "run-A", messages: [] });
1354
1816
  assert.equal("id_slot" in JSON.parse(calls[0].init.body as string), false);
1355
1817
  });
1356
1818
 
1357
1819
  test("slot affinity: a worker past the LRU window (slotCount*8) loses its pin; recent workers stay sticky", async () => {
1358
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, supportsSlotPinning: true, slotCount: 2 });
1820
+ 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, supportsSlotPinning: true, slotCount: 2 });
1359
1821
  const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1360
1822
  const slotOf = (i: number) => JSON.parse(calls[i].init.body as string).id_slot;
1361
1823
  for (let i = 0; i < 16; i++) await p.generate({ workerId: `r${i}`, messages: [] }); // fills the 16-entry window {r0..r15}
@@ -1369,7 +1831,7 @@ test("slot affinity: a worker past the LRU window (slotCount*8) loses its pin; r
1369
1831
 
1370
1832
  test("streaming:false: a non-ok response rejects as a classified ProviderError (covers the non-streamed transport)", async () => {
1371
1833
  const { ProviderError } = await import("./errors.ts");
1372
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, streaming: false, source: "provider:test" });
1834
+ 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, streaming: false, source: "provider:test" });
1373
1835
  mock.method(globalThis, "fetch", async () => new Response("boom", { status: 500 }));
1374
1836
  await assert.rejects(() => p.generate({ workerId: "r", messages: [] }), (err: unknown) => {
1375
1837
  assert.ok(err instanceof ProviderError, `expected ProviderError, got ${String(err)}`);
@@ -1380,14 +1842,14 @@ test("streaming:false: a non-ok response rejects as a classified ProviderError (
1380
1842
  });
1381
1843
 
1382
1844
  test("generate fail-hards on a missing or empty workerId", async () => {
1383
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
1845
+ 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 });
1384
1846
  installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1385
1847
  await assert.rejects(() => p.generate({ workerId: "", messages: [] }), /workerId is required/);
1386
1848
  await assert.rejects(() => (p.generate as (a: object) => Promise<unknown>)({ messages: [] }), /workerId is required/);
1387
1849
  });
1388
1850
 
1389
1851
  test("messages pass through verbatim — the provider injects no turn (PLAN lives in the grammar, never a provider prefill)", async () => {
1390
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
1852
+ 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 });
1391
1853
  const calls = installFetch([{ choices: [{ delta: { content: "out" } }] }]);
1392
1854
  const input = [{ role: "user" as const, content: "hi" }];
1393
1855
  const res = await p.generate({ workerId: "r", messages: input });
@@ -1397,7 +1859,7 @@ test("messages pass through verbatim — the provider injects no turn (PLAN live
1397
1859
 
1398
1860
  test("generate wraps an HTTP failure as a ProviderError carrying Problem Details", async () => {
1399
1861
  const { ProviderError } = await import("./errors.ts");
1400
- const p = new AiSdkProvider({ 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" });
1862
+ 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" });
1401
1863
  mock.method(globalThis, "fetch", async () => new Response("rate limited", { status: 429 }));
1402
1864
  await assert.rejects(() => p.generate({ workerId: "r", messages: [] }), (err: unknown) => {
1403
1865
  assert.ok(err instanceof ProviderError, `expected ProviderError, got ${String(err)}`);
@@ -1411,14 +1873,14 @@ test("generate wraps an HTTP failure as a ProviderError carrying Problem Details
1411
1873
  });
1412
1874
 
1413
1875
  test("generate rejects on a pre-aborted external signal", async () => {
1414
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
1876
+ 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 });
1415
1877
  installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1416
1878
  const signal = AbortSignal.abort(new Error("nope"));
1417
1879
  await assert.rejects(() => p.generate({ workerId: "r", messages: [], signal }));
1418
1880
  });
1419
1881
 
1420
1882
  test("configured headers and url are sent verbatim", async () => {
1421
- const p = new AiSdkProvider({
1883
+ const p = testProvider({
1422
1884
  model: "m", url: "http://host/custom/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0,
1423
1885
  headers: { Authorization: "Bearer secret", "X-Title": "plurnk" },
1424
1886
  });
@@ -1445,14 +1907,16 @@ const stalledStreamResponse = (): Response => new Response(new ReadableStream({
1445
1907
 
1446
1908
  test("retry: a transient failure retries and a later success resolves", async () => {
1447
1909
  const calls = installFetchScript([
1910
+ { status: 408, retryAfter: 0 },
1911
+ { status: 409, retryAfter: 0 },
1448
1912
  { status: 429, retryAfter: 0 },
1449
1913
  { status: 503, retryAfter: 0 },
1450
1914
  { status: 200, chunks: [{ choices: [{ delta: { content: "ok" } }] }] },
1451
1915
  ]);
1452
- const p = new AiSdkProvider({ ...retryCfg, retryAttempts: 3 });
1916
+ const p = testProvider({ ...retryCfg, retryAttempts: 4 });
1453
1917
  const res = await p.generate({ workerId: "r", messages: [] });
1454
1918
  assert.equal(res.assistant.content, "ok");
1455
- assert.equal(calls.length, 3); // 429 → 503 → 200
1919
+ assert.equal(calls.length, 5); // 408 → 409 → 429 → 503 → 200
1456
1920
  });
1457
1921
 
1458
1922
  test("streamed-body silence retries and returns the retry's complete output", async () => {
@@ -1469,7 +1933,7 @@ test("streamed-body silence retries and returns the retry's complete output", as
1469
1933
  },
1470
1934
  }), { status: 200 });
1471
1935
  });
1472
- const p = new AiSdkProvider({
1936
+ const p = testProvider({
1473
1937
  model: "m",
1474
1938
  url: "http://x/v1/chat/completions",
1475
1939
  fetchTimeoutMs: 5000,
@@ -1492,7 +1956,7 @@ test("streamed-body silence does not replay when retries are disabled", async ()
1492
1956
  calls++;
1493
1957
  return stalledStreamResponse();
1494
1958
  });
1495
- const p = new AiSdkProvider({
1959
+ const p = testProvider({
1496
1960
  model: "m",
1497
1961
  url: "http://x/v1/chat/completions",
1498
1962
  fetchTimeoutMs: 1000,
@@ -1506,7 +1970,8 @@ test("streamed-body silence does not replay when retries are disabled", async ()
1506
1970
  await assert.rejects(
1507
1971
  p.generate({ workerId: "r", messages: [] }),
1508
1972
  (error: ProviderError) => error.kind === "network_failure"
1509
- && /chunk timeout/i.test(error.message),
1973
+ && error.problem.timeoutPhase === "stream_idle"
1974
+ && error.problem.timeoutMs === 10,
1510
1975
  );
1511
1976
  assert.equal(calls, 1, "zero retries permits exactly one provider request");
1512
1977
  mock.restoreAll();
@@ -1518,7 +1983,7 @@ test("streamed-body silence exhausts the configured retry budget once", async ()
1518
1983
  calls++;
1519
1984
  return stalledStreamResponse();
1520
1985
  });
1521
- const p = new AiSdkProvider({
1986
+ const p = testProvider({
1522
1987
  model: "m",
1523
1988
  url: "http://x/v1/chat/completions",
1524
1989
  fetchTimeoutMs: 5000,
@@ -1540,6 +2005,116 @@ test("streamed-body silence exhausts the configured retry budget once", async ()
1540
2005
  mock.restoreAll();
1541
2006
  });
1542
2007
 
2008
+ test("an attempt timeout retries within the larger operation deadline and settles every physical request", async () => {
2009
+ let calls = 0;
2010
+ mock.method(globalThis, "fetch", async (_input: string | URL | Request, init?: RequestInit) => {
2011
+ calls++;
2012
+ if (calls > 1) {
2013
+ return new Response(sseStream([
2014
+ { choices: [{ delta: { content: "recovered" }, finish_reason: "stop" }] },
2015
+ ]), { status: 200 });
2016
+ }
2017
+ return await new Promise<Response>((_resolve, reject) => {
2018
+ const signal = init?.signal;
2019
+ signal?.addEventListener("abort", () => reject(signal.reason), { once: true });
2020
+ });
2021
+ });
2022
+ const connectivity = { operationTimeoutMs: 5_000 };
2023
+ const settled: Array<{ outcome: string }> = [];
2024
+ const p = testProvider({
2025
+ model: "m",
2026
+ url: "http://x/v1/chat/completions",
2027
+ fetchTimeoutMs: 10,
2028
+ streamIdleTimeoutMs: 0,
2029
+ temperature: 0.2,
2030
+ repeatPenalty: 1.15,
2031
+ reasoning: { mode: "off", budget: null },
2032
+ retryAttempts: 1,
2033
+ source: "provider:test",
2034
+ ...connectivity,
2035
+ });
2036
+ const result = await p.generate({
2037
+ workerId: "r",
2038
+ messages: [],
2039
+ observeRequest: async () => async (accounting) => { settled.push(accounting); },
2040
+ });
2041
+ assert.equal(result.assistant.content, "recovered");
2042
+ assert.equal(calls, 2);
2043
+ assert.deepEqual(settled.map(({ outcome }) => outcome), ["error", "response"]);
2044
+ assert.deepEqual(result.accounting.map(({ outcome }) => outcome), ["error", "response"]);
2045
+ mock.restoreAll();
2046
+ });
2047
+
2048
+ test("first-content silence retries independently of the stream-idle deadline", async () => {
2049
+ let calls = 0;
2050
+ mock.method(globalThis, "fetch", async () => {
2051
+ calls++;
2052
+ if (calls > 1) {
2053
+ return new Response(sseStream([
2054
+ { choices: [{ delta: { content: "recovered" }, finish_reason: "stop" }] },
2055
+ ]), { status: 200 });
2056
+ }
2057
+ return new Response(new ReadableStream({
2058
+ start(controller) {
2059
+ setTimeout(() => controller.close(), 100);
2060
+ },
2061
+ }), { status: 200 });
2062
+ });
2063
+ const connectivity = { operationTimeoutMs: 5_000, firstContentTimeoutMs: 10 };
2064
+ const p = testProvider({
2065
+ model: "m",
2066
+ url: "http://x/v1/chat/completions",
2067
+ fetchTimeoutMs: 5_000,
2068
+ streamIdleTimeoutMs: 0,
2069
+ temperature: 0.2,
2070
+ repeatPenalty: 1.15,
2071
+ reasoning: { mode: "off", budget: null },
2072
+ retryAttempts: 1,
2073
+ source: "provider:test",
2074
+ ...connectivity,
2075
+ });
2076
+ const result = await p.generate({ workerId: "r", messages: [] });
2077
+ assert.equal(result.assistant.content, "recovered");
2078
+ assert.equal(calls, 2);
2079
+ mock.restoreAll();
2080
+ });
2081
+
2082
+ test("operation-deadline exhaustion is a distinct non-retryable failure", async () => {
2083
+ let calls = 0;
2084
+ mock.method(globalThis, "fetch", async (_input: string | URL | Request, init?: RequestInit) => {
2085
+ calls++;
2086
+ return await new Promise<Response>((_resolve, reject) => {
2087
+ const signal = init?.signal;
2088
+ signal?.addEventListener("abort", () => reject(signal.reason), { once: true });
2089
+ });
2090
+ });
2091
+ const connectivity = { operationTimeoutMs: 10 };
2092
+ const p = testProvider({
2093
+ model: "m",
2094
+ url: "http://x/v1/chat/completions",
2095
+ fetchTimeoutMs: 50,
2096
+ streamIdleTimeoutMs: 0,
2097
+ temperature: 0.2,
2098
+ repeatPenalty: 1.15,
2099
+ reasoning: { mode: "off", budget: null },
2100
+ retryAttempts: 3,
2101
+ source: "provider:test",
2102
+ ...connectivity,
2103
+ });
2104
+ await assert.rejects(
2105
+ p.generate({ workerId: "r", messages: [] }),
2106
+ (error: ProviderError) => error.kind === "deadline_exceeded"
2107
+ && error.status === 504
2108
+ && error.problem.retryable === false
2109
+ && error.problem.timeoutPhase === "operation"
2110
+ && error.problem.timeoutMs === 10
2111
+ && error.accounting.length === 1
2112
+ && error.accounting[0]?.outcome === "error",
2113
+ );
2114
+ assert.equal(calls, 1);
2115
+ mock.restoreAll();
2116
+ });
2117
+
1543
2118
  test("the total generation deadline spans stalled-stream retry scheduling", async () => {
1544
2119
  let calls = 0;
1545
2120
  mock.method(globalThis, "fetch", async () => {
@@ -1556,10 +2131,11 @@ test("the total generation deadline spans stalled-stream retry scheduling", asyn
1556
2131
  }
1557
2132
  return stalledStreamResponse();
1558
2133
  });
1559
- const p = new AiSdkProvider({
2134
+ const p = testProvider({
1560
2135
  model: "m",
1561
2136
  url: "http://x/v1/chat/completions",
1562
- fetchTimeoutMs: 50,
2137
+ fetchTimeoutMs: 5000,
2138
+ operationTimeoutMs: 50,
1563
2139
  streamIdleTimeoutMs: 10,
1564
2140
  temperature: 0.2,
1565
2141
  repeatPenalty: 1.15,
@@ -1570,7 +2146,8 @@ test("the total generation deadline spans stalled-stream retry scheduling", asyn
1570
2146
  const started = Date.now();
1571
2147
  await assert.rejects(
1572
2148
  p.generate({ workerId: "r", messages: [] }),
1573
- (error: ProviderError) => error.kind === "network_failure",
2149
+ (error: ProviderError) => error.kind === "deadline_exceeded"
2150
+ && error.problem.timeoutPhase === "operation",
1574
2151
  );
1575
2152
  assert.ok(Date.now() - started < 500, "the configured total deadline ends retry scheduling");
1576
2153
  assert.equal(calls, 1, "the total deadline expires before another request begins");
@@ -1586,7 +2163,7 @@ test("a zero stream-idle timeout permits a slow inter-chunk pause", async () =>
1586
2163
  controller.close();
1587
2164
  },
1588
2165
  }), { status: 200 }));
1589
- const p = new AiSdkProvider({
2166
+ const p = testProvider({
1590
2167
  model: "m",
1591
2168
  url: "http://x/v1/chat/completions",
1592
2169
  fetchTimeoutMs: 1000,
@@ -1604,7 +2181,7 @@ test("a zero stream-idle timeout permits a slow inter-chunk pause", async () =>
1604
2181
  test("retry: exhausting the budget surfaces the classified ProviderError", async () => {
1605
2182
  const { ProviderError } = await import("./errors.ts");
1606
2183
  const calls = installFetchScript([{ status: 429, retryAfter: 0 }]); // always rate-limited
1607
- const p = new AiSdkProvider({ ...retryCfg, retryAttempts: 2 });
2184
+ const p = testProvider({ ...retryCfg, retryAttempts: 2 });
1608
2185
  await assert.rejects(
1609
2186
  () => p.generate({ workerId: "r", messages: [] }),
1610
2187
  (err: unknown) => { assert.ok(err instanceof ProviderError); assert.equal(err.kind, "rate_limit"); return true; },
@@ -1617,7 +2194,7 @@ test("retry: a Retry-After HTTP-date is honored — a past date parses to a 0ms
1617
2194
  { status: 503, retryAfter: "Wed, 21 Oct 2015 07:28:00 GMT" }, // date form, in the past → max(0, past−now) = 0
1618
2195
  { status: 200, chunks: [{ choices: [{ delta: { content: "ok" }, finish_reason: "stop" }] }] },
1619
2196
  ]);
1620
- const p = new AiSdkProvider({ ...retryCfg, retryAttempts: 1 });
2197
+ const p = testProvider({ ...retryCfg, retryAttempts: 1 });
1621
2198
  const { assistant } = await p.generate({ workerId: "r", messages: [] });
1622
2199
  assert.equal(assistant.content, "ok");
1623
2200
  assert.equal(calls.length, 2); // initial 503 + one retry, no real wall-clock wait
@@ -1625,14 +2202,14 @@ test("retry: a Retry-After HTTP-date is honored — a past date parses to a 0ms
1625
2202
 
1626
2203
  test("retry: a terminal error (401 unauthorized) is never retried", async () => {
1627
2204
  const calls = installFetchScript([{ status: 401 }]);
1628
- const p = new AiSdkProvider({ ...retryCfg, retryAttempts: 5 });
2205
+ const p = testProvider({ ...retryCfg, retryAttempts: 5 });
1629
2206
  await assert.rejects(() => p.generate({ workerId: "r", messages: [] }), /401/);
1630
2207
  assert.equal(calls.length, 1); // terminal — no retry despite budget
1631
2208
  });
1632
2209
 
1633
2210
  test("retry: retryAttempts 0 surfaces the first transient failure immediately", async () => {
1634
2211
  const calls = installFetchScript([{ status: 503, retryAfter: 0 }]);
1635
- const p = new AiSdkProvider({ ...retryCfg, retryAttempts: 0 });
2212
+ const p = testProvider({ ...retryCfg, retryAttempts: 0 });
1636
2213
  await assert.rejects(() => p.generate({ workerId: "r", messages: [] }));
1637
2214
  assert.equal(calls.length, 1); // no retry budget
1638
2215
  });
@@ -1640,7 +2217,7 @@ test("retry: retryAttempts 0 surfaces the first transient failure immediately",
1640
2217
  test("retry: a caller abort during backoff rejects promptly with no further attempt", async () => {
1641
2218
  const ac = new AbortController();
1642
2219
  const calls = installFetchScript([{ status: 503, retryAfter: 5 }]); // 5s backoff we never wait out
1643
- const p = new AiSdkProvider({ ...retryCfg, retryAttempts: 3 });
2220
+ const p = testProvider({ ...retryCfg, retryAttempts: 3 });
1644
2221
  const promise = p.generate({ workerId: "r", messages: [], signal: ac.signal });
1645
2222
  await flush(); // attempt 0 fails, enters the backoff sleep
1646
2223
  assert.equal(calls.length, 1);
@@ -1651,23 +2228,29 @@ test("retry: a caller abort during backoff rejects promptly with no further atte
1651
2228
 
1652
2229
  // — Anthropic reasoning style (wire `thinking` parameter) —
1653
2230
 
1654
- test("reasoningStyle 'anthropic' maps the budget to the thinking param", async () => {
2231
+ test("reasoningStyle 'anthropic' maps the configured reasoning subset to the thinking parameter", async () => {
1655
2232
  // N>0 → enabled with budget_tokens
1656
- const capped = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, retryAttempts: 0, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "on", budget: 4096 }, reasoningStyle: "anthropic" });
2233
+ const capped = testProvider({ model: "m", url: "http://x/v1/chat/completions", outputBudget: 8192, reasoningBudget: 4096, fetchTimeoutMs: 5000, retryAttempts: 0, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "on", budget: 4096 }, reasoningStyle: "anthropic" });
1657
2234
  let calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1658
2235
  await capped.generate({ workerId: "r", messages: [] });
1659
2236
  assert.deepEqual(JSON.parse(calls[0].init.body as string).thinking, { type: "enabled", budget_tokens: 4096 });
1660
2237
 
2238
+ mock.restoreAll();
2239
+ const unbudgeted = testProvider({ model: "m", url: "http://x/v1/chat/completions", contextWindow: 8192, outputBudget: 4096, reasoningBudget: 2048, fetchTimeoutMs: 5000, retryAttempts: 0, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "on", budget: 2048 }, reasoningStyle: "anthropic" });
2240
+ calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
2241
+ await unbudgeted.generate({ workerId: "r", messages: [] });
2242
+ assert.deepEqual(JSON.parse(calls[0].init.body as string).thinking, { type: "enabled", budget_tokens: 2048 });
2243
+
1661
2244
  mock.restoreAll();
1662
2245
  // 0 → explicit disabled
1663
- const off = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, retryAttempts: 0, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, reasoningStyle: "anthropic" });
2246
+ const off = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, retryAttempts: 0, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, reasoningStyle: "anthropic" });
1664
2247
  calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1665
2248
  await off.generate({ workerId: "r", messages: [] });
1666
2249
  assert.deepEqual(JSON.parse(calls[0].init.body as string).thinking, { type: "disabled" });
1667
2250
 
1668
2251
  mock.restoreAll();
1669
2252
  // -1 adaptive → omit (API default depth)
1670
- const adaptive = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, retryAttempts: 0, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "adaptive", budget: null }, reasoningStyle: "anthropic" });
2253
+ const adaptive = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, retryAttempts: 0, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "adaptive", budget: null }, reasoningStyle: "anthropic" });
1671
2254
  calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1672
2255
  await adaptive.generate({ workerId: "r", messages: [] });
1673
2256
  assert.equal("thinking" in JSON.parse(calls[0].init.body as string), false);
@@ -1685,14 +2268,14 @@ test("streaming:false posts without stream and parses the single JSON response",
1685
2268
  usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 },
1686
2269
  }), { status: 200, headers: { "Content-Type": "application/json" } });
1687
2270
  });
1688
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, streaming: false });
2271
+ 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, streaming: false });
1689
2272
  const res = await p.generate({ workerId: "r", messages: [] });
1690
2273
  const sent = JSON.parse(calls[0].body);
1691
2274
  assert.equal("stream" in sent, false); // no streaming flag
1692
2275
  assert.equal(res.assistant.content, "hello"); // content from message.content
1693
2276
  assert.equal(res.assistant.reasoning, "because"); // reasoning_content mapped
1694
2277
  assert.equal(res.assistant.finishReason, "stop");
1695
- assert.equal(res.assistant.usage.total, 4);
2278
+ assert.equal(res.accounting[0]?.usage?.totalTokens, 4);
1696
2279
  mock.restoreAll();
1697
2280
  });
1698
2281
 
@@ -1701,7 +2284,7 @@ const captureBase = { model: "m", url: "http://x/v1/chat/completions", fetchTime
1701
2284
 
1702
2285
  test("logprobs OFF by default: no wire request, no assistant.logprobs, no rawBody", async () => {
1703
2286
  const calls = installFetch([{ model: "m", choices: [{ delta: { content: "hi" }, finish_reason: "stop" }], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } }]);
1704
- const p = new AiSdkProvider({ ...captureBase });
2287
+ const p = testProvider({ ...captureBase });
1705
2288
  const res = await p.generate({ workerId: "r", messages: [{ role: "user", content: "q" }] });
1706
2289
  const body = JSON.parse((calls[0].init.body as string));
1707
2290
  assert.equal("logprobs" in body, false);
@@ -1718,7 +2301,7 @@ test("logprobs ON (streamed): requests logprobs+top_logprobs, surfaces raw logpr
1718
2301
  { token: "no", logprob: -0.1, sampling_logprob: -0.1, top_logprobs: [{ token: "no", logprob: -0.1 }] },
1719
2302
  ] } }] };
1720
2303
  const calls = installFetch([chunk]);
1721
- const p = new AiSdkProvider({ ...captureBase, topLogprobs: 2 });
2304
+ const p = testProvider({ ...captureBase, topLogprobs: 2 });
1722
2305
  const res = await p.generate({ workerId: "r", messages: [{ role: "user", content: "q" }] });
1723
2306
  const body = JSON.parse((calls[0].init.body as string));
1724
2307
  assert.equal(body.logprobs, true);
@@ -1732,7 +2315,7 @@ test("logprobs ON (streamed): requests logprobs+top_logprobs, surfaces raw logpr
1732
2315
  test("rawBody ON (non-streamed): verbatim wire body incl. sampling_logprob preserved", async () => {
1733
2316
  const wire = { model: "m", extra_top_level: "kept", choices: [{ message: { content: "no" }, finish_reason: "stop", logprobs: { content: [{ token: "no", logprob: -0.1, sampling_logprob: -0.1, token_id: 42 }] } }], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } };
1734
2317
  installFetchJson(wire);
1735
- const p = new AiSdkProvider({ ...captureBase, streaming: false, topLogprobs: 0, rawBody: true });
2318
+ const p = testProvider({ ...captureBase, streaming: false, topLogprobs: 0, rawBody: true });
1736
2319
  const res = await p.generate({ workerId: "r", messages: [{ role: "user", content: "q" }] });
1737
2320
  assert.deepEqual(res.rawBody, wire); // verbatim
1738
2321
  assert.equal((res.rawBody as typeof wire).choices[0].logprobs.content[0].sampling_logprob, -0.1);
@@ -1743,7 +2326,7 @@ test("rawBody ON (non-streamed): verbatim wire body incl. sampling_logprob prese
1743
2326
 
1744
2327
  test("caller sampling cannot forge logprobs (reserved keys): the env flag is the only control", async () => {
1745
2328
  const calls = installFetch([{ model: "m", choices: [{ delta: { content: "hi" }, finish_reason: "stop" }], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } }]);
1746
- const p = new AiSdkProvider({ ...captureBase }); // logprobs OFF
2329
+ const p = testProvider({ ...captureBase }); // logprobs OFF
1747
2330
  await p.generate({ workerId: "r", messages: [{ role: "user", content: "q" }], sampling: { logprobs: true, top_logprobs: 5 } });
1748
2331
  const body = JSON.parse((calls[0].init.body as string));
1749
2332
  assert.equal("logprobs" in body, false); // sampling passthrough stripped it
@@ -1754,7 +2337,7 @@ test("caller sampling cannot forge logprobs (reserved keys): the env flag is the
1754
2337
  // — turn coordinate headers ({§lifecycle-terms}): same gate as every first-party signal —
1755
2338
 
1756
2339
  test("workspaceId/loop/turn ride as Plurnk-Workspace-Id/Loop/Turn under the first-party gate", async () => {
1757
- const p = new AiSdkProvider({ 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 });
2340
+ 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 });
1758
2341
  const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1759
2342
  await p.generate({ workerId: "r", messages: [], workspaceId: "s-9", loop: 3, turn: 41 });
1760
2343
  const headers = new Headers(calls[0].init.headers);
@@ -1764,7 +2347,7 @@ test("workspaceId/loop/turn ride as Plurnk-Workspace-Id/Loop/Turn under the firs
1764
2347
  });
1765
2348
 
1766
2349
  test("third-party providers structurally DROP the coordinate (gate off by default)", async () => {
1767
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
2350
+ 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 });
1768
2351
  const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1769
2352
  await p.generate({ workerId: "r", messages: [], workspaceId: "s-9", loop: 3, turn: 41 });
1770
2353
  const headers = new Headers(calls[0].init.headers);
@@ -1774,7 +2357,7 @@ test("third-party providers structurally DROP the coordinate (gate off by defaul
1774
2357
  });
1775
2358
 
1776
2359
  test("coordinates are 1-based — 0/absent/empty emit no header", async () => {
1777
- const p = new AiSdkProvider({ 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 });
2360
+ 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 });
1778
2361
  const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1779
2362
  await p.generate({ workerId: "r", messages: [], workspaceId: "", loop: 0, turn: 0 });
1780
2363
  const headers = new Headers(calls[0].init.headers);
@@ -1786,20 +2369,26 @@ test("coordinates are 1-based — 0/absent/empty emit no header", async () => {
1786
2369
 
1787
2370
  // -- {§provider-generation-envelope} --
1788
2371
 
1789
- test("reserves derive from the detected window; absolutes stand alone; null window + percent = no claim", () => {
2372
+ test("the adapter exposes independent model limits and the resolved generation envelope", () => {
1790
2373
  const base = { model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 1000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null } as const, retryAttempts: 0 };
1791
- const derived = new AiSdkProvider({ ...base, contextWindow: 49152, reasoningReserve: { percent: 0.1 }, completionReserve: { percent: 0.25 } });
1792
- assert.equal(derived.reasoningReserve, 4915); // jennifer/turboderp: 10% of 49152
1793
- assert.equal(derived.completionReserve, 12288); // 25% of 49152
1794
- const pinned = new AiSdkProvider({ ...base, contextWindow: null, reasoningReserve: { tokens: 4096 }, completionReserve: { percent: 0.25 } });
1795
- assert.equal(pinned.reasoningReserve, 4096); // absolute pin needs no window
1796
- assert.equal(pinned.completionReserve, null); // percent without a window = underivable
1797
- const legacy = new AiSdkProvider({ ...base, contextWindow: 49152 });
1798
- assert.equal(legacy.reasoningReserve, null); // out-of-date sibling: no claim
2374
+ const bounded = testProvider({
2375
+ ...base,
2376
+ contextWindow: 49_152,
2377
+ maxInputTokens: 40_000,
2378
+ maxOutputTokens: 16_384,
2379
+ outputBudget: 12_288,
2380
+ });
2381
+ assert.equal(bounded.contextWindow, 49_152);
2382
+ assert.equal(bounded.maxInputTokens, 40_000);
2383
+ assert.equal(bounded.maxOutputTokens, 16_384);
2384
+ assert.equal(bounded.outputBudget, 12_288);
2385
+ assert.equal(bounded.reasoningBudget, null);
2386
+ const unknown = testProvider({ ...base, contextWindow: null });
2387
+ assert.equal(unknown.outputBudget, null);
1799
2388
  });
1800
2389
 
1801
2390
  test("router-owned tuning: tuningFloors:false drops the temperature/penalty floors, caller sampling still rides", async () => {
1802
- const p = new AiSdkProvider({ 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 });
2391
+ 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 });
1803
2392
  const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1804
2393
  await p.generate({ workerId: "r", messages: [], sampling: { temperature: 0.9 } });
1805
2394
  const body = JSON.parse(calls[0].init.body as string);
@@ -1807,25 +2396,194 @@ test("router-owned tuning: tuningFloors:false drops the temperature/penalty floo
1807
2396
  assert.equal("frequency_penalty" in body, false); // the floor is suppressed; the router owns tuning
1808
2397
  });
1809
2398
 
1810
- // -- prompt-cache affinity (workerId -> prompt_cache_key) --
2399
+ // -- {§provider-cache-affinity} / {§provider-cache-write-policy} --
1811
2400
 
1812
- test("promptCacheKey on: body sends prompt_cache_key = workerId (serverless replica affinity)", async () => {
1813
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, promptCacheKey: true });
2401
+ test("a compatible route's declared body affinity is managed by workerId", async () => {
2402
+ const p = testProvider({
2403
+ model: "m",
2404
+ url: "http://x/v1/chat/completions",
2405
+ fetchTimeoutMs: 5000,
2406
+ temperature: 0.2,
2407
+ repeatPenalty: 1.15,
2408
+ reasoning: { mode: "off", budget: null },
2409
+ retryAttempts: 0,
2410
+ cacheAffinity: { target: "body", name: "prompt_cache_key" },
2411
+ });
1814
2412
  const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1815
- await p.generate({ workerId: "worker-abc", messages: [] });
2413
+ await p.generate({ workerId: "worker-abc", messages: [], sampling: { prompt_cache_key: "hijack" } });
1816
2414
  assert.equal(JSON.parse(calls[0].init.body as string).prompt_cache_key, "worker-abc");
1817
2415
  });
1818
2416
 
1819
- test("promptCacheKey off (default): no prompt_cache_key on the wire", async () => {
1820
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
2417
+ test("an undeclared compatible route receives no guessed cache field", async () => {
2418
+ 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 });
1821
2419
  const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1822
2420
  await p.generate({ workerId: "worker-abc", messages: [] });
1823
2421
  assert.equal("prompt_cache_key" in JSON.parse(calls[0].init.body as string), false);
1824
2422
  });
1825
2423
 
1826
- test("prompt_cache_key is managed: caller sampling cannot forge/override the affinity key", async () => {
1827
- const p = new AiSdkProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, promptCacheKey: true });
2424
+ test("a compatible route's declared header affinity composes with static headers", async () => {
2425
+ const p = testProvider({
2426
+ model: "m",
2427
+ url: "http://x/v1/chat/completions",
2428
+ headers: { Authorization: "Bearer key" },
2429
+ fetchTimeoutMs: 5000,
2430
+ temperature: 0.2,
2431
+ repeatPenalty: 1.15,
2432
+ reasoning: { mode: "off", budget: null },
2433
+ retryAttempts: 0,
2434
+ cacheAffinity: { target: "header", name: "x-grok-conv-id" },
2435
+ });
1828
2436
  const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
1829
- await p.generate({ workerId: "worker-abc", messages: [], sampling: { prompt_cache_key: "hijack" } });
1830
- assert.equal(JSON.parse(calls[0].init.body as string).prompt_cache_key, "worker-abc"); // managed wins
2437
+ await p.generate({ workerId: "worker-abc", messages: [] });
2438
+ const headers = new Headers(calls[0].init.headers);
2439
+ assert.equal(headers.get("authorization"), "Bearer key");
2440
+ assert.equal(headers.get("x-grok-conv-id"), "worker-abc");
2441
+ });
2442
+
2443
+ test("native request projections compose reasoning visibility, affinity, and system cache control", async () => {
2444
+ let request: Record<string, unknown> | undefined;
2445
+ const usage = {
2446
+ inputTokens: { total: 2, noCache: 2, cacheRead: 0, cacheWrite: 0 },
2447
+ outputTokens: { total: 1, text: 1, reasoning: 0 },
2448
+ };
2449
+ const languageModel = {
2450
+ specificationVersion: "v4",
2451
+ provider: "native.test",
2452
+ modelId: "native-cache",
2453
+ supportedUrls: {},
2454
+ doGenerate: async (options: Record<string, unknown>) => {
2455
+ request = options;
2456
+ return {
2457
+ content: [{ type: "text", text: "ok" }],
2458
+ finishReason: { unified: "stop", raw: "stop" },
2459
+ usage,
2460
+ response: { id: "response", modelId: "native-cache" },
2461
+ warnings: [],
2462
+ };
2463
+ },
2464
+ doStream: async () => { throw new Error("streaming is not under test"); },
2465
+ } as unknown as LanguageModel;
2466
+ const p = testProvider({
2467
+ model: "native-cache",
2468
+ languageModel,
2469
+ fetchTimeoutMs: 5000,
2470
+ temperature: 0.2,
2471
+ repeatPenalty: 1.15,
2472
+ reasoning: { mode: "adaptive", budget: null },
2473
+ retryAttempts: 0,
2474
+ streaming: false,
2475
+ cacheAffinity: { target: "provider-option", provider: "openai", name: "promptCacheKey" },
2476
+ reasoningResponseProviderOptions: {
2477
+ google: { thinkingConfig: { includeThoughts: true } },
2478
+ },
2479
+ systemCacheProviderOptions: {
2480
+ anthropic: { cacheControl: { type: "ephemeral" } },
2481
+ },
2482
+ });
2483
+ await p.generate({
2484
+ workerId: "worker-native",
2485
+ messages: [
2486
+ { role: "system", content: "stable definition" },
2487
+ { role: "system", content: "stable policy" },
2488
+ { role: "user", content: "changing packet" },
2489
+ ],
2490
+ });
2491
+
2492
+ assert.deepEqual(request?.providerOptions, {
2493
+ google: { thinkingConfig: { includeThoughts: true } },
2494
+ openai: { promptCacheKey: "worker-native" },
2495
+ });
2496
+ assert.deepEqual(request?.prompt, [
2497
+ { role: "system", content: "stable definition", providerOptions: undefined },
2498
+ {
2499
+ role: "system",
2500
+ content: "stable policy",
2501
+ providerOptions: { anthropic: { cacheControl: { type: "ephemeral" } } },
2502
+ },
2503
+ { role: "user", content: [{ type: "text", text: "changing packet" }], providerOptions: undefined },
2504
+ ]);
2505
+ });
2506
+
2507
+ test("native AI SDK reasoning turns on without an operator token budget", async () => {
2508
+ let request: Record<string, unknown> | undefined;
2509
+ const languageModel = {
2510
+ specificationVersion: "v4",
2511
+ provider: "native.test",
2512
+ modelId: "native-reasoning",
2513
+ supportedUrls: {},
2514
+ doGenerate: async (options: Record<string, unknown>) => {
2515
+ request = options;
2516
+ return {
2517
+ content: [{ type: "reasoning", text: "consider" }, { type: "text", text: "ok" }],
2518
+ finishReason: { unified: "stop", raw: "stop" },
2519
+ usage: {
2520
+ inputTokens: { total: 2, noCache: 2, cacheRead: 0, cacheWrite: 0 },
2521
+ outputTokens: { total: 2, text: 1, reasoning: 1 },
2522
+ },
2523
+ response: { id: "response", modelId: "native-reasoning" },
2524
+ warnings: [],
2525
+ };
2526
+ },
2527
+ doStream: async () => { throw new Error("streaming is not under test"); },
2528
+ } as unknown as LanguageModel;
2529
+ const p = testProvider({
2530
+ model: "native-reasoning",
2531
+ languageModel,
2532
+ fetchTimeoutMs: 5000,
2533
+ temperature: 0.2,
2534
+ repeatPenalty: 1.15,
2535
+ reasoning: { mode: "on", budget: null },
2536
+ retryAttempts: 0,
2537
+ streaming: false,
2538
+ });
2539
+ const response = await p.generate({ workerId: "worker-native", messages: [{ role: "user", content: "hello" }] });
2540
+
2541
+ assert.equal(request?.reasoning, "medium");
2542
+ assert.equal(response.assistant.reasoning, "consider");
2543
+ });
2544
+
2545
+ test("native additive-reasoning adapters preserve one total output budget", async () => {
2546
+ for (const [provider, expected] of [
2547
+ ["anthropic", { anthropic: { thinking: { type: "enabled", budgetTokens: 1499 } } }],
2548
+ ["bedrock", { bedrock: { reasoningConfig: { type: "enabled", budgetTokens: 1499 } } }],
2549
+ ] as const) {
2550
+ let request: Record<string, unknown> | undefined;
2551
+ const languageModel = {
2552
+ specificationVersion: "v4",
2553
+ provider: `native.${provider}`,
2554
+ modelId: "native-additive",
2555
+ supportedUrls: {},
2556
+ doGenerate: async (options: Record<string, unknown>) => {
2557
+ request = options;
2558
+ return {
2559
+ content: [{ type: "text", text: "ok" }],
2560
+ finishReason: { unified: "stop", raw: "stop" },
2561
+ usage: {
2562
+ inputTokens: { total: 2, noCache: 2, cacheRead: 0, cacheWrite: 0 },
2563
+ outputTokens: { total: 1, text: 1, reasoning: 0 },
2564
+ },
2565
+ response: { id: "response", modelId: "native-additive" },
2566
+ warnings: [],
2567
+ };
2568
+ },
2569
+ doStream: async () => { throw new Error("streaming is not under test"); },
2570
+ } as unknown as LanguageModel;
2571
+ const p = testProvider({
2572
+ model: "native-additive",
2573
+ languageModel,
2574
+ additiveReasoningProvider: provider,
2575
+ outputBudget: 8192,
2576
+ reasoningBudget: 2048,
2577
+ fetchTimeoutMs: 5000,
2578
+ temperature: 0.2,
2579
+ repeatPenalty: 1.15,
2580
+ reasoning: { mode: "on", budget: 2048 },
2581
+ retryAttempts: 0,
2582
+ streaming: false,
2583
+ });
2584
+ await p.generate({ workerId: `worker-${provider}`, messages: [], maxOutputTokens: 1500 });
2585
+ assert.equal(request?.maxOutputTokens, 1);
2586
+ assert.equal(request?.reasoning, "provider-default");
2587
+ assert.deepEqual(request?.providerOptions, expected);
2588
+ }
1831
2589
  });