接入教程

中转 API 报错速查:sse stream ended without [DONE] 等

搜状态码常常搜不到自己那一种——400 底下藏着十几个完全不同的问题。报错正文里的那句英文才有区分度。这一页按报错原文组织,用 Ctrl+F 把你屏幕上那句话贴进来就能跳到对应小节。

中转 API 常见报错原文速查:SSE 未收到 DONE、密钥无效、分组不允许端点、参数取值不支持四类报错及其定位方向
按报错原文分类,每条给出成因和一条可复制的确认命令

按原文查,比按状态码查快

遇到报错时,多数人第一反应是搜状态码。但 400 这个数字底下能藏十几种完全不同的问题,搜出来的结果十有八九不是你那一种。

报错正文里的那句英文才是有区分度的。 它通常直接点名了是哪一层、哪个参数、哪个端点出的问题。所以这一页按你屏幕上看到的那句话组织,每一节的标题就是一条报错原文。

用浏览器的页内查找(Ctrl+F / Cmd+F)把你那句话贴进去,直接跳到对应小节。

这三页怎么分工

sse stream ended without [DONE]

完整的报错常常是 本轮运行失败SSE stream ended without [DONE]:前半句是 DeepSeek Harness(DSH)界面上的失败标题,后半句才是原因,复制时两段文字连在了一起。出处和 DSH 里的处理办法见本节最后。

这句话的字面意思是:流式响应断了,但没收到约定的结束标记。

OpenAI 兼容的流式响应,正常结尾是一行 data: [DONE]。请求里带了 "stream_options":{"include_usage":true} 时,[DONE] 前面还会多一个 choices 为空、带 usage 的块:

data: {"id":"resp_035363...","object":"chat.completion.chunk","choices":[],
       "usage":{"prompt_tokens":9,"completion_tokens":31,"total_tokens":40}}

data: [DONE]

客户端就是靠 [DONE] 判断「说完了」。没等到它,客户端只能认为这轮是失败的——哪怕你已经看到大半截正常的回复。

四种原因,靠断开的规律区分

原因典型表现怎么确认
代理或网关超时掐断每次都在固定时长断,比如整 60 秒秒表卡一下,看是不是固定值
上游中途报错被吞掉短回复正常,长回复才断用小 max_tokens 对比测一次
网关自身不发 [DONE]任何长度都断,但内容其实完整看是否已经收到 usage 块
客户端整体超时和网络无关,改客户端配置就好关掉客户端超时再试

自己抓一次原始流

绕开客户端,直接看服务端到底发了什么:

curl -sN "$BASE_URL/chat/completions" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"model":"你的模型ID","messages":[{"role":"user","content":"写 800 字"}],
       "max_tokens":1500,"stream":true,"stream_options":{"include_usage":true}}' | tail -5
tail -5 看到什么说明
有 usage 块 + data: [DONE]服务端没问题,是客户端解析或超时
有 usage 块,没有 [DONE]网关漏发结束标记,内容其实是完整的
什么都没有,直接断连接被掐,查代理超时或网关流式支持
最后一块是一段错误 JSON上游报错了,按那段错误的正文继续往下查

第二种最容易被误判。 内容明明完整,只因为少一行标记就被客户端判为失败,换个客户端可能就「好了」——问题其实一直在网关。

「本轮运行失败」从哪来,DSH 里怎么处理

带这个前缀的版本出自 DeepSeek 开源的 DeepSeek Harness(DSH)。「本轮运行失败」是 DSH 网页界面里一轮对话失败时的通用标题(界面文案),本身不说明原因。社区桌面版 DSH Desktop 打包的是同一套界面。

