推理强度与服务层
本文承接 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
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
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
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(¤t_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
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
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
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
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
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 request | fast/default/unsupported/None | 只发送 catalog 支持的显式值 | 不证明服务端路由质量 |
| Bedrock reasoning | 变体 catalog | 复制 default reasoning 并追加 Max | 不证明 Bedrock 实际推理深度 |
| Bedrock tier | 规范化 catalog | tiers 和 default tier 清空 | 不证明隐式 default 的服务端实现 |
| core serialization | Flex service tier | wire JSON 为 flex | 不覆盖完整 Responses 请求 |
| session switch | 当前 effort 与目标 supported levels | 支持则保留,不支持则重选 | 不覆盖迁移提示的用户交互 |
这些测试分别覆盖 reasoning 值解析、service tier 过滤、Bedrock catalog 裁剪和 Flex 序列化。它们不能证明 provider 接受未知 reasoning 值、服务层的实际质量,或模型切换后的完整用户交互。
10. 复现路径
在本版本源码对应的 codex-rs workspace 中执行:
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 和服务层字段。
