mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-16 23:41:43 +00:00
Merge pull request #41093 from BerriAI/litellm_fallback_backfill_opt_in_main
feat(model_info): provider-scoped fill_missing_for_providers backfill from fallback generalization rules
This commit is contained in:
commit
c6e4c5582d
5 changed files with 298 additions and 13 deletions
|
|
@ -34,6 +34,11 @@ rules never mix the two and never use ``extends``. A rule whose
|
|||
Rules are only consulted after exact and case-insensitive lookups miss, so an
|
||||
exact cost-map entry always takes precedence over any rule.
|
||||
|
||||
Rules flagged with ``fill_missing_for_providers: [..]`` also fill only keys
|
||||
missing from an exact cost-map entry when the entry's ``litellm_provider`` is
|
||||
listed, while values already present on the entry win on conflict. Only flagged
|
||||
capability rules participate in this fill; routing rules never do.
|
||||
|
||||
Patterns are matched case-insensitively with ``re.search`` and are not implicitly
|
||||
anchored: a rule must include ``^`` and ``$`` to bind to the whole model name,
|
||||
otherwise it matches as a substring. Keeping anchoring in the regex makes the rule
|
||||
|
|
@ -46,17 +51,19 @@ Rules are compiled and classified once, at install time. The match functions are
|
|||
O(number of rules); callers must only invoke them on a cache miss.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from typing import Final
|
||||
|
||||
from litellm._logging import verbose_logger
|
||||
|
||||
verbose_logger: Final = logging.getLogger("LiteLLM")
|
||||
NAME_FIELD: Final = "name"
|
||||
PATTERN_FIELD: Final = "pattern"
|
||||
MODEL_INFO_FIELD: Final = "model_info"
|
||||
PROVIDER_KEY: Final = "litellm_provider"
|
||||
LEGACY_EXTENDS_FIELD: Final = "extends"
|
||||
FILL_MISSING_FOR_PROVIDERS_FIELD: Final = "fill_missing_for_providers"
|
||||
|
||||
|
||||
def _resolve_legacy_extends(rules: list) -> list:
|
||||
|
|
@ -98,11 +105,28 @@ class _RoutingRule:
|
|||
class _CapabilityRule:
|
||||
pattern: re.Pattern
|
||||
model_info: dict
|
||||
fill_missing_for_providers: frozenset[str]
|
||||
|
||||
|
||||
_CompiledRule = _RoutingRule | _CapabilityRule
|
||||
|
||||
|
||||
def _parse_fill_missing_for_providers(rule: Mapping[str, object], pattern_label: object) -> frozenset[str] | None:
|
||||
if FILL_MISSING_FOR_PROVIDERS_FIELD not in rule:
|
||||
return frozenset()
|
||||
raw_fill_missing_for_providers: Final = rule.get(FILL_MISSING_FOR_PROVIDERS_FIELD)
|
||||
if not isinstance(raw_fill_missing_for_providers, (list, tuple)) or not all(
|
||||
isinstance(provider, str) for provider in raw_fill_missing_for_providers
|
||||
):
|
||||
verbose_logger.warning(
|
||||
"LiteLLM: skipping malformed fallback generalization rule %s ('%s' must be a list of provider strings).",
|
||||
rule.get(NAME_FIELD, pattern_label),
|
||||
FILL_MISSING_FOR_PROVIDERS_FIELD,
|
||||
)
|
||||
return None
|
||||
return frozenset(raw_fill_missing_for_providers)
|
||||
|
||||
|
||||
def _compile_rule(rule: object) -> tuple[_CompiledRule, ...]:
|
||||
if not isinstance(rule, dict):
|
||||
return ()
|
||||
|
|
@ -125,8 +149,17 @@ def _compile_rule(rule: object) -> tuple[_CompiledRule, ...]:
|
|||
e,
|
||||
)
|
||||
return ()
|
||||
fill_missing_for_providers: Final = _parse_fill_missing_for_providers(rule, pattern)
|
||||
if fill_missing_for_providers is None:
|
||||
return ()
|
||||
if PROVIDER_KEY not in model_info:
|
||||
return (_CapabilityRule(pattern=compiled, model_info=model_info),)
|
||||
return (
|
||||
_CapabilityRule(
|
||||
pattern=compiled,
|
||||
model_info=model_info,
|
||||
fill_missing_for_providers=fill_missing_for_providers,
|
||||
),
|
||||
)
|
||||
provider: Final = model_info[PROVIDER_KEY]
|
||||
if not isinstance(provider, str):
|
||||
verbose_logger.warning(
|
||||
|
|
@ -140,7 +173,11 @@ def _compile_rule(rule: object) -> tuple[_CompiledRule, ...]:
|
|||
return (_RoutingRule(pattern=compiled, provider=provider),)
|
||||
return (
|
||||
_RoutingRule(pattern=compiled, provider=provider),
|
||||
_CapabilityRule(pattern=compiled, model_info=model_info),
|
||||
_CapabilityRule(
|
||||
pattern=compiled,
|
||||
model_info=model_info,
|
||||
fill_missing_for_providers=fill_missing_for_providers,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -151,6 +188,7 @@ class _FallbackGeneralizations:
|
|||
self.rules: list = []
|
||||
self.routing_rules: tuple = ()
|
||||
self.capability_rules: tuple = ()
|
||||
self.fill_missing_rules: tuple[_CapabilityRule, ...] = ()
|
||||
|
||||
def set_rules(self, rules: list | None) -> None:
|
||||
installed: Final = rules if isinstance(rules, list) else []
|
||||
|
|
@ -158,6 +196,7 @@ class _FallbackGeneralizations:
|
|||
self.rules = installed
|
||||
self.routing_rules = tuple(rule for rule in compiled if isinstance(rule, _RoutingRule))
|
||||
self.capability_rules = tuple(rule for rule in compiled if isinstance(rule, _CapabilityRule))
|
||||
self.fill_missing_rules = tuple(rule for rule in self.capability_rules if rule.fill_missing_for_providers)
|
||||
|
||||
def match_routing(self, model: str) -> str | None:
|
||||
if not model:
|
||||
|
|
@ -175,6 +214,21 @@ class _FallbackGeneralizations:
|
|||
return None
|
||||
return {key: value for model_info in matched for key, value in model_info.items()}
|
||||
|
||||
def match_fill_missing(self, model: str, provider: str) -> Mapping[str, object] | None:
|
||||
if not model or not provider:
|
||||
return None
|
||||
matched = tuple(
|
||||
rule.model_info
|
||||
for rule in self.fill_missing_rules
|
||||
if provider in rule.fill_missing_for_providers and rule.pattern.search(model) is not None
|
||||
)
|
||||
if not matched:
|
||||
return None
|
||||
fill_missing: Final[Mapping[str, object]] = {
|
||||
key: value for model_info in matched for key, value in model_info.items() if key != PROVIDER_KEY
|
||||
}
|
||||
return fill_missing or None
|
||||
|
||||
|
||||
_registry: Final = _FallbackGeneralizations()
|
||||
|
||||
|
|
@ -210,3 +264,14 @@ def match_capability_generalizations(model: str) -> dict | None:
|
|||
capability rule matches. O(number of rules); only call once exact lookups have missed.
|
||||
"""
|
||||
return _registry.match_capabilities(model)
|
||||
|
||||
|
||||
def match_fill_missing_generalizations(model: str, provider: str) -> Mapping[str, object] | None:
|
||||
"""Return flagged capability rules matching ``model`` for ``provider``.
|
||||
|
||||
Later rules override earlier ones on key conflicts. Only rules listing
|
||||
``provider`` in ``fill_missing_for_providers`` contribute. Returns ``None``
|
||||
when no flagged rule matches. O(number of rules); only call once exact
|
||||
lookups have matched.
|
||||
"""
|
||||
return _registry.match_fill_missing(model, provider)
|
||||
|
|
|
|||
|
|
@ -57716,8 +57716,9 @@
|
|||
},
|
||||
{
|
||||
"name": "claude-adaptive-thinking",
|
||||
"pattern": "claude-[a-z]+-(?:4[-._](?:[6-9]|[1-9]\\d)(?!\\d)|(?:[5-9]|[1-9]\\d)(?!\\d)(?:[-._]\\d{1,2}(?!\\d))?)",
|
||||
"description": "Claude at version 4.6 or higher, in any id shape that contains claude-<family>-: minors 4.6 through 4.99, any later major-minor, and bare 5+ majors so a new family shaped like claude-fable-5 matches. Requiring the claude- prefix keeps non-Claude names such as team-sonnet-5-1 out. The minor is capped at two digits so an 8-digit date suffix such as claude-opus-4-20250514 is never read as a >= 4.6 minor. Turns on adaptive thinking for new versions and new families with no code change.",
|
||||
"pattern": "claude-[a-z]+-(?:4[-._](?:[6-9]|[1-9]\\d)(?!\\d)|[5-9](?!\\d)(?:[-._]\\d{1,2}(?!\\d))?)",
|
||||
"fill_missing_for_providers": ["anthropic", "azure_ai", "bedrock", "bedrock_converse", "vertex_ai-anthropic_models"],
|
||||
"description": "Claude at version 4.6 or higher, in any id shape that contains claude-<family>-: minors 4.6 through 4.99, any later major-minor, and bare 5+ majors so a new family shaped like claude-fable-5 matches. Requiring the claude- prefix keeps non-Claude names such as team-sonnet-5-1 out. The minor is capped at two digits so an 8-digit date suffix such as claude-opus-4-20250514 is never read as a >= 4.6 minor. Two-digit majors are deliberately not matched so ids like claude-opus-41 (4.1) are not read as major 41. Turns on adaptive thinking for new versions and new families with no code change.",
|
||||
"model_info": {
|
||||
"supports_adaptive_thinking": true
|
||||
}
|
||||
|
|
@ -57725,6 +57726,7 @@
|
|||
{
|
||||
"name": "claude-legacy-thinking",
|
||||
"pattern": "claude-[a-z]+-4[-._]6(?!\\d)",
|
||||
"fill_missing_for_providers": ["anthropic", "azure_ai", "bedrock", "bedrock_converse", "vertex_ai-anthropic_models"],
|
||||
"description": "Claude at version 4.6 exactly, in any id shape that contains claude-<family>-4-6 (dotted and underscored minors included, dated releases such as claude-sonnet-4-6-20260219 too). The 4.6 family is adaptive-thinking yet still accepts legacy thinking.type=enabled with budget_tokens, so the caller's hard budget cap is forwarded verbatim instead of being rewritten to an uncapped output_config.effort. The lookahead keeps two-digit minors such as 4-60 from matching. 4.7+ and 5+ majors reject the legacy shape and stay on the adaptive translation.",
|
||||
"model_info": {
|
||||
"supports_legacy_thinking": true
|
||||
|
|
@ -57740,8 +57742,9 @@
|
|||
},
|
||||
{
|
||||
"name": "claude-mid-conversation-system",
|
||||
"pattern": "claude-[a-z]+-(?:4[-._](?:[89]|[1-9]\\d)(?!\\d)|(?:[5-9]|[1-9]\\d)(?!\\d)(?:[-._]\\d{1,2}(?!\\d))?)",
|
||||
"description": "Claude at version 4.8 or higher, in any id shape that contains claude-<family>-: minors 4.8 through 4.99, any later major-minor, and bare 5+ majors so a new family like claude-fable-5 matches. Anthropic introduced mid-conversation system messages with Opus 4.8 and every newer Claude keeps them; 4.7 and below reject the system role inside messages.",
|
||||
"pattern": "claude-[a-z]+-(?:4[-._](?:[89]|[1-9]\\d)(?!\\d)|[5-9](?!\\d)(?:[-._]\\d{1,2}(?!\\d))?)",
|
||||
"fill_missing_for_providers": ["anthropic", "azure_ai", "bedrock", "bedrock_converse", "vertex_ai-anthropic_models"],
|
||||
"description": "Claude at version 4.8 or higher, in any id shape that contains claude-<family>-: minors 4.8 through 4.99, any later major-minor, and bare 5+ majors so a new family like claude-fable-5 matches. Two-digit majors are deliberately not matched so ids like claude-opus-41 (4.1) are not read as major 41. Anthropic introduced mid-conversation system messages with Opus 4.8 and every newer Claude keeps them; 4.7 and below reject the system role inside messages.",
|
||||
"model_info": {
|
||||
"supports_mid_conversation_system": true
|
||||
}
|
||||
|
|
@ -57757,6 +57760,7 @@
|
|||
{
|
||||
"name": "openai-reasoning-family-baseline",
|
||||
"pattern": "^(?!.*search-api)(?:[a-z0-9_.-]+/)*(?:ft:)?(?:o[1-9]\\d*(?![a-z0-9])|gpt-[5-9](?:\\.\\d+)?(?![0-9.])|(?:gpt-\\d+(?:\\.\\d+)?(?:-[a-z0-9]+)*-)?(?:codex|deep-research|chat-latest)(?![a-z0-9]))",
|
||||
"fill_missing_for_providers": ["azure", "azure_ai", "openai"],
|
||||
"description": "OpenAI reasoning families by id shape, under any provider namespace and with an optional ft: prefix: the o-series (o1, o3-pro, o4-mini), gpt-5 through gpt-9 majors including dotted minors and suffixed variants (gpt-5.5-cyber, gpt-6-astra), and the codex, deep-research and chat-latest lines when standalone or on a gpt base. gpt-5-search-api is excluded because it is a search-only surface. Every model here is a reasoning model, and the Responses API drops the caller's reasoning param for any mapped OpenAI model whose info lacks supports_reasoning, so an id the registry has not named yet keeps its reasoning settings instead of silently losing them. Rules lose to exact entries. Carries no mode and no pricing, so cost stays on the standard unpriced behavior.",
|
||||
"model_info": {
|
||||
"supports_reasoning": true
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ from litellm.constants import (
|
|||
from litellm.litellm_core_utils.core_helpers import normalize_drop_params
|
||||
from litellm.litellm_core_utils.fallback_generalizations import (
|
||||
match_capability_generalizations,
|
||||
match_fill_missing_generalizations,
|
||||
)
|
||||
from litellm.litellm_core_utils.sensitive_data_masker import redact_credentials_in_payload
|
||||
|
||||
|
|
@ -254,6 +255,7 @@ from litellm.types.utils import (
|
|||
)
|
||||
|
||||
_CALL_TYPE_ENUM_MAP: Final[dict] = {ct.value: ct for ct in CallTypes}
|
||||
_BACKFILL_MODES: Final = frozenset({"chat", "responses"})
|
||||
|
||||
# +-----------------------------------------------+
|
||||
# | |
|
||||
|
|
@ -5804,6 +5806,14 @@ def _get_model_info_helper(
|
|||
):
|
||||
_model_info = None
|
||||
|
||||
if _model_info is not None and key is not None and _model_info.get("mode", "chat") in _BACKFILL_MODES:
|
||||
fill_missing: Final = match_fill_missing_generalizations(key, _model_info.get("litellm_provider", ""))
|
||||
if fill_missing is not None:
|
||||
_model_info = {
|
||||
**{k: v for k, v in fill_missing.items() if k not in _model_info},
|
||||
**_model_info,
|
||||
}
|
||||
|
||||
if _model_info is None:
|
||||
generalization: Final = _get_model_info_from_generalization(
|
||||
model=model,
|
||||
|
|
|
|||
|
|
@ -57716,8 +57716,9 @@
|
|||
},
|
||||
{
|
||||
"name": "claude-adaptive-thinking",
|
||||
"pattern": "claude-[a-z]+-(?:4[-._](?:[6-9]|[1-9]\\d)(?!\\d)|(?:[5-9]|[1-9]\\d)(?!\\d)(?:[-._]\\d{1,2}(?!\\d))?)",
|
||||
"description": "Claude at version 4.6 or higher, in any id shape that contains claude-<family>-: minors 4.6 through 4.99, any later major-minor, and bare 5+ majors so a new family shaped like claude-fable-5 matches. Requiring the claude- prefix keeps non-Claude names such as team-sonnet-5-1 out. The minor is capped at two digits so an 8-digit date suffix such as claude-opus-4-20250514 is never read as a >= 4.6 minor. Turns on adaptive thinking for new versions and new families with no code change.",
|
||||
"pattern": "claude-[a-z]+-(?:4[-._](?:[6-9]|[1-9]\\d)(?!\\d)|[5-9](?!\\d)(?:[-._]\\d{1,2}(?!\\d))?)",
|
||||
"fill_missing_for_providers": ["anthropic", "azure_ai", "bedrock", "bedrock_converse", "vertex_ai-anthropic_models"],
|
||||
"description": "Claude at version 4.6 or higher, in any id shape that contains claude-<family>-: minors 4.6 through 4.99, any later major-minor, and bare 5+ majors so a new family shaped like claude-fable-5 matches. Requiring the claude- prefix keeps non-Claude names such as team-sonnet-5-1 out. The minor is capped at two digits so an 8-digit date suffix such as claude-opus-4-20250514 is never read as a >= 4.6 minor. Two-digit majors are deliberately not matched so ids like claude-opus-41 (4.1) are not read as major 41. Turns on adaptive thinking for new versions and new families with no code change.",
|
||||
"model_info": {
|
||||
"supports_adaptive_thinking": true
|
||||
}
|
||||
|
|
@ -57725,6 +57726,7 @@
|
|||
{
|
||||
"name": "claude-legacy-thinking",
|
||||
"pattern": "claude-[a-z]+-4[-._]6(?!\\d)",
|
||||
"fill_missing_for_providers": ["anthropic", "azure_ai", "bedrock", "bedrock_converse", "vertex_ai-anthropic_models"],
|
||||
"description": "Claude at version 4.6 exactly, in any id shape that contains claude-<family>-4-6 (dotted and underscored minors included, dated releases such as claude-sonnet-4-6-20260219 too). The 4.6 family is adaptive-thinking yet still accepts legacy thinking.type=enabled with budget_tokens, so the caller's hard budget cap is forwarded verbatim instead of being rewritten to an uncapped output_config.effort. The lookahead keeps two-digit minors such as 4-60 from matching. 4.7+ and 5+ majors reject the legacy shape and stay on the adaptive translation.",
|
||||
"model_info": {
|
||||
"supports_legacy_thinking": true
|
||||
|
|
@ -57740,8 +57742,9 @@
|
|||
},
|
||||
{
|
||||
"name": "claude-mid-conversation-system",
|
||||
"pattern": "claude-[a-z]+-(?:4[-._](?:[89]|[1-9]\\d)(?!\\d)|(?:[5-9]|[1-9]\\d)(?!\\d)(?:[-._]\\d{1,2}(?!\\d))?)",
|
||||
"description": "Claude at version 4.8 or higher, in any id shape that contains claude-<family>-: minors 4.8 through 4.99, any later major-minor, and bare 5+ majors so a new family like claude-fable-5 matches. Anthropic introduced mid-conversation system messages with Opus 4.8 and every newer Claude keeps them; 4.7 and below reject the system role inside messages.",
|
||||
"pattern": "claude-[a-z]+-(?:4[-._](?:[89]|[1-9]\\d)(?!\\d)|[5-9](?!\\d)(?:[-._]\\d{1,2}(?!\\d))?)",
|
||||
"fill_missing_for_providers": ["anthropic", "azure_ai", "bedrock", "bedrock_converse", "vertex_ai-anthropic_models"],
|
||||
"description": "Claude at version 4.8 or higher, in any id shape that contains claude-<family>-: minors 4.8 through 4.99, any later major-minor, and bare 5+ majors so a new family like claude-fable-5 matches. Two-digit majors are deliberately not matched so ids like claude-opus-41 (4.1) are not read as major 41. Anthropic introduced mid-conversation system messages with Opus 4.8 and every newer Claude keeps them; 4.7 and below reject the system role inside messages.",
|
||||
"model_info": {
|
||||
"supports_mid_conversation_system": true
|
||||
}
|
||||
|
|
@ -57757,6 +57760,7 @@
|
|||
{
|
||||
"name": "openai-reasoning-family-baseline",
|
||||
"pattern": "^(?!.*search-api)(?:[a-z0-9_.-]+/)*(?:ft:)?(?:o[1-9]\\d*(?![a-z0-9])|gpt-[5-9](?:\\.\\d+)?(?![0-9.])|(?:gpt-\\d+(?:\\.\\d+)?(?:-[a-z0-9]+)*-)?(?:codex|deep-research|chat-latest)(?![a-z0-9]))",
|
||||
"fill_missing_for_providers": ["azure", "azure_ai", "openai"],
|
||||
"description": "OpenAI reasoning families by id shape, under any provider namespace and with an optional ft: prefix: the o-series (o1, o3-pro, o4-mini), gpt-5 through gpt-9 majors including dotted minors and suffixed variants (gpt-5.5-cyber, gpt-6-astra), and the codex, deep-research and chat-latest lines when standalone or on a gpt base. gpt-5-search-api is excluded because it is a search-only surface. Every model here is a reasoning model, and the Responses API drops the caller's reasoning param for any mapped OpenAI model whose info lacks supports_reasoning, so an id the registry has not named yet keeps its reasoning settings instead of silently losing them. Rules lose to exact entries. Carries no mode and no pricing, so cost stays on the standard unpriced behavior.",
|
||||
"model_info": {
|
||||
"supports_reasoning": true
|
||||
|
|
|
|||
|
|
@ -11,12 +11,12 @@ import logging
|
|||
|
||||
import pytest
|
||||
|
||||
|
||||
import litellm
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.litellm_core_utils.fallback_generalizations import (
|
||||
get_fallback_generalization_rules,
|
||||
match_capability_generalizations,
|
||||
match_fill_missing_generalizations,
|
||||
match_routing_generalization,
|
||||
set_fallback_generalizations,
|
||||
)
|
||||
|
|
@ -116,6 +116,67 @@ def test_capability_union_is_last_wins_in_file_order(restore_generalizations):
|
|||
}
|
||||
|
||||
|
||||
def test_fill_missing_requires_per_rule_opt_in(restore_generalizations):
|
||||
restore_generalizations(
|
||||
[
|
||||
{"name": "base", "pattern": r"^acme-", "model_info": {"supports_reasoning": True}},
|
||||
{
|
||||
"name": "opt-in",
|
||||
"pattern": r"^acme-",
|
||||
"fill_missing_for_providers": ["openai"],
|
||||
"model_info": {"supports_vision": True},
|
||||
},
|
||||
]
|
||||
)
|
||||
assert match_fill_missing_generalizations("acme-1", "openai") == {"supports_vision": True}
|
||||
assert match_fill_missing_generalizations("acme-1", "azure") is None
|
||||
assert match_capability_generalizations("acme-1") == {
|
||||
"supports_reasoning": True,
|
||||
"supports_vision": True,
|
||||
}
|
||||
|
||||
restore_generalizations(
|
||||
[{"name": "base", "pattern": r"^acme-", "model_info": {"supports_reasoning": True}}]
|
||||
)
|
||||
assert match_fill_missing_generalizations("acme-1", "openai") is None
|
||||
|
||||
restore_generalizations(
|
||||
[
|
||||
{
|
||||
"name": "mixed",
|
||||
"pattern": r"^acme-",
|
||||
"fill_missing_for_providers": ["openai"],
|
||||
"model_info": {"litellm_provider": "openai", "supports_vision": True},
|
||||
}
|
||||
]
|
||||
)
|
||||
assert match_fill_missing_generalizations("acme-1", "openai") == {"supports_vision": True}
|
||||
|
||||
restore_generalizations(
|
||||
[
|
||||
{
|
||||
"name": "route",
|
||||
"pattern": r"^acme-",
|
||||
"fill_missing_for_providers": ["openai"],
|
||||
"model_info": {"litellm_provider": "openai"},
|
||||
}
|
||||
]
|
||||
)
|
||||
assert match_fill_missing_generalizations("acme-1", "openai") is None
|
||||
|
||||
restore_generalizations(
|
||||
[
|
||||
{
|
||||
"name": "malformed",
|
||||
"pattern": r"^acme-",
|
||||
"fill_missing_for_providers": "openai",
|
||||
"model_info": {"supports_vision": True},
|
||||
}
|
||||
]
|
||||
)
|
||||
assert match_fill_missing_generalizations("acme-1", "openai") is None
|
||||
|
||||
|
||||
def test_routing_rules_are_excluded_from_capability_results(restore_generalizations):
|
||||
restore_generalizations(
|
||||
[
|
||||
|
|
@ -299,6 +360,76 @@ def test_exact_entry_takes_precedence_over_rule(restore_generalizations):
|
|||
assert info["input_cost_per_token"] != 999.0
|
||||
|
||||
|
||||
def test_exact_entries_fill_only_missing_fields(restore_generalizations, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
litellm,
|
||||
"model_cost",
|
||||
{
|
||||
**litellm.model_cost,
|
||||
"acme-full": {
|
||||
"input_cost_per_token": 1e-6,
|
||||
"output_cost_per_token": 2e-6,
|
||||
"litellm_provider": "openai",
|
||||
"mode": "chat",
|
||||
"max_tokens": 7,
|
||||
"supports_reasoning": False,
|
||||
},
|
||||
"acme-bare": {
|
||||
"input_cost_per_token": 3e-6,
|
||||
"output_cost_per_token": 4e-6,
|
||||
"litellm_provider": "openai",
|
||||
"mode": "chat",
|
||||
},
|
||||
"acme-image": {
|
||||
"input_cost_per_token": 5e-6,
|
||||
"output_cost_per_token": 6e-6,
|
||||
"litellm_provider": "openai",
|
||||
"mode": "image_generation",
|
||||
},
|
||||
"acme-other": {
|
||||
"input_cost_per_token": 7e-6,
|
||||
"output_cost_per_token": 8e-6,
|
||||
"litellm_provider": "openrouter",
|
||||
"mode": "chat",
|
||||
},
|
||||
},
|
||||
)
|
||||
restore_generalizations(
|
||||
[
|
||||
{
|
||||
"name": "acme-backfill",
|
||||
"pattern": r"^acme-",
|
||||
"fill_missing_for_providers": ["openai"],
|
||||
"model_info": {"supports_reasoning": True, "max_tokens": 5},
|
||||
}
|
||||
]
|
||||
)
|
||||
litellm.get_model_info.cache_clear()
|
||||
|
||||
full = litellm.get_model_info("acme-full", custom_llm_provider="openai")
|
||||
assert full["supports_reasoning"] is False
|
||||
assert full["max_tokens"] == 7
|
||||
|
||||
bare = litellm.get_model_info("acme-bare", custom_llm_provider="openai")
|
||||
assert bare["supports_reasoning"] is True
|
||||
assert bare["max_tokens"] == 5
|
||||
assert bare["input_cost_per_token"] == 3e-6
|
||||
assert bare["key"] == "acme-bare"
|
||||
|
||||
other = litellm.get_model_info("acme-other", custom_llm_provider="openrouter")
|
||||
assert other.get("supports_reasoning") is None
|
||||
|
||||
image = litellm.get_model_info("acme-image", custom_llm_provider="openai")
|
||||
assert image.get("supports_reasoning") is None
|
||||
|
||||
restore_generalizations(
|
||||
[{"name": "acme-backfill", "pattern": r"^acme-", "model_info": {"supports_reasoning": True, "max_tokens": 5}}]
|
||||
)
|
||||
litellm.get_model_info.cache_clear()
|
||||
unflagged = litellm.get_model_info("acme-bare", custom_llm_provider="openai")
|
||||
assert unflagged.get("supports_reasoning") is None
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Shipped rules (bundled cost map)
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
|
@ -415,6 +546,18 @@ def test_shipped_version_boundaries(shipped_cost_map, model, provider, adaptive,
|
|||
assert info.get("supports_mid_conversation_system") is mid_conversation, model
|
||||
|
||||
|
||||
def test_shipped_claude_version_regex_excludes_undelimited_41(shipped_cost_map):
|
||||
unmatched = match_capability_generalizations("github_copilot/claude-opus-41")
|
||||
assert unmatched is None or "supports_adaptive_thinking" not in unmatched
|
||||
assert unmatched is None or "supports_mid_conversation_system" not in unmatched
|
||||
|
||||
for model in ("claude-opus-5", "claude-sonnet-4-8"):
|
||||
matched = match_capability_generalizations(model)
|
||||
assert matched is not None
|
||||
assert matched["supports_adaptive_thinking"] is True
|
||||
assert matched["supports_mid_conversation_system"] is True
|
||||
|
||||
|
||||
def test_shipped_rules_cover_new_families_like_fable_at_5_plus(shipped_cost_map):
|
||||
"""Both version gates accept any claude-<family>- id at major 5 or higher, bare
|
||||
major or major-minor, so a new family shaped like claude-fable-5 gets adaptive
|
||||
|
|
@ -605,6 +748,10 @@ def test_shipped_wandb_rule_loses_to_mapped_non_reasoning_entries(shipped_cost_m
|
|||
assert litellm.supports_reasoning(model=model, custom_llm_provider="wandb") is False, model
|
||||
|
||||
|
||||
def test_shipped_wandb_rule_does_not_fill_missing_mapped_entries(shipped_cost_map):
|
||||
assert match_fill_missing_generalizations("wandb/meta-llama/Llama-3.1-8B-Instruct", "wandb") is None
|
||||
|
||||
|
||||
def test_shipped_wandb_rule_is_anchored_to_the_wandb_namespace(shipped_cost_map):
|
||||
"""``^wandb/`` is anchored, so it cannot leak onto another provider's ids."""
|
||||
assert match_capability_generalizations("wandb/some-new-model") == {"supports_reasoning": True}
|
||||
|
|
@ -722,3 +869,58 @@ def test_shipped_openai_reasoning_rule_skips_non_reasoning_gpt_ids(shipped_cost_
|
|||
def test_shipped_openai_reasoning_rule_loses_to_mapped_entries(shipped_cost_map):
|
||||
assert "gpt-5-search-api" in litellm.model_cost
|
||||
assert litellm.supports_reasoning(model="gpt-5-search-api", custom_llm_provider="openai") is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model,provider,expected_supports_reasoning",
|
||||
[
|
||||
("azure/us/o1-2024-12-17", "azure", True),
|
||||
("github_copilot/gpt-5", "github_copilot", None),
|
||||
("openrouter/openai/o1", "openrouter", None),
|
||||
("perplexity/openai/gpt-5.4-mini", "perplexity", None),
|
||||
],
|
||||
)
|
||||
def test_shipped_openai_reasoning_rule_backfills_only_approved_providers(
|
||||
shipped_cost_map, model, provider, expected_supports_reasoning
|
||||
):
|
||||
assert model in litellm.model_cost
|
||||
raw_entry = litellm.model_cost[model]
|
||||
assert "supports_reasoning" not in raw_entry
|
||||
model_without_provider = model.removeprefix(f"{provider}/")
|
||||
info = litellm.get_model_info(model=model_without_provider, custom_llm_provider=provider)
|
||||
assert info.get("supports_reasoning") is expected_supports_reasoning
|
||||
assert info["input_cost_per_token"] == raw_entry.get("input_cost_per_token", 0)
|
||||
|
||||
|
||||
def test_shipped_openai_reasoning_rule_matches_only_openai(shipped_cost_map):
|
||||
assert match_fill_missing_generalizations("gpt-5.4", "openai") == {"supports_reasoning": True}
|
||||
assert match_fill_missing_generalizations("gpt-5.4", "openrouter") is None
|
||||
|
||||
|
||||
def test_shipped_openai_reasoning_rule_skips_non_text_modes(shipped_cost_map):
|
||||
model = "gemini/deep-research-pro-preview-12-2025"
|
||||
assert model in litellm.model_cost
|
||||
raw_entry = litellm.model_cost[model]
|
||||
assert "supports_reasoning" not in raw_entry
|
||||
assert raw_entry["mode"] == "image_generation"
|
||||
|
||||
info = litellm.get_model_info("deep-research-pro-preview-12-2025", custom_llm_provider="gemini")
|
||||
assert info.get("supports_reasoning") is None
|
||||
|
||||
|
||||
def test_shipped_claude_thinking_rules_backfill_only_anthropic(shipped_cost_map):
|
||||
model = "perplexity/anthropic/claude-sonnet-4-6"
|
||||
assert model in litellm.model_cost
|
||||
raw_entry = litellm.model_cost[model]
|
||||
assert "supports_adaptive_thinking" not in raw_entry
|
||||
assert "max_input_tokens" not in raw_entry
|
||||
|
||||
info = litellm.get_model_info(model="anthropic/claude-sonnet-4-6", custom_llm_provider="perplexity")
|
||||
assert info.get("supports_adaptive_thinking") is None
|
||||
assert info.get("supports_legacy_thinking") is None
|
||||
assert info.get("max_input_tokens") is None
|
||||
assert match_fill_missing_generalizations("claude-sonnet-4-6", "anthropic") == {
|
||||
"supports_adaptive_thinking": True,
|
||||
"supports_legacy_thinking": True,
|
||||
}
|
||||
assert match_fill_missing_generalizations("claude-sonnet-4-6", "perplexity") is None
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue