diff --git a/codex-rs/codex-api/src/requests/chat.rs b/codex-rs/codex-api/src/requests/chat.rs index c1ba89f37..d5ac188ef 100644 --- a/codex-rs/codex-api/src/requests/chat.rs +++ b/codex-rs/codex-api/src/requests/chat.rs @@ -74,7 +74,7 @@ impl<'a> ChatRequestBuilder<'a> { ResponseItem::CustomToolCallOutput { .. } => {} ResponseItem::WebSearchCall { .. } => {} ResponseItem::GhostSnapshot { .. } => {} - ResponseItem::CompactionSummary { .. } => {} + ResponseItem::Compaction { .. } => {} } } @@ -303,7 +303,7 @@ impl<'a> ChatRequestBuilder<'a> { ResponseItem::Reasoning { .. } | ResponseItem::WebSearchCall { .. } | ResponseItem::Other - | ResponseItem::CompactionSummary { .. } => { + | ResponseItem::Compaction { .. } => { continue; } } diff --git a/codex-rs/core/gpt_5_2_prompt.md b/codex-rs/core/gpt_5_2_prompt.md index fdb1e3d5d..99a3f49bb 100644 --- a/codex-rs/core/gpt_5_2_prompt.md +++ b/codex-rs/core/gpt_5_2_prompt.md @@ -33,33 +33,6 @@ Unless the user explicitly asks for a plan, asks a question about the code, is b ## Responsiveness -### User Updates Spec -You'll work for stretches with tool calls — it's critical to keep the user updated as you work. - -Frequency & Length: -- Send short updates (1–2 sentences) whenever there is a meaningful, important insight you need to share with the user to keep them informed. -- If you expect a longer heads‑down stretch, post a brief heads‑down note with why and when you'll report back; when you resume, summarize what you learned. -- Only the initial plan, plan updates, and final recap can be longer, with multiple bullets and paragraphs - -Tone: -- Friendly, confident, senior-engineer energy. Positive, collaborative, humble; fix mistakes quickly. - -Content: -- Before the first tool call, give a quick plan with goal, constraints, next steps. -- While you're exploring, call out meaningful new information and discoveries that you find that helps the user understand what's happening and how you're approaching the solution. -- If you change the plan (e.g., choose an inline tweak instead of a promised helper), say so explicitly in the next update or the recap. - -**Examples:** - -- “I’ve explored the repo; now checking the API route definitions.” -- “Next, I’ll patch the config and update the related tests.” -- “I’m about to scaffold the CLI commands and helper functions.” -- “Ok cool, so I’ve wrapped my head around the repo. Now digging into the API routes.” -- “Config’s looking tidy. Next up is patching helpers to keep things in sync.” -- “Finished poking at the DB gateway. I will now chase down error handling.” -- “Alright, build pipeline order is interesting. Checking how it reports failures.” -- “Spotted a clever caching util; now hunting where it gets used.” - ## Planning You 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. @@ -221,15 +194,7 @@ If you're operating in an existing codebase, you should make sure you do exactly You should use judicious initiative to decide on the right level of detail and complexity to deliver based on the user's needs. This means showing good judgment that you're capable of doing the right extras without gold-plating. This might be demonstrated by high-value, creative touches when scope of the task is vague; while being surgical and targeted when scope is tightly specified. -## Sharing progress updates - -For especially longer tasks that you work on (i.e. requiring many tool calls, or a plan with multiple steps), you should provide progress updates back to the user at reasonable intervals. These updates should be structured as a concise sentence or two (no more than 8-10 words long) recapping progress so far in plain language: this update demonstrates your understanding of what needs to be done, progress so far (i.e. files explores, subtasks complete), and where you're going next. - -Before doing large chunks of work that may incur latency as experienced by the user (i.e. writing a new file), you should send a concise message to the user with an update indicating what you're about to do to ensure they know what you're spending time on. Don't start editing or writing large files before informing the user what you are doing and why. - -The messages you send before tool calls should describe what is immediately about to be done next in very concise language. If there was previous work done, this preamble message should also include a note about the work done so far to bring the user along. - -## Presenting your work and final message +## Presenting your work Your final message should read naturally, like an update from a concise teammate. For casual conversation, brainstorming tasks, or quick questions from the user, respond in a friendly, conversational tone. You should ask questions, suggest ideas, and adapt to the user’s style. If you've finished a large amount of work, when describing what you've done to the user, you should follow the final answer formatting guidelines to communicate substantive changes. You don't need to add structured formatting for one-word answers, greetings, or purely conversational exchanges. diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index c546bb583..04603152b 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -2175,7 +2175,10 @@ pub(crate) async fn run_task( // as long as compaction works well in getting us way below the token limit, we shouldn't worry about being in an infinite loop. if token_limit_reached { - if should_use_remote_compact_task(&sess) { + if should_use_remote_compact_task( + sess.as_ref(), + &turn_context.client.get_provider(), + ) { run_inline_remote_auto_compact_task(sess.clone(), turn_context.clone()) .await; } else { diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index 7ce325a75..1a90b7b22 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use crate::ModelProviderInfo; use crate::Prompt; use crate::client_common::ResponseEvent; use crate::codex::Session; @@ -18,7 +19,6 @@ use crate::truncate::TruncationPolicy; use crate::truncate::approx_token_count; use crate::truncate::truncate_text; use crate::util::backoff; -use codex_app_server_protocol::AuthMode; use codex_protocol::items::TurnItem; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseInputItem; @@ -32,13 +32,11 @@ pub const SUMMARIZATION_PROMPT: &str = include_str!("../templates/compact/prompt pub const SUMMARY_PREFIX: &str = include_str!("../templates/compact/summary_prefix.md"); const COMPACT_USER_MESSAGE_MAX_TOKENS: usize = 20_000; -pub(crate) fn should_use_remote_compact_task(session: &Session) -> bool { - session - .services - .auth_manager - .auth() - .is_some_and(|auth| auth.mode == AuthMode::ChatGPT) - && session.enabled(Feature::RemoteCompaction) +pub(crate) fn should_use_remote_compact_task( + session: &Session, + provider: &ModelProviderInfo, +) -> bool { + provider.is_openai() && session.enabled(Feature::RemoteCompaction) } pub(crate) async fn run_inline_auto_compact_task( diff --git a/codex-rs/core/src/context_manager/history.rs b/codex-rs/core/src/context_manager/history.rs index 9b75c836a..c18ad7df8 100644 --- a/codex-rs/core/src/context_manager/history.rs +++ b/codex-rs/core/src/context_manager/history.rs @@ -92,7 +92,7 @@ impl ContextManager { encrypted_content: Some(content), .. } - | ResponseItem::CompactionSummary { + | ResponseItem::Compaction { encrypted_content: content, } => estimate_reasoning_length(content.len()) as i64, item => { @@ -258,7 +258,7 @@ impl ContextManager { | ResponseItem::FunctionCall { .. } | ResponseItem::WebSearchCall { .. } | ResponseItem::CustomToolCall { .. } - | ResponseItem::CompactionSummary { .. } + | ResponseItem::Compaction { .. } | ResponseItem::GhostSnapshot { .. } | ResponseItem::Other => item.clone(), } @@ -277,7 +277,7 @@ fn is_api_message(message: &ResponseItem) -> bool { | ResponseItem::LocalShellCall { .. } | ResponseItem::Reasoning { .. } | ResponseItem::WebSearchCall { .. } - | ResponseItem::CompactionSummary { .. } => true, + | ResponseItem::Compaction { .. } => true, ResponseItem::GhostSnapshot { .. } => false, ResponseItem::Other => false, } diff --git a/codex-rs/core/src/model_provider_info.rs b/codex-rs/core/src/model_provider_info.rs index 22f0d9998..961739223 100644 --- a/codex-rs/core/src/model_provider_info.rs +++ b/codex-rs/core/src/model_provider_info.rs @@ -28,6 +28,8 @@ const MAX_STREAM_MAX_RETRIES: u64 = 100; const MAX_REQUEST_MAX_RETRIES: u64 = 100; pub const CHAT_WIRE_API_DEPRECATION_SUMMARY: &str = r#"Support for the "chat" wire API is deprecated and will soon be removed. Update your model provider definition in config.toml to use wire_api = "responses"."#; +const OPENAI_PROVIDER_NAME: &str = "OpenAI"; + /// Wire protocol that the provider speaks. Most third-party services only /// implement the classic OpenAI Chat Completions JSON schema, whereas OpenAI /// itself (and a handful of others) additionally expose the more modern @@ -210,7 +212,7 @@ impl ModelProviderInfo { } pub fn create_openai_provider() -> ModelProviderInfo { ModelProviderInfo { - name: "OpenAI".into(), + name: OPENAI_PROVIDER_NAME.into(), // Allow users to override the default OpenAI endpoint by // exporting `OPENAI_BASE_URL`. This is useful when pointing // Codex at a proxy, mock server, or Azure-style deployment @@ -247,6 +249,10 @@ impl ModelProviderInfo { requires_openai_auth: true, } } + + pub fn is_openai(&self) -> bool { + self.name == OPENAI_PROVIDER_NAME + } } pub const DEFAULT_LMSTUDIO_PORT: u16 = 1234; diff --git a/codex-rs/core/src/rollout/policy.rs b/codex-rs/core/src/rollout/policy.rs index fc6e4b9af..47f051df4 100644 --- a/codex-rs/core/src/rollout/policy.rs +++ b/codex-rs/core/src/rollout/policy.rs @@ -28,7 +28,7 @@ pub(crate) fn should_persist_response_item(item: &ResponseItem) -> bool { | ResponseItem::CustomToolCallOutput { .. } | ResponseItem::WebSearchCall { .. } | ResponseItem::GhostSnapshot { .. } - | ResponseItem::CompactionSummary { .. } => true, + | ResponseItem::Compaction { .. } => true, ResponseItem::Other => false, } } diff --git a/codex-rs/core/src/tasks/compact.rs b/codex-rs/core/src/tasks/compact.rs index 293116c16..a2a268fbb 100644 --- a/codex-rs/core/src/tasks/compact.rs +++ b/codex-rs/core/src/tasks/compact.rs @@ -25,7 +25,10 @@ impl SessionTask for CompactTask { _cancellation_token: CancellationToken, ) -> Option { let session = session.clone_session(); - if crate::compact::should_use_remote_compact_task(&session) { + if crate::compact::should_use_remote_compact_task( + session.as_ref(), + &ctx.client.get_provider(), + ) { crate::compact_remote::run_remote_compact_task(session, ctx).await } else { crate::compact::run_compact_task(session, ctx, input).await diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index 521a76845..bffb601eb 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -38,6 +38,7 @@ use core_test_support::responses::sse_failed; use core_test_support::responses::start_mock_server; use pretty_assertions::assert_eq; use serde_json::json; +use wiremock::MockServer; // --- Test helpers ----------------------------------------------------------- pub(super) const FIRST_REPLY: &str = "FIRST_REPLY"; @@ -100,6 +101,13 @@ fn json_fragment(text: &str) -> String { .to_string() } +fn non_openai_model_provider(server: &MockServer) -> ModelProviderInfo { + let mut provider = built_in_model_providers()["openai"].clone(); + provider.name = "OpenAI (test)".into(); + provider.base_url = Some(format!("{}/v1", server.uri())); + provider +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn summarize_context_three_requests_and_instructions() { skip_if_no_network!(); @@ -127,10 +135,7 @@ async fn summarize_context_three_requests_and_instructions() { let request_log = mount_sse_sequence(&server, vec![sse1, sse2, sse3]).await; // Build config pointing to the mock server and spawn Codex. - let model_provider = ModelProviderInfo { - base_url: Some(format!("{}/v1", server.uri())), - ..built_in_model_providers()["openai"].clone() - }; + let model_provider = non_openai_model_provider(&server); let home = TempDir::new().unwrap(); let mut config = load_default_config_for_test(&home); config.model_provider = model_provider; @@ -324,10 +329,7 @@ async fn manual_compact_uses_custom_prompt() { let custom_prompt = "Use this compact prompt instead"; - let model_provider = ModelProviderInfo { - base_url: Some(format!("{}/v1", server.uri())), - ..built_in_model_providers()["openai"].clone() - }; + let model_provider = non_openai_model_provider(&server); let home = TempDir::new().unwrap(); let mut config = load_default_config_for_test(&home); config.model_provider = model_provider; @@ -407,10 +409,7 @@ async fn manual_compact_emits_api_and_local_token_usage_events() { ]); mount_sse_once(&server, sse_compact).await; - let model_provider = ModelProviderInfo { - base_url: Some(format!("{}/v1", server.uri())), - ..built_in_model_providers()["openai"].clone() - }; + let model_provider = non_openai_model_provider(&server); let home = TempDir::new().unwrap(); let mut config = load_default_config_for_test(&home); config.model_provider = model_provider; @@ -467,7 +466,11 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() { let server = start_mock_server().await; + let non_openai_provider_name = non_openai_model_provider(&server).name; let codex = test_codex() + .with_config(move |config| { + config.model_provider.name = non_openai_provider_name; + }) .build(&server) .await .expect("build codex") @@ -1050,10 +1053,7 @@ async fn auto_compact_runs_after_token_limit_hit() { }; mount_sse_once_match(&server, fourth_matcher, sse4).await; - let model_provider = ModelProviderInfo { - base_url: Some(format!("{}/v1", server.uri())), - ..built_in_model_providers()["openai"].clone() - }; + let model_provider = non_openai_model_provider(&server); let home = TempDir::new().unwrap(); let mut config = load_default_config_for_test(&home); @@ -1299,10 +1299,7 @@ async fn auto_compact_persists_rollout_entries() { }; mount_sse_once_match(&server, third_matcher, sse3).await; - let model_provider = ModelProviderInfo { - base_url: Some(format!("{}/v1", server.uri())), - ..built_in_model_providers()["openai"].clone() - }; + let model_provider = non_openai_model_provider(&server); let home = TempDir::new().unwrap(); let mut config = load_default_config_for_test(&home); @@ -1403,10 +1400,7 @@ async fn manual_compact_retries_after_context_window_error() { ) .await; - let model_provider = ModelProviderInfo { - base_url: Some(format!("{}/v1", server.uri())), - ..built_in_model_providers()["openai"].clone() - }; + let model_provider = non_openai_model_provider(&server); let home = TempDir::new().unwrap(); let mut config = load_default_config_for_test(&home); @@ -1539,10 +1533,7 @@ async fn manual_compact_twice_preserves_latest_user_messages() { ) .await; - let model_provider = ModelProviderInfo { - base_url: Some(format!("{}/v1", server.uri())), - ..built_in_model_providers()["openai"].clone() - }; + let model_provider = non_openai_model_provider(&server); let home = TempDir::new().unwrap(); let mut config = load_default_config_for_test(&home); @@ -1743,10 +1734,7 @@ async fn auto_compact_allows_multiple_attempts_when_interleaved_with_other_turn_ mount_sse_sequence(&server, vec![sse1, sse2, sse3, sse4, sse5, sse6]).await; - let model_provider = ModelProviderInfo { - base_url: Some(format!("{}/v1", server.uri())), - ..built_in_model_providers()["openai"].clone() - }; + let model_provider = non_openai_model_provider(&server); let home = TempDir::new().unwrap(); let mut config = load_default_config_for_test(&home); @@ -1856,10 +1844,7 @@ async fn auto_compact_triggers_after_function_call_over_95_percent_usage() { // We don't assert on the post-compact request, so no need to keep its mock. mount_sse_once(&server, post_auto_compact_turn).await; - let model_provider = ModelProviderInfo { - base_url: Some(format!("{}/v1", server.uri())), - ..built_in_model_providers()["openai"].clone() - }; + let model_provider = non_openai_model_provider(&server); let home = TempDir::new().unwrap(); let mut config = load_default_config_for_test(&home); @@ -1961,13 +1946,18 @@ async fn auto_compact_counts_encrypted_reasoning_before_last_user() { ) .await; - let compacted_history = vec![codex_protocol::models::ResponseItem::Message { - id: None, - role: "assistant".to_string(), - content: vec![codex_protocol::models::ContentItem::OutputText { - text: "REMOTE_COMPACT_SUMMARY".to_string(), - }], - }]; + let compacted_history = vec![ + codex_protocol::models::ResponseItem::Message { + id: None, + role: "assistant".to_string(), + content: vec![codex_protocol::models::ContentItem::OutputText { + text: "REMOTE_COMPACT_SUMMARY".to_string(), + }], + }, + codex_protocol::models::ResponseItem::Compaction { + encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), + }, + ]; let compact_mock = mount_compact_json_once(&server, serde_json::json!({ "output": compacted_history })).await; @@ -2028,4 +2018,8 @@ async fn auto_compact_counts_encrypted_reasoning_before_last_user() { resume_body.contains("REMOTE_COMPACT_SUMMARY") || resume_body.contains(FINAL_REPLY), "resume request should follow remote compact and use compacted history" ); + assert!( + resume_body.contains("ENCRYPTED_COMPACTION_SUMMARY"), + "resume request should include compaction summary item" + ); } diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index 49fd5791c..34e44419b 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -51,13 +51,18 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> { ) .await; - let compacted_history = vec![ResponseItem::Message { - id: None, - role: "user".to_string(), - content: vec![ContentItem::InputText { - text: "REMOTE_COMPACTED_SUMMARY".to_string(), - }], - }]; + let compacted_history = vec![ + ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { + text: "REMOTE_COMPACTED_SUMMARY".to_string(), + }], + }, + ResponseItem::Compaction { + encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), + }, + ]; let compact_mock = responses::mount_compact_json_once( harness.server(), serde_json::json!({ "output": compacted_history.clone() }), @@ -120,6 +125,10 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> { follow_up_body.contains("REMOTE_COMPACTED_SUMMARY"), "expected follow-up request to use compacted history" ); + assert!( + follow_up_body.contains("ENCRYPTED_COMPACTION_SUMMARY"), + "expected follow-up request to include compaction summary item" + ); assert!( !follow_up_body.contains("FIRST_REMOTE_REPLY"), "expected follow-up request to drop pre-compaction assistant messages" @@ -159,13 +168,18 @@ async fn remote_compact_runs_automatically() -> Result<()> { ) .await; - let compacted_history = vec![ResponseItem::Message { - id: None, - role: "user".to_string(), - content: vec![ContentItem::InputText { - text: "REMOTE_COMPACTED_SUMMARY".to_string(), - }], - }]; + let compacted_history = vec![ + ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { + text: "REMOTE_COMPACTED_SUMMARY".to_string(), + }], + }, + ResponseItem::Compaction { + encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), + }, + ]; let compact_mock = responses::mount_compact_json_once( harness.server(), serde_json::json!({ "output": compacted_history.clone() }), @@ -190,6 +204,7 @@ async fn remote_compact_runs_automatically() -> Result<()> { assert_eq!(compact_mock.requests().len(), 1); let follow_up_body = responses_mock.single_request().body_json().to_string(); assert!(follow_up_body.contains("REMOTE_COMPACTED_SUMMARY")); + assert!(follow_up_body.contains("ENCRYPTED_COMPACTION_SUMMARY")); Ok(()) } @@ -226,6 +241,9 @@ async fn remote_compact_persists_replacement_history_in_rollout() -> Result<()> text: "COMPACTED_USER_SUMMARY".to_string(), }], }, + ResponseItem::Compaction { + encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), + }, ResponseItem::Message { id: None, role: "assistant".to_string(), diff --git a/codex-rs/core/tests/suite/compact_resume_fork.rs b/codex-rs/core/tests/suite/compact_resume_fork.rs index 5d3d9e4b8..188e38da1 100644 --- a/codex-rs/core/tests/suite/compact_resume_fork.rs +++ b/codex-rs/core/tests/suite/compact_resume_fork.rs @@ -857,6 +857,7 @@ async fn start_test_conversation( model: Option<&str>, ) -> (TempDir, Config, ConversationManager, Arc) { let model_provider = ModelProviderInfo { + name: "Non-OpenAI Model provider".into(), base_url: Some(format!("{}/v1", server.uri())), ..built_in_model_providers()["openai"].clone() }; diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 5c609c3c4..722e91561 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -150,8 +150,8 @@ pub enum ResponseItem { GhostSnapshot { ghost_commit: GhostCommit, }, - #[serde(alias = "compaction")] - CompactionSummary { + #[serde(alias = "compaction_summary")] + Compaction { encrypted_content: String, }, #[serde(other)] @@ -671,13 +671,13 @@ mod tests { #[test] fn deserializes_compaction_alias() -> Result<()> { - let json = r#"{"type":"compaction","encrypted_content":"abc"}"#; + let json = r#"{"type":"compaction_summary","encrypted_content":"abc"}"#; let item: ResponseItem = serde_json::from_str(json)?; assert_eq!( item, - ResponseItem::CompactionSummary { + ResponseItem::Compaction { encrypted_content: "abc".into(), } );