Repository: huggingface/smolagents

Issue:
[BUG] o4-mini and o3 don't support stop_sequences parameter
o4-mini and o3 don't support stop_sequences parameter.

this is used with litellm in the current `agents.py`

Discussion:
@alexub: Here is a hacky workaround:

```
import litellm
from smolagents import LiteLLMModel

# Patch litellm so it doesn't use the "stop" kwarg, which is unsupported by o3/o4-mini
_og_litellm_completion = litellm.completion

def _patched_litellm_completion(*args, **kwargs):
    print(f"Patched litellm completion called with args: {args} and kwargs: {kwargs}")
    kwargs_stop_removed = {k: v for k, v in kwargs.items() if k != 'stop'}
    return _og_litellm_completion(*args, **kwargs_stop_removed)

litellm.completion = _patched_litellm_completion


# Patch smolagents LiteLLMModel so it doesn't use the "stop_sequences" kwarg, which is unsupported by o3/o4-mini
def _patch_model_call(model: LiteLLMModel):
    _og_call = model.__call__
    def _patched_call(*args, **kwargs):
        print(f"Model {model.model_id} called with args: {args} and kwargs: {kwargs}")
        return _og_call(
            *args,
            **{k: v for k, v in kwargs.items() if k != 'stop_sequences'}
        )
    model.__call__ = _patched_call

def _maybe_patch_model_call(model: LiteLLMModel):
    if model.model_id.endswith('o3') or 'o4' in model.model_id:
        print(f"Patching model {model.model_id}")
        _patch_model_call(model)


BLITZ_MODEL_ID = "vertex_ai/gemini-2.0-flash-001"
FAST_MODEL_ID = "o4-mini"  # "openai/o4-mini"
SLOW_MODEL_ID = "o3"  # "openai/o3"


# Call _maybe_patch_model_call on every model we define
# It will patch the call for o3-mini and o4, and will not patch other models
BLITZ_MODEL = LiteLLMModel(
    model_id=BLITZ_MODEL_ID,
)
_maybe_patch_model_call(BLITZ_MODEL)

FAST_MODEL = LiteLLMModel(
    model_id=FAST_MODEL_ID,
)
_maybe_patch_model_call(FAST_MODEL)

SLOW_MODEL = LiteLLMModel(
    model_id=SLOW_MODEL_ID,
)
_maybe_patch_model_call(SLOW_MODEL)

# Now use your model in an agent...
```@albertvillanova: Thanks for reporting, @benediktstroebl.

We should definitely handle this.@joejoe2: Does smolagents work with model without stop sequence ?@bdeck8317: @albertvillanova still having this issue in smolagents==1.17.0 using litellm. Tried both ```o3-mini``` and ```o4-mini```

Solution patch:
diff --git a/README.md b/README.md
index 7862dc0..0484ae9 100644
--- a/README.md
+++ b/README.md
@@ -236,6 +236,14 @@ We've created [`CodeAgent`](https://huggingface.co/docs/smolagents/reference/age
 
 This comparison shows that open-source models can now take on the best closed models!
 
+## Security
+
+Security is a critical consideration when working with code-executing agents. Our library provides:
+- Sandboxed execution options using [E2B](https://e2b.dev/) or Docker
+- Best practices for running agent code securely
+
+For security policies, vulnerability reporting, and more information on secure agent execution, please see our [Security Policy](SECURITY.md).
+
 ## Contribute
 
 Everyone is welcome to contribute, get started with our [contribution guide](https://github.com/huggingface/smolagents/blob/main/CONTRIBUTING.md).
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..0a55a56
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,9 @@
+# Security Policy
+
+## Reporting a Vulnerability
+
+To report a security vulnerability, please contact: security@huggingface.co
+
+## Learning More About Security
+
+To learn more about running agents more securely, please see the [Secure Code Execution tutorial](docs/source/en/tutorials/secure_code_execution.mdx) which covers sandboxing with E2B and Docker.
diff --git a/examples/open_deep_research/scripts/text_web_browser.py b/examples/open_deep_research/scripts/text_web_browser.py
index ef40f85..2bca4fb 100644
--- a/examples/open_deep_research/scripts/text_web_browser.py
+++ b/examples/open_deep_research/scripts/text_web_browser.py
@@ -397,7 +397,7 @@ class VisitTool(Tool):
     inputs = {"url": {"type": "string", "description": "The relative or absolute url of the webpage to visit."}}
     output_type = "string"
 
-    def __init__(self, browser):
+    def __init__(self, browser=None):
         super().__init__()
         self.browser = browser
 
@@ -421,6 +421,8 @@ DO NOT use this tool for .pdf or .txt or .htm files: for these types of files us
         self.browser = browser
 
     def forward(self, url: str) -> str:
+        import requests
+
         if "arxiv" in url:
             url = url.replace("abs", "pdf")
         response = requests.get(url)
@@ -452,11 +454,13 @@ class ArchiveSearchTool(Tool):
     }
     output_type = "string"
 
-    def __init__(self, browser):
+    def __init__(self, browser=None):
         super().__init__()
         self.browser = browser
 
     def forward(self, url, date) -> str:
+        import requests
+
         no_timestamp_url = f"https://archive.org/wayback/available?url={url}"
         archive_url = no_timestamp_url + f"&timestamp={date}"
         response = requests.get(archive_url).json()
@@ -487,7 +491,7 @@ class PageUpTool(Tool):
     inputs = {}
     output_type = "string"
 
-    def __init__(self, browser):
+    def __init__(self, browser=None):
         super().__init__()
         self.browser = browser
 
@@ -505,7 +509,7 @@ class PageDownTool(Tool):
     inputs = {}
     output_type = "string"
 
-    def __init__(self, browser):
+    def __init__(self, browser=None):
         super().__init__()
         self.browser = browser
 
@@ -526,7 +530,7 @@ class FinderTool(Tool):
     }
     output_type = "string"
 
