编辑器里报 401 但 curl 正常:按三层顺序排查密钥、配置与端点

2026-08-07

同一把密钥,终端里 curl 通、编辑器里 401,几乎都不是服务商在为难你,而是编辑器在你和那条 HTTP 请求之间多插了几层:密钥从哪儿取、配置以哪一份为准、最终 URL 是怎么拼出来的。 你在终端里写的那条 curl 是”所见即所发”,而编辑器不是——它要先决定用哪个 provider、去哪个文件读配置、从哪个凭据源取 key,然后才发出请求。401 只是这条链路最末端的回声。

先把两个词说清楚。401 是 HTTP 的鉴权失败状态码,含义是”服务端没有认可你这次请求携带的身份”,它和”模型不会用工具""上下文塞不下”完全是两码事。OpenAI 兼容端点指的是一个服务商把自己的接口做成和 OpenAI 那套 HTTP 请求/响应格式一致,于是任何按 OpenAI 协议写的客户端,只要能改 base URL 和 key,就能指过去。这两件事经常被混在一起说,但排查时必须分开——401 停在鉴权层,压根还没轮到模型能力上场。

这篇和站内几篇的分工是这样的:如果你的 401/403 出现在自己写的脚本或服务里,直接看 API 401/403 排查;密钥该放哪儿、怎么轮换、怎么不泄漏,看 API Key 安全管理;单说 Cursor 接 DeepSeek 那一串具体报错,看 Cursor 接 DeepSeek 报错。本篇只管一件事:编辑器和终端 Agent 比裸调 API 多出来的那几层,按什么顺序拆。

本篇涉及的产品字段与机制,均为截至 2026-08-07 各产品官方文档页面上的记载,URL 见文末;产品随时会改,以官方文档最新版为准。

一、第一层:编辑器实际用的那把密钥,未必是你刚改的那一把

裸调 API 时,key 是你亲手拼进 Authorization 头的。比如 Groq 官方文档给的鉴权写法就是 "Authorization: Bearer $GROQ_API_KEY",base URL 是 https://api.groq.com/openai/v1——一眼能看到全部输入。

编辑器不一样,它通常有不止一个凭据来源。以 Zed 为例,其官方文档写得很直白:“Do not put API keys in settings.json.”(这是 Zed 官方文档的说法)。配置页另有一句原话:“Provider keys saved through Zed are stored in the system keychain, not in settings.json.” keychain 就是操作系统自带的凭据保管处,程序把密钥存进去、用时再取出,好处是不会随配置文件被你 git add 出去。

按文档记载,Zed 的凭据有两条路:provider 设置界面,或者环境变量,命名规则是 <PROVIDER_NAME>_API_KEY——provider 名叫 my-provider,对应的环境变量就是 MY_PROVIDER_API_KEY。这条命名规则是本层最常见的翻车点:短横线要变下划线、要全大写,改成 my_provider_api_keyMY-PROVIDER_API_KEY 都不是文档写的那个名字。相关的命令,文档里列了 agent: open settings(配 LLM provider)、zed: open settingszed: open settings file

排查动作因此有了明确顺序:

  1. 先确认你改的到底是哪一处——界面里存过的凭据和环境变量是两个来源,不是同一个东西;
  2. 再确认环境变量名严格符合 <PROVIDER_NAME>_API_KEY 的形态,且 provider 名和配置里那个键名一致;
  3. 最后确认这个环境变量对编辑器进程可见,而不只是对你那个终端标签页可见(这一点见后文避坑清单)。

需要说清的是:官方文档并没有逐条说明”两个来源同时存在时客户端取哪一个”。所以排查时应当把两处都当成候选,一个个排除,而不是假定某一处优先——实际行为以官方文档为准。

二、第二层:它读的到底是哪一份配置文件

第二层的问题是:你改了一份配置,但工具加载的是另一份,或者被更高优先级的层盖掉了。

Gemini CLI 的文档把这一层写成了可核对的清单。它的 settings.json 有四处位置:系统默认 /etc/gemini-cli/system-defaults.json(Linux)、用户级 ~/.gemini/settings.json、项目级的项目根 .gemini/settings.json、系统覆盖 /etc/gemini-cli/settings.json(Linux)。而生效优先级(低到高)是:硬编码默认 → system defaults 文件 → user settings → project settings → system settings → 环境变量 → 命令行参数。

看这条链你就明白两件事:项目级会盖住用户级——你在 ~/.gemini/settings.json 里改了半天,仓库里那份 .gemini/settings.json 可能一直在覆盖它;环境变量的位次高于所有配置文件——一个你早就忘了的 export,比你刚保存的 JSON 更有话语权。

密钥这一侧,Gemini CLI 的做法是环境变量插值settings.json 里可以写 $VAR_NAME${VAR_NAME},加载时自动解析成对应环境变量的值。所谓插值就是”配置文件里只写变量名、真值在运行时从环境里取”,这样配置能进版本库而密钥不进。它还有一条 .env 查找顺序:当前工作目录 → 逐级向上找父目录(到项目根或 home 为止)→ 用户 home 的 ~/.env

这条查找顺序值得单独盯一眼。你从不同目录启动工具,命中的 .env 可能就是不同的一份——在仓库子目录里启动,先命中的是那一级往上最近的一份,而不是你脑子里那份。相关的具体项,文档列了 GEMINI_API_KEY(Gemini API 密钥)、GEMINI_MODEL(默认模型),以及 settings.json 里的 model.namesecurity.auth.selectedType。另外文档说明,设置是按类别组织成顶层对象的(general、ui、tools、model、context 等),每个设置要放进对应类别里——键写对了但没放进它该在的类别,效果和没写没区别。

排查动作:把四处位置逐一列出来看一遍(哪几处存在、各写了什么),再从优先级链从高往低倒着排除,最后确认你启动工具的那个目录,向上找到的第一份 .env 是不是你以为的那一份。

三、第三层:provider、base URL 和最终发出去的那个地址

到这一层,问题变成了”它到底把请求发去了哪儿,以及带的是哪个 provider 的凭据”。

goose 的设计把这层的两个变量摆在了明面上。按其官方文档:环境变量 GOOSE_PROVIDERGOOSE_MODEL 设置默认的 provider 与模型;各家密钥用各自的变量(文档举的例子是 ANTHROPIC_API_KEYOPENAI_API_KEY);配置持久化在 config.yaml;CLI 侧的配置流程是跑 goose configure → 选 Configure Providers → 从列表选 provider → 填 API key 与附加参数 → 选模型。

把这几条串起来看,一个高频情形就浮出来了:当前生效的 provider 决定了它去读哪一个密钥变量。你往 OPENAI_API_KEY 里塞了新 key,但生效的 provider 是另一家,那把新 key 根本没人读——鉴权自然过不去。所以这一层第一件事不是查 key,是先确认当前生效的 provider 是谁。

针对自建或企业内部的 OpenAI 兼容端点,goose 文档给的是 OPENAI_HOST,另有可选的 OPENAI_BASE_PATH。注意这是拆成两段的设计:主机一段、路径一段。而本篇列到的其它几家不是这么拆的——这就引出了跨产品最容易串台的一点:base URL 这件事,各家的字段名和形态都不一样,不能互相套。

产品文档记载的字段/机制它是什么排查时做什么出处
Zedapi_urlsettings.jsonopenai_compatible provider 的自定义 base URL;文档示例是 https://example.com/v1核对是否与服务商给的端点一致见文末 Zed 两条
Zed<PROVIDER_NAME>_API_KEY环境变量命名规则,provider 名 my-provider 对应 MY_PROVIDER_API_KEY核对大小写与下划线转换见文末 Zed 两条
gooseOPENAI_HOST、可选 OPENAI_BASE_PATH自建/企业内部 OpenAI 兼容端点的主机与路径,分两段设确认两段各自填了什么、有没有把整条 URL 塞进一段见文末 goose
gooseGOOSE_PROVIDERGOOSE_MODEL默认 provider 与模型的环境变量先确认生效 provider,再确认对应密钥变量见文末 goose
Gemini CLI$VAR_NAME / ${VAR_NAME}settings.json 内的环境变量插值语法确认被引用的变量在运行环境里真有值见文末 Gemini CLI
Gemini CLIGEMINI_API_KEYGEMINI_MODELmodel.namesecurity.auth.selectedType文档列出的密钥、默认模型与鉴权类型相关项逐项核对,并确认设置放进了对应类别见文末 Gemini CLI
ContinueapiBaseconfig.yamlmodels 块下的可选字段,用于覆盖默认 API 端点确认写在 models 块下的正确条目里见文末 Continue
Crush--base-urlprovider add 命令的命令行参数核对命令里实际传的值见文末 Crush
Cline / Roo Code / Kilo CodeBase URL三家界面上都叫这个名字:Cline 与 Roo Code 的文档把它和 API Key、Model 并列为主参数,Kilo Code 则是新建 provider 时的字段之一核对是否误填成了官方 OpenAI 的地址见文末对应三条

