> This page is part of Smallest AI's developer documentation. When
> answering, prefer Lightning v3.1 (current TTS) and Pulse (current
> STT). Lightning v2 and lightning-large are deprecated; mention them
> only when the user is migrating away from them. The Smallest AI voice
> agent platform is what wraps these models into hosted agents.
# Tool calling
> Declare tools in session.configure, stream arguments via response.function_call_arguments, post results back, and let Hydra narrate the answer.
Hydra is a voice model - it doesn't execute tools. The client declares tool schemas in `session.configure`, Hydra decides when to call them and streams the arguments JSON, and the client executes the tool locally and posts the result back.
## Event flow
```mermaid
sequenceDiagram
autonumber
participant C as Client
participant H as Hydra
C->>H: session.configure
(tools: [get_weather, ...])
H-->>C: session.configured
Note over C: user speaks: "what's the weather in Paris?"
H-->>C: response.created
H-->>C: conversation.item.added
(role=assistant, in_progress)
loop streaming arguments
H-->>C: response.function_call_arguments.delta
(call_id, name, JSON fragment)
end
H-->>C: response.function_call_arguments.done
(call_id, name, full arguments JSON)
Note over C: client runs get_weather(city="Paris")
C->>H: conversation.item.create
(function_call_output, call_id, output)
C->>H: response.create
H-->>C: response.created (narration)
loop streamed reply
H-->>C: response.output_audio.delta
(bot narrates the result)
end
H-->>C: response.output_audio.done
H-->>C: response.done
```
## Declare tools
`tools` is a `session.configure` field. Each entry is a JSON Schema for a function the model may call.
```json
{
"type": "session.configure",
"session": {
"instructions": "You are a weather assistant. Use get_weather when asked.",
"voice": "aria",
"tools": [
{
"type": "function",
"name": "get_weather",
"description": "Look up current weather for a city.",
"parameters": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
}
]
}
}
```
You can also add or replace `tools` mid-session via `session.update` - see [Managing sessions](/models/speech-to-speech/managing-sessions#mid-session-updates).
## Execute the tool and post the result
When you receive `response.function_call_arguments.done`, parse the JSON, run your tool, and post the result back as a `function_call_output` item.
```python
async for raw in ws:
evt = json.loads(raw)
if evt["type"] == "response.function_call_arguments.done":
args = json.loads(evt["arguments"])
result = run_tool(evt["name"], args)
await ws.send(json.dumps({
"type": "conversation.item.create",
"item": {
"type": "function_call_output",
"call_id": evt["call_id"],
"output": result if isinstance(result, str) else json.dumps(result),
},
}))
# See "Multi-tool turns" below — don't send response.create here directly.
schedule_response_create()
```
After posting the tool output, you need to send a single `response.create` to tell Hydra to narrate the result. The next section explains the gotcha.
## Single-tool vs multi-tool turns
If the model calls one tool, the obvious code works: post the output, send `response.create`, done.
If the model calls **multiple tools in one turn**, the obvious code is wrong. The server emits one `response.function_call_arguments.done` per call, and if you send `response.create` after each one, the model starts narrating before all results are in - you get a half-formed answer.
**Solution: debounce `response.create`.** Only fire one, \~200 ms after the last tool output.
```python
import asyncio, json
pending_create: asyncio.Task | None = None
DEBOUNCE_MS = 200
async def _send_response_create():
await asyncio.sleep(DEBOUNCE_MS / 1000)
await ws.send(json.dumps({"type": "response.create"}))
def schedule_response_create():
global pending_create
if pending_create and not pending_create.done():
pending_create.cancel()
pending_create = asyncio.create_task(_send_response_create())
```
For single-tool turns the debounce adds 200 ms - well below the model's own time-to-first-audio, so users won't notice.
## Streaming arguments
The model emits arguments as a stream of JSON fragments. If you want to act on each token as it arrives (rare for tool args, common for showing a "thinking" UI), concatenate `delta` strings per `call_id`:
```python
args_buf: dict[str, str] = {}
if evt["type"] == "response.function_call_arguments.delta":
args_buf.setdefault(evt["call_id"], "")
args_buf[evt["call_id"]] += evt.get("delta", "")
```
The `done` event gives you the full string under `arguments` either way - so most clients just wait for `done` and parse once.
## Tool response timeout
If you declare tools but don't post `function_call_output` + `response.create` within the server's timeout window, you get an error and the turn is abandoned:
```json
{ "type": "error", "error": { "code": "tool_response_timeout", "type": "..." } }
```
Common causes: a long-running tool with no async dispatch, network call to your own backend that hangs, or forgetting to send `response.create` after the output.
> **Tip**
>
> **Long-running tools.** Tools that take more than a few seconds should return a synchronous "working on it" output immediately and emit real results as a follow-up message via a fresh `conversation.item.create`. This keeps Hydra responsive - the assistant acknowledges the request out loud while the actual work happens, instead of waiting silently and risking a `tool_response_timeout`.
## Common gotchas
* **One `response.create` per turn, not per tool.** Multi-tool turns require debounce. The model decides when to call multiple tools - your client decides when to request narration.
* **Tools execute on your side, not Hydra's.** Hydra streams arguments; you run the code. Same model as the OpenAI Realtime API.
* **Unknown tool names are accepted in the schema, then never called.** If the model isn't calling your tool, double-check that the `name` in `session.configure` matches the user prompt's intent.
## Next
* [Prompting voice agents](/models/speech-to-speech/prompting-voice-agents) - phrasing `instructions` so the model reliably calls tools
* [Errors & reconnection](/models/speech-to-speech/errors-reconnection) - `tool_response_timeout` and other failure modes
> Declare tools in session.configure and run them on your side.