OMP /compact 与 Responses 网关联调排错

makoMakoGo 于 2026-05-06 发布

1. 先说结论

这次排查最后确认的是:

  1. OMP 这边要把 gateway 配成 openai provider,不是 openai-codex
  2. OMP 对 gateway 发 compact 时,正确外部路径是 /v1/responses/compact
  3. gateway 这一层是 new-api,对外暴露的是 OpenAI Responses 风格接口
  4. gateway 内部可以把 compact 请求继续路由到 Codex channel,再交给 CLIProxyAPI(CPA)
  5. 最开始失败不是 URL 错,而是 new-api 没有为 compact 模型提供可用 channel
  6. 修好后,compact 成功走到了 gpt-5.4(high)-openai-compact,并命中了 gateway 后台中的 Codex 渠道

一句话概括整条链路:

最容易混淆的地方在于:

也就是说:

这两件事不冲突。


2. 这次一开始在查什么

目标本来有两件:

  1. 搞清楚 CPA 的 compact 接口到底是什么
  2. 把本地 OMP 配成走 gateway 的 compact 接口

排查过程中,实际上涉及了三层:

  1. OMP 本身的 compact 实现
  2. gateway 这一层的 new-api
  3. new-api 背后的 CLIProxyAPI / CPA

所以要搞清楚的问题就变成了:


3. OMP 里到底有哪两种“远端 compact”

OMP 实际上有两条不同的 compact 路:

3.1 通用 compaction.remoteEndpoint

代码在:

这条路的特点是:

{
  "systemPrompt": "...",
  "prompt": "..."
}
{
  "summary": "..."
}

所以它更像一个“自定义摘要服务接口”,不是 OpenAI/Codex 原生 compact 接口。

3.2 OpenAI / Codex 原生 remote compact

代码在:

OMP 只有在下面这个条件成立时,才会走这条“原生 remote compact”分支:

function shouldUseOpenAiRemoteCompaction(model: Model): boolean {
  return model.provider === "openai" || model.provider === "openai-codex";
}

也就是说,决定 compact 路由的关键不是 api: openai-responses,而是:

这一点后面非常关键。


4. OMP 为什么把 gateway 改成 openai 就能工作

一开始最容易误解的是:

但这其实是不对的。

4.1 OMP 的 provider 决定的是“它怎么访问外部网关”

OMP 里 custom provider 的名字会直接变成模型的 provider 字段。

代码在:

return {
  id: modelDef.id,
  provider: providerName,
  api,
  ...
}

所以如果配置写成:

providers:
  gateway-openai:

那这个模型的 provider 就真的是:

这会导致一个后果:

因为 OMP 只认:

不认任意自定义 provider 名。

4.2 为什么不能改成 openai-codex

如果 OMP 模型的 provideropenai-codex,compact endpoint 会按 Codex 规则拼。

代码在:

if (model.provider === "openai-codex") {
  return resolveOpenAiCodexCompactEndpoint(model.baseUrl);
}

其中 Codex compact endpoint 的规则是:

if (/\/codex(?:\/v\d+)?$/.test(normalizedBase)) return `${normalizedBase}/responses/compact`;
return `${normalizedBase}/codex/responses/compact`;

也就是 OMP 会去打:

同时它还会额外加 Codex 专用 headers:

代码在:

但 gateway/new-api 对 OMP 暴露出来的外部入口并不是 Codex 风格,而是:

所以如果 OMP 配成 openai-codex,它访问 gateway 的外部 URL 形态就会错。

4.3 为什么改成 openai 正好匹配 gateway

如果 provider === "openai",OMP 会把 compact endpoint 拼成:

代码在:

const rawBase = model.baseUrl && model.baseUrl.length > 0 ? model.baseUrl : defaultBase;
const normalizedBase = rawBase.endsWith("/") ? rawBase.slice(0, -1) : rawBase;
if (normalizedBase.endsWith("/v1")) return `${normalizedBase}/responses/compact`;
return `${normalizedBase}/v1/responses/compact`;

这和 gateway/new-api 对外暴露的 compact path 正好一致。

所以最终的正确理解是:


5. 本地 OMP 最开始是什么配置

本地最开始的配置是:

~/.omp/agent/config.yml

~/.omp/agent/models.yml

也就是说,最开始 gateway 只是被当成一个自定义 OpenAI Responses provider 来用。

普通请求没问题,但 compact 的路由条件并不满足。

后来改成了:

~/.omp/agent/models.yml

把 provider 名从:

改成:

保留:

~/.omp/agent/config.yml

把角色切到:

这样 OMP 才会把 gateway 识别为“OpenAI remote compact provider”。


6. 一开始为什么失败:不是 URL 错,而是 new-api 选不到 compact 渠道

改完 OMP 配置后,第一次手动 /compact 失败了。

OMP 本地日志:

关键信息是:

{
  "message": "OpenAI remote compaction failed",
  "endpoint": "https://your-gateway.example.com/v1/responses/compact",
  "status": 503,
  "errorText": "{\"error\":{\"code\":\"model_not_found\",\"message\":\"No available channel for model gpt-5.4(high)-openai-compact under group svip (distributor) ...\"}}"
}

以及:

{
  "message": "OpenAI remote compaction failed, falling back to local summarization"
}

这条错误很关键,因为它告诉我们:

  1. OMP 的 compact URL 已经是:
    • https://your-gateway.example.com/v1/responses/compact
  2. 这个 URL 不是 404,而是进入了 compact 业务逻辑
  3. new-api 在 distributor 阶段把模型看成了:
    • gpt-5.4(high)-openai-compact
  4. 失败点是:
    • svip 组下没有这个 compact 模型的可用 channel

也就是说,失败不是卡在 OMP,也不是卡在 URL 拼接;而是卡在 gateway/new-api 的渠道选择阶段。


7. 为什么 new-api 会把 compact 请求改成 -openai-compact

为了确认这是不是 new-api 的设计,而不是线上实例自己的奇怪逻辑,我去看了 new-api 最新源码。

本地 local-new-api-worktree 的工作树分叉严重,而且有未提交改动,所以没有直接改它;而是:

7.1 最新 new-api 已经原生支持 /v1/responses/compact

路由在:

httpRouter.POST("/responses", func(c *gin.Context) {
  controller.Relay(c, types.RelayFormatOpenAIResponses)
})
httpRouter.POST("/responses/compact", func(c *gin.Context) {
  controller.Relay(c, types.RelayFormatOpenAIResponsesCompaction)
})

7.2 它在 distributor 里会给 compact 模型自动加后缀

代码在:

if strings.HasPrefix(c.Request.URL.Path, "/v1/responses/compact") && modelRequest.Model != "" {
  modelRequest.Model = ratio_setting.WithCompactModelSuffix(modelRequest.Model)
}

compact suffix 定义在:

const CompactModelSuffix = "-openai-compact"

func WithCompactModelSuffix(modelName string) string {
  if strings.HasSuffix(modelName, CompactModelSuffix) {
    return modelName
  }
  return modelName + CompactModelSuffix
}

所以在 new-api 看来:

这和日志里的报错完全一致。

7.3 选到 channel 后,它又会把后缀去掉再做映射/转发

代码在:

这段逻辑的意思是:

所以它的设计其实是:


8. 最新 CLIProxyAPI / CPA 对 compact 做了什么

一开始查 CPA 时,本地那份源码里没看到 compact 路由;后来拉了最新 main 才看到 compact 支持已经补上了。

8.1 CPA 最新版公开暴露了 /v1/responses/compact

代码在:

v1.GET("/responses", openaiResponsesHandlers.ResponsesWebsocket)
v1.POST("/responses", openaiResponsesHandlers.Responses)
v1.POST("/responses/compact", openaiResponsesHandlers.Compact)

对应 handler 在:

它会把 compact 请求以 alt = "responses/compact" 交给执行器。

8.2 CPA 内部会根据执行器类型继续转发 compact

Codex executor

代码在:

最终 URL:

url := strings.TrimSuffix(baseURL, "/") + "/responses/compact"

OpenAI-compat executor

代码在:

compact 情况下 endpoint 会改成:

所以对 CPA 来说,compact 也已经是一级公民了,不再只是普通 /responses


