Fix model sections: separate Provider Models and Public-Facing Models
- Provider Models section: restored OpenCode integration with dropdown - Public-Facing Models section: completely separate manual entry - Provider Chain section: fallback chain with up/down buttons (unchanged) - Added separate arrays: providerModels and publicModels - Added reorder support for both provider and public models - Updated server to handle providerModel type and reorder by type
This commit is contained in:
@@ -68,25 +68,25 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="admin-grid">
|
<div class="admin-grid">
|
||||||
<!-- Public Facing Models Section -->
|
<!-- Provider Models Section (with OpenCode integration) -->
|
||||||
<div class="admin-card">
|
<div class="admin-card">
|
||||||
<header>
|
<header>
|
||||||
<h3>Add Public-Facing Model</h3>
|
<h3>Add Provider Model</h3>
|
||||||
<div class="pill">Public</div>
|
<div class="pill">OpenCode</div>
|
||||||
</header>
|
</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);">Add models from OpenCode that will be available to users. These models use the unified provider chain for fallback.</p>
|
||||||
<form id="public-model-form" class="admin-form">
|
<form id="provider-model-form" class="admin-form">
|
||||||
<label>
|
<label>
|
||||||
Model ID (e.g., claude-3-5-sonnet, gpt-4o)
|
Choose a model from OpenCode
|
||||||
<input id="public-model-name" type="text" placeholder="Model ID from OpenCode" required />
|
<select id="available-models"></select>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Display name shown to users
|
Display name shown to users
|
||||||
<input id="public-model-label" type="text" placeholder="Friendly label (e.g., Claude 3.5 Sonnet)" required />
|
<input id="provider-model-label" type="text" placeholder="Friendly label (e.g., Claude 3.5 Sonnet)" required />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Model tier (for plan limits)
|
Model tier (for plan limits)
|
||||||
<select id="public-model-tier">
|
<select id="provider-model-tier">
|
||||||
<option value="free">Free (1x multiplier)</option>
|
<option value="free">Free (1x multiplier)</option>
|
||||||
<option value="plus">Plus (2x multiplier)</option>
|
<option value="plus">Plus (2x multiplier)</option>
|
||||||
<option value="pro">Pro (3x multiplier)</option>
|
<option value="pro">Pro (3x multiplier)</option>
|
||||||
@@ -94,28 +94,29 @@
|
|||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Icon (files in /assets)
|
Icon (files in /assets)
|
||||||
<select id="public-model-icon">
|
<select id="provider-model-icon">
|
||||||
<option value="">No icon</option>
|
<option value="">No icon</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label style="display: flex; align-items: center; gap: 8px; margin-top: 8px;">
|
<label style="display: flex; align-items: center; gap: 8px; margin-top: 8px;">
|
||||||
<input id="public-model-media" type="checkbox" style="width: auto;" />
|
<input id="provider-model-media" type="checkbox" style="width: auto;" />
|
||||||
<span>Supports image uploads</span>
|
<span>Supports image uploads</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="admin-actions">
|
<div class="admin-actions">
|
||||||
<button type="submit" class="primary">Add Public Model</button>
|
<button type="submit" class="primary">Add Provider Model</button>
|
||||||
|
<button type="button" id="reload-available" class="ghost">Reload available models</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-line" id="public-model-status"></div>
|
<div class="status-line" id="provider-model-status"></div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Provider Model Chain Section -->
|
<!-- Provider Chain Add Section -->
|
||||||
<div class="admin-card">
|
<div class="admin-card">
|
||||||
<header>
|
<header>
|
||||||
<h3>Provider Model Chain</h3>
|
<h3>Add to Provider Chain</h3>
|
||||||
<div class="pill">Backend</div>
|
<div class="pill">Fallback</div>
|
||||||
</header>
|
</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>
|
<p style="margin-top:0; color: var(--muted);">Add providers to the unified fallback chain. 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">
|
<form id="provider-chain-form" class="admin-form">
|
||||||
<label>
|
<label>
|
||||||
Provider
|
Provider
|
||||||
@@ -154,6 +155,55 @@
|
|||||||
<div id="provider-chain-list" class="admin-list"></div>
|
<div id="provider-chain-list" class="admin-list"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Public-Facing Models Section -->
|
||||||
|
<div class="admin-card" style="margin-top: 16px;">
|
||||||
|
<header>
|
||||||
|
<h3>Public-Facing Models</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 are separate from the provider models and can be used to create curated model lists for users.</p>
|
||||||
|
<form id="public-model-form" class="admin-form" style="margin-bottom: 16px;">
|
||||||
|
<div class="admin-grid" style="grid-template-columns: 1fr 1fr;">
|
||||||
|
<label>
|
||||||
|
Model ID
|
||||||
|
<input id="public-model-name" type="text" placeholder="e.g., custom-model-1" required />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Display name
|
||||||
|
<input id="public-model-label" type="text" placeholder="Friendly label" required />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<label>
|
||||||
|
Model tier
|
||||||
|
<select id="public-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="public-model-icon">
|
||||||
|
<option value="">No icon</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label style="display: flex; align-items: center; gap: 8px; margin-top: 8px;">
|
||||||
|
<input id="public-model-media" type="checkbox" style="width: auto;" />
|
||||||
|
<span>Supports image uploads</span>
|
||||||
|
</label>
|
||||||
|
<div class="admin-actions">
|
||||||
|
<button type="submit" class="primary">Add Public Model</button>
|
||||||
|
</div>
|
||||||
|
<div class="status-line" id="public-model-status"></div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<header style="margin-top: 24px;">
|
||||||
|
<h3>Public Models List</h3>
|
||||||
|
<div class="pill" id="public-models-count">0</div>
|
||||||
|
</header>
|
||||||
|
<div id="public-models-list" class="admin-list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="admin-grid" style="margin-top: 16px;">
|
<div class="admin-grid" style="margin-top: 16px;">
|
||||||
<!-- Legacy Model Form (hidden but kept for compatibility) -->
|
<!-- Legacy Model Form (hidden but kept for compatibility) -->
|
||||||
<div class="admin-card" style="display: none;">
|
<div class="admin-card" style="display: none;">
|
||||||
|
|||||||
@@ -4,10 +4,11 @@
|
|||||||
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 = {
|
||||||
available: [],
|
available: [], // Models available from OpenCode
|
||||||
configured: [],
|
configured: [], // Legacy - provider models from OpenCode
|
||||||
publicModels: [], // New unified structure - public facing models
|
providerModels: [], // Provider models (from OpenCode)
|
||||||
providerChain: [], // New unified structure - provider fallback chain
|
providerChain: [], // Unified fallback chain
|
||||||
|
publicModels: [], // Public-facing models (completely separate)
|
||||||
icons: [],
|
icons: [],
|
||||||
accounts: [],
|
accounts: [],
|
||||||
affiliates: [],
|
affiliates: [],
|
||||||
@@ -17,12 +18,27 @@
|
|||||||
providerUsage: [],
|
providerUsage: [],
|
||||||
opencodeBackupModel: '',
|
opencodeBackupModel: '',
|
||||||
providerOptions: [],
|
providerOptions: [],
|
||||||
providerModels: {},
|
|
||||||
tokenRates: {},
|
tokenRates: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const el = {
|
const el = {
|
||||||
// New unified model elements
|
// Provider Models (with OpenCode integration)
|
||||||
|
providerModelForm: document.getElementById('provider-model-form'),
|
||||||
|
availableModels: document.getElementById('available-models'),
|
||||||
|
providerModelLabel: document.getElementById('provider-model-label'),
|
||||||
|
providerModelTier: document.getElementById('provider-model-tier'),
|
||||||
|
providerModelIcon: document.getElementById('provider-model-icon'),
|
||||||
|
providerModelMedia: document.getElementById('provider-model-media'),
|
||||||
|
providerModelStatus: document.getElementById('provider-model-status'),
|
||||||
|
reloadAvailable: document.getElementById('reload-available'),
|
||||||
|
// Provider Chain
|
||||||
|
providerChainForm: document.getElementById('provider-chain-form'),
|
||||||
|
chainProvider: document.getElementById('chain-provider'),
|
||||||
|
chainModel: document.getElementById('chain-model'),
|
||||||
|
providerChainStatus: document.getElementById('provider-chain-status'),
|
||||||
|
providerChainList: document.getElementById('provider-chain-list'),
|
||||||
|
providerChainCount: document.getElementById('provider-chain-count'),
|
||||||
|
// Public-Facing Models (completely separate)
|
||||||
publicModelForm: document.getElementById('public-model-form'),
|
publicModelForm: document.getElementById('public-model-form'),
|
||||||
publicModelName: document.getElementById('public-model-name'),
|
publicModelName: document.getElementById('public-model-name'),
|
||||||
publicModelLabel: document.getElementById('public-model-label'),
|
publicModelLabel: document.getElementById('public-model-label'),
|
||||||
@@ -30,14 +46,9 @@
|
|||||||
publicModelIcon: document.getElementById('public-model-icon'),
|
publicModelIcon: document.getElementById('public-model-icon'),
|
||||||
publicModelMedia: document.getElementById('public-model-media'),
|
publicModelMedia: document.getElementById('public-model-media'),
|
||||||
publicModelStatus: document.getElementById('public-model-status'),
|
publicModelStatus: document.getElementById('public-model-status'),
|
||||||
providerChainForm: document.getElementById('provider-chain-form'),
|
publicModelsList: document.getElementById('public-models-list'),
|
||||||
chainProvider: document.getElementById('chain-provider'),
|
publicModelsCount: document.getElementById('public-models-count'),
|
||||||
chainModel: document.getElementById('chain-model'),
|
|
||||||
providerChainStatus: document.getElementById('provider-chain-status'),
|
|
||||||
providerChainList: document.getElementById('provider-chain-list'),
|
|
||||||
providerChainCount: document.getElementById('provider-chain-count'),
|
|
||||||
// Legacy elements
|
// Legacy elements
|
||||||
availableModels: document.getElementById('available-models'),
|
|
||||||
displayLabel: document.getElementById('display-label'),
|
displayLabel: document.getElementById('display-label'),
|
||||||
modelTier: document.getElementById('model-tier'),
|
modelTier: document.getElementById('model-tier'),
|
||||||
iconSelect: document.getElementById('icon-select'),
|
iconSelect: document.getElementById('icon-select'),
|
||||||
@@ -198,6 +209,12 @@
|
|||||||
el.autoModelStatus.style.color = isError ? 'var(--danger)' : 'inherit';
|
el.autoModelStatus.style.color = isError ? 'var(--danger)' : 'inherit';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setProviderModelStatus(msg, isError = false) {
|
||||||
|
if (!el.providerModelStatus) return;
|
||||||
|
el.providerModelStatus.textContent = msg || '';
|
||||||
|
el.providerModelStatus.style.color = isError ? 'var(--danger)' : 'inherit';
|
||||||
|
}
|
||||||
|
|
||||||
function setPublicModelStatus(msg, isError = false) {
|
function setPublicModelStatus(msg, isError = false) {
|
||||||
if (!el.publicModelStatus) return;
|
if (!el.publicModelStatus) return;
|
||||||
el.publicModelStatus.textContent = msg || '';
|
el.publicModelStatus.textContent = msg || '';
|
||||||
@@ -546,7 +563,22 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate new public model icon select
|
// Populate provider model icon select
|
||||||
|
if (el.providerModelIcon) {
|
||||||
|
el.providerModelIcon.innerHTML = '';
|
||||||
|
const none = document.createElement('option');
|
||||||
|
none.value = '';
|
||||||
|
none.textContent = 'No icon';
|
||||||
|
el.providerModelIcon.appendChild(none);
|
||||||
|
state.icons.forEach((iconPath) => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = iconPath;
|
||||||
|
opt.textContent = iconPath.replace('/assets/', '');
|
||||||
|
el.providerModelIcon.appendChild(opt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate public model icon select
|
||||||
if (el.publicModelIcon) {
|
if (el.publicModelIcon) {
|
||||||
el.publicModelIcon.innerHTML = '';
|
el.publicModelIcon.innerHTML = '';
|
||||||
const none = document.createElement('option');
|
const none = document.createElement('option');
|
||||||
@@ -588,19 +620,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simplified renderConfigured for new publicModels structure
|
// Render Provider Models (from OpenCode)
|
||||||
function renderConfigured() {
|
function renderProviderModels() {
|
||||||
if (!el.configuredList) return;
|
if (!el.configuredList) return;
|
||||||
el.configuredList.innerHTML = '';
|
el.configuredList.innerHTML = '';
|
||||||
|
|
||||||
// Use publicModels if available, otherwise fall back to configured
|
const modelsToRender = state.providerModels;
|
||||||
const modelsToRender = state.publicModels.length > 0 ? state.publicModels : state.configured;
|
|
||||||
|
|
||||||
if (el.configuredCount) el.configuredCount.textContent = modelsToRender.length.toString();
|
if (el.configuredCount) el.configuredCount.textContent = modelsToRender.length.toString();
|
||||||
if (!modelsToRender.length) {
|
if (!modelsToRender.length) {
|
||||||
const empty = document.createElement('div');
|
const empty = document.createElement('div');
|
||||||
empty.className = 'muted';
|
empty.className = 'muted';
|
||||||
empty.textContent = 'No public-facing models configured yet.';
|
empty.textContent = 'No provider models configured yet. Add models from OpenCode above.';
|
||||||
el.configuredList.appendChild(empty);
|
el.configuredList.appendChild(empty);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -677,7 +708,7 @@
|
|||||||
const next = [...modelsToRender];
|
const next = [...modelsToRender];
|
||||||
const [item] = next.splice(idx, 1);
|
const [item] = next.splice(idx, 1);
|
||||||
next.splice(idx - 1, 0, item);
|
next.splice(idx - 1, 0, item);
|
||||||
await persistPublicModelsOrder(next);
|
await persistProviderModelsOrder(next);
|
||||||
});
|
});
|
||||||
headerActions.appendChild(upBtn);
|
headerActions.appendChild(upBtn);
|
||||||
|
|
||||||
@@ -692,7 +723,7 @@
|
|||||||
const next = [...modelsToRender];
|
const next = [...modelsToRender];
|
||||||
const [item] = next.splice(idx, 1);
|
const [item] = next.splice(idx, 1);
|
||||||
next.splice(idx + 1, 0, item);
|
next.splice(idx + 1, 0, item);
|
||||||
await persistPublicModelsOrder(next);
|
await persistProviderModelsOrder(next);
|
||||||
});
|
});
|
||||||
headerActions.appendChild(downBtn);
|
headerActions.appendChild(downBtn);
|
||||||
|
|
||||||
@@ -705,7 +736,7 @@
|
|||||||
await api(`/api/admin/models/${m.id}`, { method: 'DELETE' });
|
await api(`/api/admin/models/${m.id}`, { method: 'DELETE' });
|
||||||
await loadConfigured();
|
await loadConfigured();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setStatus(err.message, true);
|
setProviderModelStatus(err.message, true);
|
||||||
}
|
}
|
||||||
delBtn.disabled = false;
|
delBtn.disabled = false;
|
||||||
});
|
});
|
||||||
@@ -741,8 +772,8 @@
|
|||||||
saveBtn.addEventListener('click', async () => {
|
saveBtn.addEventListener('click', async () => {
|
||||||
saveBtn.disabled = true;
|
saveBtn.disabled = true;
|
||||||
try {
|
try {
|
||||||
await persistPublicModelChanges(m.id, { icon: sel.value });
|
await persistProviderModelChanges(m.id, { icon: sel.value });
|
||||||
} catch (err) { setStatus(err.message, true); }
|
} catch (err) { setProviderModelStatus(err.message, true); }
|
||||||
saveBtn.disabled = false;
|
saveBtn.disabled = false;
|
||||||
});
|
});
|
||||||
editor.appendChild(saveBtn);
|
editor.appendChild(saveBtn);
|
||||||
@@ -769,7 +800,7 @@
|
|||||||
mediaCheckbox.addEventListener('change', async () => {
|
mediaCheckbox.addEventListener('change', async () => {
|
||||||
mediaCheckbox.disabled = true;
|
mediaCheckbox.disabled = true;
|
||||||
try {
|
try {
|
||||||
await persistPublicModelChanges(m.id, { supportsMedia: mediaCheckbox.checked });
|
await persistProviderModelChanges(m.id, { supportsMedia: mediaCheckbox.checked });
|
||||||
} catch (err) { setStatus(err.message, true); }
|
} catch (err) { setStatus(err.message, true); }
|
||||||
mediaCheckbox.disabled = false;
|
mediaCheckbox.disabled = false;
|
||||||
});
|
});
|
||||||
@@ -834,11 +865,294 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render Public Models (completely separate from OpenCode)
|
||||||
|
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-facing models configured yet.';
|
||||||
|
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';
|
||||||
|
|
||||||
|
// Order number badge
|
||||||
|
const orderBadge = document.createElement('span');
|
||||||
|
orderBadge.className = 'pill';
|
||||||
|
orderBadge.textContent = `#${idx + 1}`;
|
||||||
|
orderBadge.style.background = idx === 0 ? 'var(--shopify-green)' : 'var(--primary)';
|
||||||
|
orderBadge.style.fontWeight = '700';
|
||||||
|
info.appendChild(orderBadge);
|
||||||
|
|
||||||
|
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 () => {
|
||||||
|
if (idx === 0) return;
|
||||||
|
const next = [...state.publicModels];
|
||||||
|
const [item] = next.splice(idx, 1);
|
||||||
|
next.splice(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 () => {
|
||||||
|
if (idx === state.publicModels.length - 1) return;
|
||||||
|
const next = [...state.publicModels];
|
||||||
|
const [item] = next.splice(idx, 1);
|
||||||
|
next.splice(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}`, { method: 'DELETE' });
|
||||||
|
await loadConfigured();
|
||||||
|
} catch (err) {
|
||||||
|
setPublicModelStatus(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 persistPublicModelChanges(m.id, { icon: sel.value });
|
||||||
|
} catch (err) { setPublicModelStatus(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 persistPublicModelChanges(m.id, { supportsMedia: mediaCheckbox.checked });
|
||||||
|
} catch (err) { setPublicModelStatus(err.message, true); }
|
||||||
|
mediaCheckbox.disabled = false;
|
||||||
|
});
|
||||||
|
mediaToggle.appendChild(mediaCheckbox);
|
||||||
|
const mediaLabel = document.createElement('span');
|
||||||
|
mediaLabel.textContent = 'Supports image uploads';
|
||||||
|
mediaLabel.style.fontSize = '12px';
|
||||||
|
mediaLabel.style.color = 'var(--muted)';
|
||||||
|
mediaToggle.appendChild(mediaLabel);
|
||||||
|
headerActions.appendChild(mediaToggle);
|
||||||
|
|
||||||
|
// Tier editor button
|
||||||
|
const editTierBtn = document.createElement('button');
|
||||||
|
editTierBtn.className = 'ghost';
|
||||||
|
editTierBtn.textContent = 'Edit tier/multiplier';
|
||||||
|
editTierBtn.addEventListener('click', () => {
|
||||||
|
let editor = header.querySelector('.tier-editor');
|
||||||
|
if (editor) return editor.remove();
|
||||||
|
editor = document.createElement('div');
|
||||||
|
editor.className = 'tier-editor';
|
||||||
|
|
||||||
|
const sel = document.createElement('select');
|
||||||
|
const options = [
|
||||||
|
{ value: 'free', label: 'Free (1x)' },
|
||||||
|
{ value: 'plus', label: 'Plus (2x)' },
|
||||||
|
{ value: 'pro', label: 'Pro (3x)' }
|
||||||
|
];
|
||||||
|
options.forEach((opt) => {
|
||||||
|
const o = document.createElement('option');
|
||||||
|
o.value = opt.value;
|
||||||
|
o.textContent = opt.label;
|
||||||
|
sel.appendChild(o);
|
||||||
|
});
|
||||||
|
sel.value = m.tier || 'free';
|
||||||
|
editor.appendChild(sel);
|
||||||
|
|
||||||
|
const saveBtn = document.createElement('button');
|
||||||
|
saveBtn.className = 'primary';
|
||||||
|
saveBtn.textContent = 'Save';
|
||||||
|
saveBtn.addEventListener('click', async () => {
|
||||||
|
saveBtn.disabled = true;
|
||||||
|
try {
|
||||||
|
await persistPublicModelChanges(m.id, { tier: sel.value });
|
||||||
|
} catch (err) { setPublicModelStatus(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(editTierBtn);
|
||||||
|
|
||||||
|
header.appendChild(headerActions);
|
||||||
|
row.appendChild(header);
|
||||||
|
el.publicModelsList.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persistProviderModelChanges(modelId, changes) {
|
||||||
|
setProviderModelStatus('Saving...');
|
||||||
|
const model = state.providerModels.find((m) => m.id === modelId);
|
||||||
|
if (!model) {
|
||||||
|
setProviderModelStatus('Model not found', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
type: 'providerModel',
|
||||||
|
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.providerModels.findIndex((m) => m.id === modelId);
|
||||||
|
if (idx >= 0) state.providerModels[idx] = { ...state.providerModels[idx], ...data.providerModel };
|
||||||
|
renderProviderModels();
|
||||||
|
setProviderModelStatus('Saved');
|
||||||
|
setTimeout(() => setProviderModelStatus(''), 1500);
|
||||||
|
} catch (err) {
|
||||||
|
setProviderModelStatus(err.message, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persistProviderModelsOrder(orderedModels) {
|
||||||
|
setProviderModelStatus('Saving order...');
|
||||||
|
try {
|
||||||
|
const res = await api('/api/admin/models/reorder', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ type: 'providerModels', models: orderedModels }),
|
||||||
|
});
|
||||||
|
state.providerModels = res.providerModels || orderedModels;
|
||||||
|
renderProviderModels();
|
||||||
|
setProviderModelStatus('Order saved');
|
||||||
|
setTimeout(() => setProviderModelStatus(''), 1500);
|
||||||
|
} catch (err) {
|
||||||
|
setProviderModelStatus(err.message, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function persistPublicModelChanges(modelId, changes) {
|
async function persistPublicModelChanges(modelId, changes) {
|
||||||
setStatus('Saving...');
|
setPublicModelStatus('Saving...');
|
||||||
const model = state.publicModels.find((m) => m.id === modelId);
|
const model = state.publicModels.find((m) => m.id === modelId);
|
||||||
if (!model) {
|
if (!model) {
|
||||||
setStatus('Model not found', true);
|
setPublicModelStatus('Model not found', true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -865,20 +1179,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function persistPublicModelsOrder(orderedModels) {
|
async function persistPublicModelsOrder(orderedModels) {
|
||||||
setStatus('Saving order...');
|
setPublicModelStatus('Saving order...');
|
||||||
try {
|
try {
|
||||||
// Use the reorder endpoint to save the new order
|
// Use the reorder endpoint to save the new order
|
||||||
const res = await api('/api/admin/models/reorder', {
|
const res = await api('/api/admin/models/reorder', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ models: orderedModels }),
|
body: JSON.stringify({ type: 'publicModels', models: orderedModels }),
|
||||||
});
|
});
|
||||||
// Update local state with new order from server
|
// Update local state with new order from server
|
||||||
state.publicModels = res.publicModels || orderedModels;
|
state.publicModels = res.publicModels || orderedModels;
|
||||||
renderConfigured();
|
renderPublicModels();
|
||||||
setStatus('Order saved');
|
setPublicModelStatus('Order saved');
|
||||||
setTimeout(() => setStatus(''), 1500);
|
setTimeout(() => setPublicModelStatus(''), 1500);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setStatus(err.message, true);
|
setPublicModelStatus(err.message, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1329,11 +1643,13 @@
|
|||||||
|
|
||||||
async function loadConfigured() {
|
async function loadConfigured() {
|
||||||
const data = await api('/api/admin/models');
|
const data = await api('/api/admin/models');
|
||||||
// Handle new unified structure
|
// Handle new structure - separate provider models and public models
|
||||||
|
state.providerModels = data.providerModels || [];
|
||||||
state.publicModels = data.publicModels || [];
|
state.publicModels = data.publicModels || [];
|
||||||
state.providerChain = data.providerChain || [];
|
state.providerChain = data.providerChain || [];
|
||||||
state.configured = data.models || []; // Legacy support
|
state.configured = data.models || []; // Legacy support
|
||||||
renderConfigured();
|
renderProviderModels();
|
||||||
|
renderPublicModels();
|
||||||
renderProviderChain();
|
renderProviderChain();
|
||||||
const selectedAutoModel = state.planSettings && typeof state.planSettings.freePlanModel === 'string'
|
const selectedAutoModel = state.planSettings && typeof state.planSettings.freePlanModel === 'string'
|
||||||
? state.planSettings.freePlanModel
|
? state.planSettings.freePlanModel
|
||||||
@@ -2281,7 +2597,62 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// New public model form handler
|
// Provider model form handler (with OpenCode integration)
|
||||||
|
if (el.providerModelForm) {
|
||||||
|
el.providerModelForm.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const model = el.availableModels.value;
|
||||||
|
const label = el.providerModelLabel.value.trim();
|
||||||
|
const icon = el.providerModelIcon ? el.providerModelIcon.value : '';
|
||||||
|
const tier = el.providerModelTier ? el.providerModelTier.value : 'free';
|
||||||
|
const supportsMedia = el.providerModelMedia ? el.providerModelMedia.checked : false;
|
||||||
|
|
||||||
|
if (!model) {
|
||||||
|
setProviderModelStatus('Please select a model from OpenCode.', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!label) {
|
||||||
|
setProviderModelStatus('Display name is required.', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setProviderModelStatus('Saving...');
|
||||||
|
try {
|
||||||
|
await api('/api/admin/models', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
type: 'providerModel',
|
||||||
|
name: model,
|
||||||
|
label,
|
||||||
|
icon,
|
||||||
|
tier,
|
||||||
|
supportsMedia
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
setProviderModelStatus('Saved');
|
||||||
|
el.providerModelLabel.value = '';
|
||||||
|
await loadConfigured();
|
||||||
|
} catch (err) {
|
||||||
|
setProviderModelStatus(err.message, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload available models button
|
||||||
|
if (el.reloadAvailable) {
|
||||||
|
el.reloadAvailable.addEventListener('click', async () => {
|
||||||
|
setProviderModelStatus('Loading...');
|
||||||
|
try {
|
||||||
|
await loadAvailable();
|
||||||
|
setProviderModelStatus('Models loaded');
|
||||||
|
setTimeout(() => setProviderModelStatus(''), 1500);
|
||||||
|
} catch (err) {
|
||||||
|
setProviderModelStatus(err.message, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public model form handler (completely separate from OpenCode)
|
||||||
if (el.publicModelForm) {
|
if (el.publicModelForm) {
|
||||||
el.publicModelForm.addEventListener('submit', async (e) => {
|
el.publicModelForm.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
150
chat/server.js
150
chat/server.js
@@ -1525,7 +1525,8 @@ const adminSessions = new Map();
|
|||||||
let adminModels = [];
|
let adminModels = [];
|
||||||
let adminModelIndex = new Map();
|
let adminModelIndex = new Map();
|
||||||
// New unified model chain structure
|
// New unified model chain structure
|
||||||
let publicModels = []; // Models displayed in builder dropdown [{id, name, label, icon, tier, supportsMedia, multiplier}]
|
let providerModels = []; // Provider models from OpenCode [{id, name, label, icon, tier, supportsMedia, multiplier}]
|
||||||
|
let publicModels = []; // Public-facing models (completely separate) [{id, name, label, icon, tier, supportsMedia, multiplier}]
|
||||||
let providerChain = []; // Unified fallback chain [{provider, model, primary}]
|
let providerChain = []; // Unified fallback chain [{provider, model, primary}]
|
||||||
let openrouterSettings = {
|
let openrouterSettings = {
|
||||||
primaryModel: OPENROUTER_MODEL_PRIMARY,
|
primaryModel: OPENROUTER_MODEL_PRIMARY,
|
||||||
@@ -5762,14 +5763,15 @@ async function loadAdminModelStore() {
|
|||||||
// Check if using new unified structure or old structure
|
// Check if using new unified structure or old structure
|
||||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||||
// New unified structure
|
// New unified structure
|
||||||
|
providerModels = Array.isArray(parsed.providerModels) ? parsed.providerModels : [];
|
||||||
publicModels = Array.isArray(parsed.publicModels) ? parsed.publicModels : [];
|
publicModels = Array.isArray(parsed.publicModels) ? parsed.publicModels : [];
|
||||||
providerChain = Array.isArray(parsed.providerChain) ? parsed.providerChain : [];
|
providerChain = Array.isArray(parsed.providerChain) ? parsed.providerChain : [];
|
||||||
adminModels = Array.isArray(parsed.adminModels) ? parsed.adminModels : [];
|
adminModels = Array.isArray(parsed.adminModels) ? parsed.adminModels : [];
|
||||||
} else if (Array.isArray(parsed)) {
|
} else if (Array.isArray(parsed)) {
|
||||||
// Old array structure - migrate to new
|
// Old array structure - migrate to new
|
||||||
adminModels = parsed;
|
adminModels = parsed;
|
||||||
// Create public models from admin models
|
// Create provider models from admin models (these are from OpenCode)
|
||||||
publicModels = parsed.map((m) => ({
|
providerModels = parsed.map((m) => ({
|
||||||
id: m.id || randomUUID(),
|
id: m.id || randomUUID(),
|
||||||
name: m.name,
|
name: m.name,
|
||||||
label: m.label || m.name,
|
label: m.label || m.name,
|
||||||
@@ -5778,6 +5780,8 @@ async function loadAdminModelStore() {
|
|||||||
supportsMedia: m.supportsMedia ?? false,
|
supportsMedia: m.supportsMedia ?? false,
|
||||||
multiplier: getTierMultiplier(m.tier),
|
multiplier: getTierMultiplier(m.tier),
|
||||||
})).filter((m) => !!m.name);
|
})).filter((m) => !!m.name);
|
||||||
|
// Public models start empty (user can add these manually later)
|
||||||
|
publicModels = [];
|
||||||
// Create unified provider chain from all unique providers
|
// Create unified provider chain from all unique providers
|
||||||
const allProviders = new Map();
|
const allProviders = new Map();
|
||||||
parsed.forEach((m) => {
|
parsed.forEach((m) => {
|
||||||
@@ -5797,11 +5801,23 @@ async function loadAdminModelStore() {
|
|||||||
});
|
});
|
||||||
providerChain = Array.from(allProviders.values());
|
providerChain = Array.from(allProviders.values());
|
||||||
} else {
|
} else {
|
||||||
|
providerModels = [];
|
||||||
publicModels = [];
|
publicModels = [];
|
||||||
providerChain = [];
|
providerChain = [];
|
||||||
adminModels = [];
|
adminModels = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure all provider models have required fields
|
||||||
|
providerModels = providerModels.map((m) => ({
|
||||||
|
id: m.id || randomUUID(),
|
||||||
|
name: m.name,
|
||||||
|
label: m.label || m.name,
|
||||||
|
icon: m.icon || '',
|
||||||
|
tier: normalizeTier(m.tier),
|
||||||
|
supportsMedia: m.supportsMedia ?? false,
|
||||||
|
multiplier: m.multiplier || getTierMultiplier(m.tier),
|
||||||
|
})).filter((m) => !!m.name);
|
||||||
|
|
||||||
// Ensure all public models have required fields
|
// Ensure all public models have required fields
|
||||||
publicModels = publicModels.map((m) => ({
|
publicModels = publicModels.map((m) => ({
|
||||||
id: m.id || randomUUID(),
|
id: m.id || randomUUID(),
|
||||||
@@ -5822,12 +5838,14 @@ async function loadAdminModelStore() {
|
|||||||
|
|
||||||
refreshAdminModelIndex();
|
refreshAdminModelIndex();
|
||||||
log('Loaded admin model store', {
|
log('Loaded admin model store', {
|
||||||
|
providerModels: providerModels.length,
|
||||||
publicModels: publicModels.length,
|
publicModels: publicModels.length,
|
||||||
providerChain: providerChain.length,
|
providerChain: providerChain.length,
|
||||||
legacyAdminModels: adminModels.length
|
legacyAdminModels: adminModels.length
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log('Failed to load admin models, starting empty', { error: String(error) });
|
log('Failed to load admin models, starting empty', { error: String(error) });
|
||||||
|
providerModels = [];
|
||||||
publicModels = [];
|
publicModels = [];
|
||||||
providerChain = [];
|
providerChain = [];
|
||||||
adminModels = [];
|
adminModels = [];
|
||||||
@@ -5840,10 +5858,11 @@ async function persistAdminModels() {
|
|||||||
await ensureAssetsDir();
|
await ensureAssetsDir();
|
||||||
// Save new unified structure
|
// Save new unified structure
|
||||||
const payload = JSON.stringify({
|
const payload = JSON.stringify({
|
||||||
|
providerModels,
|
||||||
publicModels,
|
publicModels,
|
||||||
providerChain,
|
providerChain,
|
||||||
adminModels, // Keep legacy for backwards compatibility
|
adminModels, // Keep legacy for backwards compatibility
|
||||||
version: 2, // Schema version
|
version: 3, // Schema version - added providerModels
|
||||||
}, null, 2);
|
}, null, 2);
|
||||||
await safeWriteFile(ADMIN_MODELS_FILE, payload);
|
await safeWriteFile(ADMIN_MODELS_FILE, payload);
|
||||||
refreshAdminModelIndex();
|
refreshAdminModelIndex();
|
||||||
@@ -15857,7 +15876,8 @@ async function handleAdminModelsList(req, res) {
|
|||||||
if (!session) return;
|
if (!session) return;
|
||||||
// Return new unified structure
|
// Return new unified structure
|
||||||
sendJson(res, 200, {
|
sendJson(res, 200, {
|
||||||
publicModels: publicModels,
|
providerModels,
|
||||||
|
publicModels,
|
||||||
providerChain,
|
providerChain,
|
||||||
// Legacy support
|
// Legacy support
|
||||||
models: getConfiguredModels('opencode'),
|
models: getConfiguredModels('opencode'),
|
||||||
@@ -15870,9 +15890,45 @@ async function handleAdminModelUpsert(req, res) {
|
|||||||
try {
|
try {
|
||||||
const body = await parseJsonBody(req);
|
const body = await parseJsonBody(req);
|
||||||
|
|
||||||
// Check if this is a public model or provider chain update
|
// Check if this is a provider model, public model, or provider chain update
|
||||||
if (body.type === 'publicModel') {
|
if (body.type === 'providerModel') {
|
||||||
// Handle public model update
|
// Handle provider model update (from OpenCode)
|
||||||
|
const modelName = (body.name || body.model || '').trim();
|
||||||
|
const label = (body.label || body.displayName || modelName).trim();
|
||||||
|
const tier = normalizeTier(body.tier);
|
||||||
|
if (!modelName) return sendJson(res, 400, { error: 'Model name is required' });
|
||||||
|
const id = body.id || randomUUID();
|
||||||
|
const existingIndex = providerModels.findIndex((m) => m.id === id);
|
||||||
|
let icon = providerModels[existingIndex]?.icon || '';
|
||||||
|
if (typeof body.icon === 'string' && body.icon.trim()) {
|
||||||
|
icon = await normalizeIconPath(body.icon);
|
||||||
|
if (!icon) return sendJson(res, 400, { error: 'Icon must be stored in /assets' });
|
||||||
|
}
|
||||||
|
const supportsMedia = typeof body.supportsMedia === 'boolean' ? body.supportsMedia : false;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
id,
|
||||||
|
name: modelName,
|
||||||
|
label: label || modelName,
|
||||||
|
icon,
|
||||||
|
tier,
|
||||||
|
supportsMedia,
|
||||||
|
multiplier: getTierMultiplier(tier),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (existingIndex >= 0) providerModels[existingIndex] = { ...providerModels[existingIndex], ...payload };
|
||||||
|
else providerModels.push(payload);
|
||||||
|
|
||||||
|
await persistAdminModels();
|
||||||
|
sendJson(res, 200, {
|
||||||
|
providerModel: payload,
|
||||||
|
providerModels,
|
||||||
|
publicModels,
|
||||||
|
providerChain,
|
||||||
|
models: getConfiguredModels('opencode'),
|
||||||
|
});
|
||||||
|
} else if (body.type === 'publicModel') {
|
||||||
|
// Handle public model update (completely separate from OpenCode)
|
||||||
const modelName = (body.name || body.model || '').trim();
|
const modelName = (body.name || body.model || '').trim();
|
||||||
const label = (body.label || body.displayName || modelName).trim();
|
const label = (body.label || body.displayName || modelName).trim();
|
||||||
const tier = normalizeTier(body.tier);
|
const tier = normalizeTier(body.tier);
|
||||||
@@ -15902,7 +15958,8 @@ async function handleAdminModelUpsert(req, res) {
|
|||||||
await persistAdminModels();
|
await persistAdminModels();
|
||||||
sendJson(res, 200, {
|
sendJson(res, 200, {
|
||||||
publicModel: payload,
|
publicModel: payload,
|
||||||
publicModels: publicModels.sort((a, b) => (a.label || '').localeCompare(b.label || '')),
|
providerModels,
|
||||||
|
publicModels,
|
||||||
providerChain,
|
providerChain,
|
||||||
models: getConfiguredModels('opencode'),
|
models: getConfiguredModels('opencode'),
|
||||||
});
|
});
|
||||||
@@ -15973,28 +16030,52 @@ async function handleAdminModelsReorder(req, res) {
|
|||||||
return sendJson(res, 400, { error: 'models array is required' });
|
return sendJson(res, 400, { error: 'models array is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate that all provided IDs exist
|
const reorderType = body.type || 'publicModels';
|
||||||
const currentIds = new Set(publicModels.map(m => m.id));
|
|
||||||
const newIds = body.models.map(m => m.id);
|
|
||||||
const allExist = newIds.every(id => currentIds.has(id));
|
|
||||||
|
|
||||||
if (!allExist) {
|
if (reorderType === 'providerModels') {
|
||||||
return sendJson(res, 400, { error: 'Invalid model IDs in reorder request' });
|
// Validate that all provided IDs exist in providerModels
|
||||||
|
const currentIds = new Set(providerModels.map(m => m.id));
|
||||||
|
const newIds = body.models.map(m => m.id);
|
||||||
|
const allExist = newIds.every(id => currentIds.has(id));
|
||||||
|
|
||||||
|
if (!allExist) {
|
||||||
|
return sendJson(res, 400, { error: 'Invalid model IDs in reorder request' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reorder providerModels based on the provided order
|
||||||
|
const reordered = [];
|
||||||
|
body.models.forEach(m => {
|
||||||
|
const model = providerModels.find(pm => pm.id === m.id);
|
||||||
|
if (model) reordered.push(model);
|
||||||
|
});
|
||||||
|
|
||||||
|
providerModels = reordered;
|
||||||
|
} else {
|
||||||
|
// Validate that all provided IDs exist in publicModels
|
||||||
|
const currentIds = new Set(publicModels.map(m => m.id));
|
||||||
|
const newIds = body.models.map(m => m.id);
|
||||||
|
const allExist = newIds.every(id => currentIds.has(id));
|
||||||
|
|
||||||
|
if (!allExist) {
|
||||||
|
return sendJson(res, 400, { error: 'Invalid model IDs in reorder request' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reorder publicModels based on the provided order
|
||||||
|
const reordered = [];
|
||||||
|
body.models.forEach(m => {
|
||||||
|
const model = publicModels.find(pm => pm.id === m.id);
|
||||||
|
if (model) reordered.push(model);
|
||||||
|
});
|
||||||
|
|
||||||
|
publicModels = reordered;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reorder publicModels based on the provided order
|
|
||||||
const reordered = [];
|
|
||||||
body.models.forEach(m => {
|
|
||||||
const model = publicModels.find(pm => pm.id === m.id);
|
|
||||||
if (model) reordered.push(model);
|
|
||||||
});
|
|
||||||
|
|
||||||
publicModels = reordered;
|
|
||||||
await persistAdminModels();
|
await persistAdminModels();
|
||||||
|
|
||||||
sendJson(res, 200, {
|
sendJson(res, 200, {
|
||||||
ok: true,
|
ok: true,
|
||||||
publicModels: publicModels,
|
providerModels,
|
||||||
|
publicModels,
|
||||||
providerChain,
|
providerChain,
|
||||||
models: getConfiguredModels('opencode'),
|
models: getConfiguredModels('opencode'),
|
||||||
});
|
});
|
||||||
@@ -16006,13 +16087,26 @@ async function handleAdminModelsReorder(req, res) {
|
|||||||
async function handleAdminModelDelete(req, res, id) {
|
async function handleAdminModelDelete(req, res, id) {
|
||||||
const session = requireAdminAuth(req, res);
|
const session = requireAdminAuth(req, res);
|
||||||
if (!session) return;
|
if (!session) return;
|
||||||
const before = publicModels.length;
|
|
||||||
|
// Try to delete from providerModels first
|
||||||
|
const beforeProvider = providerModels.length;
|
||||||
|
providerModels = providerModels.filter((m) => m.id !== id);
|
||||||
|
const deletedFromProvider = providerModels.length < beforeProvider;
|
||||||
|
|
||||||
|
// Try to delete from publicModels
|
||||||
|
const beforePublic = publicModels.length;
|
||||||
publicModels = publicModels.filter((m) => m.id !== id);
|
publicModels = publicModels.filter((m) => m.id !== id);
|
||||||
if (publicModels.length === before) return sendJson(res, 404, { error: 'Model not found' });
|
const deletedFromPublic = publicModels.length < beforePublic;
|
||||||
|
|
||||||
|
if (!deletedFromProvider && !deletedFromPublic) {
|
||||||
|
return sendJson(res, 404, { error: 'Model not found' });
|
||||||
|
}
|
||||||
|
|
||||||
await persistAdminModels();
|
await persistAdminModels();
|
||||||
sendJson(res, 200, {
|
sendJson(res, 200, {
|
||||||
ok: true,
|
ok: true,
|
||||||
publicModels: publicModels.sort((a, b) => (a.label || '').localeCompare(b.label || '')),
|
providerModels,
|
||||||
|
publicModels,
|
||||||
providerChain,
|
providerChain,
|
||||||
models: getConfiguredModels('opencode'),
|
models: getConfiguredModels('opencode'),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user