@plurnk/plurnk-providers 1.13.0 → 1.14.0
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 +11 -6
- 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 +25 -6
- 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 +20 -8
- package/dist/sdkModels.js.map +1 -1
- package/package.json +6 -6
- package/src/AiSdkProvider.test.ts +14 -0
- package/src/AiSdkProvider.ts +13 -6
- 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 +119 -17
- package/src/catalogProvider.ts +31 -5
- package/src/env.test.ts +22 -5
- package/src/env.ts +29 -0
- package/src/providerDefaults.test.ts +9 -8
- package/src/sdkModels.test.ts +30 -19
- package/src/sdkModels.ts +25 -8
package/src/aiSdkTransport.ts
CHANGED
|
@@ -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,
|
|
@@ -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
|
-
|
|
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
110
|
assert.deepEqual(low?.supportedReasoningPolicies, ["adaptive", "low", "medium"]);
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
179
|
+
PLURNK_PROVIDERS_PROVIDER_CLOUDFLARE_WORKERS_AI_REASONING_EFFORTS: "none,high",
|
|
180
180
|
}, "@cf/qwen/qwen3.8-27b");
|
|
181
181
|
assert.deepEqual(withOff?.supportedReasoningPolicies, ["off", "adaptive", "low", "medium", "high"]);
|
|
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
|
-
|
|
188
|
+
PLURNK_PROVIDERS_PROVIDER_CLOUDFLARE_WORKERS_AI_REASONING_EFFORTS: "low,turbo",
|
|
189
189
|
}, "@cf/zai-org/glm-5.3-flash"),
|
|
190
|
-
/
|
|
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
|
+
// "max" stays outside the portable wire vocabulary; "off" requires a declared "none".
|
|
852
|
+
assert.deepEqual(provider?.supportedReasoningPolicies, ["adaptive", "low", "high"]);
|
|
853
|
+
});
|
package/src/catalogProvider.ts
CHANGED
|
@@ -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,
|
|
@@ -289,7 +290,7 @@ export const providerFromSdkModel = ({
|
|
|
289
290
|
: "none" as const;
|
|
290
291
|
|
|
291
292
|
const catalogCost = info?.cost;
|
|
292
|
-
const
|
|
293
|
+
const catalogRates = catalogCost === undefined ? null : {
|
|
293
294
|
input: catalogCost.inputPer1M,
|
|
294
295
|
output: catalogCost.outputPer1M,
|
|
295
296
|
...(catalogCost.reasoningPer1M === undefined
|
|
@@ -302,8 +303,23 @@ export const providerFromSdkModel = ({
|
|
|
302
303
|
? {}
|
|
303
304
|
: { cacheWrite: catalogCost.cacheWritePer1M }),
|
|
304
305
|
};
|
|
306
|
+
// {§operator-cost-override} (#461): the catalog is the starting point; a declared
|
|
307
|
+
// override merges over it, and the estimate's source names the overlay.
|
|
308
|
+
const costOverride = costOverrideFromEnv(env, name);
|
|
309
|
+
if (costOverride !== null && catalogRates === null
|
|
310
|
+
&& (costOverride.input === undefined || costOverride.output === undefined)) {
|
|
311
|
+
throw new Error(
|
|
312
|
+
`${name} provider: PLURNK_PROVIDERS_COST without catalog rates must declare input and output`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
const rates = costOverride === null
|
|
316
|
+
? catalogRates
|
|
317
|
+
: { ...(catalogRates ?? {}), ...costOverride } as NonNullable<typeof catalogRates>;
|
|
318
|
+
const rateSource = costOverride === null
|
|
319
|
+
? "Models.dev catalog rates"
|
|
320
|
+
: "operator PLURNK_PROVIDERS_COST override over Models.dev catalog rates";
|
|
305
321
|
const estimateCost = (usage: Parameters<typeof estimateProviderCost>[0]) =>
|
|
306
|
-
estimateProviderCost(usage, rates,
|
|
322
|
+
estimateProviderCost(usage, rates, rateSource);
|
|
307
323
|
const affinityEnabled = cacheAffinityFromEnv(env, name);
|
|
308
324
|
const cacheWritePolicy = cacheWritePolicyFromEnv(env, name);
|
|
309
325
|
|
|
@@ -319,6 +335,8 @@ export const providerFromSdkModel = ({
|
|
|
319
335
|
maxOutputTokens,
|
|
320
336
|
outputBudget: envelope.outputBudget,
|
|
321
337
|
reasoningBudget: reasoning.budget,
|
|
338
|
+
// #457 — a catalog-declared toggle control makes `adaptive` an explicit enable.
|
|
339
|
+
reasoningToggle: info?.reasoningOptions?.some((option) => option.type === "toggle") === true,
|
|
322
340
|
supportedReasoningPolicies: supportedReasoningPolicies({
|
|
323
341
|
info,
|
|
324
342
|
native: languageModel !== undefined,
|
|
@@ -377,9 +395,6 @@ export const catalogProviderFromEnv = (
|
|
|
377
395
|
);
|
|
378
396
|
}
|
|
379
397
|
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
398
|
const info = resolved?.info;
|
|
384
399
|
const contextWindow = effectiveContextWindow(contextOverride, info?.contextWindow ?? null);
|
|
385
400
|
if (contextWindow === null) {
|
|
@@ -387,6 +402,17 @@ export const catalogProviderFromEnv = (
|
|
|
387
402
|
`${name} provider: context window unresolved for "${wireModel}" — set PLURNK_PROVIDERS_CONTEXT_WINDOW or update the Models.dev snapshot`,
|
|
388
403
|
);
|
|
389
404
|
}
|
|
405
|
+
const maxOutputTokens = info?.maxOutputTokens === undefined
|
|
406
|
+
? null
|
|
407
|
+
: Math.min(info.maxOutputTokens, contextWindow);
|
|
408
|
+
const reasoning = reasoningFromEnv(
|
|
409
|
+
env,
|
|
410
|
+
name,
|
|
411
|
+
generationEnvelopeFromEnv(env, name, contextWindow, maxOutputTokens).reasoningBudget,
|
|
412
|
+
);
|
|
413
|
+
const sdk = createSdkModel(name, wireModel, env, baseUrlOverride, reasoning.mode, reasoning.budget);
|
|
414
|
+
if (sdk === null) return null;
|
|
415
|
+
|
|
390
416
|
return providerFromSdkModel({
|
|
391
417
|
name,
|
|
392
418
|
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,
|
|
@@ -59,15 +61,15 @@ test("reasoningFromEnv: durable policy is independent from an optional explicit
|
|
|
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
|
});
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
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,25 @@ 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
|
+
return { reasoning: { effort: reasoning === "off" ? "none" : reasoning } };
|
|
332
|
+
};
|
|
321
333
|
|
|
322
334
|
const resolveSdkProvider = (
|
|
323
335
|
provider: string,
|
|
@@ -339,6 +351,7 @@ export const createSdkModel = (
|
|
|
339
351
|
env: NodeJS.ProcessEnv,
|
|
340
352
|
baseUrlOverride?: string,
|
|
341
353
|
reasoning?: ReasoningPolicy,
|
|
354
|
+
reasoningBudget: number | null = null,
|
|
342
355
|
): SdkModel | null => {
|
|
343
356
|
const resolved = resolveSdkProvider(provider, env, baseUrlOverride);
|
|
344
357
|
if (resolved === null) return null;
|
|
@@ -430,7 +443,11 @@ export const createSdkModel = (
|
|
|
430
443
|
apiKey: requireApiKey(provider, env, catalog),
|
|
431
444
|
baseURL: url,
|
|
432
445
|
headers: openRouterHeaders(provider, env, catalog),
|
|
433
|
-
}).languageModel(model, openRouterReasoningSettings(
|
|
446
|
+
}).languageModel(model, openRouterReasoningSettings(
|
|
447
|
+
reasoning,
|
|
448
|
+
reasoningBudget,
|
|
449
|
+
resolveModel(provider, model)?.info.reasoning === true,
|
|
450
|
+
) as OpenRouterChatSettings),
|
|
434
451
|
...(catalog.id === "openrouter"
|
|
435
452
|
? { cacheAffinity: { target: "header" as const, name: "x-session-id" } }
|
|
436
453
|
: {}),
|