这条链路到底在学习什么?

要理解 AI 客户端、中转平台、官方 API 之间的关系,核心不是“某个软件怎么改配置”,而是理解一套通用思想:

flowchart LR
    A["AI 客户端<br/>组装用户输入、上下文、工具定义"] --> B["模型供应商配置<br/>决定地址、协议、认证"]
    B --> C["本地代理或远程 API 网关<br/>判断路由并做协议适配"]
    C --> D["上游模型服务<br/>执行推理并返回文本、工具调用或流式事件"]
    D --> E["AI 客户端<br/>消费响应,展示回复或继续执行工具"]

在 Codex 这类支持自定义 provider 的客户端里,这套思想可以落到一个很小的切入点:不改客户端内核,只改 model_provider 的入口地址,把模型请求导向本地代理,再由本地代理转发到官方或第三方中转。

AI 客户端的模型请求本质是什么?

AI 客户端并不是直接把“用户一句话”交给模型。它会把很多信息组装成结构化请求:

  • 用户当前输入。
  • 系统/开发者指令。
  • 仓库上下文和文件片段。
  • 可用工具列表。
  • sandbox、审批、网络策略等运行边界。
  • 历史对话和工具调用结果。
    这些内容会被编码成 JSON,通过 HTTP 发给模型服务。
flowchart LR
    A["用户输入"] --> B["AI 客户端组装请求 JSON"]
    B --> C["POST /v1/responses<br/>或 /v1/chat/completions"]
    C --> D["模型服务返回 stream 事件或完整 JSON"]
    D --> E["客户端根据响应继续对话、调用工具或编辑文件"]

所以“AI 之间怎么请求”更准确地说是:客户端和模型服务之间通过 HTTP API 传递结构化上下文,模型不直接访问你的电脑,真正执行工具的是本地客户端。

provider 是整条链路的分流点

以 Codex 的配置模型为例,model_provider 决定模型请求发到哪里。一个 provider 通常包含:

[model_providers.example]
name = "Example Provider"
base_url = "https://example.com/v1"
wire_api = "responses"
api_key = "..."
requires_openai_auth = false
supports_websockets = false

最关键的是:

  • model_provider:当前选中的供应商。
  • model:当前使用的模型名。
  • base_url:请求要发往的 API 入口。
  • wire_api:客户端和 provider 之间使用的协议形态。
  • api_key / env_key / requires_openai_auth:认证方式。
    只要把 base_url 从官方 API 换成一个本地地址,就可以让客户端把模型请求先交给本地代理。

本地代理为什么能只接管某个 AI 客户端?

它不是系统代理,而是 provider 级代理

flowchart LR
    subgraph S["系统代理模式"]
        S1["所有 App 的网络请求"] --> S2["系统代理端口"] --> S3["代理服务"]
    end
    subgraph P["provider 级代理模式"]
        P1["配置了该 provider 的模型请求"] --> P2["model_provider.base_url"] --> P3["本地代理端口"]
    end

这就是“只代理某个客户端”的关键:客户端主动请求本地端口,其它软件不会自动经过这个端口。
常见隔离层包括:

  • 本地代理绑定 127.0.0.1,只允许本机访问。
  • 客户端 provider 配置里的 base_url 指向本地端口。
  • URL 中带 provider id,用来区分不同上游。
  • 客户端配置中使用占位 key,真实上游 key 由代理保存。

本地代理在中间做什么?

本地代理承担的是模型请求网关职责。

flowchart LR
    A["客户端模型请求"] --> B["匹配 provider<br/>读取本地配置"]
    B --> C["适配请求<br/>模型名、认证、协议字段"]
    C --> D["转发上游<br/>官方或第三方中转"]
    D --> E["适配响应<br/>普通响应或流式事件"]

伪代码可以理解为:

async fn handle_model_request(req: Request) -> Response {
    let provider_id = parse_provider_id(req.path());
    let provider = load_provider_config(provider_id);
    let upstream_key = load_upstream_key(provider_id);
    let body = read_json_body(req).await;
 
    let upstream_body = adapt_request_body(body, provider.wire_api, provider.model);
    let upstream_url = build_upstream_url(provider.base_url, provider.wire_api);
 
    forward(upstream_url, upstream_key, upstream_body).await
}

真实实现还需要处理:

  • SSE 流式返回。
  • 错误码映射。
  • 超时和重试。
  • 模型名映射。
  • Responses API 和 Chat Completions API 之间的转换。
  • 工具调用、图片输入、reasoning 参数等能力差异。

官方 API 和中转 API 是如何切换的?

切换本质是请求入口和上游配置的变化

直接走官方

这种方式路径短,兼容性最好。

走第三方中转

这种方式更灵活,可以接入非官方模型、统一管理 key、做模型映射和协议适配。

统一从本地代理分发

这种方式最像“智能路由”:客户端永远只知道本地代理,具体走哪里由代理决定。

