Separate OpenCode and Public model management in admin panel

- Add OpenCode Models section with dropdown selection from available models
- Add Public Models section with manual model ID input
- Both sections have up/down ordering buttons for fallback chain priority
- OpenCode models used for execution fallback when rate limits/errors occur
- Public models displayed in builder dropdown for user selection
- Remove unified provider chain in favor of two separate lists
- Keep all existing functionality: Auto Model, Provider Limits, Icon Library, etc.
This commit is contained in:
southseact-3d
2026-02-18 19:49:46 +00:00
parent f2d7b48743
commit d3205dafe5
3 changed files with 503 additions and 121 deletions

View File

@@ -68,17 +68,61 @@
</div>
<div class="admin-grid">
<!-- Public Facing Models Section -->
<!-- OpenCode Models Section -->
<div class="admin-card">
<header>
<h3>Add Public-Facing Model</h3>
<h3>Add OpenCode Model</h3>
<div class="pill">OpenCode</div>
</header>
<p style="margin-top:0; color: var(--muted);">These are the models available in OpenCode. They form the fallback chain for build execution. When rate limits are reached or errors occur, the system falls back to the next model in the list.</p>
<form id="opencode-model-form" class="admin-form">
<label>
Select model from OpenCode
<select id="opencode-model-select">
<option value="">-- Select a model --</option>
</select>
</label>
<label>
Display name shown to users
<input id="opencode-model-label" type="text" placeholder="Friendly label (e.g., Claude 3.5 Sonnet)" required />
</label>
<label>
Model tier (for plan limits)
<select id="opencode-model-tier">
<option value="free">Free (1x multiplier)</option>
<option value="plus">Plus (2x multiplier)</option>
<option value="pro">Pro (3x multiplier)</option>
</select>
</label>
<label>
Icon (files in /assets)
<select id="opencode-model-icon">
<option value="">No icon</option>
</select>
</label>
<label style="display: flex; align-items: center; gap: 8px; margin-top: 8px;">
<input id="opencode-model-media" type="checkbox" style="width: auto;" />
<span>Supports image uploads</span>
</label>
<div class="admin-actions">
<button type="submit" class="primary">Add OpenCode Model</button>
<button type="button" id="reload-opencode-models" class="ghost">Reload Models</button>
</div>
<div class="status-line" id="opencode-model-status"></div>
</form>
</div>
<!-- Public Models Section -->
<div class="admin-card">
<header>
<h3>Add Public Model</h3>
<div class="pill">Public</div>
</header>
<p style="margin-top:0; color: var(--muted);">These models are displayed to users in the builder dropdown. They all share the same unified provider fallback chain.</p>
<p style="margin-top:0; color: var(--muted);">These models are displayed to users in the builder dropdown for selection. This is separate from the OpenCode fallback chain.</p>
<form id="public-model-form" class="admin-form">
<label>
Model ID (e.g., claude-3-5-sonnet, gpt-4o)
<input id="public-model-name" type="text" placeholder="Model ID from OpenCode" required />
<input id="public-model-name" type="text" placeholder="Enter model ID manually" required />
</label>
<label>
Display name shown to users
@@ -108,50 +152,26 @@
<div class="status-line" id="public-model-status"></div>
</form>
</div>
<!-- Provider Model Chain Section -->
<div class="admin-card">
<header>
<h3>Provider Model Chain</h3>
<div class="pill">Backend</div>
</header>
<p style="margin-top:0; color: var(--muted);">This is the unified fallback chain used for ALL models. When rate limits are reached or errors occur, the system automatically falls back to the next provider in this chain.</p>
<form id="provider-chain-form" class="admin-form">
<label>
Provider
<select id="chain-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>
Model Name
<input id="chain-model" type="text" placeholder="e.g., anthropic/claude-3.5-sonnet" required />
</label>
<div class="admin-actions">
<button type="submit" class="primary">Add to Chain</button>
</div>
<div class="status-line" id="provider-chain-status"></div>
</form>
</div>
</div>
<!-- Provider Chain List with Ordering -->
<!-- OpenCode Models List with Ordering -->
<div class="admin-card" style="margin-top: 16px;">
<header>
<h3>Unified Provider Chain Order</h3>
<div class="pill" id="provider-chain-count">0</div>
<h3>OpenCode Models (Fallback Chain)</h3>
<div class="pill" id="opencode-models-count">0</div>
</header>
<p class="muted" style="margin-top:0;">Arrange the order of providers below. The system will try each provider in order when rate limits are reached or errors occur. The first provider is the primary.</p>
<div id="provider-chain-list" class="admin-list"></div>
<p class="muted" style="margin-top:0;">Arrange the order of OpenCode models below. When rate limits are reached or errors occur, the system falls back to the next model in this chain. The first model is the primary.</p>
<div id="opencode-models-list" class="admin-list"></div>
</div>
<!-- Public Models List with Ordering -->
<div class="admin-card" style="margin-top: 16px;">
<header>
<h3>Public Models</h3>
<div class="pill" id="public-models-count">0</div>
</header>
<p class="muted" style="margin-top:0;">These models are displayed to users in the builder dropdown. Arrange the order to set their display priority.</p>
<div id="public-models-list" class="admin-list"></div>
</div>
<div class="admin-grid" style="margin-top: 16px;">
@@ -329,16 +349,6 @@
<div id="provider-usage" class="admin-list"></div>
</div>
</div>
<!-- Public Models List -->
<div class="admin-card" style="margin-top: 16px;">
<header>
<h3>Public-Facing Models</h3>
<div class="pill" id="configured-count">0</div>
</header>
<p class="muted" style="margin-top:0;">These models are displayed to users in the builder. All models use the unified provider chain above for fallback.</p>
<div id="configured-list" class="admin-list"></div>
</div>
</main>
</div>
<script src="/admin.js"></script>

