@plurnk/plurnk-providers 1.18.0 → 1.19.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 +8 -4
- package/README.md +4 -16
- package/SPEC.md +31 -30
- package/dist/AiSdkProvider.d.ts +1 -3
- package/dist/AiSdkProvider.d.ts.map +1 -1
- package/dist/AiSdkProvider.js +7 -20
- package/dist/AiSdkProvider.js.map +1 -1
- package/dist/AiSdkRequestBody.d.ts +1 -4
- package/dist/AiSdkRequestBody.d.ts.map +1 -1
- package/dist/AiSdkRequestBody.js +1 -48
- package/dist/AiSdkRequestBody.js.map +1 -1
- package/dist/Mock.d.ts +3 -4
- package/dist/Mock.d.ts.map +1 -1
- package/dist/Mock.js +3 -4
- package/dist/Mock.js.map +1 -1
- package/dist/ProviderRegistry.js +2 -2
- package/dist/ProviderRegistry.js.map +1 -1
- package/dist/accounting.d.ts +0 -1
- package/dist/accounting.d.ts.map +1 -1
- package/dist/accounting.js +1 -32
- package/dist/accounting.js.map +1 -1
- package/dist/aiSdkTransport.d.ts +0 -8
- package/dist/aiSdkTransport.d.ts.map +1 -1
- package/dist/aiSdkTransport.js +23 -39
- package/dist/aiSdkTransport.js.map +1 -1
- package/dist/catalogProvider.d.ts.map +1 -1
- package/dist/catalogProvider.js +10 -2
- package/dist/catalogProvider.js.map +1 -1
- package/dist/compatibleProvider.d.ts +1 -1
- package/dist/compatibleProvider.d.ts.map +1 -1
- package/dist/compatibleProvider.js +13 -25
- package/dist/compatibleProvider.js.map +1 -1
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +1 -0
- package/dist/env.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/sdkModels.d.ts.map +1 -1
- package/dist/sdkModels.js +0 -10
- package/dist/sdkModels.js.map +1 -1
- package/dist/types.d.ts +0 -15
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -8
- package/src/AiSdkProvider.test.ts +97 -246
- package/src/AiSdkProvider.ts +7 -24
- package/src/AiSdkRequestBody.ts +2 -42
- package/src/Mock.test.ts +11 -3
- package/src/Mock.ts +4 -6
- package/src/ProviderRegistry.test.ts +0 -1
- package/src/ProviderRegistry.ts +1 -1
- package/src/accounting.test.ts +3 -25
- package/src/accounting.ts +0 -34
- package/src/aiSdkTransport.test.ts +2 -1
- package/src/aiSdkTransport.ts +25 -41
- package/src/boundaries.test.ts +1 -1
- package/src/capacity.test.ts +2 -2
- package/src/catalogProvider.test.ts +7 -85
- package/src/catalogProvider.ts +10 -2
- package/src/compatibleProvider.test.ts +6 -6
- package/src/compatibleProvider.ts +13 -28
- package/src/cost.test.ts +3 -3
- package/src/discover.test.ts +11 -2
- package/src/env.test.ts +3 -0
- package/src/env.ts +2 -1
- package/src/errors.test.ts +2 -2
- package/src/index.ts +1 -2
- package/src/inputModalities.test.ts +2 -1
- package/src/sdkModels.test.ts +0 -10
- package/src/sdkModels.ts +0 -10
- package/src/types.ts +8 -46
|
@@ -106,12 +106,6 @@ test("provider adapters advertise only reasoning policies they can preserve", ()
|
|
|
106
106
|
}, "mistral-small-latest");
|
|
107
107
|
assert.deepEqual(mistral?.supportedReasoningPolicies, ["off", "adaptive", "high"], "Mistral's low/medium coercion is not advertised as exact support");
|
|
108
108
|
|
|
109
|
-
const grok = catalogProviderFromEnv("xai", {
|
|
110
|
-
...env,
|
|
111
|
-
XAI_API_KEY: "test-key",
|
|
112
|
-
PLURNK_PROVIDERS_REASONING: "adaptive",
|
|
113
|
-
}, "grok-4.6");
|
|
114
|
-
assert.deepEqual(grok?.supportedReasoningPolicies, ["adaptive", "low", "medium", "high", "xhigh"], "Grok 4.6 cannot disable reasoning");
|
|
115
109
|
|
|
116
110
|
const gemini = catalogProviderFromEnv("google", {
|
|
117
111
|
...env,
|
|
@@ -368,79 +362,6 @@ test("official AI SDK provider owns the native request while PLURNK owns call se
|
|
|
368
362
|
assert.equal(calls[0]?.body.prompt_cache_key, "worker", "the official OpenAI SDK projects the documented affinity key");
|
|
369
363
|
});
|
|
370
364
|
|
|
371
|
-
test("xAI's native chat contract requests its strongest cataloged effort and caps the complete reasoning response", async () => {
|
|
372
|
-
let call: { headers: Headers; body: Record<string, unknown> } | undefined;
|
|
373
|
-
mock.method(globalThis, "fetch", async (_input: string | URL | Request, init?: RequestInit) => {
|
|
374
|
-
call = {
|
|
375
|
-
headers: new Headers(init?.headers),
|
|
376
|
-
body: JSON.parse(String(init?.body)) as Record<string, unknown>,
|
|
377
|
-
};
|
|
378
|
-
return new Response([
|
|
379
|
-
`data: ${JSON.stringify({
|
|
380
|
-
id: "response-xai",
|
|
381
|
-
object: "chat.completion.chunk",
|
|
382
|
-
created: 1,
|
|
383
|
-
model: "grok-4.6",
|
|
384
|
-
choices: [{ index: 0, delta: { reasoning_content: "consider" }, finish_reason: null }],
|
|
385
|
-
})}`,
|
|
386
|
-
`data: ${JSON.stringify({
|
|
387
|
-
id: "response-xai",
|
|
388
|
-
object: "chat.completion.chunk",
|
|
389
|
-
created: 2,
|
|
390
|
-
model: "grok-4.6",
|
|
391
|
-
choices: [{ index: 0, delta: { content: "OK" }, finish_reason: "stop" }],
|
|
392
|
-
})}`,
|
|
393
|
-
`data: ${JSON.stringify({
|
|
394
|
-
id: "response-xai",
|
|
395
|
-
object: "chat.completion.chunk",
|
|
396
|
-
created: 3,
|
|
397
|
-
model: "grok-4.6",
|
|
398
|
-
choices: [],
|
|
399
|
-
usage: {
|
|
400
|
-
prompt_tokens: 5,
|
|
401
|
-
completion_tokens: 4,
|
|
402
|
-
total_tokens: 9,
|
|
403
|
-
prompt_tokens_details: { cached_tokens: 2 },
|
|
404
|
-
completion_tokens_details: { reasoning_tokens: 3 },
|
|
405
|
-
cost_in_usd_ticks: 1_230_000,
|
|
406
|
-
},
|
|
407
|
-
})}`,
|
|
408
|
-
"data: [DONE]",
|
|
409
|
-
].join("\n\n"), { headers: { "content-type": "text/event-stream" } });
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
const provider = catalogProviderFromEnv("xai", {
|
|
413
|
-
...env,
|
|
414
|
-
XAI_API_KEY: "test-key",
|
|
415
|
-
PLURNK_PROVIDERS_REASONING: "adaptive",
|
|
416
|
-
}, "grok-4.6");
|
|
417
|
-
const result = await provider?.generate({
|
|
418
|
-
workerId: "xai-worker",
|
|
419
|
-
messages: [{ role: "user", content: "hello" }],
|
|
420
|
-
maxOutputTokens: 16,
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
assert.equal(call?.body.max_completion_tokens, 16);
|
|
424
|
-
assert.equal(call?.body.reasoning_effort, "xhigh", "adaptive uses the strongest route-advertised generic effort");
|
|
425
|
-
assert.equal("max_tokens" in (call?.body ?? {}), false);
|
|
426
|
-
assert.equal(call?.headers.get("x-grok-conv-id"), "xai-worker");
|
|
427
|
-
assert.equal(result?.assistant.reasoning, "consider");
|
|
428
|
-
assert.equal(result?.assistant.content, "OK");
|
|
429
|
-
assert.deepEqual(result?.accounting[0]?.usage, {
|
|
430
|
-
inputTokens: 5,
|
|
431
|
-
outputTokens: 4,
|
|
432
|
-
totalTokens: 9,
|
|
433
|
-
inputTokenDetails: { cacheReadTokens: 2 },
|
|
434
|
-
outputTokenDetails: { textTokens: 1, reasoningTokens: 3 },
|
|
435
|
-
});
|
|
436
|
-
assert.deepEqual(result?.accounting[0]?.cost, {
|
|
437
|
-
kind: "charged",
|
|
438
|
-
amount: { amount: "1230000", currency: "USDTICK" },
|
|
439
|
-
usdEquivalent: "0.000123",
|
|
440
|
-
source: "xAI response usage.cost_in_usd_ticks",
|
|
441
|
-
});
|
|
442
|
-
});
|
|
443
|
-
|
|
444
365
|
test("Cerebras explicit and adaptive reasoning do not invent a token budget", async () => {
|
|
445
366
|
let body: Record<string, unknown> | undefined;
|
|
446
367
|
mock.method(globalThis, "fetch", async (_input: string | URL | Request, init?: RequestInit) => {
|
|
@@ -852,12 +773,12 @@ test("native provider routes project their documented cache controls through the
|
|
|
852
773
|
|
|
853
774
|
test("cataloged unknown model fails unless its context is explicit", () => {
|
|
854
775
|
assert.throws(
|
|
855
|
-
() => catalogProviderFromEnv("
|
|
776
|
+
() => catalogProviderFromEnv("groq", env, "not-in-the-catalog"),
|
|
856
777
|
/context window unresolved/,
|
|
857
778
|
);
|
|
858
|
-
const provider = catalogProviderFromEnv("
|
|
779
|
+
const provider = catalogProviderFromEnv("groq", {
|
|
859
780
|
...env,
|
|
860
|
-
|
|
781
|
+
GROQ_API_KEY: "test-key",
|
|
861
782
|
PLURNK_PROVIDERS_CONTEXT_WINDOW: "8192",
|
|
862
783
|
PLURNK_PROVIDERS_REASONING: "adaptive",
|
|
863
784
|
}, "not-in-the-catalog");
|
|
@@ -865,7 +786,7 @@ test("cataloged unknown model fails unless its context is explicit", () => {
|
|
|
865
786
|
assert.deepEqual(provider?.supportedReasoningPolicies, ["off", "adaptive"]);
|
|
866
787
|
});
|
|
867
788
|
|
|
868
|
-
test("Models.dev is the only fallback rate table", async () => {
|
|
789
|
+
test("{§provider-monetary-evidence} Models.dev is the only fallback rate table", async () => {
|
|
869
790
|
mock.method(globalThis, "fetch", async () => new Response([
|
|
870
791
|
`data: ${JSON.stringify({
|
|
871
792
|
id: "response",
|
|
@@ -901,9 +822,9 @@ test("Models.dev is the only fallback rate table", async () => {
|
|
|
901
822
|
source: "Models.dev catalog rates",
|
|
902
823
|
});
|
|
903
824
|
|
|
904
|
-
const uncataloged = catalogProviderFromEnv("
|
|
825
|
+
const uncataloged = catalogProviderFromEnv("groq", {
|
|
905
826
|
...env,
|
|
906
|
-
|
|
827
|
+
GROQ_API_KEY: "test-key",
|
|
907
828
|
PLURNK_PROVIDERS_CONTEXT_WINDOW: "8192",
|
|
908
829
|
PLURNK_PROVIDERS_REASONING: "adaptive",
|
|
909
830
|
}, "not-in-the-catalog");
|
|
@@ -963,3 +884,4 @@ test("(#458) declared efforts union into the supported set under the models.dev-
|
|
|
963
884
|
// (#474) "max" joined the portable vocabulary; "off" still requires a declared "none".
|
|
964
885
|
assert.deepEqual(provider?.supportedReasoningPolicies, ["adaptive", "low", "high", "max"]);
|
|
965
886
|
});
|
|
887
|
+
|
package/src/catalogProvider.ts
CHANGED
|
@@ -38,19 +38,25 @@ import type { LanguageModel } from "ai";
|
|
|
38
38
|
import type { AiSdkProviderOptions, CacheAffinity } from "./AiSdkProvider.ts";
|
|
39
39
|
import type { PluginAttribution, PluginAttributionContext } from "@plurnk/plurnk-meta";
|
|
40
40
|
|
|
41
|
+
// {§provider-reasoning-style} — the provider-wide declaration, unless the bare knob (alias-scopable:
|
|
42
|
+
// PLURNK_PROVIDERS_REASONING_STYLE_<alias>) names the wire for one route; one provider can serve
|
|
43
|
+
// models whose reasoning controls differ (Cloudflare's gateway hosts `@cf/…` and Gemini alike).
|
|
41
44
|
const reasoningStyleFromEnv = (
|
|
42
45
|
env: NodeJS.ProcessEnv,
|
|
43
46
|
name: string,
|
|
44
47
|
): ReasoningStyle | undefined => {
|
|
45
48
|
const prefix = name.replaceAll(/[^a-zA-Z0-9]/g, "_").toUpperCase();
|
|
46
|
-
const
|
|
49
|
+
const routeKey = "PLURNK_PROVIDERS_REASONING_STYLE";
|
|
50
|
+
const providerKey = `PLURNK_PROVIDERS_PROVIDER_${prefix}_REASONING_STYLE`;
|
|
51
|
+
const key = env[routeKey] !== undefined && env[routeKey].length > 0 ? routeKey : providerKey;
|
|
52
|
+
const value = env[key];
|
|
47
53
|
if (value === undefined || value.length === 0) return undefined;
|
|
48
54
|
const styles: readonly ReasoningStyle[] = [
|
|
49
55
|
"none", "think", "include_reasoning", "effort",
|
|
50
56
|
"effort_explicit", "effort_required", "thinking_effort", "template", "anthropic",
|
|
51
57
|
];
|
|
52
58
|
if (!styles.includes(value as ReasoningStyle)) {
|
|
53
|
-
throw new Error(`${name} provider:
|
|
59
|
+
throw new Error(`${name} provider: ${key} has invalid value "${value}"`);
|
|
54
60
|
}
|
|
55
61
|
return value as ReasoningStyle;
|
|
56
62
|
};
|
|
@@ -167,6 +173,8 @@ const supportedReasoningPolicies = ({
|
|
|
167
173
|
// not, and a word the template does not know fails loudly on the first request.
|
|
168
174
|
if (style === "template") return REASONING_POLICIES;
|
|
169
175
|
if (info !== undefined && info.reasoning !== true) return activationPolicies;
|
|
176
|
+
// The declared wire's whole vocabulary: a route that reasons unconditionally still
|
|
177
|
+
// admits only the levels the catalog advertises for it.
|
|
170
178
|
if (info?.reasoningOptions !== undefined) {
|
|
171
179
|
return catalogSupportedReasoningPolicies({ info, native, style, declared });
|
|
172
180
|
}
|
|
@@ -44,7 +44,7 @@ test("an undifferentiated compatible endpoint receives no guessed prompt-cache f
|
|
|
44
44
|
return streamedChatResponse("ok");
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
const provider = await compatibleProviderFromEnv(
|
|
47
|
+
const provider = await compatibleProviderFromEnv(env, "local");
|
|
48
48
|
await provider.generate({
|
|
49
49
|
workerId: "worker-affinity",
|
|
50
50
|
messages: [{ role: "user", content: "hello" }],
|
|
@@ -65,7 +65,7 @@ test("the server-wide DRY-off floor emits no DRY request fields", async () => {
|
|
|
65
65
|
return streamedChatResponse("ok");
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
const provider = await compatibleProviderFromEnv(
|
|
68
|
+
const provider = await compatibleProviderFromEnv({
|
|
69
69
|
...env,
|
|
70
70
|
PLURNK_PROVIDERS_DRY_MULTIPLIER: "0",
|
|
71
71
|
// Stale or independently supplied shape values cannot activate DRY.
|
|
@@ -90,7 +90,7 @@ test("(#483) a detected llama-server rail admits the operator's stated effort",
|
|
|
90
90
|
if (url.endsWith("/props")) return new Response(JSON.stringify({ total_slots: 1 }));
|
|
91
91
|
throw new Error(`unexpected request ${url}`);
|
|
92
92
|
});
|
|
93
|
-
const provider = await compatibleProviderFromEnv(
|
|
93
|
+
const provider = await compatibleProviderFromEnv({ ...env, PLURNK_PROVIDERS_REASONING: "medium" }, "local");
|
|
94
94
|
assert.ok(provider.supportedReasoningPolicies.includes("medium"), "the template governs: medium is admitted on a llama-server rail");
|
|
95
95
|
assert.ok(provider.supportedReasoningPolicies.includes("low") && provider.supportedReasoningPolicies.includes("high"), "the whole policy vocabulary rides; the template refuses unknown words itself");
|
|
96
96
|
});
|
|
@@ -118,7 +118,7 @@ test("detected llama-server measures the complete chat request through input_tok
|
|
|
118
118
|
throw new Error(`unexpected request ${url}`);
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
-
const provider = await compatibleProviderFromEnv(
|
|
121
|
+
const provider = await compatibleProviderFromEnv(env, "local");
|
|
122
122
|
const messages = [
|
|
123
123
|
{ role: "system" as const, content: "system slot" },
|
|
124
124
|
{ role: "user" as const, content: "漢漢漢" },
|
|
@@ -134,7 +134,7 @@ test("detected llama-server measures the complete chat request through input_tok
|
|
|
134
134
|
assert.deepEqual(countBody?.chat_template_kwargs, { enable_thinking: false });
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
-
test("a missing llama-server input-token endpoint degrades explicitly, never to a claimed bound", async () => {
|
|
137
|
+
test("{§provider-prompt-measurement} a missing llama-server input-token endpoint degrades explicitly, never to a claimed bound", async () => {
|
|
138
138
|
mock.method(globalThis, "fetch", async (input: string | URL | Request) => {
|
|
139
139
|
const url = String(input);
|
|
140
140
|
if (url.endsWith("/models")) {
|
|
@@ -147,7 +147,7 @@ test("a missing llama-server input-token endpoint degrades explicitly, never to
|
|
|
147
147
|
throw new Error(`unexpected request ${url}`);
|
|
148
148
|
});
|
|
149
149
|
|
|
150
|
-
const provider = await compatibleProviderFromEnv(
|
|
150
|
+
const provider = await compatibleProviderFromEnv(env, "local");
|
|
151
151
|
assert.deepEqual(await provider.countPromptTokens([{ role: "user", content: "漢漢漢" }]), {
|
|
152
152
|
kind: "estimate",
|
|
153
153
|
tokens: 2,
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
reasoningResponseStyleFromEnv,
|
|
17
17
|
} from "./env.ts";
|
|
18
18
|
import { providerSource } from "./notices.ts";
|
|
19
|
-
import { plurnkCostNormalizer } from "./accounting.ts";
|
|
20
19
|
import type { Provider } from "./types.ts";
|
|
21
20
|
import { emitWarningOnce } from "./warnings.ts";
|
|
22
21
|
|
|
@@ -27,22 +26,16 @@ type EndpointProbe = {
|
|
|
27
26
|
failed: boolean;
|
|
28
27
|
};
|
|
29
28
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
override
|
|
34
|
-
): string => {
|
|
35
|
-
const configured = override
|
|
36
|
-
?? (provider === "openai"
|
|
37
|
-
? env.OPENAI_BASE_URL ?? env.OPENAI_API_BASE
|
|
38
|
-
: env.PLURNK_BASE_URL);
|
|
29
|
+
const provider = "openai";
|
|
30
|
+
|
|
31
|
+
const chatUrl = (env: NodeJS.ProcessEnv, override?: string): string => {
|
|
32
|
+
const configured = override ?? env.OPENAI_BASE_URL ?? env.OPENAI_API_BASE;
|
|
39
33
|
if (configured === undefined || configured.length === 0) {
|
|
40
|
-
throw new Error(`${provider} provider:
|
|
34
|
+
throw new Error(`${provider} provider: OPENAI_BASE_URL or OPENAI_API_BASE must be set`);
|
|
41
35
|
}
|
|
42
36
|
const base = configured.replace(/\/+$/, "");
|
|
43
37
|
if (base.endsWith("/chat/completions")) return base;
|
|
44
|
-
|
|
45
|
-
return `${base}/chat/completions`;
|
|
38
|
+
return `${base.replace(/\/v1$/, "")}/v1/chat/completions`;
|
|
46
39
|
};
|
|
47
40
|
|
|
48
41
|
const probeModels = async (
|
|
@@ -114,18 +107,16 @@ const probeProps = async (
|
|
|
114
107
|
};
|
|
115
108
|
|
|
116
109
|
export const compatibleProviderFromEnv = async (
|
|
117
|
-
provider: "openai" | "plurnk",
|
|
118
110
|
env: NodeJS.ProcessEnv,
|
|
119
111
|
model: string,
|
|
120
112
|
baseUrlOverride?: string,
|
|
121
113
|
): Promise<Provider> => {
|
|
122
|
-
// The knobs remain universal and fail hard when malformed, but this local
|
|
123
|
-
//
|
|
124
|
-
// already owns slot affinity and the first-party endpoint receives worker metadata.
|
|
114
|
+
// The knobs remain universal and fail hard when malformed, but this local compatible
|
|
115
|
+
// route declares no vendor cache projection: llama-server already owns slot affinity.
|
|
125
116
|
cacheAffinityFromEnv(env, provider);
|
|
126
117
|
cacheWritePolicyFromEnv(env, provider);
|
|
127
|
-
const url = chatUrl(
|
|
128
|
-
const apiKey =
|
|
118
|
+
const url = chatUrl(env, baseUrlOverride);
|
|
119
|
+
const apiKey = env.OPENAI_API_KEY;
|
|
129
120
|
const headers: Record<string, string> = apiKey === undefined || apiKey.length === 0
|
|
130
121
|
? {}
|
|
131
122
|
: { Authorization: `Bearer ${apiKey}` };
|
|
@@ -145,8 +136,8 @@ export const compatibleProviderFromEnv = async (
|
|
|
145
136
|
throw new Error(`${provider} provider: PLURNK_PROVIDERS_LLAMA_SERVER must be "1", "0", or unset`);
|
|
146
137
|
}
|
|
147
138
|
const pinned = pinRaw === undefined || pinRaw === "" ? null : pinRaw === "1";
|
|
148
|
-
const llamaServer =
|
|
149
|
-
if (probe.failed && pinned === null
|
|
139
|
+
const llamaServer = pinned ?? probe.llamaServer;
|
|
140
|
+
if (probe.failed && pinned === null) {
|
|
150
141
|
emitWarningOnce(
|
|
151
142
|
`${provider} provider: llama-server detection failed after ${attempts} attempts; pin PLURNK_PROVIDERS_LLAMA_SERVER=1 when this is a llama-server`,
|
|
152
143
|
"PLURNK_PROBE_FAILED",
|
|
@@ -160,7 +151,7 @@ export const compatibleProviderFromEnv = async (
|
|
|
160
151
|
}
|
|
161
152
|
|
|
162
153
|
let grammarStyle: GrammarStyle = "none";
|
|
163
|
-
let reasoningStyle: ReasoningStyle =
|
|
154
|
+
let reasoningStyle: ReasoningStyle = "think";
|
|
164
155
|
let slotCount: number | null = null;
|
|
165
156
|
let eosText: string | undefined;
|
|
166
157
|
let tokenizeUrl: string | undefined;
|
|
@@ -210,17 +201,11 @@ export const compatibleProviderFromEnv = async (
|
|
|
210
201
|
dryBase: parseOptionalFloat(env.PLURNK_PROVIDERS_DRY_BASE, "PLURNK_PROVIDERS_DRY_BASE", provider, 0) ?? undefined,
|
|
211
202
|
dryAllowedLength: parseOptionalInt(env.PLURNK_PROVIDERS_DRY_ALLOWED_LENGTH, "PLURNK_PROVIDERS_DRY_ALLOWED_LENGTH", provider) ?? undefined,
|
|
212
203
|
repeatLastN: parseOptionalInt(env.PLURNK_PROVIDERS_REPEAT_LAST_N, "PLURNK_PROVIDERS_REPEAT_LAST_N", provider) ?? undefined,
|
|
213
|
-
tuningFloors: provider !== "plurnk",
|
|
214
204
|
retryAttempts: parseRequiredInt(env.PLURNK_PROVIDERS_RETRY_ATTEMPTS, "PLURNK_PROVIDERS_RETRY_ATTEMPTS", provider),
|
|
215
205
|
errorDetailLimit: parseRequiredInt(env.PLURNK_PROVIDERS_ERROR_DETAIL_LIMIT, "PLURNK_PROVIDERS_ERROR_DETAIL_LIMIT", provider),
|
|
216
206
|
source: providerSource(provider),
|
|
217
207
|
grammarStyle,
|
|
218
208
|
...dataCaptureFromEnv(env, provider),
|
|
219
|
-
firstPartyMetadata: provider === "plurnk",
|
|
220
|
-
normalizeCost: provider === "plurnk" ? plurnkCostNormalizer : undefined,
|
|
221
|
-
apiKeyRejectedMessage: provider === "plurnk"
|
|
222
|
-
? "PLURNK_API_KEY was rejected by plurnk.ai (invalid or expired)."
|
|
223
|
-
: undefined,
|
|
224
209
|
supportsSlotPinning: llamaServer,
|
|
225
210
|
slotCount,
|
|
226
211
|
eosText,
|
package/src/cost.test.ts
CHANGED
|
@@ -16,7 +16,7 @@ const usage: ProviderUsage = {
|
|
|
16
16
|
totalTokens: 2,
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
test("direct charged evidence wins over a Models.dev estimate", () => {
|
|
19
|
+
test("{§provider-monetary-evidence} direct charged evidence wins over a Models.dev estimate", () => {
|
|
20
20
|
const charged = {
|
|
21
21
|
kind: "charged",
|
|
22
22
|
amount: { amount: "0.0000042", currency: "XMR" },
|
|
@@ -28,7 +28,7 @@ test("direct charged evidence wins over a Models.dev estimate", () => {
|
|
|
28
28
|
assert.equal(providerCostUsd(charged), "0.73");
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
test("an exact zero estimate remains distinguishable from unknown cost", () => {
|
|
31
|
+
test("{§provider-cost} an exact zero estimate remains distinguishable from unknown cost", () => {
|
|
32
32
|
const zero = estimateProviderCost(usage, { input: 0, output: 0 }, "Models.dev");
|
|
33
33
|
const unknown = estimateProviderCost(usage, null, "Models.dev");
|
|
34
34
|
assert.deepEqual(zero, {
|
|
@@ -104,7 +104,7 @@ test("decimal aggregation is exact and becomes unknown if any request is unknown
|
|
|
104
104
|
]), null);
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
-
test("malformed charged money is rejected instead of coerced", () => {
|
|
107
|
+
test("{§provider-cost} malformed charged money is rejected instead of coerced", () => {
|
|
108
108
|
assert.throws(() => validateChargedCost({
|
|
109
109
|
kind: "charged",
|
|
110
110
|
amount: { amount: "1e3", currency: "usd" },
|
package/src/discover.test.ts
CHANGED
|
@@ -5,6 +5,11 @@ import os from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { discover } from "./discover.ts";
|
|
7
7
|
|
|
8
|
+
// This file's fixtures are third-party packages, so it exercises the operator who admitted them
|
|
9
|
+
// ({§executor-trust}); the shipped panel admits only `@plurnk/*`. Tests of the gate itself state
|
|
10
|
+
// their own value below and override this one.
|
|
11
|
+
process.env.PLURNK_PLUGINS_TRUSTED_ONLY = "0";
|
|
12
|
+
|
|
8
13
|
// Create a temp dir and register its removal on the test context, so it is
|
|
9
14
|
// cleaned on a GREEN or RED run. A trailing rm after the assertions leaks the dir
|
|
10
15
|
// whenever one throws — thousands accumulate on a shared box at drill frequency.
|
|
@@ -166,13 +171,17 @@ const trustFixture = (t: TestContext) => buildModules(t, {
|
|
|
166
171
|
"@acme/acme-provider-foo": { name: "@acme/acme-provider-foo", plurnk: { kind: "provider", name: "foo" } },
|
|
167
172
|
});
|
|
168
173
|
|
|
169
|
-
test("trust gate OFF (
|
|
174
|
+
test("trust gate OFF ('' or '0'): every provider is trusted; unset defers to the panel", async (t) => {
|
|
170
175
|
const root = await trustFixture(t);
|
|
171
|
-
for (const gate of [
|
|
176
|
+
for (const gate of ["", "0"]) {
|
|
172
177
|
const { registry, skipped } = await discover({ cwd: root, env: { PLURNK_PLUGINS_TRUSTED_ONLY: gate } as NodeJS.ProcessEnv });
|
|
173
178
|
assert.deepEqual([...registry.keys()].sort(), ["foo", "native"]);
|
|
174
179
|
assert.equal(skipped.size, 0);
|
|
175
180
|
}
|
|
181
|
+
// An unset key is answered by @plurnk/plurnk-meta's own panel, which ships `1`.
|
|
182
|
+
const { registry, skipped } = await discover({ cwd: root, env: {} as NodeJS.ProcessEnv });
|
|
183
|
+
assert.deepEqual([...registry.keys()].sort(), ["native"], "only the first-party provider survives the shipped gate");
|
|
184
|
+
assert.equal(skipped.size, 1);
|
|
176
185
|
});
|
|
177
186
|
|
|
178
187
|
test("trust gate ON: @plurnk/* always trusted; third party declined → skipped, not registered", async (t) => {
|
package/src/env.test.ts
CHANGED
|
@@ -113,6 +113,7 @@ test("scopeEnvToAlias: suffixed knob wins, bare is the fallback, other aliases i
|
|
|
113
113
|
PLURNK_PROVIDERS_REASONING_turboderp: "high",
|
|
114
114
|
PLURNK_PROVIDERS_REASONING_BUDGET_TURBODERP: "4096", // case-folds like PLURNK_MODEL_ keys
|
|
115
115
|
PLURNK_PROVIDERS_REASONING_RESPONSE_STYLE_TURBODERP: "think-tags",
|
|
116
|
+
PLURNK_PROVIDERS_REASONING_STYLE_turboderp: "thinking_config",
|
|
116
117
|
PLURNK_PROVIDERS_CONTEXT_WINDOW_turboderp: "8000",
|
|
117
118
|
PLURNK_PROVIDERS_OUTPUT_BUDGET_turboderp: "4096",
|
|
118
119
|
PLURNK_PROVIDERS_CONTEXT_WINDOW_other: "1",
|
|
@@ -121,9 +122,11 @@ test("scopeEnvToAlias: suffixed knob wins, bare is the fallback, other aliases i
|
|
|
121
122
|
assert.equal(scoped.PLURNK_PROVIDERS_REASONING, "high");
|
|
122
123
|
assert.equal(scoped.PLURNK_PROVIDERS_REASONING_BUDGET, "4096");
|
|
123
124
|
assert.equal(scoped.PLURNK_PROVIDERS_REASONING_RESPONSE_STYLE, "think-tags");
|
|
125
|
+
assert.equal(scoped.PLURNK_PROVIDERS_REASONING_STYLE, "thinking_config");
|
|
124
126
|
assert.equal(scoped.PLURNK_PROVIDERS_CONTEXT_WINDOW, "8000");
|
|
125
127
|
assert.equal(scoped.PLURNK_PROVIDERS_OUTPUT_BUDGET, "4096");
|
|
126
128
|
assert.equal(scopeEnvToAlias(env, "plain").PLURNK_PROVIDERS_REASONING, "off"); // fallback intact
|
|
129
|
+
assert.equal(scopeEnvToAlias(env, "plain").PLURNK_PROVIDERS_REASONING_STYLE, undefined, "another alias's style never leaks");
|
|
127
130
|
});
|
|
128
131
|
|
|
129
132
|
test("scopeEnvToAlias: aliases with underscores resolve; a bare knob is never mistaken for a suffix", async () => {
|
package/src/env.ts
CHANGED
|
@@ -243,7 +243,7 @@ export const resolveGenerationEnvelopeFromEnv = (
|
|
|
243
243
|
// PLURNK_PROVIDERS_REASONING_BUDGET optional reasoning subset of the total
|
|
244
244
|
// output budget, used for tier/budget mapping where the backend supports it.
|
|
245
245
|
// The provider maps intent to the backend's mechanism; the consumer states
|
|
246
|
-
// intent, never mechanism.
|
|
246
|
+
// intent, never mechanism. working memory is separate from provider reasoning.
|
|
247
247
|
export type Reasoning = { mode: ReasoningPolicy; budget: number | null };
|
|
248
248
|
|
|
249
249
|
export const parseReasoningPolicy = (value: unknown, label: string): ReasoningPolicy => {
|
|
@@ -294,6 +294,7 @@ export const PROVIDERS_KNOBS = Object.freeze([
|
|
|
294
294
|
"PLURNK_PROVIDERS_COST",
|
|
295
295
|
"PLURNK_PROVIDERS_OUTPUT_BUDGET",
|
|
296
296
|
"PLURNK_PROVIDERS_REASONING_RESPONSE_STYLE",
|
|
297
|
+
"PLURNK_PROVIDERS_REASONING_STYLE",
|
|
297
298
|
"PLURNK_PROVIDERS_REASONING_BUDGET",
|
|
298
299
|
"PLURNK_PROVIDERS_REASONING",
|
|
299
300
|
"PLURNK_PROVIDERS_CONTEXT_WINDOW",
|
package/src/errors.test.ts
CHANGED
|
@@ -15,7 +15,7 @@ const apiError = (statusCode: number, responseBody = "body") => new APICallError
|
|
|
15
15
|
|
|
16
16
|
const SOURCE_PATTERN = /^[a-z]+(:[a-z][a-z0-9-]*)?$/;
|
|
17
17
|
|
|
18
|
-
test("HTTP request rejection preserves its cause without becoming a response-contract strike", () => {
|
|
18
|
+
test("{§provider-request-rejection} HTTP request rejection preserves its cause without becoming a response-contract strike", () => {
|
|
19
19
|
for (const status of [400, 404, 405, 422]) {
|
|
20
20
|
const message = "The requested model is not available on this endpoint.";
|
|
21
21
|
const cause = apiError(status, JSON.stringify({ error: { message } }));
|
|
@@ -54,7 +54,7 @@ test("classifyProviderError maps HTTP status to kind", () => {
|
|
|
54
54
|
assert.equal(k(404), "request_rejected");
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
test("capacity normalization prefers structured provider codes and keeps generic 400s distinct", () => {
|
|
57
|
+
test("{§provider-capacity-failure} capacity normalization prefers structured provider codes and keeps generic 400s distinct", () => {
|
|
58
58
|
const openai = apiError(400, JSON.stringify({
|
|
59
59
|
error: {
|
|
60
60
|
type: "invalid_request_error",
|
package/src/index.ts
CHANGED
|
@@ -9,7 +9,7 @@ export type {
|
|
|
9
9
|
AiSdkProviderPlugin,
|
|
10
10
|
ProviderOptions,
|
|
11
11
|
ProviderResponse,
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
ProviderAccounting,
|
|
14
14
|
ProviderCost,
|
|
15
15
|
ProviderCostNormalizer,
|
|
@@ -78,7 +78,6 @@ export {
|
|
|
78
78
|
} from "./cost.ts";
|
|
79
79
|
export {
|
|
80
80
|
aggregateProviderAccounting,
|
|
81
|
-
plurnkCostNormalizer,
|
|
82
81
|
providerCostNormalizer,
|
|
83
82
|
validateProviderRequestAccounting,
|
|
84
83
|
} from "./accounting.ts";
|
|
@@ -16,6 +16,7 @@ const parts: ChatMessage = {
|
|
|
16
16
|
{ type: "text", text: "look at this" },
|
|
17
17
|
{ type: "file", data: PNG, mediaType: "image/png" },
|
|
18
18
|
{ type: "file", data: PDF, mediaType: "application/pdf" },
|
|
19
|
+
{ type: "file", data: new Uint8Array([82, 73, 70, 70]), mediaType: "audio/wav" },
|
|
19
20
|
],
|
|
20
21
|
};
|
|
21
22
|
|
|
@@ -37,7 +38,7 @@ test("{§provider-input-modalities} the Mock declares modalities by option and r
|
|
|
37
38
|
const seeing = new Mock({ contextWindow: 200000, responses: [], inputModalities: ["image", "pdf"] });
|
|
38
39
|
assert.ok(seeing.inputModalities.has("image") && seeing.inputModalities.has("pdf") && !seeing.inputModalities.has("audio"));
|
|
39
40
|
assert.equal(tokensOf(await seeing.countPromptTokens([parts])), Math.ceil("look at this".length / 2));
|
|
40
|
-
await assert.rejects(seeing.generate({ messages: [parts], maxOutputTokens: 16, workerId: "1",
|
|
41
|
+
await assert.rejects(seeing.generate({ messages: [parts], maxOutputTokens: 16, workerId: "1", callKind: "bare" } as never));
|
|
41
42
|
assert.equal(seeing.received.length, 1, "the request was recorded before the empty queue refused it");
|
|
42
43
|
const received = seeing.received[0]?.[0]?.content;
|
|
43
44
|
assert.ok(Array.isArray(received) && received[1]?.type === "file" && received[1].mediaType === "image/png" && received[2]?.type === "file" && received[2].mediaType === "application/pdf");
|
package/src/sdkModels.test.ts
CHANGED
|
@@ -103,16 +103,6 @@ test("{§model-catalog-readiness}: an authenticated operator declaration reports
|
|
|
103
103
|
);
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
-
test("createSdkModel uses Models.dev provider facts and operator credentials", () => {
|
|
107
|
-
const sdk = createSdkModel("xai", "grok-build-0.1", { XAI_API_KEY: "test-key" });
|
|
108
|
-
assert.notEqual(sdk, null);
|
|
109
|
-
assert.equal(sdk?.catalog?.npm, "@ai-sdk/xai");
|
|
110
|
-
assert.notEqual(sdk?.languageModel, undefined);
|
|
111
|
-
assert.equal(sdk?.compatible, undefined);
|
|
112
|
-
assert.deepEqual(sdk?.cacheAffinity, { target: "header", name: "x-grok-conv-id" });
|
|
113
|
-
assert.notEqual(sdk?.normalizeCost, undefined);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
106
|
test("createSdkModel constructs Cerebras from Models.dev facts", () => {
|
|
117
107
|
const sdk = createSdkModel("cerebras", "gemma-4-31b", {
|
|
118
108
|
CEREBRAS_API_KEY: "test-key",
|
package/src/sdkModels.ts
CHANGED
|
@@ -7,7 +7,6 @@ import { createGroq } from "@ai-sdk/groq";
|
|
|
7
7
|
import { createMistral } from "@ai-sdk/mistral";
|
|
8
8
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
9
9
|
import { createTogetherAI } from "@ai-sdk/togetherai";
|
|
10
|
-
import { createXai } from "@ai-sdk/xai";
|
|
11
10
|
import { createOpenRouter, type OpenRouterChatSettings } from "@openrouter/ai-sdk-provider";
|
|
12
11
|
import {
|
|
13
12
|
resolveModel,
|
|
@@ -393,15 +392,6 @@ export const createSdkModel = (
|
|
|
393
392
|
},
|
|
394
393
|
catalog,
|
|
395
394
|
};
|
|
396
|
-
case "@ai-sdk/xai":
|
|
397
|
-
return {
|
|
398
|
-
languageModel: createXai({ apiKey: requireApiKey(provider, env, catalog), baseURL: url }).chat(model),
|
|
399
|
-
...(catalog.id === "xai"
|
|
400
|
-
? { cacheAffinity: { target: "header" as const, name: "x-grok-conv-id" } }
|
|
401
|
-
: {}),
|
|
402
|
-
...(normalizeCost === undefined ? {} : { normalizeCost }),
|
|
403
|
-
catalog,
|
|
404
|
-
};
|
|
405
395
|
case "@ai-sdk/anthropic":
|
|
406
396
|
return {
|
|
407
397
|
languageModel: createAnthropic({ apiKey: requireApiKey(provider, env, catalog), baseURL: url }).languageModel(model),
|
package/src/types.ts
CHANGED
|
@@ -163,20 +163,9 @@ export interface TokenLogprob {
|
|
|
163
163
|
readonly top?: readonly TokenAlternative[];
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
// {§provider-encrypted-reasoning} `id` is provider detail identity; `subtype`
|
|
167
|
-
// is the provider's evidence-backed classification. Neither is a client entity
|
|
168
|
-
// correlation, so consumers must not substitute `id` for a message/tool-call ID.
|
|
169
|
-
export interface ProviderEncryptedReasoningItem {
|
|
170
|
-
readonly id: string | null;
|
|
171
|
-
readonly subtype: string;
|
|
172
|
-
readonly encrypted: ReadonlyArray<{ data: string; format: string | null }>;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
166
|
export interface ProviderAssistant<TFinish extends ProviderAttemptFinishReason = FinishReason> {
|
|
176
167
|
readonly content: string;
|
|
177
168
|
readonly reasoning: string | null;
|
|
178
|
-
// Encrypted reasoning remains distinct from readable `reasoning`.
|
|
179
|
-
readonly reasoningEncrypted?: ReadonlyArray<ProviderEncryptedReasoningItem>;
|
|
180
169
|
readonly finishReason: TFinish;
|
|
181
170
|
readonly model: string;
|
|
182
171
|
// Per-token logprobs, present only when PLURNK_PROVIDERS_TOP_LOGPROBS is set
|
|
@@ -230,16 +219,13 @@ export type ProviderAttempt = ProviderResponse<ProviderAttemptFinishReason>;
|
|
|
230
219
|
export interface ProviderGenerateArgs {
|
|
231
220
|
readonly messages: ChatMessage[];
|
|
232
221
|
readonly workerId: string;
|
|
233
|
-
readonly primaryWorkerId?: string;
|
|
234
222
|
readonly signal?: AbortSignal;
|
|
235
223
|
readonly grammar?: string;
|
|
236
224
|
readonly maxOutputTokens?: number;
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
225
|
+
// The workspace this call serves: ordinary call context for an adapter that
|
|
226
|
+
// fronts more than one workspace. It is never transported to a backend and
|
|
227
|
+
// never model-facing.
|
|
240
228
|
readonly workspaceId?: string;
|
|
241
|
-
readonly loop?: number;
|
|
242
|
-
readonly turn?: number;
|
|
243
229
|
readonly sampling?: Record<string, unknown>;
|
|
244
230
|
readonly observeRequest?: ProviderRequestObserver;
|
|
245
231
|
readonly observeReasoning?: ProviderReasoningObserver;
|
|
@@ -265,42 +251,18 @@ export interface Provider {
|
|
|
265
251
|
// its content. The consumer never sees or chooses backend resources
|
|
266
252
|
// (slot integers, connections); the mechanism is the provider's.
|
|
267
253
|
//
|
|
268
|
-
// `attributions` is opaque consumer-supplied creator telemetry; the consumer
|
|
269
|
-
// owns what contribution that set claims ({§attribution}). `client` is the
|
|
270
|
-
// consumer's workspace-stable, self-identified frontend. They are forwarded ONLY by a
|
|
271
|
-
// provider whose spec opts in (the first-party `plurnk` endpoint, via
|
|
272
|
-
// `Plurnk-Attribution` / `Plurnk-Client` headers); every other provider DROPS
|
|
273
|
-
// them — the gate is structural so first-party metadata can never leak to a
|
|
274
|
-
// third-party backend.
|
|
275
|
-
//
|
|
276
254
|
// `sampling` is an optional bag of standard OpenAI-compat sampling params
|
|
277
255
|
// (temperature, top_p, top_k, min_p, penalties, stop, seed, …) forwarded into
|
|
278
256
|
// the request body UNDER the provider's managed fields — model/messages/grammar/
|
|
279
257
|
// reasoning/max_tokens/slot always win, and transport/protocol keys (stream,
|
|
280
258
|
// response_format, grammar, id_slot) are stripped, so it carries sampling intent
|
|
281
259
|
// only and can't bypass grammar transport ({§provider-request-authority}). A
|
|
282
|
-
// proxy consumer
|
|
283
|
-
//
|
|
284
|
-
// knobs through; a direct consumer typically leaves it unset.
|
|
285
|
-
//
|
|
286
|
-
// `strikes` is the worker's CURRENT rail-strike streak at time-of-generate
|
|
287
|
-
// (0 = clean; a clean turn zeroes it; every loop starts at 0 — contract
|
|
288
|
-
// {§strikes-first-party-metadata}). Forwarded as a `Plurnk-Strikes` header ONLY under the
|
|
289
|
-
// same firstPartyMetadata gate as attributions/client; dropped everywhere
|
|
290
|
-
// else. Headers only — the packet NEVER carries strike state (the model must
|
|
291
|
-
// not see engine accounting; it would become a metric to game).
|
|
292
|
-
//
|
|
293
|
-
// `workspaceId`/`loop`/`turn` are the turn coordinate ({§lifecycle-terms}) — the
|
|
294
|
-
// daemon-side sequence of the turn being generated, which the endpoint can
|
|
295
|
-
// never scrape from the wire. Forwarded as `Plurnk-Workspace-Id`/`Plurnk-Loop`/
|
|
296
|
-
// `Plurnk-Turn` ONLY under the same firstPartyMetadata gate; dropped
|
|
297
|
-
// everywhere else. Coordinates are 1-based: absent/0 emits no header (no
|
|
298
|
-
// strikes-style zero exception). Headers only, never the packet.
|
|
260
|
+
// proxy consumer fronting its own backends uses it to pass its caller's
|
|
261
|
+
// sampling knobs through; a direct consumer typically leaves it unset.
|
|
299
262
|
//
|
|
300
|
-
// `callKind` is the caller's explicit output contract
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
// supplies `emission` or `bare`.
|
|
263
|
+
// `callKind` is the caller's explicit output contract, never inferred from
|
|
264
|
+
// the request shape. Generic callers may omit it; Core always supplies
|
|
265
|
+
// `emission` or `bare`.
|
|
304
266
|
generate(args: ProviderGenerateArgs): Promise<ProviderResponse>;
|
|
305
267
|
// {§model-fact-resolution} — effective total context envelope in tokens,
|
|
306
268
|
// including any stricter operator cap. `null` means unknown; under
|