@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.
- package/.env.defaults +7 -3
- package/SPEC.md +36 -4
- package/dist/AiSdkProvider.d.ts +1 -0
- package/dist/AiSdkProvider.d.ts.map +1 -1
- package/dist/AiSdkProvider.js +23 -8
- package/dist/AiSdkProvider.js.map +1 -1
- package/dist/aiSdkTransport.d.ts.map +1 -1
- package/dist/aiSdkTransport.js +20 -0
- package/dist/aiSdkTransport.js.map +1 -1
- package/dist/catalogProvider.d.ts.map +1 -1
- package/dist/catalogProvider.js +27 -7
- package/dist/catalogProvider.js.map +1 -1
- package/dist/env.d.ts +4 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +29 -0
- package/dist/env.js.map +1 -1
- package/dist/sdkModels.d.ts +1 -1
- package/dist/sdkModels.d.ts.map +1 -1
- package/dist/sdkModels.js +26 -8
- package/dist/sdkModels.js.map +1 -1
- package/package.json +6 -6
- package/src/AiSdkProvider.test.ts +14 -0
- package/src/AiSdkProvider.ts +26 -9
- package/src/ProviderRegistry.test.ts +12 -12
- package/src/aiSdkTransport.test.ts +35 -0
- package/src/aiSdkTransport.ts +20 -0
- package/src/catalogProvider.test.ts +123 -21
- package/src/catalogProvider.ts +33 -6
- package/src/env.test.ts +26 -9
- package/src/env.ts +29 -0
- package/src/providerDefaults.test.ts +9 -8
- package/src/sdkModels.test.ts +30 -19
- package/src/sdkModels.ts +31 -8
package/src/sdkModels.test.ts
CHANGED
|
@@ -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
|
-
|
|
139
|
-
assert.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
314
|
-
// route
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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(
|
|
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
|
: {}),
|