Skip to content

feat(models): dynamic, animated model selector with provider logos - #586

Open
beng360 wants to merge 10 commits into
mainfrom
feat/model-selector-revamp
Open

feat(models): dynamic, animated model selector with provider logos#586
beng360 wants to merge 10 commits into
mainfrom
feat/model-selector-revamp

Conversation

@beng360

@beng360 beng360 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

What

Reworks the chat model selector so its list is driven by the backend rather than a hardcoded array, adds an env-configurable allowlist, registers the newest Claude models, and gives the selector a polished UI with provider logos and animations.

Why

The selector hardcoded its model list in the component, which drifted from what the backend actually served and couldn't be restricted per-deployment. Downstream builds (e.g. a Bedrock-only SaaS deployment that should expose Claude models only) had no way to narrow the list without editing code.

Backend

  • model_catalog.py — new module holding display metadata (name, provider, context window, pricing) plus an ENABLED_MODELS env allowlist. Unset → full catalog (default OSS behavior). Set → only those IDs. A bogus/empty-matching allowlist logs a warning and falls back to the full catalog so the selector is never empty.
  • GET /api/llm-config/models — new require_auth_only endpoint returning the catalog with metadata. The existing /supported-models is admin-only (llm_config read) and returns bare IDs, so it can't back a selector that renders for every user.
  • MODEL_MAPPINGS — adds anthropic/claude-opus-4-8, claude-sonnet-5, claude-fable-5 (dash + dot aliases). The Bedrock provider already derives their inference-profile IDs automatically (bare → us.anthropic.claude-<name>); the derivation was checked against the live AWS Bedrock list-inference-profiles API for all catalogued Claude models.

Frontend

  • ModelSelector.tsx rebuilt: fetches the live catalog (static fallback if the request fails), groups models by provider, and adds framer-motion animations — spring dropdown open, staggered row fade-in, animated selection check, chevron rotate, hover lift.
  • provider-icons.tsx — official Anthropic (Claude), OpenAI, and Gemini brand marks, vendored inline from @lobehub/icons-static-svg (MIT). Inlined rather than added as a dependency because the published @lobehub/icons requires React 19 + antd, which conflicts with this app's React 18 stack. Path data is upstream verbatim, not redrawn.
  • /api/llm-models Next.js proxy route; ENABLED_MODELS documented in .env.example.

Config

Restrict the selector for a deployment:

ENABLED_MODELS=anthropic/claude-opus-4-8,anthropic/claude-sonnet-5,anthropic/claude-fable-5,anthropic/claude-haiku-4.5

Testing

  • Backend catalog + allowlist logic exercised directly (default, Claude-only, and bogus-allowlist fallback paths).
  • Bedrock ID derivation verified against the AWS Bedrock API.
  • tsc --noEmit clean for all new/changed TS files (repo has pre-existing unrelated TS errors and a broken eslint config, both untouched here).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Chat model selector now loads an authenticated model catalog from the backend with provider grouping, a featured-first default view, and live search (including support for custom provider/model entries).
    • Added provider icons and updated the selector popover UI for improved selection/search interactions.
    • Introduced ENABLED_MODELS configuration to control which models appear in the selector (allowlist).
  • Bug Fixes
    • Workflow errors related to model/provider issues now show friendlier, more actionable messages.
  • Chores
    • Improved OpenRouter model catalog fetching with caching and expanded model-name mappings.

Rework the chat model selector so its list is driven by the backend
instead of a hardcoded array, and give it a polished UI.

Backend:
- Add a model_catalog module with display metadata (name, provider,
  context window, pricing) and an ENABLED_MODELS env allowlist. When
  unset the full catalog is offered; a deployment can restrict the
  selector to a subset (e.g. Claude-only via Bedrock) with no code
  change. A bogus allowlist logs and falls back to the full catalog.
- Expose GET /api/llm-config/models (require_auth_only) so the selector
  renders for every user; the existing /supported-models stays admin-only.
- Add claude-opus-4-8, claude-sonnet-5, claude-fable-5 to MODEL_MAPPINGS
  (dash + dot aliases). The Bedrock provider already derives their
  inference-profile IDs, verified against the AWS Bedrock API.

Frontend:
- Rebuild ModelSelector: fetches the live catalog (static fallback if the
  request fails), groups by provider, framer-motion animations (spring
  dropdown, staggered rows, animated selection check, chevron rotate).
- Vendor official Anthropic/OpenAI/Gemini brand marks from
  @lobehub/icons-static-svg as inline components (the published package
  requires React 19 + antd, incompatible with this React 18 app).
- Add /api/llm-models Next proxy route and document ENABLED_MODELS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@beng360
beng360 requested a review from a team as a code owner July 8, 2026 18:42
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR adds a backend-served model catalog with provider discovery, routing and allowlist filtering, exposes it through authenticated backend and Next.js routes, and updates the model selector to fetch, group, search, and persist catalog-backed model selections.

Changes

Configurable model catalog and selector UI

Layer / File(s) Summary
Model catalog assembly and provider discovery
server/chat/backend/agent/model_catalog.py, server/chat/backend/agent/model_mapper.py, server/chat/backend/agent/providers/openrouter_provider.py, .env.example, docker-compose*.yml
Adds curated and discovered model metadata, canonical model IDs, routability and ENABLED_MODELS filtering, Anthropic mappings, OpenRouter live catalog caching, and environment propagation.
Authenticated model catalog API
server/routes/llm_config.py, client/src/app/api/llm-models/route.ts
Adds authenticated backend catalog retrieval and a Next.js proxy route using the shared forwarding helper.
Provider icons and command primitives
client/src/components/icons/provider-icons.tsx, client/src/components/ui/command.tsx
Adds provider SVG components and styled cmdk wrappers for the selector interface.
Catalog-driven model selector
client/src/components/ModelSelector.tsx
Loads cached or live catalog data, reconciles persisted selections, groups featured and searched models by provider, supports custom IDs, and renders the new popover command interface.
Workflow error message mapping
server/main_chatbot.py
Maps model/provider-related workflow exceptions to specific user-facing messages while retaining a generic fallback.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • Arvo-AI/aurora#132: Shares model mapping and provider-routing code used by catalog canonicalization and filtering.
  • Arvo-AI/aurora#357: Also updates model IDs and selector-facing model metadata.

Suggested reviewers: isiddharthsingh, zarlanx

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ModelSelector
  participant NextRoute
  participant BackendRoute
  participant ModelCatalog
  participant Providers
  User->>ModelSelector: Open model selector
  ModelSelector->>NextRoute: Fetch model catalog
  NextRoute->>BackendRoute: Forward authenticated request
  BackendRoute->>ModelCatalog: Build enabled models
  ModelCatalog->>Providers: Discover supported models
  Providers-->>ModelCatalog: Return model IDs
  ModelCatalog-->>BackendRoute: Return filtered catalog
  BackendRoute-->>ModelSelector: Return catalog response
  User->>ModelSelector: Search or select model
  ModelSelector-->>User: Display grouped model options
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main frontend change: a dynamic, animated model selector with provider logos.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/model-selector-revamp

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

arvo-ai-staging[bot]
arvo-ai-staging Bot previously approved these changes Jul 8, 2026

@arvo-ai-staging arvo-ai-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Aurora Risk Review

Verdict: SAFE

No risks identified. This change looks safe to ship.


Aurora reviews PRs for incident prevention.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.env.example:
- Around line 163-168: Add ENABLED_MODELS to the shared Compose environment
block so the container receives the allowlist setting. Update the environment
passed by both docker-compose.yaml and docker-compose.prod-local.yml so
server/chat/backend/agent/model_catalog.py can read ENABLED_MODELS and apply the
selector restriction instead of falling back to the full catalog.

In `@client/src/app/api/llm-models/route.ts`:
- Around line 19-22: The llm-models route is duplicating backend proxy behavior
with a direct fetch instead of using the shared proxy helper. Update the route
handler in route.ts to delegate to forwardRequest from
client/src/lib/backend-proxy.ts, preserving the existing headers/auth handling
through that helper and removing the manual BACKEND_URL fetch logic. Use the
route handler and forwardRequest symbols to locate and replace the current proxy
call.

