feat(models): fetch live model lists from CLIs, allow custom ids

Each agent definition now declares an optional `listModels` spec; the
daemon runs the CLI's own list-models command (e.g. `opencode models`,
`cursor-agent models`) during agent detection and uses the result as
the dropdown options. Hardcoded entries shrink to a `fallbackModels`
hint that only kicks in when the CLI has no listing command (Claude,
Codex, Gemini, Qwen) or when the listing fails (e.g. unauth'd
cursor-agent).

UI groups `provider/model` ids by provider via <optgroup> so opencode's
~175 live models stay navigable, and the Settings dialog gains a
"Custom…" entry that opens a free-text input for any model id the
listing didn't surface yet. Daemon validates picks against the live
cache + fallback, with a permissive sanitizer for custom ids.
This commit is contained in:
pftom
2026-04-29 00:32:03 +08:00
parent f2d28a1cca
commit bbeb868040
8 changed files with 331 additions and 58 deletions
+14 -5
View File
@@ -6,7 +6,12 @@ import { fileURLToPath } from 'node:url';
import path from 'node:path';
import fs from 'node:fs';
import os from 'node:os';
import { detectAgents, getAgentDef } from './agents.js';
import {
detectAgents,
getAgentDef,
isKnownModel,
sanitizeCustomModel,
} from './agents.js';
import { listSkills } from './skills.js';
import { listDesignSystems, readDesignSystem } from './design-systems.js';
import { createClaudeStreamHandler } from './claude-stream.js';
@@ -782,11 +787,15 @@ export async function startServer({ port = 7456 } = {}) {
(d) => fs.existsSync(d),
);
// Per-agent model + reasoning the user picked in the model menu.
// Validated against the agent's declared options so a stale or hostile
// value can't smuggle arbitrary flags into the spawned argv.
// Trust the value when it matches the most recent /api/agents listing
// (live or fallback). Otherwise allow it through if it passes a
// permissive sanitizer — that's the path for user-typed custom model
// ids the CLI's listing didn't surface yet.
const safeModel =
typeof model === 'string' && Array.isArray(def.models)
? def.models.find((m) => m.id === model)?.id ?? null
typeof model === 'string'
? isKnownModel(def, model)
? model
: sanitizeCustomModel(model)
: null;
const safeReasoning =
typeof reasoning === 'string' && Array.isArray(def.reasoningOptions)