From f81d8ae077feea68919edc509935c7caa834cc0d Mon Sep 17 00:00:00 2001 From: Mateo Wang <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:16:01 -0700 Subject: [PATCH] [internal copy of #29232] feat: route future Claude models to Anthropic provider via pattern matching (#29239) * feat: route future Claude models to Anthropic provider via pattern matching Add pattern-based matching for Claude model names so that future models (e.g., claude-opus-4-9, claude-sonnet-5-0) are automatically routed to the Anthropic provider without requiring model_prices_and_context_window.json updates. The pattern matches: claude-{opus|sonnet|haiku}-{major}-{minor}[-YYYYMMDD] https://claude.ai/code/session_017asCVDN5jBFMBcZRjiQR6C * fix: don't hard-code the tier names Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * style: move import re to module level (PEP 8) Move `import re` from inside the module body to the top-level imports section, following PEP 8 style guidelines that all imports should appear at the top of the file. https://claude.ai/code/session_01Dt8fzn81eYMfxu1MoBa5hN * test: fix claude-mini-4-5 assertion to match generic-tier pattern The pattern intentionally accepts any [a-z]+ tier (see f835f84) rather than a hard-coded opus|sonnet|haiku list, so claude-mini-4-5 routes to anthropic and the old 'is False' assertion was wrong. Replace it with a positive test that locks in the generic-tier behavior and guards against a regression to hard-coded tier names. * style: collapse _CLAUDE_PATTERN to one line (black) Black 26.3.1 (CI) collapses the re.compile call onto a single line since it fits within the line limit. Fixes the failing lint check. --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- .../get_llm_provider_logic.py | 23 ++++ tests/local_testing/test_get_llm_provider.py | 105 ++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/litellm/litellm_core_utils/get_llm_provider_logic.py b/litellm/litellm_core_utils/get_llm_provider_logic.py index ba6d438f16c..bb3f3fae9f0 100644 --- a/litellm/litellm_core_utils/get_llm_provider_logic.py +++ b/litellm/litellm_core_utils/get_llm_provider_logic.py @@ -1,3 +1,4 @@ +import re from typing import Optional, Tuple from urllib.parse import urlparse @@ -71,6 +72,25 @@ def _is_azure_claude_model(model: str) -> bool: return False +_CLAUDE_PATTERN = re.compile(r"^claude-[a-z]+-\d+-\d+(?:-\d{8})?$", re.IGNORECASE) + + +def _matches_claude_model_pattern(model: str) -> bool: + """ + Check if a model string matches the Claude model naming pattern. + + Matches patterns like: + - claude-opus-4-7 + - claude-sonnet-4-6 + - claude-haiku-4-5 + - claude-opus-5-1-20270101 (with optional date suffix) + + This allows future Claude models to be routed to the Anthropic provider + without requiring updates to model_prices_and_context_window.json. + """ + return _CLAUDE_PATTERN.match(model) is not None + + def handle_cohere_chat_model_custom_llm_provider( model: str, custom_llm_provider: Optional[str] = None ) -> Tuple[str, Optional[str]]: @@ -398,6 +418,9 @@ def get_llm_provider( # noqa: PLR0915 custom_llm_provider = "anthropic_text" else: custom_llm_provider = "anthropic" + ## anthropic - pattern-based matching for future Claude models + elif _matches_claude_model_pattern(model): + custom_llm_provider = "anthropic" ## cohere elif model in litellm.cohere_models or model in litellm.cohere_embedding_models: custom_llm_provider = "cohere" diff --git a/tests/local_testing/test_get_llm_provider.py b/tests/local_testing/test_get_llm_provider.py index 14b9e8cd136..46f2f89b3ce 100644 --- a/tests/local_testing/test_get_llm_provider.py +++ b/tests/local_testing/test_get_llm_provider.py @@ -478,3 +478,108 @@ def test_get_llm_provider_use_proxy_arg_true_with_direct_args(): assert key == arg_api_key # Should use the argument key assert base == arg_api_base # Should use the argument base + +# -------- Tests for Claude model pattern matching --------- + + +class TestClaudeModelPatternMatching: + """ + Tests for _matches_claude_model_pattern which routes future Claude models + to the Anthropic provider without requiring model_prices_and_context_window.json updates. + """ + + def test_matches_claude_opus_pattern(self): + """Test claude-opus-X-Y pattern matching.""" + from litellm.litellm_core_utils.get_llm_provider_logic import ( + _matches_claude_model_pattern, + ) + + assert _matches_claude_model_pattern("claude-opus-4-7") is True + assert _matches_claude_model_pattern("claude-opus-4-9") is True + assert _matches_claude_model_pattern("claude-opus-5-1") is True + + def test_matches_claude_sonnet_pattern(self): + """Test claude-sonnet-X-Y pattern matching.""" + from litellm.litellm_core_utils.get_llm_provider_logic import ( + _matches_claude_model_pattern, + ) + + assert _matches_claude_model_pattern("claude-sonnet-4-6") is True + assert _matches_claude_model_pattern("claude-sonnet-5-0") is True + + def test_matches_claude_haiku_pattern(self): + """Test claude-haiku-X-Y pattern matching.""" + from litellm.litellm_core_utils.get_llm_provider_logic import ( + _matches_claude_model_pattern, + ) + + assert _matches_claude_model_pattern("claude-haiku-4-5") is True + assert _matches_claude_model_pattern("claude-haiku-5-0") is True + + def test_matches_claude_with_date_suffix(self): + """Test claude model pattern with date suffix.""" + from litellm.litellm_core_utils.get_llm_provider_logic import ( + _matches_claude_model_pattern, + ) + + assert _matches_claude_model_pattern("claude-opus-5-1-20270101") is True + assert _matches_claude_model_pattern("claude-sonnet-4-7-20260601") is True + assert _matches_claude_model_pattern("claude-haiku-4-6-20251201") is True + + def test_matches_unknown_tier_name(self): + """A tier segment we don't know about today should still route to anthropic. + + The pattern intentionally accepts any ``[a-z]+`` tier rather than a + hard-coded ``opus|sonnet|haiku`` list so a future tier (e.g. a new + "mini" line) is covered without a code change. This guards against a + regression back to hard-coded tier names. + """ + from litellm.litellm_core_utils.get_llm_provider_logic import ( + _matches_claude_model_pattern, + ) + + assert _matches_claude_model_pattern("claude-mini-4-5") is True + assert _matches_claude_model_pattern("claude-neptune-6-0") is True + + def test_rejects_non_claude_models(self): + """Test that non-Claude models are not matched.""" + from litellm.litellm_core_utils.get_llm_provider_logic import ( + _matches_claude_model_pattern, + ) + + assert _matches_claude_model_pattern("gpt-4") is False + assert _matches_claude_model_pattern("mistral-large") is False + assert _matches_claude_model_pattern("llama-3") is False + + def test_rejects_invalid_claude_patterns(self): + """Test that invalid Claude model patterns are not matched.""" + from litellm.litellm_core_utils.get_llm_provider_logic import ( + _matches_claude_model_pattern, + ) + + # Wrong order (variant before name) + assert _matches_claude_model_pattern("claude-4-opus") is False + # Missing version numbers + assert _matches_claude_model_pattern("claude-opus") is False + # Old format (claude-3-opus instead of claude-opus-3) + assert _matches_claude_model_pattern("claude-3-opus-20240229") is False + + def test_get_llm_provider_future_claude_model(self): + """Test that get_llm_provider routes future Claude models to anthropic.""" + model, custom_llm_provider, dynamic_api_key, api_base = ( + litellm.get_llm_provider( + model="claude-opus-4-9", + ) + ) + assert custom_llm_provider == "anthropic" + assert model == "claude-opus-4-9" + + def test_get_llm_provider_future_claude_model_with_date(self): + """Test that get_llm_provider routes future Claude models with date suffix.""" + model, custom_llm_provider, dynamic_api_key, api_base = ( + litellm.get_llm_provider( + model="claude-opus-5-1-20270101", + ) + ) + assert custom_llm_provider == "anthropic" + assert model == "claude-opus-5-1-20270101"