后面那句英文来自 DSH 0.1.5 及更早版本的 DeepSeek 适配器:它请求 /chat/completions 流式接口,流结束时还没读到字面量 [DONE],就抛出 STREAM_CLOSED(源码)。DSH 默认只自动重试 EMPTY_RESPONSE、RATE_LIMIT、SERVER、TIMEOUT、TRANSPORT(重试说明),不含 STREAM_CLOSED,所以这一轮直接失败,要手动重发。0.1.6 起该适配器默认改走 Messages 接口,只有手动设了 protocol: chat-completions 才会走老路(0.1.6 配置源码);0.1.7 起只走 Messages 接口。走 Messages 时,同类问题的报错变成 DeepSeek Messages stream ended before message_stop。

  1. 先看接口有没有发 [DONE]。用上面的 curl 直接请求 DSH 里填的接口地址,看流的结尾。没有 [DONE] 时,对照上面的表判断:有 usage 块没 [DONE] 是中转或本地代理漏发,找服务方修;什么都没有就断了,是超时掐断;最后一块是错误 JSON,是上游报错。
  2. 或者升级到 0.1.7 及以后的版本。新版 DeepSeek 适配器只走 Messages 接口;用它接中转站时,Base URL 要填兼容 Messages 协议的根地址,DSH 会自己拼 /v1/messages。
  3. 确实只缺结束标记,才考虑加重试。在提供方配置的 retryPolicy 里写上 mode: normal(必填,校验源码),再把 STREAM_CLOSED 加进 retryableCodes。这个列表会整体替换默认值,原来的五个要一起写上;重试等于重新生成一遍,可能重复计费。
retryPolicy:
  mode: normal
  retryableCodes: [EMPTY_RESPONSE, RATE_LIMIT, SERVER, TIMEOUT, TRANSPORT, STREAM_CLOSED]

长连接为什么会被掐、超时该设多少,见Claude Code 国内接入:延迟测量、线路选择与稳定性配置;流式事件本身的完整性判断见流式响应与工具调用兼容性。

Invalid API key / INVALID_API_KEY

字面意思很清楚,但有三个地方容易白查半天。

第一,鉴权头用错了,不是密钥错。 Anthropic 协议要 x-api-key,OpenAI 协议要 Authorization: Bearer。发错头,服务端只会说密钥无效,不会告诉你头用错了。

第二,Base URL 带多了路径。 只写到 /v1,后面的路径由客户端拼。写成完整端点会变成 /v1/chat/completions/chat/completions,落到一个没有鉴权上下文的路由上,报错可能就是密钥无效。

第三,密钥复制时带了不可见字符。 从网页复制常常会带上首尾空格或换行。验一下长度:

printf %s "$KEY" | wc -c        # 和控制台显示的位数对得上吗
printf %s "$KEY" | od -c | tail -2   # 结尾有没有 \n 或空格

一个会误导客户端库的情况

OpenAI 的标准错误结构是这样:

{"error":{"message":"...","type":"invalid_request_error"}}

但有的网关在鉴权失败时返回的是另一种形状,比如把 code 和 message 平铺在顶层,没有外层的 error。客户端库按标准结构解析,取不到字段,最后抛给你的可能是一句毫无信息量的「未知错误」,甚至是解析异常。

判断办法是用 curl 直接看原始响应,不要隔着客户端:

curl -s "$BASE_URL/chat/completions" -H "Authorization: Bearer 故意写错" \
  -H "content-type: application/json" \
  -d '{"model":"你的模型ID","messages":[]}'

选网关时这一项值得单独测——错误路径是你以后花最多时间的地方。判断标准见Claude Code 中转站怎么选:九项可验证的判断标准,密钥本身的存放与轮换见API 密钥安全。

This group does not allow /v1/messages dispatch

返回 403,type 是 permission_error。

这不是密钥无效,也不是端点不存在,是你这个密钥所在的分组没开这个端点。 同一家服务的两个套餐,可能一个能用 Anthropic 的 /v1/messages、另一个只能用 OpenAI 的 /v1/chat/completions。

为什么这条值得单独说:Claude Code 和 Anthropic 官方 SDK 走的就是 /v1/messages。买之前只测了 /v1/chat/completions 能通,接 Claude Code 时才撞上这个 403,是很常见的顺序。

这句原文出自开源中转项目 Sub2API。它的 OpenAI 分组有一个「允许 /v1/messages 调度」开关;开关关着时,这个分组的 Key 请求 /v1/messages 就会拿到这条 403(源码)。开关在服务方后台,用户这边改不了:要么换一个 Claude 分组的 Key,要么请服务方打开。注意打开之后,Opus、Sonnet、Haiku 请求会按服务方配置映射到 GPT 模型,回答你的并不是 Claude。

下单前把你实际要用的端点逐个打一遍:

for p in /models /chat/completions /messages /embeddings; do
  printf "%-20s " "$p"
  curl -s -o /dev/null -w "%{http_code}\n" "$BASE_URL$p" \
    -H "Authorization: Bearer $KEY"
done

端点覆盖属于下单前必验的四件事之一,其余三件见OpenAI API Key 怎么获取:官方渠道与兼容网关的取舍。

API Error: 422 content contains prohibited words

常见的完整形态有两种:Claude Code 终端里的 API Error: 422 content contains prohibited words,和 Cherry Studio 智能体里的 ClaudeCodeResultError: API Error: 422 content contains prohibited words。

前面两层都是包装,真正的报错只有最后半句。 Claude Code 把接口返回的错误显示成 API Error: 状态码 消息(Claude Code 错误参考);Cherry Studio 的智能体通过 Claude Agent SDK 运行 Claude Code,把失败的运行结果包成 ClaudeCodeResultError 抛出(源码)。拆掉这两层,意思是:你配置的接口地址返回了 HTTP 422,正文说请求内容里有被禁止的词。

这句话是谁返回的

不是 Anthropic 官方。 Claude API 的错误码列表里没有 422;官方的内容拦截在 Claude Code 里显示为 API Error: Output blocked by content filtering policy 或使用政策拒答,措辞完全不同。

也不是常见开源网关的内置文案。 截至 2026-10-09,New API、One API、Sub2API、Claude Relay Service、CLIProxyAPI、LiteLLM、CC Switch 的公开源码里都检索不到这句原文。拿 New API 对比:它自带的敏感词过滤命中时,返回 user sensitive words detected: 命中的词,错误码 sensitive_words_detected,默认 HTTP 500(源码)。

所以这条 422 多半来自你所用中转站(或它的上游)自己加的关键词过滤:请求正文命中词表,就被直接拒绝。

为什么换个说法还是报错

这类关键词过滤通常扫整个请求正文,不只是你刚输入的那句。Messages API 是无状态的,客户端每一轮都要把完整的对话历史重新发一遍(Anthropic 文档)。所以 Claude Code 的请求里通常带着:

  • 每次会话开始就加载的 CLAUDE.md;
  • 前面读过的文件内容、命令输出等工具结果;
  • 之前几轮的对话。

命中的内容只要还在历史里,之后每一轮多半都会被拒。422 原样重发只会得到同样的结果(MDN 对 422 的说明),在 Claude Code 里换个问法也没用。

怎么处理

  1. 先确认是内容问题。用同一个 Key 发一条只有 hi 的最小请求(命令见下)。最小请求能通,就是对话内容命中了过滤;最小请求也报 422,回头查 Key、分组和网关配置。
  2. 开新会话。在 Claude Code 里用 /clear 清掉当前对话再试,不要用 --continue 或 --resume 接回原会话。
  3. 新会话也报,查每轮都会带上的内容。先看 CLAUDE.md,再看这一步要读的文件和命令输出。把可疑段落挪走再试,逐段缩小范围。
  4. 找中转站要请求 ID 或词表。规则在服务方手里,只有他们能告诉你命中了哪个词、能不能放行。

BASE_URL 要带 /v1。Claude Code 里填的 ANTHROPIC_BASE_URL 一般是网关根地址,不带 /v1(官方示例里请求时才拼 /v1/messages),抄过来时记得补上。返回 404 多半是地址拼错了,和过滤无关。

最小请求,macOS / Linux(bash、zsh):

BASE_URL="https://你的网关/v1"; KEY="你的密钥"

curl -s -w "\n--- HTTP %{http_code} ---\n" "$BASE_URL/messages" \
  -H "x-api-key: $KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{"model":"你的模型ID","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}'

Windows 上用 PowerShell 7 及以上(命令是 pwsh)。-SkipHttpErrorCheck 让 4xx 也照常拿到响应体,这个参数从 PowerShell 7 起才有,Windows 自带的 Windows PowerShell 5.1 不支持,需要先装 PowerShell 7:

$env:BASE_URL = "https://你的网关/v1"; $env:KEY = "你的密钥"

$r = Invoke-WebRequest -Method Post -Uri "$env:BASE_URL/messages" -SkipHttpErrorCheck `
  -Headers @{ "x-api-key" = $env:KEY; "anthropic-version" = "2023-06-01" } `
  -ContentType "application/json; charset=utf-8" `
  -Body '{"model":"你的模型ID","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}'
"HTTP $($r.StatusCode)"; $r.Content

另外,Claude Code 里看到的 422 不全是这一种。比如 CC Switch 的本地代理把协议格式转换失败也返回 422,正文以 格式转换错误: 开头(源码),那是格式问题,和违禁词无关。

Unsupported value: '...' is not supported with the '...' model

完整报错会把支持的取值列出来,这是它最有用的地方:

Unsupported value: 'minimal' is not supported with the 'gpt-6-astra' model.
Supported values are: 'none', 'low', 'medium', 'high', 'xhigh'.

不要把一个模型的档位列表套用到另一个模型上。 同一家服务下的不同模型,支持的取值可以不一样;同一个模型在不同时间也可能不一样——上游随时会加档位或去掉档位。

我们自己踩过这个坑:给某个模型登记参数时,直接沿用了同系列另一个模型的档位列表,结果其中一档那个模型当时并不支持。后来上游又把那一档加上了,同一份记录先错后对,中间没有任何通知。

所以正确做法不是背下来,是需要时当场问一次——故意传一个肯定不支持的值,让服务端把清单吐出来:

curl -s "$BASE_URL/chat/completions" -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"model":"你的模型ID","reasoning_effort":"definitely-not-a-real-value",
       "messages":[{"role":"user","content":"hi"}],"max_tokens":8}'

报错里的 Supported values are: 就是当下的准确答案。这个技巧对任何枚举型参数都管用。

The image data you provided does not represent a valid image

后面通常跟着支持的格式清单,比如 ['image/jpeg', 'image/png', 'image/gif', 'image/webp']。

两个最常见的原因:

一是格式真的不在清单里。 SVG 是最容易踩的——它在网页里到处都是,但视觉模型基本都不收。清单里没有的格式,先转成 PNG。

二是传了 URL,而上游没拉到那张图。 传 URL 的方式需要上游主动去访问那个地址,失败原因可能是上游的出网限制、地址本身不可达、或者网关没有正确透传。这类失败的报错措辞五花八门,有时是下载失败,有时就是这句「不是有效图片」。

遇到图片相关的报错,先换 base64 试一次。 请求体会变大,但少一个不受你控制的环节:

B64=$(base64 -i ./test.png | tr -d '\n')
curl -s "$BASE_URL/chat/completions" -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d "{\"model\":\"你的模型ID\",\"messages\":[{\"role\":\"user\",
       \"content\":[{\"type\":\"text\",\"text\":\"这是什么\"},
       {\"type\":\"image_url\",\"image_url\":{\"url\":\"data:image/png;base64,$B64\"}}]}],
       \"max_tokens\":64}"

base64 能通、URL 不能通,那问题就在上游拉取那一段,和你的图片本身无关。

图像模型还有一批和文本完全不同的行为——按张计费、尺寸参数可能不被采纳,见GPT Image 2 中转接入:按张计费、参数差异与验收方法。

Service temporarily unavailable

这句话看着像临时故障,但在中转场景下,它常常不是临时的。

典型情况是某个端点压根没开通。比如向一个没接嵌入模型的网关请求 /v1/embeddings,返回的可能就是这句——而不是更准确的「该端点不可用」。你会以为等一会儿就好,实际等多久都一样。

区分真临时故障和没开通,看两件事:

观察真·临时故障端点没开通
换个端点试也一起失败其他端点正常
隔十分钟重试可能恢复一模一样
服务商状态页通常有记录没有记录

/v1/embeddings 是最容易缺的一个。要做知识库或检索的话,这一项必须在下单前确认,事后加不了。

怎么抓到一手报错,而不是客户端转述的

上面每一条都强调用 curl 直接看。原因是客户端会对错误做二次包装:有的把原文吞掉只留一句「请求失败」,有的把 401 转成 500,有的干脆只打印异常栈。

一个通用的抓取模板,把状态码、耗时和完整响应体一次拿全:

BASE_URL="https://你的网关/v1"; KEY="你的密钥"

curl -s -w "\n--- HTTP %{http_code}  耗时 %{time_total}s ---\n" \
  "$BASE_URL/chat/completions" \
  -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"model":"你的模型ID","messages":[{"role":"user","content":"hi"}],"max_tokens":16}'

贴报错给别人看之前,先把密钥抹掉。响应体里一般不会带密钥,但你的命令行会。

把这些变成长期可查的记录,而不是每次临时抓,做法见中转站可观测性。

核验日期

本文引用的第三方报错出处(DeepSeek Harness、Sub2API、New API、CC Switch、Cherry Studio)和 Claude Code、Claude API 官方文档于 2026-10-09 复核。这些项目更新很快,报错措辞可能随版本变化,以你所用版本的源码和文档为准。

常见问题

报错里没有英文原文,只有一句中文「请求失败」,怎么办?

那是客户端包装过的。用本文最后一节的 curl 模板直接打一次同样的请求,服务端的原始响应体里才有可查的信息。客户端转述的错误基本无法定位。

同一条报错,换个客户端就好了,说明什么?

多半说明服务端行为是有问题的,只是两个客户端的容错程度不同。最典型的是网关漏发 data: [DONE],严格的客户端判为失败,宽松的客户端照常结束。这种情况服务端才是根因。

「本轮运行失败」后面跟着 SSE stream ended without [DONE],是哪个软件报的?

这是 DeepSeek Harness(DSH)的报错。「本轮运行失败」是界面的通用失败标题,后面的英文才是原因:流结束时没收到 data: [DONE]。先用 curl 看你填的接口地址在流的结尾有没有发 [DONE],没有就按正文的表区分是漏发、超时掐断还是上游报错。DSH 从 0.1.6 起默认、0.1.7 起只走 Messages 接口,同类问题的报错变成 DeepSeek Messages stream ended before message_stop。

报 422 content contains prohibited words,换个说法为什么还是被拒?

这类过滤通常扫整个请求,包括对话历史、CLAUDE.md 和读过的文件内容。命中的内容只要还在历史里,之后每一轮多半都会被拒。先开新会话(Claude Code 里用 /clear)再试;新会话也报,就查每轮都会带上的内容,或找中转站要命中的词。

报错里提到的模型名和我填的不一样,是怎么回事?

转发层通常原样透传上游的错误文本,所以报错里可能出现上游实际使用的模型名。这反过来是个有用的验证手段——能用来核对网关声称的模型和实际调用的是不是同一个。

为什么建议故意传一个错误的参数值?

因为服务端在拒绝时会把支持的取值列出来,这是获取当下准确清单最省事的办法。文档可能过期,报错不会。

这些报错是通用的吗,还是只在某一家出现?

一部分是协议层的通用报错,比如参数取值不支持、图片无效,措辞由上游模型服务决定,在不同网关上大同小异。另一部分出自具体软件:本轮运行失败来自 DeepSeek Harness,This group does not allow /v1/messages dispatch 来自 Sub2API,content contains prohibited words 多半来自中转站自己加的关键词过滤。哪些端点开通、错误结构是否标准,各家不同,需要自己测。

API 中转站错误码排查依次保存证据、识别层级、判断副作用并选择动作工程实践

API 中转站错误码:400、401、404、422、429 与 5xx 排查

API 中转站返回 400、401、404、422、429 或 502、503、504 时该怎么查?用一张错误码对照表判断问题出在客户端、网关还是上游,附脱敏诊断包的写法、请求 ID 的记录方法,以及停止、回滚与恢复步骤。

API 中转站错误码HTTP 错误码API 401 错误
API 流式输出与工具调用从 SSE 分帧到调用聚合、工具结果和完成或错误的协议状态机图工程实践

API 流式输出与工具调用兼容性:SSE 状态机测试

API 流式输出和工具调用兼容吗?用 SSE 状态机对齐 Anthropic、Responses 和 Chat Completions 三种协议,跑 12 组回归测试,检查分帧、工具参数聚合、错误结束和用量,不兼容时按清单降级。

API 流式输出与工具调用兼容性SSE 状态机Anthropic Messages 流式