diff --git a/plugins/model-providers/upstage/__init__.py b/plugins/model-providers/upstage/__init__.py index d36c0734a3a9..b050051badef 100644 --- a/plugins/model-providers/upstage/__init__.py +++ b/plugins/model-providers/upstage/__init__.py @@ -76,18 +76,20 @@ class UpstageProfile(ProviderProfile): return {}, top_level # Map Hermes' effort vocabulary onto Solar's accepted set. xhigh/max/ - # ultra collapse to high (Solar's strongest). minimal → off (omit). An - # enabled request with no recognised effort uses the default effort. + # ultra collapse to high (Solar's strongest). minimal → off (omit). + # Unknown-but-enabled efforts (future vocabulary additions above + # "high", per the max/ultra precedent in #62650) also collapse to + # high rather than silently downgrading to the medium default. effort = (reasoning_config.get("effort") or "").strip().lower() + if not effort: + top_level["reasoning_effort"] = _DEFAULT_REASONING_EFFORT + return {}, top_level mapped = { "minimal": None, "low": "low", "medium": "medium", "high": "high", - "xhigh": "high", - "max": "high", - "ultra": "high", - }.get(effort, _DEFAULT_REASONING_EFFORT) + }.get(effort, "high") if mapped: top_level["reasoning_effort"] = mapped diff --git a/tests/hermes_cli/test_upstage_provider.py b/tests/hermes_cli/test_upstage_provider.py index 4e2829f61419..6f7da10761f7 100644 --- a/tests/hermes_cli/test_upstage_provider.py +++ b/tests/hermes_cli/test_upstage_provider.py @@ -12,7 +12,6 @@ from __future__ import annotations import sys import types -import pytest if "dotenv" not in sys.modules: fake_dotenv = types.ModuleType("dotenv") diff --git a/tests/plugins/model_providers/test_upstage_profile.py b/tests/plugins/model_providers/test_upstage_profile.py index 257900aed198..ce227b675d8f 100644 --- a/tests/plugins/model_providers/test_upstage_profile.py +++ b/tests/plugins/model_providers/test_upstage_profile.py @@ -49,9 +49,12 @@ class TestUpstageProfile: # Only the agentic, tool-calling Solar Pro models belong in the offline # catalog — Mini is capable but not agentic, so it's never promoted as a # default. Live /v1/models still surfaces everything when a key is set. - assert upstage_profile.fallback_models == ( - "solar-pro3", - ) + # Behavior contract (not a frozen list): non-empty, no denied families. + assert upstage_profile.fallback_models + for denied in ("solar-mini", "syn-pro"): + assert not any( + denied in m for m in upstage_profile.fallback_models + ), f"non-agentic family {denied!r} must not be a fallback default" def test_default_model_is_solar_pro3(self, upstage_profile): # Entry [0] is the setup default (get_default_model_for_provider). @@ -85,6 +88,16 @@ class TestUpstageReasoning: ) assert top_level == {"reasoning_effort": "high"} + def test_unknown_future_effort_collapses_to_high(self, upstage_profile): + # Guard against the #62650 recurrence: a future effort level Hermes + # adds above "high" must collapse to Solar's strongest, not silently + # downgrade to the medium default. + _, top_level = upstage_profile.build_api_kwargs_extras( + reasoning_config={"enabled": True, "effort": "hyperthink"}, + model="solar-pro3", + ) + assert top_level == {"reasoning_effort": "high"} + def test_pro_enabled_without_effort_defaults_on(self, upstage_profile): _, top_level = upstage_profile.build_api_kwargs_extras( reasoning_config={"enabled": True}, model="solar-pro3"