diff --git a/codex-rs/config/src/types.rs b/codex-rs/config/src/types.rs index 449b920f7f..a16ad96b34 100644 --- a/codex-rs/config/src/types.rs +++ b/codex-rs/config/src/types.rs @@ -21,6 +21,7 @@ pub use codex_protocol::config_types::AltScreenMode; pub use codex_protocol::config_types::ApprovalsReviewer; pub use codex_protocol::config_types::ModeKind; pub use codex_protocol::config_types::Personality; +use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE; pub use codex_protocol::config_types::ServiceTier; use codex_protocol::config_types::ToolExposureSurface; pub use codex_protocol::config_types::WebSearchMode; @@ -353,6 +354,8 @@ pub struct MemoriesToml { pub extract_model: Option, /// Model used for memory consolidation. pub consolidation_model: Option, + /// Service tier for memory consolidation. Defaults to Standard (`"default"`). + pub consolidation_service_tier: Option, } /// Effective memories settings after defaults are applied. @@ -372,6 +375,7 @@ pub struct MemoriesConfig { pub min_rate_limit_remaining_percent: i64, pub extract_model: Option, pub consolidation_model: Option, + pub consolidation_service_tier: String, } impl Default for MemoriesConfig { @@ -391,6 +395,7 @@ impl Default for MemoriesConfig { min_rate_limit_remaining_percent: DEFAULT_MEMORIES_MIN_RATE_LIMIT_REMAINING_PERCENT, extract_model: None, consolidation_model: None, + consolidation_service_tier: SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string(), } } } @@ -439,6 +444,13 @@ impl From for MemoriesConfig { .clamp(0, 100), extract_model: toml.extract_model, consolidation_model: toml.consolidation_model, + consolidation_service_tier: toml + .consolidation_service_tier + .map(|tier| { + ServiceTier::from_request_value(&tier) + .map_or(tier, |tier| tier.request_value().to_string()) + }) + .unwrap_or(defaults.consolidation_service_tier), } } } diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 1348a9ef9e..f11dfba29c 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -2840,6 +2840,10 @@ "description": "Model used for memory consolidation.", "type": "string" }, + "consolidation_service_tier": { + "description": "Service tier for memory consolidation. Defaults to Standard (`\"default\"`).", + "type": "string" + }, "dedicated_tools": { "description": "When `true`, expose dedicated memory tools through the extension tool surface.", "type": "boolean" diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 52d68896c2..0c4e222207 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -336,6 +336,7 @@ min_rollout_idle_hours = 24 min_rate_limit_remaining_percent = 12 extract_model = "gpt-5-mini" consolidation_model = "gpt-5.2" +consolidation_service_tier = "fast" "#; let memories_cfg = toml::from_str::(memories).expect("TOML deserialization should succeed"); @@ -355,6 +356,7 @@ consolidation_model = "gpt-5.2" min_rate_limit_remaining_percent: Some(12), extract_model: Some("gpt-5-mini".to_string()), consolidation_model: Some("gpt-5.2".to_string()), + consolidation_service_tier: Some("fast".to_string()), }), memories_cfg.memories ); @@ -383,6 +385,7 @@ consolidation_model = "gpt-5.2" min_rate_limit_remaining_percent: 12, extract_model: Some("gpt-5-mini".to_string()), consolidation_model: Some("gpt-5.2".to_string()), + consolidation_service_tier: "priority".to_string(), } ); diff --git a/codex-rs/memories/write/src/phase2.rs b/codex-rs/memories/write/src/phase2.rs index 277db05e3f..549d707681 100644 --- a/codex-rs/memories/write/src/phase2.rs +++ b/codex-rs/memories/write/src/phase2.rs @@ -365,6 +365,7 @@ mod agent { .unwrap_or_else(|| provider.memory_consolidation_preferred_model().to_string()), ); agent_config.model_reasoning_effort = Some(crate::stage_two::REASONING_EFFORT); + agent_config.service_tier = Some(config.memories.consolidation_service_tier.clone()); Some(agent_config) } diff --git a/codex-rs/memories/write/src/startup_tests.rs b/codex-rs/memories/write/src/startup_tests.rs index 612b3ac984..d37bf2f042 100644 --- a/codex-rs/memories/write/src/startup_tests.rs +++ b/codex-rs/memories/write/src/startup_tests.rs @@ -24,6 +24,8 @@ use codex_protocol::ThreadId; use codex_protocol::config_types::ServiceTier; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; +use codex_protocol::openai_models::ModelInfo; +use codex_protocol::openai_models::ModelServiceTier; use codex_protocol::openai_models::ModelsResponse; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::EventMsg; @@ -48,6 +50,7 @@ use core_test_support::responses::sse; use core_test_support::responses::sse_failed; use core_test_support::responses::start_mock_server; use core_test_support::test_codex::TestCodex; +use core_test_support::test_codex::TestCodexBuilder; use core_test_support::test_codex::test_codex; use core_test_support::wait_for_event; use pretty_assertions::assert_eq; @@ -86,7 +89,9 @@ async fn dual_write_prepares_both_roots_without_importing_old_notes() -> anyhow: tokio::fs::write(old_notes.join("old.md"), "old memory").await?; let mut memories = startup_test_memories_config(); memories.dual_write = true; - let test = build_test_codex_with_memories_config(&server, Arc::clone(&home), memories).await?; + let test = memory_test_codex(Arc::clone(&home), memories) + .build(&server) + .await?; trigger_memories_startup(&test).await; wait_for_dir(&home.path().join("memories/extensions/ad_hoc")).await?; wait_for_dir(&home.path().join("memories_v2/extensions/ad_hoc")).await?; @@ -788,7 +793,7 @@ async fn memories_startup_phase1_provider_default_drives_request_model() -> anyh } #[tokio::test] -async fn memories_startup_phase2_provider_default_drives_request_model() -> anyhow::Result<()> { +async fn memories_startup_phase2_uses_provider_model_and_standard_tier() -> anyhow::Result<()> { let server = start_mock_server().await; let home = Arc::new(TempDir::new()?); let request = @@ -799,6 +804,7 @@ async fn memories_startup_phase2_provider_default_drives_request_model() -> anyh request.body_json()["model"].as_str(), Some(MOCK_PROVIDER_PHASE_TWO_MODEL) ); + assert_eq!(request.body_json().get("service_tier"), None); Ok(()) } @@ -843,7 +849,9 @@ async fn run_memory_phase_one_model_request_test( memories: MemoriesConfig, ) -> anyhow::Result { let version = memories.version; - let test = build_test_codex_with_memories_config(server, Arc::clone(&home), memories).await?; + let test = memory_test_codex(Arc::clone(&home), memories) + .build(server) + .await?; let provider = Arc::new(MockMemoryModelProvider::new( test.config.model_provider.clone(), Some(test.thread_manager.auth_manager()), @@ -1055,7 +1063,27 @@ async fn run_memory_phase_two_model_request_test( memories: MemoriesConfig, ) -> anyhow::Result { let version = memories.version; - let test = build_test_codex_with_memories_config(server, home.clone(), memories).await?; + // Both models support Ultrafast so an inherited tier would reach the wire. + let advertise_ultrafast = |model: &mut ModelInfo| { + model.service_tiers.push(ModelServiceTier { + id: "ultrafast".to_string(), + name: "Ultrafast".to_string(), + description: String::new(), + }); + model.default_service_tier = Some("ultrafast".to_string()); + }; + let test = memory_test_codex(Arc::clone(&home), memories) + .with_model_info_override(MOCK_PROVIDER_PHASE_TWO_MODEL, advertise_ultrafast) + .with_model_info_override("mock.interactive", advertise_ultrafast) + .with_config(|config| { + config + .features + .enable(Feature::UltrafastMode) + .expect("enable Ultrafast"); + config.service_tier = Some("ultrafast".to_string()); + }) + .build_with_auto_env(server) + .await?; let provider = Arc::new(MockMemoryModelProvider::new( test.config.model_provider.clone(), Some(test.thread_manager.auth_manager()), @@ -1165,27 +1193,21 @@ async fn build_test_codex( server: &wiremock::MockServer, home: Arc, ) -> anyhow::Result { - build_test_codex_with_memories_config(server, home, startup_test_memories_config()).await -} - -async fn build_test_codex_with_memories_config( - server: &wiremock::MockServer, - home: Arc, - memories: MemoriesConfig, -) -> anyhow::Result { - test_codex() - .with_home(home) - .with_config(move |config| { - config - .features - .enable(Feature::Sqlite) - .expect("test config should allow feature update"); - config.memories = memories; - }) + memory_test_codex(home, startup_test_memories_config()) .build(server) .await } +fn memory_test_codex(home: Arc, memories: MemoriesConfig) -> TestCodexBuilder { + test_codex().with_home(home).with_config(move |config| { + config + .features + .enable(Feature::Sqlite) + .expect("test config should allow feature update"); + config.memories = memories; + }) +} + async fn init_state_db(home: &Arc) -> anyhow::Result> { let db = codex_state::StateRuntime::init( codex_state::SqliteConfig::new_for_testing(home.path().abs()), @@ -1613,6 +1635,8 @@ async fn memories_startup_phase2_v2_consolidates_without_a_handbook() -> anyhow: let home = Arc::new(TempDir::new()?); let mut memories = startup_test_memories_config(); memories.version = codex_protocol::MemoryVersion::V2; - run_memory_phase_two_model_request_test(&server, home, memories).await?; + memories.consolidation_service_tier = "flex".to_string(); + let request = run_memory_phase_two_model_request_test(&server, home, memories).await?; + assert_eq!(request.body_json()["service_tier"], "flex"); Ok(()) }