Skip to content

Transcript Formats: How Providers Represent Tool Use

One toy exchange in three API envelopes

Anthropic

OpenAI Chat

Gemini GenerateContent

Abbreviated bodies. Preserve the complete returned messages, call IDs, and required reasoning state in an actual round-trip.

Providers encode tool exchanges differently. Anthropic uses content blocks. OpenAI Chat Completions uses a tool_calls field and a tool-role response message. Gemini uses functionCall and functionResponse parts. An adapter has to preserve the call and its matching result through those differences.

The wire formats differ, but a useful common structure is an ordered sequence of messages, tool calls, observations, and optional reasoning items. It does not make them interchangeable: preserving provider-specific fields is part of preserving the conversation.

The transcript carries control flow as well as text. Roles and content types tell the runtime what to execute and tell the model what kind of continuation it is receiving. Dropping that structure can change the next turn even when the visible words look unchanged.


The problem this layer solves

A text-only chat message can represent a request and an answer, but it does not identify an executable tool request or link a tool result to its originating call. An application can invent a text convention for that structure; a dedicated API representation makes the convention explicit.

ReAct-style traces used textual labels such as Thought:, Action:, and Observation:. A runtime parsing such output must decide how to handle malformed actions. Structured API fields move part of that parsing contract into the provider interface.


Prereqs

Two earlier topics provide the background. First, chat templates from arc 3: the provider-side JSON you send is not what the model sees. A chat template flattens structured messages down to a raw token stream, injecting special tokens like <|im_start|> or <|start_header_id|> that delimit roles. The transcript formats in this post are inputs to that rendering step; they are the structured side, not the token side.

Second, function calling from earlier in this arc: a model can return structured tool requests, with schema enforcement dependent on the API mode. What we're focused on now is how the resulting call, and the tool's response, are expressed in the next request you send.


The underlying structure

For text and tool exchanges, a common abstraction is:

Transcript=[ (role1,[block1,1,block1,2,…]),  (role2,[block2,1,…]),  … ]\text{Transcript} = [\,(\text{role}_1, [\text{block}_{1,1}, \text{block}_{1,2}, \ldots]),\ \ (\text{role}_2, [\text{block}_{2,1}, \ldots]),\ \ \ldots\,]

An ordered list of turns. Each turn has a role (user, assistant, sometimes system or tool). Each turn carries an ordered list of content blocks. A block is one of:

  • text — ordinary natural-language content.
  • tool call — "I want to run name with these arguments, under this id."
  • tool result — "Here is the output of the call with this id."
  • (optional) reasoning / thinking — private scratchpad that may or may not go back to the model on the next turn.

Every provider's format is some encoding of that list. Anthropic keeps blocks inside a content array on every message. OpenAI Chat Completions flattens most of it into fields on the message and promotes tool results to a separate tool-role message. Gemini renames blocks to "parts" and messages to {role, parts}. This toy exchange has corresponding calls and results across the three formats, while their full capabilities and validation rules differ.

Same concepts · different wire shapes
ConceptAnthropicOpenAI Chat CompletionsGemini
−Assistant tool call
tool_use block inside assistant messagetool_calls array on assistant messagefunctionCall part inside model content
wire JSON
{
  "role": "assistant",
  "content": [
    { "type": "text", "text": "Let me check." },
    { "type": "tool_use",
      "id": "toolu_A1",
      "name": "get_weather",
      "input": { "location": "Tokyo" } }
  ]
}
{
  "role": "assistant",
  "content": "Let me check.",
  "tool_calls": [
    { "id": "call_1",
      "type": "function",
      "function": {
        "name": "get_weather",
        "arguments": "{\"location\":\"Tokyo\"}"
      } }
  ]
}
{
  "role": "model",
  "parts": [
    { "text": "Let me check." },
    { "functionCall": {
        "id": "call_g1",
        "name": "get_weather",
        "args": { "location": "Tokyo" } } }
  ]
}
Roles used
user · assistant; system is a separate parametersystem · user · assistant · tooluser · model (tool results ride in 'user')
Multi-call in one turn
multiple tool_use blocks in one assistant messagemultiple entries in the tool_calls arraymultiple functionCall parts in one model message
Pairing ID
tool_use.id ⇄ tool_result.tool_use_idtool_calls[i].id ⇄ tool message's tool_call_idecho returned call IDs; older modes use name/order
Reasoning block
thinking / redacted_thinking blocksNo reasoning item in this Chat example; Responses is a separate APIthoughtSignature opaque token on parts

The same six concepts across three providers. Select a row to see the wire JSON side by side. The concepts are stable; the names and nesting are not.

