English 书架

Provider Boundary:Transport 会变,Event 保持稳定

阅读契约: 把本章当作 model provider 与 Codex runtime 之间的源码地图。 重点跟住 provider data、runtime provider behavior、turn-local transport state,以及旁路运行的 runtime path。读完后,你应该能解释为什么 HTTP streaming、 Responses-over-WebSocket、local provider、Bedrock signing、model catalog、 realtime session 和 backend task 不该合并成同一个“模型调用”。

Provider data 进入 runtime provider,runtime provider 负责 auth、capabilities、model manager、typed API 和 turn loop 边界
Provider configuration 一开始只是数据;turn loop 真正接触的是 runtime provider,它能解析 auth、capabilities、model metadata 和 typed API 行为。

源码边界: 本章直接源码判断固定在 OpenAI Codex commit 569ff6a1c400bd514ff79f5f1050a684dc3afde3ModelProviderInfoModelProviderModelClientSessionResponseEventModelsManager、Bedrock auth、Ollama readiness、realtime conversation startup 和 CloudBackend 以正文里的固定源码锚点为 verified source。 “为什么 runtime 要把 transport mechanics 压到同一套事件词汇之下”这类判断, 是从可见类型与调用点得出的 surrounding contract inference,不是对 provider 私有内部的断言。

本文会反复使用四个局部术语。Provider data 指可序列化的 ModelProviderInfo:URL、auth fields、retry policy、headers、wire API 和 WebSocket support。Runtime providerModelProvider trait object:它在 运行时解析 account state、auth、capabilities、API-provider shape 和 model manager,并把这些数据变成当前进程可执行的行为。Typed event vocabulary 指 transport 解析之后交给 turn loop 的 ResponseEventAdjacent runtime path 指可能共享 credential 或 base URL、但生命周期不同的路径,例如 realtime media/session setup 或 cloud backend tasks。

第 6 章把一次 turn 追到 sampling 边界。本章打开这个边界。真正的压力很直接: turn loop 只想消费一条稳定的 runtime 事实流,但 provider 的差异会落在 auth、 signing、transport、catalog、locality 和 cloud workflow 上。Codex 的做法不是让 turn loop 学会所有方言,而是在 scheduler policy 继续运行之前,先把这些差异收束到 一个较小的 runtime contract。

问题:provider 差异会落到 URL、credential、request signing、stream framing、retry、model visibility、local readiness 和 cloud task 上,但 turn loop 不能理解每一种 provider dialect。

主张:Codex 会先把 provider-specific mechanics 转成 runtime provider behavior、typed API request 和 normalized response events,然后才让 turn scheduler 继续做 policy decision。

心智模型:provider data 描述通信的可能形态;runtime provider behavior 决定当前 session 怎样通信;turn loop 接收 typed events 和 adjacent task results。

导读问题:什么只是配置?什么是运行时行为?当前 turn 激活了哪种 transport?哪些路径根本不是 model inference?

一、Provider Data 不是 Runtime Behavior

1.1 ModelProviderInfo 是可序列化契约

数据层在 codex-rs/model-provider-info/src/lib.rs 里非常明确。ModelProviderInfo 是可序列化的 provider definition。它可以保存 base URL、API key 环境变量、不推荐但可用的 literal bearer token、command-backed auth、AWS SigV4 auth、wire API、query parameters、静态或环境变量驱动的 headers、 retry settings、stream idle timeout、WebSocket connect timeout、是否需要 OpenAI auth,以及是否支持 Responses-over-WebSocket。下面是为本章论证裁剪过的 excerpt, 只保留 provider boundary 需要看的字段:

pub struct ModelProviderInfo {
    pub name: String,
    pub base_url: Option<String>,
    pub env_key: Option<String>,
    // ...
    pub experimental_bearer_token: Option<String>,
    pub auth: Option<ModelProviderAuthInfo>,
    pub aws: Option<ModelProviderAwsAuthInfo>,
    pub wire_api: WireApi,
    pub query_params: Option<HashMap<String, String>>,
    pub http_headers: Option<HashMap<String, String>>,
    pub env_http_headers: Option<HashMap<String, String>>,
    pub request_max_retries: Option<u64>,
    pub stream_max_retries: Option<u64>,
    pub stream_idle_timeout_ms: Option<u64>,
    pub websocket_connect_timeout_ms: Option<u64>,
    pub requires_openai_auth: bool,
    pub supports_websockets: bool,
}

