@plurnk/plurnk-providers 1.10.1 → 1.12.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 +6 -0
- package/SPEC.md +69 -32
- package/dist/AiSdkProvider.d.ts +6 -2
- package/dist/AiSdkProvider.d.ts.map +1 -1
- package/dist/AiSdkProvider.js +42 -3
- package/dist/AiSdkProvider.js.map +1 -1
- package/dist/accounting.d.ts.map +1 -1
- package/dist/accounting.js +13 -3
- package/dist/accounting.js.map +1 -1
- package/dist/aiSdkTransport.d.ts +1 -0
- package/dist/aiSdkTransport.d.ts.map +1 -1
- package/dist/aiSdkTransport.js +40 -11
- package/dist/aiSdkTransport.js.map +1 -1
- package/dist/catalogProvider.d.ts.map +1 -1
- package/dist/catalogProvider.js +96 -35
- package/dist/catalogProvider.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/notices.d.ts +1 -1
- package/dist/notices.d.ts.map +1 -1
- package/dist/sdkModels.d.ts +13 -1
- package/dist/sdkModels.d.ts.map +1 -1
- package/dist/sdkModels.js +103 -5
- package/dist/sdkModels.js.map +1 -1
- package/dist/types.d.ts +2 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +7 -6
- package/src/AiSdkProvider.test.ts +21 -4
- package/src/AiSdkProvider.ts +55 -7
- package/src/ProviderRegistry.test.ts +6 -5
- package/src/accounting.test.ts +39 -0
- package/src/accounting.ts +13 -4
- package/src/aiSdkTransport.test.ts +41 -1
- package/src/aiSdkTransport.ts +38 -9
- package/src/catalogProvider.test.ts +163 -4
- package/src/catalogProvider.ts +131 -39
- package/src/discover.test.ts +1 -1
- package/src/index.ts +2 -1
- package/src/notices.ts +1 -1
- package/src/sdkModels.test.ts +42 -1
- package/src/sdkModels.ts +144 -7
- package/src/types.ts +4 -15
|
@@ -2,7 +2,11 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { APICallError } from "ai";
|
|
4
4
|
import { ProviderTimeoutError } from "./errors.ts";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
executeOpenAICompatible,
|
|
7
|
+
normalizeRetryAttemptError,
|
|
8
|
+
transportFailureOutputObserved,
|
|
9
|
+
} from "./aiSdkTransport.ts";
|
|
6
10
|
|
|
7
11
|
const request = {
|
|
8
12
|
url: "https://example.test/v1/chat/completions",
|
|
@@ -41,6 +45,42 @@ test("the transport performs exactly one physical request", async () => {
|
|
|
41
45
|
assert.equal(calls, 1);
|
|
42
46
|
});
|
|
43
47
|
|
|
48
|
+
test("stream failure evidence distinguishes semantic output from pre-output failure ({§provider-connectivity})", async (t) => {
|
|
49
|
+
const cases = [
|
|
50
|
+
{ name: "text", delta: { content: "partial" }, expected: true },
|
|
51
|
+
{ name: "reasoning", delta: { reasoning_content: "partial" }, expected: true },
|
|
52
|
+
{ name: "empty", delta: {}, expected: false },
|
|
53
|
+
] as const;
|
|
54
|
+
for (const specimen of cases) {
|
|
55
|
+
await t.test(specimen.name, async () => {
|
|
56
|
+
await assert.rejects(
|
|
57
|
+
executeOpenAICompatible({
|
|
58
|
+
...request,
|
|
59
|
+
streaming: true,
|
|
60
|
+
fetch: async () => new Response(new ReadableStream({
|
|
61
|
+
start(controller) {
|
|
62
|
+
controller.enqueue(new TextEncoder().encode(
|
|
63
|
+
`data: ${JSON.stringify({
|
|
64
|
+
id: "interrupted",
|
|
65
|
+
object: "chat.completion.chunk",
|
|
66
|
+
created: 1,
|
|
67
|
+
model: "test-model",
|
|
68
|
+
choices: [{ index: 0, delta: specimen.delta, finish_reason: null }],
|
|
69
|
+
})}\n\n`,
|
|
70
|
+
));
|
|
71
|
+
setTimeout(() => controller.error(new TypeError("terminated")), 10);
|
|
72
|
+
},
|
|
73
|
+
}), { status: 200 }),
|
|
74
|
+
}),
|
|
75
|
+
(error) => {
|
|
76
|
+
assert.equal(transportFailureOutputObserved(error), specimen.expected);
|
|
77
|
+
return true;
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
44
84
|
test("the adapter preserves PLURNK request extensions and response evidence", async () => {
|
|
45
85
|
let body: Record<string, unknown> | undefined;
|
|
46
86
|
const responseBody = {
|
package/src/aiSdkTransport.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createOpenAICompatible, type ProviderErrorStructure } from "@ai-sdk/openai-compatible";
|
|
2
2
|
import { APICallError, generateText, streamText, type CallWarning, type JSONValue, type LanguageModel, type LanguageModelUsage } from "ai";
|
|
3
|
-
import { z } from "zod
|
|
3
|
+
import { z } from "zod";
|
|
4
4
|
import type { ChatMessage, ProviderAttemptFinishReason, ProviderChargeEvidence, ProviderReasoningObserver, ProviderUsage, TokenLogprob } from "./types.ts";
|
|
5
5
|
import { normalizeUsage, type RawUsage } from "./usage.ts";
|
|
6
6
|
import { emitWarningOnce } from "./warnings.ts";
|
|
@@ -222,13 +222,36 @@ const transportTimeout = (
|
|
|
222
222
|
};
|
|
223
223
|
|
|
224
224
|
const streamFailureValues = new WeakMap<object, readonly unknown[]>();
|
|
225
|
+
const streamFailureOutput = new WeakSet<object>();
|
|
225
226
|
|
|
226
227
|
const retainStreamFailureValues = <T extends object>(source: object, target: T): T => {
|
|
227
228
|
const values = streamFailureValues.get(source);
|
|
228
229
|
if (values !== undefined) streamFailureValues.set(target, values);
|
|
230
|
+
if (streamFailureOutput.has(source)) streamFailureOutput.add(target);
|
|
229
231
|
return target;
|
|
230
232
|
};
|
|
231
233
|
|
|
234
|
+
const preserveStreamFailure = (
|
|
235
|
+
error: unknown,
|
|
236
|
+
rawChunks: readonly unknown[],
|
|
237
|
+
outputObserved: boolean,
|
|
238
|
+
): void => {
|
|
239
|
+
if (typeof error !== "object" || error === null) return;
|
|
240
|
+
streamFailureValues.set(error, [...rawChunks, error]);
|
|
241
|
+
if (outputObserved) streamFailureOutput.add(error);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
export const transportFailureOutputObserved = (error: unknown): boolean => {
|
|
245
|
+
const seen = new Set<unknown>();
|
|
246
|
+
let current = error;
|
|
247
|
+
while (typeof current === "object" && current !== null && !seen.has(current)) {
|
|
248
|
+
if (streamFailureOutput.has(current)) return true;
|
|
249
|
+
seen.add(current);
|
|
250
|
+
current = (current as { cause?: unknown }).cause;
|
|
251
|
+
}
|
|
252
|
+
return false;
|
|
253
|
+
};
|
|
254
|
+
|
|
232
255
|
export const normalizeRetryAttemptError = (error: unknown): unknown => {
|
|
233
256
|
if (!APICallError.isInstance(error)) {
|
|
234
257
|
// Attempt, first-content, and stream-idle deadlines are retryable network
|
|
@@ -410,17 +433,23 @@ const executeModelOnce = async (
|
|
|
410
433
|
});
|
|
411
434
|
const rawChunks: unknown[] = [];
|
|
412
435
|
let streamError: unknown;
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
436
|
+
let outputObserved = false;
|
|
437
|
+
try {
|
|
438
|
+
for await (const part of result.fullStream) {
|
|
439
|
+
if (part.type === "raw") rawChunks.push(part.rawValue);
|
|
440
|
+
if (part.type === "text-delta" && part.text.length > 0) outputObserved = true;
|
|
441
|
+
if (part.type === "reasoning-delta" && part.text.length > 0) {
|
|
442
|
+
outputObserved = true;
|
|
443
|
+
request.observeReasoning?.(part.text);
|
|
444
|
+
}
|
|
445
|
+
if (part.type === "error") streamError ??= part.error;
|
|
417
446
|
}
|
|
418
|
-
|
|
447
|
+
} catch (error) {
|
|
448
|
+
preserveStreamFailure(error, rawChunks, outputObserved);
|
|
449
|
+
throw error;
|
|
419
450
|
}
|
|
420
451
|
if (streamError !== undefined) {
|
|
421
|
-
|
|
422
|
-
streamFailureValues.set(streamError, [...rawChunks, streamError]);
|
|
423
|
-
}
|
|
452
|
+
preserveStreamFailure(streamError, rawChunks, outputObserved);
|
|
424
453
|
throw streamError;
|
|
425
454
|
}
|
|
426
455
|
const evidence = extractEvidence(rawChunks);
|
|
@@ -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", "high"]);
|
|
51
|
+
assert.deepEqual(deepseek?.supportedReasoningPolicies, ["off", "adaptive", "low", "high"]);
|
|
52
52
|
|
|
53
53
|
assert.throws(
|
|
54
54
|
() => catalogProviderFromEnv("deepseek", {
|
|
@@ -57,7 +57,7 @@ test("provider adapters advertise only reasoning policies they can preserve", ()
|
|
|
57
57
|
PLURNK_PROVIDERS_REASONING: "medium",
|
|
58
58
|
PLURNK_PROVIDERS_PROVIDER_DEEPSEEK_REASONING_STYLE: "thinking_effort",
|
|
59
59
|
}, "deepseek-v4-flash"),
|
|
60
|
-
/reasoning policy 'medium' is unsupported; supported policies: off, adaptive, high/,
|
|
60
|
+
/reasoning policy 'medium' is unsupported; supported policies: off, adaptive, low, high/,
|
|
61
61
|
);
|
|
62
62
|
|
|
63
63
|
const mistral = catalogProviderFromEnv("mistral", {
|
|
@@ -82,6 +82,115 @@ test("provider adapters advertise only reasoning policies they can preserve", ()
|
|
|
82
82
|
assert.deepEqual(gemini?.supportedReasoningPolicies, ["adaptive", "low", "medium", "high"], "Gemini 3's mandatory minimum is not advertised as off");
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
+
test("Models.dev controls Cloudflare's exact effort vocabulary", async () => {
|
|
86
|
+
const bodies: Record<string, unknown>[] = [];
|
|
87
|
+
mock.method(globalThis, "fetch", async (_input: string | URL | Request, init?: RequestInit) => {
|
|
88
|
+
bodies.push(JSON.parse(String(init?.body)) as Record<string, unknown>);
|
|
89
|
+
return new Response([
|
|
90
|
+
`data: ${JSON.stringify({
|
|
91
|
+
id: "cloudflare-effort",
|
|
92
|
+
object: "chat.completion.chunk",
|
|
93
|
+
created: 1,
|
|
94
|
+
model: "@cf/qwen/qwen3.8-27b",
|
|
95
|
+
choices: [{ index: 0, delta: { content: "done" }, finish_reason: "stop" }],
|
|
96
|
+
})}`,
|
|
97
|
+
"data: [DONE]",
|
|
98
|
+
].join("\n\n"), { headers: { "content-type": "text/event-stream" } });
|
|
99
|
+
});
|
|
100
|
+
const cloudflareEnv = {
|
|
101
|
+
...env,
|
|
102
|
+
CLOUDFLARE_ACCOUNT_ID: "account",
|
|
103
|
+
CLOUDFLARE_API_KEY: "token",
|
|
104
|
+
PLURNK_PROVIDERS_PROVIDER_CLOUDFLARE_REASONING_STYLE: "effort_required",
|
|
105
|
+
};
|
|
106
|
+
const low = catalogProviderFromEnv("cloudflare", {
|
|
107
|
+
...cloudflareEnv,
|
|
108
|
+
PLURNK_PROVIDERS_REASONING: "low",
|
|
109
|
+
}, "@cf/qwen/qwen3.8-27b");
|
|
110
|
+
assert.deepEqual(low?.supportedReasoningPolicies, ["adaptive", "low", "medium"]);
|
|
111
|
+
await low?.generate({ workerId: "cloudflare-low", messages: [{ role: "user", content: "hello" }] });
|
|
112
|
+
|
|
113
|
+
const adaptive = catalogProviderFromEnv("cloudflare", {
|
|
114
|
+
...cloudflareEnv,
|
|
115
|
+
PLURNK_PROVIDERS_REASONING: "adaptive",
|
|
116
|
+
}, "@cf/qwen/qwen3.8-27b");
|
|
117
|
+
await adaptive?.generate({ workerId: "cloudflare-adaptive", messages: [{ role: "user", content: "hello" }] });
|
|
118
|
+
|
|
119
|
+
const nonReasoning = catalogProviderFromEnv("cloudflare", {
|
|
120
|
+
...cloudflareEnv,
|
|
121
|
+
PLURNK_PROVIDERS_REASONING: "adaptive",
|
|
122
|
+
}, "@cf/ibm-granite/granite-4.0-h-micro");
|
|
123
|
+
assert.deepEqual(nonReasoning?.supportedReasoningPolicies, ["off", "adaptive"]);
|
|
124
|
+
await nonReasoning?.generate({ workerId: "cloudflare-granite", messages: [{ role: "user", content: "hello" }] });
|
|
125
|
+
|
|
126
|
+
const ungradedReasoner = catalogProviderFromEnv("cloudflare", {
|
|
127
|
+
...cloudflareEnv,
|
|
128
|
+
PLURNK_PROVIDERS_REASONING: "adaptive",
|
|
129
|
+
}, "@cf/zai-org/glm-5.3-flash");
|
|
130
|
+
assert.deepEqual(ungradedReasoner?.supportedReasoningPolicies, ["adaptive"]);
|
|
131
|
+
await ungradedReasoner?.generate({ workerId: "cloudflare-ungraded", messages: [{ role: "user", content: "hello" }] });
|
|
132
|
+
|
|
133
|
+
assert.deepEqual(bodies.map((body) => body.reasoning_effort), ["low", "xhigh", undefined, undefined]);
|
|
134
|
+
assert.throws(
|
|
135
|
+
() => catalogProviderFromEnv("cloudflare", cloudflareEnv, "@cf/qwen/qwen3.8-27b"),
|
|
136
|
+
/reasoning policy 'off' is unsupported; supported policies: adaptive, low, medium/,
|
|
137
|
+
);
|
|
138
|
+
assert.throws(
|
|
139
|
+
() => catalogProviderFromEnv("cloudflare", {
|
|
140
|
+
...cloudflareEnv,
|
|
141
|
+
PLURNK_PROVIDERS_REASONING: "high",
|
|
142
|
+
}, "@cf/qwen/qwen3.8-27b"),
|
|
143
|
+
/reasoning policy 'high' is unsupported; supported policies: adaptive, low, medium/,
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("an operator-declared effort vocabulary extends Models.dev's for a provider's reasoning routes (#439)", async () => {
|
|
148
|
+
const bodies: Record<string, unknown>[] = [];
|
|
149
|
+
mock.method(globalThis, "fetch", async (_input: string | URL | Request, init?: RequestInit) => {
|
|
150
|
+
bodies.push(JSON.parse(String(init?.body)) as Record<string, unknown>);
|
|
151
|
+
return new Response([
|
|
152
|
+
`data: ${JSON.stringify({
|
|
153
|
+
id: "cloudflare-declared",
|
|
154
|
+
object: "chat.completion.chunk",
|
|
155
|
+
created: 1,
|
|
156
|
+
model: "@cf/zai-org/glm-5.3-flash",
|
|
157
|
+
choices: [{ index: 0, delta: { content: "done" }, finish_reason: "stop" }],
|
|
158
|
+
})}`,
|
|
159
|
+
"data: [DONE]",
|
|
160
|
+
].join("\n\n"), { headers: { "content-type": "text/event-stream" } });
|
|
161
|
+
});
|
|
162
|
+
const declaredEnv = {
|
|
163
|
+
...env,
|
|
164
|
+
CLOUDFLARE_ACCOUNT_ID: "account",
|
|
165
|
+
CLOUDFLARE_API_KEY: "token",
|
|
166
|
+
PLURNK_PROVIDERS_PROVIDER_CLOUDFLARE_REASONING_STYLE: "effort_required",
|
|
167
|
+
PLURNK_PROVIDERS_PROVIDER_CLOUDFLARE_REASONING_EFFORTS: "low, medium,high",
|
|
168
|
+
};
|
|
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");
|
|
171
|
+
assert.deepEqual(low?.supportedReasoningPolicies, ["adaptive", "low", "medium", "high"]);
|
|
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");
|
|
174
|
+
await adaptive?.generate({ workerId: "declared-adaptive", messages: [{ role: "user", content: "hello" }] });
|
|
175
|
+
assert.deepEqual(bodies.map((body) => body.reasoning_effort), ["low", "high"], "a fixed level keeps its name; adaptive takes the strongest declared effort");
|
|
176
|
+
// A declared `none` admits `off` on an effort transport; the catalog's own vocabulary stays in the union.
|
|
177
|
+
const withOff = catalogProviderFromEnv("cloudflare", {
|
|
178
|
+
...declaredEnv,
|
|
179
|
+
PLURNK_PROVIDERS_PROVIDER_CLOUDFLARE_REASONING_EFFORTS: "none,high",
|
|
180
|
+
}, "@cf/qwen/qwen3.8-27b");
|
|
181
|
+
assert.deepEqual(withOff?.supportedReasoningPolicies, ["off", "adaptive", "low", "medium", "high"]);
|
|
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");
|
|
184
|
+
assert.deepEqual(nonReasoning?.supportedReasoningPolicies, ["off", "adaptive"]);
|
|
185
|
+
assert.throws(
|
|
186
|
+
() => catalogProviderFromEnv("cloudflare", {
|
|
187
|
+
...declaredEnv,
|
|
188
|
+
PLURNK_PROVIDERS_PROVIDER_CLOUDFLARE_REASONING_EFFORTS: "low,turbo",
|
|
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/,
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
85
194
|
test("an operator context window caps catalog physics and percentage output policy", () => {
|
|
86
195
|
const provider = catalogProviderFromEnv("openai", {
|
|
87
196
|
...env,
|
|
@@ -153,7 +262,7 @@ test("official AI SDK provider owns the native request while PLURNK owns call se
|
|
|
153
262
|
assert.equal(calls[0]?.body.prompt_cache_key, "worker", "the official OpenAI SDK projects the documented affinity key");
|
|
154
263
|
});
|
|
155
264
|
|
|
156
|
-
test("xAI's native chat contract
|
|
265
|
+
test("xAI's native chat contract requests its strongest cataloged effort and caps the complete reasoning response", async () => {
|
|
157
266
|
let call: { headers: Headers; body: Record<string, unknown> } | undefined;
|
|
158
267
|
mock.method(globalThis, "fetch", async (_input: string | URL | Request, init?: RequestInit) => {
|
|
159
268
|
call = {
|
|
@@ -206,7 +315,7 @@ test("xAI's native chat contract affirmatively requests adaptive high and caps t
|
|
|
206
315
|
});
|
|
207
316
|
|
|
208
317
|
assert.equal(call?.body.max_completion_tokens, 16);
|
|
209
|
-
assert.equal(call?.body.reasoning_effort, "
|
|
318
|
+
assert.equal(call?.body.reasoning_effort, "xhigh", "adaptive uses the strongest route-advertised generic effort");
|
|
210
319
|
assert.equal("max_tokens" in (call?.body ?? {}), false);
|
|
211
320
|
assert.equal(call?.headers.get("x-grok-conv-id"), "xai-worker");
|
|
212
321
|
assert.equal(result?.assistant.reasoning, "consider");
|
|
@@ -278,6 +387,50 @@ test("Cerebras explicit reasoning activation needs no operator effort or token b
|
|
|
278
387
|
assert.equal(result?.accounting[0]?.usage?.outputTokenDetails?.reasoningTokens, 1);
|
|
279
388
|
});
|
|
280
389
|
|
|
390
|
+
test("Meta Muse adaptive reasoning is requested even when the endpoint returns no readable trace", async () => {
|
|
391
|
+
let body: Record<string, unknown> | undefined;
|
|
392
|
+
mock.method(globalThis, "fetch", async (_input: string | URL | Request, init?: RequestInit) => {
|
|
393
|
+
body = JSON.parse(String(init?.body)) as Record<string, unknown>;
|
|
394
|
+
return new Response([
|
|
395
|
+
`data: ${JSON.stringify({
|
|
396
|
+
id: "chatcmpl-meta",
|
|
397
|
+
object: "chat.completion.chunk",
|
|
398
|
+
created: 1,
|
|
399
|
+
model: "muse-spark-1.2-contributor",
|
|
400
|
+
choices: [{ index: 0, delta: { content: "done" }, finish_reason: "stop" }],
|
|
401
|
+
})}`,
|
|
402
|
+
`data: ${JSON.stringify({
|
|
403
|
+
id: "chatcmpl-meta",
|
|
404
|
+
object: "chat.completion.chunk",
|
|
405
|
+
created: 2,
|
|
406
|
+
model: "muse-spark-1.2-contributor",
|
|
407
|
+
choices: [],
|
|
408
|
+
usage: {
|
|
409
|
+
prompt_tokens: 2,
|
|
410
|
+
completion_tokens: 38,
|
|
411
|
+
total_tokens: 40,
|
|
412
|
+
completion_tokens_details: { reasoning_tokens: 37 },
|
|
413
|
+
},
|
|
414
|
+
})}`,
|
|
415
|
+
"data: [DONE]",
|
|
416
|
+
].join("\n\n"), { headers: { "content-type": "text/event-stream" } });
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const provider = catalogProviderFromEnv("meta", {
|
|
420
|
+
...env,
|
|
421
|
+
META_MODEL_API_KEY: "test-key",
|
|
422
|
+
PLURNK_PROVIDERS_REASONING: "adaptive",
|
|
423
|
+
}, "muse-spark-1.2-contributor");
|
|
424
|
+
const result = await provider?.generate({
|
|
425
|
+
workerId: "worker",
|
|
426
|
+
messages: [{ role: "user", content: "hello" }],
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
assert.equal(body?.reasoning_effort, "xhigh");
|
|
430
|
+
assert.equal(result?.assistant.reasoning, null);
|
|
431
|
+
assert.equal(result?.accounting[0]?.usage?.outputTokenDetails?.reasoningTokens, 37);
|
|
432
|
+
});
|
|
433
|
+
|
|
281
434
|
test("Google adaptive reasoning requests and preserves readable thought summaries", async () => {
|
|
282
435
|
const bodies: Array<{
|
|
283
436
|
generationConfig?: { thinkingConfig?: { includeThoughts?: boolean; thinkingLevel?: string; thinkingBudget?: number } };
|
|
@@ -412,6 +565,7 @@ test("native Anthropic adaptive policy uses adaptive thinking rather than a fixe
|
|
|
412
565
|
contextWindow: 16_384,
|
|
413
566
|
maxOutputTokens: 8_192,
|
|
414
567
|
reasoning: true,
|
|
568
|
+
reasoningOptions: [{ type: "toggle" }],
|
|
415
569
|
attachment: true,
|
|
416
570
|
toolCall: true,
|
|
417
571
|
modalities: { input: ["text", "image"], output: ["text"] },
|
|
@@ -543,8 +697,10 @@ test("cataloged unknown model fails unless its context is explicit", () => {
|
|
|
543
697
|
...env,
|
|
544
698
|
XAI_API_KEY: "test-key",
|
|
545
699
|
PLURNK_PROVIDERS_CONTEXT_WINDOW: "8192",
|
|
700
|
+
PLURNK_PROVIDERS_REASONING: "adaptive",
|
|
546
701
|
}, "not-in-the-catalog");
|
|
547
702
|
assert.equal(provider?.contextWindow, 8192);
|
|
703
|
+
assert.deepEqual(provider?.supportedReasoningPolicies, ["off", "adaptive"]);
|
|
548
704
|
});
|
|
549
705
|
|
|
550
706
|
test("Models.dev is the only fallback rate table", async () => {
|
|
@@ -570,6 +726,8 @@ test("Models.dev is the only fallback rate table", async () => {
|
|
|
570
726
|
const cataloged = catalogProviderFromEnv("deepseek", {
|
|
571
727
|
...env,
|
|
572
728
|
DEEPSEEK_API_KEY: "test-key",
|
|
729
|
+
PLURNK_PROVIDERS_REASONING: "adaptive",
|
|
730
|
+
PLURNK_PROVIDERS_PROVIDER_DEEPSEEK_REASONING_STYLE: "thinking_effort",
|
|
573
731
|
}, "deepseek-v4-flash");
|
|
574
732
|
assert.notEqual(cataloged, null);
|
|
575
733
|
const catalogedResponse = await cataloged!.generate({ workerId: "cataloged", messages: [] });
|
|
@@ -583,6 +741,7 @@ test("Models.dev is the only fallback rate table", async () => {
|
|
|
583
741
|
...env,
|
|
584
742
|
XAI_API_KEY: "test-key",
|
|
585
743
|
PLURNK_PROVIDERS_CONTEXT_WINDOW: "8192",
|
|
744
|
+
PLURNK_PROVIDERS_REASONING: "adaptive",
|
|
586
745
|
}, "not-in-the-catalog");
|
|
587
746
|
const uncatalogedResponse = await uncataloged!.generate({ workerId: "uncataloged", messages: [] });
|
|
588
747
|
assert.deepEqual(uncatalogedResponse.accounting[0]?.cost, {
|
package/src/catalogProvider.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
lookupProvider,
|
|
3
|
+
resolveModel,
|
|
4
|
+
type ModelInfo,
|
|
5
|
+
type ModelReasoningEffort,
|
|
6
|
+
} from "@plurnk/plurnk-models";
|
|
2
7
|
import {
|
|
3
8
|
contextWindowFromEnv,
|
|
4
9
|
effectiveContextWindow,
|
|
@@ -12,7 +17,13 @@ import {
|
|
|
12
17
|
reasoningFromEnv,
|
|
13
18
|
reasoningResponseStyleFromEnv,
|
|
14
19
|
} from "./env.ts";
|
|
15
|
-
import AiSdkProvider, {
|
|
20
|
+
import AiSdkProvider, {
|
|
21
|
+
type AiSdkProviderConfig,
|
|
22
|
+
type CompatibleReasoningEffort,
|
|
23
|
+
type GrammarStyle,
|
|
24
|
+
type NativeReasoningEffort,
|
|
25
|
+
type ReasoningStyle,
|
|
26
|
+
} from "./AiSdkProvider.ts";
|
|
16
27
|
import { configuredProviderInfo, createSdkModel } from "./sdkModels.ts";
|
|
17
28
|
import { providerSource } from "./notices.ts";
|
|
18
29
|
import type { Provider, ProviderCostNormalizer } from "./types.ts";
|
|
@@ -32,7 +43,7 @@ const reasoningStyleFromEnv = (
|
|
|
32
43
|
if (value === undefined || value.length === 0) return undefined;
|
|
33
44
|
const styles: readonly ReasoningStyle[] = [
|
|
34
45
|
"none", "think", "include_reasoning", "effort",
|
|
35
|
-
"effort_explicit", "thinking_effort", "template", "anthropic",
|
|
46
|
+
"effort_explicit", "effort_required", "thinking_effort", "template", "anthropic",
|
|
36
47
|
];
|
|
37
48
|
if (!styles.includes(value as ReasoningStyle)) {
|
|
38
49
|
throw new Error(`${name} provider: PLURNK_PROVIDERS_PROVIDER_${prefix}_REASONING_STYLE has invalid value "${value}"`);
|
|
@@ -40,23 +51,92 @@ const reasoningStyleFromEnv = (
|
|
|
40
51
|
return value as ReasoningStyle;
|
|
41
52
|
};
|
|
42
53
|
|
|
54
|
+
// {§provider-reasoning-policy} — the operator's affirmative declaration of efforts a provider's
|
|
55
|
+
// reasoning routes accept beyond Models.dev; the daemon adds none on its own (#439).
|
|
56
|
+
const DECLARABLE_EFFORTS: ReadonlySet<ModelReasoningEffort> = new Set([
|
|
57
|
+
"none", "minimal", "low", "medium", "high", "xhigh", "max",
|
|
58
|
+
]);
|
|
59
|
+
const declaredEffortsFromEnv = (
|
|
60
|
+
env: NodeJS.ProcessEnv,
|
|
61
|
+
name: string,
|
|
62
|
+
): readonly ModelReasoningEffort[] => {
|
|
63
|
+
const prefix = name.replaceAll(/[^a-zA-Z0-9]/g, "_").toUpperCase();
|
|
64
|
+
const key = `PLURNK_PROVIDERS_PROVIDER_${prefix}_REASONING_EFFORTS`;
|
|
65
|
+
const value = env[key];
|
|
66
|
+
if (value === undefined || value.trim().length === 0) return [];
|
|
67
|
+
const efforts = value.split(",").map((part) => part.trim()).filter((part) => part.length > 0);
|
|
68
|
+
const invalid = efforts.find((effort) => !DECLARABLE_EFFORTS.has(effort as ModelReasoningEffort));
|
|
69
|
+
if (invalid !== undefined) {
|
|
70
|
+
throw new Error(`${name} provider: ${key} has invalid value "${invalid}"; declarable efforts: ${[...DECLARABLE_EFFORTS].join(", ")}`);
|
|
71
|
+
}
|
|
72
|
+
return [...new Set(efforts as ModelReasoningEffort[])];
|
|
73
|
+
};
|
|
43
74
|
const activationPolicies = Object.freeze(["off", "adaptive"] as const);
|
|
44
75
|
const deepSeekPolicies = Object.freeze(["off", "adaptive", "high"] as const);
|
|
45
|
-
const adaptiveOnly = Object.freeze(["adaptive"] as const);
|
|
46
76
|
const reasoningWithoutOff = Object.freeze(["adaptive", "low", "medium", "high"] as const);
|
|
47
77
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
78
|
+
const reasoningEffortOrder = Object.freeze([
|
|
79
|
+
"minimal", "low", "medium", "high", "xhigh", "max",
|
|
80
|
+
] as const);
|
|
81
|
+
const nativeReasoningEfforts = new Set<NativeReasoningEffort>([
|
|
82
|
+
"minimal", "low", "medium", "high", "xhigh",
|
|
83
|
+
]);
|
|
84
|
+
const compatibleReasoningEfforts = new Set<CompatibleReasoningEffort>(reasoningEffortOrder);
|
|
85
|
+
const compatibleEffortStyles = new Set<ReasoningStyle>([
|
|
86
|
+
"effort", "effort_explicit", "effort_required", "thinking_effort",
|
|
87
|
+
]);
|
|
88
|
+
const compatibleToggleStyles = new Set<ReasoningStyle>([
|
|
89
|
+
"think", "include_reasoning", "effort_explicit", "thinking_effort", "template", "anthropic",
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
const catalogEfforts = (
|
|
93
|
+
info: ModelInfo,
|
|
94
|
+
declared: readonly ModelReasoningEffort[] = [],
|
|
95
|
+
): readonly ModelReasoningEffort[] => [
|
|
96
|
+
...new Set([
|
|
97
|
+
...(info.reasoningOptions
|
|
98
|
+
?.filter((option) => option.type === "effort")
|
|
99
|
+
.flatMap((option) => option.values) ?? []),
|
|
100
|
+
...declared,
|
|
101
|
+
]),
|
|
102
|
+
];
|
|
53
103
|
|
|
54
|
-
const
|
|
55
|
-
|
|
104
|
+
const strongestCatalogEffort = <T extends NativeReasoningEffort | CompatibleReasoningEffort>(
|
|
105
|
+
info: ModelInfo,
|
|
106
|
+
supported: ReadonlySet<T>,
|
|
107
|
+
declared: readonly ModelReasoningEffort[] = [],
|
|
108
|
+
): T | undefined => {
|
|
109
|
+
const values = new Set(catalogEfforts(info, declared));
|
|
110
|
+
return reasoningEffortOrder.findLast((effort) => supported.has(effort as T) && values.has(effort)) as T | undefined;
|
|
111
|
+
};
|
|
56
112
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
113
|
+
const catalogSupportsToggle = (info: ModelInfo): boolean =>
|
|
114
|
+
info.reasoningOptions?.some((option) => option.type === "toggle") === true;
|
|
115
|
+
|
|
116
|
+
const catalogSupportedReasoningPolicies = ({
|
|
117
|
+
info,
|
|
118
|
+
native,
|
|
119
|
+
style,
|
|
120
|
+
declared,
|
|
121
|
+
}: {
|
|
122
|
+
info: ModelInfo;
|
|
123
|
+
native: boolean;
|
|
124
|
+
style: ReasoningStyle;
|
|
125
|
+
declared: readonly ModelReasoningEffort[];
|
|
126
|
+
}): readonly ReasoningPolicy[] => {
|
|
127
|
+
if (!info.reasoning) return activationPolicies;
|
|
128
|
+
const efforts = new Set(catalogEfforts(info, declared));
|
|
129
|
+
const effortTransport = native || compatibleEffortStyles.has(style);
|
|
130
|
+
const off = native
|
|
131
|
+
? efforts.has("none") || catalogSupportsToggle(info)
|
|
132
|
+
: (efforts.has("none") && compatibleEffortStyles.has(style))
|
|
133
|
+
|| (catalogSupportsToggle(info) && compatibleToggleStyles.has(style));
|
|
134
|
+
return REASONING_POLICIES.filter((policy) => policy === "adaptive"
|
|
135
|
+
|| policy === "off" && off
|
|
136
|
+
|| (policy === "low" || policy === "medium" || policy === "high")
|
|
137
|
+
&& effortTransport
|
|
138
|
+
&& efforts.has(policy));
|
|
139
|
+
};
|
|
60
140
|
|
|
61
141
|
const anthropicSupportsAdaptiveThinking = (model: string): boolean =>
|
|
62
142
|
/claude-(?:opus-(?:4-[678]|5)|sonnet-(?:4-6|5)|fable-5)/.test(model);
|
|
@@ -65,30 +145,20 @@ const supportedReasoningPolicies = ({
|
|
|
65
145
|
info,
|
|
66
146
|
native,
|
|
67
147
|
style,
|
|
68
|
-
|
|
69
|
-
model,
|
|
148
|
+
declared,
|
|
70
149
|
}: {
|
|
71
150
|
info?: ModelInfo;
|
|
72
151
|
native: boolean;
|
|
73
152
|
style: ReasoningStyle;
|
|
74
|
-
|
|
75
|
-
model: string;
|
|
153
|
+
declared: readonly ModelReasoningEffort[];
|
|
76
154
|
}): readonly ReasoningPolicy[] => {
|
|
77
155
|
if (info !== undefined && info.reasoning !== true) return activationPolicies;
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
return mistralSupportsEffort(model) ? deepSeekPolicies : adaptiveOnly;
|
|
81
|
-
}
|
|
82
|
-
if (sdkPackage === "@ai-sdk/xai") {
|
|
83
|
-
if (xaiReasoningIsModelFixed(model)) return adaptiveOnly;
|
|
84
|
-
if (model === "grok-4.6") return reasoningWithoutOff;
|
|
85
|
-
}
|
|
86
|
-
if (sdkPackage === "@ai-sdk/google" && googleReasoningCannotBeOff(model)) {
|
|
87
|
-
return reasoningWithoutOff;
|
|
88
|
-
}
|
|
89
|
-
return REASONING_POLICIES;
|
|
156
|
+
if (info?.reasoningOptions !== undefined) {
|
|
157
|
+
return catalogSupportedReasoningPolicies({ info, native, style, declared });
|
|
90
158
|
}
|
|
159
|
+
if (native) return activationPolicies;
|
|
91
160
|
if (style === "effort" || style === "effort_explicit") return REASONING_POLICIES;
|
|
161
|
+
if (style === "effort_required") return reasoningWithoutOff;
|
|
92
162
|
if (style === "thinking_effort") return deepSeekPolicies;
|
|
93
163
|
return activationPolicies;
|
|
94
164
|
};
|
|
@@ -97,10 +167,14 @@ const adaptiveReasoningProjection = ({
|
|
|
97
167
|
sdkPackage,
|
|
98
168
|
model,
|
|
99
169
|
reasoningCapable,
|
|
170
|
+
info,
|
|
171
|
+
declared,
|
|
100
172
|
}: {
|
|
101
173
|
sdkPackage?: string;
|
|
102
174
|
model: string;
|
|
103
175
|
reasoningCapable: boolean;
|
|
176
|
+
info?: ModelInfo;
|
|
177
|
+
declared: readonly ModelReasoningEffort[];
|
|
104
178
|
}): Pick<AiSdkProviderConfig, "adaptiveReasoning" | "adaptiveReasoningProviderOptions"> => {
|
|
105
179
|
if (!reasoningCapable) return { adaptiveReasoning: "provider-default" };
|
|
106
180
|
if (sdkPackage === "@ai-sdk/google" && /^gemini-2\.5(?:-|$)/i.test(model)) {
|
|
@@ -127,13 +201,12 @@ const adaptiveReasoningProjection = ({
|
|
|
127
201
|
},
|
|
128
202
|
};
|
|
129
203
|
}
|
|
130
|
-
if (
|
|
131
|
-
return {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return { adaptiveReasoning: "provider-default" };
|
|
204
|
+
if (info?.reasoningOptions !== undefined) {
|
|
205
|
+
return {
|
|
206
|
+
adaptiveReasoning: strongestCatalogEffort(info, nativeReasoningEfforts, declared) ?? "provider-default",
|
|
207
|
+
};
|
|
135
208
|
}
|
|
136
|
-
return { adaptiveReasoning: "
|
|
209
|
+
return { adaptiveReasoning: "provider-default" };
|
|
137
210
|
};
|
|
138
211
|
|
|
139
212
|
export const providerFromSdkModel = ({
|
|
@@ -189,13 +262,31 @@ export const providerFromSdkModel = ({
|
|
|
189
262
|
maxOutputTokens,
|
|
190
263
|
);
|
|
191
264
|
const reasoning = reasoningFromEnv(env, name, envelope.reasoningBudget);
|
|
192
|
-
const reasoningStyle = reasoningStyleFromEnv(env, name) ?? "none";
|
|
193
265
|
const reasoningCapable = info?.reasoning === true;
|
|
266
|
+
const declaredEfforts = declaredEffortsFromEnv(env, name);
|
|
267
|
+
const declaredReasoningStyle = info?.reasoning === false
|
|
268
|
+
? "none"
|
|
269
|
+
: reasoningStyleFromEnv(env, name) ?? "none";
|
|
270
|
+
const reasoningStyle = info?.reasoningOptions !== undefined
|
|
271
|
+
&& (declaredReasoningStyle === "effort" || declaredReasoningStyle === "effort_required")
|
|
272
|
+
&& catalogEfforts(info, declaredEfforts).length === 0
|
|
273
|
+
? "none"
|
|
274
|
+
: declaredReasoningStyle;
|
|
194
275
|
const adaptiveReasoning = adaptiveReasoningProjection({
|
|
195
276
|
sdkPackage,
|
|
196
277
|
model,
|
|
197
278
|
reasoningCapable,
|
|
279
|
+
info,
|
|
280
|
+
declared: declaredEfforts,
|
|
198
281
|
});
|
|
282
|
+
const compatibleAdaptiveReasoning = languageModel !== undefined || info?.reasoningOptions === undefined
|
|
283
|
+
? undefined
|
|
284
|
+
: strongestCatalogEffort(info, compatibleReasoningEfforts, declaredEfforts) ?? "provider-default";
|
|
285
|
+
const compatibleOffReasoning = languageModel !== undefined
|
|
286
|
+
|| info === undefined
|
|
287
|
+
|| !catalogEfforts(info, declaredEfforts).includes("none")
|
|
288
|
+
? undefined
|
|
289
|
+
: "none" as const;
|
|
199
290
|
|
|
200
291
|
const catalogCost = info?.cost;
|
|
201
292
|
const rates = catalogCost === undefined ? null : {
|
|
@@ -232,10 +323,11 @@ export const providerFromSdkModel = ({
|
|
|
232
323
|
info,
|
|
233
324
|
native: languageModel !== undefined,
|
|
234
325
|
style: reasoningStyle,
|
|
235
|
-
|
|
236
|
-
model,
|
|
326
|
+
declared: declaredEfforts,
|
|
237
327
|
}),
|
|
238
328
|
...adaptiveReasoning,
|
|
329
|
+
...(compatibleAdaptiveReasoning === undefined ? {} : { compatibleAdaptiveReasoning }),
|
|
330
|
+
...(compatibleOffReasoning === undefined ? {} : { compatibleOffReasoning }),
|
|
239
331
|
...(additiveReasoningProvider === undefined || !reasoningCapable
|
|
240
332
|
? {}
|
|
241
333
|
: { additiveReasoningProvider }),
|
package/src/discover.test.ts
CHANGED
|
@@ -30,7 +30,7 @@ test("discover: the node_modules scan is scope-agnostic — third-party scopes a
|
|
|
30
30
|
"@acme/acme-provider-foo": { name: "@acme/acme-provider-foo", plurnk: { kind: "provider", name: "foo" } },
|
|
31
31
|
"unscoped-provider-bar": { name: "unscoped-provider-bar", plurnk: { kind: "provider", name: "bar" } },
|
|
32
32
|
"left-pad": { name: "left-pad" }, // no plurnk block → ignored
|
|
33
|
-
"@
|
|
33
|
+
"@acme/acme-execs-wrong-kind": { name: "@acme/acme-execs-wrong-kind", plurnk: { kind: "exec", runtimes: [] } }, // wrong kind → ignored
|
|
34
34
|
});
|
|
35
35
|
const { registry } = await discover({ cwd: root });
|
|
36
36
|
assert.deepEqual([...registry.keys()].sort(), ["bar", "foo", "native"]);
|
package/src/index.ts
CHANGED
|
@@ -45,7 +45,8 @@ export {
|
|
|
45
45
|
loadActiveProvider,
|
|
46
46
|
resetDiscoveryCache,
|
|
47
47
|
} from "./ProviderRegistry.ts";
|
|
48
|
-
export { providerReadiness } from "./sdkModels.ts";
|
|
48
|
+
export { createEmbeddingModel, providerReadiness } from "./sdkModels.ts";
|
|
49
|
+
export type { EmbeddingModelResolution } from "./sdkModels.ts";
|
|
49
50
|
|
|
50
51
|
// Scope-agnostic plugin discovery ({§plugin-family-kind}).
|
|
51
52
|
export { discover } from "./discover.ts";
|
package/src/notices.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ProviderNoticeKind = "grammar_unenforced" | "provider_warning";
|
|
1
|
+
export type ProviderNoticeKind = "grammar_unenforced" | "provider_warning" | "provider_retry";
|
|
2
2
|
|
|
3
3
|
// Observations about a completed model exchange. These never represent a
|
|
4
4
|
// failed provider operation; transport failures throw ProviderError with an
|