From 1c9e06076c944a8b3d5ee338d29aa7bef0749c0f Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 18 Aug 2026 03:20:44 +0000 Subject: [PATCH] open_ai: Classify Responses API send failures as transport errors (#62660) OpenAI Responses API send failures were being converted into the catch-all `Other` error before they reached the language model layer. A DNS failure from the ChatGPT Subscription provider could therefore appear as an unexpected model error instead of using the existing transport-aware messaging and retry behavior. The shared Chat Completions transport already preserves this distinction. This change applies the same `RequestError::HttpSend` classification to Responses API streaming and compaction requests, allowing the existing conversion to produce `LanguageModelCompletionError::HttpSend` with the provider and underlying cause intact. Separate regression tests cover send failures in both request paths. Testing performed: - `cargo test -p open_ai reports_http_send_errors -- --nocapture` - `cargo test -p open_ai` - `cargo check -p openai_subscribed` - `cargo fmt --all --check` - `git diff --check` - `./script/clippy -p open_ai` Release Notes: - Fixed ChatGPT Subscription connection failures showing a generic error instead of a network-specific message. --- crates/open_ai/src/responses.rs | 91 ++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) diff --git a/crates/open_ai/src/responses.rs b/crates/open_ai/src/responses.rs index 6d6c903ea3..0a11d91e9f 100644 --- a/crates/open_ai/src/responses.rs +++ b/crates/open_ai/src/responses.rs @@ -743,7 +743,13 @@ pub async fn compact_response( )) .map_err(|error| RequestError::Other(error.into()))?; - let mut response = client.send(request).await?; + let mut response = client + .send(request) + .await + .map_err(|error| RequestError::HttpSend { + provider: provider_name.to_owned(), + error, + })?; let mut body = String::new(); response .body_mut() @@ -784,7 +790,13 @@ pub async fn stream_response( )) .map_err(|e| RequestError::Other(e.into()))?; - let mut response = client.send(request).await?; + let mut response = client + .send(request) + .await + .map_err(|error| RequestError::HttpSend { + provider: provider_name.to_owned(), + error, + })?; if response.status().is_success() { if is_streaming { let reader = BufReader::new(response.into_body()); @@ -1111,6 +1123,60 @@ mod tests { ); } + #[test] + fn stream_response_reports_http_send_errors() { + let http_client = + FakeHttpClient::create(|_| async move { Err(anyhow::anyhow!("DNS lookup failed")) }); + + let error = block_on(stream_response( + http_client.as_ref(), + "ChatGPT Subscription", + "https://chatgpt.com/backend-api/codex", + "secret", + response_test_request(), + &CustomHeaders::default(), + )); + let error = match error { + Ok(_) => panic!("expected request to fail"), + Err(error) => language_model_core::LanguageModelCompletionError::from(error), + }; + + match error { + language_model_core::LanguageModelCompletionError::HttpSend { provider, error } => { + assert_eq!(provider.0.as_ref(), "ChatGPT Subscription"); + assert_eq!(error.to_string(), "DNS lookup failed"); + } + error => panic!("expected an HTTP send error, got {error:?}"), + } + } + + #[test] + fn compact_response_reports_http_send_errors() { + let http_client = + FakeHttpClient::create(|_| async move { Err(anyhow::anyhow!("DNS lookup failed")) }); + + let error = block_on(compact_response( + http_client.as_ref(), + "ChatGPT Subscription", + "https://chatgpt.com/backend-api/codex", + "secret", + compact_test_request(), + &CustomHeaders::default(), + )); + let error = match error { + Ok(_) => panic!("expected request to fail"), + Err(error) => language_model_core::LanguageModelCompletionError::from(error), + }; + + match error { + language_model_core::LanguageModelCompletionError::HttpSend { provider, error } => { + assert_eq!(provider.0.as_ref(), "ChatGPT Subscription"); + assert_eq!(error.to_string(), "DNS lookup failed"); + } + error => panic!("expected an HTTP send error, got {error:?}"), + } + } + #[test] fn compacted_response_preserves_canonical_output_items() { let output = vec![ @@ -1308,4 +1374,25 @@ mod tests { service_tier: Some(ServiceTier::Priority), } } + + fn response_test_request() -> Request { + Request { + model: "gpt-5.4".to_string(), + instructions: None, + input: ResponseInput::new(Vec::new(), Vec::new()), + include: Vec::new(), + stream: true, + temperature: None, + top_p: None, + max_output_tokens: None, + parallel_tool_calls: None, + tool_choice: None, + tools: Vec::new(), + prompt_cache_key: None, + reasoning: None, + store: None, + service_tier: None, + context_management: None, + } + } }