In `@client/src/components/ModelSelector.tsx`:
- Around line 183-229: The custom model option elements in ModelSelector are
plain motion.button nodes inside DropdownMenuContent, so they bypass Radix
dropdown keyboard behavior and expose incomplete ARIA state. Update the model
list rendering to use DropdownMenuItem asChild around the existing motion.button
(or otherwise give the item proper menuitem semantics and selection state) so
arrow-key navigation, focus handling, and screen reader state work correctly.
Make sure the selected model state in handleModelSelect is reflected with an
accessible attribute such as aria-selected or aria-checked, not just the check
icon and background styling.
- Around line 195-214: The `AnimatePresence` block in `ModelSelector` only
animates the `Check` because `ProviderIcon` is a plain component and cannot
participate in exit animations. Wrap the `ProviderIcon` branch in a `motion`
element with the same kind of enter/exit transitions used for `motion.span` on
the check mark, so switching between `isSelected` states animates both
directions smoothly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2946324f-d54c-4198-a750-eec84473b6f6

📥 Commits

Reviewing files that changed from the base of the PR and between 816f677 and 830fd5d.

📒 Files selected for processing (7)
  • .env.example
  • client/src/app/api/llm-models/route.ts
  • client/src/components/ModelSelector.tsx
  • client/src/components/icons/provider-icons.tsx
  • server/chat/backend/agent/model_catalog.py
  • server/chat/backend/agent/model_mapper.py
  • server/routes/llm_config.py

Comment thread .env.example
Comment thread client/src/app/api/llm-models/route.ts Outdated
Comment thread client/src/components/ModelSelector.tsx Outdated
Comment thread client/src/components/ModelSelector.tsx Outdated
…r text, accent hover

Remove the generic Zap/Gauge reasoning indicators; each row is now just the
provider mark, name, context length, and a check on the selected model. Bump
row and trigger text (xs -> sm) for legibility, and switch row hover/selection
to the standard bg-accent dropdown-item style instead of a muted wash + x-shift.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@arvo-ai-staging arvo-ai-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Aurora Risk Review — Latest changes

Verdict: SAFE

No new incident risk in the latest changes.


Aurora reviews PRs for incident prevention.

Wire the ENABLED_MODELS allowlist into the compose env (default empty),
alongside MAIN_MODEL/RCA_MODEL, so the model selector allowlist works in
docker-compose deployments — not just helm.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@arvo-ai-staging arvo-ai-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Aurora Risk Review — Latest changes

Verdict: SAFE

No new incident risk in the latest changes.


