fix(api): enrich JSON parse errors with response body, provider, and model

Raw 'json_error: no field X' now includes truncated response body,
provider name, and model ID for debugging context.
This commit is contained in:
YeonGyu-Kim
2026-04-07 14:21:52 +09:00
parent cd83c0ff68
commit 2a642871ad
4 changed files with 210 additions and 40 deletions

View File

@@ -131,7 +131,15 @@ impl OpenAiCompatClient {
preflight_message_request(&request)?;
let response = self.send_with_retry(&request).await?;
let request_id = request_id_from_headers(response.headers());
let payload = response.json::<ChatCompletionResponse>().await?;
let body = response.text().await.map_err(ApiError::from)?;
let payload = serde_json::from_str::<ChatCompletionResponse>(&body).map_err(|error| {
ApiError::json_deserialize(
self.config.provider_name,
&request.model,
&body,
error,
)
})?;
let mut normalized = normalize_response(&request.model, payload)?;
if normalized.request_id.is_none() {
normalized.request_id = request_id;
@@ -150,7 +158,10 @@ impl OpenAiCompatClient {
Ok(MessageStream {
request_id: request_id_from_headers(response.headers()),
response,
parser: OpenAiSseParser::new(),
parser: OpenAiSseParser::with_context(
self.config.provider_name,
request.model.clone(),
),
pending: VecDeque::new(),
done: false,
state: StreamState::new(request.model.clone()),
@@ -282,11 +293,17 @@ impl MessageStream {
#[derive(Debug, Default)]
struct OpenAiSseParser {
buffer: Vec<u8>,
provider: String,
model: String,
}
impl OpenAiSseParser {
fn new() -> Self {
Self::default()
fn with_context(provider: impl Into<String>, model: impl Into<String>) -> Self {
Self {
buffer: Vec::new(),
provider: provider.into(),
model: model.into(),
}
}
fn push(&mut self, chunk: &[u8]) -> Result<Vec<ChatCompletionChunk>, ApiError> {
@@ -294,7 +311,7 @@ impl OpenAiSseParser {
let mut events = Vec::new();
while let Some(frame) = next_sse_frame(&mut self.buffer) {
if let Some(event) = parse_sse_frame(&frame)? {
if let Some(event) = parse_sse_frame(&frame, &self.provider, &self.model)? {
events.push(event);
}
}
@@ -835,7 +852,11 @@ fn next_sse_frame(buffer: &mut Vec<u8>) -> Option<String> {
Some(String::from_utf8_lossy(&frame[..frame_len]).into_owned())
}
fn parse_sse_frame(frame: &str) -> Result<Option<ChatCompletionChunk>, ApiError> {
fn parse_sse_frame(
frame: &str,
provider: &str,
model: &str,
) -> Result<Option<ChatCompletionChunk>, ApiError> {
let trimmed = frame.trim();
if trimmed.is_empty() {
return Ok(None);
@@ -857,9 +878,9 @@ fn parse_sse_frame(frame: &str) -> Result<Option<ChatCompletionChunk>, ApiError>
if payload == "[DONE]" {
return Ok(None);
}
serde_json::from_str(&payload)
serde_json::from_str::<ChatCompletionChunk>(&payload)
.map(Some)
.map_err(ApiError::from)
.map_err(|error| ApiError::json_deserialize(provider, model, &payload, error))
}
fn read_env_non_empty(key: &str) -> Result<Option<String>, ApiError> {