tools

On this page

llm.tools — standalone module that defines the unified tool-calling schema, converts it to each provider's native shape, and parses each provider's response back into one shape. Has no dependency on BaseLLMInterface or any provider SDK; every provider module imports only the specific converter/parser pair it needs.

See Tool Calling for the full usage contract and per-provider conversion table — this page documents the module's exports directly.

ToolCall

Python · 5 lines
@dataclass
class ToolCall:
    id: Optional[str]
    name: str
    arguments: dict

The internal representation every parse_*_tool_calls() function returns a list of, before tool_calls_to_dicts() converts them to the plain dicts that actually reach calling code. id is None for providers/responses that don't carry a call id (Gemini always; Ollama when the model/server doesn't supply one).

_normalize_tool(tool: dict) -> dict

Python · 7 lines
def _normalize_tool(tool: dict) -> dict:
    name = tool.get("name")
    if not name:
        raise ValueError("Each tool must have a 'name'.")
    description = tool.get("description", "")
    schema = tool.get("input_schema") or tool.get("parameters") or {"type": "object", "properties": {}}
    return {"name": name, "description": description, "schema": schema}

Internal helper called by every to_*_tools() converter. Accepts either input_schema (Anthropic-style key) or parameters (OpenAI-style key) for the JSON-schema object — whichever is present wins; if neither is given, an empty {"type": "object", "properties": {}} schema is used rather than raising. name is the only required field; a missing name raises ValueError immediately, uncaught by any of the to_*_tools() callers, so it propagates straight out of generate()/generate_async() on whichever provider you called (which then usually re-catches it as part of that provider's generic exception handling — see each provider's page for the exact resulting return shape).

Input converters

FunctionProvider(s)Output shape
to_openai_tools(tools)OpenAI (canonical implementation)[{"type": "function", "function": {"name", "description", "parameters"}}, ...]
to_groq_toolsGroqAlias of to_openai_tools — same object, not a wrapper.
to_mistral_toolsMistralAlias of to_openai_tools.
to_ollama_toolsOllamaAlias of to_openai_tools.
to_anthropic_tools(tools)Anthropic[{"name", "description", "input_schema"}, ...]
to_bedrock_tools(tools)AWS Bedrock{"tools": [{"toolSpec": {"name", "description", "inputSchema": {"json": ...}}}, ...]} — note this returns the full toolConfig dict shape, not just a list, since Bedrock nests tools under a "tools" key alongside a sibling "toolChoice" key that AWSInterface adds separately when tool_choice is given.
to_gemini_tools(tools)Gemini[{"function_declarations": [{"name", "description", "parameters"}, ...]}] — a single-element list wrapping all declarations together, per the Gemini SDK's expected tools shape.

Every converter returns None if tools is falsy (None or []), so callers can do kwargs["tools"] = to_openai_tools(tools) unconditionally inside an if tools: guard without a separate None-check — though every provider module in this package still gates the whole block behind if tools: for clarity rather than relying on this.

Response parsers

FunctionReads fromNotes
parse_openai_tool_calls(message)message.tool_calls[i].id / .function.name / .function.argumentsShared by OpenAI, Groq, and Mistral — all three SDKs expose this same attribute shape on the response message. arguments is a JSON string on all three; parsed via json.loads, falling back to {"_raw": raw_args} if parsing fails (json.JSONDecodeError or TypeError, e.g. if the SDK ever hands back None) rather than raising.
parse_anthropic_tool_calls(content_blocks)Blocks in response.content with type == "tool_use".input is already a dict on Anthropic's response — no JSON parsing needed. Non-tool-use blocks (e.g. text) are skipped, not errored on.
parse_gemini_tool_calls(response)Each candidate's content.parts[i].function_call.args is coerced to a plain dict via dict(fc.args). id is always None — Gemini function-call parts don't carry a call id.
parse_bedrock_tool_calls(content_blocks)Blocks (plain dicts) with a "toolUse" key: {"toolUse": {"toolUseId", "name", "input"}}Unlike the others, content_blocks here is a list of plain dicts (Bedrock's Converse API returns JSON-native structures, not SDK objects with attributes), so this function reads via .get()/key access rather than getattr(). input is already a dict.
parse_ollama_tool_calls(message)message["tool_calls"], each {"function": {"name", "arguments"}}arguments is parsed from a JSON string via json.loads only if the model returned it as a str; if it's already a dict (also seen in practice from some Ollama versions/models), it's used as-is. Falls back to {"_raw": args} on a json.JSONDecodeError.

Each parser returns [] (not None) when there are no tool calls in the response, so if calls: is always a safe truthiness check in calling code — every provider module in this package relies on exactly that pattern.

tool_calls_to_dicts(calls: list[ToolCall]) -> list[dict]

Python · 2 lines
def tool_calls_to_dicts(calls: list[ToolCall]) -> list[dict]:
    return [{"id": c.id, "name": c.name, "arguments": c.arguments} for c in calls]

The last step every provider's generate/generate_async calls before building its {"content", "tool_calls", "stop_reason"} return dict — converts the internal ToolCall dataclass list into the plain dicts that actually reach calling code, so nothing provider-internal (like the ToolCall type itself) leaks into the public return shape.

  • Tool Calling — the full usage contract, written for callers rather than as a module reference.
  • BaseLLMInterface — the tools/tool_choice parameters this module's converters feed into.
  • Any concrete provider page for which converter/parser pair that specific class imports and how it wires them into generate/generate_async.

Related pages