From 2d91c98680bb78bf11d1e478794039fc27971b73 Mon Sep 17 00:00:00 2001 From: twtrubiks Date: Thu, 15 Oct 2026 07:18:17 +0800 Subject: [PATCH] fix(runtime): name the session model in the system prompt The environment section rendered "Model family: Claude Opus 4.6" for every Anthropic model, so claude-opus-5-5, claude-sonnet-4-6, and any other Claude model told users it was Opus 4.6. Render a neutral "Claude" family label followed by the resolved model ID, and rebuild the system prompt on /model so the line follows a mid-session switch instead of keeping the startup model. --- rust/crates/runtime/src/lib.rs | 2 +- rust/crates/runtime/src/prompt.rs | 67 +++++++++++++++++++----- rust/crates/rusty-claude-cli/src/main.rs | 6 ++- rust/crates/tools/src/lib.rs | 4 +- 4 files changed, 64 insertions(+), 15 deletions(-) diff --git a/rust/crates/runtime/src/lib.rs b/rust/crates/runtime/src/lib.rs index fd2aee4e1b..50959ac300 100644 --- a/rust/crates/runtime/src/lib.rs +++ b/rust/crates/runtime/src/lib.rs @@ -151,7 +151,7 @@ pub use policy_engine::{ pub use prompt::{ load_system_prompt, load_system_prompt_with_context, prepend_bullets, ContextFile, ModelFamilyIdentity, ProjectContext, PromptBuildError, SystemPromptBuilder, - FRONTIER_MODEL_NAME, SYSTEM_PROMPT_DYNAMIC_BOUNDARY, + SYSTEM_PROMPT_DYNAMIC_BOUNDARY, }; pub use recovery_recipes::{ attempt_recovery, recipe_for, EscalationPolicy, FailureScenario, RecoveryAttemptState, diff --git a/rust/crates/runtime/src/prompt.rs b/rust/crates/runtime/src/prompt.rs index 9868311254..bb8052b583 100644 --- a/rust/crates/runtime/src/prompt.rs +++ b/rust/crates/runtime/src/prompt.rs @@ -38,8 +38,6 @@ impl From for PromptBuildError { /// Marker separating static prompt scaffolding from dynamic runtime context. pub const SYSTEM_PROMPT_DYNAMIC_BOUNDARY: &str = "__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__"; -/// Human-readable default frontier model name embedded into generated prompts. -pub const FRONTIER_MODEL_NAME: &str = "Claude Opus 4.6"; const MAX_INSTRUCTION_FILE_CHARS: usize = 4_000; const MAX_TOTAL_INSTRUCTION_CHARS: usize = 12_000; const MAX_GIT_DIFF_CHARS: usize = 50_000; @@ -56,7 +54,7 @@ impl ModelFamilyIdentity { #[must_use] pub const fn family_label(self) -> &'static str { match self { - Self::Claude => FRONTIER_MODEL_NAME, + Self::Claude => "Claude", Self::Generic => "an AI assistant", } } @@ -158,6 +156,7 @@ pub struct SystemPromptBuilder { os_name: Option, os_version: Option, model_family: Option, + model: Option, append_sections: Vec, project_context: Option, config: Option, @@ -191,6 +190,12 @@ impl SystemPromptBuilder { self } + #[must_use] + pub fn with_model(mut self, model: impl Into) -> Self { + self.model = Some(model.into()); + self + } + #[must_use] pub fn with_project_context(mut self, project_context: ProjectContext) -> Self { self.project_context = Some(project_context); @@ -294,9 +299,11 @@ impl SystemPromptBuilder { |context| context.current_date.clone(), ); let identity = self.model_family.unwrap_or_default(); - let mut lines = vec!["# Environment context".to_string()]; - lines.extend(prepend_bullets(vec![ - format!("Model family: {}", identity.family_label()), + let mut items = vec![format!("Model family: {}", identity.family_label())]; + if let Some(model) = &self.model { + items.push(format!("Model: {model}")); + } + items.extend([ format!("Working directory: {cwd}"), format!("Date: {date}"), format!( @@ -304,7 +311,9 @@ impl SystemPromptBuilder { self.os_name.as_deref().unwrap_or("unknown"), self.os_version.as_deref().unwrap_or("unknown") ), - ])); + ]); + let mut lines = vec!["# Environment context".to_string()]; + lines.extend(prepend_bullets(items)); lines.join("\n") } } @@ -670,11 +679,18 @@ pub fn load_system_prompt( current_date: impl Into, os_name: impl Into, os_version: impl Into, + model: impl Into, model_family: ModelFamilyIdentity, ) -> Result, PromptBuildError> { let cwd = cwd.into(); - let (sections, _) = - load_system_prompt_with_context(cwd, current_date, os_name, os_version, model_family)?; + let (sections, _) = load_system_prompt_with_context( + cwd, + current_date, + os_name, + os_version, + model, + model_family, + )?; Ok(sections) } @@ -684,6 +700,7 @@ pub fn load_system_prompt_with_context( current_date: impl Into, os_name: impl Into, os_version: impl Into, + model: impl Into, model_family: ModelFamilyIdentity, ) -> Result<(Vec, ProjectContext), PromptBuildError> { let cwd = cwd.into(); @@ -693,6 +710,7 @@ pub fn load_system_prompt_with_context( let memory_enabled = config.feature_config().auto_memory_enabled(); let mut builder = SystemPromptBuilder::new() .with_os(os_name, os_version) + .with_model(model) .with_model_family(model_family) .with_project_context(project_context.clone()) .with_runtime_config(config); @@ -1255,6 +1273,7 @@ mod tests { "2026-03-31", "linux", "6.8", + "claude-opus-4-6", ModelFamilyIdentity::Claude, ) .expect("system prompt should load") @@ -1304,6 +1323,7 @@ mod tests { "2026-03-31", "linux", "6.8", + "claude-opus-4-6", ModelFamilyIdentity::Claude, ) .expect("system prompt should load") @@ -1341,7 +1361,30 @@ mod tests { .render(); // then: the Claude model family label is preserved by default - assert!(prompt.contains("Model family: Claude Opus 4.6")); + assert!(prompt.contains(" - Model family: Claude\n")); + assert!(!prompt.contains("Model:")); + } + + #[test] + fn renders_configured_model_instead_of_a_fixed_claude_version() { + // given: a prompt builder for the model the session actually uses + let project_context = ProjectContext { + cwd: PathBuf::from("/tmp/project"), + current_date: "2026-03-31".to_string(), + ..ProjectContext::default() + }; + + // when: rendering the system prompt environment section + let prompt = SystemPromptBuilder::new() + .with_os("linux", "6.8") + .with_model("claude-sonnet-4-6") + .with_model_family(ModelFamilyIdentity::Claude) + .with_project_context(project_context) + .render(); + + // then: the prompt names that model rather than a hardcoded Opus release + assert!(prompt.contains(" - Model family: Claude\n - Model: claude-sonnet-4-6\n")); + assert!(!prompt.contains("Opus")); } #[test] @@ -1364,9 +1407,9 @@ mod tests { .find(|line| line.contains("Model family:")) .expect("model family line should render"); - // then: the model family line is neutral and excludes Claude Opus 4.6 + // then: the model family line is neutral and excludes Claude assert_eq!(model_family_line, " - Model family: an AI assistant"); - assert!(!model_family_line.contains("Claude Opus 4.6")); + assert!(!model_family_line.contains("Claude")); } #[test] diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 934fce0a1c..0f673d4ed8 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -5012,6 +5012,7 @@ fn print_system_prompt( date, env::consts::OS, "unknown", + model, model_family_identity_for(model), )?; let (project_root, _) = @@ -8749,11 +8750,12 @@ impl LiveCli { let previous = self.model.clone(); let session = self.runtime.session().clone(); let message_count = session.messages.len(); + let system_prompt = build_system_prompt(&model)?; let runtime = build_runtime( session, &self.session.id, model.clone(), - self.system_prompt.clone(), + system_prompt.clone(), true, true, TerminalTail::new(), @@ -8763,6 +8765,7 @@ impl LiveCli { )?; self.replace_runtime(runtime)?; self.model.clone_from(&model); + self.system_prompt = system_prompt; println!( "{}", format_model_switch_report(&previous, &model, message_count) @@ -12360,6 +12363,7 @@ fn build_system_prompt(model: &str) -> Result, Box Result