mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
feat(usage): implement cost tracking backend and pricing configuration
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+157
-1
@@ -41,7 +41,8 @@ const defaultData = {
|
|||||||
settings: {
|
settings: {
|
||||||
cloudEnabled: false,
|
cloudEnabled: false,
|
||||||
stickyRoundRobinLimit: 3
|
stickyRoundRobinLimit: 3
|
||||||
}
|
},
|
||||||
|
pricing: {} // NEW: pricing configuration
|
||||||
};
|
};
|
||||||
|
|
||||||
// Singleton instance
|
// Singleton instance
|
||||||
@@ -528,3 +529,158 @@ export async function isCloudEnabled() {
|
|||||||
return settings.cloudEnabled === true;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
+71
-2
@@ -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
|
* Get aggregated usage stats
|
||||||
*/
|
*/
|
||||||
@@ -258,6 +314,7 @@ export async function getUsageStats() {
|
|||||||
totalRequests: history.length,
|
totalRequests: history.length,
|
||||||
totalPromptTokens: 0,
|
totalPromptTokens: 0,
|
||||||
totalCompletionTokens: 0,
|
totalCompletionTokens: 0,
|
||||||
|
totalCost: 0, // NEW
|
||||||
byProvider: {},
|
byProvider: {},
|
||||||
byModel: {},
|
byModel: {},
|
||||||
byAccount: {},
|
byAccount: {},
|
||||||
@@ -300,7 +357,8 @@ export async function getUsageStats() {
|
|||||||
bucketMap[bucketKey] = {
|
bucketMap[bucketKey] = {
|
||||||
requests: 0,
|
requests: 0,
|
||||||
promptTokens: 0,
|
promptTokens: 0,
|
||||||
completionTokens: 0
|
completionTokens: 0,
|
||||||
|
cost: 0
|
||||||
};
|
};
|
||||||
stats.last10Minutes.push(bucketMap[bucketKey]);
|
stats.last10Minutes.push(bucketMap[bucketKey]);
|
||||||
}
|
}
|
||||||
@@ -310,8 +368,12 @@ export async function getUsageStats() {
|
|||||||
const completionTokens = entry.tokens?.completion_tokens || 0;
|
const completionTokens = entry.tokens?.completion_tokens || 0;
|
||||||
const entryTime = new Date(entry.timestamp);
|
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.totalPromptTokens += promptTokens;
|
||||||
stats.totalCompletionTokens += completionTokens;
|
stats.totalCompletionTokens += completionTokens;
|
||||||
|
stats.totalCost += entryCost;
|
||||||
|
|
||||||
// Last 10 minutes aggregation - floor entry time to its minute
|
// Last 10 minutes aggregation - floor entry time to its minute
|
||||||
if (entryTime >= tenMinutesAgo && entryTime <= now) {
|
if (entryTime >= tenMinutesAgo && entryTime <= now) {
|
||||||
@@ -320,6 +382,7 @@ export async function getUsageStats() {
|
|||||||
bucketMap[entryMinuteStart].requests++;
|
bucketMap[entryMinuteStart].requests++;
|
||||||
bucketMap[entryMinuteStart].promptTokens += promptTokens;
|
bucketMap[entryMinuteStart].promptTokens += promptTokens;
|
||||||
bucketMap[entryMinuteStart].completionTokens += completionTokens;
|
bucketMap[entryMinuteStart].completionTokens += completionTokens;
|
||||||
|
bucketMap[entryMinuteStart].cost += entryCost;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,12 +391,14 @@ export async function getUsageStats() {
|
|||||||
stats.byProvider[entry.provider] = {
|
stats.byProvider[entry.provider] = {
|
||||||
requests: 0,
|
requests: 0,
|
||||||
promptTokens: 0,
|
promptTokens: 0,
|
||||||
completionTokens: 0
|
completionTokens: 0,
|
||||||
|
cost: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
stats.byProvider[entry.provider].requests++;
|
stats.byProvider[entry.provider].requests++;
|
||||||
stats.byProvider[entry.provider].promptTokens += promptTokens;
|
stats.byProvider[entry.provider].promptTokens += promptTokens;
|
||||||
stats.byProvider[entry.provider].completionTokens += completionTokens;
|
stats.byProvider[entry.provider].completionTokens += completionTokens;
|
||||||
|
stats.byProvider[entry.provider].cost += entryCost;
|
||||||
|
|
||||||
// By Model
|
// By Model
|
||||||
// Format: "modelName (provider)" if provider is known
|
// Format: "modelName (provider)" if provider is known
|
||||||
@@ -344,6 +409,7 @@ export async function getUsageStats() {
|
|||||||
requests: 0,
|
requests: 0,
|
||||||
promptTokens: 0,
|
promptTokens: 0,
|
||||||
completionTokens: 0,
|
completionTokens: 0,
|
||||||
|
cost: 0,
|
||||||
rawModel: entry.model,
|
rawModel: entry.model,
|
||||||
provider: entry.provider,
|
provider: entry.provider,
|
||||||
lastUsed: entry.timestamp
|
lastUsed: entry.timestamp
|
||||||
@@ -352,6 +418,7 @@ export async function getUsageStats() {
|
|||||||
stats.byModel[modelKey].requests++;
|
stats.byModel[modelKey].requests++;
|
||||||
stats.byModel[modelKey].promptTokens += promptTokens;
|
stats.byModel[modelKey].promptTokens += promptTokens;
|
||||||
stats.byModel[modelKey].completionTokens += completionTokens;
|
stats.byModel[modelKey].completionTokens += completionTokens;
|
||||||
|
stats.byModel[modelKey].cost += entryCost;
|
||||||
if (new Date(entry.timestamp) > new Date(stats.byModel[modelKey].lastUsed)) {
|
if (new Date(entry.timestamp) > new Date(stats.byModel[modelKey].lastUsed)) {
|
||||||
stats.byModel[modelKey].lastUsed = entry.timestamp;
|
stats.byModel[modelKey].lastUsed = entry.timestamp;
|
||||||
}
|
}
|
||||||
@@ -367,6 +434,7 @@ export async function getUsageStats() {
|
|||||||
requests: 0,
|
requests: 0,
|
||||||
promptTokens: 0,
|
promptTokens: 0,
|
||||||
completionTokens: 0,
|
completionTokens: 0,
|
||||||
|
cost: 0,
|
||||||
rawModel: entry.model,
|
rawModel: entry.model,
|
||||||
provider: entry.provider,
|
provider: entry.provider,
|
||||||
connectionId: entry.connectionId,
|
connectionId: entry.connectionId,
|
||||||
@@ -377,6 +445,7 @@ export async function getUsageStats() {
|
|||||||
stats.byAccount[accountKey].requests++;
|
stats.byAccount[accountKey].requests++;
|
||||||
stats.byAccount[accountKey].promptTokens += promptTokens;
|
stats.byAccount[accountKey].promptTokens += promptTokens;
|
||||||
stats.byAccount[accountKey].completionTokens += completionTokens;
|
stats.byAccount[accountKey].completionTokens += completionTokens;
|
||||||
|
stats.byAccount[accountKey].cost += entryCost;
|
||||||
if (new Date(entry.timestamp) > new Date(stats.byAccount[accountKey].lastUsed)) {
|
if (new Date(entry.timestamp) > new Date(stats.byAccount[accountKey].lastUsed)) {
|
||||||
stats.byAccount[accountKey].lastUsed = entry.timestamp;
|
stats.byAccount[accountKey].lastUsed = entry.timestamp;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user