From 9cce68e804669b34303b0d7c01d1fccded294ea8 Mon Sep 17 00:00:00 2001 From: Lyubomir Bozhinov <12250247+lyubomir-bozhinov@users.noreply.github.com> Date: Mon, 20 Jul 2026 18:34:26 +0300 Subject: [PATCH] feat(settings): Universal API Config panel with server-side model fetch (#90) (#101) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(settings): Universal API Config panel with server-side model fetch (#90) Consolidates provider configuration into one "Universal API Config" panel in Settings — provider dropdown (OpenAI-compatible / Anthropic), base URL, API key, model with a live "Fetch models" button, and max context tokens — above the existing per-provider key sections. Backward-compatible by design: the panel reads and writes the SAME state.settings keys the individual sections already use (${provider}Key, selectedModel), so nothing migrates and nothing is lost; the individual sections keep working and stay in sync. - src/config/provider-models.ts (new): listProviderModels queries a provider's own model endpoint (OpenAI-compatible Bearer /models, Anthropic x-api-key /v1/models, OpenRouter public list); resolveModels wraps it fail-open. - POST /api/models (new route): server-side so the browser dodges provider CORS. Binds key+baseUrl by source — a caller-supplied base URL must bring its own key, so the server never lends its configured key to a body-chosen URL. Always responds, failing open to the static AVAILABLE_MODELS list on a missing key, network error, or unknown provider (incl. local-agent) — never hangs. - Model options are built via the DOM API (createElement/textContent), never innerHTML, so untrusted provider-returned model ids cannot inject markup. Scope notes: augment-not-replace (removing the individual sections is deferred to maintainer preference); base URL + context cap are stored and used for model discovery and server/agent flows — in-browser mission routing still uses OpenRouter/Venice, matching the existing "not wired" hints; the "Model Reasoning (Eino)" sub-proposal is a separate follow-up pending a mapping. Verified on macOS: 12 unit tests (RED->GREEN) for the model module, full suite 462 green, and Playwright QA (17 checks) driving the real panel — render, provider switch, persistence across reload, backward-compat, and both the static and live Fetch paths. Refs #90 * fix(settings): harden model list fetch --------- Co-authored-by: Joseph Magly <1159087+jmagly@users.noreply.github.com> --- docs/index.html | 148 +++++++++++++++++++++++++ src/__tests__/provider-models.test.ts | 149 ++++++++++++++++++++++++++ src/config/provider-models.ts | 125 +++++++++++++++++++++ src/server.ts | 37 ++++++- 4 files changed, 457 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/provider-models.test.ts create mode 100644 src/config/provider-models.ts diff --git a/docs/index.html b/docs/index.html index bbb67ea..83cc41e 100755 --- a/docs/index.html +++ b/docs/index.html @@ -5514,6 +5514,49 @@
+
+

🎛️ Universal API Config

+

Configure your LLM providers in one place — provider, key, base URL, model, and context cap — shared with the individual provider sections below (fully backward-compatible).

+
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+
+ + +
+
In-browser mission routing currently uses OpenRouter or Venice (or a connected local agent). Keys for other providers, custom base URLs, and the context cap are stored and used for model discovery and server-side / agent flows.
+ +
+
+

🔑 API Keys

@@ -7386,6 +7429,7 @@ Correlating findings across agents, identifying patterns, producing actionable i if (page === 'operators' && typeof window.loadOperativesRoster === 'function') setTimeout(window.loadOperativesRoster, 80); if (page === 'evidence' && typeof window.hydrateFindings === 'function') setTimeout(window.hydrateFindings, 60); if (page === 'selfimprove' && typeof window.renderSelfImprove === 'function') setTimeout(window.renderSelfImprove, 60); + if (page === 'settings' && typeof window.uacInit === 'function') setTimeout(window.uacInit, 40); } document.querySelectorAll('.nav-item').forEach(i => i.addEventListener('click', () => navigateTo(i.dataset.page))); @@ -8816,6 +8860,110 @@ o.id === 'coordinator' ? `1. MISSION_STATUS: Progress assessment if (input) input.value = state.settings?.fallbackModel || 'nousresearch/hermes-3-llama-3.1-405b'; } + // ── Universal API Config panel (#90) ──────────────────────────────────────────────── + // One place to set the active provider's key / base URL / model / max-context. Reads and + // writes the SAME state.settings keys the individual provider sections use (${p}Key, + // selectedModel), so it is fully backward-compatible — nothing migrates, nothing is lost. + const UAC_DEFAULT_BASE = { + openrouter: 'https://openrouter.ai/api/v1', + venice: 'https://api.venice.ai/api/v1', + openai: 'https://api.openai.com/v1', + anthropic: 'https://api.anthropic.com', + }; + function uacProvider() { return document.getElementById('uacProvider')?.value || 'openrouter'; } + // Model ids come from a provider's /models response (and the user can point Base URL at ANY + // endpoint), so they are untrusted. Build