Skip to content

模型子系统总览

追踪 Codex 的 provider、模型目录、客户端和回合状态如何协作。

基于rust-v0.150.0
CodexRustModelArchitecture

模型子系统总览 ​

本文回答一个具体问题:Codex 从配置中的 provider 到一次 turn 的模型请求,中间哪些对象拥有配置、能力、目录和传输状态?本文不展开 Responses wire item、SSE 事件字段或具体认证协议,它们分别由后续文章负责。

读者需要能阅读 Rust trait、Arc、异步函数和基本 HTTP 概念。建议先带着四个问题阅读:谁决定“连到哪里”,谁决定“模型能做什么”,谁决定“当前可选模型”,谁保存一次 turn 内的状态?如果你刚读完 模型上下文体系总览,可以把本文的 TurnContext 看成上一系列输出的消费者;文末用源码测试和一个可执行检查把这四个问题闭合。

1. 边界地图 ​

模型子系统不是一个巨型 client,而是四层对象的组合。配置元数据进入 provider;provider 暴露能力和认证适配;manager 维护模型目录;core client 把稳定会话状态和本回合参数送入 API。

图中箭头是数据依赖,不表示每层都直接调用下一层。TurnContext 消费的是已经解析好的 ModelInfo;它不会从 TOML 重新解析 provider,也不会负责模型目录刷新。

源码位置:codex-rs/model-provider-info/src/lib.rs :: ModelProviderInfo

rust
pub struct ModelProviderInfo {
    pub name: String,
    pub base_url: Option<String>,
    pub env_key: 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 supports_websockets: bool,
}

这里的字段描述“如何请求 provider”,不是“模型能力”。例如 supports_websockets 只表示传输上限;模型是否支持图像、并行工具或搜索,要看模型目录和 provider capability 的交集。

2. Provider对象 ​

create_model_provider 是配置进入运行时抽象的分界点。当前版本只在 Amazon Bedrock 时选择专用实现,其余配置进入 ConfiguredModelProvider。这意味着“OpenAI-compatible”是默认实现路径,不等于所有 provider 拥有相同认证或功能。

ModelProvider 的消费者包括 core client、模型目录 endpoint 和账户状态展示。其 capabilities() 是 provider-owned upper bound:调用方可以通过配置关闭能力,但不能把 provider 标成不支持后又无条件暴露给上层。

源码位置:codex-rs/model-provider/src/provider.rs :: create_model_provider

rust
pub fn create_model_provider(
    provider_info: ModelProviderInfo,
    auth_manager: Option<Arc<AuthManager>>,
) -> SharedModelProvider {
    if provider_info.is_amazon_bedrock() {
        Arc::new(AmazonBedrockModelProvider::new(provider_info, auth_manager))
    } else {
        Arc::new(ConfiguredModelProvider::new(provider_info, auth_manager))
    }
}

失败边界也在这里开始:ModelProviderInfo::validate 会拒绝 AWS 与 bearer/auth 配置冲突、空 command,以及已移除的 wire_api = "chat"。这些是配置错误,不应被误写成网络失败。

3. 能力对象 ​

模型元数据和 provider 能力解决不同问题。ModelInfo 记录某个 slug 的上下文窗口、reasoning、工具、输入模态和 service tier;ProviderCapabilities 描述当前后端能提供的上限。实际可用能力是二者以及配置开关的交集。

源码位置:codex-rs/model-provider/src/provider.rs :: ProviderCapabilities

rust
pub struct ProviderCapabilities {
    pub namespace_tools: bool,
    pub image_generation: bool,
    pub web_search: bool,
    pub external_web_access: bool,
    pub remote_compaction: RemoteCompactionSupport,
}

源码位置:codex-rs/core/src/session/turn_context.rs :: TurnContext

rust
pub struct TurnContext {
    pub(crate) model_info: Arc<ModelInfo>,
    pub(crate) provider: SharedModelProvider,
    pub(crate) reasoning_effort: Option<ReasoningEffortConfig>,
    pub(crate) available_models: Vec<ModelPreset>,
    pub(crate) final_output_json_schema: Option<Value>,
}

TurnContext 保存的是本回合稳定快照。模型切换或新的会话配置应重新构造 turn context,而不是在请求中偷偷替换 model_info。

4. 目录生命周期 ​

ModelsManager 的 owner 是目录快照、刷新策略和派生 preset;ModelsEndpointClient owner 是 provider-specific 的认证与传输。manager 只在策略允许时调用 endpoint。

源码位置:codex-rs/models-manager/src/manager.rs :: RefreshStrategy

rust
pub enum RefreshStrategy {
    Online,
    Offline,
    OnlineIfUncached,
}

OnlineIfUncached 的“失败恢复”不是清空模型列表,而是 raw_model_catalog 记录刷新错误后返回当前内存目录。这个设计让已有会话继续工作,但不能证明远端目录是最新的。另一个重要边界在 try_load_cache 的 TODO:缓存资格当前按版本和 TTL 判断,尚未把 provider identity 纳入资格,因此切换 provider 时要谨慎解释缓存结果。

