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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user