@plurnk/plurnk-providers 1.18.0 → 1.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/.env.defaults +8 -4
  2. package/README.md +4 -16
  3. package/SPEC.md +31 -30
  4. package/dist/AiSdkProvider.d.ts +1 -3
  5. package/dist/AiSdkProvider.d.ts.map +1 -1
  6. package/dist/AiSdkProvider.js +7 -20
  7. package/dist/AiSdkProvider.js.map +1 -1
  8. package/dist/AiSdkRequestBody.d.ts +1 -4
  9. package/dist/AiSdkRequestBody.d.ts.map +1 -1
  10. package/dist/AiSdkRequestBody.js +1 -48
  11. package/dist/AiSdkRequestBody.js.map +1 -1
  12. package/dist/Mock.d.ts +3 -4
  13. package/dist/Mock.d.ts.map +1 -1
  14. package/dist/Mock.js +3 -4
  15. package/dist/Mock.js.map +1 -1
  16. package/dist/ProviderRegistry.js +2 -2
  17. package/dist/ProviderRegistry.js.map +1 -1
  18. package/dist/accounting.d.ts +0 -1
  19. package/dist/accounting.d.ts.map +1 -1
  20. package/dist/accounting.js +1 -32
  21. package/dist/accounting.js.map +1 -1
  22. package/dist/aiSdkTransport.d.ts +0 -8
  23. package/dist/aiSdkTransport.d.ts.map +1 -1
  24. package/dist/aiSdkTransport.js +23 -39
  25. package/dist/aiSdkTransport.js.map +1 -1
  26. package/dist/catalogProvider.d.ts.map +1 -1
  27. package/dist/catalogProvider.js +10 -2
  28. package/dist/catalogProvider.js.map +1 -1
  29. package/dist/compatibleProvider.d.ts +1 -1
  30. package/dist/compatibleProvider.d.ts.map +1 -1
  31. package/dist/compatibleProvider.js +13 -25
  32. package/dist/compatibleProvider.js.map +1 -1
  33. package/dist/env.d.ts.map +1 -1
  34. package/dist/env.js +1 -0
  35. package/dist/env.js.map +1 -1
  36. package/dist/index.d.ts +2 -2
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +1 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/sdkModels.d.ts.map +1 -1
  41. package/dist/sdkModels.js +0 -10
  42. package/dist/sdkModels.js.map +1 -1
  43. package/dist/types.d.ts +0 -15
  44. package/dist/types.d.ts.map +1 -1
  45. package/package.json +7 -8
  46. package/src/AiSdkProvider.test.ts +97 -246
  47. package/src/AiSdkProvider.ts +7 -24
  48. package/src/AiSdkRequestBody.ts +2 -42
  49. package/src/Mock.test.ts +11 -3
  50. package/src/Mock.ts +4 -6
  51. package/src/ProviderRegistry.test.ts +0 -1
  52. package/src/ProviderRegistry.ts +1 -1
  53. package/src/accounting.test.ts +3 -25
  54. package/src/accounting.ts +0 -34
  55. package/src/aiSdkTransport.test.ts +2 -1
  56. package/src/aiSdkTransport.ts +25 -41
  57. package/src/boundaries.test.ts +1 -1
  58. package/src/capacity.test.ts +2 -2
  59. package/src/catalogProvider.test.ts +7 -85
  60. package/src/catalogProvider.ts +10 -2
  61. package/src/compatibleProvider.test.ts +6 -6
  62. package/src/compatibleProvider.ts +13 -28
  63. package/src/cost.test.ts +3 -3
  64. package/src/discover.test.ts +11 -2
  65. package/src/env.test.ts +3 -0
  66. package/src/env.ts +2 -1
  67. package/src/errors.test.ts +2 -2
  68. package/src/index.ts +1 -2
  69. package/src/inputModalities.test.ts +2 -1
  70. package/src/sdkModels.test.ts +0 -10
  71. package/src/sdkModels.ts +0 -10
  72. package/src/types.ts +8 -46
