Add back sections 2, 3, 4: Auto Model, Provider Limits, and supporting functionality

- Section 2: Auto Model for Hobby/Free Plan form and handlers
- Section 3: Provider Limits & Usage with configurable rate limits
- Added state management for planSettings and providerLimits
- Added API integration for plan-settings and provider-limits endpoints
- Added populateAutoModelSelect and updateLimitModelOptions functions
- Added renderProviderUsage to display usage data
This commit is contained in:
southseact-3d
2026-02-18 16:39:08 +00:00
parent ce81f1c999
commit b635c80d51
2 changed files with 292 additions and 2 deletions

View File

@@ -176,7 +176,98 @@
<div id="public-models-list" class="admin-list"></div>
</div>
<!-- Other Admin Sections -->
<!-- Section 2: Auto Model for Hobby/Free Plan -->
<div class="admin-card" style="margin-top: 16px;">
<header>
<h3>Auto Model for Hobby/Free Plan</h3>
<div class="pill">Free Plan</div>
</header>
<p style="margin-top:0; color: var(--muted);">Select which model Hobby and Free plan users will automatically use. Paid plan users can select their own models.</p>
<form id="auto-model-form" class="admin-form">
<label>
Model for hobby/free users
<select id="auto-model-select">
<option value="">Auto (use first configured model)</option>
</select>
</label>
<div class="admin-actions">
<button type="submit" class="primary">Save auto model</button>
</div>
<div class="status-line" id="auto-model-status"></div>
</form>
</div>
<!-- Section 3: Provider Limits & Usage -->
<div class="admin-grid" style="margin-top: 16px;">
<div class="admin-card">
<header>
<h3>Provider Limits & Usage</h3>
<div class="pill">Rate limits</div>
</header>
<p style="margin-top:0; color: var(--muted);">Configure token/request limits per provider or per model and monitor current usage.</p>
<form id="provider-limit-form" class="admin-form">
<label>
Provider
<select id="limit-provider">
<option value="openrouter">OpenRouter</option>
<option value="mistral">Mistral</option>
<option value="google">Google</option>
<option value="groq">Groq</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="cohere">Cohere</option>
</select>
</label>
<label>
Scope
<select id="limit-scope">
<option value="provider">Per Provider</option>
<option value="model">Per Model</option>
</select>
</label>
<label>
Model (for per-model limits)
<select id="limit-model">
<option value="">Any model</option>
</select>
</label>
<label>
Tokens per minute
<input id="limit-tpm" type="number" min="0" step="1" placeholder="0 = unlimited" />
</label>
<label>
Tokens per hour
<input id="limit-tph" type="number" min="0" step="1" placeholder="0 = unlimited" />
</label>
<label>
Tokens per day
<input id="limit-tpd" type="number" min="0" step="1" placeholder="0 = unlimited" />
</label>
<label>
Requests per minute
<input id="limit-rpm" type="number" min="0" step="1" placeholder="0 = unlimited" />
</label>
<label>
Requests per hour
<input id="limit-rph" type="number" min="0" step="1" placeholder="0 = unlimited" />
</label>
<label>
Requests per day
<input id="limit-rpd" type="number" min="0" step="1" placeholder="0 = unlimited" />
</label>
<div class="admin-actions">
<button type="submit" class="primary">Save limits</button>
</div>
<div class="status-line" id="provider-limit-status"></div>
</form>
<div id="provider-usage" class="admin-list"></div>
</div>
</div>
<!-- Section 4: Other Admin Sections -->
<div class="admin-grid" style="margin-top: 16px;">
<div class="admin-card">
<header>

View File

