Clean up model structure: OpenCode Models, Chain, and Public Models
- Simplified to 3 clear sections: 1. OpenCode Models: OpenCode dropdown + display name + order buttons 2. OpenCode Chain: Fallback chain with add form + order buttons 3. Public Models: Manual entry + order buttons (completely separate) - New state variables: opencodeModels, opencodeChain, publicModels - Clean API endpoints for chain operations - Removed all confusing legacy code and naming
This commit is contained in:
@@ -67,57 +67,71 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-grid">
|
||||
<!-- Provider Models Section (with OpenCode integration) -->
|
||||
<div class="admin-card">
|
||||
<header>
|
||||
<h3>Add Provider Model</h3>
|
||||
<div class="pill">OpenCode</div>
|
||||
</header>
|
||||
<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="provider-model-form" class="admin-form">
|
||||
<!-- Section 1: OpenCode Models -->
|
||||
<div class="admin-card">
|
||||
<header>
|
||||
<h3>OpenCode Models</h3>
|
||||
<div class="pill">Backend</div>
|
||||
</header>
|
||||
<p style="margin-top:0; color: var(--muted);">Add models from OpenCode. These models process requests and use the OpenCode Chain below for fallback when rate limits are reached.</p>
|
||||
|
||||
<!-- Add Form -->
|
||||
<form id="opencode-model-form" class="admin-form" style="margin-bottom: 24px;">
|
||||
<label>
|
||||
Choose from OpenCode
|
||||
<select id="opencode-model-select"></select>
|
||||
</label>
|
||||
<label>
|
||||
Display name
|
||||
<input id="opencode-model-label" type="text" placeholder="e.g., GPT-4 Turbo" required />
|
||||
</label>
|
||||
<div class="admin-grid" style="grid-template-columns: 1fr 1fr; gap: 12px;">
|
||||
<label>
|
||||
Choose a model from OpenCode
|
||||
<select id="available-models"></select>
|
||||
</label>
|
||||
<label>
|
||||
Display name shown to users
|
||||
<input id="provider-model-label" type="text" placeholder="Friendly label (e.g., Claude 3.5 Sonnet)" required />
|
||||
</label>
|
||||
<label>
|
||||
Model tier (for plan limits)
|
||||
<select id="provider-model-tier">
|
||||
<option value="free">Free (1x multiplier)</option>
|
||||
<option value="plus">Plus (2x multiplier)</option>
|
||||
<option value="pro">Pro (3x multiplier)</option>
|
||||
Tier
|
||||
<select id="opencode-model-tier">
|
||||
<option value="free">Free (1x)</option>
|
||||
<option value="plus">Plus (2x)</option>
|
||||
<option value="pro">Pro (3x)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Icon (files in /assets)
|
||||
<select id="provider-model-icon">
|
||||
Icon
|
||||
<select id="opencode-model-icon">
|
||||
<option value="">No icon</option>
|
||||
</select>
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 8px; margin-top: 8px;">
|
||||
<input id="provider-model-media" type="checkbox" style="width: auto;" />
|
||||
<span>Supports image uploads</span>
|
||||
</label>
|
||||
<div class="admin-actions">
|
||||
<button type="submit" class="primary">Add Provider Model</button>
|
||||
<button type="button" id="reload-available" class="ghost">Reload available models</button>
|
||||
</div>
|
||||
<div class="status-line" id="provider-model-status"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<label style="display: flex; align-items: center; gap: 8px; margin-top: 8px;">
|
||||
<input id="opencode-model-media" type="checkbox" style="width: auto;" />
|
||||
<span>Supports image uploads</span>
|
||||
</label>
|
||||
<div class="admin-actions">
|
||||
<button type="submit" class="primary">Add OpenCode Model</button>
|
||||
<button type="button" id="reload-opencode-models" class="ghost">Reload Models</button>
|
||||
</div>
|
||||
<div class="status-line" id="opencode-model-status"></div>
|
||||
</form>
|
||||
|
||||
<!-- Provider Chain Add Section -->
|
||||
<div class="admin-card">
|
||||
<header>
|
||||
<h3>Add to Provider Chain</h3>
|
||||
<div class="pill">Fallback</div>
|
||||
</header>
|
||||
<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">
|
||||
<!-- List -->
|
||||
<header style="margin-top: 24px; border-top: 1px solid var(--border); padding-top: 16px;">
|
||||
<h3>OpenCode Models List</h3>
|
||||
<div class="pill" id="opencode-models-count">0</div>
|
||||
</header>
|
||||
<p class="muted" style="margin-top:0;">Arrange the order below. This controls which model is primary for OpenCode requests.</p>
|
||||
<div id="opencode-models-list" class="admin-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Section 2: OpenCode Chain (Fallback) -->
|
||||
<div class="admin-card" style="margin-top: 16px;">
|
||||
<header>
|
||||
<h3>OpenCode Chain</h3>
|
||||
<div class="pill">Fallback</div>
|
||||
</header>
|
||||
<p style="margin-top:0; color: var(--muted);">When rate limits are reached or errors occur, the system falls back through this chain of providers.</p>
|
||||
|
||||
<!-- Add Form -->
|
||||
<form id="chain-form" class="admin-form" style="margin-bottom: 24px;">
|
||||
<div class="admin-grid" style="grid-template-columns: 1fr 2fr; gap: 12px;">
|
||||
<label>
|
||||
Provider
|
||||
<select id="chain-provider">
|
||||
@@ -129,65 +143,66 @@
|
||||
<option value="chutes">Chutes</option>
|
||||
<option value="cerebras">Cerebras</option>
|
||||
<option value="ollama">Ollama</option>
|
||||
<option value="opencode">OpenCode</option>
|
||||
<option value="cohere">Cohere</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Model Name
|
||||
Model ID
|
||||
<input id="chain-model" type="text" placeholder="e.g., anthropic/claude-3.5-sonnet" required />
|
||||
</label>
|
||||
<div class="admin-actions">
|
||||
<button type="submit" class="primary">Add to Chain</button>
|
||||
</div>
|
||||
<div class="status-line" id="provider-chain-status"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-actions">
|
||||
<button type="submit" class="primary">Add to Chain</button>
|
||||
</div>
|
||||
<div class="status-line" id="chain-status"></div>
|
||||
</form>
|
||||
|
||||
<!-- List -->
|
||||
<header style="margin-top: 24px; border-top: 1px solid var(--border); padding-top: 16px;">
|
||||
<h3>Chain Order</h3>
|
||||
<div class="pill" id="chain-count">0</div>
|
||||
</header>
|
||||
<p class="muted" style="margin-top:0;">The system tries providers in this order. Use arrows to reorder.</p>
|
||||
<div id="chain-list" class="admin-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Provider Chain List with Ordering -->
|
||||
<!-- Section 3: Public Models (Completely Separate) -->
|
||||
<div class="admin-card" style="margin-top: 16px;">
|
||||
<header>
|
||||
<h3>Unified Provider Chain Order</h3>
|
||||
<div class="pill" id="provider-chain-count">0</div>
|
||||
<h3>Public Models</h3>
|
||||
<div class="pill">User-Facing</div>
|
||||
</header>
|
||||
<p class="muted" style="margin-top: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.</p>
|
||||
<div id="provider-chain-list" class="admin-list"></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;">
|
||||
<p style="margin-top:0; color: var(--muted);">These models are displayed to users in the builder dropdown. They are completely separate from OpenCode models.</p>
|
||||
|
||||
<!-- Add Form -->
|
||||
<form id="public-model-form" class="admin-form" style="margin-bottom: 24px;">
|
||||
<div class="admin-grid" style="grid-template-columns: 1fr 1fr; gap: 12px;">
|
||||
<label>
|
||||
Model ID
|
||||
<input id="public-model-name" type="text" placeholder="e.g., custom-model-1" required />
|
||||
<input id="public-model-name" type="text" placeholder="e.g., my-custom-model" required />
|
||||
</label>
|
||||
<label>
|
||||
Display name
|
||||
<input id="public-model-label" type="text" placeholder="Friendly label" required />
|
||||
<input id="public-model-label" type="text" placeholder="e.g., My Custom Model" 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;">
|
||||
<div class="admin-grid" style="grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 12px;">
|
||||
<label>
|
||||
Tier
|
||||
<select id="public-model-tier">
|
||||
<option value="free">Free (1x)</option>
|
||||
<option value="plus">Plus (2x)</option>
|
||||
<option value="pro">Pro (3x)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Icon
|
||||
<select id="public-model-icon">
|
||||
<option value="">No icon</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<label style="display: flex; align-items: center; gap: 8px; margin-top: 12px;">
|
||||
<input id="public-model-media" type="checkbox" style="width: auto;" />
|
||||
<span>Supports image uploads</span>
|
||||
</label>
|
||||
@@ -196,60 +211,18 @@
|
||||
</div>
|
||||
<div class="status-line" id="public-model-status"></div>
|
||||
</form>
|
||||
|
||||
<header style="margin-top: 24px;">
|
||||
|
||||
<!-- List -->
|
||||
<header style="margin-top: 24px; border-top: 1px solid var(--border); padding-top: 16px;">
|
||||
<h3>Public Models List</h3>
|
||||
<div class="pill" id="public-models-count">0</div>
|
||||
</header>
|
||||
<p class="muted" style="margin-top:0;">These are shown to users. Use arrows to reorder.</p>
|
||||
<div id="public-models-list" class="admin-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Other Admin Sections -->
|
||||
<div class="admin-grid" style="margin-top: 16px;">
|
||||
<!-- Legacy Model Form (hidden but kept for compatibility) -->
|
||||
<div class="admin-card" style="display: none;">
|
||||
<header>
|
||||
<h3>Add / Update Model (Legacy)</h3>
|
||||
<div class="pill">Step 1</div>
|
||||
</header>
|
||||
<form id="model-form" class="admin-form">
|
||||
<label>
|
||||
Choose a model from OpenCode
|
||||
<select id="available-models"></select>
|
||||
</label>
|
||||
<label>
|
||||
Display name shown to users
|
||||
<input id="display-label" type="text" placeholder="Friendly label (e.g. Fast GPT-4)" required />
|
||||
</label>
|
||||
<label>
|
||||
Model tier (for plan limits)
|
||||
<select id="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="icon-select">
|
||||
<option value="">No icon</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Provider priority (comma separated provider:model)
|
||||
<input id="provider-order" type="text" placeholder="openrouter:anthropic/claude-3.5-sonnet, mistral:mistral-large-latest" />
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 8px; margin-top: 8px;">
|
||||
<input id="supports-media" type="checkbox" style="width: auto;" />
|
||||
<span>Supports image uploads</span>
|
||||
</label>
|
||||
<div class="admin-actions">
|
||||
<button type="submit" class="primary">Save model</button>
|
||||
<button type="button" id="reload-available" class="ghost">Reload available models</button>
|
||||
</div>
|
||||
<div class="status-line" id="admin-status"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<header>
|
||||
<h3>System Actions</h3>
|
||||
@@ -261,136 +234,18 @@
|
||||
<div class="status-line" id="cancel-messages-status"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<header>
|
||||
<h3>Icon Library</h3>
|
||||
<div class="pill">Step 0</div>
|
||||
<div class="pill">Assets</div>
|
||||
</header>
|
||||
<p style="margin-top:0; color: var(--muted);">Upload icon files to <strong>/chat/public/assets</strong> and pick them here. PNG, JPG, SVG, and WEBP are supported.</p>
|
||||
<p style="margin-top:0; color: var(--muted);">Upload icon files to <strong>/chat/public/assets</strong>. PNG, JPG, SVG, and WEBP are supported.</p>
|
||||
<div id="icon-list" class="admin-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-card" style="margin-top: 16px;">
|
||||
<header>
|
||||
<h3>OpenCode Ultimate Backup Model</h3>
|
||||
<div class="pill">Fallback</div>
|
||||
</header>
|
||||
<p style="margin-top:0; color: var(--muted);">Configure the ultimate fallback model that will be used when all providers fail. This is the last-resort backup for reliability.</p>
|
||||
<form id="opencode-backup-form" class="admin-form">
|
||||
<label>
|
||||
OpenCode backup model
|
||||
<select id="opencode-backup"></select>
|
||||
</label>
|
||||
<div class="admin-actions">
|
||||
<button type="submit" class="primary">Save backup model</button>
|
||||
</div>
|
||||
<div class="status-line" id="opencode-backup-status"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="admin-card" style="margin-top: 16px;">
|
||||
<header>
|
||||
<h3>Auto Model for Hobby/Free Plan</h3>
|
||||
<div class="pill">Free Plan</div>
|
||||
</header>
|
||||
<p style="margin-top:0; color: var(--muted);">Select which model Hobby and Free plan users will automatically use. Paid plan users can select their own models.</p>
|
||||
<form id="auto-model-form" class="admin-form">
|
||||
<label>
|
||||
Model for hobby/free users
|
||||
<select id="auto-model-select">
|
||||
<option value="">Auto (use first configured model)</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="admin-actions">
|
||||
<button type="submit" class="primary">Save auto model</button>
|
||||
</div>
|
||||
<div class="status-line" id="auto-model-status"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="admin-grid" style="margin-top: 16px;">
|
||||
<div class="admin-card">
|
||||
<header>
|
||||
<h3>Provider Limits & Usage</h3>
|
||||
<div class="pill">Rate limits</div>
|
||||
</header>
|
||||
<p style="margin-top:0; color: var(--muted);">Configure token/request limits per provider or per model and monitor current usage.</p>
|
||||
<form id="provider-limit-form" class="admin-form">
|
||||
<label>
|
||||
Provider
|
||||
<select id="limit-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="opencode">OpenCode</option>
|
||||
<option value="cohere">Cohere</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Scope
|
||||
<select id="limit-scope">
|
||||
<option value="provider">Per Provider</option>
|
||||
<option value="model">Per Model</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Model (for per-model limits)
|
||||
<select id="limit-model">
|
||||
<option value="">Any model</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Tokens per minute
|
||||
<input id="limit-tpm" type="number" min="0" step="1" placeholder="0 = unlimited" />
|
||||
</label>
|
||||
<label>
|
||||
Tokens per hour
|
||||
<input id="limit-tph" type="number" min="0" step="1" placeholder="0 = unlimited" />
|
||||
</label>
|
||||
<label>
|
||||
Tokens per day
|
||||
<input id="limit-tpd" type="number" min="0" step="1" placeholder="0 = unlimited" />
|
||||
</label>
|
||||
<label>
|
||||
Requests per minute
|
||||
<input id="limit-rpm" type="number" min="0" step="1" placeholder="0 = unlimited" />
|
||||
</label>
|
||||
<label>
|
||||
Requests per hour
|
||||
<input id="limit-rph" type="number" min="0" step="1" placeholder="0 = unlimited" />
|
||||
</label>
|
||||
<label>
|
||||
Requests per day
|
||||
<input id="limit-rpd" type="number" min="0" step="1" placeholder="0 = unlimited" />
|
||||
</label>
|
||||
<div class="admin-actions">
|
||||
<button type="submit" class="primary">Save limits</button>
|
||||
</div>
|
||||
<div class="status-line" id="provider-limit-status"></div>
|
||||
</form>
|
||||
<div id="provider-usage" class="admin-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Public Models List -->
|
||||
<div class="admin-card" style="margin-top: 16px;">
|
||||
<header>
|
||||
<h3>Public-Facing Models</h3>
|
||||
<div class="pill" id="configured-count">0</div>
|
||||
</header>
|
||||
<p class="muted" style="margin-top:0;">These models are displayed to users in the builder. All models use the unified provider chain above for fallback.</p>
|
||||
<div id="configured-list" class="admin-list"></div>
|
||||
</div>
|
||||
</main>
|
||||
<script src="/admin.js"></script>
|
||||
</div>
|
||||
<script src="/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
3318
chat/public/admin.js
3318
chat/public/admin.js
File diff suppressed because it is too large
Load Diff
376
chat/server.js
376
chat/server.js
@@ -1524,10 +1524,10 @@ const MODELS_DEV_CACHE_TTL = 3600000; // 1 hour
|
||||
const adminSessions = new Map();
|
||||
let adminModels = [];
|
||||
let adminModelIndex = new Map();
|
||||
// New unified model chain structure
|
||||
let providerModels = []; // Provider models from OpenCode [{id, name, label, icon, tier, supportsMedia, multiplier}]
|
||||
// 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}]
|
||||
let providerChain = []; // Unified fallback chain [{provider, model, primary}]
|
||||
let openrouterSettings = {
|
||||
primaryModel: OPENROUTER_MODEL_PRIMARY,
|
||||
backupModel1: OPENROUTER_MODEL_BACKUP_1,
|
||||
@@ -5757,21 +5757,21 @@ async function loadAdminModelStore() {
|
||||
try {
|
||||
await ensureStateFile();
|
||||
await ensureAssetsDir();
|
||||
const raw = await fs.readFile(ADMIN_MODELS_FILE, 'utf8').catch(() => '[]');
|
||||
const parsed = JSON.parse(raw || '[]');
|
||||
const raw = await fs.readFile(ADMIN_MODELS_FILE, 'utf8').catch(() => '{}');
|
||||
const parsed = JSON.parse(raw || '{}');
|
||||
|
||||
// Check if using new unified structure or old structure
|
||||
// Load clean structure
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
// New unified structure
|
||||
providerModels = Array.isArray(parsed.providerModels) ? parsed.providerModels : [];
|
||||
// New clean structure
|
||||
opencodeModels = Array.isArray(parsed.opencodeModels) ? parsed.opencodeModels : [];
|
||||
opencodeChain = Array.isArray(parsed.opencodeChain) ? parsed.opencodeChain : [];
|
||||
publicModels = Array.isArray(parsed.publicModels) ? parsed.publicModels : [];
|
||||
providerChain = Array.isArray(parsed.providerChain) ? parsed.providerChain : [];
|
||||
adminModels = Array.isArray(parsed.adminModels) ? parsed.adminModels : [];
|
||||
} else if (Array.isArray(parsed)) {
|
||||
// Old array structure - migrate to new
|
||||
// Old array structure - migrate
|
||||
adminModels = parsed;
|
||||
// Create provider models from admin models (these are from OpenCode)
|
||||
providerModels = parsed.map((m) => ({
|
||||
// Migrate old admin models to opencode models
|
||||
opencodeModels = parsed.map((m) => ({
|
||||
id: m.id || randomUUID(),
|
||||
name: m.name,
|
||||
label: m.label || m.name,
|
||||
@@ -5780,35 +5780,33 @@ async function loadAdminModelStore() {
|
||||
supportsMedia: m.supportsMedia ?? false,
|
||||
multiplier: getTierMultiplier(m.tier),
|
||||
})).filter((m) => !!m.name);
|
||||
// Public models start empty (user can add these manually later)
|
||||
publicModels = [];
|
||||
// Create unified provider chain from all unique providers
|
||||
// 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, primary: true }];
|
||||
: [{ 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,
|
||||
primary: allProviders.size === 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
providerChain = Array.from(allProviders.values());
|
||||
opencodeChain = Array.from(allProviders.values());
|
||||
} else {
|
||||
providerModels = [];
|
||||
opencodeModels = [];
|
||||
opencodeChain = [];
|
||||
publicModels = [];
|
||||
providerChain = [];
|
||||
adminModels = [];
|
||||
}
|
||||
|
||||
// Ensure all provider models have required fields
|
||||
providerModels = providerModels.map((m) => ({
|
||||
// Validate opencode models
|
||||
opencodeModels = opencodeModels.map((m) => ({
|
||||
id: m.id || randomUUID(),
|
||||
name: m.name,
|
||||
label: m.label || m.name,
|
||||
@@ -5818,7 +5816,7 @@ async function loadAdminModelStore() {
|
||||
multiplier: m.multiplier || getTierMultiplier(m.tier),
|
||||
})).filter((m) => !!m.name);
|
||||
|
||||
// Ensure all public models have required fields
|
||||
// Validate public models
|
||||
publicModels = publicModels.map((m) => ({
|
||||
id: m.id || randomUUID(),
|
||||
name: m.name,
|
||||
@@ -5829,25 +5827,23 @@ async function loadAdminModelStore() {
|
||||
multiplier: m.multiplier || getTierMultiplier(m.tier),
|
||||
})).filter((m) => !!m.name);
|
||||
|
||||
// Ensure all provider chain entries have required fields
|
||||
providerChain = providerChain.map((p, idx) => ({
|
||||
// Validate chain
|
||||
opencodeChain = opencodeChain.map((p) => ({
|
||||
provider: normalizeProviderName(p.provider || 'opencode'),
|
||||
model: (p.model || '').trim(),
|
||||
primary: idx === 0,
|
||||
})).filter((p) => !!p.model);
|
||||
|
||||
refreshAdminModelIndex();
|
||||
log('Loaded admin model store', {
|
||||
providerModels: providerModels.length,
|
||||
publicModels: publicModels.length,
|
||||
providerChain: providerChain.length,
|
||||
legacyAdminModels: adminModels.length
|
||||
opencodeModels: opencodeModels.length,
|
||||
opencodeChain: opencodeChain.length,
|
||||
publicModels: publicModels.length,
|
||||
});
|
||||
} catch (error) {
|
||||
log('Failed to load admin models, starting empty', { error: String(error) });
|
||||
providerModels = [];
|
||||
opencodeModels = [];
|
||||
opencodeChain = [];
|
||||
publicModels = [];
|
||||
providerChain = [];
|
||||
adminModels = [];
|
||||
refreshAdminModelIndex();
|
||||
}
|
||||
@@ -5856,13 +5852,13 @@ async function loadAdminModelStore() {
|
||||
async function persistAdminModels() {
|
||||
await ensureStateFile();
|
||||
await ensureAssetsDir();
|
||||
// Save new unified structure
|
||||
// Save clean structure
|
||||
const payload = JSON.stringify({
|
||||
providerModels,
|
||||
opencodeModels,
|
||||
opencodeChain,
|
||||
publicModels,
|
||||
providerChain,
|
||||
adminModels, // Keep legacy for backwards compatibility
|
||||
version: 3, // Schema version - added providerModels
|
||||
version: 4, // Clean structure
|
||||
}, null, 2);
|
||||
await safeWriteFile(ADMIN_MODELS_FILE, payload);
|
||||
refreshAdminModelIndex();
|
||||
@@ -10278,8 +10274,8 @@ function buildOpencodeAttemptChain(cli, preferredModel) {
|
||||
const chain = [];
|
||||
const seen = new Set();
|
||||
|
||||
// Build chain from unified providerChain with user's preferred model
|
||||
providerChain.forEach((p, idx) => {
|
||||
// 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}`;
|
||||
if (seen.has(key)) return;
|
||||
@@ -10293,31 +10289,15 @@ function buildOpencodeAttemptChain(cli, preferredModel) {
|
||||
});
|
||||
});
|
||||
|
||||
// If no provider chain, fall back to old behavior
|
||||
// If no chain configured, fall back to default
|
||||
if (chain.length === 0) {
|
||||
const addProviderOptions = (modelName) => {
|
||||
const providers = resolveModelProviders(modelName);
|
||||
providers.forEach((p, idx) => {
|
||||
const key = `${p.provider}:${p.model || modelName}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
chain.push({
|
||||
provider: p.provider,
|
||||
model: p.model || modelName,
|
||||
primary: typeof p.primary === 'boolean' ? p.primary : idx === 0,
|
||||
cli: normalizeCli(p.cli || cli || 'opencode'),
|
||||
sourceModel: modelName,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (typeof preferredModel === 'string' && preferredModel.trim()) {
|
||||
addProviderOptions(preferredModel);
|
||||
}
|
||||
getConfiguredModels(cli).forEach((m) => {
|
||||
if (m.name && m.name !== preferredModel) addProviderOptions(m.name);
|
||||
chain.push({
|
||||
provider: 'opencode',
|
||||
model: preferredModel || 'default',
|
||||
primary: true,
|
||||
cli: normalizeCli(cli || 'opencode'),
|
||||
sourceModel: preferredModel || 'default',
|
||||
});
|
||||
addProviderOptions('default');
|
||||
}
|
||||
|
||||
// Log the built chain for debugging
|
||||
@@ -11358,20 +11338,20 @@ async function processMessage(sessionId, message) {
|
||||
|
||||
function getConfiguredModels(cliParam = 'opencode') {
|
||||
const cli = normalizeCli(cliParam || 'opencode');
|
||||
// Use new publicModels array - filter by CLI if needed (though public models don't have CLI field)
|
||||
const mapped = publicModels.map((m) => ({
|
||||
// Return opencode models for backwards compatibility
|
||||
const mapped = opencodeModels.map((m) => ({
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
label: m.label || m.name,
|
||||
icon: m.icon || '',
|
||||
cli: cli, // All public models work with opencode CLI
|
||||
providers: providerChain, // Use unified provider chain
|
||||
primaryProvider: providerChain[0]?.provider || 'opencode',
|
||||
cli: cli,
|
||||
providers: opencodeChain,
|
||||
primaryProvider: opencodeChain[0]?.provider || 'opencode',
|
||||
tier: m.tier || 'free',
|
||||
multiplier: getTierMultiplier(m.tier || 'free'),
|
||||
supportsMedia: m.supportsMedia ?? false,
|
||||
}));
|
||||
return mapped.sort((a, b) => (a.label || '').localeCompare(b.label || ''));
|
||||
return mapped;
|
||||
}
|
||||
|
||||
async function handleModels(_req, res, cliParam = null) {
|
||||
@@ -15874,11 +15854,11 @@ async function handleAdminListIcons(req, res) {
|
||||
async function handleAdminModelsList(req, res) {
|
||||
const session = requireAdminAuth(req, res);
|
||||
if (!session) return;
|
||||
// Return new unified structure
|
||||
// Return clean structure
|
||||
sendJson(res, 200, {
|
||||
providerModels,
|
||||
opencodeModels,
|
||||
opencodeChain,
|
||||
publicModels,
|
||||
providerChain,
|
||||
// Legacy support
|
||||
models: getConfiguredModels('opencode'),
|
||||
});
|
||||
@@ -15889,132 +15869,54 @@ async function handleAdminModelUpsert(req, res) {
|
||||
if (!session) return;
|
||||
try {
|
||||
const body = await parseJsonBody(req);
|
||||
const modelName = (body.name || body.model || '').trim();
|
||||
const label = (body.label || body.displayName || modelName).trim();
|
||||
const tier = normalizeTier(body.tier);
|
||||
|
||||
// Check if this is a provider model, public model, or provider chain update
|
||||
if (body.type === 'providerModel') {
|
||||
// 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);
|
||||
if (!modelName) return sendJson(res, 400, { error: 'Model name is required' });
|
||||
|
||||
let icon = '';
|
||||
if (typeof body.icon === 'string' && body.icon.trim()) {
|
||||
icon = await normalizeIconPath(body.icon) || '';
|
||||
}
|
||||
const supportsMedia = typeof body.supportsMedia === 'boolean' ? body.supportsMedia : false;
|
||||
|
||||
const payload = {
|
||||
id: body.id || randomUUID(),
|
||||
name: modelName,
|
||||
label: label || modelName,
|
||||
icon,
|
||||
tier,
|
||||
supportsMedia,
|
||||
multiplier: getTierMultiplier(tier),
|
||||
};
|
||||
|
||||
if (body.type === 'opencode') {
|
||||
// Add to opencode models
|
||||
const existingIndex = body.id ? opencodeModels.findIndex((m) => m.id === body.id) : -1;
|
||||
if (existingIndex >= 0) opencodeModels[existingIndex] = { ...opencodeModels[existingIndex], ...payload };
|
||||
else opencodeModels.push(payload);
|
||||
|
||||
await persistAdminModels();
|
||||
sendJson(res, 200, {
|
||||
providerModel: payload,
|
||||
providerModels,
|
||||
opencodeModels,
|
||||
opencodeChain,
|
||||
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 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 = publicModels.findIndex((m) => m.id === id);
|
||||
let icon = publicModels[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),
|
||||
};
|
||||
|
||||
} else if (body.type === 'public') {
|
||||
// Add to public models
|
||||
const existingIndex = body.id ? publicModels.findIndex((m) => m.id === body.id) : -1;
|
||||
if (existingIndex >= 0) publicModels[existingIndex] = { ...publicModels[existingIndex], ...payload };
|
||||
else publicModels.push(payload);
|
||||
|
||||
await persistAdminModels();
|
||||
sendJson(res, 200, {
|
||||
publicModel: payload,
|
||||
providerModels,
|
||||
opencodeModels,
|
||||
opencodeChain,
|
||||
publicModels,
|
||||
providerChain,
|
||||
models: getConfiguredModels('opencode'),
|
||||
});
|
||||
} else if (body.type === 'providerChain') {
|
||||
// Handle provider chain update
|
||||
if (!Array.isArray(body.chain)) {
|
||||
return sendJson(res, 400, { error: 'Provider chain must be an array' });
|
||||
}
|
||||
|
||||
providerChain = body.chain.map((p, idx) => ({
|
||||
provider: normalizeProviderName(p.provider || 'opencode'),
|
||||
model: (p.model || '').trim(),
|
||||
primary: idx === 0,
|
||||
})).filter((p) => !!p.model);
|
||||
|
||||
await persistAdminModels();
|
||||
sendJson(res, 200, {
|
||||
providerChain,
|
||||
publicModels: publicModels.sort((a, b) => (a.label || '').localeCompare(b.label || '')),
|
||||
models: getConfiguredModels('opencode'),
|
||||
});
|
||||
} else {
|
||||
// Legacy support - treat as public model
|
||||
const modelName = (body.name || body.model || '').trim();
|
||||
if (!modelName) return sendJson(res, 400, { error: 'Model name is required' });
|
||||
const id = body.id || randomUUID();
|
||||
const label = (body.label || body.displayName || modelName).trim();
|
||||
const tier = normalizeTier(body.tier);
|
||||
let icon = '';
|
||||
if (typeof body.icon === 'string' && body.icon.trim()) {
|
||||
icon = await normalizeIconPath(body.icon) || '';
|
||||
}
|
||||
const supportsMedia = typeof body.supportsMedia === 'boolean' ? body.supportsMedia : false;
|
||||
|
||||
const payload = {
|
||||
id,
|
||||
name: modelName,
|
||||
label: label || modelName,
|
||||
icon,
|
||||
tier,
|
||||
supportsMedia,
|
||||
multiplier: getTierMultiplier(tier),
|
||||
};
|
||||
|
||||
const existingIndex = publicModels.findIndex((m) => m.id === id);
|
||||
if (existingIndex >= 0) publicModels[existingIndex] = { ...publicModels[existingIndex], ...payload };
|
||||
else publicModels.push(payload);
|
||||
|
||||
await persistAdminModels();
|
||||
sendJson(res, 200, {
|
||||
model: payload,
|
||||
publicModels: publicModels,
|
||||
providerChain,
|
||||
models: getConfiguredModels('opencode'),
|
||||
});
|
||||
return sendJson(res, 400, { error: 'Invalid type. Use "opencode" or "public"' });
|
||||
}
|
||||
} catch (error) {
|
||||
sendJson(res, 400, { error: error.message || 'Unable to save model' });
|
||||
@@ -16030,11 +15932,11 @@ async function handleAdminModelsReorder(req, res) {
|
||||
return sendJson(res, 400, { error: 'models array is required' });
|
||||
}
|
||||
|
||||
const reorderType = body.type || 'publicModels';
|
||||
const reorderType = body.type || 'public';
|
||||
|
||||
if (reorderType === 'providerModels') {
|
||||
// Validate that all provided IDs exist in providerModels
|
||||
const currentIds = new Set(providerModels.map(m => m.id));
|
||||
if (reorderType === 'opencode') {
|
||||
// Validate that all provided IDs exist in opencodeModels
|
||||
const currentIds = new Set(opencodeModels.map(m => m.id));
|
||||
const newIds = body.models.map(m => m.id);
|
||||
const allExist = newIds.every(id => currentIds.has(id));
|
||||
|
||||
@@ -16042,14 +15944,14 @@ async function handleAdminModelsReorder(req, res) {
|
||||
return sendJson(res, 400, { error: 'Invalid model IDs in reorder request' });
|
||||
}
|
||||
|
||||
// Reorder providerModels based on the provided order
|
||||
// Reorder opencodeModels based on the provided order
|
||||
const reordered = [];
|
||||
body.models.forEach(m => {
|
||||
const model = providerModels.find(pm => pm.id === m.id);
|
||||
const model = opencodeModels.find(pm => pm.id === m.id);
|
||||
if (model) reordered.push(model);
|
||||
});
|
||||
|
||||
providerModels = reordered;
|
||||
opencodeModels = reordered;
|
||||
} else {
|
||||
// Validate that all provided IDs exist in publicModels
|
||||
const currentIds = new Set(publicModels.map(m => m.id));
|
||||
@@ -16074,10 +15976,9 @@ async function handleAdminModelsReorder(req, res) {
|
||||
|
||||
sendJson(res, 200, {
|
||||
ok: true,
|
||||
providerModels,
|
||||
opencodeModels,
|
||||
opencodeChain,
|
||||
publicModels,
|
||||
providerChain,
|
||||
models: getConfiguredModels('opencode'),
|
||||
});
|
||||
} catch (error) {
|
||||
sendJson(res, 400, { error: error.message || 'Unable to reorder models' });
|
||||
@@ -16088,30 +15989,93 @@ async function handleAdminModelDelete(req, res, id) {
|
||||
const session = requireAdminAuth(req, res);
|
||||
if (!session) return;
|
||||
|
||||
// Try to delete from providerModels first
|
||||
const beforeProvider = providerModels.length;
|
||||
providerModels = providerModels.filter((m) => m.id !== id);
|
||||
const deletedFromProvider = providerModels.length < beforeProvider;
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
const deleteType = url.searchParams.get('type') || 'public';
|
||||
|
||||
// Try to delete from publicModels
|
||||
const beforePublic = publicModels.length;
|
||||
publicModels = publicModels.filter((m) => m.id !== id);
|
||||
const deletedFromPublic = publicModels.length < beforePublic;
|
||||
|
||||
if (!deletedFromProvider && !deletedFromPublic) {
|
||||
return sendJson(res, 404, { error: 'Model not found' });
|
||||
if (deleteType === 'opencode') {
|
||||
const before = opencodeModels.length;
|
||||
opencodeModels = opencodeModels.filter((m) => m.id !== id);
|
||||
if (opencodeModels.length === before) {
|
||||
return sendJson(res, 404, { error: 'Model not found' });
|
||||
}
|
||||
} else {
|
||||
const before = publicModels.length;
|
||||
publicModels = publicModels.filter((m) => m.id !== id);
|
||||
if (publicModels.length === before) {
|
||||
return sendJson(res, 404, { error: 'Model not found' });
|
||||
}
|
||||
}
|
||||
|
||||
await persistAdminModels();
|
||||
sendJson(res, 200, {
|
||||
ok: true,
|
||||
providerModels,
|
||||
opencodeModels,
|
||||
opencodeChain,
|
||||
publicModels,
|
||||
providerChain,
|
||||
models: getConfiguredModels('opencode'),
|
||||
});
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -19012,6 +18976,8 @@ 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);
|
||||
|
||||
Reference in New Issue
Block a user