From 2f2d36d4eb4434c8ed34750e12f2c330bfa5969c Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Sat, 20 Jun 2026 23:41:31 -0400 Subject: [PATCH] fix(#4536): gate service tier on effective send model, not saved default --- api/config.py | 28 ++++++++++++++++------ api/gateway_chat.py | 13 +++++++++++ api/streaming.py | 6 ++++- tests/test_issue4536_service_tier.py | 35 ++++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 8 deletions(-) diff --git a/api/config.py b/api/config.py index 49c8ec9eb..7ca3f3952 100644 --- a/api/config.py +++ b/api/config.py @@ -3544,19 +3544,33 @@ def _public_main_service_tier(model_cfg: dict) -> str: return "priority" if service_tier == "priority" else "" -def _main_model_request_overrides(config_data: dict) -> dict: - """Return supported runtime request overrides for the main chat model.""" +def _main_model_request_overrides( + config_data: dict, + effective_model: str | None = None, + effective_provider: str | None = None, +) -> dict: + """Return supported runtime request overrides for the main chat model. + + When *effective_model* / *effective_provider* are supplied, the + service-tier gate checks those instead of the saved default model, + so a per-session model switch to a non-OpenAI provider does not + leak ``service_tier`` onto an unsupported request. + """ if not isinstance(config_data, dict): return {} model_cfg = config_data.get("model", {}) if not isinstance(model_cfg, dict): return {} overrides = {} - model_id = str(model_cfg.get("default") or model_cfg.get("name") or "").strip() - resolved_provider = str(model_cfg.get("provider") or "").strip().lower() - if not resolved_provider: - _, resolved_provider, _ = resolve_model_provider(model_id) - if _main_model_supports_service_tier(model_id, resolved_provider): + gate_model = effective_model + gate_provider = effective_provider + if not gate_model: + gate_model = str(model_cfg.get("default") or model_cfg.get("name") or "").strip() + if not gate_provider: + gate_provider = str(model_cfg.get("provider") or "").strip().lower() + if not gate_provider: + _, gate_provider, _ = resolve_model_provider(gate_model) + if _main_model_supports_service_tier(gate_model, gate_provider): service_tier = str(model_cfg.get("service_tier") or "").strip().lower() if service_tier == "priority": overrides["service_tier"] = "priority" diff --git a/api/gateway_chat.py b/api/gateway_chat.py index 3cc421666..f5d21da1a 100644 --- a/api/gateway_chat.py +++ b/api/gateway_chat.py @@ -591,6 +591,15 @@ def _run_gateway_chat_streaming( prefill_messages = [] base_url = _gateway_base_url(cfg) api_key = _gateway_api_key() + try: + from api.config import _main_model_request_overrides + _gw_overrides = _main_model_request_overrides( + cfg, + effective_model=model, + effective_provider=model_provider, + ) + except Exception: + _gw_overrides = {} # Capability gate: use runs API when gateway advertises approval support. _use_runs_api = _gateway_use_runs_api_enabled(cfg) and gateway_supports_approval(base_url, api_key) if _use_runs_api: @@ -599,6 +608,8 @@ def _run_gateway_chat_streaming( body_extras["provider"] = model_provider if reasoning_effort is not None: body_extras["reasoning_effort"] = reasoning_effort + if _gw_overrides.get("service_tier"): + body_extras["service_tier"] = _gw_overrides["service_tier"] try: final_text, usage = _run_gateway_runs_api_streaming( session_id, msg_text, model, workspace, stream_id, @@ -661,6 +672,8 @@ def _run_gateway_chat_streaming( body["provider"] = model_provider if reasoning_effort is not None: body["reasoning_effort"] = reasoning_effort + if _gw_overrides.get("service_tier"): + body["service_tier"] = _gw_overrides["service_tier"] req = urllib.request.Request( url, data=json.dumps(body).encode("utf-8"), diff --git a/api/streaming.py b/api/streaming.py index af28636a2..69a90410b 100644 --- a/api/streaming.py +++ b/api/streaming.py @@ -7026,7 +7026,11 @@ def _run_agent_streaming( _prefill_context = _load_webui_prefill_context(_cfg) _prefill_messages = _prefill_messages_with_webui_context(_prefill_context, _cfg) _prefill_messages = _normalize_prefill_messages_before_user_turn(_prefill_messages) - _main_request_overrides = _main_model_request_overrides(_cfg) + _main_request_overrides = _main_model_request_overrides( + _cfg, + effective_model=resolved_model, + effective_provider=resolved_provider, + ) put('context_status', { 'session_id': session_id, 'prefill': _public_prefill_context_status(_prefill_context), diff --git a/tests/test_issue4536_service_tier.py b/tests/test_issue4536_service_tier.py index 3ebaad939..e0de22f13 100644 --- a/tests/test_issue4536_service_tier.py +++ b/tests/test_issue4536_service_tier.py @@ -94,6 +94,41 @@ class TestIssue4536ServiceTier: assert payload["main"]["service_tier"] == "" + def test_effective_model_gates_service_tier_forwarding(self): + """service_tier must be gated on the effective send model, not the saved default.""" + from api import config + + cfg_priority = {"model": {"provider": "openai", "default": "gpt-5.5", "service_tier": "priority"}} + + openai_effective = config._main_model_request_overrides( + cfg_priority, effective_model="gpt-5.5", effective_provider="openai", + ) + assert openai_effective.get("service_tier") == "priority" + + openrouter_effective = config._main_model_request_overrides( + cfg_priority, effective_model="meta-llama/llama-3.1", effective_provider="openrouter", + ) + assert openrouter_effective == {} + + custom_effective = config._main_model_request_overrides( + cfg_priority, effective_model="my-local-model", effective_provider="custom:local", + ) + assert custom_effective == {} + + codex_effective = config._main_model_request_overrides( + cfg_priority, effective_model="gpt-5.3-codex", effective_provider="openai-codex", + ) + assert codex_effective == {} + + def test_no_effective_params_falls_back_to_saved_default(self): + """Without effective_model/provider, the function still reads the saved config.""" + from api import config + + result = config._main_model_request_overrides( + {"model": {"provider": "openai", "default": "gpt-5.5", "service_tier": "priority"}}, + ) + assert result == {"service_tier": "priority"} + def test_switching_main_model_away_from_openai_clears_service_tier(self, monkeypatch, tmp_path): """A non-OpenAI default-model save should remove stale OpenAI service-tier state.""" from api import config