Aurora reviews PRs for incident prevention.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docker-compose.yaml`:
- Line 69: The compose configuration is missing the ENABLED_MODELS environment
entry in the prod-local variant, which should stay aligned with
docker-compose.yaml and .env.example. Update docker-compose.prod-local.yml so
the service definition includes ENABLED_MODELS alongside the other environment
variables, matching the existing compose patterns and keeping the variants in
sync.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0db8ec1c-c6b1-4453-b8c2-cdbcae06f4e4

📥 Commits

Reviewing files that changed from the base of the PR and between ac65a7a and f192bb9.

📒 Files selected for processing (1)
  • docker-compose.yaml

Comment thread docker-compose.yaml
Make the selector reach everything Aurora can route, not a curated list:

- model_catalog: the offered list is now the UNION of the curated flagship
  catalog (rich display metadata) and live get_supported_models() from every
  configured+available provider, de-duped by canonical id. Uncatalogued models
  (Ollama pulls, OpenRouter long tail) get derived display names. ENABLED_MODELS
  still narrows the result for restricted deployments.
- openrouter_provider: get_supported_models() now fetches OpenRouter's live
  /models catalog (hundreds of models) with a 10-min cache; falls back to the
  mapped set when the key is unset or the API is unreachable (no hang, never
  empty).
- ModelSelector: rebuilt on a cmdk command palette (Popover + Command) — type
  to filter across all providers, grouped, keyboard nav, provider logos with a
  generic fallback. Plus a custom-model row: typing any provider/model id routes
  it through (passthrough), so truly any model is reachable.
- Add shadcn command.tsx wrapper.

Verified: 346 models served with OpenRouter configured (0ms cached), graceful
27-model fallback with no key, search filters instantly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@arvo-ai-staging arvo-ai-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Aurora Risk Review — Latest changes

Verdict: SAFE

No new incident risk in the latest changes.


Aurora reviews PRs for incident prevention.

Comment thread server/chat/backend/agent/model_catalog.py Fixed
346 models is a data dump, not a picker. Split reach from display:

- Backend tags each model featured=true/false. Featured = the curated
  flagship catalog + models from direct providers (Ollama pulls, direct
  Anthropic/OpenAI/Google/Bedrock) — short, high-intent lists. The
  OpenRouter long tail is featured=false. A FEATURED_MODELS env var
  overrides the default set per deployment (same shape as ENABLED_MODELS).
- Selector shows only featured models when the search box is empty, and
  widens to the entire catalog the moment you type. A 'Search to browse N
  more models' hint shows how many are behind search. Custom passthrough
  entry unchanged.

This keeps universal reach (any model, incl. the full OpenRouter set and
locally-pulled Ollama models, is selectable) without dumping hundreds of
rows into the default view.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@arvo-ai-staging arvo-ai-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Aurora Risk Review — Latest changes

Verdict: SAFE

No new incident risk in the latest changes.


Aurora reviews PRs for incident prevention.

Comment thread server/chat/backend/agent/model_catalog.py Fixed
'What you see is what you can use' was not actually true: the selector
listed every available provider's models regardless of LLM_PROVIDER_MODE,
so e.g. in bedrock/vertex/direct mode it showed ~300 OpenRouter models that
would error at call time, and in openrouter mode it showed ollama ids that
don't route there.

Filter the catalog through the SAME router the agent uses
(get_provider_for_model under the env LLM_PROVIDER_MODE); drop anything it
can't resolve. Fails open (allow) if the registry is unavailable so the
selector never empties. Verified per mode:
  - openrouter: all 346 (everything routes through OpenRouter)
  - direct/bedrock/vertex: ~107 (anthropic/openai/google); OpenRouter-only
    providers (qwen/deepseek/mistral/…) dropped; bedrock keeps Claude via
    native ids + Gemini via Vertex fallback
  - ollama ids only appear in direct mode with Ollama reachable
Adds ~1ms (in-memory registry lookup). Custom passthrough entry still lets
power users type any id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@arvo-ai-staging arvo-ai-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Aurora Risk Review — Latest changes

Verdict: SAFE

No new incident risk in the latest changes.


Aurora reviews PRs for incident prevention.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/chat/backend/agent/providers/openrouter_provider.py`:
- Around line 113-140: Cache the fallback result in
OpenRouterProvider.get_supported_models() when the /models request fails or
returns a non-200 response, instead of only caching successful live results.
Reuse the existing _models_cache and _models_cache_time fields so repeated calls
from get_enabled_models() within a short TTL return the mapped fallback list
without reissuing the network request. Update the failure branches in the
requests.get try/except path to store mapped before returning it, while keeping
the success path unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 67b9fa0a-510d-4bb2-a8d5-61ac0b3142f7

📥 Commits

Reviewing files that changed from the base of the PR and between f192bb9 and 5ef816e.

📒 Files selected for processing (4)
  • client/src/components/ModelSelector.tsx
  • client/src/components/ui/command.tsx
  • server/chat/backend/agent/model_catalog.py
  • server/chat/backend/agent/providers/openrouter_provider.py

Comment thread server/chat/backend/agent/providers/openrouter_provider.py Outdated
beng360 and others added 2 commits July 9, 2026 09:55
- llm-models route: use shared forwardAuthenticatedGet helper instead of
  hand-rolled fetch (auth/timeout/error-normalization handled centrally).
- openrouter_provider: cache the fallback list for 60s on /models failure so
  a slow/down OpenRouter doesn't re-incur the 5s timeout on every selector load.
- model_catalog: replace empty except in _canonical_id with a debug log.
- docker-compose.prod-local.yml: pass ENABLED_MODELS through (sync with
  docker-compose.yaml).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SonarCloud (11 issues):
- provider-icons.tsx: mark props Readonly; drop role='img' (icons are
  decorative, always beside a text label) — now aria-hidden. Fixes the
  a11y-role and readonly-props smells.
- command.tsx: remove redundant manual cmdk-input-wrapper attr (cmdk sets
  its own; nothing targets it) — fixes unknown-DOM-property smell.
- model_mapper.py: hoist the duplicated claude-opus-4.8 mapping to a
  single _OPUS_4_8 dict referenced by both alias keys — fixes the
  duplicated-literal (critical) smell.
- llm_config.py: logger.exception() instead of logger.error(exc_info=True).

