Skip to content

bug(models): session model picker loses provider identity when multiple providers expose the same model ID #1228

@hacker1e7

Description

@hacker1e7

Summary

When multiple providers expose the same model ID/name in the conversation model picker, the picker loses provider identity.

In my current local setup, /api/models returns a duplicate model ID:

  • custom:edith -> gpt-5.4
  • openai-codex -> gpt-5.4

This causes two visible problems in the chat/session model picker:

  1. all duplicate rows render as selected/active at the same time
  2. clicking another provider's copy of the same model is treated as a no-op, so the effective provider never switches

In practice it looks like the UI always keeps using the first matching provider entry.

Reproduction

  1. Configure multiple providers that expose the same upstream model name / ID
    • example from my local setup: custom:edith and openai-codex both expose gpt-5.4
  2. Open the conversation model dropdown in the session footer
  3. Select the gpt-5.4 entry under one provider
  4. Re-open the dropdown and click the gpt-5.4 entry under the other provider

Actual behavior

  • both duplicate rows appear active/selected
  • clicking the other provider's duplicate does not really switch provider identity
  • the persisted session model remains the same raw string value, so the request path keeps using whichever provider mapping wins first

Expected behavior

Each provider/model entry should be uniquely selectable, even when multiple providers expose the same upstream model string.

Root cause from source inspection

This looks like a provider-identity bug caused by using the raw model string as the only option identity.

1. /api/models can emit duplicate IDs across providers

In api/config.py, _apply_provider_prefix() skips prefixing when active_provider is empty:

# api/config.py
_active = (active_provider or "").lower()
if not _active or provider_id == _active:
    return list(raw_models)

So if active_provider is None, multiple providers can emit the same bare ID unchanged.

2. Named custom provider groups also bypass provider-unique encoding

In the named custom-provider group path, models are appended as raw IDs and then added directly to groups:

# api/config.py
_named_custom_groups[_slug][1].append({"id": _cp_model, "label": _cp_label})
...
groups.append({"provider": _nc_display, "provider_id": pid, "models": _nc_models})

That means a named custom provider like custom:edith can contribute bare gpt-5.4, which then collides with another provider group that also exposes bare gpt-5.4.

3. The frontend selection state is keyed only by value

In static/ui.js, the custom dropdown marks rows active only by comparing m.value === sel.value:

row.className = 'model-opt' + (m.value === sel.value ? ' active' : '');
row.onclick = () => selectModelFromDropdown(m.value);

So once two rows share the same value, both rows render active.

4. Clicking a duplicate value is treated as "no change"

selectModelFromDropdown() returns early when the current <select> already has the same raw value:

if(!sel || sel.value === value) { closeModelDropdown(); return; }

So clicking the other provider's copy of the same value does not switch anything.

5. Session persistence only stores the raw model string

static/boot.js sends only model:selectedModel to /api/session/update:

const selectedModel = $('modelSelect').value;
await api('/api/session/update', {
  method: 'POST',
  body: JSON.stringify({
    session_id: S.session.session_id,
    workspace: S.session.workspace,
    model: selectedModel,
  })
});

So once provider identity is lost in the dropdown value, it is also lost in session persistence / backend resolution.

Why this seems distinct from earlier related issues

This looks related to earlier provider-routing / dedup work such as #138 and #907, but the current bug is specifically about duplicate option values across providers in the session model picker, which breaks:

  • visual active state
  • click-to-switch behavior
  • persisted provider identity

Suggested fix direction

I think the safest fix is: ensure every provider/model entry in the picker has a globally unique value.

Concretely:

  1. do not rely on the bare model string as the unique identity when multiple providers may expose the same model
  2. encode provider identity into the option value for ambiguous entries (for example @provider:model)
  3. do this even when active_provider is None
  4. make sure named custom-provider groups also go through the same uniqueness logic
  5. optionally add a defensive duplicate-value check when building the dropdown/API response

That would preserve provider identity across:

  • visual selection state
  • click handling
  • session persistence
  • backend provider/model resolution

Local evidence

From my current local /api/models response:

active_provider = None

gpt-5.4 => ['custom:edith', 'openai-codex']

This duplicate is enough to reproduce the bug in the session model picker.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingprioritysprint-candidateStrong candidate for next sprint

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions