api: proper error messages for connection failures and HTTP errors

- Connection errors now show cause (refused/timeout/request error),
  URL, and the underlying error without redundant URL repetition
- HTTP errors show status code, URL, and up to 1000 chars of body
- Unparseable SSE events logged with content preview instead of
  silently dropped — may contain error info from vllm/server
- Stream errors already had good context (kept as-is)

You can't debug what you can't see.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kent Overstreet 2026-03-21 12:15:08 -04:00
parent b1d83b55c0
commit b28b7def19
2 changed files with 26 additions and 6 deletions

View file

@ -138,7 +138,18 @@ pub(crate) async fn send_and_check(
.json(body) .json(body)
.send() .send()
.await .await
.map_err(|e| anyhow::anyhow!("Failed to send request to API: {}", e))?; .map_err(|e| {
let cause = if e.is_connect() {
"connection refused"
} else if e.is_timeout() {
"request timed out"
} else if e.is_request() {
"request error"
} else {
"unknown"
};
anyhow::anyhow!("{} ({}): {}", cause, url, e.without_url())
})?;
let status = response.status(); let status = response.status();
let elapsed = start.elapsed(); let elapsed = start.elapsed();
@ -164,12 +175,13 @@ pub(crate) async fn send_and_check(
if !status.is_success() { if !status.is_success() {
let body = response.text().await.unwrap_or_default(); let body = response.text().await.unwrap_or_default();
let _ = ui_tx.send(UiMessage::Debug(format!( let _ = ui_tx.send(UiMessage::Debug(format!(
"API error {} after {:.1}s: {}", "HTTP {} after {:.1}s ({}): {}",
status, status,
elapsed.as_secs_f64(), elapsed.as_secs_f64(),
&body[..body.len().min(300)] url,
&body[..body.len().min(500)]
))); )));
anyhow::bail!("API error {}: {}", status, &body[..body.len().min(500)]); anyhow::bail!("HTTP {} ({}): {}", status, url, &body[..body.len().min(1000)]);
} }
if debug { if debug {

View file

@ -79,9 +79,17 @@ pub async fn stream(
anyhow::bail!("API error in stream: {} {}", err_msg, raw); anyhow::bail!("API error in stream: {} {}", err_msg, raw);
} }
let chunk: ChatCompletionChunk = match serde_json::from_value(event) { let chunk: ChatCompletionChunk = match serde_json::from_value(event.clone()) {
Ok(c) => c, Ok(c) => c,
Err(_) => continue, Err(e) => {
// Log unparseable events — they may contain error info
let preview = event.to_string();
let _ = ui_tx.send(UiMessage::Debug(format!(
"unparseable SSE event ({}): {}",
e, &preview[..preview.len().min(300)]
)));
continue;
}
}; };
if chunk.usage.is_some() { if chunk.usage.is_some() {