🤖
AI审核中

给Codex加上一层边界:我的 AI 能力私有化实践

Java 21分钟 127浏览 2评论

很多人第一次想到“把 Codex 反代出去”时,脑海里的方案往往是:找到一个上游地址,再让 nginx 直接转发。但真正做起来会发现,Codex 并不是一个天然监听在 HTTP 端口上的 OpenAI API 服务。

这次实践最终得到的公网入口是:

Base URL: https://zxfhq.cn/v1
Model:    gpt-5.6-luna

外部程序可以使用 Responses API 或 Chat Completions API 的常见调用方式访问它,并且支持流式文本和图片识别。不过,它本质上仍然是一个自建的兼容网关,而不是 OpenAI 官方 Platform API。

本文记录完整架构、关键配置、安全边界和实际踩坑。

说明:gpt-5.6-luna 是本次网关对外公布的模型标识,不应被理解为 OpenAI 官方公共 API 的标准模型名称。本文不会展示任何真实密钥、服务器 IP、隧道凭据或 Codex 登录令牌。

一、先把“反代”这件事说准确

这套方案并不是把 Codex 网页或桌面应用直接代理到公网,而是分成两层:

  1. 使用 codex app-server 调用本机现有的 Codex 登录状态;
  2. 在 App Server 前面增加一个 OpenAI 风格的 HTTP 兼容层。

官方文档将 Codex App Server 定义为支撑 Codex 客户端深度集成的接口。它使用接近 JSON-RPC 2.0 的消息协议,典型流程包括 initializethread/startturn/start 和增量事件。App Server 更偏向客户端集成、开发和调试,并不是可以原样暴露到公网的生产 API。

因此,更准确的名称应该是:

Codex App Server 兼容网关,而不是 Codex HTTP 接口直通代理。

二、最终架构

整条链路如下:

外部客户端
    │
    │ HTTPS + Bearer Token
    ▼
https://zxfhq.cn/v1
    │
    │ nginx:精确路由、限流、并发限制
    ▼
Cloudflare Named Tunnel
    │
    │ 只转发三条白名单路径
    ▼
127.0.0.1:18081
    │
    │ OpenAI 风格兼容层
    ▼
codex app-server
    │
    ▼
当前 Codex / ChatGPT 登录与使用额度

这里最重要的设计是:codex app-server 和本地兼容层都不直接监听公网地址。公网流量必须依次经过 HTTPS、nginx、Cloudflare 路由白名单和 Bearer 鉴权。

三、本地兼容层做了什么

兼容层是一个仅监听回环地址的 Node.js 服务:

http://127.0.0.1:18081

它启动 codex app-server 子进程,通过标准输入输出交换 JSONL 消息,然后把 App Server 事件重新组织成外部客户端熟悉的 HTTP 响应。

一次普通文本请求大致经历以下过程:

  1. HTTP 服务校验 Bearer Token;
  2. 校验请求模型是否为 gpt-5.6-luna
  3. 创建临时 Codex Thread;
  4. 调用 turn/start
  5. 收集 item/agentMessage/delta 等增量事件;
  6. 转换成 Responses 或 Chat Completions 响应;
  7. 请求结束后释放临时上下文。

每次请求使用临时 Thread,可以降低不同外部调用之间串上下文的风险。模型提供方被固定,不允许自动回退到其他模型。

当前兼容接口包括:

GET  /v1/models
POST /v1/responses
POST /v1/chat/completions

它支持普通 JSON 响应和 SSE 流式响应,但不宣称兼容完整的 OpenAI Platform API。Embedding、Audio、Files、Batch 等未实现接口不会被“猜测性转发”。

四、为什么不能直接把 App Server 暴露出去

官方文档明确建议:非本地连接需要认证和 TLS,普通 ws:// 只适合 localhost 或 SSH 端口转发。更重要的是,App Server 能承载线程、审批、工具执行和文件操作等能力,直接公开会让攻击面远大于一个纯文本生成接口。

本次兼容层对模型执行权限做了额外收缩:

  • approvalPolicy=never
  • 使用只读沙箱;
  • 禁止网络访问;
  • 禁止修改文件;
  • 开发者指令禁止 Shell、网页浏览、Codex 工具和子智能体;
  • 不开放内部账户、额度和健康检查接口。

因此,外部请求获得的是文本与图片理解能力,而不是对宿主电脑的远程控制权。

五、nginx 只开放精确路径

nginx 不应使用一个宽泛的 location /v1/ 将所有请求交给上游。更稳妥的做法是只声明确定需要的路径:

