Codex 源码导读:第六部分——模型请求与流式网络层

第六部分:模型请求与流式网络层

主流程

text 复制代码
Session 历史 + 当前 Turn 配置
  ↓
build_prompt()
  ↓
ModelClientSession::stream()
  ↓
选择 WebSocket 或 HTTP Responses API
  ↓
SSE / WebSocket 原始事件
  ↓
ResponseEvent
  ↓
Core 的 run_sampling_request()
  ↓
写入 Session + 发布 EventMsg + 触发工具

请求传入的内容

run_sampling_request 将这些内容交给 client_session.stream():

  • 已编译的 Prompt:系统指令、历史消息、工具定义和当前输入。
  • ModelInfo:模型标识、输入模态、Responses Lite 等模型能力。
  • 推理强度、推理摘要配置、服务等级等采样参数。
  • CodexResponsesMetadata:Session、Thread 等请求标识。
  • InferenceTraceContext 和会话遥测信息。

因此网络层接收的是一个已经完成上下文编译的请求,不负责重新决定 Session 历史或工具权限。

Prompt 如何变成 ResponsesApiRequest

Prompt 进入 ModelClientSession::stream() 后,由 build_responses_request() 编译成供应商请求:

rust 复制代码
let mut input = prompt
    .get_formatted_input_for_request(
        model_info.use_responses_lite,
    );
// input 是已经规范化的历史:用户消息、模型输出、tool-call、tool-result

let request = ResponsesApiRequest {
    model: model_info.slug.clone(),
    instructions,
    input,
    tools,
    tool_choice: "auto".to_string(),
    parallel_tool_calls:
        prompt.parallel_tool_calls
        && !model_info.use_responses_lite,
    reasoning: Some(reasoning),
    store: false,
    stream: true,
    include,
    service_tier,
    prompt_cache_key,
    text,
    client_metadata: Some(
        responses_metadata.client_metadata(),
    ),
    access_programs: None,
};

普通 Responses API 中,字段关系是:

text 复制代码
instructions = 系统指令
input        = 会话历史和工具结果
tools        = 本轮模型可见的工具 JSON Schema

Responses Lite 则把工具定义和系统指令插入 input 前缀:

text 复制代码
input = [AdditionalTools] + [系统指令] + [会话历史]

这里做的是协议形态适配,不是重新编译 Session;请求数据仍然来自上一层的 Prompt。

ModelClientSession::stream() 做什么

client.rs:2016 的 stream() 根据 Provider 的 WireApi 选择传输方式:

  1. Responses API 且 WebSocket 可用:尝试复用或建立 WebSocket。
  2. WebSocket 未启用,或返回明确的 FallbackToHttp:走 HTTP Responses API;并非所有 WebSocket 错误都立即回退,其他错误会向上传递,由上层重试逻辑处理。
  3. HTTP 路径建立请求、认证、兼容性 Header 和遥测信息,再发送流式请求。

对应代码:

  • client.rs:1563:构建 Responses API 请求、Header、Session/Thread 标识和遥测信息。
  • client.rs:1720:WebSocket 流路径。
  • client.rs:2016:传输选择与 HTTP fallback。

这层的职责是统一传输和 Provider 适配,不负责 Agent Loop 的继续/结束判断。

HTTP 路径:JSON 请求进入 SSE 流

HTTP Responses 路径会把 ResponsesApiRequest 编码成 JSON,并设置 SSE 接受类型:

rust 复制代码
let body = EncodedJsonBody::encode(&request)?;

req.headers.insert(
    http::header::ACCEPT,
    HeaderValue::from_static(
        "text/event-stream",
    ),
);

let stream_response = self
    .session
    .stream_encoded_json_with(
        Method::POST,
        self.endpoint.path(),
        extra_headers,
        Some(body),
        ...,
    )
    .await?;

源码:responses.rs

请求头还会携带 Session/Thread 标识、认证信息、路由提示和压缩配置。响应建立后进入 spawn_response_stream()。

SSE 不是最终协议对象

HTTP 返回的是 SSE 数据帧。Codex 的 SSE 解析器负责:

text 复制代码
HTTP byte stream
  → SSE data frame
  → 字符串数据
  → ResponseEvent

