From 828a9dad410c8d83ab660b92ab8b929a1760027c Mon Sep 17 00:00:00 2001 From: southseact-3d Date: Wed, 18 Feb 2026 16:18:31 +0000 Subject: [PATCH] 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 --- chat/public/admin.html | 65 ++------------ chat/public/admin.js | 122 +------------------------- chat/server.js | 194 +++++++++++------------------------------ 3 files changed, 60 insertions(+), 321 deletions(-) diff --git a/chat/public/admin.html b/chat/public/admin.html index b521dcf..fd29822 100644 --- a/chat/public/admin.html +++ b/chat/public/admin.html @@ -16,18 +16,6 @@ } /* Slightly tighten cards on the build page */ 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); - } @@ -79,13 +67,13 @@ - +

OpenCode Models

Backend
-

Add models from OpenCode. These models process requests and can fall back to the provider chain when rate limits are reached.

+

Add models from OpenCode. When rate limits are reached, the system automatically falls back to the next model in the order below.

@@ -126,57 +114,14 @@
-

OpenCode Models List

+

OpenCode Models Order

0
-

Arrange the order below. This controls which model is primary for OpenCode requests.

+

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.

- - -
-

Fallback Chain (for when rate limits are reached)

-

When the primary OpenCode model hits rate limits, the system falls back through these providers in order.

- - - -
- - -
-
- -
-
- - - -
-
- Fallback Chain Order - 0 -
-
-
-
- +

Public Models

