Skip to content

feat: unified tools= parameter #191

Description

@Kamilbenkirane

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:

  1. TypesTool enum, ToolCall model, TOOL_RESULT in Role, TOOLS in TextParameter, tool_calls + message on TextOutput
  2. One ToolsMapper per provider — replaces WebSearchMapper, XSearchMapper, CodeExecutionMapper
  3. Input mappingTool enum → translate; dict with name → normalize schema; other dict → passthrough
  4. Output parsing — normalizes provider tool call response into ToolCall objects
  5. Round-trip mappingRole.TOOL_RESULT messages converted to provider wire format in _init_request
  6. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions