From 9fc0c416e45f554574afc43d3277e4db0a5c62c1 Mon Sep 17 00:00:00 2001 From: axobase001 <138223345+axobase001@users.noreply.github.com> Date: Thu, 7 May 2026 19:51:13 +0800 Subject: [PATCH] fix(cache): clear history on model scope changes (#1052) --- crates/tui/src/commands/core.rs | 54 ++++++++++++++++++++++++++++++--- crates/tui/src/tui/app.rs | 9 ++++++ crates/tui/src/tui/ui.rs | 15 ++++----- crates/tui/src/tui/ui/tests.rs | 29 +++++++++++++++++- 4 files changed, 95 insertions(+), 12 deletions(-) diff --git a/crates/tui/src/commands/core.rs b/crates/tui/src/commands/core.rs index 0bc9b239..8529233d 100644 --- a/crates/tui/src/commands/core.rs +++ b/crates/tui/src/commands/core.rs @@ -99,14 +99,19 @@ pub fn model(app: &mut App, model_name: Option<&str>) -> CommandResult { if let Some(name) = model_name { if name.trim().eq_ignore_ascii_case("auto") { let old_model = app.model_display_label(); + let model_changed = !app.auto_model || app.model != "auto"; app.auto_model = true; app.model = "auto".to_string(); app.last_effective_model = None; app.reasoning_effort = ReasoningEffort::Auto; app.last_effective_reasoning_effort = None; app.update_model_compaction_budget(); - app.session.last_prompt_tokens = None; - app.session.last_completion_tokens = None; + if model_changed { + app.clear_model_scoped_telemetry(); + } else { + app.session.last_prompt_tokens = None; + app.session.last_completion_tokens = None; + } return CommandResult::with_message_and_action( tr(app.ui_locale, MessageId::ModelChanged) .replace("{old}", &old_model) @@ -121,12 +126,17 @@ pub fn model(app: &mut App, model_name: Option<&str>) -> CommandResult { )); }; let old_model = app.model_display_label(); + let model_changed = app.auto_model || app.model != model_id; app.auto_model = false; app.model = model_id.clone(); app.last_effective_model = None; app.update_model_compaction_budget(); - app.session.last_prompt_tokens = None; - app.session.last_completion_tokens = None; + if model_changed { + app.clear_model_scoped_telemetry(); + } else { + app.session.last_prompt_tokens = None; + app.session.last_completion_tokens = None; + } CommandResult::with_message_and_action( tr(app.ui_locale, MessageId::ModelChanged) .replace("{old}", &old_model) @@ -485,6 +495,42 @@ mod tests { assert_eq!(app.session.last_completion_tokens, None); } + #[test] + fn model_switch_clears_turn_cache_history() { + let mut app = create_test_app(); + app.push_turn_cache_record(TurnCacheRecord { + input_tokens: 100, + output_tokens: 25, + cache_hit_tokens: Some(70), + cache_miss_tokens: Some(30), + reasoning_replay_tokens: Some(12), + recorded_at: Instant::now(), + }); + + let result = model(&mut app, Some("deepseek-v4-flash")); + + assert!(result.message.is_some()); + assert!(app.session.turn_cache_history.is_empty()); + } + + #[test] + fn model_reset_same_model_keeps_turn_cache_history() { + let mut app = create_test_app(); + app.push_turn_cache_record(TurnCacheRecord { + input_tokens: 100, + output_tokens: 25, + cache_hit_tokens: Some(70), + cache_miss_tokens: Some(30), + reasoning_replay_tokens: Some(12), + recorded_at: Instant::now(), + }); + + let result = model(&mut app, Some("deepseek-v4-pro")); + + assert!(result.message.is_some()); + assert_eq!(app.session.turn_cache_history.len(), 1); + } + #[test] fn test_model_auto_enables_auto_thinking() { let mut app = create_test_app(); diff --git a/crates/tui/src/tui/app.rs b/crates/tui/src/tui/app.rs index dd699190..bc6204c3 100644 --- a/crates/tui/src/tui/app.rs +++ b/crates/tui/src/tui/app.rs @@ -1081,6 +1081,15 @@ impl App { } } + pub(crate) fn clear_model_scoped_telemetry(&mut self) { + self.session.last_prompt_tokens = None; + self.session.last_completion_tokens = None; + self.session.last_prompt_cache_hit_tokens = None; + self.session.last_prompt_cache_miss_tokens = None; + self.session.last_reasoning_replay_tokens = None; + self.session.turn_cache_history.clear(); + } + pub fn tr(&self, id: MessageId) -> &'static str { tr(self.ui_locale, id) } diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index b33023ac..b717568c 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -3884,11 +3884,7 @@ async fn apply_model_picker_choice( app.last_effective_model = None; app.model = model.clone(); app.update_model_compaction_budget(); - app.session.last_prompt_tokens = None; - app.session.last_completion_tokens = None; - app.session.last_prompt_cache_hit_tokens = None; - app.session.last_prompt_cache_miss_tokens = None; - app.session.last_reasoning_replay_tokens = None; + app.clear_model_scoped_telemetry(); } if effort_changed { app.reasoning_effort = effort; @@ -4006,11 +4002,16 @@ async fn switch_provider( } let new_model = config.default_model(); + let cache_scope_changed = previous_provider != target || previous_model != new_model; app.api_provider = target; app.model = new_model.clone(); app.update_model_compaction_budget(); - app.session.last_prompt_tokens = None; - app.session.last_completion_tokens = None; + if cache_scope_changed { + app.clear_model_scoped_telemetry(); + } else { + app.session.last_prompt_tokens = None; + app.session.last_completion_tokens = None; + } let _ = engine_handle.send(Op::Shutdown).await; let engine_config = build_engine_config(app, config); diff --git a/crates/tui/src/tui/ui/tests.rs b/crates/tui/src/tui/ui/tests.rs index bce4705d..608f8878 100644 --- a/crates/tui/src/tui/ui/tests.rs +++ b/crates/tui/src/tui/ui/tests.rs @@ -1,5 +1,5 @@ use super::*; -use crate::config::Config; +use crate::config::{ApiProvider, Config}; use crate::config_ui::{self, WebConfigSession, WebConfigSessionEvent}; use crate::core::engine::mock_engine_handle; use crate::tui::file_mention::{ @@ -899,6 +899,33 @@ async fn model_change_update_syncs_engine_model_before_compaction() { } } +#[tokio::test] +async fn provider_switch_clears_turn_cache_history() { + let mut app = create_test_app(); + app.push_turn_cache_record(crate::tui::app::TurnCacheRecord { + input_tokens: 100, + output_tokens: 25, + cache_hit_tokens: Some(70), + cache_miss_tokens: Some(30), + reasoning_replay_tokens: Some(12), + recorded_at: Instant::now(), + }); + let mut engine = mock_engine_handle(); + let mut config = Config::default(); + + switch_provider( + &mut app, + &mut engine.handle, + &mut config, + ApiProvider::Ollama, + None, + ) + .await; + + assert_eq!(app.api_provider, ApiProvider::Ollama); + assert!(app.session.turn_cache_history.is_empty()); +} + #[tokio::test] async fn dispatch_user_message_failed_send_clears_loading_state() { let mut app = create_test_app();