diff --git a/litellm/utils.py b/litellm/utils.py index fc8982f382..7c7591cdba 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -4993,6 +4993,25 @@ def _strip_model_name(model: str, custom_llm_provider: Optional[str]) -> str: return model +def _get_model_cost_key(potential_key: str) -> Optional[str]: + """ + Get the actual key from model_cost, with case-insensitive fallback. + + Returns the key if found (exact match preferred, then case-insensitive), or None if not found. + """ + # Try exact match first (most common case, O(1)) + if potential_key in litellm.model_cost: + return potential_key + + # Fallback to case-insensitive match + potential_key_lower = potential_key.lower() + for key in litellm.model_cost: + if key.lower() == potential_key_lower: + return key + + return None + + def _get_model_info_from_model_cost(key: str) -> dict: return litellm.model_cost[key] @@ -5142,10 +5161,10 @@ def _is_potential_model_name_in_model_cost( potential_model_names: PotentialModelNamesAndCustomLLMProvider, ) -> bool: """ - Check if the potential model name is in the model cost. + Check if the potential model name is in the model cost (case-insensitive). """ return any( - potential_model_name in litellm.model_cost + _get_model_cost_key(str(potential_model_name)) is not None for potential_model_name in potential_model_names.values() ) @@ -5223,44 +5242,51 @@ def _get_model_info_helper( # noqa: PLR0915 _model_info: Optional[Dict[str, Any]] = None key: Optional[str] = None - if combined_model_name in litellm.model_cost: - key = combined_model_name - _model_info = _get_model_info_from_model_cost(key=cast(str, key)) - if not _check_provider_match( - model_info=_model_info, custom_llm_provider=custom_llm_provider - ): - _model_info = None - if _model_info is None and model in litellm.model_cost: - key = model - _model_info = _get_model_info_from_model_cost(key=cast(str, key)) - if not _check_provider_match( - model_info=_model_info, custom_llm_provider=custom_llm_provider - ): - _model_info = None - if ( - _model_info is None - and combined_stripped_model_name in litellm.model_cost - ): - key = combined_stripped_model_name - _model_info = _get_model_info_from_model_cost(key=cast(str, key)) - if not _check_provider_match( - model_info=_model_info, custom_llm_provider=custom_llm_provider - ): - _model_info = None - if _model_info is None and stripped_model_name in litellm.model_cost: - key = stripped_model_name - _model_info = _get_model_info_from_model_cost(key=cast(str, key)) - if not _check_provider_match( - model_info=_model_info, custom_llm_provider=custom_llm_provider - ): - _model_info = None - if _model_info is None and split_model in litellm.model_cost: - key = split_model + # Use case-insensitive lookup for all model name checks + _matched_key = _get_model_cost_key(combined_model_name) + if _matched_key is not None: + key = _matched_key _model_info = _get_model_info_from_model_cost(key=cast(str, key)) if not _check_provider_match( model_info=_model_info, custom_llm_provider=custom_llm_provider ): _model_info = None + if _model_info is None: + _matched_key = _get_model_cost_key(model) + if _matched_key is not None: + key = _matched_key + _model_info = _get_model_info_from_model_cost(key=cast(str, key)) + if not _check_provider_match( + model_info=_model_info, custom_llm_provider=custom_llm_provider + ): + _model_info = None + if _model_info is None: + _matched_key = _get_model_cost_key(combined_stripped_model_name) + if _matched_key is not None: + key = _matched_key + _model_info = _get_model_info_from_model_cost(key=cast(str, key)) + if not _check_provider_match( + model_info=_model_info, custom_llm_provider=custom_llm_provider + ): + _model_info = None + if _model_info is None: + _matched_key = _get_model_cost_key(stripped_model_name) + if _matched_key is not None: + key = _matched_key + _model_info = _get_model_info_from_model_cost(key=cast(str, key)) + if not _check_provider_match( + model_info=_model_info, custom_llm_provider=custom_llm_provider + ): + _model_info = None + if _model_info is None: + _matched_key = _get_model_cost_key(split_model) + if _matched_key is not None: + key = _matched_key + _model_info = _get_model_info_from_model_cost(key=cast(str, key)) + if not _check_provider_match( + model_info=_model_info, custom_llm_provider=custom_llm_provider + ): + _model_info = None if _model_info is None or key is None: raise ValueError( diff --git a/tests/local_testing/test_get_model_info.py b/tests/local_testing/test_get_model_info.py index 823f03185d..b84fc22af0 100644 --- a/tests/local_testing/test_get_model_info.py +++ b/tests/local_testing/test_get_model_info.py @@ -372,3 +372,86 @@ def test_get_model_info_cost_calculator_bedrock_region_cris_stripped(model, prov print("info", info) assert info["key"] == "us.anthropic.claude-3-haiku-20240307-v1:0" assert info["litellm_provider"] == "bedrock" + + +def test_get_model_info_case_insensitive_lookup(monkeypatch): + """ + Test that model info lookup is case-insensitive. + + This ensures that users can use lowercase model names even when the model cost + map has mixed-case keys (e.g., "Qwen/Qwen3-Next-80B-A3B-Thinking"). + + Related Slack discussion: Users were getting "does not support parameters: ['tools']" + errors when using lowercase model names like "qwen/qwen3-next-80b-a3b-thinking" + because the lookup was case-sensitive. + """ + monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") + litellm.model_cost = litellm.get_model_cost_map(url="") + + # Register a test model with mixed-case name + litellm.register_model( + { + "together_ai/Qwen/Qwen3-Next-80B-A3B-Thinking": { + "input_cost_per_token": 0.0001, + "output_cost_per_token": 0.0002, + "litellm_provider": "together_ai", + "supports_function_calling": True, + } + } + ) + + # Test 1: Exact case should work + info = litellm.get_model_info( + model="Qwen/Qwen3-Next-80B-A3B-Thinking", custom_llm_provider="together_ai" + ) + assert info is not None + assert info["supports_function_calling"] is True + + # Test 2: Lowercase should also work (case-insensitive lookup) + info_lower = litellm.get_model_info( + model="qwen/qwen3-next-80b-a3b-thinking", custom_llm_provider="together_ai" + ) + assert info_lower is not None + assert info_lower["supports_function_calling"] is True + + # Test 3: Mixed case should also work + info_mixed = litellm.get_model_info( + model="QWEN/qwen3-NEXT-80b-a3b-thinking", custom_llm_provider="together_ai" + ) + assert info_mixed is not None + assert info_mixed["supports_function_calling"] is True + + +def test_get_model_info_case_insensitive_supports_function_calling(monkeypatch): + """ + Test that supports_function_calling check works with case-insensitive model lookup. + """ + monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") + litellm.model_cost = litellm.get_model_cost_map(url="") + + # Register a model with mixed-case name that supports function calling + litellm.register_model( + { + "test_provider/TestModel-ABC": { + "input_cost_per_token": 0.0001, + "output_cost_per_token": 0.0002, + "litellm_provider": "test_provider", + "supports_function_calling": True, + } + } + ) + + # Test that supports_function_calling works with lowercase model name + from litellm.utils import supports_function_calling + + # Exact case + assert ( + supports_function_calling("TestModel-ABC", custom_llm_provider="test_provider") + is True + ) + + # Lowercase (should now work with case-insensitive lookup) + assert ( + supports_function_calling("testmodel-abc", custom_llm_provider="test_provider") + is True + )