Skip to content

Compact模型Fallback

解释模型切换时为什么先用旧模型压缩、哪些条件允许改用当前模型重试,以及 fallback 成功后如何保持上下文结果一致。

基于rust-v0.150.0
CodexRustContextCompact

Compact模型Fallback ​

本文承接 远程Compact-V2尝试与降级,研究“模型已经发生变化”之后 compact 的选择和结果一致性。这里有两个不同的 fallback:Session 启动时 requested model 不可用,模型目录选择 provider default;以及一次 turn 切换模型后,为旧模型执行 pre-turn compact,失败时改用当前模型重试。

本文只深入第二种 compaction fallback,并用第一种作为边界对照;不覆盖 provider 服务端的摘要质量或真实网络恢复时延。读者需要先理解 ContextWindow与模型限制 的窗口约束、远程Compact请求协议 的 request metadata 和 本地Compact执行与写回 的 replacement history 提交。读完后,读者应能判断一次模型切换是否会触发 compact、fallback context 是否会被创建、错误是否允许换模型,以及成功结果由哪个模型的上下文安装。

1. 两种回退 ​

Session 启动回退发生在模型目录解析阶段;compaction fallback 发生在已有 turn 的 pre-turn compact 阶段。两者都可能出现“requested model”和“实际使用模型”不同,但所有者、触发时机和失败后状态完全不同。

图中 E 是 Session 初始化的模型选择结果,不是 compact 结果;J 才是本文的重点。模型 fallback 只有在 attempt 已经失败且错误分类允许时才发生,不能从“模型切换”直接推断一定会重试。

2. 启动回退 ​

Session 创建时,allow_provider_model_fallback 作为 StartThreadOptions 的字段一路传入 SessionSpawnArgs,最终交给 ModelsManager::get_default_model。这一步决定 turn 的初始 model_info,还没有 history compact。

源码位置:codex-rs/core/src/session/mod.rs :: Session::spawn_internal

rust
let model = models_manager
    .get_default_model(
        &config.model,
        allow_provider_model_fallback,
        refresh_strategy,
        config.http_client_factory(),
    )
    .await;
if allow_provider_model_fallback
    && let Some(requested_model) = config.model.as_ref()
    && model != *requested_model
{
    info!(
        model_provider = %config.model_provider_id,
        requested_model,
        fallback_model = %model,
        "replaced unavailable requested model with provider default"
    );
}

这个 fallback 可能改变 model_info 的 metadata,包括 context window、reasoning preset 和 compaction hash;后续 turn 会把实际模型写入 PreviousTurnSettings。它不生成 CompactionReason::ModelDownshift,也不调用 run_auto_compact。

3. 切换快照 ​

每个已完成 turn 会保存当前模型和 comp_hash。下一个 turn 进入 maybe_run_previous_model_inline_compact 时,先把上一 turn 的模型重新解析成 previous_model_turn_context,再与当前模型的 turn context 比较。

源码位置:codex-rs/core/src/session/turn.rs :: maybe_run_previous_model_inline_compact

rust
let Some(previous_turn_settings) = sess.previous_turn_settings().await else {
    return Ok(());
};
let should_compact_for_comp_hash_change = comp_hash_changed(
    previous_turn_settings.comp_hash.as_deref(),
    turn_context.model_info.comp_hash.as_deref(),
);
let previous_model = previous_turn_settings.model;
let previous_model_turn_context = Arc::new(
    turn_context
        .with_model(previous_model.clone(), &sess.services.models_manager)
        .await,
);

这段代码产生一个请求级旧模型快照,而不是修改当前 turn 的模型。compact 的第一 attempt 使用旧模型快照;如果成功,摘要仍然是按照旧模型的 compaction 兼容性和窗口边界生成的。

两个 StepContext 都是独立请求快照:旧模型快照服务第一次 compact,当前模型快照只在分类允许时消费。它们不共享可变的 model metadata,因此 fallback 不会在原 attempt 中途替换模型字段。

4. Hash门控 ​

comp_hash_changed 只有在两个 hash 都存在且值不同才返回 true。缺失 hash 不足以证明模型的 compact 语义发生变化,因此不会仅凭一个 None 触发“旧模型压缩”。

源码位置:codex-rs/core/src/session/turn.rs :: comp_hash_changed

rust
fn comp_hash_changed(previous: Option<&str>, current: Option<&str>) -> bool {
    previous
        .zip(current)
        .is_some_and(|(previous, current)| previous != current)
}

hash 变化时,代码为旧模型捕获 StepContext,并把当前模型的 fallback context 一并准备好。

源码位置:codex-rs/core/src/session/turn.rs :: maybe_run_previous_model_inline_compact

rust
if should_compact_for_comp_hash_change {
    let step_context = sess
        .capture_step_context(Arc::clone(&previous_model_turn_context), cancellation_token)
        .await?;
    let fallback_step_context = capture_current_model_fallback_step_context(
        sess,
        turn_context,
        previous_model.as_str(),
        cancellation_token,
    )
    .await?;
    run_auto_compact(
        sess,
        step_context,
        fallback_step_context,
        client_session,
        InitialContextInjection::DoNotInject,
        CompactionReason::CompHashChanged,
        CompactionPhase::PreTurn,
    )
    .await?;
    return Ok(());
}

DoNotInject 是因为这是 pre-turn compact;无论第一次使用旧模型还是 fallback 使用当前模型,安装后下一次普通 turn 都会重新建立当前模型所需的初始上下文。

5. 窗口门控 ​

hash 没有变化时,模型切换仍可能因为上下文窗口变小而需要 compact。代码同时比较旧模型窗口、当前模型窗口和当前 token 使用量,并要求旧窗口确实大于新窗口。

源码位置:codex-rs/core/src/session/turn.rs :: maybe_run_previous_model_inline_compact

rust
let active_context_tokens = sess.get_total_token_usage().await;
let previous_model_limit_reached = match turn_context
    .config
    .model_auto_compact_token_limit_scope
{
    AutoCompactTokenLimitScope::Total => {
        let new_auto_compact_limit = turn_context
            .model_info
            .auto_compact_token_limit()
            .unwrap_or(i64::MAX);
        active_context_tokens > new_auto_compact_limit
            || active_context_tokens >= new_context_window
    }
    AutoCompactTokenLimitScope::BodyAfterPrefix => active_context_tokens >= new_context_window,
};
let should_run = previous_model_limit_reached
    && previous_model_turn_context.model_info.slug != turn_context.model_info.slug
    && old_context_window > new_context_window;

Total 会把自动 compact token limit 与完整窗口都纳入判断;BodyAfterPrefix 只比较当前窗口。这里的 token 是 session 的估算/观测状态,不是根据模型名称猜测。只要任一条件不满足,就不会为了模型切换额外执行 compact。

6. 后端门控 ​

capture_current_model_fallback_step_context 还会检查认证后端、provider 类型和模型是否真的不同。当前实现只允许 Codex backend、OpenAI provider,且 previous model 与 current model 不相同。

源码位置:codex-rs/core/src/session/turn.rs :: capture_current_model_fallback_step_context

rust
let uses_codex_backend = turn_context
    .auth_manager
    .as_deref()
    .is_some_and(codex_login::AuthManager::current_auth_uses_codex_backend);
if !uses_codex_backend
    || !turn_context.provider.info().is_openai()
    || previous_model == turn_context.model_info.slug
{
    return Ok(None);
}
sess.capture_step_context(Arc::clone(turn_context), cancellation_token)
    .await
    .map(Some)

这解释了一个常见误判:即使错误属于可 fallback 类别,如果当前 provider 不是 OpenAI,或者 auth 不使用 Codex backend,上层也拿不到 fallback_step_context,最终只能返回原错误。

7. 错误分类 ​

模型 fallback 不是对所有错误的通用重试。should_retry_with_current_model 只接受可能由模型或模型容量引起的错误:invalid request、unexpected status、context window exceeded、usage limit、server overloaded、internal server error 和 retry limit。

这个判断函数只回答“是否允许换当前模型再试”,不负责决定 fallback 模型是谁,也不负责记录 fallback 结果。 后者由调用方传入的 fallback StepContext 和 record_model_fallback telemetry 完成。V2 与 v1 都复用同一 错误分类,但各自的 attempt 类型和 CompactionImplementation 标签不同。

源码位置:codex-rs/core/src/compact_model_fallback.rs :: should_retry_with_current_model

rust
pub(crate) fn should_retry_with_current_model(error: &CodexErr) -> bool {
    matches!(
        error.details(),
        CodexErrorDetails::InvalidRequest(_)
            | CodexErrorDetails::UnexpectedStatus(_)
            | CodexErrorDetails::ContextWindowExceeded
            | CodexErrorDetails::UsageLimitReached(_)
            | CodexErrorDetails::ServerOverloaded
            | CodexErrorDetails::InternalServerError
            | CodexErrorDetails::RetryLimit(_)
    )
}

TurnAborted、本地构造错误和未知错误不在集合中。尤其是取消:它表达的是控制面停止当前操作,不应被改写成“旧模型不可用”,否则会在用户已经取消后额外发起一个 fallback 请求。

8. 结果一致性 ​

旧模型 attempt 和当前模型 fallback 都调用同一个 run_remote_compact_*_attempt,区别只在 StepContext。成功后上层使用实际成功 attempt 对应的 compaction_turn_context,并据此写入 token、reference context 和完成事件。

源码位置:codex-rs/core/src/compact_remote.rs :: run_remote_compact_task_inner_impl

rust
let (attempt, compaction_turn_context) = match attempt {
    Ok(attempt) => (attempt, turn_context),
    Err(error) => {
        let Some(fallback_step_context) = fallback_step_context else {
            return Err(error);
        };
        if !should_retry_with_current_model(&error) {
            return Err(error);
        }
        let fallback_turn_context = &fallback_step_context.turn;
        let fallback_result = run_remote_compact_attempt(
            sess,
            fallback_step_context,
            turn_state,
            &fallback_compaction_trace,
            compaction_metadata,
            analytics_details,
        )
        .await;
        match fallback_result {
            Ok(attempt) => (attempt, fallback_turn_context),
            Err(_) => return Err(error),
        }
    }
};

如果 fallback 失败,函数返回第一次错误,而不是第二次错误。这保留了原始失败的诊断语义;record_model_fallback 另行用 telemetry 记录 fallback 是否失败。

图中的“使用成功 attempt”是结果一致性的核心:不能使用旧模型的 TurnContext 去重算 fallback 模型生成的 token,也不能在第一次失败时先推进 window。真正提交发生在 attempt 选择完成之后。

9. 遥测边界 ​

fallback 计数只记录“发生过一次 fallback 尝试”,并以 outcome 区分成功与失败;它不是模型质量评分,也不表示第二次请求一定比第一次更好。

源码位置:codex-rs/core/src/compact_model_fallback.rs :: record_model_fallback

rust
let outcome = if fallback_error.is_none() {
    "succeeded"
} else {
    "failed"
};
session_telemetry.counter(
    "codex.compaction.model_fallback",
    /*inc*/ 1,
    &[
        ("reason", reason_tag),
        ("implementation", implementation_tag),
        ("outcome", outcome),
    ],
);
warn!(
    previous_model,
    current_model,
    ?reason,
    ?implementation,
    outcome,
    ?fallback_error,
    "previous-model compaction failed; retried with current model"
);

reason 能区分 comp_hash_changed、model_downshift 和用户/窗口触发;implementation 能区分本地 Responses、V2 和 v1 compact。读 telemetry 时必须同时看这两个标签,否则会把不同 compact 后端的 fallback 混为一类。

10. 提交边界 ​

fallback 选择完成后,V1、V2 和本地 compact 进入各自 attempt 的成功写回路径。失败分类、fallback telemetry 和 error event 可以已经发生,但只要 replace_compacted_history 尚未调用,旧 history 仍然是 live 状态。

11. Fallback测试 ​

测试或源码断言输入断言覆盖范围未覆盖边界
comp_hash_changed两个 hash、缺失 hash、相同 hash、不同 hash只有双方存在且不同才触发hash 门控provider 如何生成 hash
remote_compact_v2_retries_failures_with_stream_retry_budget首次 500、流失败、随后成功流重试后的最终摘要才进入 follow-upattempt 输出隔离模型切换 fallback
remote_compact_v2_reuses_compaction_trigger_for_followupsV2 开启、模型请求后 compactcompaction metadata、window 和 replacement item 保持一致V2 写回消费fallback 模型质量
record_model_fallbackreason、implementation、成功/失败 errortelemetry 标签和 outcome 正确观测记录指标上报链路之外的 UI 显示
capture_current_model_fallback_step_contextCodex/OpenAI、其他 provider、相同模型只有允许的 backend/provider/模型组合产生 fallback contextfallback 前置条件真实认证刷新

这些测试和源码路径可以说明“何时选择 fallback”和“失败结果不提交”,不能说明 fallback 摘要与旧模型摘要语义等价,也不能说明 provider 的限流、服务过载或取消会按某个具体延迟恢复。

12. 源码练习 ​

  1. 为 comp_hash_changed 写四组表格测试:None/None、Some/None、相同值和不同值,说明为什么缺失 hash 不能直接代表不兼容。
  2. 在 capture_current_model_fallback_step_context 中临时把 provider 条件去掉,运行非 OpenAI provider fixture,观察它如何改变 fallback 请求边界。
  3. 让旧模型返回 ContextWindowExceeded,让当前模型返回有效 compaction,检查 follow-up 使用哪个 window_id、摘要 item 和 token context。
  4. 让 fallback 返回 TurnAborted,确认上层返回第一次模型错误还是取消错误,并检查是否出现第二个网络请求;这能帮助你区分错误保留与控制面中断。

13. 可执行验证 ​

在本版本源码对应的 codex-rs workspace 中,先运行 compaction 的本地门控测试,再运行请求级 V2 测试。测试汇总 入口必须报告实际运行数量;显示 0 tests 只能说明过滤器没有命中。

bash
cd codex-rs
RUST_MIN_STACK=16777216 cargo test -p codex-core --lib session::turn::tests -- --nocapture
RUST_MIN_STACK=16777216 cargo test -p codex-core --lib compact_remote_v2::tests -- --nocapture
RUST_MIN_STACK=16777216 cargo test -p codex-core --test all remote_compact_v2_retries_failures_with_stream_retry_budget -- --nocapture

第一条命令覆盖 turn 级模型切换辅助逻辑(若该模块未导出测试则以实际 target 输出为准);第二条验证 V2 输出和保留规则;第三条验证请求级 retry。它们不替代真实 provider fallback、认证刷新和取消资源回收测试。