Claude Messages 协议
Claude Messages 是 Anthropic 的原生对话协议。如果你的客户端已经按 Anthropic 规范开发,把 Base URL 和 API Key 换成 RouteAPI 即可直接使用,不需要改写请求结构。
Claude Messages 用一个 messages 数组表达多轮对话,用独立的 system 字段表达系统提示词,并要求显式声明 max_tokens。相比 OpenAI 兼容格式,它的内容块(content block)结构更统一:文本、图像、工具调用、工具结果都是同一个数组里的不同 type。
适用场景:
| 场景 | 说明 |
|---|---|
| Claude Code | Anthropic 官方编码代理,只认 /v1/messages |
| Anthropic SDK | anthropic Python / TypeScript SDK,改 base_url 即可 |
| 原生消息格式客户端 | 已经按 content block 结构组织提示词的应用 |
| 扩展思考与提示缓存 | 依赖 thinking、cache_control 等 Claude 特有能力 |
如果你的客户端只支持 OpenAI 协议,请改用 Chat Completions。RouteAPI 会在内部完成必要的格式适配,但优先选择客户端原生支持的协议,兼容性最好。
POST /v1/messages完整地址:
https://api.routeapi.ai/v1/messages请求头支持两种鉴权写法,都使用同一个 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 是 Anthropic SDK 的默认写法,RouteAPI 在 /v1/messages 路径上会自动把它识别为 Token,所以官方 SDK 无需额外配置。anthropic-version 会原样透传给上游,官方 SDK 会自动带上。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
model | string | 是 | 模型 ID,必须是当前账户可用模型 |
messages | array | 是 | 对话消息列表,至少一条,role 需交替出现 |
max_tokens | integer | 是 | 最大输出 token 数,Claude 协议强制要求 |
system | string/array | 否 | 系统提示词,独立字段,不放在 messages 里 |
temperature | number | 否 | 采样温度,取值 0 到 1 |
top_p | number | 否 | nucleus sampling 参数 |
top_k | integer | 否 | 只从概率最高的 K 个 token 中采样 |
stream | boolean | 否 | 是否使用 SSE 流式输出 |
stop_sequences | array | 否 | 自定义停止序列 |
tools | array | 否 | 工具定义列表 |
tool_choice | object | 否 | 工具选择策略 |
thinking | object | 否 | 扩展思考配置,取决于模型是否支持 |
metadata | object | 否 | 请求元信息,Claude 特有 |
max_tokens 是必填参数
Section titled “max_tokens 是必填参数”这是从 OpenAI 迁移过来最容易踩的坑。OpenAI 的 max_tokens 省略时会使用模型默认上限,Claude 协议没有默认值,缺失时上游会直接返回 invalid_request_error。
{ "model": "claude-sonnet-4-5", "max_tokens": 1024, "messages": [{ "role": "user", "content": "你好" }]}max_tokens 是输出上限,不包含输入 token,也不是精确长度承诺:模型可能提前结束(stop_reason: "end_turn"),也可能正好被截断(stop_reason: "max_tokens")。生产环境建议按业务预期的回复长度设置,留一定余量,同时检查 stop_reason 判断是否被截断。
system 是独立字段
Section titled “system 是独立字段”Claude 协议不接受 role: "system" 的消息。系统提示词必须放在请求顶层的 system 字段里,messages 数组只能包含 user 和 assistant。
正确写法:
{ "model": "claude-sonnet-4-5", "max_tokens": 1024, "system": "你是一个严谨的技术助手,回答保持简洁。", "messages": [{ "role": "user", "content": "解释什么是 API 网关" }]}错误写法(Claude 协议会拒绝):
{ "messages": [ { "role": "system", "content": "你是一个严谨的技术助手。" }, { "role": "user", "content": "解释什么是 API 网关" } ]}system 也支持数组形式,用于给不同段落单独设置提示缓存:
{ "system": [ { "type": "text", "text": "你是一个代码审查助手。" }, { "type": "text", "text": "以下是项目编码规范全文……", "cache_control": { "type": "ephemeral" } } ]}metadata
Section titled “metadata”metadata 用于携带请求元信息,目前只有 user_id 一个字段,用于上游的滥用检测。不要在这里放邮箱、手机号等可识别个人身份的信息,建议传哈希值或内部 ID。
{ "metadata": { "user_id": "a3f1c2d4e5b6" }}messages 数组中每条消息包含 role 和 content 两个字段。role 只能是 user 或 assistant,且必须交替出现,第一条必须是 user。
content 支持两种形式。字符串是单文本的简写:
{ "role": "user", "content": "请用一句话介绍 RouteAPI" }数组形式由内容块组成,每个块用 type 区分:
| type | 出现位置 | 说明 |
|---|---|---|
text | user / assistant | 纯文本内容 |
image | user | 图像输入,支持 base64 和 URL |
document | user | 文档输入,取决于模型是否支持 |
tool_use | assistant | 模型请求调用工具 |
tool_result | user | 客户端回传的工具执行结果 |
thinking | assistant | 扩展思考内容块 |
图像通过 source 字段传入。base64 方式需要同时给出 media_type:
{ "role": "user", "content": [ { "type": "image", "source": { "type": "base64", "media_type": "image/jpeg", "data": "/9j/4AAQSkZJRgABAQAAAQ..." } }, { "type": "text", "text": "这张图里有哪些控件?" } ]}URL 方式更简洁,但要求图片地址可被上游访问:
{ "role": "user", "content": [ { "type": "image", "source": { "type": "url", "url": "https://example.com/screenshot.png" } }, { "type": "text", "text": "描述这个界面的布局" } ]}把文本块放在图像块之后通常效果更好。一次请求可以放多张图,但会显著增加输入 token,建议先压缩尺寸。
tools 定义格式
Section titled “tools 定义格式”Claude 的工具定义是平铺结构,参数 schema 字段叫 input_schema:
{ "tools": [ { "name": "get_weather", "description": "查询指定城市的当前天气。城市名使用中文全称。", "input_schema": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名,如 北京" }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"] } }, "required": ["city"] } } ]}对比 OpenAI 的嵌套结构,差异在于 Claude 没有外层 type: "function" 包装,也没有 function 嵌套层,且 parameters 改名为 input_schema:
{ "tools": [ { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的当前天气。", "parameters": { "type": "object", "properties": {} } } } ]}description 的质量直接决定模型是否会正确选用工具,建议写清用途、参数格式和边界条件。
tool_choice 选项
Section titled “tool_choice 选项”| 写法 | 行为 |
|---|---|
{ "type": "auto" } | 模型自行决定是否调用工具,默认值 |
{ "type": "any" } | 必须调用工具,但由模型选择调用哪个 |
{ "type": "tool", "name": "get_weather" } | 强制调用指定工具 |
{ "type": "none" } | 禁止调用工具 |
追加 "disable_parallel_tool_use": true 可以限制模型单次只发起一个工具调用。
工具结果传递
Section titled “工具结果传递”工具调用是一次完整的对话往返。模型返回 tool_use 块后,你需要把原始 assistant 消息和执行结果一起回传。
第一步,模型返回工具调用请求:
{ "role": "assistant", "content": [ { "type": "text", "text": "我来查一下北京的天气。" }, { "type": "tool_use", "id": "toolu_01A09q90qw90lq917835lq9", "name": "get_weather", "input": { "city": "北京", "unit": "celsius" } } ]}第二步,把这条 assistant 消息原样加入 messages,再追加一条 user 消息携带结果。tool_use_id 必须与上一步的 id 完全一致:
{ "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": "北京,晴,气温 23 摄氏度,湿度 45%。" } ]}工具执行失败时用 is_error 标记,让模型知道需要换策略而不是重试:
{ "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": "天气服务超时,未获取到数据。", "is_error": true}注意 tool_result 属于 user 角色,Claude 协议里没有 OpenAI 那样独立的 role: "tool"。如果模型一次返回了多个 tool_use 块,所有对应的 tool_result 必须放在同一条 user 消息的 content 数组里。
{ "id": "msg_01XFDUDYJgAACzvnptvVoYEL", "type": "message", "role": "assistant", "model": "claude-sonnet-4-5", "content": [ { "type": "text", "text": "RouteAPI 是一个统一管理多家 AI 模型供应商的 API 网关。" } ], "stop_reason": "end_turn", "stop_sequence": null, "usage": { "input_tokens": 24, "output_tokens": 18 }}content 始终是数组,即使只有一段文本。客户端不要假设 content[0] 就是文本块,模型开启扩展思考或发起工具调用时,第一个块可能是 thinking 或 tool_use。
stop_reason 的取值:
| 值 | 含义 |
|---|---|
end_turn | 模型自然结束回复 |
max_tokens | 达到 max_tokens 上限被截断 |
stop_sequence | 命中 stop_sequences 中的序列 |
tool_use | 模型请求调用工具,等待结果回传 |
设置 stream: true 后返回 SSE。Claude 的流式格式与 OpenAI 差异较大:每个事件都有明确的 event: 类型名,结束标志是 message_stop 事件,而不是 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":" 是一个"}}
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"}事件类型说明:
| 事件 | 说明 |
|---|---|
message_start | 消息开始,携带初始 usage(此时 output_tokens 不准) |
content_block_start | 一个内容块开始,index 标识位置 |
content_block_delta | 增量内容,文本用 text_delta,工具参数用 input_json_delta |
content_block_stop | 当前内容块结束 |
message_delta | 消息级增量,携带最终 stop_reason 和累计 output_tokens |
message_stop | 整个响应结束 |
ping | 心跳事件,可忽略 |
error | 流中途出错 |
工具调用的参数是逐片返回的 JSON 字符串,需要把所有 input_json_delta 的 partial_json 拼接完整后再解析:
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":"\"北京\"}"}}按 index 分组累积,不要在拼接过程中尝试解析中间态。
usage 字段
Section titled “usage 字段”| 字段 | 说明 |
|---|---|
input_tokens | 输入 token 数,不含缓存命中部分 |
output_tokens | 输出 token 数 |
cache_creation_input_tokens | 写入提示缓存的 token 数 |
cache_read_input_tokens | 从提示缓存读取的 token 数 |
server_tool_use | 服务端工具用量,如 web_search_requests |
流式响应中 output_tokens 要以 message_delta 事件里的值为准,message_start 里的是初始占位。计费口径以控制台日志为准,实际支持的字段取决于所选模型。
与 OpenAI 格式对比
Section titled “与 OpenAI 格式对比”| Claude Messages | OpenAI Chat Completions | 差异说明 |
|---|---|---|
model | model | 一致 |
system(顶层字段) | messages[0] 中 role: "system" | 位置不同,Claude 不接受 system 消息 |
messages | messages | Claude 只允许 user / assistant 交替 |
max_tokens | max_tokens / max_completion_tokens | Claude 必填,OpenAI 可选 |
stop_sequences | stop | 名称不同 |
temperature | temperature | Claude 上限 1,OpenAI 上限 2 |
top_k | 无对应 | OpenAI 不支持 |
tools[].input_schema | tools[].function.parameters | 层级和字段名都不同 |
tool_choice: {"type":"any"} | tool_choice: "required" | 写法不同 |
metadata.user_id | user | 位置不同 |
thinking | reasoning_effort | 控制方式不同 |
| 无对应 | n | Claude 不支持一次生成多个候选 |
| 无对应 | frequency_penalty / presence_penalty | Claude 不支持 |
| 无对应 | response_format | Claude 用工具或提示词约束输出结构 |
响应结构差异:
| 项目 | Claude Messages | OpenAI Chat Completions |
|---|---|---|
| 顶层内容 | content 数组 | choices[0].message |
| 文本位置 | content[0].text | choices[0].message.content |
| 结束原因 | stop_reason | finish_reason |
| 工具调用 | content 中的 tool_use 块 | message.tool_calls |
| 工具结果角色 | user 消息中的 tool_result 块 | 独立的 role: "tool" |
| 输入用量 | usage.input_tokens | usage.prompt_tokens |
| 输出用量 | usage.output_tokens | usage.completion_tokens |
| 总量字段 | 无,需自行相加 | usage.total_tokens |
| 流式结束 | message_stop 事件 | data: [DONE] |
迁移注意事项
Section titled “迁移注意事项”从 OpenAI 迁移到 Claude Messages 时,按以下顺序检查:
- 把 system 消息从
messages数组移到顶层system字段。 - 补上
max_tokens,这是必填项。 - 确认
messages首条是user,且角色严格交替,没有连续两条同角色消息。 - 工具定义去掉
type和function包装层,parameters改名input_schema。 - 工具结果从
role: "tool"改为user消息里的tool_result块,并对齐tool_use_id。 temperature如果原来大于1,需要下调到 Claude 的取值范围内。- 响应解析改为遍历
content数组按type分发,不要假设固定下标。 - 流式解析改为按
event:类型分发,结束条件换成message_stop。
如果改造成本较高,也可以继续用 OpenAI 协议调用 Claude 系列模型,由 RouteAPI 完成格式转换。代价是部分 Claude 特有能力(如扩展思考的完整控制、细粒度提示缓存)在 OpenAI 格式下无法完整表达。
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": "你是一个严谨的技术助手,回答保持简洁。", "messages": [ { "role": "user", "content": "请用一句话介绍 RouteAPI" } ] }'Anthropic Python SDK,只需改 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="你是一个严谨的技术助手,回答保持简洁。", messages=[ {"role": "user", "content": "请用一句话介绍 RouteAPI"}, ],)
print(message.content[0].text)print(message.usage.input_tokens, message.usage.output_tokens)base_url 填到域名即可,SDK 会自动拼接 /v1/messages。流式调用用 client.messages.stream():
with client.messages.stream( model="claude-sonnet-4-5", max_tokens=1024, messages=[{"role": "user", "content": "逐步解释什么是 API 网关"}],) 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)curl,使用 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": "这张图里有哪些界面控件?" } ] } ] }'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": "这张图里有哪些界面控件?"}, ], } ],)
print(message.content[0].text)完整两轮往返,包含结果回传:
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": "查询指定城市的当前天气。城市名使用中文全称。", "input_schema": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名,如 北京"}, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}, }, "required": ["city"], }, }]
def get_weather(city: str, unit: str = "celsius") -> str: # 这里替换为真实的天气服务调用 return f"{city},晴,气温 23 摄氏度,湿度 45%。"
messages = [{"role": "user", "content": "北京现在天气怎么样?"}]
response = client.messages.create( model="claude-sonnet-4-5", max_tokens=1024, tools=tools, messages=messages,)
# stop_reason 为 tool_use 时才需要执行工具并回传if response.stop_reason == "tool_use": # 原始 assistant 消息必须原样加回,否则 tool_use_id 无法对齐 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"工具执行失败:{exc}" is_error = True tool_results.append( { "type": "tool_result", "tool_use_id": block.id, "content": result, "is_error": is_error, } )
# 同一轮的所有工具结果放在一条 user 消息里 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)对应的 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, "tools": [ { "name": "get_weather", "description": "查询指定城市的当前天气。", "input_schema": { "type": "object", "properties": { "city": { "type": "string" } }, "required": ["city"] } } ], "messages": [ { "role": "user", "content": "北京现在天气怎么样?" }, { "role": "assistant", "content": [ { "type": "tool_use", "id": "toolu_01A09q90qw90lq917835lq9", "name": "get_weather", "input": { "city": "北京" } } ] }, { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": "北京,晴,气温 23 摄氏度,湿度 45%。" } ] } ] }'- 参数的实际支持程度取决于所选模型和上游服务能力,
thinking、cache_control、mcp_servers等可选能力建议先在测试环境验证。 - 明确传入
0或false的可选参数会被视为用户显式设置,不会当作缺省值丢弃。 - 生产环境建议固定模型 ID,不要依赖临时别名或展示名称。
- 记录每次请求的 request ID、模型 ID、状态码和 token 用量,便于排查延迟与成本异常。
- 错误响应遵循 Claude 的
{"type": "error", "error": {...}}结构,详见 错误处理。