diff --git a/crates/open_ai/src/completion.rs b/crates/open_ai/src/completion.rs index af140237564..b332cd4c2a6 100644 --- a/crates/open_ai/src/completion.rs +++ b/crates/open_ai/src/completion.rs @@ -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::(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::(json!({ diff --git a/crates/open_ai/src/responses.rs b/crates/open_ai/src/responses.rs index 7465bc1ca46..5fae769f695 100644 --- a/crates/open_ai/src/responses.rs +++ b/crates/open_ai/src/responses.rs @@ -158,6 +158,43 @@ pub struct ResponseError { pub param: Option, } +/// 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, +} + +#[derive(Deserialize, Debug, Clone, Default)] +struct PartialResponseError { + #[serde(default)] + code: Option, + #[serde(default)] + message: Option, + #[serde(default)] + param: Option, +} + +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,