From a36afaa85e640b383af9140f30e1e4a8bbcaef8b Mon Sep 17 00:00:00 2001 From: Catalin Stanciu Date: Tue, 6 Jan 2026 23:59:49 +0200 Subject: [PATCH] feat(usage): implement cost tracking backend and pricing configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add pricing constants with default rates for all providers - Update localDb to support pricing configuration schema - Add cost calculation logic to usageDb - Add pricing management API endpoints - Fix provider alias mapping for accurate cost lookups 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/app/api/pricing/route.js | 134 +++++++ src/lib/localDb.js | 158 ++++++++- src/lib/usageDb.js | 73 +++- src/shared/constants/pricing.js | 598 ++++++++++++++++++++++++++++++++ 4 files changed, 960 insertions(+), 3 deletions(-) create mode 100644 src/app/api/pricing/route.js create mode 100644 src/shared/constants/pricing.js diff --git a/src/app/api/pricing/route.js b/src/app/api/pricing/route.js new file mode 100644 index 0000000..7d553a4 --- /dev/null +++ b/src/app/api/pricing/route.js @@ -0,0 +1,134 @@ +import { NextResponse } from "next/server"; +import { getPricing, updatePricing, resetPricing, resetAllPricing } from "@/lib/localDb.js"; +import { getDefaultPricing } from "@/shared/constants/pricing.js"; + +/** + * GET /api/pricing + * Get current pricing configuration (merged user + defaults) + */ +export async function GET() { + try { + const pricing = await getPricing(); + return NextResponse.json(pricing); + } catch (error) { + console.error("Error fetching pricing:", error); + return NextResponse.json( + { error: "Failed to fetch pricing" }, + { status: 500 } + ); + } +} + +/** + * PATCH /api/pricing + * Update pricing configuration + * Body: { provider: { model: { input: number, output: number, cached: number, ... } } } + */ +export async function PATCH(request) { + try { + const body = await request.json(); + + // Validate body structure + if (typeof body !== "object" || body === null) { + return NextResponse.json( + { error: "Invalid pricing data format" }, + { status: 400 } + ); + } + + // Validate pricing structure + for (const [provider, models] of Object.entries(body)) { + if (typeof models !== "object" || models === null) { + return NextResponse.json( + { error: `Invalid pricing for provider: ${provider}` }, + { status: 400 } + ); + } + + for (const [model, pricing] of Object.entries(models)) { + if (typeof pricing !== "object" || pricing === null) { + return NextResponse.json( + { error: `Invalid pricing for model: ${provider}/${model}` }, + { status: 400 } + ); + } + + // Validate pricing fields + const validFields = ["input", "output", "cached", "reasoning", "cache_creation"]; + for (const [key, value] of Object.entries(pricing)) { + if (!validFields.includes(key)) { + return NextResponse.json( + { error: `Invalid pricing field: ${key} for ${provider}/${model}` }, + { status: 400 } + ); + } + if (typeof value !== "number" || isNaN(value) || value < 0) { + return NextResponse.json( + { error: `Invalid pricing value for ${key} in ${provider}/${model}: must be non-negative number` }, + { status: 400 } + ); + } + } + } + } + + const updatedPricing = await updatePricing(body); + return NextResponse.json(updatedPricing); + } catch (error) { + console.error("Error updating pricing:", error); + return NextResponse.json( + { error: "Failed to update pricing" }, + { status: 500 } + ); + } +} + +/** + * DELETE /api/pricing + * Reset pricing to defaults + * Query params: ?provider=xxx&model=yyy (optional) + */ +export async function DELETE(request) { + try { + const { searchParams } = new URL(request.url); + const provider = searchParams.get("provider"); + const model = searchParams.get("model"); + + if (provider && model) { + // Reset specific model + await resetPricing(provider, model); + } else if (provider) { + // Reset entire provider + await resetPricing(provider); + } else { + // Reset all pricing + await resetAllPricing(); + } + + const pricing = await getPricing(); + return NextResponse.json(pricing); + } catch (error) { + console.error("Error resetting pricing:", error); + return NextResponse.json( + { error: "Failed to reset pricing" }, + { status: 500 } + ); + } +} + +/** + * GET /api/pricing/defaults + * Get default pricing configuration + */ +export async function GET_DEFAULTS() { + try { + const defaultPricing = getDefaultPricing(); + return NextResponse.json(defaultPricing); + } catch (error) { + console.error("Error fetching default pricing:", error); + return NextResponse.json( + { error: "Failed to fetch default pricing" }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/lib/localDb.js b/src/lib/localDb.js index 213064f..7cd4a26 100644 --- a/src/lib/localDb.js +++ b/src/lib/localDb.js @@ -41,7 +41,8 @@ const defaultData = { settings: { cloudEnabled: false, stickyRoundRobinLimit: 3 - } + }, + pricing: {} // NEW: pricing configuration }; // Singleton instance @@ -528,3 +529,158 @@ export async function isCloudEnabled() { return settings.cloudEnabled === true; } +// ============ Pricing ============ + +/** + * Get pricing configuration + * Returns merged user pricing with defaults + */ +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(); + + // Merge user pricing with defaults + // User pricing overrides defaults for specific provider/model combinations + const mergedPricing = {}; + + for (const [provider, models] of Object.entries(defaultPricing)) { + mergedPricing[provider] = { ...models }; + + // Apply user overrides if they exist + 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; + } + } + } + } + + // Add any user-only pricing entries + for (const [provider, models] of Object.entries(userPricing)) { + if (!mergedPricing[provider]) { + mergedPricing[provider] = { ...models }; + } else { + for (const [model, pricing] of Object.entries(models)) { + if (!mergedPricing[provider][model]) { + mergedPricing[provider][model] = pricing; + } + } + } + } + + return mergedPricing; +} + +/** + * Get pricing for a specific provider and model + */ +export async function getPricingForModel(provider, model) { + const pricing = await getPricing(); + + // Try direct lookup + if (pricing[provider] && pricing[provider][model]) { + return pricing[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", + 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; + } + + return null; +} + +/** + * Update pricing configuration + * @param {object} pricingData - New pricing data to merge + */ +export async function updatePricing(pricingData) { + const db = await getDb(); + + // Ensure pricing object exists + if (!db.data.pricing) { + db.data.pricing = {}; + } + + // Merge new pricing data + for (const [provider, models] of Object.entries(pricingData)) { + if (!db.data.pricing[provider]) { + db.data.pricing[provider] = {}; + } + + for (const [model, pricing] of Object.entries(models)) { + db.data.pricing[provider][model] = pricing; + } + } + + await db.write(); + return db.data.pricing; +} + +/** + * Reset pricing to defaults for specific provider/model + * @param {string} provider - Provider ID + * @param {string} model - Model ID (optional, if not provided resets entire provider) + */ +export async function resetPricing(provider, model) { + const db = await getDb(); + + if (!db.data.pricing) { + db.data.pricing = {}; + } + + if (model) { + // Reset specific model + if (db.data.pricing[provider]) { + delete db.data.pricing[provider][model]; + // Clean up empty provider objects + if (Object.keys(db.data.pricing[provider]).length === 0) { + delete db.data.pricing[provider]; + } + } + } else { + // Reset entire provider + delete db.data.pricing[provider]; + } + + await db.write(); + return db.data.pricing; +} + +/** + * Reset all pricing to defaults + */ +export async function resetAllPricing() { + const db = await getDb(); + db.data.pricing = {}; + await db.write(); + return db.data.pricing; +} + diff --git a/src/lib/usageDb.js b/src/lib/usageDb.js index 21afcb1..99d6498 100644 --- a/src/lib/usageDb.js +++ b/src/lib/usageDb.js @@ -229,6 +229,62 @@ export async function getRecentLogs(limit = 200) { } } +/** + * Calculate cost for a usage entry + * @param {string} provider - Provider ID + * @param {string} model - Model ID + * @param {object} tokens - Token counts + * @returns {number} Cost in dollars + */ +async function calculateCost(provider, model, tokens) { + if (!tokens || !provider || !model) return 0; + + try { + const { getPricingForModel } = await import("@/lib/localDb.js"); + const pricing = await getPricingForModel(provider, model); + + if (!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)); + + // Cached tokens + if (cachedTokens > 0) { + const cachedRate = pricing.cached || pricing.input; // Fallback to input rate + cost += (cachedTokens * (cachedRate / 1000000)); + } + + // Output tokens + const outputTokens = tokens.completion_tokens || tokens.output_tokens || 0; + 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)); + } + + // 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)); + } + + return cost; + } catch (error) { + console.error("Error calculating cost:", error); + return 0; + } +} + /** * Get aggregated usage stats */ @@ -258,6 +314,7 @@ export async function getUsageStats() { totalRequests: history.length, totalPromptTokens: 0, totalCompletionTokens: 0, + totalCost: 0, // NEW byProvider: {}, byModel: {}, byAccount: {}, @@ -300,7 +357,8 @@ export async function getUsageStats() { bucketMap[bucketKey] = { requests: 0, promptTokens: 0, - completionTokens: 0 + completionTokens: 0, + cost: 0 }; stats.last10Minutes.push(bucketMap[bucketKey]); } @@ -310,8 +368,12 @@ export async function getUsageStats() { const completionTokens = entry.tokens?.completion_tokens || 0; const entryTime = new Date(entry.timestamp); + // Calculate cost for this entry + const entryCost = await calculateCost(entry.provider, entry.model, entry.tokens); + stats.totalPromptTokens += promptTokens; stats.totalCompletionTokens += completionTokens; + stats.totalCost += entryCost; // Last 10 minutes aggregation - floor entry time to its minute if (entryTime >= tenMinutesAgo && entryTime <= now) { @@ -320,6 +382,7 @@ export async function getUsageStats() { bucketMap[entryMinuteStart].requests++; bucketMap[entryMinuteStart].promptTokens += promptTokens; bucketMap[entryMinuteStart].completionTokens += completionTokens; + bucketMap[entryMinuteStart].cost += entryCost; } } @@ -328,12 +391,14 @@ export async function getUsageStats() { stats.byProvider[entry.provider] = { requests: 0, promptTokens: 0, - completionTokens: 0 + completionTokens: 0, + cost: 0 }; } stats.byProvider[entry.provider].requests++; stats.byProvider[entry.provider].promptTokens += promptTokens; stats.byProvider[entry.provider].completionTokens += completionTokens; + stats.byProvider[entry.provider].cost += entryCost; // By Model // Format: "modelName (provider)" if provider is known @@ -344,6 +409,7 @@ export async function getUsageStats() { requests: 0, promptTokens: 0, completionTokens: 0, + cost: 0, rawModel: entry.model, provider: entry.provider, lastUsed: entry.timestamp @@ -352,6 +418,7 @@ export async function getUsageStats() { stats.byModel[modelKey].requests++; stats.byModel[modelKey].promptTokens += promptTokens; stats.byModel[modelKey].completionTokens += completionTokens; + stats.byModel[modelKey].cost += entryCost; if (new Date(entry.timestamp) > new Date(stats.byModel[modelKey].lastUsed)) { stats.byModel[modelKey].lastUsed = entry.timestamp; } @@ -367,6 +434,7 @@ export async function getUsageStats() { requests: 0, promptTokens: 0, completionTokens: 0, + cost: 0, rawModel: entry.model, provider: entry.provider, connectionId: entry.connectionId, @@ -377,6 +445,7 @@ export async function getUsageStats() { stats.byAccount[accountKey].requests++; stats.byAccount[accountKey].promptTokens += promptTokens; stats.byAccount[accountKey].completionTokens += completionTokens; + stats.byAccount[accountKey].cost += entryCost; if (new Date(entry.timestamp) > new Date(stats.byAccount[accountKey].lastUsed)) { stats.byAccount[accountKey].lastUsed = entry.timestamp; } diff --git a/src/shared/constants/pricing.js b/src/shared/constants/pricing.js new file mode 100644 index 0000000..4e9506a --- /dev/null +++ b/src/shared/constants/pricing.js @@ -0,0 +1,598 @@ +// 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-5-20251101": { + input: 15.00, + output: 75.00, + cached: 7.50, + reasoning: 75.00, + cache_creation: 15.00 + }, + "claude-sonnet-4-5-20250929": { + input: 3.00, + output: 15.00, + cached: 1.50, + reasoning: 15.00, + cache_creation: 3.00 + }, + "claude-haiku-4-5-20251001": { + input: 0.50, + output: 2.50, + cached: 0.25, + reasoning: 2.50, + cache_creation: 0.50 + } + }, + + // OpenAI Codex (cx) + cx: { + "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": { + 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 + } + }, + + // 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 + }, + "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": { + input: 0.50, + output: 2.00, + cached: 0.25, + reasoning: 3.00, + cache_creation: 0.50 + }, + "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 + } + }, + + // Antigravity (ag) - User-provided pricing + ag: { + "gemini-3-pro-low": { + input: 2.00, + output: 12.00, + cached: 0.25, + reasoning: 18.00, + cache_creation: 2.00 + }, + "gemini-3-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-5": { + input: 3.00, + output: 15.00, + cached: 0.30, + reasoning: 22.50, + cache_creation: 3.00 + }, + "claude-sonnet-4-5-thinking": { + 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 + } + }, + + // GitHub Copilot (gh) + gh: { + "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.1-codex": { + input: 4.00, + output: 16.00, + cached: 2.00, + reasoning: 24.00, + cache_creation: 4.00 + }, + "gpt-5.1-codex-max": { + input: 8.00, + output: 32.00, + cached: 4.00, + reasoning: 48.00, + cache_creation: 8.00 + }, + "gpt-4.1": { + input: 2.50, + output: 10.00, + cached: 1.25, + reasoning: 15.00, + cache_creation: 2.50 + }, + "claude-4.5-sonnet": { + input: 3.00, + output: 15.00, + cached: 0.30, + reasoning: 22.50, + cache_creation: 3.00 + }, + "claude-4.5-opus": { + input: 5.00, + output: 25.00, + cached: 0.50, + reasoning: 37.50, + cache_creation: 5.00 + }, + "claude-4.5-haiku": { + input: 0.50, + output: 2.50, + cached: 0.05, + reasoning: 3.75, + cache_creation: 0.50 + }, + "gemini-3-pro": { + input: 2.00, + output: 12.00, + cached: 0.25, + reasoning: 18.00, + cache_creation: 2.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 + }, + "grok-code-fast-1": { + input: 0.50, + output: 2.00, + cached: 0.25, + reasoning: 3.00, + 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: 75.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-latest": { + input: 1.00, + output: 4.00, + cached: 0.50, + reasoning: 6.00, + cache_creation: 1.00 + } + }, + + // MiniMax + minimax: { + "MiniMax-M2.1": { + input: 0.50, + output: 2.00, + cached: 0.25, + reasoning: 3.00, + cache_creation: 0.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 + */ +export function getPricingForModel(provider, model) { + if (!provider || !model) return null; + + const providerPricing = DEFAULT_PRICING[provider]; + if (!providerPricing) return null; + + return providerPricing[model] || null; +} + +/** + * Get all pricing data + * @returns {object} All default pricing + */ +export function getDefaultPricing() { + return DEFAULT_PRICING; +} + +/** + * Format cost for display + * @param {number} cost - Cost in dollars + * @returns {string} Formatted cost string + */ +export function formatCost(cost) { + if (cost === null || cost === undefined || isNaN(cost)) return "$0.00"; + return `$${cost.toFixed(2)}`; +} + +/** + * Calculate cost from tokens and pricing + * @param {object} tokens - Token counts + * @param {object} pricing - Pricing object + * @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)); + + // Cached tokens + if (cachedTokens > 0) { + const cachedRate = pricing.cached || pricing.input; // Fallback to input rate + cost += (cachedTokens * (cachedRate / 1000000)); + } + + // Output tokens + const outputTokens = tokens.completion_tokens || tokens.output_tokens || 0; + 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)); + } + + // 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)); + } + + return cost; +} \ No newline at end of file