@@ -126,7 +126,6 @@ export type AiSdkProviderConfig = {
126
126
  // a fixed deployment choice and therefore wins on every request.
127
127
  serviceTier?: string;
128
128
  streaming?: boolean; // SSE transport (default true); false → one non-streamed JSON
129
- firstPartyMetadata?: boolean; // forward per-turn attributions + client as Plurnk-* headers (plurnk only); default false
130
129
  apiKeyRejectedMessage?: string; // friendly hint when a present key is 401/403-rejected (distinct from unset); default undefined
131
130
  eosText?: string; // server-reported eos_token, stripped from the content tail (--special renders it as text); default undefined
132
131
  // Slot affinity wiring is provider-internal, never consumer-facing.
@@ -199,10 +198,6 @@ export type AiSdkProviderConfig = {
199
198
  // the standard factory always supplies them. Resolved against contextWindow
200
199
  // at read time (getters), so a probe that lands after config assembly still
201
200
  // derives correctly.
202
- // The plurnk.ai router owns tuning — false suppresses the
203
- // client-side temperature/penalty FLOORS on this provider (caller `sampling`
204
- // still passes through verbatim). Default true (floors ride).
205
- tuningFloors?: boolean;
206
201
  };
207
202
 
208
203
  class ProviderRequestObserverError extends Error {
@@ -296,13 +291,11 @@ export default class AiSdkProvider implements Provider {
296
291
  #reasoningResponseProviderOptions: AiSdkProviderOptions | undefined;
297
292
  #serviceTier: string | undefined;
298
293
  #streaming: boolean;
299
- #firstPartyMetadata: boolean;
300
294
  #supportsSlotPinning: boolean;
301
295
  #slotCount: number | null;
302
296
  #retryAttempts: number;
303
297
  #errorDetailLimit: number | undefined;
304
298
  #topLogprobs: number | null;
305
- #tuningFloors: boolean;
306
299
  #rawBody: boolean;
307
300
  #servedModel: string | undefined;
308
301
  #requiresOutputBudget: boolean | undefined;
@@ -418,14 +411,12 @@ export default class AiSdkProvider implements Provider {
418
411
  }
419
412
  this.#serviceTier = config.serviceTier;
420
413
  this.#streaming = config.streaming ?? true;
421
- this.#firstPartyMetadata = config.firstPartyMetadata ?? false;
422
414
  this.#apiKeyRejectedMessage = config.apiKeyRejectedMessage;
423
415
  this.#eosText = config.eosText;
424
416
  this.#hasApiKey = "Authorization" in this.#headers;
425
417
  this.#supportsSlotPinning = config.supportsSlotPinning ?? false;
426
418
  this.#slotCount = config.slotCount ?? null;
427
419
  this.#topLogprobs = config.topLogprobs ?? null;
428
- this.#tuningFloors = config.tuningFloors ?? true;
429
420
  this.#rawBody = config.rawBody ?? false;
430
421
  this.#servedModel = config.servedModel;
431
422
  this.#requiresOutputBudget = config.requiresOutputBudget;
@@ -477,7 +468,7 @@ export default class AiSdkProvider implements Provider {
477
468
  return tokens;
478
469
  };
479
470
  }
480
- this.#requestBody = new AiSdkRequestBody({ reasoningBudget: this.#reasoningBudget, additiveReasoningProvider: this.#additiveReasoningProvider, reasoning: this.#reasoning, reasoningToggle: this.#reasoningToggle, compatibleAdaptiveReasoning: this.#compatibleAdaptiveReasoning, compatibleOffReasoning: this.#compatibleOffReasoning, adaptiveReasoningProviderOptions: this.#adaptiveReasoningProviderOptions, repeatPenalty: this.#repeatPenalty, frequencyPenalty: this.#frequencyPenalty, dryMultiplier: this.#dryMultiplier, dryBase: this.#dryBase, dryAllowedLength: this.#dryAllowedLength, repeatLastN: this.#repeatLastN, reasoningStyle: this.#reasoningStyle, source: this.#source, grammarStyle: this.#grammarStyle, cacheAffinity: this.#cacheAffinity, reasoningResponseProviderOptions: this.#reasoningResponseProviderOptions, firstPartyMetadata: this.#firstPartyMetadata, supportsSlotPinning: this.#supportsSlotPinning, slotCount: this.#slotCount });
471
+ this.#requestBody = new AiSdkRequestBody({ reasoningBudget: this.#reasoningBudget, additiveReasoningProvider: this.#additiveReasoningProvider, reasoning: this.#reasoning, reasoningToggle: this.#reasoningToggle, compatibleAdaptiveReasoning: this.#compatibleAdaptiveReasoning, compatibleOffReasoning: this.#compatibleOffReasoning, adaptiveReasoningProviderOptions: this.#adaptiveReasoningProviderOptions, repeatPenalty: this.#repeatPenalty, frequencyPenalty: this.#frequencyPenalty, dryMultiplier: this.#dryMultiplier, dryBase: this.#dryBase, dryAllowedLength: this.#dryAllowedLength, repeatLastN: this.#repeatLastN, reasoningStyle: this.#reasoningStyle, source: this.#source, grammarStyle: this.#grammarStyle, cacheAffinity: this.#cacheAffinity, reasoningResponseProviderOptions: this.#reasoningResponseProviderOptions, supportsSlotPinning: this.#supportsSlotPinning, slotCount: this.#slotCount });
481
472
  }
482
473
 
483
474
  get contextWindow(): number | null { return this.#contextWindow; }
@@ -613,7 +604,7 @@ export default class AiSdkProvider implements Provider {
613
604
  });
614
605
  }
615
606
 
