diff --git a/crates/aether-ai/formats/src/formats/openai/request_contract.rs b/crates/aether-ai/formats/src/formats/openai/request_contract.rs index 7eb7107bd..b145d7c69 100644 --- a/crates/aether-ai/formats/src/formats/openai/request_contract.rs +++ b/crates/aether-ai/formats/src/formats/openai/request_contract.rs @@ -381,10 +381,21 @@ mod tests { #[test] fn finalization_strips_non_replayable_responses_reasoning_history() { + let gemini_carrier = + crate::formats::openai::responses::encode_gemini_tool_signature_carrier( + "opaque-gemini-thought-signature", + ) + .expect("Gemini signature carrier"); let mut body = json!({ "model": "gpt-5.4", "input": [ {"type": "reasoning", "id": "rs_provider_123", "summary": []}, + { + "type": "reasoning", + "id": "rs_aether_55070860f6d45c6b8f6fa11efd9dff8a", + "summary": [], + "encrypted_content": gemini_carrier + }, { "type": "reasoning", "id": "item_72d3bd8d367d01977ace23f1", diff --git a/crates/aether-ai/formats/src/formats/openai/responses/mod.rs b/crates/aether-ai/formats/src/formats/openai/responses/mod.rs index 9a9328ff8..36053f67e 100644 --- a/crates/aether-ai/formats/src/formats/openai/responses/mod.rs +++ b/crates/aether-ai/formats/src/formats/openai/responses/mod.rs @@ -167,9 +167,10 @@ pub fn normalize_openai_responses_message_item_ids(body: &mut Value) -> usize { /// Removes reasoning history items that cannot be replayed against an OpenAI Responses backend. /// /// Reasoning IDs are opaque provider references and must never be repaired by changing their -/// prefix. Foreign IDs (for example `item_...`) are therefore removed. Aether-synthesized -/// reasoning summaries are also removed unless they carry encrypted reasoning state that can be -/// replayed statelessly. +/// prefix. Foreign IDs (for example `item_...`) are therefore removed. Aether's Gemini signature +/// carriers are also removed: they are intentionally transported through the Responses +/// `encrypted_content` field so they can be restored on a later Gemini tool turn, but they are not +/// OpenAI ciphertext and must never be replayed to an OpenAI/Codex backend. pub fn strip_incompatible_openai_responses_reasoning_items( body: &mut Value, provider_api_format: &str, @@ -221,6 +222,13 @@ fn openai_responses_reasoning_item_is_replayable( if object.get("type").and_then(Value::as_str) != Some("reasoning") { return true; } + if object + .get("encrypted_content") + .and_then(Value::as_str) + .is_some_and(|value| value.starts_with(GEMINI_TOOL_SIGNATURE_CARRIER_PREFIX)) + { + return false; + } if policy == OpenAiResponsesReasoningReplayPolicy::DeepSeekOpaque && deepseek_opaque_reasoning_item_is_replayable(object) { @@ -475,6 +483,43 @@ mod tests { assert_eq!(input[2]["id"], "item_message_123"); } + #[test] + fn strips_gemini_signature_carriers_before_openai_replay() { + let gemini_item_id = openai_responses_synthetic_reasoning_item_id("resp_gemini", 0); + let openai_item_id = openai_responses_synthetic_reasoning_item_id("resp_openai", 0); + let carrier = encode_gemini_tool_signature_carrier_with_direction( + "opaque-gemini-thought-signature", + GeminiToolSignatureCarrierDirection::Next, + ) + .expect("Gemini signature carrier"); + let mut body = json!({ + "input": [ + { + "type": "reasoning", + "id": gemini_item_id, + "summary": [], + "encrypted_content": carrier + }, + { + "type": "reasoning", + "id": openai_item_id, + "summary": [], + "encrypted_content": "provider-encrypted-state" + }, + {"type": "reasoning", "id": "rs_provider_123", "summary": []} + ] + }); + + assert_eq!( + strip_incompatible_openai_responses_reasoning_items(&mut body, "openai:responses"), + 1 + ); + let input = body["input"].as_array().expect("input array"); + assert_eq!(input.len(), 2); + assert_eq!(input[0]["encrypted_content"], "provider-encrypted-state"); + assert_eq!(input[1]["id"], "rs_provider_123"); + } + #[test] fn reasoning_item_sanitizer_is_scoped_to_responses_targets() { let mut body = json!({