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