diff --git a/api/routes.py b/api/routes.py index 455d5bed..c7f287ac 100644 --- a/api/routes.py +++ b/api/routes.py @@ -1512,7 +1512,12 @@ def _resolve_compatible_session_model_state( # qualifier — qualified strings require the catalog to decide whether # the qualifier matches the active provider (see slow path below). bare_model, explicit_provider = _split_provider_qualified_model(model) - if not explicit_provider: + model_prefix = model.split("/", 1)[0].strip().lower() if "/" in model else "" + stale_codex_openai_slash_id = ( + requested_provider == "openai-codex" + and model_prefix == "openai" + ) + if not explicit_provider and not stale_codex_openai_slash_id: return model, requested_provider, False catalog = get_available_models() @@ -1533,7 +1538,14 @@ def _resolve_compatible_session_model_state( bare_for_context, explicit_provider = _split_provider_qualified_model(model) if requested_provider and not explicit_provider: - return model, requested_provider, False + model_prefix = model.split("/", 1)[0].strip().lower() if "/" in model else "" + stale_codex_openai_slash_id = ( + raw_active_provider == "openai-codex" + and requested_provider == "openai-codex" + and model_prefix == "openai" + ) + if not stale_codex_openai_slash_id: + return model, requested_provider, False if model.startswith("@") and ":" in model: provider_raw = explicit_provider or "" @@ -1643,7 +1655,7 @@ def _resolve_compatible_session_model_state( if ( raw_active_provider == "openai-codex" and model_provider == "openai" - and requested_provider is None + and requested_provider in {None, "openai-codex"} and default_model ): # Persist provider_context = "openai-codex" unconditionally on this diff --git a/tests/test_issue1855_resolve_model_provider_fast_path.py b/tests/test_issue1855_resolve_model_provider_fast_path.py index a7aa86f1..0cbb321d 100644 --- a/tests/test_issue1855_resolve_model_provider_fast_path.py +++ b/tests/test_issue1855_resolve_model_provider_fast_path.py @@ -61,13 +61,12 @@ class TestFastPathInvocation: ) assert result == ("gpt-5.5", "openai-codex", False) - def test_fast_path_with_slash_qualified_model_skips_catalog(self): - """Slash-qualified IDs (openrouter/...) still hit the fast path. + def test_fast_path_with_openrouter_slash_qualified_model_skips_catalog(self): + """OpenRouter slash-qualified IDs still hit the fast path. - The fast path only excludes @provider:model strings, not slash- - qualified ones — those are valid model IDs that the picker emits - for OpenRouter and custom-provider routing, and a stored - model_provider is the authoritative routing decision. + Slash-qualified IDs are valid picker output for OpenRouter and a stored + model_provider is the authoritative routing decision. This remains fast + for explicit OpenRouter selections. """ from api.routes import _resolve_compatible_session_model_state @@ -80,6 +79,33 @@ class TestFastPathInvocation: assert mock_catalog.call_count == 0 assert result == ("anthropic/claude-opus-4.7", "openrouter", False) + def test_codex_with_stale_openai_slash_id_uses_catalog_repair(self): + """Codex must repair stale OpenRouter-shaped OpenAI IDs. + + Browser/localStorage state can submit ``openai/gpt-...`` while the + session/provider is ``openai-codex``. If the fast path preserves that + pair, runtime resolution routes through OpenRouter instead of Codex. + The Codex + ``openai/...`` shape must therefore use the slow-path repair + and normalize back to the active Codex default. + """ + from api.routes import _resolve_compatible_session_model_state + + with patch("api.routes.get_available_models") as mock_catalog: + mock_catalog.return_value = { + "active_provider": "openai-codex", + "default_model": "gpt-5.5", + "groups": [ + {"provider_id": "openai-codex", "models": [{"id": "gpt-5.5"}]} + ], + } + result = _resolve_compatible_session_model_state( + "openai/gpt-5.4-mini", + "openai-codex", + ) + + assert mock_catalog.call_count == 1 + assert result == ("gpt-5.5", "openai-codex", True) + def test_fast_path_normalizes_provider_default_alias(self): """`'default'` is treated as None by _clean_session_model_provider.