编辑器里报 401 但 curl 正常:按三层顺序排查密钥、配置与端点
同一把密钥,终端里 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_key 或 MY-PROVIDER_API_KEY 都不是文档写的那个名字。相关的命令,文档里列了 agent: open settings(配 LLM provider)、zed: open settings、zed: open settings file。
排查动作因此有了明确顺序:
- 先确认你改的到底是哪一处——界面里存过的凭据和环境变量是两个来源,不是同一个东西;
- 再确认环境变量名严格符合
<PROVIDER_NAME>_API_KEY的形态,且 provider 名和配置里那个键名一致; - 最后确认这个环境变量对编辑器进程可见,而不只是对你那个终端标签页可见(这一点见后文避坑清单)。
需要说清的是:官方文档并没有逐条说明”两个来源同时存在时客户端取哪一个”。所以排查时应当把两处都当成候选,一个个排除,而不是假定某一处优先——实际行为以官方文档为准。
二、第二层:它读的到底是哪一份配置文件
第二层的问题是:你改了一份配置,但工具加载的是另一份,或者被更高优先级的层盖掉了。
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.name 和 security.auth.selectedType。另外文档说明,设置是按类别组织成顶层对象的(general、ui、tools、model、context 等),每个设置要放进对应类别里——键写对了但没放进它该在的类别,效果和没写没区别。
排查动作:把四处位置逐一列出来看一遍(哪几处存在、各写了什么),再从优先级链从高往低倒着排除,最后确认你启动工具的那个目录,向上找到的第一份 .env 是不是你以为的那一份。
三、第三层:provider、base URL 和最终发出去的那个地址
到这一层,问题变成了”它到底把请求发去了哪儿,以及带的是哪个 provider 的凭据”。
goose 的设计把这层的两个变量摆在了明面上。按其官方文档:环境变量 GOOSE_PROVIDER、GOOSE_MODEL 设置默认的 provider 与模型;各家密钥用各自的变量(文档举的例子是 ANTHROPIC_API_KEY、OPENAI_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 这件事,各家的字段名和形态都不一样,不能互相套。
| 产品 | 文档记载的字段/机制 | 它是什么 | 排查时做什么 | 出处 |
|---|---|---|---|---|
| Zed | api_url | settings.json 里 openai_compatible provider 的自定义 base URL;文档示例是 https://example.com/v1 | 核对是否与服务商给的端点一致 | 见文末 Zed 两条 |
| Zed | <PROVIDER_NAME>_API_KEY | 环境变量命名规则,provider 名 my-provider 对应 MY_PROVIDER_API_KEY | 核对大小写与下划线转换 | 见文末 Zed 两条 |
| goose | OPENAI_HOST、可选 OPENAI_BASE_PATH | 自建/企业内部 OpenAI 兼容端点的主机与路径,分两段设 | 确认两段各自填了什么、有没有把整条 URL 塞进一段 | 见文末 goose |
| goose | GOOSE_PROVIDER、GOOSE_MODEL | 默认 provider 与模型的环境变量 | 先确认生效 provider,再确认对应密钥变量 | 见文末 goose |
| Gemini CLI | $VAR_NAME / ${VAR_NAME} | settings.json 内的环境变量插值语法 | 确认被引用的变量在运行环境里真有值 | 见文末 Gemini CLI |
| Gemini CLI | GEMINI_API_KEY、GEMINI_MODEL、model.name、security.auth.selectedType | 文档列出的密钥、默认模型与鉴权类型相关项 | 逐项核对,并确认设置放进了对应类别 | 见文末 Gemini CLI |
| Continue | apiBase | config.yaml 中 models 块下的可选字段,用于覆盖默认 API 端点 | 确认写在 models 块下的正确条目里 | 见文末 Continue |
| Crush | --base-url | provider add 命令的命令行参数 | 核对命令里实际传的值 | 见文末 Crush |
| Cline / Roo Code / Kilo Code | Base 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_HOST 与 OPENAI_BASE_PATH 是拆开两段的设计,而本篇列到的其它几家是一个字段填完整 base URL,你在多个工具之间来回切时很容易把习惯带过去。怎么避:换工具就重新看那家文档的字段说明,别拿上一个工具的填法照抄——apiBase、api_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。各产品文档随时可能变更,请以官方文档最新版为准。
- Zed(
settings.json结构、api_url、available_models、密钥不入 settings.json、<PROVIDER_NAME>_API_KEY、相关命令):https://zed.dev/docs/ai/use-api-access、https://zed.dev/docs/ai/configuration - goose(
goose configure流程、GOOSE_PROVIDER/GOOSE_MODEL、OPENAI_HOST/OPENAI_BASE_PATH、config.yaml):https://goose-docs.ai/docs/getting-started/providers/(核对日block.github.io/goose/docs/getting-started/providers返回 404,文档站现为goose-docs.ai) - Gemini CLI(
settings.json四处位置、优先级链、$VAR_NAME插值、.env查找顺序、GEMINI_API_KEY等):https://google-gemini.github.io/gemini-cli/docs/get-started/configuration.html - Cline(Base URL / API Key / Model 三项,v0 Quickstart 示例):https://docs.cline.bot/provider-config/openai-compatible
- Roo Code(native tool calling 原话):https://roocodeinc.github.io/Roo-Code/providers/openai-compatible(核对日
docs.roocode.com/providers/openai-compatible301 永久跳转到该域名) - Kilo Code(Base URL 两种形态、Headers、Azure
max_tokens提醒):https://kilo.ai/docs/providers/openai-compatible(核对日kilocode.ai/docs/...308 永久跳转到kilo.ai) - Continue(
apiBase、requestOptions):https://docs.continue.dev/reference - aider(
extra_params与extra_headers):https://aider.chat/docs/config/adv-model-settings.html - Crush(
provider add与--base-url):https://raw.githubusercontent.com/charmbracelet/crush/main/README.md - Groq(base URL 与
Authorization头写法):https://console.groq.com/docs/api-reference - Windsurf 相关的可复现事实(核对日 307 跳转与页面未说明 BYOK 配置):https://docs.windsurf.com/windsurf/models 跳转至 https://docs.devin.ai/desktop/models
本篇没有写什么,以及为什么:
- 不写任何产品的价格、免费额度、订阅档位、限速数字。 这类数字变动频繁,本次也未逐项核实,写出来只会误导你做预算判断。请以各产品官方文档与控制台为准。
- 不写完整模型清单和版本号。 模型上下线频繁,任何写死的清单都会过期;文中出现的模型 ID 仅为各家文档里的示例值。
- 不写界面菜单的逐级层级、提示文案与校验时机。 本篇依据的是文档中的配置层记载,界面细节以你实际看到的为准。
- 不给各家文档或产品排座次。 上文列出的差异(字段名、配置形态、密钥存放方式)只是设计取向不同,不构成优劣判断。
- 不推断产品之间的公司关系或归属。 Windsurf 那一条只陈述核对日当天可复现的跳转事实,不做任何延伸解读。
延伸阅读:同一组里的 自定义模型接入后 token 变贵、模型列表拉不出来怎么排查;接完之后照 接完自定义模型别急着干活 逐项过一遍,才算真接通。