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..d1abc6cc9 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(()) } +// 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(); + 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 {