@plurnk/plurnk-providers 1.4.0 → 1.6.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 +40 -34
- package/README.md +3 -0
- package/SPEC.md +153 -62
- package/dist/AiSdkProvider.d.ts +19 -25
- package/dist/AiSdkProvider.d.ts.map +1 -1
- package/dist/AiSdkProvider.js +353 -120
- package/dist/AiSdkProvider.js.map +1 -1
- package/dist/Mock.d.ts +7 -13
- package/dist/Mock.d.ts.map +1 -1
- package/dist/Mock.js +36 -8
- package/dist/Mock.js.map +1 -1
- package/dist/Pool.d.ts +2 -21
- package/dist/Pool.d.ts.map +1 -1
- package/dist/Pool.js +19 -14
- package/dist/Pool.js.map +1 -1
- package/dist/accounting.d.ts +6 -0
- package/dist/accounting.d.ts.map +1 -0
- package/dist/accounting.js +168 -0
- package/dist/accounting.js.map +1 -0
- package/dist/aiSdkTransport.d.ts +11 -3
- package/dist/aiSdkTransport.d.ts.map +1 -1
- package/dist/aiSdkTransport.js +198 -29
- package/dist/aiSdkTransport.js.map +1 -1
- package/dist/catalogProvider.d.ts +7 -2
- package/dist/catalogProvider.d.ts.map +1 -1
- package/dist/catalogProvider.js +32 -26
- package/dist/catalogProvider.js.map +1 -1
- package/dist/compatibleProvider.d.ts.map +1 -1
- package/dist/compatibleProvider.js +18 -7
- 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 +88 -43
- package/dist/cost.js.map +1 -1
- package/dist/env.d.ts +5 -7
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +30 -32
- package/dist/env.js.map +1 -1
- package/dist/errors.d.ts +14 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +60 -2
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/ollama.js +3 -3
- package/dist/ollama.js.map +1 -1
- package/dist/sdkModels.d.ts +6 -0
- package/dist/sdkModels.d.ts.map +1 -1
- package/dist/sdkModels.js +46 -3
- package/dist/sdkModels.js.map +1 -1
- package/dist/types.d.ts +40 -29
- package/dist/types.d.ts.map +1 -1
- package/dist/usage.d.ts +21 -4
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +188 -74
- package/dist/usage.js.map +1 -1
- package/package.json +9 -7
- package/src/AiSdkProvider.test.ts +1039 -182
- package/src/AiSdkProvider.ts +428 -141
- package/src/Mock.test.ts +37 -12
- package/src/Mock.ts +46 -12
- package/src/Pool.test.ts +19 -6
- package/src/Pool.ts +20 -16
- package/src/ProviderRegistry.test.ts +16 -11
- package/src/accounting.test.ts +94 -0
- package/src/accounting.ts +190 -0
- package/src/aiSdkTransport.test.ts +42 -49
- package/src/aiSdkTransport.ts +218 -32
- package/src/boundaries.test.ts +2 -0
- package/src/catalogProvider.test.ts +271 -24
- package/src/catalogProvider.ts +44 -28
- package/src/compatibleProvider.test.ts +6 -3
- package/src/compatibleProvider.ts +20 -7
- package/src/cost.test.ts +55 -35
- package/src/cost.ts +110 -54
- package/src/defaults.test.ts +13 -3
- package/src/env.test.ts +50 -26
- package/src/env.ts +43 -42
- package/src/errors.test.ts +47 -2
- package/src/errors.ts +68 -3
- package/src/index.ts +21 -5
- package/src/ollama.test.ts +4 -1
- package/src/ollama.ts +3 -3
- package/src/sdkModels.test.ts +94 -3
- package/src/sdkModels.ts +53 -3
- package/src/types.ts +91 -33
- package/src/usage.test.ts +112 -108
- package/src/usage.ts +233 -84
package/src/cost.test.ts
CHANGED
|
@@ -1,61 +1,81 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
addDecimals,
|
|
5
|
+
estimateProviderCost,
|
|
5
6
|
providerCostUsd,
|
|
6
|
-
|
|
7
|
+
resolveProviderCost,
|
|
8
|
+
sumProviderCostsUsd,
|
|
9
|
+
validateChargedCost,
|
|
7
10
|
} from "./cost.ts";
|
|
8
11
|
import type { ProviderUsage } from "./types.ts";
|
|
9
12
|
|
|
10
13
|
const usage: ProviderUsage = {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
cached: 0,
|
|
15
|
-
total: 2,
|
|
14
|
+
inputTokens: 1,
|
|
15
|
+
outputTokens: 1,
|
|
16
|
+
totalTokens: 2,
|
|
16
17
|
};
|
|
17
18
|
|
|
18
|
-
test("
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
calculateCharge: () => ({ kind: "estimated", usd: "12", source: "catalog" } as const),
|
|
22
|
-
};
|
|
23
|
-
const charge = {
|
|
24
|
-
kind: "authoritative",
|
|
19
|
+
test("direct charged evidence wins over a Models.dev estimate", () => {
|
|
20
|
+
const charged = {
|
|
21
|
+
kind: "charged",
|
|
25
22
|
amount: { amount: "0.0000042", currency: "XMR" },
|
|
26
23
|
usdEquivalent: "0.73",
|
|
27
|
-
source: "settled upstream
|
|
24
|
+
source: "settled upstream request charge",
|
|
28
25
|
} as const;
|
|
29
|
-
|
|
30
|
-
assert.
|
|
26
|
+
const estimated = estimateProviderCost(usage, { input: 1, output: 1 }, "Models.dev");
|
|
27
|
+
assert.deepEqual(resolveProviderCost(charged, estimated), charged);
|
|
28
|
+
assert.equal(providerCostUsd(charged), "0.73");
|
|
31
29
|
});
|
|
32
30
|
|
|
33
|
-
test("
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
test("an exact zero estimate remains distinguishable from unknown cost", () => {
|
|
32
|
+
const zero = estimateProviderCost(usage, { input: 0, output: 0 }, "Models.dev");
|
|
33
|
+
const unknown = estimateProviderCost(usage, null, "Models.dev");
|
|
34
|
+
assert.deepEqual(zero, {
|
|
35
|
+
kind: "estimated",
|
|
36
|
+
amount: { amount: "0", currency: "USD" },
|
|
37
|
+
source: "Models.dev",
|
|
38
|
+
});
|
|
40
39
|
assert.deepEqual(unknown, {
|
|
41
40
|
kind: "unknown",
|
|
42
|
-
reason: "
|
|
41
|
+
reason: "Models.dev has no complete rate for this model",
|
|
43
42
|
});
|
|
44
|
-
assert.equal(providerCostUsd(
|
|
43
|
+
assert.equal(providerCostUsd(zero), "0");
|
|
45
44
|
assert.equal(providerCostUsd(unknown), null);
|
|
46
45
|
});
|
|
47
46
|
|
|
48
|
-
test("
|
|
49
|
-
assert.deepEqual(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
test("a distinct cache rate requires the applicable token category", () => {
|
|
48
|
+
assert.deepEqual(
|
|
49
|
+
estimateProviderCost(usage, { input: 1, output: 1, cacheRead: 0.1 }, "Models.dev"),
|
|
50
|
+
{
|
|
51
|
+
kind: "unknown",
|
|
52
|
+
reason: "the provider response omitted a token category with a distinct Models.dev rate",
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("decimal aggregation is exact and becomes unknown if any request is unknown", () => {
|
|
58
|
+
assert.equal(addDecimals(["0.1", "0.02", "3"]), "3.12");
|
|
59
|
+
assert.equal(sumProviderCostsUsd([
|
|
60
|
+
{
|
|
61
|
+
kind: "charged",
|
|
62
|
+
amount: { amount: "0.1", currency: "USD" },
|
|
63
|
+
source: "provider",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
kind: "estimated",
|
|
67
|
+
amount: { amount: "0.02", currency: "USD" },
|
|
68
|
+
source: "Models.dev",
|
|
69
|
+
},
|
|
70
|
+
]), "0.12");
|
|
71
|
+
assert.equal(sumProviderCostsUsd([
|
|
72
|
+
{ kind: "unknown", reason: "no evidence" },
|
|
73
|
+
]), null);
|
|
54
74
|
});
|
|
55
75
|
|
|
56
|
-
test("
|
|
57
|
-
assert.throws(() =>
|
|
58
|
-
kind: "
|
|
76
|
+
test("malformed charged money is rejected instead of coerced", () => {
|
|
77
|
+
assert.throws(() => validateChargedCost({
|
|
78
|
+
kind: "charged",
|
|
59
79
|
amount: { amount: "1e3", currency: "usd" },
|
|
60
80
|
usdEquivalent: "1000",
|
|
61
81
|
source: "wire",
|
package/src/cost.ts
CHANGED
|
@@ -1,83 +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
|
-
|
|
51
|
-
|
|
73
|
+
export const estimateProviderCost = (
|
|
74
|
+
usage: ProviderUsage | undefined,
|
|
75
|
+
rates: TokenRates | null,
|
|
76
|
+
source: string,
|
|
52
77
|
): ProviderCost => {
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (
|
|
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" };
|
|
58
83
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
+
};
|
|
62
95
|
};
|
|
63
96
|
|
|
64
|
-
export const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
+
}
|
|
73
116
|
};
|
|
74
117
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
);
|
|
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,15 @@
|
|
|
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
|
+
parseRequiredInt,
|
|
7
|
+
parseOptionalInt,
|
|
8
|
+
parseTimeoutMs,
|
|
9
|
+
requireEnv,
|
|
10
|
+
reasoningFromEnv,
|
|
11
|
+
reasoningResponseStyleFromEnv,
|
|
12
|
+
} from "./env.ts";
|
|
4
13
|
|
|
5
14
|
test("parseRequiredInt: parses a non-negative integer", () => {
|
|
6
15
|
assert.equal(parseRequiredInt("600000", "PLURNK_PROVIDERS_FETCH_TIMEOUT", "openai"), 600000);
|
|
@@ -18,6 +27,15 @@ test("parseRequiredInt: rejects non-numeric, fractional, and negative values", (
|
|
|
18
27
|
assert.throws(() => parseRequiredInt("-1", "PLURNK_PROVIDERS_FETCH_TIMEOUT", "openai"), /must be a non-negative integer \(got "-1"\)/);
|
|
19
28
|
});
|
|
20
29
|
|
|
30
|
+
test("parseTimeoutMs accepts disabled deadlines and rejects timer overflow", () => {
|
|
31
|
+
assert.equal(parseTimeoutMs("0", "PLURNK_PROVIDERS_OPERATION_TIMEOUT", "openai"), 0);
|
|
32
|
+
assert.equal(parseTimeoutMs("2147483647", "PLURNK_PROVIDERS_OPERATION_TIMEOUT", "openai"), 2_147_483_647);
|
|
33
|
+
assert.throws(
|
|
34
|
+
() => parseTimeoutMs("2147483648", "PLURNK_PROVIDERS_OPERATION_TIMEOUT", "openai"),
|
|
35
|
+
/must be at most 2147483647 milliseconds/,
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
21
39
|
test("parseOptionalInt: absent → null, present → integer", () => {
|
|
22
40
|
assert.equal(parseOptionalInt(undefined, "PLURNK_PROVIDERS_CONTEXT_WINDOW", "openai"), null);
|
|
23
41
|
assert.equal(parseOptionalInt("", "PLURNK_PROVIDERS_CONTEXT_WINDOW", "openai"), null);
|
|
@@ -29,13 +47,13 @@ test("parseOptionalInt: rejects fractional and negative values", () => {
|
|
|
29
47
|
assert.throws(() => parseOptionalInt("-8", "PLURNK_PROVIDERS_CONTEXT_WINDOW", "openai"), /must be a non-negative integer/);
|
|
30
48
|
});
|
|
31
49
|
|
|
32
|
-
test("reasoningFromEnv: activation
|
|
50
|
+
test("reasoningFromEnv: activation is independent from an optional explicit budget", () => {
|
|
33
51
|
assert.deepEqual(reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: "off" }, "openai"), { mode: "off", budget: null });
|
|
34
52
|
assert.deepEqual(reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: "adaptive" }, "openai"), { mode: "adaptive", budget: null });
|
|
53
|
+
assert.deepEqual(reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: "on" }, "openai"), { mode: "on", budget: null });
|
|
35
54
|
assert.deepEqual(reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: "on", PLURNK_PROVIDERS_REASONING_BUDGET: "4096" }, "openai"), { mode: "on", budget: 4096 });
|
|
36
55
|
assert.throws(() => reasoningFromEnv({}, "openai"), /PLURNK_PROVIDERS_REASONING must be set/);
|
|
37
56
|
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
57
|
assert.throws(() => reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: "on", PLURNK_PROVIDERS_REASONING_BUDGET: "0" }, "openai"), /positive integer/);
|
|
40
58
|
assert.throws(() => reasoningFromEnv({ PLURNK_PROVIDERS_REASONING: "on", PLURNK_PROVIDERS_REASONING_BUDGET: "1.5" }, "openai"), /positive integer/);
|
|
41
59
|
});
|
|
@@ -59,28 +77,28 @@ 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
|
|
|
62
|
-
test("
|
|
63
|
-
assert.equal(
|
|
64
|
-
assert.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
cached: 0.3,
|
|
79
|
-
output: 15,
|
|
80
|
-
});
|
|
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", () => {
|
|
81
96
|
assert.throws(
|
|
82
|
-
() =>
|
|
83
|
-
|
|
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/,
|
|
84
102
|
);
|
|
85
103
|
});
|
|
86
104
|
|
|
@@ -111,10 +129,16 @@ test("scopeEnvToAlias: aliases with underscores resolve; a bare knob is never mi
|
|
|
111
129
|
const env = {
|
|
112
130
|
PLURNK_PROVIDERS_FETCH_TIMEOUT: "600000",
|
|
113
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",
|
|
114
136
|
PLURNK_PROVIDERS_REASONING: "off",
|
|
115
137
|
PLURNK_PROVIDERS_REASONING_BUDGET: "4096", // bare budget — NOT a "_capacity" alias override of REASONING
|
|
116
138
|
} as NodeJS.ProcessEnv;
|
|
117
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");
|
|
118
142
|
assert.equal(scopeEnvToAlias(env, "budget").PLURNK_PROVIDERS_REASONING, "off"); // collision guard
|
|
119
143
|
});
|
|
120
144
|
|
|
@@ -200,11 +224,11 @@ test("still-set old THINKING names fail hard with the rename pointer", () => {
|
|
|
200
224
|
);
|
|
201
225
|
});
|
|
202
226
|
|
|
203
|
-
test("the shipped floor
|
|
227
|
+
test("the shipped floor defers reasoning posture to the provider by default (adaptive)", async () => {
|
|
204
228
|
const { readFileSync } = await import("node:fs");
|
|
205
229
|
const defaults = readFileSync(new URL("../.env.defaults", import.meta.url), "utf8");
|
|
206
230
|
assert.ok(defaults.includes("PLURNK_PROVIDERS_REASONING=adaptive"), "floor must ship REASONING=adaptive");
|
|
207
|
-
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");
|
|
208
232
|
});
|
|
209
233
|
|
|
210
234
|
test("the shipped DRY floor is off and claims no universally safe shape", async () => {
|
package/src/env.ts
CHANGED
|
@@ -8,6 +8,16 @@ export const parseRequiredInt = (raw: string | undefined, name: string, label: s
|
|
|
8
8
|
return n;
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
export const MAX_PROVIDER_TIMEOUT_MS = 2_147_483_647;
|
|
12
|
+
|
|
13
|
+
export const parseTimeoutMs = (raw: string | undefined, name: string, label: string): number => {
|
|
14
|
+
const timeoutMs = parseRequiredInt(raw, name, label);
|
|
15
|
+
if (timeoutMs > MAX_PROVIDER_TIMEOUT_MS) {
|
|
16
|
+
throw new Error(`${label} provider: ${name} must be at most ${MAX_PROVIDER_TIMEOUT_MS} milliseconds (got "${raw}")`);
|
|
17
|
+
}
|
|
18
|
+
return timeoutMs;
|
|
19
|
+
};
|
|
20
|
+
|
|
11
21
|
export const parseOptionalInt = (raw: string | undefined, name: string, label: string): number | null => {
|
|
12
22
|
if (raw === undefined || raw.length === 0) return null;
|
|
13
23
|
const n = Number(raw);
|
|
@@ -34,15 +44,6 @@ export const requireEnv = (raw: string | undefined, name: string, label: string)
|
|
|
34
44
|
return raw;
|
|
35
45
|
};
|
|
36
46
|
|
|
37
|
-
export const promptCacheKeyFromEnv = (env: NodeJS.ProcessEnv, label: string): boolean => {
|
|
38
|
-
const name = "PLURNK_PROVIDERS_PROMPT_CACHE_KEY";
|
|
39
|
-
const value = env[name];
|
|
40
|
-
if (value !== "0" && value !== "1") {
|
|
41
|
-
throw new Error(`${label} provider: ${name} must be "0" or "1"`);
|
|
42
|
-
}
|
|
43
|
-
return value === "1";
|
|
44
|
-
};
|
|
45
|
-
|
|
46
47
|
// {§provider-configuration} A still-set retired knob fails
|
|
47
48
|
// hard pointing at its successor — never silently coexists with the new floor.
|
|
48
49
|
// The retired names appear ONLY as this function's ARGUMENTS at the call sites
|
|
@@ -52,6 +53,27 @@ const shedRenamed = (env: NodeJS.ProcessEnv, oldName: string, newName: string, l
|
|
|
52
53
|
if (stale !== undefined && stale.length > 0) throw new Error(`${label} provider: ${oldName} was renamed to ${newName} (${ref}); update the env`);
|
|
53
54
|
};
|
|
54
55
|
|
|
56
|
+
export type CacheWritePolicy = "off" | "stable-system";
|
|
57
|
+
|
|
58
|
+
export const cacheAffinityFromEnv = (env: NodeJS.ProcessEnv, label: string): boolean => {
|
|
59
|
+
const name = "PLURNK_PROVIDERS_CACHE_AFFINITY";
|
|
60
|
+
shedRenamed(env, "PLURNK_PROVIDERS_PROMPT_CACHE_KEY", name, label, "{§provider-cache-affinity}"); // lexicon-allow
|
|
61
|
+
const value = env[name];
|
|
62
|
+
if (value !== "0" && value !== "1") {
|
|
63
|
+
throw new Error(`${label} provider: ${name} must be "0" or "1"`);
|
|
64
|
+
}
|
|
65
|
+
return value === "1";
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const cacheWritePolicyFromEnv = (env: NodeJS.ProcessEnv, label: string): CacheWritePolicy => {
|
|
69
|
+
const name = "PLURNK_PROVIDERS_CACHE_WRITE_POLICY";
|
|
70
|
+
const value = env[name];
|
|
71
|
+
if (value !== "off" && value !== "stable-system") {
|
|
72
|
+
throw new Error(`${label} provider: ${name} must be "off" or "stable-system"`);
|
|
73
|
+
}
|
|
74
|
+
return value;
|
|
75
|
+
};
|
|
76
|
+
|
|
55
77
|
// {§provider-evidence} Data-capture knobs are read identically by every provider
|
|
56
78
|
// (standard AND
|
|
57
79
|
// plugin) so the opt-in surface is one source of truth. Both OFF by default —
|
|
@@ -79,8 +101,8 @@ export const contextWindowFromEnv = (env: NodeJS.ProcessEnv, label: string): num
|
|
|
79
101
|
return parseOptionalInt(env.PLURNK_PROVIDERS_CONTEXT_WINDOW, "PLURNK_PROVIDERS_CONTEXT_WINDOW", label);
|
|
80
102
|
};
|
|
81
103
|
|
|
82
|
-
// {§model-fact-resolution} — an operator value caps known model
|
|
83
|
-
// declares the
|
|
104
|
+
// {§model-fact-resolution} — an operator value caps known natural model
|
|
105
|
+
// capacity and declares the envelope only when no natural value is known.
|
|
84
106
|
export function effectiveContextWindow(operatorCap: number | null, naturalWindow: number): number;
|
|
85
107
|
export function effectiveContextWindow(operatorCap: number | null, naturalWindow: number | null): number | null;
|
|
86
108
|
export function effectiveContextWindow(operatorCap: number | null, naturalWindow: number | null): number | null {
|
|
@@ -91,32 +113,11 @@ export function effectiveContextWindow(operatorCap: number | null, naturalWindow
|
|
|
91
113
|
: Math.min(operatorCap, naturalWindow);
|
|
92
114
|
}
|
|
93
115
|
|
|
94
|
-
export type ProviderTokenRates = { input: number; cached: number; output: number };
|
|
95
|
-
|
|
96
|
-
// {§model-fact-resolution} — any explicit rate opts into one complete operator
|
|
97
|
-
// estimate; cached input alone may default to the explicit input rate.
|
|
98
|
-
export const tokenRatesFromEnv = (env: NodeJS.ProcessEnv, label: string): ProviderTokenRates | null => {
|
|
99
|
-
const inputName = "PLURNK_PROVIDERS_INPUT_USD_PER_MILLION";
|
|
100
|
-
const cachedName = "PLURNK_PROVIDERS_CACHE_READ_USD_PER_MILLION";
|
|
101
|
-
const outputName = "PLURNK_PROVIDERS_OUTPUT_USD_PER_MILLION";
|
|
102
|
-
const configured = [inputName, cachedName, outputName]
|
|
103
|
-
.some((name) => env[name] !== undefined && env[name] !== "");
|
|
104
|
-
if (!configured) return null;
|
|
105
|
-
const input = parseRequiredFloat(env[inputName], inputName, label, 0);
|
|
106
|
-
return {
|
|
107
|
-
input,
|
|
108
|
-
cached: env[cachedName] === undefined || env[cachedName] === ""
|
|
109
|
-
? input
|
|
110
|
-
: parseRequiredFloat(env[cachedName], cachedName, label, 0),
|
|
111
|
-
output: parseRequiredFloat(env[outputName], outputName, label, 0),
|
|
112
|
-
};
|
|
113
|
-
};
|
|
114
|
-
|
|
115
116
|
// {§provider-generation-envelope} How much of a
|
|
116
|
-
//
|
|
117
|
+
// effective context window is reserved for reasoning and for completion — the
|
|
117
118
|
// remainder (minus the consumer's own packing-safety margin) is the prompt
|
|
118
|
-
//
|
|
119
|
-
// it. Each knob accepts a percentage of the window
|
|
119
|
+
// gauge. Provider-owned: the window is the effective hard envelope and these are
|
|
120
|
+
// amounts OF it. Each knob accepts a percentage of the window
|
|
120
121
|
// ("10%") or an absolute token count ("4096"); the floor ships percentages so
|
|
121
122
|
// every window-advertising endpoint (llama-server n_ctx, the plurnk.ai router,
|
|
122
123
|
// a cataloged cloud model) arrives at sane defaults with ZERO operator tuning.
|
|
@@ -165,8 +166,8 @@ export const resolveEnvelopeFromEnv = (env: NodeJS.ProcessEnv, window: number |
|
|
|
165
166
|
// {§provider-configuration} The side-channel reasoning knobs — activation and budget
|
|
166
167
|
// are separate vars, so a numeric budget can never silently flip wire flags:
|
|
167
168
|
// PLURNK_PROVIDERS_REASONING off | adaptive | on (REQUIRED, fail-hard)
|
|
168
|
-
// PLURNK_PROVIDERS_REASONING_BUDGET positive int
|
|
169
|
-
//
|
|
169
|
+
// PLURNK_PROVIDERS_REASONING_BUDGET optional positive int when REASONING=on —
|
|
170
|
+
// an explicit magnitude for tier/budget mapping. On llama-server it is the
|
|
170
171
|
// request-scoped allowance and cannot exceed the physical reasoning reserve.
|
|
171
172
|
// The provider maps intent to the backend's mechanism; the consumer states
|
|
172
173
|
// intent, never mechanism. PLAN is a separate public intended-goals record.
|
|
@@ -198,7 +199,7 @@ export const reasoningFromEnv = (env: NodeJS.ProcessEnv, label: string): Reasoni
|
|
|
198
199
|
if (raw !== "on") return { mode: raw, budget: null };
|
|
199
200
|
const capName = "PLURNK_PROVIDERS_REASONING_BUDGET";
|
|
200
201
|
const capRaw = env[capName];
|
|
201
|
-
if (capRaw === undefined || capRaw.length === 0)
|
|
202
|
+
if (capRaw === undefined || capRaw.length === 0) return { mode: "on", budget: null };
|
|
202
203
|
const n = Number(capRaw);
|
|
203
204
|
if (!Number.isInteger(n) || n <= 0) throw new Error(`${label} provider: ${capName} must be a positive integer (got "${capRaw}")`);
|
|
204
205
|
return { mode: "on", budget: n };
|
|
@@ -218,19 +219,19 @@ export const PROVIDERS_KNOBS = Object.freeze([
|
|
|
218
219
|
"PLURNK_PROVIDERS_REASONING_BUDGET",
|
|
219
220
|
"PLURNK_PROVIDERS_REASONING",
|
|
220
221
|
"PLURNK_PROVIDERS_CONTEXT_WINDOW",
|
|
221
|
-
"PLURNK_PROVIDERS_INPUT_USD_PER_MILLION",
|
|
222
|
-
"PLURNK_PROVIDERS_CACHE_READ_USD_PER_MILLION",
|
|
223
|
-
"PLURNK_PROVIDERS_OUTPUT_USD_PER_MILLION",
|
|
224
222
|
"PLURNK_PROVIDERS_RETRY_ATTEMPTS",
|
|
225
223
|
"PLURNK_PROVIDERS_ERROR_DETAIL_LIMIT",
|
|
224
|
+
"PLURNK_PROVIDERS_OPERATION_TIMEOUT",
|
|
226
225
|
"PLURNK_PROVIDERS_FETCH_TIMEOUT",
|
|
226
|
+
"PLURNK_PROVIDERS_FIRST_CONTENT_TIMEOUT",
|
|
227
227
|
"PLURNK_PROVIDERS_STREAM_IDLE_TIMEOUT",
|
|
228
228
|
"PLURNK_PROVIDERS_LLAMA_SERVER",
|
|
229
229
|
"PLURNK_PROVIDERS_TEMPERATURE",
|
|
230
230
|
"PLURNK_PROVIDERS_REPEAT_PENALTY",
|
|
231
231
|
"PLURNK_PROVIDERS_FREQUENCY_PENALTY",
|
|
232
232
|
"PLURNK_PROVIDERS_SERVICE_TIER",
|
|
233
|
-
"
|
|
233
|
+
"PLURNK_PROVIDERS_CACHE_WRITE_POLICY",
|
|
234
|
+
"PLURNK_PROVIDERS_CACHE_AFFINITY",
|
|
234
235
|
"PLURNK_PROVIDERS_REPEAT_LAST_N",
|
|
235
236
|
"PLURNK_PROVIDERS_DRY_MULTIPLIER",
|
|
236
237
|
"PLURNK_PROVIDERS_DRY_BASE",
|