limit_req_zone  $binary_remote_addr zone=codex_api_rate:10m rate=60r/m;
limit_conn_zone $binary_remote_addr zone=codex_api_conn:10m;

server {
    listen 443 ssl http2;
    server_name zxfhq.cn;

    location = /v1/models {
        include /etc/nginx/snippets/codex-api-proxy.conf;
    }

    location = /v1/responses {
        include /etc/nginx/snippets/codex-api-proxy.conf;
    }

    location = /v1/chat/completions {
        include /etc/nginx/snippets/codex-api-proxy.conf;
    }

    location = /v1 {
        default_type application/json;
        return 404 '{"error":{"message":"Route not found"}}';
    }

    location ^~ /v1/ {
        default_type application/json;
        return 404 '{"error":{"message":"Route not found"}}';
    }
}

公共代理片段可以这样设置:

client_max_body_size 24m;

limit_req zone=codex_api_rate burst=20 nodelay;
limit_req_status 429;

limit_conn codex_api_conn 3;
limit_conn_status 429;

proxy_pass https://<你的 Cloudflare 隧道主机名>;
proxy_http_version 1.1;
proxy_ssl_server_name on;

proxy_set_header Authorization $http_authorization;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";

proxy_buffering off;
proxy_cache off;
proxy_read_timeout 360s;
proxy_send_timeout 60s;

如果服务器没有 IPv6 出口,而 Cloudflare 域名同时解析出 AAAA 记录,可以考虑在 nginx resolver 中加入 ipv6=off,避免无效的 IPv6 连接尝试拖慢首次请求。

六、Cloudflare Tunnel 再做一层白名单

Cloudflare Named Tunnel 的入口同样只允许三条路径:

tunnel: <TUNNEL_ID>
credentials-file: <CREDENTIALS_FILE>

ingress:
  - hostname: <你的隧道主机名>
    path: ^/v1/models$
    service: http://localhost:18081

  - hostname: <你的隧道主机名>
    path: ^/v1/responses$
    service: http://localhost:18081

  - hostname: <你的隧道主机名>
    path: ^/v1/chat/completions$
    service: http://localhost:18081

  - hostname: <你的隧道主机名>
    service: http_status:404

  - service: http_status:404

这样,即便 nginx 配置以后被误改,隧道层仍然不会把本地 /health/codex/account 或其他管理接口带到公网。

七、密钥、限流和并发

公网入口使用高熵 Bearer Token:

Authorization: Bearer <YOUR_API_KEY>

密钥不应写入前端源码、Git 仓库、截图或日志。本机 Windows 环境可以使用 DPAPI 加密保存,进程启动时解密到环境变量,随后立即清理环境变量中的明文。

当前网关采用以下限制:

  • 每个公网 IP 平均每分钟 60 次请求;
  • 允许短时 burst 20;
  • 每个 IP 最多 3 个并发连接;
  • 本地模型全局最多 3 个并发任务,超出的请求 FIFO 排队;
  • 单个请求体最大 24 MiB;
  • 固定模型 gpt-5.6-luna,无 fallback;
  • 没有每日用量上限和分用户计费。

最后一条尤其重要:一个共享密钥意味着所有调用者共同消耗同一份 Codex 使用额度。密钥泄露后,即使攻击者无法控制主机,也能持续消耗额度。因此,多人使用时最好继续增加“每人一把子密钥、每日配额、审计和随时吊销”的网关层。

八、文字调用

先把密钥放进环境变量,不要写死在命令里:

export ZXFHQ_API_KEY="你的密钥"

Responses API:

curl https://zxfhq.cn/v1/responses \
  -H "Authorization: Bearer $ZXFHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-5.6-luna",
    "input": "请只回复:连接成功",
    "stream": false
  }'

Chat Completions 流式调用:

curl -N https://zxfhq.cn/v1/chat/completions \
  -H "Authorization: Bearer $ZXFHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-5.6-luna",
    "messages": [
      {"role": "user", "content": "用三句话解释什么是反向代理"}
    ],
    "stream": true
  }'

九、图片识别

官方 OpenAI 图片输入格式支持 URL、Base64 Data URL 和 File ID。为了降低 SSRF 与内网探测风险,本次自建网关只实现 Base64 Data URL,不接受任意远程图片 URL,也不实现 File ID。

当前限制为:

  • PNG、JPEG、WebP;
  • 每次最多 4 张;
  • 单张最大 8 MiB;
  • 图片合计最大 16 MiB;
  • 请求完成后删除临时图片;
  • OCR 场景建议使用 detail="original"

