mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
feat(router): per-group supported reasoning efforts with max and ultra levels
This commit is contained in:
parent
d0da90ee6d
commit
0e96491554
25 changed files with 551 additions and 102 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
53
litellm/router_utils/reasoning_effort_capability.py
Normal file
53
litellm/router_utils/reasoning_effort_capability.py
Normal 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)
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -702,6 +702,9 @@
|
|||
"supports_tool_search": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"supports_ultra_reasoning_effort": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"supports_url_context": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"]) == ()
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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" }]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
2
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
2
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue