fix: add codex reasoning defaults and stream rewrite tests

This commit is contained in:
zhefox
2026-05-13 14:34:16 +08:00
parent 3c2497f019
commit 1726df1169
3 changed files with 156 additions and 0 deletions

View File

@@ -723,6 +723,32 @@ fn openai_responses_to_openai_chat_stream_rewriter_converts_text_deltas_immediat
assert!(rewriter.finish().expect("finish should succeed").is_empty());
}
#[test]
fn openai_responses_to_openai_chat_stream_rewriter_converts_reasoning_deltas_immediately() {
let report_context = json!({
"provider_api_format": "openai:responses",
"client_api_format": "openai:chat",
"needs_conversion": true,
"mapped_model": "gpt-5.4",
});
let mut rewriter =
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
let output = rewriter
.push_chunk(
concat!(
"event: response.reasoning_summary_text.delta\n",
"data: {\"type\":\"response.reasoning_summary_text.delta\",\"response_id\":\"resp_reasoning_stream_123\",\"item_id\":\"rs_123\",\"output_index\":0,\"summary_index\":0,\"delta\":\"Need to inspect first.\"}\n\n"
)
.as_bytes(),
)
.expect("rewrite should succeed");
let output_text = String::from_utf8(output).expect("utf8 should decode");
assert!(output_text.contains("\"object\":\"chat.completion.chunk\""));
assert!(output_text.contains("\"reasoning_content\":\"Need to inspect first.\""));
assert!(!output_text.contains("\"content\""));
assert!(!output_text.contains("data: [DONE]"));
}
#[test]
fn openai_responses_to_openai_chat_stream_rewriter_converts_completed_event_without_buffering() {
let report_context = json!({

View File

@@ -139,6 +139,50 @@ fn inject_codex_default_variation_prompt(body_object: &mut serde_json::Map<Strin
);
}
fn ensure_codex_reasoning_summary(body_object: &mut serde_json::Map<String, Value>) {
let reasoning = body_object
.entry("reasoning".to_string())
.or_insert_with(|| json!({}));
if !reasoning.is_object() {
*reasoning = json!({});
}
let Some(reasoning_object) = reasoning.as_object_mut() else {
return;
};
reasoning_object
.entry("effort".to_string())
.or_insert_with(|| json!("medium"));
reasoning_object
.entry("summary".to_string())
.or_insert_with(|| json!("auto"));
}
fn ensure_codex_reasoning_include(body_object: &mut serde_json::Map<String, Value>) {
const REASONING_ENCRYPTED_CONTENT: &str = "reasoning.encrypted_content";
match body_object.get_mut("include") {
Some(Value::Array(include)) => {
let has_reasoning_encrypted_content = include
.iter()
.any(|value| value.as_str() == Some(REASONING_ENCRYPTED_CONTENT));
if !has_reasoning_encrypted_content {
include.push(json!(REASONING_ENCRYPTED_CONTENT));
}
}
Some(_) | None => {
body_object.insert("include".to_string(), json!([REASONING_ENCRYPTED_CONTENT]));
}
}
}
fn ensure_codex_responses_defaults(body_object: &mut serde_json::Map<String, Value>) {
ensure_codex_reasoning_summary(body_object);
ensure_codex_reasoning_include(body_object);
body_object
.entry("parallel_tool_calls".to_string())
.or_insert_with(|| json!(true));
}
fn build_stable_codex_prompt_cache_key(user_api_key_id: &str) -> Option<String> {
let normalized = user_api_key_id.trim();
if normalized.is_empty() {
@@ -318,6 +362,7 @@ pub fn apply_codex_openai_responses_special_body_edits(
} else if !body_rules_handle_path(body_rules, "store") {
body_object.insert("store".to_string(), json!(false));
}
ensure_codex_responses_defaults(body_object);
if !body_rules_handle_path(body_rules, "instructions")
&& !body_object.contains_key("instructions")
{
@@ -423,6 +468,67 @@ mod tests {
};
use serde_json::json;
#[test]
fn codex_responses_body_edits_request_reasoning_summary_stream() {
let mut provider_request_body = json!({
"input": [{
"role": "user",
"content": "hello"
}],
"model": "gpt-5.4",
"stream": true
});
apply_codex_openai_responses_special_body_edits(
&mut provider_request_body,
"codex",
"openai:responses",
None,
None,
);
assert_eq!(
provider_request_body["reasoning"]["effort"],
json!("medium")
);
assert_eq!(provider_request_body["reasoning"]["summary"], json!("auto"));
assert_eq!(
provider_request_body["include"],
json!(["reasoning.encrypted_content"])
);
assert_eq!(provider_request_body["parallel_tool_calls"], json!(true));
}
#[test]
fn codex_responses_body_edits_preserve_existing_reasoning_and_include() {
let mut provider_request_body = json!({
"input": [],
"model": "gpt-5.4",
"include": ["file_search_call.results"],
"reasoning": {"effort": "high", "summary": "detailed"},
"parallel_tool_calls": false
});
apply_codex_openai_responses_special_body_edits(
&mut provider_request_body,
"codex",
"openai:responses",
None,
None,
);
assert_eq!(provider_request_body["reasoning"]["effort"], json!("high"));
assert_eq!(
provider_request_body["reasoning"]["summary"],
json!("detailed")
);
assert_eq!(
provider_request_body["include"],
json!(["file_search_call.results", "reasoning.encrypted_content"])
);
assert_eq!(provider_request_body["parallel_tool_calls"], json!(false));
}
#[test]
fn codex_image_body_edits_force_tool_choice_and_default_generate_tool_fields() {
let mut provider_request_body = json!({

View File

@@ -502,6 +502,30 @@ data: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_123\",\"object\
assert!(!output.contains("\"model\":\"gpt-5.5\""));
}
#[test]
fn standard_rewriter_converts_openai_responses_reasoning_delta_to_chat() {
let report_context = json!({
"provider_api_format": "openai:responses",
"client_api_format": "openai:chat",
"needs_conversion": true,
"mapped_model": "gpt-5.4",
});
let mut rewriter = maybe_build_ai_surface_stream_rewriter(Some(&report_context))
.expect("rewriter should exist");
let output = rewriter
.push_chunk(
b"event: response.reasoning_summary_text.delta\n\
data: {\"type\":\"response.reasoning_summary_text.delta\",\"response_id\":\"resp_reasoning_stream_123\",\"item_id\":\"rs_123\",\"output_index\":0,\"summary_index\":0,\"delta\":\"Need to inspect first.\"}\n\n",
)
.expect("rewrite should succeed");
let output = String::from_utf8(output).expect("output should be utf8");
assert!(output.contains("\"object\":\"chat.completion.chunk\""));
assert!(output.contains("\"reasoning_content\":\"Need to inspect first.\""));
assert!(!output.contains("\"content\""));
assert!(!output.contains("data: [DONE]"));
}
#[test]
fn resolves_openai_image_mode_for_same_format_image_streams() {
let report_context = json!({