From bcc26f016501e2b2348d763932861cdc4d68e57e Mon Sep 17 00:00:00 2001 From: Blake Date: Wed, 12 Aug 2026 13:22:40 -0500 Subject: [PATCH 1/2] fix(translation): preserve reasoning order in mixed stream chunks Signed-off-by: Blake --- .../src/codecs/openai_chat/stream.rs | 16 ++-- .../tests/stream_translation.rs | 78 +++++++++++++++++++ 2 files changed, 86 insertions(+), 8 deletions(-) diff --git a/crates/switchyard-translation/src/codecs/openai_chat/stream.rs b/crates/switchyard-translation/src/codecs/openai_chat/stream.rs index b4c6273b1..8ee7feab2 100644 --- a/crates/switchyard-translation/src/codecs/openai_chat/stream.rs +++ b/crates/switchyard-translation/src/codecs/openai_chat/stream.rs @@ -97,14 +97,6 @@ fn decode_openai_chat_stream( continue; }; if let Some(delta) = choice.get("delta").and_then(Value::as_object) { - if let Some(text) = delta.get("content").and_then(Value::as_str) - && !text.is_empty() - { - out.push(LlmResponseChunk::TextDelta { - index: 0, - text: text.to_string(), - }); - } for reasoning_key in ["reasoning_content", "reasoning"] { if let Some(text) = delta.get(reasoning_key).and_then(Value::as_str) && !text.is_empty() @@ -115,6 +107,14 @@ fn decode_openai_chat_stream( }); } } + if let Some(text) = delta.get("content").and_then(Value::as_str) + && !text.is_empty() + { + out.push(LlmResponseChunk::TextDelta { + index: 0, + text: text.to_string(), + }); + } if let Some(tool_calls) = delta.get("tool_calls").and_then(Value::as_array) { for tool_call in tool_calls { if let Some(tool_call) = tool_call.as_object() { diff --git a/crates/switchyard-translation/tests/stream_translation.rs b/crates/switchyard-translation/tests/stream_translation.rs index 3aeed7159..4aaa099f9 100644 --- a/crates/switchyard-translation/tests/stream_translation.rs +++ b/crates/switchyard-translation/tests/stream_translation.rs @@ -303,6 +303,84 @@ fn openai_chat_stream_event_translates_to_anthropic_message_events() -> TestResu Ok(()) } +// Verifies mixed reasoning and content emit reasoning before text for Anthropic clients for Anthropic clients. +#[test] +fn openai_chat_mixed_reasoning_and_content_stream_in_reasoning_first_order() -> TestResult { + let engine = TranslationEngine::default(); + let mut state = + StreamTranslationState::new(WireFormat::OpenAiChat, WireFormat::AnthropicMessages); + let chunk = json!({ + "id": "chatcmpl-test", + "object": "chat.completion.chunk", + "model": "nvidia/nvidia/nemotron-3-ultra-nvfp4", + "choices": [{ + "index": 0, + "delta": { + "reasoning_content": ".", + "content": "Hello" + }, + "finish_reason": null + }] + }); + + let events = engine.translate_event( + &mut state, + WireFormat::OpenAiChat, + WireFormat::AnthropicMessages, + &chunk, + )?; + + assert_eq!( + events, + vec![ + json!({ + "type": "message_start", + "message": { + "id": "msg_chatcmpl-test", + "type": "message", + "role": "assistant", + "model": "nvidia/nvidia/nemotron-3-ultra-nvfp4", + "content": [], + "stop_reason": null, + "stop_sequence": null, + "usage": {"input_tokens": 0, "output_tokens": 0} + } + }), + json!({ + "type": "content_block_start", + "index": 0, + "content_block": { + "type": "thinking", + "thinking": "", + "signature": "" + } + }), + json!({ + "type": "content_block_delta", + "index": 0, + "delta": {"type": "thinking_delta", "thinking": "."} + }), + json!({ + "type": "content_block_delta", + "index": 0, + "delta": {"type": "signature_delta", "signature": ""} + }), + json!({"type": "content_block_stop", "index": 0}), + json!({ + "type": "content_block_start", + "index": 1, + "content_block": {"type": "text", "text": ""} + }), + json!({ + "type": "content_block_delta", + "index": 1, + "delta": {"type": "text_delta", "text": "Hello"} + }), + ] + ); + Ok(()) +} + // Verifies Anthropic usage and stop events become terminal OpenAI chunks. #[test] fn anthropic_stream_usage_and_stop_translate_to_openai_chunks() -> TestResult { From 25da6feba35bf7f60f6381f130732c50778a867d Mon Sep 17 00:00:00 2001 From: Blake Date: Wed, 12 Aug 2026 13:53:14 -0500 Subject: [PATCH 2/2] fix(translation): deduplicate test doc comment Signed-off-by: Blake --- crates/switchyard-translation/tests/stream_translation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/switchyard-translation/tests/stream_translation.rs b/crates/switchyard-translation/tests/stream_translation.rs index 4aaa099f9..d1abc6cc9 100644 --- a/crates/switchyard-translation/tests/stream_translation.rs +++ b/crates/switchyard-translation/tests/stream_translation.rs @@ -303,7 +303,7 @@ fn openai_chat_stream_event_translates_to_anthropic_message_events() -> TestResu Ok(()) } -// Verifies mixed reasoning and content emit reasoning before text for Anthropic clients for Anthropic clients. +// A mixed chunk must emit reasoning before text, matching the buffered decoder. #[test] fn openai_chat_mixed_reasoning_and_content_stream_in_reasoning_first_order() -> TestResult { let engine = TranslationEngine::default();