diff --git a/api/config.py b/api/config.py index e74fe911a..59adcf497 100644 --- a/api/config.py +++ b/api/config.py @@ -1703,6 +1703,7 @@ _PROVIDER_MODELS = { {"id": "gemini-3.5-flash", "label": "Gemini 3.5 Flash"}, {"id": "gemini-2.5-pro", "label": "Gemini 2.5 Pro"}, {"id": "mai-code-1-flash-picker", "label": "MAI Code 1 Flash"}, + {"id": "gpt-4o", "label": "GPT-4o"}, ], # Cursor ACP — models served via Cursor CLI agent acp "cursor-acp": [ diff --git a/api/routes.py b/api/routes.py index 753ca21a2..8bc32e13c 100644 --- a/api/routes.py +++ b/api/routes.py @@ -5888,6 +5888,23 @@ def _read_profile_model_config( return _provider, _default +def _moa_fast_path_model_state(model: str) -> tuple[str, str, bool]: + """Strip an optional ``@moa:``/``moa/`` prefix from an MoA-routed model. + + Split out of ``_resolve_compatible_session_model_state`` so the MoA + fast-path stays a single-line call in that function body (see + ``test_issue1855_resolve_model_provider_fast_path.py``, the fast-path/ + catalog-call ordering check scans a bounded window of that function's + source, and inlining this here previously pushed the catalog call just + past that window). + """ + if model.startswith("@moa:"): + return model.split(":", 1)[1].strip(), "moa", True + if model.lower().startswith("moa/"): + return model.split("/", 1)[1].strip(), "moa", True + return model, "moa", False + + def _resolve_compatible_session_model_state( model_id: str | None, model_provider: str | None = None, @@ -5914,7 +5931,7 @@ def _resolve_compatible_session_model_state( after paying the full catalog-build cost. Avoiding the catalog here keeps ``POST /api/chat/start`` snappy even when the model catalog is cold and the rebuild has to make network calls (custom OpenAI-compat endpoints, - OpenRouter ``/models``, LM Studio ``/models``, credential pool refresh) — + OpenRouter ``/models``, LM Studio ``/models``, credential pool refresh), those used to wedge the handler for >100s and trigger 502s on default-60s reverse proxies, even though the WebUI itself eventually responded. @@ -5922,7 +5939,7 @@ def _resolve_compatible_session_model_state( non-blocking: it resolves from the warm/disk cache or a network-free minimal catalog and NEVER triggers a live per-provider rebuild (the Copilot token-exchange HTTPS call that hangs a server-initiated wakeup - turn — see rebase report §1/§3/model-resolve-hang). Human-initiated + turn, see rebase report §1/§3/model-resolve-hang). Human-initiated chat/start leaves this False to keep full live discovery; a session that already has a persisted model still resolves correctly because the persisted model wins over the catalog and the catalog is only consulted @@ -5931,11 +5948,7 @@ def _resolve_compatible_session_model_state( model = str(model_id or "").strip() requested_provider = _clean_session_model_provider(model_provider) if model and requested_provider == "moa": - if model.startswith("@moa:"): - return model.split(":", 1)[1].strip(), "moa", True - if model.lower().startswith("moa/"): - return model.split("/", 1)[1].strip(), "moa", True - return model, "moa", False + return _moa_fast_path_model_state(model) if model and requested_provider and model.startswith(f"@{requested_provider}:"): try: from api.config import cfg as _active_cfg