feat(model_prices): let a map entry declare its exact reasoning_effort levels (#38481)

Kimi K3 accepts exactly low, high and max, defaults to max, and always thinks.
The map could not say that: medium and high have no supports_*_reasoning_effort
flag because every other reasoning model takes them, so the ten kimi-k3 entries
carried supports_reasoning alone and resolved to unknown. The dashboard then fell
back to a capability-blind level list that deliberately omits max, which is why a
kimi-k3 tier cannot be set to max thinking today.

Add reasoning_effort_levels, an array key in the shape the map already uses for
supported_endpoints and supported_modalities. Where present it is read first and
wins whole; every other entry keeps answering through the per-level flags,
unchanged. It is deliberately a different name from the computed
ModelGroupInfo.supported_reasoning_efforts, which stays derived from a group's
deployments and is never seeded from one deployment's model_info.

The levels are per entry rather than per model, because the deployments differ:
Moonshot, Together, Fireworks and Azure Foundry all forward the level unchanged
and get the model's own low/high/max, while Perplexity documents a six-value
enum it maps down internally and gets that. The /v1/messages degradation chain
consults the same declaration, so the level the map advertises is the level that
path forwards.
This commit is contained in:
tin-berri 2026-08-27 15:38:01 -07:00 committed by GitHub
parent 3c41392893
commit 30ff3723b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 435 additions and 5 deletions

View file

@ -73,6 +73,11 @@ ARRAY_KEYS: dict[str, JsonSchema] = {
"description": "Output modalities the model can produce.",
"items": {"type": "string", "enum": ["text", "image", "audio", "video", "code"]},
},
"reasoning_effort_levels": {
"type": "array",
"description": "Exact reasoning_effort levels this deployment accepts; wins over supports_* flags.",
"items": {"type": "string", "enum": ["none", "minimal", "low", "medium", "high", "xhigh", "max"]},
},
"supported_regions": {
"type": "array",
"description": "Cloud regions the model is available in ('global' or region ids).",

View file

@ -1,4 +1,6 @@
import os
from collections.abc import Mapping
from types import MappingProxyType
from typing import Final
import litellm
@ -23,6 +25,29 @@ def is_reasoning_auto_summary_enabled() -> bool:
return litellm.reasoning_auto_summary or os.getenv("LITELLM_REASONING_AUTO_SUMMARY", "false").lower() == "true"
_DECLARED_DEGRADATION_CHAINS: Final[Mapping[str, tuple[str, ...]]] = MappingProxyType(
{"max": ("max", "xhigh", "high"), "xhigh": ("xhigh", "high"), "minimal": ("minimal", "low")}
)
def _effort_from_declaration(model_info: ModelInfo, effort: str) -> str | None:
"""A declared level set is the WHOLE answer for this gate, so a level it omits degrades even
where a per-level flag would have allowed it. Honoring both would let /model_group/info and
this path disagree about the same entry. None means the entry declares nothing, and the flag
chain below decides as before.
A declaration that omits every level in a chain still lands on that chain's terminal, which can
itself be undeclared. Picking a nearer declared level instead would need a strength ordering,
and the advertisement order is presentation only by design, so the terminal stays the answer."""
from litellm.router_utils.reasoning_effort_capability import declared_reasoning_efforts
declared: Final = declared_reasoning_efforts(model_info)
if declared is None:
return None
chain: Final = _DECLARED_DEGRADATION_CHAINS[effort]
return next((level for level in chain if level in declared), chain[-1])
def normalize_reasoning_effort_value(
effort: str,
model: str,
@ -48,6 +73,10 @@ def normalize_reasoning_effort_value(
except Exception:
model_info = None
declared_effort: Final = _effort_from_declaration(model_info, effort) if model_info is not None else None
if declared_effort is not None:
return declared_effort
if effort == "max":
if model_info and model_info.get("supports_max_reasoning_effort"):
return "max"

View file

@ -9315,6 +9315,11 @@
"max_tokens": 131072,
"mode": "chat",
"output_cost_per_token": 1.65e-05,
"reasoning_effort_levels": [
"low",
"high",
"max"
],
"source": "https://techcommunity.microsoft.com/blog/azure-ai-foundry-blog/introducing-kimi-k3-through-fireworks-ai-on-microsoft-foundry/4540187",
"supported_modalities": [
"text",
@ -31897,6 +31902,11 @@
"max_tokens": 1048576,
"mode": "chat",
"output_cost_per_token": 1.5e-05,
"reasoning_effort_levels": [
"low",
"high",
"max"
],
"source": "https://platform.kimi.ai/docs/pricing/chat-k3",
"supports_function_calling": true,
"supports_reasoning": true,
@ -36505,6 +36515,14 @@
"litellm_provider": "perplexity",
"mode": "responses",
"output_cost_per_token": 1.5e-05,
"reasoning_effort_levels": [
"minimal",
"low",
"medium",
"high",
"xhigh",
"max"
],
"source": "https://docs.perplexity.ai/docs/agent-api/models",
"supports_web_search": true,
"supports_reasoning": true,
@ -38986,6 +39004,11 @@
"max_tokens": 1048576,
"mode": "chat",
"output_cost_per_token": 1.5e-05,
"reasoning_effort_levels": [
"low",
"high",
"max"
],
"source": "https://docs.together.ai/docs/serverless-models",
"supports_function_calling": true,
"supports_parallel_function_calling": true,
@ -51774,6 +51797,11 @@
"max_tokens": 131072,
"mode": "chat",
"output_cost_per_token": 1.5e-05,
"reasoning_effort_levels": [
"low",
"high",
"max"
],
"source": "https://docs.fireworks.ai/serverless/pricing",
"supports_function_calling": true,
"supports_reasoning": true,
@ -51838,6 +51866,11 @@
"max_tokens": 131072,
"mode": "chat",
"output_cost_per_token": 1.5e-05,
"reasoning_effort_levels": [
"low",
"high",
"max"
],
"source": "https://docs.fireworks.ai/serverless/pricing",
"supports_function_calling": true,
"supports_reasoning": true,
@ -51854,6 +51887,11 @@
"max_tokens": 131072,
"mode": "chat",
"output_cost_per_token": 2.25e-05,
"reasoning_effort_levels": [
"low",
"high",
"max"
],
"source": "https://docs.fireworks.ai/serverless/pricing",
"supports_function_calling": true,
"supports_reasoning": true,
@ -51870,6 +51908,11 @@
"max_tokens": 131072,
"mode": "chat",
"output_cost_per_token": 1.65e-05,
"reasoning_effort_levels": [
"low",
"high",
"max"
],
"source": "https://docs.fireworks.ai/serverless/pricing",
"supports_function_calling": true,
"supports_reasoning": true,
@ -52042,6 +52085,11 @@
"max_tokens": 131072,
"mode": "chat",
"output_cost_per_token": 2.25e-05,
"reasoning_effort_levels": [
"low",
"high",
"max"
],
"source": "https://docs.fireworks.ai/serverless/pricing",
"supports_function_calling": true,
"supports_reasoning": true,
@ -52058,6 +52106,11 @@
"max_tokens": 131072,
"mode": "chat",
"output_cost_per_token": 1.65e-05,
"reasoning_effort_levels": [
"low",
"high",
"max"
],
"source": "https://docs.fireworks.ai/serverless/pricing",
"supports_function_calling": true,
"supports_reasoning": true,

View file

@ -1,10 +1,13 @@
"""Resolve which reasoning_effort values a deployment, and by intersection a model group, accepts.
The model map's supports_*_reasoning_effort flags are the only signal, and each level's polarity
mirrors how a request path reads that same flag. medium and high are unconditional for a reasoning
model. minimal and low are opt-out: openai/chat/gpt_5_transformation.py refuses them only when the
map says false. xhigh and max are opt-in. none is opt-out everywhere except the azure gpt-5 family,
whose config raises UnsupportedParamsError without an explicit true.
An entry that states its levels outright in reasoning_effort_levels is read first and wins
whole, for a model whose set the per-level flags cannot express: Kimi K3 takes low, high and max,
and no flag can drop medium because medium has none. Every other entry answers through the
supports_*_reasoning_effort flags below, whose polarity mirrors how a request path reads that same
flag. medium and high are unconditional for a reasoning model. minimal and low are opt-out:
openai/chat/gpt_5_transformation.py refuses them only when the map says false. xhigh and max are
opt-in. none is opt-out everywhere except the azure gpt-5 family, whose config raises
UnsupportedParamsError without an explicit true.
xhigh is gated on the request path by the openai and azure gpt-5 configs. max is not gated there at
all: every entry carrying supports_max_reasoning_effort is Claude-family, and
@ -41,6 +44,7 @@ _EFFORT_FLAGS: Final = (
("xhigh", "supports_xhigh_reasoning_effort"),
("max", "supports_max_reasoning_effort"),
)
_DECLARED_EFFORTS_KEY: Final = "reasoning_effort_levels"
_OPT_OUT_EFFORTS: Final = ("minimal", "low")
_OPT_IN_EFFORTS: Final = ("xhigh", "max")
_UNCONDITIONAL_EFFORTS: Final = frozenset(("medium", "high"))
@ -69,6 +73,20 @@ def _declared_effort_flags(model_info: Mapping[str, object]) -> Mapping[str, obj
)
def declared_reasoning_efforts(model_info: Mapping[str, object]) -> tuple[str, ...] | None:
"""The entry's own answer, read through the same bare twin as the flags so both spellings of one
model agree. Present-and-a-list IS the answer, so a declared [] correctly empties the group and
an unknown level is dropped rather than raised: the bundled map is enum-validated by
validate-model-prices-json, but an operator can put this key on a config.yaml model_info block
where that schema never runs, and one mistyped level must not fail every sibling on the proxy."""
own: Final = model_info.get(_DECLARED_EFFORTS_KEY)
raw: Final = own if own is not None else _bare_model_entry(model_info).get(_DECLARED_EFFORTS_KEY)
if not isinstance(raw, Sequence) or isinstance(raw, (str, bytes)):
return None
declared: Final = frozenset(effort for effort in raw if isinstance(effort, str))
return tuple(effort for effort in REASONING_EFFORT_ADVERTISEMENT_ORDER if effort in declared)
def _supports_none_reasoning_effort(model_info: Mapping[str, object], flag: object) -> bool:
"""Opt-in only where a request path refuses the level. AzureOpenAIGPT5Config raises
UnsupportedParamsError on reasoning_effort='none' without an explicit true, and it is selected
@ -119,6 +137,10 @@ def resolve_supported_reasoning_efforts(
if supports_reasoning is not True:
return () if supports_reasoning is False or deployment_is_mapped else None
declared: Final = declared_reasoning_efforts(model_info)
if declared is not None:
return declared
flags: Final = _declared_effort_flags(model_info)
if all(value is None for value in flags.values()):
return 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
reasoning_effort_levels: ReadOnly[Sequence[str] | None]
supports_output_config: bool | None
supports_image_size: bool | None
bedrock_output_config_effort_ceiling: Literal["low", "medium", "high", "max", "xhigh"] | None

View file

@ -5889,6 +5889,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),
reasoning_effort_levels=_model_info.get("reasoning_effort_levels", 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

@ -9315,6 +9315,11 @@
"max_tokens": 131072,
"mode": "chat",
"output_cost_per_token": 1.65e-05,
"reasoning_effort_levels": [
"low",
"high",
"max"
],
"source": "https://techcommunity.microsoft.com/blog/azure-ai-foundry-blog/introducing-kimi-k3-through-fireworks-ai-on-microsoft-foundry/4540187",
"supported_modalities": [
"text",
@ -31897,6 +31902,11 @@
"max_tokens": 1048576,
"mode": "chat",
"output_cost_per_token": 1.5e-05,
"reasoning_effort_levels": [
"low",
"high",
"max"
],
"source": "https://platform.kimi.ai/docs/pricing/chat-k3",
"supports_function_calling": true,
"supports_reasoning": true,
@ -36505,6 +36515,14 @@
"litellm_provider": "perplexity",
"mode": "responses",
"output_cost_per_token": 1.5e-05,
"reasoning_effort_levels": [
"minimal",
"low",
"medium",
"high",
"xhigh",
"max"
],
"source": "https://docs.perplexity.ai/docs/agent-api/models",
"supports_web_search": true,
"supports_reasoning": true,
@ -38986,6 +39004,11 @@
"max_tokens": 1048576,
"mode": "chat",
"output_cost_per_token": 1.5e-05,
"reasoning_effort_levels": [
"low",
"high",
"max"
],
"source": "https://docs.together.ai/docs/serverless-models",
"supports_function_calling": true,
"supports_parallel_function_calling": true,
@ -51774,6 +51797,11 @@
"max_tokens": 131072,
"mode": "chat",
"output_cost_per_token": 1.5e-05,
"reasoning_effort_levels": [
"low",
"high",
"max"
],
"source": "https://docs.fireworks.ai/serverless/pricing",
"supports_function_calling": true,
"supports_reasoning": true,
@ -51838,6 +51866,11 @@
"max_tokens": 131072,
"mode": "chat",
"output_cost_per_token": 1.5e-05,
"reasoning_effort_levels": [
"low",
"high",
"max"
],
"source": "https://docs.fireworks.ai/serverless/pricing",
"supports_function_calling": true,
"supports_reasoning": true,
@ -51854,6 +51887,11 @@
"max_tokens": 131072,
"mode": "chat",
"output_cost_per_token": 2.25e-05,
"reasoning_effort_levels": [
"low",
"high",
"max"
],
"source": "https://docs.fireworks.ai/serverless/pricing",
"supports_function_calling": true,
"supports_reasoning": true,
@ -51870,6 +51908,11 @@
"max_tokens": 131072,
"mode": "chat",
"output_cost_per_token": 1.65e-05,
"reasoning_effort_levels": [
"low",
"high",
"max"
],
"source": "https://docs.fireworks.ai/serverless/pricing",
"supports_function_calling": true,
"supports_reasoning": true,
@ -52042,6 +52085,11 @@
"max_tokens": 131072,
"mode": "chat",
"output_cost_per_token": 2.25e-05,
"reasoning_effort_levels": [
"low",
"high",
"max"
],
"source": "https://docs.fireworks.ai/serverless/pricing",
"supports_function_calling": true,
"supports_reasoning": true,
@ -52058,6 +52106,11 @@
"max_tokens": 131072,
"mode": "chat",
"output_cost_per_token": 1.65e-05,
"reasoning_effort_levels": [
"low",
"high",
"max"
],
"source": "https://docs.fireworks.ai/serverless/pricing",
"supports_function_calling": true,
"supports_reasoning": true,

View file

@ -532,6 +532,22 @@
"type": "object",
"description": "Provider-internal routing hints (e.g. bedrock_invocation_schema)."
},
"reasoning_effort_levels": {
"type": "array",
"description": "Exact reasoning_effort levels this deployment accepts; wins over supports_* flags.",
"items": {
"type": "string",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh",
"max"
]
}
},
"regional_endpoint_uplift_multiplier": {
"type": "number",
"minimum": 1,

View file

@ -14,6 +14,8 @@ from unittest.mock import patch
import pytest
import litellm
from litellm.llms.anthropic.experimental_pass_through.utils import (
normalize_reasoning_effort_value,
)
@ -291,3 +293,91 @@ class TestAdapterAdaptiveThinking:
)
assert result is not None
assert result["effort"] == "medium"
class TestDeclaredEffortsAnswerTheDegradationGate:
"""Without this the chain reads only the per-level booleans, so a kimi-k3 request asking for
max silently arrives as high."""
@pytest.mark.parametrize(
"model, provider",
[("kimi-k3", "moonshot"), ("kimi-k3", "fireworks_ai"), ("kimi-k3-us", "fireworks_ai")],
)
def test_a_declared_level_survives_instead_of_degrading(self, local_model_cost_map, model, provider):
assert normalize_reasoning_effort_value("max", model, provider) == "max"
def test_a_level_the_entry_does_not_declare_still_degrades(self, local_model_cost_map):
"""xhigh is not on kimi-k3's declaration, so it must keep degrading rather than be waved
past by the mere presence of one."""
assert normalize_reasoning_effort_value("xhigh", "kimi-k3", "moonshot") == "high"
assert normalize_reasoning_effort_value("minimal", "kimi-k3", "moonshot") == "low"
def test_the_wider_perplexity_entry_keeps_the_levels_it_declares(self, local_model_cost_map):
assert normalize_reasoning_effort_value("xhigh", "perplexity/kimi-k3", "perplexity") == "xhigh"
assert normalize_reasoning_effort_value("minimal", "perplexity/kimi-k3", "perplexity") == "minimal"
@pytest.mark.parametrize(
"model, provider, effort, expected",
[
("claude-opus-4-7", "anthropic", "max", "max"),
("claude-sonnet-4-6", "anthropic", "minimal", "low"),
("gpt-5-mini", "azure", "max", "high"),
],
)
def test_an_entry_on_the_per_level_flags_is_untouched(
self, local_model_cost_map, model, provider, effort, expected
):
"""The negative class that bounds this change to entries carrying the key."""
assert normalize_reasoning_effort_value(effort, model, provider) == expected
class TestDeclarationBeatsThePerLevelFlags:
"""An entry can carry both shapes. The declaration wins whole, or /model_group/info and this
path would disagree about the same deployment. Driven through the public entry point over a
seeded map entry rather than a patched get_model_info, so it pins behaviour and not wiring."""
MODEL = "declared-and-flagged"
@pytest.fixture
def seeded(self, local_model_cost_map, monkeypatch):
def _seed(**entry):
monkeypatch.setitem(
litellm.model_cost,
self.MODEL,
{"litellm_provider": "openai", "mode": "chat", "supports_reasoning": True, **entry},
)
litellm.get_model_info.cache_clear()
return _seed
@pytest.mark.parametrize("effort, expected", [("max", "max"), ("xhigh", "high"), ("minimal", "low")])
def test_a_flag_cannot_re_add_a_level_the_declaration_omits(self, seeded, effort, expected):
seeded(
reasoning_effort_levels=["low", "high", "max"],
supports_xhigh_reasoning_effort=True,
supports_minimal_reasoning_effort=True,
supports_max_reasoning_effort=False,
)
assert normalize_reasoning_effort_value(effort, self.MODEL, "openai") == expected
def test_a_flag_cannot_keep_max_when_the_declaration_drops_it(self, seeded):
seeded(
reasoning_effort_levels=["low", "high"],
supports_max_reasoning_effort=True,
supports_xhigh_reasoning_effort=True,
)
assert normalize_reasoning_effort_value("max", self.MODEL, "openai") == "high"
def test_a_false_flag_cannot_remove_a_level_the_declaration_names(self, seeded):
seeded(reasoning_effort_levels=["high", "xhigh"], supports_xhigh_reasoning_effort=False)
assert normalize_reasoning_effort_value("xhigh", self.MODEL, "openai") == "xhigh"
assert normalize_reasoning_effort_value("max", self.MODEL, "openai") == "xhigh"
def test_a_chain_the_declaration_omits_entirely_lands_on_its_terminal(self, seeded):
"""Documented residual: no strength ordering exists to pick a nearer declared level."""
seeded(reasoning_effort_levels=["high", "xhigh"])
assert normalize_reasoning_effort_value("minimal", self.MODEL, "openai") == "low"

View file

@ -1,5 +1,6 @@
import pytest
import litellm
from litellm.router_utils.reasoning_effort_capability import (
deployment_is_catalog_mapped,
intersect_supported_reasoning_efforts,
@ -196,3 +197,158 @@ class TestIntersectSupportedReasoningEfforts:
def test_disjoint_sets_intersect_to_empty(self):
assert intersect_supported_reasoning_efforts(["max"], ["minimal"]) == ()
class TestDeclaredEffortList:
"""reasoning_effort_levels is what the catalog DECLARES per deployment;
ModelGroupInfo.supported_reasoning_efforts is what a group COMPUTED. test_router.py pins that
the computed one is never seeded from model_info, so the two names must stay apart."""
def test_a_declared_list_answers_where_no_flag_could(self):
"""No flag can drop medium, so before this key the entry could only stay silent or
over-advertise a level the model does not document."""
resolved = resolve_supported_reasoning_efforts(
{"supports_reasoning": True, "reasoning_effort_levels": ["low", "high", "max"]},
deployment_is_mapped=True,
)
assert resolved == ("low", "high", "max")
def test_a_declared_list_wins_whole_over_the_flags(self):
resolved = resolve_supported_reasoning_efforts(
{
"supports_reasoning": True,
"reasoning_effort_levels": ["low", "high", "max"],
"supports_none_reasoning_effort": True,
"supports_minimal_reasoning_effort": True,
"supports_xhigh_reasoning_effort": True,
"supports_max_reasoning_effort": False,
},
deployment_is_mapped=True,
)
assert resolved == ("low", "high", "max")
def test_a_declaration_is_reordered_into_the_advertisement_order(self):
resolved = resolve_supported_reasoning_efforts(
{"supports_reasoning": True, "reasoning_effort_levels": ["max", "low", "high"]},
deployment_is_mapped=True,
)
assert resolved == ("low", "high", "max")
def test_a_declared_empty_list_empties_the_group(self):
assert (
resolve_supported_reasoning_efforts(
{"supports_reasoning": True, "reasoning_effort_levels": []},
deployment_is_mapped=True,
)
== ()
)
@pytest.mark.parametrize("declared", [["low", "bogus"], ["bogus"], ["low", 7, None]])
def test_an_unknown_level_is_dropped_rather_than_raised(self, declared):
"""A config.yaml model_info block bypasses the map's enum schema, and one mistyped level
must not fail every sibling on the proxy."""
resolved = resolve_supported_reasoning_efforts(
{"supports_reasoning": True, "reasoning_effort_levels": declared},
deployment_is_mapped=True,
)
assert resolved == tuple(effort for effort in ("low",) if effort in declared)
@pytest.mark.parametrize("malformed", ["low,high,max", {"low": True}, 3, True])
def test_a_malformed_declaration_falls_through_to_the_flags(self, malformed):
resolved = resolve_supported_reasoning_efforts(
{
"supports_reasoning": True,
"reasoning_effort_levels": malformed,
"supports_max_reasoning_effort": True,
},
deployment_is_mapped=True,
)
assert resolved == ("none", "minimal", "low", "medium", "high", "max")
def test_a_model_the_map_calls_non_reasoning_ignores_its_declaration(self):
assert (
resolve_supported_reasoning_efforts(
{"supports_reasoning": False, "reasoning_effort_levels": ["low", "high", "max"]},
deployment_is_mapped=True,
)
== ()
)
def test_a_declaration_is_read_through_the_bare_twin(self, monkeypatch):
monkeypatch.setitem(
litellm.model_cost,
"some-declared-reasoner",
{"supports_reasoning": True, "reasoning_effort_levels": ["low", "max"]},
)
resolved = resolve_supported_reasoning_efforts(
{
"supports_reasoning": True,
"litellm_provider": "openai",
"key": "openai/some-declared-reasoner",
},
deployment_is_mapped=True,
)
assert resolved == ("low", "max")
KIMI_K3_PASSTHROUGH_KEYS = (
"azure_ai/FW-Kimi-K3",
"moonshot/kimi-k3",
"together_ai/moonshotai/Kimi-K3",
"fireworks_ai/kimi-k3",
"fireworks_ai/kimi-k3-fast",
"fireworks_ai/kimi-k3-us",
"fireworks_ai/accounts/fireworks/models/kimi-k3",
"fireworks_ai/accounts/fireworks/routers/kimi-k3-fast",
"fireworks_ai/accounts/fireworks/routers/kimi-k3-us",
)
KIMI_K3_PERPLEXITY_KEY = "perplexity/perplexity/kimi-k3"
class TestKimiK3AdvertisesItsDocumentedLevels:
@pytest.mark.parametrize("model_key", KIMI_K3_PASSTHROUGH_KEYS)
def test_a_passthrough_entry_advertises_the_models_own_levels(self, local_model_cost_map, model_key):
"""platform.kimi.ai documents exactly low, high and max, and these providers forward the
level unchanged. Undeclared, each entry resolves to unknown and the dashboard falls back to
a capability-blind list that omits max."""
entry = dict(litellm.model_cost[model_key], key=model_key)
assert resolve_supported_reasoning_efforts(entry, deployment_is_mapped=True) == ("low", "high", "max")
def test_the_perplexity_entry_advertises_the_wider_set_it_maps_down(self, local_model_cost_map):
"""Perplexity's Agent API takes a six-value enum and maps it down internally, so this
deployment is legitimately wider than a passthrough. One blanket list could not say both."""
entry = dict(litellm.model_cost[KIMI_K3_PERPLEXITY_KEY], key=KIMI_K3_PERPLEXITY_KEY)
assert resolve_supported_reasoning_efforts(entry, deployment_is_mapped=True) == (
"minimal",
"low",
"medium",
"high",
"xhigh",
"max",
)
@pytest.mark.parametrize("model, provider", [("kimi-k3", "moonshot"), ("kimi-k3", "fireworks_ai")])
def test_the_declaration_survives_model_info_hydration(self, local_model_cost_map, model, provider):
"""The hydration line is the load-bearing seam: without it the key the map carries never
reaches the resolver and reads as absent everywhere downstream."""
from litellm.utils import _get_model_info_helper
model_info = dict(_get_model_info_helper(model=model, custom_llm_provider=provider))
assert model_info["reasoning_effort_levels"] == ["low", "high", "max"]
assert resolve_supported_reasoning_efforts(model_info, deployment_is_mapped=True) == ("low", "high", "max")
def test_a_kimi_k3_deployment_now_narrows_a_mixed_group(self, local_model_cost_map):
"""kimi used to contribute unknown, which never narrows, so the group advertised whatever
its other deployments agreed on."""
kimi = resolve_supported_reasoning_efforts(
dict(litellm.model_cost["fireworks_ai/kimi-k3"], key="fireworks_ai/kimi-k3"),
deployment_is_mapped=True,
)
assert intersect_supported_reasoning_efforts(("none", "minimal", "low", "medium", "high", "xhigh"), kimi) == (
"low",
"high",
)

View file

@ -1014,6 +1014,10 @@ 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"},
"reasoning_effort_levels": {
"type": "array",
"items": {"type": "string", "enum": ["none", "minimal", "low", "medium", "high", "xhigh", "max"]},
},
"supports_adaptive_thinking": {"type": "boolean"},
"supports_legacy_thinking": {"type": "boolean"},
"thinking_always_on": {"type": "boolean"},