feat(router): per-group supported reasoning efforts with max and ultra levels

This commit is contained in:
Tin Chi Lo 2026-08-21 15:28:22 -07:00
parent d0da90ee6d
commit 0e96491554
25 changed files with 551 additions and 102 deletions

View file

@ -1113,22 +1113,13 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
litellm.reasoning_auto_summary or os.getenv("LITELLM_REASONING_AUTO_SUMMARY", "false").lower() == "true"
)
# If string is passed, map with optional summary based on flag/env var
if reasoning_effort == "none":
return Reasoning(effort="none", summary="detailed") if auto_summary_enabled else Reasoning(effort="none")
elif reasoning_effort == "high":
return Reasoning(effort="high", summary="detailed") if auto_summary_enabled else Reasoning(effort="high")
elif reasoning_effort == "xhigh":
return Reasoning(effort="xhigh", summary="detailed") if auto_summary_enabled else Reasoning(effort="xhigh")
elif reasoning_effort == "medium":
# Level-agnostic: providers own effort validation, so an unknown level (max, ultra, future
# ones) passes through instead of being silently dropped here.
if reasoning_effort:
return (
Reasoning(effort="medium", summary="detailed") if auto_summary_enabled else Reasoning(effort="medium")
)
elif reasoning_effort == "low":
return Reasoning(effort="low", summary="detailed") if auto_summary_enabled else Reasoning(effort="low")
elif reasoning_effort == "minimal":
return (
Reasoning(effort="minimal", summary="detailed") if auto_summary_enabled else Reasoning(effort="minimal")
Reasoning(effort=reasoning_effort, summary="detailed")
if auto_summary_enabled
else Reasoning(effort=reasoning_effort)
)
return None

View file

@ -16,7 +16,7 @@ def _normalize_reasoning_effort_for_chat_completion(
) -> str | None:
"""Convert reasoning_effort to the string format expected by OpenAI chat completion API.
The chat completion API expects a simple string: 'none', 'low', 'medium', 'high', or 'xhigh'.
The chat completion API expects a simple effort string ('none' through 'ultra').
Config/deployments may pass the Responses API format: {'effort': 'high', 'summary': 'detailed'}.
"""
if value is None:
@ -222,8 +222,8 @@ class OpenAIGPT5Config(OpenAIGPTConfig):
if "reasoning_effort" in optional_params:
optional_params["reasoning_effort"] = normalized
if effective_effort == "xhigh":
# xhigh is an opt-in capability: only allow if model explicitly supports it.
if effective_effort in ("xhigh", "max", "ultra"):
# xhigh/max/ultra are opt-in capabilities: only allow if the model explicitly supports them.
if not self._supports_reasoning_effort_level(model, effective_effort):
if litellm.drop_params or drop_params:
non_default_params.pop("reasoning_effort", None)

View file

@ -416,7 +416,7 @@ async def acompletion(
logprobs: bool | None = None,
top_logprobs: int | None = None,
deployment_id=None,
reasoning_effort: Literal["none", "minimal", "low", "medium", "high", "xhigh", "default"] | None = None,
reasoning_effort: Literal["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra", "default"] | None = None,
verbosity: Literal["low", "medium", "high"] | None = None,
safety_identifier: str | None = None,
service_tier: str | None = None,
@ -4920,7 +4920,7 @@ def completion(
logit_bias: dict | None = None,
user: str | None = None,
# openai v1.0+ new params
reasoning_effort: Literal["none", "minimal", "low", "medium", "high", "xhigh", "default"] | None = None,
reasoning_effort: Literal["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra", "default"] | None = None,
verbosity: Literal["low", "medium", "high"] | None = None,
response_format: dict | type[BaseModel] | None = None,
seed: int | None = None,

View file

@ -6639,7 +6639,9 @@
"supports_web_search": true,
"supports_none_reasoning_effort": true,
"supports_xhigh_reasoning_effort": true,
"supports_minimal_reasoning_effort": false
"supports_minimal_reasoning_effort": false,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"azure/gpt-5.6-sol": {
"cache_read_input_token_cost": 5e-07,
@ -6690,7 +6692,9 @@
"supports_web_search": true,
"supports_none_reasoning_effort": true,
"supports_xhigh_reasoning_effort": true,
"supports_minimal_reasoning_effort": false
"supports_minimal_reasoning_effort": false,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"azure/gpt-5.6-terra": {
"cache_read_input_token_cost": 2e-07,
@ -6741,7 +6745,9 @@
"supports_web_search": true,
"supports_none_reasoning_effort": true,
"supports_xhigh_reasoning_effort": true,
"supports_minimal_reasoning_effort": false
"supports_minimal_reasoning_effort": false,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"azure/gpt-5.6-luna": {
"cache_read_input_token_cost": 2e-08,
@ -6792,7 +6798,9 @@
"supports_web_search": true,
"supports_none_reasoning_effort": true,
"supports_xhigh_reasoning_effort": true,
"supports_minimal_reasoning_effort": false
"supports_minimal_reasoning_effort": false,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"azure/us/gpt-5.6": {
"cache_read_input_token_cost": 5.5e-07,
@ -6839,7 +6847,9 @@
"supports_web_search": true,
"supports_none_reasoning_effort": true,
"supports_xhigh_reasoning_effort": true,
"supports_minimal_reasoning_effort": false
"supports_minimal_reasoning_effort": false,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"azure/us/gpt-5.6-sol": {
"cache_read_input_token_cost": 5.5e-07,
@ -6887,7 +6897,9 @@
"supports_web_search": true,
"supports_none_reasoning_effort": true,
"supports_xhigh_reasoning_effort": true,
"supports_minimal_reasoning_effort": false
"supports_minimal_reasoning_effort": false,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"azure/us/gpt-5.6-terra": {
"cache_read_input_token_cost": 2.2e-07,
@ -6935,7 +6947,9 @@
"supports_web_search": true,
"supports_none_reasoning_effort": true,
"supports_xhigh_reasoning_effort": true,
"supports_minimal_reasoning_effort": false
"supports_minimal_reasoning_effort": false,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"azure/us/gpt-5.6-luna": {
"cache_read_input_token_cost": 2.2e-08,
@ -6983,7 +6997,9 @@
"supports_web_search": true,
"supports_none_reasoning_effort": true,
"supports_xhigh_reasoning_effort": true,
"supports_minimal_reasoning_effort": false
"supports_minimal_reasoning_effort": false,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"azure/eu/gpt-5.6": {
"cache_read_input_token_cost": 5.5e-07,
@ -7030,7 +7046,9 @@
"supports_web_search": true,
"supports_none_reasoning_effort": true,
"supports_xhigh_reasoning_effort": true,
"supports_minimal_reasoning_effort": false
"supports_minimal_reasoning_effort": false,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"azure/eu/gpt-5.6-sol": {
"cache_read_input_token_cost": 5.5e-07,
@ -7078,7 +7096,9 @@
"supports_web_search": true,
"supports_none_reasoning_effort": true,
"supports_xhigh_reasoning_effort": true,
"supports_minimal_reasoning_effort": false
"supports_minimal_reasoning_effort": false,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"azure/eu/gpt-5.6-terra": {
"cache_read_input_token_cost": 2.2e-07,
@ -7126,7 +7146,9 @@
"supports_web_search": true,
"supports_none_reasoning_effort": true,
"supports_xhigh_reasoning_effort": true,
"supports_minimal_reasoning_effort": false
"supports_minimal_reasoning_effort": false,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"azure/eu/gpt-5.6-luna": {
"cache_read_input_token_cost": 2.2e-08,
@ -7174,7 +7196,9 @@
"supports_web_search": true,
"supports_none_reasoning_effort": true,
"supports_xhigh_reasoning_effort": true,
"supports_minimal_reasoning_effort": false
"supports_minimal_reasoning_effort": false,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"azure/gpt-5.5": {
"deprecation_date": "2027-10-26",
@ -26405,7 +26429,9 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_web_search": true,
"supports_xhigh_reasoning_effort": true
"supports_xhigh_reasoning_effort": true,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"gpt-5.6-sol": {
"cache_creation_input_token_cost": 5e-06,
@ -26469,7 +26495,9 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_web_search": true,
"supports_xhigh_reasoning_effort": true
"supports_xhigh_reasoning_effort": true,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"gpt-5.6-terra": {
"cache_creation_input_token_cost": 2.5e-06,
@ -26532,7 +26560,9 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_web_search": true,
"supports_xhigh_reasoning_effort": true
"supports_xhigh_reasoning_effort": true,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"gpt-5.6-luna": {
"cache_creation_input_token_cost": 2.5e-07,
@ -26595,7 +26625,9 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_web_search": true,
"supports_xhigh_reasoning_effort": true
"supports_xhigh_reasoning_effort": true,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"gpt-5.6-cyber": {
"cache_creation_input_token_cost": 1.5625e-05,
@ -49028,7 +49060,9 @@
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
"supports_vision": true,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"bedrock_mantle/openai.gpt-5.6-terra": {
"input_cost_per_token": 2.2e-06,
@ -49060,7 +49094,9 @@
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
"supports_vision": true,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"bedrock_mantle/openai.gpt-5.6-luna": {
"input_cost_per_token": 2.2e-07,
@ -49092,7 +49128,9 @@
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
"supports_vision": true,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"us.openai.gpt-5.6-sol": {
"input_cost_per_token": 5.5e-06,

View file

@ -167,6 +167,10 @@ from litellm.router_utils.pre_call_checks.model_rate_limit_check import (
from litellm.router_utils.pre_call_checks.prompt_caching_deployment_check import (
PromptCachingDeploymentCheck,
)
from litellm.router_utils.reasoning_effort_capability import (
intersect_supported_reasoning_efforts,
resolve_supported_reasoning_efforts,
)
from litellm.router_utils.router_callbacks.track_deployment_metrics import (
increment_deployment_failures_for_current_minute,
increment_deployment_successes_for_current_minute,
@ -9558,6 +9562,11 @@ class Router:
if model_info.get("rpm", None) is not None and _deployment_rpm is None:
_deployment_rpm = model_info.get("rpm")
model_group_info.supported_reasoning_efforts = intersect_supported_reasoning_efforts(
model_group_info.supported_reasoning_efforts,
resolve_supported_reasoning_efforts(model_info),
)
if _deployment_tpm is not None:
if total_tpm is None:
total_tpm = 0

View file

@ -0,0 +1,53 @@
"""Resolve which reasoning_effort values a deployment, and by intersection a model group, accepts.
The model-map flags carry different polarity per level, mirroring the provider gates
(gpt_5_transformation.py restricts xhigh to explicit opt-in and treats minimal/low as opt-out;
anthropic/chat/transformation.py rejects only xhigh/max without an explicit flag): medium and high
are unconditional for any reasoning model, none/minimal/low are supported unless the map explicitly
says false, and xhigh/max require an explicit true. Shipping the resolved list keeps that polarity
in one place instead of re-encoding it in every consumer.
"""
from collections.abc import Mapping, Sequence
from typing import Final
REASONING_EFFORT_CAPABILITY_ORDER: Final = ("none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra")
_OPT_OUT_FLAGS: Final = (
("none", "supports_none_reasoning_effort"),
("minimal", "supports_minimal_reasoning_effort"),
("low", "supports_low_reasoning_effort"),
)
_OPT_IN_FLAGS: Final = (
("xhigh", "supports_xhigh_reasoning_effort"),
("max", "supports_max_reasoning_effort"),
("ultra", "supports_ultra_reasoning_effort"),
)
_UNCONDITIONAL_EFFORTS: Final = frozenset(("medium", "high"))
def resolve_supported_reasoning_efforts(model_info: Mapping[str, object]) -> tuple[str, ...] | None:
"""None = no capability metadata for this deployment (e.g. a model absent from the model map,
whose stub info carries no supports_reasoning key at all); () = reasoning unsupported."""
if "supports_reasoning" not in model_info:
return None
if model_info.get("supports_reasoning") is not True:
return ()
opt_out: Final = frozenset(effort for effort, flag in _OPT_OUT_FLAGS if model_info.get(flag) is not False)
opt_in: Final = frozenset(effort for effort, flag in _OPT_IN_FLAGS if model_info.get(flag) is True)
allowed: Final = opt_out | _UNCONDITIONAL_EFFORTS | opt_in
return tuple(effort for effort in REASONING_EFFORT_CAPABILITY_ORDER if effort in allowed)
def intersect_supported_reasoning_efforts(
current: Sequence[str] | None,
resolved: Sequence[str] | None,
) -> tuple[str, ...] | None:
"""Deployments without metadata (None) never narrow the group; an effort survives only when
every deployment with metadata accepts it, so the group offers nothing routing could reject."""
if resolved is None:
return tuple(current) if current is not None else None
if current is None:
return tuple(resolved)
keep: Final = frozenset(current) & frozenset(resolved)
return tuple(effort for effort in REASONING_EFFORT_CAPABILITY_ORDER if effort in keep)

View file

@ -1840,7 +1840,7 @@ ResponsesAPIStreamingResponse = Annotated[
]
REASONING_EFFORT = Literal["none", "minimal", "low", "medium", "high", "xhigh"]
REASONING_EFFORT = Literal["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"]
class OpenAIRealtimeStreamSession(TypedDict, total=False):

View file

@ -637,6 +637,7 @@ class ModelGroupInfo(BaseModel):
supports_url_context: bool = Field(default=False)
supports_reasoning: bool = Field(default=False)
supports_function_calling: bool = Field(default=False)
supported_reasoning_efforts: tuple[str, ...] | None = Field(default=None)
supported_openai_params: list[str] | None = Field(default=[])
configurable_clientside_auth_params: CONFIGURABLE_CLIENTSIDE_AUTH_PARAMS = None

View file

@ -164,6 +164,7 @@ class ProviderSpecificModelInfo(TypedDict, total=False):
supports_low_reasoning_effort: bool | None
supports_xhigh_reasoning_effort: bool | None
supports_max_reasoning_effort: bool | None
supports_ultra_reasoning_effort: bool | None # writable-ok: Pydantic warns on ReadOnly TypedDict fields
supports_output_config: bool | None
supports_image_size: bool | None
bedrock_output_config_effort_ceiling: Literal["low", "medium", "high", "max", "xhigh"] | None

View file

@ -5764,6 +5764,7 @@ def _get_model_info_helper(
supports_low_reasoning_effort=_model_info.get("supports_low_reasoning_effort", None),
supports_xhigh_reasoning_effort=_model_info.get("supports_xhigh_reasoning_effort", None),
supports_max_reasoning_effort=_model_info.get("supports_max_reasoning_effort", None),
supports_ultra_reasoning_effort=_model_info.get("supports_ultra_reasoning_effort", None),
bedrock_output_config_effort_ceiling=_model_info.get("bedrock_output_config_effort_ceiling", None),
bedrock_converse_supports_strict_tools=_model_info.get("bedrock_converse_supports_strict_tools", None),
supports_computer_use=_model_info.get("supports_computer_use", None),

View file

@ -6639,7 +6639,9 @@
"supports_web_search": true,
"supports_none_reasoning_effort": true,
"supports_xhigh_reasoning_effort": true,
"supports_minimal_reasoning_effort": false
"supports_minimal_reasoning_effort": false,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"azure/gpt-5.6-sol": {
"cache_read_input_token_cost": 5e-07,
@ -6690,7 +6692,9 @@
"supports_web_search": true,
"supports_none_reasoning_effort": true,
"supports_xhigh_reasoning_effort": true,
"supports_minimal_reasoning_effort": false
"supports_minimal_reasoning_effort": false,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"azure/gpt-5.6-terra": {
"cache_read_input_token_cost": 2e-07,
@ -6741,7 +6745,9 @@
"supports_web_search": true,
"supports_none_reasoning_effort": true,
"supports_xhigh_reasoning_effort": true,
"supports_minimal_reasoning_effort": false
"supports_minimal_reasoning_effort": false,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"azure/gpt-5.6-luna": {
"cache_read_input_token_cost": 2e-08,
@ -6792,7 +6798,9 @@
"supports_web_search": true,
"supports_none_reasoning_effort": true,
"supports_xhigh_reasoning_effort": true,
"supports_minimal_reasoning_effort": false
"supports_minimal_reasoning_effort": false,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"azure/us/gpt-5.6": {
"cache_read_input_token_cost": 5.5e-07,
@ -6839,7 +6847,9 @@
"supports_web_search": true,
"supports_none_reasoning_effort": true,
"supports_xhigh_reasoning_effort": true,
"supports_minimal_reasoning_effort": false
"supports_minimal_reasoning_effort": false,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"azure/us/gpt-5.6-sol": {
"cache_read_input_token_cost": 5.5e-07,
@ -6887,7 +6897,9 @@
"supports_web_search": true,
"supports_none_reasoning_effort": true,
"supports_xhigh_reasoning_effort": true,
"supports_minimal_reasoning_effort": false
"supports_minimal_reasoning_effort": false,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"azure/us/gpt-5.6-terra": {
"cache_read_input_token_cost": 2.2e-07,
@ -6935,7 +6947,9 @@
"supports_web_search": true,
"supports_none_reasoning_effort": true,
"supports_xhigh_reasoning_effort": true,
"supports_minimal_reasoning_effort": false
"supports_minimal_reasoning_effort": false,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"azure/us/gpt-5.6-luna": {
"cache_read_input_token_cost": 2.2e-08,
@ -6983,7 +6997,9 @@
"supports_web_search": true,
"supports_none_reasoning_effort": true,
"supports_xhigh_reasoning_effort": true,
"supports_minimal_reasoning_effort": false
"supports_minimal_reasoning_effort": false,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"azure/eu/gpt-5.6": {
"cache_read_input_token_cost": 5.5e-07,
@ -7030,7 +7046,9 @@
"supports_web_search": true,
"supports_none_reasoning_effort": true,
"supports_xhigh_reasoning_effort": true,
"supports_minimal_reasoning_effort": false
"supports_minimal_reasoning_effort": false,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"azure/eu/gpt-5.6-sol": {
"cache_read_input_token_cost": 5.5e-07,
@ -7078,7 +7096,9 @@
"supports_web_search": true,
"supports_none_reasoning_effort": true,
"supports_xhigh_reasoning_effort": true,
"supports_minimal_reasoning_effort": false
"supports_minimal_reasoning_effort": false,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"azure/eu/gpt-5.6-terra": {
"cache_read_input_token_cost": 2.2e-07,
@ -7126,7 +7146,9 @@
"supports_web_search": true,
"supports_none_reasoning_effort": true,
"supports_xhigh_reasoning_effort": true,
"supports_minimal_reasoning_effort": false
"supports_minimal_reasoning_effort": false,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"azure/eu/gpt-5.6-luna": {
"cache_read_input_token_cost": 2.2e-08,
@ -7174,7 +7196,9 @@
"supports_web_search": true,
"supports_none_reasoning_effort": true,
"supports_xhigh_reasoning_effort": true,
"supports_minimal_reasoning_effort": false
"supports_minimal_reasoning_effort": false,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"azure/gpt-5.5": {
"deprecation_date": "2027-10-26",
@ -26405,7 +26429,9 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_web_search": true,
"supports_xhigh_reasoning_effort": true
"supports_xhigh_reasoning_effort": true,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"gpt-5.6-sol": {
"cache_creation_input_token_cost": 5e-06,
@ -26469,7 +26495,9 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_web_search": true,
"supports_xhigh_reasoning_effort": true
"supports_xhigh_reasoning_effort": true,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"gpt-5.6-terra": {
"cache_creation_input_token_cost": 2.5e-06,
@ -26532,7 +26560,9 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_web_search": true,
"supports_xhigh_reasoning_effort": true
"supports_xhigh_reasoning_effort": true,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"gpt-5.6-luna": {
"cache_creation_input_token_cost": 2.5e-07,
@ -26595,7 +26625,9 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_web_search": true,
"supports_xhigh_reasoning_effort": true
"supports_xhigh_reasoning_effort": true,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"gpt-5.6-cyber": {
"cache_creation_input_token_cost": 1.5625e-05,
@ -49028,7 +49060,9 @@
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
"supports_vision": true,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"bedrock_mantle/openai.gpt-5.6-terra": {
"input_cost_per_token": 2.2e-06,
@ -49060,7 +49094,9 @@
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
"supports_vision": true,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"bedrock_mantle/openai.gpt-5.6-luna": {
"input_cost_per_token": 2.2e-07,
@ -49092,7 +49128,9 @@
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
"supports_vision": true,
"supports_max_reasoning_effort": true,
"supports_ultra_reasoning_effort": true
},
"us.openai.gpt-5.6-sol": {
"input_cost_per_token": 5.5e-06,

View file

@ -702,6 +702,9 @@
"supports_tool_search": {
"type": "boolean"
},
"supports_ultra_reasoning_effort": {
"type": "boolean"
},
"supports_url_context": {
"type": "boolean"
},

View file

@ -1585,10 +1585,15 @@ def test_map_reasoning_effort_adds_summary_detailed(monkeypatch):
assert result_dict["summary"] == "custom_summary"
print("✓ Dict input is passed through without modification")
# Test 5: None/unknown values return None
result_unknown = handler._map_reasoning_effort("unknown_value")
assert result_unknown is None
print("✓ Unknown reasoning_effort values return None")
# Test 5: levels this bridge does not enumerate (max, ultra, future ones) pass through so the
# provider can judge them, instead of being silently dropped before the request is built
from litellm.types.llms.openai import Reasoning
for effort in ("max", "ultra", "unknown_value"):
result_passthrough = handler._map_reasoning_effort(effort)
assert result_passthrough == Reasoning(effort=effort)
assert handler._map_reasoning_effort("") is None
print("✓ Unenumerated reasoning_effort levels pass through to the provider")
print(
"✓ All reasoning_effort behaviors work correctly with flag/env var control"

View file

@ -1309,3 +1309,36 @@ def test_responses_gpt54_allow_temperature_effort_none(
drop_params=False,
)
assert params["temperature"] == 0.7
@pytest.mark.parametrize("effort", ["max", "ultra"])
def test_gpt5_6_allows_opt_in_reasoning_efforts(config: OpenAIConfig, effort: str):
params = config.map_openai_params(
non_default_params={"reasoning_effort": effort},
optional_params={},
model="gpt-5.6",
drop_params=False,
)
assert params["reasoning_effort"] == effort
@pytest.mark.parametrize("effort", ["max", "ultra"])
def test_gpt5_rejects_opt_in_reasoning_efforts_for_other_models(config: OpenAIConfig, effort: str):
with pytest.raises(litellm.utils.UnsupportedParamsError):
config.map_openai_params(
non_default_params={"reasoning_effort": effort},
optional_params={},
model="gpt-5.1",
drop_params=False,
)
@pytest.mark.parametrize("effort", ["max", "ultra"])
def test_gpt5_drops_opt_in_reasoning_efforts_when_requested(config: OpenAIConfig, effort: str):
params = config.map_openai_params(
non_default_params={"reasoning_effort": effort},
optional_params={},
model="gpt-5.1",
drop_params=True,
)
assert "reasoning_effort" not in params

View file

@ -1352,7 +1352,9 @@ class TestParseCursorModelVariant:
("gemini-3.0-pro-thinking-low", "gemini-3.0-pro", "low"),
("claude-opus-5-fast", "claude-opus-5", None),
("gpt-5.6-sol", "gpt-5.6-sol", None),
("foo-thinking-ultra-fast", "foo-thinking-ultra", None),
("gpt-5.6-thinking-ultra-fast", "gpt-5.6", "ultra"),
("gpt-5.6-thinking-max", "gpt-5.6", "max"),
("foo-thinking-mega-fast", "foo-thinking-mega", None),
("-thinking-high", "-thinking-high", None),
],
)

View file

@ -0,0 +1,79 @@
from litellm.router_utils.reasoning_effort_capability import (
intersect_supported_reasoning_efforts,
resolve_supported_reasoning_efforts,
)
class TestResolveSupportedReasoningEfforts:
def test_no_metadata_resolves_to_unknown(self):
assert resolve_supported_reasoning_efforts({}) is None
def test_non_reasoning_model_supports_no_efforts(self):
assert resolve_supported_reasoning_efforts({"supports_reasoning": None}) == ()
assert resolve_supported_reasoning_efforts({"supports_reasoning": False}) == ()
def test_reasoning_model_with_no_flags_gets_the_opt_out_levels_only(self):
# The kimi shape: supports_reasoning true, zero effort flags. medium/high are unconditional,
# none/minimal/low are opt-out so absence means supported, xhigh/max are opt-in so absence
# means unsupported.
assert resolve_supported_reasoning_efforts({"supports_reasoning": True}) == (
"none",
"minimal",
"low",
"medium",
"high",
)
def test_explicit_false_removes_an_opt_out_level(self):
# The gpt-5.5-pro shape from the model map: only medium/high/xhigh are accepted upstream.
resolved = resolve_supported_reasoning_efforts(
{
"supports_reasoning": True,
"supports_none_reasoning_effort": False,
"supports_minimal_reasoning_effort": False,
"supports_low_reasoning_effort": False,
"supports_xhigh_reasoning_effort": True,
}
)
assert resolved == ("medium", "high", "xhigh")
def test_explicit_true_adds_the_opt_in_levels(self):
# The claude-opus shape: xhigh and max explicitly true, everything else absent.
resolved = resolve_supported_reasoning_efforts(
{
"supports_reasoning": True,
"supports_xhigh_reasoning_effort": True,
"supports_max_reasoning_effort": True,
}
)
assert resolved == ("none", "minimal", "low", "medium", "high", "xhigh", "max")
def test_ultra_is_opt_in(self):
without_flag = resolve_supported_reasoning_efforts({"supports_reasoning": True})
with_flag = resolve_supported_reasoning_efforts(
{"supports_reasoning": True, "supports_ultra_reasoning_effort": True}
)
assert without_flag is not None and "ultra" not in without_flag
assert with_flag is not None and with_flag[-1] == "ultra"
def test_opt_in_flag_set_false_stays_excluded(self):
resolved = resolve_supported_reasoning_efforts(
{"supports_reasoning": True, "supports_xhigh_reasoning_effort": False}
)
assert resolved is not None
assert "xhigh" not in resolved
class TestIntersectSupportedReasoningEfforts:
def test_unknown_never_narrows(self):
assert intersect_supported_reasoning_efforts(["medium", "high"], None) == ("medium", "high")
assert intersect_supported_reasoning_efforts(None, ["medium", "high"]) == ("medium", "high")
assert intersect_supported_reasoning_efforts(None, None) is None
def test_intersection_keeps_canonical_order(self):
assert intersect_supported_reasoning_efforts(
["max", "high", "medium", "xhigh"], ["xhigh", "medium", "minimal"]
) == ("medium", "xhigh")
def test_disjoint_sets_intersect_to_empty(self):
assert intersect_supported_reasoning_efforts(["max"], ["minimal"]) == ()

View file

@ -8878,3 +8878,87 @@ class TestAzureBaseModelFallbackLogging:
deployment=None, received_model_name="my-group", id="azure-base-model-test-id"
)
assert model_info["max_input_tokens"] == litellm.model_cost["azure/gpt-4o-mini"]["max_input_tokens"]
def test_model_group_info_intersects_supported_reasoning_efforts():
router = litellm.Router(
model_list=[
{
"model_name": "smart-group",
"litellm_params": {"model": "anthropic/opus-like"},
"model_info": {"id": "opus-like-deployment"},
},
{
"model_name": "smart-group",
"litellm_params": {"model": "openai/mini-like"},
"model_info": {"id": "mini-like-deployment"},
},
]
)
def _model_info(model_id: str, model_name: str):
if model_id == "opus-like-deployment":
return {
"key": model_name,
"litellm_provider": "anthropic",
"mode": "chat",
"supports_reasoning": True,
"supports_xhigh_reasoning_effort": True,
"supports_max_reasoning_effort": True,
}
return {
"key": model_name,
"litellm_provider": "openai",
"mode": "chat",
"supports_reasoning": True,
"supports_none_reasoning_effort": False,
"supports_minimal_reasoning_effort": True,
"supports_xhigh_reasoning_effort": False,
}
with patch.object(router, "get_deployment_model_info", side_effect=_model_info):
result = router._set_model_group_info(
model_group="smart-group",
user_facing_model_group_name="smart-group",
)
assert result is not None
# opus-like offers all seven levels, mini-like lacks none/xhigh/max; only the common set survives,
# so the group never advertises an effort routing could hand to a deployment that rejects it.
assert result.supported_reasoning_efforts == ("minimal", "low", "medium", "high")
def test_model_group_info_reasoning_efforts_ignore_deployments_without_metadata():
router = litellm.Router(
model_list=[
{
"model_name": "smart-group",
"litellm_params": {"model": "anthropic/opus-like"},
"model_info": {"id": "opus-like-deployment"},
},
{
"model_name": "smart-group",
"litellm_params": {"model": "openai/unmapped-model"},
"model_info": {"id": "unmapped-deployment"},
},
]
)
def _model_info(model_id: str, model_name: str):
if model_id == "opus-like-deployment":
return {
"key": model_name,
"litellm_provider": "anthropic",
"mode": "chat",
"supports_reasoning": True,
"supports_max_reasoning_effort": True,
}
return {"key": model_name, "litellm_provider": "openai", "mode": "chat"}
with patch.object(router, "get_deployment_model_info", side_effect=_model_info):
result = router._set_model_group_info(
model_group="smart-group",
user_facing_model_group_name="smart-group",
)
assert result is not None
assert result.supported_reasoning_efforts == ("none", "minimal", "low", "medium", "high", "max")

View file

@ -997,6 +997,7 @@ def test_aaamodel_prices_and_context_window_json_is_valid():
"supports_none_reasoning_effort": {"type": "boolean"},
"supports_xhigh_reasoning_effort": {"type": "boolean"},
"supports_max_reasoning_effort": {"type": "boolean"},
"supports_ultra_reasoning_effort": {"type": "boolean"},
"supports_adaptive_thinking": {"type": "boolean"},
"supports_legacy_thinking": {"type": "boolean"},
"thinking_always_on": {"type": "boolean"},

View file

@ -8,7 +8,12 @@ vi.mock(
);
const mockModelInfo = [
{ model_group: "gpt-4", mode: "chat", supports_reasoning: true },
{
model_group: "gpt-4",
mode: "chat",
supports_reasoning: true,
supported_reasoning_efforts: ["medium", "high", "xhigh"],
},
{ model_group: "gpt-3.5-turbo", mode: "chat" },
{ model_group: "claude-3-opus", mode: "chat", supports_reasoning: true },
{ model_group: "text-embedding-3-small", mode: "embedding" },
@ -943,3 +948,37 @@ describe("ComplexityRouterConfig reasoning effort gating", () => {
).toHaveTextContent("low");
});
});
describe("ComplexityRouterConfig per-model effort filtering", () => {
it("offers only the efforts the model group supports", async () => {
renderWithProviders(<ComplexityRouterConfig {...baseProps} />);
const user = userEvent.setup();
await user.click(screen.getByRole("combobox", { name: "Reasoning effort for gpt-4 in the Complex tier" }));
const options = (await screen.findAllByRole("option")).map((option) => option.textContent);
expect(options).toEqual(["Default", "medium", "high", "xhigh"]);
});
it("falls back to every effort when the group only reports supports_reasoning", async () => {
renderWithProviders(<ComplexityRouterConfig {...baseProps} />);
const user = userEvent.setup();
await user.click(
screen.getByRole("combobox", { name: "Reasoning effort for claude-3-opus in the Reasoning tier" }),
);
const options = (await screen.findAllByRole("option")).map((option) => option.textContent);
expect(options).toEqual(["Default", "none", "minimal", "low", "medium", "high", "xhigh"]);
});
// Hand-authored configs can carry a level outside the supported set (e.g. max); it must render
// and stay clearable rather than being masked as Default.
it("keeps showing a stored effort outside the supported set", () => {
renderWithProviders(
<ComplexityRouterConfig
{...baseProps}
value={{ ...defaultValue, tier_model_params: { COMPLEX: { "gpt-4": { reasoning_effort: "max" } } } }}
/>,
);
expect(screen.getByRole("combobox", { name: "Reasoning effort for gpt-4 in the Complex tier" })).toHaveTextContent(
"max",
);
});
});

View file

@ -13,6 +13,7 @@ import { ModelGroup } from "@/components/llm_calls/fetch_models";
import AdaptiveRoutingConfig from "./AdaptiveRoutingConfig";
import ClassificationMethodConfig from "./ClassificationMethodConfig";
import {
REASONING_EFFORT_OPTIONS,
ReasoningEffort,
TierModelParamsByTier,
pruneTierModelParams,
@ -251,8 +252,13 @@ const ComplexityRouterConfig: React.FC<ComplexityRouterConfigProps> = ({
const defaultModel = resolveComplexityDefaultModel(value.tiers, value.default_model);
// Embedding models can't serve a chat-completion role, so they're excluded here.
const reasoningModels = new Set(
modelInfo.filter((model) => model.supports_reasoning).map((model) => model.model_group),
// The backend list is the per-group intersection of accepted effort levels; when a proxy does not
// send it yet, fall back to the coarse supports_reasoning gate with every level offered.
const effortOptionsByModel: Record<string, string[]> = Object.fromEntries(
modelInfo.map((model) => [
model.model_group,
model.supported_reasoning_efforts ?? (model.supports_reasoning ? [...REASONING_EFFORT_OPTIONS] : []),
]),
);
const modelOptions = modelInfo
@ -365,7 +371,7 @@ const ComplexityRouterConfig: React.FC<ComplexityRouterConfigProps> = ({
<TierModelEffortRows
tierLabel={label}
models={value.tiers[tier]}
reasoningModels={reasoningModels}
effortOptionsByModel={effortOptionsByModel}
paramsByModel={value.tier_model_params?.[tier]}
onEffortChange={(model, effort) => handleTierModelEffortChange(tier, model, effort)}
/>

View file

@ -2,20 +2,19 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { SimpleTooltip } from "@/components/ui/tooltip";
import { Info } from "lucide-react";
import React from "react";
import { REASONING_EFFORT_OPTIONS, ReasoningEffort, TierModelParams } from "./complexity_router_tiers";
import { ReasoningEffort, TierModelParams } from "./complexity_router_tiers";
const PROVIDER_DEFAULT = "__provider_default__";
const asEffort = (params: TierModelParams | undefined): ReasoningEffort | undefined => {
const storedEffort = (params: TierModelParams | undefined): ReasoningEffort | undefined => {
const stored = params?.reasoning_effort;
if (typeof stored !== "string") return undefined;
return REASONING_EFFORT_OPTIONS.find((option) => option === stored);
return typeof stored === "string" && stored ? stored : undefined;
};
interface TierModelEffortRowsProps {
tierLabel: string;
models: string[];
reasoningModels: ReadonlySet<string>;
effortOptionsByModel: Record<string, string[]>;
paramsByModel: Record<string, TierModelParams> | undefined;
onEffortChange: (model: string, effort: ReasoningEffort | undefined) => void;
}
@ -23,14 +22,21 @@ interface TierModelEffortRowsProps {
const TierModelEffortRows: React.FC<TierModelEffortRowsProps> = ({
tierLabel,
models,
reasoningModels,
effortOptionsByModel,
paramsByModel,
onEffortChange,
}) => {
const shown = models.filter(
(model) => reasoningModels.has(model) || Object.keys(paramsByModel?.[model] ?? {}).length > 0,
);
if (shown.length === 0) return null;
const rows = models
.map((model) => {
const effort = storedEffort(paramsByModel?.[model]);
const supported = effortOptionsByModel[model] ?? [];
// A stored effort outside the supported set (hand-authored, or capabilities changed since it
// was saved) stays listed so it renders and can be cleared.
const options = effort !== undefined && !supported.includes(effort) ? [...supported, effort] : supported;
return { model, effort, options };
})
.filter(({ model, options }) => options.length > 0 || Object.keys(paramsByModel?.[model] ?? {}).length > 0);
if (rows.length === 0) return null;
return (
<div className="mt-2 space-y-1">
<div className="flex items-center gap-1">
@ -41,18 +47,17 @@ const TierModelEffortRows: React.FC<TierModelEffortRowsProps> = ({
<Info className="size-3 text-muted-foreground/70" />
</SimpleTooltip>
</div>
{shown.map((model) => (
{rows.map(({ model, effort, options }) => (
<div key={model} className="flex items-center justify-between gap-2">
<span className="truncate text-xs">{model}</span>
<Select
items={[
{ value: PROVIDER_DEFAULT, label: "Default" },
...REASONING_EFFORT_OPTIONS.map((option) => ({ value: option, label: option })),
...options.map((option) => ({ value: option, label: option })),
]}
value={asEffort(paramsByModel?.[model]) ?? PROVIDER_DEFAULT}
value={effort ?? PROVIDER_DEFAULT}
onValueChange={(selected: string | null) =>
selected !== null &&
onEffortChange(model, selected === PROVIDER_DEFAULT ? undefined : (selected as ReasoningEffort))
selected !== null && onEffortChange(model, selected === PROVIDER_DEFAULT ? undefined : selected)
}
>
<SelectTrigger
@ -64,7 +69,7 @@ const TierModelEffortRows: React.FC<TierModelEffortRowsProps> = ({
</SelectTrigger>
<SelectContent>
<SelectItem value={PROVIDER_DEFAULT}>Default</SelectItem>
{REASONING_EFFORT_OPTIONS.map((option) => (
{options.map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>

View file

@ -5,8 +5,18 @@ export type TierModelParams = Record<string, unknown>;
export type TierModelParamsByTier = Record<string, Record<string, TierModelParams>>;
/**
* Fallback offered only when a proxy does not report supported_reasoning_efforts per model group.
* max is deliberately absent: it is opt-in per model in the model map, so a capability-blind list
* must not offer it; a proxy that reports capabilities supplies max itself where supported.
*/
export const REASONING_EFFORT_OPTIONS = ["none", "minimal", "low", "medium", "high", "xhigh"] as const;
export type ReasoningEffort = (typeof REASONING_EFFORT_OPTIONS)[number];
/**
* Open on purpose: the valid set is per model group at runtime (supported_reasoning_efforts), and
* hand-authored configs can carry any level, so the known literals only add autocompletion.
*/
export type ReasoningEffort = (typeof REASONING_EFFORT_OPTIONS)[number] | (string & {});
const asRecord = (raw: unknown): Record<string, unknown> | undefined =>
typeof raw === "object" && raw !== null && !Array.isArray(raw) ? (raw as Record<string, unknown>) : undefined;

View file

@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { modelAvailableCall } from "@/components/networking";
import { modelAvailableCall, modelHubCall } from "@/components/networking";
import { fetchAvailableModelsForTeam } from "./fetch_models";
vi.mock("@/components/networking", () => ({
@ -31,3 +31,38 @@ describe("fetchAvailableModelsForTeam", () => {
expect(await fetchAvailableModelsForTeam("token", "team-123")).toEqual([]);
});
});
describe("fetchAvailableModelsForTeam capability join", () => {
it("carries model-group capabilities onto team-allowed names that have a group entry", async () => {
modelAvailableCallMock.mockResolvedValue({ data: [{ id: "gpt-5-mini" }, { id: "team-only-byok" }] });
vi.mocked(modelHubCall).mockResolvedValue({
data: [
{
model_group: "gpt-5-mini",
mode: "chat",
supports_reasoning: true,
supported_reasoning_efforts: ["minimal", "low", "medium", "high"],
},
],
});
const models = await fetchAvailableModelsForTeam("token", "team-123");
expect(models).toEqual([
{
model_group: "gpt-5-mini",
mode: "chat",
supports_reasoning: true,
supported_reasoning_efforts: ["minimal", "low", "medium", "high"],
},
{ model_group: "team-only-byok" },
]);
});
it("keeps the team list usable when the group-info fetch fails", async () => {
modelAvailableCallMock.mockResolvedValue({ data: [{ id: "gpt-5-mini" }] });
vi.mocked(modelHubCall).mockRejectedValue(new Error("boom"));
expect(await fetchAvailableModelsForTeam("token", "team-123")).toEqual([{ model_group: "gpt-5-mini" }]);
});
});

View file

@ -7,6 +7,7 @@ export interface ModelGroup {
model_group: string;
mode?: string;
supports_reasoning?: boolean;
supported_reasoning_efforts?: string[];
}
interface AvailableModel {
@ -15,15 +16,36 @@ interface AvailableModel {
id?: string | null;
mode?: string | null;
supports_reasoning?: boolean | null;
supported_reasoning_efforts?: string[] | null;
}
const toModelGroup = (item: AvailableModel): ModelGroup => {
const groupName = (item.model_group || item.id || item.model_name) ?? "";
return {
model_group: groupName,
...(item.mode && { mode: item.mode }),
...(item.supports_reasoning === true && { supports_reasoning: true }),
...(item.supported_reasoning_efforts && { supported_reasoning_efforts: item.supported_reasoning_efforts }),
};
};
/**
* /models carries no capability metadata, so the team-allowed names are joined against
* /model_group/info (one extra parallel request; the endpoint has no team filter of its own, and it
* serves team-scoped tokens row-filtered, verified live). A name without a group entry keeps every
* capability field absent; a failed group fetch is console.error'd by fetchAvailableModels.
*/
export const fetchAvailableModelsForTeam = async (accessToken: string, teamId: string): Promise<ModelGroup[]> => {
const response = await modelAvailableCall(accessToken, "", "", false, teamId);
const [response, groups] = await Promise.all([
modelAvailableCall(accessToken, "", "", false, teamId),
fetchAvailableModels(accessToken).catch(() => [] as ModelGroup[]),
]);
const byGroup = new Map(groups.map((group) => [group.model_group, group]));
const modelNames: string[] = (response?.data ?? []).map((model: { id: string }) => model.id);
return excludeProxyWideSentinel(Array.from(new Set(modelNames)))
.sort((a, b) => a.localeCompare(b))
.map((model) => ({ model_group: model }));
.map((model) => byGroup.get(model) ?? { model_group: model });
};
/**
@ -32,20 +54,11 @@ export const fetchAvailableModelsForTeam = async (accessToken: string, teamId: s
export const fetchAvailableModels = async (accessToken: string): Promise<ModelGroup[]> => {
try {
const fetchedModels = await modelHubCall(accessToken);
if (fetchedModels?.data.length > 0) {
const models: ModelGroup[] = fetchedModels.data
.map((item: AvailableModel) => ({
model_group: item.model_group || item.id || item.model_name || "",
mode: item.mode || undefined,
supports_reasoning: item.supports_reasoning === true || undefined,
}))
.filter((model: ModelGroup) => model.model_group !== "");
models.sort((a, b) => a.model_group.localeCompare(b.model_group));
return Array.from(new Map(models.map((model) => [model.model_group, model])).values());
}
return [];
const models: ModelGroup[] = (fetchedModels?.data ?? [])
.map(toModelGroup)
.filter((model: ModelGroup) => model.model_group !== "")
.sort((a: ModelGroup, b: ModelGroup) => a.model_group.localeCompare(b.model_group));
return Array.from(new Map(models.map((model) => [model.model_group, model])).values());
} catch (error) {
console.error("Error fetching model info:", error);
throw error;

View file

@ -29526,6 +29526,8 @@ export interface components {
* @default []
*/
supported_openai_params: string[] | null;
/** Supported Reasoning Efforts */
supported_reasoning_efforts?: string[] | null;
/**
* Supports Function Calling
* @default false