fix: forward Google's exact error messages to client
Root cause: errors from Google were being swallowed, replaced with placeholders like 'Google API returned HTTP 400' or '[Timeout waiting for response]', or silently converted to fake 'incomplete' responses. Changes across all endpoints (/v1/chat/completions, /v1/responses, /v1/gemini, /v1/search): Error message fidelity: - UpstreamError message now includes Google's status prefix: [STATUS] msg - Falls back to raw body if JSON parsing fails (protobuf, HTML, etc.) - ErrorDetail gains optional code and param fields Timeout handling: - poll_for_response returns UpstreamError(504, DEADLINE_EXCEEDED) on timeout instead of '[Timeout waiting for AI response]' placeholder text - Streaming timeouts emit proper error events, not fake content - Sync bypass timeouts return 504 Gateway Timeout, not 200 incomplete Missing error checks added: - responses.rs sync bypass: added upstream_error check in polling loop - gemini.rs sync bypass: added upstream_error check in polling loop - gemini.rs streaming: added upstream_error check in polling loop (was completely missing — errors only handled in sync path) DRY helpers: - upstream_error_message(): shared exact message extraction - upstream_error_type(): shared Google→OpenAI error type mapping - All streaming handlers use these instead of inline formatting
This commit is contained in:
@@ -615,6 +615,11 @@ async fn handle_responses_sync(
|
||||
if has_custom_tools {
|
||||
let start = std::time::Instant::now();
|
||||
while start.elapsed().as_secs() < timeout {
|
||||
// Check for upstream errors from MITM (Google API errors)
|
||||
if let Some(err) = state.mitm_store.take_upstream_error().await {
|
||||
return upstream_err_response(&err);
|
||||
}
|
||||
|
||||
// Check for function calls
|
||||
let captured = state.mitm_store.take_function_calls(&cascade_id).await;
|
||||
if let Some(ref raw_calls) = captured {
|
||||
@@ -706,21 +711,12 @@ async fn handle_responses_sync(
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
|
||||
}
|
||||
|
||||
// Timeout
|
||||
let resp = build_response_object(
|
||||
ResponseData {
|
||||
id: response_id,
|
||||
model: model_name,
|
||||
status: "incomplete",
|
||||
created_at,
|
||||
completed_at: None,
|
||||
output: vec![],
|
||||
usage: Some(Usage::estimate(¶ms.user_text, "")),
|
||||
thinking_signature: None,
|
||||
},
|
||||
¶ms,
|
||||
// Timeout — return proper error, not fake incomplete response
|
||||
return err_response(
|
||||
StatusCode::GATEWAY_TIMEOUT,
|
||||
format!("Timeout: no response from Google API after {timeout}s"),
|
||||
"upstream_error",
|
||||
);
|
||||
return Json(resp).into_response();
|
||||
}
|
||||
|
||||
// ── Normal LS path (no custom tools) ──
|
||||
@@ -904,8 +900,8 @@ async fn handle_responses_stream(
|
||||
while start.elapsed().as_secs() < timeout {
|
||||
// Check for upstream errors from MITM (Google API errors)
|
||||
if let Some(err) = state.mitm_store.take_upstream_error().await {
|
||||
let error_msg = err.message.clone()
|
||||
.unwrap_or_else(|| format!("Google API returned HTTP {}", err.status));
|
||||
let error_msg = super::util::upstream_error_message(&err);
|
||||
let error_type = super::util::upstream_error_type(&err);
|
||||
yield Ok(responses_sse_event(
|
||||
"response.failed",
|
||||
serde_json::json!({
|
||||
@@ -915,7 +911,7 @@ async fn handle_responses_stream(
|
||||
"id": &response_id,
|
||||
"status": "failed",
|
||||
"error": {
|
||||
"type": err.error_status.as_deref().unwrap_or("upstream_error"),
|
||||
"type": error_type,
|
||||
"message": error_msg,
|
||||
"code": err.status,
|
||||
},
|
||||
@@ -1202,26 +1198,21 @@ async fn handle_responses_stream(
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(poll_ms)).await;
|
||||
}
|
||||
|
||||
// Timeout in bypass mode
|
||||
let timeout_resp = build_response_object(
|
||||
ResponseData {
|
||||
id: response_id.clone(),
|
||||
model: model_name.clone(),
|
||||
status: "incomplete",
|
||||
created_at,
|
||||
completed_at: None,
|
||||
output: vec![],
|
||||
usage: Some(Usage::estimate(¶ms.user_text, "")),
|
||||
thinking_signature: None,
|
||||
},
|
||||
¶ms,
|
||||
);
|
||||
// Timeout in bypass mode — emit error, not fake incomplete
|
||||
yield Ok(responses_sse_event(
|
||||
"response.completed",
|
||||
"response.failed",
|
||||
serde_json::json!({
|
||||
"type": "response.completed",
|
||||
"type": "response.failed",
|
||||
"sequence_number": next_seq(),
|
||||
"response": response_to_json(&timeout_resp),
|
||||
"response": {
|
||||
"id": &response_id,
|
||||
"status": "failed",
|
||||
"error": {
|
||||
"type": "upstream_error",
|
||||
"message": format!("Timeout: no response from Google API after {timeout}s"),
|
||||
"code": 504,
|
||||
},
|
||||
},
|
||||
}),
|
||||
));
|
||||
return;
|
||||
@@ -1247,8 +1238,8 @@ async fn handle_responses_stream(
|
||||
while start.elapsed().as_secs() < timeout {
|
||||
// Check for upstream errors from MITM (Google API errors)
|
||||
if let Some(err) = state.mitm_store.take_upstream_error().await {
|
||||
let error_msg = err.message.clone()
|
||||
.unwrap_or_else(|| format!("Google API returned HTTP {}", err.status));
|
||||
let error_msg = super::util::upstream_error_message(&err);
|
||||
let error_type = super::util::upstream_error_type(&err);
|
||||
yield Ok(responses_sse_event(
|
||||
"response.failed",
|
||||
serde_json::json!({
|
||||
@@ -1258,7 +1249,7 @@ async fn handle_responses_stream(
|
||||
"id": &response_id,
|
||||
"status": "failed",
|
||||
"error": {
|
||||
"type": err.error_status.as_deref().unwrap_or("upstream_error"),
|
||||
"type": error_type,
|
||||
"message": error_msg,
|
||||
"code": err.status,
|
||||
},
|
||||
@@ -1507,26 +1498,21 @@ async fn handle_responses_stream(
|
||||
}
|
||||
}
|
||||
|
||||
// Timeout — emit incomplete response
|
||||
let timeout_resp = build_response_object(
|
||||
ResponseData {
|
||||
id: response_id.clone(),
|
||||
model: model_name.clone(),
|
||||
status: "incomplete",
|
||||
created_at,
|
||||
completed_at: None,
|
||||
output: vec![],
|
||||
usage: Some(Usage::estimate(¶ms.user_text, "")),
|
||||
thinking_signature: None,
|
||||
},
|
||||
¶ms,
|
||||
);
|
||||
// Timeout — emit error, not fake incomplete response
|
||||
yield Ok(responses_sse_event(
|
||||
"response.completed",
|
||||
"response.failed",
|
||||
serde_json::json!({
|
||||
"type": "response.completed",
|
||||
"type": "response.failed",
|
||||
"sequence_number": next_seq(),
|
||||
"response": response_to_json(&timeout_resp),
|
||||
"response": {
|
||||
"id": &response_id,
|
||||
"status": "failed",
|
||||
"error": {
|
||||
"type": "upstream_error",
|
||||
"message": format!("Timeout: no response from Google API after {timeout}s"),
|
||||
"code": 504,
|
||||
},
|
||||
},
|
||||
}),
|
||||
));
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user