mirror of
https://github.com/openai/codex.git
synced 2026-11-07 16:35:32 +00:00
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:
1 parent
bbcf5e10fb
commit
df72fdb415
27 files changed
+655
-311
No files matched your search
@@ -268,7 +268,6 @@ fn remote_model_with_auto_review_override(slug: &str, review_model: &str) -> Mod
|
||||
service_tiers: Vec::new(),
|
||||
default_service_tier: None,
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
include_skills_usage_instructions: false,
|
||||
supports_reasoning_summary_parameter: true,
|
||||
|
||||
@@ -97,7 +97,6 @@ fn test_model_info(
|
||||
service_tiers: Vec::new(),
|
||||
default_service_tier: None,
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
include_skills_usage_instructions: false,
|
||||
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(),
|
||||
default_service_tier: None,
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
include_skills_usage_instructions: false,
|
||||
supports_reasoning_summary_parameter: true,
|
||||
|
||||
@@ -13,6 +13,7 @@ use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::openai_models::ConfigShellToolType;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ModelMessages;
|
||||
use codex_protocol::openai_models::ModelVisibility;
|
||||
use codex_protocol::openai_models::ModelsResponse;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
@@ -34,6 +35,7 @@ use core_test_support::wait_for_event;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use wiremock::MockServer;
|
||||
|
||||
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<()> {
|
||||
let server = MockServer::start().await;
|
||||
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(
|
||||
&server,
|
||||
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());
|
||||
builder = builder
|
||||
.with_pre_build_hook(move |home| {
|
||||
let cache = ModelsCache {
|
||||
let mut cache = serde_json::to_value(ModelsCache {
|
||||
fetched_at: Utc::now(),
|
||||
etag: None,
|
||||
client_version: Some(client_version_to_whole()),
|
||||
models: vec![cached_model],
|
||||
};
|
||||
let cache_path = home.join(CACHE_FILE);
|
||||
write_cache_sync(&cache_path, &cache).expect("write cache");
|
||||
})
|
||||
.expect("serialize 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| {
|
||||
config.model = Some(VERSIONED_MODEL.to_string());
|
||||
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 = models_manager
|
||||
.list_models(
|
||||
@@ -314,6 +331,11 @@ async fn uses_cache_when_version_matches() -> Result<()> {
|
||||
0,
|
||||
"/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(())
|
||||
}
|
||||
@@ -480,8 +502,15 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo {
|
||||
service_tiers: Vec::new(),
|
||||
default_service_tier: None,
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
model_messages: Some(ModelMessages {
|
||||
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,
|
||||
supports_reasoning_summary_parameter: true,
|
||||
default_reasoning_summary: ReasoningSummary::Auto,
|
||||
|
||||
@@ -110,6 +110,7 @@ async fn catalog_approval_message_is_sent_in_initial_permissions() -> Result<()>
|
||||
)
|
||||
.await;
|
||||
let model_slug = "catalog-approvals-model";
|
||||
let override_instructions = "literal {{ personality }} override";
|
||||
let model = model_with_approval_messages(
|
||||
model_slug,
|
||||
"catalog user approval instructions",
|
||||
@@ -118,16 +119,19 @@ async fn catalog_approval_message_is_sent_in_initial_permissions() -> Result<()>
|
||||
let mut builder = test_codex()
|
||||
.with_model(model_slug)
|
||||
.with_config(move |config| {
|
||||
config.base_instructions = Some(override_instructions.to_string());
|
||||
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
config.model_catalog = Some(ModelsResponse {
|
||||
models: vec![model],
|
||||
});
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
let test = builder.build_with_auto_env(&server).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!(permissions[0].contains("catalog user approval instructions"));
|
||||
assert!(permissions[0].contains("Filesystem sandboxing defines"));
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use codex_config::types::Personality;
|
||||
use codex_features::Feature;
|
||||
use codex_models_manager::bundled_models_response;
|
||||
use codex_models_manager::manager::RefreshStrategy;
|
||||
use codex_models_manager::manager::SharedModelsManager;
|
||||
use codex_models_manager::model_info::BASE_INSTRUCTIONS;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::openai_models::ConfigShellToolType;
|
||||
@@ -18,7 +20,6 @@ use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::Op;
|
||||
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_sse_once;
|
||||
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::wait_for_event;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::Duration;
|
||||
use tokio::time::Instant;
|
||||
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)]
|
||||
async fn user_turn_personality_none_does_not_add_update_message() -> anyhow::Result<()> {
|
||||
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 mut builder = test_codex()
|
||||
.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();
|
||||
model_info.model_messages = None;
|
||||
if let Some(model_messages) = model_info.model_messages.as_mut() {
|
||||
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| {
|
||||
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)]
|
||||
async fn instructions_uses_base_if_feature_disabled() -> anyhow::Result<()> {
|
||||
let codex_home = TempDir::new().expect("create temp dir");
|
||||
let mut config = load_default_config_for_test(&codex_home).await;
|
||||
config
|
||||
.features
|
||||
.disable(Feature::Personality)
|
||||
.expect("test config should allow feature update");
|
||||
config.personality = Some(Personality::Friendly);
|
||||
async fn disabled_personality_sends_remote_default_instructions() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = MockServer::start().await;
|
||||
let remote_slug = "remote-disabled-personality";
|
||||
let default_personality = "default remote personality";
|
||||
let friendly_personality = "friendly remote personality";
|
||||
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!(
|
||||
model_info.get_model_instructions(config.personality),
|
||||
model_info.base_instructions
|
||||
response.single_request().instructions_text(),
|
||||
format!("remote base\n{default_personality}")
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -587,6 +584,11 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()>
|
||||
|
||||
let requests = resp_mock.requests();
|
||||
assert_eq!(requests.len(), 2, "expected two requests");
|
||||
assert!(
|
||||
requests
|
||||
.iter()
|
||||
.all(|request| request.instructions_text() == BASE_INSTRUCTIONS)
|
||||
);
|
||||
let request = requests
|
||||
.last()
|
||||
.expect("expected personality update request");
|
||||
@@ -631,7 +633,6 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow
|
||||
service_tiers: Vec::new(),
|
||||
default_service_tier: None,
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: Some(ModelMessages {
|
||||
instructions_template: Some("Base instructions\n{{ personality }}\n".to_string()),
|
||||
instructions_variables: Some(ModelInstructionsVariables {
|
||||
@@ -752,7 +753,6 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
|
||||
service_tiers: Vec::new(),
|
||||
default_service_tier: None,
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: Some(ModelMessages {
|
||||
instructions_template: Some("Base instructions\n{{ personality }}\n".to_string()),
|
||||
instructions_variables: Some(ModelInstructionsVariables {
|
||||
|
||||
@@ -154,7 +154,7 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> {
|
||||
})
|
||||
.build(&server)
|
||||
.await?;
|
||||
let base_instructions = thread_manager
|
||||
let model_info = thread_manager
|
||||
.get_models_manager()
|
||||
.get_model_info(
|
||||
config
|
||||
@@ -163,8 +163,8 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> {
|
||||
.expect("test config should have a model"),
|
||||
&config.to_models_manager_config(),
|
||||
)
|
||||
.await
|
||||
.base_instructions;
|
||||
.await;
|
||||
let base_instructions = model_info.get_model_instructions(config.personality);
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
|
||||
@@ -6,10 +6,12 @@ use codex_model_provider_info::built_in_model_providers;
|
||||
use codex_models_manager::bundled_models_response;
|
||||
use codex_models_manager::manager::RefreshStrategy;
|
||||
use codex_models_manager::manager::SharedModelsManager;
|
||||
use codex_models_manager::model_info::BASE_INSTRUCTIONS;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::openai_models::ConfigShellToolType;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ModelMessages;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::openai_models::ModelVisibility;
|
||||
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_sequence;
|
||||
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_sandbox;
|
||||
use core_test_support::test_codex::TestCodex;
|
||||
@@ -49,10 +52,36 @@ use tokio::time::Instant;
|
||||
use tokio::time::sleep;
|
||||
use tokio::time::timeout;
|
||||
use wiremock::BodyPrintLimit;
|
||||
use wiremock::Mock;
|
||||
use wiremock::MockServer;
|
||||
use wiremock::ResponseTemplate;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
|
||||
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)]
|
||||
async fn remote_models_get_model_info_uses_longest_matching_prefix() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
@@ -73,12 +102,28 @@ async fn remote_models_get_model_info_uses_longest_matching_prefix() -> Result<(
|
||||
);
|
||||
let specific = ModelInfo {
|
||||
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
|
||||
};
|
||||
let generic = ModelInfo {
|
||||
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
|
||||
};
|
||||
mount_models_once(
|
||||
@@ -115,7 +160,10 @@ async fn remote_models_get_model_info_uses_longest_matching_prefix() -> Result<(
|
||||
.await;
|
||||
|
||||
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(())
|
||||
}
|
||||
@@ -487,7 +535,6 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> {
|
||||
service_tiers: Vec::new(),
|
||||
default_service_tier: None,
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
include_skills_usage_instructions: false,
|
||||
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)]
|
||||
async fn remote_models_apply_remote_base_instructions() -> Result<()> {
|
||||
async fn remote_models_apply_legacy_instructions() -> Result<()> {
|
||||
skip_if_no_network!(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 remote_base = "Use the remote base instructions only.";
|
||||
let remote_instructions = "Use the remote instructions template only.";
|
||||
let remote_model = ModelInfo {
|
||||
slug: model.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(),
|
||||
default_service_tier: None,
|
||||
upgrade: None,
|
||||
base_instructions: remote_base.to_string(),
|
||||
model_messages: None,
|
||||
model_messages: Some(ModelMessages {
|
||||
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,
|
||||
supports_reasoning_summary_parameter: true,
|
||||
default_reasoning_summary: ReasoningSummary::Auto,
|
||||
@@ -761,21 +815,34 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
|
||||
effective_context_window_percent: 95,
|
||||
experimental_supported_tools: Vec::new(),
|
||||
};
|
||||
mount_models_once(
|
||||
&server,
|
||||
ModelsResponse {
|
||||
models: vec![remote_model],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
let mut models_response = serde_json::to_value(ModelsResponse {
|
||||
models: vec![remote_model],
|
||||
})?;
|
||||
models_response["models"][0]
|
||||
.as_object_mut()
|
||||
.expect("model should serialize as an object")
|
||||
.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,
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("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;
|
||||
|
||||
@@ -795,6 +862,31 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
|
||||
let models_manager = thread_manager.get_models_manager();
|
||||
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(
|
||||
&codex,
|
||||
codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
@@ -804,7 +896,6 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let cwd_path = cwd.abs();
|
||||
let (sandbox_policy, permission_profile) =
|
||||
turn_permission_fields(PermissionProfile::Disabled, cwd_path.as_path());
|
||||
codex
|
||||
@@ -832,9 +923,20 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
|
||||
let base_model_info = models_manager
|
||||
.get_model_info("gpt-5.2", &config.to_models_manager_config())
|
||||
.await;
|
||||
let body = response_mock.single_request().body_json();
|
||||
let instructions = body["instructions"].as_str().unwrap();
|
||||
assert_eq!(instructions, base_model_info.base_instructions);
|
||||
let requests = response_mock.requests();
|
||||
assert_eq!(requests.len(), 2, "expected two model requests");
|
||||
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(())
|
||||
}
|
||||
@@ -1268,7 +1370,6 @@ fn test_remote_model_with_policy(
|
||||
service_tiers: Vec::new(),
|
||||
default_service_tier: None,
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
include_skills_usage_instructions: false,
|
||||
supports_reasoning_summary_parameter: true,
|
||||
|
||||
@@ -2259,7 +2259,6 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re
|
||||
service_tiers: Vec::new(),
|
||||
default_service_tier: None,
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
include_skills_usage_instructions: false,
|
||||
supports_reasoning_summary_parameter: true,
|
||||
|
||||
@@ -78,7 +78,6 @@ fn test_model_info(
|
||||
service_tiers,
|
||||
default_service_tier: None,
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
include_skills_usage_instructions: false,
|
||||
supports_reasoning_summary_parameter: true,
|
||||
|
||||
@@ -1370,7 +1370,6 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an
|
||||
service_tiers: Vec::new(),
|
||||
default_service_tier: None,
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
include_skills_usage_instructions: false,
|
||||
supports_reasoning_summary_parameter: true,
|
||||
|
||||
Reference in new issue
Block a user