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
#[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
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
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
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_retries | to_api_provider → RetryConfig.max_attempts | 默认 4,上限 100 | codex-client 的 unary/transport retry policy |
stream_max_retries | core 的 turn、compact、remote compact | 默认 5,上限 100 | 流断开后的重新连接/重试循环 |
stream_idle_timeout_ms | ApiProvider.stream_idle_timeout | 默认 300000 ms | 流事件之间的 idle timeout |
websocket_connect_timeout_ms | ModelClient::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
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 中执行:
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 配置,这是跨进程边界的重要限制。