diff --git a/chat/public/admin.js b/chat/public/admin.js index d221356..f258f93 100644 --- a/chat/public/admin.js +++ b/chat/public/admin.js @@ -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'; console.log('Admin JS loaded, pageType:', pageType); - // Clean state structure + // Clean state structure - just two things const state = { - opencodeModels: [], // Models from OpenCode - opencodeChain: [], // Fallback chain for OpenCode + opencodeModels: [], // Models from OpenCode (order determines fallback) publicModels: [], // User-facing models (completely separate) icons: [], availableOpencodeModels: [], // Loaded from OpenCode }; - // Element references - clean structure matching the HTML + // Element references const el = { // OpenCode Models opencodeModelForm: document.getElementById('opencode-model-form'), @@ -27,14 +24,6 @@ opencodeModelsList: document.getElementById('opencode-models-list'), 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 publicModelForm: document.getElementById('public-model-form'), publicModelName: document.getElementById('public-model-name'), @@ -158,10 +147,8 @@ try { const data = await api('/api/admin/models'); state.opencodeModels = data.opencodeModels || []; - state.opencodeChain = data.opencodeChain || []; state.publicModels = data.publicModels || []; renderOpencodeModels(); - renderOpencodeChain(); renderPublicModels(); } catch (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 = '
No fallback chain configured. Add providers above.
'; - return; - } - - state.opencodeChain.forEach((entry, idx) => { - const row = document.createElement('div'); - row.className = 'provider-row slim'; - row.innerHTML = ` -
-
- ${idx === 0 ? 'Primary' : `#${idx + 1}`} - ${entry.provider} - ${entry.model} -
-
- - - -
-
- `; - - 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 function renderPublicModels() { 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 async function movePublicModel(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 if (el.opencodeModelForm) { 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) { el.publicModelForm.addEventListener('submit', async (e) => { e.preventDefault(); diff --git a/chat/server.js b/chat/server.js index 3505acc..548cb3c 100644 --- a/chat/server.js +++ b/chat/server.js @@ -1524,10 +1524,9 @@ const MODELS_DEV_CACHE_TTL = 3600000; // 1 hour const adminSessions = new Map(); let adminModels = []; let adminModelIndex = new Map(); -// Clean model structure -let opencodeModels = []; // Models from OpenCode [{id, name, label, icon, tier, supportsMedia, multiplier}] -let opencodeChain = []; // OpenCode fallback chain [{provider, model}] -let publicModels = []; // Public-facing models (completely separate) [{id, name, label, icon, tier, supportsMedia, multiplier}] +// Simple model structure - just two lists +let opencodeModels = []; // Models from OpenCode - order determines fallback chain +let publicModels = []; // Public-facing models (completely separate) let openrouterSettings = { primaryModel: OPENROUTER_MODEL_PRIMARY, backupModel1: OPENROUTER_MODEL_BACKUP_1, @@ -5161,11 +5160,12 @@ async function ensureOpencodeConfig(session) { // Find which providers are used in models const usedProviders = new Set(); - // Check opencode chain first - if (opencodeChain.length > 0) { - for (const p of opencodeChain) { - if (p.provider) { - usedProviders.add(p.provider.toLowerCase()); + // Check opencode models for provider prefixes in names (e.g., "openrouter/model-name") + for (const model of opencodeModels) { + if (model.name && model.name.includes('/')) { + const providerFromName = model.name.split('/')[0].toLowerCase(); + if (providerFromName && providerFromName !== 'opencode') { + usedProviders.add(providerFromName); } } } @@ -5205,8 +5205,7 @@ async function ensureOpencodeConfig(session) { log('Detected providers from models', { usedProviders: Array.from(usedProviders), - count: usedProviders.size, - source: opencodeChain.length > 0 ? 'opencodeChain' : 'legacy' + count: usedProviders.size }); // Provider configurations with their base URLs @@ -5760,17 +5759,14 @@ async function loadAdminModelStore() { const raw = await fs.readFile(ADMIN_MODELS_FILE, 'utf8').catch(() => '{}'); const parsed = JSON.parse(raw || '{}'); - // Load clean structure + // Load simple structure if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { - // New clean structure opencodeModels = Array.isArray(parsed.opencodeModels) ? parsed.opencodeModels : []; - opencodeChain = Array.isArray(parsed.opencodeChain) ? parsed.opencodeChain : []; publicModels = Array.isArray(parsed.publicModels) ? parsed.publicModels : []; adminModels = Array.isArray(parsed.adminModels) ? parsed.adminModels : []; } else if (Array.isArray(parsed)) { // Old array structure - migrate adminModels = parsed; - // Migrate old admin models to opencode models opencodeModels = parsed.map((m) => ({ id: m.id || randomUUID(), name: m.name, @@ -5781,26 +5777,8 @@ async function loadAdminModelStore() { multiplier: getTierMultiplier(m.tier), })).filter((m) => !!m.name); 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 { opencodeModels = []; - opencodeChain = []; publicModels = []; adminModels = []; } @@ -5827,22 +5805,14 @@ async function loadAdminModelStore() { multiplier: m.multiplier || getTierMultiplier(m.tier), })).filter((m) => !!m.name); - // Validate chain - opencodeChain = opencodeChain.map((p) => ({ - provider: normalizeProviderName(p.provider || 'opencode'), - model: (p.model || '').trim(), - })).filter((p) => !!p.model); - refreshAdminModelIndex(); log('Loaded admin model store', { opencodeModels: opencodeModels.length, - opencodeChain: opencodeChain.length, publicModels: publicModels.length, }); } catch (error) { log('Failed to load admin models, starting empty', { error: String(error) }); opencodeModels = []; - opencodeChain = []; publicModels = []; adminModels = []; refreshAdminModelIndex(); @@ -5852,13 +5822,12 @@ async function loadAdminModelStore() { async function persistAdminModels() { await ensureStateFile(); await ensureAssetsDir(); - // Save clean structure + // Save simple structure const payload = JSON.stringify({ opencodeModels, - opencodeChain, publicModels, - adminModels, // Keep legacy for backwards compatibility - version: 4, // Clean structure + adminModels, + version: 5, }, null, 2); await safeWriteFile(ADMIN_MODELS_FILE, payload); refreshAdminModelIndex(); @@ -6052,20 +6021,25 @@ function collectProviderSeeds() { const normalized = normalizeProviderName(p); if (validProviders.has(normalized)) seeds.add(normalized); }); - // Use opencode chain if available, otherwise fall back to legacy adminModels - if (opencodeChain.length > 0) { - opencodeChain.forEach((p) => { + // Collect providers from opencode models (checking for provider prefixes in model names) + opencodeModels.forEach((m) => { + 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); 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) => { const normalized = normalizeProviderName(entry.provider); if (validProviders.has(normalized)) seeds.add(normalized); @@ -6109,14 +6083,17 @@ async function discoverProviderModels() { collectProviderSeeds().forEach((p) => add(p)); - // Use opencode chain if available, otherwise fall back to legacy adminModels - if (opencodeChain.length > 0) { - opencodeChain.forEach((p) => add(extractProviderName(p), p.model)); - } else { - adminModels.forEach((m) => { - (m.providers || []).forEach((p) => add(extractProviderName(p), p.model || m.name)); - }); - } + // Collect models from opencodeModels (checking for provider prefixes) + opencodeModels.forEach((m) => { + if (m.name && m.name.includes('/')) { + add(extractProviderName(m.name), m.name); + } + }); + + // Also check legacy adminModels + adminModels.forEach((m) => { + (m.providers || []).forEach((p) => add(extractProviderName(p), p.model || m.name)); + }); (planSettings.planningChain || []).forEach((entry) => { add(entry.provider, entry.model); @@ -10274,14 +10251,15 @@ function buildOpencodeAttemptChain(cli, preferredModel) { const chain = []; const seen = new Set(); - // Build chain from opencodeChain with user's preferred model - opencodeChain.forEach((p, idx) => { - const modelToUse = idx === 0 && preferredModel ? preferredModel : p.model; - const key = `${p.provider}:${modelToUse}`; + // Build chain from opencodeModels order + // The order in the list determines fallback priority + opencodeModels.forEach((m, idx) => { + const modelToUse = idx === 0 && preferredModel ? preferredModel : m.name; + const key = `${modelToUse}`; if (seen.has(key)) return; seen.add(key); chain.push({ - provider: p.provider, + provider: 'opencode', model: modelToUse, primary: idx === 0, 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) { chain.push({ provider: 'opencode', @@ -10305,7 +10283,7 @@ function buildOpencodeAttemptChain(cli, preferredModel) { cli, preferredModel: (typeof preferredModel === 'string' && preferredModel.trim()) ? preferredModel : '(none)', 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; @@ -11339,14 +11317,14 @@ async function processMessage(sessionId, message) { function getConfiguredModels(cliParam = 'opencode') { const cli = normalizeCli(cliParam || 'opencode'); // Return opencode models for backwards compatibility - const mapped = opencodeModels.map((m) => ({ + const mapped = opencodeModels.map((m, idx) => ({ id: m.id, name: m.name, label: m.label || m.name, icon: m.icon || '', cli: cli, - providers: opencodeChain, - primaryProvider: opencodeChain[0]?.provider || 'opencode', + providers: [], + primaryProvider: 'opencode', tier: m.tier || 'free', multiplier: getTierMultiplier(m.tier || 'free'), supportsMedia: m.supportsMedia ?? false, @@ -15857,7 +15835,6 @@ async function handleAdminModelsList(req, res) { // Return clean structure sendJson(res, 200, { opencodeModels, - opencodeChain, publicModels, // Legacy support models: getConfiguredModels('opencode'), @@ -15900,7 +15877,6 @@ async function handleAdminModelUpsert(req, res) { await persistAdminModels(); sendJson(res, 200, { opencodeModels, - opencodeChain, publicModels, }); } else if (body.type === 'public') { @@ -15912,7 +15888,6 @@ async function handleAdminModelUpsert(req, res) { await persistAdminModels(); sendJson(res, 200, { opencodeModels, - opencodeChain, publicModels, }); } else { @@ -15977,7 +15952,6 @@ async function handleAdminModelsReorder(req, res) { sendJson(res, 200, { ok: true, opencodeModels, - opencodeChain, publicModels, }); } catch (error) { @@ -16010,72 +15984,10 @@ async function handleAdminModelDelete(req, res, id) { sendJson(res, 200, { ok: true, opencodeModels, - opencodeChain, 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) { const session = requireAdminAuth(req, res); 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 === '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/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 === 'POST' && pathname === '/api/admin/openrouter-settings') return handleAdminOpenRouterSettingsPost(req, res); if (req.method === 'GET' && pathname === '/api/admin/mistral-settings') return handleAdminMistralSettingsGet(req, res);