OpenAI Responses uses a different public shape: function_call and function_call_output items pair through call_id, alongside messages and reasoning items. The examples here use Chat Completions; do not send them unchanged to Responses.


What the SDK actually does with this

When you call client.messages.create or client.chat.completions.create or the Gemini equivalent, the SDK sends structured messages to the service. The service prepares the model input using its own formatting and multimodal processing.

The assistant doesn't "see" an Anthropic tool_use block or an OpenAI tool_calls array. It sees whatever tokens the provider's chat template chose to emit for that block (things like <|python_tag|> on Llama 3, <tool_call>…</tool_call> on ChatML-based models, or Harmony's channel-tagged segments on gpt-oss). The block structure is there so the service can render it correctly, and so the next turn can reference it by id.

When the model emits a tool call, what actually comes back from the inference engine is tokens. Those tokens get parsed by the provider into the structured block you see in the response. If the model's tokens are malformed (bad JSON in the arguments, a missing close-tag), the parse can fail in ways that are hard to debug from the SDK side. The provider is doing serialization in both directions, and the transcript format is the schema that serialization follows.

Anthropic content blocks · building a 2-tool turn
step 7 / 7
user
text
What's the weather in Tokyo and Paris right now?
assistant
text
I'll check both cities.
assistant
tool_useid: toolu_A1get_weather
input: { "location": "Tokyo" }
assistant
tool_useid: toolu_A2get_weather
input: { "location": "Paris" }
user
tool_resulttool_use_id: toolu_A1 ✓
output: 18C, cloudy
user
tool_resulttool_use_id: toolu_A2 ✓
output: 11C, light rain
assistant
text
Tokyo is 18C and cloudy. Paris is 11C with light rain.

Scrub through a two-tool interaction built up one content block at a time. Anthropic style: tool_use blocks live in the assistant's content array, tool_result blocks live in the next user message, and the two are paired by matching ids.

Pair each result with its originating call using the provider’s rules. Anthropic and OpenAI use explicit call identifiers. Gemini also supports call IDs; preserve and echo them when returned. Older Gemini examples without IDs rely on the documented name/order convention. Missing or mismatched identifiers may be rejected by the API and must not be silently repaired by guessing.


The same turn in three formats

Here is the same exchange in each provider’s format: a user question, one tool call, its result, and the assistant’s answer.

Anthropic puts everything in a content array. A tool call is a block inside the assistant message; the tool result is a block inside the next user message, because Anthropic's format doesn't have a dedicated tool role at all.

[
  { "role": "user",
    "content": [{ "type": "text", "text": "Weather in Tokyo?" }] },
  { "role": "assistant",
    "content": [
      { "type": "text", "text": "Let me check." },
      { "type": "tool_use",
        "id": "toolu_A1",
        "name": "get_weather",
        "input": { "location": "Tokyo" } }
    ] },
  { "role": "user",
    "content": [
      { "type": "tool_result",
        "tool_use_id": "toolu_A1",
        "content": "18C, cloudy" }
    ] },
  { "role": "assistant",
    "content": [{ "type": "text", "text": "It's 18C and cloudy in Tokyo." }] }
]

OpenAI Chat Completions splits the same conversation across more messages. The assistant's tool call is a sibling field (tool_calls) on the assistant message, not a block inside content. The tool's response is its own message with a dedicated role: "tool" and a tool_call_id linking it back.

[
  { "role": "user", "content": "Weather in Tokyo?" },
  { "role": "assistant",
    "content": "Let me check.",
    "tool_calls": [
      { "id": "call_1",
        "type": "function",
        "function": {
          "name": "get_weather",
          "arguments": "{\"location\":\"Tokyo\"}"
        } }
    ] },
  { "role": "tool",
    "tool_call_id": "call_1",
    "content": "18C, cloudy" },
  { "role": "assistant", "content": "It's 18C and cloudy in Tokyo." }
]

Gemini renames messages to contents, roles to user and model, and blocks to parts. Tool calls become functionCall parts inside the model's message. Tool results become functionResponse parts inside a user message. Gemini, like Anthropic, has no tool role.

{
  "contents": [
    { "role": "user",
      "parts": [{ "text": "Weather in Tokyo?" }] },
    { "role": "model",
      "parts": [
        { "text": "Let me check." },
        { "functionCall": {
            "id": "call_g1",
            "name": "get_weather",
            "args": { "location": "Tokyo" } } }
      ] },
    { "role": "user",
      "parts": [
        { "functionResponse": {
            "id": "call_g1",
            "name": "get_weather",
            "response": { "content": "18C, cloudy" } } }
      ] },
    { "role": "model",
      "parts": [{ "text": "It's 18C and cloudy in Tokyo." }] }
  ]
}

An implementation sketch: normalize to the underlying shape

A normalizer can make a small text-and-tool example easier to inspect. The following is deliberately limited to the shown message shapes, not a lossless adapter: a production version must preserve thinking, signatures, images, unknown fields, and provider-specific metadata. The Gemini parser requires an ID supplied by the API or by an explicit pending-call matcher.

from dataclasses import dataclass
from typing import Literal
 
# A limited inspection shape for these text-and-tool examples.
Role = Literal["user", "assistant", "system"]
 
@dataclass
class TextBlock:
    text: str
 
@dataclass
class ToolCall:
    id: str
    name: str
    args: dict
 
@dataclass
class ToolResult:
    call_id: str
    output: str
 
Block = TextBlock | ToolCall | ToolResult
 
@dataclass
class Turn:
    role: Role
    blocks: list[Block]
 
# --- Parsing three providers into the internal shape ---
 
def from_anthropic(msg: dict) -> Turn:
    role = "assistant" if msg["role"] == "assistant" else "user"
    blocks: list[Block] = []
    for b in msg["content"]:
        if b["type"] == "text":
            blocks.append(TextBlock(b["text"]))
        elif b["type"] == "tool_use":
            blocks.append(ToolCall(b["id"], b["name"], b["input"]))
        elif b["type"] == "tool_result":
            blocks.append(ToolResult(b["tool_use_id"], b["content"]))
    return Turn(role, blocks)
 
def from_openai(msg: dict) -> Turn:
    if msg["role"] == "tool":
        # OpenAI's tool role folds into 'user' in our model.
        return Turn("user", [ToolResult(msg["tool_call_id"], msg["content"])])
    blocks: list[Block] = []
    if msg.get("content"):
        blocks.append(TextBlock(msg["content"]))
    for tc in msg.get("tool_calls") or []:
        import json
        blocks.append(ToolCall(
            tc["id"], tc["function"]["name"], json.loads(tc["function"]["arguments"])
        ))
    return Turn(msg["role"], blocks)
 
def from_gemini(msg: dict) -> Turn:
    role = "assistant" if msg["role"] == "model" else "user"
    blocks: list[Block] = []
    for p in msg["parts"]:
        if "text" in p:
            blocks.append(TextBlock(p["text"]))
        elif "functionCall" in p:
            fc = p["functionCall"]
            if not fc.get("id"):
                raise ValueError("An ID or pending-call matcher is required")
            blocks.append(ToolCall(fc["id"], fc["name"], fc["args"]))
        elif "functionResponse" in p:
            fr = p["functionResponse"]
            if not fr.get("id"):
                raise ValueError("An ID or pending-call matcher is required")
            blocks.append(ToolResult(fr["id"], fr["response"]["content"]))
    return Turn(role, blocks)
 
# Three example payloads in; one inspection shape out. Preserve original
# provider objects separately when building the next actual request.

The Gemini parser refuses ambiguous matching rather than inventing an ID it cannot later recover. The Chat Completions parser must decode the arguments string because those arguments are JSON inside a JSON message.

To serialize in the other direction, define how call IDs and tool results map to each provider’s fields, including any reasoning state required for continuation.


Thinking, reasoning, and 2026's new twist

Reasoning APIs also represent state that may need to survive a tool turn. Providers differ in what they return and what the client must replay.

Anthropic added thinking and redacted_thinking blocks as first-class content types. On extended-thinking requests, the assistant message contains a thinking block before any user-visible text or tool calls. You pass those blocks back verbatim on the next turn when tools are in play. The model relies on its own reasoning being in context to pick up where it left off.

Gemini uses thoughtSignature: opaque encrypted reasoning-state data attached to response parts. Preserve the signature and its association with the original part when constructing the next request. It is not merely an ID pointing to a server-side scratchpad.

OpenAI Responses has explicit reasoning items and function-call items. You can continue stored state with previous_response_id, or manage history yourself and replay the required items. For stateless use, supported models can return encrypted reasoning content to carry forward. Harmony, used by gpt-oss, is a rendered token format with channels and recipients; it is a different layer from the Responses JSON API.

Carrying reasoning state across a tool turn

Anthropic

Returned: thinking / redacted_thinking blocks, including signatures

Next request: Replay the complete required blocks with the assistant's tool calls.

Visible thinking may be a summary. Do not reconstruct a signed block from displayed text.

Gemini

Returned: thoughtSignature on response parts

Next request: Return the original parts with their signatures intact.

The signature carries opaque reasoning-state data; keep its association with the original part.

OpenAI Responses

Returned: reasoning items and a response ID

Next request: Continue stored state with previous_response_id, or replay items yourself.

Stateless workflows can carry encrypted reasoning content when supported. A server-held ID is one option, not the only one.

Examples of state carried across a tool turn. Preserve complete provider-required blocks or items; readable summaries alone may not contain the state needed to resume reasoning.

OpenAI Chat Completions vs. Harmony
Chat Completions (JSON)
{
"role": "assistant",
"content": "Let me check.",
"tool_calls": [
{ "id": "call_1",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"location\":\"Tokyo\"}"
} }
]
}
One assistant message with a tool_calls field. This Chat Completions example has no reasoning item; Responses is a different JSON API.
Harmony (rendered tokens)
<|start|>assistant
<|channel|>analysis<|message|>
The user asked about Tokyo weather. I should call get_weather.
<|end|>
<|start|>assistant
<|channel|>commentary to=functions.get_weather
<|constrain|>json<|message|>{"location":"Tokyo"}
<|call|>
Each assistant utterance carries a channel (analysis / commentary / final) and tool calls are their own recipient-tagged message ending in <|call|>.