616
- async generate({ messages, workerId, primaryWorkerId, signal, grammar, maxOutputTokens, attributions, client, strikes, workspaceId, loop, turn, sampling, observeRequest, observeReasoning, callKind }: ProviderGenerateArgs): Promise<ProviderResponse> {
607
+ async generate({ messages, workerId, signal, grammar, maxOutputTokens, sampling, observeRequest, observeReasoning, callKind }: ProviderGenerateArgs): Promise<ProviderResponse> {
617
608
  // {§provider-interface} The worker identity is required.
618
609
  if (workerId === undefined || workerId.length === 0) throw new Error("generate: workerId is required — the worker's stable, opaque identity");
619
610
  if (callKind !== undefined && callKind !== "emission" && callKind !== "bare") {
@@ -655,9 +646,8 @@ export default class AiSdkProvider implements Provider {
655
646
  // paths and the name promises every request) < the caller's `sampling`
656
647
  // < the managed fields, which always win.
657
648
  const body: Record<string, unknown> = {
658
- // Floors are suppressed on router-owned-tuning providers (plurnk) —
659
- // the router's per-model tuning must not be overridden by client floors.
660
- ...(this.#tuningFloors ? { ...(this.#temperature !== null ? { temperature: this.#temperature } : {}), ...this.#requestBody.repetitionPenaltyBody() } : {}),
649
+ ...(this.#temperature !== null ? { temperature: this.#temperature } : {}),
650
+ ...this.#requestBody.repetitionPenaltyBody(),
661
651
  ...this.#requestBody.samplingBody(sampling),
662
652
  ...(this.#serviceTier !== undefined ? { service_tier: this.#serviceTier } : {}),
663
653
  ...this.#requestBody.reasoningBody(preserveGrammarSentence, capacity.reasoningBudget),
@@ -672,13 +662,11 @@ export default class AiSdkProvider implements Provider {
672
662
  : {}),
673
663
  };
674
664
 
675
- // Per-request headers = static auth/routing + any first-party telemetry.
676
- const metaHeaders = this.#requestBody.metadataHeaders(attributions, client, strikes, workerId, primaryWorkerId, workspaceId, loop, turn, callKind);
665
+ // Per-request headers = static auth/routing only.
677
666
  const headers = new Headers(this.#headers);
678
667
  if (this.#cacheAffinity?.target === "header") {
679
668
  headers.set(this.#cacheAffinity.name, workerId);
680
669
  }
681
- for (const [name, value] of Object.entries(metaHeaders)) headers.set(name, value);
682
670
  const requestHeaders = Object.fromEntries(headers.entries());
683
671
  const accounting: ProviderRequestAccounting[] = [];
684
672
  const operationTimeout = this.#operationTimeoutMs > 0
@@ -803,15 +791,13 @@ export default class AiSdkProvider implements Provider {
803
791
  streaming: this.#streaming,
804
792
  captureRawBody: this.#rawBody,
805
793
  ...observers,
806
- temperature: this.#tuningFloors
807
- ? (typeof sampling?.temperature === "number" ? sampling.temperature : this.#temperature ?? undefined)
808
- : typeof sampling?.temperature === "number" ? sampling.temperature : undefined,
794
+ temperature: typeof sampling?.temperature === "number" ? sampling.temperature : this.#temperature ?? undefined,
809
795
  topP: typeof sampling?.top_p === "number" ? sampling.top_p : undefined,
810
796
  topK: typeof sampling?.top_k === "number" ? sampling.top_k : undefined,
811
797
  presencePenalty: typeof sampling?.presence_penalty === "number" ? sampling.presence_penalty : undefined,
812
798
  frequencyPenalty: typeof sampling?.frequency_penalty === "number"
813
799
  ? sampling.frequency_penalty
814
- : this.#tuningFloors && this.#frequencyPenalty > 0 ? this.#frequencyPenalty : undefined,
800
+ : this.#frequencyPenalty > 0 ? this.#frequencyPenalty : undefined,
815
801
  stopSequences: typeof sampling?.stop === "string"
816
802
  ? [sampling.stop]
817
803
  : Array.isArray(sampling?.stop) && sampling.stop.every((value) => typeof value === "string")
@@ -1007,9 +993,6 @@ export default class AiSdkProvider implements Provider {
1007
993
  const assistant = {
1008
994
  content: raw.content,
1009
995
  reasoning: raw.reasoning.length > 0 ? raw.reasoning : null,
1010
- ...(raw.reasoningEncrypted.length > 0
1011
- ? { reasoningEncrypted: raw.reasoningEncrypted }
1012
- : {}),
1013
996
  model: raw.model,
1014
997
  ...(logprobs !== undefined ? { logprobs, meanLogprob } : {}),
1015
998
  };
@@ -1,5 +1,5 @@
1
1
  // The provider-specific request body and headers one generate call sends: reasoning, grammar, sampling, repetition, slots, metadata. Split out of AiSdkProvider; every knob it reads is injected.
2
- import type { ProviderCallKind, ReasoningPolicy } from "./types.ts";
2
+ import type { ReasoningPolicy } from "./types.ts";
3
3
  import type { JSONValue } from "ai";
4
4
  import { type Reasoning } from "./env.ts";
5
5
  import { fixedEffort } from "./reasoning-effort.ts";
@@ -80,13 +80,12 @@ export default class AiSdkRequestBody {
80
80
  readonly #grammarStyle: GrammarStyle;
81
81
  readonly #cacheAffinity: CacheAffinity | undefined;
82
82
  readonly #reasoningResponseProviderOptions: AiSdkProviderOptions | undefined;
83
- readonly #firstPartyMetadata: boolean;
84
83
  readonly #supportsSlotPinning: boolean;
85
84
  readonly #slotCount: number | null;
86
85
  #runSlots = new Map<string, number>();
87
86
  #nextSlot = 0;
88
87
 
89
- constructor({ reasoningBudget, additiveReasoningProvider, reasoning, reasoningToggle, compatibleAdaptiveReasoning, compatibleOffReasoning, adaptiveReasoningProviderOptions, repeatPenalty, frequencyPenalty, dryMultiplier, dryBase, dryAllowedLength, repeatLastN, reasoningStyle, source, grammarStyle, cacheAffinity, reasoningResponseProviderOptions, firstPartyMetadata, supportsSlotPinning, slotCount }: {
88
+ constructor({ reasoningBudget, additiveReasoningProvider, reasoning, reasoningToggle, compatibleAdaptiveReasoning, compatibleOffReasoning, adaptiveReasoningProviderOptions, repeatPenalty, frequencyPenalty, dryMultiplier, dryBase, dryAllowedLength, repeatLastN, reasoningStyle, source, grammarStyle, cacheAffinity, reasoningResponseProviderOptions, supportsSlotPinning, slotCount }: {
90
89
  reasoningBudget: number | null;
91
90
  additiveReasoningProvider: "anthropic" | "bedrock" | undefined;
92
91
  reasoning: Reasoning;
@@ -105,7 +104,6 @@ export default class AiSdkRequestBody {
105
104
  grammarStyle: GrammarStyle;
106
105
  cacheAffinity: CacheAffinity | undefined;
107
106
  reasoningResponseProviderOptions: AiSdkProviderOptions | undefined;
108
- firstPartyMetadata: boolean;
109
107
  supportsSlotPinning: boolean;
110
108
  slotCount: number | null;
111
109
  }) {
@@ -127,7 +125,6 @@ export default class AiSdkRequestBody {
127
125
  this.#grammarStyle = grammarStyle;
128
126
  this.#cacheAffinity = cacheAffinity;
129
127
  this.#reasoningResponseProviderOptions = reasoningResponseProviderOptions;
130
- this.#firstPartyMetadata = firstPartyMetadata;
131
128
  this.#supportsSlotPinning = supportsSlotPinning;
132
129
  this.#slotCount = slotCount;
133
130
  }
@@ -294,43 +291,6 @@ export default class AiSdkRequestBody {
294
291
  }
295
292
 
296
293
 
297
- // First-party telemetry headers ({§provider-request-authority} {§provider-call-kind}): forwarded only when the spec
298
- // opted in (the plurnk endpoint). The gate is here, not at the call site, so
299
- // attributions/client/strikes can never reach a third-party backend even if
300
- // the consumer passes them to the wrong provider. Empty values emit no header
301
- // — EXCEPT strikes, where 0 is a real value (clean streak) distinct from
302
- // absent (consumer didn't report); contract {§strikes-first-party-metadata}. Strikes
303
- // ride HTTP headers only — the packet never carries them (the model must
304
- // never see strike state; engine accounting is not a metric to game).
305
- metadataHeaders(attributions: string[] | undefined, client: string | undefined, strikes: number | undefined, workerId: string, primaryWorkerId: string | undefined, workspaceId: string | undefined, loop: number | undefined, turn: number | undefined, callKind: ProviderCallKind | undefined): Record<string, string> {
306
- if (!this.#firstPartyMetadata) return {};
307
- const h: Record<string, string> = {};
308
- if (attributions !== undefined && attributions.length > 0) h["Plurnk-Attribution"] = JSON.stringify(attributions);
309
- if (client !== undefined && client.length > 0) h["Plurnk-Client"] = client;
310
- if (strikes !== undefined && Number.isInteger(strikes) && strikes >= 0) h["Plurnk-Strikes"] = String(strikes);
311
- // Worker identity: the opaque workerId
312
- // the consumer already supplies, forwarded so the endpoint can key
313
- // per-worker affinity/telemetry — same gate as every first-party signal.
314
- h["Plurnk-Worker-Id"] = workerId;
315
- // Root worker of the lineage ({§worker-primary}): the no-parent ancestor of this turn's
316
- // worker tree. The consumer classifies primary-vs-spawned by equality
317
- // (primaryWorkerId == workerId ⇒ the primary/root worker). The provider
318
- // EMITS what the consumer supplies and never invents a primary; the
319
- // consumer's contract is to stamp it EVERY turn (including the primary's
320
- // own, where it equals workerId). Absence is the consumer's violation for
321
- // the endpoint to surface, not a provider default.
322
- if (primaryWorkerId !== undefined && primaryWorkerId.length > 0) h["Plurnk-Worker-Primary"] = primaryWorkerId;
323
- // Turn coordinate ({§lifecycle-terms}): workspace/loop/turn, the
324
- // daemon-side sequence the endpoint can never scrape from the wire.
325
- // Coordinates are 1-based — 0 is not a real value, so no strikes-style
326
- // zero exception; absent/empty/0 emits no header.
327
- if (workspaceId !== undefined && workspaceId.length > 0) h["Plurnk-Workspace-Id"] = workspaceId;
328
- if (loop !== undefined && Number.isInteger(loop) && loop >= 1) h["Plurnk-Loop"] = String(loop);
329
- if (turn !== undefined && Number.isInteger(turn) && turn >= 1) h["Plurnk-Turn"] = String(turn);
330
- if (callKind !== undefined) h["Plurnk-Call-Kind"] = callKind;
331
- return h;
332
- }
333
-
334
294
 
335
295
  requestProviderOptions(
336
296
  workerId: string,
package/src/Mock.test.ts CHANGED
@@ -8,10 +8,18 @@ import { ProviderError } from "./errors.ts";
8
8
  const build = (responses: MockResponse[] = [{ assistant: { content: "hi", reasoning: null } }]) =>
9
9
  new Mock({ contextWindow: 100000, responses });
10
10
 
11
+ // The suite runs on this package's own panel, which ships an output budget. "No budget" is therefore
12
+ // something a test states — the panel's explicit empty value — never an ambient absence of the key.
13
+ const withoutOutputBudget = <T>(body: () => T): T => {
14
+ const panel = process.env.PLURNK_PROVIDERS_OUTPUT_BUDGET;
15
+ process.env.PLURNK_PROVIDERS_OUTPUT_BUDGET = "";
16
+ try { return body(); } finally { process.env.PLURNK_PROVIDERS_OUTPUT_BUDGET = panel; }
17
+ };
18
+
11
19
  // — Identity ({§provider-interface}) —
12
20
 
13
21
  test("Mock: contextWindow and model are stable across reads", () => {
14
- const m = build();
22
+ const m = withoutOutputBudget(() => build());
15
23
  assert.equal(m.contextWindow, 100000);
16
24
  assert.equal(m.inputCapacity, null);
17
25
  assert.equal(m.contextWindow, 100000);
@@ -193,8 +201,8 @@ test("Mock resolves percentage and absolute generation budgets against its windo
193
201
  }
194
202
  });
195
203
 
196
- test("no generation-budget env leaves a bare Mock unbounded", () => {
197
- const m = new Mock({ contextWindow: 49152, responses: [] });
204
+ test("an empty generation budget leaves a bare Mock unbounded", () => {
205
+ const m = withoutOutputBudget(() => new Mock({ contextWindow: 49152, responses: [] }));
198
206
  assert.equal(m.outputBudget, null);
199
207
  assert.equal(m.reasoningBudget, null);
200
208
  assert.equal(m.inputCapacity, null);
package/src/Mock.ts CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { chatMessageText } from "./types.ts";
9
9
  import type { InputModality } from "./types.ts";
10
- import type { ChatMessage, FinishReason, GrammarEvidence, PromptTokenMeasurement, Provider, ProviderAssistant, ProviderCost, ProviderEncryptedReasoningItem, ProviderRequestAccounting, ProviderRequestCapacity, ProviderResponse, ProviderUsage } from "./types.ts";
10
+ import type { ChatMessage, FinishReason, GrammarEvidence, PromptTokenMeasurement, Provider, ProviderAssistant, ProviderCost, ProviderRequestAccounting, ProviderRequestCapacity, ProviderResponse, ProviderUsage } from "./types.ts";
11
11
  import { resolveGenerationEnvelopeFromEnv } from "./env.ts";
12
12
  import { REASONING_POLICIES } from "@plurnk/plurnk-contracts";
13
13
  import { validateProviderRequestAccounting } from "./accounting.ts";
@@ -20,7 +20,6 @@ export type MockAssistant = {
20
20
  finishReason?: FinishReason;
21
21
  model?: string;
22
22
  // Provider-normalized encrypted reasoning fixture.
23
- reasoningEncrypted?: ReadonlyArray<ProviderEncryptedReasoningItem>;
24
23
  // Pre-parsed ops — intg-only escape hatch. Typed `unknown[]` so the
25
24
  // framework carries no parser dependency; plurnk-service
26
25
  // casts these to PlurnkStatement[] on its side. Production providers never
@@ -41,7 +40,7 @@ export type MockResponse = {
41
40
  export type MockReturnedAssistant = ProviderAssistant & { ops?: unknown[] };
42
41
  export type MockReturnedResponse = ProviderResponse & { assistant: MockReturnedAssistant };
43
42
 
44
- const DEFAULT_USAGE: ProviderUsage = {
43
+ const MOCK_USAGE: ProviderUsage = {
45
44
  inputTokens: 0,
46
45
  outputTokens: 0,
47
46
  totalTokens: 0,
@@ -154,7 +153,7 @@ export default class Mock implements Provider {
154
153
  }
155
154
  const a = next.assistant;
156
155
  const usage: ProviderUsage = {
157
- ...DEFAULT_USAGE,
156
+ ...MOCK_USAGE,
158
157
  ...next.usage,
159
158
  };
160
159
  const requestAccounting: ProviderRequestAccounting = validateProviderRequestAccounting({
@@ -172,7 +171,6 @@ export default class Mock implements Provider {
172
171
  const assistant: MockReturnedAssistant = {
173
172
  content: a.content,
174
173
  reasoning: a.reasoning,
175
- ...(a.reasoningEncrypted !== undefined ? { reasoningEncrypted: a.reasoningEncrypted } : {}),
176
174
  finishReason: a.finishReason ?? "stop",
177
175
  model: a.model ?? "mock",
178
176
  ...(a.ops !== undefined ? { ops: a.ops } : {}),
@@ -196,4 +194,4 @@ export default class Mock implements Provider {
196
194
  get remaining(): number { return this.#queue.length; }
197
195
  }
198
196
 
199
- export { DEFAULT_USAGE as mockDefaultUsage };
197
+ export { MOCK_USAGE as mockDefaultUsage };
@@ -73,7 +73,6 @@ test("instantiateProvider: a selected plugin composes its static and runtime att
73
73
  const context: PluginAttributionContext = {
74
74
  workspaceId: "workspace",
75
75
  workerId: "worker",
76
- primaryWorkerId: "primary",
77
76
  loop: 3,
78
77
  turn: 2,
79
78
  attempt: 1,
@@ -60,7 +60,7 @@ export const instantiateProvider = async (
60
60
  const catalog = catalogProviderFromEnv(name, env, model, baseUrl);
61
61
  if (catalog !== null) return catalog;
62
62
  if (name === "ollama") return ollamaProviderFromEnv(env, model, baseUrl === undefined ? undefined : { baseUrl });
63
- if (name === "openai" || name === "plurnk") return compatibleProviderFromEnv(name, env, model, baseUrl);
63
+ if (name === "openai") return compatibleProviderFromEnv(env, model, baseUrl);
64
64
  const { registry, skipped, packageAttributions = new Map(), grammarStyles = new Map() } = await providerPackages(discoverFn, env);
65
65
  const specifier = registry.get(name);
66
66
  if (specifier === undefined) {
@@ -2,7 +2,6 @@ import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import {
4
4
  aggregateProviderAccounting,
5
- plurnkCostNormalizer,
6
5
  providerCostNormalizer,
7
6
  } from "./accounting.ts";
8
7
 
@@ -17,17 +16,6 @@ const evidence = ({ providerMetadata, usage, charge }: {
17
16
  response: { id: "response-1" },
18
17
  });
19
18
 
20
- test("xAI response ticks normalize to a directly charged request", () => {
21
- const normalize = providerCostNormalizer("@ai-sdk/xai");
22
- assert.notEqual(normalize, undefined);
23
- assert.deepEqual(normalize!(evidence({ usage: { cost_in_usd_ticks: 15_493_500 } })), {
24
- kind: "charged",
25
- amount: { amount: "15493500", currency: "USDTICK" },
26
- usdEquivalent: "0.00154935",
27
- source: "xAI response usage.cost_in_usd_ticks",
28
- });
29
- });
30
-
31
19
  test("OpenRouter response cost normalizes without rate reconstruction", () => {
32
20
  const normalize = providerCostNormalizer("@openrouter/ai-sdk-provider");
33
21
  assert.notEqual(normalize, undefined);
@@ -48,22 +36,12 @@ test("DeepInfra's documented response estimate remains estimated", () => {
48
36
  });
49
37
  });
50
38
 
51
- test("first-party charged evidence is validated at its adapter boundary", () => {
52
- const charged = {
53
- kind: "charged",
54
- amount: { amount: "0.01", currency: "USD" },
55
- source: "plurnk endpoint",
56
- } as const;
57
- assert.deepEqual(plurnkCostNormalizer(evidence({ charge: charged })), charged);
58
- assert.equal(plurnkCostNormalizer(evidence({})), undefined);
59
- });
60
-
61
39
  test("response cost normalization is an explicit adapter capability", () => {
62
40
  assert.equal(providerCostNormalizer("@ai-sdk/anthropic"), undefined);
63
- assert.equal(providerCostNormalizer("@ai-sdk/xai")!(evidence({ usage: {} })), undefined);
41
+ assert.equal(providerCostNormalizer("@ai-sdk/deepinfra")!(evidence({ usage: {} })), undefined);
64
42
  assert.throws(
65
- () => providerCostNormalizer("@ai-sdk/xai")!(evidence({ usage: { cost_in_usd_ticks: "1" } })),
66
- /cost_in_usd_ticks must be numeric/,
43
+ () => providerCostNormalizer("@ai-sdk/deepinfra")!(evidence({ usage: { estimated_cost: "1" } })),
44
+ /estimated_cost must be numeric/,
67
45
  );
68
46
  });
69
47
 
package/src/accounting.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import type {
2
- ChargedCost,
3
2
  ProviderAccounting,
4
3
  ProviderCostNormalizer,
5
4
  ProviderRequestAccounting,
@@ -7,7 +6,6 @@ import type {
7
6
  } from "./types.ts";
8
7
  import {
9
8
  sumProviderCostsUsd,
10
- validateChargedCost,
11
9
  validateProviderCost,
12
10
  } from "./cost.ts";
13
11
  import { validateProviderUsage } from "./usage.ts";
@@ -33,31 +31,6 @@ const decimalFromNumber = (value: number, subject: string): string => {
33
31
  return `${digits.slice(0, point)}.${digits.slice(point)}`;
34
32
  };
35
33
 
36
- const usdFromTicks = (ticks: number): string => {
37
- if (!Number.isSafeInteger(ticks) || ticks < 0) {
38
- throw new TypeError("xAI costInUsdTicks must be a non-negative safe integer");
39
- }
40
- const digits = String(ticks).padStart(11, "0");
41
- const integer = digits.slice(0, -10).replace(/^0+(?=\d)/, "");
42
- const fraction = digits.slice(-10).replace(/0+$/, "");
43
- return fraction === "" ? integer : `${integer}.${fraction}`;
44
- };
45
-
46
- const xaiCost: ProviderCostNormalizer = ({ usage }) => {
47
- const wireUsage = recordOf(usage);
48
- if (wireUsage === null || !("cost_in_usd_ticks" in wireUsage)) return undefined;
49
- const ticks = wireUsage.cost_in_usd_ticks;
50
- if (typeof ticks !== "number") {
51
- throw new TypeError("xAI usage.cost_in_usd_ticks must be numeric");
52
- }
53
- return {
54
- kind: "charged",
55
- amount: { amount: String(ticks), currency: "USDTICK" },
56
- usdEquivalent: usdFromTicks(ticks),
57
- source: "xAI response usage.cost_in_usd_ticks",
58
- };
59
- };
60
-
61
34
  const openRouterCost: ProviderCostNormalizer = ({ providerMetadata }) => {
62
35
  const usage = recordOf(recordOf(recordOf(providerMetadata)?.openrouter)?.usage);
63
36
  if (usage === null || !("cost" in usage)) return undefined;
@@ -82,17 +55,10 @@ const deepInfraCost: ProviderCostNormalizer = ({ usage }) => {
82
55
  };
83
56
  };
84
57
 
85
- // The first-party endpoint owns the direct charged-cost wire field.
86
- export const plurnkCostNormalizer: ProviderCostNormalizer = ({ charge }) => {
87
- if (charge === undefined) return undefined;
88
- return validateChargedCost(charge) as ChargedCost;
89
- };
90
-
91
58
  export const providerCostNormalizer = (
92
59
  sdkPackage: string,
93
60
  ): ProviderCostNormalizer | undefined => {
94
61
  switch (sdkPackage) {
95
- case "@ai-sdk/xai": return xaiCost;
96
62
  case "@ai-sdk/deepinfra": return deepInfraCost;
97
63
  case "@openrouter/ai-sdk-provider": return openRouterCost;
98
64
  default: return undefined;
@@ -19,7 +19,7 @@ const request = {
19
19
  captureRawBody: false,
20
20
  };
21
21
 
22
- test("the transport performs exactly one physical request", async () => {
22
+ test("{§provider-sdk-boundary} the transport performs exactly one physical request", async () => {
23
23
  let calls = 0;
24
24
  await assert.rejects(
25
25
  executeOpenAICompatible({
@@ -416,3 +416,4 @@ test("inconsistent usage counters refuse normalization without failing the respo
416
416
  assert.equal(result.usageRefusal, undefined);
417
417
  });
418
418
  });
419
+
@@ -201,11 +201,6 @@ export type AiSdkTransportResponse = {
201
201
  usage?: ProviderUsage;
202
202
  usageRefusal?: UsageRefusal;
203
203
  metadata: Record<string, unknown>;
204
- reasoningEncrypted: Array<{
205
- id: string | null;
206
- subtype: string;
207
- encrypted: Array<{ data: string; format: string | null }>;
208
- }>;
209
204
  logprobs: TokenLogprob[];
210
205
  chargeEvidence: ProviderChargeEvidence;
211
206
  rawBody?: unknown;
@@ -413,6 +408,18 @@ const executeModelOnce = async (
413
408
  ? { role: "assistant", content: chatMessageText(message) }
414
409
  : { role: "system", content: chatMessageText(message) };
415
410
  });
411
+ // {§provider-connectivity} — a streamed attempt's deadline holds only until semantic content
412
+ // flows; after that, stream-idle catches a stall and the operation deadline bounds the whole.
413
+ // A healthy stream still producing (long reasoning) is never cut off and its tokens wasted.
414
+ const attemptDeadline = request.streaming && request.fetchTimeoutMs > 0 ? new AbortController() : null;
415
+ const attemptTimer = attemptDeadline === null ? null : setTimeout(
416
+ () => attemptDeadline.abort(new ProviderTimeoutError("attempt", request.fetchTimeoutMs)),
417
+ request.fetchTimeoutMs,
418
+ );
419
+ const liftAttemptDeadline = (): void => { if (attemptTimer !== null) clearTimeout(attemptTimer); };
420
+ const abortSignal = attemptDeadline === null
421
+ ? request.signal
422
+ : request.signal === undefined ? attemptDeadline.signal : AbortSignal.any([request.signal, attemptDeadline.signal]);
416
423
  const common = {
417
424
  model,
418
425
  ...(instructions.length === 0 ? {} : { instructions }),
@@ -422,10 +429,10 @@ const executeModelOnce = async (
422
429
  // AiSdkProvider owns retries so every physical request is independently
423
430
  // observed and accounted. The SDK transport executes exactly once.
424
431
  maxRetries: 0,
425
- abortSignal: request.signal,
432
+ abortSignal,
426
433
  headers: request.headers,
427
434
  timeout: {
428
- ...(request.fetchTimeoutMs > 0 ? { totalMs: request.fetchTimeoutMs } : {}),
435
+ ...(request.fetchTimeoutMs > 0 && !request.streaming ? { totalMs: request.fetchTimeoutMs } : {}),
429
436
  ...(request.streaming
430
437
  && request.firstContentTimeoutMs !== undefined
431
438
  && request.firstContentTimeoutMs > 0
@@ -460,7 +467,6 @@ const executeModelOnce = async (
460
467
  ...(rawFinishReason === undefined ? {} : { rawFinishReason }),
461
468
  ...settledUsage(values, result.usage),
462
469
  metadata: metadataOf(values),
463
- reasoningEncrypted: evidence.reasoningEncrypted,
464
470
  logprobs: evidence.logprobs,
465
471
  chargeEvidence: {
466
472
  ...(wireChargeEvidenceOf(values) === undefined
@@ -490,15 +496,21 @@ const executeModelOnce = async (
490
496
  const rawChunks: unknown[] = [];
491
497
  let streamError: unknown;
492
498
  let outputObserved = false;
499
+ let answer = "";
493
500
  try {
494
501
  for await (const part of result.fullStream) {
495
- if (part.type === "raw") rawChunks.push(part.rawValue);
502
+ if (part.type === "raw") {
503
+ rawChunks.push(part.rawValue);
504
+ }
496
505
  if (part.type === "text-delta" && part.text.length > 0) {
497
506
  outputObserved = true;
507
+ liftAttemptDeadline();
508
+ answer += part.text;
498
509
  request.observeText?.(part.text);
499
510
  }
500
511
  if (part.type === "reasoning-delta" && part.text.length > 0) {
501
512
  outputObserved = true;
513
+ liftAttemptDeadline();
502
514
  request.observeReasoning?.(part.text);
503
515
  }
504
516
  if (part.type === "error") streamError ??= part.error;
@@ -506,6 +518,8 @@ const executeModelOnce = async (
506
518
  } catch (error) {
507
519
  preserveStreamFailure(error, rawChunks, outputObserved);
508
520
  throw error;
521
+ } finally {
522
+ liftAttemptDeadline();
509
523
  }
510
524
  if (streamError !== undefined) {
511
525
  preserveStreamFailure(streamError, rawChunks, outputObserved);
@@ -530,7 +544,6 @@ const executeModelOnce = async (
530
544
  ...(rawFinishReason === undefined ? {} : { rawFinishReason }),
531
545
  ...settledUsage(rawChunks, await result.usage),
532
546
  metadata: metadataOf(rawChunks),
533
- reasoningEncrypted: evidence.reasoningEncrypted,
534
547
  logprobs: evidence.logprobs,
535
548
  chargeEvidence: {
536
549
  ...(wireChargeEvidenceOf(rawChunks) === undefined
@@ -637,16 +650,13 @@ export const transportFailureEvidence = (
637
650
  };
638
651
 
639
652
  const extractEvidence = (values: unknown[]): {
640
- reasoningEncrypted: AiSdkTransportResponse["reasoningEncrypted"];
641
653
  logprobs: TokenLogprob[];
642
654
  reasoning: string;
643
655
  reasoningProjected: boolean;
644
656
  } => {
645
- const encrypted = new Map<string, AiSdkTransportResponse["reasoningEncrypted"][number]>();
646
657
  const logprobs: TokenLogprob[] = [];
647
658
  let reasoning = "";
648
659
  let reasoningProjected = false;
649
- let anonymous = 0;
650
660
  for (const value of values) {
651
661
  const choices = recordOf(value)?.choices;
652
662
  if (!Array.isArray(choices)) continue;
@@ -671,42 +681,16 @@ const extractEvidence = (values: unknown[]): {
671
681
  : { token: entry.token, logprob: entry.logprob, top });
672
682
  }
673
683
  }
674
- const message = recordOf(choice.delta) ?? recordOf(choice.message) ?? {};
684
+ const delta = recordOf(choice.delta);
685
+ const message = delta ?? recordOf(choice.message) ?? {};
675
686
  for (const key of ["reasoning_content", "reasoning", "thinking"]) { // lexicon-allow: backend wire fields
676
687
  if (typeof message[key] === "string") {
677
688
  reasoningProjected = true;
678
689
  reasoning += message[key];
679
690
  }
680
691
  }
681
- if (!Array.isArray(message.reasoning_details)) continue;
682
- for (const value of message.reasoning_details) {
683
- const detail = recordOf(value);
684
- if (detail?.type !== "reasoning.encrypted" || typeof detail.data !== "string") continue;
685
- const id = typeof detail.id === "string" ? detail.id : null;
686
- const key = typeof detail.index === "number"
687
- ? `index:${detail.index}`
688
- : id === null ? `anonymous:${anonymous++}` : `id:${id}`;
689
- const item: AiSdkTransportResponse["reasoningEncrypted"][number] = encrypted.get(key) ?? {
690
- id,
691
- // {§provider-encrypted-reasoning} The documented wire location
692
- // is the assistant message. `id` above still identifies only
693
- // this provider detail, never a downstream message entity.
694
- subtype: "message",
695
- encrypted: [],
696
- };
697
- const format = typeof detail.format === "string" ? detail.format : null;
698
- const prior = item.encrypted.at(-1);
699
- if (prior !== undefined) {
700
- prior.data += detail.data;
701
- if (prior.format === null && format !== null) prior.format = format;
702
- } else {
703
- item.encrypted.push({ data: detail.data, format });
704
- }
705
- encrypted.set(key, item);
706
- }
707
692
  }
708
693
  return {
709
- reasoningEncrypted: [...encrypted.values()],
710
694
  logprobs,
711
695
  reasoning,
712
696
  reasoningProjected,
@@ -64,7 +64,7 @@ test("the OpenAI-compatible entrypoint excludes Node-owned provider machinery",
64
64
  ]));
65
65
  });
66
66
 
67
- test("the normalized-error entrypoint excludes transport and Node machinery", () => {
67
+ test("{§provider-runtime-neutral-errors} the normalized-error entrypoint excludes transport and Node machinery", () => {
68
68
  assertRuntimeNeutralGraph("providerError.ts", new Set([
69
69
  "notices.ts",
70
70
  "providerError.ts",
@@ -27,7 +27,7 @@ test("call-specific output tightening also tightens its reasoning subset", () =>
27
27
  );
28
28
  });
29
29
 
30
- test("capacity applies independent input and combined-context limits", () => {
30
+ test("{§provider-capacity-admission} capacity applies independent input and combined-context limits", () => {
31
31
  assert.equal(effectiveInputCapacity({
32
32
  contextWindow: 100_000,
33
33
  maxInputTokens: 70_000,
@@ -61,7 +61,7 @@ test("a known combined context must leave positive input capacity", () => {
61
61
  );
62
62
  });
63
63
 
64
- test("only exact overflow rejects before provider I/O", () => {
64
+ test("{§provider-capacity-admission} only exact overflow rejects before provider I/O", () => {
65
65
  const base = {
66
66
  contextWindow: 100,
67
67
  maxInputTokens: null,