The full flow
# Cell 1 — Call with tools
import celeste
from celeste import Tool
from pydantic import BaseModel
class WeatherParams(BaseModel):
city: str
units: str = "celsius"
output = await celeste.text.generate(
"What's the weather in Paris?",
model="claude-4-sonnet",
tools=[
Tool.WEB_SEARCH,
{"name": "get_weather", "description": "Get current weather", "parameters": WeatherParams},
{"type": "bash_20250124"}, # raw provider passthrough
],
)
output.tool_calls # [ToolCall(id="toolu_xxx", name="get_weather", arguments={"city": "Paris", "units": "celsius"})]
output.finish_reason # "tool_use"
# Cell 2 — Execute the tool yourself, pass result back
from celeste.types import Message, Role
tool_call = output.tool_calls[0]
weather = get_weather(tool_call.arguments["city"]) # YOUR function — "18°C, sunny"
output2 = await celeste.text.generate(
model="claude-4-sonnet",
messages=[
Message(role=Role.USER, content="What's the weather in Paris?"),
output.message, # assistant's tool call response, replayable
Message(role=Role.TOOL_RESULT, content=weather, tool_call_id=tool_call.id, tool_name=tool_call.name),
],
tools=[{"name": "get_weather", "description": "Get current weather", "parameters": WeatherParams}],
)
# Cell 3 — Final answer
print(output2.content) # "It's 18°C and sunny in Paris!"
Summary
One tools= parameter for all tool types. Returns structured ToolCall objects. Supports the full round-trip via output.message and Role.TOOL_RESULT. No execution — just clean I/O.
Replaces web_search, x_search, code_execution boolean parameters with one unified interface.
Three tool shapes
1. Server-side tools — Tool enum
Celeste maps to provider wire format automatically.
Tool enum |
Anthropic |
OpenAI |
Google |
xAI |
Tool.WEB_SEARCH |
web_search_20250305 |
web_search |
google_search: {} |
web_search |
Tool.X_SEARCH |
— |
— |
— |
x_search |
Tool.CODE_EXECUTION |
— |
— |
— |
code_execution |
2. User-defined function tools — dict with name
parameters accepts Pydantic BaseModel (auto-converted via TypeAdapter, same as output_schema) or raw JSON Schema dict.
{"name": "get_weather", "description": "Get current weather", "parameters": WeatherParams}
| Concern |
Anthropic |
OpenAI Responses |
OpenAI Chat Completions |
Google |
| Schema field |
input_schema |
parameters |
function.parameters |
functionDeclarations[].parameters |
| Nesting |
flat |
flat |
wrapped in function: {} |
inside functionDeclarations[] |
3. Raw provider passthrough — dict without name
For provider-specific tools celeste doesn't map yet. Passed through as-is, same philosophy as extra_body.
{"type": "bash_20250124"}
Output
output.tool_calls — normalized across providers
class ToolCall(BaseModel):
id: str
name: str
arguments: dict[str, Any]
| Provider |
Source |
id |
arguments |
| Anthropic |
content type="tool_use" |
id |
input (dict) |
| OpenAI Responses |
output type="function_call" |
call_id |
arguments (JSON string → parsed) |
| Chat Completions |
tool_calls[].function |
id |
arguments (JSON string → parsed) |
| Google |
parts[].functionCall |
generated UUID |
args (dict) |
output.message — replayable assistant response
The assistant's full response (including tool call blocks) as a Message that can be passed directly back into messages for the round-trip.
Round-trip: passing tool results back
Add TOOL_RESULT to the Role enum. Since Message has extra="allow", tool result fields are passed as extras:
Message(role=Role.TOOL_RESULT, content="18°C, sunny", tool_call_id="toolu_xxx", tool_name="get_weather")
Celeste normalizes to each provider's wire format:
| Provider |
Wire format |
| Anthropic |
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "...", "content": "..."}]} |
| OpenAI Chat Completions |
{"role": "tool", "tool_call_id": "...", "content": "..."} |
| OpenAI Responses |
{"type": "function_call_output", "call_id": "...", "output": "..."} |
| Google |
{"role": "user", "parts": [{"functionResponse": {"name": "...", "response": {...}}}]} |
Migration
# Before
output = await celeste.text.generate("Search for X", model="claude-4-sonnet", web_search=True)
# After
output = await celeste.text.generate("Search for X", model="claude-4-sonnet", tools=[Tool.WEB_SEARCH])
Boolean flags emit deprecation warnings and convert internally during transition.
Implementation
Follows existing ParameterMapper pattern:
- Types —
Tool enum, ToolCall model, TOOL_RESULT in Role, TOOLS in TextParameter, tool_calls + message on TextOutput
- One
ToolsMapper per provider — replaces WebSearchMapper, XSearchMapper, CodeExecutionMapper
- Input mapping —
Tool enum → translate; dict with name → normalize schema; other dict → passthrough
- Output parsing — normalizes provider tool call response into
ToolCall objects
- Round-trip mapping —
Role.TOOL_RESULT messages converted to provider wire format in _init_request
- Deprecation shim — boolean flags warn and delegate to
ToolsMapper
Out of scope
Framework concerns that do NOT belong in celeste:
- No auto-execution loop (
max_steps, function dispatch)
- No
@tool decorator or function_to_schema
- No execution engine
Celeste is primitives. Tools are a parameter, not a framework.
The full flow
Summary
One
tools=parameter for all tool types. Returns structuredToolCallobjects. Supports the full round-trip viaoutput.messageandRole.TOOL_RESULT. No execution — just clean I/O.Replaces
web_search,x_search,code_executionboolean parameters with one unified interface.Three tool shapes
1. Server-side tools —
ToolenumCeleste maps to provider wire format automatically.
ToolenumTool.WEB_SEARCHweb_search_20250305web_searchgoogle_search: {}web_searchTool.X_SEARCHx_searchTool.CODE_EXECUTIONcode_execution2. User-defined function tools — dict with
nameparametersaccepts PydanticBaseModel(auto-converted viaTypeAdapter, same asoutput_schema) or raw JSON Schema dict.{"name": "get_weather", "description": "Get current weather", "parameters": WeatherParams}input_schemaparametersfunction.parametersfunctionDeclarations[].parametersfunction: {}functionDeclarations[]3. Raw provider passthrough — dict without
nameFor provider-specific tools celeste doesn't map yet. Passed through as-is, same philosophy as
extra_body.{"type": "bash_20250124"}Output
output.tool_calls— normalized across providersidargumentstype="tool_use"idinput(dict)type="function_call"call_idarguments(JSON string → parsed)tool_calls[].functionidarguments(JSON string → parsed)parts[].functionCallargs(dict)output.message— replayable assistant responseThe assistant's full response (including tool call blocks) as a
Messagethat can be passed directly back intomessagesfor the round-trip.Round-trip: passing tool results back
Add
TOOL_RESULTto theRoleenum. SinceMessagehasextra="allow", tool result fields are passed as extras:Celeste normalizes to each provider's wire format:
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "...", "content": "..."}]}{"role": "tool", "tool_call_id": "...", "content": "..."}{"type": "function_call_output", "call_id": "...", "output": "..."}{"role": "user", "parts": [{"functionResponse": {"name": "...", "response": {...}}}]}Migration
Boolean flags emit deprecation warnings and convert internally during transition.
Implementation
Follows existing
ParameterMapperpattern:Toolenum,ToolCallmodel,TOOL_RESULTinRole,TOOLSinTextParameter,tool_calls+messageonTextOutputToolsMapperper provider — replacesWebSearchMapper,XSearchMapper,CodeExecutionMapperToolenum → translate; dict withname→ normalize schema; other dict → passthroughToolCallobjectsRole.TOOL_RESULTmessages converted to provider wire format in_init_requestToolsMapperOut of scope
Framework concerns that do NOT belong in celeste:
max_steps, function dispatch)@tooldecorator orfunction_to_schemaCeleste is primitives. Tools are a parameter, not a framework.