fix(runtime): name the session model in the system prompt (#3359)

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.
This commit is contained in:
twtrubiks authored and GitHub committed 2026-10-19 04:52:41 +01:00
1 parent 3cc90cfc7a
commit 66418c6127
4 files changed
+64 -15

No files matched your search

+1 -1
View File
@@ -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,
+55 -12
View File
@@ -38,8 +38,6 @@ impl From<ConfigError> 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<String>,
os_version: Option<String>,
model_family: Option<ModelFamilyIdentity>,
model: Option<String>,
append_sections: Vec<String>,
project_context: Option<ProjectContext>,
config: Option<RuntimeConfig>,
@@ -191,6 +190,12 @@ impl SystemPromptBuilder {
self
}
#[must_use]
pub fn with_model(mut self, model: impl Into<String>) -> 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<String>,
os_name: impl Into<String>,
os_version: impl Into<String>,
model: impl Into<String>,
model_family: ModelFamilyIdentity,
) -> Result<Vec<String>, 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<String>,
os_name: impl Into<String>,
os_version: impl Into<String>,
model: impl Into<String>,
model_family: ModelFamilyIdentity,
) -> Result<(Vec<String>, 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]
+5 -1
View File
@@ -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<Vec<String>, Box<dyn std::error::E
DEFAULT_DATE,
env::consts::OS,
"unknown",
model,
model_family_identity_for(model),
)?)
}
+3 -1
View File
@@ -4374,6 +4374,7 @@ fn build_agent_system_prompt(subagent_type: &str, model: &str) -> Result<Vec<Str
DEFAULT_AGENT_SYSTEM_DATE.to_string(),
std::env::consts::OS,
"unknown",
model,
model_family_identity_for(model),
)
.map_err(|error| error.to_string())?;
@@ -9553,7 +9554,8 @@ mod tests {
// then: the prompt renders a generic model family identity
assert!(prompt.contains("Model family: an AI assistant"));
assert!(!prompt.contains("Model family: Claude Opus 4.6"));
assert!(!prompt.contains("Model family: Claude"));
assert!(prompt.contains("Model: openai/gpt-4.1-mini"));
fs::remove_dir_all(root).expect("cleanup temp workspace");
}