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)
.send()
.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 elapsed = start.elapsed();
@ -164,12 +175,13 @@ pub(crate) async fn send_and_check(
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
let _ = ui_tx.send(UiMessage::Debug(format!(
"API error {} after {:.1}s: {}",
"HTTP {} after {:.1}s ({}): {}",
status,
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 {

View file

@ -79,9 +79,17 @@ pub async fn stream(
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,
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() {