@plurnk/plurnk-providers 1.4.0 → 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 +9 -17
- package/README.md +3 -0
- package/SPEC.md +40 -23
- package/dist/AiSdkProvider.d.ts +2 -1
- package/dist/AiSdkProvider.d.ts.map +1 -1
- package/dist/AiSdkProvider.js +55 -34
- package/dist/AiSdkProvider.js.map +1 -1
- package/dist/Mock.d.ts +6 -7
- package/dist/Mock.d.ts.map +1 -1
- package/dist/Mock.js +1 -0
- package/dist/Mock.js.map +1 -1
- package/dist/Pool.js +1 -1
- package/dist/Pool.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 +2 -1
- package/dist/aiSdkTransport.d.ts.map +1 -1
- package/dist/aiSdkTransport.js +73 -2
- package/dist/aiSdkTransport.js.map +1 -1
- package/dist/catalogProvider.d.ts +3 -2
- package/dist/catalogProvider.d.ts.map +1 -1
- package/dist/catalogProvider.js +11 -11
- package/dist/catalogProvider.js.map +1 -1
- package/dist/cost.d.ts +1 -1
- package/dist/cost.d.ts.map +1 -1
- package/dist/cost.js +6 -9
- package/dist/cost.js.map +1 -1
- package/dist/env.d.ts +0 -6
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +0 -22
- package/dist/env.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/sdkModels.d.ts +2 -0
- package/dist/sdkModels.d.ts.map +1 -1
- package/dist/sdkModels.js +13 -3
- package/dist/sdkModels.js.map +1 -1
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/usage.d.ts +1 -0
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +38 -5
- package/dist/usage.js.map +1 -1
- package/package.json +8 -7
- package/src/AiSdkProvider.test.ts +277 -17
- package/src/AiSdkProvider.ts +70 -40
- package/src/Mock.ts +5 -2
- package/src/Pool.ts +1 -1
- package/src/accounting.test.ts +58 -0
- package/src/accounting.ts +88 -0
- package/src/aiSdkTransport.ts +77 -3
- package/src/boundaries.test.ts +1 -0
- package/src/catalogProvider.test.ts +26 -15
- package/src/catalogProvider.ts +12 -11
- package/src/cost.test.ts +7 -6
- package/src/cost.ts +4 -9
- package/src/env.test.ts +1 -26
- package/src/env.ts +0 -24
- package/src/errors.ts +1 -0
- package/src/index.ts +1 -1
- package/src/sdkModels.test.ts +22 -3
- package/src/sdkModels.ts +15 -3
- package/src/types.ts +22 -3
- package/src/usage.test.ts +9 -1
- package/src/usage.ts +40 -7
package/src/sdkModels.ts
CHANGED
|
@@ -6,13 +6,15 @@ import { createGroq } from "@ai-sdk/groq";
|
|
|
6
6
|
import { createMistral } from "@ai-sdk/mistral";
|
|
7
7
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
8
8
|
import { createTogetherAI } from "@ai-sdk/togetherai";
|
|
9
|
-
import { createXai } from "@ai-sdk/xai";
|
|
10
9
|
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
|
|
11
10
|
import { lookupProvider, type ProviderInfo } from "@plurnk/plurnk-models";
|
|
12
11
|
import type { LanguageModel } from "ai";
|
|
12
|
+
import { authoritativeChargeNormalizer } from "./accounting.ts";
|
|
13
|
+
import type { AuthoritativeChargeNormalizer } from "./types.ts";
|
|
13
14
|
|
|
14
15
|
export type SdkModel = {
|
|
15
16
|
readonly languageModel?: LanguageModel;
|
|
17
|
+
readonly normalizeCharge?: AuthoritativeChargeNormalizer;
|
|
16
18
|
readonly compatible?: {
|
|
17
19
|
readonly url: string;
|
|
18
20
|
readonly headers: Readonly<Record<string, string>>;
|
|
@@ -105,6 +107,7 @@ export const createSdkModel = (
|
|
|
105
107
|
const catalog = lookupProvider(provider) ?? configuredProviderInfo(provider, env);
|
|
106
108
|
if (catalog === null) return null;
|
|
107
109
|
const url = baseUrl(provider, env, catalog, baseUrlOverride);
|
|
110
|
+
const normalizeCharge = authoritativeChargeNormalizer(catalog.npm);
|
|
108
111
|
|
|
109
112
|
switch (catalog.npm) {
|
|
110
113
|
case "@ai-sdk/openai":
|
|
@@ -130,6 +133,7 @@ export const createSdkModel = (
|
|
|
130
133
|
case "@ai-sdk/deepinfra":
|
|
131
134
|
return {
|
|
132
135
|
languageModel: createDeepInfra({ apiKey: requireApiKey(provider, env, catalog), baseURL: url }).languageModel(model),
|
|
136
|
+
...(normalizeCharge === undefined ? {} : { normalizeCharge }),
|
|
133
137
|
catalog,
|
|
134
138
|
};
|
|
135
139
|
case "@ai-sdk/google":
|
|
@@ -137,11 +141,18 @@ export const createSdkModel = (
|
|
|
137
141
|
languageModel: createGoogle({ apiKey: requireApiKey(provider, env, catalog), baseURL: url }).languageModel(model),
|
|
138
142
|
catalog,
|
|
139
143
|
};
|
|
140
|
-
case "@ai-sdk/xai":
|
|
144
|
+
case "@ai-sdk/xai": {
|
|
145
|
+
const key = requireApiKey(provider, env, catalog);
|
|
146
|
+
const compatibleBase = url ?? "https://api.x.ai/v1";
|
|
141
147
|
return {
|
|
142
|
-
|
|
148
|
+
compatible: {
|
|
149
|
+
url: `${compatibleBase}/chat/completions`,
|
|
150
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
151
|
+
},
|
|
152
|
+
...(normalizeCharge === undefined ? {} : { normalizeCharge }),
|
|
143
153
|
catalog,
|
|
144
154
|
};
|
|
155
|
+
}
|
|
145
156
|
case "@ai-sdk/anthropic":
|
|
146
157
|
return {
|
|
147
158
|
languageModel: createAnthropic({ apiKey: requireApiKey(provider, env, catalog), baseURL: url }).languageModel(model),
|
|
@@ -169,6 +180,7 @@ export const createSdkModel = (
|
|
|
169
180
|
...(env.OPENROUTER_X_TITLE === undefined ? {} : { "X-Title": env.OPENROUTER_X_TITLE }),
|
|
170
181
|
},
|
|
171
182
|
}).languageModel(model),
|
|
183
|
+
...(normalizeCharge === undefined ? {} : { normalizeCharge }),
|
|
172
184
|
catalog,
|
|
173
185
|
};
|
|
174
186
|
case "@ai-sdk/openai-compatible":
|
package/src/types.ts
CHANGED
|
@@ -46,6 +46,25 @@ export interface ProviderUsage {
|
|
|
46
46
|
|
|
47
47
|
export type AuthoritativeCharge = Extract<ProviderCost, { kind: "authoritative" }>;
|
|
48
48
|
|
|
49
|
+
// Evidence exposed by the transport to the provider adapter that owns its
|
|
50
|
+
// vendor protocol. Core and downstream consumers receive only the normalized
|
|
51
|
+
// charge, never a requirement to understand provider metadata fields.
|
|
52
|
+
export interface ProviderChargeEvidence {
|
|
53
|
+
readonly providerMetadata?: unknown;
|
|
54
|
+
// Provider-owned raw usage projection retained independently of optional
|
|
55
|
+
// full-body capture. Accounting fields cannot disappear merely because
|
|
56
|
+
// forensic raw-body capture is disabled.
|
|
57
|
+
readonly usage?: unknown;
|
|
58
|
+
readonly response: {
|
|
59
|
+
readonly id: string;
|
|
60
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type AuthoritativeChargeNormalizer = (
|
|
65
|
+
evidence: ProviderChargeEvidence,
|
|
66
|
+
) => AuthoritativeCharge | undefined;
|
|
67
|
+
|
|
49
68
|
// A successful exchange's closed finish set. ProviderAttemptFinishReason adds
|
|
50
69
|
// the failed disposition that may occur only on ProviderError attempt evidence.
|
|
51
70
|
export type FinishReason = "stop" | "length" | "tool_calls" | "content_filter" | null;
|
|
@@ -232,10 +251,10 @@ export interface Provider {
|
|
|
232
251
|
// consumers (the tokenizer seam) prefer this over any client-side data.
|
|
233
252
|
tokenize?(text: string): Promise<number[]>;
|
|
234
253
|
// {§model-fact-resolution} — frozen 1.x local USD estimate compatibility.
|
|
235
|
-
//
|
|
254
|
+
// This is not a monetary-reporting authority.
|
|
236
255
|
calculateCost(usage: ProviderUsage): number;
|
|
237
|
-
//
|
|
238
|
-
//
|
|
256
|
+
// Models.dev-derived monetary fallback. Direct response charges travel on
|
|
257
|
+
// ProviderResponse and take precedence at the consuming attempt boundary.
|
|
239
258
|
calculateCharge?(usage: ProviderUsage): Exclude<ProviderCost, AuthoritativeCharge>;
|
|
240
259
|
}
|
|
241
260
|
|
package/src/usage.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 { normalizeUsage, calculateCostUsd } from "./usage.ts";
|
|
3
|
+
import { normalizeUsage, calculateCostUsd, calculateCostUsdDecimal } from "./usage.ts";
|
|
4
4
|
|
|
5
5
|
// — normalizeUsage —
|
|
6
6
|
|
|
@@ -143,3 +143,11 @@ test("calculateCostUsd: zero rates → 0", () => {
|
|
|
143
143
|
const usage = { prompt: 9, completion: 9, reasoning: 9, cached: 9, total: 27 };
|
|
144
144
|
assert.equal(calculateCostUsd(usage, { input: 0, output: 0, cached: 0 }), 0);
|
|
145
145
|
});
|
|
146
|
+
|
|
147
|
+
test("calculateCostUsdDecimal preserves Models.dev rates without floating-point artifacts", () => {
|
|
148
|
+
const usage = { prompt: 1_000, completion: 100, reasoning: 50, cached: 400, total: 1_150 };
|
|
149
|
+
assert.equal(
|
|
150
|
+
calculateCostUsdDecimal(usage, { input: 0.14, output: 0.28, cached: 0.0028 }),
|
|
151
|
+
"0.00012712",
|
|
152
|
+
);
|
|
153
|
+
});
|
package/src/usage.ts
CHANGED
|
@@ -90,15 +90,48 @@ export const normalizeUsage = (raw: RawUsage | null | undefined, reasoningText =
|
|
|
90
90
|
// Conventional provider pricing: USD per million tokens, matching Models.dev.
|
|
91
91
|
export type TokenRates = { input: number; output: number; cached: number };
|
|
92
92
|
|
|
93
|
+
const decimalParts = (value: number): { coefficient: bigint; scale: number } => {
|
|
94
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
95
|
+
throw new TypeError("token rates must be finite non-negative numbers");
|
|
96
|
+
}
|
|
97
|
+
const match = /^(\d+)(?:\.(\d+))?(?:e([+-]?\d+))?$/i.exec(String(value));
|
|
98
|
+
if (match === null) throw new TypeError(`cannot represent token rate ${value} as a decimal`);
|
|
99
|
+
const fraction = match[2] ?? "";
|
|
100
|
+
const exponent = Number(match[3] ?? "0");
|
|
101
|
+
const scale = fraction.length - exponent;
|
|
102
|
+
const coefficient = BigInt(`${match[1]}${fraction}`);
|
|
103
|
+
return scale < 0
|
|
104
|
+
? { coefficient: coefficient * 10n ** BigInt(-scale), scale: 0 }
|
|
105
|
+
: { coefficient, scale };
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const decimalString = (coefficient: bigint, scale: number): string => {
|
|
109
|
+
const digits = String(coefficient).padStart(scale + 1, "0");
|
|
110
|
+
if (scale === 0) return digits;
|
|
111
|
+
const integer = digits.slice(0, -scale);
|
|
112
|
+
const fraction = digits.slice(-scale).replace(/0+$/, "");
|
|
113
|
+
return fraction.length === 0 ? integer : `${integer}.${fraction}`;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Models.dev rates are decimal USD-per-million values. Calculate their
|
|
117
|
+
// projection as decimal arithmetic so ProviderCost preserves the table and
|
|
118
|
+
// exact response counts without binary floating-point artifacts.
|
|
119
|
+
export const calculateCostUsdDecimal = (usage: ProviderUsage, rates: TokenRates): string => {
|
|
120
|
+
const parts = [rates.input, rates.cached, rates.output].map(decimalParts);
|
|
121
|
+
const rateScale = Math.max(...parts.map(({ scale }) => scale));
|
|
122
|
+
const [input, cached, output] = parts.map(({ coefficient, scale }) =>
|
|
123
|
+
coefficient * 10n ** BigInt(rateScale - scale));
|
|
124
|
+
const nonCachedPrompt = Math.max(0, usage.prompt - usage.cached);
|
|
125
|
+
const outputTokens = usage.completion + usage.reasoning;
|
|
126
|
+
const coefficient = BigInt(nonCachedPrompt) * input!
|
|
127
|
+
+ BigInt(usage.cached) * cached!
|
|
128
|
+
+ BigInt(outputTokens) * output!;
|
|
129
|
+
return decimalString(coefficient, rateScale + 6);
|
|
130
|
+
};
|
|
131
|
+
|
|
93
132
|
// The one cost formula every provider uses: non-cached prompt at the input
|
|
94
133
|
// rate, cached prompt at the cache rate, and billable output (completion +
|
|
95
134
|
// reasoning) at the output rate.
|
|
96
135
|
export const calculateCostUsd = (usage: ProviderUsage, rates: TokenRates): number => {
|
|
97
|
-
|
|
98
|
-
const output = usage.completion + usage.reasoning;
|
|
99
|
-
return (
|
|
100
|
-
nonCachedPrompt * rates.input
|
|
101
|
-
+ usage.cached * rates.cached
|
|
102
|
-
+ output * rates.output
|
|
103
|
-
) / 1_000_000;
|
|
136
|
+
return Number(calculateCostUsdDecimal(usage, rates));
|
|
104
137
|
};
|