Claude Messages Protocol
Claude Messages is Anthropic’s native conversation protocol. If your client is already developed according to Anthropic specifications, simply replace the Base URL and API Key with RouteAPI to use it directly without modifying the request structure.
Protocol Overview
Section titled “Protocol Overview”Claude Messages uses a messages array to represent multi-turn conversations, an independent system field for system prompts, and requires explicit declaration of max_tokens. Compared to OpenAI-compatible format, its content block structure is more unified: text, images, tool calls, and tool results are all different type values in the same array.
Applicable scenarios:
| Scenario | Description |
|---|---|
| Claude Code | Anthropic’s official coding agent, only recognizes /v1/messages |
| Anthropic SDK | anthropic Python / TypeScript SDK, just change base_url |
| Native message format clients | Applications already organized with content block structure |
| Extended thinking & prompt caching | Depends on Claude-specific capabilities like thinking, cache_control |
If your client only supports OpenAI protocol, please use Chat Completions instead. RouteAPI will complete the necessary format adaptation internally, but prioritize the protocol natively supported by the client for best compatibility.
Endpoint Details
Section titled “Endpoint Details”POST /v1/messagesFull address:
https://api.routeapi.ai/v1/messagesRequest headers support two authentication methods, both using the same RouteAPI Token:
Authorization: Bearer sk-your-routeapi-tokenContent-Type: application/jsonx-api-key: sk-your-routeapi-tokenanthropic-version: 2023-06-01Content-Type: application/jsonx-api-key is the default method for Anthropic SDK. RouteAPI automatically recognizes it as Token on the /v1/messages path, so official SDK requires no additional configuration. anthropic-version is passed through to upstream as-is, and official SDK will automatically include it.
Request Format
Section titled “Request Format”| Field | Type | Required | Description |
|---|---|---|---|
model | string | Yes | Model ID, must be an available model for current account |
messages | array | Yes | Conversation message list, at least one, role must alternate |
max_tokens | integer | Yes | Maximum output tokens, required by Claude protocol |
system | string/array | No | System prompt, independent field, not in messages |
temperature | number | No | Sampling temperature, range 0 to 1 |
top_p | number | No | nucleus sampling parameter |
top_k | integer | No | Sample only from the K most probable tokens |
stream | boolean | No | Whether to use SSE streaming output |
stop_sequences | array | No | Custom stop sequences |
tools | array | No | Tool definition list |
tool_choice | object | No | Tool selection strategy |
thinking | object | No | Extended thinking configuration, depends on model support |
metadata | object | No | Request metadata, Claude-specific |
max_tokens is Required
Section titled “max_tokens is Required”This is the most common pitfall when migrating from OpenAI. OpenAI’s max_tokens uses model default limit when omitted, Claude protocol has no default value, and upstream will return invalid_request_error when missing.
{ "model": "claude-sonnet-4-5", "max_tokens": 1024, "messages": [{ "role": "user", "content": "Hello" }]}max_tokens is an output limit, does not include input tokens, and is not an exact length commitment: the model may finish early (stop_reason: "end_turn") or be truncated exactly at the limit (stop_reason: "max_tokens"). For production, set based on expected response length with some margin, and check stop_reason to determine if truncated.
system is an Independent Field
Section titled “system is an Independent Field”Claude protocol does not accept messages with role: "system". System prompts must be placed in the top-level system field, and the messages array can only contain user and assistant.
Correct usage:
{ "model": "claude-sonnet-4-5", "max_tokens": 1024, "system": "You are a rigorous technical assistant, keep answers concise.", "messages": [{ "role": "user", "content": "Explain what an API gateway is" }]}Incorrect usage (Claude protocol will reject):
{ "messages": [ { "role": "system", "content": "You are a rigorous technical assistant." }, { "role": "user", "content": "Explain what an API gateway is" } ]}system also supports array form for setting prompt caching on different paragraphs individually:
{ "system": [ { "type": "text", "text": "You are a code review assistant." }, { "type": "text", "text": "Here is the complete project coding standard...", "cache_control": { "type": "ephemeral" } } ]}metadata
Section titled “metadata”metadata carries request meta-information, currently only has one user_id field for upstream abuse detection. Do not place personally identifiable information like email or phone number here, recommend passing hash values or internal IDs.
{ "metadata": { "user_id": "a3f1c2d4e5b6" }}Message Structure
Section titled “Message Structure”Each message in the messages array contains role and content fields. role can only be user or assistant, must alternate, and the first must be user.
content supports two forms. String is shorthand for single text:
{ "role": "user", "content": "Please introduce RouteAPI in one sentence" }Array form consists of content blocks, each distinguished by type:
| type | Location | Description |
|---|---|---|
text | user / assistant | Plain text content |
image | user | Image input, supports base64 and URL |
document | user | Document input, depends on model support |
tool_use | assistant | Model requests to call tool |
tool_result | user | Tool execution result returned by client |
thinking | assistant | Extended thinking content block |
Multimodal Content
Section titled “Multimodal Content”Images are passed through the source field. Base64 method requires providing media_type as well:
{ "role": "user", "content": [ { "type": "image", "source": { "type": "base64", "media_type": "image/jpeg", "data": "/9j/4AAQSkZJRgABAQAAAQ..." } }, { "type": "text", "text": "What controls are in this image?" } ]}URL method is more concise but requires the image address to be accessible by upstream:
{ "role": "user", "content": [ { "type": "image", "source": { "type": "url", "url": "https://example.com/screenshot.png" } }, { "type": "text", "text": "Describe the layout of this interface" } ]}Placing text blocks after image blocks usually works better. Multiple images can be included in one request, but will significantly increase input tokens, recommend compressing size first.
Tool Calling
Section titled “Tool Calling”tools Definition Format
Section titled “tools Definition Format”Claude’s tool definition is a flat structure with parameter schema field called input_schema:
{ "tools": [ { "name": "get_weather", "description": "Query current weather for specified city. Use full city name in Chinese.", "input_schema": { "type": "object", "properties": { "city": { "type": "string", "description": "City name, e.g. Beijing" }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"] } }, "required": ["city"] } } ]}Compared to OpenAI’s nested structure, the difference is Claude has no outer type: "function" wrapper, no function nesting layer, and parameters is renamed to input_schema:
{ "tools": [ { "type": "function", "function": { "name": "get_weather", "description": "Query current weather for specified city.", "parameters": { "type": "object", "properties": {} } } } ]}The quality of description directly determines whether the model will correctly choose the tool, recommend clearly stating purpose, parameter format, and boundary conditions.
tool_choice Options
Section titled “tool_choice Options”| Format | Behavior |
|---|---|
{ "type": "auto" } | Model decides whether to call tools, default value |
{ "type": "any" } | Must call tool, but model chooses which one |
{ "type": "tool", "name": "get_weather" } | Force call specified tool |
{ "type": "none" } | Prohibit tool calls |
Adding "disable_parallel_tool_use": true can limit the model to initiate only one tool call at a time.
Tool Result Passing
Section titled “Tool Result Passing”Tool calling is a complete conversational round-trip. After the model returns a tool_use block, you need to pass back the original assistant message along with execution results.
Step one, model returns tool call request:
{ "role": "assistant", "content": [ { "type": "text", "text": "Let me check the weather in Beijing." }, { "type": "tool_use", "id": "toolu_01A09q90qw90lq917835lq9", "name": "get_weather", "input": { "city": "Beijing", "unit": "celsius" } } ]}Step two, add this assistant message as-is to messages, then append a user message carrying the result. tool_use_id must exactly match the id from previous step:
{ "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": "Beijing, sunny, temperature 23 celsius, humidity 45%." } ]}When tool execution fails, use is_error marker to let the model know it needs to change strategy rather than retry:
{ "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": "Weather service timeout, no data retrieved.", "is_error": true}Note that tool_result belongs to user role, Claude protocol has no independent role: "tool" like OpenAI. If the model returns multiple tool_use blocks at once, all corresponding tool_result must be placed in the content array of the same user message.
Response Format
Section titled “Response Format”Standard Response
Section titled “Standard Response”{ "id": "msg_01XFDUDYJgAACzvnptvVoYEL", "type": "message", "role": "assistant", "model": "claude-sonnet-4-5", "content": [ { "type": "text", "text": "RouteAPI is an API gateway that uniformly manages multiple AI model providers." } ], "stop_reason": "end_turn", "stop_sequence": null, "usage": { "input_tokens": 24, "output_tokens": 18 }}content is always an array, even with only one text segment. Clients should not assume content[0] is a text block; when the model enables extended thinking or initiates tool calls, the first block might be thinking or tool_use.
stop_reason values:
| Value | Meaning |
|---|---|
end_turn | Model naturally finished response |
max_tokens | Reached max_tokens limit and truncated |
stop_sequence | Hit a sequence in stop_sequences |
tool_use | Model requests to call tool, waiting for result |
Streaming Response
Section titled “Streaming Response”Setting stream: true returns SSE. Claude’s streaming format differs significantly from OpenAI: each event has an explicit event: type name, and the end marker is a message_stop event, not data: [DONE].
event: message_startdata: {"type":"message_start","message":{"id":"msg_01XFD","type":"message","role":"assistant","content":[],"model":"claude-sonnet-4-5","usage":{"input_tokens":24,"output_tokens":1}}}
event: content_block_startdata: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
event: content_block_deltadata: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"RouteAPI"}}
event: content_block_deltadata: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" is an"}}
event: content_block_stopdata: {"type":"content_block_stop","index":0}
event: message_deltadata: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":18}}
event: message_stopdata: {"type":"message_stop"}Event type descriptions:
| Event | Description |
|---|---|
message_start | Message start, carries initial usage (output_tokens not accurate yet) |
content_block_start | A content block starts, index identifies position |
content_block_delta | Incremental content, text uses text_delta, tool parameters use input_json_delta |
content_block_stop | Current content block ends |
message_delta | Message-level increment, carries final stop_reason and cumulative output_tokens |
message_stop | Entire response ends |
ping | Heartbeat event, can be ignored |
error | Error occurred mid-stream |
Tool call parameters are returned as JSON strings piece by piece, need to concatenate all input_json_delta’s partial_json before parsing:
event: content_block_deltadata: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"city\":"}}
event: content_block_deltadata: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"\"Beijing\"}"}}Group and accumulate by index, do not attempt to parse intermediate states during concatenation.
usage Field
Section titled “usage Field”| Field | Description |
|---|---|
input_tokens | Input token count, excludes cache hit portion |
output_tokens | Output token count |
cache_creation_input_tokens | Tokens written to prompt cache |
cache_read_input_tokens | Tokens read from prompt cache |
server_tool_use | Server-side tool usage, e.g. web_search_requests |
In streaming response, output_tokens should use the value in message_delta event, the one in message_start is initial placeholder. Billing is based on console logs, actual supported fields depend on selected model.
Comparison with OpenAI Format
Section titled “Comparison with OpenAI Format”Parameter Mapping Table
Section titled “Parameter Mapping Table”| Claude Messages | OpenAI Chat Completions | Difference |
|---|---|---|
model | model | Same |
system (top-level field) | messages[0] with role: "system" | Different location, Claude rejects system messages |
messages | messages | Claude only allows alternating user / assistant |
max_tokens | max_tokens / max_completion_tokens | Claude required, OpenAI optional |
stop_sequences | stop | Different name |
temperature | temperature | Claude limit 1, OpenAI limit 2 |
top_k | No equivalent | OpenAI does not support |
tools[].input_schema | tools[].function.parameters | Different hierarchy and field name |
tool_choice: {"type":"any"} | tool_choice: "required" | Different format |
metadata.user_id | user | Different location |
thinking | reasoning_effort | Different control method |
| No equivalent | n | Claude does not support generating multiple candidates |
| No equivalent | frequency_penalty / presence_penalty | Claude does not support |
| No equivalent | response_format | Claude uses tools or prompts to constrain output structure |
Response structure differences:
| Item | Claude Messages | OpenAI Chat Completions |
|---|---|---|
| Top-level content | content array | choices[0].message |
| Text location | content[0].text | choices[0].message.content |
| Stop reason | stop_reason | finish_reason |
| Tool calls | tool_use blocks in content | message.tool_calls |
| Tool result role | tool_result block in user message | Independent role: "tool" |
| Input usage | usage.input_tokens | usage.prompt_tokens |
| Output usage | usage.output_tokens | usage.completion_tokens |
| Total field | None, must sum manually | usage.total_tokens |
| Streaming end | message_stop event | data: [DONE] |
Migration Notes
Section titled “Migration Notes”When migrating from OpenAI to Claude Messages, check in this order:
- Move system message from
messagesarray to top-levelsystemfield. - Add
max_tokens, this is required. - Confirm first message in
messagesisuser, and roles strictly alternate with no consecutive same-role messages. - Remove
typeandfunctionwrapper layers from tool definitions, renameparameterstoinput_schema. - Change tool results from
role: "tool"totool_resultblock inusermessage, and aligntool_use_id. - If
temperaturewas originally greater than1, adjust down to Claude’s value range. - Change response parsing to iterate through
contentarray and dispatch bytype, do not assume fixed indices. - Change streaming parsing to dispatch by
event:type, replace end condition withmessage_stop.
If refactoring cost is high, you can continue using OpenAI protocol to call Claude series models, with RouteAPI completing format conversion. The tradeoff is that some Claude-specific capabilities (like complete control of extended thinking, fine-grained prompt caching) cannot be fully expressed in OpenAI format.
Complete Examples
Section titled “Complete Examples”Basic Conversation
Section titled “Basic Conversation”curl:
curl https://api.routeapi.ai/v1/messages \ -H "x-api-key: $ROUTEAPI_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-sonnet-4-5", "max_tokens": 1024, "system": "You are a rigorous technical assistant, keep answers concise.", "messages": [ { "role": "user", "content": "Please introduce RouteAPI in one sentence" } ] }'Anthropic Python SDK, just change base_url:
import osfrom anthropic import Anthropic
client = Anthropic( api_key=os.environ["ROUTEAPI_KEY"], base_url="https://api.routeapi.ai",)
message = client.messages.create( model="claude-sonnet-4-5", max_tokens=1024, system="You are a rigorous technical assistant, keep answers concise.", messages=[ {"role": "user", "content": "Please introduce RouteAPI in one sentence"}, ],)
print(message.content[0].text)print(message.usage.input_tokens, message.usage.output_tokens)base_url only needs the domain, SDK will automatically append /v1/messages. Streaming calls use client.messages.stream():
with client.messages.stream( model="claude-sonnet-4-5", max_tokens=1024, messages=[{"role": "user", "content": "Explain step by step what an API gateway is"}],) as stream: for text in stream.text_stream: print(text, end="", flush=True)
final = stream.get_final_message() print() print(final.stop_reason, final.usage.output_tokens)Image Understanding
Section titled “Image Understanding”curl, using base64:
curl https://api.routeapi.ai/v1/messages \ -H "x-api-key: $ROUTEAPI_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-sonnet-4-5", "max_tokens": 1024, "messages": [ { "role": "user", "content": [ { "type": "image", "source": { "type": "base64", "media_type": "image/jpeg", "data": "'"$(base64 -w 0 screenshot.jpg)"'" } }, { "type": "text", "text": "What UI controls are in this image?" } ] } ] }'Python SDK:
import base64import osfrom anthropic import Anthropic
client = Anthropic( api_key=os.environ["ROUTEAPI_KEY"], base_url="https://api.routeapi.ai",)
with open("screenshot.jpg", "rb") as f: image_data = base64.standard_b64encode(f.read()).decode("utf-8")
message = client.messages.create( model="claude-sonnet-4-5", max_tokens=1024, messages=[ { "role": "user", "content": [ { "type": "image", "source": { "type": "base64", "media_type": "image/jpeg", "data": image_data, }, }, {"type": "text", "text": "What UI controls are in this image?"}, ], } ],)
print(message.content[0].text)Tool Calling
Section titled “Tool Calling”Complete two-round trip, including result passing:
import jsonimport osfrom anthropic import Anthropic
client = Anthropic( api_key=os.environ["ROUTEAPI_KEY"], base_url="https://api.routeapi.ai",)
tools = [ { "name": "get_weather", "description": "Query current weather for specified city. Use full city name in Chinese.", "input_schema": { "type": "object", "properties": { "city": {"type": "string", "description": "City name, e.g. Beijing"}, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}, }, "required": ["city"], }, }]
def get_weather(city: str, unit: str = "celsius") -> str: # Replace with actual weather service call return f"{city}, sunny, temperature 23 celsius, humidity 45%."
messages = [{"role": "user", "content": "What's the weather like in Beijing now?"}]
response = client.messages.create( model="claude-sonnet-4-5", max_tokens=1024, tools=tools, messages=messages,)
# Only need to execute tool and pass back when stop_reason is tool_useif response.stop_reason == "tool_use": # Original assistant message must be added back as-is, otherwise tool_use_id cannot align messages.append({"role": "assistant", "content": response.content})
tool_results = [] for block in response.content: if block.type != "tool_use": continue try: result = get_weather(**block.input) is_error = False except Exception as exc: result = f"Tool execution failed: {exc}" is_error = True tool_results.append( { "type": "tool_result", "tool_use_id": block.id, "content": result, "is_error": is_error, } )
# All tool results from same round go in one user message messages.append({"role": "user", "content": tool_results})
response = client.messages.create( model="claude-sonnet-4-5", max_tokens=1024, tools=tools, messages=messages, )
print(response.content[0].text)Corresponding curl second-round request:
curl https://api.routeapi.ai/v1/messages \ -H "x-api-key: $ROUTEAPI_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-sonnet-4-5", "max_tokens": 1024, "tools": [ { "name": "get_weather", "description": "Query current weather for specified city.", "input_schema": { "type": "object", "properties": { "city": { "type": "string" } }, "required": ["city"] } } ], "messages": [ { "role": "user", "content": "What'\''s the weather like in Beijing now?" }, { "role": "assistant", "content": [ { "type": "tool_use", "id": "toolu_01A09q90qw90lq917835lq9", "name": "get_weather", "input": { "city": "Beijing" } } ] }, { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": "Beijing, sunny, temperature 23 celsius, humidity 45%." } ] } ] }'Compatibility Reminders
Section titled “Compatibility Reminders”- Actual parameter support depends on selected model and upstream service capabilities, optional capabilities like
thinking,cache_control,mcp_serversshould be verified in test environment first. - Optional parameters explicitly passed as
0orfalsewill be treated as user explicit settings, not discarded as default values. - Production environment should fix model ID, do not rely on temporary aliases or display names.
- Record request ID, model ID, status code, and token usage for each request to facilitate troubleshooting latency and cost anomalies.
- Error responses follow Claude’s
{"type": "error", "error": {...}}structure, see Error Handling for details.