5. 会话与回合 ​

ModelClient 跨 turn 持有 provider、thread id、认证环境遥测和 HTTP factory;ModelClientSession 每 turn 新建,保存 WebsocketSession 和 x-codex-turn-state。上一次请求、上一次 response receiver 和连接复用标记封装在 WebsocketSession 内。这两个生命周期不能互换。

注意:上图中的 Core 是参与者标签,不是源码类型;真正的入口是 ModelClient::new 和 ModelClient::new_session。复用 session 会把上一 turn 的 sticky token 带入下一 turn,源码注释明确把它定义为协议错误。

源码位置:codex-rs/core/src/client.rs :: ModelClient::new_session

rust
pub fn new_session(&self) -> ModelClientSession {
    ModelClientSession {
        client: self.clone(),
        websocket_session: self.take_cached_websocket_session(),
        turn_state: Arc::new(OnceLock::new()),
    }
}

provider-scoped unauthorized recovery 也属于运行时 provider,而不是通用 HTTP client。默认实现只把 HTTP 401 视作可恢复认证错误,具体 provider 可以扩展错误形状并先执行自己的 recovery;只有恢复成功或 auth manager 刷新后,请求层才决定是否重试。

源码位置:codex-rs/model-provider/src/provider.rs :: ModelProvider

rust
fn is_recoverable_auth_error(&self, error: &TransportError) -> bool {
    matches!(error, TransportError::Http { status, .. }
        if *status == http::StatusCode::UNAUTHORIZED)
}

fn recover_from_unauthorized(
    &self,
) -> ModelProviderFuture<'_, Result<ProviderUnauthorizedRecovery>> {
    Box::pin(async { Ok(ProviderUnauthorizedRecovery::NotConfigured) })
}

6. 请求入口 ​

一次请求的参数来自两个 owner:TurnContext 提供模型和 reasoning 等回合设置,ModelClientSession 提供连接与 sticky state。Prompt 则承载 input、tools、instructions 和 output schema;它不是目录,也不拥有 provider。

取消路径由 ResponseStream 的 Drop 触发 consumer_dropped.cancel(),通知 mapper 停止继续消费 provider stream。它解决的是消费者提前停止轮询,不等同于保证远端请求立即终止;文章只据此证明本地取消信号的传播边界。

7. 失败边界 ​

把错误按 owner 分类,调试时才能定位正确层:配置验证失败在 provider-info,目录刷新失败在 manager,认证映射失败在 provider,连接/流失败在 client transport,模型业务拒绝则来自 API 响应。

恢复也有层次:manager 刷新失败保留旧目录;client 的 WebSocket 失败可切 HTTP,并把 fallback 状态保存在 session 级 client state;turn session 被丢弃则其取消 token 和 turn state 一并失效。不要把这些恢复机制概括成“自动重试一切”。

8. 子系统测试 ​

阅读时要从测试确认 owner,而不是只看类型。建议先看 models-manager/src/manager_tests.rs 的刷新策略与缓存 fixture,再看 core 的 client 测试,最后用 session suite 检查模型切换对后续请求的影响。每类测试的覆盖范围不同:manager 测试不覆盖真实 SSE,client 测试不覆盖远端 catalog 的新鲜度。

源码位置:codex-rs/models-manager/src/manager_tests.rs :: injected_cache_hit_avoids_remote_fetch

rust
let catalog = manager
    .raw_model_catalog(
        RefreshStrategy::OnlineIfUncached,
        DEFAULT_HTTP_CLIENT_FACTORY,
    )
    .await;

assert_eq!(catalog.models, cached_models);
assert_eq!(endpoint.fetch_count(), 0);

输入是可控的缓存与 endpoint fixture,断言是目录是否读取及是否触发刷新;它证明策略分支,不证明真实 provider 返回内容。可执行验证应记录实际测试数量,不能把 0 tests 当作通过。

9. 请求追踪 ​

在本版本源码对应的 codex-rs workspace 中执行:

bash
cd codex-rs
RUST_MIN_STACK=16777216 cargo test -p codex-models-manager manager_tests
RUST_MIN_STACK=16777216 cargo test -p codex-core client --lib

练习:给 ModelsEndpointClient::list_models 的 fixture 增加一次失败,观察 manager 是否保留旧快照;再检查 ModelClient::new_session 的 turn_state 是否为新对象。你的结论应分别写成“缓存恢复已证明”和“跨 turn sticky state 未复用已证明”,不要扩大为“所有网络错误都能恢复”。

10. 架构边界 ​

阅读模型源码时先按生命周期分层:provider-info 是配置值,provider 是运行时适配,manager 是目录快照,ModelClient 是会话状态,ModelClientSession 是 turn 状态,TurnContext 是本回合消费视图。后续文章将沿这条边界分别展开字段、目录、请求和流式响应;若某个字段跨越两层,先问它的 owner、消费者和生效时机,再追调用点。