Skip to content

feat(dashboard): add OpenRouter model autocomplete to model selection#917

Merged
aaight merged 2 commits intodevfrom
feature/openrouter-model-autocomplete
Mar 16, 2026
Merged

feat(dashboard): add OpenRouter model autocomplete to model selection#917
aaight merged 2 commits intodevfrom
feature/openrouter-model-autocomplete

Conversation

@aaight
Copy link
Copy Markdown
Collaborator

@aaight aaight commented Mar 16, 2026

Summary

Adds an OpenRouter model autocomplete combobox to the dashboard's model selection fields, replacing free-text inputs for LLMist/OpenCode engines with a searchable, grouped list of 300+ models with pricing and context length details.

Card: https://trello.com/c/69b83d19683cb1ab8bc0e632

  • New OpenRouter API client (src/openrouter/client.ts, src/openrouter/types.ts) — proxies https://openrouter.ai/api/v1/models with 1-hour in-memory cache, filters text-capable models, maps to minimal shape with per-million pricing
  • New tRPC endpoint (projects.openRouterModels) — resolves OPENROUTER_API_KEY per project, serves cached model list; falls back to empty array on error
  • New Combobox UI primitive (web/src/components/ui/combobox.tsx) — reusable Radix Popover + cmdk component with option grouping, pricing detail display, and custom model ID entry
  • New OpenRouterModelCombobox wrapper (web/src/components/settings/openrouter-model-combobox.tsx) — fetches live model catalog, groups by provider (Anthropic, Google, DeepSeek, etc.), handles openrouter: prefix convention, falls back to plain <Input> on query error
  • Updated ModelField — adds optional projectId prop; renders <OpenRouterModelCombobox> when provided, falls back to plain input otherwise (preserves backward compat for the AgentConfigFormDialog which has no project context)
  • Updated callersproject-harness-form.tsx, project-agent-configs.tsx now pass projectId; project-general-form.tsx uses combobox for the progress model field

Test plan

  • Unit tests for OpenRouter client (fetch, cache, pricing conversion, filtering, error handling)
  • Unit tests for combobox utility functions (prefix handling, price/context formatting, provider grouping)
  • All 5435 existing tests pass
  • Lint and typecheck clean

Key decisions

  • Server-side caching: 1-hour TTL prevents excessive API calls across dashboard sessions; 5-min client staleTime reduces re-fetches within a session
  • allowCustom=true: Users can still type any model ID not in the OpenRouter catalog (e.g. local or other provider models)
  • Graceful degradation: If no API key is configured or the fetch fails, the combobox shows an empty list and allows custom typing; on hard error it falls back to plain <Input>
  • Prefix convention: The openrouter: prefix distinguishes OpenRouter models from direct model IDs, matching the existing backend convention

🤖 Generated with Claude Code

🕵️ claude-code · claude-sonnet-4-6 · run details

Copy link
Copy Markdown
Collaborator

@nhopeatall nhopeatall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Clean, well-structured feature with good architectural decisions (server-side caching, graceful degradation, backward-compatible ModelField changes). CI is green and the implementation matches the PR description. Two should-fix items worth addressing.

Code Issues

Should Fix

  • src/openrouter/client.ts:12,65 — The cache is a single global entry (let cache: CacheEntry | null) that ignores the apiKey parameter. If project A has an API key (potentially returning different/more models) and project B has none, whichever project fills the cache first determines what all projects see for the next hour. In practice the OpenRouter /models endpoint returns the same public list regardless of API key, so this is unlikely to cause visible bugs today — but it's a latent correctness issue if OpenRouter ever returns key-specific model lists (e.g., fine-tuned models). Consider keying the cache by a hash of the API key, or document the assumption explicitly.

  • tests/unit/web/model-field.test.ts — The test file re-implements stripPrefix, addPrefix, formatPrice, formatContext, and modelGroup as local copies and tests those rather than importing from openrouter-model-combobox.tsx. If the production implementations drift from these copies, tests will pass while production code is broken. Extract these utilities to a shared module (e.g., openrouter-utils.ts) and import in both the component and the tests.

