EZAT RouterDocs

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 部分会被丢弃。
tooltool_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 对象:

  1. 第一个 chunk 带有 delta: {"role": "assistant", "content": ""}。
  2. 文本以 delta.content 返回。工具调用以 delta.tool_calls 返回:先是 id 和 function.name,然后 function.arguments 分段返回。
  3. 最后一个带选项的 chunk 有一个空的 delta 和 finish_reason。
  4. 设置了 include_usage 时,之后还有一个 "choices": [] 且带有 usage 的 chunk。
  5. 流以 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_efforteffort是否请求思考
none、minimallow否
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 大于 1n: only 1 choice per request is supported
logprobs: truelogprobs: log probabilities are not available
top_logprobstop_logprobs: log probabilities are not available
logit_bias(非空)logit_bias: is not supported
audio,或 modalities 不是 ["text"]modalities: only text output is supported
web_search_optionsweb_search_options: is not supported
functions、function_callfunctions: the legacy functions API is not supported; use tools
response_format 的 type 为 json_objectresponse_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_effortreasoning_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"
  }
}
状态码typecode何时出现
400invalid_request_errornull请求无效,或某个字段被拒绝
401invalid_request_errorinvalid_api_key缺少 key,或 key 无效、已吊销或已过期
402insufficient_quotainsufficient_quota余额已用完,或 key 达到了消费上限
403permission_errornullkey 无权使用该模型,或组织已被暂停
404invalid_request_errormodel_not_found未知模型(param 为 model)
413invalid_request_errorrequest_too_large请求体超过 32 MB
429rate_limit_errorrate_limit_exceeded请求过多;等待 retry-after 秒后再试
500server_errornullEZAT 或模型提供方出错
503server_erroroverloaded暂无可用容量;等待 retry-after 秒后再试
504server_errortimeout非流式请求耗时超过 590 秒;请改用流式

失败的请求不计费。openai SDK 会自动重试 429 和 5xx 响应。错误说明了哪些错误值得重试。

给 AI agent 的说明

  1. POST https://api.router.ezat.io/openai/v1/chat/completions,带上 Authorization: Bearer $EZAT_API_KEY。
  2. 使用 GET https://api.router.ezat.io/openai/v1/models 返回的模型 id。
  3. 不要发送大于 1 的 n、logprobs、logit_bias、音频、web_search_options、旧版 functions API、response_format json_object 或 file_id:它们会返回 400。
  4. 不要依赖 temperature 或 top_p:在这里大多数模型会忽略它们。
  5. 流式请求要设置 stream_options.include_usage 才能拿到 token 数;读取文本时跳过 choices 为空的 chunk。
  6. 多轮对话保持提示缓存开启,并为每个对话传入一个固定的 prompt_cache_key。一次性提示词发送 x-ezat-cache: off。

本页内容