* 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>
This commit is contained in:
1 parent
010be32377
commit
9cce68e804
4 files changed
+457
-2
No files matched your search
+35
-2
@@ -16,7 +16,8 @@ import { appendFile, mkdir, readFile, writeFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { promisify } from 'util';
|
||||
import { createHash, randomUUID } from 'crypto';
|
||||
import { config } from './config/index.js';
|
||||
import { config, AVAILABLE_MODELS } from './config/index.js';
|
||||
import { resolveModels } from './config/provider-models.js';
|
||||
import { redactString, redactLedgerText, redactSecrets } from './redact.js';
|
||||
import { LLMBackbone } from './llm/index.js';
|
||||
import { TempestCommand } from './index.js';
|
||||
@@ -28,13 +29,17 @@ import { AGENT_PROMPT_PACKS, FOREFRONT_PRESSURE_LANES, OPERATOR_RUNBOOKS, RESOUR
|
||||
import { AI_REDTEAM_PLAYBOOK, AI_REDTEAM_TECHNIQUE_IDS, aiRedTeamBriefing } from './resources/ai-redteam-playbook.js';
|
||||
import { OPERATOR_SYSTEM_PROMPTS, PLINIAN_OPERATOR_DOCTRINE, THE_FIXER_SYSTEM_PROMPT } from './prompts/index.js';
|
||||
import { createTargetFromUrl, createTargetFromIP } from './target/index.js';
|
||||
import type { OperatorArchetype } from './types/index.js';
|
||||
import type { OperatorArchetype, LLMProvider } from './types/index.js';
|
||||
import { listOperatorPrompts, setOperatorOverride, resetOperatorOverride, type OperatorOverride } from './operators/index.js';
|
||||
import { ingestRepoToSourceContext, runWhiteboxAnalysis, resolveRepoSourceForAnalysis, RepoCloneError, RepoPathError } from './recon/whitebox.js';
|
||||
import { redactCredential } from './evidence/index.js';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
function isKnownLLMProvider(provider: string): provider is LLMProvider {
|
||||
return Object.prototype.hasOwnProperty.call(AVAILABLE_MODELS, provider);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PAYLOAD DATABASES (Shared with MCP server)
|
||||
// =============================================================================
|
||||
@@ -6088,6 +6093,34 @@ app.get('/api/llm/status', (_req: Request, res: Response) => {
|
||||
});
|
||||
});
|
||||
|
||||
// POST /api/models — live model list for the Universal API Config panel (#90).
|
||||
// POST (not GET) so the browser can pass its own localStorage-held key/baseUrl in the body — the
|
||||
// standalone UI holds the key, not the server (consistent with the existing per-request mission key,
|
||||
// and it stays on localhost). Body values override server config; missing → server config. Runs
|
||||
// server-side so the browser dodges provider CORS, and fails open to the static AVAILABLE_MODELS list
|
||||
// (source:'static') so a missing key / network error never leaves the panel empty.
|
||||
app.post('/api/models', async (req: Request, res: Response): Promise<void> => {
|
||||
const body = (req.body ?? {}) as { provider?: string; apiKey?: string; baseUrl?: string };
|
||||
const defaultProvider = config.getLLMConfig().provider;
|
||||
const requestedProvider = (typeof body.provider === 'string' && body.provider.trim()) || defaultProvider;
|
||||
const provider = isKnownLLMProvider(requestedProvider) ? requestedProvider : defaultProvider;
|
||||
// getLLMConfig throws for any provider it doesn't switch on (e.g. the legitimate 'local-agent', or a
|
||||
// garbage string). A synchronous throw inside this async handler would hang the request (Express 4
|
||||
// never responds), so swallow it and fall through to the static list — the route must always answer.
|
||||
let cfg: { baseUrl?: string; apiKey?: string } = {};
|
||||
try { cfg = config.getLLMConfig(provider); } catch { /* provider has no remote/server config → static fallback */ }
|
||||
const bodyBaseUrl = typeof body.baseUrl === 'string' && body.baseUrl.trim() ? body.baseUrl : undefined;
|
||||
const bodyKey = typeof body.apiKey === 'string' && body.apiKey ? body.apiKey : undefined;
|
||||
// Bind key+baseUrl by source: a caller-supplied custom baseUrl must bring its OWN key — never lend
|
||||
// the server's configured apiKey to a body-chosen URL (that would disclose the stored key). The
|
||||
// server key is only ever sent to the server's own configured provider endpoint.
|
||||
const baseUrl = bodyBaseUrl ?? cfg.baseUrl;
|
||||
const apiKey = bodyBaseUrl ? bodyKey : (bodyKey ?? cfg.apiKey);
|
||||
const staticFallback = AVAILABLE_MODELS[provider].map((m: { id: string }) => ({ id: m.id }));
|
||||
const resolved = await resolveModels(provider, { baseUrl, apiKey, staticFallback });
|
||||
res.json({ provider, ...resolved });
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// TOOL EXECUTION ENDPOINTS
|
||||
// =============================================================================
|
||||
|
||||
Reference in new issue
Block a user