Skip to content

Provider解析流程

从配置层、内置注册表和运行时覆盖追踪 Codex 如何解析并选定模型 Provider。

基于rust-v0.150.0
CodexRustModelProviderConfiguration

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

rust
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

rust
[
    (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

rust
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

rust
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

rust
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

rust
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

rust
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 中执行:

bash
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

还可以用只读搜索复述选择链:

bash
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。