Chat Completions places tool calls in a field on the assistant message. Harmony makes them a distinct message with a recipient, and separates reasoning into its own channel. Both render the same logical turn; the boundaries live in different places.

Harmony documents token boundaries that the API schemas leave to the provider’s renderer. Its explicit channels and recipients make those boundaries available to inspect.


Misconceptions

"All three formats are equivalent, so it doesn't matter which you use." The formats encode the same logical object, but the encoding is not lossless in practice. Anthropic's tool_result can be a list of content blocks including images; OpenAI's tool message content is historically a string. A Gemini adapter that drops returned IDs or thought signatures can break a continuation. If you switch providers for the same agent without updating the transcript handling, you can get subtle misbehavior that's hard to trace to the transcript layer.

"The model sees the JSON I sent." No. The model sees tokens. The JSON is input to a chat-template step that renders structured messages down to a raw token stream with provider-specific special tokens. When Anthropic says the model "handles tool_use blocks natively," they mean the model was trained on token sequences that encoded tool_use blocks in a particular way, and the service prepares the corresponding model input. Your JSON and the model's prompt are related by a formatter, not by identity.

"Tool results come from the tool role." That is the Chat Completions convention. Anthropic puts tool results inside a user message with a tool_result block. Gemini does the same with a functionResponse part inside a user message. An adapter must use the destination API’s role and content conventions.

"You can skip the id field and the model will figure it out." Preserve the identifiers required by the API, even for one call. Missing or mismatched identifiers may cause request rejection or associate a result with the wrong request. Parallel calls make correct pairing especially important.

What's next

The next post covers the Model Context Protocol, a cross-system standard for the tool surface that lets one MCP server serve Anthropic, OpenAI, and Gemini clients without caring which transcript format each one is speaking.


Additional reading (and watching)

  • Yao, S., et al. (2022). ReAct: Synergizing Reasoning and Acting in Language Models. arXiv:2210.03629. The original pattern of interleaving reasoning and action strings inside a single text completion, before provider APIs had typed tool-use blocks.

  • Hugging Face. (2025). Chat Templates. Reference for how structured messages (including tool calls) get rendered to raw token strings via Jinja templates. Makes the "model sees tokens, not JSON" point concrete.

  • Anthropic. (2025). Messages API: Tool use. Describes the tool_use / tool_result block pair and the tool_use_id linking convention.

  • Anthropic. (2025). Messages API reference. Canonical definition of the content-block array and the set of block types (text, tool_use, tool_result, thinking, redacted_thinking, server_tool_use).

  • OpenAI. (2025). Chat Completions API: Function calling. The tool_calls field on assistant messages and the tool role with tool_call_id.

  • Google. (2025). Gemini API: Function calling. The functionCall and functionResponse parts inside contents, and the user/model role convention.

  • Anthropic. (2025). Extended thinking. Explains thinking and redacted_thinking blocks, and why they must be passed back verbatim on subsequent turns when tools are involved.

  • Google. (2025). Thinking with Gemini. Describes thoughtSignature and how it preserves reasoning state across turns without exposing the reasoning contents to the client.

  • OpenAI. (2025). Harmony response format and the Responses API. Harmony's channel-based messaging and the Responses API's server-stored conversation state.

  • Vercel. (2025). AI SDK: Message and tool-call shapes. Documentation of a provider-neutral message normalization layer.

  • Anthropic. (2024). Introducing the Model Context Protocol and the MCP specification. MCP deliberately defines the tool surface and leaves the host-side transcript format out of scope.

  • OpenAI. Reasoning models. Preserving reasoning items with stored and stateless Responses workflows.