CI:
- .env.example: document FEATURED_MODELS (was read in code but missing,
  failing the Validate Environment Variables drift check).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
putComment timed out

@arvo-ai-staging arvo-ai-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Aurora Risk Review — Latest changes

Verdict: SAFE

No new incident risk in the latest changes.


Aurora reviews PRs for incident prevention.

…LS alias-safe

- Remove the FEATURED_MODELS env override (unused, added drift-check
  surface). The built-in featured rule is unchanged, so the default view is
  identical (12 flagships). Also removes it from .env.example.
- ENABLED_MODELS now matches on canonical id, so a dot/dash alias
  (claude-opus-4.8 vs claude-opus-4-8) resolves either way instead of
  silently dropping the model — important for the prod Claude-only lockdown.

Verified: prod env (LLM_PROVIDER_MODE=bedrock + claude-only ENABLED_MODELS,
no OpenRouter key) yields exactly the 4 Claude models; full catalog (341)
unchanged when no allowlist set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@arvo-ai-staging arvo-ai-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Aurora Risk Review — Latest changes

Verdict: SAFE

No new incident risk in the latest changes.


Aurora reviews PRs for incident prevention.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
server/chat/backend/agent/model_catalog.py (1)

314-380: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Allowlist and featured checks should use canonical ids for dash/dot alias consistency.

get_enabled_models() de-duplicates by _canonical_id() so anthropic/claude-opus-4-8 and anthropic/claude-opus-4.8 collapse to one entry. However, the ENABLED_MODELS filter (line 370) and the FEATURED_MODELS override check (line 332) compare raw m["id"] / entry["id"] against the env-var set. If an operator writes ENABLED_MODELS="anthropic/claude-opus-4-8" (dash) but the catalog entry id is anthropic/claude-opus-4.8 (dot), the model won't match, the allowlist matches nothing, and the selector falls back to the full list with a warning — even though the model is available and _canonical_id was designed to treat both forms as the same.

Canonicalize both the allowlist/override sets and the id checks so alias forms match consistently.

🔧 Proposed fix
     featured_override = _featured_model_ids()
+    if featured_override is not None:
+        featured_override = {_canonical_id(a) for a in featured_override}

     def _mark_featured(entry: Dict, is_featured: bool) -> Dict:
         out = dict(entry)
         out["featured"] = (
-            entry["id"] in featured_override
+            _canonical_id(entry["id"]) in featured_override
             if featured_override is not None
             else is_featured
         )
         return out
     allowed = _enabled_model_ids()
     if allowed is None:
         return models

-    filtered = [m for m in models if m["id"] in allowed]
+    allowed = {_canonical_id(a) for a in allowed}
+    filtered = [m for m in models if _canonical_id(m["id"]) in allowed]
     if not filtered:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/chat/backend/agent/model_catalog.py` around lines 314 - 380,
`get_enabled_models()` is comparing raw model ids against `ENABLED_MODELS` and
`FEATURED_MODELS`, which breaks dash/dot aliases even though the function
already deduplicates with `_canonical_id()`. Update the allowlist and featured
override logic to normalize both the env-var ids and the candidate ids via
`_canonical_id()` so `entry["id"]`, `m["id"]`, and alias forms like
`anthropic/claude-opus-4-8` and `anthropic/claude-opus-4.8` match consistently.
Keep the change localized to `get_enabled_models()`, `_featured_model_ids()`,
and the final allowlist filter so the canonical comparison is used everywhere
ids are checked.
client/src/components/icons/provider-icons.tsx (1)

62-119: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

GeminiIcon uses fixed gradient IDs, colliding when multiple instances render.

The model selector groups entries by provider and renders ProviderIcon per list item plus the selected model, so multiple Google/Vertex models produce multiple GeminiIcon instances sharing the same lobe-icons-gemini-fill-0/1/2 IDs. Duplicate SVG IDs are invalid and url(#id) resolves to the first definition in document order, which can cause subtle rendering bugs across browsers.

Use React 18's useId() to namespace the gradient IDs per instance.

🛠️ Proposed fix using `useId()`
 export function GeminiIcon({ className, size = 16 }: Readonly<ProviderIconProps>) {
+  const gid = useId();
   const p =
     "M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z";
   return (
     <svg
       width={size}
       height={size}
       viewBox="0 0 24 24"
       xmlns="http://www.w3.org/2000/svg"
       className={className}
       aria-hidden
     >
       <path d={p} fill="`#3186FF`" />
