Skip to content

TokenBudget计算与传播

追踪 TokenBudget 从配置和模型默认值进入 Turn,再传播到窗口提示、提醒、查询工具和自动压缩的完整路径。

基于rust-v0.150.0
CodexRustContextToken

TokenBudget计算与传播 ​

Codex 的 TokenBudget 不是一个“把上下文窗口减去已用 token”的孤立函数,而是一个受 feature gate 控制的 上下文协议。配置解析出提醒阈值、提示模板、guidance 和 fallback 参数;模型 catalog 可以在没有显式配置时 提供整套默认值;每个 TurnContext 再次应用这些默认值;窗口状态把 scope 水位与 hard cap 合并成 base_window_tokens_remaining;最后这个剩余值被提醒、get_context_remaining 和自动压缩共同消费。

本文面向已经读过 ContextWindow与模型限制 的读者。本文只追踪 TokenBudget 配置和剩余量的传播,不展开 rollout budget 的共享单位,也不展开摘要内容如何生成。读完后应能 回答三个问题:模型默认值何时覆盖配置、同一个剩余值为何能触发不同消费者、以及 token budget feature 关闭或 窗口未知时哪些路径会自然失效。

1. 先分三种预算 ​

源码中至少有三种容易混淆的预算:

名称所属计量对象主要作用
TokenBudgetfeatures.token_budget当前 context window 剩余 token提醒、查询、自动 rollover
RolloutBudgetfeatures.rollout_budgetprovider 报告的 rollout units根 thread 与 sub-agent 共享限额
Tool output budgettool_output_token_limit单个工具输出写入历史前截断工具结果

TokenBudget 的剩余量来自 ContextWindow与模型限制 的窗口状态,而不是 provider 报告的 rollout units。后两者即使数值相同, 也不能互相替代。

2. 配置解析 ​

配置解析只有在 Feature::TokenBudget 开启时才返回 Some(TokenBudgetConfig)。没有开启 feature,即使 TOML 中写了 token budget 字段,核心也不会把它传播到 Turn。

源码位置:codex-rs/core/src/config/mod.rs :: resolve_token_budget_config。

rust
fn resolve_token_budget_config(
    config_toml: &ConfigToml,
    features: &ManagedFeatures,
) -> std::io::Result<Option<TokenBudgetConfig>> {
    if !features.enabled(Feature::TokenBudget) {
        return Ok(None);
    }

    let token_budget_config = token_budget_toml_config(config_toml.features.as_ref());
    let mode = token_budget_config
        .and_then(|config| config.mode)
        .unwrap_or_default();
    let reminder_threshold_tokens =
        token_budget_config.and_then(|config| config.reminder_threshold_tokens);
    let reminder_message_template = token_budget_config
        .and_then(|config| config.reminder_message_template.clone())
        .unwrap_or_else(|| DEFAULT_TOKEN_BUDGET_REMINDER_MESSAGE_TEMPLATE.to_string());
    let guidance_message = token_budget_config
        .and_then(|config| config.guidance_message.clone())
        .filter(|message| !message.trim().is_empty());
    let auto_compact_fallback_prompt = token_budget_config
        .and_then(|config| config.auto_compact_fallback_prompt.as_deref())
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(str::to_string);
    let auto_compact_fallback_buffer_tokens =
        token_budget_config.and_then(|config| config.auto_compact_fallback_buffer_tokens);
    let use_history_notes_extension = token_budget_config
        .and_then(|config| config.use_history_notes_extension)
        .unwrap_or(false);

    let token_budget = TokenBudgetConfig {
        mode,
        reminder_threshold_tokens,
        reminder_message_template,
        guidance_message,
        auto_compact_fallback_prompt,
        auto_compact_fallback_buffer_tokens,
        use_history_notes_extension,
    };
    token_budget.validate()?;
    Ok(Some(token_budget))
}

这里有三个重要边界:默认提醒模板只在本地没有模板时补入;空白 guidance 和 fallback prompt 会被转成 None;fallback prompt 一旦存在,就必须同时提供正数 buffer。校验失败会让配置加载失败,而不是把非法值 悄悄带入窗口计算。

源码位置:codex-rs/core/src/config/mod.rs :: TokenBudgetConfig::validate、fallback_buffer_tokens。

rust
pub(crate) fn fallback_buffer_tokens(&self) -> i64 {
    if self.auto_compact_fallback_prompt.is_some() {
        self.auto_compact_fallback_buffer_tokens.unwrap_or(0)
    } else {
        0
    }
}

因此,单独配置一个 buffer 并不会改变 ContextWindowTokenStatus;只有 fallback prompt 也存在时,buffer 才会进入自动压缩水位。

3. 默认值来源 ​

模型默认值放在 ModelInfo::model_messages.token_budget,不是直接放在 context_window 字段旁边。它包含 提醒阈值、提醒模板、guidance、fallback prompt 和 fallback buffer 五个字段。

源码位置:codex-rs/protocol/src/openai_models.rs :: ModelMessages、ModelTokenBudgetConfig。

rust
pub struct ModelMessages {
    pub instructions_template: Option<String>,
    pub instructions_variables: Option<ModelInstructionsVariables>,
    pub approvals: Option<ApprovalMessages>,
    pub collaboration_modes: Option<CollaborationModeMessages>,
    pub auto_review: Option<AutoReviewMessages>,
    pub permissions: Option<PermissionMessages>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub token_budget: Option<ModelTokenBudgetConfig>,
}

pub struct ModelTokenBudgetConfig {
    pub reminder_threshold_tokens: i64,
    pub reminder_message_template: String,
    pub guidance_message: String,
    pub auto_compact_fallback_prompt: String,
    pub auto_compact_fallback_buffer_tokens: i64,
}

模型默认值不是无条件覆盖。apply_model_defaults 只有在 feature 已开启且配置没有显式 token budget 设置时 才会运行;显式设置可以只覆盖一部分字段,但“存在显式设置”会阻止整套模型默认值注入。

源码位置:codex-rs/core/src/session/token_budget.rs :: has_explicit_settings、apply_model_defaults。

rust
pub(super) fn apply_model_defaults(config: &mut Config, model_info: &ModelInfo) {
    if !config.features.enabled(Feature::TokenBudget) || has_explicit_settings(config) {
        return;
    }

    let Some(model_defaults) = model_info
        .model_messages
        .as_ref()
        .and_then(|messages| messages.token_budget.as_ref())
    else {
        return;
    };

    let token_budget = TokenBudgetConfig {
        mode: config
            .token_budget
            .as_ref()
            .map(|token_budget| token_budget.mode)
            .unwrap_or_default(),
        reminder_threshold_tokens: Some(model_defaults.reminder_threshold_tokens),
        reminder_message_template: model_defaults.reminder_message_template.clone(),
        guidance_message: Some(model_defaults.guidance_message.clone()),
        auto_compact_fallback_prompt: Some(model_defaults.auto_compact_fallback_prompt.clone()),
        auto_compact_fallback_buffer_tokens: Some(
            model_defaults.auto_compact_fallback_buffer_tokens,
        ),
    };

    if let Err(error) = token_budget.validate() {
        tracing::warn!(
            model = %model_info.slug,
            %error,
            "ignoring invalid model-owned token-budget defaults"
        );
        return;
    }

    config.token_budget = Some(token_budget);
}

模型默认值验证失败时只记录 warning 并放弃默认值,不会把无效模型 catalog 变成有效本地配置。这个行为和 本地 TOML 校验失败不同:本地配置在解析阶段返回错误,模型默认值在 Turn 构造阶段被忽略。

4. Turn级应用 ​

Session 创建和每个 Turn 创建都可能接触模型信息。真正把模型默认值放入当前 Turn 的位置是 TurnContext::make_turn_context,因此文章不能只看 Session 初始化。

源码位置:codex-rs/core/src/session/turn_context.rs :: make_turn_context。

rust
let mut per_turn_config = per_turn_config;
super::token_budget::apply_model_defaults(&mut per_turn_config, &model_info);
per_turn_config.service_tier = get_service_tier(
    per_turn_config.service_tier,
    per_turn_config.features.enabled(Feature::FastMode),
    &model_info,
);
let per_turn_config = Arc::new(per_turn_config);

这意味着模型切换后的新 Turn 可以获得新模型提供的 token budget 默认值;但如果用户已经显式配置过 token budget,模型切换不会用 catalog 默认值覆盖用户选择。Config 是 Turn 的快照,不能据此推断后续 Turn 必然 使用同一模型默认值。

