@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
package/src/discover.test.ts
CHANGED
|
@@ -5,7 +5,7 @@ import os from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { discover } from "./discover.ts";
|
|
7
7
|
|
|
8
|
-
//
|
|
8
|
+
// Create a temp dir and register its removal on the test context, so it is
|
|
9
9
|
// cleaned on a GREEN or RED run. A trailing rm after the assertions leaks the dir
|
|
10
10
|
// whenever one throws — thousands accumulate on a shared box at drill frequency.
|
|
11
11
|
const tempDir = async (t: TestContext, prefix: string): Promise<string> => {
|
|
@@ -47,6 +47,17 @@ test("discover: a provider package missing plurnk.name is ignored, not crashed",
|
|
|
47
47
|
assert.deepEqual([...registry.keys()], ["named"]);
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
+
test("discover: an array kind claims no provider family", async (t) => {
|
|
51
|
+
const root = await buildModules(t, {
|
|
52
|
+
"@acme/dual": {
|
|
53
|
+
name: "@acme/dual",
|
|
54
|
+
plurnk: { kind: ["provider", "scheme"], name: "dual" },
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
const { registry } = await discover({ cwd: root });
|
|
58
|
+
assert.equal(registry.size, 0);
|
|
59
|
+
});
|
|
60
|
+
|
|
50
61
|
test("discover: a name claimed by two packages is a fail-hard collision", async (t) => {
|
|
51
62
|
const root = await buildModules(t, {
|
|
52
63
|
"@plurnk/plurnk-provider-native": { name: "@plurnk/plurnk-provider-native", plurnk: { kind: "provider", name: "native" } },
|
|
@@ -71,18 +82,48 @@ test("discover: node_modules entries with no package.json or malformed JSON are
|
|
|
71
82
|
assert.deepEqual([...registry.keys()], ["native"]); // only the well-formed provider survives
|
|
72
83
|
});
|
|
73
84
|
|
|
74
|
-
test("discover:
|
|
85
|
+
test("discover: normalizes attribution per package and preserves the published provider projection", async (t) => {
|
|
75
86
|
const root = await buildModules(t, {
|
|
76
87
|
"@acme/provider-solo": { name: "@acme/provider-solo", plurnk: { kind: "provider", name: "solo", attribution: "@acme/solo" } },
|
|
77
88
|
"@acme/provider-multi": { name: "@acme/provider-multi", plurnk: { kind: "provider", name: "multi", attribution: ["@acme/a", "@acme/b"] } },
|
|
78
89
|
"@acme/provider-none": { name: "@acme/provider-none", plurnk: { kind: "provider", name: "none" } },
|
|
79
|
-
"@acme/provider-bad": { name: "@acme/provider-bad", plurnk: { kind: "provider", name: "bad", attribution: 42 } }, // non-string/array
|
|
80
90
|
});
|
|
81
|
-
const { attributions } = await discover({ cwd: root });
|
|
91
|
+
const { attributions, packageAttributions } = await discover({ cwd: root });
|
|
82
92
|
assert.equal(attributions.get("solo"), "@acme/solo");
|
|
83
93
|
assert.deepEqual(attributions.get("multi"), ["@acme/a", "@acme/b"]);
|
|
84
|
-
assert.equal(attributions.has("none"), false);
|
|
85
|
-
assert.
|
|
94
|
+
assert.equal(attributions.has("none"), false);
|
|
95
|
+
assert.deepEqual([...packageAttributions], [
|
|
96
|
+
["@acme/provider-multi", ["@acme/a", "@acme/b"]],
|
|
97
|
+
["@acme/provider-solo", ["@acme/solo"]],
|
|
98
|
+
]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("discover: validates trusted attribution before provider admission", async (t) => {
|
|
102
|
+
const root = await buildModules(t, {
|
|
103
|
+
"@acme/provider-bad": { name: "@acme/provider-bad", plurnk: { kind: "provider", name: "bad", attribution: 42 } },
|
|
104
|
+
});
|
|
105
|
+
await assert.rejects(
|
|
106
|
+
discover({ cwd: root }),
|
|
107
|
+
/plugin '@acme\/provider-bad': plurnk\.attribution must be a non-empty string or string\[\]/,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const reservedRoot = await buildModules(t, {
|
|
111
|
+
"@acme/provider-reserved": { name: "@acme/provider-reserved", plurnk: { kind: "provider", name: "reserved", attribution: "@plurnk/staff" } },
|
|
112
|
+
});
|
|
113
|
+
await assert.rejects(discover({ cwd: reservedRoot }), /'@plurnk\/' is reserved/);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("discover: an untrusted malformed attribution is withheld before validation", async (t) => {
|
|
117
|
+
const root = await buildModules(t, {
|
|
118
|
+
"@acme/provider-bad": { name: "@acme/provider-bad", plurnk: { kind: "provider", name: "bad", attribution: ["ok", 42] } },
|
|
119
|
+
});
|
|
120
|
+
const result = await discover({
|
|
121
|
+
cwd: root,
|
|
122
|
+
env: { PLURNK_PLUGINS_TRUSTED_ONLY: "1" } as NodeJS.ProcessEnv,
|
|
123
|
+
});
|
|
124
|
+
assert.equal(result.registry.size, 0);
|
|
125
|
+
assert.equal(result.skipped.get("bad"), "@acme/provider-bad");
|
|
126
|
+
assert.equal(result.packageAttributions.size, 0);
|
|
86
127
|
});
|
|
87
128
|
|
|
88
129
|
test("discover: missing node_modules yields an empty registry, not an error", async (t) => {
|
|
@@ -91,7 +132,7 @@ test("discover: missing node_modules yields an empty registry, not an error", as
|
|
|
91
132
|
assert.equal(registry.size, 0);
|
|
92
133
|
});
|
|
93
134
|
|
|
94
|
-
// — trust gate (
|
|
135
|
+
// — trust gate ({§plugin-trust-boundary}) —
|
|
95
136
|
|
|
96
137
|
const trustFixture = (t: TestContext) => buildModules(t, {
|
|
97
138
|
"@plurnk/plurnk-provider-native": { name: "@plurnk/plurnk-provider-native", plurnk: { kind: "provider", name: "native" } },
|
package/src/discover.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import Meta from "@plurnk/plurnk-meta";
|
|
4
|
+
import type {
|
|
5
|
+
PackageAttributions,
|
|
6
|
+
PluginAttribution,
|
|
7
|
+
PluginAttributionDeclaration,
|
|
8
|
+
} from "@plurnk/plurnk-meta";
|
|
4
9
|
|
|
5
|
-
// Scope-agnostic discovery of installed AI SDK provider packages
|
|
10
|
+
// Scope-agnostic discovery of installed AI SDK provider packages
|
|
11
|
+
// ({§plugin-family-kind}).
|
|
6
12
|
// Parallel to @plurnk/plurnk-execs' discover(): scan every installed package
|
|
7
13
|
// under `<cwd>/node_modules` — scoped (`@scope/name`) and unscoped — and keep
|
|
8
14
|
// the ones declaring `plurnk.kind === "provider"`. Scope-agnostic so a THIRD
|
|
@@ -16,7 +22,7 @@ import Meta from "@plurnk/plurnk-meta";
|
|
|
16
22
|
//
|
|
17
23
|
// Cataloged and operator-declared providers resolve before this scan.
|
|
18
24
|
//
|
|
19
|
-
// Host plugin trust gate (PLURNK_PLUGINS_TRUSTED_ONLY
|
|
25
|
+
// {§plugin-trust-boundary} Host plugin trust gate (PLURNK_PLUGINS_TRUSTED_ONLY)
|
|
20
26
|
// — enforced uniformly across the four scope-agnostic families. An untrusted
|
|
21
27
|
// package is discovered-but-declined (recorded in `skipped`, never registered,
|
|
22
28
|
// never thrown), so the consumer can name it in a precise error.
|
|
@@ -31,12 +37,9 @@ export type DiscoverOptions = {
|
|
|
31
37
|
export type Discovery = {
|
|
32
38
|
registry: Map<string, string>; // trusted providers, eligible to instantiate
|
|
33
39
|
skipped: Map<string, string>; // declined by the trust gate (untrusted)
|
|
34
|
-
// name
|
|
35
|
-
// provider package's manifest, for crediting the provider's author (#21,
|
|
36
|
-
// plurnk-service#249). Surfaced verbatim — the consumer applies the reservation
|
|
37
|
-
// policy (`@plurnk/` tags only from `@plurnk/`-scoped packages). Absent for
|
|
38
|
-
// providers that declare none.
|
|
40
|
+
// Published name-keyed projection retained for 1.x consumers.
|
|
39
41
|
attributions: Map<string, string | string[]>;
|
|
42
|
+
packageAttributions: PackageAttributions;
|
|
40
43
|
};
|
|
41
44
|
|
|
42
45
|
|
|
@@ -46,7 +49,8 @@ export const discover = async (options: DiscoverOptions = {}): Promise<Discovery
|
|
|
46
49
|
|
|
47
50
|
const registry = new Map<string, string>();
|
|
48
51
|
const skipped = new Map<string, string>();
|
|
49
|
-
const attributions = new Map<string,
|
|
52
|
+
const attributions = new Map<string, PluginAttributionDeclaration>();
|
|
53
|
+
const packageAttributions = new Map<string, PluginAttribution>();
|
|
50
54
|
for (const dir of dirs) {
|
|
51
55
|
const info = await readProviderInfo(dir);
|
|
52
56
|
if (info === null) continue;
|
|
@@ -61,10 +65,13 @@ export const discover = async (options: DiscoverOptions = {}): Promise<Discovery
|
|
|
61
65
|
+ `${existing} and ${info.packageName}`,
|
|
62
66
|
);
|
|
63
67
|
}
|
|
68
|
+
const tags = Meta.normalizeAttribution(info.attribution, info.packageName);
|
|
64
69
|
registry.set(info.name, info.packageName);
|
|
65
|
-
|
|
70
|
+
const attribution = attributionProjection(info.attribution, tags);
|
|
71
|
+
if (attribution !== undefined) attributions.set(info.name, attribution);
|
|
72
|
+
if (tags.length > 0) packageAttributions.set(info.packageName, tags);
|
|
66
73
|
}
|
|
67
|
-
return { registry, skipped, attributions };
|
|
74
|
+
return { registry, skipped, attributions, packageAttributions };
|
|
68
75
|
};
|
|
69
76
|
|
|
70
77
|
// Enumerate every installed package directory — scoped and unscoped — under
|
|
@@ -74,11 +81,10 @@ const defaultPackageDirs = async (cwd: string): Promise<string[]> => {
|
|
|
74
81
|
return (await Meta.packageDirs(nm)).map((c) => c.dir).toSorted();
|
|
75
82
|
};
|
|
76
83
|
|
|
77
|
-
// One
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
type ProviderInfo = { name: string; packageName: string; attribution?: string | string[] };
|
|
84
|
+
// One inert manifest record for a provider package, or null for anything that
|
|
85
|
+
// isn't one. Attribution remains unknown until trust admission, then the shared
|
|
86
|
+
// {§plugin-attribution} boundary validates it.
|
|
87
|
+
type ProviderInfo = { name: string; packageName: string; attribution: unknown };
|
|
82
88
|
|
|
83
89
|
const readProviderInfo = async (dir: string): Promise<ProviderInfo | null> => {
|
|
84
90
|
let raw: string;
|
|
@@ -98,12 +104,16 @@ const readProviderInfo = async (dir: string): Promise<ProviderInfo | null> => {
|
|
|
98
104
|
const plurnk = record.plurnk;
|
|
99
105
|
if (typeof plurnk !== "object" || plurnk === null) return null;
|
|
100
106
|
const plurnkRec = plurnk as Record<string, unknown>;
|
|
101
|
-
if (plurnkRec
|
|
107
|
+
if (!Meta.declaresKind(plurnkRec, "provider")) return null;
|
|
102
108
|
if (typeof plurnkRec.name !== "string" || plurnkRec.name === "") return null;
|
|
103
109
|
if (typeof record.name !== "string" || record.name === "") return null;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
110
|
+
return { name: plurnkRec.name, packageName: record.name, attribution: plurnkRec.attribution };
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const attributionProjection = (
|
|
114
|
+
raw: unknown,
|
|
115
|
+
tags: PluginAttribution,
|
|
116
|
+
): PluginAttributionDeclaration | undefined => {
|
|
117
|
+
if (raw === undefined || raw === null) return undefined;
|
|
118
|
+
return typeof raw === "string" ? raw : [...tags];
|
|
109
119
|
};
|
package/src/env.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import { strict as assert } from "node:assert";
|
|
3
|
-
import { parseRequiredInt, parseOptionalInt, requireEnv, reasoningFromEnv,
|
|
3
|
+
import { parseRequiredInt, parseOptionalInt, requireEnv, reasoningFromEnv, reasoningResponseStyleFromEnv } from "./env.ts";
|
|
4
4
|
|
|
5
5
|
test("parseRequiredInt: parses a non-negative integer", () => {
|
|
6
6
|
assert.equal(parseRequiredInt("600000", "PLURNK_PROVIDERS_FETCH_TIMEOUT", "openai"), 600000);
|
|
@@ -29,7 +29,7 @@ test("parseOptionalInt: rejects fractional and negative values", () => {
|
|
|
29
29
|
assert.throws(() => parseOptionalInt("-8", "PLURNK_PROVIDERS_CONTEXT_WINDOW", "openai"), /must be a non-negative integer/);
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
test("reasoningFromEnv: activation modes parse; budget required IFF on; fail-hard on everything else
|
|
32
|
+
test("reasoningFromEnv: activation modes parse; budget required IFF on; fail-hard on everything else", () => {
|
|
33
33
|
assert.deepEqual(reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: "off" }, "openai"), { mode: "off", budget: null });
|
|
34
34
|
assert.deepEqual(reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: "adaptive" }, "openai"), { mode: "adaptive", budget: null });
|
|
35
35
|
assert.deepEqual(reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: "on", PLURNK_PROVIDERS_REASONING_BUDGET: "4096" }, "openai"), { mode: "on", budget: 4096 });
|
|
@@ -40,37 +40,25 @@ test("reasoningFromEnv: activation modes parse; budget required IFF on; fail-har
|
|
|
40
40
|
assert.throws(() => reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: "on", PLURNK_PROVIDERS_REASONING_BUDGET: "1.5" }, "openai"), /positive integer/);
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
+
test("{§provider-tagged-reasoning} response style is explicit and invalid values fail at the provider boundary", () => {
|
|
44
|
+
assert.equal(reasoningResponseStyleFromEnv({}, "cloudflare"), "verbatim");
|
|
45
|
+
assert.equal(reasoningResponseStyleFromEnv({
|
|
46
|
+
PLURNK_PROVIDERS_REASONING_RESPONSE_STYLE: "think-tags",
|
|
47
|
+
}, "cloudflare"), "think-tags");
|
|
48
|
+
assert.throws(
|
|
49
|
+
() => reasoningResponseStyleFromEnv({
|
|
50
|
+
PLURNK_PROVIDERS_REASONING_RESPONSE_STYLE: "auto",
|
|
51
|
+
}, "cloudflare"),
|
|
52
|
+
/cloudflare provider: PLURNK_PROVIDERS_REASONING_RESPONSE_STYLE must be "verbatim" or "think-tags" \(got "auto"\)/,
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
43
56
|
test("requireEnv: returns the value or throws a named error", () => {
|
|
44
57
|
assert.equal(requireEnv("sk-x", "OPENAI_API_KEY", "openai"), "sk-x");
|
|
45
58
|
assert.throws(() => requireEnv(undefined, "GROQ_API_KEY", "groq"), /groq provider: GROQ_API_KEY must be set/);
|
|
46
59
|
assert.throws(() => requireEnv("", "GROQ_API_KEY", "groq"), /must be set/);
|
|
47
60
|
});
|
|
48
61
|
|
|
49
|
-
test("tokenRatesFromEnv is all-or-nothing, with cached input defaulting to input", () => {
|
|
50
|
-
assert.equal(tokenRatesFromEnv({}, "cloudflare"), null);
|
|
51
|
-
assert.deepEqual(tokenRatesFromEnv({
|
|
52
|
-
PLURNK_PROVIDERS_INPUT_USD_PER_MILLION: "0.435",
|
|
53
|
-
PLURNK_PROVIDERS_OUTPUT_USD_PER_MILLION: "0.87",
|
|
54
|
-
}, "cloudflare"), {
|
|
55
|
-
input: 0.435,
|
|
56
|
-
cached: 0.435,
|
|
57
|
-
output: 0.87,
|
|
58
|
-
});
|
|
59
|
-
assert.deepEqual(tokenRatesFromEnv({
|
|
60
|
-
PLURNK_PROVIDERS_INPUT_USD_PER_MILLION: "3",
|
|
61
|
-
PLURNK_PROVIDERS_CACHE_READ_USD_PER_MILLION: "0.3",
|
|
62
|
-
PLURNK_PROVIDERS_OUTPUT_USD_PER_MILLION: "15",
|
|
63
|
-
}, "cloudflare"), {
|
|
64
|
-
input: 3,
|
|
65
|
-
cached: 0.3,
|
|
66
|
-
output: 15,
|
|
67
|
-
});
|
|
68
|
-
assert.throws(
|
|
69
|
-
() => tokenRatesFromEnv({ PLURNK_PROVIDERS_INPUT_USD_PER_MILLION: "1" }, "cloudflare"),
|
|
70
|
-
/PLURNK_PROVIDERS_OUTPUT_USD_PER_MILLION must be set/,
|
|
71
|
-
);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
62
|
// — per-alias knob scoping (per-alias scoping doctrine, user 2026-07-03) —
|
|
75
63
|
|
|
76
64
|
test("scopeEnvToAlias: suffixed knob wins, bare is the fallback, other aliases ignored", async () => {
|
|
@@ -79,15 +67,17 @@ test("scopeEnvToAlias: suffixed knob wins, bare is the fallback, other aliases i
|
|
|
79
67
|
PLURNK_PROVIDERS_REASONING: "off",
|
|
80
68
|
PLURNK_PROVIDERS_REASONING_turboderp: "on",
|
|
81
69
|
PLURNK_PROVIDERS_REASONING_BUDGET_TURBODERP: "4096", // case-folds like PLURNK_MODEL_ keys
|
|
82
|
-
|
|
70
|
+
PLURNK_PROVIDERS_REASONING_RESPONSE_STYLE_TURBODERP: "think-tags",
|
|
71
|
+
PLURNK_PROVIDERS_CONTEXT_WINDOW_turboderp: "8000",
|
|
83
72
|
PLURNK_PROVIDERS_COMPLETION_RESERVE_turboderp: "4096",
|
|
84
73
|
PLURNK_PROVIDERS_CONTEXT_WINDOW_other: "1",
|
|
85
74
|
} as NodeJS.ProcessEnv;
|
|
86
75
|
const scoped = scopeEnvToAlias(env, "turboderp");
|
|
87
76
|
assert.equal(scoped.PLURNK_PROVIDERS_REASONING, "on");
|
|
88
77
|
assert.equal(scoped.PLURNK_PROVIDERS_REASONING_BUDGET, "4096");
|
|
89
|
-
assert.equal(scoped.
|
|
90
|
-
assert.equal(scoped.
|
|
78
|
+
assert.equal(scoped.PLURNK_PROVIDERS_REASONING_RESPONSE_STYLE, "think-tags");
|
|
79
|
+
assert.equal(scoped.PLURNK_PROVIDERS_CONTEXT_WINDOW, "8000");
|
|
80
|
+
assert.equal(scoped.PLURNK_PROVIDERS_COMPLETION_RESERVE, "4096");
|
|
91
81
|
assert.equal(scopeEnvToAlias(env, "plain").PLURNK_PROVIDERS_REASONING, "off"); // fallback intact
|
|
92
82
|
});
|
|
93
83
|
|
|
@@ -103,7 +93,7 @@ test("scopeEnvToAlias: aliases with underscores resolve; a bare knob is never mi
|
|
|
103
93
|
assert.equal(scopeEnvToAlias(env, "budget").PLURNK_PROVIDERS_REASONING, "off"); // collision guard
|
|
104
94
|
});
|
|
105
95
|
|
|
106
|
-
test("
|
|
96
|
+
test("dataCaptureFromEnv: both knobs OFF by default, ON when set (TOP_LOGPROBS = the OpenAI top_logprobs count)", async () => {
|
|
107
97
|
const { dataCaptureFromEnv } = await import("./env.ts");
|
|
108
98
|
assert.deepEqual(dataCaptureFromEnv({} as NodeJS.ProcessEnv, "x"), { topLogprobs: null, rawBody: false });
|
|
109
99
|
assert.deepEqual(dataCaptureFromEnv({ PLURNK_PROVIDERS_RAWBODY: "0" } as NodeJS.ProcessEnv, "x"), { topLogprobs: null, rawBody: false });
|
|
@@ -120,7 +110,7 @@ test("OpenAI-lexicon shed: a still-set PLURNK_PROVIDERS_LOGPROB fails hard with
|
|
|
120
110
|
);
|
|
121
111
|
});
|
|
122
112
|
|
|
123
|
-
test("
|
|
113
|
+
test("contextWindowFromEnv: reads the new name, sheds CONTEXT_SIZE hard, null when unset", async () => {
|
|
124
114
|
const { contextWindowFromEnv } = await import("./env.ts");
|
|
125
115
|
assert.equal(contextWindowFromEnv({ PLURNK_PROVIDERS_CONTEXT_WINDOW: "131072" } as NodeJS.ProcessEnv, "openai"), 131072);
|
|
126
116
|
assert.equal(contextWindowFromEnv({} as NodeJS.ProcessEnv, "openai"), null);
|
|
@@ -130,7 +120,7 @@ test("#472 contextWindowFromEnv: reads the new name, sheds CONTEXT_SIZE hard, nu
|
|
|
130
120
|
);
|
|
131
121
|
});
|
|
132
122
|
|
|
133
|
-
test("scopeEnvToAlias: a caller-supplied knob list scopes
|
|
123
|
+
test("scopeEnvToAlias: a caller-supplied knob list scopes consumer-owned vars", async () => {
|
|
134
124
|
const { scopeEnvToAlias } = await import("./env.ts");
|
|
135
125
|
const SERVICE_KNOBS = ["PLURNK_SERVICE_MAX_TURNS", "PLURNK_SERVICE_LOOP_TIMEOUT", "PLURNK_SERVICE_EXEC_HOLD_MS", "PLURNK_SERVICE_SAFETY"];
|
|
136
126
|
const env = {
|
|
@@ -150,7 +140,7 @@ test("scopeEnvToAlias: a caller-supplied knob list scopes CONSUMER vars (service
|
|
|
150
140
|
assert.equal(mixed.PLURNK_PROVIDERS_REASONING, "off");
|
|
151
141
|
});
|
|
152
142
|
|
|
153
|
-
test("
|
|
143
|
+
test("capture knobs are per-alias scopable: enable on a scraping alias, serving alias stays clean", async () => {
|
|
154
144
|
const { scopeEnvToAlias, dataCaptureFromEnv } = await import("./env.ts");
|
|
155
145
|
const env = {
|
|
156
146
|
PLURNK_PROVIDERS_TOP_LOGPROBS_fireslow: "3",
|
|
@@ -160,10 +150,10 @@ test("#36 capture knobs are per-alias scopable: enable on a scraping alias, serv
|
|
|
160
150
|
assert.deepEqual(dataCaptureFromEnv(scopeEnvToAlias(env, "grokfast"), "x"), { topLogprobs: null, rawBody: false });
|
|
161
151
|
});
|
|
162
152
|
|
|
163
|
-
//
|
|
153
|
+
// Every knob the code reads appears in
|
|
164
154
|
// the shipped .env.defaults — set (the floor) or commented (documented optional).
|
|
165
155
|
// The file IS the operator documentation; this keeps it from drifting off the code.
|
|
166
|
-
test("
|
|
156
|
+
test("every PROVIDERS_KNOBS entry appears in the shipped .env.defaults", async () => {
|
|
167
157
|
const { readFileSync } = await import("node:fs");
|
|
168
158
|
const { PROVIDERS_KNOBS } = await import("./env.ts");
|
|
169
159
|
const defaults = readFileSync(new URL("../.env.defaults", import.meta.url), "utf8");
|
|
@@ -172,37 +162,37 @@ test("#44: every PROVIDERS_KNOBS entry appears in the shipped .env.defaults", as
|
|
|
172
162
|
assert.ok(defaults.includes("PLURNK_PROVIDERS_GBNF="), "GBNF (service-read, providers-namespace) must be declared with its default");
|
|
173
163
|
});
|
|
174
164
|
|
|
175
|
-
//
|
|
165
|
+
// The family word is REASONING (industry standard). Old names fail hard
|
|
176
166
|
// with the migration pointer — never silently coexist with the new floor.
|
|
177
|
-
test("
|
|
167
|
+
test("still-set old THINKING names fail hard with the rename pointer", () => {
|
|
178
168
|
assert.throws(
|
|
179
169
|
() => reasoningFromEnv({ PLURNK_PROVIDERS_THINKING: "on", PLURNK_PROVIDERS_REASONING: "adaptive" }, "openai"),
|
|
180
|
-
/PLURNK_PROVIDERS_THINKING was renamed to PLURNK_PROVIDERS_REASONING \(
|
|
170
|
+
/PLURNK_PROVIDERS_THINKING was renamed to PLURNK_PROVIDERS_REASONING \(provider configuration contract\)/,
|
|
181
171
|
);
|
|
182
172
|
assert.throws(
|
|
183
173
|
() => reasoningFromEnv({ PLURNK_PROVIDERS_THINKING_CAPACITY: "4096", PLURNK_PROVIDERS_REASONING: "adaptive" }, "openai"),
|
|
184
|
-
/PLURNK_PROVIDERS_THINKING_CAPACITY was renamed to PLURNK_PROVIDERS_REASONING_BUDGET \(
|
|
174
|
+
/PLURNK_PROVIDERS_THINKING_CAPACITY was renamed to PLURNK_PROVIDERS_REASONING_BUDGET \(provider configuration contract\)/,
|
|
185
175
|
);
|
|
186
176
|
});
|
|
187
177
|
|
|
188
|
-
test("
|
|
178
|
+
test("the shipped floor activates reasoning by default (adaptive)", async () => {
|
|
189
179
|
const { readFileSync } = await import("node:fs");
|
|
190
180
|
const defaults = readFileSync(new URL("../.env.defaults", import.meta.url), "utf8");
|
|
191
181
|
assert.ok(defaults.includes("PLURNK_PROVIDERS_REASONING=adaptive"), "floor must ship REASONING=adaptive");
|
|
192
182
|
assert.ok(!defaults.match(/^PLURNK_PROVIDERS_REASONING_BUDGET=/m), "no shipped magnitude — budget is on-mode only");
|
|
193
183
|
});
|
|
194
184
|
|
|
195
|
-
test("
|
|
185
|
+
test("the shipped DRY floor is off and claims no universally safe shape", async () => {
|
|
196
186
|
const { readFileSync } = await import("node:fs");
|
|
197
187
|
const defaults = readFileSync(new URL("../.env.defaults", import.meta.url), "utf8");
|
|
198
|
-
assert.match(defaults, /^PLURNK_PROVIDERS_DRY_MULTIPLIER=0$/m, "
|
|
199
|
-
assert.
|
|
200
|
-
assert.
|
|
188
|
+
assert.match(defaults, /^PLURNK_PROVIDERS_DRY_MULTIPLIER=0$/m, "a fidelity-corrupting sampler cannot be a portable floor");
|
|
189
|
+
assert.doesNotMatch(defaults, /^PLURNK_PROVIDERS_DRY_BASE=/m);
|
|
190
|
+
assert.doesNotMatch(defaults, /^PLURNK_PROVIDERS_DRY_ALLOWED_LENGTH=/m);
|
|
201
191
|
});
|
|
202
192
|
|
|
203
|
-
// --
|
|
193
|
+
// -- {§provider-generation-envelope} --
|
|
204
194
|
|
|
205
|
-
test("
|
|
195
|
+
test("envelopeFromEnv: percentages and absolutes parse; missing/invalid fail hard", async () => {
|
|
206
196
|
const { envelopeFromEnv } = await import("./env.ts");
|
|
207
197
|
assert.deepEqual(
|
|
208
198
|
envelopeFromEnv({ PLURNK_PROVIDERS_REASONING_RESERVE: "10%", PLURNK_PROVIDERS_COMPLETION_RESERVE: "4096" } as NodeJS.ProcessEnv, "x"),
|
|
@@ -213,7 +203,7 @@ test("#507 envelopeFromEnv: percentages and absolutes parse; missing/invalid fai
|
|
|
213
203
|
assert.throws(() => envelopeFromEnv({ PLURNK_PROVIDERS_REASONING_RESERVE: "-5", PLURNK_PROVIDERS_COMPLETION_RESERVE: "25%" } as NodeJS.ProcessEnv, "x"), /positive integer token count/);
|
|
214
204
|
});
|
|
215
205
|
|
|
216
|
-
test("
|
|
206
|
+
test("envelope knobs are per-alias scopable (measured envelope per box)", async () => {
|
|
217
207
|
const { scopeEnvToAlias, envelopeFromEnv } = await import("./env.ts");
|
|
218
208
|
const env = {
|
|
219
209
|
PLURNK_PROVIDERS_REASONING_RESERVE: "10%", PLURNK_PROVIDERS_COMPLETION_RESERVE: "25%",
|
package/src/env.ts
CHANGED
|
@@ -43,7 +43,7 @@ export const promptCacheKeyFromEnv = (env: NodeJS.ProcessEnv, label: string): bo
|
|
|
43
43
|
return value === "1";
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
-
//
|
|
46
|
+
// {§provider-configuration} A still-set retired knob fails
|
|
47
47
|
// hard pointing at its successor — never silently coexists with the new floor.
|
|
48
48
|
// The retired names appear ONLY as this function's ARGUMENTS at the call sites
|
|
49
49
|
// (each lexicon-allow), never as a live identifier.
|
|
@@ -52,7 +52,8 @@ const shedRenamed = (env: NodeJS.ProcessEnv, oldName: string, newName: string, l
|
|
|
52
52
|
if (stale !== undefined && stale.length > 0) throw new Error(`${label} provider: ${oldName} was renamed to ${newName} (${ref}); update the env`);
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
-
// Data-capture knobs
|
|
55
|
+
// {§provider-evidence} Data-capture knobs are read identically by every provider
|
|
56
|
+
// (standard AND
|
|
56
57
|
// plugin) so the opt-in surface is one source of truth. Both OFF by default —
|
|
57
58
|
// the flag is the isolation, so serving turns request and carry nothing.
|
|
58
59
|
// PLURNK_PROVIDERS_TOP_LOGPROBS "off" or a non-negative int = the OpenAI
|
|
@@ -71,41 +72,30 @@ export const dataCaptureFromEnv = (env: NodeJS.ProcessEnv, label: string): { top
|
|
|
71
72
|
};
|
|
72
73
|
};
|
|
73
74
|
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
// contextWindow); CONTEXT_SIZE was home-grown. One reader for base AND
|
|
77
|
-
// plugins, so the shed fires everywhere the knob is honored.
|
|
75
|
+
// {§model-fact-resolution} — one context-window reader for every provider path;
|
|
76
|
+
// the retired CONTEXT_SIZE spelling fails visibly at the same boundary.
|
|
78
77
|
export const contextWindowFromEnv = (env: NodeJS.ProcessEnv, label: string): number | null => {
|
|
79
|
-
shedRenamed(env, "PLURNK_PROVIDERS_CONTEXT_SIZE", "PLURNK_PROVIDERS_CONTEXT_WINDOW", label, "
|
|
78
|
+
shedRenamed(env, "PLURNK_PROVIDERS_CONTEXT_SIZE", "PLURNK_PROVIDERS_CONTEXT_WINDOW", label, "{§model-fact-resolution}"); // lexicon-allow
|
|
80
79
|
return parseOptionalInt(env.PLURNK_PROVIDERS_CONTEXT_WINDOW, "PLURNK_PROVIDERS_CONTEXT_WINDOW", label);
|
|
81
80
|
};
|
|
82
81
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
export
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
? input
|
|
97
|
-
: parseRequiredFloat(env[cachedName], cachedName, label, 0),
|
|
98
|
-
output: parseRequiredFloat(env[outputName], outputName, label, 0),
|
|
99
|
-
};
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
// The generation-envelope reserves (#507, owner-ruled migration): how much of a
|
|
82
|
+
// {§model-fact-resolution} — an operator value caps known model physics and
|
|
83
|
+
// declares the window only when no natural value is known.
|
|
84
|
+
export function effectiveContextWindow(operatorCap: number | null, naturalWindow: number): number;
|
|
85
|
+
export function effectiveContextWindow(operatorCap: number | null, naturalWindow: number | null): number | null;
|
|
86
|
+
export function effectiveContextWindow(operatorCap: number | null, naturalWindow: number | null): number | null {
|
|
87
|
+
return operatorCap === null
|
|
88
|
+
? naturalWindow
|
|
89
|
+
: naturalWindow === null
|
|
90
|
+
? operatorCap
|
|
91
|
+
: Math.min(operatorCap, naturalWindow);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// {§provider-generation-envelope} How much of a
|
|
103
95
|
// DETECTED context window is reserved for reasoning and for completion — the
|
|
104
96
|
// remainder (minus the consumer's own packing-safety margin) is the prompt
|
|
105
97
|
// budget. Provider-owned: the window is a provider fact and these are amounts OF
|
|
106
|
-
// it
|
|
107
|
-
// provider quantities wearing a service prefix (born as #352 packet parameters
|
|
108
|
-
// before this surface existed). Each knob accepts a percentage of the window
|
|
98
|
+
// it. Each knob accepts a percentage of the window
|
|
109
99
|
// ("10%") or an absolute token count ("4096"); the floor ships percentages so
|
|
110
100
|
// every window-advertising endpoint (llama-server n_ctx, the plurnk.ai router,
|
|
111
101
|
// a cataloged cloud model) arrives at sane defaults with ZERO operator tuning.
|
|
@@ -151,21 +141,35 @@ export const resolveEnvelopeFromEnv = (env: NodeJS.ProcessEnv, window: number |
|
|
|
151
141
|
};
|
|
152
142
|
};
|
|
153
143
|
|
|
154
|
-
// The side-channel reasoning knobs
|
|
144
|
+
// {§provider-configuration} The side-channel reasoning knobs — activation and budget
|
|
155
145
|
// are separate vars, so a numeric budget can never silently flip wire flags:
|
|
156
146
|
// PLURNK_PROVIDERS_REASONING off | adaptive | on (REQUIRED, fail-hard)
|
|
157
147
|
// PLURNK_PROVIDERS_REASONING_BUDGET positive int, REQUIRED iff REASONING=on —
|
|
158
|
-
// the magnitude for tier/budget mapping. On llama
|
|
159
|
-
//
|
|
160
|
-
// env budget and launch flag are the same number, changed together.
|
|
148
|
+
// the magnitude for tier/budget mapping. On llama-server it is the explicit
|
|
149
|
+
// request-scoped allowance and cannot exceed the physical reasoning reserve.
|
|
161
150
|
// The provider maps intent to the backend's mechanism; the consumer states
|
|
162
|
-
// intent, never mechanism.
|
|
151
|
+
// intent, never mechanism. PLAN is a separate public intended-goals record.
|
|
163
152
|
export type ReasoningMode = "off" | "adaptive" | "on";
|
|
164
153
|
export type Reasoning = { mode: ReasoningMode; budget: number | null };
|
|
165
154
|
|
|
155
|
+
export type ReasoningResponseStyle = "verbatim" | "think-tags";
|
|
156
|
+
|
|
157
|
+
export const reasoningResponseStyleFromEnv = (
|
|
158
|
+
env: NodeJS.ProcessEnv,
|
|
159
|
+
label: string,
|
|
160
|
+
): ReasoningResponseStyle => {
|
|
161
|
+
const name = "PLURNK_PROVIDERS_REASONING_RESPONSE_STYLE";
|
|
162
|
+
const raw = env[name];
|
|
163
|
+
if (raw === undefined || raw.length === 0) return "verbatim";
|
|
164
|
+
if (raw !== "verbatim" && raw !== "think-tags") {
|
|
165
|
+
throw new Error(`${label} provider: ${name} must be "verbatim" or "think-tags" (got "${raw}")`);
|
|
166
|
+
}
|
|
167
|
+
return raw;
|
|
168
|
+
};
|
|
169
|
+
|
|
166
170
|
export const reasoningFromEnv = (env: NodeJS.ProcessEnv, label: string): Reasoning => {
|
|
167
|
-
shedRenamed(env, "PLURNK_PROVIDERS_THINKING", "PLURNK_PROVIDERS_REASONING", label, "
|
|
168
|
-
shedRenamed(env, "PLURNK_PROVIDERS_THINKING_CAPACITY", "PLURNK_PROVIDERS_REASONING_BUDGET", label, "
|
|
171
|
+
shedRenamed(env, "PLURNK_PROVIDERS_THINKING", "PLURNK_PROVIDERS_REASONING", label, "provider configuration contract"); // lexicon-allow
|
|
172
|
+
shedRenamed(env, "PLURNK_PROVIDERS_THINKING_CAPACITY", "PLURNK_PROVIDERS_REASONING_BUDGET", label, "provider configuration contract"); // lexicon-allow
|
|
169
173
|
const name = "PLURNK_PROVIDERS_REASONING";
|
|
170
174
|
const raw = env[name];
|
|
171
175
|
if (raw === undefined || raw.length === 0) throw new Error(`${label} provider: ${name} must be set (off | adaptive | on)`);
|
|
@@ -189,13 +193,12 @@ export const reasoningFromEnv = (env: NodeJS.ProcessEnv, label: string): Reasoni
|
|
|
189
193
|
export const PROVIDERS_KNOBS = Object.freeze([
|
|
190
194
|
"PLURNK_PROVIDERS_REASONING_RESERVE",
|
|
191
195
|
"PLURNK_PROVIDERS_COMPLETION_RESERVE",
|
|
196
|
+
"PLURNK_PROVIDERS_REASONING_RESPONSE_STYLE",
|
|
192
197
|
"PLURNK_PROVIDERS_REASONING_BUDGET",
|
|
193
198
|
"PLURNK_PROVIDERS_REASONING",
|
|
194
199
|
"PLURNK_PROVIDERS_CONTEXT_WINDOW",
|
|
195
|
-
"PLURNK_PROVIDERS_INPUT_USD_PER_MILLION",
|
|
196
|
-
"PLURNK_PROVIDERS_CACHE_READ_USD_PER_MILLION",
|
|
197
|
-
"PLURNK_PROVIDERS_OUTPUT_USD_PER_MILLION",
|
|
198
200
|
"PLURNK_PROVIDERS_RETRY_ATTEMPTS",
|
|
201
|
+
"PLURNK_PROVIDERS_ERROR_DETAIL_LIMIT",
|
|
199
202
|
"PLURNK_PROVIDERS_FETCH_TIMEOUT",
|
|
200
203
|
"PLURNK_PROVIDERS_STREAM_IDLE_TIMEOUT",
|
|
201
204
|
"PLURNK_PROVIDERS_LLAMA_SERVER",
|