Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Tool use

llmshim translates tool definitions, tool calls, and tool results. It never executes a tool. Your application owns that part of the loop.

Availability: Rust: top-level tools · CLI: no tool loop · Proxy/clients: provider_config.tools

The loop is always:

define tools → receive tool_calls → execute in your app → send tool results → continue

1. Define tools

Use the OpenAI Chat Completions function format. The function object is nested inside the tool definition:

{
  "type": "function",
  "function": {
    "name": "get_weather",
    "description": "Get the current weather for a city",
    "parameters": {
      "type": "object",
      "properties": {
        "city": {"type": "string"}
      },
      "required": ["city"]
    }
  }
}

In a Rust request, tools is a top-level field:

{
  "model": "anthropic/claude-sonnet-5-5",
  "messages": [{"role": "user", "content": "What is the weather in Tokyo?"}],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_weather",
        "description": "Get the current weather for a city",
        "parameters": {
          "type": "object",
          "properties": {"city": {"type": "string"}},
          "required": ["city"]
        }
      }
    }
  ]
}

Through the proxy, place the identical array at provider_config.tools:

{
  "model": "anthropic/claude-sonnet-5-5",
  "messages": [{"role": "user", "content": "What is the weather in Tokyo?"}],
  "provider_config": {
    "tools": [
      {
        "type": "function",
        "function": {
          "name": "get_weather",
          "description": "Get the current weather for a city",
          "parameters": {
            "type": "object",
            "properties": {"city": {"type": "string"}},
            "required": ["city"]
          }
        }
      }
    ]
  }
}

Python and Ruby expose convenience tools arguments that build this provider_config field. TypeScript and Go expose provider_config directly.

2. Receive a tool call

llmshim normalizes a provider's request to call a function into the OpenAI shape:

{
  "role": "assistant",
  "content": null,
  "tool_calls": [
    {
      "id": "call_ls_example",
      "wire_ids": [{"provider":"openai","wire":"openai-responses","scope":"response_example","part_id":"0","id":"call_provider_123","item_id":"fc_example"}],
      "type": "function",
      "function": {
        "name": "get_weather",
        "arguments": "{\"city\":\"Tokyo\"}"
      }
    }
  ]
}

arguments is a JSON-encoded string. Parse and validate it before calling your application code. Treat model-generated arguments as untrusted input.

3. Execute it in your application

Dispatch get_weather in your own code. llmshim does not have access to that function and does not decide whether it is safe to run.

Keep the returned assistant message in the conversation, including its tool_calls. Preserve any additional fields on those calls; for example, Gemini can return a thought_signature object with data and origin. Every generated call also has wire_ids; retain that complete array. Then append one tool-result message for each call:

[
  {
    "role": "assistant",
    "content": null,
    "tool_calls": [
      {
        "id": "call_ls_example",
      "wire_ids": [{"provider":"openai","wire":"openai-responses","scope":"response_example","part_id":"0","id":"call_provider_123","item_id":"fc_example"}],
        "type": "function",
        "function": {
          "name": "get_weather",
          "arguments": "{\"city\":\"Tokyo\"}"
        }
      }
    ]
  },
  {
    "role": "tool",
    "tool_call_id": "call_ls_example",
    "content": "{\"temperature_c\":24,\"conditions\":\"clear\"}"
  }
]

The tool_call_id connects the result to the request. Send the expanded history and tool definitions in another completion so the model can use the result and produce an answer.

Identity and validation

llmshim mints each canonical id. wire_ids keeps the native correlation id and, for Responses, its separate item id. Reply with the canonical id and keep the original assistant call unchanged; the adapter translates both sides back to the same wire identity. Switching providers creates a deterministic target wire mapping without changing stored history. Dropping the mapping from an owned call is a local error.

All adapters validate that each tool call is answered exactly once before the next assistant turn. Unknown results, unanswered calls, duplicate owned ids, and invalid argument JSON fail before the provider request. Anthropic thinking blocks must precede text/tools. Invalid history is reported, rather than silently removing calls or results.

Gemini preserves whether the original function call had an id. When it did, its function response gets the same id. Otherwise result ordering preserves correlation, including repeated calls to the same function. The first call in a current Gemini 3 parallel batch carries the required signature; later calls retain their original signature presence/absence. See Google's signature contract. A compacted tool history must retain the user turn that introduced its calls.

Streaming calls

llmshim::stream uses one per-response assembler for every transport, including ChatGPT. Argument fragments, ids, and signatures accumulate under stable part ids. Completed calls appear once after terminal metadata, with full JSON arguments and their wire mappings; incomplete JSON never becomes a callable record. This applies to typed proxy tool_call events too.

For manually read SSE, create provider.stream_normalizer(model) and feed native data strings to push. Call finish on EOF. The lower-level stateless transform_stream_chunk only handles ordinary content/reasoning; it cannot assemble tool calls across events. Rust chunks retain separate choices; the single-message proxy contract projects choice zero.

What llmshim translates

ProviderNative representation
OpenAI ResponsesFlat function definitions, function_call, and function_call_output items
Anthropic Messagesinput_schema, tool_use, and tool_result blocks
GeminifunctionDeclarations, functionCall, and functionResponse parts
xAI ResponsesFlat function definitions and Responses-style call/result items

Responses and stream chunks are translated back to the OpenAI tool_calls shape before they cross the Rust boundary. The proxy then exposes the same call as message.tool_calls or a typed tool_call stream event.

For the broader rule behind the surface-specific placement, see Two contracts, one engine.