-      <path d={p} fill="url(`#lobe-icons-gemini-fill-0`)" />
-      <path d={p} fill="url(`#lobe-icons-gemini-fill-1`)" />
-      <path d={p} fill="url(`#lobe-icons-gemini-fill-2`)" />
+      <path d={p} fill={`url(#${gid}-0)`} />
+      <path d={p} fill={`url(#${gid}-1)`} />
+      <path d={p} fill={`url(#${gid}-2)`} />
       <defs>
         <linearGradient
           gradientUnits="userSpaceOnUse"
-          id="lobe-icons-gemini-fill-0"
+          id={`${gid}-0`}
           x1="7"
           x2="11"
           y1="15.5"
           y2="12"
         >
           <stop stopColor="`#08B962`" />
           <stop offset="1" stopColor="`#08B962`" stopOpacity="0" />
         </linearGradient>
         <linearGradient
           gradientUnits="userSpaceOnUse"
-          id="lobe-icons-gemini-fill-1"
+          id={`${gid}-1`}
           x1="8"
           x2="11.5"
           y1="5.5"
           y2="11"
         >
           <stop stopColor="`#F94543`" />
           <stop offset="1" stopColor="`#F94543`" stopOpacity="0" />
         </linearGradient>
         <linearGradient
           gradientUnits="userSpaceOnUse"
-          id="lobe-icons-gemini-fill-2"
+          id={`${gid}-2`}
           x1="3.5"
           x2="17.5"
           y1="13.5"
           y2="12"
         >
           <stop stopColor="`#FABC12`" />
           <stop offset=".46" stopColor="`#FABC12`" stopOpacity="0" />
         </linearGradient>
       </defs>
     </svg>
   );
 }

And add the import at the top of the file:

