feat: propagate Google upstream errors to client
When Google returns an error (400, 429, 500, etc.), the MITM proxy now captures it and the API handlers return it immediately instead of hanging until timeout. - UpstreamError struct stored in MitmStore - MITM proxy parses Google error JSON (message + status) - Polling handler checks for upstream errors each cycle - Streaming handlers emit response.failed / SSE error events - Error status mapped to OpenAI-style types (invalid_request_error, rate_limit_error, authentication_error, server_error, etc.) - All handlers clear stale errors at request start
This commit is contained in:
@@ -16,7 +16,7 @@ use tracing::{debug, info};
|
||||
use super::models::{lookup_model, DEFAULT_MODEL, MODELS};
|
||||
use super::polling::{extract_response_text, is_response_done, poll_for_response, extract_model_usage, extract_thinking_signature, extract_thinking_content};
|
||||
use super::types::*;
|
||||
use super::util::{err_response, now_unix, responses_sse_event};
|
||||
use super::util::{err_response, upstream_err_response, now_unix, responses_sse_event};
|
||||
use super::AppState;
|
||||
use crate::mitm::store::PendingToolResult;
|
||||
use crate::mitm::modify::{openai_tools_to_gemini, openai_tool_choice_to_gemini};
|
||||
@@ -552,8 +552,9 @@ async fn handle_responses_sync(
|
||||
let created_at = now_unix();
|
||||
let has_custom_tools = state.mitm_store.get_tools().await.is_some();
|
||||
|
||||
// Clear stale captured response
|
||||
// Clear stale captured response and upstream errors
|
||||
state.mitm_store.clear_response_async().await;
|
||||
state.mitm_store.clear_upstream_error().await;
|
||||
|
||||
// ── MITM bypass: poll MitmStore directly when custom tools active ──
|
||||
if has_custom_tools {
|
||||
@@ -653,6 +654,9 @@ async fn handle_responses_sync(
|
||||
|
||||
// ── Normal LS path (no custom tools) ──
|
||||
let poll_result = poll_for_response(&state, &cascade_id, timeout).await;
|
||||
if let Some(ref err) = poll_result.upstream_error {
|
||||
return upstream_err_response(err);
|
||||
}
|
||||
let completed_at = now_unix();
|
||||
let msg_id = format!(
|
||||
"msg_{}",
|
||||
@@ -806,8 +810,9 @@ async fn handle_responses_stream(
|
||||
let reasoning_id = format!("rs_{}", uuid::Uuid::new_v4().to_string().replace('-', ""));
|
||||
let has_custom_tools = state.mitm_store.get_tools().await.is_some();
|
||||
|
||||
// Clear stale captured response
|
||||
// Clear stale captured response and upstream errors
|
||||
state.mitm_store.clear_response_async().await;
|
||||
state.mitm_store.clear_upstream_error().await;
|
||||
|
||||
// ── MITM bypass mode (when custom tools are active) ──
|
||||
// Skip LS entirely — read text, thinking, and tool calls directly from MitmStore.
|
||||
@@ -815,6 +820,29 @@ async fn handle_responses_stream(
|
||||
let mut last_thinking = String::new();
|
||||
|
||||
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));
|
||||
yield Ok(responses_sse_event(
|
||||
"response.failed",
|
||||
serde_json::json!({
|
||||
"type": "response.failed",
|
||||
"sequence_number": next_seq(),
|
||||
"response": {
|
||||
"id": &response_id,
|
||||
"status": "failed",
|
||||
"error": {
|
||||
"type": err.error_status.as_deref().unwrap_or("upstream_error"),
|
||||
"message": error_msg,
|
||||
"code": err.status,
|
||||
},
|
||||
},
|
||||
}),
|
||||
));
|
||||
break;
|
||||
}
|
||||
|
||||
// Check for function calls first
|
||||
let captured = state.mitm_store.take_any_function_calls().await;
|
||||
if let Some(ref raw_calls) = captured {
|
||||
@@ -1135,6 +1163,29 @@ async fn handle_responses_stream(
|
||||
let mut last_thinking_len: usize = 0;
|
||||
|
||||
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));
|
||||
yield Ok(responses_sse_event(
|
||||
"response.failed",
|
||||
serde_json::json!({
|
||||
"type": "response.failed",
|
||||
"sequence_number": next_seq(),
|
||||
"response": {
|
||||
"id": &response_id,
|
||||
"status": "failed",
|
||||
"error": {
|
||||
"type": err.error_status.as_deref().unwrap_or("upstream_error"),
|
||||
"message": error_msg,
|
||||
"code": err.status,
|
||||
},
|
||||
},
|
||||
}),
|
||||
));
|
||||
break;
|
||||
}
|
||||
|
||||
if let Ok((status, data)) = state.backend.get_steps(&cascade_id).await {
|
||||
if status == 200 {
|
||||
if let Some(steps) = data["steps"].as_array() {
|
||||
|
||||
Reference in New Issue
Block a user