@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.
@@ -37,21 +37,21 @@ test("{§model-catalog-readiness}: readiness uses the construction credential an
37
37
  });
38
38
  assert.doesNotMatch(JSON.stringify(providerReadiness("google", { GEMINI_API_KEY: "secret-value" })), /secret-value/);
39
39
 
40
- assert.deepEqual(providerReadiness("cloudflare", { CLOUDFLARE_API_KEY: "key" }), {
40
+ assert.deepEqual(providerReadiness("cloudflare-workers-ai", { CLOUDFLARE_API_KEY: "key" }), {
41
41
  ready: false,
42
42
  causes: [{
43
43
  kind: "configuration",
44
44
  alternatives: [["CLOUDFLARE_ACCOUNT_ID"]],
45
45
  }],
46
46
  });
47
- assert.deepEqual(providerReadiness("cloudflare", {
47
+ assert.deepEqual(providerReadiness("cloudflare-workers-ai", {
48
48
  CLOUDFLARE_ACCOUNT_ID: "account",
49
49
  CLOUDFLARE_API_KEY: "key",
50
50
  }), { ready: true, causes: [] });
51
51
  });
52
52
 
53
53
  test("{§model-catalog-readiness}: Bedrock reports its actual alternative authentication sets and region requirement", () => {
54
- assert.deepEqual(providerReadiness("bedrock", {}), {
54
+ assert.deepEqual(providerReadiness("amazon-bedrock", {}), {
55
55
  ready: false,
56
56
  causes: [{
57
57
  kind: "credential",
@@ -63,11 +63,11 @@ test("{§model-catalog-readiness}: Bedrock reports its actual alternative authen
63
63
  ],
64
64
  }],
65
65
  });
66
- assert.deepEqual(providerReadiness("bedrock", {
66
+ assert.deepEqual(providerReadiness("amazon-bedrock", {
67
67
  AWS_BEARER_TOKEN_BEDROCK: "bearer",
68
68
  AWS_REGION: "us-east-1",
69
69
  }), { ready: true, causes: [] });
70
- assert.deepEqual(providerReadiness("bedrock", {
70
+ assert.deepEqual(providerReadiness("amazon-bedrock", {
71
71
  AWS_ACCESS_KEY_ID: "access",
72
72
  AWS_SECRET_ACCESS_KEY: "secret",
73
73
  }), {
@@ -129,14 +129,25 @@ test("{§provider-reasoning-policy} createSdkModel represents a fixed OpenRouter
129
129
  OPENROUTER_HTTP_REFERER: "https://github.com/plurnk/plurnk-service",
130
130
  OPENROUTER_APP_TITLE: "Plurnk",
131
131
  };
132
- const settings = (reasoning?: "off" | "adaptive" | "low" | "medium" | "high"): unknown =>
133
- (createSdkModel("openrouter", "z-ai/glm-5.3-flash", env, undefined, reasoning)?.languageModel as { settings?: { reasoning?: unknown } } | undefined)?.settings?.reasoning;
132
+ const settings = (reasoning?: "off" | "adaptive" | "low" | "medium" | "high", budget: number | null = null): unknown =>
133
+ (createSdkModel("openrouter", "z-ai/glm-5.3-flash", env, undefined, reasoning, budget)?.languageModel as { settings?: { reasoning?: unknown } } | undefined)?.settings?.reasoning;
134
134
  assert.deepEqual(settings("low"), { effort: "low" });
135
135
  assert.deepEqual(settings("medium"), { effort: "medium" });
136
136
  assert.deepEqual(settings("high"), { effort: "high" });
137
137
  assert.deepEqual(settings("off"), { effort: "none" });
138
- assert.equal(settings("adaptive"), undefined);
139
- assert.equal(settings(), undefined);
138
+ // #457 — adaptive on a reasoning-capable route declares the toggle on.
139
+ assert.deepEqual(settings("adaptive"), { enabled: true });
140
+ assert.equal(settings(), undefined, "no declared policy emits nothing");
141
+ // A resolved budget is the max_tokens form of the same dial; a fixed policy keeps effort.
142
+ assert.deepEqual(settings("adaptive", 2048), { max_tokens: 2048 });
143
+ assert.equal(settings(undefined, 2048), undefined, "a budget rides a policy, never replaces one");
144
+ assert.deepEqual(settings("low", 2048), { effort: "low" });
145
+ assert.deepEqual(settings("off", 2048), { effort: "none" });
146
+ // The budget_tokens-only motivating route (#466) and a non-reasoning route.
147
+ const qwen = (createSdkModel("openrouter", "qwen/qwen3.8-flash", env, undefined, "adaptive", null)?.languageModel as { settings?: { reasoning?: unknown } } | undefined)?.settings?.reasoning;
148
+ assert.deepEqual(qwen, { enabled: true });
149
+ const prose = (createSdkModel("openrouter", "tencent/hy-mt2-30b-a3b", env, undefined, "adaptive", null)?.languageModel as { settings?: { reasoning?: unknown } } | undefined)?.settings?.reasoning;
150
+ assert.equal(prose, undefined, "a non-reasoning route stays untouched");
140
151
  });
141
152
 
142
153
  test("{§openrouter-app-attribution} attribution rejects malformed URLs and the retired title name", () => {
@@ -183,7 +194,7 @@ test("createSdkModel attaches DeepInfra's documented response-cost normalizer",
183
194
  });
184
195
 
185
196
  test("{§provider-fact-authority} catalog credential names are law without any package or operator alias", () => {
186
- const sdk = createSdkModel("cloudflare", "@cf/google/gemma-4-26b-a4b-it", {
197
+ const sdk = createSdkModel("cloudflare-workers-ai", "@cf/google/gemma-4-26b-a4b-it", {
187
198
  CLOUDFLARE_ACCOUNT_ID: "account",
188
199
  CLOUDFLARE_API_KEY: "key",
189
200
  });
@@ -196,10 +207,10 @@ test("{§provider-fact-authority} catalog credential names are law without any p
196
207
  });
197
208
 
198
209
  test("{§provider-fact-authority} an operator credential override holds one exact name", () => {
199
- const sdk = createSdkModel("cloudflare", "@cf/google/gemma-4-26b-a4b-it", {
210
+ const sdk = createSdkModel("cloudflare-workers-ai", "@cf/google/gemma-4-26b-a4b-it", {
200
211
  CLOUDFLARE_ACCOUNT_ID: "account",
201
212
  MY_ORG_CLOUDFLARE_KEY: "key",
202
- PLURNK_PROVIDERS_PROVIDER_CLOUDFLARE_API_KEY_ENV: "MY_ORG_CLOUDFLARE_KEY",
213
+ PLURNK_PROVIDERS_PROVIDER_CLOUDFLARE_WORKERS_AI_API_KEY_ENV: "MY_ORG_CLOUDFLARE_KEY",
203
214
  });
204
215
  assert.deepEqual(sdk?.compatible, {
205
216
  url: "https://api.cloudflare.com/client/v4/accounts/account/ai/v1/chat/completions",
@@ -209,10 +220,10 @@ test("{§provider-fact-authority} an operator credential override holds one exac
209
220
 
210
221
  test("{§provider-fact-authority} ordered credential fallbacks are rejected at construction", () => {
211
222
  assert.throws(
212
- () => createSdkModel("cloudflare", "@cf/google/gemma-4-26b-a4b-it", {
223
+ () => createSdkModel("cloudflare-workers-ai", "@cf/google/gemma-4-26b-a4b-it", {
213
224
  CLOUDFLARE_ACCOUNT_ID: "account",
214
225
  CLOUDFLARE_API_TOKEN: "token",
215
- PLURNK_PROVIDERS_PROVIDER_CLOUDFLARE_API_KEY_ENV: "CLOUDFLARE_API_TOKEN,CLOUDFLARE_API_KEY",
226
+ PLURNK_PROVIDERS_PROVIDER_CLOUDFLARE_WORKERS_AI_API_KEY_ENV: "CLOUDFLARE_API_TOKEN,CLOUDFLARE_API_KEY",
216
227
  }),
217
228
  /one exact name, never an ordered fallback/,
218
229
  );
@@ -239,7 +250,7 @@ test("catalog routes own their documented cache-affinity request projection", ()
239
250
  { target: "header", name: "x-session-id" },
240
251
  );
241
252
  assert.deepEqual(
242
- createSdkModel("fireworks", "accounts/fireworks/models/test", { FIREWORKS_API_KEY: "key" })?.cacheAffinity,
253
+ createSdkModel("fireworks-ai", "accounts/fireworks/models/test", { FIREWORKS_API_KEY: "key" })?.cacheAffinity,
243
254
  { target: "body", name: "prompt_cache_key" },
244
255
  );
245
256
  });
@@ -292,19 +303,19 @@ test("createSdkModel fails clearly for a declared but unsupported SDK package",
292
303
  });
293
304
 
294
305
  test("{§provider-embedding-resolution} compatible catalog and declared routes share the standard embedding adapter", () => {
295
- const cloudflare = createEmbeddingModel("cloudflare", "@cf/qwen/qwen3-embedding-0.6b", {
306
+ const cloudflare = createEmbeddingModel("cloudflare-workers-ai", "@cf/qwen/qwen3-embedding-0.6b", {
296
307
  CLOUDFLARE_ACCOUNT_ID: "account",
297
308
  CLOUDFLARE_API_KEY: "key",
298
309
  });
299
310
  assert.equal(cloudflare?.providerId, "cloudflare-workers-ai");
300
- assert.equal(cloudflare?.embeddingModel.provider, "cloudflare.embedding");
311
+ assert.equal(cloudflare?.embeddingModel.provider, "cloudflare-workers-ai.embedding");
301
312
  assert.equal(cloudflare?.embeddingModel.modelId, "@cf/qwen/qwen3-embedding-0.6b");
302
313
 
303
- const fireworks = createEmbeddingModel("fireworks", "fireworks/qwen3-embedding-8b", {
314
+ const fireworks = createEmbeddingModel("fireworks-ai", "fireworks/qwen3-embedding-8b", {
304
315
  FIREWORKS_API_KEY: "key",
305
316
  });
306
317
  assert.equal(fireworks?.providerId, "fireworks-ai");
307
- assert.equal(fireworks?.embeddingModel.provider, "fireworks.embedding");
318
+ assert.equal(fireworks?.embeddingModel.provider, "fireworks-ai.embedding");
308
319
 
309
320
  const local = createEmbeddingModel("local-embed", "Qwen/Qwen3-Embedding-0.6B", {
310
321
  PLURNK_PROVIDERS_PROVIDER_LOCAL_EMBED_NPM: "@ai-sdk/openai-compatible",
package/src/sdkModels.ts CHANGED
@@ -10,8 +10,9 @@ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
10
10
  import { createTogetherAI } from "@ai-sdk/togetherai";
11
11
  import { createXai } from "@ai-sdk/xai";
12
12
  import type { EmbeddingModelV4, EmbeddingModelV4Result } from "@ai-sdk/provider";
13
- import { createOpenRouter } from "@openrouter/ai-sdk-provider";
13
+ import { createOpenRouter, type OpenRouterChatSettings } from "@openrouter/ai-sdk-provider";
14
14
  import {
15
+ resolveModel,
15
16
  isProviderCredentialName,
16
17
  lookup,
17
18
  lookupProvider,
@@ -310,14 +311,31 @@ const requireApiKey = (
310
311
 
311
312
  // {§provider-reasoning-policy} — the OpenRouter SDK builds its request from the model
312
313
  // settings alone (neither the generic `reasoning` call setting nor provider options reach
313
- // its body), so a fixed policy and `off` are represented there; `adaptive` leaves the
314
- // route's provider default.
314
+ // its body), so a fixed policy and `off` are represented there. `adaptive` on a
315
+ // reasoning-capable route declares the toggle on (`enabled` — #457: unconfigured
316
+ // toggle routes ran reasoning-off and strike-spiralled; `off` stays the explicit
317
+ // opt-out), and a resolved reasoning budget names the wire's `max_tokens` form —
318
+ // the only dial a budget_tokens-only route understands; a fixed policy keeps the
319
+ // `effort` form. `enabled` alone is documented OpenRouter wire; the SDK's settings
320
+ // type under-models it (requires effort | max_tokens), hence the call-site cast.
315
321
  const openRouterReasoningSettings = (
316
322
  reasoning: ReasoningPolicy | undefined,
317
- ): { reasoning?: { effort: "none" | "low" | "medium" | "high" } } =>
318
- reasoning === undefined || reasoning === "adaptive"
319
- ? {}
320
- : { reasoning: { effort: reasoning === "off" ? "none" : reasoning } };
323
+ reasoningBudget: number | null,
324
+ reasoningCapable: boolean,
325
+ ): { reasoning?: { effort: "none" | "low" | "medium" | "high" } | { max_tokens: number } | { enabled: true } } => {
326
+ if (reasoning === undefined) return {};
327
+ if (reasoning === "adaptive") {
328
+ if (reasoningBudget !== null) return { reasoning: { max_tokens: reasoningBudget } };
329
+ return reasoningCapable ? { reasoning: { enabled: true } } : {};
330
+ }
331
+ // OpenRouter documents effort none/low/medium/high; xhigh/max reach this
332
+ // seam only if a catalog row declares them — refuse with the gap named
333
+ // rather than ship an unverified wire value (#474 widens on evidence).
334
+ if (reasoning === "xhigh" || reasoning === "max") {
335
+ throw new TypeError(`openrouter reasoning effort '${reasoning}' is not a documented wire value`);
336
+ }
337
+ return { reasoning: { effort: reasoning === "off" ? "none" : reasoning } };
338
+ };
321
339
 
322
340
  const resolveSdkProvider = (
323
341
  provider: string,
@@ -339,6 +357,7 @@ export const createSdkModel = (
339
357
  env: NodeJS.ProcessEnv,
340
358
  baseUrlOverride?: string,
341
359
  reasoning?: ReasoningPolicy,
360
+ reasoningBudget: number | null = null,
342
361
  ): SdkModel | null => {
343
362
  const resolved = resolveSdkProvider(provider, env, baseUrlOverride);
344
363
  if (resolved === null) return null;
@@ -430,7 +449,11 @@ export const createSdkModel = (
430
449
  apiKey: requireApiKey(provider, env, catalog),
431
450
  baseURL: url,
432
451
  headers: openRouterHeaders(provider, env, catalog),
433
- }).languageModel(model, openRouterReasoningSettings(reasoning)),
452
+ }).languageModel(model, openRouterReasoningSettings(
453
+ reasoning,
454
+ reasoningBudget,
455
+ resolveModel(provider, model)?.info.reasoning === true,
456
+ ) as OpenRouterChatSettings),
434
457
  ...(catalog.id === "openrouter"
435
458
  ? { cacheAffinity: { target: "header" as const, name: "x-session-id" } }
436
459
  : {}),