diff --git a/package.json b/package.json index 0274dc1..fc3a3f9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "9router-app", - "version": "0.3.73", + "version": "0.3.74", "description": "9Router web dashboard", "private": true, "scripts": { diff --git a/src/lib/localDb.js b/src/lib/localDb.js index 00983b6..aa81fb5 100644 --- a/src/lib/localDb.js +++ b/src/lib/localDb.js @@ -1080,93 +1080,65 @@ export async function getCloudUrl() { /** * Get pricing configuration - * Returns merged user pricing with defaults + * Returns merged: PROVIDER_PRICING defaults + user overrides */ export async function getPricing() { const db = await getDb(); const userPricing = db.data.pricing || {}; - // Import default pricing - const { getDefaultPricing } = await import("@/shared/constants/pricing.js"); - const defaultPricing = getDefaultPricing(); + const { PROVIDER_PRICING } = await import("@/shared/constants/pricing.js"); - // Merge user pricing with defaults - // User pricing overrides defaults for specific provider/model combinations - const mergedPricing = {}; + // Deep merge PROVIDER_PRICING + user overrides + const merged = {}; - for (const [provider, models] of Object.entries(defaultPricing)) { - mergedPricing[provider] = { ...models }; - - // Apply user overrides if they exist + for (const [provider, models] of Object.entries(PROVIDER_PRICING)) { + merged[provider] = { ...models }; if (userPricing[provider]) { for (const [model, pricing] of Object.entries(userPricing[provider])) { - if (mergedPricing[provider][model]) { - mergedPricing[provider][model] = { ...mergedPricing[provider][model], ...pricing }; - } else { - mergedPricing[provider][model] = pricing; - } + merged[provider][model] = merged[provider][model] + ? { ...merged[provider][model], ...pricing } + : pricing; } } } - // Add any user-only pricing entries + // User-only providers not in PROVIDER_PRICING for (const [provider, models] of Object.entries(userPricing)) { - if (!mergedPricing[provider]) { - mergedPricing[provider] = { ...models }; + if (!merged[provider]) { + merged[provider] = { ...models }; } else { for (const [model, pricing] of Object.entries(models)) { - if (!mergedPricing[provider][model]) { - mergedPricing[provider][model] = pricing; - } + if (!merged[provider][model]) merged[provider][model] = pricing; } } } - return mergedPricing; + return merged; } /** - * Get pricing for a specific provider and model + * Get pricing for a specific provider and model. + * Delegates to getPricingForModel in pricing.js which handles the full fallback chain: + * 1. PROVIDER_PRICING[provider][model] — provider-specific override + * 2. MODEL_PRICING[model] — canonical model price + * 3. PATTERN_PRICING — glob pattern match + * + * Also checks user DB overrides first. */ export async function getPricingForModel(provider, model) { - const pricing = await getPricing(); + if (!model) return null; - // Try direct lookup - if (pricing[provider]?.[model]) { - return pricing[provider][model]; + const db = await getDb(); + const userPricing = db.data.pricing || {}; + + // User override takes top priority + if (provider && userPricing[provider]?.[model]) { + return userPricing[provider][model]; } - // Try mapping provider ID to alias - // We need to duplicate the mapping here or import it - // Since we can't easily import from open-sse, we'll implement the mapping locally - const PROVIDER_ID_TO_ALIAS = { - claude: "cc", - codex: "cx", - "gemini-cli": "gc", - qwen: "qw", - iflow: "if", - antigravity: "ag", - github: "gh", - kiro: "kr", - openai: "openai", - anthropic: "anthropic", - gemini: "gemini", - openrouter: "openrouter", - glm: "glm", - kimi: "kimi", - minimax: "minimax", - }; - - const alias = PROVIDER_ID_TO_ALIAS[provider]; - if (alias && pricing[alias]) { - return pricing[alias][model] || null; - } - - // Fallback: strip vendor prefix (e.g. "deepseek/deepseek-chat" → "deepseek-chat") - // then lookup in MODEL_PRICING (provider-agnostic explicit map) - const { MODEL_PRICING } = await import("@/shared/constants/pricing.js"); - const baseModel = model.includes("/") ? model.split("/").pop() : model; - return MODEL_PRICING[baseModel] || MODEL_PRICING[model] || null; + // Delegate to constants fallback chain + const { getPricingForModel: resolve } = await import("@/shared/constants/pricing.js"); + return resolve(provider, model); } /** diff --git a/src/lib/usageDb.js b/src/lib/usageDb.js index c3a6a3c..f6d24bc 100644 --- a/src/lib/usageDb.js +++ b/src/lib/usageDb.js @@ -92,6 +92,12 @@ if (!global._statsEmitter) { } export const statsEmitter = global._statsEmitter; +// Safety timers — force-clear pending counts after 1 min if END was never called +if (!global._pendingTimers) global._pendingTimers = {}; +const pendingTimers = global._pendingTimers; + +const PENDING_TIMEOUT_MS = 60 * 1000; // 1 minute + /** * Track a pending request * @param {string} model @@ -102,6 +108,7 @@ export const statsEmitter = global._statsEmitter; */ export function trackPendingRequest(model, provider, connectionId, started, error = false) { const modelKey = provider ? `${model} (${provider})` : model; + const timerKey = `${connectionId}|${modelKey}`; // Track by model if (!pendingRequests.byModel[modelKey]) pendingRequests.byModel[modelKey] = 0; @@ -109,10 +116,28 @@ export function trackPendingRequest(model, provider, connectionId, started, erro // Track by account if (connectionId) { - const accountKey = connectionId; - if (!pendingRequests.byAccount[accountKey]) pendingRequests.byAccount[accountKey] = {}; - if (!pendingRequests.byAccount[accountKey][modelKey]) pendingRequests.byAccount[accountKey][modelKey] = 0; - pendingRequests.byAccount[accountKey][modelKey] = Math.max(0, pendingRequests.byAccount[accountKey][modelKey] + (started ? 1 : -1)); + if (!pendingRequests.byAccount[connectionId]) pendingRequests.byAccount[connectionId] = {}; + if (!pendingRequests.byAccount[connectionId][modelKey]) pendingRequests.byAccount[connectionId][modelKey] = 0; + pendingRequests.byAccount[connectionId][modelKey] = Math.max(0, pendingRequests.byAccount[connectionId][modelKey] + (started ? 1 : -1)); + } + + if (started) { + // Safety timeout: force-clear if END is never called (client disconnect, crash, etc.) + clearTimeout(pendingTimers[timerKey]); + pendingTimers[timerKey] = setTimeout(() => { + delete pendingTimers[timerKey]; + if (pendingRequests.byModel[modelKey] > 0) { + pendingRequests.byModel[modelKey] = 0; + } + if (connectionId && pendingRequests.byAccount[connectionId]?.[modelKey] > 0) { + pendingRequests.byAccount[connectionId][modelKey] = 0; + } + statsEmitter.emit("pending"); + }, PENDING_TIMEOUT_MS); + } else { + // END called normally — cancel the safety timer + clearTimeout(pendingTimers[timerKey]); + delete pendingTimers[timerKey]; } // Track error provider (auto-clears after 10s) diff --git a/src/shared/components/NineRemoteButton.js b/src/shared/components/NineRemoteButton.js index 8c2d1af..c71ed8e 100644 --- a/src/shared/components/NineRemoteButton.js +++ b/src/shared/components/NineRemoteButton.js @@ -1,104 +1,23 @@ "use client"; -import { useState, useEffect, useRef } from "react"; -import NineRemoteModal from "./NineRemoteModal"; - -// step 0-4 từ 9remote SSE: 0=Stopped,1=Preparing,2=Connecting,3=Tunneling,4=Ready -const STEP = { STOPPED: 0, PREPARING: 1, CONNECTING: 2, TUNNELING: 3, READY: 4 }; - -// Retry interval khi 9remote chưa chạy (30s để không spam) -const RETRY_MS = 30000; - -const stepStyle = (step, installed) => { - if (!installed) return { color: "text-text-muted hover:text-text-main hover:bg-black/5 dark:hover:bg-white/5", glow: null }; - switch (step) { - case STEP.READY: return { color: "text-emerald-500 hover:bg-emerald-500/10", glow: "drop-shadow(0 0 6px rgb(16 185 129 / 0.7))" }; - case STEP.STOPPED: return { color: "text-text-muted hover:text-text-main hover:bg-black/5 dark:hover:bg-white/5", glow: null }; - default: return { color: "text-amber-400 hover:bg-amber-400/10", glow: null }; - } -}; +import { useState } from "react"; +import NineRemotePromoModal from "./NineRemotePromoModal"; export default function NineRemoteButton() { const [isOpen, setIsOpen] = useState(false); - const [installed, setInstalled] = useState(false); - const [step, setStep] = useState(STEP.STOPPED); - const esRef = useRef(null); - const retryRef = useRef(null); - - const scheduleRetry = () => { - clearTimeout(retryRef.current); - retryRef.current = setTimeout(connect, RETRY_MS); - }; - - const connect = () => { - esRef.current?.close(); - clearTimeout(retryRef.current); - - const es = new EventSource("http://localhost:2208/api/ui/events"); - - es.onmessage = (e) => { - try { - const data = JSON.parse(e.data); - if (data.type === "state") { - setInstalled(true); - setStep(data.step ?? STEP.STOPPED); - } - } catch {} - }; - - es.onerror = () => { - es.close(); - setStep(STEP.STOPPED); - // 9remote not running — retry after delay - scheduleRetry(); - }; - - esRef.current = es; - }; - - useEffect(() => { - // Check installed once on mount (no polling, just 1 call) - fetch("/api/9remote/status") - .then((r) => r.json()) - .then((d) => setInstalled(d.installed)) - .catch(() => {}); - - connect(); - - return () => { - esRef.current?.close(); - clearTimeout(retryRef.current); - }; - }, []); - - // When modal closes, reconnect SSE immediately (user may have just started 9remote) - const handleClose = () => { - setIsOpen(false); - setTimeout(connect, 800); - }; - - const { color, glow } = stepStyle(step, installed); - const isPulsing = installed && step >= STEP.PREPARING && step <= STEP.TUNNELING; return ( <> - setInstalled(true)} - /> + setIsOpen(false)} /> ); } diff --git a/src/shared/components/NineRemotePromoModal.js b/src/shared/components/NineRemotePromoModal.js new file mode 100644 index 0000000..7fc4cb8 --- /dev/null +++ b/src/shared/components/NineRemotePromoModal.js @@ -0,0 +1,103 @@ +"use client"; + +import { useEffect } from "react"; +import { createPortal } from "react-dom"; + +const FEATURES = [ + { icon: "terminal", label: "Terminal", desc: "Full shell access" }, + { icon: "cast", label: "Desktop", desc: "Screen sharing" }, + { icon: "folder_open", label: "Files", desc: "Browse & edit files" }, +]; + +const BULLETS = [ + { icon: "qr_code_scanner", text: "Scan QR to connect instantly" }, + { icon: "wifi_off", text: "No port forwarding needed" }, + { icon: "devices", text: "Works on any device" }, +]; + +const NINE_REMOTE_URL = "https://9remote.cc"; + +export default function NineRemotePromoModal({ isOpen, onClose }) { + useEffect(() => { + if (!isOpen) return; + document.body.style.overflow = "hidden"; + const onEsc = (e) => { if (e.key === "Escape") onClose(); }; + document.addEventListener("keydown", onEsc); + return () => { document.body.style.overflow = ""; document.removeEventListener("keydown", onEsc); }; + }, [isOpen, onClose]); + + if (!isOpen) return null; + + return createPortal( +
+
+ +
+ {/* Header */} +
+
+
+ terminal +
+ 9Remote +
+ +
+ + {/* Body */} +
+ {/* Hero */} +
+
+ terminal +
+

9Remote

+

+ Access your terminal, desktop & files from anywhere +

+
+ + {/* Feature cards */} +
+ {FEATURES.map(({ icon, label, desc }) => ( +
+ {icon} +

{label}

+

{desc}

+
+ ))} +
+ + {/* Bullets */} +
+ {BULLETS.map(({ icon, text }) => ( +
+ {icon} + {text} +
+ ))} +
+ + {/* CTA */} + +
+
+
, + document.body + ); +} diff --git a/src/shared/constants/pricing.js b/src/shared/constants/pricing.js index f96038e..8df1bf6 100644 --- a/src/shared/constants/pricing.js +++ b/src/shared/constants/pricing.js @@ -1,829 +1,257 @@ -// Default pricing rates for AI models -// All rates are in dollars per million tokens ($/1M tokens) -// Based on user-provided pricing for Antigravity models and industry standards for others - -export const DEFAULT_PRICING = { - // OAuth Providers (using aliases) - - // Claude Code (cc) - cc: { - "claude-opus-4-6": { - input: 5.00, - output: 25.00, - cached: 0.50, - reasoning: 25.00, - cache_creation: 6.25 - }, - "claude-opus-4-5-20251101": { - input: 5.00, - output: 25.00, - cached: 0.50, - reasoning: 25.00, - cache_creation: 6.25 - }, - "claude-sonnet-4-6": { - input: 3.00, - output: 15.00, - cached: 0.30, - reasoning: 15.00, - cache_creation: 3.75 - }, - "claude-sonnet-4-5-20250929": { - input: 3.00, - output: 15.00, - cached: 0.30, - reasoning: 15.00, - cache_creation: 3.75 - }, - "claude-haiku-4-5-20251001": { - input: 1.00, - output: 5.00, - cached: 0.10, - reasoning: 5.00, - cache_creation: 1.25 - } - }, - - // OpenAI Codex (cx) - cx: { - "gpt-5.3-codex": { - input: 6.00, - output: 24.00, - cached: 3.00, - reasoning: 36.00, - cache_creation: 6.00 - }, - "gpt-5.3-codex-xhigh": { - input: 10.00, - output: 40.00, - cached: 5.00, - reasoning: 60.00, - cache_creation: 10.00 - }, - "gpt-5.3-codex-high": { - input: 8.00, - output: 32.00, - cached: 4.00, - reasoning: 48.00, - cache_creation: 8.00 - }, - "gpt-5.3-codex-low": { - input: 4.00, - output: 16.00, - cached: 2.00, - reasoning: 24.00, - cache_creation: 4.00 - }, - "gpt-5.3-codex-none": { - input: 3.00, - output: 12.00, - cached: 1.50, - reasoning: 18.00, - cache_creation: 3.00 - }, - "gpt-5.3-codex-spark": { - input: 3.00, - output: 12.00, - cached: 0.30, - reasoning: 12.00, - cache_creation: 3.00 - }, - "gpt-5.2-codex": { - input: 5.00, - output: 20.00, - cached: 2.50, - reasoning: 30.00, - cache_creation: 5.00 - }, - "gpt-5.2": { - input: 5.00, - output: 20.00, - cached: 2.50, - reasoning: 30.00, - cache_creation: 5.00 - }, - "gpt-5.1-codex-max": { - input: 8.00, - output: 32.00, - cached: 4.00, - reasoning: 48.00, - cache_creation: 8.00 - }, - "gpt-5.1-codex": { - input: 4.00, - output: 16.00, - cached: 2.00, - reasoning: 24.00, - cache_creation: 4.00 - }, - "gpt-5.1-codex-mini": { - input: 1.50, - output: 6.00, - cached: 0.75, - reasoning: 9.00, - cache_creation: 1.50 - }, - "gpt-5.1-codex-mini-high": { - input: 2.00, - output: 8.00, - cached: 1.00, - reasoning: 12.00, - cache_creation: 2.00 - }, - "gpt-5.1": { - input: 4.00, - output: 16.00, - cached: 2.00, - reasoning: 24.00, - cache_creation: 4.00 - }, - "gpt-5-codex": { - input: 3.00, - output: 12.00, - cached: 1.50, - reasoning: 18.00, - cache_creation: 3.00 - }, - "gpt-5-codex-mini": { - input: 1.00, - output: 4.00, - cached: 0.50, - reasoning: 6.00, - cache_creation: 1.00 - } - }, - - // Gemini CLI (gc) - gc: { - "gemini-3-flash-preview": { - input: 0.50, - output: 3.00, - cached: 0.03, - reasoning: 4.50, - cache_creation: 0.50 - }, - "gemini-3-pro-preview": { - input: 2.00, - output: 12.00, - cached: 0.25, - reasoning: 18.00, - cache_creation: 2.00 - }, - "gemini-2.5-pro": { - input: 2.00, - output: 12.00, - cached: 0.25, - reasoning: 18.00, - cache_creation: 2.00 - }, - "gemini-2.5-flash": { - input: 0.30, - output: 2.50, - cached: 0.03, - reasoning: 3.75, - cache_creation: 0.30 - }, - "gemini-2.5-flash-lite": { - input: 0.15, - output: 1.25, - cached: 0.015, - reasoning: 1.875, - cache_creation: 0.15 - } - }, - - // Qwen Code (qw) - qw: { - "qwen3-coder-plus": { - input: 1.00, - output: 4.00, - cached: 0.50, - reasoning: 6.00, - cache_creation: 1.00 - }, - "qwen3-coder-flash": { - input: 0.50, - output: 2.00, - cached: 0.25, - reasoning: 3.00, - cache_creation: 0.50 - }, - "vision-model": { - input: 1.50, - output: 6.00, - cached: 0.75, - reasoning: 9.00, - cache_creation: 1.50 - }, - "coder-model": { - input: 1.50, - output: 6.00, - cached: 0.75, - reasoning: 9.00, - cache_creation: 1.50 - } - }, - - // iFlow AI (if) - if: { - "qwen3-coder-plus": { - input: 1.00, - output: 4.00, - cached: 0.50, - reasoning: 6.00, - cache_creation: 1.00 - }, - "kimi-k2": { - input: 1.00, - output: 4.00, - cached: 0.50, - reasoning: 6.00, - cache_creation: 1.00 - }, - "kimi-k2-thinking": { - input: 1.50, - output: 6.00, - cached: 0.75, - reasoning: 9.00, - cache_creation: 1.50 - }, - "kimi-k2.5": { - input: 1.20, - output: 4.80, - cached: 0.60, - reasoning: 7.20, - cache_creation: 1.20 - }, - "deepseek-r1": { - input: 0.75, - output: 3.00, - cached: 0.375, - reasoning: 4.50, - cache_creation: 0.75 - }, - "deepseek-v3.2-chat": { - input: 0.50, - output: 2.00, - cached: 0.25, - reasoning: 3.00, - cache_creation: 0.50 - }, - "deepseek-v3.2-reasoner": { - input: 0.75, - output: 3.00, - cached: 0.375, - reasoning: 4.50, - cache_creation: 0.75 - }, - "minimax-m2.1": { - input: 0.50, - output: 2.00, - cached: 0.25, - reasoning: 3.00, - cache_creation: 0.50 - }, - "minimax-m2.5": { - input: 0.60, - output: 2.40, - cached: 0.30, - reasoning: 3.60, - cache_creation: 0.60 - }, - "glm-4.6": { - input: 0.50, - output: 2.00, - cached: 0.25, - reasoning: 3.00, - cache_creation: 0.50 - }, - "glm-4.7": { - input: 0.75, - output: 3.00, - cached: 0.375, - reasoning: 4.50, - cache_creation: 0.75 - }, - "glm-5": { - input: 1.00, - output: 4.00, - cached: 0.50, - reasoning: 6.00, - cache_creation: 1.00 - } - }, - - // Antigravity (ag) - User-provided pricing - ag: { - "gemini-3.1-pro-low": { - input: 2.00, - output: 12.00, - cached: 0.25, - reasoning: 18.00, - cache_creation: 2.00 - }, - "gemini-3.1-pro-high": { - input: 4.00, - output: 18.00, - cached: 0.50, - reasoning: 27.00, - cache_creation: 4.00 - }, - "gemini-3-flash": { - input: 0.50, - output: 3.00, - cached: 0.03, - reasoning: 4.50, - cache_creation: 0.50 - }, - "gemini-2.5-flash": { - input: 0.30, - output: 2.50, - cached: 0.03, - reasoning: 3.75, - cache_creation: 0.30 - }, - "claude-sonnet-4-6": { - input: 3.00, - output: 15.00, - cached: 0.30, - reasoning: 22.50, - cache_creation: 3.00 - }, - "claude-opus-4-5-thinking": { - input: 5.00, - output: 25.00, - cached: 0.50, - reasoning: 37.50, - cache_creation: 5.00 - }, - "claude-opus-4-6-thinking": { - input: 5.00, - output: 25.00, - cached: 0.50, - reasoning: 37.50, - cache_creation: 5.00 - }, - "gpt-oss-120b-medium": { - input: 0.50, - output: 2.00, - cached: 0.25, - reasoning: 3.00, - cache_creation: 0.50 - } - }, - - // GitHub Copilot (gh) - gh: { - "gpt-3.5-turbo": { - input: 0.50, - output: 1.50, - cached: 0.25, - reasoning: 2.25, - cache_creation: 0.50 - }, - "gpt-4": { - input: 2.50, - output: 10.00, - cached: 1.25, - reasoning: 15.00, - cache_creation: 2.50 - }, - "gpt-4o": { - input: 2.50, - output: 10.00, - cached: 1.25, - reasoning: 15.00, - cache_creation: 2.50 - }, - "gpt-4o-mini": { - input: 0.15, - output: 0.60, - cached: 0.075, - reasoning: 0.90, - cache_creation: 0.15 - }, - "gpt-4.1": { - input: 2.50, - output: 10.00, - cached: 1.25, - reasoning: 15.00, - cache_creation: 2.50 - }, - "gpt-5": { - input: 3.00, - output: 12.00, - cached: 1.50, - reasoning: 18.00, - cache_creation: 3.00 - }, - "gpt-5-mini": { - input: 0.75, - output: 3.00, - cached: 0.375, - reasoning: 4.50, - cache_creation: 0.75 - }, - "gpt-5-codex": { - input: 3.00, - output: 12.00, - cached: 1.50, - reasoning: 18.00, - cache_creation: 3.00 - }, - "gpt-5.1": { - input: 4.00, - output: 16.00, - cached: 2.00, - reasoning: 24.00, - cache_creation: 4.00 - }, - "gpt-5.1-codex": { - input: 4.00, - output: 16.00, - cached: 2.00, - reasoning: 24.00, - cache_creation: 4.00 - }, - "gpt-5.1-codex-mini": { - input: 1.50, - output: 6.00, - cached: 0.75, - reasoning: 9.00, - cache_creation: 1.50 - }, - "gpt-5.1-codex-max": { - input: 8.00, - output: 32.00, - cached: 4.00, - reasoning: 48.00, - cache_creation: 8.00 - }, - "gpt-5.2": { - input: 5.00, - output: 20.00, - cached: 2.50, - reasoning: 30.00, - cache_creation: 5.00 - }, - "gpt-5.2-codex": { - input: 5.00, - output: 20.00, - cached: 2.50, - reasoning: 30.00, - cache_creation: 5.00 - }, - "gpt-5.3-codex": { - input: 1.75, - output: 14.00, - cached: 0.175, - reasoning: 14.00, - cache_creation: 1.75 - }, - "claude-haiku-4.5": { - input: 0.50, - output: 2.50, - cached: 0.05, - reasoning: 3.75, - cache_creation: 0.50 - }, - "claude-opus-4.1": { - input: 5.00, - output: 25.00, - cached: 0.50, - reasoning: 37.50, - cache_creation: 5.00 - }, - "claude-opus-4.5": { - input: 5.00, - output: 25.00, - cached: 0.50, - reasoning: 37.50, - cache_creation: 5.00 - }, - "claude-sonnet-4": { - input: 3.00, - output: 15.00, - cached: 0.30, - reasoning: 22.50, - cache_creation: 3.00 - }, - "claude-sonnet-4.5": { - input: 3.00, - output: 15.00, - cached: 0.30, - reasoning: 22.50, - cache_creation: 3.00 - }, - "claude-sonnet-4.6": { - input: 3.00, - output: 15.00, - cached: 0.30, - reasoning: 22.50, - cache_creation: 3.00 - }, - "claude-opus-4.6": { - input: 5.00, - output: 25.00, - cached: 0.50, - reasoning: 37.50, - cache_creation: 5.00 - }, - "gemini-2.5-pro": { - input: 2.00, - output: 12.00, - cached: 0.25, - reasoning: 18.00, - cache_creation: 2.00 - }, - "gemini-3-flash-preview": { - input: 0.50, - output: 3.00, - cached: 0.03, - reasoning: 4.50, - cache_creation: 0.50 - }, - "gemini-3-pro-preview": { - input: 2.00, - output: 12.00, - cached: 0.25, - reasoning: 18.00, - cache_creation: 2.00 - }, - "grok-code-fast-1": { - input: 0.50, - output: 2.00, - cached: 0.25, - reasoning: 3.00, - cache_creation: 0.50 - }, - "oswe-vscode-prime": { - input: 1.00, - output: 4.00, - cached: 0.50, - reasoning: 6.00, - cache_creation: 1.00 - } - }, - - // Kiro AI (kr) - AWS CodeWhisperer - kr: { - "claude-sonnet-4.5": { - input: 3.00, - output: 15.00, - cached: 0.30, - reasoning: 22.50, - cache_creation: 3.00 - }, - "claude-haiku-4.5": { - input: 0.50, - output: 2.50, - cached: 0.05, - reasoning: 3.75, - cache_creation: 0.50 - } - }, - - // API Key Providers (alias = id) - - // OpenAI - openai: { - "gpt-4o": { - input: 2.50, - output: 10.00, - cached: 1.25, - reasoning: 15.00, - cache_creation: 2.50 - }, - "gpt-4o-mini": { - input: 0.15, - output: 0.60, - cached: 0.075, - reasoning: 0.90, - cache_creation: 0.15 - }, - "gpt-4-turbo": { - input: 10.00, - output: 30.00, - cached: 5.00, - reasoning: 45.00, - cache_creation: 10.00 - }, - "o1": { - input: 15.00, - output: 60.00, - cached: 7.50, - reasoning: 90.00, - cache_creation: 15.00 - }, - "o1-mini": { - input: 3.00, - output: 12.00, - cached: 1.50, - reasoning: 18.00, - cache_creation: 3.00 - } - }, - - // Anthropic - anthropic: { - "claude-sonnet-4-20250514": { - input: 3.00, - output: 15.00, - cached: 1.50, - reasoning: 15.00, - cache_creation: 3.00 - }, - "claude-opus-4-20250514": { - input: 15.00, - output: 25.00, - cached: 7.50, - reasoning: 112.50, - cache_creation: 15.00 - }, - "claude-3-5-sonnet-20241022": { - input: 3.00, - output: 15.00, - cached: 1.50, - reasoning: 15.00, - cache_creation: 3.00 - } - }, - - // Gemini - gemini: { - "gemini-3-pro-preview": { - input: 2.00, - output: 12.00, - cached: 0.25, - reasoning: 18.00, - cache_creation: 2.00 - }, - "gemini-2.5-pro": { - input: 2.00, - output: 12.00, - cached: 0.25, - reasoning: 18.00, - cache_creation: 2.00 - }, - "gemini-2.5-flash": { - input: 0.30, - output: 2.50, - cached: 0.03, - reasoning: 3.75, - cache_creation: 0.30 - }, - "gemini-2.5-flash-lite": { - input: 0.15, - output: 1.25, - cached: 0.015, - reasoning: 1.875, - cache_creation: 0.15 - } - }, - - // OpenRouter - openrouter: { - "auto": { - input: 2.00, - output: 8.00, - cached: 1.00, - reasoning: 12.00, - cache_creation: 2.00 - } - }, - - // GLM - glm: { - "glm-4.7": { - input: 0.75, - output: 3.00, - cached: 0.375, - reasoning: 4.50, - cache_creation: 0.75 - }, - "glm-4.6": { - input: 0.50, - output: 2.00, - cached: 0.25, - reasoning: 3.00, - cache_creation: 0.50 - }, - "glm-4.6v": { - input: 0.75, - output: 3.00, - cached: 0.375, - reasoning: 4.50, - cache_creation: 0.75 - } - }, - - // Kimi - kimi: { - "kimi-k2.5": { - input: 1.20, - output: 4.80, - cached: 0.60, - reasoning: 7.20, - cache_creation: 1.20 - }, - "kimi-k2.5-thinking": { - input: 1.80, - output: 7.20, - cached: 0.90, - reasoning: 10.80, - cache_creation: 1.80 - }, - "kimi-latest": { - input: 1.00, - output: 4.00, - cached: 0.50, - reasoning: 6.00, - cache_creation: 1.00 - } - }, - - // MiniMax - minimax: { - "MiniMax-M2.7": { - input: 0.50, - output: 2.00, - cached: 0.25, - reasoning: 3.00, - cache_creation: 0.50 - }, - "MiniMax-M2.5": { - input: 0.50, - output: 2.00, - cached: 0.25, - reasoning: 3.00, - cache_creation: 0.50 - }, - "MiniMax-M2.1": { - input: 0.50, - output: 2.00, - cached: 0.25, - reasoning: 3.00, - cache_creation: 0.50 - } - }, - - // DeepSeek (official API pricing: https://api-docs.deepseek.com/quick_start/pricing) - deepseek: { - "deepseek-chat": { - input: 0.28, - output: 0.42, - cached: 0.028, - reasoning: 0.42, - cache_creation: 0.28 - }, - "deepseek-reasoner": { - input: 0.28, - output: 0.42, - cached: 0.028, - reasoning: 0.42, - cache_creation: 0.28 - } - } -}; +// Pricing rates for AI models — all rates in $/1M tokens +// +// Fallback order (first match wins): +// 1. PROVIDER_PRICING[provider][model] — provider-specific override +// 2. MODEL_PRICING[model] — canonical model price (provider-agnostic) +// 3. PATTERN_PRICING — glob pattern match (e.g. "codex-*") /** - * Provider-agnostic fallback pricing, keyed by model base name. - * Used when provider-specific lookup fails (e.g. openrouter, cu, fireworks use prefixed model IDs). - * Strip prefix before lookup: "deepseek/deepseek-chat" → "deepseek-chat" + * Canonical model pricing — provider-agnostic. + * Cover all known models; deduplicated across providers. */ export const MODEL_PRICING = { - // DeepSeek - "deepseek-chat": { input: 0.28, output: 0.42, cached: 0.028, reasoning: 0.42, cache_creation: 0.28 }, - "deepseek-reasoner": { input: 0.28, output: 0.42, cached: 0.028, reasoning: 0.42, cache_creation: 0.28 }, + // === Anthropic / Claude === + "claude-opus-4-6": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 25.00, cache_creation: 6.25 }, + "claude-opus-4-5-20251101": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 25.00, cache_creation: 6.25 }, + "claude-sonnet-4-6": { input: 3.00, output: 15.00, cached: 0.30, reasoning: 15.00, cache_creation: 3.75 }, + "claude-sonnet-4-5-20250929": { input: 3.00, output: 15.00, cached: 0.30, reasoning: 15.00, cache_creation: 3.75 }, + "claude-haiku-4-5-20251001": { input: 1.00, output: 5.00, cached: 0.10, reasoning: 5.00, cache_creation: 1.25 }, + "claude-sonnet-4-20250514": { input: 3.00, output: 15.00, cached: 1.50, reasoning: 15.00, cache_creation: 3.00 }, + "claude-opus-4-20250514": { input: 15.00, output: 25.00, cached: 7.50, reasoning: 112.50, cache_creation: 15.00 }, + "claude-3-5-sonnet-20241022": { input: 3.00, output: 15.00, cached: 1.50, reasoning: 15.00, cache_creation: 3.00 }, + "claude-haiku-4.5": { input: 0.50, output: 2.50, cached: 0.05, reasoning: 3.75, cache_creation: 0.50 }, + "claude-opus-4.1": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 37.50, cache_creation: 5.00 }, + "claude-opus-4.5": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 37.50, cache_creation: 5.00 }, + "claude-opus-4.6": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 37.50, cache_creation: 5.00 }, + "claude-sonnet-4": { input: 3.00, output: 15.00, cached: 0.30, reasoning: 22.50, cache_creation: 3.00 }, + "claude-sonnet-4.5": { input: 3.00, output: 15.00, cached: 0.30, reasoning: 22.50, cache_creation: 3.00 }, + "claude-sonnet-4.6": { input: 3.00, output: 15.00, cached: 0.30, reasoning: 22.50, cache_creation: 3.00 }, + "claude-opus-4-5-thinking": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 37.50, cache_creation: 5.00 }, + "claude-opus-4-6-thinking": { input: 5.00, output: 25.00, cached: 0.50, reasoning: 37.50, cache_creation: 5.00 }, + + // === OpenAI / GPT === + "gpt-3.5-turbo": { input: 0.50, output: 1.50, cached: 0.25, reasoning: 2.25, cache_creation: 0.50 }, + "gpt-4": { input: 2.50, output: 10.00, cached: 1.25, reasoning: 15.00, cache_creation: 2.50 }, + "gpt-4-turbo": { input: 10.00, output: 30.00, cached: 5.00, reasoning: 45.00, cache_creation: 10.00 }, + "gpt-4o": { input: 2.50, output: 10.00, cached: 1.25, reasoning: 15.00, cache_creation: 2.50 }, + "gpt-4o-mini": { input: 0.15, output: 0.60, cached: 0.075, reasoning: 0.90, cache_creation: 0.15 }, + "gpt-4.1": { input: 2.50, output: 10.00, cached: 1.25, reasoning: 15.00, cache_creation: 2.50 }, + "gpt-5": { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 }, + "gpt-5-mini": { input: 0.75, output: 3.00, cached: 0.375, reasoning: 4.50, cache_creation: 0.75 }, + "gpt-5-codex": { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 }, + "gpt-5.1": { input: 4.00, output: 16.00, cached: 2.00, reasoning: 24.00, cache_creation: 4.00 }, + "gpt-5.1-codex": { input: 4.00, output: 16.00, cached: 2.00, reasoning: 24.00, cache_creation: 4.00 }, + "gpt-5.1-codex-mini": { input: 1.50, output: 6.00, cached: 0.75, reasoning: 9.00, cache_creation: 1.50 }, + "gpt-5.1-codex-mini-high": { input: 2.00, output: 8.00, cached: 1.00, reasoning: 12.00, cache_creation: 2.00 }, + "gpt-5.1-codex-max": { input: 8.00, output: 32.00, cached: 4.00, reasoning: 48.00, cache_creation: 8.00 }, + "gpt-5.2": { input: 5.00, output: 20.00, cached: 2.50, reasoning: 30.00, cache_creation: 5.00 }, + "gpt-5.2-codex": { input: 5.00, output: 20.00, cached: 2.50, reasoning: 30.00, cache_creation: 5.00 }, + "gpt-5.3-codex": { input: 6.00, output: 24.00, cached: 3.00, reasoning: 36.00, cache_creation: 6.00 }, + "gpt-5.3-codex-xhigh": { input: 10.00, output: 40.00, cached: 5.00, reasoning: 60.00, cache_creation: 10.00 }, + "gpt-5.3-codex-high": { input: 8.00, output: 32.00, cached: 4.00, reasoning: 48.00, cache_creation: 8.00 }, + "gpt-5.3-codex-low": { input: 4.00, output: 16.00, cached: 2.00, reasoning: 24.00, cache_creation: 4.00 }, + "gpt-5.3-codex-none": { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 }, + "gpt-5.3-codex-spark": { input: 3.00, output: 12.00, cached: 0.30, reasoning: 12.00, cache_creation: 3.00 }, + "o1": { input: 15.00, output: 60.00, cached: 7.50, reasoning: 90.00, cache_creation: 15.00 }, + "o1-mini": { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 }, + + // === Gemini === + "gemini-3-flash-preview": { input: 0.50, output: 3.00, cached: 0.03, reasoning: 4.50, cache_creation: 0.50 }, + "gemini-3-pro-preview": { input: 2.00, output: 12.00, cached: 0.25, reasoning: 18.00, cache_creation: 2.00 }, + "gemini-3.1-pro-low": { input: 2.00, output: 12.00, cached: 0.25, reasoning: 18.00, cache_creation: 2.00 }, + "gemini-3.1-pro-high": { input: 4.00, output: 18.00, cached: 0.50, reasoning: 27.00, cache_creation: 4.00 }, + "gemini-3-flash": { input: 0.50, output: 3.00, cached: 0.03, reasoning: 4.50, cache_creation: 0.50 }, + "gemini-2.5-pro": { input: 2.00, output: 12.00, cached: 0.25, reasoning: 18.00, cache_creation: 2.00 }, + "gemini-2.5-flash": { input: 0.30, output: 2.50, cached: 0.03, reasoning: 3.75, cache_creation: 0.30 }, + "gemini-2.5-flash-lite": { input: 0.15, output: 1.25, cached: 0.015, reasoning: 1.875, cache_creation: 0.15 }, + + // === Qwen === + "qwen3-coder-plus": { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 }, + "qwen3-coder-flash": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 }, + + // === Kimi === + "kimi-k2": { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 }, + "kimi-k2-thinking": { input: 1.50, output: 6.00, cached: 0.75, reasoning: 9.00, cache_creation: 1.50 }, + "kimi-k2.5": { input: 1.20, output: 4.80, cached: 0.60, reasoning: 7.20, cache_creation: 1.20 }, + "kimi-k2.5-thinking": { input: 1.80, output: 7.20, cached: 0.90, reasoning: 10.80, cache_creation: 1.80 }, + "kimi-latest": { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 }, + + // === DeepSeek === + "deepseek-chat": { input: 0.28, output: 0.42, cached: 0.028, reasoning: 0.42, cache_creation: 0.28 }, + "deepseek-reasoner": { input: 0.28, output: 0.42, cached: 0.028, reasoning: 0.42, cache_creation: 0.28 }, + "deepseek-r1": { input: 0.75, output: 3.00, cached: 0.375, reasoning: 4.50, cache_creation: 0.75 }, + "deepseek-v3.2-chat": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 }, + "deepseek-v3.2-reasoner": { input: 0.75, output: 3.00, cached: 0.375, reasoning: 4.50, cache_creation: 0.75 }, + + // === GLM === + "glm-4.6": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 }, + "glm-4.6v": { input: 0.75, output: 3.00, cached: 0.375, reasoning: 4.50, cache_creation: 0.75 }, + "glm-4.7": { input: 0.75, output: 3.00, cached: 0.375, reasoning: 4.50, cache_creation: 0.75 }, + "glm-5": { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 }, + + // === MiniMax === + "MiniMax-M2.1": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 }, + "MiniMax-M2.5": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 }, + "MiniMax-M2.7": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 }, + "minimax-m2.1": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 }, + "minimax-m2.5": { input: 0.60, output: 2.40, cached: 0.30, reasoning: 3.60, cache_creation: 0.60 }, + + // === Grok === + "grok-code-fast-1": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 }, + + // === OpenRouter fallback === + "auto": { input: 2.00, output: 8.00, cached: 1.00, reasoning: 12.00, cache_creation: 2.00 }, + + // === Misc === + "oswe-vscode-prime": { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 }, + "gpt-oss-120b-medium": { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 }, + "vision-model": { input: 1.50, output: 6.00, cached: 0.75, reasoning: 9.00, cache_creation: 1.50 }, + "coder-model": { input: 1.50, output: 6.00, cached: 0.75, reasoning: 9.00, cache_creation: 1.50 }, }; /** - * Get pricing for a specific provider and model - * @param {string} provider - Provider ID (e.g., "openai", "cc", "gc") - * @param {string} model - Model ID - * @returns {object|null} Pricing object or null if not found + * Provider-specific pricing overrides. + * Only include entries where price DIFFERS from MODEL_PRICING. + * Keyed by provider alias (cc, cx, gc, gh, ...) or provider id (openai, anthropic, ...). */ -export function getPricingForModel(provider, model) { - if (!provider || !model) return null; +export const PROVIDER_PRICING = { + // GitHub Copilot (gh) — gpt-5.3-codex has different rate than canonical + gh: { + "gpt-5.3-codex": { input: 1.75, output: 14.00, cached: 0.175, reasoning: 14.00, cache_creation: 1.75 }, + }, +}; - const providerPricing = DEFAULT_PRICING[provider]; - if (!providerPricing) return null; +/** + * Pattern-based pricing fallback — matched when no exact model entry found. + * Patterns use simple glob: "*" matches any substring. + * First match wins — order matters. + */ +export const PATTERN_PRICING = [ + // --- Codex variants --- + { pattern: "*-codex-xhigh", pricing: { input: 10.00, output: 40.00, cached: 5.00, reasoning: 60.00, cache_creation: 10.00 } }, + { pattern: "*-codex-high", pricing: { input: 8.00, output: 32.00, cached: 4.00, reasoning: 48.00, cache_creation: 8.00 } }, + { pattern: "*-codex-max", pricing: { input: 8.00, output: 32.00, cached: 4.00, reasoning: 48.00, cache_creation: 8.00 } }, + { pattern: "*-codex-mini-*", pricing: { input: 1.50, output: 6.00, cached: 0.75, reasoning: 9.00, cache_creation: 1.50 } }, + { pattern: "*-codex-mini", pricing: { input: 1.50, output: 6.00, cached: 0.75, reasoning: 9.00, cache_creation: 1.50 } }, + { pattern: "*-codex-low", pricing: { input: 4.00, output: 16.00, cached: 2.00, reasoning: 24.00, cache_creation: 4.00 } }, + { pattern: "*-codex-none", pricing: { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 } }, + { pattern: "*-codex-spark", pricing: { input: 3.00, output: 12.00, cached: 0.30, reasoning: 12.00, cache_creation: 3.00 } }, + { pattern: "codex-*", pricing: { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 } }, + { pattern: "*-codex", pricing: { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 } }, - return providerPricing[model] || null; + // --- Claude --- + { pattern: "claude-opus-*", pricing: { input: 5.00, output: 25.00, cached: 0.50, reasoning: 25.00, cache_creation: 6.25 } }, + { pattern: "claude-sonnet-*", pricing: { input: 3.00, output: 15.00, cached: 0.30, reasoning: 15.00, cache_creation: 3.75 } }, + { pattern: "claude-haiku-*", pricing: { input: 1.00, output: 5.00, cached: 0.10, reasoning: 5.00, cache_creation: 1.25 } }, + { pattern: "claude-*", pricing: { input: 3.00, output: 15.00, cached: 0.30, reasoning: 15.00, cache_creation: 3.75 } }, + + // --- Gemini (specific trước, chung sau) --- + { pattern: "gemini-*-flash-lite", pricing: { input: 0.15, output: 1.25, cached: 0.015, reasoning: 1.875, cache_creation: 0.15 } }, + { pattern: "gemini-*-flash", pricing: { input: 0.30, output: 2.50, cached: 0.03, reasoning: 3.75, cache_creation: 0.30 } }, + { pattern: "gemini-*-pro", pricing: { input: 2.00, output: 12.00, cached: 0.25, reasoning: 18.00, cache_creation: 2.00 } }, + { pattern: "gemini-3-*", pricing: { input: 0.50, output: 3.00, cached: 0.03, reasoning: 4.50, cache_creation: 0.50 } }, + { pattern: "gemini-2.5-*", pricing: { input: 0.30, output: 2.50, cached: 0.03, reasoning: 3.75, cache_creation: 0.30 } }, + { pattern: "gemini-*", pricing: { input: 0.50, output: 3.00, cached: 0.03, reasoning: 4.50, cache_creation: 0.50 } }, + + // --- GPT (specific trước, chung sau) --- + { pattern: "gpt-5.3-*", pricing: { input: 6.00, output: 24.00, cached: 3.00, reasoning: 36.00, cache_creation: 6.00 } }, + { pattern: "gpt-5.2-*", pricing: { input: 5.00, output: 20.00, cached: 2.50, reasoning: 30.00, cache_creation: 5.00 } }, + { pattern: "gpt-5.1-*", pricing: { input: 4.00, output: 16.00, cached: 2.00, reasoning: 24.00, cache_creation: 4.00 } }, + { pattern: "gpt-5-*", pricing: { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 } }, + { pattern: "gpt-5*", pricing: { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 } }, + { pattern: "gpt-4o-*", pricing: { input: 0.15, output: 0.60, cached: 0.075, reasoning: 0.90, cache_creation: 0.15 } }, + { pattern: "gpt-4o", pricing: { input: 2.50, output: 10.00, cached: 1.25, reasoning: 15.00, cache_creation: 2.50 } }, + { pattern: "gpt-4*", pricing: { input: 2.50, output: 10.00, cached: 1.25, reasoning: 15.00, cache_creation: 2.50 } }, + + // --- o1 / o-series --- + { pattern: "o1-*", pricing: { input: 3.00, output: 12.00, cached: 1.50, reasoning: 18.00, cache_creation: 3.00 } }, + { pattern: "o1", pricing: { input: 15.00, output: 60.00, cached: 7.50, reasoning: 90.00, cache_creation: 15.00 } }, + { pattern: "o3-*", pricing: { input: 10.00, output: 40.00, cached: 5.00, reasoning: 60.00, cache_creation: 10.00 } }, + { pattern: "o4-*", pricing: { input: 2.00, output: 8.00, cached: 1.00, reasoning: 12.00, cache_creation: 2.00 } }, + + // --- Qwen --- + { pattern: "qwen3-coder-*", pricing: { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 } }, + { pattern: "qwen*-coder-*", pricing: { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 } }, + { pattern: "qwen*", pricing: { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 } }, + + // --- Kimi --- + { pattern: "kimi-*-thinking", pricing: { input: 1.80, output: 7.20, cached: 0.90, reasoning: 10.80, cache_creation: 1.80 } }, + { pattern: "kimi-k2*", pricing: { input: 1.20, output: 4.80, cached: 0.60, reasoning: 7.20, cache_creation: 1.20 } }, + { pattern: "kimi-*", pricing: { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 } }, + + // --- DeepSeek --- + { pattern: "deepseek-*reasoner*", pricing: { input: 0.75, output: 3.00, cached: 0.375, reasoning: 4.50, cache_creation: 0.75 } }, + { pattern: "deepseek-r*", pricing: { input: 0.75, output: 3.00, cached: 0.375, reasoning: 4.50, cache_creation: 0.75 } }, + { pattern: "deepseek-v*", pricing: { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 } }, + { pattern: "deepseek-*", pricing: { input: 0.28, output: 0.42, cached: 0.028, reasoning: 0.42, cache_creation: 0.28 } }, + + // --- GLM --- + { pattern: "glm-5*", pricing: { input: 1.00, output: 4.00, cached: 0.50, reasoning: 6.00, cache_creation: 1.00 } }, + { pattern: "glm-4*", pricing: { input: 0.75, output: 3.00, cached: 0.375, reasoning: 4.50, cache_creation: 0.75 } }, + { pattern: "glm-*", pricing: { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 } }, + + // --- MiniMax --- + { pattern: "MiniMax-*", pricing: { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 } }, + { pattern: "minimax-*", pricing: { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 } }, + + // --- Grok --- + { pattern: "grok-code-*", pricing: { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 } }, + { pattern: "grok-*", pricing: { input: 0.50, output: 2.00, cached: 0.25, reasoning: 3.00, cache_creation: 0.50 } }, +]; + +/** + * Match a model ID against a glob pattern (* = wildcard). + */ +function matchPattern(pattern, model) { + const regex = new RegExp("^" + pattern.split("*").map(s => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$"); + return regex.test(model); } /** - * Get all pricing data - * @returns {object} All default pricing + * Resolve pricing for a model using the 3-step fallback chain: + * 1. PROVIDER_PRICING[provider][model] + * 2. MODEL_PRICING[model] + * 3. PATTERN_PRICING (glob match) + * + * @param {string} provider + * @param {string} model + * @returns {object|null} + */ +export function getPricingForModel(provider, model) { + if (!model) return null; + + // 1. Provider-specific override + if (provider && PROVIDER_PRICING[provider]?.[model]) { + return PROVIDER_PRICING[provider][model]; + } + + // 2. Canonical model pricing (strip vendor prefix if needed: "deepseek/deepseek-chat" → "deepseek-chat") + const baseModel = model.includes("/") ? model.split("/").pop() : model; + if (MODEL_PRICING[baseModel]) return MODEL_PRICING[baseModel]; + if (MODEL_PRICING[model]) return MODEL_PRICING[model]; + + // 3. Pattern match + for (const { pattern, pricing } of PATTERN_PRICING) { + if (matchPattern(pattern, baseModel) || matchPattern(pattern, model)) { + return pricing; + } + } + + return null; +} + +/** + * Get all provider pricing (for UI / API). + * Returns PROVIDER_PRICING — consumers should fall back to MODEL_PRICING for unlisted models. */ export function getDefaultPricing() { - return DEFAULT_PRICING; + return PROVIDER_PRICING; } /** * Format cost for display - * @param {number} cost - Cost in dollars - * @returns {string} Formatted cost string + * @param {number} cost + * @returns {string} */ export function formatCost(cost) { if (cost === null || cost === undefined || isNaN(cost)) return "$0.00"; @@ -832,44 +260,36 @@ export function formatCost(cost) { /** * Calculate cost from tokens and pricing - * @param {object} tokens - Token counts - * @param {object} pricing - Pricing object - * @returns {number} Cost in dollars + * @param {object} tokens + * @param {object} pricing + * @returns {number} cost in dollars */ export function calculateCostFromTokens(tokens, pricing) { if (!tokens || !pricing) return 0; let cost = 0; - // Input tokens (non-cached) const inputTokens = tokens.prompt_tokens || tokens.input_tokens || 0; const cachedTokens = tokens.cached_tokens || tokens.cache_read_input_tokens || 0; const nonCachedInput = Math.max(0, inputTokens - cachedTokens); - cost += (nonCachedInput * (pricing.input / 1000000)); + cost += nonCachedInput * (pricing.input / 1000000); - // Cached tokens if (cachedTokens > 0) { - const cachedRate = pricing.cached || pricing.input; // Fallback to input rate - cost += (cachedTokens * (cachedRate / 1000000)); + cost += cachedTokens * ((pricing.cached || pricing.input) / 1000000); } - // Output tokens const outputTokens = tokens.completion_tokens || tokens.output_tokens || 0; - cost += (outputTokens * (pricing.output / 1000000)); + cost += outputTokens * (pricing.output / 1000000); - // Reasoning tokens const reasoningTokens = tokens.reasoning_tokens || 0; if (reasoningTokens > 0) { - const reasoningRate = pricing.reasoning || pricing.output; // Fallback to output rate - cost += (reasoningTokens * (reasoningRate / 1000000)); + cost += reasoningTokens * ((pricing.reasoning || pricing.output) / 1000000); } - // Cache creation tokens const cacheCreationTokens = tokens.cache_creation_input_tokens || 0; if (cacheCreationTokens > 0) { - const cacheCreationRate = pricing.cache_creation || pricing.input; // Fallback to input rate - cost += (cacheCreationTokens * (cacheCreationRate / 1000000)); + cost += cacheCreationTokens * ((pricing.cache_creation || pricing.input) / 1000000); } return cost;