@plurnk/plurnk-providers 1.5.0 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.defaults +41 -34
- package/README.md +15 -0
- package/SPEC.md +242 -89
- package/dist/AiSdkProvider.d.ts +33 -33
- package/dist/AiSdkProvider.d.ts.map +1 -1
- package/dist/AiSdkProvider.js +442 -133
- package/dist/AiSdkProvider.js.map +1 -1
- package/dist/Mock.d.ts +10 -11
- package/dist/Mock.d.ts.map +1 -1
- package/dist/Mock.js +87 -25
- package/dist/Mock.js.map +1 -1
- package/dist/Pool.d.ts +9 -24
- package/dist/Pool.d.ts.map +1 -1
- package/dist/Pool.js +86 -25
- package/dist/Pool.js.map +1 -1
- package/dist/accounting.d.ts +5 -2
- package/dist/accounting.d.ts.map +1 -1
- package/dist/accounting.js +100 -16
- package/dist/accounting.js.map +1 -1
- package/dist/accountingPublic.d.ts +5 -0
- package/dist/accountingPublic.d.ts.map +1 -0
- package/dist/accountingPublic.js +3 -0
- package/dist/accountingPublic.js.map +1 -0
- package/dist/aiSdkTransport.d.ts +9 -2
- package/dist/aiSdkTransport.d.ts.map +1 -1
- package/dist/aiSdkTransport.js +160 -62
- package/dist/aiSdkTransport.js.map +1 -1
- package/dist/capacity.d.ts +26 -0
- package/dist/capacity.d.ts.map +1 -0
- package/dist/capacity.js +90 -0
- package/dist/capacity.js.map +1 -0
- package/dist/catalogProvider.d.ts +8 -3
- package/dist/catalogProvider.d.ts.map +1 -1
- package/dist/catalogProvider.js +45 -41
- package/dist/catalogProvider.js.map +1 -1
- package/dist/compatibleProvider.d.ts.map +1 -1
- package/dist/compatibleProvider.js +26 -12
- package/dist/compatibleProvider.js.map +1 -1
- package/dist/cost.d.ts +10 -10
- package/dist/cost.d.ts.map +1 -1
- package/dist/cost.js +90 -42
- package/dist/cost.js.map +1 -1
- package/dist/env.d.ts +13 -11
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +83 -46
- package/dist/env.js.map +1 -1
- package/dist/errors.d.ts +17 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +91 -8
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/ollama.js +3 -3
- package/dist/ollama.js.map +1 -1
- package/dist/promptTokens.d.ts.map +1 -1
- package/dist/promptTokens.js +7 -4
- package/dist/promptTokens.js.map +1 -1
- package/dist/sdkModels.d.ts +7 -2
- package/dist/sdkModels.d.ts.map +1 -1
- package/dist/sdkModels.js +43 -13
- package/dist/sdkModels.js.map +1 -1
- package/dist/types.d.ts +55 -33
- package/dist/types.d.ts.map +1 -1
- package/dist/usage.d.ts +22 -5
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +169 -83
- package/dist/usage.js.map +1 -1
- package/package.json +18 -7
- package/src/AiSdkProvider.test.ts +964 -206
- package/src/AiSdkProvider.ts +545 -155
- package/src/Mock.test.ts +69 -30
- package/src/Mock.ts +99 -29
- package/src/Pool.test.ts +90 -19
- package/src/Pool.ts +96 -27
- package/src/ProviderRegistry.test.ts +16 -11
- package/src/accounting.test.ts +58 -22
- package/src/accounting.ts +119 -18
- package/src/accountingPublic.ts +9 -0
- package/src/aiSdkTransport.test.ts +42 -49
- package/src/aiSdkTransport.ts +174 -62
- package/src/boundaries.test.ts +2 -0
- package/src/capacity.test.ts +92 -0
- package/src/capacity.ts +140 -0
- package/src/catalogProvider.test.ts +339 -30
- package/src/catalogProvider.ts +65 -47
- package/src/compatibleProvider.test.ts +7 -5
- package/src/compatibleProvider.ts +29 -13
- package/src/cost.test.ts +86 -36
- package/src/cost.ts +111 -50
- package/src/defaults.test.ts +13 -3
- package/src/env.test.ts +103 -25
- package/src/env.ts +153 -65
- package/src/errors.test.ts +80 -2
- package/src/errors.ts +107 -8
- package/src/index.ts +26 -7
- package/src/ollama.test.ts +5 -3
- package/src/ollama.ts +3 -3
- package/src/promptTokens.ts +8 -5
- package/src/sdkModels.test.ts +77 -8
- package/src/sdkModels.ts +51 -15
- package/src/types.ts +112 -51
- package/src/usage.test.ts +112 -116
- package/src/usage.ts +214 -93
package/src/cost.ts
CHANGED
|
@@ -1,78 +1,139 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
2
|
+
ChargedCost,
|
|
3
|
+
ProviderCost,
|
|
3
4
|
ProviderUsage,
|
|
4
5
|
} from "./types.ts";
|
|
5
|
-
import
|
|
6
|
+
import {
|
|
7
|
+
calculateCostUsdDecimal,
|
|
8
|
+
canonicalDecimal,
|
|
9
|
+
type TokenRates,
|
|
10
|
+
} from "./usage.ts";
|
|
6
11
|
|
|
7
12
|
const DECIMAL = /^(?:0|[1-9]\d*)(?:\.\d+)?$/;
|
|
8
13
|
const CURRENCY = /^[A-Z][A-Z0-9]{2,11}$/;
|
|
9
14
|
|
|
10
|
-
const
|
|
11
|
-
if (value
|
|
15
|
+
const recordOf = (value: unknown, name: string): Record<string, unknown> => {
|
|
16
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
17
|
+
throw new TypeError(`${name} must be an object`);
|
|
18
|
+
}
|
|
19
|
+
return value as Record<string, unknown>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const nonEmpty = (value: unknown, name: string): string => {
|
|
23
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
24
|
+
throw new TypeError(`${name} must be a non-empty string`);
|
|
25
|
+
}
|
|
12
26
|
return value;
|
|
13
27
|
};
|
|
14
28
|
|
|
15
|
-
const
|
|
16
|
-
if (!DECIMAL.test(value))
|
|
29
|
+
export const validateDecimal = (value: unknown, name: string): string => {
|
|
30
|
+
if (typeof value !== "string" || !DECIMAL.test(value)) {
|
|
31
|
+
throw new TypeError(`${name} must be a canonical non-negative decimal string`);
|
|
32
|
+
}
|
|
17
33
|
return value;
|
|
18
34
|
};
|
|
19
35
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (!CURRENCY.test(
|
|
24
|
-
throw new TypeError(
|
|
36
|
+
const monetaryAmount = (value: unknown, name: string): { amount: string; currency: string } => {
|
|
37
|
+
const amount = recordOf(value, name);
|
|
38
|
+
const decimalAmount = validateDecimal(amount.amount, `${name} amount`);
|
|
39
|
+
if (typeof amount.currency !== "string" || !CURRENCY.test(amount.currency)) {
|
|
40
|
+
throw new TypeError(`${name} currency must be an uppercase currency code`);
|
|
25
41
|
}
|
|
26
|
-
|
|
27
|
-
nonEmpty(charge.source, "provider charge source");
|
|
28
|
-
return charge;
|
|
42
|
+
return { amount: decimalAmount, currency: amount.currency };
|
|
29
43
|
};
|
|
30
44
|
|
|
31
|
-
export const
|
|
45
|
+
export const validateChargedCost = (value: unknown): ChargedCost => {
|
|
46
|
+
const cost = recordOf(value, "provider charged cost");
|
|
47
|
+
if (cost.kind !== "charged") throw new TypeError("provider charged cost kind must be charged");
|
|
48
|
+
monetaryAmount(cost.amount, "provider charged cost");
|
|
49
|
+
if (cost.usdEquivalent !== undefined) {
|
|
50
|
+
validateDecimal(cost.usdEquivalent, "provider charged cost USD equivalent");
|
|
51
|
+
}
|
|
52
|
+
nonEmpty(cost.source, "provider charged cost source");
|
|
53
|
+
return value as ChargedCost;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const validateProviderCost = (value: unknown): ProviderCost => {
|
|
57
|
+
const cost = recordOf(value, "provider cost");
|
|
32
58
|
switch (cost.kind) {
|
|
33
|
-
case "
|
|
34
|
-
return
|
|
59
|
+
case "charged":
|
|
60
|
+
return validateChargedCost(value);
|
|
35
61
|
case "estimated":
|
|
36
|
-
|
|
37
|
-
nonEmpty(cost.source, "provider cost
|
|
38
|
-
return
|
|
39
|
-
case "free":
|
|
40
|
-
nonEmpty(cost.source, "provider free source");
|
|
41
|
-
return cost;
|
|
62
|
+
monetaryAmount(cost.amount, "provider estimated cost");
|
|
63
|
+
nonEmpty(cost.source, "provider estimated cost source");
|
|
64
|
+
return value as ProviderCost;
|
|
42
65
|
case "unknown":
|
|
43
66
|
nonEmpty(cost.reason, "provider unknown-cost reason");
|
|
44
|
-
return
|
|
67
|
+
return value as ProviderCost;
|
|
68
|
+
default:
|
|
69
|
+
throw new TypeError("provider cost kind must be charged, estimated, or unknown");
|
|
45
70
|
}
|
|
46
71
|
};
|
|
47
72
|
|
|
48
|
-
export const
|
|
49
|
-
|
|
50
|
-
|
|
73
|
+
export const estimateProviderCost = (
|
|
74
|
+
usage: ProviderUsage | undefined,
|
|
75
|
+
rates: TokenRates | null,
|
|
76
|
+
source: string,
|
|
51
77
|
): ProviderCost => {
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
78
|
+
if (usage === undefined) {
|
|
79
|
+
return { kind: "unknown", reason: "the provider response reported no normalized usage" };
|
|
80
|
+
}
|
|
81
|
+
if (rates === null) {
|
|
82
|
+
return { kind: "unknown", reason: "Models.dev has no complete rate for this model" };
|
|
83
|
+
}
|
|
84
|
+
const usd = calculateCostUsdDecimal(usage, rates);
|
|
85
|
+
return usd === null
|
|
86
|
+
? {
|
|
87
|
+
kind: "unknown",
|
|
88
|
+
reason: "the provider response omitted a token category with a distinct Models.dev rate",
|
|
89
|
+
}
|
|
90
|
+
: {
|
|
91
|
+
kind: "estimated",
|
|
92
|
+
amount: { amount: usd, currency: "USD" },
|
|
93
|
+
source,
|
|
94
|
+
};
|
|
58
95
|
};
|
|
59
96
|
|
|
60
|
-
export const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
97
|
+
export const resolveProviderCost = (
|
|
98
|
+
direct: ProviderCost | undefined,
|
|
99
|
+
estimated: ProviderCost,
|
|
100
|
+
): ProviderCost => direct === undefined
|
|
101
|
+
? validateProviderCost(estimated)
|
|
102
|
+
: validateProviderCost(direct);
|
|
103
|
+
|
|
104
|
+
export const providerCostUsd = (cost: ProviderCost): string | null => {
|
|
105
|
+
const validated = validateProviderCost(cost);
|
|
106
|
+
switch (validated.kind) {
|
|
107
|
+
case "charged":
|
|
108
|
+
return validated.amount.currency === "USD"
|
|
109
|
+
? validated.amount.amount
|
|
110
|
+
: validated.usdEquivalent ?? null;
|
|
111
|
+
case "estimated":
|
|
112
|
+
return validated.amount.currency === "USD" ? validated.amount.amount : null;
|
|
113
|
+
case "unknown":
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
69
116
|
};
|
|
70
117
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
);
|
|
118
|
+
const decimalParts = (value: string): { coefficient: bigint; scale: number } => {
|
|
119
|
+
validateDecimal(value, "decimal amount");
|
|
120
|
+
const [integer, fraction = ""] = value.split(".");
|
|
121
|
+
return { coefficient: BigInt(`${integer}${fraction}`), scale: fraction.length };
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const addDecimals = (values: readonly string[]): string => {
|
|
125
|
+
const parts = values.map(decimalParts);
|
|
126
|
+
const scale = Math.max(0, ...parts.map((part) => part.scale));
|
|
127
|
+
const coefficient = parts.reduce(
|
|
128
|
+
(sum, part) => sum + part.coefficient * 10n ** BigInt(scale - part.scale),
|
|
129
|
+
0n,
|
|
130
|
+
);
|
|
131
|
+
return canonicalDecimal(coefficient, scale);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const sumProviderCostsUsd = (costs: readonly ProviderCost[]): string | null => {
|
|
135
|
+
const values = costs.map(providerCostUsd);
|
|
136
|
+
return values.some((value) => value === null)
|
|
137
|
+
? null
|
|
138
|
+
: addDecimals(values as string[]);
|
|
139
|
+
};
|
package/src/defaults.test.ts
CHANGED
|
@@ -4,17 +4,27 @@ import { withProviderDefaults } from "./defaults.ts";
|
|
|
4
4
|
|
|
5
5
|
test("withProviderDefaults supplies the package-owned operational floor", () => {
|
|
6
6
|
const env = withProviderDefaults({});
|
|
7
|
-
assert.equal(env.
|
|
7
|
+
assert.equal(env.PLURNK_PROVIDERS_CACHE_AFFINITY, "1");
|
|
8
|
+
assert.equal(env.PLURNK_PROVIDERS_CACHE_WRITE_POLICY, "stable-system");
|
|
9
|
+
assert.equal(env.PLURNK_PROVIDERS_OPERATION_TIMEOUT, "2700000");
|
|
8
10
|
assert.equal(env.PLURNK_PROVIDERS_FETCH_TIMEOUT, "600000");
|
|
11
|
+
assert.equal(env.PLURNK_PROVIDERS_FIRST_CONTENT_TIMEOUT, "600000");
|
|
12
|
+
assert.equal(env.PLURNK_PROVIDERS_STREAM_IDLE_TIMEOUT, "120000");
|
|
9
13
|
assert.equal(env.PLURNK_PROVIDERS_RETRY_ATTEMPTS, "3");
|
|
10
14
|
assert.equal(env.PLURNK_PROVIDERS_ERROR_DETAIL_LIMIT, "512");
|
|
11
15
|
});
|
|
12
16
|
|
|
13
17
|
test("withProviderDefaults preserves every explicit operator value", () => {
|
|
14
18
|
const env = withProviderDefaults({
|
|
15
|
-
|
|
19
|
+
PLURNK_PROVIDERS_CACHE_AFFINITY: "malformed",
|
|
20
|
+
PLURNK_PROVIDERS_CACHE_WRITE_POLICY: "off",
|
|
21
|
+
PLURNK_PROVIDERS_OPERATION_TIMEOUT: "84",
|
|
16
22
|
PLURNK_PROVIDERS_FETCH_TIMEOUT: "42",
|
|
23
|
+
PLURNK_PROVIDERS_FIRST_CONTENT_TIMEOUT: "21",
|
|
17
24
|
});
|
|
18
|
-
assert.equal(env.
|
|
25
|
+
assert.equal(env.PLURNK_PROVIDERS_CACHE_AFFINITY, "malformed");
|
|
26
|
+
assert.equal(env.PLURNK_PROVIDERS_CACHE_WRITE_POLICY, "off");
|
|
27
|
+
assert.equal(env.PLURNK_PROVIDERS_OPERATION_TIMEOUT, "84");
|
|
19
28
|
assert.equal(env.PLURNK_PROVIDERS_FETCH_TIMEOUT, "42");
|
|
29
|
+
assert.equal(env.PLURNK_PROVIDERS_FIRST_CONTENT_TIMEOUT, "21");
|
|
20
30
|
});
|
package/src/env.test.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import { strict as assert } from "node:assert";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
cacheAffinityFromEnv,
|
|
5
|
+
cacheWritePolicyFromEnv,
|
|
6
|
+
generationEnvelopeFromEnv,
|
|
7
|
+
parseRequiredInt,
|
|
8
|
+
parseOptionalInt,
|
|
9
|
+
parseTimeoutMs,
|
|
10
|
+
requireEnv,
|
|
11
|
+
reasoningFromEnv,
|
|
12
|
+
reasoningResponseStyleFromEnv,
|
|
13
|
+
} from "./env.ts";
|
|
4
14
|
|
|
5
15
|
test("parseRequiredInt: parses a non-negative integer", () => {
|
|
6
16
|
assert.equal(parseRequiredInt("600000", "PLURNK_PROVIDERS_FETCH_TIMEOUT", "openai"), 600000);
|
|
@@ -18,6 +28,15 @@ test("parseRequiredInt: rejects non-numeric, fractional, and negative values", (
|
|
|
18
28
|
assert.throws(() => parseRequiredInt("-1", "PLURNK_PROVIDERS_FETCH_TIMEOUT", "openai"), /must be a non-negative integer \(got "-1"\)/);
|
|
19
29
|
});
|
|
20
30
|
|
|
31
|
+
test("parseTimeoutMs accepts disabled deadlines and rejects timer overflow", () => {
|
|
32
|
+
assert.equal(parseTimeoutMs("0", "PLURNK_PROVIDERS_OPERATION_TIMEOUT", "openai"), 0);
|
|
33
|
+
assert.equal(parseTimeoutMs("2147483647", "PLURNK_PROVIDERS_OPERATION_TIMEOUT", "openai"), 2_147_483_647);
|
|
34
|
+
assert.throws(
|
|
35
|
+
() => parseTimeoutMs("2147483648", "PLURNK_PROVIDERS_OPERATION_TIMEOUT", "openai"),
|
|
36
|
+
/must be at most 2147483647 milliseconds/,
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
21
40
|
test("parseOptionalInt: absent → null, present → integer", () => {
|
|
22
41
|
assert.equal(parseOptionalInt(undefined, "PLURNK_PROVIDERS_CONTEXT_WINDOW", "openai"), null);
|
|
23
42
|
assert.equal(parseOptionalInt("", "PLURNK_PROVIDERS_CONTEXT_WINDOW", "openai"), null);
|
|
@@ -29,15 +48,14 @@ test("parseOptionalInt: rejects fractional and negative values", () => {
|
|
|
29
48
|
assert.throws(() => parseOptionalInt("-8", "PLURNK_PROVIDERS_CONTEXT_WINDOW", "openai"), /must be a non-negative integer/);
|
|
30
49
|
});
|
|
31
50
|
|
|
32
|
-
test("reasoningFromEnv: activation
|
|
51
|
+
test("reasoningFromEnv: activation is independent from an optional explicit budget", () => {
|
|
33
52
|
assert.deepEqual(reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: "off" }, "openai"), { mode: "off", budget: null });
|
|
34
53
|
assert.deepEqual(reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: "adaptive" }, "openai"), { mode: "adaptive", budget: null });
|
|
35
|
-
assert.deepEqual(reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: "on"
|
|
54
|
+
assert.deepEqual(reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: "on" }, "openai"), { mode: "on", budget: null });
|
|
55
|
+
assert.deepEqual(reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: "on" }, "openai", 4096), { mode: "on", budget: 4096 });
|
|
56
|
+
assert.deepEqual(reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: "adaptive" }, "openai", 4096), { mode: "adaptive", budget: 4096 });
|
|
36
57
|
assert.throws(() => reasoningFromEnv({}, "openai"), /PLURNK_PROVIDERS_REASONING must be set/);
|
|
37
58
|
assert.throws(() => reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: "8192" }, "openai"), /must be one of "off", "adaptive", "on"/); // the old numeric habit fails loudly
|
|
38
|
-
assert.throws(() => reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: "on" }, "openai"), /PLURNK_PROVIDERS_REASONING_BUDGET must be set when/);
|
|
39
|
-
assert.throws(() => reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: "on", PLURNK_PROVIDERS_REASONING_BUDGET: "0" }, "openai"), /positive integer/);
|
|
40
|
-
assert.throws(() => reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: "on", PLURNK_PROVIDERS_REASONING_BUDGET: "1.5" }, "openai"), /positive integer/);
|
|
41
59
|
});
|
|
42
60
|
|
|
43
61
|
test("{§provider-tagged-reasoning} response style is explicit and invalid values fail at the provider boundary", () => {
|
|
@@ -59,6 +77,31 @@ test("requireEnv: returns the value or throws a named error", () => {
|
|
|
59
77
|
assert.throws(() => requireEnv("", "GROQ_API_KEY", "groq"), /must be set/);
|
|
60
78
|
});
|
|
61
79
|
|
|
80
|
+
test("cache policy keeps cost-neutral affinity separate from paid cache writes", () => {
|
|
81
|
+
assert.equal(cacheAffinityFromEnv({ PLURNK_PROVIDERS_CACHE_AFFINITY: "1" }, "openai"), true);
|
|
82
|
+
assert.equal(cacheAffinityFromEnv({ PLURNK_PROVIDERS_CACHE_AFFINITY: "0" }, "openai"), false);
|
|
83
|
+
assert.equal(cacheWritePolicyFromEnv({ PLURNK_PROVIDERS_CACHE_WRITE_POLICY: "stable-system" }, "anthropic"), "stable-system");
|
|
84
|
+
assert.equal(cacheWritePolicyFromEnv({ PLURNK_PROVIDERS_CACHE_WRITE_POLICY: "off" }, "anthropic"), "off");
|
|
85
|
+
assert.throws(
|
|
86
|
+
() => cacheAffinityFromEnv({ PLURNK_PROVIDERS_CACHE_AFFINITY: "auto" }, "openai"),
|
|
87
|
+
/PLURNK_PROVIDERS_CACHE_AFFINITY must be "0" or "1"/,
|
|
88
|
+
);
|
|
89
|
+
assert.throws(
|
|
90
|
+
() => cacheWritePolicyFromEnv({ PLURNK_PROVIDERS_CACHE_WRITE_POLICY: "everything" }, "anthropic"),
|
|
91
|
+
/PLURNK_PROVIDERS_CACHE_WRITE_POLICY must be "off" or "stable-system"/,
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("the generic prompt-cache-key knob is retired rather than retained as a compatibility path", () => {
|
|
96
|
+
assert.throws(
|
|
97
|
+
() => cacheAffinityFromEnv({
|
|
98
|
+
PLURNK_PROVIDERS_PROMPT_CACHE_KEY: "1",
|
|
99
|
+
PLURNK_PROVIDERS_CACHE_AFFINITY: "1",
|
|
100
|
+
}, "fireworks"),
|
|
101
|
+
/PLURNK_PROVIDERS_PROMPT_CACHE_KEY was renamed to PLURNK_PROVIDERS_CACHE_AFFINITY/,
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
62
105
|
// — per-alias knob scoping (per-alias scoping doctrine, user 2026-07-03) —
|
|
63
106
|
|
|
64
107
|
test("scopeEnvToAlias: suffixed knob wins, bare is the fallback, other aliases ignored", async () => {
|
|
@@ -69,7 +112,7 @@ test("scopeEnvToAlias: suffixed knob wins, bare is the fallback, other aliases i
|
|
|
69
112
|
PLURNK_PROVIDERS_REASONING_BUDGET_TURBODERP: "4096", // case-folds like PLURNK_MODEL_ keys
|
|
70
113
|
PLURNK_PROVIDERS_REASONING_RESPONSE_STYLE_TURBODERP: "think-tags",
|
|
71
114
|
PLURNK_PROVIDERS_CONTEXT_WINDOW_turboderp: "8000",
|
|
72
|
-
|
|
115
|
+
PLURNK_PROVIDERS_OUTPUT_BUDGET_turboderp: "4096",
|
|
73
116
|
PLURNK_PROVIDERS_CONTEXT_WINDOW_other: "1",
|
|
74
117
|
} as NodeJS.ProcessEnv;
|
|
75
118
|
const scoped = scopeEnvToAlias(env, "turboderp");
|
|
@@ -77,7 +120,7 @@ test("scopeEnvToAlias: suffixed knob wins, bare is the fallback, other aliases i
|
|
|
77
120
|
assert.equal(scoped.PLURNK_PROVIDERS_REASONING_BUDGET, "4096");
|
|
78
121
|
assert.equal(scoped.PLURNK_PROVIDERS_REASONING_RESPONSE_STYLE, "think-tags");
|
|
79
122
|
assert.equal(scoped.PLURNK_PROVIDERS_CONTEXT_WINDOW, "8000");
|
|
80
|
-
assert.equal(scoped.
|
|
123
|
+
assert.equal(scoped.PLURNK_PROVIDERS_OUTPUT_BUDGET, "4096");
|
|
81
124
|
assert.equal(scopeEnvToAlias(env, "plain").PLURNK_PROVIDERS_REASONING, "off"); // fallback intact
|
|
82
125
|
});
|
|
83
126
|
|
|
@@ -86,10 +129,16 @@ test("scopeEnvToAlias: aliases with underscores resolve; a bare knob is never mi
|
|
|
86
129
|
const env = {
|
|
87
130
|
PLURNK_PROVIDERS_FETCH_TIMEOUT: "600000",
|
|
88
131
|
PLURNK_PROVIDERS_FETCH_TIMEOUT_my_box: "5000",
|
|
132
|
+
PLURNK_PROVIDERS_OPERATION_TIMEOUT: "2700000",
|
|
133
|
+
PLURNK_PROVIDERS_OPERATION_TIMEOUT_my_box: "15000",
|
|
134
|
+
PLURNK_PROVIDERS_FIRST_CONTENT_TIMEOUT: "600000",
|
|
135
|
+
PLURNK_PROVIDERS_FIRST_CONTENT_TIMEOUT_my_box: "2500",
|
|
89
136
|
PLURNK_PROVIDERS_REASONING: "off",
|
|
90
137
|
PLURNK_PROVIDERS_REASONING_BUDGET: "4096", // bare budget — NOT a "_capacity" alias override of REASONING
|
|
91
138
|
} as NodeJS.ProcessEnv;
|
|
92
139
|
assert.equal(scopeEnvToAlias(env, "my_box").PLURNK_PROVIDERS_FETCH_TIMEOUT, "5000");
|
|
140
|
+
assert.equal(scopeEnvToAlias(env, "my_box").PLURNK_PROVIDERS_OPERATION_TIMEOUT, "15000");
|
|
141
|
+
assert.equal(scopeEnvToAlias(env, "my_box").PLURNK_PROVIDERS_FIRST_CONTENT_TIMEOUT, "2500");
|
|
93
142
|
assert.equal(scopeEnvToAlias(env, "budget").PLURNK_PROVIDERS_REASONING, "off"); // collision guard
|
|
94
143
|
});
|
|
95
144
|
|
|
@@ -122,16 +171,16 @@ test("contextWindowFromEnv: reads the new name, sheds CONTEXT_SIZE hard, null wh
|
|
|
122
171
|
|
|
123
172
|
test("scopeEnvToAlias: a caller-supplied knob list scopes consumer-owned vars", async () => {
|
|
124
173
|
const { scopeEnvToAlias } = await import("./env.ts");
|
|
125
|
-
const SERVICE_KNOBS = ["PLURNK_SERVICE_MAX_TURNS", "PLURNK_SERVICE_LOOP_TIMEOUT", "PLURNK_SERVICE_EXEC_HOLD_MS", "
|
|
174
|
+
const SERVICE_KNOBS = ["PLURNK_SERVICE_MAX_TURNS", "PLURNK_SERVICE_LOOP_TIMEOUT", "PLURNK_SERVICE_EXEC_HOLD_MS", "PLURNK_SERVICE_PROMPT_PROJECTION"];
|
|
126
175
|
const env = {
|
|
127
|
-
PLURNK_SERVICE_MAX_TURNS: "163840", PLURNK_SERVICE_LOOP_TIMEOUT: "16384", PLURNK_SERVICE_EXEC_HOLD_MS: "49152",
|
|
176
|
+
PLURNK_SERVICE_MAX_TURNS: "163840", PLURNK_SERVICE_LOOP_TIMEOUT: "16384", PLURNK_SERVICE_EXEC_HOLD_MS: "49152", PLURNK_SERVICE_PROMPT_PROJECTION: "25%",
|
|
128
177
|
PLURNK_SERVICE_MAX_TURNS_turboderp: "78848", PLURNK_SERVICE_LOOP_TIMEOUT_turboderp: "4096", PLURNK_SERVICE_EXEC_HOLD_MS_TURBODERP: "8192", // case-folds
|
|
129
178
|
} as NodeJS.ProcessEnv;
|
|
130
179
|
const gemma = scopeEnvToAlias(env, "turboderp", SERVICE_KNOBS);
|
|
131
180
|
assert.equal(gemma.PLURNK_SERVICE_MAX_TURNS, "78848");
|
|
132
181
|
assert.equal(gemma.PLURNK_SERVICE_LOOP_TIMEOUT, "4096");
|
|
133
182
|
assert.equal(gemma.PLURNK_SERVICE_EXEC_HOLD_MS, "8192");
|
|
134
|
-
assert.equal(gemma.
|
|
183
|
+
assert.equal(gemma.PLURNK_SERVICE_PROMPT_PROJECTION, "25%"); // bare fallback intact
|
|
135
184
|
const cloud = scopeEnvToAlias(env, "fireslow", SERVICE_KNOBS);
|
|
136
185
|
assert.equal(cloud.PLURNK_SERVICE_LOOP_TIMEOUT, "16384"); // 64k envelope untouched by gemma overrides
|
|
137
186
|
assert.equal(cloud.PLURNK_SERVICE_EXEC_HOLD_MS, "49152");
|
|
@@ -175,11 +224,11 @@ test("still-set old THINKING names fail hard with the rename pointer", () => {
|
|
|
175
224
|
);
|
|
176
225
|
});
|
|
177
226
|
|
|
178
|
-
test("the shipped floor
|
|
227
|
+
test("the shipped floor defers reasoning posture to the provider by default (adaptive)", async () => {
|
|
179
228
|
const { readFileSync } = await import("node:fs");
|
|
180
229
|
const defaults = readFileSync(new URL("../.env.defaults", import.meta.url), "utf8");
|
|
181
230
|
assert.ok(defaults.includes("PLURNK_PROVIDERS_REASONING=adaptive"), "floor must ship REASONING=adaptive");
|
|
182
|
-
assert.ok(!defaults.match(/^PLURNK_PROVIDERS_REASONING_BUDGET=/m), "no shipped magnitude —
|
|
231
|
+
assert.ok(!defaults.match(/^PLURNK_PROVIDERS_REASONING_BUDGET=/m), "no shipped magnitude — an explicit on-mode budget is optional");
|
|
183
232
|
});
|
|
184
233
|
|
|
185
234
|
test("the shipped DRY floor is off and claims no universally safe shape", async () => {
|
|
@@ -192,23 +241,52 @@ test("the shipped DRY floor is off and claims no universally safe shape", async
|
|
|
192
241
|
|
|
193
242
|
// -- {§provider-generation-envelope} --
|
|
194
243
|
|
|
195
|
-
test("
|
|
196
|
-
|
|
244
|
+
test("generationEnvelopeFromEnv: output is total and reasoning is an optional subset", () => {
|
|
245
|
+
assert.deepEqual(
|
|
246
|
+
generationEnvelopeFromEnv({
|
|
247
|
+
PLURNK_PROVIDERS_OUTPUT_BUDGET: "35%",
|
|
248
|
+
PLURNK_PROVIDERS_REASONING_BUDGET: "4096",
|
|
249
|
+
} as NodeJS.ProcessEnv, "x", 100_000, 32_000),
|
|
250
|
+
{ outputBudget: 32_000, reasoningBudget: 4096 },
|
|
251
|
+
);
|
|
252
|
+
assert.throws(() => generationEnvelopeFromEnv({}, "x", 100_000, null), /PLURNK_PROVIDERS_OUTPUT_BUDGET must be set/);
|
|
253
|
+
assert.throws(() => generationEnvelopeFromEnv({ PLURNK_PROVIDERS_OUTPUT_BUDGET: "150%" }, "x", 100_000, null), /percentage must be in \(0, 100\)/);
|
|
254
|
+
assert.throws(() => generationEnvelopeFromEnv({ PLURNK_PROVIDERS_OUTPUT_BUDGET: "-5" }, "x", 100_000, null), /positive integer token count/);
|
|
255
|
+
assert.throws(() => generationEnvelopeFromEnv({ PLURNK_PROVIDERS_OUTPUT_BUDGET: "100000" }, "x", 100_000, null), /must leave positive input capacity/);
|
|
256
|
+
assert.throws(() => generationEnvelopeFromEnv({
|
|
257
|
+
PLURNK_PROVIDERS_OUTPUT_BUDGET: "20%",
|
|
258
|
+
PLURNK_PROVIDERS_REASONING_BUDGET: "25%",
|
|
259
|
+
}, "x", 100_000, null), /reasoning is a subset of total output/);
|
|
197
260
|
assert.deepEqual(
|
|
198
|
-
|
|
199
|
-
{
|
|
261
|
+
generationEnvelopeFromEnv({ PLURNK_PROVIDERS_OUTPUT_BUDGET: "1%" }, "x", 2, null),
|
|
262
|
+
{ outputBudget: 1, reasoningBudget: null },
|
|
263
|
+
"a valid percentage always resolves to at least one whole token",
|
|
264
|
+
);
|
|
265
|
+
assert.throws(
|
|
266
|
+
() => generationEnvelopeFromEnv({ PLURNK_PROVIDERS_OUTPUT_BUDGET: "35%" }, "x", 1, null),
|
|
267
|
+
/must leave positive input capacity/,
|
|
200
268
|
);
|
|
201
|
-
assert.throws(() => envelopeFromEnv({ PLURNK_PROVIDERS_COMPLETION_RESERVE: "25%" } as NodeJS.ProcessEnv, "x"), /PLURNK_PROVIDERS_REASONING_RESERVE must be set/);
|
|
202
|
-
assert.throws(() => envelopeFromEnv({ PLURNK_PROVIDERS_REASONING_RESERVE: "150%", PLURNK_PROVIDERS_COMPLETION_RESERVE: "25%" } as NodeJS.ProcessEnv, "x"), /percentage must be in \(0, 100\)/);
|
|
203
|
-
assert.throws(() => envelopeFromEnv({ PLURNK_PROVIDERS_REASONING_RESERVE: "-5", PLURNK_PROVIDERS_COMPLETION_RESERVE: "25%" } as NodeJS.ProcessEnv, "x"), /positive integer token count/);
|
|
204
269
|
});
|
|
205
270
|
|
|
206
271
|
test("envelope knobs are per-alias scopable (measured envelope per box)", async () => {
|
|
207
|
-
const { scopeEnvToAlias
|
|
272
|
+
const { scopeEnvToAlias } = await import("./env.ts");
|
|
208
273
|
const env = {
|
|
209
|
-
|
|
210
|
-
|
|
274
|
+
PLURNK_PROVIDERS_OUTPUT_BUDGET: "35%",
|
|
275
|
+
PLURNK_PROVIDERS_REASONING_BUDGET: "10%",
|
|
276
|
+
PLURNK_PROVIDERS_OUTPUT_BUDGET_turboderp: "8192",
|
|
277
|
+
PLURNK_PROVIDERS_REASONING_BUDGET_turboderp: "4096",
|
|
211
278
|
} as NodeJS.ProcessEnv;
|
|
212
|
-
assert.deepEqual(
|
|
213
|
-
assert.deepEqual(
|
|
279
|
+
assert.deepEqual(generationEnvelopeFromEnv(scopeEnvToAlias(env, "turboderp"), "x", 49_152, null), { outputBudget: 8192, reasoningBudget: 4096 });
|
|
280
|
+
assert.deepEqual(generationEnvelopeFromEnv(scopeEnvToAlias(env, "jennifer"), "x", 100_000, null), { outputBudget: 35_000, reasoningBudget: 10_000 });
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("retired additive reserve knobs fail rather than creating a dual contract", () => {
|
|
284
|
+
assert.throws(() => generationEnvelopeFromEnv({
|
|
285
|
+
PLURNK_PROVIDERS_OUTPUT_BUDGET: "35%",
|
|
286
|
+
PLURNK_PROVIDERS_REASONING_RESERVE: "10%",
|
|
287
|
+
}, "x", 100_000, null), /PLURNK_PROVIDERS_REASONING_RESERVE is retired/);
|
|
288
|
+
assert.throws(() => generationEnvelopeFromEnv({
|
|
289
|
+
PLURNK_PROVIDERS_OUTPUT_BUDGET: "35%",
|
|
290
|
+
PLURNK_PROVIDERS_COMPLETION_RESERVE: "25%",
|
|
291
|
+
}, "x", 100_000, null), /PLURNK_PROVIDERS_COMPLETION_RESERVE is retired/);
|
|
214
292
|
});
|