Minor / Dead Code

  • web/src/components/settings/openrouter-model-combobox.tsx:11stripPrefix is defined but never used. Can be removed.
  • web/src/components/settings/openrouter-model-combobox.tsx:100-108handleChange has a dead conditional: both the if and else branches execute onChange(newValue). The function could be simplified to just onChange.

🕵️ claude-code · claude-opus-4-6 · run details

@nhopeatall
Copy link
Copy Markdown
Collaborator

Inline review comments

Since the review tool had issues with inline comments, here are the per-file details:


src/openrouter/client.ts:12[SHOULD_FIX] The cache is a single global entry that ignores the apiKey parameter. If two projects have different OpenRouter API keys (or one has a key and one doesn't), whichever project populates the cache first determines what all projects see for the next hour.

In practice the /api/v1/models endpoint likely returns the same public catalog regardless of API key, so this won't cause visible bugs today. But it is a latent correctness issue — consider keying the cache by apiKey ?? '__public__', or at minimum adding a code comment documenting the assumption.


tests/unit/web/model-field.test.ts:7[SHOULD_FIX] These utility functions (stripPrefix, addPrefix, formatPrice, formatContext, modelGroup) are re-implemented here as local copies rather than imported from the production code. If the implementations in openrouter-model-combobox.tsx drift from these copies, the tests will pass while production code is broken.

Consider extracting these utilities to a shared module (e.g., web/src/lib/openrouter-utils.ts or src/openrouter/utils.ts) and importing in both the component and the test file.


web/src/components/settings/openrouter-model-combobox.tsx:11[MINOR] stripPrefix is defined but never called in this file. Dead code — can be removed.


web/src/components/settings/openrouter-model-combobox.tsx:100[MINOR] Dead conditional — both branches of the handleChange if/else execute onChange(newValue). The entire function could be replaced with just onChange.

🕵️ claude-code · claude-opus-4-6 · run details

- Key cache by API key (or '__public__') to prevent cross-project cache
  pollution if OpenRouter ever returns key-specific model lists
- Extract prefix/formatting/grouping utilities to web/src/lib/openrouter-utils.ts
  so tests import production implementations directly instead of local copies
- Remove unused stripPrefix from openrouter-model-combobox.tsx
- Simplify dead handleChange conditional to a direct onChange pass-through

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

@nhopeatall nhopeatall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

LGTM — well-structured feature addition with clean architecture, proper error handling, and comprehensive test coverage.

The implementation correctly layers: generic Combobox UI primitive → OpenRouterModelCombobox wrapper with tRPC data fetching → ModelField integration preserving backward compatibility for engines with select-type model selection (Claude Code, Codex). The openrouter: prefix convention is well-established throughout the backend and the combobox correctly applies it for catalog selections while allowing custom model IDs.

Key things verified:

  • ModelField ordering: The engineDefinition?.modelSelection.type === 'select' check correctly takes precedence before the OpenRouter combobox, ensuring Claude Code and Codex engines still render their fixed dropdown menus
  • Backward compatibility: The AgentConfigFormDialog (which lacks project context) omits projectId, so ModelField falls back to plain <Input> as before
  • Server-side caching: Per-API-key cache with 1-hour TTL is reasonable; the Map won't grow unbounded in practice since the number of distinct project API keys is small
  • Graceful degradation chain: OpenRouter API failure → empty model list → combobox still allows custom typing; tRPC query error → falls back to plain <Input>
  • Type sharing: web/tsconfig.json including ../src/openrouter/**/* follows the existing pattern for ../src/api/**/* and ../src/db/**/*
  • CI: All 5 checks pass (Docker builds, lint-and-test, integration-tests, validate-commits)

🕵️ claude-code · claude-opus-4-6 · run details

@aaight aaight merged commit 43e3e2f into dev Mar 16, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants