这次排错来自一个很容易误判的问题:把 Codex 接到Tencent LKEAP Token Plan 的 OpenAI 兼容接口后,客户端返回 401 Unauthorized。
很多人看到 401,第一反应都是 API Key 错了、环境变量没生效、账号权限不够。这个方向当然要查,但在这次问题里,真正有价值的证据不是 401 本身,而是错误日志里的请求 URL:
https://api.lkeap.cloud.tencent.com/plan/v3/chat/completions/responses
注意最后这一段:/chat/completions/responses。

这个 URL 形状本身就不对。Tencent LKEAP 这里提供的是 OpenAI Chat Completions 风格入口,而新版 Codex 期望的是 Responses API 风格入口。你把 base_url 配到 /chat/completions,Codex 又按照 wire_api = "responses" 继续追加 /responses,最后就拼出了一个服务端本来不该接收的路径。
所以这篇不是单纯讲“怎么配Tencent LKEAP”,而是复盘一次更通用的排错方法:当一个 AI 编程工具接入所谓 OpenAI-compatible 服务失败时,不能只盯着 Key,要先确认客户端的 wire protocol、服务端的 endpoint shape,以及最终请求路径是不是同一种协议。
现场现象:看起来像鉴权失败
当时配置大概是这样的:
[model_providers.custom]
name = "custom"
wire_api = "responses"
requires_openai_auth = true
base_url = "https://api.lkeap.cloud.tencent.com/plan/v3/chat/completions"
这段配置表面看起来有道理:Tencent 这边给的是 chat completions 路径,Codex 这边需要一个 base URL,于是就把完整路径填进去。
但问题就在这里。新版 Codex 不是在这个 URL 上直接发 Chat Completions 请求,而是根据 wire_api = "responses" 去构造 Responses API 请求。于是最终请求变成:
/plan/v3/chat/completions/responses

服务端返回 401,表面像鉴权失败,实际已经不是正常的鉴权链路了。路径错了,后面再怎么换 Key、改环境变量,都可能只是在错误方向上反复试。
这也是我后来总结的一个经验:遇到 API 报错,不要只看状态码,要把完整 URL、方法、协议形态一起看。状态码是症状,URL 形状才经常暴露病因。
为什么不能直接改成 Chat Completions?
如果问题只是路径拼错,那能不能把 Codex 改回 Chat Completions 模式?
这正是第二个坑。新版 Codex 已经不接受 wire_api = "chat" 这类配置。你尝试切回 Chat Completions,客户端会直接提示:
invalid configuration: `wire_api = "chat"` is no longer supported.
How to fix: set `wire_api = "responses"` in your provider config.
也就是说,这不是“base_url 少写一层”那么简单,而是客户端和服务端对协议的期待不一致:
Codex 这一侧,希望你提供一个 Responses API 风格的服务;
Tencent LKEAP 这个入口,实际暴露的是 Chat Completions 风格的服务;
你把 Chat Completions 的 URL 填给一个只会发 Responses 请求的客户端,它当然会拼出奇怪的地址。

这类问题以后会越来越常见。很多平台会说自己是 OpenAI-compatible,但“兼容”通常要看具体兼容哪一层:是兼容鉴权方式、模型参数、Chat Completions 路径,还是完整兼容 Responses API、工具调用、流式事件和多轮状态?
这些不是一个词能概括的。
真正的排错顺序
我现在遇到类似问题,会按这个顺序查。
第一步,看最终请求 URL。
如果 URL 已经出现 /chat/completions/responses、/v1/v1、/responses/chat/completions 这种明显叠层,就不要急着换 Key。先判断是不是 base URL 和客户端自动追加路径冲突。
第二步,看客户端要求的 wire protocol。
Codex 这里明确要求 responses。这意味着它发出的请求体、响应解析逻辑、流式事件形态,都是围绕 Responses API 设计的。服务端只支持 Chat Completions 时,哪怕鉴权过了,后面也可能在响应字段、工具调用、stream chunk 上继续出问题。
第三步,看服务端实际暴露的 endpoint。
Tencent LKEAP 这里的目标入口是:
https://api.lkeap.cloud.tencent.com/plan/v3/chat/completions
这个路径本身已经说明,它是 Chat Completions 形态。把它当成 Responses API base URL 使用,风险很高。
第四步,才是检查 Key、环境变量和权限。
当然,鉴权永远要查。但顺序很重要。如果路径都错了,Key 查一小时也不会有结果。
可行思路:本地做一层协议转换
这次记录里的解决思路,是写一个本地 Node.js 代理。
它不让 Codex 直接打Tencent 的 /chat/completions,而是让 Codex 先请求本地:
http://127.0.0.1:15722/v1/responses
本地代理再把 Responses 风格的最小请求转换成 Chat Completions 请求,转发到:
https://api.lkeap.cloud.tencent.com/plan/v3/chat/completions
返回时,再把 Chat Completions 的结果包装成 Codex 能接受的 Responses 形态。

