@plurnk/plurnk-providers 1.7.0 → 1.9.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 +25 -13
- package/README.md +8 -1
- package/SPEC.md +103 -15
- package/dist/AiSdkProvider.d.ts +7 -4
- package/dist/AiSdkProvider.d.ts.map +1 -1
- package/dist/AiSdkProvider.js +150 -53
- package/dist/AiSdkProvider.js.map +1 -1
- package/dist/Mock.d.ts +2 -1
- package/dist/Mock.d.ts.map +1 -1
- package/dist/Mock.js +6 -1
- package/dist/Mock.js.map +1 -1
- package/dist/Pool.d.ts +2 -0
- package/dist/Pool.d.ts.map +1 -1
- package/dist/Pool.js +3 -0
- package/dist/Pool.js.map +1 -1
- package/dist/ProviderRegistry.d.ts.map +1 -1
- package/dist/ProviderRegistry.js +11 -10
- package/dist/ProviderRegistry.js.map +1 -1
- package/dist/accounting.d.ts.map +1 -1
- package/dist/accounting.js +16 -8
- package/dist/accounting.js.map +1 -1
- package/dist/aiSdkTransport.d.ts +4 -2
- package/dist/aiSdkTransport.d.ts.map +1 -1
- package/dist/aiSdkTransport.js +33 -6
- package/dist/aiSdkTransport.js.map +1 -1
- package/dist/catalogProvider.d.ts +4 -1
- package/dist/catalogProvider.d.ts.map +1 -1
- package/dist/catalogProvider.js +94 -3
- package/dist/catalogProvider.js.map +1 -1
- package/dist/compatibleProvider.d.ts.map +1 -1
- package/dist/compatibleProvider.js +2 -0
- package/dist/compatibleProvider.js.map +1 -1
- package/dist/cost.d.ts.map +1 -1
- package/dist/cost.js +5 -4
- package/dist/cost.js.map +1 -1
- package/dist/discover.d.ts +2 -0
- package/dist/discover.d.ts.map +1 -1
- package/dist/discover.js +13 -2
- package/dist/discover.js.map +1 -1
- package/dist/env.d.ts +3 -2
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +11 -4
- package/dist/env.js.map +1 -1
- package/dist/index.d.ts +10 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/notices.d.ts +1 -1
- package/dist/notices.d.ts.map +1 -1
- package/dist/openai.d.ts +1 -1
- package/dist/openai.d.ts.map +1 -1
- package/dist/openai.js +1 -1
- package/dist/openai.js.map +1 -1
- package/dist/sdkModels.d.ts +2 -0
- package/dist/sdkModels.d.ts.map +1 -1
- package/dist/sdkModels.js +163 -19
- package/dist/sdkModels.js.map +1 -1
- package/dist/types.d.ts +10 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +10 -1
- package/dist/types.js.map +1 -1
- package/package.json +9 -9
- package/src/AiSdkProvider.test.ts +214 -34
- package/src/AiSdkProvider.ts +181 -56
- package/src/Mock.test.ts +6 -1
- package/src/Mock.ts +6 -1
- package/src/Pool.test.ts +1 -0
- package/src/Pool.ts +5 -0
- package/src/ProviderRegistry.test.ts +27 -14
- package/src/ProviderRegistry.ts +19 -10
- package/src/accounting.test.ts +30 -2
- package/src/accounting.ts +16 -8
- package/src/aiSdkTransport.test.ts +3 -0
- package/src/aiSdkTransport.ts +38 -8
- package/src/catalogProvider.test.ts +151 -19
- package/src/catalogProvider.ts +125 -3
- package/src/compatibleProvider.test.ts +13 -10
- package/src/compatibleProvider.ts +2 -0
- package/src/cost.ts +5 -4
- package/src/discover.test.ts +27 -0
- package/src/discover.ts +20 -3
- package/src/env.test.ts +23 -8
- package/src/env.ts +18 -9
- package/src/errors.test.ts +2 -2
- package/src/index.ts +17 -8
- package/src/notices.ts +1 -1
- package/src/openai.ts +1 -1
- package/src/providerDefaults.test.ts +50 -0
- package/src/sdkModels.test.ts +142 -8
- package/src/sdkModels.ts +201 -19
- package/src/types.ts +22 -0
|
@@ -4,7 +4,7 @@ import { instantiateProvider, loadActiveProvider, resetDiscoveryCache } from "./
|
|
|
4
4
|
import type { PluginAttributionContext } from "@plurnk/plurnk-meta";
|
|
5
5
|
|
|
6
6
|
const mapOf = (entries: Record<string, string>, skipped: Record<string, string> = {}) =>
|
|
7
|
-
async () => ({ registry: new Map(Object.entries(entries)), skipped: new Map(Object.entries(skipped)), attributions: new Map<string, string | string[]>() });
|
|
7
|
+
async () => ({ registry: new Map(Object.entries(entries)), skipped: new Map(Object.entries(skipped)), attributions: new Map<string, string | string[]>(), grammarStyles: new Map() });
|
|
8
8
|
|
|
9
9
|
// Alias parsing is tested in @plurnk/plurnk-aliases (its owner). Here we
|
|
10
10
|
// exercise the resolution + two-tier instantiation this module owns; the active
|
|
@@ -31,7 +31,7 @@ test("instantiateProvider: cataloged name resolves in-framework, no scan, no imp
|
|
|
31
31
|
let scanned = false;
|
|
32
32
|
const p = await instantiateProvider("openai", { ...fullEnv }, "m",
|
|
33
33
|
async (s) => { imports.push(s); return {}; },
|
|
34
|
-
async () => { scanned = true; return { registry: new Map(), skipped: new Map(), attributions: new Map() }; });
|
|
34
|
+
async () => { scanned = true; return { registry: new Map(), skipped: new Map(), attributions: new Map(), grammarStyles: new Map() }; });
|
|
35
35
|
assert.equal(p.model, "m");
|
|
36
36
|
assert.deepEqual(imports, []); // tier 1 never touches the importer…
|
|
37
37
|
assert.equal(scanned, false); // …nor the scan
|
|
@@ -77,7 +77,7 @@ test("instantiateProvider: a selected plugin composes its static and runtime att
|
|
|
77
77
|
registry: new Map([["acme", "@acme/ai-provider"]]),
|
|
78
78
|
skipped: new Map(),
|
|
79
79
|
attributions: new Map([["acme", "static:provider"]]),
|
|
80
|
-
packageAttributions: new Map([["@acme/ai-provider", ["static:provider"]]]),
|
|
80
|
+
packageAttributions: new Map([["@acme/ai-provider", ["static:provider"]]]), grammarStyles: new Map(),
|
|
81
81
|
}),
|
|
82
82
|
);
|
|
83
83
|
|
|
@@ -107,7 +107,7 @@ test("instantiateProvider: a per-alias baseUrl drives the standard openai probe
|
|
|
107
107
|
return new Response(JSON.stringify({ data: [] }), { status: 200 });
|
|
108
108
|
});
|
|
109
109
|
await instantiateProvider("openai", { ...fullEnv }, "m", // fullEnv.OPENAI_BASE_URL is http://x — the override must win
|
|
110
|
-
async () => ({}), async () => ({ registry: new Map(), skipped: new Map(), attributions: new Map() }),
|
|
110
|
+
async () => ({}), async () => ({ registry: new Map(), skipped: new Map(), attributions: new Map(), grammarStyles: new Map() }),
|
|
111
111
|
"http://hazel2:8080/v1");
|
|
112
112
|
assert.ok(probed.some((u) => u === "http://hazel2:8080/v1/models"), `probe hit the override host; saw ${probed.join(", ")}`);
|
|
113
113
|
assert.equal(probed.some((u) => u.startsWith("http://x")), false); // never the per-name OPENAI_BASE_URL
|
|
@@ -193,13 +193,13 @@ test("instantiateProvider: per-alias knobs scope through to the provider (per-al
|
|
|
193
193
|
});
|
|
194
194
|
const env = { ...fullEnv, PLURNK_PROVIDERS_CONTEXT_WINDOW_turbo: "12345", PLURNK_PROVIDERS_LLAMA_SERVER_turbo: "1" };
|
|
195
195
|
const p = await instantiateProvider("openai", env, "m",
|
|
196
|
-
async () => ({}), async () => ({ registry: new Map(), skipped: new Map(), attributions: new Map() }),
|
|
196
|
+
async () => ({}), async () => ({ registry: new Map(), skipped: new Map(), attributions: new Map(), grammarStyles: new Map() }),
|
|
197
197
|
undefined, "turbo");
|
|
198
198
|
assert.equal(p.contextWindow, 12345); // _turbo CONTEXT_WINDOW reached the provider
|
|
199
199
|
assert.equal(p.constrainsOutput, true); // _turbo LLAMA_SERVER pin reached it too
|
|
200
200
|
// same env, DIFFERENT alias: neither override applies
|
|
201
201
|
const q = await instantiateProvider("openai", env, "m",
|
|
202
|
-
async () => ({}), async () => ({ registry: new Map(), skipped: new Map(), attributions: new Map() }),
|
|
202
|
+
async () => ({}), async () => ({ registry: new Map(), skipped: new Map(), attributions: new Map(), grammarStyles: new Map() }),
|
|
203
203
|
undefined, "plain");
|
|
204
204
|
assert.equal(q.contextWindow, null);
|
|
205
205
|
assert.equal(q.constrainsOutput, false);
|
|
@@ -310,7 +310,7 @@ test("a catalog provider with unknown model metadata never falls through to plug
|
|
|
310
310
|
"cloudflare",
|
|
311
311
|
{
|
|
312
312
|
CLOUDFLARE_ACCOUNT_ID: "account",
|
|
313
|
-
|
|
313
|
+
CLOUDFLARE_API_KEY: "token",
|
|
314
314
|
},
|
|
315
315
|
"vendor/model-outside-snapshot",
|
|
316
316
|
async () => { throw new Error("plugin import must not run"); },
|
|
@@ -319,7 +319,7 @@ test("a catalog provider with unknown model metadata never falls through to plug
|
|
|
319
319
|
return {
|
|
320
320
|
registry: new Map([["cloudflare", "@plurnk/plurnk-providers-cloudflare"]]),
|
|
321
321
|
skipped: new Map(),
|
|
322
|
-
attributions: new Map(),
|
|
322
|
+
attributions: new Map(), grammarStyles: new Map(),
|
|
323
323
|
};
|
|
324
324
|
},
|
|
325
325
|
),
|
|
@@ -334,7 +334,7 @@ test("explicit metadata constructs an out-of-snapshot Cloudflare model in the co
|
|
|
334
334
|
"cloudflare",
|
|
335
335
|
{
|
|
336
336
|
CLOUDFLARE_ACCOUNT_ID: "account",
|
|
337
|
-
|
|
337
|
+
CLOUDFLARE_API_KEY: "token",
|
|
338
338
|
PLURNK_PROVIDERS_CONTEXT_WINDOW: "128000",
|
|
339
339
|
},
|
|
340
340
|
"vendor/model-outside-snapshot",
|
|
@@ -344,7 +344,7 @@ test("explicit metadata constructs an out-of-snapshot Cloudflare model in the co
|
|
|
344
344
|
return {
|
|
345
345
|
registry: new Map([["cloudflare", "@plurnk/plurnk-providers-cloudflare"]]),
|
|
346
346
|
skipped: new Map(),
|
|
347
|
-
attributions: new Map(),
|
|
347
|
+
attributions: new Map(), grammarStyles: new Map(),
|
|
348
348
|
};
|
|
349
349
|
},
|
|
350
350
|
);
|
|
@@ -382,7 +382,7 @@ test("{§provider-tagged-reasoning} a Cloudflare model alias carries its explici
|
|
|
382
382
|
{
|
|
383
383
|
...fullEnv,
|
|
384
384
|
CLOUDFLARE_ACCOUNT_ID: "account",
|
|
385
|
-
|
|
385
|
+
CLOUDFLARE_API_KEY: "token",
|
|
386
386
|
PLURNK_PROVIDERS_REASONING_RESPONSE_STYLE: "verbatim",
|
|
387
387
|
PLURNK_PROVIDERS_REASONING_RESPONSE_STYLE_cfds1: "think-tags",
|
|
388
388
|
},
|
|
@@ -406,7 +406,17 @@ test("two Fireworks aliases independently select default and priority service ti
|
|
|
406
406
|
const bodies: Record<string, unknown>[] = [];
|
|
407
407
|
mock.method(globalThis, "fetch", async (_url: string, init?: RequestInit) => {
|
|
408
408
|
bodies.push(JSON.parse(String(init?.body)) as Record<string, unknown>);
|
|
409
|
-
|
|
409
|
+
const chunk = {
|
|
410
|
+
id: "fireworks-test",
|
|
411
|
+
object: "chat.completion.chunk",
|
|
412
|
+
created: 1,
|
|
413
|
+
model: "fireworks-test",
|
|
414
|
+
choices: [{ index: 0, delta: { content: "ok" }, finish_reason: "stop" }],
|
|
415
|
+
};
|
|
416
|
+
return new Response(`data: ${JSON.stringify(chunk)}\n\ndata: [DONE]\n\n`, {
|
|
417
|
+
status: 200,
|
|
418
|
+
headers: { "content-type": "text/event-stream" },
|
|
419
|
+
});
|
|
410
420
|
});
|
|
411
421
|
const env = {
|
|
412
422
|
...fullEnv,
|
|
@@ -419,7 +429,7 @@ test("two Fireworks aliases independently select default and priority service ti
|
|
|
419
429
|
PLURNK_PROVIDERS_SERVICE_TIER_standard: "default",
|
|
420
430
|
};
|
|
421
431
|
const imports = async () => ({});
|
|
422
|
-
const discover = async () => ({ registry: new Map(), skipped: new Map(), attributions: new Map() });
|
|
432
|
+
const discover = async () => ({ registry: new Map(), skipped: new Map(), attributions: new Map(), grammarStyles: new Map() });
|
|
423
433
|
const fast = await instantiateProvider("fireworks", env, "accounts/fireworks/routers/glm-5p2-fast", imports, discover, undefined, "fast");
|
|
424
434
|
const standard = await instantiateProvider("fireworks", env, "deepseek-v4-pro", imports, discover, undefined, "standard");
|
|
425
435
|
await fast.generate({ workerId: "fast-worker", messages: [] });
|
|
@@ -445,5 +455,8 @@ test("loadActiveProvider: resolves the alias cascade to an installed AI SDK prov
|
|
|
445
455
|
});
|
|
446
456
|
|
|
447
457
|
test("loadActiveProvider: throws a named error when no alias is active", async () => {
|
|
448
|
-
await assert.rejects(
|
|
458
|
+
await assert.rejects(
|
|
459
|
+
() => loadActiveProvider({ ...fullEnv }),
|
|
460
|
+
/set PLURNK_MODEL to a declared alias or provider\/model route/,
|
|
461
|
+
);
|
|
449
462
|
});
|
package/src/ProviderRegistry.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// Provider instantiation + active-
|
|
1
|
+
// Provider instantiation + active model-route resolution. Alias parsing (the
|
|
2
2
|
// PLURNK_MODEL_<alias>=<provider>/<model> cascade + PLURNK_BASEURL_<alias>
|
|
3
|
-
// overrides) lives in @plurnk/plurnk-aliases
|
|
4
|
-
//
|
|
3
|
+
// overrides) lives in @plurnk/plurnk-aliases; this module resolves the active
|
|
4
|
+
// alias-or-route selector 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
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import type { AiSdkProviderPlugin, Provider } from "./types.ts";
|
|
12
12
|
import { catalogProviderFromEnv, providerFromSdkModel } from "./catalogProvider.ts";
|
|
13
13
|
import { discover, type DiscoverOptions, type Discovery } from "./discover.ts";
|
|
14
|
-
import {
|
|
14
|
+
import { resolveActiveRoute } from "@plurnk/plurnk-aliases";
|
|
15
15
|
import { scopeEnvToAlias } from "./env.ts";
|
|
16
16
|
import { ollamaProviderFromEnv } from "./ollama.ts";
|
|
17
17
|
import { compatibleProviderFromEnv } from "./compatibleProvider.ts";
|
|
@@ -61,7 +61,7 @@ export const instantiateProvider = async (
|
|
|
61
61
|
if (catalog !== null) return catalog;
|
|
62
62
|
if (name === "ollama") return ollamaProviderFromEnv(env, model, baseUrl === undefined ? undefined : { baseUrl });
|
|
63
63
|
if (name === "openai" || name === "plurnk") return compatibleProviderFromEnv(name, env, model, baseUrl);
|
|
64
|
-
const { registry, skipped, packageAttributions = new Map() } = await providerPackages(discoverFn, env);
|
|
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) {
|
|
67
67
|
const declined = skipped.get(name);
|
|
@@ -99,21 +99,30 @@ export const instantiateProvider = async (
|
|
|
99
99
|
languageModel: sdkProvider.languageModel(model),
|
|
100
100
|
contextWindow,
|
|
101
101
|
attributions,
|
|
102
|
+
...(grammarStyles.get(name) === undefined ? {} : { grammarStyle: grammarStyles.get(name) }),
|
|
102
103
|
});
|
|
103
104
|
};
|
|
104
105
|
|
|
105
106
|
// Test-only: drop the memoized discovery so a fresh scan/injection runs next.
|
|
106
107
|
export const resetDiscoveryCache = (): void => { discoveredCache = null; };
|
|
107
108
|
|
|
108
|
-
// Boot convenience: resolve the active
|
|
109
|
+
// Boot convenience: resolve the active selector and instantiate its exact route.
|
|
109
110
|
export const loadActiveProvider = async (
|
|
110
111
|
env: NodeJS.ProcessEnv = process.env,
|
|
111
112
|
importImpl: ImportModule = importModule,
|
|
112
113
|
discoverFn: DiscoverFn = discover,
|
|
113
114
|
): Promise<Provider> => {
|
|
114
|
-
const
|
|
115
|
-
if (
|
|
116
|
-
throw new Error("no active provider: set PLURNK_MODEL to
|
|
115
|
+
const route = resolveActiveRoute(env);
|
|
116
|
+
if (route === null) {
|
|
117
|
+
throw new Error("no active provider: set PLURNK_MODEL to a declared alias or provider/model route");
|
|
117
118
|
}
|
|
118
|
-
return instantiateProvider(
|
|
119
|
+
return instantiateProvider(
|
|
120
|
+
route.provider,
|
|
121
|
+
env,
|
|
122
|
+
route.model,
|
|
123
|
+
importImpl,
|
|
124
|
+
discoverFn,
|
|
125
|
+
route.baseUrl,
|
|
126
|
+
route.alias,
|
|
127
|
+
);
|
|
119
128
|
};
|
package/src/accounting.test.ts
CHANGED
|
@@ -89,6 +89,34 @@ test("aggregateProviderAccounting preserves request order and only sums known fi
|
|
|
89
89
|
},
|
|
90
90
|
]);
|
|
91
91
|
assert.deepEqual(accounting.requests.map(({ provider }) => provider), ["provider:a", "provider:b"]);
|
|
92
|
-
assert.
|
|
93
|
-
|
|
92
|
+
assert.deepEqual(accounting.usage, {
|
|
93
|
+
inputTokens: 2,
|
|
94
|
+
outputTokens: 3,
|
|
95
|
+
totalTokens: 5,
|
|
96
|
+
}, "a response-less failure is skipped, never allowed to erase reported usage");
|
|
97
|
+
assert.equal(accounting.costUsd, "0.25", "a response-less failure is skipped; the expressible cost survives");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("aggregateProviderAccounting omits unknown nested usage fields from its JSON projection", () => {
|
|
101
|
+
const accounting = aggregateProviderAccounting([{
|
|
102
|
+
provider: "provider:a",
|
|
103
|
+
model: "m",
|
|
104
|
+
outcome: "response",
|
|
105
|
+
usage: {
|
|
106
|
+
inputTokens: 2,
|
|
107
|
+
outputTokens: 3,
|
|
108
|
+
totalTokens: 5,
|
|
109
|
+
inputTokenDetails: { cacheReadTokens: 1 },
|
|
110
|
+
outputTokenDetails: { reasoningTokens: 2 },
|
|
111
|
+
},
|
|
112
|
+
cost: { kind: "unknown", reason: "no direct cost" },
|
|
113
|
+
}]);
|
|
114
|
+
|
|
115
|
+
assert.deepEqual(accounting.usage, {
|
|
116
|
+
inputTokens: 2,
|
|
117
|
+
outputTokens: 3,
|
|
118
|
+
totalTokens: 5,
|
|
119
|
+
inputTokenDetails: { cacheReadTokens: 1 },
|
|
120
|
+
outputTokenDetails: { reasoningTokens: 2 },
|
|
121
|
+
});
|
|
94
122
|
});
|
package/src/accounting.ts
CHANGED
|
@@ -126,12 +126,13 @@ const sumKnown = (
|
|
|
126
126
|
requests: readonly ProviderRequestAccounting[],
|
|
127
127
|
read: (usage: ProviderUsage) => number | undefined,
|
|
128
128
|
): number | undefined => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
? undefined
|
|
134
|
-
|
|
129
|
+
// {§tokenomics-provider-usage} — aggregate usage sums every reported
|
|
130
|
+
// quantity; an unreported one (a response-less failure) is skipped, never
|
|
131
|
+
// invented as zero and never allowed to erase the reported evidence.
|
|
132
|
+
const known = requests
|
|
133
|
+
.map((request) => request.usage === undefined ? undefined : read(request.usage))
|
|
134
|
+
.filter((value): value is number => value !== undefined);
|
|
135
|
+
return known.length === 0 ? undefined : known.reduce((sum, value) => sum + value, 0);
|
|
135
136
|
};
|
|
136
137
|
|
|
137
138
|
export const aggregateProviderAccounting = (
|
|
@@ -167,10 +168,17 @@ export const aggregateProviderAccounting = (
|
|
|
167
168
|
const inputTokenDetails = noCacheTokens === undefined
|
|
168
169
|
&& cacheReadTokens === undefined && cacheWriteTokens === undefined
|
|
169
170
|
? undefined
|
|
170
|
-
: {
|
|
171
|
+
: {
|
|
172
|
+
...(noCacheTokens === undefined ? {} : { noCacheTokens }),
|
|
173
|
+
...(cacheReadTokens === undefined ? {} : { cacheReadTokens }),
|
|
174
|
+
...(cacheWriteTokens === undefined ? {} : { cacheWriteTokens }),
|
|
175
|
+
};
|
|
171
176
|
const outputTokenDetails = textTokens === undefined && reasoningTokens === undefined
|
|
172
177
|
? undefined
|
|
173
|
-
: {
|
|
178
|
+
: {
|
|
179
|
+
...(textTokens === undefined ? {} : { textTokens }),
|
|
180
|
+
...(reasoningTokens === undefined ? {} : { reasoningTokens }),
|
|
181
|
+
};
|
|
174
182
|
const usage = inputTokens === undefined && outputTokens === undefined && totalTokens === undefined
|
|
175
183
|
&& inputTokenDetails === undefined && outputTokenDetails === undefined
|
|
176
184
|
? null
|
|
@@ -208,6 +208,7 @@ test("the adapter preserves nonstandard reasoning accounting after SDK parsing",
|
|
|
208
208
|
});
|
|
209
209
|
|
|
210
210
|
await t.test("streamed Fireworks-style channels do not invent an output split", async () => {
|
|
211
|
+
const reasoning: string[] = [];
|
|
211
212
|
const chunks = [
|
|
212
213
|
{
|
|
213
214
|
id: "response-1",
|
|
@@ -232,6 +233,7 @@ test("the adapter preserves nonstandard reasoning accounting after SDK parsing",
|
|
|
232
233
|
const result = await executeOpenAICompatible({
|
|
233
234
|
...request,
|
|
234
235
|
streaming: true,
|
|
236
|
+
observeReasoning: (delta) => reasoning.push(delta),
|
|
235
237
|
fetch: async () => new Response(
|
|
236
238
|
`${chunks.map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`).join("")}data: [DONE]\n\n`,
|
|
237
239
|
{ headers: { "content-type": "text/event-stream" } },
|
|
@@ -242,5 +244,6 @@ test("the adapter preserves nonstandard reasoning accounting after SDK parsing",
|
|
|
242
244
|
outputTokens: 10,
|
|
243
245
|
totalTokens: 12,
|
|
244
246
|
});
|
|
247
|
+
assert.equal(reasoning.join(""), "bbbbbb", "readable reasoning is observed before transport completion");
|
|
245
248
|
});
|
|
246
249
|
});
|
package/src/aiSdkTransport.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createOpenAICompatible, type ProviderErrorStructure } from "@ai-sdk/openai-compatible";
|
|
2
|
-
import { APICallError, generateText, streamText, type JSONValue, type LanguageModel, type LanguageModelUsage } from "ai";
|
|
2
|
+
import { APICallError, generateText, streamText, type CallWarning, type JSONValue, type LanguageModel, type LanguageModelUsage } from "ai";
|
|
3
3
|
import { z } from "zod/v4";
|
|
4
|
-
import type { ChatMessage, ProviderAttemptFinishReason, ProviderChargeEvidence, ProviderUsage, TokenLogprob } from "./types.ts";
|
|
4
|
+
import type { ChatMessage, ProviderAttemptFinishReason, ProviderChargeEvidence, ProviderReasoningObserver, ProviderUsage, TokenLogprob } from "./types.ts";
|
|
5
5
|
import { normalizeUsage, type RawUsage } from "./usage.ts";
|
|
6
6
|
import { emitWarningOnce } from "./warnings.ts";
|
|
7
7
|
import { ProviderTimeoutError, providerTimeoutOf } from "./errors.ts";
|
|
@@ -157,6 +157,7 @@ export type AiSdkTransportRequest = {
|
|
|
157
157
|
streamIdleTimeoutMs?: number;
|
|
158
158
|
streaming: boolean;
|
|
159
159
|
captureRawBody: boolean;
|
|
160
|
+
observeReasoning?: ProviderReasoningObserver;
|
|
160
161
|
};
|
|
161
162
|
|
|
162
163
|
export type AiSdkTransportResponse = {
|
|
@@ -176,6 +177,7 @@ export type AiSdkTransportResponse = {
|
|
|
176
177
|
logprobs: TokenLogprob[];
|
|
177
178
|
chargeEvidence: ProviderChargeEvidence;
|
|
178
179
|
rawBody?: unknown;
|
|
180
|
+
warnings: readonly CallWarning[];
|
|
179
181
|
};
|
|
180
182
|
|
|
181
183
|
export type AiSdkModelRequest = Omit<AiSdkTransportRequest, "url" | "model" | "body" | "fetch"> & {
|
|
@@ -221,11 +223,32 @@ const transportTimeout = (
|
|
|
221
223
|
|
|
222
224
|
const streamFailureValues = new WeakMap<object, readonly unknown[]>();
|
|
223
225
|
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
+
const retainStreamFailureValues = <T extends object>(source: object, target: T): T => {
|
|
227
|
+
const values = streamFailureValues.get(source);
|
|
228
|
+
if (values !== undefined) streamFailureValues.set(target, values);
|
|
229
|
+
return target;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const normalizeRetryAttemptError = (error: unknown): unknown => {
|
|
233
|
+
if (!APICallError.isInstance(error)) {
|
|
234
|
+
// Node's Undici stream reader reports a peer-aborted HTTP/2 body as this
|
|
235
|
+
// raw TypeError after headers have arrived. Normalize it at the attempt
|
|
236
|
+
// boundary so the owned scheduler sees the same retryability that the
|
|
237
|
+
// public ProviderError contract would otherwise assign too late.
|
|
238
|
+
if (error instanceof TypeError && error.message.trim().toLowerCase() === "terminated") {
|
|
239
|
+
return retainStreamFailureValues(error, new APICallError({
|
|
240
|
+
message: error.message,
|
|
241
|
+
url: "model:generation",
|
|
242
|
+
requestBodyValues: {},
|
|
243
|
+
cause: error,
|
|
244
|
+
isRetryable: true,
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
return error;
|
|
248
|
+
}
|
|
226
249
|
const directed = retryDirective(error.statusCode, error.responseHeaders ?? {});
|
|
227
250
|
if (directed === null || directed === error.isRetryable) return error;
|
|
228
|
-
return new APICallError({
|
|
251
|
+
return retainStreamFailureValues(error, new APICallError({
|
|
229
252
|
message: error.message,
|
|
230
253
|
url: error.url,
|
|
231
254
|
requestBodyValues: error.requestBodyValues,
|
|
@@ -235,7 +258,7 @@ const applyRetryDirective = (error: unknown): unknown => {
|
|
|
235
258
|
cause: error,
|
|
236
259
|
isRetryable: directed,
|
|
237
260
|
data: error.data,
|
|
238
|
-
});
|
|
261
|
+
}));
|
|
239
262
|
};
|
|
240
263
|
|
|
241
264
|
const executeModel = async (
|
|
@@ -246,7 +269,7 @@ const executeModel = async (
|
|
|
246
269
|
} catch (cause) {
|
|
247
270
|
if (request.signal?.aborted) throw request.signal.reason;
|
|
248
271
|
const timeout = transportTimeout(cause, request);
|
|
249
|
-
if (timeout === null) throw
|
|
272
|
+
if (timeout === null) throw normalizeRetryAttemptError(cause);
|
|
250
273
|
throw new APICallError({
|
|
251
274
|
message: timeout.message,
|
|
252
275
|
url: "model:generation",
|
|
@@ -364,6 +387,7 @@ const executeModelOnce = async (
|
|
|
364
387
|
},
|
|
365
388
|
},
|
|
366
389
|
...(request.captureRawBody ? { rawBody } : {}),
|
|
390
|
+
warnings: result.warnings ?? [],
|
|
367
391
|
};
|
|
368
392
|
}
|
|
369
393
|
|
|
@@ -376,6 +400,9 @@ const executeModelOnce = async (
|
|
|
376
400
|
let streamError: unknown;
|
|
377
401
|
for await (const part of result.fullStream) {
|
|
378
402
|
if (part.type === "raw") rawChunks.push(part.rawValue);
|
|
403
|
+
if (part.type === "reasoning-delta" && part.text.length > 0) {
|
|
404
|
+
request.observeReasoning?.(part.text);
|
|
405
|
+
}
|
|
379
406
|
if (part.type === "error") streamError ??= part.error;
|
|
380
407
|
}
|
|
381
408
|
if (streamError !== undefined) {
|
|
@@ -389,9 +416,10 @@ const executeModelOnce = async (
|
|
|
389
416
|
const content = await result.text;
|
|
390
417
|
const reasoningText = evidence.reasoning || (await result.reasoningText) || "";
|
|
391
418
|
const rawFinishReason = await result.rawFinishReason;
|
|
392
|
-
const [response, providerMetadata] = await Promise.all([
|
|
419
|
+
const [response, providerMetadata, warnings] = await Promise.all([
|
|
393
420
|
result.response,
|
|
394
421
|
result.providerMetadata,
|
|
422
|
+
result.warnings,
|
|
395
423
|
]);
|
|
396
424
|
return {
|
|
397
425
|
model: response.modelId,
|
|
@@ -416,6 +444,7 @@ const executeModelOnce = async (
|
|
|
416
444
|
},
|
|
417
445
|
},
|
|
418
446
|
...(request.captureRawBody ? { rawBody: rawChunks } : {}),
|
|
447
|
+
warnings: warnings ?? [],
|
|
419
448
|
};
|
|
420
449
|
};
|
|
421
450
|
|
|
@@ -450,6 +479,7 @@ export const executeOpenAICompatible = async (
|
|
|
450
479
|
streamIdleTimeoutMs: request.streamIdleTimeoutMs,
|
|
451
480
|
streaming: request.streaming,
|
|
452
481
|
captureRawBody: request.captureRawBody,
|
|
482
|
+
...(request.observeReasoning === undefined ? {} : { observeReasoning: request.observeReasoning }),
|
|
453
483
|
});
|
|
454
484
|
};
|
|
455
485
|
|