From 1d056cf947a41e4debbe5406451ad2e01fb43c47 Mon Sep 17 00:00:00 2001 From: Ryan Lempka Date: Fri, 30 Oct 2026 14:14:56 -0500 Subject: [PATCH 1/2] fix(translation): normalize Responses built-in tool roles Signed-off-by: Ryan Lempka --- .../src/codecs/openai_chat/buffered.rs | 5 +++++ .../src/codecs/responses/buffered.rs | 15 ++++++++++++--- .../tests/request_translation.rs | 9 +++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/crates/switchyard-translation/src/codecs/openai_chat/buffered.rs b/crates/switchyard-translation/src/codecs/openai_chat/buffered.rs index 7597e6c3e..7ce8519c5 100644 --- a/crates/switchyard-translation/src/codecs/openai_chat/buffered.rs +++ b/crates/switchyard-translation/src/codecs/openai_chat/buffered.rs @@ -1090,6 +1090,11 @@ pub(crate) fn encode_openai_content( return Ok(Value::String(text_from_blocks(content, "\n"))); } if role != Role::User { + for block in content { + if let ContentBlock::Unknown { provider, raw } = block { + reject_responses_builtin_tool_item(provider, raw, WireFormat::OpenAiChat)?; + } + } push_lossy( diagnostics, policy, diff --git a/crates/switchyard-translation/src/codecs/responses/buffered.rs b/crates/switchyard-translation/src/codecs/responses/buffered.rs index b5ce2a8b1..721b5b075 100644 --- a/crates/switchyard-translation/src/codecs/responses/buffered.rs +++ b/crates/switchyard-translation/src/codecs/responses/buffered.rs @@ -731,7 +731,17 @@ fn decode_responses_input( Some("additional_tools") => {} _ => { let message = Message { - role: Role::User, + role: match item_type { + Some("shell_call" | "apply_patch_call" | "computer_call") => { + Role::Assistant + } + Some( + "shell_call_output" + | "apply_patch_call_output" + | "computer_call_output", + ) => Role::Tool, + _ => Role::User, + }, content: vec![ContentBlock::Unknown { provider: WireFormat::OpenAiResponses.into(), raw: Value::Object(item.clone()), @@ -1288,8 +1298,7 @@ fn encode_responses_input( for message in messages { // The decoder carries each valid top-level built-in tool item as one // provider-qualified block, so replay that block in place. - if message.role == Role::User - && let [ContentBlock::Unknown { provider, raw }] = message.content.as_slice() + if let [ContentBlock::Unknown { provider, raw }] = message.content.as_slice() && provider.as_str() == WireFormat::OpenAiResponses.as_str() && is_responses_builtin_tool_item(raw) { diff --git a/crates/switchyard-translation/tests/request_translation.rs b/crates/switchyard-translation/tests/request_translation.rs index b2c23a97b..6131a9494 100644 --- a/crates/switchyard-translation/tests/request_translation.rs +++ b/crates/switchyard-translation/tests/request_translation.rs @@ -836,6 +836,8 @@ fn anthropic_tool_result_error_survives_target_prompt() -> TestResult { // Target prompts force normalized re-encoding; built-in tool history must retain its wire shape. #[test] fn responses_builtin_tool_history_survives_target_prompt() -> TestResult { + use switchyard_translation::Role; + let engine = TranslationEngine::default(); let policy = TranslationPolicy::default(); let input = json!([ @@ -873,6 +875,13 @@ fn responses_builtin_tool_history_survives_target_prompt() -> TestResult { .decode_request(WireFormat::OpenAiResponses, &body, &policy)? .request; + let roles: Vec<_> = request + .messages + .iter() + .map(|message| message.role) + .collect(); + assert_eq!(roles, [Role::Assistant, Role::Tool].repeat(3)); + prepare_request_for_target(&mut request, &"target".into(), Some("target prompt")); let output = engine From f8faf1f3501506952a47fb79541dbed053b2d4d6 Mon Sep 17 00:00:00 2001 From: Ryan Lempka Date: Fri, 30 Oct 2026 14:29:43 -0500 Subject: [PATCH 2/2] fix(translation): preserve image generation tool history Signed-off-by: Ryan Lempka --- .../src/codecs/responses/buffered.rs | 9 ++++++--- crates/switchyard-translation/src/util.rs | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/switchyard-translation/src/codecs/responses/buffered.rs b/crates/switchyard-translation/src/codecs/responses/buffered.rs index 721b5b075..a3558c147 100644 --- a/crates/switchyard-translation/src/codecs/responses/buffered.rs +++ b/crates/switchyard-translation/src/codecs/responses/buffered.rs @@ -732,9 +732,12 @@ fn decode_responses_input( _ => { let message = Message { role: match item_type { - Some("shell_call" | "apply_patch_call" | "computer_call") => { - Role::Assistant - } + Some( + "shell_call" + | "apply_patch_call" + | "computer_call" + | "image_generation_call", + ) => Role::Assistant, Some( "shell_call_output" | "apply_patch_call_output" diff --git a/crates/switchyard-translation/src/util.rs b/crates/switchyard-translation/src/util.rs index 5d3351c7c..38bec12c3 100644 --- a/crates/switchyard-translation/src/util.rs +++ b/crates/switchyard-translation/src/util.rs @@ -110,6 +110,7 @@ pub(crate) fn is_responses_builtin_tool_item(item: &Value) -> bool { | "shell_call_output" | "computer_call" | "computer_call_output" + | "image_generation_call" ) ) }