这样做的好处是边界清楚:Codex 认为自己在和 Responses API 说话;Tencent LKEAP 仍然接收 Chat Completions 请求;中间的协议差异由本地代理承担。
配置上,Codex 指向本地代理:
model_provider = "tencent_lkeap_proxy"
model = "glm-5.1"
disable_response_storage = true
[model_providers.tencent_lkeap_proxy]
name = "Tencent LKEAP via local Responses proxy"
wire_api = "responses"
base_url = "http://127.0.0.1:15722/v1"
requires_openai_auth = true
env_key = "OPENAI_API_KEY"
Tencent 侧真实 Key 不放进 Codex 配置,而是放在代理进程的环境变量里:
$env:TENCENT_LKEAP_API_KEY="REDACTED"
$env:TENCENT_LKEAP_MODEL="glm-5.1"
node tools\codex-responses-to-chat-proxy.mjs
因为 Codex 这个 provider 形态仍然要求一个 OpenAI auth 变量,可以给它一个本地 dummy 值:
$env:OPENAI_API_KEY="local-proxy-dummy"
本地代理忽略这个 dummy header,只使用 TENCENT_LKEAP_API_KEY 调 Tencent上游。这样至少不会把真实密钥散落在多个客户端配置里。
这不是“完美解决”,而是兼容层方案
这里要讲清楚边界。
原始记录里,已经完成的是代理脚本语法检查,比如:
node --check tools\codex-responses-to-chat-proxy.mjs
但完整的端到端 smoke test、复杂工具调用、长任务流式事件,还需要继续验证。第一版本地代理更适合先处理普通文本和基础 streaming,不应该直接宣传成“完全兼容 Codex 所有能力”。
这点很重要。很多技术文章的问题不是没有方案,而是把一个 workaround 写成了最终答案。实际工程里,协议转换层通常要逐步加固:普通文本、流式输出、错误格式、工具调用、取消请求、超时、日志脱敏,每一项都可能有细节。
所以我更愿意把它称为:一个可验证的排错方向,一个局部可行的兼容思路,而不是一键解决所有问题。
什么时候不建议上代理?
本地代理很有用,但不是每次都应该上。
如果上游已经提供原生 Responses API,优先用原生入口。原生入口通常会更好地处理流式事件、工具调用、错误码、请求取消和计费字段。代理只适合在“客户端只能说 A 协议、服务端只会听 B 协议”的过渡场景里使用。
如果只是环境变量没读到、Key 填错、模型名写错,也不需要上代理。先用最小请求把上游跑通,比如直接用 curl 或一个短 Node 脚本请求目标 endpoint,确认模型能回答,再接入 Codex。否则你可能把简单配置问题包装成复杂架构问题。
如果团队里没有人愿意维护这层转换,也要谨慎。代理不是一次性脚本,它会变成运行链路的一部分。后续 Codex 改了 Responses 字段,或者Tencent LKEAP 调整了返回格式,你都要跟着改。对个人实验来说可以接受,对生产链路来说就必须有日志、版本、回滚和监控。
我更推荐的使用方式是:先把代理当成排错工具,用它证明协议转换这条路能走;确认价值后,再决定要不要沉淀成长期工具。
可以复用的排错清单
以后再遇到类似“OpenAI-compatible 接不上 AI 编程工具”的问题,我会直接套这个清单。
先记录原始报错,不要只截最后一句。至少保留状态码、完整 URL、请求方法、客户端版本、模型名和 provider 配置。
再判断 URL 有没有被重复拼接。/v1/v1、/chat/completions/responses、/anthropic/v1/messages/messages 这类路径,一眼就能看出 base URL 和客户端自动路径规则冲突。
然后确认客户端到底要哪种协议。Codex、OpenCode、Claude Code、不同 AI SDK 的 provider,背后使用的 wire protocol 可能完全不同。同样叫 OpenAI 兼容,有的要 Chat Completions,有的要 Responses,有的还会要求特定的 stream event。
最后才进入鉴权排查:Key 是否在当前进程环境里,代理是否改写了 header,服务端 token plan 是否支持这个模型,账号是否有对应权限。这样排查会慢一点开始,但整体更快结束。
这次排错带来的通用经验
第一,看到 401 不要立刻判定 Key 错。鉴权问题当然常见,但 URL 形状、请求方法、协议类型更值得先看。
第二,OpenAI-compatible 不是一句万能保证。Chat Completions 兼容、Responses API 兼容、Anthropic-compatible、工具调用兼容,是不同层级的事情。
第三,AI 编程工具越来越依赖客户端协议。以前很多问题是“模型能不能回答”,现在更多问题是“客户端和服务端能不能按同一种事件格式对话”。
第四,本地代理是个实用办法,但要带着边界使用。它适合把路径、鉴权、请求体、响应体做显式转换,也方便打日志;但它也会带来维护成本,不能替代上游原生支持。
第五,排错记录要保留证据。像 /chat/completions/responses 这种错误 URL,比一句“报 401”有价值得多。它能让后面的人少走很多弯路。
这次问题最值得记住的一句话是:
当 AI 编程工具接入第三方兼容接口失败时,不要只问 Key 对不对,要先问:客户端想说的协议,服务端真的听得懂吗?