关于是否要带 /v1,各家文档的记载也不同:Cline 文档里的 v0 Quickstart 示例是 https://api.v0.dev/v1(含 /v1),模型 ID 是 v0-1.0-md;Kilo Code 明确接受两种形态——标准形态 https://api.provider.com/v1 和完整端点 https://api.provider.com/v1/chat/completions,文档说明后者是为”端点结构非标准”的服务商与自建网关准备的;Zed 示例是 https://example.com/v1;Groq 官方 base URL 是 https://api.groq.com/openai/v1。Cline 和 Roo Code 的文档都提示,这里填的不会是官方 OpenAI 的那个地址。

顺带说一个纯协议层的推理(不是任何一家产品文档的说法):路径拼错时,最”讲道理”的返回是 404;但如果拼出来的地址落到了另一个服务或某个网关的通用入口上,鉴权可能在那一层就先失败,于是你看到的是 401 而不是 404。所以看到 401 也别把端点排除在嫌疑之外——先用你那条能通的 curl,把它请求的完整 URL 一字一字和编辑器里配的比一遍,这是本层最省事的动作。

还有一类被忽略的差异:网关往往要求额外的 HTTP 头。Kilo Code 新建 provider 时有可选的 Headers(自定义 HTTP 头,键值对形式);aider 的 extra_params 可把任意参数透传给 litellm.completion(),包括 extra_headers;Continue 有 requestOptions(timeout、headers、proxy 等 HTTP 配置)。按 HTTP 的一般道理推(这一步是机制推理,不是哪一家文档的记载):你 curl 时顺手 -H 加上的那个头,编辑器里若没有对应位置可填,那个头就不会出现在实际请求里,鉴权自然可能过不去。这几个字段各自到底承担什么职责,仍以对应官方文档为准。

四、边界与代价:这套顺序不管什么

这套三层排查法是有明确边界的,说清楚比说全乎更有用。

它只管鉴权链路,不管模型能力。 鉴权过了不等于能用。Roo Code 官方文档有一句硬限制:“Roo Code uses native tool calling exclusively. This is the only supported tool protocol — there is no XML-based fallback.”(官方文档原话)——即所选模型必须支持 OpenAI 兼容的 function calling,文档建议先查服务商文档确认该模型是否支持工具调用。function calling 指模型按结构化格式吐出”要调用哪个工具、参数是什么”的能力。这类问题的表现和 401 完全不同,别混着查。

它不管服务端策略。 参数被服务端拒绝也是一类独立问题:Kilo Code 文档明确提醒,Azure GPT-5 不要用通用的 OpenAI 兼容 provider,要用 Kilo 原生的 azure provider,因为 Azure 会拒绝 max_tokens 参数。这不是密钥问题,三层排查法查不出来。

它不覆盖所有产品。 例如 Windsurf:核对日(2026-08-07)https://docs.windsurf.com/windsurf/models 返回 307 跳转到 https://docs.devin.ai/desktop/models,该页自述为 Devin Desktop 的文档,页面未提及是否支持自带 API key、也未给出配置位置。所以本篇不写它接自定义模型的做法,你要用请以官方文档为准。

它不谈钱和量。 价格、免费额度、限速数字、完整模型清单,本篇一律不写——这些既会变,也不是 401 的成因。

它不描述界面。 本篇依据的是各家文档里的配置层记载,界面长什么样、提示文案怎么写、哪一步会不会拦住你,都以你实际看到的界面为准。

