Fix Model Price

This commit is contained in:
decolua
2026-04-04 14:40:49 +07:00
parent 2b1faeb2c6
commit 341a1c6490
6 changed files with 412 additions and 973 deletions
+31 -59
View File
@@ -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);
}
/**
+29 -4
View File
@@ -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)