@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
@@ -0,0 +1,140 @@
1
+ import type {
2
+ PromptTokenMeasurement,
3
+ ProviderRequestCapacity,
4
+ } from "./types.ts";
5
+ import { assertPromptTokenMeasurement } from "./promptTokens.ts";
6
+
7
+ const positiveOrNull = (value: number | null, name: string): number | null => {
8
+ if (value === null) return null;
9
+ if (!Number.isSafeInteger(value) || value <= 0) {
10
+ throw new TypeError(`${name} must be a positive safe integer or null`);
11
+ }
12
+ return value;
13
+ };
14
+
15
+ export const effectiveOutputBudget = ({
16
+ requested,
17
+ configured,
18
+ maxOutputTokens,
19
+ contextWindow,
20
+ }: {
21
+ requested?: number;
22
+ configured: number | null;
23
+ maxOutputTokens: number | null;
24
+ contextWindow: number | null;
25
+ }): number | null => {
26
+ if (requested !== undefined && (!Number.isSafeInteger(requested) || requested <= 0)) {
27
+ throw new TypeError("maxOutputTokens must be a positive safe integer");
28
+ }
29
+ const policies = [requested ?? null, configured].filter((value): value is number => value !== null);
30
+ if (policies.length === 0) return null;
31
+ const physical = [
32
+ positiveOrNull(maxOutputTokens, "maxOutputTokens"),
33
+ positiveOrNull(contextWindow, "contextWindow"),
34
+ ].filter((value): value is number => value !== null);
35
+ return Math.min(...policies, ...physical);
36
+ };
37
+
38
+ export const effectiveReasoningBudget = ({
39
+ configured,
40
+ outputBudget,
41
+ }: {
42
+ configured: number | null;
43
+ outputBudget: number | null;
44
+ }): number | null => {
45
+ positiveOrNull(configured, "reasoningBudget");
46
+ positiveOrNull(outputBudget, "outputBudget");
47
+ if (configured === null) return null;
48
+ if (outputBudget === null) {
49
+ throw new TypeError("a reasoning budget requires a resolved total output budget");
50
+ }
51
+ if (outputBudget < 2) {
52
+ throw new TypeError("maxOutputTokens must leave at least one token outside the reasoning budget");
53
+ }
54
+ return Math.min(configured, outputBudget - 1);
55
+ };
56
+
57
+ export const effectiveInputCapacity = ({
58
+ contextWindow,
59
+ maxInputTokens,
60
+ outputBudget,
61
+ }: {
62
+ contextWindow: number | null;
63
+ maxInputTokens: number | null;
64
+ outputBudget: number | null;
65
+ }): number | null => {
66
+ positiveOrNull(contextWindow, "contextWindow");
67
+ positiveOrNull(maxInputTokens, "maxInputTokens");
68
+ positiveOrNull(outputBudget, "outputBudget");
69
+ const combinedCapacity = contextWindow !== null && outputBudget !== null
70
+ ? contextWindow - outputBudget
71
+ : null;
72
+ if (combinedCapacity !== null && combinedCapacity <= 0) {
73
+ throw new TypeError(
74
+ `outputBudget (${outputBudget}) must leave positive input capacity inside contextWindow (${contextWindow})`,
75
+ );
76
+ }
77
+ const capacities = [
78
+ maxInputTokens,
79
+ combinedCapacity,
80
+ ].filter((value): value is number => value !== null);
81
+ return capacities.length === 0 ? null : Math.min(...capacities);
82
+ };
83
+
84
+ export const requestCapacityDecision = (
85
+ inputCapacity: number | null,
86
+ measurement: PromptTokenMeasurement,
87
+ ): ProviderRequestCapacity["decision"] => {
88
+ const prompt = assertPromptTokenMeasurement(measurement, "provider capacity");
89
+ // An upper bound can prove fit when it is below the limit, but exceeding
90
+ // the limit proves nothing about the unknown exact count. Estimates never
91
+ // authorize or reject; the provider remains the capacity oracle.
92
+ return inputCapacity === null
93
+ || prompt.kind === "estimate"
94
+ || prompt.kind === "unavailable"
95
+ ? "defer"
96
+ : prompt.tokens <= inputCapacity
97
+ ? "admit"
98
+ : prompt.kind === "exact"
99
+ ? "reject"
100
+ : "defer";
101
+ };
102
+
103
+ export const assessRequestCapacity = ({
104
+ contextWindow,
105
+ maxInputTokens,
106
+ maxOutputTokens,
107
+ outputBudget,
108
+ reasoningBudget,
109
+ measurement,
110
+ }: {
111
+ contextWindow: number | null;
112
+ maxInputTokens: number | null;
113
+ maxOutputTokens: number | null;
114
+ outputBudget: number | null;
115
+ reasoningBudget: number | null;
116
+ measurement: PromptTokenMeasurement;
117
+ }): ProviderRequestCapacity => {
118
+ positiveOrNull(contextWindow, "contextWindow");
119
+ positiveOrNull(maxInputTokens, "maxInputTokens");
120
+ positiveOrNull(maxOutputTokens, "maxOutputTokens");
121
+ positiveOrNull(outputBudget, "outputBudget");
122
+ positiveOrNull(reasoningBudget, "reasoningBudget");
123
+ if (reasoningBudget !== null
124
+ && (outputBudget === null || reasoningBudget >= outputBudget)) {
125
+ throw new TypeError("reasoningBudget must be a strict subset of outputBudget");
126
+ }
127
+ const prompt = assertPromptTokenMeasurement(measurement, "provider capacity");
128
+ const inputCapacity = effectiveInputCapacity({ contextWindow, maxInputTokens, outputBudget });
129
+
130
+ return {
131
+ decision: requestCapacityDecision(inputCapacity, prompt),
132
+ contextWindow,
133
+ maxInputTokens,
134
+ maxOutputTokens,
135
+ outputBudget,
136
+ reasoningBudget,
137
+ inputCapacity,
138
+ prompt,
139
+ };
140
+ };
@@ -1,23 +1,26 @@
1
1
  import test, { mock } from "node:test";
