diff --git a/chat/public/admin.html b/chat/public/admin.html index cc48398..02ba08c 100644 --- a/chat/public/admin.html +++ b/chat/public/admin.html @@ -187,6 +187,9 @@ Google Groq NVIDIA + Chutes + Cerebras + Ollama OpenCode @@ -207,6 +210,10 @@ Tokens per minute + + Tokens per hour + + Tokens per day @@ -215,6 +222,10 @@ Requests per minute + + Requests per hour + + Requests per day diff --git a/chat/public/admin.js b/chat/public/admin.js index 9f2e237..5499d9a 100644 --- a/chat/public/admin.js +++ b/chat/public/admin.js @@ -1,6 +1,6 @@ (() => { - const DEFAULT_PROVIDERS = ['openrouter', 'mistral', 'google', 'groq', 'nvidia', 'chutes', 'opencode']; - const PLANNING_PROVIDERS = ['openrouter', 'mistral', 'google', 'groq', 'nvidia', 'chutes', 'ollama']; + const DEFAULT_PROVIDERS = ['openrouter', 'mistral', 'google', 'groq', 'nvidia', 'chutes', 'cerebras', 'ollama', 'opencode']; + const PLANNING_PROVIDERS = ['openrouter', 'mistral', 'google', 'groq', 'nvidia', 'chutes', 'cerebras', 'ollama']; const pageType = document?.body?.dataset?.page || 'build'; console.log('Admin JS loaded, pageType:', pageType); const state = { @@ -68,8 +68,10 @@ limitModel: document.getElementById('limit-model'), limitModelInput: document.getElementById('limit-model-input'), limitTpm: document.getElementById('limit-tpm'), + limitTph: document.getElementById('limit-tph'), limitTpd: document.getElementById('limit-tpd'), limitRpm: document.getElementById('limit-rpm'), + limitRph: document.getElementById('limit-rph'), limitRpd: document.getElementById('limit-rpd'), limitBackup: document.getElementById('limit-backup'), providerLimitStatus: document.getElementById('provider-limit-status'), @@ -1354,8 +1356,10 @@ el.limitModel.value = selectedScope === 'model' ? (modelKey || '') : ''; } if (el.limitTpm) el.limitTpm.value = target.tokensPerMinute ?? ''; + if (el.limitTph) el.limitTph.value = target.tokensPerHour ?? ''; if (el.limitTpd) el.limitTpd.value = target.tokensPerDay ?? ''; if (el.limitRpm) el.limitRpm.value = target.requestsPerMinute ?? ''; + if (el.limitRph) el.limitRph.value = target.requestsPerHour ?? ''; if (el.limitRpd) el.limitRpd.value = target.requestsPerDay ?? ''; if (el.limitBackup && state.opencodeBackupModel !== undefined) el.limitBackup.value = state.opencodeBackupModel || ''; } @@ -2304,8 +2308,10 @@ scope, model: (pageType === 'plan' && el.limitModelInput) ? el.limitModelInput.value.trim() : el.limitModel.value.trim(), tokensPerMinute: Number(el.limitTpm.value || 0), + tokensPerHour: Number(el.limitTph.value || 0), tokensPerDay: Number(el.limitTpd.value || 0), requestsPerMinute: Number(el.limitRpm.value || 0), + requestsPerHour: Number(el.limitRph.value || 0), requestsPerDay: Number(el.limitRpd.value || 0), opencodeBackupModel: el.limitBackup.value.trim(), }; diff --git a/chat/server.js b/chat/server.js index 9d707a9..d09049a 100644 --- a/chat/server.js +++ b/chat/server.js @@ -324,8 +324,9 @@ const TOPUP_PRICES = { const BILLING_CYCLES = ['monthly', 'yearly']; const SUPPORTED_CURRENCIES = ['usd', 'gbp', 'eur']; const MINUTE_MS = 60_000; -const DODO_PRODUCTS_CACHE_TTL_MS = Math.max(30_000, Number(process.env.DODO_PRODUCTS_CACHE_TTL_MS || 5 * MINUTE_MS)); +const HOUR_MS = 3_600_000; const DAY_MS = 86_400_000; +const DODO_PRODUCTS_CACHE_TTL_MS = Math.max(30_000, Number(process.env.DODO_PRODUCTS_CACHE_TTL_MS || 5 * MINUTE_MS)); const FORTY_EIGHT_HOURS_MS = 48 * DAY_MS; const AVG_CHARS_PER_TOKEN = 4; // rough heuristic const MAX_JSON_BODY_SIZE = Number(process.env.MAX_JSON_BODY_SIZE || 6_000_000); // 6 MB default for JSON payloads (attachments) @@ -516,7 +517,7 @@ const PLAN_PRICES = { }; const AUTO_MODEL_TOKEN = 'auto'; const DEFAULT_PROVIDER_FALLBACK = 'opencode'; -const DEFAULT_PROVIDER_SEEDS = ['openrouter', 'mistral', 'google', 'groq', 'nvidia', 'chutes', DEFAULT_PROVIDER_FALLBACK]; +const DEFAULT_PROVIDER_SEEDS = ['openrouter', 'mistral', 'google', 'groq', 'nvidia', 'chutes', 'cerebras', 'ollama', DEFAULT_PROVIDER_FALLBACK]; const PROVIDER_PERSIST_DEBOUNCE_MS = 200; const TOKEN_ESTIMATION_BUFFER = 400; const BOOST_PACK_SIZE = 500_000; @@ -5728,8 +5729,10 @@ function defaultProviderLimit(provider) { provider: normalizeProviderName(provider), scope: 'provider', tokensPerMinute: 0, + tokensPerHour: 0, tokensPerDay: 0, requestsPerMinute: 0, + requestsPerHour: 0, requestsPerDay: 0, perModel: {}, }; @@ -5771,16 +5774,20 @@ function ensureProviderLimitDefaults(provider) { const cfg = providerLimits.limits[key]; cfg.scope = cfg.scope === 'model' ? 'model' : 'provider'; cfg.tokensPerMinute = sanitizeLimitNumber(cfg.tokensPerMinute); + cfg.tokensPerHour = sanitizeLimitNumber(cfg.tokensPerHour); cfg.tokensPerDay = sanitizeLimitNumber(cfg.tokensPerDay); cfg.requestsPerMinute = sanitizeLimitNumber(cfg.requestsPerMinute); + cfg.requestsPerHour = sanitizeLimitNumber(cfg.requestsPerHour); cfg.requestsPerDay = sanitizeLimitNumber(cfg.requestsPerDay); cfg.perModel = cfg.perModel && typeof cfg.perModel === 'object' ? cfg.perModel : {}; Object.keys(cfg.perModel).forEach((model) => { const entry = cfg.perModel[model] || {}; cfg.perModel[model] = { tokensPerMinute: sanitizeLimitNumber(entry.tokensPerMinute), + tokensPerHour: sanitizeLimitNumber(entry.tokensPerHour), tokensPerDay: sanitizeLimitNumber(entry.tokensPerDay), requestsPerMinute: sanitizeLimitNumber(entry.requestsPerMinute), + requestsPerHour: sanitizeLimitNumber(entry.requestsPerHour), requestsPerDay: sanitizeLimitNumber(entry.requestsPerDay), }; }); @@ -6927,13 +6934,16 @@ function summarizeProviderUsage(provider, model) { const key = normalizeUsageProvider(provider, model); const now = Date.now(); const minuteAgo = now - MINUTE_MS; + const hourAgo = now - HOUR_MS; const dayAgo = now - DAY_MS; const entries = ensureProviderUsageBucket(key); const filterByModel = !!(model && providerLimits.limits[key] && providerLimits.limits[key].scope === 'model'); const result = { tokensLastMinute: 0, + tokensLastHour: 0, tokensLastDay: 0, requestsLastMinute: 0, + requestsLastHour: 0, requestsLastDay: 0, perModel: {}, }; @@ -6943,22 +6953,31 @@ function summarizeProviderUsage(provider, model) { const matchesModel = !filterByModel || (entry.model && model && entry.model === model); const targetKey = entry.model || 'unknown'; const isMinute = entry.ts >= minuteAgo; + const isHour = entry.ts >= hourAgo; const isDay = entry.ts >= dayAgo; if (isMinute && matchesModel) { result.tokensLastMinute += Number(entry.tokens || 0); result.requestsLastMinute += Number(entry.requests || 0); } + if (isHour && matchesModel) { + result.tokensLastHour += Number(entry.tokens || 0); + result.requestsLastHour += Number(entry.requests || 0); + } if (isDay && matchesModel) { result.tokensLastDay += Number(entry.tokens || 0); result.requestsLastDay += Number(entry.requests || 0); } if (!result.perModel[targetKey]) { - result.perModel[targetKey] = { tokensLastMinute: 0, tokensLastDay: 0, requestsLastMinute: 0, requestsLastDay: 0 }; + result.perModel[targetKey] = { tokensLastMinute: 0, tokensLastHour: 0, tokensLastDay: 0, requestsLastMinute: 0, requestsLastHour: 0, requestsLastDay: 0 }; } if (isMinute) { result.perModel[targetKey].tokensLastMinute += Number(entry.tokens || 0); result.perModel[targetKey].requestsLastMinute += Number(entry.requests || 0); } + if (isHour) { + result.perModel[targetKey].tokensLastHour += Number(entry.tokens || 0); + result.perModel[targetKey].requestsLastHour += Number(entry.requests || 0); + } if (isDay) { result.perModel[targetKey].tokensLastDay += Number(entry.tokens || 0); result.perModel[targetKey].requestsLastDay += Number(entry.requests || 0); @@ -6975,8 +6994,10 @@ function isProviderLimited(provider, model) { const modelCfg = (cfg.scope === 'model' && model && cfg.perModel[model]) ? cfg.perModel[model] : cfg; const checks = [ ['tokensPerMinute', usage.tokensLastMinute, 'minute tokens'], + ['tokensPerHour', usage.tokensLastHour, 'hourly tokens'], ['tokensPerDay', usage.tokensLastDay, 'daily tokens'], ['requestsPerMinute', usage.requestsLastMinute, 'minute requests'], + ['requestsPerHour', usage.requestsLastHour, 'hourly requests'], ['requestsPerDay', usage.requestsLastDay, 'daily requests'], ]; for (const [field, used, label] of checks) { @@ -14530,7 +14551,7 @@ async function handleAdminProviderLimitsPost(req, res) { const targetModel = (body.model || '').trim(); const target = cfg.scope === 'model' && targetModel ? (cfg.perModel[targetModel] = cfg.perModel[targetModel] || {}) : cfg; - ['tokensPerMinute', 'tokensPerDay', 'requestsPerMinute', 'requestsPerDay'].forEach((field) => { + ['tokensPerMinute', 'tokensPerHour', 'tokensPerDay', 'requestsPerMinute', 'requestsPerHour', 'requestsPerDay'].forEach((field) => { if (body[field] !== undefined) target[field] = sanitizeLimitNumber(body[field]); });