Consolidate model instructions in ModelMessages (#36787)

## What changed

- Remove `ModelInfo.base_instructions` as an in-memory instruction source and use `model_messages.instructions_template` consistently for bundled, remote, fallback, and overridden model metadata.
- Preserve compatibility by promoting legacy `base_instructions` values when reading model responses and caches, and by including rendered legacy instructions when serializing `ModelsResponse` for older clients.
- Treat templates without instruction variables as literal text and retain the other model-message fields when applying instruction overrides.

This completes the consolidation proposed in https://github.com/openai/codex/pull/31302.

## Testing

- Cover legacy response and cache migration, canonical-template precedence, fallback instructions, personality rendering, overrides, and model switching.

GitOrigin-RevId: 089d986ca5e30da67db2c77a1b6a046d2cff52dc
This commit is contained in:
rhan-oai authored and copyberry committed 2026-08-03 19:30:40 +00:00
1 parent bbcf5e10fb
commit df72fdb415
27 files changed
+655 -311

No files matched your search

+50 -32
View File
@@ -50,22 +50,48 @@ pub fn with_config_overrides(mut model: ModelInfo, config: &ModelsManagerConfig)
}
if let Some(base_instructions) = &config.base_instructions {
model.base_instructions = base_instructions.clone();
clear_instruction_messages(&mut model);
let model_messages = model.model_messages.get_or_insert(ModelMessages {
instructions_template: None,
instructions_variables: None,
approvals: None,
collaboration_modes: None,
auto_review: None,
permissions: None,
token_budget: None,
});
model_messages.instructions_template = Some(base_instructions.clone());
model_messages.instructions_variables = None;
} else {
if config.personality_enabled && config.personality == Some(Personality::None) {
model.base_instructions = strip_personality_section(model.base_instructions);
if let Some(instructions_template) = model
if config.personality_enabled
&& config.personality == Some(Personality::None)
&& let Some(instructions_template) = model
.model_messages
.as_mut()
.and_then(|messages| messages.instructions_template.as_mut())
{
*instructions_template =
strip_personality_section(std::mem::take(instructions_template));
}
{
*instructions_template =
strip_personality_section(std::mem::take(instructions_template));
}
if !config.personality_enabled {
clear_instruction_messages(&mut model);
let uses_local_personality_template = model.used_fallback_model_metadata
&& matches!(
model.slug.as_str(),
"gpt-5.2-codex" | "exp-codex-personality"
);
if !config.personality_enabled
&& let Some(model_messages) = model.model_messages.as_mut()
{
if uses_local_personality_template {
model_messages.instructions_template = Some(BASE_INSTRUCTIONS.to_string());
} else {
let personality_default = model_messages
.get_personality_message(/*personality*/ None)
.unwrap_or_default();
if let Some(instructions_template) = model_messages.instructions_template.as_mut() {
*instructions_template = instructions_template
.replace(PERSONALITY_PLACEHOLDER, &personality_default);
}
}
model_messages.instructions_variables = None;
}
}
@@ -108,21 +134,6 @@ fn is_h1_heading(line: &str) -> bool {
rest.is_empty() || rest.starts_with(' ') || rest.starts_with('\t')
}
fn clear_instruction_messages(model: &mut ModelInfo) {
if let Some(model_messages) = &mut model.model_messages {
model_messages.instructions_template = None;
model_messages.instructions_variables = None;
if model_messages.approvals.is_none()
&& model_messages.collaboration_modes.is_none()
&& model_messages.auto_review.is_none()
&& model_messages.permissions.is_none()
&& model_messages.token_budget.is_none()
{
model.model_messages = None;
}
}
}
/// Build a minimal fallback model descriptor for missing/unknown slugs.
pub fn model_info_from_slug(slug: &str) -> ModelInfo {
warn!("Unknown model {slug} is used. This will use fallback model metadata.");
@@ -141,8 +152,7 @@ pub fn model_info_from_slug(slug: &str) -> ModelInfo {
default_service_tier: None,
availability_nux: None,
upgrade: None,
base_instructions: BASE_INSTRUCTIONS.to_string(),
model_messages: local_personality_messages_for_slug(slug),
model_messages: Some(local_model_messages_for_slug(slug)),
include_skills_usage_instructions: false,
supports_reasoning_summary_parameter: true,
default_reasoning_summary: ReasoningSummary::Auto,
@@ -169,9 +179,9 @@ pub fn model_info_from_slug(slug: &str) -> ModelInfo {
}
}
fn local_personality_messages_for_slug(slug: &str) -> Option<ModelMessages> {
fn local_model_messages_for_slug(slug: &str) -> ModelMessages {
match slug {
"gpt-5.2-codex" | "exp-codex-personality" => Some(ModelMessages {
"gpt-5.2-codex" | "exp-codex-personality" => ModelMessages {
instructions_template: Some(format!(
"{DEFAULT_PERSONALITY_HEADER}\n\n{PERSONALITY_PLACEHOLDER}\n\n{BASE_INSTRUCTIONS}"
)),
@@ -185,8 +195,16 @@ fn local_personality_messages_for_slug(slug: &str) -> Option<ModelMessages> {
auto_review: None,
permissions: None,
token_budget: None,
}),
_ => None,
},
_ => ModelMessages {
instructions_template: Some(BASE_INSTRUCTIONS.to_string()),
instructions_variables: None,
approvals: None,
collaboration_modes: None,
auto_review: None,
permissions: None,
token_budget: None,
},
}
}