Skip to content

Provider配置字段

追踪 ModelProviderInfo 字段的解析、校验、转换和实际消费者。

基于rust-v0.150.0
CodexRustModelProvider

Provider配置字段 ​

本文面向已经读过 模型子系统总览 的读者,继续回答一个更窄的问题:ModelProviderInfo 的字段从 config.toml 进入运行时后,分别在哪一层生效?本文只覆盖 provider 配置对象、校验、API provider 转换、认证解析和传输开关;Responses 请求体、模型目录字段和 AWS 签名细节不在本文展开。

读者需要能阅读 Rust 的 Option、serde 反序列化、trait 方法和 Duration。读完后,你应能从一个字段反查它的 owner、失败条件和消费者,并能解释为什么 request_max_retries、stream_max_retries、websocket_connect_timeout_ms 虽然都像“重试/超时配置”,却由不同代码路径消费。配置入口在 config/src/config_toml.rs,核心定义在 model-provider-info/src/lib.rs。

1. 字段分层 ​

ModelProviderInfo 不是请求对象,而是序列化后的 provider 描述。字段可以按生效层分成五组:地址与协议、凭据来源、请求修饰、失败策略、能力开关。分层的意义是避免把“允许 WebSocket”误读成“连接一定成功”,也避免把 env_http_headers 当成已经解析出的 header 值。

图中的 D 是配置快照,V 只负责拒绝不合法组合;它不会检查 provider 的远端 URL 是否可达。真正的网络错误要等 A 生成 codex_api::Provider 后,由 transport 层报告。

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

rust
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct ModelProviderInfo {
    pub name: String,
    pub base_url: Option<String>,
    pub env_key: Option<String>,
    pub env_key_instructions: Option<String>,
    pub experimental_bearer_token: Option<RedactedString>,
    pub auth: Option<ModelProviderAuthInfo>,
    pub aws: Option<ModelProviderAwsAuthInfo>,
    pub wire_api: WireApi,
    pub query_params: Option<HashMap<String, RedactedString>>,
    pub http_headers: Option<HashMap<String, RedactedString>>,
    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,
    pub supports_standalone_web_search: bool,
}

2. 解析入口 ​

TOML 反序列化由 serde 完成,#[serde(default)] 只作用于标注的字段:wire_api 默认 responses,布尔能力默认 false,而可选字符串和数值保持 None。#[schemars(deny_unknown_fields)] 影响 schema 生成和配置契约,但未知字段是否在 TOML 入口被拒绝,还要以具体 serde 配置和测试为准,不能从 derive 名称推断全部行为。

源码位置:codex-rs/config/src/config_toml.rs :: validate_model_providers

rust
pub fn validate_model_providers(
    model_providers: &HashMap<String, ModelProviderInfo>,
) -> Result<(), String> {
    validate_reserved_model_provider_ids(model_providers)?;
    for (key, provider) in model_providers {
        if key != AMAZON_BEDROCK_PROVIDER_ID {
            if provider.aws.is_some() {
                return Err(format!(
                    "model_providers.{key}: provider aws is only supported for `amazon-bedrock`"
                ));
            }
            if provider.name.trim().is_empty() {
                return Err(format!(
                    "model_providers.{key}: provider name must not be empty"
                ));
            }
        }
        provider
            .validate()
            .map_err(|message| format!("model_providers.{key}: {message}"))?;
    }
    Ok(())
}

这里有两个 owner:config/src/config_toml.rs 负责 provider ID 的保留名和 AWS 归属,ModelProviderInfo::validate 负责单个对象内部的认证冲突。把两者合并成“字段校验”会丢掉错误发生的层次。

3. 地址协议 ​

base_url、wire_api 和 query_params 最终进入 codex_api::Provider。当前 WireApi 只有 Responses;显式写入已经移除的 chat 会在反序列化阶段返回帮助信息,而不是稍后发出一个错误的 Chat Completions 请求。

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

rust
let default_base_url = if matches!(
    auth_mode,
    Some(AuthMode::Chatgpt | AuthMode::ChatgptAuthTokens | AuthMode::Headers
        | AuthMode::AgentIdentity | AuthMode::PersonalAccessToken)
) {
    CHATGPT_CODEX_BASE_URL
} else {
    "https://api.openai.com/v1"
};
let base_url = self
    .base_url
    .clone()
    .unwrap_or_else(|| default_base_url.to_string());

因此 base_url = None 不是“没有地址”,而是等待 auth mode 决定默认地址。测试 test_personal_access_token_uses_chatgpt_codex_base_url 和 test_header_auth_uses_chatgpt_codex_base_url 反向证明了这一条件分支。

codex_api::Provider::url_for_path 会把 query map 直接拼入 URL。它不会替你 URL encode key/value;配置值包含特殊字符时,必须以源码实际接受的格式为准,不能把这个 map 当作通用 URI 构造器。

4. 认证来源 ​

认证字段表达的是“凭据从哪里来”,而不是最终的 Authorization header。env_key 读取环境变量,experimental_bearer_token 直接提供 token,auth 运行外部命令,requires_openai_auth 让 provider 走 Codex 管理的 first-party auth 路径。AWS 配置是另一条互斥路径。

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

rust
pub fn api_key(&self) -> CodexResult<Option<String>> {
    match &self.env_key {
        Some(env_key) => {
            let api_key = std::env::var(env_key)
                .ok()
                .filter(|v| !v.trim().is_empty())
                .ok_or_else(|| CodexErr::EnvVar(EnvVarError {
                    var: env_key.clone(),
                    instructions: self.env_key_instructions.clone(),
                }))?;
            Ok(Some(api_key))
        }
        None => Ok(None),
    }
}

