Chat Completions
详解 POST /openai/v1/chat/completions:消息、流式输出、工具、图片、推理、缓存、会被忽略或拒绝的字段,以及错误。
何时阅读本页
你向 EZAT Router 发送 OpenAI Chat Completions 请求,需要了解每个字段的行为。配置方法(base URL、key、SDK)见 OpenAI 兼容 API。
EZAT 把每个请求转换为 Anthropic 的 Messages 格式,像其他请求一样处理(相同的路由、限额和计费),再把回答转换回来。大多数请求无需修改即可使用。差异列在下文。
请求与响应
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.router.ezat.io/openai/v1", api_key=os.environ["EZAT_API_KEY"])
completion = client.chat.completions.create(
model="anthropic/claude-sonnet-5-5",
max_completion_tokens=64,
messages=[
{"role": "system", "content": "Answer in one sentence."},
{"role": "user", "content": "What is a prompt cache?"},
],
)
print(completion.choices[0].message.content)
print(completion.choices[0].finish_reason, completion.usage){
"id": "chatcmpl-…",
"object": "chat.completion",
"created": 1793318400,
"model": "anthropic/claude-sonnet-5-5",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "A prompt cache stores…", "refusal": null },
"logprobs": null,
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 27,
"completion_tokens": 20,
"total_tokens": 47,
"prompt_tokens_details": { "cached_tokens": 0, "cache_write_tokens": 0 },
"completion_tokens_details": { "reasoning_tokens": 0 }
}
}id是chatcmpl-加上 EZAT 的请求 id,与响应头request-id的值相同。联系支持团队时请提供它。model与你发送的名称完全一致。choices始终只有一个选项。usage.prompt_tokens统计全部输入,无论是否命中缓存。prompt_tokens_details.cached_tokens是从提示缓存(prompt caching)读取的部分,cache_write_tokens是写入缓存的部分。completion_tokens_details.reasoning_tokens统计思考 token,按输出计费。思考文本本身不会返回。
消息
| 角色或内容部分 | EZAT 如何处理 |
|---|---|
system、developer | 合并为一个系统提示词,无论它们出现在 messages 的什么位置。仅支持文本。 |
user | 字符串,或由以下部分组成:text、image_url、file(以 file_data 发送的 PDF)。 |
assistant | 文本和 tool_calls。refusal 部分会被丢弃。 |
tool | tool_call_id 对应的工具结果。同一个 assistant 轮次之后的多个结果会一起发送。 |
function 角色、input_audio 部分 | 以 400 拒绝。 |
messages 中至少要有一条不是 system 或 developer 的消息,否则请求会失败,报错 messages: must contain at least one user message。
流式输出
设置 "stream": true。要在流中获得 token 数,还要设置 "stream_options": {"include_usage": true}。
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.router.ezat.io/openai/v1", api_key=os.environ["EZAT_API_KEY"])
stream = client.chat.completions.create(
model="anthropic/claude-sonnet-5-5",
max_tokens=64,
stream=True,
stream_options={"include_usage": True},
messages=[{"role": "user", "content": "Count from 1 to 10."}],
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
if chunk.usage:
print("\n", chunk.usage)流由一系列 data: 行组成,每行是一个 chat.completion.chunk 对象:
- 第一个 chunk 带有
delta: {"role": "assistant", "content": ""}。 - 文本以
delta.content返回。工具调用以delta.tool_calls返回:先是id和function.name,然后function.arguments分段返回。 - 最后一个带选项的 chunk 有一个空的
delta和finish_reason。 - 设置了
include_usage时,之后还有一个"choices": []且带有usage的 chunk。 - 流以
data: [DONE]结束。它在 EZAT 记录完该请求的用量之后发送。
长时间停顿时,EZAT 会发送 : ping 注释行,SDK 会跳过它们。如果请求在流开始后失败,你会收到 data: {"error": {…}},然后是 data: [DONE],没有用量 chunk,且该请求不计费。如果你提前关闭流,需要为已生成的内容付费,其中输出 token 为估算值(见计费)。
工具
函数工具的用法与 OpenAI 相同:tools、tool_choice、parallel_tool_calls、响应中的 tool_calls,以及携带结果的 tool 消息。
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.router.ezat.io/openai/v1", api_key=os.environ["EZAT_API_KEY"])
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current weather for a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
}
]
messages = [{"role": "user", "content": "What's the weather in Oslo? Use the tool."}]
first = client.chat.completions.create(model="anthropic/claude-sonnet-5-5", max_tokens=64, messages=messages, tools=tools)
reply = first.choices[0].message
print(first.choices[0].finish_reason, reply.tool_calls)
if first.choices[0].finish_reason == "tool_calls":
messages.append(reply)
for call in reply.tool_calls:
messages.append({"role": "tool", "tool_call_id": call.id, "content": "9°C and clear"})
second = client.chat.completions.create(model="anthropic/claude-sonnet-5-5", max_tokens=64, messages=messages, tools=tools)
print(second.choices[0].message.content)| 字段 | 转换为 |
|---|---|
tools(type 为 function,设置了 strict 时一并保留) | 名称、描述和 JSON schema 都相同的工具。其他类型的工具会被拒绝。 |
tool_choice: "auto" / "none" | 相同 |
tool_choice: "required" | 模型必须调用一个工具 |
tool_choice: {"type": "function", "function": {"name": …}} | 模型必须调用该工具 |
parallel_tool_calls: false | 每轮最多调用一个工具 |
没有 tools 时,tool_choice 和 parallel_tool_calls 会被忽略。有些模型会以 400 拒绝强制选择(required 或指定函数);对这些模型请使用 auto。参数以 JSON 字符串形式放在 function.arguments 中返回,与 OpenAI 相同。
图片和 PDF
图片放在 image_url 部分中,可以是 data URL,也可以是 http(s) URL:
{
"role": "user",
"content": [
{ "type": "text", "text": "What is in this image?" },
{ "type": "image_url", "image_url": { "url": "data:image/png;base64,iVBORw0KGgo…" } },
{ "type": "image_url", "image_url": { "url": "https://example.com/photo.jpg" } }
]
}- Data URL 必须是 base64:
data:<type>;base64,<data>,type 为image/jpeg、image/png、image/gif或image/webp。 - http(s) URL 由 EZAT 下载,因为模型只接受内联图片。限制:每张图片 10 MB,每个请求最多 20 个图片 URL、下载总量 32 MB,每次下载 15 秒,最多 3 次重定向。主机必须是公网地址。文件必须是 JPEG、PNG、GIF 或 WebP 图片(EZAT 检查的是文件字节,而不是
content-type)。下载失败会返回400并指出是哪个部分,例如messages[0].content[2].image_url.url: could not download the image (HTTP 404)。 detail会被忽略。- PDF 以 data URL 形式放在
file部分中:{"type": "file", "file": {"file_data": "data:application/pdf;base64,…"}}。不支持file_id,因为没有 Files API。
整个请求体最大 32 MB。
推理
reasoning_effort 设置模型的 effort,并要求模型思考:
reasoning_effort | effort | 是否请求思考 |
|---|---|---|
none、minimal | low | 否 |
low、medium、high | 相同 | 是(adaptive:由模型决定思考多少) |
xhigh、max | 相同 | 是(adaptive) |
其他值会被拒绝。一些较早的模型没有 xhigh 等级。当 tool_choice 强制调用工具时,不会请求思考。没有请求思考,或没有发送 reasoning_effort 时,使用模型自己的默认设置,较新的模型仍可能会思考。无论哪种情况,思考 token 都按输出计费,并计入 usage.completion_tokens_details.reasoning_tokens,思考文本不会返回。
采样参数
temperature 和 top_p 只会转发给 capabilities.sampling 为 true 的模型(见 GET https://api.router.ezat.io/api/models),并且只在思考关闭时转发。temperature 最大为 1,top_p 只在小于 1 时发送。对其他所有模型,它们会被直接丢弃而不报错,因为这些模型只接受默认值。很多 OpenAI 客户端默认发送 temperature: 0,这样处理能让它们正常工作。
输出长度与格式
max_completion_tokens或max_tokens设置输出上限。两者都发送时,以max_completion_tokens为准。两者都不发送时,上限为 32,000 个 token;如果模型的最大输出更低,则以模型的最大输出为准。按实际生成的 token 计费,而不是按上限计费。stop(字符串或列表):输出遇到其中任一字符串时结束。response_format: {"type": "json_schema", "json_schema": {"schema": …}}让模型输出符合该 schema 的 JSON。schema 中的对象需要"additionalProperties": false。response_format: {"type": "json_object"}会被拒绝:没有可以强制执行的 schema。请使用json_schema。
结束原因
finish_reason | 模型停止的原因 |
|---|---|
stop | 正常结束,或遇到了 stop 字符串 |
length | 达到了输出上限或上下文窗口 |
tool_calls | 模型要调用工具 |
content_filter | 模型拒绝回答 |
提示缓存
这个 API 上的缓存是自动的。当请求自己没有缓存断点、且模型支持缓存时,EZAT 会标记两个 5 分钟的断点:一个在稳定前缀之后(系统提示词;没有系统提示词时为工具),另一个在最新的消息上,供下一轮读取。这样,长的多轮对话中已经发送过的内容都按更便宜的缓存读取价格计费。见价格。
- 自己设置断点。像 OpenRouter 一样,在内容部分(
text、image_url、file、系统消息的文本部分)或工具上加上cache_control,EZAT 就不会改动你的断点,也不会再添加:{"type": "text", "text": "…", "cache_control": {"type": "ephemeral"}}。 - 关闭缓存:使用请求头
x-ezat-cache: off。缓存写入比普通输入贵,所以不会重复发送的一次性提示词不缓存更省钱。短于模型最小可缓存长度的提示词本来就不会被缓存。 prompt_cache_key(最多 256 个字符)为对话命名,让它的请求留在保存其缓存的部署上。它的作用与x-ezat-session请求头相同,两者都发送时以请求头为准。
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.router.ezat.io/openai/v1", api_key=os.environ["EZAT_API_KEY"])
completion = client.chat.completions.create(
model="anthropic/claude-sonnet-5-5",
max_tokens=64,
# Keeps every turn of this conversation on one deployment.
prompt_cache_key="conversation-42",
messages=[{"role": "user", "content": "Say hello in five words."}],
# Turns automatic caching off: this prompt won't be sent again.
extra_headers={"x-ezat-cache": "off"},
)
print(completion.usage)两种 API 上会话和缓存如何工作,见提示缓存与会话。
被忽略和被拒绝的字段
EZAT 用不到的字段会被忽略,不会传给模型:user、seed、presence_penalty、frequency_penalty、store、metadata、service_tier,以及本页没有提到的任何字段。n: 1 和 logprobs: false 会被接受。
会改变回答、但又没有对应功能的字段,会以 400 invalid_request_error 拒绝。错误消息会指出字段名:
| 字段 | 错误消息 |
|---|---|
n 大于 1 | n: only 1 choice per request is supported |
logprobs: true | logprobs: log probabilities are not available |
top_logprobs | top_logprobs: log probabilities are not available |
logit_bias(非空) | logit_bias: is not supported |
audio,或 modalities 不是 ["text"] | modalities: only text output is supported |
web_search_options | web_search_options: is not supported |
functions、function_call | functions: the legacy functions API is not supported; use tools |
response_format 的 type 为 json_object | response_format: type "json_object" is not supported; use "json_schema" with a schema (objects need "additionalProperties": false) |
type 不是 function 的工具 | tools[0].type: only function tools are supported |
未知的 reasoning_effort | reasoning_effort: use one of none, minimal, low, medium, high, xhigh, max |
input_audio 内容部分 | messages[0].content[0]: audio input is not supported |
带有 file_id 的 file 部分 | messages[0].content[0].file: send the PDF as file_data (a base64 data URL); file_id is not supported |
function 角色 | messages[0].role: the legacy "function" role is not supported; use tools |
错误消息中的索引指向你请求中被拒绝的那一部分。
错误
错误采用 OpenAI 的外层结构,HTTP 状态码与 Anthropic 格式的 API 相同,只有 529 变为 503。type 和 code 使用 OpenAI 的取值,例如未知模型为 404,code 为 model_not_found。
{
"error": {
"message": "The model `gpt-9` does not exist or you do not have access to it.",
"type": "invalid_request_error",
"param": "model",
"code": "model_not_found"
}
}| 状态码 | type | code | 何时出现 |
|---|---|---|---|
400 | invalid_request_error | null | 请求无效,或某个字段被拒绝 |
401 | invalid_request_error | invalid_api_key | 缺少 key,或 key 无效、已吊销或已过期 |
402 | insufficient_quota | insufficient_quota | 余额已用完,或 key 达到了消费上限 |
403 | permission_error | null | key 无权使用该模型,或组织已被暂停 |
404 | invalid_request_error | model_not_found | 未知模型(param 为 model) |
413 | invalid_request_error | request_too_large | 请求体超过 32 MB |
429 | rate_limit_error | rate_limit_exceeded | 请求过多;等待 retry-after 秒后再试 |
500 | server_error | null | EZAT 或模型提供方出错 |
503 | server_error | overloaded | 暂无可用容量;等待 retry-after 秒后再试 |
504 | server_error | timeout | 非流式请求耗时超过 590 秒;请改用流式 |
失败的请求不计费。openai SDK 会自动重试 429 和 5xx 响应。错误说明了哪些错误值得重试。
给 AI agent 的说明
POST https://api.router.ezat.io/openai/v1/chat/completions,带上Authorization: Bearer $EZAT_API_KEY。- 使用
GET https://api.router.ezat.io/openai/v1/models返回的模型 id。 - 不要发送大于 1 的
n、logprobs、logit_bias、音频、web_search_options、旧版functionsAPI、response_formatjson_object或file_id:它们会返回400。 - 不要依赖
temperature或top_p:在这里大多数模型会忽略它们。 - 流式请求要设置
stream_options.include_usage才能拿到 token 数;读取文本时跳过choices为空的 chunk。 - 多轮对话保持提示缓存开启,并为每个对话传入一个固定的
prompt_cache_key。一次性提示词发送x-ezat-cache: off。