From 00b67e3cf90e05e58ed4c5fd97e2362759ceba45 Mon Sep 17 00:00:00 2001 From: Graham King Date: Tue, 29 Sep 2026 08:56:36 -0400 Subject: [PATCH] fix(responses): preserve streamed response IDs for conversation continuation Chat-backed Responses streams added a resp_ prefix to response IDs, but stored history remained keyed by the upstream ID. Follow-up requests using the returned ID silently lost prior turns. Now we preserve the upstream ID during streaming, matching buffered responses and history storage. Fixes: https://linear.app/nvidia/issue/SWITCH-1564 Assisted-by: Pi:GPT 6 Astra medium Reviewed-by: Claude:Opus 5.5 medium Signed-off-by: Graham King --- .../src/codecs/responses/stream.rs | 13 ++++--------- .../tests/stream_translation.rs | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/crates/switchyard-translation/src/codecs/responses/stream.rs b/crates/switchyard-translation/src/codecs/responses/stream.rs index a179d4ee7..9a76fb0e0 100644 --- a/crates/switchyard-translation/src/codecs/responses/stream.rs +++ b/crates/switchyard-translation/src/codecs/responses/stream.rs @@ -1148,14 +1148,9 @@ fn fnv1a_64(text: &str) -> u64 { }) } -// Converts any upstream message ID into a Responses-looking response ID. +// Keep the response ID aligned with buffered responses and stored conversation history. fn responses_id(state: &StreamTranslationState) -> String { - let Some(id) = target_message_id_or_source_message_id(state) else { - return "resp_switchyard".to_string(); - }; - if id.starts_with("resp_") { - id.to_string() - } else { - format!("resp_{id}") - } + target_message_id_or_source_message_id(state) + .unwrap_or("resp_switchyard") + .to_string() } diff --git a/crates/switchyard-translation/tests/stream_translation.rs b/crates/switchyard-translation/tests/stream_translation.rs index 01e9cc0da..e2cd468c5 100644 --- a/crates/switchyard-translation/tests/stream_translation.rs +++ b/crates/switchyard-translation/tests/stream_translation.rs @@ -828,6 +828,22 @@ fn openai_chat_to_responses_uses_served_model_without_losing_source_model() -> T Ok(()) } +// Stored conversation history is keyed by the upstream ID, so clients must see it unchanged. +#[test] +fn openai_chat_to_responses_keeps_the_upstream_id() -> TestResult { + let mut state = + StreamTranslationState::new(WireFormat::OpenAiChat, WireFormat::OpenAiResponses); + let events = TranslationEngine::default().translate_event( + &mut state, + WireFormat::OpenAiChat, + WireFormat::OpenAiResponses, + &json!({"id": "chatcmpl-test", "choices": [{"index": 0, "delta": {"content": "hi"}}]}), + )?; + + assert_eq!(events[0]["response"]["id"], "chatcmpl-test"); + Ok(()) +} + // Verifies OpenAI Chat finish emits a terminal chunk when the source closes without one. #[test] fn openai_chat_finish_synthesizes_terminal_chunk_after_incomplete_source() -> TestResult {