Python 调用示例:

import os
import base64
from pathlib import Path
from openai import OpenAI

client = OpenAI(
    base_url="https://zxfhq.cn/v1",
    api_key=os.environ["ZXFHQ_API_KEY"],
    timeout=180,
)

image = Path("screenshot.png")
data_url = (
    "data:image/png;base64,"
    + base64.b64encode(image.read_bytes()).decode()
)

response = client.responses.create(
    model="gpt-5.6-luna",
    input=[{
        "role": "user",
        "content": [
            {
                "type": "input_text",
                "text": "识别这张截图中的文字,并概括画面内容"
            },
            {
                "type": "input_image",
                "image_url": data_url,
                "detail": "original"
            }
        ]
    }]
)

print(response.output_text)

Base64 会增加请求体积,大图还会增加视觉 Token 和处理时间。通过公网隧道发送大图时,应设置足够长的客户端超时,并在上传前主动压缩或缩放图片。

十、真正的验收不能只看 /v1/models

/v1/models 返回 200,只能证明域名、TLS、路由和鉴权部分可用,不能证明模型推理链路真的正常。

完整验收至少应包括:

  1. 未携带密钥访问模型列表,确认返回 401;
  2. 携带密钥访问模型列表,确认只返回允许的模型;
  3. 发起一次非流式 Responses 请求;
  4. 发起一次 Chat Completions SSE 请求,确认只有一个完成事件和 [DONE]
  5. 使用真实图片完成 OCR;
  6. 检查未公开路径返回 404;
  7. 重启隧道和本地服务后再次完成真实推理;
  8. 确认临时图片目录清空;
  9. 确认项目与日志中没有明文密钥。

在本次部署中,文字 Responses、Chat SSE、Responses 图片输入和 Chat 图片输入均完成了真实公网验证。

十一、最容易踩的坑

1. 域名原来是否已经使用 /v1

修改 nginx 之前必须检查访问日志和现有应用配置。如果旧网站、Java 服务或桌面客户端已经调用 /v1/chat/completions,新路由会直接接管这些流量,既可能改变业务结果,也可能意外消耗 Codex 额度。

2. App Server 不是稳定的公共生产 API

Codex App Server 的协议和生成 Schema 与具体 Codex 版本相关。升级 Codex CLI 后,应该重新生成 Schema、执行回归测试,并准备随时回滚。

3. 不要导出或复制 OAuth Token

兼容层只应复用 Codex 已建立的登录状态,不应该把 OAuth Token 写入配置文件、数据库或转发给外部客户端。外部只接触网关自己生成的 Bearer Token。

4. 流式连接也占并发

SSE 在输出完成前一直占据连接。如果单 IP 并发限制是 3,一个客户端同时开启三个长回答,就会占满自己的连接配额。

5. 有并发限制,不等于有额度保护

并发 3 只能限制瞬时压力,无法阻止一天内持续调用。真正对外共享时,还需要日额度、用户级密钥、请求审计、异常告警和一键吊销。

十二、适合什么场景

这套方案适合:

  • 个人设备之间的受控调用;
  • 小范围内部测试;
  • 验证 OpenAI 风格客户端与 Codex 的兼容性;
  • 为已有脚本提供统一的文本和图片入口。

它不适合作为无人监管的大规模公共服务,也不建议直接用于商业转售。它依赖本机在线、Codex 登录有效、隧道运行正常以及账户仍有可用额度,任何一层离线都会使公网 API 不可用。

结语

把 Codex 接到自己的域名,技术上并不难;真正困难的是把边界收紧。

一个可用的版本只需要“请求能返回”,一个可以长期维护的版本则必须同时做到:

  • 本地服务不直接暴露;
  • 公网只开放精确路径;
  • 密钥与 Codex 登录凭据彻底分离;
  • 模型权限保持只读;
  • 有限流、并发和请求体限制;
  • 用真实推理而不是模型列表做验收;
  • 对共享额度和协议变化保持清醒。

最终,https://zxfhq.cn/v1 更像是一座受控的桥:外部程序得到熟悉的调用方式,而宿主机、Codex 登录和内部管理能力仍留在边界之内。

参考资料

2 条评论
如果你觉得文章对你有帮助,那就请作者喝杯咖啡吧☕
微信
支付宝
  2 条评论
Codex   湖南省衡阳市

召田最帅boy 博主   湖南省衡阳市

当前博客的评论/留言审核、自动回复均已接入原生gpt-5.6-luna。 拒绝中转站,从我做起ku