diff --git a/src/app/runtime/context/mod.rs b/src/app/runtime/context/mod.rs index c153a92..9164975 100644 --- a/src/app/runtime/context/mod.rs +++ b/src/app/runtime/context/mod.rs @@ -11,3 +11,4 @@ //! `TurnEvent::Compacted`. pub mod tokens; +pub mod window; diff --git a/src/app/runtime/context/window.rs b/src/app/runtime/context/window.rs new file mode 100644 index 0000000..307cd92 --- /dev/null +++ b/src/app/runtime/context/window.rs @@ -0,0 +1,74 @@ +//! Single source of truth for resolving the active model's context +//! window size, replacing three copies of the same lookup that had +//! drifted (`Action::Compact`, `spawn_turn`, and `view/status.rs` each +//! had their own inline version — the status bar's copy additionally +//! displayed "?" on no match instead of falling back like the other two, +//! an inconsistency this unifies away). + +use crate::model::app_config::AppConfig; +use crate::model::settings::Settings; + +/// Resolve the context-window size (in tokens) for the currently +/// configured provider/model. +/// +/// Flow: find the `ModelRole` whose `provider`+`model` match +/// `settings` -> use its `context_window` if set -> otherwise fall back +/// to `app_config.default_context_window`. +/// +/// Return: always a concrete token count, never "unknown". +pub fn resolve(app_config: &AppConfig, settings: &Settings) -> usize { + app_config.model_roles.values() + .find(|role| role.provider == settings.provider && role.model == settings.model) + .and_then(|role| role.context_window) + .unwrap_or(app_config.default_context_window) as usize +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::model::app_config::ModelRole; + + #[test] + fn resolves_context_window_from_matching_model_role() { + let mut app_config = AppConfig::default(); + app_config.model_roles.insert("default".to_string(), ModelRole { + provider: "zen".to_string(), + model: "deepseek-v4-flash-free".to_string(), + max_tokens: None, + context_window: Some(128_000), + temperature: None, + }); + let mut settings = Settings::default(); + settings.provider = "zen".to_string(); + settings.model = "deepseek-v4-flash-free".to_string(); + + assert_eq!(resolve(&app_config, &settings), 128_000); + } + + #[test] + fn falls_back_to_default_context_window_when_no_role_matches() { + let app_config = AppConfig::default(); + let mut settings = Settings::default(); + settings.provider = "nonexistent".to_string(); + settings.model = "nonexistent-model".to_string(); + + assert_eq!(resolve(&app_config, &settings), app_config.default_context_window as usize); + } + + #[test] + fn falls_back_to_default_when_matching_role_has_no_context_window_set() { + let mut app_config = AppConfig::default(); + app_config.model_roles.insert("default".to_string(), ModelRole { + provider: "zen".to_string(), + model: "deepseek-v4-flash-free".to_string(), + max_tokens: None, + context_window: None, + temperature: None, + }); + let mut settings = Settings::default(); + settings.provider = "zen".to_string(); + settings.model = "deepseek-v4-flash-free".to_string(); + + assert_eq!(resolve(&app_config, &settings), app_config.default_context_window as usize); + } +}