@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.
- package/.env.defaults +41 -34
- package/README.md +15 -0
- package/SPEC.md +242 -89
- package/dist/AiSdkProvider.d.ts +33 -33
- package/dist/AiSdkProvider.d.ts.map +1 -1
- package/dist/AiSdkProvider.js +442 -133
- package/dist/AiSdkProvider.js.map +1 -1
- package/dist/Mock.d.ts +10 -11
- package/dist/Mock.d.ts.map +1 -1
- package/dist/Mock.js +87 -25
- package/dist/Mock.js.map +1 -1
- package/dist/Pool.d.ts +9 -24
- package/dist/Pool.d.ts.map +1 -1
- package/dist/Pool.js +86 -25
- package/dist/Pool.js.map +1 -1
- package/dist/accounting.d.ts +5 -2
- package/dist/accounting.d.ts.map +1 -1
- package/dist/accounting.js +100 -16
- package/dist/accounting.js.map +1 -1
- package/dist/accountingPublic.d.ts +5 -0
- package/dist/accountingPublic.d.ts.map +1 -0
- package/dist/accountingPublic.js +3 -0
- package/dist/accountingPublic.js.map +1 -0
- package/dist/aiSdkTransport.d.ts +9 -2
- package/dist/aiSdkTransport.d.ts.map +1 -1
- package/dist/aiSdkTransport.js +160 -62
- package/dist/aiSdkTransport.js.map +1 -1
- package/dist/capacity.d.ts +26 -0
- package/dist/capacity.d.ts.map +1 -0
- package/dist/capacity.js +90 -0
- package/dist/capacity.js.map +1 -0
- package/dist/catalogProvider.d.ts +8 -3
- package/dist/catalogProvider.d.ts.map +1 -1
- package/dist/catalogProvider.js +45 -41
- package/dist/catalogProvider.js.map +1 -1
- package/dist/compatibleProvider.d.ts.map +1 -1
- package/dist/compatibleProvider.js +26 -12
- package/dist/compatibleProvider.js.map +1 -1
- package/dist/cost.d.ts +10 -10
- package/dist/cost.d.ts.map +1 -1
- package/dist/cost.js +90 -42
- package/dist/cost.js.map +1 -1
- package/dist/env.d.ts +13 -11
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +83 -46
- package/dist/env.js.map +1 -1
- package/dist/errors.d.ts +17 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +91 -8
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/ollama.js +3 -3
- package/dist/ollama.js.map +1 -1
- package/dist/promptTokens.d.ts.map +1 -1
- package/dist/promptTokens.js +7 -4
- package/dist/promptTokens.js.map +1 -1
- package/dist/sdkModels.d.ts +7 -2
- package/dist/sdkModels.d.ts.map +1 -1
- package/dist/sdkModels.js +43 -13
- package/dist/sdkModels.js.map +1 -1
- package/dist/types.d.ts +55 -33
- package/dist/types.d.ts.map +1 -1
- package/dist/usage.d.ts +22 -5
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +169 -83
- package/dist/usage.js.map +1 -1
- package/package.json +18 -7
- package/src/AiSdkProvider.test.ts +964 -206
- package/src/AiSdkProvider.ts +545 -155
- package/src/Mock.test.ts +69 -30
- package/src/Mock.ts +99 -29
- package/src/Pool.test.ts +90 -19
- package/src/Pool.ts +96 -27
- package/src/ProviderRegistry.test.ts +16 -11
- package/src/accounting.test.ts +58 -22
- package/src/accounting.ts +119 -18
- package/src/accountingPublic.ts +9 -0
- package/src/aiSdkTransport.test.ts +42 -49
- package/src/aiSdkTransport.ts +174 -62
- package/src/boundaries.test.ts +2 -0
- package/src/capacity.test.ts +92 -0
- package/src/capacity.ts +140 -0
- package/src/catalogProvider.test.ts +339 -30
- package/src/catalogProvider.ts +65 -47
- package/src/compatibleProvider.test.ts +7 -5
- package/src/compatibleProvider.ts +29 -13
- package/src/cost.test.ts +86 -36
- package/src/cost.ts +111 -50
- package/src/defaults.test.ts +13 -3
- package/src/env.test.ts +103 -25
- package/src/env.ts +153 -65
- package/src/errors.test.ts +80 -2
- package/src/errors.ts +107 -8
- package/src/index.ts +26 -7
- package/src/ollama.test.ts +5 -3
- package/src/ollama.ts +3 -3
- package/src/promptTokens.ts +8 -5
- package/src/sdkModels.test.ts +77 -8
- package/src/sdkModels.ts +51 -15
- package/src/types.ts +112 -51
- package/src/usage.test.ts +112 -116
- 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 {
|
|
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
|
|
134
|
+
const streamed = await testProvider({ ...injectedBase, fetch: streamingFetch, rawBody: true })
|
|
94
135
|
.generate({ workerId: "stream", messages: [{ role: "user", content: "hello" }] });
|
|
95
|
-
const buffered = await
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
210
|
-
await assert.rejects(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
493
|
+
test("injected prompt measurement preserves provenance and request cost estimation stays internal", async () => {
|
|
281
494
|
const seen: string[] = [];
|
|
282
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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: "
|
|
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
|
-
|
|
657
|
+
normalizeCost: providerCostNormalizer("@openrouter/ai-sdk-provider"),
|
|
368
658
|
};
|
|
369
659
|
|
|
370
660
|
await t.test("buffered", async () => {
|
|
371
|
-
const response = await
|
|
661
|
+
const response = await testProvider({ ...config, streaming: false })
|
|
372
662
|
.generate({ workerId: "buffered", messages: [] });
|
|
373
|
-
assert.deepEqual(response.
|
|
663
|
+
assert.deepEqual(response.accounting[0]?.cost, charge);
|
|
374
664
|
});
|
|
375
665
|
await t.test("streamed", async () => {
|
|
376
|
-
const response = await
|
|
666
|
+
const response = await testProvider(config)
|
|
377
667
|
.generate({ workerId: "streamed", messages: [] });
|
|
378
|
-
assert.deepEqual(response.
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
407
|
-
kind: "
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
440
|
-
kind: "
|
|
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 =
|
|
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 =
|
|
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.
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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 })
|
|
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 =
|
|
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 })
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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.
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
|
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.
|
|
619
|
-
assert.equal(response.
|
|
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
|
|
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.
|
|
639
|
-
|
|
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
|
|
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.
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
|
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.
|
|
670
|
-
assert.equal(buffered.
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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, "
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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'
|
|
794
|
-
const on =
|
|
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 =
|
|
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'
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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'
|
|
1105
|
-
const base = { model: "m", url: "http://x/v1/chat/completions", contextWindow: 640,
|
|
1106
|
-
const p =
|
|
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
|
-
() =>
|
|
1114
|
-
/
|
|
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("
|
|
1119
|
-
const
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 = () =>
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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("
|
|
1322
|
-
const p =
|
|
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: [],
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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,
|
|
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 =
|
|
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 =
|
|
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
|
-
&&
|
|
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 =
|
|
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 =
|
|
2134
|
+
const p = testProvider({
|
|
1560
2135
|
model: "m",
|
|
1561
2136
|
url: "http://x/v1/chat/completions",
|
|
1562
|
-
fetchTimeoutMs:
|
|
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 === "
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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.
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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("
|
|
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
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
assert.equal(
|
|
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 =
|
|
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
|
-
// --
|
|
2399
|
+
// -- {§provider-cache-affinity} / {§provider-cache-write-policy} --
|
|
1811
2400
|
|
|
1812
|
-
test("
|
|
1813
|
-
const p =
|
|
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("
|
|
1820
|
-
const p =
|
|
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("
|
|
1827
|
-
const p =
|
|
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: []
|
|
1830
|
-
|
|
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
|
});
|