LOADING
2825 words
14 minutes
我是怎么把 CPA + NewAPI 串起来的

这几天我把自己的一套模型调用链路重新整理了一遍,核心是把 CLIProxyAPI(这里简称 CPA)NewAPI 串起来,再通过域名暴露出一个更稳定、可控、方便统一接入的入口。

这篇文章记录一下这套结构是怎么搭起来的、每一层各自负责什么、踩过哪些坑,以及为什么我最后会保留这样的分层设计。

CPA + NewAPI 实际调用链路图


一、先说结论:我最后采用的是两层结构

整体结构可以概括成这样:

客户端 / Hermes / 其他工具
NewAPI(3000)
CPA / CLIProxyAPI(8317)
上游模型供应商

再加上对外访问层,就是:

https://newapi.innilove.xyz
Nginx 反向代理
本机 NewAPI :3000
Channel #1 → 127.0.0.1:8317
CPA / CLIProxyAPI
各模型上游

这套方案里:

  • CPA / CLIProxyAPI 更像底层能力层,负责真正和上游模型打交道
  • NewAPI 是外层统一入口,负责渠道、模型映射、统一鉴权和对外接口组织
  • Nginx + 域名 负责把本地服务变成一个可直接接入的公网入口

二、为什么要分成两层,而不是直接一个服务顶到底

一开始很多人都会想:既然最终都是转发给模型,为什么不直接让客户端打底层?

我最后还是保留两层,主要是因为下面几个原因。

1. 对外入口和底层代理的职责不一样

底层的 CPA 更像是“真正出请求的人”,而 NewAPI 更像是“统一门面”。

这样拆开之后:

  • 外层可以统一接 OpenAI 兼容调用
  • 内层可以按自己的方式维护上游渠道
  • 某一层需要替换时,不至于把整条链路一起拆掉

2. 更适合做模型映射和渠道管理

我这里实际会出现这种情况:

  • 对外暴露的是一个统一模型名
  • 内部真正走的是某个 channel
  • channel 再转发给底层 CPA

比如我的实际配置里就有一条关键链路:

  • NewAPI 运行在 3000 端口
  • Channel #1(CodeX)转发到 http://127.0.0.1:8317
  • 底层 8317 就是 CPA / CLIProxyAPI

这个设计的好处是,外部接入方不需要知道底层到底是哪一个代理进程、跑在什么容器里,也不需要直接接触上游供应商细节。

3. 更容易做日志排查

分层之后,日志能清楚分成两类:

  • NewAPI 日志:看外层请求有没有进来、路由到了哪里
  • CPA 日志:看底层真正打上游时发生了什么

这对排查特别重要。很多时候问题不是“服务没起来”,而是:

  • 外层路由到了错误渠道
  • 底层某个上游没有可用 auth
  • 或者流式请求中途断开

如果所有逻辑都揉在一个服务里,排错会非常痛苦。


三、我这套环境的实际落地结构

1. NewAPI:外层统一入口

我的 NewAPI 本地运行在:

  • 127.0.0.1:3000

它是通过 systemd —user 方式启动的,启动参数里显式指定了日志目录。

实际启动命令是:

Terminal window
/home/agentuser/new-api --port 3000 --log-dir /home/agentuser/newapi-logs

所以它的关键位置是:

  • 服务文件:/home/agentuser/.config/systemd/user/newapi.service
  • 日志目录:/home/agentuser/newapi-logs

这意味着 NewAPI 本身不是一个“跑一下就算了”的临时进程,而是一个比较正式的本地常驻服务。


2. 对外域名:newapi.innilove.xyz

本地 3000 端口并没有直接裸露到外网,而是通过 Nginx 反向代理 对外暴露。

主入口域名是:

  • http://newapi.innilove.xyz

后来也补上了 HTTPS,证书是 Let’s Encrypt,443 已经配置完成。

这一层的意义很直接:

  • 对客户端来说,不需要关心本地端口
  • 可以统一走域名
  • 后续换端口、换服务实现、换内部结构,外部接入方都不用跟着改

3. CPA / CLIProxyAPI:底层真实代理

底层我实际使用的是 CLIProxyAPI,它跑在:

  • 8317 端口

并且是 Docker 部署 的。

也就是说:

  • 3000 是外层 NewAPI
  • 8317 是底层 CPA

这一点非常关键。因为我一开始排查时也踩过一个很典型的误区:

当某个模型调用失败时,不能简单地认为是 3000 端口挂了。

很多时候真实情况是:

  • NewAPI 本身还活着
  • 3000 也在监听
  • 只是它转发到的上游请求失败了

比如我在排查 gpt-5.4 时,见过这些典型日志:

  • 503 auth_unavailable / no auth available
  • 408 stream disconnected before response.completed

这说明问题可能出在:

  • 底层上游授权不可用
  • 某次流式请求中途断掉
  • 某个 channel 当前不可用

而不是 NewAPI 进程本身已经挂掉。


四、日志路径要分清,不然排错会很绕

这套分层里,最重要的经验之一就是:

先分清你要看的是哪一层的日志。

1. NewAPI 日志

NewAPI 的日志目录:

Terminal window
/home/agentuser/newapi-logs

这个目录适合用来查:

  • 服务有没有正常启动
  • 请求有没有打到外层
  • 外层有没有报路由错误
  • 基础接口是否正常

2. CPA / CLIProxyAPI 日志

CLIProxyAPI 是 Docker 部署,但日志不是只能进容器里看。

我这里它的日志已经挂载到了宿主机:

  • 宿主机路径:/home/agentuser/CLIProxyAPI/logs
  • 容器内路径:/CLIProxyAPI/logs

也就是说,不一定非得 docker exec 进去,直接在宿主机上看就行。

这个目录更适合查:

  • 上游实际报错
  • 某个模型请求失败细节
  • 某次 chat/completions 错误原因

如果看到类似下面这种文件名,基本就是底层真实报错:

error-v1-chat-completions-xxxx.log

五、为什么我后来强调“验证时优先看真实域名,而不是只看 localhost”

这是这次实践里一个非常实际的教训。

理论上,服务本地通了,很多人会觉得“那就没问题了”。但实际上不是。

因为在真实链路里,还多了一层:

  • Nginx
  • 域名
  • HTTPS
  • 反向代理头
  • 外网访问路径

所以如果只验证:

Terminal window
curl http://127.0.0.1:3000

那你只能证明:

  • NewAPI 本机端口可能活着

但你不能证明

  • 域名一定通
  • 反向代理没配错
  • 外部客户端也能正常访问

所以后面我的验证习惯就变成了:

第一优先级

直接验证真实外部入口:

Terminal window
http://newapi.innilove.xyz

第二优先级

再看本地服务端口:

Terminal window
http://127.0.0.1:3000

这个顺序更接近真实用户视角,也更容易提前发现反向代理层的问题。


六、这套结构带来的几个实际好处

1. 调用入口统一

对外只需要记住一个域名入口,不用关心底层到底连了几个模型、几个渠道、几个代理。

2. 可替换性更高

未来如果我要:

  • 替换底层代理实现
  • 新增别的 provider
  • 对某个模型单独调路由
  • 做灰度切换

都可以优先在内层做,而不影响外部调用方式。

3. 更适合做个人化中间层

像我这种场景,不只是“把请求转发出去”,而是还希望有:

  • 自定义模型名
  • 渠道映射
  • 统一接口风格
  • 统一日志入口
  • 更顺手的域名访问

那 NewAPI 作为外层就非常合适。

4. 出问题时更容易定位

可以快速判断问题在哪一层:

  • 域名不通 → 先看 Nginx / 443 / 反代
  • 3000 正常、请求失败 → 看 NewAPI 路由
  • NewAPI 正常、上游报错 → 看 CPA / CLIProxyAPI
  • 底层日志显示 auth unavailable → 看上游认证池

这种分层定位,比单体结构的“全堆在一起”清晰太多了。


七、这次实践里踩到的坑

坑 1:把“端口活着”和“整条链路正常”混为一谈

3000 端口活着,不代表:

  • 上游一定可用
  • 某个模型一定可用
  • 流式输出一定稳定

服务“活着”和“能正确完成某类请求”,是两回事。


坑 2:看到失败就以为外层挂了

尤其是遇到:

  • 503 no auth available
  • 408 stream disconnected

这种报错时,很容易本能觉得是外层服务炸了。

但实际上,很多时候只是:

  • 外层还在
  • 路由还在
  • 底层某个上游请求失败了

这个判断如果错了,会把排障方向带偏。


坑 3:日志不分层

一旦把 NewAPI 日志和 CPA 日志混在一起看,信息会非常乱。

正确做法是:

  • 先看外层有没有收到请求
  • 再看外层转发到了哪里
  • 最后看底层上游为什么失败

这样会省很多时间。


八、如果你也想复刻这套结构,可以怎么搭

一个最简思路如下:

第一步:先把底层代理单独跑起来

先让 CPA / CLIProxyAPI 在本机某个端口稳定运行,比如:

127.0.0.1:8317

确保它本身可以完成最基础的模型请求。


第二步:再让 NewAPI 接到它前面

在 NewAPI 里配置一个 channel,把模型请求转发给:

http://127.0.0.1:8317

先只做一条最小可用链路,不要一开始就堆太多模型和太多路由规则。


第三步:把 NewAPI 作为本地常驻服务托管

用 systemd、supervisor 或容器都行,但核心是:

  • 服务要能稳定常驻
  • 重启后能自动恢复
  • 日志目录要单独留出来

第四步:最后再上域名和 HTTPS

先保证本机链路通,再上反向代理。

推荐顺序:

  1. 本地 3000 -> 8317
  2. Nginx 反代通
  3. 域名通
  4. HTTPS 通
  5. 再做真实客户端接入验证

不要一上来就一边调反代一边调底层模型,那样会把变量搅在一起。


九、我现在怎么看这套架构

如果只是“能用就行”,它看起来有点绕。

但如果你已经有下面这些需求:

  • 多个模型来源
  • 多渠道转发
  • 统一接口风格
  • 域名入口
  • 后续可能扩展更多 provider
  • 希望出问题时能快速分层排查

CPA + NewAPI 这种两层结构其实很顺手。

它不是最短路径,但对“长期维护”和“自己掌控整条链路”来说,体验会好很多。

我自己这次折腾下来最大的感受就是:

真正重要的,不只是把服务跑起来,而是把“入口、转发、日志、排障”这四件事都理顺。

只有这样,这套东西后面才不会越用越乱。


十、最后做个简短总结

我的这套链路核心就是:

  • NewAPI 在外层:负责统一入口、渠道映射、对外接口
  • CPA / CLIProxyAPI 在内层:负责真实打上游模型
  • Nginx + 域名在最外面:负责公网访问和 HTTPS

已知关键点:

  • NewAPI:3000
  • CPA / CLIProxyAPI:8317
  • NewAPI 对外域名:newapi.innilove.xyz
  • NewAPI 日志:/home/agentuser/newapi-logs
  • CPA 日志:/home/agentuser/CLIProxyAPI/logs

如果后面我再继续完善这套链路,我大概率会继续补这几块:

  • 更稳定的超时与重试策略
  • 更细的模型/渠道健康检查
  • 更清晰的错误分层提示
  • 更方便复用的部署文档

如果你也在折腾自己的模型网关,希望这篇记录能帮你少踩几个坑。

我是怎么把 CPA + NewAPI 串起来的
/posts/cpa-newapi-blog/
Author
swrited
Published at
2026-04-29
License
CC BY-NC-SA 4.0

Some information may be outdated