Skip to content

Commit f08c3d5

Browse files
feat: unified tools= parameter for tool calling (#199)
* feat: unified tools= parameter for tool calling (#191) Replace individual boolean parameters (web_search, x_search, code_execution) with a single tools= list parameter that accepts Tool instances, user-defined dicts, and raw passthrough dicts. - Add Tool, WebSearch, XSearch, CodeExecution classes with provider-specific ToolMappers that translate to wire format - Add ToolCall on Output and ToolResult(Message) for multi-turn tool use - Add ToolSupport constraint for model-level tool validation - Add _parse_tool_calls to all providers (Anthropic, OpenAI, xAI, Google, OpenResponses) for non-streaming tool call extraction - Add _aggregate_tool_calls to streaming for all providers - Update templates for new tools parameter pattern * test: add integration tests for tools= parameter - WebSearch (non-streaming + streaming) across all 4 providers - User-defined function tool with ToolCall parsing across all 4 providers - xAI XSearch server-side tool - Fix grok-3-mini model config: remove server-side tool support (xAI API only supports server-side tools on grok-4+ family) * feat: add multi-turn tool calling support * feat: shared tool call parsing for Chat Completions and OpenResponses protocols Add protocol-level shared helpers so providers don't duplicate parsing logic: - Chat Completions: parse_tool_calls, serialize_messages, ToolsMapper, streaming tool call deltas - OpenResponses: parse_tool_calls, parse_text_content, serialize_messages - ToolCall extra="allow" for provider-specific fields (e.g. Google thoughtSignature) - Accept ToolResult in message lists for multi-turn tool conversations * refactor: add ChatCompletionsTextClient protocol base class Mirrors OpenResponsesTextClient — shared generate(), analyze(), _init_request(), _parse_content(), _parse_tool_calls() for all Chat Completions providers. DeepSeek, Groq, HuggingFace, Mistral, Moonshot now inherit from it, keeping only parameter_mappers() and _stream_class() overrides (Mistral also keeps _parse_content for thinking models). Also renames parse_text_content → parse_content in OpenResponses tools. * fix: correct Groq WebSearch wire format and add model tool support Map WebSearch to browser_search (Groq's native format) and add ToolSupport constraints to all Groq models. GPT-OSS models support WebSearch and CodeExecution; others support user-defined function tools only. * feat: add tool calling support for DeepSeek, Groq, HuggingFace, Mistral, Moonshot Add ToolSupport constraints to all models and ToolsMapper parameter mappers for the 5 Chat Completions providers. Groq GPT-OSS models support WebSearch + CodeExecution, Moonshot models support WebSearch, all others support user-defined function tools only. * feat: tool call serialization, deprecated param shims, and OpenResponses protocol * fix: use serialize_messages in OpenResponses provider _init_request methods * refactor: remove provider-level OpenResponses text client, use protocol (#229) OpenResponses is a protocol, not a provider. The provider-level `modalities/text/providers/openresponses/` was a parallel hierarchy that reimplemented tool call parsing inline instead of delegating to the canonical `parse_tool_calls()` from the protocol layer. The inline versions were also less robust (KeyError vs .get() with fallback). OpenAI, xAI, and Ollama now inherit from the protocol-level `OpenResponsesTextClient`, matching the ChatCompletions pattern where DeepSeek, Groq, Mistral etc. inherit from `ChatCompletionsTextClient`. Fixes #219 * fix: raise InvalidToolError for non-dict/non-Tool items in tools list (#218) (#228) 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. * fix: add ToolSupport declarations to legacy OpenAI and xAI models (#232) gpt-4-turbo, gpt-4, gpt-3.5-turbo, and grok-2-vision-1212 all support tool calling but were missing TextParameter.TOOLS in their parameter constraints. Added ToolSupport(tools=[]) so they properly advertise tool support in model metadata. Fixes #225 * fix: improve tool error messages (#226) (#234) - Name the protocol in ChatCompletions error: "not supported by Chat Completions" - ToolSupport with empty tools list now says "not supported by this model" instead of "Supported: []" * fix: warn when WebSearch config fields are dropped by provider (#235) Tool mappers now emit UnsupportedParameterWarning when a non-None WebSearch field (e.g. blocked_domains, max_uses) is not supported by the target provider. Each mapper declares _supported_fields so new WebSearch fields automatically trigger warnings on providers that don't map them. Closes #231 * chore: bump version to 0.11.0 Unified tool calling across all providers — the bridge between LLM reasoning and real-world action. This release adds a single tools= parameter that works across 11 providers, replacing web_search/x_search/code_execution booleans. Three tool shapes: built-in (WebSearch, XSearch, CodeExecution), user-defined function dicts, and raw passthrough for provider-specific tools. Streaming tool call deltas, multi-turn via ToolResult, and full backward compatibility with deprecated param shims. 68 files changed, 8 new types, 2 protocol implementations.
1 parent 380d179 commit f08c3d5

66 files changed

Lines changed: 1724 additions & 724 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "celeste-ai"
3-
version = "0.10.2"
3+
version = "0.11.0"
44
description = "Open source, type-safe primitives for multi-modal AI. All capabilities, all providers, one interface"
55
authors = [{name = "Kamilbenkirane", email = "kamil@withceleste.ai"}]
66
readme = "README.md"

src/celeste/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
StrictJsonSchemaGenerator,
5353
StrictRefResolvingJsonSchemaGenerator,
5454
)
55+
from celeste.tools import CodeExecution, Tool, ToolCall, ToolResult, WebSearch, XSearch
5556
from celeste.types import Content, JsonValue, Message, Role
5657
from celeste.websocket import WebSocketClient, WebSocketConnection, close_all_ws_clients
5758

@@ -246,6 +247,7 @@ def create_client(
246247
"Authentication",
247248
"Capability",
248249
"ClientNotFoundError",
250+
"CodeExecution",
249251
"ConstraintViolationError",
250252
"Content",
251253
"Error",
@@ -271,15 +273,20 @@ def create_client(
271273
"StreamingNotSupportedError",
272274
"StrictJsonSchemaGenerator",
273275
"StrictRefResolvingJsonSchemaGenerator",
276+
"Tool",
277+
"ToolCall",
278+
"ToolResult",
274279
"UnsupportedCapabilityError",
275280
"UnsupportedParameterError",
276281
"UnsupportedParameterWarning",
277282
"UnsupportedProviderError",
278283
"Usage",
279284
"UsageField",
280285
"ValidationError",
286+
"WebSearch",
281287
"WebSocketClient",
282288
"WebSocketConnection",
289+
"XSearch",
283290
"audio",
284291
"close_all_http_clients",
285292
"close_all_ws_clients",

src/celeste/client.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from celeste.models import Model
2020
from celeste.parameters import ParameterMapper, Parameters
2121
from celeste.streaming import Stream, enrich_stream_errors
22+
from celeste.tools import ToolCall
2223
from celeste.types import RawUsage
2324

2425

@@ -206,13 +207,19 @@ async def _predict(
206207
)
207208
content = self._parse_content(response_data)
208209
content = self._transform_output(content, **parameters)
210+
tool_calls = self._parse_tool_calls(response_data)
209211
return self._output_class()(
210212
content=content,
211213
usage=self._get_usage(response_data),
212214
finish_reason=self._get_finish_reason(response_data),
213215
metadata=self._build_metadata(response_data),
216+
tool_calls=tool_calls,
214217
)
215218

219+
def _parse_tool_calls(self, response_data: dict[str, Any]) -> list[ToolCall]:
220+
"""Parse tool calls from response. Override in providers that support tools."""
221+
return []
222+
216223
def _stream(
217224
self,
218225
inputs: In,

src/celeste/constraints.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
from abc import ABC, abstractmethod
66
from typing import Any, ClassVar, get_args, get_origin
77

8-
from pydantic import BaseModel, Field, computed_field
8+
from pydantic import BaseModel, Field, computed_field, field_serializer
99

1010
from celeste.artifacts import AudioArtifact, ImageArtifact, VideoArtifact
1111
from celeste.exceptions import ConstraintViolationError
1212
from celeste.mime_types import AudioMimeType, ImageMimeType, MimeType, VideoMimeType
13+
from celeste.tools import Tool
1314

1415

1516
class Constraint(BaseModel, ABC):
@@ -367,6 +368,29 @@ class AudiosConstraint(_MediaListConstraint[AudioMimeType]):
367368
_media_label = "audio"
368369

369370

371+
class ToolSupport(Constraint):
372+
"""Tool support constraint - validates Tool instances are supported by the model."""
373+
374+
tools: list[type[Tool]]
375+
376+
@field_serializer("tools")
377+
@classmethod
378+
def _serialize_tools(cls, v: list[type[Tool]]) -> list[str]:
379+
return [t.__name__ for t in v]
380+
381+
def __call__(self, value: list) -> list:
382+
"""Validate tools list against supported tools."""
383+
for item in value:
384+
if isinstance(item, Tool) and type(item) not in self.tools:
385+
if self.tools:
386+
supported = [t.__name__ for t in self.tools]
387+
msg = f"Tool '{type(item).__name__}' not supported. Supported: {supported}"
388+
else:
389+
msg = f"Tool '{type(item).__name__}' is not supported by this model"
390+
raise ConstraintViolationError(msg)
391+
return value
392+
393+
370394
__all__ = [
371395
"AudioConstraint",
372396
"AudiosConstraint",
@@ -382,6 +406,7 @@ class AudiosConstraint(_MediaListConstraint[AudioMimeType]):
382406
"Range",
383407
"Schema",
384408
"Str",
409+
"ToolSupport",
385410
"VideoConstraint",
386411
"VideosConstraint",
387412
]

src/celeste/exceptions.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,15 @@ def __init__(self, provider: str) -> None:
233233
)
234234

235235

236+
class InvalidToolError(ValidationError):
237+
"""Raised when a tool item is not a Tool instance or dict."""
238+
239+
def __init__(self, item: object) -> None:
240+
"""Initialize with the invalid item."""
241+
self.item = item
242+
super().__init__(f"Expected Tool instance or dict, got {type(item).__name__}")
243+
244+
236245
class UnsupportedParameterError(ValidationError):
237246
"""Raised when a parameter is not supported by a model."""
238247

@@ -253,6 +262,7 @@ class UnsupportedParameterWarning(UserWarning):
253262
"ClientNotFoundError",
254263
"ConstraintViolationError",
255264
"Error",
265+
"InvalidToolError",
256266
"MissingCredentialsError",
257267
"MissingDependencyError",
258268
"ModalityNotFoundError",

src/celeste/io.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from celeste.artifacts import AudioArtifact, ImageArtifact, VideoArtifact
1010
from celeste.constraints import Constraint
1111
from celeste.core import InputType
12+
from celeste.tools import ToolCall
1213

1314

1415
class Input(BaseModel):
@@ -38,6 +39,7 @@ class Output[Content](BaseModel):
3839
usage: Usage = Field(default_factory=Usage)
3940
finish_reason: FinishReason | None = None
4041
metadata: dict[str, Any] = Field(default_factory=dict)
42+
tool_calls: list[ToolCall] = Field(default_factory=list)
4143

4244

4345
class Chunk[Content](BaseModel):

src/celeste/modalities/text/client.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
"""Text modality client."""
22

3-
from typing import Any, Unpack
3+
import warnings
4+
from typing import Any, ClassVar, Unpack
45

56
from asgiref.sync import async_to_sync
67

78
from celeste.client import ModalityClient
89
from celeste.core import InputType, Modality
10+
from celeste.tools import CodeExecution, WebSearch, XSearch
911
from celeste.types import AudioContent, ImageContent, Message, TextContent, VideoContent
1012

1113
from .io import TextChunk, TextFinishReason, TextInput, TextOutput, TextUsage
@@ -25,11 +27,45 @@ class TextClient(
2527
_usage_class = TextUsage
2628
_finish_reason_class = TextFinishReason
2729

30+
# Deprecated param → Tool class mapping.
31+
# TODO(deprecation): Remove on 2026-06-07.
32+
_DEPRECATED_TOOL_PARAMS: ClassVar[dict[str, type]] = {
33+
"web_search": WebSearch,
34+
"x_search": XSearch,
35+
"code_execution": CodeExecution,
36+
}
37+
2838
@classmethod
2939
def _output_class(cls) -> type[TextOutput]:
3040
"""Return the Output class for text modality."""
3141
return TextOutput
3242

43+
def _build_request(
44+
self,
45+
inputs: TextInput,
46+
extra_body: dict[str, Any] | None = None,
47+
streaming: bool = False,
48+
**parameters: Unpack[TextParameters],
49+
) -> dict[str, Any]:
50+
"""Build request, migrating deprecated boolean tool params first.
51+
52+
TODO(deprecation): Remove this override on 2026-06-07.
53+
"""
54+
for old_param, tool_cls in self._DEPRECATED_TOOL_PARAMS.items():
55+
value = parameters.pop(old_param, None) # type: ignore[misc]
56+
if value:
57+
warnings.warn(
58+
f"'{old_param}=True' is deprecated, "
59+
f"use tools=[{tool_cls.__name__}()] instead. "
60+
"Will be removed on 2026-06-07.",
61+
DeprecationWarning,
62+
stacklevel=4,
63+
)
64+
parameters.setdefault("tools", []).append(tool_cls())
65+
return super()._build_request(
66+
inputs, extra_body=extra_body, streaming=streaming, **parameters
67+
)
68+
3369
def _check_media_support(
3470
self,
3571
image: ImageContent | None,

src/celeste/modalities/text/io.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,22 @@
1010
from pydantic import Field
1111

1212
from celeste.io import Chunk, FinishReason, Input, Output, Usage
13-
from celeste.types import AudioContent, ImageContent, Message, TextContent, VideoContent
13+
from celeste.tools import ToolResult
14+
from celeste.types import (
15+
AudioContent,
16+
ImageContent,
17+
Message,
18+
Role,
19+
TextContent,
20+
VideoContent,
21+
)
1422

1523

1624
class TextInput(Input):
1725
"""Input for text operations."""
1826

1927
prompt: str | None = None
20-
messages: list[Message] | None = None
28+
messages: list[ToolResult | Message] | None = None
2129
text: str | list[str] | None = None
2230
image: ImageContent | None = None
2331
video: VideoContent | None = None
@@ -46,6 +54,15 @@ class TextOutput(Output[TextContent]):
4654
usage: TextUsage = Field(default_factory=TextUsage)
4755
finish_reason: TextFinishReason | None = None
4856

57+
@property
58+
def message(self) -> Message:
59+
"""The assistant message for multi-turn conversations."""
60+
return Message(
61+
role=Role.ASSISTANT,
62+
content=self.content,
63+
tool_calls=self.tool_calls if self.tool_calls else None,
64+
)
65+
4966

5067
class TextChunk(Chunk[str]):
5168
"""Chunk for text streaming."""

src/celeste/modalities/text/parameters.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from pydantic import BaseModel
1010

1111
from celeste.parameters import Parameters
12+
from celeste.tools import ToolDefinition
1213

1314

1415
class TextParameter(StrEnum):
@@ -23,8 +24,12 @@ class TextParameter(StrEnum):
2324
THINKING_BUDGET = "thinking_budget"
2425
THINKING_LEVEL = "thinking_level"
2526
OUTPUT_SCHEMA = "output_schema"
26-
WEB_SEARCH = "web_search"
27+
TOOLS = "tools"
2728
VERBOSITY = "verbosity"
29+
30+
# Deprecated: use tools=[WebSearch()], tools=[XSearch()], tools=[CodeExecution()] instead.
31+
# TODO(deprecation): Remove on 2026-06-07.
32+
WEB_SEARCH = "web_search"
2833
X_SEARCH = "x_search"
2934
CODE_EXECUTION = "code_execution"
3035

@@ -46,8 +51,12 @@ class TextParameters(Parameters):
4651
thinking_budget: int | str
4752
thinking_level: str
4853
output_schema: type[BaseModel]
49-
web_search: bool
54+
tools: list[ToolDefinition]
5055
verbosity: str
56+
57+
# Deprecated: use tools=[WebSearch()], tools=[XSearch()], tools=[CodeExecution()] instead.
58+
# TODO(deprecation): Remove on 2026-06-07.
59+
web_search: bool
5160
x_search: bool
5261
code_execution: bool
5362

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Text modality protocol implementations."""

0 commit comments

Comments
 (0)