From 100ecc0bfa96acc8d94f448ed70515251f53d78d Mon Sep 17 00:00:00 2001 From: kamilbenkirane Date: Wed, 25 Mar 2026 15:47:31 +0100 Subject: [PATCH] fix: raise InvalidToolError for non-dict/non-Tool items in tools list (#218) ToolsMapper.map() silently dropped items that were neither Tool instances nor dicts (e.g. tools=[WebSearch] without ()). Add InvalidToolError and else clause in all 4 ToolsMapper implementations. --- src/celeste/exceptions.py | 10 ++++++++ .../protocols/chatcompletions/parameters.py | 3 +++ .../protocols/openresponses/parameters.py | 3 +++ .../anthropic/messages/parameters.py | 3 +++ .../google/generate_content/parameters.py | 3 +++ tests/unit_tests/test_exceptions.py | 23 +++++++++++++++++++ 6 files changed, 45 insertions(+) diff --git a/src/celeste/exceptions.py b/src/celeste/exceptions.py index 4db48b9a..83f74ac8 100644 --- a/src/celeste/exceptions.py +++ b/src/celeste/exceptions.py @@ -233,6 +233,15 @@ def __init__(self, provider: str) -> None: ) +class InvalidToolError(ValidationError): + """Raised when a tool item is not a Tool instance or dict.""" + + def __init__(self, item: object) -> None: + """Initialize with the invalid item.""" + self.item = item + super().__init__(f"Expected Tool instance or dict, got {type(item).__name__}") + + class UnsupportedParameterError(ValidationError): """Raised when a parameter is not supported by a model.""" @@ -253,6 +262,7 @@ class UnsupportedParameterWarning(UserWarning): "ClientNotFoundError", "ConstraintViolationError", "Error", + "InvalidToolError", "MissingCredentialsError", "MissingDependencyError", "ModalityNotFoundError", diff --git a/src/celeste/protocols/chatcompletions/parameters.py b/src/celeste/protocols/chatcompletions/parameters.py index 44e15bee..d6f2493e 100644 --- a/src/celeste/protocols/chatcompletions/parameters.py +++ b/src/celeste/protocols/chatcompletions/parameters.py @@ -5,6 +5,7 @@ from pydantic import BaseModel, TypeAdapter +from celeste.exceptions import InvalidToolError from celeste.models import Model from celeste.parameters import FieldMapper, ParameterMapper from celeste.structured_outputs import StrictJsonSchemaGenerator @@ -105,6 +106,8 @@ def map( tools.append(self._map_user_tool(item)) elif isinstance(item, dict): tools.append(item) + else: + raise InvalidToolError(item) return request diff --git a/src/celeste/protocols/openresponses/parameters.py b/src/celeste/protocols/openresponses/parameters.py index 5ccfb340..1761e2ab 100644 --- a/src/celeste/protocols/openresponses/parameters.py +++ b/src/celeste/protocols/openresponses/parameters.py @@ -5,6 +5,7 @@ from pydantic import BaseModel, TypeAdapter +from celeste.exceptions import InvalidToolError from celeste.models import Model from celeste.parameters import FieldMapper, ParameterMapper from celeste.structured_outputs import StrictJsonSchemaGenerator @@ -159,6 +160,8 @@ def map( tools.append(self._map_user_tool(item)) elif isinstance(item, dict): tools.append(item) + else: + raise InvalidToolError(item) return request diff --git a/src/celeste/providers/anthropic/messages/parameters.py b/src/celeste/providers/anthropic/messages/parameters.py index 191ee170..82489782 100644 --- a/src/celeste/providers/anthropic/messages/parameters.py +++ b/src/celeste/providers/anthropic/messages/parameters.py @@ -5,6 +5,7 @@ from pydantic import BaseModel, TypeAdapter +from celeste.exceptions import InvalidToolError from celeste.models import Model from celeste.parameters import FieldMapper, ParameterMapper from celeste.structured_outputs import StrictJsonSchemaGenerator @@ -98,6 +99,8 @@ def map( tools.append(self._map_user_tool(item)) elif isinstance(item, dict): tools.append(item) + else: + raise InvalidToolError(item) return request diff --git a/src/celeste/providers/google/generate_content/parameters.py b/src/celeste/providers/google/generate_content/parameters.py index 5a4d93e2..0f44828a 100644 --- a/src/celeste/providers/google/generate_content/parameters.py +++ b/src/celeste/providers/google/generate_content/parameters.py @@ -7,6 +7,7 @@ from pydantic import BaseModel, TypeAdapter from celeste.artifacts import ImageArtifact +from celeste.exceptions import InvalidToolError from celeste.mime_types import ApplicationMimeType from celeste.models import Model from celeste.parameters import ParameterMapper @@ -213,6 +214,8 @@ def map( fn_declarations.append(self._map_user_tool(item)) elif isinstance(item, dict): tools.append(item) + else: + raise InvalidToolError(item) if fn_declarations: tools.append({"functionDeclarations": fn_declarations}) diff --git a/tests/unit_tests/test_exceptions.py b/tests/unit_tests/test_exceptions.py index 22b53071..12126c29 100644 --- a/tests/unit_tests/test_exceptions.py +++ b/tests/unit_tests/test_exceptions.py @@ -7,6 +7,7 @@ ClientNotFoundError, ConstraintViolationError, Error, + InvalidToolError, MissingCredentialsError, ModalityNotFoundError, ModelNotFoundError, @@ -16,6 +17,7 @@ UnsupportedCapabilityError, UnsupportedParameterError, UnsupportedProviderError, + ValidationError, ) from celeste.models import Model @@ -368,3 +370,24 @@ def test_inherits_from_credentials_error(self) -> None: exc = UnsupportedProviderError("test_provider") assert isinstance(exc, CredentialsError) assert isinstance(exc, Error) + + +class TestInvalidToolError: + """Test InvalidToolError exception.""" + + def test_creates_with_item(self) -> None: + """Test exception stores the invalid item.""" + exc = InvalidToolError(42) + assert exc.item == 42 + assert "int" in str(exc) + + def test_message_includes_type_name(self) -> None: + """Test exception message shows the actual type passed.""" + exc = InvalidToolError("web_search") + assert str(exc) == "Expected Tool instance or dict, got str" + + def test_inherits_from_validation_error(self) -> None: + """Test InvalidToolError inherits from ValidationError.""" + exc = InvalidToolError(42) + assert isinstance(exc, ValidationError) + assert isinstance(exc, Error)