@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.
Files changed (50) hide show
  1. package/.env.defaults +21 -9
  2. package/SPEC.md +54 -12
  3. package/dist/AiSdkProvider.d.ts +2 -2
  4. package/dist/AiSdkProvider.d.ts.map +1 -1
  5. package/dist/AiSdkProvider.js +36 -15
  6. package/dist/AiSdkProvider.js.map +1 -1
  7. package/dist/Pool.d.ts.map +1 -1
  8. package/dist/Pool.js +2 -0
  9. package/dist/Pool.js.map +1 -1
  10. package/dist/aiSdkTransport.d.ts.map +1 -1
  11. package/dist/aiSdkTransport.js +29 -32
  12. package/dist/aiSdkTransport.js.map +1 -1
  13. package/dist/capacity.d.ts +8 -0
  14. package/dist/capacity.d.ts.map +1 -1
  15. package/dist/capacity.js +21 -0
  16. package/dist/capacity.js.map +1 -1
  17. package/dist/catalogProvider.d.ts.map +1 -1
  18. package/dist/catalogProvider.js +10 -4
  19. package/dist/catalogProvider.js.map +1 -1
  20. package/dist/compatibleProvider.d.ts.map +1 -1
  21. package/dist/compatibleProvider.js +6 -3
  22. package/dist/compatibleProvider.js.map +1 -1
  23. package/dist/errors.d.ts.map +1 -1
  24. package/dist/errors.js +23 -2
  25. package/dist/errors.js.map +1 -1
  26. package/dist/sdkModels.d.ts.map +1 -1
  27. package/dist/sdkModels.js +6 -0
  28. package/dist/sdkModels.js.map +1 -1
  29. package/dist/types.d.ts +1 -0
  30. package/dist/types.d.ts.map +1 -1
  31. package/dist/types.js +9 -1
  32. package/dist/types.js.map +1 -1
  33. package/package.json +6 -6
  34. package/src/AiSdkProvider.test.ts +138 -112
  35. package/src/AiSdkProvider.ts +41 -20
  36. package/src/Pool.test.ts +2 -0
  37. package/src/Pool.ts +2 -0
  38. package/src/aiSdkTransport.test.ts +31 -16
  39. package/src/aiSdkTransport.ts +32 -34
  40. package/src/capacity.test.ts +33 -1
  41. package/src/capacity.ts +34 -0
  42. package/src/catalogProvider.test.ts +28 -6
  43. package/src/catalogProvider.ts +9 -3
  44. package/src/compatibleProvider.test.ts +12 -0
  45. package/src/compatibleProvider.ts +6 -3
  46. package/src/env.test.ts +4 -4
  47. package/src/errors.test.ts +1 -0
  48. package/src/errors.ts +22 -1
  49. package/src/sdkModels.ts +6 -0
  50. 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
- const supportedReasoningPolicies = ["off", "adaptive"] as const;
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: parseRequiredFloat(env.PLURNK_PROVIDERS_TEMPERATURE, "PLURNK_PROVIDERS_TEMPERATURE", provider, 0),
204
- repeatPenalty: parseRequiredFloat(env.PLURNK_PROVIDERS_REPEAT_PENALTY, "PLURNK_PROVIDERS_REPEAT_PENALTY", provider, 0),
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", "xhigh"]) {
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
  });
@@ -126,6 +126,7 @@ test("#161: ProviderError carries resource-interrupted attempt evidence outside
126
126
  maxInputTokens: null,
127
127
  maxOutputTokens: null,
128
128
  outputBudget: null,
129
+ responseMax: null,
129
130
  reasoningBudget: null,
130
131
  inputCapacity: null,
131
132
  prompt: {
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
- if (status === 0 && err.isRetryable) return { kind: "network_failure", message };
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
- super(`${source}: reasoning policy '${policy}' is unsupported; supported policies: ${supported.join(", ")}`);
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