@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.
Files changed (72) hide show
  1. package/.env.defaults +8 -4
  2. package/README.md +4 -16
  3. package/SPEC.md +31 -30
  4. package/dist/AiSdkProvider.d.ts +1 -3
  5. package/dist/AiSdkProvider.d.ts.map +1 -1
  6. package/dist/AiSdkProvider.js +7 -20
  7. package/dist/AiSdkProvider.js.map +1 -1
  8. package/dist/AiSdkRequestBody.d.ts +1 -4
  9. package/dist/AiSdkRequestBody.d.ts.map +1 -1
  10. package/dist/AiSdkRequestBody.js +1 -48
  11. package/dist/AiSdkRequestBody.js.map +1 -1
  12. package/dist/Mock.d.ts +3 -4
  13. package/dist/Mock.d.ts.map +1 -1
  14. package/dist/Mock.js +3 -4
  15. package/dist/Mock.js.map +1 -1
  16. package/dist/ProviderRegistry.js +2 -2
  17. package/dist/ProviderRegistry.js.map +1 -1
  18. package/dist/accounting.d.ts +0 -1
  19. package/dist/accounting.d.ts.map +1 -1
  20. package/dist/accounting.js +1 -32
  21. package/dist/accounting.js.map +1 -1
  22. package/dist/aiSdkTransport.d.ts +0 -8
  23. package/dist/aiSdkTransport.d.ts.map +1 -1
  24. package/dist/aiSdkTransport.js +23 -39
  25. package/dist/aiSdkTransport.js.map +1 -1
  26. package/dist/catalogProvider.d.ts.map +1 -1
  27. package/dist/catalogProvider.js +10 -2
  28. package/dist/catalogProvider.js.map +1 -1
  29. package/dist/compatibleProvider.d.ts +1 -1
  30. package/dist/compatibleProvider.d.ts.map +1 -1
  31. package/dist/compatibleProvider.js +13 -25
  32. package/dist/compatibleProvider.js.map +1 -1
  33. package/dist/env.d.ts.map +1 -1
  34. package/dist/env.js +1 -0
  35. package/dist/env.js.map +1 -1
  36. package/dist/index.d.ts +2 -2
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +1 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/sdkModels.d.ts.map +1 -1
  41. package/dist/sdkModels.js +0 -10
  42. package/dist/sdkModels.js.map +1 -1
  43. package/dist/types.d.ts +0 -15
  44. package/dist/types.d.ts.map +1 -1
  45. package/package.json +7 -8
  46. package/src/AiSdkProvider.test.ts +97 -246
  47. package/src/AiSdkProvider.ts +7 -24
  48. package/src/AiSdkRequestBody.ts +2 -42
  49. package/src/Mock.test.ts +11 -3
  50. package/src/Mock.ts +4 -6
  51. package/src/ProviderRegistry.test.ts +0 -1
  52. package/src/ProviderRegistry.ts +1 -1
  53. package/src/accounting.test.ts +3 -25
  54. package/src/accounting.ts +0 -34
  55. package/src/aiSdkTransport.test.ts +2 -1
  56. package/src/aiSdkTransport.ts +25 -41
  57. package/src/boundaries.test.ts +1 -1
  58. package/src/capacity.test.ts +2 -2
  59. package/src/catalogProvider.test.ts +7 -85
  60. package/src/catalogProvider.ts +10 -2
  61. package/src/compatibleProvider.test.ts +6 -6
  62. package/src/compatibleProvider.ts +13 -28
  63. package/src/cost.test.ts +3 -3
  64. package/src/discover.test.ts +11 -2
  65. package/src/env.test.ts +3 -0
  66. package/src/env.ts +2 -1
  67. package/src/errors.test.ts +2 -2
  68. package/src/index.ts +1 -2
  69. package/src/inputModalities.test.ts +2 -1
  70. package/src/sdkModels.test.ts +0 -10
  71. package/src/sdkModels.ts +0 -10
  72. 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("xai", env, "not-in-the-catalog"),
776
+ () => catalogProviderFromEnv("groq", env, "not-in-the-catalog"),
856
777
  /context window unresolved/,
857
778
  );
858
- const provider = catalogProviderFromEnv("xai", {
779
+ const provider = catalogProviderFromEnv("groq", {
859
780
  ...env,
860
- XAI_API_KEY: "test-key",
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("xai", {
825
+ const uncataloged = catalogProviderFromEnv("groq", {
905
826
  ...env,
906
- XAI_API_KEY: "test-key",
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
+
@@ -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 value = env[`PLURNK_PROVIDERS_PROVIDER_${prefix}_REASONING_STYLE`];
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: PLURNK_PROVIDERS_PROVIDER_${prefix}_REASONING_STYLE has invalid value "${value}"`);
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("openai", env, "local");
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("openai", {
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("openai", { ...env, PLURNK_PROVIDERS_REASONING: "medium" }, "local");
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("openai", env, "local");
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("openai", env, "local");
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 chatUrl = (
31
- provider: "openai" | "plurnk",
32
- env: NodeJS.ProcessEnv,
33
- override?: string,
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: ${provider === "openai" ? "OPENAI_BASE_URL or OPENAI_API_BASE" : "PLURNK_BASE_URL"} must be set`);
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
- if (provider === "openai") return `${base.replace(/\/v1$/, "")}/v1/chat/completions`;
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
- // first-party compatible route declares no vendor cache projection. llama-server
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(provider, env, baseUrlOverride);
128
- const apiKey = provider === "openai" ? env.OPENAI_API_KEY : env.PLURNK_API_KEY;
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 = provider === "openai" && (pinned ?? probe.llamaServer);
149
- if (probe.failed && pinned === null && provider === "openai") {
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 = provider === "openai" ? "think" : "none";
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" },
@@ -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 (unset/empty/0): every provider is trusted", async (t) => {
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 [undefined, "", "0"]) {
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. TASK inventory is separate from provider reasoning.
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",
@@ -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
- ProviderEncryptedReasoningItem,
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", primaryWorkerId: "1", callKind: "bare" } as never));
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");
@@ -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
- readonly attributions?: string[];
238
- readonly client?: string;
239
- readonly strikes?: number;
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 (the
283
- // plurnk endpoint fronting its own backends) uses it to pass its caller's sampling
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. It is transported as
301
- // `Plurnk-Call-Kind` only under the first-party metadata gate and never
302
- // inferred from the request shape. Generic callers may omit it; Core always
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