Skip to content

Commit 6d5bf3a

Browse files
fix(translation): strip Codex compaction markers before Responses decode
Signed-off-by: Zengyuan Liu <zengyuanl@nvidia.com>
1 parent 23e00cd commit 6d5bf3a

2 files changed

Lines changed: 59 additions & 0 deletions

File tree

crates/switchyard-translation/src/codecs/responses/buffered.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ impl FormatCodec for OpenAiResponsesCodec {
4040

4141
fn decode_request(&self, body: &Value, policy: &TranslationPolicy) -> Result<DecodedRequest> {
4242
let body = crate::util::object(body, "$")?;
43+
// Codex marks remote-compact requests with a `compaction_trigger` input
44+
// item. It is codex-internal protocol that strict upstream parsers
45+
// reject, so drop it before preservation capture and normalization.
46+
let sanitized = strip_codex_compaction_markers(body);
47+
let body = sanitized.as_ref().unwrap_or(body);
4348
let mut diagnostics = Vec::new();
4449
let mut request = LlmRequest {
4550
model: body
@@ -1108,6 +1113,23 @@ fn pair_tool_calls_with_outputs(items: &mut Vec<Value>) {
11081113
}
11091114
}
11101115

1116+
// Returns the body without Codex `compaction_trigger` input items, or `None`
1117+
// when there are none (the common case, sparing the clone).
1118+
fn strip_codex_compaction_markers(body: &Map<String, Value>) -> Option<Map<String, Value>> {
1119+
fn is_marker(item: &Value) -> bool {
1120+
item.get("type").and_then(Value::as_str) == Some("compaction_trigger")
1121+
}
1122+
let input = body.get("input")?.as_array()?;
1123+
if !input.iter().any(is_marker) {
1124+
return None;
1125+
}
1126+
let mut sanitized = body.clone();
1127+
if let Some(Value::Array(items)) = sanitized.get_mut("input") {
1128+
items.retain(|item| !is_marker(item));
1129+
}
1130+
Some(sanitized)
1131+
}
1132+
11111133
// Encodes IR blocks that Responses represents as top-level input items.
11121134
fn encode_responses_special_input(
11131135
block: &ContentBlock,

crates/switchyard-translation/tests/request_translation.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2533,3 +2533,40 @@ fn responses_parallel_tool_calls_pair_with_their_outputs() -> TestResult {
25332533
);
25342534
Ok(())
25352535
}
2536+
2537+
// Verifies Codex `compaction_trigger` marker items never reach the upstream.
2538+
#[test]
2539+
fn responses_codex_compaction_markers_are_stripped() -> TestResult {
2540+
let engine = TranslationEngine::default();
2541+
let policy = TranslationPolicy {
2542+
preservation: switchyard_translation::PreservationPolicy::Disabled,
2543+
..TranslationPolicy::default()
2544+
};
2545+
let body = json!({
2546+
"model": "gpt-5",
2547+
"input": [
2548+
{"type": "message", "role": "user", "content": "Continue"},
2549+
{"type": "compaction_trigger"},
2550+
{"type": "function_call", "name": "shell", "call_id": "call-a", "arguments": "{}"},
2551+
{"type": "function_call_output", "call_id": "call-a", "output": "ok"}
2552+
]
2553+
});
2554+
2555+
let output = engine
2556+
.translate_request(
2557+
WireFormat::OpenAiResponses,
2558+
WireFormat::OpenAiResponses,
2559+
&body,
2560+
&policy,
2561+
)?
2562+
.body;
2563+
2564+
let types = output["input"]
2565+
.as_array()
2566+
.ok_or("input is not an array")?
2567+
.iter()
2568+
.filter_map(|item| item["type"].as_str())
2569+
.collect::<Vec<_>>();
2570+
assert_eq!(types, vec!["message", "function_call", "function_call_output"]);
2571+
Ok(())
2572+
}

0 commit comments

Comments
 (0)