How to Implement Tool Calling and Function Calling with the Speech-to-Speech Voice Pipeline
The Hugging Face Speech-to-Speech pipeline implements tool calling through OpenAI Realtime-compatible function calls wrapped in <code>...</code> delimiters, parsed during streaming and validated against JSON Schema definitions before emission as ResponseFunctionToolCall objects.
The huggingface/speech-to-speech repository provides a complete voice-to-voice stack that mirrors the OpenAI Realtime protocol for tool calling and function calling with the voice pipeline. This guide walks through the implementation layers, from declaring tools to parsing LLM-generated calls during real-time streaming.
Understanding the Tool-Calling Architecture
The implementation spans three architectural layers, each with dedicated source files:
| Layer | Purpose | Source File |
|---|---|---|
| Tool definition | FunctionTool subclasses RealtimeFunctionTool and renders Python-style signatures for system prompts |
src/speech_to_speech/LLM/tool_call/function_tool.py |
| Prompt construction | build_tool_system_prompt injects tool definitions and <code>/</code> delimiters into the system prompt |
src/speech_to_speech/LLM/tool_call/tool_prompt.py |
| Runtime parsing | extract_function_calls_from_text tokenizes output, parses calls, validates arguments, and produces ResponseFunctionToolCall objects |
src/speech_to_speech/LLM/tool_call/function_call.py and src/speech_to_speech/LLM/language_model.py |
Declaring a Tool with FunctionTool
Tools inherit from RealtimeFunctionTool through the FunctionTool class. Define the tool's JSON Schema parameters and metadata:
from speech_to_speech.LLM.tool_call.function_tool import FunctionTool
light_tool = FunctionTool()
light_tool.name = "set_lights"
light_tool.description = "Set the smart-home lights to a colour or brightness."
light_tool.type = "function"
light_tool.parameters = {
"type": "object",
"properties": {
"room": {"type": "string", "description": "Room identifier, e.g. 'kitchen'"},
"color": {"type": "string", "enum": ["red", "green", "blue"], "description": "Desired colour"},
"brightness": {"type": "integer", "minimum": 0, "maximum": 100}
},
"required": ["room"]
}
The parameters field follows standard JSON Schema. The FunctionTool class automatically renders this into a Python-style function signature for inclusion in the LLM's system prompt.
Attaching Tools to a Realtime Session
Register tools in the session configuration before processing begins. The BaseLanguageModelHandler._apply_instructions method extracts these definitions and builds the tool-aware system prompt:
from speech_to_speech.api.openai_realtime.runtime_config import RuntimeConfig
runtime_cfg = RuntimeConfig()
runtime_cfg.session.tools = [light_tool]
runtime_cfg.session.tool_choice = "auto" # or "none", "required", or a specific tool name
The tool_choice parameter controls invocation behavior:
"auto"— Model decides whether to call a tool"none"— Disables tool calling"required"— Forces at least one tool call- Specific tool name — Restricts to that tool only
Building the Tool-Aware System Prompt
Generate the prompt section using build_tool_system_prompt:
from speech_to_speech.LLM.tool_call.tool_prompt import (
build_tool_system_prompt,
ENTER_CODE,
END_CODE,
)
tool_prompt = build_tool_system_prompt(
tools=[light_tool],
text_only=False, # False = voice mode with "speak first" instruction
)
Setting text_only=False adds voice-specific instructions requiring the model to emit natural speech before any <code> block. The resulting prompt resembles:
Available tools:
def set_lights(room: str, color: str = None, brightness: int = None):
"""Set the smart-home lights to a colour or brightness.
Args:
room: Room identifier, e.g. 'kitchen'
color: Desired colour
brightness: 0-100
"""
...
To call a tool, put exactly one named-argument function call inside <code>...</code>:
<code>function_name(arg='value')</code>
Parsing Tool Calls During Streaming
When the LLM emits output containing a tool call, the pipeline handles it in BaseLanguageModelHandler._process_printable_text. The method detects the ENTER_CODE delimiter (<code>), splits pre-code text into sent-tokenized speech chunks, and delegates parsing to extract_function_calls_from_text:
from speech_to_speech.LLM.tool_call.function_call import extract_function_calls_from_text
# Inside the streaming handler
outside_text, tool_calls = extract_function_calls_from_text(
text_with_code, # Raw LLM output containing <code>...</code>
block_regex=ctx.block_regex # Compiled pattern matching delimiters
)
The parse_function_call function uses a tokenizer-aware parser to handle nested parentheses, quoted strings, tuples, and dictionaries—critical for robust extraction from streaming text.
Validating and Converting Tool Calls
Each extracted FunctionToolCall undergoes validation via to_realtime_function_tool_call:
- Positional arguments rejected — Only named arguments
(key=value)permitted - Schema validation — Required parameters verified, undeclared parameters discarded
- Tool existence check — Called name must exist in
ctx.function_tools - JSON serialization — Arguments encoded and unique
call_idassigned
Failed validation logs a warning and drops the call, allowing the LLM to continue speaking rather than crashing the pipeline.
The validated call becomes a ResponseFunctionToolCall stored in ctx.tools and yielded in an LLMResponseChunk:
# From messages.py - LLMResponseChunk carries tool calls downstream
@dataclass
class LLMResponseChunk:
text: Optional[str] = None
tools: Optional[List[ResponseFunctionToolCall]] = None
usage: Optional[TokenUsage] = None
finish_reason: Optional[str] = None
Emitting Tool Calls to the Client
The Realtime server serializes tool calls into RealtimeConversationItemFunctionCall events:
original_chat.add_item(
RealtimeConversationItemFunctionCall(
type="function_call",
id=tool.id,
call_id=tool.call_id, # Unique identifier for this invocation
name=tool.name, # Tool name
arguments=tool.arguments, # JSON-encoded parameters
status=tool.status, # "in_progress", "completed", etc.
)
)
The client receives this event, executes the requested action, and may return a tool result message to continue the conversation.
Complete Implementation Example
from speech_to_speech.LLM.tool_call.function_tool import FunctionTool
from speech_to_speech.LLM.tool_call.tool_prompt import build_tool_system_prompt
from speech_to_speech.LLM.language_model import LanguageModelHandler
from speech_to_speech.api.openai_realtime.runtime_config import RuntimeConfig
from speech_to_speech.pipeline.handler_types import GenerateResponseRequest
# 1. Define the tool
weather_tool = FunctionTool()
weather_tool.name = "get_weather"
weather_tool.description = "Retrieve current weather for a location."
weather_tool.parameters = {
"type": "object",
"properties": {
"location": {"type": "string", "description": "City name"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["location"]
}
# 2. Configure session with tool
runtime_cfg = RuntimeConfig()
runtime_cfg.session.tools = [weather_tool]
runtime_cfg.session.tool_choice = "auto"
# 3. Build system prompt for voice mode
system_prompt = build_tool_system_prompt([weather_tool], text_only=False)
# 4. Initialize handler
handler = LanguageModelHandler()
handler.setup(
model_name="Qwen/Qwen2.5-7B-Instruct",
device="cuda",
torch_dtype="bfloat16",
)
# 5. Process request and handle tool calls
request = GenerateResponseRequest(
turn_id="turn-001",
runtime_config=runtime_cfg,
)
for chunk in handler.process(request):
if chunk.tools:
for tool in chunk.tools:
print(f"CALL {tool.call_id}: {tool.name}({tool.arguments})")
# Execute tool, then send result back to conversation
elif chunk.text:
print(f"ASSISTANT: {chunk.text}")
Enforcing Turn-Taking and Safety Rules
The pipeline implements three critical constraints:
- Single call per response —
BaseLanguageModelHandler._process_printable_textfilters extra calls with a warning - Voice preamble required — The voice prompt template mandates natural speech before
<code>blocks - Speculative cancellation — New user turns can abort in-progress generation, maintaining responsiveness
These rules align with OpenAI Realtime protocol behavior and ensure tool calling and function calling with the voice pipeline remains robust in real-time scenarios.
Key Source Files Reference
| File | Function |
|---|---|
src/speech_to_speech/LLM/tool_call/function_tool.py |
FunctionTool class definition and signature rendering |
src/speech_to_speech/LLM/tool_call/tool_prompt.py |
build_tool_system_prompt() and delimiter constants (ENTER_CODE, END_CODE) |
src/speech_to_speech/LLM/tool_call/function_call.py |
extract_function_calls_from_text(), parse_function_call(), FunctionToolCall validation |
src/speech_to_speech/LLM/language_model.py |
BaseLanguageModelHandler with _process_printable_text() and _apply_instructions() |
src/speech_to_speech/pipeline/messages.py |
LLMResponseChunk, ResponseFunctionToolCall data models |
Summary
- Tool calling and function calling with the voice pipeline uses
<code>...</code>delimiters compatible with OpenAI Realtime FunctionTooldeclares schemas,build_tool_system_promptinjects them, andextract_function_calls_from_textparses streaming output- The tokenizer-aware parser handles complex nested structures robustly
- Validation enforces named-only arguments, schema compliance, and declared tool existence
- Tool calls propagate through
LLMResponseChunktoRealtimeConversationItemFunctionCallevents
Frequently Asked Questions
What delimiter format does the Speech-to-Speech pipeline use for tool calls?
The pipeline uses <code>...</code> as the default delimiter pair, defined by ENTER_CODE and END_CODE constants in tool_prompt.py. The LLM must wrap function calls inside these tags: <code>tool_name(arg='value')</code>.
Can I force the model to use a specific tool?
Yes. Set runtime_cfg.session.tool_choice = "your_tool_name" instead of "auto". This restricts the model to that exact tool, matching OpenAI's tool_choice behavior.
How does the pipeline handle invalid tool calls?
Invalid calls are logged and discarded. The to_realtime_function_tool_call method validates against the tool schema; failures return None and emit a warning, allowing the conversation to continue rather than erroring.
Is text-only mode available without voice instructions?
Yes. Pass text_only=True to build_tool_system_prompt. This removes the "speak first" requirement, suitable for chatbot interfaces without TTS or when integrating with non-voice clients.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →