Provider解析流程
本文承接 Provider配置字段,但不再逐字段解释 ModelProviderInfo,而是追踪一个 provider 从哪里出现、如何进入目录、怎样被选中,以及配置错误在哪个阶段暴露。本文聚焦 config、core/config 和 model-provider-info;认证执行、模型目录刷新以及 Ollama、LM Studio 的网络探测不在本文展开。
读者需要了解 TOML table、Rust HashMap 和 Option。读完后,你应能解释为什么自定义 provider 可以新增 openai-custom,却不能覆盖 openai;为什么 amazon-bedrock 又能覆盖少数字段;以及 CLI/runtime override、配置值和默认值之间的选择顺序。最重要的源码入口是 Config::load_config_with_layer_stack,而不是某个 provider 构造函数。
1. 解析主线
Provider 解析分成四个阶段:配置层先合并原始 TOML,ConfigToml 反序列化并校验用户声明,内置注册表生成默认目录,最后 core 合并目录并按 provider ID 选出一个 ModelProviderInfo 快照。
这条主线有两个容易混淆的“合并”:ConfigLayerStack 合并的是不同来源的配置值,merge_configured_model_providers 合并的是内置 provider 目录和已解析的用户 provider 表。后者不会重新读取配置文件。
源码位置:codex-rs/core/src/config/mod.rs :: Config::load_config_with_layer_stack
let config_layer_stack = load_config_layers_state(
LOCAL_FS.as_ref(),
&codex_home,
Some(cwd),
&cli_overrides,
ConfigLoadOptions {
loader_overrides,
strict_config,
cloud_config_bundle,
},
thread_config_loader,
)
.await?;
let merged_toml = config_layer_stack.effective_config();
let config_toml: ConfigToml = merged_toml.try_into()?;2. 内置目录
当前 release 注册四个内置 ID:openai、amazon-bedrock、ollama 和 lmstudio。前两者有各自构造器,后两者共用 create_oss_provider,区别主要是默认端口。内置目录不是远端模型目录,它只描述 provider 连接入口。
源码位置:codex-rs/model-provider-info/src/lib.rs :: built_in_model_providers
[
(OPENAI_PROVIDER_ID, openai_provider),
(AMAZON_BEDROCK_PROVIDER_ID, amazon_bedrock_provider),
(
OLLAMA_OSS_PROVIDER_ID,
create_oss_provider(DEFAULT_OLLAMA_PORT, WireApi::Responses),
),
(
LMSTUDIO_OSS_PROVIDER_ID,
create_oss_provider(DEFAULT_LMSTUDIO_PORT, WireApi::Responses),
),
]
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect()openai_base_url 是内置 OpenAI provider 的专用覆盖入口。core 会过滤空字符串,再把结果传给 built_in_model_providers;它不是一个新 provider ID,也不会改变 ollama 或自定义 provider。
3. OSS地址
ollama 和 lmstudio 都在创建内置目录时读取 CODEX_OSS_PORT 与 CODEX_OSS_BASE_URL。优先级是完整 base URL 优先于端口;端口解析失败或变量为空时回到调用方提供的默认端口。
源码位置:codex-rs/model-provider-info/src/lib.rs :: create_oss_provider
let default_codex_oss_base_url = format!(
"http://localhost:{codex_oss_port}/v1",
codex_oss_port = std::env::var("CODEX_OSS_PORT")
.ok()
.filter(|value| !value.trim().is_empty())
.and_then(|value| value.parse::<u16>().ok())
.unwrap_or(default_provider_port)
);
let codex_oss_base_url = std::env::var("CODEX_OSS_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty())
.unwrap_or(default_codex_oss_base_url);这两个环境变量是进程级输入,同一次 built_in_model_providers 调用会让 ollama 和 lmstudio 看到相同的全局覆盖。设置 CODEX_OSS_PORT 后,两者不会再分别保持 11434 和 1234;这是共享覆盖的直接结果。
4. 自定义目录
用户 provider 来自 [model_providers.<id>]。反序列化完成后,validate_reserved_model_provider_ids 先阻止普通用户表使用四个内置 ID;因此自定义 provider 的正常模式是新增一个不同 ID,例如 openai-custom。
源码位置:codex-rs/config/src/config_toml.rs :: RESERVED_MODEL_PROVIDER_IDS
const RESERVED_MODEL_PROVIDER_IDS: [&str; 4] = [
AMAZON_BEDROCK_PROVIDER_ID,
OPENAI_PROVIDER_ID,
OLLAMA_OSS_PROVIDER_ID,
LMSTUDIO_OSS_PROVIDER_ID,
];注意校验中的 Bedrock 例外:amazon-bedrock 虽然是保留 ID,却被排除在“禁止声明”之外。它必须先通过单 provider 校验,随后进入专门的部分覆盖逻辑。
5. 合并规则
普通自定义 provider 使用 entry(key).or_insert(provider)。这意味着合并函数本身也不会覆盖已有 ID;即使绕过前面的配置校验,已有内置值仍优先。Bedrock 则先取出允许覆盖的字段,检查剩余对象是否等于默认值,再把允许字段写回内置对象。
源码位置:codex-rs/model-provider-info/src/lib.rs :: merge_configured_model_providers
if key == AMAZON_BEDROCK_PROVIDER_ID {
let base_url_override = provider.base_url.take();
let auth_override = provider.auth.take();
let aws_override = provider.aws.take();
let http_headers_override = provider.http_headers.take();
if provider != ModelProviderInfo::default() {
return Err(format!(
"model_providers.amazon-bedrock only supports changing ..."
));
}
// 将允许字段写回内置 Bedrock provider
} else {
model_providers.entry(key).or_insert(provider);
}Bedrock header 使用 extend 合并,而不是整个替换:内置 x-amzn-mantle-client-agent 会保留,用户 header 追加到同一 map。若键相同,HashMap::extend 的后值覆盖前值;这是集合操作语义,不是独立的 header 优先级框架。
6. 选择顺序
合并后的目录只是候选集,model_provider_id 才决定实际使用谁。选择顺序非常短:运行时 override 优先,其次是有效配置中的 model_provider,两者都没有时使用 openai。显式未知 ID 不会回退到 openai,而是在 Config 构造阶段返回 NotFound。
源码位置:codex-rs/core/src/config/mod.rs :: provider selection
let model_provider_id = model_provider
.or(cfg.model_provider)
.unwrap_or_else(|| "openai".to_string());
let model_provider = model_providers
.get(&model_provider_id)
.ok_or_else(|| {
let message = if model_provider_id == LEGACY_OLLAMA_CHAT_PROVIDER_ID {
OLLAMA_CHAT_PROVIDER_REMOVED_ERROR.to_string()
} else {
format!("Model provider `{model_provider_id}` not found")
};
std::io::Error::new(std::io::ErrorKind::NotFound, message)
})?
.clone();这里没有“找不到就回退到 OpenAI”的逻辑。默认 openai 只在调用方没有选择任何 ID 时生效;一旦显式选择了不存在的 ID,配置加载失败。
7. 失效路径
Provider 解析的失败应按阶段定位:TOML 反序列化阶段拒绝保留 ID、空名称和冲突字段;目录合并阶段拒绝 Bedrock 非法覆盖;选择阶段拒绝不存在或已移除的 ID。网络可达性和认证有效性不属于这一阶段。
| 现象 | 失败阶段 | 主要入口 |
|---|---|---|
自定义 [model_providers.openai] | 反序列化校验 | validate_reserved_model_provider_ids |
自定义 provider 带 aws | 反序列化校验 | validate_model_providers |
Bedrock 修改 supports_websockets | 目录合并 | merge_configured_model_providers |
| 选择未知 ID | 最终选择 | Config::load_config_with_layer_stack |
选择 ollama-chat | 最终选择 | 专用迁移错误分支 |
解析失败发生在有效 Config 生成前,因此没有 session、turn 或网络资源需要清理。恢复方式是修正配置或 runtime override 后重新加载,而不是在当前会话中重试请求。
8. 解析测试
test_merge_configured_model_providers_adds_custom_provider 的输入是内置目录加一个全新 custom ID,断言结果等于手工向内置 map 插入该对象;它证明扩展语义,不证明自定义 endpoint 可达。
当前配置测试还覆盖 openai-custom 的选择、Bedrock transport override、OSS provider 显式覆盖和 legacy ollama-chat 迁移错误。它们分别验证新增 provider、保留 ID 例外、runtime/config 选择和移除 provider 的失败文案。
load_config_applies_amazon_bedrock_transport_overrides 构造包含 base URL、command auth 和自定义 header 的 TOML,经过完整 Config 加载后,断言 provider ID 为 amazon-bedrock,并比较合并后的整个 ModelProviderInfo。它同时覆盖了解析、合并和选择,但不执行 token command 或 AWS 请求。
源码位置:codex-rs/core/src/config/config_tests.rs :: load_config_applies_amazon_bedrock_transport_overrides
let config = Config::load_from_base_config_with_overrides(
cfg,
ConfigOverrides::default(),
tempdir().expect("tempdir").abs(),
)
.await
.expect("load config");
assert_eq!(config.model_provider_id, "amazon-bedrock");
assert_eq!(config.model_provider, expected_provider);失败侧由 load_config_rejects_unsupported_amazon_bedrock_overrides 和 test_load_config_rejects_legacy_ollama_chat_provider_with_helpful_error 覆盖:前者断言 InvalidData,后者断言 NotFound 和迁移说明。不同错误种类反映不同失败阶段。
9. 复现方法
在本版本源码对应的 codex-rs workspace 中执行:
cd codex-rs
RUST_MIN_STACK=16777216 cargo test -p codex-model-provider-info merge_configured_model_providers
RUST_MIN_STACK=16777216 cargo test -p codex-core load_config_applies_amazon_bedrock --lib
RUST_MIN_STACK=16777216 cargo test -p codex-core test_load_config_rejects_legacy_ollama_chat_provider --lib还可以用只读搜索复述选择链:
rg -n "built_in_model_providers|merge_configured_model_providers|model_provider_id" \
core/src/config/mod.rs model-provider-info/src/lib.rs config/src/config_toml.rs验证结果应能区分三个结论:新增 ID 会扩展目录,Bedrock 只允许部分覆盖,显式未知 ID 不会自动回退。接下来阅读 模型子系统总览 中的运行时 provider 边界,可以继续追踪选定的 ModelProviderInfo 如何变成 SharedModelProvider。
