Claude Code 报错 Invalid API key · Fix external API key 怎么排查:Not logged in、Credit balance is too low、API Error 401/429/529 原文与重试实测
问题背景
把 Claude Code 接到 DeepSeek 这类兼容端点、换了一把 key、或者额度用完之后,跑 claude -p 常会看到下面几句中的一句:
Invalid API key · Fix external API key
Not logged in · Please run /login
Credit balance is too low
API Error: Request rejected (429) · …
API Error: 529 Overloaded. This is a server-side issue, usually temporary — …
报错本身不难看懂,难的是这几个问题:
- 这句话是 Claude Code 本地判断出来的,还是后端返回的?请求到底发出去了没有?
- 为什么 key 错了,命令要卡将近 3 分钟才报错?
- 写脚本时,报错在 stdout 还是 stderr?退出码是多少?
--output-format json里该看哪个字段? - 如果你是第三方网关,应该读
x-api-key还是Authorization: Bearer?
本文的报错原文和数字,全部来自 2026-09-27 在本机 Claude Code 2.1.280 上的实测:28 组 case,共 61 次 claude -p。后端不是真实 API,而是一个本地 stub,这样可以任意指定返回码,并记下每个请求到达的时间和携带的 header。
问题分析
先在 2.1.280 的原生二进制(claude.exe,217 MB)里找到了这几句文案的定义:
var _Ye="Not logged in \xB7 Please run /login",
Gke="Invalid API key \xB7 Fix external API key",
pat="Invalid auth token \xB7 Fix external auth token",
mat="Invalid ANTHROPIC_CUSTOM_HEADERS \xB7 Fix the environment variable"
还有一条校验:function JM(e){return/^[a-zA-Z0-9-_]+$/.test(e)},不通过就抛 Invalid API key format. API key must contain only alphanumeric characters, dashes, and underscores.。
光看静态代码,很容易得出「key 带特殊字符会在本地被拦」的结论。实测推翻了这一点:ANTHROPIC_API_KEY='bad key!@#' 被原样放进 x-api-key 发给了后端,stub 返回正常响应后 exit 0。回头再查代码,edo() 的唯一调用点在 OAuth 创建 API key 的流程里(r.data?.raw_key → edo(s,n)),环境变量里的 key 根本不经过它。所以本文所有结论都以实测为准,静态代码只用来解释现象。
读代码时还看到一处决定文案的逻辑,后面实测也印证了:401 会不会显示 Invalid API key,取决于错误 message 里是否包含 x-api-key 这个字符串:
function uat(e){return e instanceof Error&&e.message.toLowerCase().includes("x-api-key")}
// ……
if(uat(e)){ …; return Ro({error:"authentication_failed",
content: M==="ANTHROPIC_API_KEY"||M==="apiKeyHelper" ? Gke : _Ye}) }
技术方案与选型
目标是断言「请求发没发、发了几次、隔多久、带了哪个 header」,所以需要能控制后端返回什么,并记录每一个到达的请求。
| 方案 | 结论 | 理由 |
|---|---|---|
| 本地 stub(零依赖 Node) | ✅ 采用 | 返回码和 body 可以任意指定;每个请求按毫秒时间戳和全量 header 写进 jsonl;不花钱,也不打扰真实服务 |
| 用真实 key 打 api.anthropic.com | ❌ 排除 | 429/529/5xx 造不出来;错 key 反复重试,等于对官方接口做无意义的压测 |
| 接真实第三方端点(DeepSeek 等) | ❌ 排除 | 错误体的格式由对方决定,无法控制变量;同一个错误在不同时间的返回可能不一样 |
| mitmproxy 抓包 | ❌ 排除 | 需要改证书信任链,还会引入一个额外变量;而 ANTHROPIC_BASE_URL 可以直接指向 http 的 stub,用不上 |
| 只读二进制里的字符串 | ❌ 不能单独用 | 上面的格式校验就是反例:代码里有,但这条路径上不生效 |
具体做法:
lab/stub.mjs监听127.0.0.1,按 case 配置对POST /v1/messages返回合法 SSE、指定的状态码和 body,或者直接断开连接。- 每次运行用
env -i清空环境,配一个全新的空CLAUDE_CONFIG_DIR,避免复用本机的 OAuth 登录态,工作目录也单独建。 HTTPS_PROXY同样指向 stub:遇到CONNECT就记录目标主机并返回 403。这样任何外连都会被记录并拦截,保证整个实验没有请求打到真实的 api.anthropic.com。- 统一的调用方式:
claude -p 'reply with exactly OK' --model haiku --output-format text|json < /dev/null。
实测过程
**对照组先跑通。**stub 返回合法的 Messages 流(message_start → content_block_delta: "OK" → message_stop)。3 次都是 exit 0,stdout 逐字节为 4f 4b 0a(OK\n),stub 各收到 1 个请求,带 x-api-key、anthropic-version: 2023-06-01、user-agent: claude-cli/2.1.280 (external, sdk-cli)。stub 可信,后面的失败才能归因。
**所有 case 都跑了至少 2 次,报错文本、退出码和请求数全部一致。**下面这张表是本文的核心,「stub 请求数」是 stub 实际记到的数,不是推断:
| 报错原文(stdout) | 触发条件 | 本地还是后端 | stub 请求数 | 是否重试 | 墙钟 |
|---|---|---|---|---|---|
Not logged in · Please run /login |
没设 ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN,也没登录 |
本地 | 0 | — | 0.28 / 0.30 s |
Invalid API key · Fix external API key · Invalid X-Api-Key header value from ANTHROPIC_API_KEY: it contains a non-ASCII character at character 4 (9 characters). |
key 含中文(sk-密钥-123) |
本地 | 0 | 否 | 0.28 / 0.27 s |
… it contains a line break at character 7 (10 characters on 2 lines). |
key 中间夹换行 | 本地 | 0 | 否 | 0.27 / 0.27 s |
Invalid auth token · Fix external auth token · Invalid Authorization header value from ANTHROPIC_AUTH_TOKEN: it contains a non-ASCII character at character 5 (10 characters). |
token 含中文 | 本地 | 0 | 否 | 0.28 / 0.28 s |
Invalid API key · Fix external API key |
后端 401,message 含 invalid x-api-key |
后端 | 11 | 是,10 次 | 179.43 / 168.78 s |
| 同上 | 后端 401,type 换成 invalid_request_error,message 仍含 x-api-key |
后端 | 11 | 是 | 176.93 / 183.28 s |
Failed to authenticate. API Error: 401 invalid api key |
后端 401,message 为 invalid api key(不含 x-api-key) |
后端 | 11 | 是 | 180.78 / 176.96 s |
Not logged in · Please run /login |
只设了 ANTHROPIC_AUTH_TOKEN,后端 401 |
后端 | 11 | 是 | 181.07 / 179.16 s |
Credit balance is too low |
402 billing_error,message 为官方的 credit balance 文案 |
后端 | 1 | 否 | 0.93 / 0.75 s |
Credit balance is too low |
400 invalid_request_error,message 同上 |
后端 | 1 | 否 | 0.99 / 0.71 s |
Failed to authenticate. API Error: 403 Your API key does not have permission to use the specified resource. |
403 permission_error |
后端 | 1 | 否 | 0.98 / 0.72 s |
API Error: 400 Model Not Exist |
400 invalid_request_error |
后端 | 1 | 否 | 0.96 / 0.68 s |
There's an issue with the selected model (claude-haiku-4-5-20251001). It may not exist or you may not have access to it. Run --model to pick a different model. |
404 not_found_error |
后端 | 2 | 只追加 1 次非流式请求 | 0.95 / 0.77 s |
API Error: Request rejected (429) · Number of request tokens has exceeded your per-minute rate limit |
429 + retry-after: 1 |
后端 | 11 | 是,10 次 | 179.06 / 184.21 s |
API Error: 529 Overloaded. This is a server-side issue, usually temporary — try again in a moment. If it persists, check your inference gateway (127.0.0.1:18913). |
529 overloaded_error |
后端 | 11 | 是 | 180.24 / 180.05 s |
API Error: 500 Internal server error. …check your inference gateway (…) |
500 api_error |
后端 | 11 | 是 | 179.81 / 180.51 s |
API Error: 502 Bad Gateway. …check your inference gateway (…) |
502 + nginx HTML 页 | 后端 | 11 | 是 | 178.63 / 171.14 s |
API Error: API returned an empty or malformed response (HTTP 200) — check for a proxy or gateway intercepting the request. … |
200,但 body 是 HTML 或空 | 后端 | 2 | 只追加 1 次非流式请求 | 0.96 / 0.78 s |
API Error: Connection dropped (ECONNRESET) |
stub 收到请求后直接断开 | 后端(网络) | 11 | 是 | 185.36 / 171.91 s |
API Error: Connection refused — a firewall or proxy may be blocking it (ECONNREFUSED) |
base URL 端口没人监听 | 本地网络层 | 0 | 是,照样重试满 | 184.27 / 182.76 s |
有两点要特别注意:
- 同一句报错可能有两个来源。
Invalid API key · Fix external API key既可能是后端 401(重试 3 分钟),也可能是本地 header 校验(0.28 秒,后面多一段· Invalid X-Api-Key header value…)。Not logged in也一样:既可能是本地完全没有凭据,也可能是用 token 鉴权时被后端 401。后一种情况会误导你去跑/login,其实该换 token。 - 格式校验没有触发。
ANTHROPIC_API_KEY='bad key!@#'两次都 exit 0、输出OK,stub 收到的x-api-key值就是bad key!@#。Invalid API key format…这句未复现。另外,key 末尾多一个换行不会报错:stub 收到的是去掉换行后的sk-abc123。

重试时序:401 也会重试 10 次
stub 记下的 401 case 请求间隔(毫秒,r1):
545, 1020, 2254, 4300, 8248, 17336, 34291, 34866, 39673, 36396
也就是大约 0.5 → 1 → 2 → 4 → 8 → 16 秒逐次翻倍,从第 7 次开始封顶在 32~40 秒,重试 10 次后放弃,首尾跨度 168.5~184.8 秒。429、529、500、502、ECONNRESET 的 11 组数据走的是同一条曲线。所有请求的 x-stainless-retry-count header 都是 0,说明这是 Claude Code 应用层自己的重试,不是 SDK 层的。
429 的 retry-after: 1 会被遵守:3 次运行的首次间隔分别是 1009 / 1005 / 1006 ms,而其他错误码的首次间隔在 529~628 ms 之间。之后的间隔和其他错误码一样。
代码里的默认值和实测对得上:Vur=10(默认重试次数)、GCe=15(CLAUDE_CODE_MAX_RETRIES 的上限,超过会被夹小并告警)。另有一个 Yur=300,是某种模式下的默认值,触发条件没有查清。

把重试关掉:CLAUDE_CODE_MAX_RETRIES
同样是 401 stub,只加一个环境变量:
| 设置 | stub 请求数 | 请求间隔 | 墙钟(2 次) | stdout |
|---|---|---|---|---|
| 默认 | 11 | 见上 | 179.43 / 168.78 s | Invalid API key · Fix external API key |
CLAUDE_CODE_MAX_RETRIES=2 |
3 | 628, 1082 / 548, 1124 ms | 2.01 / 1.97 s | 同上 |
CLAUDE_CODE_MAX_RETRIES=0 |
1 | — | 0.28 / 0.27 s | 同上 |
stdout、stderr、退出码
61 次运行里,stderr 全部是 0 字节;所有报错都打在 stdout,末尾有 \n;失败一律 exit 1,成功 exit 0。也就是说:
2>err.log捕获不到任何报错内容,「stderr 为空就算成功」的判断方法会误判。- 退出码能发现失败,但不能区分 key 错、限流还是网关挂了,都是 1。

--output-format json 长什么样
三次 json 模式运行的关键字段:
| case | subtype |
is_error |
api_error_status |
terminal_reason |
result |
duration_ms |
|---|---|---|---|---|---|---|
| 无凭据 | "success" |
true |
null |
"api_error" |
Not logged in · Please run /login |
130 |
| 401 | "success" |
true |
401 |
"api_error" |
Invalid API key · Fix external API key |
171725 |
| 429 | "success" |
true |
429 |
"api_error" |
API Error: Request rejected (429) · … |
182737 |
subtype 在 API 报错时仍然是 "success",只看它会误判为成功。判断要看 is_error 或 terminal_reason。result 键存在,内容就是报错原文。这和 Reached max turns 那篇不同:那边是 error_max_turns 子类型,result 键可能根本不存在。api_error_status 可以用来区分来源:后端错误给出 HTTP 状态码,本地凭据问题为 null。
第三方网关:到底发哪个 header
| 环境变量 | stub 收到的鉴权 header |
|---|---|
只设 ANTHROPIC_API_KEY |
x-api-key: <key> |
只设 ANTHROPIC_AUTH_TOKEN |
authorization: Bearer <token> |
| 两个都设 | 两个同时发:x-api-key: sk-stub-KEY-111 + authorization: Bearer tok-stub-TOKEN-222 |
两个都设时,Claude Code 不会替你选,网关收到两个 header,按它自己的优先级取。如果两个值来自不同的账号,出了问题会非常难查。
实践效果
把读数整理成可以直接照做的结论:
**1. 用户侧:ANTHROPIC_API_KEY 和 ANTHROPIC_AUTH_TOKEN 只设一个。**网关文档要求 Bearer 就只设 TOKEN,要求 x-api-key 就只设 KEY。设完先用下面的命令快速验证,不用干等 3 分钟:
CLAUDE_CODE_MAX_RETRIES=0 claude -p "reply with exactly OK" --output-format json < /dev/null \
| jq '{is_error, api_error_status, result}'
api_error_status 为 null 且 is_error:true,说明问题出在本地凭据(没设,或者含非 ASCII 字符、中间有换行);是 401,说明 key 被后端拒绝。
**2. 看到 Not logged in · Please run /login,先看自己设的是哪个变量。**如果设的是 ANTHROPIC_AUTH_TOKEN,这句话很可能是 token 被 401 了,跑 /login 没有用。
**3. 脚本 / CI 里:**判断成败用退出码或 is_error,报错内容从 stdout 取;健康检查类的调用加 CLAUDE_CODE_MAX_RETRIES=0 或一个小值,否则一个配错的 key 会让流水线卡 3 分钟。长任务里不要设为 0,429/529 本来就应该退避重试。
4. 网关侧(给在做兼容层的人):
x-api-key和Authorization: Bearer两个都要认;两个都带时,要明确自己的优先级。- key 无效时返回 401,Claude Code 会再重试 10 次,一次失败的调用你会收到 11 个请求。403 和 400 实测都不重试(1 个请求)。要不要为了这一点改状态码,取决于你的语义,这里只给出实测行为。
- 401 的 message 里带上
x-api-key字样,用户就会看到Invalid API key · Fix external API key;不带的话,看到的是Failed to authenticate. API Error: 401 <你的 message>。两种文案都实测过。 - 余额不足时,只要 message 包含
credit balance is too low,402 和 400 都会显示成Credit balance is too low。只返回 402、message 不含这段文案的情况没有测。 - 5xx 报错里会带上
check your inference gateway (<你的 host:port>),用户第一眼就会怀疑你的网关。
5. 走 base URL 的更多坑:ANTHROPIC_BASE_URL 设了却不生效的情况见 这篇,接 DeepSeek 的完整配置见 Claude Code 接 DeepSeek 实测。
本轮 61 次运行全部打在本地 stub 上,真实 API 花费为 0。JSON 里对照组的 total_cost_usd: 0.000015 是按 stub 返回的 usage(10 输入 / 1 输出 token)用 haiku 标价折算出来的,不是真实计费。
踩坑
- **设了
ANTHROPIC_BASE_URL仍会连 api.anthropic.com。**每次运行都有CONNECT api.anthropic.com:443:正常运行 5 次,无凭据 2 次,只设 token 2 次,重试满 10 次的运行 18~23 次。全程共 690 次,目标全是api.anthropic.com:443,都被 stub 代理 403 拦下,对照组依然 exit 0。这些连接是做什么的,debug 日志里没写,没有归因。如果你的网络环境访问不到这个域名,这部分应该不影响主请求(本次就是被拦的状态),但只有这一种网络条件下的数据。 - **一开始信了静态线索。**原计划把「key 格式非法 → 本地抛
Invalid API key format」当主角,实测bad key!@#直接成功了。静态字符串只能说明代码里有这句话,不能说明它在你的路径上会触发。 - **本机
grep扫大二进制太慢。**用 BSD grep 的.{90}片段.{140}扫 217 MB,约 35 分钟才出 2 个片段,后来改用 python mmap 按字面量找偏移再切片。这是 Bash 超时那篇 记过的坑,这次又犯了。 - **重试类 case 每个约 3 分钟。**第一轮 4xx 组串行跑,超过 600 秒才意识到 401 也在重试,中途停掉改成每个 case 各一个 stub 并行跑。被打断的那次运行已重跑,表中是重跑的数据。
- **截图说明。**三张图的输出文字,是实验 runner 捕获的原始 stdout 字节;
real行是 runner 记录的墙钟,不是 shell 的time实际输出;图 2 命令里的sk-wrong是展示用占位,实际打的 key 是sk-stub-valid-123,由 stub 返回 401;图 1 的 key 通过环境变量传入(sk-密钥-123),命令行里没有显示。另外用 script(1) 真 TTY 录了 5 个快速 case,和 runner 的 stdout 逐字节比对一致(-p的 text 模式没有颜色)。
未验证清单:Invalid API key format…(未复现,只在 OAuth 建 key 流程里);Invalid ANTHROPIC_CUSTOM_HEADERS(未测);402 但 message 不含 credit 文案(未测);429 之后恢复成功的「失败→成功」序列(未测);交互式(非 -p)会话里的显示和重试(未测);Yur=300 的触发条件和那些 CONNECT 的用途(未归因)。每个 case 只跑了 2 次,重试间隔的抖动范围只基于这些样本。