@plurnk/plurnk-providers 1.13.0 → 1.14.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.
@@ -208,7 +208,7 @@ test("instantiateProvider: per-alias knobs scope through to the provider (per-al
208
208
 
209
209
  test("public construction applies the package floor to a sparse consumer environment", async () => {
210
210
  const provider = await instantiateProvider(
211
- "fireworks",
211
+ "fireworks-ai",
212
212
  { FIREWORKS_API_KEY: "fw" },
213
213
  "deepseek-v4-pro-0813",
214
214
  async () => ({}),
@@ -290,7 +290,7 @@ test("{§deepseek-reasoning-request} #157: direct DeepSeek composes catalog fact
290
290
  test("an explicit malformed cache-affinity override still fails at its owning contract", async () => {
291
291
  await assert.rejects(
292
292
  () => instantiateProvider(
293
- "fireworks",
293
+ "fireworks-ai",
294
294
  {
295
295
  FIREWORKS_API_KEY: "fw",
296
296
  PLURNK_PROVIDERS_CACHE_AFFINITY: "malformed",
@@ -299,7 +299,7 @@ test("an explicit malformed cache-affinity override still fails at its owning co
299
299
  async () => ({}),
300
300
  mapOf({}),
301
301
  ),
302
- /fireworks provider: PLURNK_PROVIDERS_CACHE_AFFINITY must be "0" or "1"/,
302
+ /fireworks-ai provider: PLURNK_PROVIDERS_CACHE_AFFINITY must be "0" or "1"/,
303
303
  );
304
304
  });
305
305
 
@@ -307,7 +307,7 @@ test("a catalog provider with unknown model metadata never falls through to plug
307
307
  let scanned = false;
308
308
  await assert.rejects(
309
309
  () => instantiateProvider(
310
- "cloudflare",
310
+ "cloudflare-workers-ai",
311
311
  {
312
312
  CLOUDFLARE_ACCOUNT_ID: "account",
313
313
  CLOUDFLARE_API_KEY: "token",
@@ -317,13 +317,13 @@ test("a catalog provider with unknown model metadata never falls through to plug
317
317
  async () => {
318
318
  scanned = true;
319
319
  return {
320
- registry: new Map([["cloudflare", "@plurnk/plurnk-providers-cloudflare"]]),
320
+ registry: new Map([["cloudflare-workers-ai", "@plurnk/plurnk-providers-cloudflare"]]),
321
321
  skipped: new Map(),
322
322
  attributions: new Map(), grammarStyles: new Map(),
323
323
  };
324
324
  },
325
325
  ),
326
- /cloudflare provider: context window unresolved for "vendor\/model-outside-snapshot" — set PLURNK_PROVIDERS_CONTEXT_WINDOW or update the Models.dev snapshot/,
326
+ /cloudflare-workers-ai provider: context window unresolved for "vendor\/model-outside-snapshot" — set PLURNK_PROVIDERS_CONTEXT_WINDOW or update the Models.dev snapshot/,
327
327
  );
328
328
  assert.equal(scanned, false);
329
329
  });
@@ -331,7 +331,7 @@ test("a catalog provider with unknown model metadata never falls through to plug
331
331
  test("explicit metadata constructs an out-of-snapshot Cloudflare model in the consolidated transport", async () => {
332
332
  let scanned = false;
333
333
  const provider = await instantiateProvider(
334
- "cloudflare",
334
+ "cloudflare-workers-ai",
335
335
  {
336
336
  CLOUDFLARE_ACCOUNT_ID: "account",
337
337
  CLOUDFLARE_API_KEY: "token",
@@ -342,7 +342,7 @@ test("explicit metadata constructs an out-of-snapshot Cloudflare model in the co
342
342
  async () => {
343
343
  scanned = true;
344
344
  return {
345
- registry: new Map([["cloudflare", "@plurnk/plurnk-providers-cloudflare"]]),
345
+ registry: new Map([["cloudflare-workers-ai", "@plurnk/plurnk-providers-cloudflare"]]),
346
346
  skipped: new Map(),
347
347
  attributions: new Map(), grammarStyles: new Map(),
348
348
  };
@@ -378,7 +378,7 @@ test("{§provider-tagged-reasoning} a Cloudflare model alias carries its explici
378
378
  return new Response(body, { status: 200, headers: { "Content-Type": "text/event-stream" } });
379
379
  });
380
380
  const provider = await instantiateProvider(
381
- "cloudflare",
381
+ "cloudflare-workers-ai",
382
382
  {
383
383
  ...fullEnv,
384
384
  CLOUDFLARE_ACCOUNT_ID: "account",
@@ -424,15 +424,15 @@ test("two Fireworks aliases independently select default and priority service ti
424
424
  FIREWORKS_BASE_URL: "https://api.fireworks.ai/inference/v1",
425
425
  FIREWORKS_API_KEY: "fw",
426
426
  PLURNK_PROVIDERS_CONTEXT_WINDOW: "8192",
427
- PLURNK_PROVIDERS_PROVIDER_FIREWORKS_REASONING_STYLE: "effort_explicit",
427
+ PLURNK_PROVIDERS_PROVIDER_FIREWORKS_AI_REASONING_STYLE: "effort_explicit",
428
428
  PLURNK_PROVIDERS_TOP_LOGPROBS: "2",
429
429
  PLURNK_PROVIDERS_SERVICE_TIER_fast: "priority",
430
430
  PLURNK_PROVIDERS_SERVICE_TIER_standard: "default",
431
431
  };
432
432
  const imports = async () => ({});
433
433
  const discover = async () => ({ registry: new Map(), skipped: new Map(), attributions: new Map(), grammarStyles: new Map() });
434
- const fast = await instantiateProvider("fireworks", env, "accounts/fireworks/routers/glm-5p2-fast", imports, discover, undefined, "fast");
435
- const standard = await instantiateProvider("fireworks", env, "deepseek-v4-pro-0813", imports, discover, undefined, "standard");
434
+ const fast = await instantiateProvider("fireworks-ai", env, "accounts/fireworks/routers/glm-5p2-fast", imports, discover, undefined, "fast");
435
+ const standard = await instantiateProvider("fireworks-ai", env, "deepseek-v4-pro-0813", imports, discover, undefined, "standard");
436
436
  await fast.generate({ workerId: "fast-worker", messages: [] });
437
437
  await standard.generate({ workerId: "standard-worker", messages: [] });
438
438
  assert.deepEqual(bodies.map((body) => body.service_tier), ["priority", "default"]);
@@ -300,3 +300,38 @@ test("normalizeRetryAttemptError — attempt, first-content, and stream-idle dea
300
300
  const operation = new ProviderTimeoutError("operation", 2700000);
301
301
  assert.equal(normalizeRetryAttemptError(operation), operation);
302
302
  });
303
+
304
+ test("normalizeRetryAttemptError — a 2xx APICallError without a directive becomes retryable; a directive outranks; non-2xx untouched (#446)", () => {
305
+ const garbage = new APICallError({
306
+ message: "Failed to process successful response",
307
+ url: "https://api.example/v1/chat/completions",
308
+ requestBodyValues: {},
309
+ statusCode: 200,
310
+ responseBody: "not json",
311
+ isRetryable: false,
312
+ });
313
+ const normalized = normalizeRetryAttemptError(garbage) as APICallError;
314
+ assert.ok(APICallError.isInstance(normalized));
315
+ assert.equal(normalized.isRetryable, true, "2xx invalid-response consumes the retry budget");
316
+ assert.equal(normalized.cause, garbage, "the original failure rides as the cause");
317
+ assert.equal(normalized.statusCode, 200);
318
+
319
+ const directed = new APICallError({
320
+ message: "Failed to process successful response",
321
+ url: "https://api.example/v1/chat/completions",
322
+ requestBodyValues: {},
323
+ statusCode: 200,
324
+ responseHeaders: { "x-should-retry": "false" },
325
+ isRetryable: false,
326
+ });
327
+ assert.equal((normalizeRetryAttemptError(directed) as APICallError).isRetryable, false, "an explicit directive outranks the 2xx default");
328
+
329
+ const clientError = new APICallError({
330
+ message: "bad request",
331
+ url: "https://api.example/v1/chat/completions",
332
+ requestBodyValues: {},
333
+ statusCode: 400,
334
+ isRetryable: false,
335
+ });
336
+ assert.equal(normalizeRetryAttemptError(clientError), clientError, "non-2xx stays untouched");
337
+ });
@@ -282,6 +282,26 @@ export const normalizeRetryAttemptError = (error: unknown): unknown => {
282
282
  return error;
283
283
  }
284
284
  const directed = retryDirective(error.statusCode, error.responseHeaders ?? {});
285
+ // A 2xx APICallError with no explicit retry directive is a provider
286
+ // invalid-response: the exchange succeeded and the body was unusable (a
287
+ // serializer hiccup, a truncated frame). That is the same transient class as
288
+ // a transport failure and consumes the same bounded retry budget; an explicit
289
+ // `x-should-retry` directive outranks this default, and the terminal
290
+ // classification after the budget stays the non-retryable 502 (#446).
291
+ if (directed === null
292
+ && typeof error.statusCode === "number" && error.statusCode >= 200 && error.statusCode < 300
293
+ && error.isRetryable !== true) {
294
+ return retainStreamFailureValues(error, new APICallError({
295
+ message: error.message,
296
+ url: error.url,
297
+ requestBodyValues: error.requestBodyValues,
298
+ statusCode: error.statusCode,
299
+ responseHeaders: error.responseHeaders,
300
+ responseBody: error.responseBody,
301
+ cause: error,
302
+ isRetryable: true,
303
+ }));
304
+ }
285
305
  if (directed === null || directed === error.isRetryable) return error;
286
306
  return retainStreamFailureValues(error, new APICallError({
287
307
  message: error.message,
@@ -48,7 +48,7 @@ test("provider adapters advertise only reasoning policies they can preserve", ()
48
48
  PLURNK_PROVIDERS_REASONING: "adaptive",
49
49
  PLURNK_PROVIDERS_PROVIDER_DEEPSEEK_REASONING_STYLE: "thinking_effort",
50
50
  }, "deepseek-v4-flash");
51
- assert.deepEqual(deepseek?.supportedReasoningPolicies, ["off", "adaptive", "low", "high"]);
51
+ assert.deepEqual(deepseek?.supportedReasoningPolicies, ["off", "adaptive", "low", "high", "max"]);
52
52
 
53
53
  assert.throws(
54
54
  () => catalogProviderFromEnv("deepseek", {
@@ -72,7 +72,7 @@ test("provider adapters advertise only reasoning policies they can preserve", ()
72
72
  XAI_API_KEY: "test-key",
73
73
  PLURNK_PROVIDERS_REASONING: "adaptive",
74
74
  }, "grok-4.6");
75
- assert.deepEqual(grok?.supportedReasoningPolicies, ["adaptive", "low", "medium", "high"], "Grok 4.6 cannot disable reasoning");
75
+ assert.deepEqual(grok?.supportedReasoningPolicies, ["adaptive", "low", "medium", "high", "xhigh"], "Grok 4.6 cannot disable reasoning");
76
76
 
77
77
  const gemini = catalogProviderFromEnv("google", {
78
78
  ...env,
@@ -101,29 +101,29 @@ test("Models.dev controls Cloudflare's exact effort vocabulary", async () => {
101
101
  ...env,
102
102
  CLOUDFLARE_ACCOUNT_ID: "account",
103
103
  CLOUDFLARE_API_KEY: "token",
104
- PLURNK_PROVIDERS_PROVIDER_CLOUDFLARE_REASONING_STYLE: "effort_required",
104
+ PLURNK_PROVIDERS_PROVIDER_CLOUDFLARE_WORKERS_AI_REASONING_STYLE: "effort_required",
105
105
  };
106
- const low = catalogProviderFromEnv("cloudflare", {
106
+ const low = catalogProviderFromEnv("cloudflare-workers-ai", {
107
107
  ...cloudflareEnv,
108
108
  PLURNK_PROVIDERS_REASONING: "low",
109
109
  }, "@cf/qwen/qwen3.8-27b");
110
- assert.deepEqual(low?.supportedReasoningPolicies, ["adaptive", "low", "medium"]);
110
+ assert.deepEqual(low?.supportedReasoningPolicies, ["adaptive", "low", "medium", "xhigh"]);
111
111
  await low?.generate({ workerId: "cloudflare-low", messages: [{ role: "user", content: "hello" }] });
112
112
 
113
- const adaptive = catalogProviderFromEnv("cloudflare", {
113
+ const adaptive = catalogProviderFromEnv("cloudflare-workers-ai", {
114
114
  ...cloudflareEnv,
115
115
  PLURNK_PROVIDERS_REASONING: "adaptive",
116
116
  }, "@cf/qwen/qwen3.8-27b");
117
117
  await adaptive?.generate({ workerId: "cloudflare-adaptive", messages: [{ role: "user", content: "hello" }] });
118
118
 
119
- const nonReasoning = catalogProviderFromEnv("cloudflare", {
119
+ const nonReasoning = catalogProviderFromEnv("cloudflare-workers-ai", {
120
120
  ...cloudflareEnv,
121
121
  PLURNK_PROVIDERS_REASONING: "adaptive",
122
122
  }, "@cf/ibm-granite/granite-4.0-h-micro");
123
123
  assert.deepEqual(nonReasoning?.supportedReasoningPolicies, ["off", "adaptive"]);
124
124
  await nonReasoning?.generate({ workerId: "cloudflare-granite", messages: [{ role: "user", content: "hello" }] });
125
125
 
126
- const ungradedReasoner = catalogProviderFromEnv("cloudflare", {
126
+ const ungradedReasoner = catalogProviderFromEnv("cloudflare-workers-ai", {
127
127
  ...cloudflareEnv,
128
128
  PLURNK_PROVIDERS_REASONING: "adaptive",
129
129
  }, "@cf/zai-org/glm-5.3-flash");
@@ -132,11 +132,11 @@ test("Models.dev controls Cloudflare's exact effort vocabulary", async () => {
132
132
 
133
133
  assert.deepEqual(bodies.map((body) => body.reasoning_effort), ["low", "xhigh", undefined, undefined]);
134
134
  assert.throws(
135
- () => catalogProviderFromEnv("cloudflare", cloudflareEnv, "@cf/qwen/qwen3.8-27b"),
135
+ () => catalogProviderFromEnv("cloudflare-workers-ai", cloudflareEnv, "@cf/qwen/qwen3.8-27b"),
136
136
  /reasoning policy 'off' is unsupported; supported policies: adaptive, low, medium/,
137
137
  );
138
138
  assert.throws(
139
- () => catalogProviderFromEnv("cloudflare", {
139
+ () => catalogProviderFromEnv("cloudflare-workers-ai", {
140
140
  ...cloudflareEnv,
141
141
  PLURNK_PROVIDERS_REASONING: "high",
142
142
  }, "@cf/qwen/qwen3.8-27b"),
@@ -163,31 +163,31 @@ test("an operator-declared effort vocabulary extends Models.dev's for a provider
163
163
  ...env,
164
164
  CLOUDFLARE_ACCOUNT_ID: "account",
165
165
  CLOUDFLARE_API_KEY: "token",
166
- PLURNK_PROVIDERS_PROVIDER_CLOUDFLARE_REASONING_STYLE: "effort_required",
167
- PLURNK_PROVIDERS_PROVIDER_CLOUDFLARE_REASONING_EFFORTS: "low, medium,high",
166
+ PLURNK_PROVIDERS_PROVIDER_CLOUDFLARE_WORKERS_AI_REASONING_STYLE: "effort_required",
167
+ PLURNK_PROVIDERS_PROVIDER_CLOUDFLARE_WORKERS_AI_REASONING_EFFORTS: "low, medium,high",
168
168
  };
169
169
  // Models.dev lists this route as reasoning with no effort vocabulary; the declaration supplies one.
170
- const low = catalogProviderFromEnv("cloudflare", { ...declaredEnv, PLURNK_PROVIDERS_REASONING: "low" }, "@cf/zai-org/glm-5.3-flash");
170
+ const low = catalogProviderFromEnv("cloudflare-workers-ai", { ...declaredEnv, PLURNK_PROVIDERS_REASONING: "low" }, "@cf/zai-org/glm-5.3-flash");
171
171
  assert.deepEqual(low?.supportedReasoningPolicies, ["adaptive", "low", "medium", "high"]);
172
172
  await low?.generate({ workerId: "declared-low", messages: [{ role: "user", content: "hello" }] });
173
- const adaptive = catalogProviderFromEnv("cloudflare", { ...declaredEnv, PLURNK_PROVIDERS_REASONING: "adaptive" }, "@cf/zai-org/glm-5.3-flash");
173
+ const adaptive = catalogProviderFromEnv("cloudflare-workers-ai", { ...declaredEnv, PLURNK_PROVIDERS_REASONING: "adaptive" }, "@cf/zai-org/glm-5.3-flash");
174
174
  await adaptive?.generate({ workerId: "declared-adaptive", messages: [{ role: "user", content: "hello" }] });
175
175
  assert.deepEqual(bodies.map((body) => body.reasoning_effort), ["low", "high"], "a fixed level keeps its name; adaptive takes the strongest declared effort");
176
176
  // A declared `none` admits `off` on an effort transport; the catalog's own vocabulary stays in the union.
177
- const withOff = catalogProviderFromEnv("cloudflare", {
177
+ const withOff = catalogProviderFromEnv("cloudflare-workers-ai", {
178
178
  ...declaredEnv,
179
- PLURNK_PROVIDERS_PROVIDER_CLOUDFLARE_REASONING_EFFORTS: "none,high",
179
+ PLURNK_PROVIDERS_PROVIDER_CLOUDFLARE_WORKERS_AI_REASONING_EFFORTS: "none,high",
180
180
  }, "@cf/qwen/qwen3.8-27b");
181
- assert.deepEqual(withOff?.supportedReasoningPolicies, ["off", "adaptive", "low", "medium", "high"]);
181
+ assert.deepEqual(withOff?.supportedReasoningPolicies, ["off", "adaptive", "low", "medium", "high", "xhigh"]);
182
182
  // The declaration never turns a non-reasoning route into a reasoning one.
183
- const nonReasoning = catalogProviderFromEnv("cloudflare", { ...declaredEnv, PLURNK_PROVIDERS_REASONING: "adaptive" }, "@cf/ibm-granite/granite-4.0-h-micro");
183
+ const nonReasoning = catalogProviderFromEnv("cloudflare-workers-ai", { ...declaredEnv, PLURNK_PROVIDERS_REASONING: "adaptive" }, "@cf/ibm-granite/granite-4.0-h-micro");
184
184
  assert.deepEqual(nonReasoning?.supportedReasoningPolicies, ["off", "adaptive"]);
185
185
  assert.throws(
186
- () => catalogProviderFromEnv("cloudflare", {
186
+ () => catalogProviderFromEnv("cloudflare-workers-ai", {
187
187
  ...declaredEnv,
188
- PLURNK_PROVIDERS_PROVIDER_CLOUDFLARE_REASONING_EFFORTS: "low,turbo",
188
+ PLURNK_PROVIDERS_PROVIDER_CLOUDFLARE_WORKERS_AI_REASONING_EFFORTS: "low,turbo",
189
189
  }, "@cf/zai-org/glm-5.3-flash"),
190
- /PLURNK_PROVIDERS_PROVIDER_CLOUDFLARE_REASONING_EFFORTS has invalid value "turbo"; declarable efforts: none, minimal, low, medium, high, xhigh, max/,
190
+ /PLURNK_PROVIDERS_PROVIDER_CLOUDFLARE_WORKERS_AI_REASONING_EFFORTS has invalid value "turbo"; declarable efforts: none, minimal, low, medium, high, xhigh, max/,
191
191
  );
192
192
  });
193
193
 
@@ -686,6 +686,60 @@ test("native provider routes project their documented cache controls through the
686
686
  content: "changing user packet",
687
687
  });
688
688
  });
689
+
690
+ await t.test("{§provider-reasoning-policy} a resolved reasoning budget reaches the OpenRouter wire as max_tokens", async () => {
691
+ let call: { body: Record<string, unknown> } | undefined;
692
+ const server = createServer(async (request, response) => {
693
+ const chunks: Buffer[] = [];
694
+ for await (const chunk of request) chunks.push(Buffer.from(chunk));
695
+ call = { body: JSON.parse(Buffer.concat(chunks).toString("utf8")) as Record<string, unknown> };
696
+ response.writeHead(200, { "content-type": "text/event-stream" });
697
+ response.end([
698
+ `data: ${JSON.stringify({
699
+ id: "response",
700
+ object: "chat.completion.chunk",
701
+ created: 1,
702
+ model: "served",
703
+ choices: [{ index: 0, delta: { content: "ok" }, finish_reason: "stop" }],
704
+ })}`,
705
+ "data: [DONE]",
706
+ ].join("\n\n"));
707
+ });
708
+ server.listen(0, "127.0.0.1");
709
+ await once(server, "listening");
710
+ t.after(() => new Promise<void>((resolve, reject) => {
711
+ server.close((error) => error === undefined ? resolve() : reject(error));
712
+ }));
713
+ const address = server.address();
714
+ if (address === null || typeof address === "string") {
715
+ throw new Error("OpenRouter budget-capture server did not bind a TCP address");
716
+ }
717
+ const budgetEnv = {
718
+ ...env,
719
+ OPENROUTER_API_KEY: "test-key",
720
+ OPENROUTER_HTTP_REFERER: "https://github.com/plurnk/plurnk-service",
721
+ OPENROUTER_APP_TITLE: "Plurnk",
722
+ PLURNK_PROVIDERS_REASONING_BUDGET: "2048",
723
+ };
724
+ const adaptive = catalogProviderFromEnv("openrouter", {
725
+ ...budgetEnv,
726
+ PLURNK_PROVIDERS_REASONING: "adaptive",
727
+ }, "anthropic/claude-sonnet-4.6", `http://127.0.0.1:${address.port}/api/v1`);
728
+ await adaptive?.generate({
729
+ workerId: "openrouter-budget",
730
+ messages: [{ role: "user", content: "budgeted" }],
731
+ });
732
+ assert.deepEqual(call?.body.reasoning, { max_tokens: 2048 });
733
+ const fixed = catalogProviderFromEnv("openrouter", {
734
+ ...budgetEnv,
735
+ PLURNK_PROVIDERS_REASONING: "low",
736
+ }, "anthropic/claude-sonnet-4.6", `http://127.0.0.1:${address.port}/api/v1`);
737
+ await fixed?.generate({
738
+ workerId: "openrouter-budget",
739
+ messages: [{ role: "user", content: "budgeted" }],
740
+ });
741
+ assert.deepEqual(call?.body.reasoning, { effort: "low" }, "a fixed policy keeps the effort form");
742
+ });
689
743
  });
690
744
 
691
745
  test("cataloged unknown model fails unless its context is explicit", () => {
@@ -749,3 +803,51 @@ test("Models.dev is the only fallback rate table", async () => {
749
803
  reason: "Models.dev has no complete rate for this model",
750
804
  });
751
805
  });
806
+
807
+ test("{§operator-cost-override} declared rates overlay the catalog and the source names the override", async () => {
808
+ mock.method(globalThis, "fetch", async () => new Response([
809
+ `data: ${JSON.stringify({
810
+ id: "response",
811
+ model: "served",
812
+ choices: [{ index: 0, delta: { content: "ok" }, finish_reason: "stop" }],
813
+ })}`,
814
+ `data: ${JSON.stringify({
815
+ id: "response",
816
+ model: "served",
817
+ choices: [],
818
+ usage: {
819
+ prompt_tokens: 1_000,
820
+ prompt_tokens_details: { cached_tokens: 400 },
821
+ completion_tokens: 100,
822
+ total_tokens: 1_150,
823
+ },
824
+ })}`,
825
+ "data: [DONE]",
826
+ ].join("\n\n"), { headers: { "content-type": "text/event-stream" } }));
827
+ const overridden = catalogProviderFromEnv("deepseek", {
828
+ ...env,
829
+ DEEPSEEK_API_KEY: "test-key",
830
+ PLURNK_PROVIDERS_REASONING: "adaptive",
831
+ PLURNK_PROVIDERS_PROVIDER_DEEPSEEK_REASONING_STYLE: "thinking_effort",
832
+ PLURNK_PROVIDERS_COST: "input=0.22,output=0.66,cacheRead=0.007",
833
+ }, "deepseek-v4-flash");
834
+ const response = await overridden!.generate({ workerId: "overridden", messages: [] });
835
+ assert.deepEqual(response.accounting[0]?.cost, {
836
+ kind: "estimated",
837
+ amount: { amount: "0.0002148", currency: "USD" },
838
+ source: "operator PLURNK_PROVIDERS_COST override over Models.dev catalog rates",
839
+ });
840
+ mock.restoreAll();
841
+ });
842
+
843
+ test("(#458) declared efforts union into the supported set under the models.dev-id prefix", () => {
844
+ const provider = catalogProviderFromEnv("fireworks-ai", {
845
+ ...env,
846
+ FIREWORKS_API_KEY: "test-key",
847
+ PLURNK_PROVIDERS_REASONING: "adaptive",
848
+ PLURNK_PROVIDERS_PROVIDER_FIREWORKS_AI_REASONING_STYLE: "effort_explicit",
849
+ PLURNK_PROVIDERS_PROVIDER_FIREWORKS_AI_REASONING_EFFORTS: "low,high,max",
850
+ }, "accounts/fireworks/models/glm-5p3-flash");
851
+ // (#474) "max" joined the portable vocabulary; "off" still requires a declared "none".
852
+ assert.deepEqual(provider?.supportedReasoningPolicies, ["adaptive", "low", "high", "max"]);
853
+ });
@@ -5,6 +5,7 @@ import {
5
5
  type ModelReasoningEffort,
6
6
  } from "@plurnk/plurnk-models";
7
7
  import {
8
+ costOverrideFromEnv,
8
9
  contextWindowFromEnv,
9
10
  effectiveContextWindow,
10
11
  dataCaptureFromEnv,
@@ -133,7 +134,8 @@ const catalogSupportedReasoningPolicies = ({
133
134
  || (catalogSupportsToggle(info) && compatibleToggleStyles.has(style));
134
135
  return REASONING_POLICIES.filter((policy) => policy === "adaptive"
135
136
  || policy === "off" && off
136
- || (policy === "low" || policy === "medium" || policy === "high")
137
+ || (policy === "low" || policy === "medium" || policy === "high"
138
+ || policy === "xhigh" || policy === "max")
137
139
  && effortTransport
138
140
  && efforts.has(policy));
139
141
  };
@@ -289,7 +291,7 @@ export const providerFromSdkModel = ({
289
291
  : "none" as const;
290
292
 
291
293
  const catalogCost = info?.cost;
292
- const rates = catalogCost === undefined ? null : {
294
+ const catalogRates = catalogCost === undefined ? null : {
293
295
  input: catalogCost.inputPer1M,
294
296
  output: catalogCost.outputPer1M,
295
297
  ...(catalogCost.reasoningPer1M === undefined
@@ -302,8 +304,23 @@ export const providerFromSdkModel = ({
302
304
  ? {}
303
305
  : { cacheWrite: catalogCost.cacheWritePer1M }),
304
306
  };
307
+ // {§operator-cost-override} (#461): the catalog is the starting point; a declared
308
+ // override merges over it, and the estimate's source names the overlay.
309
+ const costOverride = costOverrideFromEnv(env, name);
310
+ if (costOverride !== null && catalogRates === null
311
+ && (costOverride.input === undefined || costOverride.output === undefined)) {
312
+ throw new Error(
313
+ `${name} provider: PLURNK_PROVIDERS_COST without catalog rates must declare input and output`,
314
+ );
315
+ }
316
+ const rates = costOverride === null
317
+ ? catalogRates
318
+ : { ...(catalogRates ?? {}), ...costOverride } as NonNullable<typeof catalogRates>;
319
+ const rateSource = costOverride === null
320
+ ? "Models.dev catalog rates"
321
+ : "operator PLURNK_PROVIDERS_COST override over Models.dev catalog rates";
305
322
  const estimateCost = (usage: Parameters<typeof estimateProviderCost>[0]) =>
306
- estimateProviderCost(usage, rates, "Models.dev catalog rates");
323
+ estimateProviderCost(usage, rates, rateSource);
307
324
  const affinityEnabled = cacheAffinityFromEnv(env, name);
308
325
  const cacheWritePolicy = cacheWritePolicyFromEnv(env, name);
309
326
 
@@ -319,6 +336,8 @@ export const providerFromSdkModel = ({
319
336
  maxOutputTokens,
320
337
  outputBudget: envelope.outputBudget,
321
338
  reasoningBudget: reasoning.budget,
339
+ // #457 — a catalog-declared toggle control makes `adaptive` an explicit enable.
340
+ reasoningToggle: info?.reasoningOptions?.some((option) => option.type === "toggle") === true,
322
341
  supportedReasoningPolicies: supportedReasoningPolicies({
323
342
  info,
324
343
  native: languageModel !== undefined,
@@ -377,9 +396,6 @@ export const catalogProviderFromEnv = (
377
396
  );
378
397
  }
379
398
  const wireModel = resolved?.id ?? model;
380
- const sdk = createSdkModel(name, wireModel, env, baseUrlOverride, reasoningFromEnv(env, name).mode);
381
- if (sdk === null) return null;
382
-
383
399
  const info = resolved?.info;
384
400
  const contextWindow = effectiveContextWindow(contextOverride, info?.contextWindow ?? null);
385
401
  if (contextWindow === null) {
@@ -387,6 +403,17 @@ export const catalogProviderFromEnv = (
387
403
  `${name} provider: context window unresolved for "${wireModel}" — set PLURNK_PROVIDERS_CONTEXT_WINDOW or update the Models.dev snapshot`,
388
404
  );
389
405
  }
406
+ const maxOutputTokens = info?.maxOutputTokens === undefined
407
+ ? null
408
+ : Math.min(info.maxOutputTokens, contextWindow);
409
+ const reasoning = reasoningFromEnv(
410
+ env,
411
+ name,
412
+ generationEnvelopeFromEnv(env, name, contextWindow, maxOutputTokens).reasoningBudget,
413
+ );
414
+ const sdk = createSdkModel(name, wireModel, env, baseUrlOverride, reasoning.mode, reasoning.budget);
415
+ if (sdk === null) return null;
416
+
390
417
  return providerFromSdkModel({
391
418
  name,
392
419
  env,
package/src/env.test.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import test from "node:test";
2
2
  import { strict as assert } from "node:assert";
3
3
  import {
4
+ scopeEnvToAlias,
5
+ costOverrideFromEnv,
4
6
  cacheAffinityFromEnv,
5
7
  cacheWritePolicyFromEnv,
6
8
  generationEnvelopeFromEnv,
@@ -55,19 +57,19 @@ test("reasoningFromEnv: durable policy is independent from an optional explicit
55
57
  assert.deepEqual(reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: "high" }, "openai", 4096), { mode: "high", budget: 4096 });
56
58
  assert.deepEqual(reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: "adaptive" }, "openai", 4096), { mode: "adaptive", budget: 4096 });
57
59
  assert.throws(() => reasoningFromEnv({}, "openai"), /PLURNK_PROVIDERS_REASONING must be set/);
58
- assert.throws(() => reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: "8192" }, "openai"), /must be one of "off", "adaptive", "low", "medium", "high"/); // the old numeric habit fails loudly
60
+ assert.throws(() => reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: "8192" }, "openai"), /must be one of "off", "adaptive", "low", "medium", "high", "xhigh", "max"/); // the old numeric habit fails loudly
59
61
  });
60
62
 
61
63
  test("{§provider-tagged-reasoning} response style is explicit and invalid values fail at the provider boundary", () => {
62
- assert.equal(reasoningResponseStyleFromEnv({}, "cloudflare"), "verbatim");
64
+ assert.equal(reasoningResponseStyleFromEnv({}, "cloudflare-workers-ai"), "verbatim");
63
65
  assert.equal(reasoningResponseStyleFromEnv({
64
66
  PLURNK_PROVIDERS_REASONING_RESPONSE_STYLE: "think-tags",
65
- }, "cloudflare"), "think-tags");
67
+ }, "cloudflare-workers-ai"), "think-tags");
66
68
  assert.throws(
67
69
  () => reasoningResponseStyleFromEnv({
68
70
  PLURNK_PROVIDERS_REASONING_RESPONSE_STYLE: "auto",
69
- }, "cloudflare"),
70
- /cloudflare provider: PLURNK_PROVIDERS_REASONING_RESPONSE_STYLE must be "verbatim" or "think-tags" \(got "auto"\)/,
71
+ }, "cloudflare-workers-ai"),
72
+ /cloudflare-workers-ai provider: PLURNK_PROVIDERS_REASONING_RESPONSE_STYLE must be "verbatim" or "think-tags" \(got "auto"\)/,
71
73
  );
72
74
  });
73
75
 
@@ -97,7 +99,7 @@ test("the generic prompt-cache-key knob is retired rather than retained as a com
97
99
  () => cacheAffinityFromEnv({
98
100
  PLURNK_PROVIDERS_PROMPT_CACHE_KEY: "1",
99
101
  PLURNK_PROVIDERS_CACHE_AFFINITY: "1",
100
- }, "fireworks"),
102
+ }, "fireworks-ai"),
101
103
  /PLURNK_PROVIDERS_PROMPT_CACHE_KEY was renamed to PLURNK_PROVIDERS_CACHE_AFFINITY/,
102
104
  );
103
105
  });
@@ -225,16 +227,16 @@ test("still-set old THINKING names fail hard with the rename pointer", () => {
225
227
  });
226
228
 
227
229
  test("reasoning policy accepts only the exact portable durable vocabulary", () => {
228
- for (const mode of ["off", "adaptive", "low", "medium", "high"] as const) {
230
+ for (const mode of ["off", "adaptive", "low", "medium", "high", "xhigh", "max"] as const) {
229
231
  assert.deepEqual(reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: mode }, "openai"), {
230
232
  mode,
231
233
  budget: null,
232
234
  });
233
235
  }
234
- for (const retired of ["on", "minimal", "xhigh"]) {
236
+ for (const retired of ["on", "minimal", "ultra"]) {
235
237
  assert.throws(
236
238
  () => reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: retired }, "openai"),
237
- /must be one of "off", "adaptive", "low", "medium", "high"/,
239
+ /must be one of "off", "adaptive", "low", "medium", "high", "xhigh", "max"/,
238
240
  );
239
241
  }
240
242
  });
@@ -312,3 +314,18 @@ test("a retired envelope knob is refused in its per-alias form too, naming the k
312
314
  /PLURNK_PROVIDERS_REASONING_RESERVE_rtxgemma is retired/,
313
315
  );
314
316
  });
317
+
318
+ test("{§operator-cost-override} costOverrideFromEnv parses the catalog vocabulary and refuses drift", () => {
319
+ assert.equal(costOverrideFromEnv({}, "x"), null);
320
+ assert.deepEqual(
321
+ costOverrideFromEnv({ PLURNK_PROVIDERS_COST: "input=0.22, output=0.66,cacheRead=0.007" }, "x"),
322
+ { input: 0.22, output: 0.66, cacheRead: 0.007 },
323
+ );
324
+ assert.throws(() => costOverrideFromEnv({ PLURNK_PROVIDERS_COST: "cache_read=0.007" }, "x"), /key=value over input, output, reasoning, cacheRead, cacheWrite/);
325
+ assert.throws(() => costOverrideFromEnv({ PLURNK_PROVIDERS_COST: "input=" }, "x"), /non-negative per-1M-token USD rate/);
326
+ assert.throws(() => costOverrideFromEnv({ PLURNK_PROVIDERS_COST: "input=-1" }, "x"), /non-negative/);
327
+ assert.throws(() => costOverrideFromEnv({ PLURNK_PROVIDERS_COST: "input=0.1,input=0.2" }, "x"), /repeats input/);
328
+ // Alias-scoped like every provider knob.
329
+ const scoped = scopeEnvToAlias({ PLURNK_PROVIDERS_COST_deepdumb: "input=0.44,output=1.32" }, "deepdumb");
330
+ assert.deepEqual(costOverrideFromEnv(scoped, "x"), { input: 0.44, output: 1.32 });
331
+ });
package/src/env.ts CHANGED
@@ -291,6 +291,7 @@ export const reasoningFromEnv = (
291
291
  // facts (API keys, canonical endpoints) remain vendor-named; the per-alias
292
292
  // endpoint override stays PLURNK_BASEURL_<alias> (its existing precedent).
293
293
  export const PROVIDERS_KNOBS = Object.freeze([
294
+ "PLURNK_PROVIDERS_COST",
294
295
  "PLURNK_PROVIDERS_OUTPUT_BUDGET",
295
296
  "PLURNK_PROVIDERS_REASONING_RESPONSE_STYLE",
296
297
  "PLURNK_PROVIDERS_REASONING_BUDGET",
@@ -330,6 +331,34 @@ export const PROVIDERS_KNOBS = Object.freeze([
330
331
  // same parser — e.g. service loop policy or prompt projection — without
331
332
  // reimplementing the suffix/collision rules. Default stays the
332
333
  // providers-family list; provider call sites pass nothing.
334
+ // {§operator-cost-override} (#461) — Models.dev is the rate starting point; the
335
+ // operator overlays exact per-1M-token USD rates when a provider reprices ahead
336
+ // of the catalog. Format: comma-separated key=value over the catalog vocabulary
337
+ // (input, output, reasoning, cacheRead, cacheWrite). Alias-scoped like every
338
+ // provider knob.
339
+ const COST_OVERRIDE_KEYS = Object.freeze(["input", "output", "reasoning", "cacheRead", "cacheWrite"] as const);
340
+ export type CostOverride = Partial<Record<(typeof COST_OVERRIDE_KEYS)[number], number>>;
341
+ export const costOverrideFromEnv = (env: NodeJS.ProcessEnv, label: string): CostOverride | null => {
342
+ const name = "PLURNK_PROVIDERS_COST";
343
+ const raw = env[name];
344
+ if (raw === undefined || raw.trim().length === 0) return null;
345
+ const out: Partial<Record<string, number>> = {};
346
+ for (const part of raw.split(",")) {
347
+ const [key, value, ...rest] = part.split("=").map((piece) => piece.trim());
348
+ if (rest.length > 0 || key === undefined || value === undefined
349
+ || !(COST_OVERRIDE_KEYS as readonly string[]).includes(key)) {
350
+ throw new Error(`${label} provider: ${name} entries are key=value over ${COST_OVERRIDE_KEYS.join(", ")} (per-1M-token USD); got ${JSON.stringify(part)}`);
351
+ }
352
+ if (key in out) throw new Error(`${label} provider: ${name} repeats ${key}`);
353
+ const parsed = Number(value);
354
+ if (value.length === 0 || !Number.isFinite(parsed) || parsed < 0) {
355
+ throw new Error(`${label} provider: ${name} ${key} must be a non-negative per-1M-token USD rate; got ${JSON.stringify(value)}`);
356
+ }
357
+ out[key] = parsed;
358
+ }
359
+ return out as CostOverride;
360
+ };
361
+
333
362
  export const scopeEnvToAlias = (env: NodeJS.ProcessEnv, alias: string, knobs: readonly string[] = PROVIDERS_KNOBS): NodeJS.ProcessEnv => {
334
363
  const folded = alias.toLowerCase();
335
364
  const out: NodeJS.ProcessEnv = { ...env };
@@ -2,7 +2,7 @@ import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
- import { lookupProvider, providerIdMap } from "@plurnk/plurnk-models";
5
+ import { lookupProvider } from "@plurnk/plurnk-models";
6
6
 
7
7
  const defaultsPath = fileURLToPath(new URL("../.env.defaults", import.meta.url));
8
8
  const declarations = readFileSync(defaultsPath, "utf8")
@@ -19,16 +19,17 @@ const providerFact = (key: string): { provider: string; fact: string } | null =>
19
19
  };
20
20
 
21
21
  test("{§provider-fact-authority} package defaults never redefine cataloged provider facts", () => {
22
- const ids = providerIdMap();
22
+ // (#459) the env prefix IS the Models.dev id, underscored; probe both spellings.
23
23
  for (const { key, value } of declarations) {
24
24
  const fact = providerFact(key);
25
25
  if (fact === null) continue;
26
- const catalogId = ids[fact.provider] ?? fact.provider;
27
- assert.equal(
28
- lookupProvider(catalogId),
29
- null,
30
- `package default '${key}=${value}' redefines a Models.dev-cataloged provider fact (${catalogId})`,
31
- );
26
+ for (const candidate of new Set([fact.provider, fact.provider.replaceAll("_", "-")])) {
27
+ assert.equal(
28
+ lookupProvider(candidate),
29
+ null,
30
+ `package default '${key}=${value}' redefines a Models.dev-cataloged provider fact (${candidate})`,
31
+ );
32
+ }
32
33
  }
33
34
  });
34
35