Session 在需要保存“由模型解析出的配置”时也会调用相同函数:

源码位置:codex-rs/core/src/session/mod.rs :: session configuration lock 分支。

rust
if config.config_lock_export_dir.is_some()
    && config.config_lock_save_fields_resolved_from_model_catalog
{
    self::token_budget::apply_model_defaults(Arc::make_mut(&mut config), &model_info);
}

这里的调用目的是导出解析后的配置锁,不是另起一套预算计算。预算真正被消费时,仍以当前 TurnContext.config 为准。

5. 窗口剩余 ​

ContextWindow与模型限制 已经说明 context_window_token_status 如何得到 hard cap 和 scope limit。本篇只抓住它对 TokenBudget 的交付接口:base_window_tokens_remaining 是两个剩余量的较小值。

源码位置:codex-rs/core/src/session/context_window.rs :: context_window_token_status。

rust
let base_window_tokens_remaining = [
    tokens_remaining(auto_compact_scope_limit, auto_compact_scope_tokens),
    tokens_remaining(full_context_window_limit, active_context_tokens),
]
.into_iter()
.flatten()
.min();

let auto_compact_fallback_buffer_tokens = turn_context
    .config
    .token_budget
    .as_ref()
    .map_or(0, crate::config::TokenBudgetConfig::fallback_buffer_tokens);
let buffered_auto_compact_limit = auto_compact_scope_limit
    .map(|limit| limit.saturating_add(auto_compact_fallback_buffer_tokens));

注意 base_window_tokens_remaining 使用未加 fallback buffer 的基础剩余量,而 token_limit_reached 使用加过 buffer 的自动压缩水位。这样 reminder 仍然报告“距离基础窗口还剩多少”,fallback 只影响是否允许继续采样 或是否触发 rollover,不会让模型看到一个被 buffer 人为放大的剩余值。

6. 提醒写回 ​

maybe_record 是剩余量进入历史的第一个消费者。它只在 feature 开启、窗口剩余已知且配置存在时工作;达到 阈值后通过 Session 状态的 claim 位保证同一个 context window 只写入一次提醒。

相关源码:

  • codex-rs/core/src/session/token_budget.rs :: maybe_record
  • codex-rs/core/src/state/auto_compact_window.rs :: claim_token_budget_reminder
rust
if config
    .reminder_threshold_tokens
    .is_some_and(|threshold| base_window_tokens_remaining <= threshold)
{
    let reminder_due = {
        let mut state = sess.state.lock().await;
        state.claim_token_budget_reminder()
    };
    if reminder_due {
        let response_item =
            ContextualUserFragment::into(crate::context::TokenBudgetReminder::new(
                &config.reminder_message_template,
                base_window_tokens_remaining,
            ));
        sess.record_conversation_items(turn_context, std::slice::from_ref(&response_item))
            .await;
    }
}

TokenBudgetReminder 是无 marker 的 developer fragment,模板中的 {n_remaining} 在构造时替换。它会进入 历史,因此下一次模型请求能看到提醒;它不是事件层的 toast,也不是只给 TUI 使用的诊断字符串。

这里的 claim 发生在 record_conversation_items 之前,和 rollout 预算“写入后再确认交付”的顺序不同。 如果任务恰好在 claim 后、历史写入完成前被取消,当前窗口不会自动重新 claim;只有窗口推进或其他恢复逻辑 重置窗口状态后,提醒才会再次获得交付资格。

7. 查询工具 ​

当 TokenBudget feature 开启时,工具注册表同时暴露 get_context_remaining 和 new_context_window。前者重新计算当前 Turn 的窗口状态,并把同一个 base_window_tokens_remaining 包装成 模型可见的 function output;Code Mode 则额外得到结构化的 tokens_left。

相关源码:

  • codex-rs/core/src/tools/spec_plan.rs :: TokenBudget tool registration
  • codex-rs/core/src/tools/handlers/get_context_remaining.rs :: GetContextRemainingHandler
rust
if features.enabled(Feature::TokenBudget) {
    registry.add_with_exposure(NewContextWindowHandler, ToolExposure::DirectModelOnly);
    registry.add(GetContextRemainingHandler);
}