2
2
  import { strict as assert } from "node:assert";
3
+ import { once } from "node:events";
4
+ import { createServer } from "node:http";
3
5
  import { catalogProviderFromEnv } from "./catalogProvider.ts";
4
- import { providerCostFor } from "./cost.ts";
5
6
  import { resetEmittedWarnings } from "./warnings.ts";
6
7
 
7
8
  const env = {
8
9
  OPENAI_API_KEY: "test-key",
9
10
  OPENAI_BASE_URL: "https://api.openai.com/v1",
10
11
  PLURNK_PROVIDERS_FETCH_TIMEOUT: "1000",
12
+ PLURNK_PROVIDERS_OPERATION_TIMEOUT: "3000",
13
+ PLURNK_PROVIDERS_FIRST_CONTENT_TIMEOUT: "1000",
11
14
  PLURNK_PROVIDERS_STREAM_IDLE_TIMEOUT: "0",
12
15
  PLURNK_PROVIDERS_REASONING: "off",
13
16
  PLURNK_PROVIDERS_TEMPERATURE: "0.2",
14
17
  PLURNK_PROVIDERS_REPEAT_PENALTY: "1.15",
15
18
  PLURNK_PROVIDERS_FREQUENCY_PENALTY: "0",
16
- PLURNK_PROVIDERS_REASONING_RESERVE: "10%",
17
- PLURNK_PROVIDERS_COMPLETION_RESERVE: "25%",
19
+ PLURNK_PROVIDERS_OUTPUT_BUDGET: "35%",
18
20
  PLURNK_PROVIDERS_RETRY_ATTEMPTS: "0",
19
21
  PLURNK_PROVIDERS_ERROR_DETAIL_LIMIT: "512",
20
- PLURNK_PROVIDERS_PROMPT_CACHE_KEY: "1",
22
+ PLURNK_PROVIDERS_CACHE_AFFINITY: "1",
23
+ PLURNK_PROVIDERS_CACHE_WRITE_POLICY: "stable-system",
21
24
  };
