diff --git a/api/config.py b/api/config.py index 9c4a984..35973c4 100644 --- a/api/config.py +++ b/api/config.py @@ -640,9 +640,12 @@ def get_available_models() -> dict: # Ensure the user's configured default_model always appears in the dropdown. # It may be missing if the model isn't in any hardcoded list (e.g. openrouter/free, # a custom local model, or any model.default not in _FALLBACK_MODELS). + # Normalize before comparing: strip provider prefix so 'anthropic/claude-opus-4.6' + # matches 'claude-opus-4.6' already in the list and avoids a duplicate entry. if default_model: - all_ids = {m['id'] for g in groups for m in g.get('models', [])} - if default_model not in all_ids: + _norm = lambda mid: mid.split('/', 1)[-1] if '/' in mid else mid + all_ids_norm = {_norm(m['id']) for g in groups for m in g.get('models', [])} + if _norm(default_model) not in all_ids_norm: # Determine which group to inject into label = default_model.split('/')[-1] if '/' in default_model else default_model injected = False diff --git a/static/ui.js b/static/ui.js index d10f756..a6c73c5 100644 --- a/static/ui.js +++ b/static/ui.js @@ -424,11 +424,16 @@ function syncTopbar(){ } else { const m=S.session.model||''; const applied=_applyModelToDropdown(m,$('modelSelect')); - // If the model isn't in the list at all, add it so the session value is preserved + // If the model isn't in the current provider list, add it as a visually marked + // "(unavailable)" entry so the session value is preserved without misleading the user. + // Selecting it will still attempt to send (same as before), but the label makes + // clear it's a stale model from a previous session. if(!applied && m){ const opt=document.createElement('option'); opt.value=m; - opt.textContent=getModelLabel(m); + opt.textContent=getModelLabel(m)+' (unavailable)'; + opt.style.color='var(--muted, #888)'; + opt.title='This model is no longer in your current provider list'; $('modelSelect').appendChild(opt); $('modelSelect').value=m; } diff --git a/tests/test_model_resolver.py b/tests/test_model_resolver.py index 38efd9e..ecad825 100644 --- a/tests/test_model_resolver.py +++ b/tests/test_model_resolver.py @@ -163,6 +163,33 @@ def test_non_default_provider_models_use_hint_prefix(): ) +def test_no_duplicate_when_default_model_is_prefixed(): + """Issue #147 Bug 2: 'anthropic/claude-opus-4.6' as default_model must not + inject a duplicate alongside the existing bare 'claude-opus-4.6' entry in + the same provider group.""" + import api.config as _cfg + old_cfg = dict(_cfg.cfg) + _cfg.cfg['model'] = { + 'provider': 'anthropic', + 'default': 'anthropic/claude-opus-4.6', + } + try: + result = _cfg.get_available_models() + norm = lambda mid: mid.split('/', 1)[-1] if '/' in mid else mid + # Check each group individually: no group should have two entries that + # normalize to the same bare model name + for g in result['groups']: + bare_ids = [norm(m['id']) for m in g['models']] + duplicates = [mid for mid in set(bare_ids) if bare_ids.count(mid) > 1] + assert not duplicates, ( + f"Provider group '{g['provider']}' has duplicate models after normalization: " + f"{duplicates}\nFull group: {[m['id'] for m in g['models']]}" + ) + finally: + _cfg.cfg.clear() + _cfg.cfg.update(old_cfg) + + def test_default_provider_models_not_prefixed(): """The active provider's models remain bare (no @prefix added).""" import api.config as _cfg