Remove separate chain concept - fallback is now determined by OpenCode models order

- Removed opencodeChain variable entirely
- Removed chain form/list from admin UI
- Fallback now uses the order of models in the OpenCode models list
- Updated buildOpencodeAttemptChain to iterate through opencodeModels
- Removed chain-related API endpoints
- Simplified to just two lists: opencodeModels and publicModels
This commit is contained in:
southseact-3d
2026-02-18 16:18:31 +00:00
parent 4bb54d38ad
commit 828a9dad41
3 changed files with 60 additions and 321 deletions

View File

@@ -16,18 +16,6 @@
} }
/* Slightly tighten cards on the build page */ /* Slightly tighten cards on the build page */
body[data-page="build"] .admin-card { padding: 12px; } body[data-page="build"] .admin-card { padding: 12px; }
/* Chain section styling */
.chain-section {
margin-top: 24px;
padding-top: 16px;
border-top: 2px solid var(--border);
}
.chain-section h4 {
margin: 0 0 8px 0;
font-size: 14px;
color: var(--muted);
}
</style> </style>
<!-- PostHog Analytics --> <!-- PostHog Analytics -->
@@ -79,13 +67,13 @@
</div> </div>
</div> </div>
<!-- Section 1: OpenCode Models (with integrated Chain) --> <!-- Section 1: OpenCode Models -->
<div class="admin-card"> <div class="admin-card">
<header> <header>
<h3>OpenCode Models</h3> <h3>OpenCode Models</h3>
<div class="pill">Backend</div> <div class="pill">Backend</div>
</header> </header>
<p style="margin-top:0; color: var(--muted);">Add models from OpenCode. These models process requests and can fall back to the provider chain when rate limits are reached.</p> <p style="margin-top:0; color: var(--muted);">Add models from OpenCode. When rate limits are reached, the system automatically falls back to the next model in the order below.</p>
<!-- Add Model Form --> <!-- Add Model Form -->
<form id="opencode-model-form" class="admin-form" style="margin-bottom: 24px;"> <form id="opencode-model-form" class="admin-form" style="margin-bottom: 24px;">
@@ -126,57 +114,14 @@
<!-- Models List --> <!-- Models List -->
<header style="margin-top: 24px; border-top: 1px solid var(--border); padding-top: 16px;"> <header style="margin-top: 24px; border-top: 1px solid var(--border); padding-top: 16px;">
<h3>OpenCode Models List</h3> <h3>OpenCode Models Order</h3>
<div class="pill" id="opencode-models-count">0</div> <div class="pill" id="opencode-models-count">0</div>
</header> </header>
<p class="muted" style="margin-top:0;">Arrange the order below. This controls which model is primary for OpenCode requests.</p> <p class="muted" style="margin-top:0;">Arrange the order below. #1 is the primary model. When it hits rate limits, the system falls back to #2, then #3, and so on.</p>
<div id="opencode-models-list" class="admin-list"></div> <div id="opencode-models-list" class="admin-list"></div>
<!-- Chain Section (integrated) -->
<div class="chain-section">
<h4>Fallback Chain (for when rate limits are reached)</h4>
<p class="muted" style="margin-top:0; margin-bottom: 12px;">When the primary OpenCode model hits rate limits, the system falls back through these providers in order.</p>
<!-- Add to Chain Form -->
<form id="chain-form" class="admin-form" style="margin-bottom: 16px;">
<div class="admin-grid" style="grid-template-columns: 1fr 2fr; gap: 12px;">
<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="cohere">Cohere</option>
</select>
</label>
<label>
Model ID
<input id="chain-model" type="text" placeholder="e.g., anthropic/claude-3.5-sonnet" required />
</label>
</div>
<div class="admin-actions" style="margin-top: 12px;">
<button type="submit" class="primary">Add to Fallback Chain</button>
</div>
<div class="status-line" id="chain-status"></div>
</form>
<!-- Chain List -->
<div style="margin-top: 16px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<span class="muted">Fallback Chain Order</span>
<span class="pill" id="chain-count">0</span>
</div>
<div id="chain-list" class="admin-list"></div>
</div>
</div>
</div> </div>
<!-- Section 2: Public Models (Completely Separate) --> <!-- Section 2: Public Models -->
<div class="admin-card" style="margin-top: 16px;"> <div class="admin-card" style="margin-top: 16px;">
<header> <header>
<h3>Public Models</h3> <h3>Public Models</h3>

View File

@@ -1,19 +1,16 @@
(() => { (() => {
const DEFAULT_PROVIDERS = ['openrouter', 'mistral', 'google', 'groq', 'nvidia', 'chutes', 'cerebras', 'ollama', 'cohere'];
const PLANNING_PROVIDERS = ['openrouter', 'mistral', 'google', 'groq', 'nvidia', 'chutes', 'cerebras', 'ollama', 'cohere'];
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);
// Clean state structure // Clean state structure - just two things
const state = { const state = {
opencodeModels: [], // Models from OpenCode opencodeModels: [], // Models from OpenCode (order determines fallback)
opencodeChain: [], // Fallback chain for OpenCode
publicModels: [], // User-facing models (completely separate) publicModels: [], // User-facing models (completely separate)
icons: [], icons: [],
availableOpencodeModels: [], // Loaded from OpenCode availableOpencodeModels: [], // Loaded from OpenCode
}; };
// Element references - clean structure matching the HTML // Element references
const el = { const el = {
// OpenCode Models // OpenCode Models
opencodeModelForm: document.getElementById('opencode-model-form'), opencodeModelForm: document.getElementById('opencode-model-form'),
@@ -27,14 +24,6 @@
opencodeModelsList: document.getElementById('opencode-models-list'), opencodeModelsList: document.getElementById('opencode-models-list'),
opencodeModelsCount: document.getElementById('opencode-models-count'), opencodeModelsCount: document.getElementById('opencode-models-count'),
// OpenCode Chain
chainForm: document.getElementById('chain-form'),
chainProvider: document.getElementById('chain-provider'),
chainModel: document.getElementById('chain-model'),
chainStatus: document.getElementById('chain-status'),
chainList: document.getElementById('chain-list'),
chainCount: document.getElementById('chain-count'),
// Public Models // Public Models
publicModelForm: document.getElementById('public-model-form'), publicModelForm: document.getElementById('public-model-form'),
publicModelName: document.getElementById('public-model-name'), publicModelName: document.getElementById('public-model-name'),
@@ -158,10 +147,8 @@
try { try {
const data = await api('/api/admin/models'); const data = await api('/api/admin/models');
state.opencodeModels = data.opencodeModels || []; state.opencodeModels = data.opencodeModels || [];
state.opencodeChain = data.opencodeChain || [];
state.publicModels = data.publicModels || []; state.publicModels = data.publicModels || [];
renderOpencodeModels(); renderOpencodeModels();
renderOpencodeChain();
renderPublicModels(); renderPublicModels();
} catch (err) { } catch (err) {
console.error('Failed to load models:', err); console.error('Failed to load models:', err);
@@ -212,46 +199,6 @@
}); });
} }
// Render OpenCode Chain
function renderOpencodeChain() {
if (!el.chainList) return;
el.chainList.innerHTML = '';
if (el.chainCount) {
el.chainCount.textContent = state.opencodeChain.length.toString();
}
if (!state.opencodeChain.length) {
el.chainList.innerHTML = '<div class="muted">No fallback chain configured. Add providers above.</div>';
return;
}
state.opencodeChain.forEach((entry, idx) => {
const row = document.createElement('div');
row.className = 'provider-row slim';
row.innerHTML = `
<div class="provider-row-header">
<div class="model-chip">
<span class="pill" style="background: ${idx === 0 ? 'var(--shopify-green)' : 'var(--primary)'}; font-weight: 700;">${idx === 0 ? 'Primary' : `#${idx + 1}`}</span>
<span class="pill" style="background: var(--primary);">${entry.provider}</span>
<span>${entry.model}</span>
</div>
<div class="provider-row-actions">
<button class="ghost move-up" ${idx === 0 ? 'disabled' : ''}>↑</button>
<button class="ghost move-down" ${idx === state.opencodeChain.length - 1 ? 'disabled' : ''}>↓</button>
<button class="ghost delete-btn">Remove</button>
</div>
</div>
`;
row.querySelector('.move-up')?.addEventListener('click', () => moveChainItem(idx, -1));
row.querySelector('.move-down')?.addEventListener('click', () => moveChainItem(idx, 1));
row.querySelector('.delete-btn')?.addEventListener('click', () => removeChainItem(idx));
el.chainList.appendChild(row);
});
}
// Render Public Models // Render Public Models
function renderPublicModels() { function renderPublicModels() {
if (!el.publicModelsList) return; if (!el.publicModelsList) return;
@@ -316,27 +263,6 @@
} }
} }
// Move Chain Item
async function moveChainItem(fromIdx, direction) {
const toIdx = fromIdx + direction;
if (toIdx < 0 || toIdx >= state.opencodeChain.length) return;
const newOrder = [...state.opencodeChain];
const [item] = newOrder.splice(fromIdx, 1);
newOrder.splice(toIdx, 0, item);
try {
await api('/api/admin/chain/reorder', {
method: 'POST',
body: JSON.stringify({ chain: newOrder }),
});
state.opencodeChain = newOrder;
renderOpencodeChain();
} catch (err) {
console.error('Failed to reorder chain:', err);
}
}
// Move Public Model // Move Public Model
async function movePublicModel(fromIdx, direction) { async function movePublicModel(fromIdx, direction) {
const toIdx = fromIdx + direction; const toIdx = fromIdx + direction;
@@ -368,21 +294,6 @@
} }
} }
// Remove Chain Item
async function removeChainItem(idx) {
const newChain = state.opencodeChain.filter((_, i) => i !== idx);
try {
await api('/api/admin/chain', {
method: 'POST',
body: JSON.stringify({ chain: newChain }),
});
state.opencodeChain = newChain;
renderOpencodeChain();
} catch (err) {
console.error('Failed to remove from chain:', err);
}
}
// Form Handlers // Form Handlers
if (el.opencodeModelForm) { if (el.opencodeModelForm) {
el.opencodeModelForm.addEventListener('submit', async (e) => { el.opencodeModelForm.addEventListener('submit', async (e) => {
@@ -425,33 +336,6 @@
}); });
} }
if (el.chainForm) {
el.chainForm.addEventListener('submit', async (e) => {
e.preventDefault();
const provider = el.chainProvider.value;
const model = el.chainModel.value.trim();
if (!model) {
setStatus(el.chainStatus, 'Model ID is required', true);
return;
}
const newChain = [...state.opencodeChain, { provider, model }];
try {
await api('/api/admin/chain', {
method: 'POST',
body: JSON.stringify({ chain: newChain }),
});
setStatus(el.chainStatus, 'Added to chain');
el.chainModel.value = '';
await loadModels();
} catch (err) {
setStatus(el.chainStatus, err.message, true);
}
});
}
if (el.publicModelForm) { if (el.publicModelForm) {
el.publicModelForm.addEventListener('submit', async (e) => { el.publicModelForm.addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();

View File

@@ -1524,10 +1524,9 @@ const MODELS_DEV_CACHE_TTL = 3600000; // 1 hour
const adminSessions = new Map(); const adminSessions = new Map();
let adminModels = []; let adminModels = [];
let adminModelIndex = new Map(); let adminModelIndex = new Map();
// Clean model structure // Simple model structure - just two lists
let opencodeModels = []; // Models from OpenCode [{id, name, label, icon, tier, supportsMedia, multiplier}] let opencodeModels = []; // Models from OpenCode - order determines fallback chain
let opencodeChain = []; // OpenCode fallback chain [{provider, model}] let publicModels = []; // Public-facing models (completely separate)
let publicModels = []; // Public-facing models (completely separate) [{id, name, label, icon, tier, supportsMedia, multiplier}]
let openrouterSettings = { let openrouterSettings = {
primaryModel: OPENROUTER_MODEL_PRIMARY, primaryModel: OPENROUTER_MODEL_PRIMARY,
backupModel1: OPENROUTER_MODEL_BACKUP_1, backupModel1: OPENROUTER_MODEL_BACKUP_1,
@@ -5161,11 +5160,12 @@ async function ensureOpencodeConfig(session) {
// Find which providers are used in models // Find which providers are used in models
const usedProviders = new Set(); const usedProviders = new Set();
// Check opencode chain first // Check opencode models for provider prefixes in names (e.g., "openrouter/model-name")
if (opencodeChain.length > 0) { for (const model of opencodeModels) {
for (const p of opencodeChain) { if (model.name && model.name.includes('/')) {
if (p.provider) { const providerFromName = model.name.split('/')[0].toLowerCase();
usedProviders.add(p.provider.toLowerCase()); if (providerFromName && providerFromName !== 'opencode') {
usedProviders.add(providerFromName);
} }
} }
} }
@@ -5205,8 +5205,7 @@ async function ensureOpencodeConfig(session) {
log('Detected providers from models', { log('Detected providers from models', {
usedProviders: Array.from(usedProviders), usedProviders: Array.from(usedProviders),
count: usedProviders.size, count: usedProviders.size
source: opencodeChain.length > 0 ? 'opencodeChain' : 'legacy'
}); });
// Provider configurations with their base URLs // Provider configurations with their base URLs
@@ -5760,17 +5759,14 @@ async function loadAdminModelStore() {
const raw = await fs.readFile(ADMIN_MODELS_FILE, 'utf8').catch(() => '{}'); const raw = await fs.readFile(ADMIN_MODELS_FILE, 'utf8').catch(() => '{}');
const parsed = JSON.parse(raw || '{}'); const parsed = JSON.parse(raw || '{}');
// Load clean structure // Load simple structure
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
// New clean structure
opencodeModels = Array.isArray(parsed.opencodeModels) ? parsed.opencodeModels : []; opencodeModels = Array.isArray(parsed.opencodeModels) ? parsed.opencodeModels : [];
opencodeChain = Array.isArray(parsed.opencodeChain) ? parsed.opencodeChain : [];
publicModels = Array.isArray(parsed.publicModels) ? parsed.publicModels : []; publicModels = Array.isArray(parsed.publicModels) ? parsed.publicModels : [];
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 // Old array structure - migrate
adminModels = parsed; adminModels = parsed;
// Migrate old admin models to opencode models
opencodeModels = parsed.map((m) => ({ opencodeModels = parsed.map((m) => ({
id: m.id || randomUUID(), id: m.id || randomUUID(),
name: m.name, name: m.name,
@@ -5781,26 +5777,8 @@ async function loadAdminModelStore() {
multiplier: getTierMultiplier(m.tier), multiplier: getTierMultiplier(m.tier),
})).filter((m) => !!m.name); })).filter((m) => !!m.name);
publicModels = []; publicModels = [];
// Create chain from providers
const allProviders = new Map();
parsed.forEach((m) => {
const providers = Array.isArray(m.providers) && m.providers.length
? m.providers
: [{ provider: 'opencode', model: m.name }];
providers.forEach((p) => {
const key = `${p.provider}:${p.model || m.name}`;
if (!allProviders.has(key)) {
allProviders.set(key, {
provider: normalizeProviderName(p.provider || 'opencode'),
model: (p.model || m.name || '').trim() || m.name,
});
}
});
});
opencodeChain = Array.from(allProviders.values());
} else { } else {
opencodeModels = []; opencodeModels = [];
opencodeChain = [];
publicModels = []; publicModels = [];
adminModels = []; adminModels = [];
} }
@@ -5827,22 +5805,14 @@ async function loadAdminModelStore() {
multiplier: m.multiplier || getTierMultiplier(m.tier), multiplier: m.multiplier || getTierMultiplier(m.tier),
})).filter((m) => !!m.name); })).filter((m) => !!m.name);
// Validate chain
opencodeChain = opencodeChain.map((p) => ({
provider: normalizeProviderName(p.provider || 'opencode'),
model: (p.model || '').trim(),
})).filter((p) => !!p.model);
refreshAdminModelIndex(); refreshAdminModelIndex();
log('Loaded admin model store', { log('Loaded admin model store', {
opencodeModels: opencodeModels.length, opencodeModels: opencodeModels.length,
opencodeChain: opencodeChain.length,
publicModels: publicModels.length, publicModels: publicModels.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) });
opencodeModels = []; opencodeModels = [];
opencodeChain = [];
publicModels = []; publicModels = [];
adminModels = []; adminModels = [];
refreshAdminModelIndex(); refreshAdminModelIndex();
@@ -5852,13 +5822,12 @@ async function loadAdminModelStore() {
async function persistAdminModels() { async function persistAdminModels() {
await ensureStateFile(); await ensureStateFile();
await ensureAssetsDir(); await ensureAssetsDir();
// Save clean structure // Save simple structure
const payload = JSON.stringify({ const payload = JSON.stringify({
opencodeModels, opencodeModels,
opencodeChain,
publicModels, publicModels,
adminModels, // Keep legacy for backwards compatibility adminModels,
version: 4, // Clean structure version: 5,
}, null, 2); }, null, 2);
await safeWriteFile(ADMIN_MODELS_FILE, payload); await safeWriteFile(ADMIN_MODELS_FILE, payload);
refreshAdminModelIndex(); refreshAdminModelIndex();
@@ -6052,20 +6021,25 @@ function collectProviderSeeds() {
const normalized = normalizeProviderName(p); const normalized = normalizeProviderName(p);
if (validProviders.has(normalized)) seeds.add(normalized); if (validProviders.has(normalized)) seeds.add(normalized);
}); });
// Use opencode chain if available, otherwise fall back to legacy adminModels // Collect providers from opencode models (checking for provider prefixes in model names)
if (opencodeChain.length > 0) { opencodeModels.forEach((m) => {
opencodeChain.forEach((p) => { if (m.name && m.name.includes('/')) {
const providerName = extractProviderName(m.name);
if (validProviders.has(providerName)) seeds.add(providerName);
}
});
// Also check legacy adminModels
adminModels.forEach((m) => {
if (m.name && m.name.includes('/')) {
const providerName = extractProviderName(m.name);
if (validProviders.has(providerName)) seeds.add(providerName);
}
(m.providers || []).forEach((p) => {
const providerName = extractProviderName(p); const providerName = extractProviderName(p);
if (validProviders.has(providerName)) seeds.add(providerName); if (validProviders.has(providerName)) seeds.add(providerName);
}); });
} else { });
adminModels.forEach((m) => {
(m.providers || []).forEach((p) => {
const providerName = extractProviderName(p);
if (validProviders.has(providerName)) seeds.add(providerName);
});
});
}
(planSettings.planningChain || []).forEach((entry) => { (planSettings.planningChain || []).forEach((entry) => {
const normalized = normalizeProviderName(entry.provider); const normalized = normalizeProviderName(entry.provider);
if (validProviders.has(normalized)) seeds.add(normalized); if (validProviders.has(normalized)) seeds.add(normalized);
@@ -6109,14 +6083,17 @@ async function discoverProviderModels() {
collectProviderSeeds().forEach((p) => add(p)); collectProviderSeeds().forEach((p) => add(p));
// Use opencode chain if available, otherwise fall back to legacy adminModels // Collect models from opencodeModels (checking for provider prefixes)
if (opencodeChain.length > 0) { opencodeModels.forEach((m) => {
opencodeChain.forEach((p) => add(extractProviderName(p), p.model)); if (m.name && m.name.includes('/')) {
} else { add(extractProviderName(m.name), m.name);
adminModels.forEach((m) => { }
(m.providers || []).forEach((p) => add(extractProviderName(p), p.model || m.name)); });
});
} // Also check legacy adminModels
adminModels.forEach((m) => {
(m.providers || []).forEach((p) => add(extractProviderName(p), p.model || m.name));
});
(planSettings.planningChain || []).forEach((entry) => { (planSettings.planningChain || []).forEach((entry) => {
add(entry.provider, entry.model); add(entry.provider, entry.model);
@@ -10274,14 +10251,15 @@ function buildOpencodeAttemptChain(cli, preferredModel) {
const chain = []; const chain = [];
const seen = new Set(); const seen = new Set();
// Build chain from opencodeChain with user's preferred model // Build chain from opencodeModels order
opencodeChain.forEach((p, idx) => { // The order in the list determines fallback priority
const modelToUse = idx === 0 && preferredModel ? preferredModel : p.model; opencodeModels.forEach((m, idx) => {
const key = `${p.provider}:${modelToUse}`; const modelToUse = idx === 0 && preferredModel ? preferredModel : m.name;
const key = `${modelToUse}`;
if (seen.has(key)) return; if (seen.has(key)) return;
seen.add(key); seen.add(key);
chain.push({ chain.push({
provider: p.provider, provider: 'opencode',
model: modelToUse, model: modelToUse,
primary: idx === 0, primary: idx === 0,
cli: normalizeCli(cli || 'opencode'), cli: normalizeCli(cli || 'opencode'),
@@ -10289,7 +10267,7 @@ function buildOpencodeAttemptChain(cli, preferredModel) {
}); });
}); });
// If no chain configured, fall back to default // If no models configured, fall back to default
if (chain.length === 0) { if (chain.length === 0) {
chain.push({ chain.push({
provider: 'opencode', provider: 'opencode',
@@ -10305,7 +10283,7 @@ function buildOpencodeAttemptChain(cli, preferredModel) {
cli, cli,
preferredModel: (typeof preferredModel === 'string' && preferredModel.trim()) ? preferredModel : '(none)', preferredModel: (typeof preferredModel === 'string' && preferredModel.trim()) ? preferredModel : '(none)',
chainLength: chain.length, chainLength: chain.length,
models: chain.map(c => `${c.provider}:${c.model}`).slice(0, 5) models: chain.map(c => c.model).slice(0, 5)
}); });
return chain; return chain;
@@ -11339,14 +11317,14 @@ async function processMessage(sessionId, message) {
function getConfiguredModels(cliParam = 'opencode') { function getConfiguredModels(cliParam = 'opencode') {
const cli = normalizeCli(cliParam || 'opencode'); const cli = normalizeCli(cliParam || 'opencode');
// Return opencode models for backwards compatibility // Return opencode models for backwards compatibility
const mapped = opencodeModels.map((m) => ({ const mapped = opencodeModels.map((m, idx) => ({
id: m.id, id: m.id,
name: m.name, name: m.name,
label: m.label || m.name, label: m.label || m.name,
icon: m.icon || '', icon: m.icon || '',
cli: cli, cli: cli,
providers: opencodeChain, providers: [],
primaryProvider: opencodeChain[0]?.provider || 'opencode', primaryProvider: 'opencode',
tier: m.tier || 'free', tier: m.tier || 'free',
multiplier: getTierMultiplier(m.tier || 'free'), multiplier: getTierMultiplier(m.tier || 'free'),
supportsMedia: m.supportsMedia ?? false, supportsMedia: m.supportsMedia ?? false,
@@ -15857,7 +15835,6 @@ async function handleAdminModelsList(req, res) {
// Return clean structure // Return clean structure
sendJson(res, 200, { sendJson(res, 200, {
opencodeModels, opencodeModels,
opencodeChain,
publicModels, publicModels,
// Legacy support // Legacy support
models: getConfiguredModels('opencode'), models: getConfiguredModels('opencode'),
@@ -15900,7 +15877,6 @@ async function handleAdminModelUpsert(req, res) {
await persistAdminModels(); await persistAdminModels();
sendJson(res, 200, { sendJson(res, 200, {
opencodeModels, opencodeModels,
opencodeChain,
publicModels, publicModels,
}); });
} else if (body.type === 'public') { } else if (body.type === 'public') {
@@ -15912,7 +15888,6 @@ async function handleAdminModelUpsert(req, res) {
await persistAdminModels(); await persistAdminModels();
sendJson(res, 200, { sendJson(res, 200, {
opencodeModels, opencodeModels,
opencodeChain,
publicModels, publicModels,
}); });
} else { } else {
@@ -15977,7 +15952,6 @@ async function handleAdminModelsReorder(req, res) {
sendJson(res, 200, { sendJson(res, 200, {
ok: true, ok: true,
opencodeModels, opencodeModels,
opencodeChain,
publicModels, publicModels,
}); });
} catch (error) { } catch (error) {
@@ -16010,72 +15984,10 @@ async function handleAdminModelDelete(req, res, id) {
sendJson(res, 200, { sendJson(res, 200, {
ok: true, ok: true,
opencodeModels, opencodeModels,
opencodeChain,
publicModels, publicModels,
}); });
} }
// Chain handlers
async function handleAdminChainPost(req, res) {
const session = requireAdminAuth(req, res);
if (!session) return;
try {
const body = await parseJsonBody(req);
if (!Array.isArray(body.chain)) {
return sendJson(res, 400, { error: 'chain must be an array' });
}
opencodeChain = body.chain.map((p) => ({
provider: normalizeProviderName(p.provider || 'opencode'),
model: (p.model || '').trim(),
})).filter((p) => !!p.model);
await persistAdminModels();
sendJson(res, 200, {
opencodeChain,
opencodeModels,
publicModels,
});
} catch (error) {
sendJson(res, 400, { error: error.message || 'Unable to update chain' });
}
}
async function handleAdminChainReorder(req, res) {
const session = requireAdminAuth(req, res);
if (!session) return;
try {
const body = await parseJsonBody(req);
if (!Array.isArray(body.chain)) {
return sendJson(res, 400, { error: 'chain array is required' });
}
// Validate all entries
const currentIds = new Set(opencodeChain.map((_, i) => i));
const newIds = body.chain.map((_, i) => i);
const allExist = newIds.every((_, i) => currentIds.has(i));
if (!allExist || body.chain.length !== opencodeChain.length) {
return sendJson(res, 400, { error: 'Invalid chain order' });
}
// Reorder
opencodeChain = body.chain.map((p) => ({
provider: normalizeProviderName(p.provider || 'opencode'),
model: (p.model || '').trim(),
})).filter((p) => !!p.model);
await persistAdminModels();
sendJson(res, 200, {
opencodeChain,
opencodeModels,
publicModels,
});
} catch (error) {
sendJson(res, 400, { error: error.message || 'Unable to reorder chain' });
}
}
async function handleAdminOpenRouterSettingsGet(req, res) { async function handleAdminOpenRouterSettingsGet(req, res) {
const session = requireAdminAuth(req, res); const session = requireAdminAuth(req, res);
if (!session) return; if (!session) return;
@@ -18976,8 +18888,6 @@ async function routeInternal(req, res, url, pathname) {
if (req.method === 'GET' && pathname === '/api/admin/models') return handleAdminModelsList(req, res); if (req.method === 'GET' && pathname === '/api/admin/models') return handleAdminModelsList(req, res);
if (req.method === 'POST' && pathname === '/api/admin/models') return handleAdminModelUpsert(req, res); if (req.method === 'POST' && pathname === '/api/admin/models') return handleAdminModelUpsert(req, res);
if (req.method === 'POST' && pathname === '/api/admin/models/reorder') return handleAdminModelsReorder(req, res); if (req.method === 'POST' && pathname === '/api/admin/models/reorder') return handleAdminModelsReorder(req, res);
if (req.method === 'POST' && pathname === '/api/admin/chain') return handleAdminChainPost(req, res);
if (req.method === 'POST' && pathname === '/api/admin/chain/reorder') return handleAdminChainReorder(req, res);
if (req.method === 'GET' && pathname === '/api/admin/openrouter-settings') return handleAdminOpenRouterSettingsGet(req, res); if (req.method === 'GET' && pathname === '/api/admin/openrouter-settings') return handleAdminOpenRouterSettingsGet(req, res);
if (req.method === 'POST' && pathname === '/api/admin/openrouter-settings') return handleAdminOpenRouterSettingsPost(req, res); if (req.method === 'POST' && pathname === '/api/admin/openrouter-settings') return handleAdminOpenRouterSettingsPost(req, res);
if (req.method === 'GET' && pathname === '/api/admin/mistral-settings') return handleAdminMistralSettingsGet(req, res); if (req.method === 'GET' && pathname === '/api/admin/mistral-settings') return handleAdminMistralSettingsGet(req, res);