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.
This commit is contained in:
Conrad Irwin authored and GitHub committed 2026-08-18 03:20:44 +00:00
1 parent 477c81b92e
commit 1c9e06076c
1 file changed
+89 -2
+89 -2
View File
@@ -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,
}
}
}