注册只决定模型能否看到工具;执行时不会读取注册阶段缓存的 token 数,而是重新查询当前 Session 和 Turn。

源码位置:codex-rs/core/src/tools/handlers/get_context_remaining.rs :: GetContextRemainingHandler::handle。

rust
let token_status = crate::session::context_window::context_window_token_status(
    invocation.session.as_ref(),
    invocation.turn.as_ref(),
)
.await;

Ok(boxed_tool_output(GetContextRemainingOutput::new(
    token_status.base_window_tokens_remaining,
)))

窗口未知时工具仍可以注册,但返回的是“unknown tokens left”,而不是伪造一个无限大数字。这个分支保留了 未知 metadata 的事实边界。

源码位置:codex-rs/core/src/context/token_budget_context.rs :: TokenBudgetRemainingContext。

rust
fn body(&self) -> String {
    match self.tokens_left {
        Some(tokens_left) => {
            format!("You have {tokens_left} tokens left in this context window.")
        }
        None => "You have unknown tokens left in this context window.".to_string(),
    }
}

8. Fallback压缩 ​

在采样后,run_turn 先重新计算窗口状态,再把 base_window_tokens_remaining 交给 maybe_record。只有不需要 rollover、没有达到 token limit 且基础剩余量恰好为 0 时,fallback prompt 才会写入历史;否则自动压缩路径优先。

源码位置:codex-rs/core/src/session/turn.rs :: post-sampling token decision。

rust
let should_roll_over =
    needs_follow_up && (sess.take_new_context_window_request().await || token_limit_reached);
let allow_auto_compact_fallback = !should_roll_over && !token_limit_reached;
super::token_budget::maybe_record(
    sess.as_ref(),
    turn_context.as_ref(),
    token_status.base_window_tokens_remaining,
    allow_auto_compact_fallback,
)
.await;

fallback prompt 自己不是压缩动作。它只是模型可见的 developer fragment;真正建立新 context window 的动作由 run_auto_compact 进入 compact_token_budget::run_inline_auto_compact_task,再调用 Session::start_new_context_window。

源码位置:codex-rs/core/src/compact_token_budget.rs :: run_inline_auto_compact_task、run_compact_task_inner。

rust
let compaction_item = TurnItem::ContextCompaction(ContextCompactionItem::new());
sess.emit_turn_item_started(turn_context, &compaction_item).await;
sess.start_new_context_window(step_context, world_state).await;
sess.emit_turn_item_completed(turn_context, compaction_item).await;

9. 身份与窗口 ​

TokenBudget 的 full-context developer fragment 还会携带窗口身份:thread id 或 agent name、first window id、 current window id 和 previous window id。TokenBudgetMode 只改变第一行身份,不改变剩余量计算。

源码位置:codex-rs/core/src/context/token_budget_context.rs :: TokenBudgetContext::body。

rust
let identity = match self.mode {
    TokenBudgetMode::Thread => format!("Thread id: {}", self.thread_id),
    TokenBudgetMode::Name => format!("Agent name: {}", self.agent_path),
};
let mut lines = vec![
    identity,
    format!("First context window id: {first_window_id}"),
    format!("Current context window id: {window_id}"),
];
if let Some(previous_window_id) = self.previous_window_id {
    lines.push(format!("Previous context window id: {previous_window_id}"));
}

Session 在构造初始上下文时才写入这段 full-context metadata;steady-state diff 不重复发射它。窗口前进时, AutoCompactWindow::advance 会清除 reminder 和 fallback 的 claim 位,所以新窗口可以再次提醒一次。

10. 预算测试 ​

以下测试围绕配置、传播和消费者执行;它们不证明 provider 的 tokenizer 与本地估算完全一致。

