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