mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 16:37:46 +08:00
fix(codex): avoid replaying static config on websocket continuations
This commit is contained in:
@@ -388,6 +388,54 @@ mod tests {
|
|||||||
assert_eq!(normalized["previous_response_id"], "resp_123");
|
assert_eq!(normalized["previous_response_id"], "resp_123");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lite_continuations_forward_only_each_turns_incremental_input() {
|
||||||
|
let normalization = ResponsesWebSocketBodyNormalization::for_tests("gpt-5.6-sol")
|
||||||
|
.with_provider_type_for_tests("codex");
|
||||||
|
|
||||||
|
for turn in 1..=4 {
|
||||||
|
let event = json!({
|
||||||
|
"type": "response.create",
|
||||||
|
"model": "gpt-5.6-sol",
|
||||||
|
"previous_response_id": format!("resp_{turn}"),
|
||||||
|
"instructions": "The same large developer instructions.",
|
||||||
|
"tools": [{
|
||||||
|
"type": "function",
|
||||||
|
"name": "shell",
|
||||||
|
"parameters": {"type": "object"}
|
||||||
|
}],
|
||||||
|
"input": [
|
||||||
|
{
|
||||||
|
"type": "reasoning",
|
||||||
|
"id": format!("rs_{turn}"),
|
||||||
|
"content": [{
|
||||||
|
"type": "reasoning_text",
|
||||||
|
"text": format!("reasoning state {turn}")
|
||||||
|
}],
|
||||||
|
"encrypted_content": format!("opaque-{turn}")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "function_call_output",
|
||||||
|
"call_id": format!("call_{turn}"),
|
||||||
|
"output": format!("result {turn}")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
let normalized = normalized_continuation(&event, &normalization);
|
||||||
|
let input = normalized["input"].as_array().expect("incremental input");
|
||||||
|
assert_eq!(input.len(), 2);
|
||||||
|
assert_eq!(input[0]["type"], "reasoning");
|
||||||
|
assert_eq!(input[0]["content"][0]["type"], "reasoning_text");
|
||||||
|
assert_eq!(input[1]["type"], "function_call_output");
|
||||||
|
assert!(normalized.get("instructions").is_none());
|
||||||
|
assert!(normalized.get("tools").is_none());
|
||||||
|
assert!(!input.iter().any(|item| item["type"] == "additional_tools"));
|
||||||
|
assert!(!input.iter().any(|item| item["role"] == "developer"));
|
||||||
|
assert_eq!(normalized["previous_response_id"], format!("resp_{turn}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn continuation_keeps_a_warmup_generate_flag() {
|
fn continuation_keeps_a_warmup_generate_flag() {
|
||||||
let event = json!({
|
let event = json!({
|
||||||
|
|||||||
@@ -1345,6 +1345,7 @@ fn codex_responses_lite_supports_request_body(provider_request_body: Option<&Val
|
|||||||
fn apply_codex_responses_lite_body_contract(
|
fn apply_codex_responses_lite_body_contract(
|
||||||
body_object: &mut serde_json::Map<String, Value>,
|
body_object: &mut serde_json::Map<String, Value>,
|
||||||
capabilities: &CodexResponsesModelCapabilities,
|
capabilities: &CodexResponsesModelCapabilities,
|
||||||
|
websocket_continuation: bool,
|
||||||
) {
|
) {
|
||||||
if !capabilities.use_responses_lite {
|
if !capabilities.use_responses_lite {
|
||||||
return;
|
return;
|
||||||
@@ -1381,6 +1382,23 @@ fn apply_codex_responses_lite_body_contract(
|
|||||||
.and_then(Value::as_array_mut)
|
.and_then(Value::as_array_mut)
|
||||||
.expect("Responses Lite input was validated as an array");
|
.expect("Responses Lite input was validated as an array");
|
||||||
|
|
||||||
|
// `previous_response_id` already references the prior response's stored
|
||||||
|
// input. The Codex client sends only the new input items on these turns;
|
||||||
|
// replaying the current top-level tools/instructions as synthetic history
|
||||||
|
// would permanently append another copy on every tool round-trip. Keep
|
||||||
|
// the Lite wire requirements below, but do not synthesize static config
|
||||||
|
// for a continuation.
|
||||||
|
if websocket_continuation {
|
||||||
|
for item in input
|
||||||
|
.iter_mut()
|
||||||
|
.filter(|item| !is_codex_responses_lite_additional_tools_item(item))
|
||||||
|
{
|
||||||
|
strip_codex_responses_lite_image_details(item);
|
||||||
|
}
|
||||||
|
body_object.insert("parallel_tool_calls".to_string(), json!(false));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let existing_additional_tools = input
|
let existing_additional_tools = input
|
||||||
.iter()
|
.iter()
|
||||||
.position(is_codex_responses_lite_additional_tools_item)
|
.position(is_codex_responses_lite_additional_tools_item)
|
||||||
@@ -1710,8 +1728,21 @@ pub fn apply_codex_openai_responses_special_body_edits_with_source_model_and_cap
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// HTTP Responses bodies do not carry an event `type`. Preserve this
|
||||||
|
// marker only for an actual WebSocket response.create continuation so it
|
||||||
|
// survives both normalization/finalization passes. The framing layer owns
|
||||||
|
// the field and will forward it verbatim to the already-bound upstream.
|
||||||
|
let websocket_continuation = body_object.get("type").and_then(Value::as_str)
|
||||||
|
== Some("response.create")
|
||||||
|
&& body_object
|
||||||
|
.get("previous_response_id")
|
||||||
|
.is_some_and(|value| !value.is_null());
|
||||||
|
|
||||||
wrap_codex_responses_string_input_for_backend(body_object, provider_api_format);
|
wrap_codex_responses_string_input_for_backend(body_object, provider_api_format);
|
||||||
for field in CODEX_OPENAI_RESPONSES_UNSUPPORTED_BODY_FIELDS {
|
for field in CODEX_OPENAI_RESPONSES_UNSUPPORTED_BODY_FIELDS {
|
||||||
|
if *field == "previous_response_id" && websocket_continuation {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if !body_rules_handle_path(body_rules, field) {
|
if !body_rules_handle_path(body_rules, field) {
|
||||||
body_object.remove(*field);
|
body_object.remove(*field);
|
||||||
}
|
}
|
||||||
@@ -1794,7 +1825,7 @@ pub fn apply_codex_openai_responses_special_body_edits_with_source_model_and_cap
|
|||||||
capabilities,
|
capabilities,
|
||||||
body_rules,
|
body_rules,
|
||||||
);
|
);
|
||||||
apply_codex_responses_lite_body_contract(body_object, capabilities);
|
apply_codex_responses_lite_body_contract(body_object, capabilities, websocket_continuation);
|
||||||
strip_codex_cache_control_fields(provider_request_body);
|
strip_codex_cache_control_fields(provider_request_body);
|
||||||
apply_codex_openai_responses_compact_body_edits(
|
apply_codex_openai_responses_compact_body_edits(
|
||||||
provider_request_body,
|
provider_request_body,
|
||||||
@@ -3195,6 +3226,93 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn codex_responses_lite_websocket_continuations_do_not_reappend_static_config() {
|
||||||
|
let incremental_input = json!([
|
||||||
|
{
|
||||||
|
"type": "reasoning",
|
||||||
|
"id": "rs_provider_123",
|
||||||
|
"content": [{
|
||||||
|
"type": "reasoning_text",
|
||||||
|
"text": "Pass the provider reasoning state through unchanged."
|
||||||
|
}],
|
||||||
|
"encrypted_content": "opaque-provider-state"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "function_call_output",
|
||||||
|
"call_id": "call_123",
|
||||||
|
"output": "small result"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
let mut provider_request_body = json!({
|
||||||
|
"type": "response.create",
|
||||||
|
"model": "gpt-5.6-sol",
|
||||||
|
"previous_response_id": "resp_123",
|
||||||
|
"instructions": "Static developer instructions.",
|
||||||
|
"tools": [{
|
||||||
|
"type": "function",
|
||||||
|
"name": "lookup",
|
||||||
|
"parameters": {"type": "object"}
|
||||||
|
}],
|
||||||
|
"input": incremental_input.clone()
|
||||||
|
});
|
||||||
|
|
||||||
|
apply_codex_openai_responses_special_body_edits(
|
||||||
|
&mut provider_request_body,
|
||||||
|
"codex",
|
||||||
|
"openai:responses",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(provider_request_body["previous_response_id"], "resp_123");
|
||||||
|
assert!(provider_request_body.get("instructions").is_none());
|
||||||
|
assert!(provider_request_body.get("tools").is_none());
|
||||||
|
assert_eq!(provider_request_body["input"], incremental_input);
|
||||||
|
assert_eq!(provider_request_body["parallel_tool_calls"], false);
|
||||||
|
|
||||||
|
let once = provider_request_body.clone();
|
||||||
|
apply_codex_openai_responses_special_body_edits(
|
||||||
|
&mut provider_request_body,
|
||||||
|
"codex",
|
||||||
|
"openai:responses",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
assert_eq!(provider_request_body, once);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn codex_responses_lite_null_previous_id_is_an_independent_turn() {
|
||||||
|
let mut provider_request_body = json!({
|
||||||
|
"type": "response.create",
|
||||||
|
"model": "gpt-5.6-sol",
|
||||||
|
"previous_response_id": null,
|
||||||
|
"instructions": "Fresh instructions.",
|
||||||
|
"tools": [{"type": "function", "name": "lookup", "parameters": {}}],
|
||||||
|
"input": [{"type": "message", "role": "user", "content": []}]
|
||||||
|
});
|
||||||
|
|
||||||
|
apply_codex_openai_responses_special_body_edits(
|
||||||
|
&mut provider_request_body,
|
||||||
|
"codex",
|
||||||
|
"openai:responses",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(provider_request_body.get("previous_response_id").is_none());
|
||||||
|
assert_eq!(
|
||||||
|
provider_request_body["input"][0]["type"],
|
||||||
|
"additional_tools"
|
||||||
|
);
|
||||||
|
assert_eq!(provider_request_body["input"][1]["role"], "developer");
|
||||||
|
assert_eq!(
|
||||||
|
provider_request_body["input"][1]["content"][0]["text"],
|
||||||
|
"Fresh instructions."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn codex_responses_body_edits_strip_name_from_hosted_web_search_tool() {
|
fn codex_responses_body_edits_strip_name_from_hosted_web_search_tool() {
|
||||||
let mut provider_request_body = json!({
|
let mut provider_request_body = json!({
|
||||||
|
|||||||
Reference in New Issue
Block a user