@@ -2,12 +2,15 @@
const pageType = document?.body?.dataset?.page || 'build';
console.log('Admin JS loaded, pageType:', pageType);
// Clean state structure - just two things
// State structure
const state = {
opencodeModels: [], // Models from OpenCode (order determines fallback)
publicModels: [], // User-facing models (completely separate)
icons: [],
availableOpencodeModels: [], // Loaded from OpenCode
planSettings: { provider: 'openrouter', freePlanModel: '', planningChain: [] },
providerLimits: {},
providerUsage: [],
};
// Element references
@@ -35,6 +38,25 @@
publicModelsList: document.getElementById('public-models-list'),
publicModelsCount: document.getElementById('public-models-count'),
// Auto Model Form
autoModelForm: document.getElementById('auto-model-form'),
autoModelSelect: document.getElementById('auto-model-select'),
autoModelStatus: document.getElementById('auto-model-status'),
// Provider Limits Form
providerLimitForm: document.getElementById('provider-limit-form'),
limitProvider: document.getElementById('limit-provider'),
limitScope: document.getElementById('limit-scope'),
limitModel: document.getElementById('limit-model'),
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'),
providerLimitStatus: document.getElementById('provider-limit-status'),
providerUsage: document.getElementById('provider-usage'),
// Other
iconList: document.getElementById('icon-list'),
adminRefresh: document.getElementById('admin-refresh'),
@@ -370,10 +392,101 @@
}
// Initialize
// Load plan settings
async function loadPlanSettings() {
try {
const data = await api('/api/admin/plan-settings');
state.planSettings = data || { provider: 'openrouter', freePlanModel: '', planningChain: [] };
populateAutoModelSelect();
if (el.autoModelSelect && state.planSettings.freePlanModel) {
el.autoModelSelect.value = state.planSettings.freePlanModel;
}
} catch (err) {
console.error('Failed to load plan settings:', err);
}
}
// Populate auto model select dropdown
function populateAutoModelSelect() {
if (!el.autoModelSelect) return;
const currentValue = el.autoModelSelect.value;
el.autoModelSelect.innerHTML = '<option value="">Auto (use first configured model)</option>';
state.opencodeModels.forEach((m) => {
const opt = document.createElement('option');
opt.value = m.name;
opt.textContent = `${m.label || m.name} (${m.name})`;
el.autoModelSelect.appendChild(opt);
});
el.autoModelSelect.value = currentValue;
}
// Load provider limits
async function loadProviderLimits() {
try {
const data = await api('/api/admin/provider-limits');
state.providerLimits = data.limits || {};
state.providerUsage = data.usage || [];
renderProviderUsage();
} catch (err) {
console.error('Failed to load provider limits:', err);
}
}
// Render provider usage
function renderProviderUsage() {
if (!el.providerUsage) return;
el.providerUsage.innerHTML = '';
if (!state.providerUsage.length) {
el.providerUsage.innerHTML = '<div class="muted">No usage data available.</div>';
return;
}
state.providerUsage.forEach((usage) => {
const row = document.createElement('div');
row.className = 'admin-row';
row.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center;">
<span class="pill">${usage.provider}</span>
<span>${usage.tokens || 0} tokens / ${usage.requests || 0} requests</span>
</div>
`;
el.providerUsage.appendChild(row);
});
}
// Update limit model options based on provider selection
function updateLimitModelOptions() {
if (!el.limitModel || !el.limitProvider) return;
const provider = el.limitProvider.value;
const currentValue = el.limitModel.value;
el.limitModel.innerHTML = '<option value="">Any model</option>';
// Add models from opencodeModels that match this provider
state.opencodeModels.forEach((m) => {
if (m.name && m.name.includes('/')) {
const modelProvider = m.name.split('/')[0];
if (modelProvider === provider) {
const opt = document.createElement('option');
opt.value = m.name;
opt.textContent = m.label || m.name;
el.limitModel.appendChild(opt);
}
}
});
el.limitModel.value = currentValue;
}
async function init() {
await loadIcons();
await loadAvailableOpencodeModels();
await loadModels();
await loadPlanSettings();
await loadProviderLimits();
}
if (el.adminRefresh) {
@@ -399,5 +512,91 @@
});
}
// Auto Model Form Handler
if (el.autoModelForm) {
el.autoModelForm.addEventListener('submit', async (e) => {
e.preventDefault();
const selectedModel = el.autoModelSelect.value;
try {
await api('/api/admin/plan-settings', {
method: 'POST',
body: JSON.stringify({
...state.planSettings,
freePlanModel: selectedModel,
}),
});
setStatus(el.autoModelStatus, 'Saved');
setTimeout(() => setStatus(el.autoModelStatus, ''), 1500);
} catch (err) {
setStatus(el.autoModelStatus, err.message, true);
}
});
}
// Provider Limit Form Handler
if (el.providerLimitForm) {
// Update model options when provider changes
el.limitProvider?.addEventListener('change', () => {
updateLimitModelOptions();
// Load existing limits for this provider if any
const provider = el.limitProvider.value;
const scope = el.limitScope.value;
const model = el.limitModel.value;
const limits = state.providerLimits[provider];
if (limits) {
const target = scope === 'model' && model ? (limits.perModel?.[model] || {}) : limits;
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 || '';
}
});
// Update form when scope changes
el.limitScope?.addEventListener('change', () => {
if (el.limitModel) {
el.limitModel.disabled = el.limitScope.value !== 'model';
if (el.limitScope.value !== 'model') el.limitModel.value = '';
}
});
el.providerLimitForm.addEventListener('submit', async (e) => {
e.preventDefault();
const provider = el.limitProvider?.value;
const scope = el.limitScope?.value;
const model = el.limitModel?.value;
const limits = {
tokensPerMinute: parseInt(el.limitTpm?.value) || 0,
tokensPerHour: parseInt(el.limitTph?.value) || 0,
tokensPerDay: parseInt(el.limitTpd?.value) || 0,
requestsPerMinute: parseInt(el.limitRpm?.value) || 0,
requestsPerHour: parseInt(el.limitRph?.value) || 0,
requestsPerDay: parseInt(el.limitRpd?.value) || 0,
};
try {
const payload = {
provider,
scope,
model: scope === 'model' ? model : null,
limits,
};
await api('/api/admin/provider-limits', {
method: 'POST',
body: JSON.stringify(payload),
});
setStatus(el.providerLimitStatus, 'Saved');
await loadProviderLimits();
setTimeout(() => setStatus(el.providerLimitStatus, ''), 1500);
} catch (err) {
setStatus(el.providerLimitStatus, err.message, true);
}
});
}
init();
})();