diff --git a/crates/switchyard-translation/src/codecs/openai_chat/buffered.rs b/crates/switchyard-translation/src/codecs/openai_chat/buffered.rs index fff2ec46c..afe99c539 100644 --- a/crates/switchyard-translation/src/codecs/openai_chat/buffered.rs +++ b/crates/switchyard-translation/src/codecs/openai_chat/buffered.rs @@ -140,7 +140,7 @@ impl FormatCodec for OpenAiChatCodec { content: vec![ContentBlock::ToolResult(ToolResult { tool_call_id, content: vec![ContentBlock::Text { text }], - is_error: None, + is_error: message.get("is_error").and_then(Value::as_bool), })], }); continue; diff --git a/crates/switchyard-translation/tests/request_translation.rs b/crates/switchyard-translation/tests/request_translation.rs index 3de0f5a47..6cd696397 100644 --- a/crates/switchyard-translation/tests/request_translation.rs +++ b/crates/switchyard-translation/tests/request_translation.rs @@ -3311,6 +3311,30 @@ fn openai_stop_string_maps_to_anthropic_stop_sequences() -> TestResult { Ok(()) } +#[test] +fn openai_tool_result_error_flag_survives_decoding() -> TestResult { + let engine = TranslationEngine::default(); + for (flag, expected) in [ + (Some(json!(true)), Some(true)), + (Some(json!(false)), Some(false)), + (None, None), + (Some(Value::Null), None), + (Some(json!("true")), None), + ] { + let mut message = json!({"role": "tool", "tool_call_id": "call_1", "content": "result"}); + if let Some(flag) = flag { + message["is_error"] = flag; + } + let body = json!({"model": "route", "messages": [message]}); + let decoded = engine.decode_request(WireFormat::OpenAiChat, &body, &normalized_policy())?; + let ContentBlock::ToolResult(result) = &decoded.request.messages[0].content[0] else { + panic!("expected tool result"); + }; + assert_eq!(result.is_error, expected); + } + Ok(()) +} + // Verifies OpenAI tool results merge into Anthropic user tool-result content. #[test] fn openai_tool_results_are_merged_when_translating_to_anthropic() -> TestResult {