@plurnk/plurnk-providers 1.14.0 → 1.14.2
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 +21 -9
- package/SPEC.md +54 -12
- package/dist/AiSdkProvider.d.ts +2 -2
- package/dist/AiSdkProvider.d.ts.map +1 -1
- package/dist/AiSdkProvider.js +36 -15
- package/dist/AiSdkProvider.js.map +1 -1
- package/dist/Pool.d.ts.map +1 -1
- package/dist/Pool.js +2 -0
- package/dist/Pool.js.map +1 -1
- package/dist/aiSdkTransport.d.ts.map +1 -1
- package/dist/aiSdkTransport.js +29 -32
- package/dist/aiSdkTransport.js.map +1 -1
- package/dist/capacity.d.ts +8 -0
- package/dist/capacity.d.ts.map +1 -1
- package/dist/capacity.js +21 -0
- package/dist/capacity.js.map +1 -1
- package/dist/catalogProvider.d.ts.map +1 -1
- package/dist/catalogProvider.js +10 -4
- package/dist/catalogProvider.js.map +1 -1
- package/dist/compatibleProvider.d.ts.map +1 -1
- package/dist/compatibleProvider.js +6 -3
- package/dist/compatibleProvider.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +23 -2
- package/dist/errors.js.map +1 -1
- package/dist/sdkModels.d.ts.map +1 -1
- package/dist/sdkModels.js +6 -0
- package/dist/sdkModels.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -1
- package/dist/types.js.map +1 -1
- package/package.json +6 -6
- package/src/AiSdkProvider.test.ts +138 -112
- package/src/AiSdkProvider.ts +41 -20
- package/src/Pool.test.ts +2 -0
- package/src/Pool.ts +2 -0
- package/src/aiSdkTransport.test.ts +31 -16
- package/src/aiSdkTransport.ts +32 -34
- package/src/capacity.test.ts +33 -1
- package/src/capacity.ts +34 -0
- package/src/catalogProvider.test.ts +28 -6
- package/src/catalogProvider.ts +9 -3
- package/src/compatibleProvider.test.ts +12 -0
- package/src/compatibleProvider.ts +6 -3
- package/src/env.test.ts +4 -4
- package/src/errors.test.ts +1 -0
- package/src/errors.ts +22 -1
- package/src/sdkModels.ts +6 -0
- package/src/types.ts +12 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { REASONING_POLICIES } from "@plurnk/plurnk-contracts";
|
|
1
2
|
import AiSdkProvider, { type GrammarStyle, type ReasoningStyle } from "./AiSdkProvider.ts";
|
|
2
3
|
import {
|
|
3
4
|
contextWindowFromEnv,
|
|
@@ -182,7 +183,9 @@ export const compatibleProviderFromEnv = async (
|
|
|
182
183
|
}
|
|
183
184
|
const envelope = generationEnvelopeFromEnv(env, provider, contextWindow, null);
|
|
184
185
|
const reasoning = reasoningFromEnv(env, provider, envelope.reasoningBudget);
|
|
185
|
-
|
|
186
|
+
// A detected llama-server rail runs the template style: the chat template governs
|
|
187
|
+
// reasoning, so the operator's stated effort is admitted and forwarded verbatim (#483).
|
|
188
|
+
const supportedReasoningPolicies = reasoningStyle === "template" ? REASONING_POLICIES : (["off", "adaptive"] as const);
|
|
186
189
|
return new AiSdkProvider({
|
|
187
190
|
model,
|
|
188
191
|
url,
|
|
@@ -200,8 +203,8 @@ export const compatibleProviderFromEnv = async (
|
|
|
200
203
|
reasoning,
|
|
201
204
|
reasoningResponseStyle: reasoningResponseStyleFromEnv(env, provider),
|
|
202
205
|
reasoningStyle,
|
|
203
|
-
temperature:
|
|
204
|
-
repeatPenalty:
|
|
206
|
+
temperature: parseOptionalFloat(env.PLURNK_PROVIDERS_TEMPERATURE, "PLURNK_PROVIDERS_TEMPERATURE", provider, 0),
|
|
207
|
+
repeatPenalty: parseOptionalFloat(env.PLURNK_PROVIDERS_REPEAT_PENALTY, "PLURNK_PROVIDERS_REPEAT_PENALTY", provider, 0),
|
|
205
208
|
frequencyPenalty: parseRequiredFloat(env.PLURNK_PROVIDERS_FREQUENCY_PENALTY, "PLURNK_PROVIDERS_FREQUENCY_PENALTY", provider, 0),
|
|
206
209
|
dryMultiplier: parseOptionalFloat(env.PLURNK_PROVIDERS_DRY_MULTIPLIER, "PLURNK_PROVIDERS_DRY_MULTIPLIER", provider, 0) ?? undefined,
|
|
207
210
|
dryBase: parseOptionalFloat(env.PLURNK_PROVIDERS_DRY_BASE, "PLURNK_PROVIDERS_DRY_BASE", provider, 0) ?? undefined,
|
package/src/env.test.ts
CHANGED
|
@@ -57,7 +57,7 @@ test("reasoningFromEnv: durable policy is independent from an optional explicit
|
|
|
57
57
|
assert.deepEqual(reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: "high" }, "openai", 4096), { mode: "high", budget: 4096 });
|
|
58
58
|
assert.deepEqual(reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: "adaptive" }, "openai", 4096), { mode: "adaptive", budget: 4096 });
|
|
59
59
|
assert.throws(() => reasoningFromEnv({}, "openai"), /PLURNK_PROVIDERS_REASONING must be set/);
|
|
60
|
-
assert.throws(() => reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: "8192" }, "openai"), /must be one of "off", "adaptive", "low", "medium", "high"/); // the old numeric habit fails loudly
|
|
60
|
+
assert.throws(() => reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: "8192" }, "openai"), /must be one of "off", "adaptive", "low", "medium", "high", "xhigh", "max"/); // the old numeric habit fails loudly
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
test("{§provider-tagged-reasoning} response style is explicit and invalid values fail at the provider boundary", () => {
|
|
@@ -227,16 +227,16 @@ test("still-set old THINKING names fail hard with the rename pointer", () => {
|
|
|
227
227
|
});
|
|
228
228
|
|
|
229
229
|
test("reasoning policy accepts only the exact portable durable vocabulary", () => {
|
|
230
|
-
for (const mode of ["off", "adaptive", "low", "medium", "high"] as const) {
|
|
230
|
+
for (const mode of ["off", "adaptive", "low", "medium", "high", "xhigh", "max"] as const) {
|
|
231
231
|
assert.deepEqual(reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: mode }, "openai"), {
|
|
232
232
|
mode,
|
|
233
233
|
budget: null,
|
|
234
234
|
});
|
|
235
235
|
}
|
|
236
|
-
for (const retired of ["on", "minimal", "
|
|
236
|
+
for (const retired of ["on", "minimal", "ultra"]) {
|
|
237
237
|
assert.throws(
|
|
238
238
|
() => reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: retired }, "openai"),
|
|
239
|
-
/must be one of "off", "adaptive", "low", "medium", "high"/,
|
|
239
|
+
/must be one of "off", "adaptive", "low", "medium", "high", "xhigh", "max"/,
|
|
240
240
|
);
|
|
241
241
|
}
|
|
242
242
|
});
|
package/src/errors.test.ts
CHANGED
package/src/errors.ts
CHANGED
|
@@ -46,6 +46,17 @@ export const providerTimeoutOf = (error: unknown): ProviderTimeoutError | null =
|
|
|
46
46
|
return null;
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
const peerTerminated = (err: unknown): boolean => {
|
|
50
|
+
const seen = new Set<unknown>();
|
|
51
|
+
let cur: unknown = err;
|
|
52
|
+
while (typeof cur === "object" && cur !== null && !seen.has(cur)) {
|
|
53
|
+
seen.add(cur);
|
|
54
|
+
if (cur instanceof TypeError && cur.message.trim().toLowerCase() === "terminated") return true;
|
|
55
|
+
cur = (cur as { cause?: unknown }).cause;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
};
|
|
59
|
+
|
|
49
60
|
const wireError = (body: string): { type: string | null; code: string | null; message: string | null } => {
|
|
50
61
|
try {
|
|
51
62
|
const { error } = JSON.parse(body) as { error?: { type?: unknown } };
|
|
@@ -100,13 +111,23 @@ export const classifyProviderError = (
|
|
|
100
111
|
: "The provider request failed without a diagnostic message.";
|
|
101
112
|
const body = err.responseBody ?? "";
|
|
102
113
|
const wire = wireError(body);
|
|
114
|
+
// A peer-terminated body inside a 2xx exchange is a network truth, not
|
|
115
|
+
// an invalid response — the SDK wraps Undici's TypeError("terminated")
|
|
116
|
+
// as a processing failure, but the KIND must stay engine-recoverable
|
|
117
|
+
// (#479). Walk the cause chain for the termination signature.
|
|
118
|
+
if (peerTerminated(err)) {
|
|
119
|
+
return { kind: "network_failure", message, retryable: err.isRetryable };
|
|
120
|
+
}
|
|
103
121
|
if (status === 401 || status === 403) return { kind: "unauthorized", message };
|
|
104
122
|
if (status === 402) return { kind: "quota_exceeded", message };
|
|
105
123
|
if (status === 429) return { kind: "rate_limit", message, retryable: err.isRetryable };
|
|
106
124
|
if (status === 408 || status === 409) {
|
|
107
125
|
return { kind: "network_failure", message, retryable: err.isRetryable };
|
|
108
126
|
}
|
|
109
|
-
|
|
127
|
+
// Status 0 is a transport-level failure (no HTTP exchange settled); its
|
|
128
|
+
// KIND stays network_failure regardless of the transport's retry policy
|
|
129
|
+
// (#479) — the engine's recovery keys on kind, never the retryable flag.
|
|
130
|
+
if (status === 0) return { kind: "network_failure", message, retryable: err.isRetryable };
|
|
110
131
|
if (status >= 500) return { kind: "network_failure", message, retryable: err.isRetryable };
|
|
111
132
|
if (status === 413 || (
|
|
112
133
|
(status === 400 || status === 422)
|
package/src/sdkModels.ts
CHANGED
|
@@ -328,6 +328,12 @@ const openRouterReasoningSettings = (
|
|
|
328
328
|
if (reasoningBudget !== null) return { reasoning: { max_tokens: reasoningBudget } };
|
|
329
329
|
return reasoningCapable ? { reasoning: { enabled: true } } : {};
|
|
330
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
|
+
}
|
|
331
337
|
return { reasoning: { effort: reasoning === "off" ? "none" : reasoning } };
|
|
332
338
|
};
|
|
333
339
|
|
package/src/types.ts
CHANGED
|
@@ -32,7 +32,15 @@ export class UnsupportedReasoningPolicyError extends Error {
|
|
|
32
32
|
readonly supported: readonly ReasoningPolicy[];
|
|
33
33
|
|
|
34
34
|
constructor(source: string, policy: ReasoningPolicy, supported: readonly ReasoningPolicy[]) {
|
|
35
|
-
|
|
35
|
+
// A graded-effort refusal names the operator's declaration lever (#472):
|
|
36
|
+
// the catalog is authoritative per model, and where a provider's API
|
|
37
|
+
// accepts more than its catalog entry lists, the remedy is the
|
|
38
|
+
// operator's, never a vendored fact in shipped defaults.
|
|
39
|
+
const providerName = /^provider:(.+)$/.exec(source)?.[1];
|
|
40
|
+
const lever = policy === "off" || policy === "adaptive"
|
|
41
|
+
? ""
|
|
42
|
+
: ` If the provider's API documents this effort beyond its catalog entry, declare it: PLURNK_PROVIDERS_PROVIDER_${(providerName ?? "<NAME>").replaceAll(/[^a-zA-Z0-9<>]/g, "_").toUpperCase()}_REASONING_EFFORTS=${policy} (comma-separated).`;
|
|
43
|
+
super(`${source}: reasoning policy '${policy}' is unsupported; supported policies: ${supported.join(", ")}.${lever}`);
|
|
36
44
|
this.name = "UnsupportedReasoningPolicyError";
|
|
37
45
|
this.policy = policy;
|
|
38
46
|
this.supported = supported;
|
|
@@ -82,6 +90,9 @@ export interface ProviderRequestCapacity {
|
|
|
82
90
|
readonly outputBudget: number | null;
|
|
83
91
|
readonly reasoningBudget: number | null;
|
|
84
92
|
readonly inputCapacity: number | null;
|
|
93
|
+
// {§provider-flexed-allowance} (#482): the response allowance actually
|
|
94
|
+
// granted this request — the floor, or the exactly-measured slack above it.
|
|
95
|
+
readonly responseMax: number | null;
|
|
85
96
|
readonly prompt: PromptTokenMeasurement;
|
|
86
97
|
}
|
|
87
98
|
|