Skip to content

推理强度与服务层

从模型目录声明到 TurnContext 和 Responses wire request,追踪 reasoning effort 与 service tier 的协商、降级和 provider 限制。

基于rust-v0.150.0
CodexRustModelReasoningServiceTier

推理强度与服务层 ​

本文承接 ModelInfo能力模型 和 模型选择迁移。前文已经说明模型如何被选中、能力元数据如何生成;本文只研究两个会改变请求行为的值:ReasoningEffort 和 service_tier。问题不是“模型支持哪些选项”,而是一个用户设置怎样经过 session、协作模式和模型能力,最终变成或被过滤出 Responses 请求。

读者需要能阅读 Rust enum、Option、配置覆盖和异步 turn 流程。本文不解释 reasoning 内容如何以 SSE delta 返回,也不把 provider 的服务质量承诺外推为服务端行为。读完后,你应能回答:为什么 ultra 在 wire 上变成 max,为什么模型目录的 default service tier 不会自动进入请求,为什么 Bedrock 的 catalog 会清空 service tiers,以及切换模型时当前 effort 何时被替换。

1. 三层值 ​

同一个“推理强度”在代码中至少有四层:目录声明的 default_reasoning_level 和 supported_reasoning_levels,Turn 配置快照,StepContext 当前有效值,以及请求构造时经过归一化的 wire effort。服务层也对应经过目录、Session/Turn 过滤、StepContext 和请求字段。当前生产请求优先读取 StepContext;TurnContext 上的 effort/summary 字段只保留为 legacy 兼容状态。

目录只是输入,不是请求结果。default_reasoning_level 只有在 turn 没有显式 effort 时才参与 build_reasoning;default_service_tier 更严格,service_tier_for_request 明确不会把它自动应用到请求。

2. 值的协议 ​

ReasoningEffort 是开放字符串协议:内置值覆盖 none、minimal、low、medium、high、xhigh、max、ultra,未知非空值进入 Custom(String)。这让新服务端值可以穿过旧客户端,而空字符串在反序列化时直接拒绝。

源码位置:codex-rs/protocol/src/openai_models.rs :: ReasoningEffort

rust
pub enum ReasoningEffort {
    None,
    Minimal,
    Low,
    #[default]
    Medium,
    High,
    XHigh,
    Max,
    Ultra,
    /// A model-defined effort value that this client does not know yet.
    Custom(String),
}

impl ReasoningEffort {
    pub fn as_str(&self) -> &str {
        match self {
            Self::None => "none",
            Self::Minimal => "minimal",
            Self::Low => "low",
            Self::Medium => "medium",
            Self::High => "high",
            Self::XHigh => "xhigh",
            Self::Max => "max",
            Self::Ultra => "ultra",
            Self::Custom(effort) => effort,
        }
    }
}

ServiceTier 则不是开放 enum:配置层只有 Fast 和 Flex,请求值分别是 priority 与 flex,另有字符串 default 作为“明确选择标准路由”的 sentinel。default 不是 catalog tier id,不能拿它与 ModelInfo.service_tiers 直接比较。

源码位置:codex-rs/protocol/src/config_types.rs :: ServiceTier

rust
pub enum ServiceTier {
    Fast,
    Flex,
}

pub const SERVICE_TIER_DEFAULT_REQUEST_VALUE: &str = "default";

impl ServiceTier {
    pub const fn request_value(self) -> &'static str {
        match self {
            Self::Fast => "priority",
            Self::Flex => "flex",
        }
    }
}

3. Turn有效值 ​

模型切换时,Codex 不会盲目把旧 effort 带到新模型。TurnContext::with_model 重新读取目标模型元数据,若当前 effort 仍在目标的 supported list 中则保留;否则从目标 supported list 取中间位置的值,再退回目标 default。

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

rust
let model_info = models_manager
    .get_model_info(model.as_str(), &config.to_models_manager_config())
    .await;
let supported_reasoning_levels = model_info
    .supported_reasoning_levels
    .iter()
    .map(|preset| preset.effort.clone())
    .collect::<Vec<_>>();
let reasoning_effort = if let Some(current_reasoning_effort) = self.reasoning_effort.clone()
{
    if supported_reasoning_levels.contains(&current_reasoning_effort) {
        Some(current_reasoning_effort)
    } else {
        supported_reasoning_levels
            .get(supported_reasoning_levels.len().saturating_sub(1) / 2)
            .cloned()
            .or_else(|| model_info.default_reasoning_level.clone())
    }
} else {
    supported_reasoning_levels
        .get(supported_reasoning_levels.len().saturating_sub(1) / 2)
        .cloned()
        .or_else(|| model_info.default_reasoning_level.clone())
};
config.model_reasoning_effort = reasoning_effort.clone();

这个算法的消费者是新的 turn 配置,不是模型目录本身。它还解释了迁移文章中的一个边界:TUI 接受升级时会直接写入目标 preset 的 default effort;运行中切换模型时,则由 with_model 根据目标 supported levels 再做一次协商。

4. Turn 配置 ​

每 turn 的配置都从原始 config 复制,但三个字段的覆盖源不同:model_reasoning_effort 取自 collaboration mode,model_reasoning_summary 和 service_tier 取自 session configuration。它们都比原始 config 更晚生效,但不能笼统地称为“协作模式覆盖”。

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

rust
let config = session_configuration.original_config_do_not_use.clone();
let mut per_turn_config = (*config).clone();
per_turn_config.cwd = cwd;
per_turn_config.model_reasoning_effort =
    session_configuration.collaboration_mode.reasoning_effort();
per_turn_config.model_reasoning_summary = session_configuration.model_reasoning_summary;
per_turn_config.service_tier = session_configuration.service_tier.clone();
per_turn_config.personality = session_configuration.personality;

随后 make_turn_context 再从 collaboration mode 取得 reasoning_effort;summary 则优先取 session configuration,缺失时使用模型的 default_reasoning_summary。service tier 还会经过 get_service_tier,结合 FastMode feature 和模型能力得到当前 turn 的有效值。因此“原始配置里写了 high 或 flex”不能单独预测 wire 字段:还必须看构造 turn 时的覆盖和模型能力过滤。

5. 请求归一化 ​

请求构造有两个独立过滤。reasoning effort 先取显式 effort,否则取模型 default,然后把 Ultra 映射成 wire 可接受的 Max;summary 只有模型声明支持且用户没有选择 None 时才发送。service tier 则调用模型的 service_tier_for_request。

源码位置:codex-rs/core/src/client.rs :: reasoning_effort_for_request 与 ModelClient::build_reasoning

rust
fn reasoning_effort_for_request(effort: ReasoningEffortConfig) -> ReasoningEffortConfig {
    match effort {
        ReasoningEffortConfig::Ultra => ReasoningEffortConfig::Max,
        effort => effort,
    }
}

fn build_reasoning(
    model_info: &ModelInfo,
    effort: Option<ReasoningEffortConfig>,
    summary: ReasoningSummaryConfig,
) -> Reasoning {
    Reasoning {
        effort: effort
            .or_else(|| model_info.default_reasoning_level.clone())
            .map(reasoning_effort_for_request),
        summary: (model_info.supports_reasoning_summary_parameter
            && summary != ReasoningSummaryConfig::None)
        .then_some(summary),
        context: model_info
            .use_responses_lite
            .then_some(ReasoningContext::AllTurns),
    }
}

Ultra → Max 是客户端 wire 兼容规则,不代表 session 内部的值被永久改成 Max;tracing 仍可能记录有效 effort。未知的 Custom("future") 不会被这段函数改写,会按原字符串序列化。

6. 服务层过滤 ​

服务层的关键函数只接受用户传入值,并要求它既不是 default sentinel,又出现在模型 catalog 的 service_tiers 中。None 和不支持的值都变成 None;catalog 的 default_service_tier 不参与补值。

源码位置:codex-rs/protocol/src/openai_models.rs :: ModelInfo::service_tier_for_request

rust
pub fn service_tier_for_request(&self, service_tier: Option<String>) -> Option<String> {
    service_tier.filter(|service_tier| {
        service_tier != SERVICE_TIER_DEFAULT_REQUEST_VALUE
            && self.supports_service_tier(service_tier)
    })
}

这条语义经测试明确锁定:显式 priority 只有在 catalog 声明支持时才发送;default 即使 catalog 的 default 是 priority 也会被省略;没有显式值时不会自动应用 catalog default。

7. Provider裁剪 ​

Bedrock 不是把 OpenAI catalog 原样复制过去。normalize_bedrock_catalog 清空 speed tiers、service tiers 和 default service tier,并把 web search 类型收窄为 text。GPT-5.6 变体在此基础上复制 OpenAI 的默认 reasoning level,再追加 Max 能力。

源码位置:codex-rs/model-provider/src/amazon_bedrock/catalog.rs :: normalize_bedrock_catalog 与 gpt_5_6_bedrock_model

rust
pub(crate) fn normalize_bedrock_catalog(mut catalog: ModelsResponse) -> ModelsResponse {
    for model in &mut catalog.models {
        // Amazon Bedrock currently only supports the implicit "default" tier for GPT models.
        model.additional_speed_tiers.clear();
        model.service_tiers.clear();
        model.default_service_tier = None;
        // Bedrock rejects the `search_content_types` field used by multimodal search.
        model.web_search_tool_type = WebSearchToolType::Text;
    }
    catalog
}

fn gpt_5_6_bedrock_model(
    openai_slug: &str,
    bedrock_slug: &str,
    display_name: &str,
    priority: i32,
) -> ModelInfo {
    let openai_model = bundled_openai_model(openai_slug);
    let mut model = gpt_5_bedrock_model(
        GPT_5_5_OPENAI_MODEL_ID,
        bedrock_slug,
        display_name,
        priority,
    );
    model.description = openai_model.description;
    model.default_reasoning_level = openai_model.default_reasoning_level;
    model.multi_agent_version = openai_model.multi_agent_version;
    model.supported_reasoning_levels.push(ReasoningEffortPreset {
        effort: ReasoningEffort::Max,
        description: "Maximum reasoning depth for the hardest problems".to_string(),
    });
    model
}

这里的 owner 是 provider catalog 构造器,不是 request builder。测试 gpt_5_6_bedrock_models_use_variant_metadata_and_max_reasoning_effort 验证变体 metadata 和 Max effort,gpt_5_bedrock_models_only_allow_default_service_tier 验证每个 Bedrock 模型的 tier 列表为空、priority 和 default sentinel 都不会进入请求。

8. 请求落线 ​

最终 request 同时消费三类输入:Prompt 提供内容与工具,ModelInfo 提供能力门控,StepContext 提供本次请求冻结的 effort、summary 和 service tier。service_tier 在构造 request 的最后一步才过滤,因此上游保存一个无效值不会直接导致 provider 请求带上它。

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

rust
let reasoning = Self::build_reasoning(model_info, effort, summary);
let service_tier = model_info.service_tier_for_request(service_tier);
let request = ResponsesApiRequest {
    model: model_info.slug.clone(),
    instructions,
    input,
    tools,
    tool_choice: "auto".to_string(),
    parallel_tool_calls: prompt.parallel_tool_calls && !model_info.use_responses_lite,
    reasoning: Some(reasoning),
    store: provider.is_azure_responses_endpoint(),
    stream: true,
    stream_options,
    include,
    service_tier,
    prompt_cache_key,
    text,
    client_metadata: Some(responses_metadata.client_metadata()),
};

这一步没有把 catalog default service tier 注入 request,也没有重新验证 reasoning effort 是否在 supported list;reasoning supported list 的协调已经在 TurnContext::with_model 和上游配置处理中完成,request builder 负责 wire 归一化与字段门控。

8.1 异常路径 ​

这条链的“失败”不一定是请求抛错:不支持的 tier 会被过滤,旧 effort 与新模型不兼容时会降级到目标模型的中间 preset 或 default,Bedrock catalog 则在归一化时清理服务层字段。这些都是可恢复的降级路径:请求仍可以继续构造,但 wire 不再保留上游的原值。真正的拒绝发生在 ReasoningEffort 解析的空字符串输入,这与请求构造阶段的过滤是两个不同边界。

9. 协商测试 ​

主题输入与动作关键断言未覆盖边界
reasoning enum已知值、未知非空值、空字符串已知值正常,未知值保留为 Custom,空值拒绝不证明服务端接受未知值
tier requestfast/default/unsupported/None只发送 catalog 支持的显式值不证明服务端路由质量
Bedrock reasoning变体 catalog复制 default reasoning 并追加 Max不证明 Bedrock 实际推理深度
Bedrock tier规范化 catalogtiers 和 default tier 清空不证明隐式 default 的服务端实现
core serializationFlex service tierwire JSON 为 flex不覆盖完整 Responses 请求
session switch当前 effort 与目标 supported levels支持则保留,不支持则重选不覆盖迁移提示的用户交互

这些测试分别覆盖 reasoning 值解析、service tier 过滤、Bedrock catalog 裁剪和 Flex 序列化。它们不能证明 provider 接受未知 reasoning 值、服务层的实际质量,或模型切换后的完整用户交互。

10. 复现路径 ​

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

bash
RUST_MIN_STACK=16777216 cargo test -p codex-protocol reasoning_effort_
RUST_MIN_STACK=16777216 cargo test -p codex-protocol service_tier_for_request_
RUST_MIN_STACK=16777216 cargo test -p codex-model-provider gpt_5_6_bedrock_models_use_variant_metadata_and_max_reasoning_effort
RUST_MIN_STACK=16777216 cargo test -p codex-model-provider gpt_5_bedrock_models_only_allow_default_service_tier
RUST_MIN_STACK=16777216 cargo test -p codex-core serializes_flex_service_tier_when_set

源码练习:把 ModelInfo.service_tiers 改成只含 priority,比较 service_tier_for_request(Some("flex")) 和 Some("priority");再把 build_reasoning 的 effort 传入 Ultra,观察内部值与 wire 值的差异。切换模型时,检查 TurnContext::with_model 是否保留了旧 effort,不能只看配置文件中的原始值。

11. 源码导航 ​

先读 ModelInfo能力模型 理解字段来源,再读本篇的 TurnContext::with_model 和 ModelClient::build_reasoning 连接 session 与 wire;provider 差异则从 Bedrock catalog 开始。后续进入请求构造专题时,重点观察同一 ModelInfo 如何继续门控工具、summary、Responses Lite 和服务层字段。