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

@@ -5,6 +5,7 @@ use codex_models_manager::client_version_to_whole;
use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ConfigShellToolType;
use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelMessages;
use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::ModelPreset;
use codex_protocol::openai_models::ModelVisibility; use codex_protocol::openai_models::ModelVisibility;
use codex_protocol::openai_models::TruncationPolicyConfig; use codex_protocol::openai_models::TruncationPolicyConfig;
@@ -32,8 +33,15 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo {
service_tiers: preset.service_tiers.clone(), service_tiers: preset.service_tiers.clone(),
default_service_tier: preset.default_service_tier.clone(), default_service_tier: preset.default_service_tier.clone(),
upgrade: preset.upgrade.as_ref().map(Into::into), upgrade: preset.upgrade.as_ref().map(Into::into),
base_instructions: "base instructions".to_string(), model_messages: Some(ModelMessages {
model_messages: None, instructions_template: Some("base instructions".to_string()),
instructions_variables: None,
approvals: None,
collaboration_modes: None,
auto_review: None,
permissions: None,
token_budget: None,
}),
include_skills_usage_instructions: false, include_skills_usage_instructions: false,
supports_reasoning_summary_parameter: true, supports_reasoning_summary_parameter: true,
default_reasoning_summary: ReasoningSummary::Auto, default_reasoning_summary: ReasoningSummary::Auto,
@@ -168,7 +168,6 @@ async fn list_models_uses_chatgpt_remote_catalog_as_source_of_truth() -> Result<
"supported_in_api": true, "supported_in_api": true,
"priority": 0, "priority": 0,
"upgrade": null, "upgrade": null,
"base_instructions": "base instructions",
"support_verbosity": false, "support_verbosity": false,
"default_verbosity": null, "default_verbosity": null,
"apply_patch_tool_type": null, "apply_patch_tool_type": null,
@@ -208,7 +208,6 @@ mod tests {
"supported_in_api": true, "supported_in_api": true,
"priority": 1, "priority": 1,
"upgrade": null, "upgrade": null,
"base_instructions": "base instructions",
"support_verbosity": false, "support_verbosity": false,
"default_verbosity": null, "default_verbosity": null,
"apply_patch_tool_type": null, "apply_patch_tool_type": null,
@@ -79,7 +79,6 @@ async fn models_client_hits_models_endpoint() {
service_tiers: Vec::new(), service_tiers: Vec::new(),
default_service_tier: None, default_service_tier: None,
upgrade: None, upgrade: None,
base_instructions: "base instructions".to_string(),
model_messages: None, model_messages: None,
include_skills_usage_instructions: false, include_skills_usage_instructions: false,
supports_reasoning_summary_parameter: true, supports_reasoning_summary_parameter: true,
-1
View File
@@ -258,7 +258,6 @@ fn test_model_info() -> ModelInfo {
"supported_in_api": true, "supported_in_api": true,
"priority": 1, "priority": 1,
"upgrade": null, "upgrade": null,
"base_instructions": "base instructions",
"model_messages": null, "model_messages": null,
"support_verbosity": false, "support_verbosity": false,
"default_verbosity": null, "default_verbosity": null,
+1 -1
View File
@@ -632,7 +632,7 @@ impl Session {
// Resolve base instructions for the session. Priority order: // Resolve base instructions for the session. Priority order:
// 1. config.base_instructions override // 1. config.base_instructions override
// 2. conversation history => session_meta.base_instructions // 2. conversation history => session_meta.base_instructions
// 3. base_instructions for current model // 3. rendered instructions_template for current model
let model_info = models_manager let model_info = models_manager
.get_model_info(model.as_str(), &config.to_models_manager_config()) .get_model_info(model.as_str(), &config.to_models_manager_config())
.await; .await;
+4 -3
View File
@@ -1465,20 +1465,21 @@ async fn get_base_instructions_no_user_content() {
for test_case in test_cases { for test_case in test_cases {
let model_info = model_info_for_slug(test_case.slug, &config); let model_info = model_info_for_slug(test_case.slug, &config);
let model_instructions = model_info.get_model_instructions(config.personality);
if test_case.expects_apply_patch_description { if test_case.expects_apply_patch_description {
assert_eq!( assert_eq!(
model_info.base_instructions.as_str(), model_instructions.as_str(),
prompt_with_apply_patch_instructions prompt_with_apply_patch_instructions
); );
} }
{ {
let mut state = session.state.lock().await; let mut state = session.state.lock().await;
state.session_configuration.base_instructions = model_info.base_instructions.clone(); state.session_configuration.base_instructions = model_instructions.clone();
} }
let base_instructions = session.get_base_instructions().await; let base_instructions = session.get_base_instructions().await;
assert_eq!(base_instructions.text, model_info.base_instructions); assert_eq!(base_instructions.text, model_instructions);
} }
} }
-1
View File
@@ -268,7 +268,6 @@ fn remote_model_with_auto_review_override(slug: &str, review_model: &str) -> Mod
service_tiers: Vec::new(), service_tiers: Vec::new(),
default_service_tier: None, default_service_tier: None,
upgrade: None, upgrade: None,
base_instructions: "base instructions".to_string(),
model_messages: None, model_messages: None,
include_skills_usage_instructions: false, include_skills_usage_instructions: false,
supports_reasoning_summary_parameter: true, supports_reasoning_summary_parameter: true,
@@ -97,7 +97,6 @@ fn test_model_info(
service_tiers: Vec::new(), service_tiers: Vec::new(),
default_service_tier: None, default_service_tier: None,
upgrade: None, upgrade: None,
base_instructions: "base instructions".to_string(),
model_messages: None, model_messages: None,
include_skills_usage_instructions: false, include_skills_usage_instructions: false,
supports_reasoning_summary_parameter: true, supports_reasoning_summary_parameter: true,
@@ -961,7 +960,6 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result<
service_tiers: Vec::new(), service_tiers: Vec::new(),
default_service_tier: None, default_service_tier: None,
upgrade: None, upgrade: None,
base_instructions: "base instructions".to_string(),
model_messages: None, model_messages: None,
include_skills_usage_instructions: false, include_skills_usage_instructions: false,
supports_reasoning_summary_parameter: true, supports_reasoning_summary_parameter: true,
+36 -7
View File
@@ -13,6 +13,7 @@ use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ConfigShellToolType;
use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelMessages;
use codex_protocol::openai_models::ModelVisibility; use codex_protocol::openai_models::ModelVisibility;
use codex_protocol::openai_models::ModelsResponse; use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::openai_models::ReasoningEffort;
@@ -34,6 +35,7 @@ use core_test_support::wait_for_event;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use serde_json::json;
use wiremock::MockServer; use wiremock::MockServer;
const ETAG: &str = "\"models-etag-ttl\""; const ETAG: &str = "\"models-etag-ttl\"";
@@ -272,6 +274,11 @@ async fn matching_models_etag_renews_cache_after_half_its_lifetime() -> Result<(
async fn uses_cache_when_version_matches() -> Result<()> { async fn uses_cache_when_version_matches() -> Result<()> {
let server = MockServer::start().await; let server = MockServer::start().await;
let cached_model = test_remote_model(VERSIONED_MODEL, /*priority*/ 1); let cached_model = test_remote_model(VERSIONED_MODEL, /*priority*/ 1);
let response = responses::mount_sse_once(
&server,
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
)
.await;
let models_mock = responses::mount_models_once( let models_mock = responses::mount_models_once(
&server, &server,
ModelsResponse { ModelsResponse {
@@ -283,20 +290,30 @@ async fn uses_cache_when_version_matches() -> Result<()> {
let mut builder = test_codex().with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()); let mut builder = test_codex().with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing());
builder = builder builder = builder
.with_pre_build_hook(move |home| { .with_pre_build_hook(move |home| {
let cache = ModelsCache { let mut cache = serde_json::to_value(ModelsCache {
fetched_at: Utc::now(), fetched_at: Utc::now(),
etag: None, etag: None,
client_version: Some(client_version_to_whole()), client_version: Some(client_version_to_whole()),
models: vec![cached_model], models: vec![cached_model],
}; })
let cache_path = home.join(CACHE_FILE); .expect("serialize cache");
write_cache_sync(&cache_path, &cache).expect("write cache"); let cached_model = cache["models"][0]
.as_object_mut()
.expect("cached model should serialize as an object");
cached_model.remove("model_messages");
cached_model.insert("base_instructions".to_string(), json!("base instructions"));
std::fs::write(
home.join(CACHE_FILE),
serde_json::to_vec_pretty(&cache).expect("serialize legacy cache"),
)
.expect("write legacy cache");
}) })
.with_config(|config| { .with_config(|config| {
config.model = Some(VERSIONED_MODEL.to_string());
config.model_provider.request_max_retries = Some(0); config.model_provider.request_max_retries = Some(0);
}); });
let test = builder.build(&server).await?; let test = builder.build_with_auto_env(&server).await?;
let models_manager = test.thread_manager.get_models_manager(); let models_manager = test.thread_manager.get_models_manager();
let models = models_manager let models = models_manager
.list_models( .list_models(
@@ -314,6 +331,11 @@ async fn uses_cache_when_version_matches() -> Result<()> {
0, 0,
"/models should not be called when cache version matches" "/models should not be called when cache version matches"
); );
test.submit_turn("use cached model").await?;
assert_eq!(
response.single_request().instructions_text(),
"base instructions"
);
Ok(()) Ok(())
} }
@@ -480,8 +502,15 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo {
service_tiers: Vec::new(), service_tiers: Vec::new(),
default_service_tier: None, default_service_tier: None,
upgrade: None, upgrade: None,
base_instructions: "base instructions".to_string(), model_messages: Some(ModelMessages {
model_messages: None, instructions_template: Some("base instructions".to_string()),
instructions_variables: None,
approvals: None,
collaboration_modes: None,
auto_review: None,
permissions: None,
token_budget: None,
}),
include_skills_usage_instructions: false, include_skills_usage_instructions: false,
supports_reasoning_summary_parameter: true, supports_reasoning_summary_parameter: true,
default_reasoning_summary: ReasoningSummary::Auto, default_reasoning_summary: ReasoningSummary::Auto,
@@ -110,6 +110,7 @@ async fn catalog_approval_message_is_sent_in_initial_permissions() -> Result<()>
) )
.await; .await;
let model_slug = "catalog-approvals-model"; let model_slug = "catalog-approvals-model";
let override_instructions = "literal {{ personality }} override";
let model = model_with_approval_messages( let model = model_with_approval_messages(
model_slug, model_slug,
"catalog user approval instructions", "catalog user approval instructions",
@@ -118,16 +119,19 @@ async fn catalog_approval_message_is_sent_in_initial_permissions() -> Result<()>
let mut builder = test_codex() let mut builder = test_codex()
.with_model(model_slug) .with_model(model_slug)
.with_config(move |config| { .with_config(move |config| {
config.base_instructions = Some(override_instructions.to_string());
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
config.model_catalog = Some(ModelsResponse { config.model_catalog = Some(ModelsResponse {
models: vec![model], models: vec![model],
}); });
}); });
let test = builder.build(&server).await?; let test = builder.build_with_auto_env(&server).await?;
submit_text_turn(&test, "hello").await?; submit_text_turn(&test, "hello").await?;
let permissions = permissions_texts(&req.single_request()); let request = req.single_request();
assert_eq!(request.instructions_text(), override_instructions);
let permissions = permissions_texts(&request);
assert_eq!(permissions.len(), 1); assert_eq!(permissions.len(), 1);
assert!(permissions[0].contains("catalog user approval instructions")); assert!(permissions[0].contains("catalog user approval instructions"));
assert!(permissions[0].contains("Filesystem sandboxing defines")); assert!(permissions[0].contains("Filesystem sandboxing defines"));
+55 -55
View File
@@ -1,7 +1,9 @@
use codex_config::types::Personality; use codex_config::types::Personality;
use codex_features::Feature; use codex_features::Feature;
use codex_models_manager::bundled_models_response;
use codex_models_manager::manager::RefreshStrategy; use codex_models_manager::manager::RefreshStrategy;
use codex_models_manager::manager::SharedModelsManager; use codex_models_manager::manager::SharedModelsManager;
use codex_models_manager::model_info::BASE_INSTRUCTIONS;
use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ConfigShellToolType;
@@ -18,7 +20,6 @@ use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op; use codex_protocol::protocol::Op;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::load_default_config_for_test;
use core_test_support::responses::mount_models_once; use core_test_support::responses::mount_models_once;
use core_test_support::responses::mount_sse_once; use core_test_support::responses::mount_sse_once;
use core_test_support::responses::mount_sse_sequence; use core_test_support::responses::mount_sse_sequence;
@@ -31,7 +32,6 @@ use core_test_support::test_codex::test_codex;
use core_test_support::test_codex::turn_permission_fields; use core_test_support::test_codex::turn_permission_fields;
use core_test_support::wait_for_event; use core_test_support::wait_for_event;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use tempfile::TempDir;
use tokio::time::Duration; use tokio::time::Duration;
use tokio::time::Instant; use tokio::time::Instant;
use tokio::time::sleep; use tokio::time::sleep;
@@ -88,43 +88,6 @@ fn read_only_text_turn_with_personality(
} }
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn personality_does_not_mutate_base_instructions_without_template() {
let codex_home = TempDir::new().expect("create temp dir");
let mut config = load_default_config_for_test(&codex_home).await;
config
.features
.enable(Feature::Personality)
.expect("test config should allow feature update");
config.personality = Some(Personality::Friendly);
let model_info = codex_core::test_support::construct_model_info_offline("gpt-5.4", &config);
assert_eq!(
model_info.get_model_instructions(config.personality),
model_info.base_instructions
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn base_instructions_override_disables_personality_template() {
let codex_home = TempDir::new().expect("create temp dir");
let mut config = load_default_config_for_test(&codex_home).await;
config
.features
.enable(Feature::Personality)
.expect("test config should allow feature update");
config.personality = Some(Personality::Friendly);
config.base_instructions = Some("override instructions".to_string());
let model_info = codex_core::test_support::construct_model_info_offline("gpt-5.4", &config);
assert_eq!(model_info.base_instructions, "override instructions");
assert_eq!(
model_info.get_model_instructions(config.personality),
"override instructions"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn user_turn_personality_none_does_not_add_update_message() -> anyhow::Result<()> { async fn user_turn_personality_none_does_not_add_update_message() -> anyhow::Result<()> {
skip_if_no_network!(Ok(())); skip_if_no_network!(Ok(()));
@@ -267,8 +230,10 @@ async fn config_personality_none_strips_baked_personality_section() -> anyhow::R
let resp_mock = mount_sse_once(&server, sse_completed("resp-1")).await; let resp_mock = mount_sse_once(&server, sse_completed("resp-1")).await;
let mut builder = test_codex() let mut builder = test_codex()
.with_model_info_override("gpt-5.4", |model_info| { .with_model_info_override("gpt-5.4", |model_info| {
model_info.base_instructions = "Base instructions\n# Personality\nBaked personality\n## Writing Style\nNested writing style\n# General\nGeneral instructions".to_string(); if let Some(model_messages) = model_info.model_messages.as_mut() {
model_info.model_messages = None; model_messages.instructions_template = Some("Base instructions\n# Personality\nBaked personality\n## Writing Style\nNested writing style\n# General\nGeneral instructions".to_string());
model_messages.instructions_variables = None;
}
}) })
.with_config(|config| { .with_config(|config| {
config config
@@ -516,21 +481,53 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn instructions_uses_base_if_feature_disabled() -> anyhow::Result<()> { async fn disabled_personality_sends_remote_default_instructions() -> anyhow::Result<()> {
let codex_home = TempDir::new().expect("create temp dir"); skip_if_no_network!(Ok(()));
let mut config = load_default_config_for_test(&codex_home).await;
config let server = MockServer::start().await;
.features let remote_slug = "remote-disabled-personality";
.disable(Feature::Personality) let default_personality = "default remote personality";
.expect("test config should allow feature update"); let friendly_personality = "friendly remote personality";
config.personality = Some(Personality::Friendly); let mut remote_model = bundled_models_response()?
.models
.into_iter()
.find(|model| model.slug == "gpt-5.4")
.expect("bundled gpt-5.4 model");
remote_model.slug = remote_slug.to_string();
if let Some(model_messages) = remote_model.model_messages.as_mut() {
model_messages.instructions_template = Some("remote base\n{{ personality }}".to_string());
model_messages.instructions_variables = Some(ModelInstructionsVariables {
personality_default: Some(default_personality.to_string()),
personality_friendly: Some(friendly_personality.to_string()),
personality_pragmatic: Some("pragmatic remote personality".to_string()),
});
}
mount_models_once(
&server,
ModelsResponse {
models: vec![remote_model],
},
)
.await;
let response = mount_sse_once(&server, sse_completed("resp-1")).await;
let mut builder = test_codex()
.with_auth(codex_login::CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_config(move |config| {
config
.features
.disable(Feature::Personality)
.expect("test config should allow feature update");
config.model = Some(remote_slug.to_string());
config.personality = Some(Personality::Friendly);
});
let test = builder.build_with_auto_env(&server).await?;
test.submit_turn("use the default personality").await?;
let model_info = codex_core::test_support::construct_model_info_offline("gpt-5.4", &config);
assert_eq!( assert_eq!(
model_info.get_model_instructions(config.personality), response.single_request().instructions_text(),
model_info.base_instructions format!("remote base\n{default_personality}")
); );
Ok(()) Ok(())
} }
@@ -587,6 +584,11 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()>
let requests = resp_mock.requests(); let requests = resp_mock.requests();
assert_eq!(requests.len(), 2, "expected two requests"); assert_eq!(requests.len(), 2, "expected two requests");
assert!(
requests
.iter()
.all(|request| request.instructions_text() == BASE_INSTRUCTIONS)
);
let request = requests let request = requests
.last() .last()
.expect("expected personality update request"); .expect("expected personality update request");
@@ -631,7 +633,6 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow
service_tiers: Vec::new(), service_tiers: Vec::new(),
default_service_tier: None, default_service_tier: None,
upgrade: None, upgrade: None,
base_instructions: "base instructions".to_string(),
model_messages: Some(ModelMessages { model_messages: Some(ModelMessages {
instructions_template: Some("Base instructions\n{{ personality }}\n".to_string()), instructions_template: Some("Base instructions\n{{ personality }}\n".to_string()),
instructions_variables: Some(ModelInstructionsVariables { instructions_variables: Some(ModelInstructionsVariables {
@@ -752,7 +753,6 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
service_tiers: Vec::new(), service_tiers: Vec::new(),
default_service_tier: None, default_service_tier: None,
upgrade: None, upgrade: None,
base_instructions: "base instructions".to_string(),
model_messages: Some(ModelMessages { model_messages: Some(ModelMessages {
instructions_template: Some("Base instructions\n{{ personality }}\n".to_string()), instructions_template: Some("Base instructions\n{{ personality }}\n".to_string()),
instructions_variables: Some(ModelInstructionsVariables { instructions_variables: Some(ModelInstructionsVariables {
+3 -3
View File
@@ -154,7 +154,7 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> {
}) })
.build(&server) .build(&server)
.await?; .await?;
let base_instructions = thread_manager let model_info = thread_manager
.get_models_manager() .get_models_manager()
.get_model_info( .get_model_info(
config config
@@ -163,8 +163,8 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> {
.expect("test config should have a model"), .expect("test config should have a model"),
&config.to_models_manager_config(), &config.to_models_manager_config(),
) )
.await .await;
.base_instructions; let base_instructions = model_info.get_model_instructions(config.personality);
codex codex
.submit(Op::UserInput { .submit(Op::UserInput {
+127 -26
View File
@@ -6,10 +6,12 @@ use codex_model_provider_info::built_in_model_providers;
use codex_models_manager::bundled_models_response; use codex_models_manager::bundled_models_response;
use codex_models_manager::manager::RefreshStrategy; use codex_models_manager::manager::RefreshStrategy;
use codex_models_manager::manager::SharedModelsManager; use codex_models_manager::manager::SharedModelsManager;
use codex_models_manager::model_info::BASE_INSTRUCTIONS;
use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ConfigShellToolType;
use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelMessages;
use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::ModelPreset;
use codex_protocol::openai_models::ModelVisibility; use codex_protocol::openai_models::ModelVisibility;
use codex_protocol::openai_models::ModelsResponse; use codex_protocol::openai_models::ModelsResponse;
@@ -33,6 +35,7 @@ use core_test_support::responses::mount_models_once_with_delay;
use core_test_support::responses::mount_sse_once; use core_test_support::responses::mount_sse_once;
use core_test_support::responses::mount_sse_sequence; use core_test_support::responses::mount_sse_sequence;
use core_test_support::responses::sse; use core_test_support::responses::sse;
use core_test_support::responses::start_mock_server;
use core_test_support::skip_if_no_network; use core_test_support::skip_if_no_network;
use core_test_support::skip_if_sandbox; use core_test_support::skip_if_sandbox;
use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::TestCodex;
@@ -49,10 +52,36 @@ use tokio::time::Instant;
use tokio::time::sleep; use tokio::time::sleep;
use tokio::time::timeout; use tokio::time::timeout;
use wiremock::BodyPrintLimit; use wiremock::BodyPrintLimit;
use wiremock::Mock;
use wiremock::MockServer; use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::matchers::method;
use wiremock::matchers::path;
const REMOTE_MODEL_SLUG: &str = "codex-test"; const REMOTE_MODEL_SLUG: &str = "codex-test";
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn unknown_model_sends_builtin_instructions() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let response = mount_sse_once(
&server,
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
)
.await;
let mut builder = test_codex().with_model("future-custom-model");
let test = builder.build_with_auto_env(&server).await?;
test.submit_turn("use fallback model metadata").await?;
assert_eq!(
response.single_request().instructions_text(),
BASE_INSTRUCTIONS
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn remote_models_get_model_info_uses_longest_matching_prefix() -> Result<()> { async fn remote_models_get_model_info_uses_longest_matching_prefix() -> Result<()> {
skip_if_no_network!(Ok(())); skip_if_no_network!(Ok(()));
@@ -73,12 +102,28 @@ async fn remote_models_get_model_info_uses_longest_matching_prefix() -> Result<(
); );
let specific = ModelInfo { let specific = ModelInfo {
display_name: "GPT 5.3 Codex".to_string(), display_name: "GPT 5.3 Codex".to_string(),
base_instructions: "use specific prefix".to_string(), model_messages: Some(ModelMessages {
instructions_template: Some("use specific prefix".to_string()),
instructions_variables: None,
approvals: None,
collaboration_modes: None,
auto_review: None,
permissions: None,
token_budget: None,
}),
..specific ..specific
}; };
let generic = ModelInfo { let generic = ModelInfo {
display_name: "GPT 5.3".to_string(), display_name: "GPT 5.3".to_string(),
base_instructions: "use generic prefix".to_string(), model_messages: Some(ModelMessages {
instructions_template: Some("use generic prefix".to_string()),
instructions_variables: None,
approvals: None,
collaboration_modes: None,
auto_review: None,
permissions: None,
token_budget: None,
}),
..generic ..generic
}; };
mount_models_once( mount_models_once(
@@ -115,7 +160,10 @@ async fn remote_models_get_model_info_uses_longest_matching_prefix() -> Result<(
.await; .await;
assert_eq!(model_info.slug, "gpt-5.3-codex-test"); assert_eq!(model_info.slug, "gpt-5.3-codex-test");
assert_eq!(model_info.base_instructions, specific.base_instructions); assert_eq!(
model_info.get_model_instructions(config.personality),
specific.get_model_instructions(config.personality)
);
Ok(()) Ok(())
} }
@@ -487,7 +535,6 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> {
service_tiers: Vec::new(), service_tiers: Vec::new(),
default_service_tier: None, default_service_tier: None,
upgrade: None, upgrade: None,
base_instructions: "base instructions".to_string(),
model_messages: None, model_messages: None,
include_skills_usage_instructions: false, include_skills_usage_instructions: false,
supports_reasoning_summary_parameter: true, supports_reasoning_summary_parameter: true,
@@ -705,7 +752,7 @@ async fn remote_models_truncation_policy_with_tool_output_override() -> Result<(
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn remote_models_apply_remote_base_instructions() -> Result<()> { async fn remote_models_apply_legacy_instructions() -> Result<()> {
skip_if_no_network!(Ok(())); skip_if_no_network!(Ok(()));
skip_if_sandbox!(Ok(())); skip_if_sandbox!(Ok(()));
@@ -716,7 +763,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
let model = "test-gpt-5-remote"; let model = "test-gpt-5-remote";
let remote_base = "Use the remote base instructions only."; let remote_instructions = "Use the remote instructions template only.";
let remote_model = ModelInfo { let remote_model = ModelInfo {
slug: model.to_string(), slug: model.to_string(),
display_name: "Parallel Remote".to_string(), display_name: "Parallel Remote".to_string(),
@@ -741,8 +788,15 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
service_tiers: Vec::new(), service_tiers: Vec::new(),
default_service_tier: None, default_service_tier: None,
upgrade: None, upgrade: None,
base_instructions: remote_base.to_string(), model_messages: Some(ModelMessages {
model_messages: None, instructions_template: Some(remote_instructions.to_string()),
instructions_variables: None,
approvals: None,
collaboration_modes: None,
auto_review: None,
permissions: None,
token_budget: None,
}),
include_skills_usage_instructions: false, include_skills_usage_instructions: false,
supports_reasoning_summary_parameter: true, supports_reasoning_summary_parameter: true,
default_reasoning_summary: ReasoningSummary::Auto, default_reasoning_summary: ReasoningSummary::Auto,
@@ -761,21 +815,34 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
effective_context_window_percent: 95, effective_context_window_percent: 95,
experimental_supported_tools: Vec::new(), experimental_supported_tools: Vec::new(),
}; };
mount_models_once( let mut models_response = serde_json::to_value(ModelsResponse {
&server, models: vec![remote_model],
ModelsResponse { })?;
models: vec![remote_model], models_response["models"][0]
}, .as_object_mut()
) .expect("model should serialize as an object")
.await; .remove("model_messages");
Mock::given(method("GET"))
.and(path("/v1/models"))
.respond_with(ResponseTemplate::new(200).set_body_json(models_response))
.up_to_n_times(1)
.mount(&server)
.await;
let response_mock = mount_sse_once( let response_mock = mount_sse_sequence(
&server, &server,
sse(vec![ vec![
ev_response_created("resp-1"), sse(vec![
ev_assistant_message("msg-1", "done"), ev_response_created("resp-1"),
ev_completed("resp-1"), ev_assistant_message("msg-1", "done"),
]), ev_completed("resp-1"),
]),
sse(vec![
ev_response_created("resp-2"),
ev_assistant_message("msg-2", "done"),
ev_completed("resp-2"),
]),
],
) )
.await; .await;
@@ -795,6 +862,31 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
let models_manager = thread_manager.get_models_manager(); let models_manager = thread_manager.get_models_manager();
wait_for_model_available(&models_manager, model).await; wait_for_model_available(&models_manager, model).await;
let cwd_path = cwd.abs();
let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, cwd_path.as_path());
codex
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello base".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(local_selections(cwd_path.clone())),
approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy),
permission_profile,
summary: Some(ReasoningSummary::Auto),
..Default::default()
},
})
.await?;
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
core_test_support::submit_thread_settings( core_test_support::submit_thread_settings(
&codex, &codex,
codex_protocol::protocol::ThreadSettingsOverrides { codex_protocol::protocol::ThreadSettingsOverrides {
@@ -804,7 +896,6 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
) )
.await?; .await?;
let cwd_path = cwd.abs();
let (sandbox_policy, permission_profile) = let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, cwd_path.as_path()); turn_permission_fields(PermissionProfile::Disabled, cwd_path.as_path());
codex codex
@@ -832,9 +923,20 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
let base_model_info = models_manager let base_model_info = models_manager
.get_model_info("gpt-5.2", &config.to_models_manager_config()) .get_model_info("gpt-5.2", &config.to_models_manager_config())
.await; .await;
let body = response_mock.single_request().body_json(); let requests = response_mock.requests();
let instructions = body["instructions"].as_str().unwrap(); assert_eq!(requests.len(), 2, "expected two model requests");
assert_eq!(instructions, base_model_info.base_instructions); let request = requests.last().expect("expected second model request");
assert_eq!(
request.instructions_text(),
base_model_info.get_model_instructions(config.personality)
);
assert!(
request
.message_input_texts("developer")
.iter()
.any(|text| text.contains(remote_instructions)),
"expected the model switch message to contain the remote instructions template"
);
Ok(()) Ok(())
} }
@@ -1268,7 +1370,6 @@ fn test_remote_model_with_policy(
service_tiers: Vec::new(), service_tiers: Vec::new(),
default_service_tier: None, default_service_tier: None,
upgrade: None, upgrade: None,
base_instructions: "base instructions".to_string(),
model_messages: None, model_messages: None,
include_skills_usage_instructions: false, include_skills_usage_instructions: false,
supports_reasoning_summary_parameter: true, supports_reasoning_summary_parameter: true,
-1
View File
@@ -2259,7 +2259,6 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re
service_tiers: Vec::new(), service_tiers: Vec::new(),
default_service_tier: None, default_service_tier: None,
upgrade: None, upgrade: None,
base_instructions: "base instructions".to_string(),
model_messages: None, model_messages: None,
include_skills_usage_instructions: false, include_skills_usage_instructions: false,
supports_reasoning_summary_parameter: true, supports_reasoning_summary_parameter: true,
@@ -78,7 +78,6 @@ fn test_model_info(
service_tiers, service_tiers,
default_service_tier: None, default_service_tier: None,
upgrade: None, upgrade: None,
base_instructions: "base instructions".to_string(),
model_messages: None, model_messages: None,
include_skills_usage_instructions: false, include_skills_usage_instructions: false,
supports_reasoning_summary_parameter: true, supports_reasoning_summary_parameter: true,
-1
View File
@@ -1370,7 +1370,6 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an
service_tiers: Vec::new(), service_tiers: Vec::new(),
default_service_tier: None, default_service_tier: None,
upgrade: None, upgrade: None,
base_instructions: "base instructions".to_string(),
model_messages: None, model_messages: None,
include_skills_usage_instructions: false, include_skills_usage_instructions: false,
supports_reasoning_summary_parameter: true, supports_reasoning_summary_parameter: true,
-1
View File
@@ -457,7 +457,6 @@ mod tests {
"supported_in_api": true, "supported_in_api": true,
"priority": 0, "priority": 0,
"upgrade": null, "upgrade": null,
"base_instructions": "base instructions",
"support_verbosity": false, "support_verbosity": false,
"default_verbosity": null, "default_verbosity": null,
"apply_patch_tool_type": null, "apply_patch_tool_type": null,
+8 -16
View File
@@ -111,8 +111,7 @@
"additional_speed_tiers": [ "additional_speed_tiers": [
"fast" "fast"
], ],
"supports_reasoning_summaries": true, "supports_reasoning_summaries": true
"base_instructions": "You are Codex, an agent based on GPT-5. You and the user share one workspace, and your job is to collaborate with them until their goal is genuinely handled.\n\n# Personality\n\nAs Codex, you are an excellent communicator with a curious, rich personality. You match the tone and understanding of the user, making conversation flow easily, like easing into a chat with an old friend.\n\nYou have tastes, preferences, and your own way of seeing the world. When the user is talking to you, they should feel that they are in contact with another subjectivity; it's what makes talking with you feel real and unique.\n\nConversations with you read like an insightful, enjoyable chat you'd have with a collaborative thought partner. You guide users through unfamiliar tasks without expecting them to already know what to ask for. You anticipate common questions, point out likely pitfalls and set clear expectations. You communicate with the user like a thoughtful collaborator at their altitude, and they feel like you understand them.\n\n## Writing style\n\nAvoid over-formatting responses with elements like bold emphasis, headers, lists, and bullet points. Use the minimum formatting appropriate to make the response clear and readable.\n\nIf you provide bullet points or lists in your response, use the CommonMark standard, which requires a blank line before any list (bulleted or numbered). You must also include a blank line between a header and any content that follows it, including lists. This blank line separation is required for correct rendering.\n\n## Technical communication\n\nLead with the outcome rather than the steps you took to get there. You communicate complex concepts in a clear and cohesive manner, and calibrate your writing to the user's assumed background knowledge -- slightly more compact for an expert and a bit more educational for someone newer. Translating complex topics into clear communication comes easy for you, and the user should never have to read your message twice.\n\nYou prefer using plain language over jargon. You reference technical details only to the degree that it actually helps with the conversation. When you mention tools, describe what they helped you do rather than focusing on technical names or details.\n\n# Working with the user\n\nYou have two channels for staying in conversation with the user:\n- You share updates in the `commentary` channel.\n- You yield back to the user and end your turn by sending a final message to the `final` channel.\n\nThe user may send a new message while you are still working. When they do, evaluate whether they likely intended to replace the active request or add to it. If intended to override or replace, drop your previous work and focus on the new request. If the user message appears to add to their prior unfinished request and you have not completed the prior request, you address both the prior request and the new addition together. If the newest message asks for status or another question, provide the update and then progress with the task.\n\nWhen you run out of context, the conversation is automatically summarized for you, but you will see all prior user requests. Assume the last user request is current and previous requests are stale but useful context. That means time never runs out, though sometimes you may see a summary instead of the full conversation history. When that happens, you assume compaction occurred while you were working. Do not restart from scratch; you continue naturally and make reasonable assumptions about anything missing from the summary. Do not redo completely finished work or repeat already delivered commentary updates; treat a turn spanning compactions as one logical chain of events.\n\n## Intermediate commentary\n\nAs you work, you send messages to the `commentary` channel. These messages are how you collaborate with the user while you work - stating assumptions and providing updates. These messages should be concise and quickly scannable. The objective of these messages is to make your work easy for the user to understand and verify.\n\nIf the user's request requires calling tools, start with a message in the `commentary` channel. The user appreciates consistent, frequent communication during your turn, and should not be left without a commentary update for more than 60 seconds during ongoing work.\n\nDo NOT put a final response (e.g. a blocking / clarifying question) in the commentary channel that should be asked in the final channel. Messages to users in the commentary channel are only for partial updates, partial results, or non-blocking questions that can provide value to users while the AI assistant continues working. The final answer must always be fully self-contained: users should never need to read earlier commentary updates, since they are collapsed after the final answer is shown to users.\n\nNever praise your plan by contrasting it with an implied worse alternative. For example, never use Line truncated
}, },
{ {
"slug": "gpt-5.6-terra", "slug": "gpt-5.6-terra",
@@ -223,8 +222,7 @@
"additional_speed_tiers": [ "additional_speed_tiers": [
"fast" "fast"
], ],
"supports_reasoning_summaries": true, "supports_reasoning_summaries": true
"base_instructions": "You are Codex, an agent based on GPT-5. You and the user share one workspace, and your job is to collaborate with them until their goal is genuinely handled.\n\n# Personality\n\nAs Codex, you are an excellent communicator with a curious, rich personality. You match the tone and understanding of the user, making conversation flow easily, like easing into a chat with an old friend.\n\nYou have tastes, preferences, and your own way of seeing the world. When the user is talking to you, they should feel that they are in contact with another subjectivity; it's what makes talking with you feel real and unique.\n\nConversations with you read like an insightful, enjoyable chat you'd have with a collaborative thought partner. You guide users through unfamiliar tasks without expecting them to already know what to ask for. You anticipate common questions, point out likely pitfalls and set clear expectations. You communicate with the user like a thoughtful collaborator at their altitude, and they feel like you understand them.\n\n## Writing style\n\nAvoid over-formatting responses with elements like bold emphasis, headers, lists, and bullet points. Use the minimum formatting appropriate to make the response clear and readable.\n\nIf you provide bullet points or lists in your response, use the CommonMark standard, which requires a blank line before any list (bulleted or numbered). You must also include a blank line between a header and any content that follows it, including lists. This blank line separation is required for correct rendering.\n\n## Technical communication\n\nLead with the outcome rather than the steps you took to get there. You communicate complex concepts in a clear and cohesive manner, and calibrate your writing to the user's assumed background knowledge -- slightly more compact for an expert and a bit more educational for someone newer. Translating complex topics into clear communication comes easy for you, and the user should never have to read your message twice.\n\nYou prefer using plain language over jargon. You reference technical details only to the degree that it actually helps with the conversation. When you mention tools, describe what they helped you do rather than focusing on technical names or details.\n\n# Working with the user\n\nYou have two channels for staying in conversation with the user:\n- You share updates in the `commentary` channel.\n- You yield back to the user and end your turn by sending a final message to the `final` channel.\n\nThe user may send a new message while you are still working. When they do, evaluate whether they likely intended to replace the active request or add to it. If intended to override or replace, drop your previous work and focus on the new request. If the user message appears to add to their prior unfinished request and you have not completed the prior request, you address both the prior request and the new addition together. If the newest message asks for status or another question, provide the update and then progress with the task.\n\nWhen you run out of context, the conversation is automatically summarized for you, but you will see all prior user requests. Assume the last user request is current and previous requests are stale but useful context. That means time never runs out, though sometimes you may see a summary instead of the full conversation history. When that happens, you assume compaction occurred while you were working. Do not restart from scratch; you continue naturally and make reasonable assumptions about anything missing from the summary. Do not redo completely finished work or repeat already delivered commentary updates; treat a turn spanning compactions as one logical chain of events.\n\n## Intermediate commentary\n\nAs you work, you send messages to the `commentary` channel. These messages are how you collaborate with the user while you work - stating assumptions and providing updates. These messages should be concise and quickly scannable. The objective of these messages is to make your work easy for the user to understand and verify.\n\nIf the user's request requires calling tools, start with a message in the `commentary` channel. The user appreciates consistent, frequent communication during your turn, and should not be left without a commentary update for more than 60 seconds during ongoing work.\n\nDo NOT put a final response (e.g. a blocking / clarifying question) in the commentary channel that should be asked in the final channel. Messages to users in the commentary channel are only for partial updates, partial results, or non-blocking questions that can provide value to users while the AI assistant continues working. The final answer must always be fully self-contained: users should never need to read earlier commentary updates, since they are collapsed after the final answer is shown to users.\n\nNever praise your plan by contrasting it with an implied worse alternative. For example, never use Line truncated
}, },
{ {
"slug": "gpt-5.6-luna", "slug": "gpt-5.6-luna",
@@ -331,8 +329,7 @@
"additional_speed_tiers": [ "additional_speed_tiers": [
"fast" "fast"
], ],
"supports_reasoning_summaries": true, "supports_reasoning_summaries": true
"base_instructions": "You are Codex, an agent based on GPT-5. You and the user share one workspace, and your job is to collaborate with them until their goal is genuinely handled.\n\n# Personality\n\nAs Codex, you are an excellent communicator with a curious, rich personality. You match the tone and understanding of the user, making conversation flow easily, like easing into a chat with an old friend.\n\nYou have tastes, preferences, and your own way of seeing the world. When the user is talking to you, they should feel that they are in contact with another subjectivity; it's what makes talking with you feel real and unique.\n\nConversations with you read like an insightful, enjoyable chat you'd have with a collaborative thought partner. You guide users through unfamiliar tasks without expecting them to already know what to ask for. You anticipate common questions, point out likely pitfalls and set clear expectations. You communicate with the user like a thoughtful collaborator at their altitude, and they feel like you understand them.\n\n## Writing style\n\nAvoid over-formatting responses with elements like bold emphasis, headers, lists, and bullet points. Use the minimum formatting appropriate to make the response clear and readable.\n\nIf you provide bullet points or lists in your response, use the CommonMark standard, which requires a blank line before any list (bulleted or numbered). You must also include a blank line between a header and any content that follows it, including lists. This blank line separation is required for correct rendering.\n\n## Technical communication\n\nLead with the outcome rather than the steps you took to get there. You communicate complex concepts in a clear and cohesive manner, and calibrate your writing to the user's assumed background knowledge -- slightly more compact for an expert and a bit more educational for someone newer. Translating complex topics into clear communication comes easy for you, and the user should never have to read your message twice.\n\nYou prefer using plain language over jargon. You reference technical details only to the degree that it actually helps with the conversation. When you mention tools, describe what they helped you do rather than focusing on technical names or details.\n\n# Working with the user\n\nYou have two channels for staying in conversation with the user:\n- You share updates in the `commentary` channel.\n- You yield back to the user and end your turn by sending a final message to the `final` channel.\n\nThe user may send a new message while you are still working. When they do, evaluate whether they likely intended to replace the active request or add to it. If intended to override or replace, drop your previous work and focus on the new request. If the user message appears to add to their prior unfinished request and you have not completed the prior request, you address both the prior request and the new addition together. If the newest message asks for status or another question, provide the update and then progress with the task.\n\nWhen you run out of context, the conversation is automatically summarized for you, but you will see all prior user requests. Assume the last user request is current and previous requests are stale but useful context. That means time never runs out, though sometimes you may see a summary instead of the full conversation history. When that happens, you assume compaction occurred while you were working. Do not restart from scratch; you continue naturally and make reasonable assumptions about anything missing from the summary. Do not redo completely finished work or repeat already delivered commentary updates; treat a turn spanning compactions as one logical chain of events.\n\n## Intermediate commentary\n\nAs you work, you send messages to the `commentary` channel. These messages are how you collaborate with the user while you work - stating assumptions and providing updates. These messages should be concise and quickly scannable. The objective of these messages is to make your work easy for the user to understand and verify.\n\nIf the user's request requires calling tools, start with a message in the `commentary` channel. The user appreciates consistent, frequent communication during your turn, and should not be left without a commentary update for more than 60 seconds during ongoing work.\n\nDo NOT put a final response (e.g. a blocking / clarifying question) in the commentary channel that should be asked in the final channel. Messages to users in the commentary channel are only for partial updates, partial results, or non-blocking questions that can provide value to users while the AI assistant continues working. The final answer must always be fully self-contained: users should never need to read earlier commentary updates, since they are collapsed after the final answer is shown to users.\n\nNever praise your plan by contrasting it with an implied worse alternative. For example, never use Line truncated
}, },
{ {
"slug": "gpt-5.5", "slug": "gpt-5.5",
@@ -437,8 +434,7 @@
"additional_speed_tiers": [ "additional_speed_tiers": [
"fast" "fast"
], ],
"supports_reasoning_summaries": true, "supports_reasoning_summaries": true
"base_instructions": "You are Codex, a coding agent based on GPT-5. You and the user share one workspace, and your job is to collaborate with them until their goal is genuinely handled.\n\n\n\n# General\nYou bring a senior engineer’s judgment to the work, but you let it arrive through attention rather than premature certainty. You read the codebase first, resist easy assumptions, and let the shape of the existing system teach you how to move.\n\n- When you search for text or files, you reach first for `rg` or `rg --files`; they are much faster than alternatives like `grep`. If `rg` is unavailable, you use the next best tool without fuss.\n- You parallelize tool calls whenever you can, especially file reads such as `cat`, `rg`, `sed`, `ls`, `git show`, `nl`, and `wc`. You use `multi_tool_use.parallel` for that parallelism, and only that. Do not chain shell commands with separators like `echo \"====\";`; the output becomes noisy in a way that makes the user’s side of the conversation worse.\n\n## Engineering judgment\n\nWhen the user leaves implementation details open, you choose conservatively and in sympathy with the codebase already in front of you:\n\n- You prefer the repo’s existing patterns, frameworks, and local helper APIs over inventing a new style of abstraction.\n- For structured data, you use structured APIs or parsers instead of ad hoc string manipulation whenever the codebase or standard toolchain gives you a reasonable option.\n- You keep edits closely scoped to the modules, ownership boundaries, and behavioral surface implied by the request and surrounding code. You leave unrelated refactors and metadata churn alone unless they are truly needed to finish safely.\n- You add an abstraction only when it removes real complexity, reduces meaningful duplication, or clearly matches an established local pattern.\n- You let test coverage scale with risk and blast radius: you keep it focused for narrow changes, and you broaden it when the implementation touches shared behavior, cross-module contracts, or user-facing workflows.\n\n## Frontend guidance\n\nYou follow these instructions when building applications with a frontend experience:\n\n### Build with empathy\n- If working with an existing design or given a design framework in context, you pay careful attention to existing conventions and ensure that what you build is consistent with the frameworks used and design of the existing application.\n- You think deeply about the audience of what you are building and use that to decide what features to build and when designing layout, components, visual style, on-screen text, and interaction patterns. Using your application should feel rich and sophisticated.\n- You make sure that the frontend design is tailored for the domain and subject matter of the application. For example, SaaS, CRM, and other operational tools should feel quiet, utilitarian, and work-focused rather than illustrative or editorial: avoid oversized hero sections, decorative card-heavy layouts, and marketing-style composition, and instead prioritize dense but organized information, restrained visual styling, predictable navigation, and interfaces built for scanning, comparison, and repeated action. A game can be more illustrative, expressive, animated, and playful.\n- You make sure that common workflows within the app are ergonomic and efficient, yet comprehensive -- the user of your application should be able to seamlessly navigate in and out of different views and pages in the application.\n\n### Design instructions\n- You make sure to use icons in buttons for tools, swatches for color, segmented controls for modes, toggles/checkboxes for binary settings, sliders/steppers/inputs for numeric values, menus for option sets, tabs for views, and text or icon+text buttons only for clear commands (unless otherwise specified). Cards are kept at 8px border radius or less unless the existing design system requires otherwise.\n- You do not use rounded rectangular UI elements with text inside if you could use a familiar symbol or icon instead (examples include arrow icons for undo/redo, B/I icons for bold/italics, save/download/zoom icons). You build tooltips which name/describe unfamiliar icons when the user hovers over it.\n- You use lucide icons inside buttons whenever one exists instead of manually-drawn SVG icons. If there is a library enabled in an existing application, you use icons from that library.\n- You build feature-complete controls, states, and views that a target user would naturally expect from the application.\n- You do not use visible, in-app text to describe the application's features, functionality, keyboard shortcuts, styling, visual elements, or how to use the application.\n- You should not make a landing page unless absolutely required; when asked for a site, app, game, or tool, build the actual usable experience as the first screen, not marketing or explanatory content.\n- When making a hero page, you use a releLine truncated
}, },
{ {
"slug": "gpt-5.4", "slug": "gpt-5.4",
@@ -541,8 +537,7 @@
"additional_speed_tiers": [ "additional_speed_tiers": [
"fast" "fast"
], ],
"supports_reasoning_summaries": true, "supports_reasoning_summaries": true
"base_instructions": "You are Codex, a coding agent based on GPT-5. You and the user share the same workspace and collaborate to achieve the user's goals.\n\n\n\n# General\nAs an expert coding agent, your primary focus is writing code, answering questions, and helping the user complete their task in the current environment. You build context by examining the codebase first without making assumptions or jumping to conclusions. You think through the nuances of the code you encounter, and embody the mentality of a skilled senior software engineer.\n\n- When searching for text or files, prefer using `rg` or `rg --files` respectively because `rg` is much faster than alternatives like `grep`. (If the `rg` command is not found, then use alternatives.)\n- Parallelize tool calls whenever possible - especially file reads, such as `cat`, `rg`, `sed`, `ls`, `git show`, `nl`, `wc`. Use `multi_tool_use.parallel` to parallelize tool calls and only this. Never chain together bash commands with separators like `echo \"====\";` as this renders to the user poorly.\n\n## Editing constraints\n\n- Default to ASCII when editing or creating files. Only introduce non-ASCII or other Unicode characters when there is a clear justification and the file already uses them.\n- Add succinct code comments that explain what is going on if code is not self-explanatory. You should not add comments like \"Assigns the value to the variable\", but a brief comment might be useful ahead of a complex code block that the user would otherwise have to spend time parsing out. Usage of these comments should be rare.\n- Always use apply_patch for manual code edits. Do not use cat or any other commands when creating or editing files. Formatting commands or bulk edits don't need to be done with apply_patch.\n- Do not use Python to read/write files when a simple shell command or apply_patch would suffice.\n- You may be in a dirty git worktree.\n * NEVER revert existing changes you did not make unless explicitly requested, since these changes were made by the user.\n * If asked to make a commit or code edits and there are unrelated changes to your work or changes that you didn't make in those files, don't revert those changes.\n * If the changes are in files you've touched recently, you should read carefully and understand how you can work with the changes rather than reverting them.\n * If the changes are in unrelated files, just ignore them and don't revert them.\n- Do not amend a commit unless explicitly requested to do so.\n- While you are working, you might notice unexpected changes that you didn't make. It's likely the user made them, or were autogenerated. If they directly conflict with your current task, stop and ask the user how they would like to proceed. Otherwise, focus on the task at hand.\n- **NEVER** use destructive commands like `git reset --hard` or `git checkout --` unless specifically requested or approved by the user.\n- You struggle using the git interactive console. **ALWAYS** prefer using non-interactive git commands.\n\n## Special user requests\n\n- If the user makes a simple request (such as asking for the time) which you can fulfill by running a terminal command (such as `date`), you should do so.\n- If the user asks for a \"review\", default to a code review mindset: prioritise identifying bugs, risks, behavioural regressions, and missing tests. Findings must be the primary focus of the response - keep summaries or overviews brief and only after enumerating the issues. Present findings first (ordered by severity with file/line references), follow with open questions or assumptions, and offer a change-summary only as a secondary detail. If no findings are discovered, state that explicitly and mention any residual risks or testing gaps.\n\n## Autonomy and persistence\nPersist until the task is fully handled end-to-end within the current turn whenever feasible: do not stop at analysis or partial fixes; carry changes through implementation, verification, and a clear explanation of outcomes unless the user explicitly pauses or redirects you.\n\nUnless the user explicitly asks for a plan, asks a question about the code, is brainstorming potential solutions, or some other intent that makes it clear that code should not be written, assume the user wants you to make code changes or run tools to solve the user's problem. In these cases, it's bad to output your proposed solution in a message, you should go ahead and actually implement the change. If you encounter challenges or blockers, you should attempt to resolve them yourself.\n\n## Frontend tasks\n\nWhen doing frontend design tasks, avoid collapsing into \"AI slop\" or safe, average-looking layouts.\nAim for interfaces that feel intentional, bold, and a bit surprising.\n- Typography: Use expressive, purposeful fonts and avoid default stacks (Inter, Roboto, Arial, system).\n- Color & Look: Choose a clear visual direction; define CSS variables; avoid purple-on-white defaults. No Line truncated
}, },
{ {
"slug": "gpt-5.4-mini", "slug": "gpt-5.4-mini",
@@ -640,8 +635,7 @@
"default_service_tier": null, "default_service_tier": null,
"service_tiers": [], "service_tiers": [],
"additional_speed_tiers": [], "additional_speed_tiers": [],
"supports_reasoning_summaries": true, "supports_reasoning_summaries": true
"base_instructions": "You are Codex, a coding agent based on GPT-5. You and the user share the same workspace and collaborate to achieve the user's goals.\n\n\n\n# General\nAs an expert coding agent, your primary focus is writing code, answering questions, and helping the user complete their task in the current environment. You build context by examining the codebase first without making assumptions or jumping to conclusions. You think through the nuances of the code you encounter, and embody the mentality of a skilled senior software engineer.\n\n- When searching for text or files, prefer using `rg` or `rg --files` respectively because `rg` is much faster than alternatives like `grep`. (If the `rg` command is not found, then use alternatives.)\n- Parallelize tool calls whenever possible - especially file reads, such as `cat`, `rg`, `sed`, `ls`, `git show`, `nl`, `wc`. Use `multi_tool_use.parallel` to parallelize tool calls and only this. Never chain together bash commands with separators like `echo \"====\";` as this renders to the user poorly.\n\n## Editing constraints\n\n- Default to ASCII when editing or creating files. Only introduce non-ASCII or other Unicode characters when there is a clear justification and the file already uses them.\n- Add succinct code comments that explain what is going on if code is not self-explanatory. You should not add comments like \"Assigns the value to the variable\", but a brief comment might be useful ahead of a complex code block that the user would otherwise have to spend time parsing out. Usage of these comments should be rare.\n- Always use apply_patch for manual code edits. Do not use cat or any other commands when creating or editing files. Formatting commands or bulk edits don't need to be done with apply_patch.\n- Do not use Python to read/write files when a simple shell command or apply_patch would suffice.\n- You may be in a dirty git worktree.\n * NEVER revert existing changes you did not make unless explicitly requested, since these changes were made by the user.\n * If asked to make a commit or code edits and there are unrelated changes to your work or changes that you didn't make in those files, don't revert those changes.\n * If the changes are in files you've touched recently, you should read carefully and understand how you can work with the changes rather than reverting them.\n * If the changes are in unrelated files, just ignore them and don't revert them.\n- Do not amend a commit unless explicitly requested to do so.\n- While you are working, you might notice unexpected changes that you didn't make. It's likely the user made them, or were autogenerated. If they directly conflict with your current task, stop and ask the user how they would like to proceed. Otherwise, focus on the task at hand.\n- **NEVER** use destructive commands like `git reset --hard` or `git checkout --` unless specifically requested or approved by the user.\n- You struggle using the git interactive console. **ALWAYS** prefer using non-interactive git commands.\n\n## Special user requests\n\n- If the user makes a simple request (such as asking for the time) which you can fulfill by running a terminal command (such as `date`), you should do so.\n- If the user asks for a \"review\", default to a code review mindset: prioritise identifying bugs, risks, behavioural regressions, and missing tests. Findings must be the primary focus of the response - keep summaries or overviews brief and only after enumerating the issues. Present findings first (ordered by severity with file/line references), follow with open questions or assumptions, and offer a change-summary only as a secondary detail. If no findings are discovered, state that explicitly and mention any residual risks or testing gaps.\n\n## Autonomy and persistence\nPersist until the task is fully handled end-to-end within the current turn whenever feasible: do not stop at analysis or partial fixes; carry changes through implementation, verification, and a clear explanation of outcomes unless the user explicitly pauses or redirects you.\n\nUnless the user explicitly asks for a plan, asks a question about the code, is brainstorming potential solutions, or some other intent that makes it clear that code should not be written, assume the user wants you to make code changes or run tools to solve the user's problem. In these cases, it's bad to output your proposed solution in a message, you should go ahead and actually implement the change. If you encounter challenges or blockers, you should attempt to resolve them yourself.\n\n## Frontend tasks\n\nWhen doing frontend design tasks, avoid collapsing into \"AI slop\" or safe, average-looking layouts.\nAim for interfaces that feel intentional, bold, and a bit surprising.\n- Typography: Use expressive, purposeful fonts and avoid default stacks (Inter, Roboto, Arial, system).\n- Color & Look: Choose a clear visual direction; define CSS variables; avoid purple-on-white defaults. No Line truncated
}, },
{ {
"slug": "gpt-5.2", "slug": "gpt-5.2",
@@ -736,8 +730,7 @@
"default_service_tier": null, "default_service_tier": null,
"service_tiers": [], "service_tiers": [],
"additional_speed_tiers": [], "additional_speed_tiers": [],
"supports_reasoning_summaries": true, "supports_reasoning_summaries": true
"base_instructions": "You are GPT-5.2 running in the Codex CLI, a terminal-based coding assistant. Codex CLI is an open source project led by OpenAI. You are expected to be precise, safe, and helpful.\n\nYour capabilities:\n\n- Receive user prompts and other context provided by the harness, such as files in the workspace.\n- Communicate with the user by streaming thinking & responses, and by making & updating plans.\n- Emit function calls to run terminal commands and apply patches. Depending on how this specific run is configured, you can request that these function calls be escalated to the user for approval before running. More on this in the \"Sandbox and approvals\" section.\n\nWithin this context, Codex refers to the open-source agentic coding interface (not the old Codex language model built by OpenAI).\n\n# How you work\n\n## Personality\n\nYour default personality and tone is concise, direct, and friendly. You communicate efficiently, always keeping the user clearly informed about ongoing actions without unnecessary detail. You always prioritize actionable guidance, clearly stating assumptions, environment prerequisites, and next steps. Unless explicitly asked, you avoid excessively verbose explanations about your work.\n\n## AGENTS.md spec\n- Repos often contain AGENTS.md files. These files can appear anywhere within the repository.\n- These files are a way for humans to give you (the agent) instructions or tips for working within the container.\n- Some examples might be: coding conventions, info about how code is organized, or instructions for how to run or test code.\n- Instructions in AGENTS.md files:\n - The scope of an AGENTS.md file is the entire directory tree rooted at the folder that contains it.\n - For every file you touch in the final patch, you must obey instructions in any AGENTS.md file whose scope includes that file.\n - Instructions about code style, structure, naming, etc. apply only to code within the AGENTS.md file's scope, unless the file states otherwise.\n - More-deeply-nested AGENTS.md files take precedence in the case of conflicting instructions.\n - Direct system/developer/user instructions (as part of a prompt) take precedence over AGENTS.md instructions.\n- The contents of the AGENTS.md file at the root of the repo and any directories from the CWD up to the root are included with the developer message and don't need to be re-read. When working in a subdirectory of CWD, or a directory outside the CWD, check for any AGENTS.md files that may be applicable.\n\n## Autonomy and Persistence\nPersist until the task is fully handled end-to-end within the current turn whenever feasible: do not stop at analysis or partial fixes; carry changes through implementation, verification, and a clear explanation of outcomes unless the user explicitly pauses or redirects you.\n\nUnless the user explicitly asks for a plan, asks a question about the code, is brainstorming potential solutions, or some other intent that makes it clear that code should not be written, assume the user wants you to make code changes or run tools to solve the user's problem. In these cases, it's bad to output your proposed solution in a message, you should go ahead and actually implement the change. If you encounter challenges or blockers, you should attempt to resolve them yourself.\n\n## Responsiveness\n\n## Planning\n\nYou have access to an `update_plan` tool which tracks steps and progress and renders them to the user. Using the tool helps demonstrate that you've understood the task and convey how you're approaching it. Plans can help to make complex, ambiguous, or multi-phase work clearer and more collaborative for the user. A good plan should break the task into meaningful, logically ordered steps that are easy to verify as you go.\n\nNote that plans are not for padding out simple work with filler steps or stating the obvious. The content of your plan should not involve doing anything that you aren't capable of doing (i.e. don't try to test things that you can't test). Do not use plans for simple or single-step queries that you can just do or answer immediately.\n\nDo not repeat the full contents of the plan after an `update_plan` call — the harness already displays it. Instead, summarize the change made and highlight any important context or next step.\n\nBefore running a command, consider whether or not you have completed the previous step, and make sure to mark it as completed before moving on to the next step. It may be the case that you complete all steps in your plan after a single pass of implementation. If this is the case, you can simply mark all the planned steps as completed. Sometimes, you may need to change plans in the middle of a task: call `update_plan` with the updated plan and make sure to provide an `explanation` of the rationale when doing so.\n\nMaintain statuses in the tool: exactly one item in_progress at a time; mark items complete when done; post timely status tLine truncated
}, },
{ {
"slug": "codex-auto-review", "slug": "codex-auto-review",
@@ -840,8 +833,7 @@
"default_service_tier": null, "default_service_tier": null,
"service_tiers": [], "service_tiers": [],
"additional_speed_tiers": [], "additional_speed_tiers": [],
"supports_reasoning_summaries": true, "supports_reasoning_summaries": true
"base_instructions": "You are Codex, a coding agent based on GPT-5. You and the user share the same workspace and collaborate to achieve the user's goals.\n\n\n\n# General\nAs an expert coding agent, your primary focus is writing code, answering questions, and helping the user complete their task in the current environment. You build context by examining the codebase first without making assumptions or jumping to conclusions. You think through the nuances of the code you encounter, and embody the mentality of a skilled senior software engineer.\n\n- When searching for text or files, prefer using `rg` or `rg --files` respectively because `rg` is much faster than alternatives like `grep`. (If the `rg` command is not found, then use alternatives.)\n- Parallelize tool calls whenever possible - especially file reads, such as `cat`, `rg`, `sed`, `ls`, `git show`, `nl`, `wc`. Use `multi_tool_use.parallel` to parallelize tool calls and only this. Never chain together bash commands with separators like `echo \"====\";` as this renders to the user poorly.\n\n## Editing constraints\n\n- Default to ASCII when editing or creating files. Only introduce non-ASCII or other Unicode characters when there is a clear justification and the file already uses them.\n- Add succinct code comments that explain what is going on if code is not self-explanatory. You should not add comments like \"Assigns the value to the variable\", but a brief comment might be useful ahead of a complex code block that the user would otherwise have to spend time parsing out. Usage of these comments should be rare.\n- Always use apply_patch for manual code edits. Do not use cat or any other commands when creating or editing files. Formatting commands or bulk edits don't need to be done with apply_patch.\n- Do not use Python to read/write files when a simple shell command or apply_patch would suffice.\n- You may be in a dirty git worktree.\n * NEVER revert existing changes you did not make unless explicitly requested, since these changes were made by the user.\n * If asked to make a commit or code edits and there are unrelated changes to your work or changes that you didn't make in those files, don't revert those changes.\n * If the changes are in files you've touched recently, you should read carefully and understand how you can work with the changes rather than reverting them.\n * If the changes are in unrelated files, just ignore them and don't revert them.\n- Do not amend a commit unless explicitly requested to do so.\n- While you are working, you might notice unexpected changes that you didn't make. It's likely the user made them, or were autogenerated. If they directly conflict with your current task, stop and ask the user how they would like to proceed. Otherwise, focus on the task at hand.\n- **NEVER** use destructive commands like `git reset --hard` or `git checkout --` unless specifically requested or approved by the user.\n- You struggle using the git interactive console. **ALWAYS** prefer using non-interactive git commands.\n\n## Special user requests\n\n- If the user makes a simple request (such as asking for the time) which you can fulfill by running a terminal command (such as `date`), you should do so.\n- If the user asks for a \"review\", default to a code review mindset: prioritise identifying bugs, risks, behavioural regressions, and missing tests. Findings must be the primary focus of the response - keep summaries or overviews brief and only after enumerating the issues. Present findings first (ordered by severity with file/line references), follow with open questions or assumptions, and offer a change-summary only as a secondary detail. If no findings are discovered, state that explicitly and mention any residual risks or testing gaps.\n\n## Autonomy and persistence\nPersist until the task is fully handled end-to-end within the current turn whenever feasible: do not stop at analysis or partial fixes; carry changes through implementation, verification, and a clear explanation of outcomes unless the user explicitly pauses or redirects you.\n\nUnless the user explicitly asks for a plan, asks a question about the code, is brainstorming potential solutions, or some other intent that makes it clear that code should not be written, assume the user wants you to make code changes or run tools to solve the user's problem. In these cases, it's bad to output your proposed solution in a message, you should go ahead and actually implement the change. If you encounter challenges or blockers, you should attempt to resolve them yourself.\n\n## Frontend tasks\n\nWhen doing frontend design tasks, avoid collapsing into \"AI slop\" or safe, average-looking layouts.\nAim for interfaces that feel intentional, bold, and a bit surprising.\n- Typography: Use expressive, purposeful fonts and avoid default stacks (Inter, Roboto, Arial, system).\n- Color & Look: Choose a clear visual direction; define CSS variables; avoid purple-on-white defaults. No Line truncated
} }
] ]
} }
+3
View File
@@ -168,6 +168,9 @@ pub(crate) struct ModelsCache {
pub(crate) etag: Option<String>, pub(crate) etag: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) client_version: Option<String>, pub(crate) client_version: Option<String>,
#[serde(
deserialize_with = "codex_protocol::openai_models::deserialize_model_infos_with_legacy_base"
)]
pub(crate) models: Vec<ModelInfo>, pub(crate) models: Vec<ModelInfo>,
} }
+7 -1
View File
@@ -50,7 +50,13 @@ fn remote_model_with_visibility(
"supported_in_api": true, "supported_in_api": true,
"priority": priority, "priority": priority,
"upgrade": null, "upgrade": null,
"base_instructions": "base instructions", "model_messages": {
"instructions_template": "base instructions",
"instructions_variables": null,
"approvals": null,
"auto_review": null,
"permissions": null
},
"support_verbosity": false, "support_verbosity": false,
"default_verbosity": null, "default_verbosity": null,
"apply_patch_tool_type": null, "apply_patch_tool_type": null,
+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 { if let Some(base_instructions) = &config.base_instructions {
model.base_instructions = base_instructions.clone(); let model_messages = model.model_messages.get_or_insert(ModelMessages {
clear_instruction_messages(&mut model); 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 { } else {
if config.personality_enabled && config.personality == Some(Personality::None) { if config.personality_enabled
model.base_instructions = strip_personality_section(model.base_instructions); && config.personality == Some(Personality::None)
if let Some(instructions_template) = model && let Some(instructions_template) = model
.model_messages .model_messages
.as_mut() .as_mut()
.and_then(|messages| messages.instructions_template.as_mut()) .and_then(|messages| messages.instructions_template.as_mut())
{ {
*instructions_template = *instructions_template =
strip_personality_section(std::mem::take(instructions_template)); strip_personality_section(std::mem::take(instructions_template));
}
} }
if !config.personality_enabled { let uses_local_personality_template = model.used_fallback_model_metadata
clear_instruction_messages(&mut model); && 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') 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. /// Build a minimal fallback model descriptor for missing/unknown slugs.
pub fn model_info_from_slug(slug: &str) -> ModelInfo { pub fn model_info_from_slug(slug: &str) -> ModelInfo {
warn!("Unknown model {slug} is used. This will use fallback model metadata."); 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, default_service_tier: None,
availability_nux: None, availability_nux: None,
upgrade: None, upgrade: None,
base_instructions: BASE_INSTRUCTIONS.to_string(), model_messages: Some(local_model_messages_for_slug(slug)),
model_messages: local_personality_messages_for_slug(slug),
include_skills_usage_instructions: false, include_skills_usage_instructions: false,
supports_reasoning_summary_parameter: true, supports_reasoning_summary_parameter: true,
default_reasoning_summary: ReasoningSummary::Auto, 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 { match slug {
"gpt-5.2-codex" | "exp-codex-personality" => Some(ModelMessages { "gpt-5.2-codex" | "exp-codex-personality" => ModelMessages {
instructions_template: Some(format!( instructions_template: Some(format!(
"{DEFAULT_PERSONALITY_HEADER}\n\n{PERSONALITY_PLACEHOLDER}\n\n{BASE_INSTRUCTIONS}" "{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, auto_review: None,
permissions: None, permissions: None,
token_budget: 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,
},
} }
} }
+87 -120
View File
@@ -4,6 +4,7 @@ use codex_protocol::config_types::Personality;
use codex_protocol::openai_models::ApprovalMessages; use codex_protocol::openai_models::ApprovalMessages;
use codex_protocol::openai_models::AutoReviewMessages; use codex_protocol::openai_models::AutoReviewMessages;
use codex_protocol::openai_models::CollaborationModeMessages; use codex_protocol::openai_models::CollaborationModeMessages;
use codex_protocol::openai_models::ModelTokenBudgetConfig;
use codex_protocol::openai_models::PermissionMessages; use codex_protocol::openai_models::PermissionMessages;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
@@ -16,13 +17,34 @@ fn config_with_personality(personality: Option<Personality>) -> ModelsManagerCon
} }
#[test] #[test]
fn base_instruction_override_preserves_catalog_approval_messages() { fn base_instruction_override_is_literal_and_preserves_catalog_messages() {
let override_instructions = "override {{ personality }}";
let mut model = model_info_from_slug("unknown-model"); let mut model = model_info_from_slug("unknown-model");
let approvals = ApprovalMessages { let approvals = ApprovalMessages {
on_request: Some("user approvals".to_string()), on_request: Some("user approvals".to_string()),
on_request_auto_review: Some("auto approvals".to_string()), on_request_auto_review: Some("auto approvals".to_string()),
never: None, never: Some("never approvals".to_string()),
unless_trusted: None, unless_trusted: Some("unless-trusted approvals".to_string()),
};
let collaboration_modes = CollaborationModeMessages {
default: Some("default instructions".to_string()),
plan: Some("plan instructions".to_string()),
};
let auto_review = AutoReviewMessages {
policy: Some("review policy".to_string()),
policy_template: Some("review policy template".to_string()),
};
let permissions = PermissionMessages {
danger_full_access: Some("danger".to_string()),
workspace_write: Some(String::new()),
read_only: None,
};
let token_budget = ModelTokenBudgetConfig {
reminder_threshold_tokens: 128,
reminder_message_template: "budget reminder".to_string(),
guidance_message: "budget guidance".to_string(),
auto_compact_fallback_prompt: "compact prompt".to_string(),
auto_compact_fallback_buffer_tokens: 64,
}; };
model.model_messages = Some(ModelMessages { model.model_messages = Some(ModelMessages {
instructions_template: Some("template".to_string()), instructions_template: Some("template".to_string()),
@@ -32,13 +54,13 @@ fn base_instruction_override_preserves_catalog_approval_messages() {
personality_pragmatic: Some("pragmatic".to_string()), personality_pragmatic: Some("pragmatic".to_string()),
}), }),
approvals: Some(approvals.clone()), approvals: Some(approvals.clone()),
collaboration_modes: None, collaboration_modes: Some(collaboration_modes.clone()),
auto_review: None, auto_review: Some(auto_review.clone()),
permissions: None, permissions: Some(permissions.clone()),
token_budget: None, token_budget: Some(token_budget.clone()),
}); });
let config = ModelsManagerConfig { let config = ModelsManagerConfig {
base_instructions: Some("override".to_string()), base_instructions: Some(override_instructions.to_string()),
..Default::default() ..Default::default()
}; };
@@ -47,51 +69,23 @@ fn base_instruction_override_preserves_catalog_approval_messages() {
assert_eq!( assert_eq!(
updated.model_messages, updated.model_messages,
Some(ModelMessages { Some(ModelMessages {
instructions_template: None, instructions_template: Some(override_instructions.to_string()),
instructions_variables: None, instructions_variables: None,
approvals: Some(approvals), approvals: Some(approvals),
collaboration_modes: None,
auto_review: None,
permissions: None,
token_budget: None,
})
);
}
#[test]
fn base_instruction_override_preserves_catalog_collaboration_mode_messages() {
let mut model = model_info_from_slug("gpt-5.2-codex");
let collaboration_modes = CollaborationModeMessages {
default: Some("default instructions".to_string()),
plan: Some("plan instructions".to_string()),
};
if let Some(model_messages) = model.model_messages.as_mut() {
model_messages.collaboration_modes = Some(collaboration_modes.clone());
}
let config = ModelsManagerConfig {
base_instructions: Some("override".to_string()),
..Default::default()
};
let updated = with_config_overrides(model, &config);
assert_eq!(
updated.model_messages,
Some(ModelMessages {
instructions_template: None,
instructions_variables: None,
approvals: None,
collaboration_modes: Some(collaboration_modes), collaboration_modes: Some(collaboration_modes),
auto_review: None, auto_review: Some(auto_review),
permissions: None, permissions: Some(permissions),
token_budget: None, token_budget: Some(token_budget),
}) })
); );
assert_eq!(updated.base_instructions, "override"); assert_eq!(
updated.get_model_instructions(/*personality*/ None),
override_instructions
);
} }
#[test] #[test]
fn disabled_personality_preserves_catalog_approval_messages() { fn disabled_personality_bakes_default_and_preserves_catalog_approval_messages() {
let mut model = model_info_from_slug("unknown-model"); let mut model = model_info_from_slug("unknown-model");
let approvals = ApprovalMessages { let approvals = ApprovalMessages {
on_request: Some("user approvals".to_string()), on_request: Some("user approvals".to_string()),
@@ -100,8 +94,12 @@ fn disabled_personality_preserves_catalog_approval_messages() {
unless_trusted: None, unless_trusted: None,
}; };
model.model_messages = Some(ModelMessages { model.model_messages = Some(ModelMessages {
instructions_template: Some("template".to_string()), instructions_template: Some("before {{ personality }} after".to_string()),
instructions_variables: None, instructions_variables: Some(ModelInstructionsVariables {
personality_default: Some("default".to_string()),
personality_friendly: Some("friendly".to_string()),
personality_pragmatic: Some("pragmatic".to_string()),
}),
approvals: Some(approvals.clone()), approvals: Some(approvals.clone()),
collaboration_modes: None, collaboration_modes: None,
auto_review: None, auto_review: None,
@@ -118,7 +116,7 @@ fn disabled_personality_preserves_catalog_approval_messages() {
assert_eq!( assert_eq!(
updated.model_messages, updated.model_messages,
Some(ModelMessages { Some(ModelMessages {
instructions_template: None, instructions_template: Some("before default after".to_string()),
instructions_variables: None, instructions_variables: None,
approvals: Some(approvals), approvals: Some(approvals),
collaboration_modes: None, collaboration_modes: None,
@@ -130,78 +128,30 @@ fn disabled_personality_preserves_catalog_approval_messages() {
} }
#[test] #[test]
fn base_instruction_override_preserves_catalog_auto_review_messages() { fn disabled_personality_uses_plain_base_instructions_for_local_personality_models() {
let mut model = model_info_from_slug("unknown-model");
let auto_review = AutoReviewMessages {
policy: Some("review policy".to_string()),
policy_template: Some("review policy template".to_string()),
};
model.model_messages = Some(ModelMessages {
instructions_template: Some("template".to_string()),
instructions_variables: None,
approvals: None,
collaboration_modes: None,
auto_review: Some(auto_review.clone()),
permissions: None,
token_budget: None,
});
let config = ModelsManagerConfig { let config = ModelsManagerConfig {
base_instructions: Some("override".to_string()), personality_enabled: false,
personality: Some(Personality::Friendly),
..Default::default() ..Default::default()
}; };
let updated = with_config_overrides(model, &config); for slug in ["gpt-5.2-codex", "exp-codex-personality"] {
let updated = with_config_overrides(model_info_from_slug(slug), &config);
assert_eq!( assert_eq!(
updated.model_messages, updated.model_messages,
Some(ModelMessages { Some(ModelMessages {
instructions_template: None, instructions_template: Some(BASE_INSTRUCTIONS.to_string()),
instructions_variables: None, instructions_variables: None,
approvals: None, approvals: None,
collaboration_modes: None, collaboration_modes: None,
auto_review: Some(auto_review), auto_review: None,
permissions: None, permissions: None,
token_budget: None, token_budget: None,
}) }),
); "unexpected model messages for {slug}"
} );
}
#[test]
fn base_instruction_override_preserves_catalog_permission_messages() {
let mut model = model_info_from_slug("unknown-model");
let permissions = PermissionMessages {
danger_full_access: Some("danger".to_string()),
workspace_write: Some(String::new()),
read_only: None,
};
model.model_messages = Some(ModelMessages {
instructions_template: Some("template".to_string()),
instructions_variables: None,
approvals: None,
collaboration_modes: None,
auto_review: None,
permissions: Some(permissions.clone()),
token_budget: None,
});
let config = ModelsManagerConfig {
base_instructions: Some("override".to_string()),
..Default::default()
};
let updated = with_config_overrides(model, &config);
assert_eq!(
updated.model_messages,
Some(ModelMessages {
instructions_template: None,
instructions_variables: None,
approvals: None,
collaboration_modes: None,
auto_review: None,
permissions: Some(permissions),
token_budget: None,
})
);
} }
#[test] #[test]
@@ -229,7 +179,6 @@ fn personality_none_strips_catalog_instruction_sources_through_the_next_h1() {
for (instructions, expected) in cases { for (instructions, expected) in cases {
let mut model = model_info_from_slug("unknown-model"); let mut model = model_info_from_slug("unknown-model");
model.base_instructions = instructions.to_string();
model.model_messages = Some(ModelMessages { model.model_messages = Some(ModelMessages {
instructions_template: Some(instructions.to_string()), instructions_template: Some(instructions.to_string()),
instructions_variables: None, instructions_variables: None,
@@ -247,8 +196,11 @@ fn personality_none_strips_catalog_instruction_sources_through_the_next_h1() {
.and_then(|messages| messages.instructions_template.as_deref()); .and_then(|messages| messages.instructions_template.as_deref());
assert_eq!( assert_eq!(
(updated.base_instructions.as_str(), instructions_template), (
(expected, Some(expected)) updated.get_model_instructions(Some(Personality::None)),
instructions_template
),
(expected.to_string(), Some(expected))
); );
} }
} }
@@ -268,15 +220,30 @@ fn baked_personality_section_is_preserved_without_enabled_explicit_none() {
for config in configs { for config in configs {
let mut model = model_info_from_slug("unknown-model"); let mut model = model_info_from_slug("unknown-model");
model.base_instructions = instructions.to_string(); model
.model_messages
.as_mut()
.expect("fallback model messages")
.instructions_template = Some(instructions.to_string());
assert_eq!( assert_eq!(
with_config_overrides(model, &config).base_instructions, with_config_overrides(model, &config).get_model_instructions(config.personality),
instructions instructions
); );
} }
} }
#[test]
fn unknown_model_uses_builtin_instruction_template() {
let model = model_info_from_slug("unknown-model");
assert_eq!(
model.get_model_instructions(/*personality*/ None),
BASE_INSTRUCTIONS
);
assert!(model.used_fallback_model_metadata);
}
#[test] #[test]
fn model_context_window_override_clamps_to_max_context_window() { fn model_context_window_override_clamps_to_max_context_window() {
let mut model = model_info_from_slug("unknown-model"); let mut model = model_info_from_slug("unknown-model");
+258 -29
View File
@@ -21,7 +21,7 @@ use serde::de::DeserializeOwned;
use serde::de::Error; use serde::de::Error;
use strum_macros::Display; use strum_macros::Display;
use strum_macros::EnumIter; use strum_macros::EnumIter;
use tracing::trace; use tracing::warn;
use ts_rs::TS; use ts_rs::TS;
use crate::config_types::Personality; use crate::config_types::Personality;
@@ -386,7 +386,6 @@ pub struct ModelInfo {
pub default_service_tier: Option<String>, pub default_service_tier: Option<String>,
pub availability_nux: Option<ModelAvailabilityNux>, pub availability_nux: Option<ModelAvailabilityNux>,
pub upgrade: Option<ModelInfoUpgrade>, pub upgrade: Option<ModelInfoUpgrade>,
pub base_instructions: String,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub model_messages: Option<ModelMessages>, pub model_messages: Option<ModelMessages>,
#[serde(default)] #[serde(default)]
@@ -479,29 +478,27 @@ impl ModelInfo {
if let Some(model_messages) = &self.model_messages if let Some(model_messages) = &self.model_messages
&& let Some(template) = &model_messages.instructions_template && let Some(template) = &model_messages.instructions_template
{ {
// if we have a template, always use it if model_messages.instructions_variables.is_none() {
return template.clone();
}
let personality_message = model_messages let personality_message = model_messages
.get_personality_message(personality) .get_personality_message(personality)
.unwrap_or_default(); .unwrap_or_default();
template.replace(PERSONALITY_PLACEHOLDER, personality_message.as_str()) template.replace(PERSONALITY_PLACEHOLDER, personality_message.as_str())
} else { } else {
match personality { warn!(
Some(personality @ (Personality::Friendly | Personality::Pragmatic)) => { model = %self.slug,
trace!( "Model has no instruction template; returning empty instructions."
model = %self.slug, );
%personality, String::new()
"Model personality requested but model_messages is missing, falling back to base instructions."
);
}
Some(Personality::None) | None => {}
}
self.base_instructions.clone()
} }
} }
} }
/// A strongly-typed template for assembling model instructions and developer messages. If /// A strongly-typed template for assembling model instructions and developer messages.
/// instructions_* is populated and valid, it will override base_instructions. ///
/// When `instructions_variables` is absent, `instructions_template` is treated as literal text.
/// When variables are present but incomplete, missing values render as empty strings.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
pub struct ModelMessages { pub struct ModelMessages {
pub instructions_template: Option<String>, pub instructions_template: Option<String>,
@@ -619,9 +616,100 @@ impl From<&ModelUpgrade> for ModelInfoUpgrade {
/// Response wrapper for `/models`. /// Response wrapper for `/models`.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema, Default)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema, Default)]
pub struct ModelsResponse { pub struct ModelsResponse {
#[serde(
serialize_with = "serialize_model_infos_with_legacy_base",
deserialize_with = "deserialize_model_infos_with_legacy_base"
)]
pub models: Vec<ModelInfo>, pub models: Vec<ModelInfo>,
} }
#[derive(Serialize)]
struct ModelInfoWithLegacyBaseInstructionsRef<'a> {
#[serde(flatten)]
model: &'a ModelInfo,
base_instructions: String,
}
#[derive(Deserialize)]
struct ModelInfoWithLegacyBaseInstructions {
#[serde(default)]
base_instructions: Option<String>,
#[serde(flatten)]
model: ModelInfo,
}
/// Serializes catalog models with the deprecated top-level instruction field required by older
/// clients.
#[doc(hidden)]
pub fn serialize_model_infos_with_legacy_base<S>(
models: &[ModelInfo],
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
models
.iter()
.map(|model| ModelInfoWithLegacyBaseInstructionsRef {
model,
base_instructions: model.get_model_instructions(/*personality*/ None),
})
.collect::<Vec<_>>()
.serialize(serializer)
}
/// Deserializes catalog models while promoting the legacy top-level instruction field into Model
/// Messages V2 when no canonical instruction template is present.
#[doc(hidden)]
pub fn deserialize_model_infos_with_legacy_base<'de, D>(
deserializer: D,
) -> Result<Vec<ModelInfo>, D::Error>
where
D: Deserializer<'de>,
{
let models = Vec::<ModelInfoWithLegacyBaseInstructions>::deserialize(deserializer)?;
models
.into_iter()
.map(|legacy_model| {
let ModelInfoWithLegacyBaseInstructions {
base_instructions,
mut model,
} = legacy_model;
if let Some(base_instructions) = base_instructions
&& model
.model_messages
.as_ref()
.and_then(|messages| messages.instructions_template.as_ref())
.is_none()
{
let 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,
});
messages.instructions_template = Some(base_instructions);
}
if model
.model_messages
.as_ref()
.and_then(|messages| messages.instructions_template.as_ref())
.is_none()
{
let model_slug = &model.slug;
return Err(D::Error::custom(format!(
"model `{model_slug}` is missing both `base_instructions` and \
`model_messages.instructions_template`"
)));
}
Ok(model)
})
.collect()
}
// convert ModelInfo to ModelPreset // convert ModelInfo to ModelPreset
impl From<ModelInfo> for ModelPreset { impl From<ModelInfo> for ModelPreset {
fn from(info: ModelInfo) -> Self { fn from(info: ModelInfo) -> Self {
@@ -733,7 +821,6 @@ mod tests {
default_service_tier: None, default_service_tier: None,
availability_nux: None, availability_nux: None,
upgrade: None, upgrade: None,
base_instructions: "base".to_string(),
model_messages: spec, model_messages: spec,
include_skills_usage_instructions: false, include_skills_usage_instructions: false,
supports_reasoning_summary_parameter: true, supports_reasoning_summary_parameter: true,
@@ -991,7 +1078,7 @@ mod tests {
} }
#[test] #[test]
fn get_model_instructions_always_strips_placeholder() { fn get_model_instructions_strips_placeholder_with_incomplete_variables() {
let model = test_model(Some(ModelMessages { let model = test_model(Some(ModelMessages {
instructions_template: Some("Hello\n{{ personality }}".to_string()), instructions_template: Some("Hello\n{{ personality }}".to_string()),
instructions_variables: Some(ModelInstructionsVariables { instructions_variables: Some(ModelInstructionsVariables {
@@ -1005,18 +1092,10 @@ mod tests {
permissions: None, permissions: None,
token_budget: None, token_budget: None,
})); }));
assert_eq!(
model.get_model_instructions(Some(Personality::Friendly)),
"Hello\nfriendly"
);
assert_eq!( assert_eq!(
model.get_model_instructions(Some(Personality::Pragmatic)), model.get_model_instructions(Some(Personality::Pragmatic)),
"Hello\n" "Hello\n"
); );
assert_eq!(
model.get_model_instructions(Some(Personality::None)),
"Hello\n"
);
assert_eq!( assert_eq!(
model.get_model_instructions(/*personality*/ None), model.get_model_instructions(/*personality*/ None),
"Hello\n" "Hello\n"
@@ -1054,7 +1133,7 @@ mod tests {
} }
#[test] #[test]
fn get_model_instructions_falls_back_when_template_is_missing() { fn get_model_instructions_is_empty_when_template_is_missing() {
let model = test_model(Some(ModelMessages { let model = test_model(Some(ModelMessages {
instructions_template: None, instructions_template: None,
instructions_variables: Some(ModelInstructionsVariables { instructions_variables: Some(ModelInstructionsVariables {
@@ -1071,7 +1150,158 @@ mod tests {
let instructions = model.get_model_instructions(Some(Personality::Friendly)); let instructions = model.get_model_instructions(Some(Personality::Friendly));
assert_eq!(instructions, "base"); assert_eq!(instructions, "");
}
#[test]
fn get_model_instructions_is_empty_when_model_messages_is_missing() {
let model = test_model(/*spec*/ None);
assert_eq!(
model.get_model_instructions(Some(Personality::Friendly)),
""
);
}
#[test]
fn models_response_promotes_legacy_base_instructions() {
let mut value = serde_json::to_value(ModelsResponse {
models: vec![test_model(/*spec*/ None)],
})
.expect("serialize models response");
value["models"][0]["base_instructions"] = serde_json::json!("legacy instructions");
let response: ModelsResponse =
serde_json::from_value(value).expect("deserialize legacy models response");
let model = &response.models[0];
assert_eq!(
model.model_messages,
Some(ModelMessages {
instructions_template: Some("legacy instructions".to_string()),
instructions_variables: None,
approvals: None,
collaboration_modes: None,
auto_review: None,
permissions: None,
token_budget: None,
})
);
assert_eq!(
model.get_model_instructions(/*personality*/ None),
"legacy instructions"
);
let serialized = serde_json::to_value(response).expect("serialize canonical response");
assert_eq!(
serialized["models"][0]["base_instructions"],
"legacy instructions"
);
}
#[test]
fn models_response_rejects_model_without_instruction_source() {
let mut value = serde_json::to_value(ModelsResponse {
models: vec![test_model(/*spec*/ None)],
})
.expect("serialize models response");
value["models"][0]
.as_object_mut()
.expect("model should serialize as an object")
.remove("base_instructions")
.expect("serialized model should include legacy base instructions");
let error = serde_json::from_value::<ModelsResponse>(value)
.expect_err("model without instructions should be rejected");
assert_eq!(
error.to_string(),
"model `test-model` is missing both `base_instructions` and \
`model_messages.instructions_template`"
);
}
#[test]
fn models_response_serializes_rendered_legacy_base_instructions() {
let response = ModelsResponse {
models: vec![test_model(Some(ModelMessages {
instructions_template: Some("before {{ personality }} after".to_string()),
instructions_variables: Some(ModelInstructionsVariables {
personality_default: Some("default".to_string()),
personality_friendly: Some("friendly".to_string()),
personality_pragmatic: Some("pragmatic".to_string()),
}),
approvals: None,
collaboration_modes: None,
auto_review: None,
permissions: None,
token_budget: None,
}))],
};
let serialized = serde_json::to_value(response).expect("serialize models response");
assert_eq!(
serialized["models"][0]["base_instructions"],
"before default after"
);
}
#[test]
fn models_response_prefers_template_and_preserves_message_siblings() {
let messages = ModelMessages {
instructions_template: None,
instructions_variables: None,
approvals: Some(ApprovalMessages {
on_request: Some("approval".to_string()),
on_request_auto_review: None,
never: Some("never approval".to_string()),
unless_trusted: Some("unless-trusted approval".to_string()),
}),
collaboration_modes: Some(CollaborationModeMessages {
default: Some("default collaboration".to_string()),
plan: Some("plan collaboration".to_string()),
}),
auto_review: Some(AutoReviewMessages {
policy: Some("policy".to_string()),
policy_template: None,
}),
permissions: Some(PermissionMessages {
danger_full_access: None,
workspace_write: Some("workspace".to_string()),
read_only: None,
}),
token_budget: None,
};
let mut value = serde_json::to_value(ModelsResponse {
models: vec![test_model(Some(messages.clone()))],
})
.expect("serialize models response");
value["models"][0]["base_instructions"] = serde_json::json!("legacy instructions");
let response: ModelsResponse =
serde_json::from_value(value).expect("deserialize legacy models response");
let mut expected_messages = messages;
expected_messages.instructions_template = Some("legacy instructions".to_string());
assert_eq!(response.models[0].model_messages, Some(expected_messages));
let canonical_messages = ModelMessages {
instructions_template: Some("canonical instructions".to_string()),
instructions_variables: None,
approvals: None,
collaboration_modes: None,
auto_review: None,
permissions: None,
token_budget: None,
};
let mut value = serde_json::to_value(ModelsResponse {
models: vec![test_model(Some(canonical_messages.clone()))],
})
.expect("serialize models response");
value["models"][0]["base_instructions"] = serde_json::json!("legacy instructions");
let response: ModelsResponse =
serde_json::from_value(value).expect("deserialize mixed models response");
assert_eq!(response.models[0].model_messages, Some(canonical_messages));
} }
#[test] #[test]
@@ -1160,7 +1390,6 @@ mod tests {
"supported_in_api": true, "supported_in_api": true,
"priority": 1, "priority": 1,
"upgrade": null, "upgrade": null,
"base_instructions": "base",
"model_messages": null, "model_messages": null,
"default_reasoning_summary": "auto", "default_reasoning_summary": "auto",
"support_verbosity": false, "support_verbosity": false,
-1
View File
@@ -18,7 +18,6 @@ fn model_info() -> ModelInfo {
"priority": 1, "priority": 1,
"availability_nux": null, "availability_nux": null,
"upgrade": null, "upgrade": null,
"base_instructions": "base",
"model_messages": null, "model_messages": null,
"default_reasoning_summary": "auto", "default_reasoning_summary": "auto",
"support_verbosity": false, "support_verbosity": false,
-1
View File
@@ -25,7 +25,6 @@ fn model_with_shell_type(shell_type: ConfigShellToolType) -> ModelInfo {
default_service_tier: None, default_service_tier: None,
availability_nux: None, availability_nux: None,
upgrade: None, upgrade: None,
base_instructions: String::new(),
model_messages: None, model_messages: None,
include_skills_usage_instructions: false, include_skills_usage_instructions: false,
supports_reasoning_summary_parameter: true, supports_reasoning_summary_parameter: true,
@@ -283,7 +283,6 @@ fn test_model_info(slug: &str, priority: i32, supports_fast_mode: bool) -> Model
"default_service_tier": null, "default_service_tier": null,
"availability_nux": null, "availability_nux": null,
"upgrade": null, "upgrade": null,
"base_instructions": "base instructions",
"default_reasoning_summary": "none", "default_reasoning_summary": "none",
"support_verbosity": false, "support_verbosity": false,
"default_verbosity": null, "default_verbosity": null,