From 6245106c66c66dea034fc775b7d6fcff949f9ac1 Mon Sep 17 00:00:00 2001 From: heyseere <34279140+heyseere@users.noreply.github.com> Date: Thu, 29 Oct 2026 00:02:52 +0800 Subject: [PATCH 1/3] fix(llms): strip matching provider prefix when an explicit provider is given --- lib/crewai/src/crewai/llm.py | 12 ++++++++++-- lib/crewai/tests/test_llm.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/lib/crewai/src/crewai/llm.py b/lib/crewai/src/crewai/llm.py index 3c6390fba8..1de2a0cbe5 100644 --- a/lib/crewai/src/crewai/llm.py +++ b/lib/crewai/src/crewai/llm.py @@ -512,9 +512,17 @@ def _resolve_route(cls, model: str, kwargs: dict[str, Any]) -> _Route: elif explicit_provider: provider = explicit_provider use_native = True - model_string = model + # An explicit provider only pins the routing; a model string that + # already carries the same provider's prefix must not keep it, + # otherwise the prefixed name is sent to the API verbatim. + prefix, separator, model_part = model.partition("/") + canonical_prefix = PROVIDER_ALIASES.get(prefix.lower(), prefix.lower()) + if separator and model_part and canonical_prefix == provider: + model_string = model_part + else: + model_string = model if provider == "deepinfra": - cls._require_deepinfra_model(model) + cls._require_deepinfra_model(model_string) elif "/" in model: prefix, _, model_part = model.partition("/") diff --git a/lib/crewai/tests/test_llm.py b/lib/crewai/tests/test_llm.py index 8fbf1b95e4..5204e6023a 100644 --- a/lib/crewai/tests/test_llm.py +++ b/lib/crewai/tests/test_llm.py @@ -1239,6 +1239,34 @@ def test_explicit_provider_kwarg_takes_priority(): assert llm2.provider == "openai" +def test_explicit_provider_strips_matching_model_prefix(): + """provider= only pins routing; a model prefix naming the same provider is + stripped instead of being sent to the API verbatim.""" + assert LLM._resolve_route("openai/gpt-4o", {"provider": "openai"}).model == "gpt-4o" + assert ( + LLM._resolve_route("claude/claude-haiku-4-5", {"provider": "anthropic"}).model + == "claude-haiku-4-5" + ) + assert ( + LLM._resolve_route( + "deepinfra/meta-llama/Llama-3-8b", {"provider": "deepinfra"} + ).model + == "meta-llama/Llama-3-8b" + ) + + # An unrelated prefix (e.g. a namespaced model id) is left untouched. + assert LLM._resolve_route("custom/gpt-4o", {"provider": "openai"}).model == "custom/gpt-4o" + + # A malformed DeepInfra id still fails validation on the stripped part. + with pytest.raises(ValueError, match="org/model"): + LLM._resolve_route("deepinfra/llama", {"provider": "deepinfra"}) + + # kwargs are read, never mutated. + kwargs = {"provider": "openai"} + LLM._resolve_route("openai/gpt-4o", kwargs) + assert kwargs == {"provider": "openai"} + + def test_resolve_route_decides_what_new_constructs(): """`LLM._resolve_route` is `__new__`'s routing, callable without building anything.""" from crewai.llms.providers.anthropic.completion import AnthropicCompletion From 8fd5e064365a112178d8ff54ade91bfa172674f3 Mon Sep 17 00:00:00 2001 From: heyseere <34279140+heyseere@users.noreply.github.com> Date: Thu, 29 Oct 2026 11:09:53 +0800 Subject: [PATCH 2/3] fix(llms): canonicalize explicit provider when stripping model prefix The prefix strip compared a canonicalized model prefix against the raw provider kwarg, so alias spellings (claude, google, aws, azure_openai) never matched and their prefixed model ids were still sent to the API verbatim. Canonicalize both sides and cover the alias routes. --- lib/crewai/src/crewai/llm.py | 9 +++++++-- lib/crewai/tests/test_llm.py | 39 ++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/lib/crewai/src/crewai/llm.py b/lib/crewai/src/crewai/llm.py index 1de2a0cbe5..5cfbbe082d 100644 --- a/lib/crewai/src/crewai/llm.py +++ b/lib/crewai/src/crewai/llm.py @@ -514,10 +514,15 @@ def _resolve_route(cls, model: str, kwargs: dict[str, Any]) -> _Route: use_native = True # An explicit provider only pins the routing; a model string that # already carries the same provider's prefix must not keep it, - # otherwise the prefixed name is sent to the API verbatim. + # otherwise the prefixed name is sent to the API verbatim. Both + # sides are canonicalized so alias spellings ("claude", "google", + # "aws", "azure_openai") match their canonical prefixes too. prefix, separator, model_part = model.partition("/") canonical_prefix = PROVIDER_ALIASES.get(prefix.lower(), prefix.lower()) - if separator and model_part and canonical_prefix == provider: + canonical_provider = PROVIDER_ALIASES.get( + provider.lower(), provider.lower() + ) + if separator and model_part and canonical_prefix == canonical_provider: model_string = model_part else: model_string = model diff --git a/lib/crewai/tests/test_llm.py b/lib/crewai/tests/test_llm.py index 5204e6023a..9f4f6fc0d9 100644 --- a/lib/crewai/tests/test_llm.py +++ b/lib/crewai/tests/test_llm.py @@ -1267,6 +1267,45 @@ def test_explicit_provider_strips_matching_model_prefix(): assert kwargs == {"provider": "openai"} +def test_explicit_alias_provider_strips_canonical_prefix(): + """An alias provider must match its canonical prefix, in both spellings. + + ``provider="claude"`` is a supported native provider, so a + ``claude/`` string has to be stripped just like + ``provider="anthropic"`` does; otherwise the alias path keeps the prefix + and the API receives a model name it rejects. + """ + assert ( + LLM._resolve_route("claude/claude-haiku-4-5", {"provider": "claude"}).model + == "claude-haiku-4-5" + ) + assert ( + LLM._resolve_route( + "anthropic/claude-haiku-4-5", {"provider": "claude"} + ).model + == "claude-haiku-4-5" + ) + assert ( + LLM._resolve_route("google/gemini-2.5-pro", {"provider": "google"}).model + == "gemini-2.5-pro" + ) + assert LLM._resolve_route("aws/titan-text", {"provider": "aws"}).model == "titan-text" + assert ( + LLM._resolve_route("azure/gpt-4o", {"provider": "azure_openai"}).model + == "gpt-4o" + ) + assert ( + LLM._resolve_route("gemini/gemini-2.5-pro", {"provider": "google"}).model + == "gemini-2.5-pro" + ) + + # A prefix naming a different provider is still left untouched. + assert ( + LLM._resolve_route("openai/gpt-4o", {"provider": "claude"}).model + == "openai/gpt-4o" + ) + + def test_resolve_route_decides_what_new_constructs(): """`LLM._resolve_route` is `__new__`'s routing, callable without building anything.""" from crewai.llms.providers.anthropic.completion import AnthropicCompletion From 8af85f94dc0c359252db478b3b57d57ed7266ad7 Mon Sep 17 00:00:00 2001 From: heyseere <34279140+heyseere@users.noreply.github.com> Date: Fri, 30 Oct 2026 00:32:40 +0800 Subject: [PATCH 3/3] test(llms): align deepinfra explicit-provider tests with prefix stripping --- .../llms/openai_compatible/test_openai_compatible.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/crewai/tests/llms/openai_compatible/test_openai_compatible.py b/lib/crewai/tests/llms/openai_compatible/test_openai_compatible.py index e3cd58c908..b52d8d9d89 100644 --- a/lib/crewai/tests/llms/openai_compatible/test_openai_compatible.py +++ b/lib/crewai/tests/llms/openai_compatible/test_openai_compatible.py @@ -364,10 +364,20 @@ def test_llm_creates_openai_compatible_for_deepinfra_explicit_provider(self): assert llm.provider == "deepinfra" assert llm.model == "deepseek-ai/DeepSeek-V4-Flash-0731" + def test_llm_explicit_deepinfra_provider_strips_matching_prefix(self): + """A redundant deepinfra/ prefix on the model is stripped, not sent verbatim.""" + with patch.dict(os.environ, {"DEEPINFRA_API_KEY": "test-key"}): + llm = LLM( + model="deepinfra/deepseek-ai/DeepSeek-V4-Flash-0731", + provider="deepinfra", + ) + assert isinstance(llm, OpenAICompatibleCompletion) + assert llm.provider == "deepinfra" + assert llm.model == "deepseek-ai/DeepSeek-V4-Flash-0731" + @pytest.mark.parametrize( "model", [ - "deepinfra/deepseek-ai/DeepSeek-V4-Flash-0731", "DeepSeek-V4-Flash-0731", "deepseek-ai/DeepSeek-V4-Flash-0731/", ],