Transcript Formats: How Providers Represent Tool Use
One toy exchange in three API envelopes
Anthropic
OpenAI Chat
Gemini GenerateContent
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:
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
namewith thesearguments, 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.
| Concept | Anthropic | OpenAI Chat Completions | Gemini |
|---|---|---|---|
−Assistant tool call | tool_use block inside assistant message | tool_calls array on assistant message | functionCall 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" } } }
]
} |
+Tool result block | tool_result block inside a user message | separate message with role: 'tool' | functionResponse part inside a user message |
Roles used | user · assistant; system is a separate parameter | system · user · assistant · tool | user · model (tool results ride in 'user') |
Multi-call in one turn | multiple tool_use blocks in one assistant message | multiple entries in the tool_calls array | multiple functionCall parts in one model message |
Pairing ID | tool_use.id ⇄ tool_result.tool_use_id | tool_calls[i].id ⇄ tool message's tool_call_id | echo returned call IDs; older modes use name/order |
Reasoning block | thinking / redacted_thinking blocks | No reasoning item in this Chat example; Responses is a separate API | thoughtSignature 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.
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.
{"role": "assistant","content": "Let me check.","tool_calls": [{ "id": "call_1","type": "function","function": {"name": "get_weather","arguments": "{\"location\":\"Tokyo\"}"} }]}
tool_calls field. This Chat Completions example has no reasoning item; Responses is a different JSON API.<|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|>
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_resultblock pair and thetool_use_idlinking 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_callsfield on assistant messages and thetoolrole withtool_call_id. -
Google. (2025). Gemini API: Function calling. The
functionCallandfunctionResponseparts insidecontents, and theuser/modelrole convention. -
Anthropic. (2025). Extended thinking. Explains
thinkingandredacted_thinkingblocks, and why they must be passed back verbatim on subsequent turns when tools are involved. -
Google. (2025). Thinking with Gemini. Describes
thoughtSignatureand 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.