diff --git a/lib/crewai-tools/pyproject.toml b/lib/crewai-tools/pyproject.toml index 8f952c512..82a2da205 100644 --- a/lib/crewai-tools/pyproject.toml +++ b/lib/crewai-tools/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "pytube>=15.0.0", "requests>=2.32.5", "docker>=7.1.0", - "crewai==1.4.0", + "crewai==1.4.1", "lancedb>=0.5.4", "tiktoken>=0.8.0", "beautifulsoup4>=4.13.4", diff --git a/lib/crewai-tools/src/crewai_tools/__init__.py b/lib/crewai-tools/src/crewai_tools/__init__.py index 1d49291b6..b9fc39f3e 100644 --- a/lib/crewai-tools/src/crewai_tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/__init__.py @@ -287,4 +287,4 @@ __all__ = [ "ZapierActionTools", ] -__version__ = "1.4.0" +__version__ = "1.4.1" diff --git a/lib/crewai/pyproject.toml b/lib/crewai/pyproject.toml index cfd93efca..aed269fdb 100644 --- a/lib/crewai/pyproject.toml +++ b/lib/crewai/pyproject.toml @@ -48,7 +48,7 @@ Repository = "https://github.com/crewAIInc/crewAI" [project.optional-dependencies] tools = [ - "crewai-tools==1.4.0", + "crewai-tools==1.4.1", ] embeddings = [ "tiktoken~=0.8.0" diff --git a/lib/crewai/src/crewai/__init__.py b/lib/crewai/src/crewai/__init__.py index 173deb4eb..c992f11f7 100644 --- a/lib/crewai/src/crewai/__init__.py +++ b/lib/crewai/src/crewai/__init__.py @@ -40,7 +40,7 @@ def _suppress_pydantic_deprecation_warnings() -> None: _suppress_pydantic_deprecation_warnings() -__version__ = "1.4.0" +__version__ = "1.4.1" _telemetry_submitted = False diff --git a/lib/crewai/src/crewai/agents/crew_agent_executor.py b/lib/crewai/src/crewai/agents/crew_agent_executor.py index 8c1eb2c0e..5b806658c 100644 --- a/lib/crewai/src/crewai/agents/crew_agent_executor.py +++ b/lib/crewai/src/crewai/agents/crew_agent_executor.py @@ -214,6 +214,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin): llm=self.llm, callbacks=self.callbacks, ) + break enforce_rpm_limit(self.request_within_rpm_limit) @@ -226,7 +227,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin): from_agent=self.agent, response_model=self.response_model, ) - formatted_answer = process_llm_response(answer, self.use_stop_words) + formatted_answer = process_llm_response(answer, self.use_stop_words) # type: ignore[assignment] if isinstance(formatted_answer, AgentAction): # Extract agent fingerprint if available @@ -258,11 +259,11 @@ class CrewAgentExecutor(CrewAgentExecutorMixin): formatted_answer, tool_result ) - self._invoke_step_callback(formatted_answer) - self._append_message(formatted_answer.text) + self._invoke_step_callback(formatted_answer) # type: ignore[arg-type] + self._append_message(formatted_answer.text) # type: ignore[union-attr,attr-defined] - except OutputParserError as e: # noqa: PERF203 - formatted_answer = handle_output_parser_exception( + except OutputParserError as e: + formatted_answer = handle_output_parser_exception( # type: ignore[assignment] e=e, messages=self.messages, iterations=self.iterations, diff --git a/lib/crewai/src/crewai/cli/templates/crew/pyproject.toml b/lib/crewai/src/crewai/cli/templates/crew/pyproject.toml index 5dd119303..1d946521f 100644 --- a/lib/crewai/src/crewai/cli/templates/crew/pyproject.toml +++ b/lib/crewai/src/crewai/cli/templates/crew/pyproject.toml @@ -5,7 +5,7 @@ description = "{{name}} using crewAI" authors = [{ name = "Your Name", email = "you@example.com" }] requires-python = ">=3.10,<3.14" dependencies = [ - "crewai[tools]==1.4.0" + "crewai[tools]==1.4.1" ] [project.scripts] diff --git a/lib/crewai/src/crewai/cli/templates/flow/pyproject.toml b/lib/crewai/src/crewai/cli/templates/flow/pyproject.toml index 67d3f7b78..45f4d76a6 100644 --- a/lib/crewai/src/crewai/cli/templates/flow/pyproject.toml +++ b/lib/crewai/src/crewai/cli/templates/flow/pyproject.toml @@ -5,7 +5,7 @@ description = "{{name}} using crewAI" authors = [{ name = "Your Name", email = "you@example.com" }] requires-python = ">=3.10,<3.14" dependencies = [ - "crewai[tools]==1.4.0" + "crewai[tools]==1.4.1" ] [project.scripts] diff --git a/lib/crewai/src/crewai/llm.py b/lib/crewai/src/crewai/llm.py index 2e2684ebe..b0cf42091 100644 --- a/lib/crewai/src/crewai/llm.py +++ b/lib/crewai/src/crewai/llm.py @@ -38,6 +38,13 @@ from crewai.events.types.tool_usage_events import ( ToolUsageStartedEvent, ) from crewai.llms.base_llm import BaseLLM +from crewai.llms.constants import ( + ANTHROPIC_MODELS, + AZURE_MODELS, + BEDROCK_MODELS, + GEMINI_MODELS, + OPENAI_MODELS, +) from crewai.utilities import InternalInstructor from crewai.utilities.exceptions.context_window_exceeding_exception import ( LLMContextLengthExceededError, @@ -323,18 +330,64 @@ class LLM(BaseLLM): completion_cost: float | None = None def __new__(cls, model: str, is_litellm: bool = False, **kwargs: Any) -> LLM: - """Factory method that routes to native SDK or falls back to LiteLLM.""" + """Factory method that routes to native SDK or falls back to LiteLLM. + + Routing priority: + 1. If 'provider' kwarg is present, use that provider with constants + 2. If only 'model' kwarg, use constants to infer provider + 3. If "/" in model name: + - Check if prefix is a native provider (openai/anthropic/azure/bedrock/gemini) + - If yes, validate model against constants + - If valid, route to native SDK; otherwise route to LiteLLM + """ if not model or not isinstance(model, str): raise ValueError("Model must be a non-empty string") - provider = model.partition("/")[0] if "/" in model else "openai" + explicit_provider = kwargs.get("provider") - native_class = cls._get_native_provider(provider) + if explicit_provider: + provider = explicit_provider + use_native = True + model_string = model + elif "/" in model: + prefix, _, model_part = model.partition("/") + + provider_mapping = { + "openai": "openai", + "anthropic": "anthropic", + "claude": "anthropic", + "azure": "azure", + "azure_openai": "azure", + "google": "gemini", + "gemini": "gemini", + "bedrock": "bedrock", + "aws": "bedrock", + } + + canonical_provider = provider_mapping.get(prefix.lower()) + + if canonical_provider and cls._validate_model_in_constants( + model_part, canonical_provider + ): + provider = canonical_provider + use_native = True + model_string = model_part + else: + provider = prefix + use_native = False + model_string = model_part + else: + provider = cls._infer_provider_from_model(model) + use_native = True + model_string = model + + native_class = cls._get_native_provider(provider) if use_native else None if native_class and not is_litellm and provider in SUPPORTED_NATIVE_PROVIDERS: try: - model_string = model.partition("/")[2] if "/" in model else model + # Remove 'provider' from kwargs if it exists to avoid duplicate keyword argument + kwargs_copy = {k: v for k, v in kwargs.items() if k != 'provider'} return cast( - Self, native_class(model=model_string, provider=provider, **kwargs) + Self, native_class(model=model_string, provider=provider, **kwargs_copy) ) except NotImplementedError: raise @@ -351,6 +404,63 @@ class LLM(BaseLLM): instance.is_litellm = True return instance + @classmethod + def _validate_model_in_constants(cls, model: str, provider: str) -> bool: + """Validate if a model name exists in the provider's constants. + + Args: + model: The model name to validate + provider: The provider to check against (canonical name) + + Returns: + True if the model exists in the provider's constants, False otherwise + """ + if provider == "openai": + return model in OPENAI_MODELS + + if provider == "anthropic" or provider == "claude": + return model in ANTHROPIC_MODELS + + if provider == "gemini": + return model in GEMINI_MODELS + + if provider == "bedrock": + return model in BEDROCK_MODELS + + if provider == "azure": + # azure does not provide a list of available models, determine a better way to handle this + return True + + return False + + @classmethod + def _infer_provider_from_model(cls, model: str) -> str: + """Infer the provider from the model name. + + Args: + model: The model name without provider prefix + + Returns: + The inferred provider name, defaults to "openai" + """ + + if model in OPENAI_MODELS: + return "openai" + + if model in ANTHROPIC_MODELS: + return "anthropic" + + if model in GEMINI_MODELS: + return "gemini" + + if model in BEDROCK_MODELS: + return "bedrock" + + if model in AZURE_MODELS: + return "azure" + + return "openai" + @classmethod def _get_native_provider(cls, provider: str) -> type | None: """Get native provider class if available.""" diff --git a/lib/crewai/src/crewai/llms/constants.py b/lib/crewai/src/crewai/llms/constants.py new file mode 100644 index 000000000..2765a9458 --- /dev/null +++ b/lib/crewai/src/crewai/llms/constants.py @@ -0,0 +1,558 @@ +from typing import Literal, TypeAlias + + +OpenAIModels: TypeAlias = Literal[ + "gpt-3.5-turbo", + "gpt-3.5-turbo-0125", + "gpt-3.5-turbo-0301", + "gpt-3.5-turbo-0613", + "gpt-3.5-turbo-1106", + "gpt-3.5-turbo-16k", + "gpt-3.5-turbo-16k-0613", + "gpt-3.5-turbo-instruct", + "gpt-3.5-turbo-instruct-0914", + "gpt-4", + "gpt-4-0125-preview", + "gpt-4-0314", + "gpt-4-0613", + "gpt-4-1106-preview", + "gpt-4-32k", + "gpt-4-32k-0314", + "gpt-4-32k-0613", + "gpt-4-turbo", + "gpt-4-turbo-2024-04-09", + "gpt-4-turbo-preview", + "gpt-4-vision-preview", + "gpt-4.1", + "gpt-4.1-2025-04-14", + "gpt-4.1-mini", + "gpt-4.1-mini-2025-04-14", + "gpt-4.1-nano", + "gpt-4.1-nano-2025-04-14", + "gpt-4o", + "gpt-4o-2024-05-13", + "gpt-4o-2024-08-06", + "gpt-4o-2024-11-20", + "gpt-4o-audio-preview", + "gpt-4o-audio-preview-2024-10-01", + "gpt-4o-audio-preview-2024-12-17", + "gpt-4o-audio-preview-2025-06-03", + "gpt-4o-mini", + "gpt-4o-mini-2024-07-18", + "gpt-4o-mini-audio-preview", + "gpt-4o-mini-audio-preview-2024-12-17", + "gpt-4o-mini-realtime-preview", + "gpt-4o-mini-realtime-preview-2024-12-17", + "gpt-4o-mini-search-preview", + "gpt-4o-mini-search-preview-2025-03-11", + "gpt-4o-mini-transcribe", + "gpt-4o-mini-tts", + "gpt-4o-realtime-preview", + "gpt-4o-realtime-preview-2024-10-01", + "gpt-4o-realtime-preview-2024-12-17", + "gpt-4o-realtime-preview-2025-06-03", + "gpt-4o-search-preview", + "gpt-4o-search-preview-2025-03-11", + "gpt-4o-transcribe", + "gpt-4o-transcribe-diarize", + "gpt-5", + "gpt-5-2025-08-07", + "gpt-5-chat", + "gpt-5-chat-latest", + "gpt-5-codex", + "gpt-5-mini", + "gpt-5-mini-2025-08-07", + "gpt-5-nano", + "gpt-5-nano-2025-08-07", + "gpt-5-pro", + "gpt-5-pro-2025-10-06", + "gpt-5-search-api", + "gpt-5-search-api-2025-10-14", + "gpt-audio", + "gpt-audio-2025-08-28", + "gpt-audio-mini", + "gpt-audio-mini-2025-10-06", + "gpt-image-1", + "gpt-image-1-mini", + "gpt-realtime", + "gpt-realtime-2025-08-28", + "gpt-realtime-mini", + "gpt-realtime-mini-2025-10-06", + "o1", + "o1-preview", + "o1-2024-12-17", + "o1-mini", + "o1-mini-2024-09-12", + "o1-pro", + "o1-pro-2025-03-19", + "o3-mini", + "o3", + "o4-mini", + "whisper-1", +] +OPENAI_MODELS: list[OpenAIModels] = [ + "gpt-3.5-turbo", + "gpt-3.5-turbo-0125", + "gpt-3.5-turbo-0301", + "gpt-3.5-turbo-0613", + "gpt-3.5-turbo-1106", + "gpt-3.5-turbo-16k", + "gpt-3.5-turbo-16k-0613", + "gpt-3.5-turbo-instruct", + "gpt-3.5-turbo-instruct-0914", + "gpt-4", + "gpt-4-0125-preview", + "gpt-4-0314", + "gpt-4-0613", + "gpt-4-1106-preview", + "gpt-4-32k", + "gpt-4-32k-0314", + "gpt-4-32k-0613", + "gpt-4-turbo", + "gpt-4-turbo-2024-04-09", + "gpt-4-turbo-preview", + "gpt-4-vision-preview", + "gpt-4.1", + "gpt-4.1-2025-04-14", + "gpt-4.1-mini", + "gpt-4.1-mini-2025-04-14", + "gpt-4.1-nano", + "gpt-4.1-nano-2025-04-14", + "gpt-4o", + "gpt-4o-2024-05-13", + "gpt-4o-2024-08-06", + "gpt-4o-2024-11-20", + "gpt-4o-audio-preview", + "gpt-4o-audio-preview-2024-10-01", + "gpt-4o-audio-preview-2024-12-17", + "gpt-4o-audio-preview-2025-06-03", + "gpt-4o-mini", + "gpt-4o-mini-2024-07-18", + "gpt-4o-mini-audio-preview", + "gpt-4o-mini-audio-preview-2024-12-17", + "gpt-4o-mini-realtime-preview", + "gpt-4o-mini-realtime-preview-2024-12-17", + "gpt-4o-mini-search-preview", + "gpt-4o-mini-search-preview-2025-03-11", + "gpt-4o-mini-transcribe", + "gpt-4o-mini-tts", + "gpt-4o-realtime-preview", + "gpt-4o-realtime-preview-2024-10-01", + "gpt-4o-realtime-preview-2024-12-17", + "gpt-4o-realtime-preview-2025-06-03", + "gpt-4o-search-preview", + "gpt-4o-search-preview-2025-03-11", + "gpt-4o-transcribe", + "gpt-4o-transcribe-diarize", + "gpt-5", + "gpt-5-2025-08-07", + "gpt-5-chat", + "gpt-5-chat-latest", + "gpt-5-codex", + "gpt-5-mini", + "gpt-5-mini-2025-08-07", + "gpt-5-nano", + "gpt-5-nano-2025-08-07", + "gpt-5-pro", + "gpt-5-pro-2025-10-06", + "gpt-5-search-api", + "gpt-5-search-api-2025-10-14", + "gpt-audio", + "gpt-audio-2025-08-28", + "gpt-audio-mini", + "gpt-audio-mini-2025-10-06", + "gpt-image-1", + "gpt-image-1-mini", + "gpt-realtime", + "gpt-realtime-2025-08-28", + "gpt-realtime-mini", + "gpt-realtime-mini-2025-10-06", + "o1", + "o1-preview", + "o1-2024-12-17", + "o1-mini", + "o1-mini-2024-09-12", + "o1-pro", + "o1-pro-2025-03-19", + "o3-mini", + "o3", + "o4-mini", + "whisper-1", +] + + +AnthropicModels: TypeAlias = Literal[ + "claude-3-7-sonnet-latest", + "claude-3-7-sonnet-20250219", + "claude-3-5-haiku-latest", + "claude-3-5-haiku-20241022", + "claude-haiku-4-5", + "claude-haiku-4-5-20251001", + "claude-sonnet-4-20250514", + "claude-sonnet-4-0", + "claude-4-sonnet-20250514", + "claude-sonnet-4-5", + "claude-sonnet-4-5-20250929", + "claude-3-5-sonnet-latest", + "claude-3-5-sonnet-20241022", + "claude-3-5-sonnet-20240620", + "claude-opus-4-0", + "claude-opus-4-20250514", + "claude-4-opus-20250514", + "claude-opus-4-1", + "claude-opus-4-1-20250805", + "claude-3-opus-latest", + "claude-3-opus-20240229", + "claude-3-sonnet-20240229", + "claude-3-haiku-latest", + "claude-3-haiku-20240307", +] +ANTHROPIC_MODELS: list[AnthropicModels] = [ + "claude-3-7-sonnet-latest", + "claude-3-7-sonnet-20250219", + "claude-3-5-haiku-latest", + "claude-3-5-haiku-20241022", + "claude-haiku-4-5", + "claude-haiku-4-5-20251001", + "claude-sonnet-4-20250514", + "claude-sonnet-4-0", + "claude-4-sonnet-20250514", + "claude-sonnet-4-5", + "claude-sonnet-4-5-20250929", + "claude-3-5-sonnet-latest", + "claude-3-5-sonnet-20241022", + "claude-3-5-sonnet-20240620", + "claude-opus-4-0", + "claude-opus-4-20250514", + "claude-4-opus-20250514", + "claude-opus-4-1", + "claude-opus-4-1-20250805", + "claude-3-opus-latest", + "claude-3-opus-20240229", + "claude-3-sonnet-20240229", + "claude-3-haiku-latest", + "claude-3-haiku-20240307", +] + +GeminiModels: TypeAlias = Literal[ + "gemini-2.5-pro", + "gemini-2.5-pro-preview-03-25", + "gemini-2.5-pro-preview-05-06", + "gemini-2.5-pro-preview-06-05", + "gemini-2.5-flash", + "gemini-2.5-flash-preview-05-20", + "gemini-2.5-flash-preview-04-17", + "gemini-2.5-flash-image", + "gemini-2.5-flash-image-preview", + "gemini-2.5-flash-lite", + "gemini-2.5-flash-lite-preview-06-17", + "gemini-2.5-flash-preview-09-2025", + "gemini-2.5-flash-lite-preview-09-2025", + "gemini-2.5-flash-preview-tts", + "gemini-2.5-pro-preview-tts", + "gemini-2.5-computer-use-preview-10-2025", + "gemini-2.0-flash", + "gemini-2.0-flash-001", + "gemini-2.0-flash-exp", + "gemini-2.0-flash-exp-image-generation", + "gemini-2.0-flash-lite", + "gemini-2.0-flash-lite-001", + "gemini-2.0-flash-lite-preview", + "gemini-2.0-flash-lite-preview-02-05", + "gemini-2.0-flash-preview-image-generation", + "gemini-2.0-flash-thinking-exp", + "gemini-2.0-flash-thinking-exp-01-21", + "gemini-2.0-flash-thinking-exp-1219", + "gemini-2.0-pro-exp", + "gemini-2.0-pro-exp-02-05", + "gemini-exp-1206", + "gemini-1.5-pro", + "gemini-1.5-flash", + "gemini-1.5-flash-8b", + "gemini-flash-latest", + "gemini-flash-lite-latest", + "gemini-pro-latest", + "gemini-2.0-flash-live-001", + "gemini-live-2.5-flash-preview", + "gemini-2.5-flash-live-preview", + "gemini-robotics-er-1.5-preview", + "gemini-gemma-2-27b-it", + "gemini-gemma-2-9b-it", + "gemma-3-1b-it", + "gemma-3-4b-it", + "gemma-3-12b-it", + "gemma-3-27b-it", + "gemma-3n-e2b-it", + "gemma-3n-e4b-it", + "learnlm-2.0-flash-experimental", +] +GEMINI_MODELS: list[GeminiModels] = [ + "gemini-2.5-pro", + "gemini-2.5-pro-preview-03-25", + "gemini-2.5-pro-preview-05-06", + "gemini-2.5-pro-preview-06-05", + "gemini-2.5-flash", + "gemini-2.5-flash-preview-05-20", + "gemini-2.5-flash-preview-04-17", + "gemini-2.5-flash-image", + "gemini-2.5-flash-image-preview", + "gemini-2.5-flash-lite", + "gemini-2.5-flash-lite-preview-06-17", + "gemini-2.5-flash-preview-09-2025", + "gemini-2.5-flash-lite-preview-09-2025", + "gemini-2.5-flash-preview-tts", + "gemini-2.5-pro-preview-tts", + "gemini-2.5-computer-use-preview-10-2025", + "gemini-2.0-flash", + "gemini-2.0-flash-001", + "gemini-2.0-flash-exp", + "gemini-2.0-flash-exp-image-generation", + "gemini-2.0-flash-lite", + "gemini-2.0-flash-lite-001", + "gemini-2.0-flash-lite-preview", + "gemini-2.0-flash-lite-preview-02-05", + "gemini-2.0-flash-preview-image-generation", + "gemini-2.0-flash-thinking-exp", + "gemini-2.0-flash-thinking-exp-01-21", + "gemini-2.0-flash-thinking-exp-1219", + "gemini-2.0-pro-exp", + "gemini-2.0-pro-exp-02-05", + "gemini-exp-1206", + "gemini-1.5-pro", + "gemini-1.5-flash", + "gemini-1.5-flash-8b", + "gemini-flash-latest", + "gemini-flash-lite-latest", + "gemini-pro-latest", + "gemini-2.0-flash-live-001", + "gemini-live-2.5-flash-preview", + "gemini-2.5-flash-live-preview", + "gemini-robotics-er-1.5-preview", + "gemini-gemma-2-27b-it", + "gemini-gemma-2-9b-it", + "gemma-3-1b-it", + "gemma-3-4b-it", + "gemma-3-12b-it", + "gemma-3-27b-it", + "gemma-3n-e2b-it", + "gemma-3n-e4b-it", + "learnlm-2.0-flash-experimental", +] + + +AzureModels: TypeAlias = Literal[ + "gpt-3.5-turbo", + "gpt-3.5-turbo-0301", + "gpt-3.5-turbo-0613", + "gpt-3.5-turbo-16k", + "gpt-3.5-turbo-16k-0613", + "gpt-35-turbo", + "gpt-35-turbo-0125", + "gpt-35-turbo-1106", + "gpt-35-turbo-16k-0613", + "gpt-35-turbo-instruct-0914", + "gpt-4", + "gpt-4-0314", + "gpt-4-0613", + "gpt-4-1106-preview", + "gpt-4-0125-preview", + "gpt-4-32k", + "gpt-4-32k-0314", + "gpt-4-32k-0613", + "gpt-4-turbo", + "gpt-4-turbo-2024-04-09", + "gpt-4-vision", + "gpt-4o", + "gpt-4o-2024-05-13", + "gpt-4o-2024-08-06", + "gpt-4o-2024-11-20", + "gpt-4o-mini", + "gpt-5", + "o1", + "o1-mini", + "o1-preview", + "o3-mini", + "o3", + "o4-mini", +] +AZURE_MODELS: list[AzureModels] = [ + "gpt-3.5-turbo", + "gpt-3.5-turbo-0301", + "gpt-3.5-turbo-0613", + "gpt-3.5-turbo-16k", + "gpt-3.5-turbo-16k-0613", + "gpt-35-turbo", + "gpt-35-turbo-0125", + "gpt-35-turbo-1106", + "gpt-35-turbo-16k-0613", + "gpt-35-turbo-instruct-0914", + "gpt-4", + "gpt-4-0314", + "gpt-4-0613", + "gpt-4-1106-preview", + "gpt-4-0125-preview", + "gpt-4-32k", + "gpt-4-32k-0314", + "gpt-4-32k-0613", + "gpt-4-turbo", + "gpt-4-turbo-2024-04-09", + "gpt-4-vision", + "gpt-4o", + "gpt-4o-2024-05-13", + "gpt-4o-2024-08-06", + "gpt-4o-2024-11-20", + "gpt-4o-mini", + "gpt-5", + "o1", + "o1-mini", + "o1-preview", + "o3-mini", + "o3", + "o4-mini", +] + + +BedrockModels: TypeAlias = Literal[ + "ai21.jamba-1-5-large-v1:0", + "ai21.jamba-1-5-mini-v1:0", + "amazon.nova-lite-v1:0", + "amazon.nova-lite-v1:0:24k", + "amazon.nova-lite-v1:0:300k", + "amazon.nova-micro-v1:0", + "amazon.nova-micro-v1:0:128k", + "amazon.nova-micro-v1:0:24k", + "amazon.nova-premier-v1:0", + "amazon.nova-premier-v1:0:1000k", + "amazon.nova-premier-v1:0:20k", + "amazon.nova-premier-v1:0:8k", + "amazon.nova-premier-v1:0:mm", + "amazon.nova-pro-v1:0", + "amazon.nova-pro-v1:0:24k", + "amazon.nova-pro-v1:0:300k", + "amazon.titan-text-express-v1", + "amazon.titan-text-express-v1:0:8k", + "amazon.titan-text-lite-v1", + "amazon.titan-text-lite-v1:0:4k", + "amazon.titan-tg1-large", + "anthropic.claude-3-5-haiku-20241022-v1:0", + "anthropic.claude-3-5-sonnet-20240620-v1:0", + "anthropic.claude-3-5-sonnet-20241022-v2:0", + "anthropic.claude-3-7-sonnet-20250219-v1:0", + "anthropic.claude-3-haiku-20240307-v1:0", + "anthropic.claude-3-haiku-20240307-v1:0:200k", + "anthropic.claude-3-haiku-20240307-v1:0:48k", + "anthropic.claude-3-opus-20240229-v1:0", + "anthropic.claude-3-opus-20240229-v1:0:12k", + "anthropic.claude-3-opus-20240229-v1:0:200k", + "anthropic.claude-3-opus-20240229-v1:0:28k", + "anthropic.claude-3-sonnet-20240229-v1:0", + "anthropic.claude-3-sonnet-20240229-v1:0:200k", + "anthropic.claude-3-sonnet-20240229-v1:0:28k", + "anthropic.claude-haiku-4-5-20251001-v1:0", + "anthropic.claude-instant-v1:2:100k", + "anthropic.claude-opus-4-1-20250805-v1:0", + "anthropic.claude-opus-4-20250514-v1:0", + "anthropic.claude-sonnet-4-20250514-v1:0", + "anthropic.claude-sonnet-4-5-20250929-v1:0", + "anthropic.claude-v2:0:100k", + "anthropic.claude-v2:0:18k", + "anthropic.claude-v2:1:18k", + "anthropic.claude-v2:1:200k", + "cohere.command-r-plus-v1:0", + "cohere.command-r-v1:0", + "cohere.rerank-v3-5:0", + "deepseek.r1-v1:0", + "meta.llama3-1-70b-instruct-v1:0", + "meta.llama3-1-8b-instruct-v1:0", + "meta.llama3-2-11b-instruct-v1:0", + "meta.llama3-2-1b-instruct-v1:0", + "meta.llama3-2-3b-instruct-v1:0", + "meta.llama3-2-90b-instruct-v1:0", + "meta.llama3-3-70b-instruct-v1:0", + "meta.llama3-70b-instruct-v1:0", + "meta.llama3-8b-instruct-v1:0", + "meta.llama4-maverick-17b-instruct-v1:0", + "meta.llama4-scout-17b-instruct-v1:0", + "mistral.mistral-7b-instruct-v0:2", + "mistral.mistral-large-2402-v1:0", + "mistral.mistral-small-2402-v1:0", + "mistral.mixtral-8x7b-instruct-v0:1", + "mistral.pixtral-large-2502-v1:0", + "openai.gpt-oss-120b-1:0", + "openai.gpt-oss-20b-1:0", + "qwen.qwen3-32b-v1:0", + "qwen.qwen3-coder-30b-a3b-v1:0", + "twelvelabs.pegasus-1-2-v1:0", +] +BEDROCK_MODELS: list[BedrockModels] = [ + "ai21.jamba-1-5-large-v1:0", + "ai21.jamba-1-5-mini-v1:0", + "amazon.nova-lite-v1:0", + "amazon.nova-lite-v1:0:24k", + "amazon.nova-lite-v1:0:300k", + "amazon.nova-micro-v1:0", + "amazon.nova-micro-v1:0:128k", + "amazon.nova-micro-v1:0:24k", + "amazon.nova-premier-v1:0", + "amazon.nova-premier-v1:0:1000k", + "amazon.nova-premier-v1:0:20k", + "amazon.nova-premier-v1:0:8k", + "amazon.nova-premier-v1:0:mm", + "amazon.nova-pro-v1:0", + "amazon.nova-pro-v1:0:24k", + "amazon.nova-pro-v1:0:300k", + "amazon.titan-text-express-v1", + "amazon.titan-text-express-v1:0:8k", + "amazon.titan-text-lite-v1", + "amazon.titan-text-lite-v1:0:4k", + "amazon.titan-tg1-large", + "anthropic.claude-3-5-haiku-20241022-v1:0", + "anthropic.claude-3-5-sonnet-20240620-v1:0", + "anthropic.claude-3-5-sonnet-20241022-v2:0", + "anthropic.claude-3-7-sonnet-20250219-v1:0", + "anthropic.claude-3-haiku-20240307-v1:0", + "anthropic.claude-3-haiku-20240307-v1:0:200k", + "anthropic.claude-3-haiku-20240307-v1:0:48k", + "anthropic.claude-3-opus-20240229-v1:0", + "anthropic.claude-3-opus-20240229-v1:0:12k", + "anthropic.claude-3-opus-20240229-v1:0:200k", + "anthropic.claude-3-opus-20240229-v1:0:28k", + "anthropic.claude-3-sonnet-20240229-v1:0", + "anthropic.claude-3-sonnet-20240229-v1:0:200k", + "anthropic.claude-3-sonnet-20240229-v1:0:28k", + "anthropic.claude-haiku-4-5-20251001-v1:0", + "anthropic.claude-instant-v1:2:100k", + "anthropic.claude-opus-4-1-20250805-v1:0", + "anthropic.claude-opus-4-20250514-v1:0", + "anthropic.claude-sonnet-4-20250514-v1:0", + "anthropic.claude-sonnet-4-5-20250929-v1:0", + "anthropic.claude-v2:0:100k", + "anthropic.claude-v2:0:18k", + "anthropic.claude-v2:1:18k", + "anthropic.claude-v2:1:200k", + "cohere.command-r-plus-v1:0", + "cohere.command-r-v1:0", + "cohere.rerank-v3-5:0", + "deepseek.r1-v1:0", + "meta.llama3-1-70b-instruct-v1:0", + "meta.llama3-1-8b-instruct-v1:0", + "meta.llama3-2-11b-instruct-v1:0", + "meta.llama3-2-1b-instruct-v1:0", + "meta.llama3-2-3b-instruct-v1:0", + "meta.llama3-2-90b-instruct-v1:0", + "meta.llama3-3-70b-instruct-v1:0", + "meta.llama3-70b-instruct-v1:0", + "meta.llama3-8b-instruct-v1:0", + "meta.llama4-maverick-17b-instruct-v1:0", + "meta.llama4-scout-17b-instruct-v1:0", + "mistral.mistral-7b-instruct-v0:2", + "mistral.mistral-large-2402-v1:0", + "mistral.mistral-small-2402-v1:0", + "mistral.mixtral-8x7b-instruct-v0:1", + "mistral.pixtral-large-2502-v1:0", + "openai.gpt-oss-120b-1:0", + "openai.gpt-oss-20b-1:0", + "qwen.qwen3-32b-v1:0", + "qwen.qwen3-coder-30b-a3b-v1:0", + "twelvelabs.pegasus-1-2-v1:0", +] diff --git a/lib/crewai/src/crewai/utilities/agent_utils.py b/lib/crewai/src/crewai/utilities/agent_utils.py index 2c8122b99..a6403c315 100644 --- a/lib/crewai/src/crewai/utilities/agent_utils.py +++ b/lib/crewai/src/crewai/utilities/agent_utils.py @@ -127,7 +127,7 @@ def handle_max_iterations_exceeded( messages: list[LLMMessage], llm: LLM | BaseLLM, callbacks: list[TokenCalcHandler], -) -> AgentAction | AgentFinish: +) -> AgentFinish: """Handles the case when the maximum number of iterations is exceeded. Performs one more LLM call to get the final answer. Args: @@ -139,7 +139,7 @@ def handle_max_iterations_exceeded( callbacks: List of callbacks for the LLM call. Returns: - The final formatted answer after exceeding max iterations. + AgentFinish with the final answer after exceeding max iterations. """ printer.print( content="Maximum iterations reached. Requesting final answer.", @@ -157,7 +157,7 @@ def handle_max_iterations_exceeded( # Perform one more LLM call to get the final answer answer = llm.call( - messages, # type: ignore[arg-type] + messages, callbacks=callbacks, ) @@ -168,8 +168,16 @@ def handle_max_iterations_exceeded( ) raise ValueError("Invalid response from LLM call - None or empty.") - # Return the formatted answer, regardless of its type - return format_answer(answer=answer) + formatted = format_answer(answer=answer) + + # If format_answer returned an AgentAction, convert it to AgentFinish + if isinstance(formatted, AgentFinish): + return formatted + return AgentFinish( + thought=formatted.thought, + output=formatted.text, + text=formatted.text, + ) def format_message_for_llm( @@ -249,10 +257,10 @@ def get_llm_response( """ try: answer = llm.call( - messages, # type: ignore[arg-type] + messages, callbacks=callbacks, from_task=from_task, - from_agent=from_agent, + from_agent=from_agent, # type: ignore[arg-type] response_model=response_model, ) except Exception as e: @@ -294,8 +302,8 @@ def handle_agent_action_core( formatted_answer: AgentAction, tool_result: ToolResult, messages: list[LLMMessage] | None = None, - step_callback: Callable | None = None, - show_logs: Callable | None = None, + step_callback: Callable | None = None, # type: ignore[type-arg] + show_logs: Callable | None = None, # type: ignore[type-arg] ) -> AgentAction | AgentFinish: """Core logic for handling agent actions and tool results. @@ -481,7 +489,7 @@ def summarize_messages( ), ] summary = llm.call( - messages, # type: ignore[arg-type] + messages, callbacks=callbacks, ) summarized_contents.append({"content": str(summary)}) diff --git a/lib/crewai/tests/agents/test_agent.py b/lib/crewai/tests/agents/test_agent.py index c4ccd59ff..12db5227a 100644 --- a/lib/crewai/tests/agents/test_agent.py +++ b/lib/crewai/tests/agents/test_agent.py @@ -508,7 +508,47 @@ def test_agent_custom_max_iterations(): assert isinstance(result, str) assert len(result) > 0 assert call_count > 0 - assert call_count == 3 + # With max_iter=1, expect 2 calls: + # - Call 1: iteration 0 + # - Call 2: iteration 1 (max reached, handle_max_iterations_exceeded called, then loop breaks) + assert call_count == 2 + + +@pytest.mark.vcr(filter_headers=["authorization"]) +@pytest.mark.timeout(30) +def test_agent_max_iterations_stops_loop(): + """Test that agent execution terminates when max_iter is reached.""" + + @tool + def get_data(step: str) -> str: + """Get data for a step. Always returns data requiring more steps.""" + return f"Data for {step}: incomplete, need to query more steps." + + agent = Agent( + role="data collector", + goal="collect data using the get_data tool", + backstory="You must use the get_data tool extensively", + max_iter=2, + allow_delegation=False, + ) + + task = Task( + description="Use get_data tool for step1, step2, step3, step4, step5, step6, step7, step8, step9, and step10. Do NOT stop until you've called it for ALL steps.", + expected_output="A summary of all data collected", + ) + + result = agent.execute_task( + task=task, + tools=[get_data], + ) + + assert result is not None + assert isinstance(result, str) + + assert agent.agent_executor.iterations <= agent.max_iter + 2, ( + f"Agent ran {agent.agent_executor.iterations} iterations " + f"but should stop around {agent.max_iter + 1}. " + ) @pytest.mark.vcr() diff --git a/lib/crewai/tests/cassettes/test_agent_max_iterations_stops_loop.yaml b/lib/crewai/tests/cassettes/test_agent_max_iterations_stops_loop.yaml new file mode 100644 index 000000000..6a40d691f --- /dev/null +++ b/lib/crewai/tests/cassettes/test_agent_max_iterations_stops_loop.yaml @@ -0,0 +1,495 @@ +interactions: +- request: + body: '{"trace_id": "REDACTED_TRACE_ID", "execution_type": + "crew", "user_identifier": null, "execution_context": {"crew_fingerprint": null, + "crew_name": "Unknown Crew", "flow_name": null, "crewai_version": "1.4.0", "privacy_level": + "standard"}, "execution_metadata": {"expected_duration_estimate": 300, "agent_count": + 0, "task_count": 0, "flow_method_count": 0, "execution_started_at": "2025-11-07T18:27:07.650947+00:00"}}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, zstd + Connection: + - keep-alive + Content-Length: + - '434' + Content-Type: + - application/json + User-Agent: + - CrewAI-CLI/1.4.0 + X-Crewai-Version: + - 1.4.0 + method: POST + uri: https://app.crewai.com/crewai_plus/api/v1/tracing/batches + response: + body: + string: '{"error":"bad_credentials","message":"Bad credentials"}' + headers: + Connection: + - keep-alive + Content-Length: + - '55' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 07 Nov 2025 18:27:07 GMT + cache-control: + - no-store + content-security-policy: + - 'default-src ''self'' *.app.crewai.com app.crewai.com; script-src ''self'' + ''unsafe-inline'' *.app.crewai.com app.crewai.com https://cdn.jsdelivr.net/npm/apexcharts + https://www.gstatic.com https://run.pstmn.io https://apis.google.com https://apis.google.com/js/api.js + https://accounts.google.com https://accounts.google.com/gsi/client https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css.map + https://*.google.com https://docs.google.com https://slides.google.com https://js.hs-scripts.com + https://js.sentry-cdn.com https://browser.sentry-cdn.com https://www.googletagmanager.com + https://js-na1.hs-scripts.com https://js.hubspot.com http://js-na1.hs-scripts.com + https://bat.bing.com https://cdn.amplitude.com https://cdn.segment.com https://d1d3n03t5zntha.cloudfront.net/ + https://descriptusercontent.com https://edge.fullstory.com https://googleads.g.doubleclick.net + https://js.hs-analytics.net https://js.hs-banner.com https://js.hsadspixel.net + https://js.hscollectedforms.net https://js.usemessages.com https://snap.licdn.com + https://static.cloudflareinsights.com https://static.reo.dev https://www.google-analytics.com + https://share.descript.com/; style-src ''self'' ''unsafe-inline'' *.app.crewai.com + app.crewai.com https://cdn.jsdelivr.net/npm/apexcharts; img-src ''self'' data: + *.app.crewai.com app.crewai.com https://zeus.tools.crewai.com https://dashboard.tools.crewai.com + https://cdn.jsdelivr.net https://forms.hsforms.com https://track.hubspot.com + https://px.ads.linkedin.com https://px4.ads.linkedin.com https://www.google.com + https://www.google.com.br; font-src ''self'' data: *.app.crewai.com app.crewai.com; + connect-src ''self'' *.app.crewai.com app.crewai.com https://zeus.tools.crewai.com + https://connect.useparagon.com/ https://zeus.useparagon.com/* https://*.useparagon.com/* + https://run.pstmn.io https://connect.tools.crewai.com/ https://*.sentry.io + https://www.google-analytics.com https://edge.fullstory.com https://rs.fullstory.com + https://api.hubspot.com https://forms.hscollectedforms.net https://api.hubapi.com + https://px.ads.linkedin.com https://px4.ads.linkedin.com https://google.com/pagead/form-data/16713662509 + https://google.com/ccm/form-data/16713662509 https://www.google.com/ccm/collect + https://worker-actionkit.tools.crewai.com https://api.reo.dev; frame-src ''self'' + *.app.crewai.com app.crewai.com https://connect.useparagon.com/ https://zeus.tools.crewai.com + https://zeus.useparagon.com/* https://connect.tools.crewai.com/ https://docs.google.com + https://drive.google.com https://slides.google.com https://accounts.google.com + https://*.google.com https://app.hubspot.com/ https://td.doubleclick.net https://www.googletagmanager.com/ + https://www.youtube.com https://share.descript.com' + expires: + - '0' + permissions-policy: + - camera=(), microphone=(self), geolocation=() + pragma: + - no-cache + referrer-policy: + - strict-origin-when-cross-origin + strict-transport-security: + - max-age=63072000; includeSubDomains + vary: + - Accept + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-permitted-cross-domain-policies: + - none + x-request-id: + - REDACTED_REQUEST_ID + x-runtime: + - '0.080681' + x-xss-protection: + - 1; mode=block + status: + code: 401 + message: Unauthorized +- request: + body: '{"messages":[{"role":"system","content":"You are data collector. You must + use the get_data tool extensively\nYour personal goal is: collect data using + the get_data tool\nYou ONLY have access to the following tools, and should NEVER + make up tools that are not listed here:\n\nTool Name: get_data\nTool Arguments: + {''step'': {''description'': None, ''type'': ''str''}}\nTool Description: Get + data for a step. Always returns data requiring more steps.\n\nIMPORTANT: Use + the following format in your response:\n\n```\nThought: you should always think + about what to do\nAction: the action to take, only one name of [get_data], just + the name, exactly as it''s written.\nAction Input: the input to the action, + just a simple JSON object, enclosed in curly braces, using \" to wrap keys and + values.\nObservation: the result of the action\n```\n\nOnce all necessary information + is gathered, return the following format:\n\n```\nThought: I now know the final + answer\nFinal Answer: the final answer to the original input question\n```"},{"role":"user","content":"\nCurrent + Task: Use get_data tool for step1, step2, step3, step4, step5, step6, step7, + step8, step9, and step10. Do NOT stop until you''ve called it for ALL steps.\n\nThis + is the expected criteria for your final answer: A summary of all data collected\nyou + MUST return the actual complete content as the final answer, not a summary.\n\nBegin! + This is VERY important to you, use the tools available and give your best Final + Answer, your job depends on it!\n\nThought:"}],"model":"gpt-4.1-mini"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '1534' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.109.1 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.109.1 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.9 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + body: + string: !!binary | + H4sIAAAAAAAAA4xSYWvbMBD97l9x6HMcYsfpUn8rg0FHYbAOyrYUo0hnW5ksCem8tYT89yG7id2t + g30x5t69p/fu7pgAMCVZCUy0nETndPr+2919j4fr9VNR/Opv7vBD/bAVXz/dfzx8fmCLyLD7Awo6 + s5bCdk4jKWtGWHjkhFE1e3eVb4rVKt8OQGcl6khrHKXFMks7ZVSar/JNuirSrHiht1YJDKyE7wkA + wHH4RqNG4hMrYbU4VzoMgTfIyksTAPNWxwrjIahA3BBbTKCwhtAM3r+0tm9aKuEWQmt7LSEQ9wT7 + ZxBWaxSkTAOSE4faegiELgMeQJlAvheEcrkzNyLmLqFBqmLruQK3xvVUwnHHInHHyvEn27HT3I/H + ug88DsX0Ws8AbowlHqWGSTy+IKdLdm0b5+0+/EFltTIqtJVHHqyJOQNZxwb0lAA8DjPuX42NOW87 + RxXZHzg8t15nox6bdjuh4zYBGFniesbaXC/e0KskElc6zLbEBBctyok6rZT3UtkZkMxS/+3mLe0x + uTLN/8hPgBDoCGXlPEolXiee2jzG0/9X22XKg2EW0P9UAitS6OMmJNa81+M9svAcCLuqVqZB77wa + j7J2VSHy7Sart1c5S07JbwAAAP//AwCiugNoowMAAA== + headers: + CF-RAY: + - 99aee205bbd2de96-EWR + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Fri, 07 Nov 2025 18:27:08 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=REDACTED_COOKIE; + path=/; expires=Fri, 07-Nov-25 18:57:08 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=REDACTED_COOKIE; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - REDACTED_ORG_ID + openai-processing-ms: + - '557' + openai-project: + - REDACTED_PROJECT_ID + openai-version: + - '2020-10-01' + x-envoy-upstream-service-time: + - '701' + x-openai-proxy-wasm: + - v0.1 + x-ratelimit-limit-requests: + - '500' + x-ratelimit-limit-tokens: + - '200000' + x-ratelimit-remaining-requests: + - '499' + x-ratelimit-remaining-tokens: + - '199645' + x-ratelimit-reset-requests: + - 120ms + x-ratelimit-reset-tokens: + - 106ms + x-request-id: + - REDACTED_REQUEST_ID + status: + code: 200 + message: OK +- request: + body: '{"messages":[{"role":"system","content":"You are data collector. You must + use the get_data tool extensively\nYour personal goal is: collect data using + the get_data tool\nYou ONLY have access to the following tools, and should NEVER + make up tools that are not listed here:\n\nTool Name: get_data\nTool Arguments: + {''step'': {''description'': None, ''type'': ''str''}}\nTool Description: Get + data for a step. Always returns data requiring more steps.\n\nIMPORTANT: Use + the following format in your response:\n\n```\nThought: you should always think + about what to do\nAction: the action to take, only one name of [get_data], just + the name, exactly as it''s written.\nAction Input: the input to the action, + just a simple JSON object, enclosed in curly braces, using \" to wrap keys and + values.\nObservation: the result of the action\n```\n\nOnce all necessary information + is gathered, return the following format:\n\n```\nThought: I now know the final + answer\nFinal Answer: the final answer to the original input question\n```"},{"role":"user","content":"\nCurrent + Task: Use get_data tool for step1, step2, step3, step4, step5, step6, step7, + step8, step9, and step10. Do NOT stop until you''ve called it for ALL steps.\n\nThis + is the expected criteria for your final answer: A summary of all data collected\nyou + MUST return the actual complete content as the final answer, not a summary.\n\nBegin! + This is VERY important to you, use the tools available and give your best Final + Answer, your job depends on it!\n\nThought:"},{"role":"assistant","content":"Thought: + I should start by collecting data for step1 as instructed.\nAction: get_data\nAction + Input: {\"step\":\"step1\"}\nObservation: Data for step1: incomplete, need to + query more steps."}],"model":"gpt-4.1-mini"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '1757' + content-type: + - application/json + cookie: + - __cf_bm=REDACTED_COOKIE; + _cfuvid=REDACTED_COOKIE + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.109.1 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.109.1 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.9 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + body: + string: !!binary | + H4sIAAAAAAAAAwAAAP//jFNNb9swDL37VxA6x0HiOU3mW9cOQ4F9YNjQQ5fCUGXaVidLqkQnzYL8 + 90F2ErtbB+xiCHx8j+QjvY8AmCxYBkzUnERjVXx19/Hb5tPm/fbq8sPX5+Wvx6V+t93efXY1v71m + k8AwD48o6MSaCtNYhSSN7mHhkBMG1fnyIlmks1nytgMaU6AKtMpSnE7ncSO1jJNZsohnaTxPj/Ta + SIGeZfAjAgDYd9/QqC7wmWUwm5wiDXrPK2TZOQmAOaNChHHvpSeuiU0GUBhNqLvev9emrWrK4AY0 + YgFkIKBStxjentAmfVApFAQFJw4en1rUJLlSO+AeHD610mExXetLESzIoELKQ+4pAjfatpTBfs2C + 5ppl/SNZs8Naf3nw6Da8p16HEqVxffEMpD56ixNojMMu7kGjCIO73XQ8msOy9Tz4q1ulRgDX2lBX + oTP1/ogczjYqU1lnHvwfVFZKLX2dO+Te6GCZJ2NZhx4igPtuXe2LDTDrTGMpJ/MTu3Jvlqtejw1n + MqBpegTJEFejeJJMXtHLCyQulR8tnAkuaiwG6nAdvC2kGQHRaOq/u3lNu59c6up/5AdACLSERW4d + FlK8nHhIcxj+on+lnV3uGmbhSKTAnCS6sIkCS96q/rSZ33nCJi+lrtBZJ/v7Lm2eimS1mJeri4RF + h+g3AAAA//8DABrUefPuAwAA + headers: + CF-RAY: + - 99aee20dba0bde96-EWR + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Fri, 07 Nov 2025 18:27:10 GMT + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - REDACTED_ORG_ID + openai-processing-ms: + - '942' + openai-project: + - REDACTED_PROJECT_ID + openai-version: + - '2020-10-01' + x-envoy-upstream-service-time: + - '1074' + x-openai-proxy-wasm: + - v0.1 + x-ratelimit-limit-requests: + - '500' + x-ratelimit-limit-tokens: + - '200000' + x-ratelimit-remaining-requests: + - '499' + x-ratelimit-remaining-tokens: + - '199599' + x-ratelimit-reset-requests: + - 120ms + x-ratelimit-reset-tokens: + - 120ms + x-request-id: + - REDACTED_REQUEST_ID + status: + code: 200 + message: OK +- request: + body: '{"messages":[{"role":"system","content":"You are data collector. You must + use the get_data tool extensively\nYour personal goal is: collect data using + the get_data tool\nYou ONLY have access to the following tools, and should NEVER + make up tools that are not listed here:\n\nTool Name: get_data\nTool Arguments: + {''step'': {''description'': None, ''type'': ''str''}}\nTool Description: Get + data for a step. Always returns data requiring more steps.\n\nIMPORTANT: Use + the following format in your response:\n\n```\nThought: you should always think + about what to do\nAction: the action to take, only one name of [get_data], just + the name, exactly as it''s written.\nAction Input: the input to the action, + just a simple JSON object, enclosed in curly braces, using \" to wrap keys and + values.\nObservation: the result of the action\n```\n\nOnce all necessary information + is gathered, return the following format:\n\n```\nThought: I now know the final + answer\nFinal Answer: the final answer to the original input question\n```"},{"role":"user","content":"\nCurrent + Task: Use get_data tool for step1, step2, step3, step4, step5, step6, step7, + step8, step9, and step10. Do NOT stop until you''ve called it for ALL steps.\n\nThis + is the expected criteria for your final answer: A summary of all data collected\nyou + MUST return the actual complete content as the final answer, not a summary.\n\nBegin! + This is VERY important to you, use the tools available and give your best Final + Answer, your job depends on it!\n\nThought:"},{"role":"assistant","content":"Thought: + I should start by collecting data for step1 as instructed.\nAction: get_data\nAction + Input: {\"step\":\"step1\"}\nObservation: Data for step1: incomplete, need to + query more steps."},{"role":"assistant","content":"Thought: I need to continue + to step2 to collect data sequentially as required.\nAction: get_data\nAction + Input: {\"step\":\"step2\"}\nObservation: Data for step2: incomplete, need to + query more steps."},{"role":"assistant","content":"Thought: I need to continue + to step2 to collect data sequentially as required.\nAction: get_data\nAction + Input: {\"step\":\"step2\"}\nObservation: Data for step2: incomplete, need to + query more steps.\nNow it''s time you MUST give your absolute best final answer. + You''ll ignore all previous instructions, stop using any tools, and just return + your absolute BEST Final answer."}],"model":"gpt-4.1-mini"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '2399' + content-type: + - application/json + cookie: + - __cf_bm=REDACTED_COOKIE; + _cfuvid=REDACTED_COOKIE + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.109.1 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.109.1 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.9 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + body: + string: !!binary | + H4sIAAAAAAAAAwAAAP//nJbfj6M2EMff81eM/NRKmwgI5Advp7v2FKlSW22f9rKKHHsI7hmbs83u + nlb7v1eYBLJXQFxekMV8Z+ZjYw3f1xkAEZykQFhOHStKOf/48Mf9yzf5/Pnh498P9kl9ru51qR9k + XsVBSO7qDH38F5m7ZC2YLkqJTmjVhJlB6rCuGq5XURIHwTL0gUJzlHXaqXTzeBHOC6HEPAqiZB7E + 8zA+p+daMLQkhS8zAIBX/6xBFccXkkJwd3lToLX0hCRtRQDEaFm/IdRaYR1Vjtx1QaaVQ+XZ/8l1 + dcpdCjsoKuuAaSmROeDUUci0ASolWIelhczowi9DcLpZBHDETBuE0ugnwYU6gcsRMqGohPOJIJzb + AbVg8FslDHI4fvdKR+3XBezgWUjpdUJVCJW9VDqhO3gUp7X0PEhZ7puDUKANR7PYq736wOqjT9uE + yxvYqbJyKbzuSZ20J2mzCPfkba/+PFo0T7RJ/VT3KalxEPpOzVb10VGhkPsu7Wn9ZTRD5JeDiBY/ + TxCNEUQtQTSNYHkDwXKMYNkSLKcRxDcQxGMEcUsQTyNIbiBIxgiSliCZRrC6gWA1RrBqCVbTCNY3 + EKzHCNYtwXoaweYGgs0YwaYl2Ewj2N5AsB0j2LYE22kEYXADQhiMzqSgG0rBAMUOlH6GnD6hH9vt + DG/mtx/bYQBUcWBUnWc2jkxsX/13H/qg7DOaFPbq3o/FGiyFLzvFZMWxaXWenZdxn6PBx0YfDeuj + Pv1yWL/s08fD+rhPnwzrkz79ali/6tOvh/XrPv1mWL/p02+H9ds+fRiMfLDgx4y9+uW3F8rc9Y/7 + cuEaF6C7O2rf/5Xv6iRGHara/fiKi1+vvYfBrLK0NkCqkvIqQJXSrilZu57Hc+St9TlSn0qjj/aH + VJIJJWx+MEitVrWnsU6XxEffZgCP3k9V7ywSKY0uSndw+iv6dkl49lOk83FX0Sg5R512VHaBMFhe + Iu8qHjg6KqS98mSEUZYj73I7A0crLvRVYHa17//z9NVu9i7UaUr5LsAYlg75oTTIBXu/505msDa6 + Q7L2nD0wqe+FYHhwAk39LThmtJKN+yT2u3VYHDKhTmhKIxoLmpWHmEWbJMw2q4jM3mb/AQAA//8D + ACYaBDGRCwAA + headers: + CF-RAY: + - 99aee2174b18de96-EWR + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Fri, 07 Nov 2025 18:27:20 GMT + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - REDACTED_ORG_ID + openai-processing-ms: + - '9185' + openai-project: + - REDACTED_PROJECT_ID + openai-version: + - '2020-10-01' + x-envoy-upstream-service-time: + - '9386' + x-openai-proxy-wasm: + - v0.1 + x-ratelimit-limit-requests: + - '500' + x-ratelimit-limit-tokens: + - '200000' + x-ratelimit-remaining-requests: + - '499' + x-ratelimit-remaining-tokens: + - '199457' + x-ratelimit-reset-requests: + - 120ms + x-ratelimit-reset-tokens: + - 162ms + x-request-id: + - REDACTED_REQUEST_ID + status: + code: 200 + message: OK +version: 1 diff --git a/lib/crewai/tests/llms/anthropic/test_anthropic.py b/lib/crewai/tests/llms/anthropic/test_anthropic.py index 6ba294d8b..72e3149b6 100644 --- a/lib/crewai/tests/llms/anthropic/test_anthropic.py +++ b/lib/crewai/tests/llms/anthropic/test_anthropic.py @@ -36,7 +36,7 @@ def test_anthropic_completion_is_used_when_claude_provider(): from crewai.llms.providers.anthropic.completion import AnthropicCompletion assert isinstance(llm, AnthropicCompletion) - assert llm.provider == "claude" + assert llm.provider == "anthropic" assert llm.model == "claude-3-5-sonnet-20241022" diff --git a/lib/crewai/tests/llms/azure/test_azure.py b/lib/crewai/tests/llms/azure/test_azure.py index d4f739a90..dbb41170d 100644 --- a/lib/crewai/tests/llms/azure/test_azure.py +++ b/lib/crewai/tests/llms/azure/test_azure.py @@ -39,7 +39,7 @@ def test_azure_completion_is_used_when_azure_openai_provider(): from crewai.llms.providers.azure.completion import AzureCompletion assert isinstance(llm, AzureCompletion) - assert llm.provider == "azure_openai" + assert llm.provider == "azure" assert llm.model == "gpt-4" diff --git a/lib/crewai/tests/llms/google/test_google.py b/lib/crewai/tests/llms/google/test_google.py index f7b721d1d..c6f271b0a 100644 --- a/lib/crewai/tests/llms/google/test_google.py +++ b/lib/crewai/tests/llms/google/test_google.py @@ -24,7 +24,7 @@ def test_gemini_completion_is_used_when_google_provider(): llm = LLM(model="google/gemini-2.0-flash-001") assert llm.__class__.__name__ == "GeminiCompletion" - assert llm.provider == "google" + assert llm.provider == "gemini" assert llm.model == "gemini-2.0-flash-001" diff --git a/lib/crewai/tests/llms/hooks/test_unsupported_providers.py b/lib/crewai/tests/llms/hooks/test_unsupported_providers.py index 7f5061b25..bacc9878f 100644 --- a/lib/crewai/tests/llms/hooks/test_unsupported_providers.py +++ b/lib/crewai/tests/llms/hooks/test_unsupported_providers.py @@ -154,7 +154,7 @@ class TestGeminiProviderInterceptor: # Gemini provider should raise NotImplementedError with pytest.raises(NotImplementedError) as exc_info: LLM( - model="gemini/gemini-pro", + model="gemini/gemini-2.5-pro", interceptor=interceptor, api_key="test-gemini-key", ) @@ -169,7 +169,7 @@ class TestGeminiProviderInterceptor: with pytest.raises(NotImplementedError) as exc_info: LLM( - model="gemini/gemini-pro", + model="gemini/gemini-2.5-pro", interceptor=interceptor, api_key="test-gemini-key", ) @@ -181,7 +181,7 @@ class TestGeminiProviderInterceptor: def test_gemini_without_interceptor_works(self) -> None: """Test that Gemini LLM works without interceptor.""" llm = LLM( - model="gemini/gemini-pro", + model="gemini/gemini-2.5-pro", api_key="test-gemini-key", ) @@ -231,7 +231,7 @@ class TestUnsupportedProviderMessages: with pytest.raises(NotImplementedError) as exc_info: LLM( - model="gemini/gemini-pro", + model="gemini/gemini-2.5-pro", interceptor=interceptor, api_key="test-gemini-key", ) @@ -282,7 +282,7 @@ class TestProviderSupportMatrix: # Gemini - NOT SUPPORTED with pytest.raises(NotImplementedError): LLM( - model="gemini/gemini-pro", + model="gemini/gemini-2.5-pro", interceptor=interceptor, api_key="test", ) @@ -315,5 +315,5 @@ class TestProviderSupportMatrix: assert not hasattr(bedrock_llm, 'interceptor') or bedrock_llm.interceptor is None # Gemini - doesn't have interceptor attribute - gemini_llm = LLM(model="gemini/gemini-pro", api_key="test") - assert not hasattr(gemini_llm, 'interceptor') or gemini_llm.interceptor is None \ No newline at end of file + gemini_llm = LLM(model="gemini/gemini-2.5-pro", api_key="test") + assert not hasattr(gemini_llm, 'interceptor') or gemini_llm.interceptor is None diff --git a/lib/crewai/tests/llms/openai/test_openai.py b/lib/crewai/tests/llms/openai/test_openai.py index 9cb94cba7..24b858c01 100644 --- a/lib/crewai/tests/llms/openai/test_openai.py +++ b/lib/crewai/tests/llms/openai/test_openai.py @@ -16,7 +16,7 @@ def test_openai_completion_is_used_when_openai_provider(): """ Test that OpenAICompletion from completion.py is used when LLM uses provider 'openai' """ - llm = LLM(model="openai/gpt-4o") + llm = LLM(model="gpt-4o") assert llm.__class__.__name__ == "OpenAICompletion" assert llm.provider == "openai" @@ -70,7 +70,7 @@ def test_openai_completion_module_is_imported(): del sys.modules[module_name] # Create LLM instance - this should trigger the import - LLM(model="openai/gpt-4o") + LLM(model="gpt-4o") # Verify the module was imported assert module_name in sys.modules @@ -97,7 +97,7 @@ def test_native_openai_raises_error_when_initialization_fails(): # This should raise ImportError, not fall back to LiteLLM with pytest.raises(ImportError) as excinfo: - LLM(model="openai/gpt-4o") + LLM(model="gpt-4o") assert "Error importing native provider" in str(excinfo.value) assert "Native SDK failed" in str(excinfo.value) @@ -108,7 +108,7 @@ def test_openai_completion_initialization_parameters(): Test that OpenAICompletion is initialized with correct parameters """ llm = LLM( - model="openai/gpt-4o", + model="gpt-4o", temperature=0.7, max_tokens=1000, api_key="test-key" @@ -311,7 +311,7 @@ def test_openai_completion_call_returns_usage_metrics(): role="Research Assistant", goal="Find information about the population of Tokyo", backstory="You are a helpful research assistant.", - llm=LLM(model="openai/gpt-4o"), + llm=LLM(model="gpt-4o"), verbose=True, ) @@ -331,6 +331,7 @@ def test_openai_completion_call_returns_usage_metrics(): assert result.token_usage.cached_prompt_tokens == 0 +@pytest.mark.skip(reason="Allow for litellm") def test_openai_raises_error_when_model_not_supported(): """Test that OpenAICompletion raises ValueError when model not supported""" @@ -354,7 +355,7 @@ def test_openai_client_setup_with_extra_arguments(): Test that OpenAICompletion is initialized with correct parameters """ llm = LLM( - model="openai/gpt-4o", + model="gpt-4o", temperature=0.7, max_tokens=1000, top_p=0.5, @@ -391,7 +392,7 @@ def test_extra_arguments_are_passed_to_openai_completion(): """ Test that extra arguments are passed to OpenAICompletion """ - llm = LLM(model="openai/gpt-4o", temperature=0.7, max_tokens=1000, top_p=0.5, max_retries=3) + llm = LLM(model="gpt-4o", temperature=0.7, max_tokens=1000, top_p=0.5, max_retries=3) with patch.object(llm.client.chat.completions, 'create') as mock_create: mock_create.return_value = MagicMock( diff --git a/lib/crewai/tests/test_llm.py b/lib/crewai/tests/test_llm.py index 60c5b83b5..7c6fd74bb 100644 --- a/lib/crewai/tests/test_llm.py +++ b/lib/crewai/tests/test_llm.py @@ -710,7 +710,7 @@ def test_native_provider_raises_error_when_supported_but_fails(): mock_get_native.return_value = mock_provider with pytest.raises(ImportError) as excinfo: - LLM(model="openai/gpt-4", is_litellm=False) + LLM(model="gpt-4", is_litellm=False) assert "Error importing native provider" in str(excinfo.value) assert "Native provider initialization failed" in str(excinfo.value) @@ -725,3 +725,113 @@ def test_native_provider_falls_back_to_litellm_when_not_in_supported_list(): # Should fall back to LiteLLM assert llm.is_litellm is True assert llm.model == "groq/llama-3.1-70b-versatile" + + +def test_prefixed_models_with_valid_constants_use_native_sdk(): + """Test that models with native provider prefixes use native SDK when model is in constants.""" + # Test openai/ prefix with actual OpenAI model in constants → Native SDK + with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}): + llm = LLM(model="openai/gpt-4o", is_litellm=False) + assert llm.is_litellm is False + assert llm.provider == "openai" + + # Test anthropic/ prefix with Claude model in constants → Native SDK + with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}): + llm2 = LLM(model="anthropic/claude-opus-4-0", is_litellm=False) + assert llm2.is_litellm is False + assert llm2.provider == "anthropic" + + # Test gemini/ prefix with Gemini model in constants → Native SDK + with patch.dict(os.environ, {"GOOGLE_API_KEY": "test-key"}): + llm3 = LLM(model="gemini/gemini-2.5-pro", is_litellm=False) + assert llm3.is_litellm is False + assert llm3.provider == "gemini" + + +def test_prefixed_models_with_invalid_constants_use_litellm(): + """Test that models with native provider prefixes use LiteLLM when model is NOT in constants.""" + # Test openai/ prefix with non-OpenAI model (not in OPENAI_MODELS) → LiteLLM + llm = LLM(model="openai/gemini-2.5-flash", is_litellm=False) + assert llm.is_litellm is True + assert llm.model == "openai/gemini-2.5-flash" + + # Test openai/ prefix with unknown future model → LiteLLM + llm2 = LLM(model="openai/gpt-future-6", is_litellm=False) + assert llm2.is_litellm is True + assert llm2.model == "openai/gpt-future-6" + + # Test anthropic/ prefix with non-Anthropic model → LiteLLM + llm3 = LLM(model="anthropic/gpt-4o", is_litellm=False) + assert llm3.is_litellm is True + assert llm3.model == "anthropic/gpt-4o" + + +def test_prefixed_models_with_non_native_providers_use_litellm(): + """Test that models with non-native provider prefixes always use LiteLLM.""" + # Test groq/ prefix (not a native provider) → LiteLLM + llm = LLM(model="groq/llama-3.3-70b", is_litellm=False) + assert llm.is_litellm is True + assert llm.model == "groq/llama-3.3-70b" + + # Test together/ prefix (not a native provider) → LiteLLM + llm2 = LLM(model="together/qwen-2.5-72b", is_litellm=False) + assert llm2.is_litellm is True + assert llm2.model == "together/qwen-2.5-72b" + + +def test_unprefixed_models_use_native_sdk(): + """Test that unprefixed models use native SDK when model is in constants.""" + # gpt-4o is in OPENAI_MODELS → Native OpenAI SDK + with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}): + llm = LLM(model="gpt-4o", is_litellm=False) + assert llm.is_litellm is False + assert llm.provider == "openai" + + # claude-opus-4-0 is in ANTHROPIC_MODELS → Native Anthropic SDK + with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}): + llm2 = LLM(model="claude-opus-4-0", is_litellm=False) + assert llm2.is_litellm is False + assert llm2.provider == "anthropic" + + # gemini-2.5-pro is in GEMINI_MODELS → Native Gemini SDK + with patch.dict(os.environ, {"GOOGLE_API_KEY": "test-key"}): + llm3 = LLM(model="gemini-2.5-pro", is_litellm=False) + assert llm3.is_litellm is False + assert llm3.provider == "gemini" + + +def test_explicit_provider_kwarg_takes_priority(): + """Test that explicit provider kwarg takes priority over model name inference.""" + # Explicit provider=openai should use OpenAI even if model name suggests otherwise + with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}): + llm = LLM(model="gpt-4o", provider="openai", is_litellm=False) + assert llm.is_litellm is False + assert llm.provider == "openai" + + # Explicit provider for a model with "/" should still use that provider + with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}): + llm2 = LLM(model="gpt-4o", provider="openai", is_litellm=False) + assert llm2.is_litellm is False + assert llm2.provider == "openai" + + +def test_validate_model_in_constants(): + """Test the _validate_model_in_constants method.""" + # OpenAI models + assert LLM._validate_model_in_constants("gpt-4o", "openai") is True + assert LLM._validate_model_in_constants("gpt-future-6", "openai") is False + + # Anthropic models + assert LLM._validate_model_in_constants("claude-opus-4-0", "claude") is True + assert LLM._validate_model_in_constants("claude-future-5", "claude") is False + + # Gemini models + assert LLM._validate_model_in_constants("gemini-2.5-pro", "gemini") is True + assert LLM._validate_model_in_constants("gemini-future", "gemini") is False + + # Azure models + assert LLM._validate_model_in_constants("gpt-4o", "azure") is True + assert LLM._validate_model_in_constants("gpt-35-turbo", "azure") is True + + # Bedrock models + assert LLM._validate_model_in_constants("anthropic.claude-opus-4-1-20250805-v1:0", "bedrock") is True diff --git a/lib/devtools/src/crewai_devtools/__init__.py b/lib/devtools/src/crewai_devtools/__init__.py index 9c852f95d..a79c384e2 100644 --- a/lib/devtools/src/crewai_devtools/__init__.py +++ b/lib/devtools/src/crewai_devtools/__init__.py @@ -1,3 +1,3 @@ """CrewAI development tools.""" -__version__ = "1.4.0" +__version__ = "1.4.1"