From dc2f26f7b526901c2f07405dc7e8e01a80ec062a Mon Sep 17 00:00:00 2001 From: Robby He <448523760@qq.com> Date: Tue, 4 Nov 2025 12:55:41 +0800 Subject: [PATCH] Fix is_api_message to correctly exclude reasoning messages (#6156) ## Problem The `is_api_message` function in `conversation_history.rs` had a misalignment between its documentation and implementation: - **Comment stated**: "Anything that is not a system message or 'reasoning' message is considered an API message" - **Code behavior**: Was returning `true` for `ResponseItem::Reasoning`, meaning reasoning messages were incorrectly treated as API messages This inconsistency could lead to reasoning messages being persisted in conversation history when they should be filtered out. ## Root Cause Investigation revealed that reasoning messages are explicitly excluded throughout the codebase: 1. **Chat completions API** (lines 267-272 in `chat_completions.rs`) omits reasoning from conversation history: ```rust ResponseItem::Reasoning { .. } | ResponseItem::Other => { // Omit these items from the conversation history. continue; } ``` 2. **Existing tests** like `drops_reasoning_when_last_role_is_user` and `ignores_reasoning_before_last_user` validate that reasoning should be excluded from API payloads ## Solution Fixed the `is_api_message` function to align with its documentation and the rest of the codebase: ```rust // Before: Reasoning was incorrectly returning true ResponseItem::Reasoning { .. } | ResponseItem::WebSearchCall { .. } => true, // After: Reasoning correctly returns false ResponseItem::WebSearchCall { .. } => true, ResponseItem::Reasoning { .. } | ResponseItem::Other => false, ``` ## Testing - Enhanced existing test to verify reasoning messages are properly filtered out - All 264 core tests pass, including 8 chat completions tests that validate reasoning behavior - No regressions introduced This ensures reasoning messages are consistently excluded from API message processing across the entire codebase. --- codex-rs/core/src/conversation_history.rs | 34 ++++++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/codex-rs/core/src/conversation_history.rs b/codex-rs/core/src/conversation_history.rs index 9a5570a24..ef493f145 100644 --- a/codex-rs/core/src/conversation_history.rs +++ b/codex-rs/core/src/conversation_history.rs @@ -516,8 +516,8 @@ fn truncate_formatted_exec_output(content: &str, total_lines: usize) -> String { result } -/// Anything that is not a system message or "reasoning" message is considered -/// an API message. +/// API messages include every non-system item (user/assistant messages, reasoning, +/// tool calls, tool outputs, shell calls, and web-search calls). fn is_api_message(message: &ResponseItem) -> bool { match message { ResponseItem::Message { role, .. } => role.as_str() != "system", @@ -542,6 +542,8 @@ mod tests { use codex_protocol::models::LocalShellAction; use codex_protocol::models::LocalShellExecAction; use codex_protocol::models::LocalShellStatus; + use codex_protocol::models::ReasoningItemContent; + use codex_protocol::models::ReasoningItemReasoningSummary; use pretty_assertions::assert_eq; fn assistant_msg(text: &str) -> ResponseItem { @@ -570,10 +572,23 @@ mod tests { } } + fn reasoning_msg(text: &str) -> ResponseItem { + ResponseItem::Reasoning { + id: String::new(), + summary: vec![ReasoningItemReasoningSummary::SummaryText { + text: "summary".to_string(), + }], + content: Some(vec![ReasoningItemContent::ReasoningText { + text: text.to_string(), + }]), + encrypted_content: None, + } + } + #[test] fn filters_non_api_messages() { let mut h = ConversationHistory::default(); - // System message is not an API message; Other is ignored. + // System message is not API messages; Other is ignored. let system = ResponseItem::Message { id: None, role: "system".to_string(), @@ -581,7 +596,8 @@ mod tests { text: "ignored".to_string(), }], }; - h.record_items([&system, &ResponseItem::Other]); + let reasoning = reasoning_msg("thinking..."); + h.record_items([&system, &reasoning, &ResponseItem::Other]); // User and assistant should be retained. let u = user_msg("hi"); @@ -592,6 +608,16 @@ mod tests { assert_eq!( items, vec![ + ResponseItem::Reasoning { + id: String::new(), + summary: vec![ReasoningItemReasoningSummary::SummaryText { + text: "summary".to_string(), + }], + content: Some(vec![ReasoningItemContent::ReasoningText { + text: "thinking...".to_string(), + }]), + encrypted_content: None, + }, ResponseItem::Message { id: None, role: "user".to_string(),