feat(settings): add AI provider model catalog picker - #205
Conversation
- daemon: GET /settings/ai-models lists the OpenRouter model catalog with a free-only filter; guarded by the X-LittleImp-Frontend header, SSRF checks (private-host base URLs and redirect final hops), a 15s timeout, and a 10 MB response cap; upstream error bodies are never relayed - settings: default OpenRouter model becomes openai/gpt-latest (drop the confusing '~' fallback prefix); stored values are normalized on load - frontend: searchable model combobox with free-only toggle, loading/error/ retry states, and custom (typed) model entry - contract, generated API docs, fixtures, and tests updated Co-authored-by: Robert Goniszewski <robertgoniszewski@outlook.com> Signed-off-by: Robert Goniszewski <robertgoniszewski@outlook.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
There was a problem hiding this comment.
Pull request overview
Adds an OpenRouter model catalog picker to the Settings UI, backed by a new daemon endpoint that proxies the provider’s public model catalog with SSRF-oriented safeguards, plus normalization of legacy ~-prefixed OpenRouter model slugs. The change centralizes catalog fetches behind the daemon, updates the API contract/docs, and expands test coverage across frontend, daemon, and e2e fixtures.
Changes:
- Daemon: add
GET /settings/ai-modelscatalog endpoint with header gating, base-URL normalization, private-host checks, timeout, and response-size cap. - Frontend: add reusable searchable
Comboboxand integrate it into Settings for OpenRouter model selection with a “Free models only” toggle and custom-entry support. - Docs/tests: regenerate API contract/OpenAPI/API.md and add/adjust unit + integration tests and fixtures for the new flow.
Reviewed changes
Copilot reviewed 26 out of 30 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/test/setup.ts | Adds jsdom stubs required by cmdk-based combobox behavior in unit tests. |
| src/pages/Settings.tsx | Integrates OpenRouter model catalog query + combobox UI; normalizes stored ~ model values; refactors provider fields. |
| src/pages/Settings.test.tsx | Updates Settings tests for tilde stripping and the new combobox-based interaction + catalog fetch behaviors. |
| src/lib/api.ts | Adds fetchAiModels client and related catalog DTO typing. |
| src/lib/api.test.ts | Adds API client unit test validating query params and required frontend header. |
| src/hooks/use-settings.test.tsx | Updates default OpenRouter model slug to remove the ~ prefix in test fixtures. |
| src/components/ui/combobox.tsx | Introduces reusable searchable combobox component (Radix Popover + cmdk). |
| src/components/ui/combobox.test.tsx | Adds unit tests covering filtering, selection, custom entry, loading/error states, and hints. |
| e2e/api-fixtures.ts | Updates e2e settings fixture to the normalized default OpenRouter model slug. |
| docs/task-reports/index.html | Adds latest task report card entry for the model picker work. |
| docs/task-reports/2026/index.html | Adds August 2026 index card for the model picker report. |
| docs/task-reports/2026/08/index.html | Adds a new August 2026 task-report index page. |
| docs/task-reports/2026/08/2026-08-03-ai-provider-model-picker/index.html | Adds the detailed task report describing UX + daemon endpoint and verification artifacts. |
| docs/openapi.json | Regenerates OpenAPI spec to include /settings/ai-models and new schemas. |
| docs/api-contract.json | Updates API contract with AiModel / AiModelCatalog schemas and /settings/ai-models endpoint. |
| daemon/src/test/integration/settings-runtime.test.ts | Updates expected OpenRouter default model slug without ~. |
| daemon/src/test/integration/backup.test.ts | Updates backup/restore expectations to the normalized OpenRouter model slug. |
| daemon/src/test/integration/ai-models.test.ts | Adds integration tests for /settings/ai-models behavior (filters, SSRF, caps, error paths). |
| daemon/src/settings.ts | Updates default OpenRouter model slug to openai/gpt-latest. |
| daemon/src/server.ts | Adds X-LittleImp-Frontend to CORS allowHeaders for the catalog request. |
| daemon/src/routes/settings.ts | Implements GET /settings/ai-models route with header gating + base URL handling. |
| daemon/src/lib/base-url.ts | Adds shared strict base-URL normalizer used by the catalog route/fetcher. |
| daemon/src/api/types.ts | Exposes new catalog DTO exports. |
| daemon/src/api/contract.ts | Adds schemas + route definition to the daemon API contract source. |
| daemon/src/ai/models-catalog.ts | Adds provider catalog fetching logic with caps, timeout, and redirect/private-host checks. |
| API.md | Regenerates human-readable API docs for the new endpoint and schemas. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| const provider = c.req.query("provider"); | ||
| if (provider !== "openrouter") { | ||
| return problem(c, 400, "Unsupported Provider", | ||
| `Model catalog is not available for provider '${provider}'`); | ||
| } |
| export function normalizeHttpsBaseUrl(raw: string, fallback: string): string { | ||
| const base = raw.trim() || fallback; | ||
| let parsed: URL; | ||
| try { | ||
| parsed = new URL(base); |
| <div className="p-3 text-xs text-destructive"> | ||
| <p className="break-words">{error}</p> | ||
| {onRetry && ( | ||
| <Button | ||
| type="button" | ||
| variant="outline" | ||
| size="sm" | ||
| className="mt-2 h-7" | ||
| onClick={() => onRetry()} | ||
| > | ||
| <RotateCcw className="mr-1.5 h-3 w-3" /> | ||
| Retry | ||
| </Button> | ||
| )} | ||
| </div> |
| } catch { | ||
| // Error body exceeded the cap; keep the generic message. | ||
| } | ||
| log.warn("Model catalog upstream error", { status: res.status, body: text.slice(0, 500) }); |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 93185cb893
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| {error ? ( | ||
| <div className="p-3 text-xs text-destructive"> | ||
| <p className="break-words">{error}</p> | ||
| {onRetry && ( | ||
| <Button | ||
| type="button" | ||
| variant="outline" | ||
| size="sm" | ||
| className="mt-2 h-7" | ||
| onClick={() => onRetry()} | ||
| > | ||
| <RotateCcw className="mr-1.5 h-3 w-3" /> | ||
| Retry | ||
| </Button> | ||
| )} | ||
| </div> | ||
| ) : ( |
There was a problem hiding this comment.
Keep custom entry available after catalog failures
When the catalog request fails—for example while offline or when a custom OpenRouter base URL has no /models endpoint—this error branch replaces the entire CommandList. Although the search input remains visible, typing cannot create the allowCustom option and never calls onValueChange, so users cannot change the model until the external catalog recovers. Render the error/retry state alongside the custom-entry list rather than instead of it.
Useful? React with 👍 / 👎.
| onValueChange={(model) => | ||
| updateAiApiKeyProvider("openrouter", { model: stripModelTilde(model) }) | ||
| } |
There was a problem hiding this comment.
Preserve explicitly entered fallback prefixes
When a user selects a custom value such as ~vendor/model, this callback immediately passes it through stripModelTilde, so the valid OpenRouter fallback marker is silently removed before saving. Legacy values may be normalized on load as intended, but an explicitly typed custom value should be retained verbatim so fallback routing remains configurable.
Useful? React with 👍 / 👎.
Resolve docs/task-reports/index.html by keeping both task-report cards (security hardening + AI model picker). Co-authored-by: Robert Goniszewski <robertgoniszewski@outlook.com> Signed-off-by: Robert Goniszewski <robertgoniszewski@outlook.com>
|



Summary
Adds a searchable OpenRouter model picker to Settings so choosing a model no longer means remembering catalog slugs — plus a new daemon catalog endpoint with proper SSRF guards.
Daemon
GET /settings/ai-modelslists the OpenRouter model catalog with afreefilter; guarded by theX-LittleImp-Frontendheader (blocks blind browser triggers), private-host checks on the configured base URL and every redirect hop, a 15s timeout, and a 10 MB response cap; upstream error bodies are never relayedopenai/gpt-latest— the confusing~fallback prefix is dropped and stored values are stripped on loadFrontend
allowCustom)Docs
AiModel/AiModelCatalogschemas, endpoint example)docs/task-reports/2026/08/2026-08-03-ai-provider-model-picker/Test plan
npm run check(lint, type-check, frontend + daemon tests, docs:api:check, build)