@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.
- package/.env.defaults +8 -4
- package/README.md +4 -16
- package/SPEC.md +31 -30
- package/dist/AiSdkProvider.d.ts +1 -3
- package/dist/AiSdkProvider.d.ts.map +1 -1
- package/dist/AiSdkProvider.js +7 -20
- package/dist/AiSdkProvider.js.map +1 -1
- package/dist/AiSdkRequestBody.d.ts +1 -4
- package/dist/AiSdkRequestBody.d.ts.map +1 -1
- package/dist/AiSdkRequestBody.js +1 -48
- package/dist/AiSdkRequestBody.js.map +1 -1
- package/dist/Mock.d.ts +3 -4
- package/dist/Mock.d.ts.map +1 -1
- package/dist/Mock.js +3 -4
- package/dist/Mock.js.map +1 -1
- package/dist/ProviderRegistry.js +2 -2
- package/dist/ProviderRegistry.js.map +1 -1
- package/dist/accounting.d.ts +0 -1
- package/dist/accounting.d.ts.map +1 -1
- package/dist/accounting.js +1 -32
- package/dist/accounting.js.map +1 -1
- package/dist/aiSdkTransport.d.ts +0 -8
- package/dist/aiSdkTransport.d.ts.map +1 -1
- package/dist/aiSdkTransport.js +23 -39
- package/dist/aiSdkTransport.js.map +1 -1
- package/dist/catalogProvider.d.ts.map +1 -1
- package/dist/catalogProvider.js +10 -2
- package/dist/catalogProvider.js.map +1 -1
- package/dist/compatibleProvider.d.ts +1 -1
- package/dist/compatibleProvider.d.ts.map +1 -1
- package/dist/compatibleProvider.js +13 -25
- package/dist/compatibleProvider.js.map +1 -1
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +1 -0
- package/dist/env.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/sdkModels.d.ts.map +1 -1
- package/dist/sdkModels.js +0 -10
- package/dist/sdkModels.js.map +1 -1
- package/dist/types.d.ts +0 -15
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -8
- package/src/AiSdkProvider.test.ts +97 -246
- package/src/AiSdkProvider.ts +7 -24
- package/src/AiSdkRequestBody.ts +2 -42
- package/src/Mock.test.ts +11 -3
- package/src/Mock.ts +4 -6
- package/src/ProviderRegistry.test.ts +0 -1
- package/src/ProviderRegistry.ts +1 -1
- package/src/accounting.test.ts +3 -25
- package/src/accounting.ts +0 -34
- package/src/aiSdkTransport.test.ts +2 -1
- package/src/aiSdkTransport.ts +25 -41
- package/src/boundaries.test.ts +1 -1
- package/src/capacity.test.ts +2 -2
- package/src/catalogProvider.test.ts +7 -85
- package/src/catalogProvider.ts +10 -2
- package/src/compatibleProvider.test.ts +6 -6
- package/src/compatibleProvider.ts +13 -28
- package/src/cost.test.ts +3 -3
- package/src/discover.test.ts +11 -2
- package/src/env.test.ts +3 -0
- package/src/env.ts +2 -1
- package/src/errors.test.ts +2 -2
- package/src/index.ts +1 -2
- package/src/inputModalities.test.ts +2 -1
- package/src/sdkModels.test.ts +0 -10
- package/src/sdkModels.ts +0 -10
- package/src/types.ts +8 -46
package/src/AiSdkProvider.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
-
|
|
659
|
-
|
|
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
|
|
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.#
|
|
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.#
|
|
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
|
};
|
package/src/AiSdkRequestBody.ts
CHANGED
|
@@ -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 {
|
|
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,
|
|
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("
|
|
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,
|
|
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
|
|
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
|
-
...
|
|
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 {
|
|
197
|
+
export { MOCK_USAGE as mockDefaultUsage };
|
package/src/ProviderRegistry.ts
CHANGED
|
@@ -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"
|
|
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) {
|
package/src/accounting.test.ts
CHANGED
|
@@ -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/
|
|
41
|
+
assert.equal(providerCostNormalizer("@ai-sdk/deepinfra")!(evidence({ usage: {} })), undefined);
|
|
64
42
|
assert.throws(
|
|
65
|
-
() => providerCostNormalizer("@ai-sdk/
|
|
66
|
-
/
|
|
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
|
+
|
package/src/aiSdkTransport.ts
CHANGED
|
@@ -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
|
|
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")
|
|
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
|
|
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,
|
package/src/boundaries.test.ts
CHANGED
|
@@ -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",
|
package/src/capacity.test.ts
CHANGED
|
@@ -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,
|