flowchart LR
    C["AI 客户端"]
    C -->|"直接走官方"| O["官方 provider base_url"] --> OA["官方 API"]
    C -->|"走第三方中转"| L["本地代理 provider base_url"] --> P["本地代理"] --> R["第三方中转平台"] --> M["真实模型服务"]
    C -->|"统一本地分发"| G["本地代理"] --> D{"路由配置"}
    D -->|"官方"| OA
    D -->|"中转"| R

One API 为什么同时有 OpenAI 和 Claude 入口?

oneapi-comate.baidu-int.com 可以理解成一个多协议兼容网关。它不是只模拟某一个官方 API,而是在同一个域名下暴露 OpenAI-compatible 和 Anthropic-compatible 两类入口,让不同 agent / SDK 用自己原生的请求格式接入。

常见入口可以这样理解:

用途地址适配对象
OpenAI SDK base_urlhttps://oneapi-comate.baidu-int.com/v1OpenAI SDK、OpenAI-compatible agent
Chat Completionshttps://oneapi-comate.baidu-int.com/v1/chat/completionsclient.chat.completions.create(...)
Responses APIhttps://oneapi-comate.baidu-int.com/v1/responsesCodex / Responses 风格 agent 流程
Claude Messageshttps://oneapi-comate.baidu-int.com/v1/messagesAnthropic SDK、Claude Code、Claude-native agent
Modelshttps://oneapi-comate.baidu-int.com/v1/models查询模型列表

这里的关键不是“哪个 URL 更高级”,而是请求/响应协议不同。OpenAI Chat、OpenAI Responses、Anthropic Messages 的 JSON 结构、流式事件格式、工具调用表达都不完全一样。One API 网关同时提供这些入口,是为了让上层客户端尽量少改代码:OpenAI agent 继续按 OpenAI 协议发,Claude agent 继续按 Anthropic Messages 协议发,中间由网关做模型路由、鉴权和必要的协议适配。

生成图片 skill 走哪条 GPT 链路?

本地 generate-image skill 默认走的是 OpenAI-compatible 图片接口,不是 Claude Messages。脚本里的默认网关是:

DEFAULT_BASE_URL = "https://oneapi-comate.baidu-int.com"
DEFAULT_MODELS = {
    "images": "gpt-image-2",
    "responses": "gpt-5.5",
    "banana": "gemini-3.1-flash-image-preview",
}

默认 backend 是 images,所以实际请求会拼成:

POST https://oneapi-comate.baidu-int.com/v1/images/generations
Content-Type: application/json
Authorization: Bearer <GENERATE_IMAGE_API_KEY>

请求体核心字段是:

{
  "model": "gpt-image-2",
  "prompt": "a red apple on a white table",
  "n": 1,
  "size": "1024x1024"
}

所以这条链路可以概括为:

flowchart LR
    A["generate-image skill"] --> B["generate_image_client.py"]
    B --> C["POST /v1/images/generations"]
    C --> D["oneapi-comate 网关"]
    D --> E["gpt-image-2 图片模型"]

⚠️ 网关文档里的示例可能使用 x-api-key: sk-...,而当前本地脚本使用的是 Authorization: Bearer ...。如果网关只接受 x-api-key,脚本需要在 _post_json() 中补充 req.add_header("x-api-key", self.api_key),或者让脚本同时带两个鉴权头。

Responses API 和 Chat API 的差异为什么重要?

Agent 型客户端通常更依赖 Responses API,因为它更适合表达 agent 工作流:

  • 流式事件。
  • 工具调用。
  • reasoning 信息。
  • 多模态输入。
  • 长任务状态。
    很多第三方中转平台或模型只支持 Chat Completions 兼容协议。于是本地代理常见职责之一就是:
flowchart LR
    A["Responses 请求"] --> B["本地代理转换"] --> C["Chat Completions 请求"]
    D["Chat stream chunks"] --> E["本地代理适配"] --> F["客户端能消费的 stream events"]

这就是为什么代理不是简单 curl 转发,它还要理解协议形态。

一次请求可以如何抽象?

客户端发给本地代理:

POST /codex/by-provider/example_provider/v1/responses HTTP/1.1
Host: 127.0.0.1:25817
Authorization: Bearer local-placeholder
Content-Type: application/json
{
  "model": "some-model",
  "input": [
    {
      "role": "user",
      "content": [
        {
          "type": "input_text",
          "text": "用户问题"
        }
      ]
    }
  ],
  "stream": true,
  "tools": []
}

代理转发给上游中转:

POST /v1/chat/completions HTTP/1.1
Host: relay.example.com
Authorization: Bearer <真实上游 key>
Content-Type: application/json
{
  "model": "mapped-model-name",
  "messages": [
    {
      "role": "user",
      "content": "用户问题"
    }
  ],
  "stream": true
}

