diff --git a/api/config.py b/api/config.py index 34c28173..a67d5139 100644 --- a/api/config.py +++ b/api/config.py @@ -3246,7 +3246,9 @@ def _filter_reasoning_efforts_for_provider( normalized = list(dict.fromkeys(normalized)) provider = _resolve_provider_alias(str(provider_id or "").strip().lower()) bare = _strip_provider_hint_for_reasoning(model_id).lower().rsplit("/", 1)[-1] - if provider == "openai-codex": + # OpenAI-family lanes (Codex, direct OpenAI, Azure Foundry) cap GPT-5 at xhigh + # and o-series at high — 'max' is a WebUI-only level none of them accept. + if provider in {"openai-codex", "openai", "openai-api", "azure-foundry", "azure-openai", "azure"}: if bare.startswith(("o1", "o3", "o4")): return [eff for eff in normalized if eff in {"low", "medium", "high"}] if bare.startswith("gpt-5"): @@ -3258,7 +3260,14 @@ def _filter_reasoning_efforts_for_provider( # Dropping 'max' here lets the existing downgrade ladder land on xhigh/high. if provider in {"gemini", "google", "google-gemini", "google-vertex", "vertex"}: return [eff for eff in normalized if eff != "max"] - if provider in {"anthropic", "claude", "anthropic-claude"} and _is_pre_adaptive_anthropic(bare): + # Legacy Claude is pre-adaptive whether served natively OR via Azure Foundry / + # Bedrock / Vertex — the ceiling follows the MODEL, not just the provider name. + _anthropic_lanes = { + "anthropic", "claude", "anthropic-claude", + "azure-foundry", "azure-openai", "azure", "bedrock", "aws-bedrock", + "vertex", "google-vertex", + } + if provider in _anthropic_lanes and "claude" in bare and _is_pre_adaptive_anthropic(bare): return [eff for eff in normalized if eff != "max"] return normalized diff --git a/tests/test_reasoning_effort_model_capabilities.py b/tests/test_reasoning_effort_model_capabilities.py index 4c438485..4a13b903 100644 --- a/tests/test_reasoning_effort_model_capabilities.py +++ b/tests/test_reasoning_effort_model_capabilities.py @@ -362,3 +362,28 @@ def test_max_effort_preserved_for_adaptive_anthropic_and_deepseek(): assert cfg.coerce_reasoning_effort_for_model( "max", model_id="deepseek-reasoner", provider_id="deepseek" ) == "max" + + +def test_max_degrades_across_all_openai_family_lanes(): + # 'max' is WebUI-only; direct OpenAI, openai-api, and Azure Foundry GPT-5 all + # cap at xhigh (o-series at high), not just openai-codex. (#4627 re-gate) + for prov in ("openai", "openai-api", "azure-foundry", "openai-codex"): + assert cfg.coerce_reasoning_effort_for_model( + "max", model_id="gpt-5.1", provider_id=prov + ) == "xhigh", f"gpt-5 on {prov} must degrade max->xhigh" + assert cfg.coerce_reasoning_effort_for_model( + "max", model_id="o3", provider_id=prov + ) == "high", f"o-series on {prov} must degrade max->high" + + +def test_max_degrades_for_azure_bedrock_hosted_legacy_claude(): + # Legacy Claude via Azure Foundry / Bedrock is still pre-adaptive; the ceiling + # follows the model, not just the provider name. (#4627 re-gate) + for prov in ("azure-foundry", "bedrock"): + assert cfg.coerce_reasoning_effort_for_model( + "max", model_id="claude-sonnet-4-20250514", provider_id=prov + ) == "xhigh", f"legacy Claude on {prov} must degrade max->xhigh" + # adaptive Claude via azure preserves max + assert cfg.coerce_reasoning_effort_for_model( + "max", model_id="claude-opus-4.6", provider_id="azure-foundry" + ) == "max"