-import React from 'react'
+import React, { useId } from 'react'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/components/icons/provider-icons.tsx` around lines 62 - 119,
GeminiIcon currently uses fixed SVG gradient IDs, so multiple rendered instances
can collide and make url(#...) resolve to the wrong definition. Update
GeminiIcon in provider-icons.tsx to namespace the three linearGradient ids per
render using React 18 useId(), and apply that generated prefix to the matching
fill references and defs so each icon instance has unique gradient identifiers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@client/src/components/icons/provider-icons.tsx`:
- Around line 62-119: GeminiIcon currently uses fixed SVG gradient IDs, so
multiple rendered instances can collide and make url(#...) resolve to the wrong
definition. Update GeminiIcon in provider-icons.tsx to namespace the three
linearGradient ids per render using React 18 useId(), and apply that generated
prefix to the matching fill references and defs so each icon instance has unique
gradient identifiers.

In `@server/chat/backend/agent/model_catalog.py`:
- Around line 314-380: `get_enabled_models()` is comparing raw model ids against
`ENABLED_MODELS` and `FEATURED_MODELS`, which breaks dash/dot aliases even
though the function already deduplicates with `_canonical_id()`. Update the
allowlist and featured override logic to normalize both the env-var ids and the
candidate ids via `_canonical_id()` so `entry["id"]`, `m["id"]`, and alias forms
like `anthropic/claude-opus-4-8` and `anthropic/claude-opus-4.8` match
consistently. Keep the change localized to `get_enabled_models()`,
`_featured_model_ids()`, and the final allowlist filter so the canonical
comparison is used everywhere ids are checked.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c921e8d4-b730-44e3-b37d-6b651cc9a8f9

📥 Commits

Reviewing files that changed from the base of the PR and between 5ef816e and 71e839a.

📒 Files selected for processing (9)
  • .env.example
  • client/src/app/api/llm-models/route.ts
  • client/src/components/icons/provider-icons.tsx
  • client/src/components/ui/command.tsx
  • docker-compose.prod-local.yml
  • server/chat/backend/agent/model_catalog.py
  • server/chat/backend/agent/model_mapper.py
  • server/chat/backend/agent/providers/openrouter_provider.py
  • server/routes/llm_config.py

@OlivierTrudeau OlivierTrudeau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I still need to test

// the shared helper (auth, timeouts, error normalization handled there).
export async function GET(request: NextRequest) {
return forwardAuthenticatedGet(request, '/api/llm-config/models', 'fetch model catalog')
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Instead of making a backend request which will add trafic for no good reason every time someone opens the chat page, why not just inject the env var in the frontend so no request to the api is done on opening the chat page

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

the catalog isn't static though — it's computed server-side from ENABLED_MODELS + LLM_PROVIDER_MODE + per-model display metadata, and it's auth-gated, so it can't just be a build-time env var (it'd differ per deployment and leak the allowlist to unauthed users). but you're right the per-open fetch is wasteful — cached it in sessionStorage in 4a4a2ea so it only hits the api once per session instead of every chat open.

every upstream provider), cached for 10 minutes. Falls back to the
statically mapped set if the API is unreachable or the key is unset, so
the selector always has something to show.
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Great but a lot don't work for X or Y reason. When this happens, we should print a friendly error message on the frontend at least.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

agreed. done in 4a4a2ea — workflow errors now detect model/provider failures (unsupported, gated, down, 4xx) and show 'the selected model isn't available or rejected this request — try a different model' instead of the generic 'try again'. the frontend already renders backend error messages as ⚠️ bot messages so it surfaces inline in chat.

Comment thread docker-compose.yaml
VISUALIZATION_ENABLED: ${VISUALIZATION_ENABLED:-false}
RCA_MODEL: ${RCA_MODEL}
MAIN_MODEL: ${MAIN_MODEL}
ENABLED_MODELS: ${ENABLED_MODELS:-}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Forgot to add in the deployment yamls

"displayName": display or name,
"provider": provider,
"tier": "pro",
"contextLength": "",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Does it make sense to have a tier if we don't restrict by tier

"tier": "free",
"contextLength": "1M",
"hasReasoning": True,
"pricing": "Low Cost ($0.30/$2.50 per 1M)",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think pricing and reasoning are ever used if ever you want to clean up a bit

Addresses review feedback:
- ModelSelector caches the catalog in sessionStorage so it doesn't refetch on
  every chat-page open (the catalog only changes with deployment config). The
  endpoint stays a dynamic, auth-gated API (honors ENABLED_MODELS/provider mode/
  display metadata) — not something injectable as a build-time env var.
- Workflow errors now map common model/provider failures to a friendly,
  actionable message ('model unavailable/rejected — try a different model')
  instead of the generic 'try again', which doesn't help when the selected
  model is the problem.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@arvo-ai-staging arvo-ai-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Aurora Risk Review — Latest changes

Verdict: SAFE

No new incident risk in the latest changes.


Aurora reviews PRs for incident prevention.

@sonarqubecloud

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/main_chatbot.py`:
- Around line 341-347: Update the model-error classification logic around the
model_signals tuple and its any check to remove generic HTTP/resource indicators
such as “400”, “404”, and “not found”, retaining only model- or
provider-specific signals. Add tests covering unrelated missing-resource and
HTTP failures to ensure they are not classified as model failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0c31619a-c67d-4c24-9ad9-f2a4c64b8815

📥 Commits

Reviewing files that changed from the base of the PR and between 71e839a and 4a4a2ea.

📒 Files selected for processing (4)
  • .env.example
  • client/src/components/ModelSelector.tsx
  • server/chat/backend/agent/model_catalog.py
  • server/main_chatbot.py
💤 Files with no reviewable changes (1)
  • .env.example

Comment thread server/main_chatbot.py
Comment on lines +341 to +347
text = str(e).lower()
model_signals = (
"model", "not a valid", "not supported", "no endpoints",
"does not exist", "invalid model", "provider", "openrouter",
"unsupported", "not found", "404", "400",
)
if any(s in text for s in model_signals):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Avoid classifying unrelated HTTP errors as model failures.

Signals such as "400", "404", and "not found" can occur in unrelated workflow errors (for example, a missing incident), causing the UI to incorrectly tell users to switch models. Restrict matching to model/provider-specific errors and add tests for unrelated resource and HTTP failures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/main_chatbot.py` around lines 341 - 347, Update the model-error
classification logic around the model_signals tuple and its any check to remove
generic HTTP/resource indicators such as “400”, “404”, and “not found”,
retaining only model- or provider-specific signals. Add tests covering unrelated
missing-resource and HTTP failures to ensure they are not classified as model
failures.

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