@velum-labs/routekit-registry 1.3.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/generated/data.d.ts +24 -0
- package/dist/generated/data.js +25 -1
- package/dist/index.d.ts +23 -8
- package/dist/index.js +3 -6
- package/dist/pricing.d.ts +6 -0
- package/dist/pricing.js +6 -0
- package/dist/services/cost/service.d.ts +66 -0
- package/dist/services/cost/service.js +225 -0
- package/dist/test/registry.test.js +115 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -3,3 +3,7 @@
|
|
|
3
3
|
Product-neutral generated provider, subscription, catalog, capability, pricing,
|
|
4
4
|
and local model metadata from `spec/registry`. RouteKit packages consume this
|
|
5
5
|
neutral registry surface only.
|
|
6
|
+
|
|
7
|
+
`CostService` is the shared pricing, pre-run estimation, and observed-actuals
|
|
8
|
+
capability. Unknown pricing, billing modes, reasoning-token multipliers, and
|
|
9
|
+
missing usage remain explicit instead of becoming zero.
|
package/dist/generated/data.d.ts
CHANGED
|
@@ -183,6 +183,15 @@ export declare const REGISTRY: {
|
|
|
183
183
|
oauth: {
|
|
184
184
|
tokenEndpoint: string;
|
|
185
185
|
clientId: string;
|
|
186
|
+
authorizeUrl: string;
|
|
187
|
+
redirectUri: string;
|
|
188
|
+
manualRedirectUri: string;
|
|
189
|
+
scopes: string;
|
|
190
|
+
codeChallengeMethod: string;
|
|
191
|
+
tokenRequestFormat: string;
|
|
192
|
+
authorizeExtraParams: {
|
|
193
|
+
code: string;
|
|
194
|
+
};
|
|
186
195
|
usageEndpoint: string;
|
|
187
196
|
profileEndpoint: string;
|
|
188
197
|
};
|
|
@@ -234,8 +243,23 @@ export declare const REGISTRY: {
|
|
|
234
243
|
omitSampling: boolean;
|
|
235
244
|
};
|
|
236
245
|
oauth: {
|
|
246
|
+
issuer: string;
|
|
237
247
|
tokenEndpoint: string;
|
|
238
248
|
clientId: string;
|
|
249
|
+
authorizeUrl: string;
|
|
250
|
+
redirectUri: string;
|
|
251
|
+
scopes: string;
|
|
252
|
+
codeChallengeMethod: string;
|
|
253
|
+
tokenRequestFormat: string;
|
|
254
|
+
authorizeExtraParams: {
|
|
255
|
+
id_token_add_organizations: string;
|
|
256
|
+
codex_cli_simplified_flow: string;
|
|
257
|
+
originator: string;
|
|
258
|
+
};
|
|
259
|
+
deviceVerificationUrl: string;
|
|
260
|
+
deviceUserCodeEndpoint: string;
|
|
261
|
+
deviceTokenPollEndpoint: string;
|
|
262
|
+
deviceRedirectUri: string;
|
|
239
263
|
usageEndpoint: string;
|
|
240
264
|
usagePathFallback: string;
|
|
241
265
|
resetCreditsEndpoint: string;
|
package/dist/generated/data.js
CHANGED
|
@@ -201,8 +201,17 @@ export const REGISTRY = {
|
|
|
201
201
|
}
|
|
202
202
|
},
|
|
203
203
|
"oauth": {
|
|
204
|
-
"tokenEndpoint": "https://
|
|
204
|
+
"tokenEndpoint": "https://platform.claude.com/v1/oauth/token",
|
|
205
205
|
"clientId": "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
|
|
206
|
+
"authorizeUrl": "https://claude.com/cai/oauth/authorize",
|
|
207
|
+
"redirectUri": "http://localhost:54545/callback",
|
|
208
|
+
"manualRedirectUri": "https://platform.claude.com/oauth/code/callback",
|
|
209
|
+
"scopes": "org:create_api_key user:profile user:inference",
|
|
210
|
+
"codeChallengeMethod": "S256",
|
|
211
|
+
"tokenRequestFormat": "json",
|
|
212
|
+
"authorizeExtraParams": {
|
|
213
|
+
"code": "true"
|
|
214
|
+
},
|
|
206
215
|
"usageEndpoint": "https://api.anthropic.com/api/oauth/usage",
|
|
207
216
|
"profileEndpoint": "https://api.anthropic.com/api/oauth/profile"
|
|
208
217
|
},
|
|
@@ -260,8 +269,23 @@ export const REGISTRY = {
|
|
|
260
269
|
"omitSampling": true
|
|
261
270
|
},
|
|
262
271
|
"oauth": {
|
|
272
|
+
"issuer": "https://auth.openai.com",
|
|
263
273
|
"tokenEndpoint": "https://auth.openai.com/oauth/token",
|
|
264
274
|
"clientId": "app_EMoamEEZ73f0CkXaXp7hrann",
|
|
275
|
+
"authorizeUrl": "https://auth.openai.com/oauth/authorize",
|
|
276
|
+
"redirectUri": "http://localhost:1455/auth/callback",
|
|
277
|
+
"scopes": "openid profile email offline_access api.connectors.read api.connectors.invoke",
|
|
278
|
+
"codeChallengeMethod": "S256",
|
|
279
|
+
"tokenRequestFormat": "form",
|
|
280
|
+
"authorizeExtraParams": {
|
|
281
|
+
"id_token_add_organizations": "true",
|
|
282
|
+
"codex_cli_simplified_flow": "true",
|
|
283
|
+
"originator": "routekit"
|
|
284
|
+
},
|
|
285
|
+
"deviceVerificationUrl": "https://auth.openai.com/codex/device",
|
|
286
|
+
"deviceUserCodeEndpoint": "https://auth.openai.com/api/accounts/deviceauth/usercode",
|
|
287
|
+
"deviceTokenPollEndpoint": "https://auth.openai.com/api/accounts/deviceauth/token",
|
|
288
|
+
"deviceRedirectUri": "https://auth.openai.com/deviceauth/callback",
|
|
265
289
|
"usageEndpoint": "https://chatgpt.com/backend-api/wham/usage",
|
|
266
290
|
"usagePathFallback": "/api/codex/usage",
|
|
267
291
|
"resetCreditsEndpoint": "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits"
|
package/dist/index.d.ts
CHANGED
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
* model data are generated from spec/registry. Product-specific identities and
|
|
6
6
|
* panel presets are deliberately excluded.
|
|
7
7
|
*/
|
|
8
|
-
import { REGISTRY } from "./generated/data.js";
|
|
9
8
|
import type { ProviderDiscoveryResponseShape } from "@velum-labs/routekit-contracts/provider-discovery";
|
|
10
|
-
|
|
9
|
+
import { REGISTRY } from "./generated/data.js";
|
|
10
|
+
import { DEFAULT_MODEL_PRICING, PRICING_ALIASES } from "./pricing.js";
|
|
11
|
+
export type { RegistryModelPricing } from "./pricing.js";
|
|
11
12
|
export type { ProviderDiscoveryResponseShape };
|
|
13
|
+
export { DEFAULT_MODEL_PRICING, PRICING_ALIASES, REGISTRY };
|
|
12
14
|
export type ProviderAuthStyle = "bearer" | "x-api-key" | "x-goog-api-key" | "aws-sdk";
|
|
13
15
|
export type ProviderKeyProbe = {
|
|
14
16
|
path: string;
|
|
@@ -46,9 +48,26 @@ export declare function defaultKeyEnv(provider: string): string | undefined;
|
|
|
46
48
|
export declare function providerKeyProbe(provider: string): ProviderKeyProbe | undefined;
|
|
47
49
|
export declare function providerDiscovery(provider: string): ProviderDiscovery | undefined;
|
|
48
50
|
export type SubscriptionMode = "claude-code" | "codex";
|
|
51
|
+
export type SubscriptionTokenRequestFormat = "form" | "json";
|
|
49
52
|
export type SubscriptionOAuthInfo = {
|
|
50
53
|
tokenEndpoint: string;
|
|
51
54
|
clientId: string;
|
|
55
|
+
authorizeUrl: string;
|
|
56
|
+
redirectUri: string;
|
|
57
|
+
/**
|
|
58
|
+
* Headless/manual redirect used when the host cannot bind a loopback listener
|
|
59
|
+
* (Claude `--no-browser`). Browser logins keep `redirectUri`.
|
|
60
|
+
*/
|
|
61
|
+
manualRedirectUri?: string;
|
|
62
|
+
scopes: string;
|
|
63
|
+
codeChallengeMethod: "S256";
|
|
64
|
+
tokenRequestFormat: SubscriptionTokenRequestFormat;
|
|
65
|
+
authorizeExtraParams?: Readonly<Record<string, string>>;
|
|
66
|
+
issuer?: string;
|
|
67
|
+
deviceVerificationUrl?: string;
|
|
68
|
+
deviceUserCodeEndpoint?: string;
|
|
69
|
+
deviceTokenPollEndpoint?: string;
|
|
70
|
+
deviceRedirectUri?: string;
|
|
52
71
|
usageEndpoint: string;
|
|
53
72
|
profileEndpoint?: string;
|
|
54
73
|
usagePathFallback?: string;
|
|
@@ -131,12 +150,8 @@ export declare function curatedModels(choice: string): readonly string[];
|
|
|
131
150
|
export declare function smokeModelForTool(tool: string): string | undefined;
|
|
132
151
|
export declare function samplingOverridesForModel(model: string): Readonly<Record<string, number>>;
|
|
133
152
|
export declare function chatTemplateKwargsForModel(model: string): Readonly<Record<string, boolean>> | undefined;
|
|
134
|
-
export type
|
|
135
|
-
|
|
136
|
-
outputPer1mTokens: number;
|
|
137
|
-
};
|
|
138
|
-
export declare const PRICING_ALIASES: Readonly<Record<string, string>>;
|
|
139
|
-
export declare const DEFAULT_MODEL_PRICING: Readonly<Record<string, RegistryModelPricing>>;
|
|
153
|
+
export type { CostActualCall, CostActuals, CostBillingMode, CostEstimate, CostEstimateCall, CostModelPricing, CostServiceShape, CostTokenUsage } from "./services/cost/service.js";
|
|
154
|
+
export { attributeModelCosts, CostService, CostServiceLive, estimateModelCosts, formatCostUsd, lookupModelPricing, makeCostService } from "./services/cost/service.js";
|
|
140
155
|
export type LocalModelRole = "general" | "coder";
|
|
141
156
|
export type LocalCatalogModel = {
|
|
142
157
|
repo: string;
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* panel presets are deliberately excluded.
|
|
7
7
|
*/
|
|
8
8
|
import { REGISTRY } from "./generated/data.js";
|
|
9
|
-
|
|
9
|
+
import { DEFAULT_MODEL_PRICING, PRICING_ALIASES } from "./pricing.js";
|
|
10
|
+
export { DEFAULT_MODEL_PRICING, PRICING_ALIASES, REGISTRY };
|
|
10
11
|
export const PROVIDERS = REGISTRY.providers;
|
|
11
12
|
export function providerDefaultBaseUrl(provider) {
|
|
12
13
|
return PROVIDERS[provider]?.baseUrl;
|
|
@@ -97,11 +98,7 @@ export function chatTemplateKwargsForModel(model) {
|
|
|
97
98
|
}
|
|
98
99
|
return undefined;
|
|
99
100
|
}
|
|
100
|
-
export
|
|
101
|
-
export const DEFAULT_MODEL_PRICING = {
|
|
102
|
-
...REGISTRY.pricing.models,
|
|
103
|
-
...REGISTRY.pricing.manualOverrides
|
|
104
|
-
};
|
|
101
|
+
export { attributeModelCosts, CostService, CostServiceLive, estimateModelCosts, formatCostUsd, lookupModelPricing, makeCostService } from "./services/cost/service.js";
|
|
105
102
|
export const LOCAL_CATALOG_ENTRIES = REGISTRY.localCatalog
|
|
106
103
|
.entries;
|
|
107
104
|
export const PREFERRED_LOCAL_MODELS = REGISTRY.localCatalog.preferred;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type RegistryModelPricing = {
|
|
2
|
+
readonly inputPer1mTokens: number;
|
|
3
|
+
readonly outputPer1mTokens: number;
|
|
4
|
+
};
|
|
5
|
+
export declare const PRICING_ALIASES: Readonly<Record<string, string>>;
|
|
6
|
+
export declare const DEFAULT_MODEL_PRICING: Readonly<Record<string, RegistryModelPricing>>;
|
package/dist/pricing.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect";
|
|
2
|
+
export type CostBillingMode = "metered-api" | "subscription" | "upstream-managed" | "unknown";
|
|
3
|
+
export type CostModelPricing = {
|
|
4
|
+
readonly inputPer1mTokens: number;
|
|
5
|
+
readonly outputPer1mTokens: number;
|
|
6
|
+
readonly currency?: string;
|
|
7
|
+
};
|
|
8
|
+
export type CostTokenUsage = {
|
|
9
|
+
readonly inputTokens?: number;
|
|
10
|
+
readonly outputTokens?: number;
|
|
11
|
+
};
|
|
12
|
+
export type CostEstimateCall = {
|
|
13
|
+
readonly model: string;
|
|
14
|
+
readonly callCount: number;
|
|
15
|
+
readonly maximumInputTokens?: number;
|
|
16
|
+
readonly maximumOutputTokens?: number;
|
|
17
|
+
readonly billingMode?: CostBillingMode;
|
|
18
|
+
readonly reasoningEffort?: string;
|
|
19
|
+
};
|
|
20
|
+
export type CostEstimate = {
|
|
21
|
+
readonly callCount: number;
|
|
22
|
+
readonly knownInputTokens: number;
|
|
23
|
+
readonly knownOutputTokens: number;
|
|
24
|
+
readonly unknownTokenCalls: number;
|
|
25
|
+
readonly maximumCostUsd?: number;
|
|
26
|
+
readonly knownPricedMaximumUsd: number;
|
|
27
|
+
readonly unpricedCalls: number;
|
|
28
|
+
readonly subscriptionUnitCalls: number;
|
|
29
|
+
readonly upstreamManagedCalls: number;
|
|
30
|
+
readonly unknownBillingCalls: number;
|
|
31
|
+
readonly unknowns: readonly string[];
|
|
32
|
+
};
|
|
33
|
+
export type CostActualCall = {
|
|
34
|
+
readonly model: string;
|
|
35
|
+
readonly usage?: CostTokenUsage;
|
|
36
|
+
readonly billingMode?: CostBillingMode;
|
|
37
|
+
readonly providerCostUsd?: number;
|
|
38
|
+
};
|
|
39
|
+
export type CostActuals = {
|
|
40
|
+
readonly callCount: number;
|
|
41
|
+
readonly knownInputTokens: number;
|
|
42
|
+
readonly knownOutputTokens: number;
|
|
43
|
+
readonly unknownTokenCalls: number;
|
|
44
|
+
readonly knownCostUsd?: number;
|
|
45
|
+
readonly knownPricedSubtotalUsd: number;
|
|
46
|
+
readonly unpricedCalls: number;
|
|
47
|
+
readonly subscriptionUnitCalls: number;
|
|
48
|
+
readonly upstreamManagedCalls: number;
|
|
49
|
+
readonly unknownBillingCalls: number;
|
|
50
|
+
readonly unknowns: readonly string[];
|
|
51
|
+
};
|
|
52
|
+
export type CostServiceShape = {
|
|
53
|
+
readonly lookupPricing: (model: string, overrides?: Readonly<Record<string, CostModelPricing>>) => Effect.Effect<CostModelPricing | undefined>;
|
|
54
|
+
readonly estimate: (calls: readonly CostEstimateCall[]) => Effect.Effect<CostEstimate>;
|
|
55
|
+
readonly actuals: (calls: readonly CostActualCall[]) => Effect.Effect<CostActuals>;
|
|
56
|
+
};
|
|
57
|
+
declare const CostService_base: Context.ServiceClass<CostService, "@velum-labs/routekit-registry/CostService", CostServiceShape>;
|
|
58
|
+
export declare class CostService extends CostService_base {
|
|
59
|
+
}
|
|
60
|
+
export declare const estimateModelCosts: (calls: readonly CostEstimateCall[], overrides?: Readonly<Record<string, CostModelPricing>>) => CostEstimate;
|
|
61
|
+
export declare const attributeModelCosts: (calls: readonly CostActualCall[], overrides?: Readonly<Record<string, CostModelPricing>>) => CostActuals;
|
|
62
|
+
export declare const makeCostService: (overrides?: Readonly<Record<string, CostModelPricing>>) => CostService["Service"];
|
|
63
|
+
export declare const CostServiceLive: Layer.Layer<CostService, never, never>;
|
|
64
|
+
export declare const lookupModelPricing: (model: string, overrides?: Readonly<Record<string, CostModelPricing>>) => CostModelPricing | undefined;
|
|
65
|
+
export declare const formatCostUsd: (amount: number, currency?: string) => string;
|
|
66
|
+
export {};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect";
|
|
2
|
+
import { REGISTRY } from "../../generated/data.js";
|
|
3
|
+
import { DEFAULT_MODEL_PRICING, PRICING_ALIASES } from "../../pricing.js";
|
|
4
|
+
export class CostService extends Context.Service()("@velum-labs/routekit-registry/CostService") {
|
|
5
|
+
}
|
|
6
|
+
const DEFAULT_CURRENCY = "USD";
|
|
7
|
+
const modelNames = (model) => [
|
|
8
|
+
model,
|
|
9
|
+
...(model.includes("/") ? [model.slice(model.indexOf("/") + 1)] : [])
|
|
10
|
+
];
|
|
11
|
+
const lookupPricing = (model, overrides = {}) => {
|
|
12
|
+
const combined = {
|
|
13
|
+
...DEFAULT_MODEL_PRICING,
|
|
14
|
+
...overrides
|
|
15
|
+
};
|
|
16
|
+
for (const name of modelNames(model)) {
|
|
17
|
+
const direct = Object.entries(combined).find(([candidate]) => candidate.toLowerCase() === name.toLowerCase())?.[1];
|
|
18
|
+
if (direct !== undefined)
|
|
19
|
+
return direct;
|
|
20
|
+
const alias = Object.entries(PRICING_ALIASES).find(([candidate]) => candidate.toLowerCase() === name.toLowerCase())?.[1];
|
|
21
|
+
if (alias === undefined)
|
|
22
|
+
continue;
|
|
23
|
+
const resolved = Object.entries(combined).find(([candidate]) => candidate.toLowerCase() === alias.toLowerCase())?.[1];
|
|
24
|
+
if (resolved !== undefined)
|
|
25
|
+
return resolved;
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
};
|
|
29
|
+
const dollars = (usage, pricing) => (usage.inputTokens * pricing.inputPer1mTokens +
|
|
30
|
+
usage.outputTokens * pricing.outputPer1mTokens) /
|
|
31
|
+
1_000_000;
|
|
32
|
+
const addUnknown = (unknowns, value) => {
|
|
33
|
+
unknowns.add(value);
|
|
34
|
+
};
|
|
35
|
+
const validCallCount = (value) => Number.isSafeInteger(value) && value >= 0;
|
|
36
|
+
const validTokenCount = (value) => value === undefined || (Number.isSafeInteger(value) && value >= 0);
|
|
37
|
+
const billingModeOf = (model, mode, pricing) => {
|
|
38
|
+
if (mode !== undefined)
|
|
39
|
+
return mode;
|
|
40
|
+
const provider = model.split("/", 1)[0]?.toLowerCase();
|
|
41
|
+
if (provider !== undefined && provider in REGISTRY.subscriptions)
|
|
42
|
+
return "subscription";
|
|
43
|
+
if (provider !== undefined && provider in REGISTRY.providers)
|
|
44
|
+
return "metered-api";
|
|
45
|
+
return pricing === undefined ? "unknown" : "metered-api";
|
|
46
|
+
};
|
|
47
|
+
export const estimateModelCosts = (calls, overrides = {}) => {
|
|
48
|
+
let callCount = 0;
|
|
49
|
+
let knownInputTokens = 0;
|
|
50
|
+
let knownOutputTokens = 0;
|
|
51
|
+
let unknownTokenCalls = 0;
|
|
52
|
+
let maximumCostUsd = 0;
|
|
53
|
+
let pricedCalls = 0;
|
|
54
|
+
let unpricedCalls = 0;
|
|
55
|
+
let subscriptionUnitCalls = 0;
|
|
56
|
+
let upstreamManagedCalls = 0;
|
|
57
|
+
let unknownBillingCalls = 0;
|
|
58
|
+
const unknowns = new Set();
|
|
59
|
+
for (const call of calls) {
|
|
60
|
+
if (!validCallCount(call.callCount) ||
|
|
61
|
+
!validTokenCount(call.maximumInputTokens) ||
|
|
62
|
+
!validTokenCount(call.maximumOutputTokens)) {
|
|
63
|
+
throw new Error("cost estimate calls require non-negative safe integer counts");
|
|
64
|
+
}
|
|
65
|
+
callCount += call.callCount;
|
|
66
|
+
const pricing = lookupPricing(call.model, overrides);
|
|
67
|
+
const billingMode = billingModeOf(call.model, call.billingMode, pricing);
|
|
68
|
+
if (call.maximumInputTokens === undefined || call.maximumOutputTokens === undefined) {
|
|
69
|
+
unknownTokenCalls += call.callCount;
|
|
70
|
+
addUnknown(unknowns, `token bounds for ${call.model}`);
|
|
71
|
+
}
|
|
72
|
+
if (call.maximumInputTokens !== undefined) {
|
|
73
|
+
knownInputTokens += call.maximumInputTokens * call.callCount;
|
|
74
|
+
}
|
|
75
|
+
if (call.maximumOutputTokens !== undefined) {
|
|
76
|
+
knownOutputTokens += call.maximumOutputTokens * call.callCount;
|
|
77
|
+
}
|
|
78
|
+
if (call.reasoningEffort !== undefined) {
|
|
79
|
+
addUnknown(unknowns, `reasoning-token multiplier for ${call.model}:${call.reasoningEffort}`);
|
|
80
|
+
}
|
|
81
|
+
if (billingMode === "subscription") {
|
|
82
|
+
subscriptionUnitCalls += call.callCount;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (billingMode === "upstream-managed") {
|
|
86
|
+
upstreamManagedCalls += call.callCount;
|
|
87
|
+
addUnknown(unknowns, `upstream-managed billing for ${call.model}`);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (billingMode === "unknown") {
|
|
91
|
+
unknownBillingCalls += call.callCount;
|
|
92
|
+
addUnknown(unknowns, `billing mode for ${call.model}`);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (pricing === undefined) {
|
|
96
|
+
unpricedCalls += call.callCount;
|
|
97
|
+
addUnknown(unknowns, `pricing for ${call.model}`);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
pricedCalls += call.callCount;
|
|
101
|
+
if (call.maximumInputTokens !== undefined && call.maximumOutputTokens !== undefined) {
|
|
102
|
+
maximumCostUsd +=
|
|
103
|
+
dollars({
|
|
104
|
+
inputTokens: call.maximumInputTokens,
|
|
105
|
+
outputTokens: call.maximumOutputTokens
|
|
106
|
+
}, pricing) * call.callCount;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const allDollarCallsPriced = unpricedCalls === 0 &&
|
|
110
|
+
upstreamManagedCalls === 0 &&
|
|
111
|
+
unknownBillingCalls === 0 &&
|
|
112
|
+
unknownTokenCalls === 0 &&
|
|
113
|
+
unknowns.size === 0;
|
|
114
|
+
return {
|
|
115
|
+
callCount,
|
|
116
|
+
knownInputTokens,
|
|
117
|
+
knownOutputTokens,
|
|
118
|
+
unknownTokenCalls,
|
|
119
|
+
...(allDollarCallsPriced && pricedCalls > 0 ? { maximumCostUsd } : {}),
|
|
120
|
+
knownPricedMaximumUsd: maximumCostUsd,
|
|
121
|
+
unpricedCalls,
|
|
122
|
+
subscriptionUnitCalls,
|
|
123
|
+
upstreamManagedCalls,
|
|
124
|
+
unknownBillingCalls,
|
|
125
|
+
unknowns: [...unknowns].sort()
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
export const attributeModelCosts = (calls, overrides = {}) => {
|
|
129
|
+
let knownInputTokens = 0;
|
|
130
|
+
let knownOutputTokens = 0;
|
|
131
|
+
let unknownTokenCalls = 0;
|
|
132
|
+
let knownCostUsd = 0;
|
|
133
|
+
let pricedCalls = 0;
|
|
134
|
+
let unpricedCalls = 0;
|
|
135
|
+
let subscriptionUnitCalls = 0;
|
|
136
|
+
let upstreamManagedCalls = 0;
|
|
137
|
+
let unknownBillingCalls = 0;
|
|
138
|
+
const unknowns = new Set();
|
|
139
|
+
for (const call of calls) {
|
|
140
|
+
if (!validTokenCount(call.usage?.inputTokens) ||
|
|
141
|
+
!validTokenCount(call.usage?.outputTokens) ||
|
|
142
|
+
(call.providerCostUsd !== undefined &&
|
|
143
|
+
(!Number.isFinite(call.providerCostUsd) || call.providerCostUsd < 0))) {
|
|
144
|
+
throw new Error("cost actuals require non-negative finite usage and cost values");
|
|
145
|
+
}
|
|
146
|
+
const pricing = lookupPricing(call.model, overrides);
|
|
147
|
+
const billingMode = billingModeOf(call.model, call.billingMode, pricing);
|
|
148
|
+
if (call.usage?.inputTokens === undefined || call.usage.outputTokens === undefined) {
|
|
149
|
+
unknownTokenCalls += 1;
|
|
150
|
+
addUnknown(unknowns, `observed token usage for ${call.model}`);
|
|
151
|
+
}
|
|
152
|
+
if (call.usage?.inputTokens !== undefined) {
|
|
153
|
+
knownInputTokens += call.usage.inputTokens;
|
|
154
|
+
}
|
|
155
|
+
if (call.usage?.outputTokens !== undefined) {
|
|
156
|
+
knownOutputTokens += call.usage.outputTokens;
|
|
157
|
+
}
|
|
158
|
+
if (call.providerCostUsd !== undefined) {
|
|
159
|
+
pricedCalls += 1;
|
|
160
|
+
knownCostUsd += call.providerCostUsd;
|
|
161
|
+
if (billingMode === "subscription")
|
|
162
|
+
subscriptionUnitCalls += 1;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (billingMode === "subscription") {
|
|
166
|
+
subscriptionUnitCalls += 1;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (billingMode === "upstream-managed") {
|
|
170
|
+
upstreamManagedCalls += 1;
|
|
171
|
+
addUnknown(unknowns, `upstream-managed billing for ${call.model}`);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (billingMode === "unknown") {
|
|
175
|
+
unknownBillingCalls += 1;
|
|
176
|
+
addUnknown(unknowns, `billing mode for ${call.model}`);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const cost = call.usage?.inputTokens === undefined ||
|
|
180
|
+
call.usage.outputTokens === undefined ||
|
|
181
|
+
pricing === undefined
|
|
182
|
+
? undefined
|
|
183
|
+
: dollars({
|
|
184
|
+
inputTokens: call.usage.inputTokens,
|
|
185
|
+
outputTokens: call.usage.outputTokens
|
|
186
|
+
}, pricing);
|
|
187
|
+
if (cost === undefined) {
|
|
188
|
+
unpricedCalls += 1;
|
|
189
|
+
addUnknown(unknowns, `actual cost for ${call.model}`);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
pricedCalls += 1;
|
|
193
|
+
knownCostUsd += cost;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const allDollarCallsPriced = unpricedCalls === 0 &&
|
|
197
|
+
upstreamManagedCalls === 0 &&
|
|
198
|
+
unknownBillingCalls === 0;
|
|
199
|
+
return {
|
|
200
|
+
callCount: calls.length,
|
|
201
|
+
knownInputTokens,
|
|
202
|
+
knownOutputTokens,
|
|
203
|
+
unknownTokenCalls,
|
|
204
|
+
...(allDollarCallsPriced && pricedCalls > 0 ? { knownCostUsd } : {}),
|
|
205
|
+
knownPricedSubtotalUsd: knownCostUsd,
|
|
206
|
+
unpricedCalls,
|
|
207
|
+
subscriptionUnitCalls,
|
|
208
|
+
upstreamManagedCalls,
|
|
209
|
+
unknownBillingCalls,
|
|
210
|
+
unknowns: [...unknowns].sort()
|
|
211
|
+
};
|
|
212
|
+
};
|
|
213
|
+
export const makeCostService = (overrides = {}) => {
|
|
214
|
+
return CostService.of({
|
|
215
|
+
lookupPricing: (model, configuredOverrides) => Effect.succeed(lookupPricing(model, { ...overrides, ...configuredOverrides })),
|
|
216
|
+
estimate: (calls) => Effect.sync(() => estimateModelCosts(calls, overrides)),
|
|
217
|
+
actuals: (calls) => Effect.sync(() => attributeModelCosts(calls, overrides))
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
export const CostServiceLive = Layer.effect(CostService, Effect.sync(() => makeCostService()));
|
|
221
|
+
export const lookupModelPricing = (model, overrides = {}) => lookupPricing(model, overrides);
|
|
222
|
+
export const formatCostUsd = (amount, currency = DEFAULT_CURRENCY) => {
|
|
223
|
+
const digits = amount >= 1 ? 2 : 4;
|
|
224
|
+
return `${currency === "USD" ? "$" : ""}${amount.toFixed(digits)}${currency === "USD" ? "" : ` ${currency}`}`;
|
|
225
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { test } from "node:test";
|
|
4
|
-
import { ACCOUNT_CONNECTORS, DEFAULT_MODEL_PRICING, GATEWAY_DEFAULT_MLX_MODEL, LOCAL_CATALOG_ENTRIES, LOCAL_PROBE_MODEL, PREFERRED_LOCAL_MODELS, REGISTRY, accountKindChoices, accountKindForCliproxyAuthType, providerDiscovery, resolveAccountConnector } from "../index.js";
|
|
4
|
+
import { ACCOUNT_CONNECTORS, DEFAULT_MODEL_PRICING, GATEWAY_DEFAULT_MLX_MODEL, LOCAL_CATALOG_ENTRIES, LOCAL_PROBE_MODEL, PREFERRED_LOCAL_MODELS, REGISTRY, accountKindChoices, accountKindForCliproxyAuthType, attributeModelCosts, estimateModelCosts, lookupModelPricing, providerDiscovery, resolveAccountConnector } from "../index.js";
|
|
5
5
|
const FORBIDDEN_PRODUCT = ["fu", "sion", "kit"].join("");
|
|
6
6
|
test("neutral generated registry excludes product and panel data", () => {
|
|
7
7
|
assert.equal("fusion" in REGISTRY, false);
|
|
@@ -38,6 +38,120 @@ test("neutral pricing and provider metadata remain available", () => {
|
|
|
38
38
|
assert.equal(REGISTRY.providers.openrouter.discovery.extraHeaders["X-Title"], "RouteKit");
|
|
39
39
|
assert.equal(REGISTRY.subscriptions.codex.defaultHeaders.originator, "routekit");
|
|
40
40
|
});
|
|
41
|
+
test("cost service centralizes lookup, estimates, and observed actuals", () => {
|
|
42
|
+
assert.deepEqual(lookupModelPricing("openai/gpt-5.5"), {
|
|
43
|
+
inputPer1mTokens: 1.25,
|
|
44
|
+
outputPer1mTokens: 10
|
|
45
|
+
});
|
|
46
|
+
const estimate = estimateModelCosts([
|
|
47
|
+
{
|
|
48
|
+
model: "openai/gpt-5.5",
|
|
49
|
+
callCount: 2,
|
|
50
|
+
maximumInputTokens: 1_000,
|
|
51
|
+
maximumOutputTokens: 200,
|
|
52
|
+
reasoningEffort: "high"
|
|
53
|
+
}
|
|
54
|
+
]);
|
|
55
|
+
assert.equal(estimate.callCount, 2);
|
|
56
|
+
assert.equal(estimate.knownInputTokens, 2_000);
|
|
57
|
+
assert.equal(estimate.knownOutputTokens, 400);
|
|
58
|
+
assert.equal(estimate.maximumCostUsd, undefined);
|
|
59
|
+
assert.equal(estimate.knownPricedMaximumUsd, 0.0065);
|
|
60
|
+
assert.deepEqual(estimate.unknowns, ["reasoning-token multiplier for openai/gpt-5.5:high"]);
|
|
61
|
+
const actuals = attributeModelCosts([
|
|
62
|
+
{
|
|
63
|
+
model: "openai/gpt-5.5",
|
|
64
|
+
usage: { inputTokens: 1_000, outputTokens: 200 }
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
model: "codex/gpt-5.5-codex",
|
|
68
|
+
billingMode: "subscription",
|
|
69
|
+
usage: { inputTokens: 500, outputTokens: 100 }
|
|
70
|
+
}
|
|
71
|
+
]);
|
|
72
|
+
assert.equal(actuals.callCount, 2);
|
|
73
|
+
assert.equal(actuals.knownInputTokens, 1_500);
|
|
74
|
+
assert.equal(actuals.knownOutputTokens, 300);
|
|
75
|
+
assert.equal(actuals.knownCostUsd, 0.00325);
|
|
76
|
+
assert.equal(actuals.knownPricedSubtotalUsd, 0.00325);
|
|
77
|
+
assert.equal(actuals.subscriptionUnitCalls, 1);
|
|
78
|
+
});
|
|
79
|
+
test("cost estimates scale with N and the candidate set", () => {
|
|
80
|
+
const once = estimateModelCosts([
|
|
81
|
+
{
|
|
82
|
+
model: "openai/gpt-5.5",
|
|
83
|
+
callCount: 1,
|
|
84
|
+
maximumInputTokens: 1_000,
|
|
85
|
+
maximumOutputTokens: 200
|
|
86
|
+
}
|
|
87
|
+
]);
|
|
88
|
+
const twice = estimateModelCosts([
|
|
89
|
+
{
|
|
90
|
+
model: "openai/gpt-5.5",
|
|
91
|
+
callCount: 2,
|
|
92
|
+
maximumInputTokens: 1_000,
|
|
93
|
+
maximumOutputTokens: 200
|
|
94
|
+
}
|
|
95
|
+
]);
|
|
96
|
+
const twoCandidates = estimateModelCosts([
|
|
97
|
+
{
|
|
98
|
+
model: "openai/gpt-5.5",
|
|
99
|
+
callCount: 1,
|
|
100
|
+
maximumInputTokens: 1_000,
|
|
101
|
+
maximumOutputTokens: 200
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
model: "openai/gpt-4.1",
|
|
105
|
+
callCount: 1,
|
|
106
|
+
maximumInputTokens: 1_000,
|
|
107
|
+
maximumOutputTokens: 200
|
|
108
|
+
}
|
|
109
|
+
]);
|
|
110
|
+
assert.equal(twice.callCount, once.callCount * 2);
|
|
111
|
+
assert.equal(twice.knownPricedMaximumUsd, once.knownPricedMaximumUsd * 2);
|
|
112
|
+
assert.equal(twoCandidates.callCount, once.callCount + 1);
|
|
113
|
+
assert.ok(twoCandidates.knownPricedMaximumUsd > once.knownPricedMaximumUsd);
|
|
114
|
+
});
|
|
115
|
+
test("cost service preserves explicit unknowns instead of inventing zero", () => {
|
|
116
|
+
const estimate = estimateModelCosts([
|
|
117
|
+
{
|
|
118
|
+
model: "openai/unlisted",
|
|
119
|
+
callCount: 3,
|
|
120
|
+
maximumInputTokens: 100,
|
|
121
|
+
maximumOutputTokens: 50
|
|
122
|
+
}
|
|
123
|
+
]);
|
|
124
|
+
assert.equal(estimate.maximumCostUsd, undefined);
|
|
125
|
+
assert.equal(estimate.unpricedCalls, 3);
|
|
126
|
+
assert.deepEqual(estimate.unknowns, ["pricing for openai/unlisted"]);
|
|
127
|
+
const actuals = attributeModelCosts([{ model: "openai/unlisted" }]);
|
|
128
|
+
assert.equal(actuals.knownCostUsd, undefined);
|
|
129
|
+
assert.equal(actuals.unpricedCalls, 1);
|
|
130
|
+
assert.equal(actuals.unknownTokenCalls, 1);
|
|
131
|
+
});
|
|
132
|
+
test("cost actuals retain partial usage and provider-reported subscription cost", () => {
|
|
133
|
+
const partial = attributeModelCosts([
|
|
134
|
+
{
|
|
135
|
+
model: "openai/gpt-5.5",
|
|
136
|
+
usage: { inputTokens: 123 }
|
|
137
|
+
}
|
|
138
|
+
]);
|
|
139
|
+
assert.equal(partial.knownInputTokens, 123);
|
|
140
|
+
assert.equal(partial.knownOutputTokens, 0);
|
|
141
|
+
assert.equal(partial.unknownTokenCalls, 1);
|
|
142
|
+
assert.equal(partial.knownCostUsd, undefined);
|
|
143
|
+
const providerReported = attributeModelCosts([
|
|
144
|
+
{
|
|
145
|
+
model: "codex/gpt-5.5-codex",
|
|
146
|
+
billingMode: "subscription",
|
|
147
|
+
usage: { inputTokens: 50, outputTokens: 10 },
|
|
148
|
+
providerCostUsd: 0.002
|
|
149
|
+
}
|
|
150
|
+
]);
|
|
151
|
+
assert.equal(providerReported.knownCostUsd, 0.002);
|
|
152
|
+
assert.equal(providerReported.knownPricedSubtotalUsd, 0.002);
|
|
153
|
+
assert.equal(providerReported.subscriptionUnitCalls, 1);
|
|
154
|
+
});
|
|
41
155
|
test("account connector map resolves canonical kinds, aliases, and auth types", () => {
|
|
42
156
|
// Native subscription kinds stay native; every native kind has full
|
|
43
157
|
// subscription metadata in the subscriptions section.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velum-labs/routekit-registry",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.4.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/velum-labs/routekit.git",
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"models"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"
|
|
36
|
+
"effect": "4.0.0-rc.108",
|
|
37
|
+
"@velum-labs/routekit-contracts": "1.4.0"
|
|
37
38
|
},
|
|
38
39
|
"scripts": {
|
|
39
40
|
"build": "tsc -b",
|