测试输入断言覆盖范围未覆盖
load_config_resolves_token_budget_configfeature 开启、mode=name、阈值与 fallback 字段TOML 解析成 TokenBudgetConfig配置字段和默认模板模型 catalog 默认值
load_config_rejects_non_positive_token_budget_reminder_threshold阈值为 0 或负数配置加载返回指定错误本地输入校验运行时动态修改
token_budget_uses_model_message_defaults只显式配置 mode=name,模型提供整套默认值身份使用 agent name,模型 guidance 进入请求模型默认值注入provider 是否遵循 guidance
token_budget_explicit_default_template_overrides_model_defaults显式写入本地默认模板不继承模型 guidance显式设置优先级任意配置层合并顺序
token_budget_defaults_follow_the_active_model从 gpt-5.2 切换到 gpt-5.4旧历史保留,新模型 guidance 只新增一次Turn 级默认值刷新compaction 后的模型切换
token_budget_ignores_invalid_model_message_defaults0 阈值、负 buffer、超长文本无效模型默认值被忽略catalog 校验失败路径warning 的外部采集
token_budget_reminder_uses_body_after_prefix_window窗口 10000、body limit 1000、两次 usageprefix 不触发提醒,后续剩余 400 时写入一次提醒BodyAfterPrefix 到 reminder摘要后的语义保留
get_context_remaining_returns_token_budget_remaining_fragment窗口 10000、累计 token 2500function output 为 6500 tokens left工具查询与历史回灌外部模型如何使用输出
get_context_remaining_returns_unknown_when_threshold_is_unbounded无可解析窗口输出 unknown,而非伪造数值未知窗口边界provider 实际服务端限制
token_budget_context_uses_new_window_after_compaction调用 new context window 工具后继续 follow-upfirst/current/previous window id 正确变化手动 reset 后的身份传播自动压缩的所有 provider 路径

精确执行命令应在 codex-rs workspace 中运行:

bash
cargo test -p codex-core --lib load_config_resolves_token_budget_config
cargo test -p codex-core --lib load_config_rejects_non_positive_token_budget_reminder_threshold
RUST_MIN_STACK=16777216 cargo test -p codex-core --test all token_budget_uses_model_message_defaults
RUST_MIN_STACK=16777216 cargo test -p codex-core --test all token_budget_explicit_default_template_overrides_model_defaults
RUST_MIN_STACK=16777216 cargo test -p codex-core --test all token_budget_model_defaults_survive_config_lock_replay
RUST_MIN_STACK=16777216 cargo test -p codex-core --test all token_budget_defaults_follow_the_active_model
RUST_MIN_STACK=16777216 cargo test -p codex-core --test all token_budget_ignores_invalid_model_message_defaults
RUST_MIN_STACK=16777216 cargo test -p codex-core --test all token_budget_guidance_follows_context_window
RUST_MIN_STACK=16777216 cargo test -p codex-core --test all token_budget_reminder_uses_body_after_prefix_window
RUST_MIN_STACK=16777216 cargo test -p codex-core --test all get_context_remaining_returns_token_budget_remaining_fragment
RUST_MIN_STACK=16777216 cargo test -p codex-core --test all get_context_remaining_returns_unknown_when_threshold_is_unbounded
RUST_MIN_STACK=16777216 cargo test -p codex-core --test all token_budget_context_uses_new_window_after_compaction
RUST_MIN_STACK=16777216 cargo test -p codex-core --test all token_budget_auto_compact_fallback_uses_buffer_until_new_context

11. 路径练习 ​

调试“模型没有看到预算提醒”“查询工具返回 unknown”或“fallback 没有触发”时,按这条顺序核对:

  1. Feature::TokenBudget 是否开启。
  2. resolve_token_budget_config 是否生成了配置,输入是否通过校验。
  3. 当前 Turn 是否因为显式设置跳过了模型默认值。
  4. TurnContext::model_context_window() 是否为 Some。
  5. ContextWindowTokenStatus 的 scope limit、hard cap 和 fallback buffer 哪一个先达到。
  6. maybe_record 是否因 reminder claim、allow_auto_compact_fallback 或 token_limit_reached 被短路。
  7. 如果是工具调用,检查工具是否在 spec_plan 中注册,以及 output 是否进入下一次 history。

可以从真实源码重建整条链:

bash
rg -n "resolve_token_budget_config|apply_model_defaults|context_window_token_status|maybe_record|get_context_remaining|run_inline_auto_compact_task" \
  codex-rs/core/src codex-rs/protocol/src codex-rs/features/src

最容易误判的地方是把模型默认值当作配置覆盖、把 fallback buffer 当作模型剩余量,或者把 get_context_remaining 的结果当作 provider 的精确 token 计数。源码只保证本文所列传播和边界,不替模型服务端 承诺未被测试证明的行为。