9. 这次为什么最后可以确定“外部 URL 是对的”

有三层证据:

9.1 OMP 源码本身

如果 provider 是 openai,OMP 就会打:

如果 provider 是 openai-codex,OMP 才会打:

9.2 live endpoint 探测

直接测过 gateway:

这说明 gateway 暴露给客户端的 compact surface 就是 OpenAI 风格,而不是 Codex 风格。

9.3 new-api 后台与 OMP 失败日志

失败日志不是 404,而是:

这表明:


10. 当时到底怎么修好的

最终修复的关键点不是 OMP,而是 gateway/new-api 的 channel 配置。

10.1 当时缺的是什么

根据失败日志,缺的是:

因为 new-api 对 compact 的选路模型名是:

所以如果 channel 的 Models 里只有:

那普通请求可以走通,但 compact 选不到 channel。

10.2 修复思路

在 gateway/new-api 后台,把那条指向 CPA 的 channel 补齐:

  1. Group 必须包含:

    • svip
  2. Models 至少要包含 compact 版本:

    • gpt-5.4(high)-openai-compact

通常也会一起保留普通模型:

如果还需要 xhigh,同理补:

  1. 这条 channel 的类型确认是:

    • Codex
  2. 这条 channel 的下游是:

    • CLIProxyAPI / CPA

也就是说,gateway/new-api 内部最终确实是把 compact 请求打给了“Codex via CPA”渠道。


11. 修好后是怎么确认成功的

最终的成功证据不是 OMP 本地日志,而是 gateway/new-api 后台截图。

后台里能看到两条关键记录:

  1. 普通请求:

    • 模型:gpt-5.4(high)
  2. compact 请求:

    • 时间:2026-04-14 12:35:40
    • 分组:svip
    • 模型:gpt-5.4(high)-openai-compact
    • 渠道:114
    • 非流式
    • 耗时约 38s

而且你确认:

所以这条证据链已经足够完整:


12. 为什么 OMP 本地日志没有“成功记录”

这个点中间也容易误会。

不是 OMP 没成功,而是 OMP 当前实现根本不记 remote compact success 日志

查看代码:

但成功路径是:

所以表现就是:

因此,这次最终的成功证据主要来自:

而不是 OMP 本地日志。


13. 如果以后要直连 CPA,OMP 应该怎么配

这个问题后来也顺手理清了。

如果以后不走 gateway/new-api,而是让 OMP 直接打 CPA:

原因也一样:

也就是说:

所以无论是:

还是:

只要对外暴露给 OMP 的接口是:

那 OMP 就应该配:

只有当外部接口本身真的是 Codex 风格,例如对外直接暴露:

才应该把 OMP 配成 openai-codex

13.1 再换一个角度看:Codex 其实有两套相关 surface

如果换成 Codex CLI 自己的视角,这件事会更好理解。

13.1.1 官方 ChatGPT / Codex cloud surface

当 Codex CLI 走 GPT Sub / OAuth,直接连 OpenAI 官方云端时,接口更接近:

这可以理解成官方 Codex cloud 那套接口形态。

13.1.2 自定义 Responses provider surface

而当 Codex CLI 不走官方云端,而是连一个自定义的 Responses provider 时,它面对的是:

这就是更通用的 OpenAI Responses 风格 surface。

这样再看 CPA,就很容易理解:

所以这次链路里:


14. 这次最重要的几个认知点

我觉得这次最值得记住的不是某个具体路径,而是下面这几个分层原则。

14.1 不要把“外部协议”和“内部执行器”混为一谈

14.2 OMP 的 compact 分支判断依赖 provider,不是 api

14.3 new-api 的 compact 选路不是拿原模型名,而是拿 -openai-compact 后缀模型名

所以以后凡是 new-api 上 compact 选不到渠道,都要先看:

14.4 失败日志的模型名非常有信息量

这次错误里出现:

实际上已经把问题层级暴露得很清楚了:

这类错误以后可以直接顺着模型名看。


15. 最后用一句最短的话总结这次问题

这次问题本质上不是“OMP 会不会发 compact”,也不是“CPA 支不支持 compact”,而是:

补齐这层后,整条链路就通了: