Add hourly rate limits (tokens/hour, requests/hour) and missing providers (chutes, cerebras, ollama)
This commit is contained in:
@@ -187,6 +187,9 @@
|
|||||||
<option value="google">Google</option>
|
<option value="google">Google</option>
|
||||||
<option value="groq">Groq</option>
|
<option value="groq">Groq</option>
|
||||||
<option value="nvidia">NVIDIA</option>
|
<option value="nvidia">NVIDIA</option>
|
||||||
|
<option value="chutes">Chutes</option>
|
||||||
|
<option value="cerebras">Cerebras</option>
|
||||||
|
<option value="ollama">Ollama</option>
|
||||||
<option value="opencode">OpenCode</option>
|
<option value="opencode">OpenCode</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
@@ -207,6 +210,10 @@
|
|||||||
Tokens per minute
|
Tokens per minute
|
||||||
<input id="limit-tpm" type="number" min="0" step="1" placeholder="0 = unlimited" />
|
<input id="limit-tpm" type="number" min="0" step="1" placeholder="0 = unlimited" />
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
Tokens per hour
|
||||||
|
<input id="limit-tph" type="number" min="0" step="1" placeholder="0 = unlimited" />
|
||||||
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Tokens per day
|
Tokens per day
|
||||||
<input id="limit-tpd" type="number" min="0" step="1" placeholder="0 = unlimited" />
|
<input id="limit-tpd" type="number" min="0" step="1" placeholder="0 = unlimited" />
|
||||||
@@ -215,6 +222,10 @@
|
|||||||
Requests per minute
|
Requests per minute
|
||||||
<input id="limit-rpm" type="number" min="0" step="1" placeholder="0 = unlimited" />
|
<input id="limit-rpm" type="number" min="0" step="1" placeholder="0 = unlimited" />
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
Requests per hour
|
||||||
|
<input id="limit-rph" type="number" min="0" step="1" placeholder="0 = unlimited" />
|
||||||
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Requests per day
|
Requests per day
|
||||||
<input id="limit-rpd" type="number" min="0" step="1" placeholder="0 = unlimited" />
|
<input id="limit-rpd" type="number" min="0" step="1" placeholder="0 = unlimited" />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
(() => {
|
(() => {
|
||||||
const DEFAULT_PROVIDERS = ['openrouter', 'mistral', 'google', 'groq', 'nvidia', 'chutes', 'opencode'];
|
const DEFAULT_PROVIDERS = ['openrouter', 'mistral', 'google', 'groq', 'nvidia', 'chutes', 'cerebras', 'ollama', 'opencode'];
|
||||||
const PLANNING_PROVIDERS = ['openrouter', 'mistral', 'google', 'groq', 'nvidia', 'chutes', 'ollama'];
|
const PLANNING_PROVIDERS = ['openrouter', 'mistral', 'google', 'groq', 'nvidia', 'chutes', 'cerebras', 'ollama'];
|
||||||
const pageType = document?.body?.dataset?.page || 'build';
|
const pageType = document?.body?.dataset?.page || 'build';
|
||||||
console.log('Admin JS loaded, pageType:', pageType);
|
console.log('Admin JS loaded, pageType:', pageType);
|
||||||
const state = {
|
const state = {
|
||||||
@@ -68,8 +68,10 @@
|
|||||||
limitModel: document.getElementById('limit-model'),
|
limitModel: document.getElementById('limit-model'),
|
||||||
limitModelInput: document.getElementById('limit-model-input'),
|
limitModelInput: document.getElementById('limit-model-input'),
|
||||||
limitTpm: document.getElementById('limit-tpm'),
|
limitTpm: document.getElementById('limit-tpm'),
|
||||||
|
limitTph: document.getElementById('limit-tph'),
|
||||||
limitTpd: document.getElementById('limit-tpd'),
|
limitTpd: document.getElementById('limit-tpd'),
|
||||||
limitRpm: document.getElementById('limit-rpm'),
|
limitRpm: document.getElementById('limit-rpm'),
|
||||||
|
limitRph: document.getElementById('limit-rph'),
|
||||||
limitRpd: document.getElementById('limit-rpd'),
|
limitRpd: document.getElementById('limit-rpd'),
|
||||||
limitBackup: document.getElementById('limit-backup'),
|
limitBackup: document.getElementById('limit-backup'),
|
||||||
providerLimitStatus: document.getElementById('provider-limit-status'),
|
providerLimitStatus: document.getElementById('provider-limit-status'),
|
||||||
@@ -1354,8 +1356,10 @@
|
|||||||
el.limitModel.value = selectedScope === 'model' ? (modelKey || '') : '';
|
el.limitModel.value = selectedScope === 'model' ? (modelKey || '') : '';
|
||||||
}
|
}
|
||||||
if (el.limitTpm) el.limitTpm.value = target.tokensPerMinute ?? '';
|
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.limitTpd) el.limitTpd.value = target.tokensPerDay ?? '';
|
||||||
if (el.limitRpm) el.limitRpm.value = target.requestsPerMinute ?? '';
|
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.limitRpd) el.limitRpd.value = target.requestsPerDay ?? '';
|
||||||
if (el.limitBackup && state.opencodeBackupModel !== undefined) el.limitBackup.value = state.opencodeBackupModel || '';
|
if (el.limitBackup && state.opencodeBackupModel !== undefined) el.limitBackup.value = state.opencodeBackupModel || '';
|
||||||
}
|
}
|
||||||
@@ -2304,8 +2308,10 @@
|
|||||||
scope,
|
scope,
|
||||||
model: (pageType === 'plan' && el.limitModelInput) ? el.limitModelInput.value.trim() : el.limitModel.value.trim(),
|
model: (pageType === 'plan' && el.limitModelInput) ? el.limitModelInput.value.trim() : el.limitModel.value.trim(),
|
||||||
tokensPerMinute: Number(el.limitTpm.value || 0),
|
tokensPerMinute: Number(el.limitTpm.value || 0),
|
||||||
|
tokensPerHour: Number(el.limitTph.value || 0),
|
||||||
tokensPerDay: Number(el.limitTpd.value || 0),
|
tokensPerDay: Number(el.limitTpd.value || 0),
|
||||||
requestsPerMinute: Number(el.limitRpm.value || 0),
|
requestsPerMinute: Number(el.limitRpm.value || 0),
|
||||||
|
requestsPerHour: Number(el.limitRph.value || 0),
|
||||||
requestsPerDay: Number(el.limitRpd.value || 0),
|
requestsPerDay: Number(el.limitRpd.value || 0),
|
||||||
opencodeBackupModel: el.limitBackup.value.trim(),
|
opencodeBackupModel: el.limitBackup.value.trim(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -324,8 +324,9 @@ const TOPUP_PRICES = {
|
|||||||
const BILLING_CYCLES = ['monthly', 'yearly'];
|
const BILLING_CYCLES = ['monthly', 'yearly'];
|
||||||
const SUPPORTED_CURRENCIES = ['usd', 'gbp', 'eur'];
|
const SUPPORTED_CURRENCIES = ['usd', 'gbp', 'eur'];
|
||||||
const MINUTE_MS = 60_000;
|
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 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 FORTY_EIGHT_HOURS_MS = 48 * DAY_MS;
|
||||||
const AVG_CHARS_PER_TOKEN = 4; // rough heuristic
|
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)
|
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 AUTO_MODEL_TOKEN = 'auto';
|
||||||
const DEFAULT_PROVIDER_FALLBACK = 'opencode';
|
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 PROVIDER_PERSIST_DEBOUNCE_MS = 200;
|
||||||
const TOKEN_ESTIMATION_BUFFER = 400;
|
const TOKEN_ESTIMATION_BUFFER = 400;
|
||||||
const BOOST_PACK_SIZE = 500_000;
|
const BOOST_PACK_SIZE = 500_000;
|
||||||
@@ -5728,8 +5729,10 @@ function defaultProviderLimit(provider) {
|
|||||||
provider: normalizeProviderName(provider),
|
provider: normalizeProviderName(provider),
|
||||||
scope: 'provider',
|
scope: 'provider',
|
||||||
tokensPerMinute: 0,
|
tokensPerMinute: 0,
|
||||||
|
tokensPerHour: 0,
|
||||||
tokensPerDay: 0,
|
tokensPerDay: 0,
|
||||||
requestsPerMinute: 0,
|
requestsPerMinute: 0,
|
||||||
|
requestsPerHour: 0,
|
||||||
requestsPerDay: 0,
|
requestsPerDay: 0,
|
||||||
perModel: {},
|
perModel: {},
|
||||||
};
|
};
|
||||||
@@ -5771,16 +5774,20 @@ function ensureProviderLimitDefaults(provider) {
|
|||||||
const cfg = providerLimits.limits[key];
|
const cfg = providerLimits.limits[key];
|
||||||
cfg.scope = cfg.scope === 'model' ? 'model' : 'provider';
|
cfg.scope = cfg.scope === 'model' ? 'model' : 'provider';
|
||||||
cfg.tokensPerMinute = sanitizeLimitNumber(cfg.tokensPerMinute);
|
cfg.tokensPerMinute = sanitizeLimitNumber(cfg.tokensPerMinute);
|
||||||
|
cfg.tokensPerHour = sanitizeLimitNumber(cfg.tokensPerHour);
|
||||||
cfg.tokensPerDay = sanitizeLimitNumber(cfg.tokensPerDay);
|
cfg.tokensPerDay = sanitizeLimitNumber(cfg.tokensPerDay);
|
||||||
cfg.requestsPerMinute = sanitizeLimitNumber(cfg.requestsPerMinute);
|
cfg.requestsPerMinute = sanitizeLimitNumber(cfg.requestsPerMinute);
|
||||||
|
cfg.requestsPerHour = sanitizeLimitNumber(cfg.requestsPerHour);
|
||||||
cfg.requestsPerDay = sanitizeLimitNumber(cfg.requestsPerDay);
|
cfg.requestsPerDay = sanitizeLimitNumber(cfg.requestsPerDay);
|
||||||
cfg.perModel = cfg.perModel && typeof cfg.perModel === 'object' ? cfg.perModel : {};
|
cfg.perModel = cfg.perModel && typeof cfg.perModel === 'object' ? cfg.perModel : {};
|
||||||
Object.keys(cfg.perModel).forEach((model) => {
|
Object.keys(cfg.perModel).forEach((model) => {
|
||||||
const entry = cfg.perModel[model] || {};
|
const entry = cfg.perModel[model] || {};
|
||||||
cfg.perModel[model] = {
|
cfg.perModel[model] = {
|
||||||
tokensPerMinute: sanitizeLimitNumber(entry.tokensPerMinute),
|
tokensPerMinute: sanitizeLimitNumber(entry.tokensPerMinute),
|
||||||
|
tokensPerHour: sanitizeLimitNumber(entry.tokensPerHour),
|
||||||
tokensPerDay: sanitizeLimitNumber(entry.tokensPerDay),
|
tokensPerDay: sanitizeLimitNumber(entry.tokensPerDay),
|
||||||
requestsPerMinute: sanitizeLimitNumber(entry.requestsPerMinute),
|
requestsPerMinute: sanitizeLimitNumber(entry.requestsPerMinute),
|
||||||
|
requestsPerHour: sanitizeLimitNumber(entry.requestsPerHour),
|
||||||
requestsPerDay: sanitizeLimitNumber(entry.requestsPerDay),
|
requestsPerDay: sanitizeLimitNumber(entry.requestsPerDay),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -6927,13 +6934,16 @@ function summarizeProviderUsage(provider, model) {
|
|||||||
const key = normalizeUsageProvider(provider, model);
|
const key = normalizeUsageProvider(provider, model);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const minuteAgo = now - MINUTE_MS;
|
const minuteAgo = now - MINUTE_MS;
|
||||||
|
const hourAgo = now - HOUR_MS;
|
||||||
const dayAgo = now - DAY_MS;
|
const dayAgo = now - DAY_MS;
|
||||||
const entries = ensureProviderUsageBucket(key);
|
const entries = ensureProviderUsageBucket(key);
|
||||||
const filterByModel = !!(model && providerLimits.limits[key] && providerLimits.limits[key].scope === 'model');
|
const filterByModel = !!(model && providerLimits.limits[key] && providerLimits.limits[key].scope === 'model');
|
||||||
const result = {
|
const result = {
|
||||||
tokensLastMinute: 0,
|
tokensLastMinute: 0,
|
||||||
|
tokensLastHour: 0,
|
||||||
tokensLastDay: 0,
|
tokensLastDay: 0,
|
||||||
requestsLastMinute: 0,
|
requestsLastMinute: 0,
|
||||||
|
requestsLastHour: 0,
|
||||||
requestsLastDay: 0,
|
requestsLastDay: 0,
|
||||||
perModel: {},
|
perModel: {},
|
||||||
};
|
};
|
||||||
@@ -6943,22 +6953,31 @@ function summarizeProviderUsage(provider, model) {
|
|||||||
const matchesModel = !filterByModel || (entry.model && model && entry.model === model);
|
const matchesModel = !filterByModel || (entry.model && model && entry.model === model);
|
||||||
const targetKey = entry.model || 'unknown';
|
const targetKey = entry.model || 'unknown';
|
||||||
const isMinute = entry.ts >= minuteAgo;
|
const isMinute = entry.ts >= minuteAgo;
|
||||||
|
const isHour = entry.ts >= hourAgo;
|
||||||
const isDay = entry.ts >= dayAgo;
|
const isDay = entry.ts >= dayAgo;
|
||||||
if (isMinute && matchesModel) {
|
if (isMinute && matchesModel) {
|
||||||
result.tokensLastMinute += Number(entry.tokens || 0);
|
result.tokensLastMinute += Number(entry.tokens || 0);
|
||||||
result.requestsLastMinute += Number(entry.requests || 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) {
|
if (isDay && matchesModel) {
|
||||||
result.tokensLastDay += Number(entry.tokens || 0);
|
result.tokensLastDay += Number(entry.tokens || 0);
|
||||||
result.requestsLastDay += Number(entry.requests || 0);
|
result.requestsLastDay += Number(entry.requests || 0);
|
||||||
}
|
}
|
||||||
if (!result.perModel[targetKey]) {
|
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) {
|
if (isMinute) {
|
||||||
result.perModel[targetKey].tokensLastMinute += Number(entry.tokens || 0);
|
result.perModel[targetKey].tokensLastMinute += Number(entry.tokens || 0);
|
||||||
result.perModel[targetKey].requestsLastMinute += Number(entry.requests || 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) {
|
if (isDay) {
|
||||||
result.perModel[targetKey].tokensLastDay += Number(entry.tokens || 0);
|
result.perModel[targetKey].tokensLastDay += Number(entry.tokens || 0);
|
||||||
result.perModel[targetKey].requestsLastDay += Number(entry.requests || 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 modelCfg = (cfg.scope === 'model' && model && cfg.perModel[model]) ? cfg.perModel[model] : cfg;
|
||||||
const checks = [
|
const checks = [
|
||||||
['tokensPerMinute', usage.tokensLastMinute, 'minute tokens'],
|
['tokensPerMinute', usage.tokensLastMinute, 'minute tokens'],
|
||||||
|
['tokensPerHour', usage.tokensLastHour, 'hourly tokens'],
|
||||||
['tokensPerDay', usage.tokensLastDay, 'daily tokens'],
|
['tokensPerDay', usage.tokensLastDay, 'daily tokens'],
|
||||||
['requestsPerMinute', usage.requestsLastMinute, 'minute requests'],
|
['requestsPerMinute', usage.requestsLastMinute, 'minute requests'],
|
||||||
|
['requestsPerHour', usage.requestsLastHour, 'hourly requests'],
|
||||||
['requestsPerDay', usage.requestsLastDay, 'daily requests'],
|
['requestsPerDay', usage.requestsLastDay, 'daily requests'],
|
||||||
];
|
];
|
||||||
for (const [field, used, label] of checks) {
|
for (const [field, used, label] of checks) {
|
||||||
@@ -14530,7 +14551,7 @@ async function handleAdminProviderLimitsPost(req, res) {
|
|||||||
|
|
||||||
const targetModel = (body.model || '').trim();
|
const targetModel = (body.model || '').trim();
|
||||||
const target = cfg.scope === 'model' && targetModel ? (cfg.perModel[targetModel] = cfg.perModel[targetModel] || {}) : cfg;
|
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]);
|
if (body[field] !== undefined) target[field] = sanitizeLimitNumber(body[field]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user