Compare commits

...

5 Commits

Author SHA1 Message Date
kevin zhao
f5ae722656 remove unecessary comment 2025-10-08 10:34:49 -07:00
kevin zhao
8b7680ce3a turn type enums 2025-10-08 10:34:24 -07:00
kevin zhao
eab5e45e5a update test case 2025-10-08 10:13:49 -07:00
kevin zhao
8f1fe7f320 remove weird test 2025-10-07 18:19:33 -07:00
kevin zhao
c1a2be732e add action type to responses header 2025-10-07 17:20:27 -07:00
5 changed files with 103 additions and 1 deletions

View File

@@ -31,6 +31,7 @@ use crate::client_common::Prompt;
use crate::client_common::ResponseEvent;
use crate::client_common::ResponseStream;
use crate::client_common::ResponsesApiRequest;
use crate::client_common::TurnType;
use crate::client_common::create_reasoning_param_for_request;
use crate::client_common::create_text_param_for_request;
use crate::config::Config;
@@ -244,7 +245,7 @@ impl ModelClient {
let max_attempts = self.provider.request_max_retries();
for attempt in 0..=max_attempts {
match self
.attempt_stream_responses(attempt, &payload_json, &auth_manager)
.attempt_stream_responses(attempt, &payload_json, &auth_manager, prompt.turn_type)
.await
{
Ok(stream) => {
@@ -272,6 +273,7 @@ impl ModelClient {
attempt: u64,
payload_json: &Value,
auth_manager: &Option<Arc<AuthManager>>,
turn_type: TurnType,
) -> std::result::Result<ResponseStream, StreamAttemptError> {
// Always fetch the latest auth in case a prior attempt refreshed the token.
let auth = auth_manager.as_ref().and_then(|m| m.auth());
@@ -293,6 +295,13 @@ impl ModelClient {
// Send session_id for compatibility.
.header("conversation_id", self.conversation_id.to_string())
.header("session_id", self.conversation_id.to_string())
.header(
"action_kind",
match turn_type {
TurnType::Review => "review",
TurnType::Regular => "turn",
},
)
.header(reqwest::header::ACCEPT, "text/event-stream")
.json(payload_json);

View File

@@ -23,6 +23,13 @@ use tokio::sync::mpsc;
/// Review thread system prompt. Edit `core/src/review_prompt.md` to customize.
pub const REVIEW_PROMPT: &str = include_str!("../review_prompt.md");
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TurnType {
#[default]
Regular,
Review,
}
/// API request payload for a single model turn
#[derive(Default, Debug, Clone)]
pub struct Prompt {
@@ -41,6 +48,9 @@ pub struct Prompt {
/// Optional the output schema for the model's response.
pub output_schema: Option<Value>,
/// The type of turn being executed
pub turn_type: TurnType,
}
impl Prompt {

View File

@@ -42,6 +42,7 @@ use crate::apply_patch::convert_apply_patch_to_protocol;
use crate::client::ModelClient;
use crate::client_common::Prompt;
use crate::client_common::ResponseEvent;
use crate::client_common::TurnType;
use crate::config::Config;
use crate::config_types::ShellEnvironmentPolicy;
use crate::conversation_history::ConversationHistory;
@@ -1933,6 +1934,13 @@ async fn run_turn(
sub_id: String,
input: Vec<ResponseItem>,
) -> CodexResult<TurnRunResult> {
fn resolve_turn_type(turn_context: &TurnContext) -> TurnType {
if turn_context.is_review_mode {
TurnType::Review
} else {
TurnType::Regular
}
}
let mcp_tools = sess.services.mcp_connection_manager.list_all_tools();
let router = Arc::new(ToolRouter::from_config(
&turn_context.tools_config,
@@ -1950,6 +1958,7 @@ async fn run_turn(
parallel_tool_calls,
base_instructions_override: turn_context.base_instructions.clone(),
output_schema: turn_context.final_output_json_schema.clone(),
turn_type: resolve_turn_type(&turn_context),
};
let mut retries = 0;

View File

@@ -349,12 +349,14 @@ async fn includes_conversation_id_and_model_headers_in_request() {
let request_conversation_id = request.headers.get("conversation_id").unwrap();
let request_authorization = request.headers.get("authorization").unwrap();
let request_originator = request.headers.get("originator").unwrap();
let turn_kind = request.headers.get("action_kind").unwrap();
assert_eq!(
request_conversation_id.to_str().unwrap(),
conversation_id.to_string()
);
assert_eq!(request_originator.to_str().unwrap(), "codex_cli_rs");
assert_eq!(turn_kind.to_str().unwrap(), "turn");
assert_eq!(
request_authorization.to_str().unwrap(),
"Bearer Test API Key"

View File

@@ -338,6 +338,78 @@ async fn review_uses_custom_review_model_from_config() {
server.verify().await;
}
/// Ensure the client marks review turns with `action_kind: review` on the
/// outbound Responses API request.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn review_sends_action_kind_header() {
skip_if_no_network!();
// Minimal stream: just a completed event
let sse_raw = r#"[
{"type":"response.completed", "response": {"id": "__ID__"}}
]"#;
// Expect 2 requests total: first the review thread, then a follow-up turn.
let server = start_responses_server_with_sse(sse_raw, 2).await;
let codex_home = TempDir::new().unwrap();
let codex = new_conversation_for_server(&server, &codex_home, |_| {}).await;
codex
.submit(Op::Review {
review_request: ReviewRequest {
prompt: "check action_kind header".to_string(),
user_facing_hint: "check action_kind header".to_string(),
},
})
.await
.unwrap();
// Wait for completion to ensure the request was sent.
let _entered = wait_for_event(&codex, |ev| matches!(ev, EventMsg::EnteredReviewMode(_))).await;
let _closed = wait_for_event(&codex, |ev| matches!(ev, EventMsg::ExitedReviewMode(_))).await;
let _complete = wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await;
// Assert the outbound request included the correct header for the review request.
let mut requests = server.received_requests().await.unwrap();
assert!(
!requests.is_empty(),
"expected at least one request (review)"
);
let review_req = &requests[0];
let review_kind = review_req.headers.get("action_kind");
assert!(
review_kind.is_some(),
"expected action_kind header on review"
);
assert_eq!(review_kind.unwrap().to_str().unwrap(), "review");
// Now send a normal follow-up turn and ensure its header is "turn".
codex
.submit(Op::UserInput {
items: vec![InputItem::Text {
text: "follow-up after review".to_string(),
}],
})
.await
.unwrap();
let _complete2 = wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await;
requests = server.received_requests().await.unwrap();
assert_eq!(
requests.len(),
2,
"expected 2 requests (review + follow-up)"
);
let follow_req = &requests[1];
let follow_kind = follow_req.headers.get("action_kind");
assert!(
follow_kind.is_some(),
"expected action_kind header on follow-up"
);
assert_eq!(follow_kind.unwrap().to_str().unwrap(), "turn");
server.verify().await;
}
/// When a review session begins, it must not prepend prior chat history from
/// the parent session. The request `input` should contain only the review
/// prompt from the user.