open_ai: Fix error message not showing up when using ChatGPT subscription (#57750)

The API seems to return nested errors, so made the error deserialise
properly in case we get `{ "error": {...} }` instead of a top-level
error

Closes #57024

For testing, you can prompt something like: `tell me about
https://registry.npmjs.org/vite-plus.`

Before:

<img width="631" height="69" alt="image"
src="https://github.com/user-attachments/assets/5d02e7ec-8176-4bff-87d7-908ac8f0b498"
/>

After:

<img width="697" height="61" alt="image"
src="https://github.com/user-attachments/assets/97fac249-8b76-463c-8483-a150f5db9857"
/>


Release Notes:

- openai: Fixed an issue where error messages would not show up properly
This commit is contained in:
Bennet Bo Fenner 2026-05-27 11:18:39 +02:00 committed by GitHub
parent 5e717a06cd
commit 88a54a2683
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 73 additions and 3 deletions

View file

@ -881,8 +881,13 @@ impl OpenAiResponseEventMapper {
let message = response_failure_message(&response);
vec![Err(LanguageModelCompletionError::Other(anyhow!(message)))]
}
ResponsesStreamEvent::Error { error }
| ResponsesStreamEvent::GenericError { error } => {
ResponsesStreamEvent::Error { error } => {
vec![Err(LanguageModelCompletionError::Other(anyhow!(
response_error_message(&error)
)))]
}
ResponsesStreamEvent::GenericError { error } => {
let error = error.into_response_error();
vec![Err(LanguageModelCompletionError::Other(anyhow!(
response_error_message(&error)
)))]
@ -2214,6 +2219,34 @@ mod tests {
assert_eq!(error.to_string(), "ERR_SOMETHING: Something went wrong");
}
#[test]
fn responses_stream_deserializes_nested_error_event() {
// In practice the Responses API often nests error fields under an
// `error` object even though the public spec documents them at the top
// level. Make sure we don't lose the message and code in that case.
let event = serde_json::from_value::<ResponsesStreamEvent>(json!({
"type": "error",
"error": {
"type": "invalid_request_error",
"code": "context_length_exceeded",
"message": "Your input exceeds the context window of this model. Please adjust your input and try again.",
"param": "input"
},
"sequence_number": 2
}))
.expect("nested error event");
let mut mapper = OpenAiResponseEventMapper::new();
let mapped = mapper.map_event(event);
assert_eq!(mapped.len(), 1);
let error = mapped.into_iter().next().unwrap().unwrap_err();
assert_eq!(
error.to_string(),
"context_length_exceeded: Your input exceeds the context window of this model. Please adjust your input and try again."
);
}
#[test]
fn responses_stream_deserializes_response_error_event() {
let event = serde_json::from_value::<ResponsesStreamEvent>(json!({

View file

@ -158,6 +158,43 @@ pub struct ResponseError {
pub param: Option<Value>,
}
/// Payload of the top-level `error` SSE event from the Responses API.
///
/// OpenAI's spec documents the error fields as being at the top level of the
/// event, but in practice the API often nests them under an `error` object.
#[derive(Deserialize, Debug, Clone, Default)]
pub struct GenericStreamErrorPayload {
#[serde(flatten)]
top_level: PartialResponseError,
#[serde(default)]
error: Option<PartialResponseError>,
}
#[derive(Deserialize, Debug, Clone, Default)]
struct PartialResponseError {
#[serde(default)]
code: Option<String>,
#[serde(default)]
message: Option<String>,
#[serde(default)]
param: Option<Value>,
}
impl GenericStreamErrorPayload {
pub fn into_response_error(self) -> ResponseError {
let nested = self.error.unwrap_or_default();
ResponseError {
code: self.top_level.code.or(nested.code),
message: self
.top_level
.message
.or(nested.message)
.unwrap_or_default(),
param: self.top_level.param.or(nested.param),
}
}
}
#[derive(Deserialize, Debug)]
#[serde(tag = "type")]
pub enum StreamEvent {
@ -278,7 +315,7 @@ pub enum StreamEvent {
#[serde(rename = "error")]
GenericError {
#[serde(flatten)]
error: ResponseError,
error: GenericStreamErrorPayload,
},
#[serde(other)]
Unknown,