@plurnk/plurnk-providers 1.3.12 → 1.4.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 +39 -22
- package/README.md +65 -4
- package/SPEC.md +222 -56
- package/dist/AiSdkProvider.d.ts +18 -5
- package/dist/AiSdkProvider.d.ts.map +1 -1
- package/dist/AiSdkProvider.js +240 -153
- package/dist/AiSdkProvider.js.map +1 -1
- package/dist/Mock.d.ts +14 -15
- package/dist/Mock.d.ts.map +1 -1
- package/dist/Mock.js +26 -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/aiSdkTransport.d.ts +4 -2
- package/dist/aiSdkTransport.d.ts.map +1 -1
- package/dist/aiSdkTransport.js +18 -3
- package/dist/aiSdkTransport.js.map +1 -1
- package/dist/catalogProvider.d.ts +3 -1
- package/dist/catalogProvider.d.ts.map +1 -1
- package/dist/catalogProvider.js +20 -7
- 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 +64 -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 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +29 -9
- 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 +150 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +11 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -4
- 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.map +1 -1
- package/dist/sdkModels.js +4 -3
- package/dist/sdkModels.js.map +1 -1
- package/dist/types.d.ts +43 -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 +3 -0
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +26 -14
- package/dist/usage.js.map +1 -1
- package/dist/warnings.js +0 -0
- package/dist/warnings.js.map +1 -1
- package/package.json +13 -9
- package/src/AiSdkProvider.test.ts +480 -159
- package/src/AiSdkProvider.ts +320 -196
- package/src/Mock.test.ts +29 -14
- package/src/Mock.ts +33 -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/aiSdkTransport.ts +25 -6
- package/src/boundaries.test.ts +8 -3
- package/src/catalogProvider.test.ts +17 -0
- package/src/catalogProvider.ts +25 -10
- package/src/compatibleProvider.test.ts +96 -0
- package/src/compatibleProvider.ts +15 -6
- package/src/cost.test.ts +63 -0
- package/src/cost.ts +83 -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 -23
- package/src/env.ts +45 -18
- package/src/errors.test.ts +148 -0
- package/src/errors.ts +207 -0
- package/src/index.ts +29 -7
- 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 +7 -0
- package/src/sdkModels.ts +4 -8
- package/src/types.ts +106 -64
- package/src/usage.test.ts +15 -4
- package/src/usage.ts +32 -14
- 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
|
|
package/src/aiSdkTransport.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createOpenAICompatible, type ProviderErrorStructure } from "@ai-sdk/openai-compatible";
|
|
2
2
|
import { generateText, streamText, type JSONValue, type LanguageModel, type LanguageModelUsage } from "ai";
|
|
3
3
|
import { z } from "zod/v4";
|
|
4
|
-
import type { ChatMessage,
|
|
4
|
+
import type { ChatMessage, ProviderAttemptFinishReason, ProviderUsage, TokenLogprob } from "./types.ts";
|
|
5
5
|
import { normalizeUsage, type RawUsage } from "./usage.ts";
|
|
6
6
|
import { emitWarningOnce } from "./warnings.ts";
|
|
7
7
|
|
|
@@ -66,7 +66,7 @@ const wireUsageOf = (
|
|
|
66
66
|
return null;
|
|
67
67
|
};
|
|
68
68
|
|
|
69
|
-
const finishReasonOf = (reason: string | undefined):
|
|
69
|
+
const finishReasonOf = (reason: string | undefined): ProviderAttemptFinishReason => {
|
|
70
70
|
switch (reason?.toLowerCase()) {
|
|
71
71
|
case "stop":
|
|
72
72
|
case "completed":
|
|
@@ -86,6 +86,8 @@ const finishReasonOf = (reason: string | undefined): FinishReason => {
|
|
|
86
86
|
case "safety":
|
|
87
87
|
case "recitation":
|
|
88
88
|
return "content_filter";
|
|
89
|
+
case "insufficient_system_resource":
|
|
90
|
+
return "resource_interrupted";
|
|
89
91
|
default:
|
|
90
92
|
if (reason !== undefined && reason.length > 0) {
|
|
91
93
|
emitWarningOnce(
|
|
@@ -133,7 +135,9 @@ export type AiSdkTransportResponse = {
|
|
|
133
135
|
model: string;
|
|
134
136
|
content: string;
|
|
135
137
|
reasoning: string;
|
|
136
|
-
|
|
138
|
+
reasoningProjected: boolean;
|
|
139
|
+
finishReason: ProviderAttemptFinishReason;
|
|
140
|
+
rawFinishReason?: string;
|
|
137
141
|
usage: ProviderUsage;
|
|
138
142
|
metadata: Record<string, unknown>;
|
|
139
143
|
reasoningEncrypted: Array<{
|
|
@@ -224,11 +228,14 @@ const executeModel = async (
|
|
|
224
228
|
const values = [rawBody];
|
|
225
229
|
const evidence = extractEvidence(values);
|
|
226
230
|
const reasoningText = evidence.reasoning || result.reasoningText || "";
|
|
231
|
+
const rawFinishReason = result.rawFinishReason;
|
|
227
232
|
return {
|
|
228
233
|
model: result.response.modelId,
|
|
229
234
|
content: result.text,
|
|
230
235
|
reasoning: reasoningText,
|
|
231
|
-
|
|
236
|
+
reasoningProjected: evidence.reasoningProjected,
|
|
237
|
+
finishReason: finishReasonOf(rawFinishReason),
|
|
238
|
+
...(rawFinishReason === undefined ? {} : { rawFinishReason }),
|
|
232
239
|
usage: wireUsageOf(values, reasoningText, result.text)
|
|
233
240
|
?? usageOf(result.usage, reasoningText, result.text),
|
|
234
241
|
metadata: metadataOf(values),
|
|
@@ -253,11 +260,14 @@ const executeModel = async (
|
|
|
253
260
|
const evidence = extractEvidence(rawChunks);
|
|
254
261
|
const content = await result.text;
|
|
255
262
|
const reasoningText = evidence.reasoning || (await result.reasoningText) || "";
|
|
263
|
+
const rawFinishReason = await result.rawFinishReason;
|
|
256
264
|
return {
|
|
257
265
|
model: (await result.response).modelId,
|
|
258
266
|
content,
|
|
259
267
|
reasoning: reasoningText,
|
|
260
|
-
|
|
268
|
+
reasoningProjected: evidence.reasoningProjected,
|
|
269
|
+
finishReason: finishReasonOf(rawFinishReason),
|
|
270
|
+
...(rawFinishReason === undefined ? {} : { rawFinishReason }),
|
|
261
271
|
usage: wireUsageOf(rawChunks, reasoningText, content)
|
|
262
272
|
?? usageOf(await result.usage, reasoningText, content),
|
|
263
273
|
metadata: metadataOf(rawChunks),
|
|
@@ -305,10 +315,12 @@ const extractEvidence = (values: unknown[]): {
|
|
|
305
315
|
reasoningEncrypted: AiSdkTransportResponse["reasoningEncrypted"];
|
|
306
316
|
logprobs: TokenLogprob[];
|
|
307
317
|
reasoning: string;
|
|
318
|
+
reasoningProjected: boolean;
|
|
308
319
|
} => {
|
|
309
320
|
const encrypted = new Map<string, AiSdkTransportResponse["reasoningEncrypted"][number]>();
|
|
310
321
|
const logprobs: TokenLogprob[] = [];
|
|
311
322
|
let reasoning = "";
|
|
323
|
+
let reasoningProjected = false;
|
|
312
324
|
let anonymous = 0;
|
|
313
325
|
for (const value of values) {
|
|
314
326
|
const choices = recordOf(value)?.choices;
|
|
@@ -336,7 +348,10 @@ const extractEvidence = (values: unknown[]): {
|
|
|
336
348
|
}
|
|
337
349
|
const message = recordOf(choice.delta) ?? recordOf(choice.message) ?? {};
|
|
338
350
|
for (const key of ["reasoning_content", "reasoning", "thinking"]) { // lexicon-allow: backend wire fields
|
|
339
|
-
if (typeof message[key] === "string")
|
|
351
|
+
if (typeof message[key] === "string") {
|
|
352
|
+
reasoningProjected = true;
|
|
353
|
+
reasoning += message[key];
|
|
354
|
+
}
|
|
340
355
|
}
|
|
341
356
|
if (!Array.isArray(message.reasoning_details)) continue;
|
|
342
357
|
for (const value of message.reasoning_details) {
|
|
@@ -348,6 +363,9 @@ const extractEvidence = (values: unknown[]): {
|
|
|
348
363
|
: id === null ? `anonymous:${anonymous++}` : `id:${id}`;
|
|
349
364
|
const item: AiSdkTransportResponse["reasoningEncrypted"][number] = encrypted.get(key) ?? {
|
|
350
365
|
id,
|
|
366
|
+
// {§provider-encrypted-reasoning} The documented wire location
|
|
367
|
+
// is the assistant message. `id` above still identifies only
|
|
368
|
+
// this provider detail, never a downstream message entity.
|
|
351
369
|
subtype: "message",
|
|
352
370
|
encrypted: [],
|
|
353
371
|
};
|
|
@@ -366,5 +384,6 @@ const extractEvidence = (values: unknown[]): {
|
|
|
366
384
|
reasoningEncrypted: [...encrypted.values()],
|
|
367
385
|
logprobs,
|
|
368
386
|
reasoning,
|
|
387
|
+
reasoningProjected,
|
|
369
388
|
};
|
|
370
389
|
};
|
package/src/boundaries.test.ts
CHANGED
|
@@ -19,17 +19,22 @@ test("provider source does not import the service or database", () => {
|
|
|
19
19
|
test("provider source does not import the PLURNK parser", () => {
|
|
20
20
|
for (const file of sourceFiles()) {
|
|
21
21
|
const source = readFileSync(join(root, file), "utf8");
|
|
22
|
-
assert.ok(
|
|
22
|
+
assert.ok(
|
|
23
|
+
!/from\s+["']@plurnk\/plurnk-contracts\/grammar["']/.test(source),
|
|
24
|
+
`${file} imports the parser rather than the runtime-neutral contracts entrypoint`,
|
|
25
|
+
);
|
|
23
26
|
}
|
|
24
27
|
});
|
|
25
28
|
|
|
26
|
-
test("
|
|
29
|
+
test("the OpenAI-compatible entrypoint excludes Node-owned provider machinery", () => {
|
|
27
30
|
const allowed = new Set([
|
|
28
31
|
"AiSdkProvider.ts",
|
|
29
32
|
"aiSdkTransport.ts",
|
|
30
33
|
"env.ts",
|
|
34
|
+
"errors.ts",
|
|
35
|
+
"notices.ts",
|
|
31
36
|
"openai.ts",
|
|
32
|
-
"
|
|
37
|
+
"promptTokens.ts",
|
|
33
38
|
"types.ts",
|
|
34
39
|
"usage.ts",
|
|
35
40
|
"warnings.ts",
|
|
@@ -15,6 +15,7 @@ const env = {
|
|
|
15
15
|
PLURNK_PROVIDERS_REASONING_RESERVE: "10%",
|
|
16
16
|
PLURNK_PROVIDERS_COMPLETION_RESERVE: "25%",
|
|
17
17
|
PLURNK_PROVIDERS_RETRY_ATTEMPTS: "0",
|
|
18
|
+
PLURNK_PROVIDERS_ERROR_DETAIL_LIMIT: "512",
|
|
18
19
|
PLURNK_PROVIDERS_PROMPT_CACHE_KEY: "1",
|
|
19
20
|
};
|
|
20
21
|
|
|
@@ -39,6 +40,22 @@ test("catalog provider resolves model physics and Models.dev USD rates", () => {
|
|
|
39
40
|
}) ?? 0) > 0);
|
|
40
41
|
});
|
|
41
42
|
|
|
43
|
+
test("an operator context window caps catalog physics and percentage reserves derive from the cap", () => {
|
|
44
|
+
const provider = catalogProviderFromEnv("openai", {
|
|
45
|
+
...env,
|
|
46
|
+
PLURNK_PROVIDERS_CONTEXT_WINDOW: "128000",
|
|
47
|
+
}, "gpt-4.1-mini");
|
|
48
|
+
assert.equal(provider?.contextWindow, 128_000);
|
|
49
|
+
assert.equal(provider?.reasoningReserve, 16_000);
|
|
50
|
+
assert.equal(provider?.completionReserve, 32_000);
|
|
51
|
+
|
|
52
|
+
const oversized = catalogProviderFromEnv("openai", {
|
|
53
|
+
...env,
|
|
54
|
+
PLURNK_PROVIDERS_CONTEXT_WINDOW: "2000000",
|
|
55
|
+
}, "gpt-4.1-mini");
|
|
56
|
+
assert.equal(oversized?.contextWindow, 1_047_576, "an operator ceiling cannot enlarge model physics");
|
|
57
|
+
});
|
|
58
|
+
|
|
42
59
|
test("official AI SDK provider owns the native request while PLURNK owns call settings", async () => {
|
|
43
60
|
const calls: Array<{ url: string; body: Record<string, unknown> }> = [];
|
|
44
61
|
mock.method(globalThis, "fetch", async (input: string | URL | Request, init?: RequestInit) => {
|
package/src/catalogProvider.ts
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
import { lookupProvider, resolveModel, type ModelInfo } from "@plurnk/plurnk-models";
|
|
2
2
|
import {
|
|
3
3
|
contextWindowFromEnv,
|
|
4
|
+
effectiveContextWindow,
|
|
4
5
|
dataCaptureFromEnv,
|
|
5
6
|
envelopeFromEnv,
|
|
6
7
|
parseRequiredFloat,
|
|
7
8
|
parseRequiredInt,
|
|
8
9
|
promptCacheKeyFromEnv,
|
|
9
10
|
reasoningFromEnv,
|
|
11
|
+
reasoningResponseStyleFromEnv,
|
|
10
12
|
resolveReserve,
|
|
11
13
|
tokenRatesFromEnv,
|
|
12
14
|
type ReserveSpec,
|
|
13
15
|
} from "./env.ts";
|
|
14
16
|
import AiSdkProvider, { type ReasoningStyle } from "./AiSdkProvider.ts";
|
|
15
17
|
import { configuredProviderInfo, createSdkModel } from "./sdkModels.ts";
|
|
16
|
-
import { providerSource } from "./
|
|
18
|
+
import { providerSource } from "./notices.ts";
|
|
17
19
|
import type { Provider, ProviderUsage } from "./types.ts";
|
|
18
20
|
import { calculateCostUsd } from "./usage.ts";
|
|
19
21
|
import { emitWarningOnce } from "./warnings.ts";
|
|
20
22
|
import type { LanguageModel } from "ai";
|
|
23
|
+
import type { PluginAttribution, PluginAttributionContext } from "@plurnk/plurnk-meta";
|
|
24
|
+
import type { ProviderCost } from "@plurnk/plurnk-contracts";
|
|
21
25
|
|
|
22
26
|
const reasoningStyleFromEnv = (
|
|
23
27
|
env: NodeJS.ProcessEnv,
|
|
@@ -28,7 +32,7 @@ const reasoningStyleFromEnv = (
|
|
|
28
32
|
if (value === undefined || value.length === 0) return undefined;
|
|
29
33
|
const styles: readonly ReasoningStyle[] = [
|
|
30
34
|
"none", "think", "include_reasoning", "effort",
|
|
31
|
-
"effort_explicit", "template", "anthropic",
|
|
35
|
+
"effort_explicit", "thinking_effort", "template", "anthropic",
|
|
32
36
|
];
|
|
33
37
|
if (!styles.includes(value as ReasoningStyle)) {
|
|
34
38
|
throw new Error(`${name} provider: PLURNK_PROVIDERS_PROVIDER_${prefix}_REASONING_STYLE has invalid value "${value}"`);
|
|
@@ -45,6 +49,7 @@ export const providerFromSdkModel = ({
|
|
|
45
49
|
headers,
|
|
46
50
|
contextWindow,
|
|
47
51
|
info,
|
|
52
|
+
attributions,
|
|
48
53
|
}: {
|
|
49
54
|
name: string;
|
|
50
55
|
env: NodeJS.ProcessEnv;
|
|
@@ -54,10 +59,11 @@ export const providerFromSdkModel = ({
|
|
|
54
59
|
headers?: Readonly<Record<string, string>>;
|
|
55
60
|
contextWindow: number;
|
|
56
61
|
info?: ModelInfo;
|
|
62
|
+
attributions?: (context: PluginAttributionContext) => PluginAttribution;
|
|
57
63
|
}): Provider => {
|
|
58
64
|
emitWarningOnce(
|
|
59
|
-
`${name} provider:
|
|
60
|
-
"
|
|
65
|
+
`${name} provider: physical prompt counting is a chars/2 estimate; over-policy recovery fails closed without exact or bounded request evidence`,
|
|
66
|
+
"PLURNK_PROMPT_COUNT_ESTIMATE",
|
|
61
67
|
);
|
|
62
68
|
|
|
63
69
|
const reasoning = reasoningFromEnv(env, name);
|
|
@@ -86,9 +92,20 @@ export const providerFromSdkModel = ({
|
|
|
86
92
|
const calculateCost = rates === null
|
|
87
93
|
? undefined
|
|
88
94
|
: (usage: ProviderUsage): number => calculateCostUsd(usage, rates);
|
|
95
|
+
const rateSource = configuredRates === null ? "Models.dev catalog rates" : "operator-configured rates";
|
|
96
|
+
const calculateCharge: (usage: ProviderUsage) => Exclude<ProviderCost, { kind: "authoritative" }> = rates === null
|
|
97
|
+
? () => ({ kind: "unknown", reason: "no provider rate or settled charge is available" })
|
|
98
|
+
: rates.input === 0 && rates.output === 0 && rates.cached === 0
|
|
99
|
+
? () => ({ kind: "free", source: rateSource })
|
|
100
|
+
: (usage: ProviderUsage) => ({
|
|
101
|
+
kind: "estimated",
|
|
102
|
+
usd: String(calculateCostUsd(usage, rates)),
|
|
103
|
+
source: rateSource,
|
|
104
|
+
});
|
|
89
105
|
|
|
90
106
|
return new AiSdkProvider({
|
|
91
107
|
model,
|
|
108
|
+
...(attributions === undefined ? {} : { attributions }),
|
|
92
109
|
...(languageModel === undefined ? {} : { languageModel }),
|
|
93
110
|
...(url === undefined ? {} : { url }),
|
|
94
111
|
...(headers === undefined ? {} : { headers: { ...headers } }),
|
|
@@ -96,16 +113,19 @@ export const providerFromSdkModel = ({
|
|
|
96
113
|
fetchTimeoutMs: parseRequiredInt(env.PLURNK_PROVIDERS_FETCH_TIMEOUT, "PLURNK_PROVIDERS_FETCH_TIMEOUT", name),
|
|
97
114
|
streamIdleTimeoutMs: parseRequiredInt(env.PLURNK_PROVIDERS_STREAM_IDLE_TIMEOUT, "PLURNK_PROVIDERS_STREAM_IDLE_TIMEOUT", name),
|
|
98
115
|
reasoning,
|
|
116
|
+
reasoningResponseStyle: reasoningResponseStyleFromEnv(env, name),
|
|
99
117
|
temperature: parseRequiredFloat(env.PLURNK_PROVIDERS_TEMPERATURE, "PLURNK_PROVIDERS_TEMPERATURE", name, 0),
|
|
100
118
|
repeatPenalty: parseRequiredFloat(env.PLURNK_PROVIDERS_REPEAT_PENALTY, "PLURNK_PROVIDERS_REPEAT_PENALTY", name, 0),
|
|
101
119
|
frequencyPenalty: parseRequiredFloat(env.PLURNK_PROVIDERS_FREQUENCY_PENALTY, "PLURNK_PROVIDERS_FREQUENCY_PENALTY", name, 0),
|
|
102
120
|
reasoningReserve,
|
|
103
121
|
completionReserve,
|
|
104
122
|
retryAttempts: parseRequiredInt(env.PLURNK_PROVIDERS_RETRY_ATTEMPTS, "PLURNK_PROVIDERS_RETRY_ATTEMPTS", name),
|
|
123
|
+
errorDetailLimit: parseRequiredInt(env.PLURNK_PROVIDERS_ERROR_DETAIL_LIMIT, "PLURNK_PROVIDERS_ERROR_DETAIL_LIMIT", name),
|
|
105
124
|
reasoningStyle: reasoningStyleFromEnv(env, name),
|
|
106
125
|
promptCacheKey: url === undefined ? false : promptCacheKeyFromEnv(env, name),
|
|
107
126
|
serviceTier: env.PLURNK_PROVIDERS_SERVICE_TIER,
|
|
108
127
|
calculateCost,
|
|
128
|
+
calculateCharge,
|
|
109
129
|
source: providerSource(name),
|
|
110
130
|
gbnfDebug: env.PLURNK_PROVIDERS_GBNF_DEBUG !== undefined
|
|
111
131
|
&& env.PLURNK_PROVIDERS_GBNF_DEBUG !== ""
|
|
@@ -133,13 +153,8 @@ export const catalogProviderFromEnv = (
|
|
|
133
153
|
const sdk = createSdkModel(name, wireModel, env, baseUrlOverride);
|
|
134
154
|
if (sdk === null) return null;
|
|
135
155
|
|
|
136
|
-
emitWarningOnce(
|
|
137
|
-
`${name} provider: countTokens is a chars/2 upper bound — exact counts come from the mimetypes tokenizer seam or tokenize()`,
|
|
138
|
-
"PLURNK_TOKENIZER_HEURISTIC",
|
|
139
|
-
);
|
|
140
|
-
|
|
141
156
|
const info = resolved?.info;
|
|
142
|
-
const contextWindow = contextOverride
|
|
157
|
+
const contextWindow = effectiveContextWindow(contextOverride, info?.contextWindow ?? null);
|
|
143
158
|
if (contextWindow === null) {
|
|
144
159
|
throw new Error(
|
|
145
160
|
`${name} provider: context window unresolved for "${wireModel}" — set PLURNK_PROVIDERS_CONTEXT_WINDOW or update the Models.dev snapshot`,
|