View File

@@ -6,8 +6,8 @@
const state = {
available: [],
configured: [],
opencodeModels: [], // Models from OpenCode (order determines fallback)
publicModels: [], // Public-facing models (completely separate)
opencodeModels: [], // Models from OpenCode (order determines fallback chain for execution)
publicModels: [], // Public-facing models (displayed to users in builder dropdown)
icons: [],
accounts: [],
affiliates: [],
@@ -22,7 +22,7 @@
};
const el = {
// OpenCode Models (new structure)
// OpenCode Models (fallback chain)
opencodeModelForm: document.getElementById('opencode-model-form'),
opencodeModelSelect: document.getElementById('opencode-model-select'),
opencodeModelLabel: document.getElementById('opencode-model-label'),
@@ -33,7 +33,7 @@
reloadOpencodeModels: document.getElementById('reload-opencode-models'),
opencodeModelsList: document.getElementById('opencode-models-list'),
opencodeModelsCount: document.getElementById('opencode-models-count'),
// Public Models (new structure)
// Public Models (user-facing selection)
publicModelForm: document.getElementById('public-model-form'),
publicModelName: document.getElementById('public-model-name'),
publicModelLabel: document.getElementById('public-model-label'),
@@ -43,6 +43,12 @@
publicModelStatus: document.getElementById('public-model-status'),
publicModelsList: document.getElementById('public-models-list'),
publicModelsCount: document.getElementById('public-models-count'),
// Provider chain (legacy - kept for compatibility)
providerChainForm: document.getElementById('provider-chain-form'),
chainProvider: document.getElementById('chain-provider'),
chainModel: document.getElementById('chain-model'),
providerChainList: document.getElementById('provider-chain-list'),
providerChainCount: document.getElementById('provider-chain-count'),
// Legacy elements (keep for compatibility)
availableModels: document.getElementById('available-models'),
displayLabel: document.getElementById('display-label'),
@@ -211,6 +217,12 @@
el.publicModelStatus.style.color = isError ? 'var(--danger)' : 'inherit';
}
function setOpencodeModelStatus(msg, isError = false) {
if (!el.opencodeModelStatus) return;
el.opencodeModelStatus.textContent = msg || '';
el.opencodeModelStatus.style.color = isError ? 'var(--danger)' : 'inherit';
}
function setProviderChainStatus(msg, isError = false) {
if (!el.providerChainStatus) return;
el.providerChainStatus.textContent = msg || '';
@@ -553,7 +565,22 @@
});
}
// Populate new public model icon select
// Populate OpenCode model icon select
if (el.opencodeModelIcon) {
el.opencodeModelIcon.innerHTML = '';
const none = document.createElement('option');
none.value = '';
none.textContent = 'No icon';
el.opencodeModelIcon.appendChild(none);
state.icons.forEach((iconPath) => {
const opt = document.createElement('option');
opt.value = iconPath;
opt.textContent = iconPath.replace('/assets/', '');
el.opencodeModelIcon.appendChild(opt);
});
}
// Populate public model icon select
if (el.publicModelIcon) {
el.publicModelIcon.innerHTML = '';
const none = document.createElement('option');
@@ -595,24 +622,21 @@
}
}
// Simplified renderConfigured for new publicModels structure
function renderConfigured() {
if (!el.configuredList) return;
el.configuredList.innerHTML = '';
// Render OpenCode models list with up/down ordering
function renderOpencodeModels() {
if (!el.opencodeModelsList) return;
el.opencodeModelsList.innerHTML = '';
if (el.opencodeModelsCount) el.opencodeModelsCount.textContent = state.opencodeModels.length.toString();
// Use publicModels if available, otherwise fall back to configured
const modelsToRender = state.publicModels.length > 0 ? state.publicModels : state.configured;
if (el.configuredCount) el.configuredCount.textContent = modelsToRender.length.toString();
if (!modelsToRender.length) {
if (!state.opencodeModels.length) {
const empty = document.createElement('div');
empty.className = 'muted';
empty.textContent = 'No public-facing models configured yet.';
el.configuredList.appendChild(empty);
empty.textContent = 'No OpenCode models configured. Add models to enable fallback chain for build execution.';
el.opencodeModelsList.appendChild(empty);
return;
}
modelsToRender.forEach((m) => {
state.opencodeModels.forEach((m, idx) => {
const row = document.createElement('div');
row.className = 'provider-row slim';
@@ -622,6 +646,13 @@
const info = document.createElement('div');
info.className = 'model-chip';
// Priority badge
const order = document.createElement('span');
order.className = 'pill';
order.textContent = idx === 0 ? 'Primary' : `#${idx + 1}`;
if (idx === 0) order.style.background = 'var(--shopify-green)';
info.appendChild(order);
if (m.icon) {
const img = document.createElement('img');
img.src = m.icon;
@@ -653,25 +684,232 @@
info.appendChild(mediaBadge);
}
// Show chain info badge
const chainBadge = document.createElement('span');
chainBadge.className = 'pill';
chainBadge.style.background = 'var(--primary)';
chainBadge.textContent = `Uses unified chain (${state.providerChain.length} providers)`;
info.appendChild(chainBadge);
header.appendChild(info);
const headerActions = document.createElement('div');
headerActions.className = 'provider-row-actions';
// Up button
const upBtn = document.createElement('button');
upBtn.className = 'ghost';
upBtn.textContent = '↑';
upBtn.title = 'Move up';
upBtn.disabled = idx === 0;
upBtn.addEventListener('click', async () => {
const next = [...state.opencodeModels];
const [item] = next.splice(idx, 1);
next.splice(Math.max(0, idx - 1), 0, item);
await persistOpencodeModelsOrder(next);
});
headerActions.appendChild(upBtn);
// Down button
const downBtn = document.createElement('button');
downBtn.className = 'ghost';
downBtn.textContent = '↓';
downBtn.title = 'Move down';
downBtn.disabled = idx === state.opencodeModels.length - 1;
downBtn.addEventListener('click', async () => {
const next = [...state.opencodeModels];
const [item] = next.splice(idx, 1);
next.splice(Math.min(state.opencodeModels.length, idx + 1), 0, item);
await persistOpencodeModelsOrder(next);
});
headerActions.appendChild(downBtn);
const delBtn = document.createElement('button');
delBtn.className = 'ghost';
delBtn.textContent = 'Delete';
delBtn.addEventListener('click', async () => {
delBtn.disabled = true;
try {
await api(`/api/admin/models/${m.id}`, { method: 'DELETE' });
await api(`/api/admin/models/${m.id}?type=opencode`, { method: 'DELETE' });
await loadConfigured();
} catch (err) {
setStatus(err.message, true);
}
delBtn.disabled = false;
});
headerActions.appendChild(delBtn);
// Inline icon editor button
const editIconBtn = document.createElement('button');
editIconBtn.className = 'ghost';
editIconBtn.textContent = 'Edit icon';
editIconBtn.addEventListener('click', () => {
let editor = header.querySelector('.icon-editor');
if (editor) return editor.remove();
editor = document.createElement('div');
editor.className = 'icon-editor';
const sel = document.createElement('select');
const none = document.createElement('option');
none.value = '';
none.textContent = 'No icon';
sel.appendChild(none);
(state.icons || []).forEach((iconPath) => {
const o = document.createElement('option');
o.value = iconPath;
o.textContent = iconPath.replace('/assets/', '');
sel.appendChild(o);
});
sel.value = m.icon || '';
editor.appendChild(sel);
const saveBtn = document.createElement('button');
saveBtn.className = 'primary';
saveBtn.textContent = 'Save';
saveBtn.addEventListener('click', async () => {
saveBtn.disabled = true;
try {
await persistOpencodeModelChanges(m.id, { icon: sel.value });
} catch (err) { setStatus(err.message, true); }
saveBtn.disabled = false;
});
editor.appendChild(saveBtn);
const cancelBtn = document.createElement('button');
cancelBtn.className = 'ghost';
cancelBtn.textContent = 'Cancel';
cancelBtn.addEventListener('click', () => editor.remove());
editor.appendChild(cancelBtn);
headerActions.appendChild(editor);
});
headerActions.appendChild(editIconBtn);
// Supports media checkbox
const mediaToggle = document.createElement('label');
mediaToggle.style.display = 'flex';
mediaToggle.style.alignItems = 'center';
mediaToggle.style.gap = '6px';
mediaToggle.style.marginLeft = '8px';
const mediaCheckbox = document.createElement('input');
mediaCheckbox.type = 'checkbox';
mediaCheckbox.checked = m.supportsMedia ?? false;
mediaCheckbox.addEventListener('change', async () => {
mediaCheckbox.disabled = true;
try {
await persistOpencodeModelChanges(m.id, { supportsMedia: mediaCheckbox.checked });
} catch (err) { setStatus(err.message, true); }
mediaCheckbox.disabled = false;
});
mediaToggle.appendChild(mediaCheckbox);
const mediaLabel = document.createElement('span');
mediaLabel.textContent = 'Media';
mediaLabel.style.fontSize = '12px';
mediaLabel.style.color = 'var(--muted)';
mediaToggle.appendChild(mediaLabel);
headerActions.appendChild(mediaToggle);
header.appendChild(headerActions);
row.appendChild(header);
el.opencodeModelsList.appendChild(row);
});
}
// Render Public models list with up/down ordering
function renderPublicModels() {
if (!el.publicModelsList) return;
el.publicModelsList.innerHTML = '';
if (el.publicModelsCount) el.publicModelsCount.textContent = state.publicModels.length.toString();
if (!state.publicModels.length) {
const empty = document.createElement('div');
empty.className = 'muted';
empty.textContent = 'No public models configured. Add models to display them to users in the builder.';
el.publicModelsList.appendChild(empty);
return;
}
state.publicModels.forEach((m, idx) => {
const row = document.createElement('div');
row.className = 'provider-row slim';
const header = document.createElement('div');
header.className = 'provider-row-header';
const info = document.createElement('div');
info.className = 'model-chip';
// Priority badge
const order = document.createElement('span');
order.className = 'pill';
order.textContent = idx === 0 ? 'Primary' : `#${idx + 1}`;
if (idx === 0) order.style.background = 'var(--shopify-green)';
info.appendChild(order);
if (m.icon) {
const img = document.createElement('img');
img.src = m.icon;
img.alt = '';
info.appendChild(img);
}
const label = document.createElement('span');
label.textContent = m.label || m.name;
info.appendChild(label);
const namePill = document.createElement('span');
namePill.className = 'pill';
namePill.textContent = m.name;
info.appendChild(namePill);
const tierMeta = document.createElement('span');
tierMeta.className = 'pill';
const tierName = (m.tier || 'free').toUpperCase();
const multiplier = m.tier === 'pro' ? 3 : (m.tier === 'plus' ? 2 : 1);
tierMeta.textContent = `${tierName} (${multiplier}x)`;
info.appendChild(tierMeta);
if (m.supportsMedia) {
const mediaBadge = document.createElement('span');
mediaBadge.className = 'pill';
mediaBadge.style.background = 'var(--shopify-green)';
mediaBadge.textContent = 'Media';
info.appendChild(mediaBadge);
}
header.appendChild(info);
const headerActions = document.createElement('div');
headerActions.className = 'provider-row-actions';
// Up button
const upBtn = document.createElement('button');
upBtn.className = 'ghost';
upBtn.textContent = '↑';
upBtn.title = 'Move up';
upBtn.disabled = idx === 0;
upBtn.addEventListener('click', async () => {
const next = [...state.publicModels];
const [item] = next.splice(idx, 1);
next.splice(Math.max(0, idx - 1), 0, item);
await persistPublicModelsOrder(next);
});
headerActions.appendChild(upBtn);
// Down button
const downBtn = document.createElement('button');
downBtn.className = 'ghost';
downBtn.textContent = '↓';
downBtn.title = 'Move down';
downBtn.disabled = idx === state.publicModels.length - 1;
downBtn.addEventListener('click', async () => {
const next = [...state.publicModels];
const [item] = next.splice(idx, 1);
next.splice(Math.min(state.publicModels.length, idx + 1), 0, item);
await persistPublicModelsOrder(next);
});
headerActions.appendChild(downBtn);
const delBtn = document.createElement('button');
delBtn.className = 'ghost';
delBtn.textContent = 'Delete';
delBtn.addEventListener('click', async () => {
delBtn.disabled = true;
try {
await api(`/api/admin/models/${m.id}?type=public`, { method: 'DELETE' });
await loadConfigured();
} catch (err) {
setStatus(err.message, true);
@@ -744,7 +982,7 @@
});
mediaToggle.appendChild(mediaCheckbox);
const mediaLabel = document.createElement('span');
mediaLabel.textContent = 'Supports image uploads';
mediaLabel.textContent = 'Media';
mediaLabel.style.fontSize = '12px';
mediaLabel.style.color = 'var(--muted)';
mediaToggle.appendChild(mediaLabel);
@@ -753,7 +991,7 @@
// Tier editor button
const editTierBtn = document.createElement('button');
editTierBtn.className = 'ghost';
editTierBtn.textContent = 'Edit tier/multiplier';
editTierBtn.textContent = 'Edit tier';
editTierBtn.addEventListener('click', () => {
let editor = header.querySelector('.tier-editor');
if (editor) return editor.remove();
@@ -799,10 +1037,74 @@
header.appendChild(headerActions);
row.appendChild(header);
el.configuredList.appendChild(row);
el.publicModelsList.appendChild(row);
});
}
// Legacy renderConfigured for backward compatibility
function renderConfigured() {
renderOpencodeModels();
renderPublicModels();
}
async function persistOpencodeModelChanges(modelId, changes) {
setStatus('Saving...');
const model = state.opencodeModels.find((m) => m.id === modelId);
if (!model) {
setStatus('Model not found', true);
return;
}
const payload = {
type: 'opencode',
id: modelId,
name: model.name,
label: model.label || model.name,
icon: changes.icon !== undefined ? changes.icon : model.icon,
tier: changes.tier !== undefined ? changes.tier : model.tier,
supportsMedia: changes.supportsMedia !== undefined ? changes.supportsMedia : model.supportsMedia,
};
try {
const data = await api('/api/admin/models', { method: 'POST', body: JSON.stringify(payload) });
const idx = state.opencodeModels.findIndex((m) => m.id === modelId);
if (idx >= 0) state.opencodeModels[idx] = { ...state.opencodeModels[idx], ...data.opencodeModel };
renderOpencodeModels();
setStatus('Saved');
setTimeout(() => setStatus(''), 1500);
} catch (err) {
setStatus(err.message, true);
}
}
async function persistOpencodeModelsOrder(nextModels) {
setStatus('Saving order...');
try {
const payload = { type: 'opencode', models: nextModels.map(m => ({ id: m.id })) };
const res = await api('/api/admin/models/reorder', { method: 'POST', body: JSON.stringify(payload) });
state.opencodeModels = res.opencodeModels || nextModels;
renderOpencodeModels();
setStatus('Saved');
setTimeout(() => setStatus(''), 1500);
} catch (err) {
setStatus(err.message, true);
}
}
async function persistPublicModelsOrder(nextModels) {
setStatus('Saving order...');
try {
const payload = { type: 'public', models: nextModels.map(m => ({ id: m.id })) };
const res = await api('/api/admin/models/reorder', { method: 'POST', body: JSON.stringify(payload) });
state.publicModels = res.publicModels || nextModels;
renderPublicModels();
setStatus('Saved');
setTimeout(() => setStatus(''), 1500);
} catch (err) {
setStatus(err.message, true);
}
}
async function persistPublicModelChanges(modelId, changes) {
setStatus('Saving...');
const model = state.publicModels.find((m) => m.id === modelId);
@@ -1281,11 +1583,12 @@
async function loadConfigured() {
const data = await api('/api/admin/models');
// Handle new unified structure
state.opencodeModels = data.opencodeModels || [];
state.publicModels = data.publicModels || [];
state.providerChain = data.providerChain || [];
state.configured = data.models || []; // Legacy support
renderConfigured();
renderProviderChain();
renderOpencodeModels();
renderPublicModels();
populateOpencodeModelSelect();
const selectedAutoModel = state.planSettings && typeof state.planSettings.freePlanModel === 'string'
? state.planSettings.freePlanModel
: (el.autoModelSelect ? el.autoModelSelect.value : '');
@@ -1295,6 +1598,37 @@
if (el.limitProvider) renderLimitModelOptions(el.limitProvider.value || 'openrouter');
}
function populateOpencodeModelSelect() {
if (!el.opencodeModelSelect) return;
el.opencodeModelSelect.innerHTML = '';
// Add placeholder
const placeholder = document.createElement('option');
placeholder.value = '';
placeholder.textContent = '-- Select a model --';
el.opencodeModelSelect.appendChild(placeholder);
// Add available models from OpenCode
(state.available || []).forEach((m) => {
const opt = document.createElement('option');
opt.value = m.name || m.id || m;
opt.textContent = m.label || m.name || m.id || m;
el.opencodeModelSelect.appendChild(opt);
});
// Also include any provider-specific models
Object.values(state.providerModels || {}).forEach((arr) => {
(arr || []).forEach((name) => {
if (name && !el.opencodeModelSelect.querySelector(`option[value="${name}"]`)) {
const opt = document.createElement('option');
opt.value = name;
opt.textContent = name;
el.opencodeModelSelect.appendChild(opt);
}
});
});
}
async function loadOpenRouterSettings() {
if (!el.orForm) return;
try {
@@ -1531,7 +1865,8 @@
return ['free', 'plus', 'pro'].includes(normalized) ? normalized : 'free';
};
const configured = Array.isArray(state.configured) ? state.configured : [];
// Use publicModels if available, fallback to configured for legacy support
const configured = state.publicModels.length > 0 ? state.publicModels : (Array.isArray(state.configured) ? state.configured : []);
const configuredByName = new Map();
configured.forEach((m) => {
const name = (m && (m.name || m.id)) ? String(m.name || m.id).trim() : '';
@@ -2232,7 +2567,60 @@
}
}
// New public model form handler
// OpenCode model form handler
if (el.opencodeModelForm) {
el.opencodeModelForm.addEventListener('submit', async (e) => {
e.preventDefault();
const name = el.opencodeModelSelect.value;
const label = el.opencodeModelLabel.value.trim();
const icon = el.opencodeModelIcon ? el.opencodeModelIcon.value : '';
const tier = el.opencodeModelTier ? el.opencodeModelTier.value : 'free';
const supportsMedia = el.opencodeModelMedia ? el.opencodeModelMedia.checked : false;
if (!name) {
setOpencodeModelStatus('Please select a model.', true);
return;
}
if (!label) {
setOpencodeModelStatus('Display name is required.', true);
return;
}
setOpencodeModelStatus('Saving...');
try {
await api('/api/admin/models', {
method: 'POST',
body: JSON.stringify({
type: 'opencode',
name,
label,
icon,
tier,
supportsMedia
}),
});
setOpencodeModelStatus('Saved');
el.opencodeModelSelect.value = '';
el.opencodeModelLabel.value = '';
await loadConfigured();
} catch (err) {
setOpencodeModelStatus(err.message, true);
}
});
}
// Reload OpenCode models button
if (el.reloadOpencodeModels) {
el.reloadOpencodeModels.addEventListener('click', async () => {
setOpencodeModelStatus('Reloading models...');
await loadAvailable();
populateOpencodeModelSelect();
setOpencodeModelStatus('Models reloaded');
setTimeout(() => setOpencodeModelStatus(''), 1500);
});
}
// Public model form handler
if (el.publicModelForm) {
el.publicModelForm.addEventListener('submit', async (e) => {
e.preventDefault();
@@ -2256,7 +2644,7 @@
await api('/api/admin/models', {
method: 'POST',
body: JSON.stringify({
type: 'publicModel',
type: 'public',
name,
label,
icon,
@@ -2274,37 +2662,6 @@
});
}
// New provider chain form handler
if (el.providerChainForm) {
el.providerChainForm.addEventListener('submit', async (e) => {
e.preventDefault();
const provider = el.chainProvider.value;
const model = el.chainModel.value.trim();
if (!model) {
setProviderChainStatus('Model name is required.', true);
return;
}
setProviderChainStatus('Adding to chain...');
try {
const newChain = [...state.providerChain, { provider, model }];
await api('/api/admin/models', {
method: 'POST',
body: JSON.stringify({
type: 'providerChain',
chain: newChain
}),
});
setProviderChainStatus('Added to chain');
el.chainModel.value = '';
await loadConfigured();
} catch (err) {
setProviderChainStatus(err.message, true);
}
});
}
if (el.modelForm) {
el.modelForm.addEventListener('submit', async (e) => {
e.preventDefault();