Skip to content

Tool calling

Tessaract has one canonical way to define function tools. Each provider adapter converts them to the native format; for OpenAI, that's a Responses API function tool.

from tessaract import FunctionTool, InputSchema, Property

Defining a tool

get_weather = FunctionTool(
    name="get_weather",
    description="Get the current temperature for a city.",
    input_schema=InputSchema(
        properties={
            "city": Property(type="string", description="City name, e.g. Paris."),
            "units": Property(type="string", description="Either 'celsius' or 'fahrenheit'."),
        },
        required=["city", "units"],
        additionalProperties=False,
    ),
    strict=True,
)

FunctionTool

Field Type Default Description
name str required The name the model uses to call the tool. Use it as the key in your dispatch table.
description str required Tells the model what the tool does and when to use it.
input_schema InputSchema \| None None The arguments. Leave it out for tools that take no arguments.
strict bool \| None None Enforce the schema exactly. The OpenAI adapter treats None as True.
provider_options dict {} Extra native fields merged into the provider's tool definition.

InputSchema

A JSON Schema object. It's validated strictly with Pydantic, and every name in required must also appear in properties. Otherwise you get a ValueError.

Field Type Default
type "object" "object"
properties dict[str, Property] {}
required list[str] []
additionalProperties bool \| None None (the OpenAI adapter sends False)

Property

Field Type
type "string", "number", "integer", "boolean", "array", "object" or "null", or a list of them such as ["string", "null"]
description str \| None
enum list \| None: the allowed values
items Property \| None: the element schema, for "array"
properties dict[str, Property] \| None: nested fields, for "object"
required list[str] \| None: required nested fields, for "object"
additionalProperties bool \| None, for "object". The OpenAI adapter sends False when it's None, which strict mode requires.

Setting items on a non-array type, or the object fields on a non-object type, raises ValueError.

A richer schema

InputSchema(
    properties={
        "units": Property(type="string", description="Temperature units.", enum=["celsius", "fahrenheit"]),
        "cities": Property(
            type="array",
            description="Cities to look up.",
            items=Property(type="string"),
        ),
        "window": Property(
            type="object",
            description="Forecast window.",
            properties={
                "start": Property(type="string", description="ISO-8601 date."),
                "days": Property(type="integer", description="Number of days."),
            },
            required=["start", "days"],
        ),
        "note": Property(type=["string", "null"], description="Optional note, or null."),
    },
    required=["units", "cities", "window", "note"],
    additionalProperties=False,
)

Strict mode

With strict=True, which is the OpenAI default here, OpenAI uses structured outputs to guarantee that the arguments match the schema. OpenAI requires that:

  • every key in properties is listed in required
  • additionalProperties is False

For an optional argument in strict mode, keep it in required and make it nullable with type=["string", "null"]. Nested objects follow the same rules.

For a tool with no arguments, leave out input_schema and set strict=False:

FunctionTool(name="get_time", description="Get the current UTC time.", strict=False)

Sending tools

response = client.send(model="oai/gpt-5.6-luna", input=history, tools=[get_weather, get_time])

Handling function calls

The model's calls come back as FunctionCallOutputItems in response.output:

for item in response.output:
    if item.type == "function_call":
        item.call_id     # "call_abc123": links the call to its result
        item.name        # "get_weather"
        item.arguments   # {"city": "Paris", "units": "celsius"}, already parsed from JSON
        item.raw         # the native OpenAI ResponseFunctionToolCall

A single response can contain several function calls, which is parallel tool calling. Handle them all.

Returning results

Send each result back as a FunctionToolResult with the matching call_id, after the call itself is in the history:

from tessaract import FunctionToolResult

history.extend(response.output)          # includes the function_call items

for call in (i for i in response.output if i.type == "function_call"):
    output = FUNCTIONS[call.name](**call.arguments)
    history.append(FunctionToolResult(call_id=call.call_id, result=output))

response = client.send(model="oai/gpt-5.6-luna", input=history, tools=tools)

FunctionToolResult

Field Type Default Description
call_id str required The call_id of the FunctionCallOutputItem this result answers
result Any required The tool's output.
is_error bool False Marks the call as failed.

The OpenAI adapter maps it to {"type": "function_call_output", "call_id": ..., "output": ...}. The output depends on result and is_error:

result is_error output sent to OpenAI
"sunny" False "sunny"
{"temp": 19} or any other non-string False '{"temp": 19}' (JSON-encoded)
a list of content-part dicts, e.g. [{"type": "input_text", ...}] False passed through as-is
anything True '{"error": <result>}' (JSON-encoded)

OpenAI has no error flag on function call outputs, so Tessaract puts the error inside the output where the model can see it:

try:
    output = FUNCTIONS[call.name](**call.arguments)
    history.append(FunctionToolResult(call_id=call.call_id, result=output))
except Exception as exc:
    history.append(FunctionToolResult(call_id=call.call_id, result=str(exc), is_error=True))

Native tool fields (provider_options)

Everything in provider_options is merged into the native tool definition, except the canonical keys type, name, description, strict and parameters, which the canonical fields always control:

FunctionTool(
    name="get_weather",
    description="Get the current temperature for a city.",
    input_schema=InputSchema(
        properties={"city": Property(type="string", description="City name.")},
        required=["city"],
        additionalProperties=False,
    ),
    provider_options={
        "output_schema": {
            "type": "object",
            "properties": {"temperature": {"type": "string"}},
            "required": ["temperature"],
            "additionalProperties": False,
        }
    },
)

Controlling tool use

Tool choice isn't part of the canonical API yet. Pass OpenAI's native parameters through request_options:

client.send(
    model="oai/gpt-5.6-luna",
    input=history,
    tools=tools,
    request_options={
        "tool_choice": "required",        # "auto" | "none" | "required" | {"type": "function", "name": "..."}
        "parallel_tool_calls": False,     # at most one call per turn
    },
)

Streaming tool calls

When streaming, a tool_call.started event (with call_id and name) fires as soon as the model starts a call. The arguments then arrive as tool_arguments.delta events, which hold raw JSON fragments. The fully parsed call arrives in an output_item.done event and in the final response.completed event. In most cases, act on the completed items and don't parse the deltas. See Streaming.