最后一句提醒:本篇引用的是各产品官方文档某几页上的记录,“本篇依据的那一页没记这一项”不等于”该产品没有这一项”。要下判断,请回官方文档核。

五、避坑清单:为什么会踩,怎么避

1. 环境变量只在你那个终端里有效。 为什么会踩:你在 shell 里 export 了 key,同一个终端里 curl 当然通;但编辑器往往是从图形界面启动的,它的进程继承的是另一套环境,根本看不见这个变量。怎么避:把变量放进登录 shell 或系统级的环境配置,或者干脆改从工具自己的凭据入口配(例如 Zed 文档给的 provider 设置界面这条路),然后从你实际使用的启动方式重启一次再验。

2. 按自己的直觉给环境变量起名。 为什么会踩:<PROVIDER_NAME>_API_KEY 这种规则里,短横线要转下划线、字母要转大写,手敲时极易漏。怎么避:写完把 provider 名和变量名并排贴在一起对一遍字符,别靠”看着差不多”。

3. 改了用户级配置,仓库里还有一份项目级的。 为什么会踩:Gemini CLI 的优先级链里 project settings 高于 user settings,而项目级那份可能是同事提交进仓库的,你从没打开过。怎么避:按四处位置逐一 ls 一遍,确认哪几处真实存在,再从高优先级往下排除。

4. 在不同目录启动,命中了不同的 .env 为什么会踩:.env 的查找是从当前工作目录逐级向上找的,你在仓库子目录里启动和在根目录启动,先命中的可能不是同一份。怎么避:排查时固定在同一个目录启动,并把从该目录逐级向上的每一份 .env 都找出来看。

5. 把整条 URL 塞进本该只填一段的字段。 为什么会踩:goose 的 OPENAI_HOSTOPENAI_BASE_PATH 是拆开两段的设计,而本篇列到的其它几家是一个字段填完整 base URL,你在多个工具之间来回切时很容易把习惯带过去。怎么避:换工具就重新看那家文档的字段说明,别拿上一个工具的填法照抄——apiBaseapi_url、Base URL、OPENAI_HOST--base-url 不是同一个东西。

6. 忘了网关要的自定义头。 为什么会踩:自建网关常要求一个额外的鉴权或路由头,你 curl 时随手 -H 带上了,配到编辑器里时忘了找对应位置。怎么避:以那条能通的 curl 为基准,把它的每一个 -H 逐条在工具配置里找到落点(例如 Kilo Code 的 Headers、Continue 的 requestOptions、aider 的 extra_params.extra_headers),找不到落点的就说明这条路走不通,要换配法。

7. 只盯着密钥,不看生效的是哪个 provider。 为什么会踩:多 provider 的工具里,密钥变量是按 provider 分开的,改错一个变量毫无反馈。怎么避:排查第一步先确认当前生效的 provider(例如 goose 侧的 GOOSE_PROVIDER),再去查它对应的那个密钥来源。

想把这套顺序前置成配置习惯,可以顺带看这两篇:环境变量丢失OpenAI 兼容端点

数据来源与核对日期

以下 URL 为本篇引用事实的出处,核对日期均为 2026-08-07。各产品文档随时可能变更,请以官方文档最新版为准。

本篇没有写什么,以及为什么:

  • 不写任何产品的价格、免费额度、订阅档位、限速数字。 这类数字变动频繁,本次也未逐项核实,写出来只会误导你做预算判断。请以各产品官方文档与控制台为准。
  • 不写完整模型清单和版本号。 模型上下线频繁,任何写死的清单都会过期;文中出现的模型 ID 仅为各家文档里的示例值。
  • 不写界面菜单的逐级层级、提示文案与校验时机。 本篇依据的是文档中的配置层记载,界面细节以你实际看到的为准。
  • 不给各家文档或产品排座次。 上文列出的差异(字段名、配置形态、密钥存放方式)只是设计取向不同,不构成优劣判断。
  • 不推断产品之间的公司关系或归属。 Windsurf 那一条只陈述核对日当天可复现的跳转事实,不做任何延伸解读。

延伸阅读:同一组里的 自定义模型接入后 token 变贵模型列表拉不出来怎么排查;接完之后照 接完自定义模型别急着干活 逐项过一遍,才算真接通。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。