{% extends "base.html" %} {% block title %}supyagent — Models{% endblock %} {% block extra_css %} .key-input-wrap { display: flex; gap: 8px; align-items: center; margin-top: 8px; } .key-input-wrap input { flex: 1; } .provider-card { display: flex; justify-content: space-between; align-items: flex-start; padding: 14px 16px; border-bottom: 1px solid var(--border); } .provider-card:last-child { border-bottom: none; } .provider-name { font-weight: 600; font-size: 13px; } .provider-desc { font-size: 12px; color: var(--text-dim); margin-top: 2px; } .role-select { width: 100%; margin-top: 4px; } .add-model-form { display: flex; gap: 8px; margin-top: 12px; } .add-model-form input { flex: 1; } .security-note { font-size: 11px; color: var(--text-muted); margin-top: 12px; padding: 8px 12px; background: var(--bg); border-radius: 4px; border: 1px solid var(--border); } {% endblock %} {% block content %}

Model Registry

Manage API keys, registered models, and role assignments.

API Keys

Loading...
Keys are stored locally with AES encryption (Fernet). Never transmitted over the network.

Registered Models

Model Provider API Key Roles
Loading...

Roles

Agents reference roles instead of specific models. Assign a model to each role.

Loading...
{% endblock %} {% block extra_js %} let state = {}; let verifyStatus = {}; // { model: "verified" | "error" | "unverified" } async function loadState() { state = await apiCall("GET", "/api/models/state"); // Initialize verify status from state (missing_keys → missing, has_key → unverified) if (state.models) { state.models.forEach(m => { if (!(m.model in verifyStatus)) { if (m.missing_keys && m.missing_keys.length > 0) { verifyStatus[m.model] = "missing"; } else if (m.has_key) { verifyStatus[m.model] = "unverified"; } else { verifyStatus[m.model] = "missing"; } } }); } renderKeys(); renderModels(); renderRoles(); } function renderKeys() { const el = document.getElementById("keys-list"); // Only show providers that are relevant (known LLM keys) const relevant = state.providers.filter(p => p.key_name.endsWith("_API_KEY") || p.key_name.endsWith("_API_BASE") ); el.innerHTML = relevant.map(p => { const statusCls = p.configured ? "status-ok" : "status-missing"; const statusText = p.configured ? (p.source === "env" ? "env" : "stored") : "not set"; return `
${p.key_name}
${p.description}
${statusText} ${p.configured ? `` : ""}
`; }).join(""); } function toggleKeyForm(keyName, show) { const form = document.getElementById(`key-form-${keyName}`); if (form) form.classList.toggle("hidden", !show); } async function saveKey(keyName) { const input = document.getElementById(`key-val-${keyName}`); const value = input.value.trim(); if (!value) return; try { await apiCall("POST", "/api/keys/set", { key_name: keyName, value }); toast(`Saved ${keyName}`); input.value = ""; await loadState(); } catch (e) { toast(`Error: ${e.message}`, "error"); } } async function deleteKey(keyName) { try { await apiCall("POST", "/api/keys/delete", { key_name: keyName }); toast(`Deleted ${keyName}`); await loadState(); } catch (e) { toast(`Error: ${e.message}`, "error"); } } function renderModels() { const tbody = document.getElementById("models-body"); if (!state.models || state.models.length === 0) { tbody.innerHTML = ` No models registered. Add one below.`; return; } tbody.innerHTML = state.models.map(m => { const vs = verifyStatus[m.model] || "unverified"; let statusHtml; if (vs === "verified") { statusHtml = `verified`; } else if (vs === "missing") { const keys = (m.missing_keys && m.missing_keys.length > 0) ? m.missing_keys : []; if (keys.length > 0) { const parts = keys.map(k => `${k} ` ).join(", "); statusHtml = `missing: ${parts}`; } else if (m.key_name) { statusHtml = `missing: ${m.key_name} `; } else { statusHtml = `missing `; } } else if (vs.startsWith("error:")) { statusHtml = `error `; } else { statusHtml = `unverified`; } const badges = []; if (m.is_default) badges.push(`default`); m.roles.forEach(r => badges.push(`${r}`)); return ` ${m.model} ${m.provider} ${statusHtml} ${badges.join(" ")} ${!m.is_default ? `` : ""} `; }).join(""); } async function verifyModel(model) { toast(`Verifying ${model}...`); try { const res = await apiCall("POST", "/api/models/verify", { model }); if (res.status === "verified") { verifyStatus[model] = "verified"; toast(`${model}: verified`); } else if (res.status === "missing_keys") { verifyStatus[model] = "missing"; toast(`${model}: missing keys — ${res.missing_keys.join(", ")}`, "error"); } else { verifyStatus[model] = "error:" + (res.error || "unknown error"); toast(`${model}: ${res.error || "verification failed"}`, "error"); } await loadState(); } catch (e) { toast(`Error: ${e.message}`, "error"); } } async function verifyAll() { const btn = document.getElementById("verify-all-btn"); btn.disabled = true; btn.textContent = "Verifying..."; let verified = 0; let total = 0; try { const toVerify = (state.models || []).filter(m => verifyStatus[m.model] !== "verified"); total = toVerify.length; for (const m of toVerify) { try { const res = await apiCall("POST", "/api/models/verify", { model: m.model }); if (res.status === "verified") { verifyStatus[m.model] = "verified"; verified++; } else if (res.status === "missing_keys") { verifyStatus[m.model] = "missing"; } else { verifyStatus[m.model] = "error:" + (res.error || "unknown error"); } } catch (e) { verifyStatus[m.model] = "error:" + e.message; } renderModels(); } await loadState(); toast(`Verified ${verified}/${total} models`); } finally { btn.disabled = false; btn.textContent = "Verify All"; } } let pendingKeyModel = null; // model string waiting for a key async function addModel() { const input = document.getElementById("new-model-input"); const model = input.value.trim(); if (!model) return; try { const res = await apiCall("POST", "/api/models/add", { model }); toast(`Registered ${model}`); input.value = ""; await loadState(); // If the model needs a key, prompt the user if (res.ok && !res.has_key) { // Use missing_keys from litellm if available, fall back to detected_key const keyToPrompt = (res.missing_keys && res.missing_keys.length > 0) ? res.missing_keys[0] : res.detected_key; showKeyPrompt(model, keyToPrompt); } } catch (e) { toast(`Error: ${e.message}`, "error"); } } function showKeyPrompt(model, detectedKey) { pendingKeyModel = model; const prompt = document.getElementById("add-model-key-prompt"); const label = document.getElementById("key-prompt-label"); const envRow = document.getElementById("key-prompt-env-row"); const valueLabel = document.getElementById("key-prompt-value-label"); prompt.classList.remove("hidden"); if (detectedKey) { // Known provider — we know the env var name, just need the value label.innerHTML = `${model} needs ${detectedKey}`; valueLabel.textContent = detectedKey; envRow.classList.add("hidden"); prompt.dataset.keyName = detectedKey; } else { // Unknown provider — ask for both env var name and value label.innerHTML = `${model} uses an unknown provider. What environment variable holds its API key?`; valueLabel.textContent = "API Key value"; envRow.classList.remove("hidden"); document.getElementById("key-prompt-env-name").value = ""; prompt.dataset.keyName = ""; } document.getElementById("key-prompt-value").value = ""; document.getElementById("key-prompt-value").focus(); } async function savePromptedKey() { const prompt = document.getElementById("add-model-key-prompt"); let keyName = prompt.dataset.keyName; const value = document.getElementById("key-prompt-value").value.trim(); if (!keyName) { keyName = document.getElementById("key-prompt-env-name").value.trim(); } if (!keyName) { toast("Enter an environment variable name", "error"); return; } if (!value) { toast("Enter an API key value", "error"); return; } try { await apiCall("POST", "/api/keys/set", { key_name: keyName, value }); toast(`Saved ${keyName}`); const modelToVerify = pendingKeyModel; dismissKeyPrompt(); await loadState(); // Auto-verify the model after saving its key if (modelToVerify) { await verifyModel(modelToVerify); } } catch (e) { toast(`Error: ${e.message}`, "error"); } } function dismissKeyPrompt() { document.getElementById("add-model-key-prompt").classList.add("hidden"); pendingKeyModel = null; } async function removeModel(model) { try { await apiCall("POST", "/api/models/remove", { model }); toast(`Removed ${model}`); await loadState(); } catch (e) { toast(`Error: ${e.message}`, "error"); } } async function setDefault(model) { try { await apiCall("POST", "/api/models/default", { model }); toast(`Default: ${model}`); await loadState(); } catch (e) { toast(`Error: ${e.message}`, "error"); } } function renderRoles() { const grid = document.getElementById("roles-grid"); const roles = state.standard_roles || ["fast", "smart", "reasoning", "cheap"]; const models = state.models || []; const descriptions = { fast: "Quick responses, lower cost", smart: "Best general-purpose model", reasoning: "Complex problem solving", cheap: "Lowest cost option", }; grid.innerHTML = roles.map(role => { const current = state.roles[role] || ""; const options = models.map(m => `` ).join(""); return `
${role}
${descriptions[role] || ""}
`; }).join(""); } async function assignRole(role, model) { try { if (!model) { await apiCall("POST", "/api/models/unassign-role", { role }); toast(`Cleared ${role} role`); } else { await apiCall("POST", "/api/models/assign-role", { role, model }); toast(`${role} → ${model}`); } await loadState(); } catch (e) { toast(`Error: ${e.message}`, "error"); } } function finish() { signalDone({ action: "models_done" }); document.querySelector(".footer-bar").innerHTML = `Closing... you can close this tab.`; } // Handle Enter key in inputs document.getElementById("new-model-input").addEventListener("keydown", e => { if (e.key === "Enter") addModel(); }); document.getElementById("key-prompt-value").addEventListener("keydown", e => { if (e.key === "Enter") savePromptedKey(); }); document.getElementById("key-prompt-env-name").addEventListener("keydown", e => { if (e.key === "Enter") document.getElementById("key-prompt-value").focus(); }); loadState(); {% endblock %}