Resolve token budgets from each step's active model (#41162)

## Why

Model settings can change between steps in the same turn. Token-budget defaults,
context-window limits, and model-visible guidance need to stay aligned with the
model captured for each step.

## What changed

- Preserve the turn's original token-budget preferences and resolve model-owned
  defaults when each `StepContext` is created.
- Use the step's model to determine context-window availability and token-budget
  guidance, while keeping explicit user settings unchanged.
- Emit one-time replacement or removal notices when context-window guidance
  changes, including compatibility with previously persisted world-state
  snapshots.
- Add `ModelInfo::usable_context_window()` to distinguish reserved-headroom
  capacity from the resolved context window and auto-compaction limit.

## Testing

- Cover mid-turn model switches with model defaults, explicit settings, config
  reloads, missing context windows, and missing destination guidance.
- Cover unchanged, replaced, removed, blank, and legacy guidance transitions.

GitOrigin-RevId: 91100a9f3625bef8a79ecd158eb028051c309eee
This commit is contained in:
Abhinav authored and copyberry committed 2026-08-27 15:01:22 +00:00
1 parent bce96bcb43
commit 8228e9b867
13 files changed
+434 -65

No files matched your search

+28
View File
@@ -486,6 +486,13 @@ impl ModelInfo {
self.context_window.or(self.max_context_window)
}
/// Context available to inference after reserving this model's configured headroom.
pub fn usable_context_window(&self) -> Option<i64> {
self.resolved_context_window().map(|context_window| {
context_window.saturating_mul(self.effective_context_window_percent) / 100
})
}
pub fn auto_compact_token_limit(&self) -> Option<i64> {
let context_limit = self
.resolved_context_window()
@@ -1828,9 +1835,30 @@ mod tests {
};
assert_eq!(model.resolved_context_window(), Some(400_000));
assert_eq!(model.usable_context_window(), Some(380_000));
assert_eq!(model.auto_compact_token_limit(), Some(360_000));
}
#[test]
fn model_context_window_limits_preserve_their_distinct_meanings() {
let model = ModelInfo {
context_window: Some(272_000),
max_context_window: Some(400_000),
auto_compact_token_limit: Some(250_000),
effective_context_window_percent: 95,
..test_model(/*spec*/ None)
};
assert_eq!(
(
model.resolved_context_window(),
model.usable_context_window(),
model.auto_compact_token_limit(),
),
(Some(272_000), Some(258_400), Some(244_800))
);
}
#[test]
fn model_preset_preserves_availability_nux() {
let preset = ModelPreset::from(ModelInfo {