tokensniff 0.1.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.
@@ -0,0 +1,145 @@
1
+ // src/shared/pricing.ts
2
+ var rateCache = /* @__PURE__ */ new Map();
3
+ var catalogPromise = null;
4
+ function normalizeModelName(raw) {
5
+ if (typeof raw !== "string" || raw.trim().length === 0) {
6
+ return "unknown";
7
+ }
8
+ const trimmed = raw.trim().toLowerCase();
9
+ const withoutNamespace = trimmed.replace(/^~?[a-zA-Z0-9_-]+\//, "");
10
+ return withoutNamespace.replace(/:[a-zA-Z0-9_-]+$/, "").replace(/-(?:tiered|thinking|preview|low|high|latest)$/, "").replace(/-(?:\d{4}-\d{2}-\d{2}|\d{8}|\d{4})$/, "").replace(/\b(\d+)-(\d+)\b/g, "$1.$2");
11
+ }
12
+ function toSafeRate(value, fallback = 0) {
13
+ if (!value) return fallback;
14
+ const parsed = Number.parseFloat(value);
15
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
16
+ }
17
+ async function syncOpenRouterCatalog() {
18
+ if (catalogPromise) {
19
+ return catalogPromise;
20
+ }
21
+ catalogPromise = (async () => {
22
+ try {
23
+ const response = await fetch("https://openrouter.ai/api/v1/models", {
24
+ signal: AbortSignal.timeout(5e3),
25
+ headers: { Accept: "application/json" }
26
+ });
27
+ if (!response.ok) {
28
+ return false;
29
+ }
30
+ const json = await response.json();
31
+ if (!Array.isArray(json?.data)) {
32
+ return false;
33
+ }
34
+ for (const item of json.data) {
35
+ if (!item?.id || !item.pricing) continue;
36
+ const promptRate = toSafeRate(item.pricing.prompt);
37
+ const completionRate = toSafeRate(item.pricing.completion);
38
+ const cacheReadRate = toSafeRate(
39
+ item.pricing.input_cache_read,
40
+ promptRate > 0 ? promptRate * 0.1 : 0
41
+ );
42
+ const cacheWriteRate = toSafeRate(item.pricing.input_cache_write, promptRate);
43
+ const rawWindow = item.context_length;
44
+ const contextWindow = typeof rawWindow === "number" && Number.isFinite(rawWindow) && rawWindow > 0 ? rawWindow : 128e3;
45
+ const resolvedRates = {
46
+ in: promptRate,
47
+ out: completionRate,
48
+ cached: cacheReadRate,
49
+ cacheWrite: cacheWriteRate,
50
+ window: contextWindow
51
+ };
52
+ const fullId = item.id.toLowerCase();
53
+ const bareSlug = item.id.includes("/") ? item.id.split("/")[1]?.toLowerCase() ?? fullId : fullId;
54
+ const cleanSlug = normalizeModelName(item.id);
55
+ rateCache.set(fullId, resolvedRates);
56
+ rateCache.set(bareSlug, resolvedRates);
57
+ rateCache.set(cleanSlug, resolvedRates);
58
+ }
59
+ return true;
60
+ } catch {
61
+ return false;
62
+ } finally {
63
+ catalogPromise = null;
64
+ }
65
+ })();
66
+ return catalogPromise;
67
+ }
68
+ async function resolveModelRates(model, overrides = {}) {
69
+ const clean = normalizeModelName(model);
70
+ const rawKey = model.trim().toLowerCase();
71
+ const matchedOverride = overrides[rawKey] ?? overrides[clean];
72
+ if (matchedOverride) {
73
+ const existing = rateCache.get(clean) ?? rateCache.get(rawKey);
74
+ const resolvedOverride = {
75
+ in: matchedOverride.in ?? existing?.in ?? 0,
76
+ out: matchedOverride.out ?? existing?.out ?? 0,
77
+ cached: matchedOverride.cached ?? existing?.cached ?? 0,
78
+ cacheWrite: matchedOverride.in ?? existing?.cacheWrite ?? 0,
79
+ window: matchedOverride.window ?? existing?.window ?? 128e3
80
+ };
81
+ rateCache.set(clean, resolvedOverride);
82
+ rateCache.set(rawKey, resolvedOverride);
83
+ return resolvedOverride;
84
+ }
85
+ const cached = rateCache.get(clean) ?? rateCache.get(rawKey);
86
+ if (cached) {
87
+ return cached;
88
+ }
89
+ await syncOpenRouterCatalog();
90
+ const postSync = rateCache.get(clean) ?? rateCache.get(rawKey);
91
+ if (postSync) {
92
+ return postSync;
93
+ }
94
+ const candidateKeys = Array.from(rateCache.keys()).sort((a, b) => b.length - a.length);
95
+ for (const key of candidateKeys) {
96
+ if (key.length > 3 && (clean.includes(key) || key.includes(clean))) {
97
+ const rates = rateCache.get(key);
98
+ if (rates) {
99
+ rateCache.set(clean, rates);
100
+ return rates;
101
+ }
102
+ }
103
+ }
104
+ const defaultBoundary = {
105
+ in: 0,
106
+ out: 0,
107
+ cached: 0,
108
+ cacheWrite: 0,
109
+ window: 128e3
110
+ };
111
+ rateCache.set(clean, defaultBoundary);
112
+ return defaultBoundary;
113
+ }
114
+ function ratesFor(model) {
115
+ const clean = normalizeModelName(model);
116
+ const rawKey = model.trim().toLowerCase();
117
+ const hit = rateCache.get(clean) ?? rateCache.get(rawKey);
118
+ if (hit) {
119
+ return hit;
120
+ }
121
+ void syncOpenRouterCatalog();
122
+ return {
123
+ in: 0,
124
+ out: 0,
125
+ cached: 0,
126
+ cacheWrite: 0,
127
+ window: 128e3
128
+ };
129
+ }
130
+ function costFor(rates, usage) {
131
+ const inputTokens = Math.max(0, usage.input ?? 0);
132
+ const outputTokens = Math.max(0, usage.output ?? 0);
133
+ const cacheReadTokens = Math.max(0, usage.cacheRead ?? 0);
134
+ const cacheCreateTokens = Math.max(0, usage.cacheCreate ?? 0);
135
+ const cacheWriteRate = rates.cacheWrite > 0 ? rates.cacheWrite : rates.in;
136
+ const total = inputTokens * rates.in + outputTokens * rates.out + cacheReadTokens * rates.cached + cacheCreateTokens * cacheWriteRate;
137
+ if (!Number.isFinite(total) || total < 0) {
138
+ return 0;
139
+ }
140
+ return Math.round(total * 1e8) / 1e8;
141
+ }
142
+
143
+ export { costFor, normalizeModelName, ratesFor, resolveModelRates, syncOpenRouterCatalog };
144
+ //# sourceMappingURL=pricing.js.map
145
+ //# sourceMappingURL=pricing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared/pricing.ts"],"names":[],"mappings":"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","file":"pricing.js","sourcesContent":["/**\n * tokensniff — Dynamic Universal Pricing & Context Capacity Engine (v1 Production)\n *\n * Provider-agnostic rate resolver mapping 1:1 to OpenRouter's live catalog schema.\n * Dynamically resolves token rates (prompt, completion, cache-read, cache-write)\n * and maximum context length (context_length) without vendor hardcoding or dummy fallbacks.\n */\n\nexport interface ModelRates {\n readonly in: number; // Cost per prompt token in USD\n readonly out: number; // Cost per completion token in USD\n readonly cached: number; // Cost per cache-read token in USD\n readonly cacheWrite: number; // Cost per cache-write token in USD\n readonly window: number; // Maximum context length in tokens\n}\n\nexport interface ModelPricingOverrides {\n readonly [model: string]: Partial<ModelRates>;\n}\n\ninterface OpenRouterPricingPayload {\n readonly prompt?: string;\n readonly completion?: string;\n readonly input_cache_read?: string;\n readonly input_cache_write?: string;\n}\n\ninterface OpenRouterModelItem {\n readonly id?: string;\n readonly context_length?: number;\n readonly pricing?: OpenRouterPricingPayload;\n}\n\n/**\n * In-memory index mapping model identifiers and clean slugs to ModelRates.\n * Guarantees O(1) latency on all subsequent turns after initial resolution.\n */\nconst rateCache = new Map<string, ModelRates>();\n\n/**\n * Concurrency guard preventing multiple redundant requests to OpenRouter.\n */\nlet catalogPromise: Promise<boolean> | null = null;\n\n/**\n * Normalizes vendor model names into canonical matching slugs.\n * Provider-agnostic: does not hardcode vendor names.\n *\n * Normalization rules:\n * 1. Strips provider namespaces (e.g. \"anthropic/...\", \"google/...\", \"~anthropic/...\")\n * 2. Strips variant flags (e.g. \":free\", \":batch\")\n * 3. Strips runtime proxy & mode suffixes (e.g. \"-tiered\", \"-thinking\", \"-preview\", \"-low\", \"-high\", \"-latest\")\n * 4. Strips calendar release dates (e.g. \"-20241022\", \"-2024-08-06\", \"-2407\")\n * 5. Standardizes numeric version separators (e.g. \"3-5\" -> \"3.5\", \"4-6\" -> \"4.6\")\n *\n * Examples:\n * - \"google/gemini-3.8-flash-tiered\" -> \"gemini-3.8-flash\"\n * - \"anthropic/claude-3-5-sonnet-20241022\" -> \"claude-3.5-sonnet\"\n * - \"claude-opus-4-6-thinking\" -> \"claude-opus-4.6\"\n * - \"meta-llama/llama-3.3-70b-instruct:free\" -> \"llama-3.3-70b-instruct\"\n */\nexport function normalizeModelName(raw: unknown): string {\n if (typeof raw !== 'string' || raw.trim().length === 0) {\n return 'unknown';\n }\n\n const trimmed = raw.trim().toLowerCase();\n\n // Strip provider namespace (anything before the first slash, including leading '~' aliases)\n const withoutNamespace = trimmed.replace(/^~?[a-zA-Z0-9_-]+\\//, '');\n\n // Strip variant flags (:free, :batch), proxy/mode tags, dates, and convert digit-dash-digit to dots\n return withoutNamespace\n .replace(/:[a-zA-Z0-9_-]+$/, '')\n .replace(/-(?:tiered|thinking|preview|low|high|latest)$/, '')\n .replace(/-(?:\\d{4}-\\d{2}-\\d{2}|\\d{8}|\\d{4})$/, '')\n .replace(/\\b(\\d+)-(\\d+)\\b/g, '$1.$2');\n}\n\n/**\n * Parses numeric rate strings from OpenRouter safely, guaranteeing non-negative finite numbers.\n */\nfunction toSafeRate(value: string | undefined, fallback = 0): number {\n if (!value) return fallback;\n const parsed = Number.parseFloat(value);\n return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;\n}\n\n/**\n * Fetches and indexes OpenRouter's live models catalog.\n * Uses native Node 24 AbortSignal.timeout to enforce a strict network boundary.\n */\nexport async function syncOpenRouterCatalog(): Promise<boolean> {\n if (catalogPromise) {\n return catalogPromise;\n }\n\n catalogPromise = (async (): Promise<boolean> => {\n try {\n const response = await fetch('https://openrouter.ai/api/v1/models', {\n signal: AbortSignal.timeout(5000),\n headers: { Accept: 'application/json' },\n });\n\n if (!response.ok) {\n return false;\n }\n\n const json = (await response.json()) as {\n readonly data?: readonly OpenRouterModelItem[];\n };\n\n if (!Array.isArray(json?.data)) {\n return false;\n }\n\n for (const item of json.data) {\n if (!item?.id || !item.pricing) continue;\n\n const promptRate = toSafeRate(item.pricing.prompt);\n const completionRate = toSafeRate(item.pricing.completion);\n const cacheReadRate = toSafeRate(\n item.pricing.input_cache_read,\n promptRate > 0 ? promptRate * 0.1 : 0,\n );\n const cacheWriteRate = toSafeRate(item.pricing.input_cache_write, promptRate);\n\n // Dynamically extract real maximum context length from OpenRouter\n const rawWindow = item.context_length;\n const contextWindow =\n typeof rawWindow === 'number' && Number.isFinite(rawWindow) && rawWindow > 0\n ? rawWindow\n : 128_000;\n\n const resolvedRates: ModelRates = {\n in: promptRate,\n out: completionRate,\n cached: cacheReadRate,\n cacheWrite: cacheWriteRate,\n window: contextWindow,\n };\n\n const fullId = item.id.toLowerCase();\n const bareSlug = item.id.includes('/')\n ? (item.id.split('/')[1]?.toLowerCase() ?? fullId)\n : fullId;\n const cleanSlug = normalizeModelName(item.id);\n\n // Multi-key indexing for instant O(1) matching\n rateCache.set(fullId, resolvedRates);\n rateCache.set(bareSlug, resolvedRates);\n rateCache.set(cleanSlug, resolvedRates);\n }\n\n return true;\n } catch {\n return false;\n } finally {\n catalogPromise = null;\n }\n })();\n\n return catalogPromise;\n}\n\n/**\n * Resolves rates and maximum context length dynamically for the active turn model.\n * Seamlessly handles mid-session model switches and user custom overrides.\n */\nexport async function resolveModelRates(\n model: string,\n overrides: ModelPricingOverrides = {},\n): Promise<ModelRates> {\n const clean = normalizeModelName(model);\n const rawKey = model.trim().toLowerCase();\n\n // 1. User config overrides always take highest priority\n const matchedOverride = overrides[rawKey] ?? overrides[clean];\n if (matchedOverride) {\n const existing = rateCache.get(clean) ?? rateCache.get(rawKey);\n const resolvedOverride: ModelRates = {\n in: matchedOverride.in ?? existing?.in ?? 0,\n out: matchedOverride.out ?? existing?.out ?? 0,\n cached: matchedOverride.cached ?? existing?.cached ?? 0,\n cacheWrite: matchedOverride.in ?? existing?.cacheWrite ?? 0,\n window: matchedOverride.window ?? existing?.window ?? 128_000,\n };\n rateCache.set(clean, resolvedOverride);\n rateCache.set(rawKey, resolvedOverride);\n return resolvedOverride;\n }\n\n // 2. Fast-path: Instant in-memory cache hit\n const cached = rateCache.get(clean) ?? rateCache.get(rawKey);\n if (cached) {\n return cached;\n }\n\n // 3. Cache-miss: Fetch OpenRouter catalog on-demand\n await syncOpenRouterCatalog();\n\n const postSync = rateCache.get(clean) ?? rateCache.get(rawKey);\n if (postSync) {\n return postSync;\n }\n\n // 4. Longest-match fallback across indexed models (prioritizes specific variants over shorter base slugs)\n const candidateKeys = Array.from(rateCache.keys()).sort((a, b) => b.length - a.length);\n for (const key of candidateKeys) {\n if (key.length > 3 && (clean.includes(key) || key.includes(clean))) {\n const rates = rateCache.get(key);\n if (rates) {\n rateCache.set(clean, rates);\n return rates;\n }\n }\n }\n\n // 5. Zero-rate boundary if completely unknown to OpenRouter\n const defaultBoundary: ModelRates = {\n in: 0,\n out: 0,\n cached: 0,\n cacheWrite: 0,\n window: 128_000,\n };\n rateCache.set(clean, defaultBoundary);\n return defaultBoundary;\n}\n\n/**\n * Synchronous cache lookup for instantaneous read during statusline rendering.\n */\nexport function ratesFor(model: string): ModelRates {\n const clean = normalizeModelName(model);\n const rawKey = model.trim().toLowerCase();\n\n const hit = rateCache.get(clean) ?? rateCache.get(rawKey);\n if (hit) {\n return hit;\n }\n\n // Kick off background sync for future turns if not yet cached\n void syncOpenRouterCatalog();\n\n return {\n in: 0,\n out: 0,\n cached: 0,\n cacheWrite: 0,\n window: 128_000,\n };\n}\n\n/**\n * Calculates exact monetary cost in USD for a turn interaction.\n * Supports prompt, completion, cache-read, and cache-write token accounting.\n */\nexport function costFor(\n rates: ModelRates,\n usage: {\n readonly input?: number;\n readonly output?: number;\n readonly cacheRead?: number;\n readonly cacheCreate?: number;\n },\n): number {\n const inputTokens = Math.max(0, usage.input ?? 0);\n const outputTokens = Math.max(0, usage.output ?? 0);\n const cacheReadTokens = Math.max(0, usage.cacheRead ?? 0);\n const cacheCreateTokens = Math.max(0, usage.cacheCreate ?? 0);\n\n const cacheWriteRate = rates.cacheWrite > 0 ? rates.cacheWrite : rates.in;\n\n const total =\n inputTokens * rates.in +\n outputTokens * rates.out +\n cacheReadTokens * rates.cached +\n cacheCreateTokens * cacheWriteRate;\n\n if (!Number.isFinite(total) || total < 0) {\n return 0;\n }\n\n // 8-decimal deterministic rounding prevents IEEE 754 floating-point drift across turns\n return Math.round(total * 100_000_000) / 100_000_000;\n}\n"]}