@plurnk/plurnk-providers 1.3.12 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.defaults +47 -38
- package/README.md +68 -4
- package/SPEC.md +245 -62
- package/dist/AiSdkProvider.d.ts +19 -5
- package/dist/AiSdkProvider.d.ts.map +1 -1
- package/dist/AiSdkProvider.js +264 -156
- package/dist/AiSdkProvider.js.map +1 -1
- package/dist/Mock.d.ts +15 -17
- package/dist/Mock.d.ts.map +1 -1
- package/dist/Mock.js +27 -10
- package/dist/Mock.js.map +1 -1
- package/dist/Pool.d.ts +8 -2
- package/dist/Pool.d.ts.map +1 -1
- package/dist/Pool.js +41 -8
- package/dist/Pool.js.map +1 -1
- package/dist/ProviderRegistry.d.ts +4 -1
- package/dist/ProviderRegistry.d.ts.map +1 -1
- package/dist/ProviderRegistry.js +7 -3
- package/dist/ProviderRegistry.js.map +1 -1
- package/dist/accounting.d.ts +3 -0
- package/dist/accounting.d.ts.map +1 -0
- package/dist/accounting.js +84 -0
- package/dist/accounting.js.map +1 -0
- package/dist/aiSdkTransport.d.ts +5 -2
- package/dist/aiSdkTransport.d.ts.map +1 -1
- package/dist/aiSdkTransport.js +91 -5
- package/dist/aiSdkTransport.js.map +1 -1
- package/dist/catalogProvider.d.ts +5 -2
- package/dist/catalogProvider.d.ts.map +1 -1
- package/dist/catalogProvider.js +24 -11
- package/dist/catalogProvider.js.map +1 -1
- package/dist/compatibleProvider.d.ts.map +1 -1
- package/dist/compatibleProvider.js +11 -4
- package/dist/compatibleProvider.js.map +1 -1
- package/dist/cost.d.ts +11 -0
- package/dist/cost.d.ts.map +1 -0
- package/dist/cost.js +61 -0
- package/dist/cost.js.map +1 -0
- package/dist/discover.d.ts +2 -0
- package/dist/discover.d.ts.map +1 -1
- package/dist/discover.js +15 -9
- package/dist/discover.js.map +1 -1
- package/dist/env.d.ts +4 -6
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +27 -29
- package/dist/env.js.map +1 -1
- package/dist/errors.d.ts +27 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +152 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +12 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -5
- package/dist/index.js.map +1 -1
- package/dist/notices.d.ts +10 -0
- package/dist/notices.d.ts.map +1 -0
- package/dist/notices.js +11 -0
- package/dist/notices.js.map +1 -0
- package/dist/ollama.d.ts.map +1 -1
- package/dist/ollama.js +3 -3
- package/dist/ollama.js.map +1 -1
- package/dist/openai.d.ts +1 -1
- package/dist/openai.d.ts.map +1 -1
- package/dist/promptTokens.d.ts +4 -0
- package/dist/promptTokens.d.ts.map +1 -0
- package/dist/promptTokens.js +32 -0
- package/dist/promptTokens.js.map +1 -0
- package/dist/sdkModels.d.ts +2 -0
- package/dist/sdkModels.d.ts.map +1 -1
- package/dist/sdkModels.js +17 -6
- package/dist/sdkModels.js.map +1 -1
- package/dist/types.d.ts +52 -16
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/dist/usage.d.ts +4 -0
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +64 -19
- package/dist/usage.js.map +1 -1
- package/dist/warnings.js +0 -0
- package/dist/warnings.js.map +1 -1
- package/package.json +15 -10
- package/src/AiSdkProvider.test.ts +750 -169
- package/src/AiSdkProvider.ts +354 -200
- package/src/Mock.test.ts +29 -14
- package/src/Mock.ts +36 -15
- package/src/Pool.test.ts +43 -6
- package/src/Pool.ts +56 -10
- package/src/ProviderRegistry.test.ts +158 -9
- package/src/ProviderRegistry.ts +19 -6
- package/src/accounting.test.ts +58 -0
- package/src/accounting.ts +88 -0
- package/src/aiSdkTransport.ts +101 -8
- package/src/boundaries.test.ts +9 -3
- package/src/catalogProvider.test.ts +43 -15
- package/src/catalogProvider.ts +32 -16
- package/src/compatibleProvider.test.ts +96 -0
- package/src/compatibleProvider.ts +15 -6
- package/src/cost.test.ts +64 -0
- package/src/cost.ts +78 -0
- package/src/defaults.test.ts +1 -0
- package/src/discover.test.ts +48 -7
- package/src/discover.ts +31 -21
- package/src/env.test.ts +38 -48
- package/src/env.ts +43 -40
- package/src/errors.test.ts +148 -0
- package/src/errors.ts +208 -0
- package/src/index.ts +30 -8
- package/src/lexicon-guard.test.ts +6 -6
- package/src/notices.ts +22 -0
- package/src/ollama.test.ts +64 -0
- package/src/ollama.ts +6 -3
- package/src/openai.ts +3 -0
- package/src/promptTokens.ts +41 -0
- package/src/sdkModels.test.ts +29 -3
- package/src/sdkModels.ts +19 -11
- package/src/types.ts +125 -64
- package/src/usage.test.ts +24 -5
- package/src/usage.ts +72 -21
- package/src/warnings.test.ts +10 -10
- package/src/warnings.ts +0 -0
- package/dist/OpenAICompat.d.ts +0 -76
- package/dist/OpenAICompat.d.ts.map +0 -1
- package/dist/OpenAICompat.js +0 -555
- package/dist/OpenAICompat.js.map +0 -1
- package/dist/openaiStream.d.ts +0 -47
- package/dist/openaiStream.d.ts.map +0 -1
- package/dist/openaiStream.js +0 -280
- package/dist/openaiStream.js.map +0 -1
- package/dist/standardProviders.d.ts +0 -31
- package/dist/standardProviders.d.ts.map +0 -1
- package/dist/standardProviders.js +0 -518
- package/dist/standardProviders.js.map +0 -1
- package/dist/telemetry.d.ts +0 -24
- package/dist/telemetry.d.ts.map +0 -1
- package/dist/telemetry.js +0 -85
- package/dist/telemetry.js.map +0 -1
- package/src/telemetry.test.ts +0 -69
- package/src/telemetry.ts +0 -116
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import test, { mock } from "node:test";
|
|
2
2
|
import { strict as assert } from "node:assert";
|
|
3
3
|
import { instantiateProvider, loadActiveProvider, resetDiscoveryCache } from "./ProviderRegistry.ts";
|
|
4
|
+
import type { PluginAttributionContext } from "@plurnk/plurnk-meta";
|
|
4
5
|
|
|
5
6
|
const mapOf = (entries: Record<string, string>, skipped: Record<string, string> = {}) =>
|
|
6
7
|
async () => ({ registry: new Map(Object.entries(entries)), skipped: new Map(Object.entries(skipped)), attributions: new Map<string, string | string[]>() });
|
|
7
8
|
|
|
8
|
-
// Alias
|
|
9
|
+
// Alias parsing is tested in @plurnk/plurnk-aliases (its owner). Here we
|
|
9
10
|
// exercise the resolution + two-tier instantiation this module owns; the active
|
|
10
11
|
// alias is driven end-to-end by loadActiveProvider below.
|
|
11
12
|
|
|
12
|
-
// — provider resolution (
|
|
13
|
+
// — provider resolution ({§provider-resolution}) —
|
|
13
14
|
|
|
14
15
|
const fullEnv = Object.freeze({
|
|
15
16
|
PLURNK_PROVIDERS_FETCH_TIMEOUT: "600000",
|
|
16
17
|
PLURNK_PROVIDERS_STREAM_IDLE_TIMEOUT: "0",
|
|
17
|
-
PLURNK_PROVIDERS_REASONING: "off", PLURNK_PROVIDERS_TEMPERATURE: "0.2", PLURNK_PROVIDERS_REPEAT_PENALTY: "1.15", PLURNK_PROVIDERS_FREQUENCY_PENALTY: "0.4", PLURNK_PROVIDERS_REASONING_RESERVE: "10%", PLURNK_PROVIDERS_COMPLETION_RESERVE: "25%", PLURNK_PROVIDERS_PROBE_ATTEMPTS: "3", PLURNK_PROVIDERS_PROBE_DELAY: "1", PLURNK_PROVIDERS_RETRY_ATTEMPTS: "0", PLURNK_PROVIDERS_PROMPT_CACHE_KEY: "1",
|
|
18
|
+
PLURNK_PROVIDERS_REASONING: "off", PLURNK_PROVIDERS_TEMPERATURE: "0.2", PLURNK_PROVIDERS_REPEAT_PENALTY: "1.15", PLURNK_PROVIDERS_FREQUENCY_PENALTY: "0.4", PLURNK_PROVIDERS_REASONING_RESERVE: "10%", PLURNK_PROVIDERS_COMPLETION_RESERVE: "25%", PLURNK_PROVIDERS_PROBE_ATTEMPTS: "3", PLURNK_PROVIDERS_PROBE_DELAY: "1", PLURNK_PROVIDERS_RETRY_ATTEMPTS: "0", PLURNK_PROVIDERS_ERROR_DETAIL_LIMIT: "512", PLURNK_PROVIDERS_PROMPT_CACHE_KEY: "1",
|
|
18
19
|
OPENAI_BASE_URL: "http://x",
|
|
19
20
|
});
|
|
20
21
|
|
|
@@ -49,6 +50,38 @@ test("instantiateProvider: an installed AI SDK provider resolves through discove
|
|
|
49
50
|
assert.deepEqual(calls, ["@acme/ai-provider", "model-a"]);
|
|
50
51
|
});
|
|
51
52
|
|
|
53
|
+
test("instantiateProvider: a selected plugin composes its static and runtime attribution sources", async () => {
|
|
54
|
+
resetDiscoveryCache();
|
|
55
|
+
const context: PluginAttributionContext = {
|
|
56
|
+
workspaceId: "workspace",
|
|
57
|
+
workerId: "worker",
|
|
58
|
+
primaryWorkerId: "primary",
|
|
59
|
+
loop: 3,
|
|
60
|
+
turn: 2,
|
|
61
|
+
attempt: 1,
|
|
62
|
+
};
|
|
63
|
+
const sdkProvider = {
|
|
64
|
+
languageModel: () => ({} as never),
|
|
65
|
+
attributions: ({ attempt }: PluginAttributionContext) => attempt === 1
|
|
66
|
+
? ["runtime:provider", "static:provider"]
|
|
67
|
+
: [],
|
|
68
|
+
};
|
|
69
|
+
const provider = await instantiateProvider(
|
|
70
|
+
"acme",
|
|
71
|
+
{ ...fullEnv, PLURNK_PROVIDERS_CONTEXT_WINDOW: "8192" },
|
|
72
|
+
"model-a",
|
|
73
|
+
async () => ({ default: sdkProvider }),
|
|
74
|
+
async () => ({
|
|
75
|
+
registry: new Map([["acme", "@acme/ai-provider"]]),
|
|
76
|
+
skipped: new Map(),
|
|
77
|
+
attributions: new Map([["acme", "static:provider"]]),
|
|
78
|
+
packageAttributions: new Map([["@acme/ai-provider", ["static:provider"]]]),
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
assert.deepEqual(provider.attributions?.(context), ["runtime:provider", "static:provider"]);
|
|
83
|
+
});
|
|
84
|
+
|
|
52
85
|
test("instantiateProvider: a per-alias baseUrl drives the built-in Ollama probe", async () => {
|
|
53
86
|
resetDiscoveryCache();
|
|
54
87
|
const calls: string[] = [];
|
|
@@ -112,7 +145,7 @@ test("instantiateProvider: unknown provider throws — no standard, no discovere
|
|
|
112
145
|
);
|
|
113
146
|
});
|
|
114
147
|
|
|
115
|
-
test("instantiateProvider: an untrusted (skipped) provider gives a precise error, not 'unknown'
|
|
148
|
+
test("instantiateProvider: an untrusted (skipped) provider gives a precise error, not 'unknown'", async () => {
|
|
116
149
|
resetDiscoveryCache();
|
|
117
150
|
const imports: string[] = [];
|
|
118
151
|
await assert.rejects(
|
|
@@ -171,7 +204,7 @@ test("instantiateProvider: per-alias knobs scope through to the provider (per-al
|
|
|
171
204
|
mock.restoreAll();
|
|
172
205
|
});
|
|
173
206
|
|
|
174
|
-
test("
|
|
207
|
+
test("public construction applies the package floor to a sparse consumer environment", async () => {
|
|
175
208
|
const provider = await instantiateProvider(
|
|
176
209
|
"fireworks",
|
|
177
210
|
{ FIREWORKS_API_KEY: "fw" },
|
|
@@ -182,7 +215,74 @@ test("#633: public construction applies the package floor to a sparse consumer e
|
|
|
182
215
|
assert.equal(provider.model, "accounts/fireworks/models/deepseek-v4-pro");
|
|
183
216
|
});
|
|
184
217
|
|
|
185
|
-
test("#
|
|
218
|
+
test("{§deepseek-reasoning-request} #157: direct DeepSeek composes catalog facts, credential, reasoning control, and cached cost", async () => {
|
|
219
|
+
const calls: Array<{ url: string; init?: RequestInit }> = [];
|
|
220
|
+
mock.method(globalThis, "fetch", async (url: string | URL | Request, init?: RequestInit) => {
|
|
221
|
+
calls.push({ url: String(url), init });
|
|
222
|
+
const chunks = [
|
|
223
|
+
{
|
|
224
|
+
id: "deepseek-test",
|
|
225
|
+
object: "chat.completion.chunk",
|
|
226
|
+
created: 1,
|
|
227
|
+
model: "deepseek-v4-flash",
|
|
228
|
+
choices: [{ index: 0, delta: { content: "ok" }, finish_reason: "stop" }],
|
|
229
|
+
usage: null,
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
id: "deepseek-test",
|
|
233
|
+
object: "chat.completion.chunk",
|
|
234
|
+
created: 1,
|
|
235
|
+
model: "deepseek-v4-flash",
|
|
236
|
+
choices: [],
|
|
237
|
+
usage: {
|
|
238
|
+
prompt_tokens: 10,
|
|
239
|
+
prompt_cache_hit_tokens: 8,
|
|
240
|
+
prompt_cache_miss_tokens: 2,
|
|
241
|
+
completion_tokens: 2,
|
|
242
|
+
total_tokens: 12,
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
];
|
|
246
|
+
const body = [...chunks.map((chunk) => `data: ${JSON.stringify(chunk)}`), "data: [DONE]"].join("\n\n");
|
|
247
|
+
return new Response(body, { status: 200, headers: { "Content-Type": "text/event-stream" } });
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const provider = await instantiateProvider(
|
|
251
|
+
"deepseek",
|
|
252
|
+
{
|
|
253
|
+
DEEPSEEK_API_KEY: "test-key",
|
|
254
|
+
PLURNK_PROVIDERS_REASONING: "off",
|
|
255
|
+
PLURNK_PROVIDERS_RETRY_ATTEMPTS: "0",
|
|
256
|
+
},
|
|
257
|
+
"deepseek-v4-flash",
|
|
258
|
+
async () => ({}),
|
|
259
|
+
mapOf({}),
|
|
260
|
+
);
|
|
261
|
+
const response = await provider.generate({
|
|
262
|
+
workerId: "worker",
|
|
263
|
+
messages: [{ role: "user", content: "Reply with ok." }],
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
assert.equal(calls.length, 1);
|
|
267
|
+
assert.equal(calls[0].url, "https://api.deepseek.com/chat/completions");
|
|
268
|
+
assert.equal(new Headers(calls[0].init?.headers).get("Authorization"), "Bearer test-key");
|
|
269
|
+
const request = JSON.parse(String(calls[0].init?.body)) as Record<string, unknown>;
|
|
270
|
+
assert.equal(request.model, "deepseek-v4-flash");
|
|
271
|
+
assert.deepEqual(request.thinking, { type: "disabled" });
|
|
272
|
+
assert.equal(provider.contextWindow, 1_000_000);
|
|
273
|
+
assert.equal(response.assistant.content, "ok");
|
|
274
|
+
assert.deepEqual(response.assistant.usage, {
|
|
275
|
+
prompt: 10,
|
|
276
|
+
completion: 2,
|
|
277
|
+
reasoning: 0,
|
|
278
|
+
cached: 8,
|
|
279
|
+
total: 12,
|
|
280
|
+
});
|
|
281
|
+
assert.ok(Math.abs(provider.calculateCost(response.assistant.usage) - 0.0000008624) < 1e-15);
|
|
282
|
+
mock.restoreAll();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("an explicit malformed operator override still fails at its owning contract", async () => {
|
|
186
286
|
await assert.rejects(
|
|
187
287
|
() => instantiateProvider(
|
|
188
288
|
"fireworks",
|
|
@@ -198,7 +298,7 @@ test("#633: an explicit malformed operator override still fails at its owning co
|
|
|
198
298
|
);
|
|
199
299
|
});
|
|
200
300
|
|
|
201
|
-
test("
|
|
301
|
+
test("a catalog provider with unknown model metadata never falls through to plugin discovery", async () => {
|
|
202
302
|
let scanned = false;
|
|
203
303
|
await assert.rejects(
|
|
204
304
|
() => instantiateProvider(
|
|
@@ -223,7 +323,7 @@ test("#634: a catalog provider with unknown model metadata never falls through t
|
|
|
223
323
|
assert.equal(scanned, false);
|
|
224
324
|
});
|
|
225
325
|
|
|
226
|
-
test("
|
|
326
|
+
test("explicit metadata constructs an out-of-snapshot Cloudflare model in the consolidated transport", async () => {
|
|
227
327
|
let scanned = false;
|
|
228
328
|
const provider = await instantiateProvider(
|
|
229
329
|
"cloudflare",
|
|
@@ -248,7 +348,56 @@ test("#634: explicit metadata constructs an out-of-snapshot Cloudflare model in
|
|
|
248
348
|
assert.equal(scanned, false);
|
|
249
349
|
});
|
|
250
350
|
|
|
251
|
-
test("
|
|
351
|
+
test("{§provider-tagged-reasoning} a Cloudflare model alias carries its explicit response style through the public registry", async () => {
|
|
352
|
+
const calls: string[] = [];
|
|
353
|
+
mock.method(globalThis, "fetch", async (input: string | URL | Request) => {
|
|
354
|
+
calls.push(String(input));
|
|
355
|
+
const chunks = [
|
|
356
|
+
{
|
|
357
|
+
id: "cloudflare-reasoning",
|
|
358
|
+
object: "chat.completion.chunk",
|
|
359
|
+
created: 1,
|
|
360
|
+
model: "@cf/deepseek-ai/deepseek-r1-distill-qwen-32b",
|
|
361
|
+
choices: [{ index: 0, delta: { content: "<think>working</think>done" }, finish_reason: "stop" }],
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
id: "cloudflare-reasoning",
|
|
365
|
+
object: "chat.completion.chunk",
|
|
366
|
+
created: 2,
|
|
367
|
+
model: "@cf/deepseek-ai/deepseek-r1-distill-qwen-32b",
|
|
368
|
+
choices: [],
|
|
369
|
+
usage: { prompt_tokens: 2, completion_tokens: 4, total_tokens: 6 },
|
|
370
|
+
},
|
|
371
|
+
];
|
|
372
|
+
const body = [...chunks.map((chunk) => `data: ${JSON.stringify(chunk)}`), "data: [DONE]"].join("\n\n");
|
|
373
|
+
return new Response(body, { status: 200, headers: { "Content-Type": "text/event-stream" } });
|
|
374
|
+
});
|
|
375
|
+
const provider = await instantiateProvider(
|
|
376
|
+
"cloudflare",
|
|
377
|
+
{
|
|
378
|
+
...fullEnv,
|
|
379
|
+
CLOUDFLARE_ACCOUNT_ID: "account",
|
|
380
|
+
CLOUDFLARE_API_TOKEN: "token",
|
|
381
|
+
PLURNK_PROVIDERS_REASONING_RESPONSE_STYLE: "verbatim",
|
|
382
|
+
PLURNK_PROVIDERS_REASONING_RESPONSE_STYLE_cfds1: "think-tags",
|
|
383
|
+
},
|
|
384
|
+
"@cf/deepseek-ai/deepseek-r1-distill-qwen-32b",
|
|
385
|
+
async () => ({}),
|
|
386
|
+
mapOf({}),
|
|
387
|
+
undefined,
|
|
388
|
+
"cfds1",
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
const response = await provider.generate({ workerId: "cloudflare-worker", messages: [] });
|
|
392
|
+
|
|
393
|
+
assert.equal(response.assistant.reasoning, "working");
|
|
394
|
+
assert.equal(response.assistant.content, "done");
|
|
395
|
+
assert.deepEqual(calls, [
|
|
396
|
+
"https://api.cloudflare.com/client/v4/accounts/account/ai/v1/chat/completions",
|
|
397
|
+
]);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("two Fireworks aliases independently select default and priority service tiers", async () => {
|
|
252
401
|
const bodies: Record<string, unknown>[] = [];
|
|
253
402
|
mock.method(globalThis, "fetch", async (_url: string, init?: RequestInit) => {
|
|
254
403
|
bodies.push(JSON.parse(String(init?.body)) as Record<string, unknown>);
|
package/src/ProviderRegistry.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Provider instantiation + active-alias resolution. Alias PARSING (the
|
|
2
2
|
// PLURNK_MODEL_<alias>=<provider>/<model> cascade + PLURNK_BASEURL_<alias>
|
|
3
3
|
// overrides) lives in @plurnk/plurnk-aliases — the zero-dep parser shared with
|
|
4
|
-
// thin clients
|
|
4
|
+
// thin clients; this module resolves the active alias to a Provider.
|
|
5
5
|
//
|
|
6
|
-
//
|
|
6
|
+
// {§provider-resolution} Models.dev catalog → PLURNK provider declaration
|
|
7
7
|
// → local protocol adapter → scope-agnostic AI SDK plugin discovery. Generic
|
|
8
8
|
// provider facts belong to models.dev or operator config; PLURNK owns only the
|
|
9
9
|
// stable Provider contract and product-specific local behavior.
|
|
@@ -17,20 +17,27 @@ import { ollamaProviderFromEnv } from "./ollama.ts";
|
|
|
17
17
|
import { compatibleProviderFromEnv } from "./compatibleProvider.ts";
|
|
18
18
|
import { contextWindowFromEnv } from "./env.ts";
|
|
19
19
|
import { withProviderDefaults } from "./defaults.ts";
|
|
20
|
+
import Meta, {
|
|
21
|
+
type PluginAttribution,
|
|
22
|
+
type PluginAttributionContext,
|
|
23
|
+
} from "@plurnk/plurnk-meta";
|
|
20
24
|
|
|
21
25
|
// Two injectable seams, both defaulting to production behavior and never passed
|
|
22
26
|
// by real callers: the module importer (tests exercise the bespoke path without
|
|
23
27
|
// a real package on disk) and the discovery scan (tests inject a fixed map).
|
|
24
28
|
type ImportModule = (specifier: string) => Promise<unknown>;
|
|
25
29
|
const importModule: ImportModule = (specifier) => import(specifier);
|
|
26
|
-
type
|
|
30
|
+
type DiscoveryInput = Omit<Discovery, "packageAttributions"> & {
|
|
31
|
+
packageAttributions?: Discovery["packageAttributions"];
|
|
32
|
+
};
|
|
33
|
+
type DiscoverFn = (options?: DiscoverOptions) => Promise<DiscoveryInput>;
|
|
27
34
|
|
|
28
35
|
// The node_modules scan is filesystem work that never changes within a process,
|
|
29
36
|
// so it runs once and the result is memoized. A long-lived daemon pays one scan
|
|
30
37
|
// at first bespoke instantiation; every later run reuses it. The trust gate is
|
|
31
38
|
// boot config, so the env from the first scan stands for the process.
|
|
32
|
-
let discoveredCache:
|
|
33
|
-
const providerPackages = async (discoverFn: DiscoverFn, env: NodeJS.ProcessEnv): Promise<
|
|
39
|
+
let discoveredCache: DiscoveryInput | null = null;
|
|
40
|
+
const providerPackages = async (discoverFn: DiscoverFn, env: NodeJS.ProcessEnv): Promise<DiscoveryInput> => {
|
|
34
41
|
discoveredCache ??= await discoverFn({ env });
|
|
35
42
|
return discoveredCache;
|
|
36
43
|
};
|
|
@@ -54,7 +61,7 @@ export const instantiateProvider = async (
|
|
|
54
61
|
if (catalog !== null) return catalog;
|
|
55
62
|
if (name === "ollama") return ollamaProviderFromEnv(env, model, baseUrl === undefined ? undefined : { baseUrl });
|
|
56
63
|
if (name === "openai" || name === "plurnk") return compatibleProviderFromEnv(name, env, model, baseUrl);
|
|
57
|
-
const { registry, skipped } = await providerPackages(discoverFn, env);
|
|
64
|
+
const { registry, skipped, packageAttributions = new Map() } = await providerPackages(discoverFn, env);
|
|
58
65
|
const specifier = registry.get(name);
|
|
59
66
|
if (specifier === undefined) {
|
|
60
67
|
const declined = skipped.get(name);
|
|
@@ -80,12 +87,18 @@ export const instantiateProvider = async (
|
|
|
80
87
|
if (contextWindow === null) {
|
|
81
88
|
throw new Error(`${specifier}: PLURNK_PROVIDERS_CONTEXT_WINDOW must be set because Models.dev has no metadata for provider "${name}"`);
|
|
82
89
|
}
|
|
90
|
+
const declared = packageAttributions.get(specifier) ?? [];
|
|
91
|
+
const attributions = (context: PluginAttributionContext): PluginAttribution => Meta.composeAttributions(
|
|
92
|
+
declared,
|
|
93
|
+
Meta.runtimeAttribution(sdkProvider, context, specifier),
|
|
94
|
+
);
|
|
83
95
|
return providerFromSdkModel({
|
|
84
96
|
name,
|
|
85
97
|
env,
|
|
86
98
|
model,
|
|
87
99
|
languageModel: sdkProvider.languageModel(model),
|
|
88
100
|
contextWindow,
|
|
101
|
+
attributions,
|
|
89
102
|
});
|
|
90
103
|
};
|
|
91
104
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { authoritativeChargeNormalizer } from "./accounting.ts";
|
|
4
|
+
|
|
5
|
+
const evidence = ({ providerMetadata, usage }: { providerMetadata?: unknown; usage?: unknown }) => ({
|
|
6
|
+
...(providerMetadata === undefined ? {} : { providerMetadata }),
|
|
7
|
+
...(usage === undefined ? {} : { usage }),
|
|
8
|
+
response: { id: "response-1" },
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("xAI response ticks normalize to an exact provider-authoritative charge", () => {
|
|
12
|
+
const normalize = authoritativeChargeNormalizer("@ai-sdk/xai");
|
|
13
|
+
assert.notEqual(normalize, undefined);
|
|
14
|
+
assert.deepEqual(normalize!(evidence({ usage: { cost_in_usd_ticks: 15_493_500 } })), {
|
|
15
|
+
kind: "authoritative",
|
|
16
|
+
amount: { amount: "15493500", currency: "USDTICK" },
|
|
17
|
+
usdEquivalent: "0.00154935",
|
|
18
|
+
source: "xAI response usage.cost_in_usd_ticks",
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("OpenRouter response cost normalizes without rate reconstruction", () => {
|
|
23
|
+
const normalize = authoritativeChargeNormalizer("@openrouter/ai-sdk-provider");
|
|
24
|
+
assert.notEqual(normalize, undefined);
|
|
25
|
+
assert.deepEqual(normalize!(evidence({ providerMetadata: { openrouter: { usage: { cost: 3.2e-7 } } } })), {
|
|
26
|
+
kind: "authoritative",
|
|
27
|
+
amount: { amount: "0.00000032", currency: "USD" },
|
|
28
|
+
usdEquivalent: "0.00000032",
|
|
29
|
+
source: "OpenRouter response usage.cost",
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("DeepInfra's documented response estimate wins over local rate reconstruction", () => {
|
|
34
|
+
const normalize = authoritativeChargeNormalizer("@ai-sdk/deepinfra");
|
|
35
|
+
assert.notEqual(normalize, undefined);
|
|
36
|
+
assert.deepEqual(normalize!(evidence({ usage: { estimated_cost: 5.04e-5 } })), {
|
|
37
|
+
kind: "authoritative",
|
|
38
|
+
amount: { amount: "0.0000504", currency: "USD" },
|
|
39
|
+
usdEquivalent: "0.0000504",
|
|
40
|
+
source: "DeepInfra response usage.estimated_cost",
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("response cost normalization is an explicit adapter capability", () => {
|
|
45
|
+
assert.equal(authoritativeChargeNormalizer("@ai-sdk/anthropic"), undefined);
|
|
46
|
+
assert.equal(
|
|
47
|
+
authoritativeChargeNormalizer("@ai-sdk/xai")!(evidence({ usage: {} })),
|
|
48
|
+
undefined,
|
|
49
|
+
);
|
|
50
|
+
assert.throws(
|
|
51
|
+
() => authoritativeChargeNormalizer("@ai-sdk/xai")!(evidence({ usage: { cost_in_usd_ticks: "1" } })),
|
|
52
|
+
/cost_in_usd_ticks must be numeric/,
|
|
53
|
+
);
|
|
54
|
+
assert.throws(
|
|
55
|
+
() => authoritativeChargeNormalizer("@ai-sdk/deepinfra")!(evidence({ usage: { estimated_cost: "1" } })),
|
|
56
|
+
/estimated_cost must be numeric/,
|
|
57
|
+
);
|
|
58
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthoritativeChargeNormalizer,
|
|
3
|
+
} from "./types.ts";
|
|
4
|
+
|
|
5
|
+
const recordOf = (value: unknown): Record<string, unknown> | null =>
|
|
6
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
7
|
+
? value as Record<string, unknown>
|
|
8
|
+
: null;
|
|
9
|
+
|
|
10
|
+
const decimalFromNumber = (value: number, subject: string): string => {
|
|
11
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
12
|
+
throw new TypeError(`${subject} must be a finite non-negative number`);
|
|
13
|
+
}
|
|
14
|
+
const source = String(value);
|
|
15
|
+
if (!/[eE]/.test(source)) return source;
|
|
16
|
+
const [coefficient, exponentSource] = source.toLowerCase().split("e");
|
|
17
|
+
const exponent = Number(exponentSource);
|
|
18
|
+
const [integer, fraction = ""] = coefficient!.split(".");
|
|
19
|
+
const digits = `${integer}${fraction}`;
|
|
20
|
+
const point = integer!.length + exponent;
|
|
21
|
+
if (point <= 0) return `0.${"0".repeat(-point)}${digits}`;
|
|
22
|
+
if (point >= digits.length) return `${digits}${"0".repeat(point - digits.length)}`;
|
|
23
|
+
return `${digits.slice(0, point)}.${digits.slice(point)}`;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const usdFromTicks = (ticks: number): string => {
|
|
27
|
+
if (!Number.isSafeInteger(ticks) || ticks < 0) {
|
|
28
|
+
throw new TypeError("xAI costInUsdTicks must be a non-negative safe integer");
|
|
29
|
+
}
|
|
30
|
+
const digits = String(ticks).padStart(11, "0");
|
|
31
|
+
const integer = digits.slice(0, -10).replace(/^0+(?=\d)/, "");
|
|
32
|
+
const fraction = digits.slice(-10).replace(/0+$/, "");
|
|
33
|
+
return fraction === "" ? integer : `${integer}.${fraction}`;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const xaiCharge: AuthoritativeChargeNormalizer = ({ usage }) => {
|
|
37
|
+
const wireUsage = recordOf(usage);
|
|
38
|
+
if (wireUsage === null || !("cost_in_usd_ticks" in wireUsage)) return undefined;
|
|
39
|
+
const ticks = wireUsage.cost_in_usd_ticks;
|
|
40
|
+
if (typeof ticks !== "number") {
|
|
41
|
+
throw new TypeError("xAI usage.cost_in_usd_ticks must be numeric");
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
kind: "authoritative",
|
|
45
|
+
amount: { amount: String(ticks), currency: "USDTICK" },
|
|
46
|
+
usdEquivalent: usdFromTicks(ticks),
|
|
47
|
+
source: "xAI response usage.cost_in_usd_ticks",
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const openRouterCharge: AuthoritativeChargeNormalizer = ({ providerMetadata }) => {
|
|
52
|
+
const usage = recordOf(recordOf(recordOf(providerMetadata)?.openrouter)?.usage);
|
|
53
|
+
if (usage === null || !("cost" in usage)) return undefined;
|
|
54
|
+
const cost = usage.cost;
|
|
55
|
+
if (typeof cost !== "number") throw new TypeError("OpenRouter usage.cost must be numeric");
|
|
56
|
+
const amount = decimalFromNumber(cost, "OpenRouter usage.cost");
|
|
57
|
+
return {
|
|
58
|
+
kind: "authoritative",
|
|
59
|
+
amount: { amount, currency: "USD" },
|
|
60
|
+
usdEquivalent: amount,
|
|
61
|
+
source: "OpenRouter response usage.cost",
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const deepInfraCharge: AuthoritativeChargeNormalizer = ({ usage }) => {
|
|
66
|
+
const wireUsage = recordOf(usage);
|
|
67
|
+
if (wireUsage === null || !("estimated_cost" in wireUsage)) return undefined;
|
|
68
|
+
const cost = wireUsage.estimated_cost;
|
|
69
|
+
if (typeof cost !== "number") throw new TypeError("DeepInfra usage.estimated_cost must be numeric");
|
|
70
|
+
const amount = decimalFromNumber(cost, "DeepInfra usage.estimated_cost");
|
|
71
|
+
return {
|
|
72
|
+
kind: "authoritative",
|
|
73
|
+
amount: { amount, currency: "USD" },
|
|
74
|
+
usdEquivalent: amount,
|
|
75
|
+
source: "DeepInfra response usage.estimated_cost",
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const authoritativeChargeNormalizer = (
|
|
80
|
+
sdkPackage: string,
|
|
81
|
+
): AuthoritativeChargeNormalizer | undefined => {
|
|
82
|
+
switch (sdkPackage) {
|
|
83
|
+
case "@ai-sdk/xai": return xaiCharge;
|
|
84
|
+
case "@ai-sdk/deepinfra": return deepInfraCharge;
|
|
85
|
+
case "@openrouter/ai-sdk-provider": return openRouterCharge;
|
|
86
|
+
default: return undefined;
|
|
87
|
+
}
|
|
88
|
+
};
|
package/src/aiSdkTransport.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createOpenAICompatible, type ProviderErrorStructure } from "@ai-sdk/openai-compatible";
|
|
2
|
-
import { generateText, streamText, type JSONValue, type LanguageModel, type LanguageModelUsage } from "ai";
|
|
2
|
+
import { APICallError, generateText, streamText, type JSONValue, type LanguageModel, type LanguageModelUsage } from "ai";
|
|
3
|
+
import { prepareRetries } from "ai/internal";
|
|
3
4
|
import { z } from "zod/v4";
|
|
4
|
-
import type { ChatMessage,
|
|
5
|
+
import type { ChatMessage, ProviderAttemptFinishReason, ProviderChargeEvidence, ProviderUsage, TokenLogprob } from "./types.ts";
|
|
5
6
|
import { normalizeUsage, type RawUsage } from "./usage.ts";
|
|
6
7
|
import { emitWarningOnce } from "./warnings.ts";
|
|
7
8
|
|
|
@@ -66,7 +67,15 @@ const wireUsageOf = (
|
|
|
66
67
|
return null;
|
|
67
68
|
};
|
|
68
69
|
|
|
69
|
-
const
|
|
70
|
+
const wireUsageEvidenceOf = (values: readonly unknown[]): unknown => {
|
|
71
|
+
for (let index = values.length - 1; index >= 0; index -= 1) {
|
|
72
|
+
const record = recordOf(values[index]);
|
|
73
|
+
if (record !== null && record.usage !== undefined) return record.usage;
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const finishReasonOf = (reason: string | undefined): ProviderAttemptFinishReason => {
|
|
70
79
|
switch (reason?.toLowerCase()) {
|
|
71
80
|
case "stop":
|
|
72
81
|
case "completed":
|
|
@@ -86,6 +95,8 @@ const finishReasonOf = (reason: string | undefined): FinishReason => {
|
|
|
86
95
|
case "safety":
|
|
87
96
|
case "recitation":
|
|
88
97
|
return "content_filter";
|
|
98
|
+
case "insufficient_system_resource":
|
|
99
|
+
return "resource_interrupted";
|
|
89
100
|
default:
|
|
90
101
|
if (reason !== undefined && reason.length > 0) {
|
|
91
102
|
emitWarningOnce(
|
|
@@ -133,7 +144,9 @@ export type AiSdkTransportResponse = {
|
|
|
133
144
|
model: string;
|
|
134
145
|
content: string;
|
|
135
146
|
reasoning: string;
|
|
136
|
-
|
|
147
|
+
reasoningProjected: boolean;
|
|
148
|
+
finishReason: ProviderAttemptFinishReason;
|
|
149
|
+
rawFinishReason?: string;
|
|
137
150
|
usage: ProviderUsage;
|
|
138
151
|
metadata: Record<string, unknown>;
|
|
139
152
|
reasoningEncrypted: Array<{
|
|
@@ -142,6 +155,7 @@ export type AiSdkTransportResponse = {
|
|
|
142
155
|
encrypted: Array<{ data: string; format: string | null }>;
|
|
143
156
|
}>;
|
|
144
157
|
logprobs: TokenLogprob[];
|
|
158
|
+
chargeEvidence: ProviderChargeEvidence;
|
|
145
159
|
rawBody?: unknown;
|
|
146
160
|
};
|
|
147
161
|
|
|
@@ -159,8 +173,46 @@ export type AiSdkModelRequest = Omit<AiSdkTransportRequest, "url" | "model" | "b
|
|
|
159
173
|
reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh" | "none" | "provider-default";
|
|
160
174
|
};
|
|
161
175
|
|
|
176
|
+
const isStreamIdleTimeout = (cause: unknown): cause is Error | DOMException =>
|
|
177
|
+
typeof cause === "object"
|
|
178
|
+
&& cause !== null
|
|
179
|
+
&& (cause as { name?: string }).name === "TimeoutError"
|
|
180
|
+
&& /chunk timeout/i.test(String((cause as { message?: unknown }).message ?? ""));
|
|
181
|
+
|
|
162
182
|
const executeModel = async (
|
|
163
183
|
request: AiSdkModelRequest,
|
|
184
|
+
): Promise<AiSdkTransportResponse> => {
|
|
185
|
+
const timeoutSignal = AbortSignal.timeout(request.fetchTimeoutMs);
|
|
186
|
+
const operationSignal = request.signal === undefined
|
|
187
|
+
? timeoutSignal
|
|
188
|
+
: AbortSignal.any([request.signal, timeoutSignal]);
|
|
189
|
+
const { retry } = prepareRetries({
|
|
190
|
+
maxRetries: request.retryAttempts,
|
|
191
|
+
abortSignal: operationSignal,
|
|
192
|
+
});
|
|
193
|
+
return retry(async () => {
|
|
194
|
+
try {
|
|
195
|
+
return await executeModelOnce({
|
|
196
|
+
...request,
|
|
197
|
+
signal: operationSignal,
|
|
198
|
+
retryAttempts: 0,
|
|
199
|
+
});
|
|
200
|
+
} catch (cause) {
|
|
201
|
+
if (operationSignal.aborted) throw operationSignal.reason;
|
|
202
|
+
if (!isStreamIdleTimeout(cause)) throw cause;
|
|
203
|
+
throw new APICallError({
|
|
204
|
+
message: cause.message,
|
|
205
|
+
url: "model:generation",
|
|
206
|
+
requestBodyValues: {},
|
|
207
|
+
cause,
|
|
208
|
+
isRetryable: true,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const executeModelOnce = async (
|
|
215
|
+
request: AiSdkModelRequest,
|
|
164
216
|
): Promise<AiSdkTransportResponse> => {
|
|
165
217
|
const {
|
|
166
218
|
languageModel: model,
|
|
@@ -223,17 +275,33 @@ const executeModel = async (
|
|
|
223
275
|
const rawBody = result.response.body;
|
|
224
276
|
const values = [rawBody];
|
|
225
277
|
const evidence = extractEvidence(values);
|
|
278
|
+
const accountingUsage = wireUsageEvidenceOf(values);
|
|
226
279
|
const reasoningText = evidence.reasoning || result.reasoningText || "";
|
|
280
|
+
const rawFinishReason = result.rawFinishReason;
|
|
227
281
|
return {
|
|
228
282
|
model: result.response.modelId,
|
|
229
283
|
content: result.text,
|
|
230
284
|
reasoning: reasoningText,
|
|
231
|
-
|
|
285
|
+
reasoningProjected: evidence.reasoningProjected,
|
|
286
|
+
finishReason: finishReasonOf(rawFinishReason),
|
|
287
|
+
...(rawFinishReason === undefined ? {} : { rawFinishReason }),
|
|
232
288
|
usage: wireUsageOf(values, reasoningText, result.text)
|
|
233
289
|
?? usageOf(result.usage, reasoningText, result.text),
|
|
234
290
|
metadata: metadataOf(values),
|
|
235
291
|
reasoningEncrypted: evidence.reasoningEncrypted,
|
|
236
292
|
logprobs: evidence.logprobs,
|
|
293
|
+
chargeEvidence: {
|
|
294
|
+
...(accountingUsage === undefined ? {} : { usage: accountingUsage }),
|
|
295
|
+
...(result.providerMetadata === undefined
|
|
296
|
+
? {}
|
|
297
|
+
: { providerMetadata: result.providerMetadata }),
|
|
298
|
+
response: {
|
|
299
|
+
id: result.response.id,
|
|
300
|
+
...(result.response.headers === undefined
|
|
301
|
+
? {}
|
|
302
|
+
: { headers: result.response.headers }),
|
|
303
|
+
},
|
|
304
|
+
},
|
|
237
305
|
...(request.captureRawBody ? { rawBody } : {}),
|
|
238
306
|
};
|
|
239
307
|
}
|
|
@@ -251,18 +319,34 @@ const executeModel = async (
|
|
|
251
319
|
}
|
|
252
320
|
if (streamError !== undefined) throw streamError;
|
|
253
321
|
const evidence = extractEvidence(rawChunks);
|
|
322
|
+
const accountingUsage = wireUsageEvidenceOf(rawChunks);
|
|
254
323
|
const content = await result.text;
|
|
255
324
|
const reasoningText = evidence.reasoning || (await result.reasoningText) || "";
|
|
325
|
+
const rawFinishReason = await result.rawFinishReason;
|
|
326
|
+
const [response, providerMetadata] = await Promise.all([
|
|
327
|
+
result.response,
|
|
328
|
+
result.providerMetadata,
|
|
329
|
+
]);
|
|
256
330
|
return {
|
|
257
|
-
model:
|
|
331
|
+
model: response.modelId,
|
|
258
332
|
content,
|
|
259
333
|
reasoning: reasoningText,
|
|
260
|
-
|
|
334
|
+
reasoningProjected: evidence.reasoningProjected,
|
|
335
|
+
finishReason: finishReasonOf(rawFinishReason),
|
|
336
|
+
...(rawFinishReason === undefined ? {} : { rawFinishReason }),
|
|
261
337
|
usage: wireUsageOf(rawChunks, reasoningText, content)
|
|
262
338
|
?? usageOf(await result.usage, reasoningText, content),
|
|
263
339
|
metadata: metadataOf(rawChunks),
|
|
264
340
|
reasoningEncrypted: evidence.reasoningEncrypted,
|
|
265
341
|
logprobs: evidence.logprobs,
|
|
342
|
+
chargeEvidence: {
|
|
343
|
+
...(accountingUsage === undefined ? {} : { usage: accountingUsage }),
|
|
344
|
+
...(providerMetadata === undefined ? {} : { providerMetadata }),
|
|
345
|
+
response: {
|
|
346
|
+
id: response.id,
|
|
347
|
+
...(response.headers === undefined ? {} : { headers: response.headers }),
|
|
348
|
+
},
|
|
349
|
+
},
|
|
266
350
|
...(request.captureRawBody ? { rawBody: rawChunks } : {}),
|
|
267
351
|
};
|
|
268
352
|
};
|
|
@@ -305,10 +389,12 @@ const extractEvidence = (values: unknown[]): {
|
|
|
305
389
|
reasoningEncrypted: AiSdkTransportResponse["reasoningEncrypted"];
|
|
306
390
|
logprobs: TokenLogprob[];
|
|
307
391
|
reasoning: string;
|
|
392
|
+
reasoningProjected: boolean;
|
|
308
393
|
} => {
|
|
309
394
|
const encrypted = new Map<string, AiSdkTransportResponse["reasoningEncrypted"][number]>();
|
|
310
395
|
const logprobs: TokenLogprob[] = [];
|
|
311
396
|
let reasoning = "";
|
|
397
|
+
let reasoningProjected = false;
|
|
312
398
|
let anonymous = 0;
|
|
313
399
|
for (const value of values) {
|
|
314
400
|
const choices = recordOf(value)?.choices;
|
|
@@ -336,7 +422,10 @@ const extractEvidence = (values: unknown[]): {
|
|
|
336
422
|
}
|
|
337
423
|
const message = recordOf(choice.delta) ?? recordOf(choice.message) ?? {};
|
|
338
424
|
for (const key of ["reasoning_content", "reasoning", "thinking"]) { // lexicon-allow: backend wire fields
|
|
339
|
-
if (typeof message[key] === "string")
|
|
425
|
+
if (typeof message[key] === "string") {
|
|
426
|
+
reasoningProjected = true;
|
|
427
|
+
reasoning += message[key];
|
|
428
|
+
}
|
|
340
429
|
}
|
|
341
430
|
if (!Array.isArray(message.reasoning_details)) continue;
|
|
342
431
|
for (const value of message.reasoning_details) {
|
|
@@ -348,6 +437,9 @@ const extractEvidence = (values: unknown[]): {
|
|
|
348
437
|
: id === null ? `anonymous:${anonymous++}` : `id:${id}`;
|
|
349
438
|
const item: AiSdkTransportResponse["reasoningEncrypted"][number] = encrypted.get(key) ?? {
|
|
350
439
|
id,
|
|
440
|
+
// {§provider-encrypted-reasoning} The documented wire location
|
|
441
|
+
// is the assistant message. `id` above still identifies only
|
|
442
|
+
// this provider detail, never a downstream message entity.
|
|
351
443
|
subtype: "message",
|
|
352
444
|
encrypted: [],
|
|
353
445
|
};
|
|
@@ -366,5 +458,6 @@ const extractEvidence = (values: unknown[]): {
|
|
|
366
458
|
reasoningEncrypted: [...encrypted.values()],
|
|
367
459
|
logprobs,
|
|
368
460
|
reasoning,
|
|
461
|
+
reasoningProjected,
|
|
369
462
|
};
|
|
370
463
|
};
|