Add hourly rate limits (tokens/hour, requests/hour) and missing providers (chutes, cerebras, ollama)

This commit is contained in:
OpenCode Dev
2026-02-10 09:04:44 +00:00
parent 58bab1c5d8
commit 960ccb5742
3 changed files with 44 additions and 6 deletions

View File

@@ -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]);
});