model-provider/src/auth.rs 先调用 api_key(),再检查 experimental_bearer_token。所以环境变量存在且非空时,inline token 不会成为优先来源。这个优先级是源码事实,不应只凭配置字段排列顺序猜测。

5. 请求修饰 ​

http_headers 是静态 header 值;env_http_headers 是“header 名 → 环境变量名”的间接映射。build_header_map 只插入能成功解析为 HeaderName/HeaderValue 的条目,并跳过未设置或空白环境变量。它不会因为单个无效条目而让整个 provider 构造失败。

OpenAI 内置 provider 还会预置 version 静态 header,并把 OPENAI_ORGANIZATION、OPENAI_PROJECT 放进环境 header 映射;这说明字段既服务自定义 provider,也服务内置 provider 的默认行为。

6. 重试超时 ​

这些字段的“数字单位”和“尝试次数语义”必须跟消费者一起读:

字段转换/消费者默认或上限作用边界
request_max_retriesto_api_provider → RetryConfig.max_attempts默认 4,上限 100codex-client 的 unary/transport retry policy
stream_max_retriescore 的 turn、compact、remote compact默认 5,上限 100流断开后的重新连接/重试循环
stream_idle_timeout_msApiProvider.stream_idle_timeout默认 300000 ms流事件之间的 idle timeout
websocket_connect_timeout_msModelClient::connect_websocket默认 15000 ms单次 WebSocket 建连等待

RetryPolicy 的循环是 0..=max_attempts,因此源码中的 max_attempts 是额外重试上限加一次初始请求,而不是“总请求数”。反过来,core 的 stream_max_retries 由 turn 代码维护独立计数;把两个字段相加会得到错误的预算。

7. 能力开关 ​

supports_websockets 只参与 ModelClient::responses_websocket_enabled,并且还要通过 session 级 disable_websockets 状态;连接失败后,session 可以进入 HTTP fallback。supports_standalone_web_search 只影响 web-search extension 的 provider 可用性判断,仍受 OpenAI provider、actor authorization 和用户 web search mode 条件影响。

8. 特殊组合 ​

校验最重要的不是逐字段允许什么,而是哪些组合明确禁止:AWS 不能与 env_key、inline bearer、command auth、first-party auth 或 WebSocket 同时出现;auth.command 不能为空;非 Bedrock provider 不能携带 AWS 配置;保留的内置 provider ID 不能被普通自定义 provider 覆盖。

Amazon Bedrock 是一个有意保留的例外:内置 provider 允许通过合并逻辑修改 base_url、auth、http_headers 和 AWS profile/region,但拒绝其它非默认字段。测试 test_merge_configured_model_providers_rejects_amazon_bedrock_non_default_fields 证明了这条特殊合并边界。

9. Provider测试 ​

第一组测试验证解析和默认值:test_deserialize_azure_model_provider_toml 断言 base_url、env_key、query 参数和默认 Responses wire API;test_deserialize_provider_auth_config_defaults 断言 command auth 的 timeout、refresh interval 和 cwd 默认值。它们证明的是反序列化结果,不证明 token command 真能成功。

第二组测试验证运行时转换和失败边界:test_personal_access_token_uses_chatgpt_codex_base_url 断言 auth mode 改变默认 endpoint,test_amazon_bedrock_provider_adds_mantle_client_agent_header 断言 Bedrock 静态 header 进入 API provider,test_validate_provider_aws_rejects_websockets 断言非法组合在运行前被拒绝。它们不能证明真实网络可达、AWS 签名正确或 WebSocket 服务端接受升级。

源码位置:codex-rs/model-provider-info/src/model_provider_info_tests.rs :: test_validate_provider_aws_rejects_websockets

rust
let provider = ModelProviderInfo {
    aws: Some(ModelProviderAwsAuthInfo {
        profile: None,
        region: None,
    }),
    requires_openai_auth: false,
    supports_websockets: true,
    ..ModelProviderInfo::create_openai_provider(/*base_url*/ None)
};

assert_eq!(
    provider.validate(),
    Err("provider aws cannot be combined with supports_websockets".to_string())
);

10. 配置追踪 ​

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

bash
cd codex-rs
RUST_MIN_STACK=16777216 cargo test -p codex-model-provider-info
RUST_MIN_STACK=16777216 cargo test -p codex-core client_common --lib

读者可以做两个小实验:把 wire_api = "chat" 写入临时 TOML,观察错误是否在反序列化阶段出现;再把 stream_max_retries = 200 传入 ModelProviderInfo,调用 accessor 并确认上限被截断为 100。第一个实验验证协议拒绝点,第二个验证数值归一化;两者都不能推断 provider 的远端行为。

11. 源码导航 ​

继续阅读时,按字段消费者跳转比按文件顺序更有效:先回看 模型子系统总览 的生命周期图,再沿源码进入 models-manager/src/manager.rs、core/src/client.rs 和 config/src/thread_config/remote.rs。后两个模型专题会分别展开目录加载和客户端依赖;当前文章先把它们共同依赖的配置契约讲清楚。model_provider_from_proto 会把远程 proto 字段重新构造成 ModelProviderInfo,但不包含 AWS 配置,这是跨进程边界的重要限制。