这个结构是描述性的。它说明什么可以配置;它自己不会获取 account state、签名 request、检查本地 server,也不会选择 model catalog。同一个文件还校验不兼容 组合。例如 AWS 分支会拒绝 supports_websockets,因为这个快照还没有实现 WebSocket upgrade request 的 SigV4 signing (lib.rs#L147-L170)。

第一条 invariant 是:provider config 可以描述有风险或特殊的形状,但 runtime 必须在 任何 turn 依赖它之前先把形状 gate 住。

1.2 ModelProvider 在请求时拥有行为

运行时行为进入 ModelProvider trait。这个 trait 仍然暴露 metadata,但也拥有 capability upper bounds、attestation support、auth-manager access、current auth、account state、API-provider conversion、 runtime base URL、request auth provider 和 model manager construction。

pub trait ModelProvider: fmt::Debug + Send + Sync {
    fn info(&self) -> &ModelProviderInfo;
    fn capabilities(&self) -> ProviderCapabilities;
    fn supports_attestation(&self) -> bool;
    fn auth_manager(&self) -> Option<Arc<AuthManager>>;
    async fn auth(&self) -> Option<CodexAuth>;
    fn account_state(&self) -> ProviderAccountResult;
    async fn api_provider(&self) -> Result<Provider>;
    async fn runtime_base_url(&self) -> Result<Option<String>>;
    async fn api_auth(&self) -> Result<SharedAuthProvider>;
    fn models_manager(&self, codex_home: PathBuf, catalog: Option<ModelsResponse>)
        -> SharedModelsManager;
}

工厂函数 create_model_provider 展示了这个拆分为什么必要:大多数 provider data 会变成 ConfiguredModelProvider, 但 Amazon Bedrock 会变成专门的 AmazonBedrockModelProvider。这不是形式上的分支。 Bedrock 需要 AWS account state、runtime base URL、body-aware signing 和 static model manager,所以 Codex 不应该只因为 request surface 看起来像 OpenAI-compatible endpoint,就把它当作普通 bearer-token endpoint。

二、Transport choice 在 event 进入 turn loop 前结束

HTTP SSE 和 WebSocket stream 进入 stream mapper,并输出 ResponseEvent 给 turn loop
HTTP SSE 与 Responses-over-WebSocket 的机制不同,但它们会在 turn loop 做 policy decision 之前收敛成 ResponseEvent

2.1 Turn loop 消费的是 ResponseEvent

稳定的事件词汇在 codex-rs/codex-api/src/common.rs。 Response stream 可以报告 creation、output items、server model metadata、verification requirements、past reasoning 是否已经由 server 计入、completion、text delta、tool-call argument delta、reasoning delta、rate limits 和 models ETag。下面的 excerpt 经过裁剪, 但保留了本节依赖的事件类别:

pub enum ResponseEvent {
    Created,
    OutputItemDone(ResponseItem),
    OutputItemAdded(ResponseItem),
    ServerModel(String),
    ModelVerifications(Vec<ModelVerification>),
    ServerReasoningIncluded(bool),
    Completed { response_id: String, token_usage: Option<TokenUsage>, end_turn: Option<bool> },
    OutputTextDelta(String),
    ToolCallInputDelta { item_id: String, call_id: Option<String>, delta: String },
    ReasoningSummaryDelta { delta: String, summary_index: i64 },
    ReasoningContentDelta { delta: String, content_index: i64 },
    ReasoningSummaryPartAdded { summary_index: i64 },
    RateLimits(RateLimitSnapshot),
    ModelsEtag(String),
}

这个 enum 是边界:provider transport 不能以 raw SSE line、WebSocket frame 或 provider-specific chunk 的形态泄露到上层。一旦 event 跨过这个边界,turn loop 就能决定 是否渲染进度、记录 completed item、更新 rate limits、分发 tool call、继续 sampling 或结束 turn。

2.2 HTTP 和 WebSocket 共享解析边界

HTTP 路径会构造 Responses request,请求 text/event-stream,并在 endpoint/responses.rs 里生成 response stream。SSE parsing 会通过 process_responses_eventprocess_sse 映射 event kinds。

WebSocket 路径会创建 ResponsesWebsocketConnection, 用合并后的 provider headers 和 auth 建连 (responses_websocket.rs#L340-L452), 然后运行 WebSocket response stream (responses_websocket.rs#L574-L685)。 这个 WebSocket loop 也会把 response events 送进同一套 Responses event processing 边界。

所以重点不是“WebSocket 更好”。重点是:只有当两种 transport 最终变成同一条 typed stream,Codex 才能安全切换 transport path。

2.3 ModelClientSession 让 transport state 属于当前 turn

ModelClient 是 session-scoped,而 ModelClientSession 是 turn-scoped。源码注释写得很清楚:ModelClientSession 会懒加载 Responses WebSocket,在同一 turn 内复用连接,记住上一份 full request 以支持 incremental WebSocket payload,并保存 x-codex-turn-state token。它不能跨 turn 复用。

Transport selection 又把这个 ownership 落到代码里。只有当 provider 支持 WebSocket、session fallback 没有禁用 WebSocket、并且 SSE fixture 不活跃时,model client 才会启用 Responses-over-WebSocket (client.rs#L767-L779)。

pub fn responses_websocket_enabled(&self) -> bool {
    if !self.state.provider.info().supports_websockets
        || self.state.disable_websockets.load(Ordering::Relaxed)
        || (*CODEX_RS_SSE_FIXTURE).is_some()
    {
        return false;
    }
    true
}

每个 turn 调用的 stream 会优先选择 Responses WebSocket;如果 WebSocket 路径返回 FallbackToHttp,它会切换 到 HTTP,再走 HTTP Responses API。try_switch_fallback_transport (client.rs#L1614-L1630) 随后会强制当前 Codex session 的后续请求使用 HTTP,并重置 turn-local WebSocket state。

第二条 invariant 是:transport optimization 不能变成隐藏的 global state,不能悄悄改变 后续 turn 的含义。

三、Model Metadata 是 Runtime Infrastructure

Bundled model catalog、models cache、remote models endpoint、model manager、visible presets 和 model info 输入 turn loop
Model metadata 是 cache-plus-overlay infrastructure:turn loop 需要 model facts,而不是硬编码的 picker list。

3.1 Model manager 产出 runtime facts

Model manager 不只是 UI picker 支撑。 ModelsManager trait 会列出 available models、返回 raw catalog、暴露 cached remote models、按 auth mode 和 visibility 过滤 picker presets、选择 default model、解析 ModelInfo,并在 ETag 变化时刷新。

async fn list_models(&self, refresh_strategy: RefreshStrategy) -> Vec<ModelPreset> {
    let catalog = self.raw_model_catalog(refresh_strategy).await;
    self.build_available_models(catalog.models)
}

fn build_available_models(&self, mut remote_models: Vec<ModelInfo>) -> Vec<ModelPreset> {
    remote_models.sort_by(|a, b| a.priority.cmp(&b.priority));
    let mut presets: Vec<ModelPreset> = remote_models.into_iter().map(Into::into).collect();
    let uses_codex_backend = self.auth_manager().is_some_and(
        AuthManager::current_auth_uses_codex_backend,
    );
    presets = ModelPreset::filter_by_auth(presets, uses_codex_backend);
    ModelPreset::mark_default_by_picker_visibility(&mut presets);
    presets
}

这些事实不只影响下拉框。ModelInfo 会影响 context limit、auto-compaction threshold、 reasoning controls、model visibility,以及当前 auth mode 下合适的默认值。第 6 章已经 展示过 turn loop 会在 compaction 和 sampling 之前读取 model info;本章补上这些 info 从哪里来。

3.2 Catalog 有 baseline、cache 和 ETag 边界

OpenAI model manager 可以组合 bundled catalog、可选 config catalog、disk cache 和 remote /models endpoint。刷新路径在 manager.rs#L225-L359 附近。Cache helper 在 cache.rs 里管理路径、TTL、freshness 和 persist。Provider endpoint 通过有界 timeout 拉取 remote catalog (models_endpoint.rs#L31-L110), API endpoint 在调用 models 时附带 client_version (endpoint/models.rs#L31-L73)。

这是一个运行时折中:bundled catalog 让 Codex 能启动并离线工作;cache 避免每个 turn 都付网络成本;ETag 让 stream 可以通知 runtime model metadata 已变化;remote refresh 让 backend 不发新版二进制也能更新 model presets。代价是:解释“model list”时, provider identity、auth mode、visibility、cache freshness 和 remote catalog state 都不能被忽略。

四、Auth 在 Request 存在之后才应用

Provider auth modes、prepared request、body signing、signed request、provider API 和 no WebSocket boundary
有些 auth mode 可以较早附加 header;Bedrock 风格 SigV4 必须签 prepared body,所以 auth boundary 位于 request construction 之后、transport send 之前。

4.1 Simple token 和 command auth 在发送前解析

通用 auth 路径通过 auth_manager_for_providerresolve_provider_auth 路由。它可以创建 bearer-token auth provider,也可以为 local/test case 创建 unauthenticated provider。重点不是 credential 的具体形态,而是 request sender 拿到 SharedAuthProvider 后,不需要知道 credential 来自 ChatGPT auth、API key、command、local provider,还是根本没有 auth。

4.2 Bedrock 说明为什么 request body 很关键

Amazon Bedrock 是让这个边界变明显的例子。它的 provider implementation 声明 provider account state,在 capability upper bounds 里关闭 namespace tools、image generation 和 web search,计算 runtime base URL,解析 AWS auth,并使用 static model manager (amazon_bedrock/mod.rs#L51-L103)。

SigV4 auth provider 随后会修改 prepared request:

async fn apply_auth(&self, request: Request) -> Result<Request, AuthError> {
    let mut request = request;
    remove_headers_not_preserved_by_bedrock_mantle(&mut request.headers);
    let prepared = request.prepare_body_for_send().map_err(AuthError::Build)?;
    let signed = self.context.sign(AwsRequestToSign {
        method: request.method.clone(),
        url: request.url.clone(),
        headers: prepared.headers.clone(),
        body: prepared.body_bytes(),
    }).await?;

    request.url = signed.url;
    request.headers = signed.headers;
    request.body = prepared.body.map(RequestBody::Raw);
    request.compression = RequestCompression::None;
    Ok(request)
}

完整代码在 amazon_bedrock/auth.rs#L88-L139。 两个细节很重要。第一,签名覆盖 method、URL、headers 和 body,所以 auth 不能在 request 构造前就最终确定。第二,代码在 prepare body 之后把 RequestCompression::None 写回去, 因为被签名的 bytes 必须就是发送出去的 bytes。

这解释了为什么这个源码快照会拒绝 AWS 加 supports_websockets。这不是在说 Bedrock 永远不能支持 WebSocket,而是一个 verified source boundary:当前实现还没有 WebSocket upgrade request 的 SigV4 signing。

五、Local Provider 仍然适合同一个 Contract

5.1 Ollama 增加 readiness work,不增加新的 agent loop

Local provider 的失败形态不同:可能没有本地 server,可能缺 model,可能本地 API 版本不兼容。 Ollama 路径把这些 operational work 编进 ensure_oss_readyensure_responses_supported。 它会检查本地 Ollama server 是否可达,拉取本地 models,缺默认 model 时 pull,并拒绝低于 Responses API 最低要求的版本。

这里的不变量不变:local readiness 是 provider work。一旦 model call 开始,runtime 想要的仍然是 typed response events,而不是 Ollama-specific agent policy。

5.2 LM Studio 是另一种 provider surface,不是另一套 runtime

LM Studio 有自己的 client checks:provider construction、server availability、model loading、 model listing,以及通过 lms 下载 model,都在 codex-rs/lmstudio/src/client.rs。 这些检查与 Ollama 的 operational work 不同,但架构位置相同。它们为 model client 准备 provider surface;它们不重写 turn loop 处理 response item、tool、history 或 continuation 的方式。

六、Backend Tasks 与 Realtime 是相邻 Runtime Paths

Turn sampling 的 ResponseEvent 进入 runtime boundary,旁路分出 realtime sideband、backend tasks、task lifecycle 和 apply diff
Realtime session 和 backend task 可以位于 model sampling 旁边,但它们携带不同生命周期,不应该被当作普通 provider transport。

6.1 Cloud backend task 有 task semantics

Cloud task client 暴露的是 task lifecycle,不是 model stream。 CloudBackend trait 可以列 task、取 summary、取 diff 和 messages、读取创建 prompt 及 assistant messages、 列 sibling attempts、dry-run apply、apply task diff,以及创建 task。下面的 excerpt 只保留足以区分 task lifecycle 和 response stream 的方法:

pub trait CloudBackend: Send + Sync {
    async fn list_tasks(&self, env: Option<&str>, limit: Option<i64>, cursor: Option<&str>)
        -> Result<TaskListPage>;
    async fn get_task_summary(&self, id: TaskId) -> Result<TaskSummary>;
    async fn get_task_diff(&self, id: TaskId) -> Result<Option<String>>;
    async fn get_task_messages(&self, id: TaskId) -> Result<Vec<String>>;
    async fn get_task_text(&self, id: TaskId) -> Result<TaskText>;
    async fn list_sibling_attempts(&self, task: TaskId, turn_id: String)
        -> Result<Vec<TurnAttempt>>;
    async fn apply_task_preflight(&self, id: TaskId, diff_override: Option<String>)
        -> Result<ApplyOutcome>;
    async fn apply_task(&self, id: TaskId, diff_override: Option<String>)
        -> Result<ApplyOutcome>;
    async fn create_task(&self, env_id: &str, prompt: &str, git_ref: &str,
        qa_mode: bool, best_of_n: usize) -> Result<CreatedTask>;
}

HTTP implementation 会在 cloud-tasks-client/src/http.rs 里单独构造 task URLs 和 payloads。Backend tasks 可能与 backend client 共享 auth 和 base URL 关注点 (backend-client/src/client.rs#L143-L225), 但它们的生命周期是 task state、attempts、diffs 和 apply outcomes。Task diff 不是 ResponseEvent

6.2 Realtime 有 media plane 和 sideband

Realtime 的相邻性来自另一种原因。它在 realtime_conversation.rs 里的 startup path 会创建有界 audio、text、handoff 和 event channels,构造 RealtimeWebsocketClient,然后选择 WebRTC call 加 sideband input task,或者直接建立 realtime WebSocket connection。Startup context 另外由 realtime_context.rs 组装,WebRTC call creation 在 endpoint/realtime_call.rs

这不意味着 realtime 是普通 Responses stream 的另一个版本。它有 media input、session configuration、handoff output、sideband headers 和 event fanout。Runtime 可以把它接回同一套产品体验, 但源码把生命周期分开了。

应用到实践

  1. Provider definition 用数据保存,但 account state、auth、capabilities 和 model-manager choice 要留在 runtime provider behavior。
  2. HTTP streaming 和 Responses-over-WebSocket 可以在解析边界以下不同,最终必须收敛到 ResponseEvent
  3. ModelClientSession 是 turn-local transport state,不要跨 turn 复用 sticky routing token。
  4. Model catalog 是 runtime infrastructure:baseline、cache、remote overlay、visibility filtering 和 ETag refresh 都属于这条边界。
  5. Body-aware auth 要在 request construction 之后应用;local provider readiness、realtime setup 和 cloud task APIs 也应贴近 inference,而不是塞进核心 model-stream abstraction。

小结

Provider boundary 让第 6 章的 turn scheduler 能专心处理 agent work。一次 turn 可以请求模型 stream,而不必理解 AWS signing、Ollama pull、LM Studio download、/models cache refresh、 WebSocket upgrade state、realtime sideband 或 cloud task apply semantics。这些细节仍然存在; 只是由能验证、能归一化它们的层来拥有。

第 8 章会从 sampling 转向 evidence:rollout persistence、trace bundle、reducer、analytics、 OTEL span 和 debug context 如何让 provider 差异被归一化之后的 runtime 变得可观察。

源码地图

概念源码锚点
Provider data shapecodex-rs/model-provider-info/src/lib.rs
AWS/WebSocket validation boundaryModelProviderInfo::validate
Runtime provider traitcodex-rs/model-provider/src/provider.rs
Provider factorycreate_model_provider
Response event vocabularyResponseEvent
HTTP Responses streamendpoint/responses.rs
WebSocket Responses streamendpoint/responses_websocket.rs
Turn-scoped model client sessionModelClientSession
Transport selection and fallbackModelClientSession::stream
Model manager contractModelsManager
Model cachemodels-manager/src/cache.rs
Bedrock runtime provideramazon_bedrock/mod.rs
Bedrock body signingamazon_bedrock/auth.rs
Ollama readiness and version gatecodex-rs/ollama/src/lib.rs
LM Studio local client checkscodex-rs/lmstudio/src/client.rs
Cloud task lifecycleCloudBackend
Realtime startuprealtime_conversation.rs