22
25
 
23
26
  test.afterEach(() => {
@@ -30,25 +33,20 @@ test("catalog provider resolves model physics and Models.dev USD rates", () => {
30
33
  assert.notEqual(provider, null);
31
34
  assert.equal(provider?.model, "gpt-4.1-mini");
32
35
  assert.equal(provider?.contextWindow, 1_047_576);
33
- assert.equal(provider?.reasoningReserve, 16_384);
34
- assert.equal(provider?.completionReserve, 32_768);
35
- assert.ok((provider?.calculateCost({
36
- prompt: 1_000_000,
37
- completion: 1_000_000,
38
- reasoning: 0,
39
- cached: 0,
40
- total: 2_000_000,
41
- }) ?? 0) > 0);
36
+ assert.equal(provider?.maxInputTokens, null);
37
+ assert.equal(provider?.maxOutputTokens, 32_768);
38
+ assert.equal(provider?.outputBudget, 32_768);
39
+ assert.equal(provider?.reasoningBudget, null);
42
40
  });
43
41
 
44
- test("an operator context window caps catalog physics and percentage reserves derive from the cap", () => {
42
+ test("an operator context window caps catalog physics and percentage output policy", () => {
45
43
  const provider = catalogProviderFromEnv("openai", {
46
44
  ...env,
47
45
  PLURNK_PROVIDERS_CONTEXT_WINDOW: "128000",
48
46
  }, "gpt-4.1-mini");
49
47
  assert.equal(provider?.contextWindow, 128_000);
50
- assert.equal(provider?.reasoningReserve, 16_000);
51
- assert.equal(provider?.completionReserve, 32_000);
48
+ assert.equal(provider?.outputBudget, 32_768, "the model output maximum remains the tighter cap");
49
+ assert.equal(provider?.reasoningBudget, null);
52
50
 
53
51
  const oversized = catalogProviderFromEnv("openai", {
54
52
  ...env,
@@ -92,12 +90,16 @@ test("official AI SDK provider owns the native request while PLURNK owns call se
92
90
  const result = await provider?.generate({
93
91
  workerId: "worker",
94
92
  messages: [{ role: "user", content: "hello" }],
95
- maxTokens: 64,
93
+ maxOutputTokens: 64,
96
94
  sampling: { top_p: 0.8, seed: 7 },
97
95
  });
98
96
 
99
97
  assert.equal(result?.assistant.content, "done");
100
- assert.equal(result?.assistant.usage.total, 3);
98
+ assert.equal(result?.accounting[0]?.usage?.totalTokens, 3);
99
+ assert.deepEqual(result?.accounting[0]?.cost, {
100
+ kind: "unknown",
101
+ reason: "the provider response omitted a token category with a distinct Models.dev rate",
102
+ });
101
103
  assert.equal(calls.length, 1);
102
104
  assert.equal(calls[0]?.url, "https://api.openai.com/v1/chat/completions");
103
105
  assert.equal(calls[0]?.body.model, "gpt-4.1-mini");
@@ -105,6 +107,299 @@ test("official AI SDK provider owns the native request while PLURNK owns call se
105
107
  assert.equal(calls[0]?.body.top_p, 0.8);
106
108
  assert.equal(calls[0]?.body.seed, 7);
107
109
  assert.equal(calls[0]?.body.max_tokens, 64);
110
+ assert.equal(calls[0]?.body.prompt_cache_key, "worker", "the official OpenAI SDK projects the documented affinity key");
111
+ });
112
+
113
+ test("xAI's native chat contract caps the complete reasoning response", async () => {
114
+ let call: { headers: Headers; body: Record<string, unknown> } | undefined;
115
+ mock.method(globalThis, "fetch", async (_input: string | URL | Request, init?: RequestInit) => {
116
+ call = {
117
+ headers: new Headers(init?.headers),
118
+ body: JSON.parse(String(init?.body)) as Record<string, unknown>,
119
+ };
120
+ return new Response([
121
+ `data: ${JSON.stringify({
122
+ id: "response-xai",
123
+ object: "chat.completion.chunk",
124
+ created: 1,
125
+ model: "grok-build-0.1",
126
+ choices: [{ index: 0, delta: { reasoning_content: "consider" }, finish_reason: null }],
127
+ })}`,
128
+ `data: ${JSON.stringify({
129
+ id: "response-xai",
130
+ object: "chat.completion.chunk",
131
+ created: 2,
132
+ model: "grok-build-0.1",
133
+ choices: [{ index: 0, delta: { content: "OK" }, finish_reason: "stop" }],
134
+ })}`,
135
+ `data: ${JSON.stringify({
136
+ id: "response-xai",
137
+ object: "chat.completion.chunk",
138
+ created: 3,
139
+ model: "grok-build-0.1",
140
+ choices: [],
141
+ usage: {
142
+ prompt_tokens: 5,
143
+ completion_tokens: 4,
144
+ total_tokens: 9,
145
+ prompt_tokens_details: { cached_tokens: 2 },
146
+ completion_tokens_details: { reasoning_tokens: 3 },
147
+ cost_in_usd_ticks: 1_230_000,
148
+ },
149
+ })}`,
150
+ "data: [DONE]",
151
+ ].join("\n\n"), { headers: { "content-type": "text/event-stream" } });
152
+ });
153
+
154
+ const provider = catalogProviderFromEnv("xai", {
155
+ ...env,
156
+ XAI_API_KEY: "test-key",
157
+ PLURNK_PROVIDERS_REASONING: "adaptive",
158
+ }, "grok-build-0.1");
159
+ const result = await provider?.generate({
160
+ workerId: "xai-worker",
161
+ messages: [{ role: "user", content: "hello" }],
162
+ maxOutputTokens: 16,
163
+ });
164
+
165
+ assert.equal(call?.body.max_completion_tokens, 16);
166
+ assert.equal("max_tokens" in (call?.body ?? {}), false);
167
+ assert.equal(call?.headers.get("x-grok-conv-id"), "xai-worker");
168
+ assert.equal(result?.assistant.reasoning, "consider");
169
+ assert.equal(result?.assistant.content, "OK");
170
+ assert.deepEqual(result?.accounting[0]?.usage, {
171
+ inputTokens: 5,
172
+ outputTokens: 4,
173
+ totalTokens: 9,
174
+ inputTokenDetails: { cacheReadTokens: 2 },
175
+ outputTokenDetails: { textTokens: 1, reasoningTokens: 3 },
176
+ });
177
+ assert.deepEqual(result?.accounting[0]?.cost, {
178
+ kind: "charged",
179
+ amount: { amount: "1230000", currency: "USDTICK" },
180
+ usdEquivalent: "0.000123",
181
+ source: "xAI response usage.cost_in_usd_ticks",
182
+ });
183
+ });
184
+
185
+ test("Cerebras explicit reasoning activation needs no operator effort or token budget", async () => {
186
+ let body: Record<string, unknown> | undefined;
187
+ mock.method(globalThis, "fetch", async (_input: string | URL | Request, init?: RequestInit) => {
188
+ body = JSON.parse(String(init?.body)) as Record<string, unknown>;
189
+ return new Response([
190
+ `data: ${JSON.stringify({
191
+ id: "chatcmpl-cerebras",
192
+ object: "chat.completion.chunk",
193
+ created: 1,
194
+ model: "gemma-4-31b",
195
+ choices: [{ index: 0, delta: { reasoning: "consider" }, finish_reason: null }],
196
+ })}`,
197
+ `data: ${JSON.stringify({
198
+ id: "chatcmpl-cerebras",
199
+ object: "chat.completion.chunk",
200
+ created: 2,
201
+ model: "gemma-4-31b",
202
+ choices: [{ index: 0, delta: { content: "done" }, finish_reason: "stop" }],
203
+ })}`,
204
+ `data: ${JSON.stringify({
205
+ id: "chatcmpl-cerebras",
206
+ object: "chat.completion.chunk",
207
+ created: 3,
208
+ model: "gemma-4-31b",
209
+ choices: [],
210
+ usage: {
211
+ prompt_tokens: 2,
212
+ completion_tokens: 2,
213
+ total_tokens: 4,
214
+ completion_tokens_details: { reasoning_tokens: 1 },
215
+ },
216
+ })}`,
217
+ "data: [DONE]",
218
+ ].join("\n\n"), { headers: { "content-type": "text/event-stream" } });
219
+ });
220
+
221
+ const provider = catalogProviderFromEnv("cerebras", {
222
+ ...env,
223
+ CEREBRAS_API_KEY: "test-key",
224
+ PLURNK_PROVIDERS_REASONING: "on",
225
+ }, "gemma-4-31b");
226
+ const result = await provider?.generate({
227
+ workerId: "worker",
228
+ messages: [{ role: "user", content: "hello" }],
229
+ });
230
+
231
+ assert.equal(body?.reasoning_effort, "medium", "the native SDK projects unqualified on to its enabled posture");
232
+ assert.equal("thinking_budget_tokens" in (body ?? {}), false, "activation does not invent a token budget");
233
+ assert.equal(result?.assistant.reasoning, "consider");
234
+ assert.equal(result?.accounting[0]?.usage?.outputTokenDetails?.reasoningTokens, 1);
235
+ });
236
+
237
+ test("Google adaptive reasoning requests and preserves readable thought summaries", async () => {
238
+ const bodies: Array<{
239
+ generationConfig?: { thinkingConfig?: { includeThoughts?: boolean } };
240
+ }> = [];
241
+ mock.method(globalThis, "fetch", async (_input: string | URL | Request, init?: RequestInit) => {
242
+ bodies.push(JSON.parse(String(init?.body)) as typeof bodies[number]);
243
+ return new Response(`data: ${JSON.stringify({
244
+ responseId: "response-gemini",
245
+ candidates: [{
246
+ content: {
247
+ role: "model",
248
+ parts: [
249
+ { text: "consider", thought: true },
250
+ { text: "done" },
251
+ ],
252
+ },
253
+ finishReason: "STOP",
254
+ }],
255
+ usageMetadata: {
256
+ promptTokenCount: 2,
257
+ candidatesTokenCount: 1,
258
+ thoughtsTokenCount: 1,
259
+ totalTokenCount: 4,
260
+ },
261
+ })}\n\n`, {
262
+ headers: { "content-type": "text/event-stream" },
263
+ });
264
+ });
265
+
266
+ const provider = catalogProviderFromEnv("google", {
267
+ ...env,
268
+ GEMINI_API_KEY: "test-key",
269
+ PLURNK_PROVIDERS_REASONING: "adaptive",
270
+ }, "gemini-3.7-flash");
271
+ const result = await provider?.generate({
272
+ workerId: "worker",
273
+ messages: [{ role: "user", content: "hello" }],
274
+ });
275
+
276
+ assert.deepEqual(bodies[0]?.generationConfig?.thinkingConfig, {
277
+ includeThoughts: true,
278
+ }, "adaptive leaves thinking depth to Google while requesting its readable summary");
279
+ assert.equal(result?.assistant.reasoning, "consider");
280
+ assert.equal(result?.assistant.content, "done");
281
+ assert.equal(result?.accounting[0]?.usage?.outputTokenDetails?.reasoningTokens, 1);
282
+
283
+ const disabled = catalogProviderFromEnv("google", {
284
+ ...env,
285
+ GEMINI_API_KEY: "test-key",
286
+ PLURNK_PROVIDERS_REASONING: "off",
287
+ }, "gemini-3.7-flash");
288
+ await disabled?.generate({
289
+ workerId: "worker",
290
+ messages: [{ role: "user", content: "hello" }],
291
+ });
292
+ assert.equal(
293
+ bodies[1]?.generationConfig?.thinkingConfig?.includeThoughts,
294
+ undefined,
295
+ "off does not request readable thoughts",
296
+ );
297
+ });
298
+
299
+ test("native provider routes project their documented cache controls through the actual SDK request", async (t) => {
300
+ const calls: Array<{ url: string; headers: Headers; body: Record<string, unknown> }> = [];
301
+ mock.method(globalThis, "fetch", async (input: string | URL | Request, init?: RequestInit) => {
302
+ calls.push({
303
+ url: String(input),
304
+ headers: new Headers(init?.headers),
305
+ body: JSON.parse(String(init?.body)) as Record<string, unknown>,
306
+ });
307
+ return new Response([
308
+ `data: ${JSON.stringify({
309
+ id: "response",
310
+ object: "chat.completion.chunk",
311
+ created: 1,
312
+ model: "served",
313
+ choices: [{ index: 0, delta: { content: "ok" }, finish_reason: "stop" }],
314
+ })}`,
315
+ `data: ${JSON.stringify({
316
+ id: "response",
317
+ object: "chat.completion.chunk",
318
+ created: 2,
319
+ model: "served",
320
+ choices: [],
321
+ usage: { prompt_tokens: 2, completion_tokens: 1, total_tokens: 3 },
322
+ })}`,
323
+ "data: [DONE]",
324
+ ].join("\n\n"), { headers: { "content-type": "text/event-stream" } });
325
+ });
326
+
327
+ await t.test("DeepInfra native options become prompt_cache_key", async () => {
328
+ const provider = catalogProviderFromEnv("deepinfra", {
329
+ ...env,
330
+ DEEPINFRA_API_KEY: "test-key",
331
+ }, "zai-org/GLM-5.2");
332
+ await provider?.generate({
333
+ workerId: "deepinfra-worker",
334
+ messages: [{ role: "user", content: "hello" }],
335
+ });
336
+ assert.equal(calls.at(-1)?.body.prompt_cache_key, "deepinfra-worker");
337
+ });
338
+
339
+ await t.test("OpenRouter carries session affinity and an Anthropic system breakpoint", async () => {
340
+ let call: { headers: Headers; body: Record<string, unknown> } | undefined;
341
+ const server = createServer(async (request, response) => {
342
+ const chunks: Buffer[] = [];
343
+ for await (const chunk of request) chunks.push(Buffer.from(chunk));
344
+ call = {
345
+ headers: new Headers(request.headers as Record<string, string>),
346
+ body: JSON.parse(Buffer.concat(chunks).toString("utf8")) as Record<string, unknown>,
347
+ };
348
+ response.writeHead(200, { "content-type": "text/event-stream" });
349
+ response.end([
350
+ `data: ${JSON.stringify({
351
+ id: "response",
352
+ object: "chat.completion.chunk",
353
+ created: 1,
354
+ model: "served",
355
+ choices: [{ index: 0, delta: { content: "ok" }, finish_reason: "stop" }],
356
+ })}`,
357
+ `data: ${JSON.stringify({
358
+ id: "response",
359
+ object: "chat.completion.chunk",
360
+ created: 2,
361
+ model: "served",
362
+ choices: [],
363
+ usage: { prompt_tokens: 2, completion_tokens: 1, total_tokens: 3 },
364
+ })}`,
365
+ "data: [DONE]",
366
+ ].join("\n\n"));
367
+ });
368
+ server.listen(0, "127.0.0.1");
369
+ await once(server, "listening");
370
+ t.after(() => new Promise<void>((resolve, reject) => {
371
+ server.close((error) => error === undefined ? resolve() : reject(error));
372
+ }));
373
+ const address = server.address();
374
+ if (address === null || typeof address === "string") {
375
+ throw new Error("OpenRouter request-capture server did not bind a TCP address");
376
+ }
377
+
378
+ const provider = catalogProviderFromEnv("openrouter", {
379
+ ...env,
380
+ OPENROUTER_API_KEY: "test-key",
381
+ }, "anthropic/claude-sonnet-4.6", `http://127.0.0.1:${address.port}/api/v1`);
382
+ await provider?.generate({
383
+ workerId: "openrouter-worker",
384
+ messages: [
385
+ { role: "system", content: "stable system packet" },
386
+ { role: "user", content: "changing user packet" },
387
+ ],
388
+ });
389
+ assert.equal(call?.headers.get("x-session-id"), "openrouter-worker");
390
+ assert.deepEqual((call?.body.messages as unknown[] | undefined)?.[0], {
391
+ role: "system",
392
+ content: [{
393
+ type: "text",
394
+ text: "stable system packet",
395
+ cache_control: { type: "ephemeral" },
396
+ }],
397
+ });
398
+ assert.deepEqual((call?.body.messages as unknown[] | undefined)?.[1], {
399
+ role: "user",
400
+ content: "changing user packet",
401
+ });
402
+ });
108
403
  });
109
404
 
110
405
  test("cataloged unknown model fails unless its context is explicit", () => {
@@ -120,22 +415,35 @@ test("cataloged unknown model fails unless its context is explicit", () => {
120
415
  assert.equal(provider?.contextWindow, 8192);
121
416
  });
122
417
 
123
- test("Models.dev is the only fallback rate table", () => {
124
- const usage = {
125
- prompt: 1_000,
126
- cached: 400,
127
- completion: 100,
128
- reasoning: 50,
129
- total: 1_150,
130
- };
418
+ test("Models.dev is the only fallback rate table", async () => {
419
+ mock.method(globalThis, "fetch", async () => new Response([
420
+ `data: ${JSON.stringify({
421
+ id: "response",
422
+ model: "served",
423
+ choices: [{ index: 0, delta: { content: "ok" }, finish_reason: "stop" }],
424
+ })}`,
425
+ `data: ${JSON.stringify({
426
+ id: "response",
427
+ model: "served",
428
+ choices: [],
429
+ usage: {
430
+ prompt_tokens: 1_000,
431
+ prompt_tokens_details: { cached_tokens: 400 },
432
+ completion_tokens: 100,
433
+ total_tokens: 1_150,
434
+ },
435
+ })}`,
436
+ "data: [DONE]",
437
+ ].join("\n\n"), { headers: { "content-type": "text/event-stream" } }));
131
438
  const cataloged = catalogProviderFromEnv("deepseek", {
132
439
  ...env,
133
440
  DEEPSEEK_API_KEY: "test-key",
134
441
  }, "deepseek-v4-flash");
135
442
  assert.notEqual(cataloged, null);
136
- assert.deepEqual(providerCostFor(cataloged!, usage), {
443
+ const catalogedResponse = await cataloged!.generate({ workerId: "cataloged", messages: [] });
444
+ assert.deepEqual(catalogedResponse.accounting[0]?.cost, {
137
445
  kind: "estimated",
138
- usd: "0.00012712",
446
+ amount: { amount: "0.00012712", currency: "USD" },
139
447
  source: "Models.dev catalog rates",
140
448
  });
141
449
 
@@ -144,8 +452,9 @@ test("Models.dev is the only fallback rate table", () => {
144
452
  XAI_API_KEY: "test-key",
145
453
  PLURNK_PROVIDERS_CONTEXT_WINDOW: "8192",
146
454
  }, "not-in-the-catalog");
147
- assert.deepEqual(providerCostFor(uncataloged!, usage), {
455
+ const uncatalogedResponse = await uncataloged!.generate({ workerId: "uncataloged", messages: [] });
456
+ assert.deepEqual(uncatalogedResponse.accounting[0]?.cost, {
148
457
  kind: "unknown",
149
- reason: "the response reported no cost and Models.dev has no rate for this model",
458
+ reason: "Models.dev has no complete rate for this model",
150
459
  });
151
460
  });