mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-12 06:00:20 +08:00
fix responses replay across Gemini and Codex
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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!({
|
||||
|
||||
Reference in New Issue
Block a user