-    def __init__(self, browser):
+    def __init__(self, browser=None):
         super().__init__()
         self.browser = browser
 
@@ -549,7 +553,7 @@ class FindNextTool(Tool):
     inputs = {}
     output_type = "string"
 
-    def __init__(self, browser):
+    def __init__(self, browser=None):
         super().__init__()
         self.browser = browser
 
diff --git a/examples/open_deep_research/scripts/visual_qa.py b/examples/open_deep_research/scripts/visual_qa.py
index 51a4d02..7a01f34 100644
--- a/examples/open_deep_research/scripts/visual_qa.py
+++ b/examples/open_deep_research/scripts/visual_qa.py
@@ -94,9 +94,6 @@ def encode_image(image_path):
         return base64.b64encode(image_file.read()).decode("utf-8")
 
 
-headers = {"Content-Type": "application/json", "Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}"}
-
-
 def resize_image(image_path):
     img = PIL.Image.open(image_path)
     width, height = img.size
@@ -150,6 +147,12 @@ def visualizer(image_path: str, question: Optional[str] = None) -> str:
         image_path: The path to the image on which to answer the question. This should be a local path to downloaded image.
         question: The question to answer.
     """
+    import mimetypes
+    import os
+
+    import requests
+
+    from .visual_qa import encode_image
 
     add_note = False
     if not question:
@@ -174,6 +177,7 @@ def visualizer(image_path: str, question: Optional[str] = None) -> str:
         ],
         "max_tokens": 1000,
     }
+    headers = {"Content-Type": "application/json", "Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}"}
     response = requests.post("https://api.openai.com/v1/chat/completions", headers=headers, json=payload)
     try:
         output = response.json()["choices"][0]["message"]["content"]
diff --git a/src/smolagents/agents.py b/src/smolagents/agents.py
index 0cb9cbb..3f40c5c 100644
--- a/src/smolagents/agents.py
+++ b/src/smolagents/agents.py
@@ -292,7 +292,9 @@ class MultiStepAgent(ABC):
 
         Args:
             task (`str`): Task to perform.
-            stream (`bool`): Whether to run in a streaming way.
+            stream (`bool`): Whether to run in streaming mode.
+                If `True`, returns a generator that yields each step as it is executed. You must iterate over this generator to process the individual steps (e.g., using a for loop or `next()`).
+                If `False`, executes all steps internally and returns only the final answer after completion.
             reset (`bool`): Whether to reset the conversation or keep it going from previous run.
             images (`list[PIL.Image.Image]`, *optional*): Image(s) objects.
             additional_args (`dict`, *optional*): Any other variables that you want to pass to the agent run, for instance images or dataframes. Give them clear names!
diff --git a/src/smolagents/local_python_executor.py b/src/smolagents/local_python_executor.py
index 7bb8603..6a80941 100644
--- a/src/smolagents/local_python_executor.py
+++ b/src/smolagents/local_python_executor.py
@@ -1273,7 +1273,7 @@ def evaluate_ast(
         static_tools (`Dict[str, Callable]`):
             Functions that may be called during the evaluation. Trying to change one of these static_tools will raise an error.
         custom_tools (`Dict[str, Callable]`):
-            Functions that may be called during the evaluation. These static_tools can be overwritten.
+            Functions that may be called during the evaluation. These custom_tools can be overwritten.
         authorized_imports (`List[str]`):
             The list of modules that can be imported by the code. By default, only a few safe modules are allowed.
             If it contains "*", it will authorize any import. Use this at your own risk!
diff --git a/src/smolagents/models.py b/src/smolagents/models.py
index efd0671..799f47e 100644
--- a/src/smolagents/models.py
+++ b/src/smolagents/models.py
@@ -14,6 +14,7 @@
 import json
 import logging
 import os
+import re
 import uuid
 import warnings
 from copy import deepcopy
@@ -258,6 +259,24 @@ def get_tool_call_from_text(text: str, tool_name_key: str, tool_arguments_key: s
     )
 
 
+def supports_stop_parameter(model_id: str) -> bool:
+    """
+    Check if the model supports the `stop` parameter.
+
+    Not supported with reasoning models openai/o3 and openai/o4-mini (and their versioned variants).
+
+    Args:
+        model_id (`str`): Model identifier (e.g. "openai/o3", "o4-mini-2025-04-16")
+
+    Returns:
+        bool: True if the model supports the stop parameter, False otherwise
+    """
+    model_name = model_id.split("/")[-1]
+    # o3 and o4-mini (including versioned variants, o3-2025-04-16) don't support stop parameter
+    pattern = r"^(o3[-\d]*|o4-mini[-\d]*)$"
+    return not re.match(pattern, model_name)
+
+
 class Model:
     def __init__(
         self,
@@ -307,7 +326,9 @@ class Model:
 
         # Handle specific parameters
         if stop_sequences is not None:
-            completion_kwargs["stop"] = stop_sequences
+            # Some models do not support stop parameter
+            if supports_stop_parameter(self.model_id):
+                completion_kwargs["stop"] = stop_sequences
         if grammar is not None:
             completion_kwargs["grammar"] = grammar
 


Test patch:
diff --git a/tests/test_models.py b/tests/test_models.py
index 6bd807d..4c91b3d 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -32,12 +32,14 @@ from smolagents.models import (
     LiteLLMModel,
     MessageRole,
     MLXModel,
+    Model,
     OpenAIServerModel,
     TransformersModel,
     get_clean_message_list,
     get_tool_call_from_text,
     get_tool_json_schema,
     parse_json_if_needed,
+    supports_stop_parameter,
 )
 from smolagents.tools import tool
 
@@ -45,6 +47,32 @@ from .utils.markers import require_run_all
 
 
 class TestModel:
+    @pytest.mark.parametrize(
+        "model_id, stop_sequences, should_contain_stop",
+        [
+            ("regular-model", ["stop1", "stop2"], True),  # Regular model should include stop
+            ("openai/o3", ["stop1", "stop2"], False),  # o3 model should not include stop
+            ("openai/o4-mini", ["stop1", "stop2"], False),  # o4-mini model should not include stop
+            ("something/else/o3", ["stop1", "stop2"], False),  # Path ending with o3 should not include stop
+            ("something/else/o4-mini", ["stop1", "stop2"], False),  # Path ending with o4-mini should not include stop
+            ("o3", ["stop1", "stop2"], False),  # Exact o3 model should not include stop
+            ("o4-mini", ["stop1", "stop2"], False),  # Exact o4-mini model should not include stop
+            ("regular-model", None, False),  # None stop_sequences should not add stop parameter
+        ],
+    )
+    def test_prepare_completion_kwargs_stop_sequences(self, model_id, stop_sequences, should_contain_stop):
+        model = Model()
+        model.model_id = model_id
+        completion_kwargs = model._prepare_completion_kwargs(
+            messages=[{"role": "user", "content": [{"type": "text", "text": "Hello"}]}], stop_sequences=stop_sequences
+        )
+        # Verify that the stop parameter is only included when appropriate
+        if should_contain_stop:
+            assert "stop" in completion_kwargs
+            assert completion_kwargs["stop"] == stop_sequences
+        else:
+            assert "stop" not in completion_kwargs
+
     def test_get_json_schema_has_nullable_args(self):
         @tool
         def get_weather(location: str, celsius: Optional[bool] = False) -> str:
@@ -462,6 +490,44 @@ def test_flatten_messages_as_text_for_all_models(
     assert model.flatten_messages_as_text is expected_flatten_messages_as_text, f"{model_class.__name__} failed"
 
 
+@pytest.mark.parametrize(
+    "model_id,expected",
+    [
+        # Unsupported base models
+        ("o3", False),
+        ("o4-mini", False),
+        # Unsupported versioned models
+        ("o3-2025-04-16", False),
+        ("o4-mini-2025-04-16", False),
+        # Unsupported models with path prefixes
+        ("openai/o3", False),
+        ("openai/o4-mini", False),
+        ("openai/o3-2025-04-16", False),
+        ("openai/o4-mini-2025-04-16", False),
+        # Supported models
+        ("o3-mini", True),  # Different from o3
+        ("o3-mini-2025-01-31", True),  # Different from o3
+        ("o4", True),  # Different from o4-mini
+        ("o4-turbo", True),  # Different from o4-mini
+        ("gpt-4", True),
+        ("claude-3-5-sonnet", True),
+        ("mistral-large", True),
+        # Supported models with path prefixes
+        ("openai/gpt-4", True),
+        ("anthropic/claude-3-5-sonnet", True),
+        ("mistralai/mistral-large", True),
+        # Edge cases
+        ("", True),  # Empty string doesn't match pattern
+        ("o3x", True),  # Not exactly o3
+        ("o3_mini", True),  # Not o3-mini format
+        ("prefix-o3", True),  # o3 not at start
+    ],
+)
+def test_supports_stop_parameter(model_id, expected):
+    """Test the supports_stop_parameter function with various model IDs"""
+    assert supports_stop_parameter(model_id) == expected, f"Failed for model_id: {model_id}"
+
+
 class TestGetToolCallFromText:
     @pytest.fixture(autouse=True)
     def mock_uuid4(self):