⚠️ 这只是抽象形态。真实请求会包含更多上下文、工具定义、reasoning 参数和客户端私有字段。

一个本地模型代理通常包含哪些层?

这类工具可以拆成几层:

flowchart LR
    A["配置管理层<br/>读写客户端配置,把 provider 指向本地代理"] --> B["状态管理层<br/>保存 provider、模型、上游地址、协议类型"]
    B --> C["密钥管理层<br/>保存真实上游 key,不直接暴露给客户端配置"]
    C --> D["本地代理层<br/>监听 127.0.0.1:PORT,接收模型请求"]
    D --> E["协议/路由层<br/>根据 provider id、wireApi 和上游配置转发请求"]

关键思想是:让客户端只面对一个本地 OpenAI-compatible 入口,复杂的上游选择和协议适配都放到本地代理里。

Codex 本地配置样本

本机 ~/.codex/config.toml 中有一组本地 relay provider:

[model_providers.aimami_relay_2597f406ab]
name = "oneapi [gpt-5.5]"
base_url = "http://127.0.0.1:25817/codex/by-provider/aimami_relay_2597f406ab/v1"
wire_api = "responses"
supports_websockets = false
api_key = "aimami-relay"
requires_openai_auth = false
 
[model_providers.aimami_relay_4b291ef3da]
name = "oneapi [ds4p]"
base_url = "http://127.0.0.1:25817/codex/by-provider/aimami_relay_4b291ef3da/v1"
wire_api = "responses"
supports_websockets = false
api_key = "aimami-relay"
requires_openai_auth = false
 
[profiles.aimami_relay]
model_provider = "aimami_relay_4b291ef3da"
model = "DeepSeek-V4-Pro"

这说明 Codex 侧看到的是本地代理:

http://127.0.0.1:25817/codex/by-provider/<provider-id>/v1

~/.codex/codexmate/relay/state.json 中保存的上游配置可以概括为:

{
  "providers": [
    {
      "id": "aimami_relay_2597f406ab",
      "baseUrl": "https://oneapi-comate.baidu-int.com/v1",
      "model": "gpt-5.5",
      "wireApi": "openai-responses"
    },
    {
      "id": "aimami_relay_4b291ef3da",
      "baseUrl": "https://oneapi-comate.baidu-int.com/v1",
      "model": "DeepSeek-V4-Pro",
      "wireApi": "openai-chat"
    }
  ],
  "proxy": {
    "port": 25817,
    "baseUrl": "http://127.0.0.1:25817"
  }
}

这个样本展示了一个典型结构:

flowchart LR
    A["客户端 provider<br/>指向本地代理"] --> B["本地 provider state<br/>记录真实上游和协议"]
    B --> C["relay key 文件<br/>保存真实上游认证"]
    C --> D["本地代理<br/>完成转发和协议适配"]

如何观察和验证这条链路?

查看客户端 provider 配置:

sed -n '/aimami-relay managed start/,/aimami-relay managed end/p' ~/.codex/config.toml

查看本地 relay 状态:

cat ~/.codex/codexmate/relay/state.json

查看代理审计:

tail -50 ~/.codex/codexmate/relay/passthrough-audit.jsonl

查看端口监听:

lsof -nP -iTCP:25817 -sTCP:LISTEN

这些命令不是为了“修某个问题”,而是为了建立链路观察能力:

  • 客户端现在请求哪里?
  • 本地代理是否存在?
  • 本地代理把请求转发给哪个上游?
  • 上游使用 Responses 还是 Chat?
  • 请求失败发生在客户端、本地代理、还是远程上游?

学习这套机制的关键抽象

抽象一:模型供应商是可替换资源

AI 客户端不应该强绑定某一个 API 地址。只要协议兼容,model_provider 可以指向官方、第三方中转、本地网关、企业内网代理。

抽象二:本地代理是协议边界

本地代理把客户端请求协议和上游模型协议隔开。客户端不必知道上游真实模型、key、路由策略。

抽象三:路由不一定发生在云端

路由可以发生在本机。这样可以保护 key、保留本地控制权,也便于调试。

抽象四:协议兼容比模型名更重要

模型名只是入口,真正决定客户端能不能正常工作的是上游是否支持它需要的协议特性:stream、tools、reasoning、multi-modal、error format。

抽象五:工具执行仍在客户端侧

模型服务返回的是“我要调用什么工具”的意图,真正执行 shell、读文件、写文件、浏览器操作的是本地客户端。

记住这张心智图

flowchart LR
    A["AI 客户端<br/>收集上下文、执行工具、展示结果"] --> B["model_provider<br/>决定模型请求入口"]
    B --> C["本地代理<br/>路由、认证、协议适配、流式转发"]
    C --> D["上游 API<br/>负责模型推理"]
    D --> E["本地代理<br/>把响应转回客户端可消费形态"]
    E --> F["AI 客户端<br/>继续 agent 循环"]
    F --> A

相关笔记