解析器先从字节流读取 SSE 帧,再把 event.data 反序列化为 Responses 事件,最后按事件类型转换:

rust 复制代码
match event.kind.as_str() {
    "response.output_text.delta" => {
        Ok(Some(ResponseEvent::OutputTextDelta(
            event.delta.unwrap_or_default(),
        )))
    }

    "response.output_item.done" => {
        let item = serde_json::from_value::<ResponseItem>(
            event.item.unwrap(),
        )?;
        Ok(Some(ResponseEvent::OutputItemDone(item)))
    }

    "response.custom_tool_call_input.delta" => {
        Ok(Some(ResponseEvent::ToolCallInputDelta {
            item_id,
            call_id,
            delta,
        }))
    }

    "response.completed" => {
        Ok(Some(ResponseEvent::Completed {
            response_id,
            token_usage,
            usage_metadata,
            end_turn,
        }))
    }
}

对应源码:process_responses_event()。网络层输出的不是原始 JSON,而是统一的 ResponseStream<Item = Result<ResponseEvent, ApiError>>。WebSocket 路径也返回相同的 ResponseStream,上层不需要区分底层传输方式。

空闲超时、连接错误、流提前关闭都会作为流错误返回,而不是伪装成正常完成。

ResponseEvent 回到 Core 后做什么

session/turn.rs:2295 开始逐条读取 ResponseEvent,主要类型包括:

事件 Core 的处理
OutputTextDelta 追加模型文本,发布文本增量事件
ReasoningSummaryDelta 更新推理摘要流
ToolCallInputDelta 累积工具参数,形成工具调用
OutputItemDone 收口一个完整输出项,写入当前 Turn
Completed 记录 token 使用量,判断是否需要 follow-up

模型的原始流式事件不会直接交给 UI。Core 将事件归属到当前输出项或工具调用:文本增量可以直接转为 EventMsg 发给 App Server,完成项与工具结果另行写入 Session 历史。写历史与发事件不具有统一的先后顺序。

核心边界

text 复制代码
模型网络层:构造请求、选择传输、解析流、统一事件
Core 编排层:根据事件更新状态、执行工具、判断下一步
App Server:把 Core 事件转换给 TUI / IDE / Desktop

所以"模型返回工具调用"并不意味着网络层直接执行工具。网络层只返回 ResponseEvent::ToolCallInputDelta 或完整工具调用,真正的工具分派仍由 Core 的 Agent Loop 完成。

这一段的完整数据流

text 复制代码
Prompt
  ↓ build_responses_request()
ResponsesApiRequest
  ↓ JSON 编码 + Header
HTTP POST /responses 或 WebSocket
  ↓
SSE / WebSocket 原始事件
  ↓ JSON 解析
ResponsesStreamEvent
  ↓ 事件类型映射
ResponseEvent
  ↓
run_sampling_request()
  ├─ 文本增量 → 当前 assistant message
  ├─ tool-call → 工具 Future 入队
  ├─ reasoning → 推理状态
  └─ completed → 返回 Loop 做继续/停止判断

源码基线:OpenAI Codex d58d0e5841e0de08e251673db2d5af8cf3a1ad51。文中的流程图用于标出本篇所处的运行阶段。

相关推荐
farerboy1 小时前
WEB 项目如何禁用 F12 等功能
前端·vue.js·架构
美好世界1 小时前
Codex 源码导读:第三部分——沙箱真实执行
架构
mldong1 小时前
引擎里没有 setStatus:状态迁移收口,不用状态机框架
后端·架构
她的男孩1 小时前
开放接口限流从 20 改到 200 还是每分钟 20 次:拆完防重放+幂等+限流,我找到 5 个静默失效的坑
java·后端·架构
据说幸运很容易1 小时前
接口测试框架重构:YAML 用例驱动 + 分层设计
后端·架构
mldong1 小时前
聚合边界:为什么 ProcessTask 没有自己的 Repository
后端·架构
Thneonl1 小时前
一行 SET NX PX 的分布式锁,四个坑一个比一个贵
后端·架构
Thneonl1 小时前
99.9% 置信被拒收,76% 靠两个佐证进场
人工智能·架构
Thneonl1 小时前
stateless 4/6 漏判,stateful 3/5 错杀:该信谁?
人工智能·架构