@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 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.
@@ -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;
@@ -201,8 +201,17 @@ export const REGISTRY = {
201
201
  }
202
202
  },
203
203
  "oauth": {
204
- "tokenEndpoint": "https://console.anthropic.com/v1/oauth/token",
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
- export { REGISTRY };
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 RegistryModelPricing = {
135
- inputPer1mTokens: number;
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
- export { REGISTRY };
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 const PRICING_ALIASES = (REGISTRY.pricing.aliases ?? {});
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>>;
@@ -0,0 +1,6 @@
1
+ import { REGISTRY } from "./generated/data.js";
2
+ export const PRICING_ALIASES = (REGISTRY.pricing.aliases ?? {});
3
+ export const DEFAULT_MODEL_PRICING = {
4
+ ...REGISTRY.pricing.models,
5
+ ...REGISTRY.pricing.manualOverrides
6
+ };
@@ -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.3.2",
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
- "@velum-labs/routekit-contracts": "1.3.2"
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",