Expose anthropic API errors to the client (#16129)

Max Brunsfeld and Marshall created

Now, when an anthropic request is invalid or anthropic's API is down,
we'll expose that to the user instead of just returning a generic 500.

Release Notes:

- N/A

Co-authored-by: Marshall <marshall@zed.dev>

Change summary

crates/collab/src/llm.rs                    | 24 ++++++++++++++--------
crates/language_model/src/provider/cloud.rs |  8 ++++--
2 files changed, 20 insertions(+), 12 deletions(-)

Detailed changes

crates/collab/src/llm.rs 🔗

@@ -207,16 +207,22 @@ async fn perform_completion(
             )
             .await
             .map_err(|err| match err {
-                anthropic::AnthropicError::ApiError(ref api_error) => {
-                    if api_error.code() == Some(anthropic::ApiErrorCode::RateLimitError) {
-                        return Error::http(
-                            StatusCode::TOO_MANY_REQUESTS,
-                            "Upstream Anthropic rate limit exceeded.".to_string(),
-                        );
+                anthropic::AnthropicError::ApiError(ref api_error) => match api_error.code() {
+                    Some(anthropic::ApiErrorCode::RateLimitError) => Error::http(
+                        StatusCode::TOO_MANY_REQUESTS,
+                        "Upstream Anthropic rate limit exceeded.".to_string(),
+                    ),
+                    Some(anthropic::ApiErrorCode::InvalidRequestError) => {
+                        Error::http(StatusCode::BAD_REQUEST, api_error.message.clone())
                     }
-
-                    Error::Internal(anyhow!(err))
-                }
+                    Some(anthropic::ApiErrorCode::OverloadedError) => {
+                        Error::http(StatusCode::SERVICE_UNAVAILABLE, api_error.message.clone())
+                    }
+                    Some(_) => {
+                        Error::http(StatusCode::INTERNAL_SERVER_ERROR, api_error.message.clone())
+                    }
+                    None => Error::Internal(anyhow!(err)),
+                },
                 anthropic::AnthropicError::Other(err) => Error::Internal(err),
             })?;
 

crates/language_model/src/provider/cloud.rs 🔗

@@ -20,7 +20,7 @@ use serde::{Deserialize, Serialize};
 use serde_json::value::RawValue;
 use settings::{Settings, SettingsStore};
 use smol::{
-    io::BufReader,
+    io::{AsyncReadExt, BufReader},
     lock::{RwLock, RwLockUpgradableReadGuard, RwLockWriteGuard},
 };
 use std::{future, sync::Arc};
@@ -334,7 +334,7 @@ impl CloudLanguageModel {
                 .header("Content-Type", "application/json")
                 .header("Authorization", format!("Bearer {token}"))
                 .body(serde_json::to_string(&body)?.into())?;
-            let response = http_client.send(request).await?;
+            let mut response = http_client.send(request).await?;
             if response.status().is_success() {
                 break response;
             } else if !did_retry
@@ -346,8 +346,10 @@ impl CloudLanguageModel {
                 did_retry = true;
                 token = llm_api_token.refresh(&client).await?;
             } else {
+                let mut body = String::new();
+                response.body_mut().read_to_string(&mut body).await?;
                 break Err(anyhow!(
-                    "cloud language model completion failed with status {}",
+                    "cloud language model completion failed with status {}: {body}",
                     response.status()
                 ))?;
             }