diff --git a/crates/anthropic/src/anthropic.rs b/crates/anthropic/src/anthropic.rs index b1eb5d0fba..18769e3ec0 100644 --- a/crates/anthropic/src/anthropic.rs +++ b/crates/anthropic/src/anthropic.rs @@ -1120,14 +1120,20 @@ pub enum ApiErrorCode { InvalidRequestError, /// 401 - `authentication_error`: There's an issue with your API key. AuthenticationError, + /// 402 - `billing_error`: There's an issue with the account's billing. + BillingError, /// 403 - `permission_error`: Your API key does not have permission to use the specified resource. PermissionError, /// 404 - `not_found_error`: The requested resource was not found. NotFoundError, + /// 409 - `conflict_error`: The request conflicts with the current state of the resource. + ConflictError, /// 413 - `request_too_large`: Request exceeds the maximum allowed number of bytes. RequestTooLarge, /// 429 - `rate_limit_error`: Your account has hit a rate limit. RateLimitError, + /// 504 - `timeout_error`: Anthropic's gateway timed out while processing the request. + TimeoutError, /// 500 - `api_error`: An unexpected error has occurred internal to Anthropic's systems. ApiError, /// 529 - `overloaded_error`: Anthropic's API is temporarily overloaded. @@ -1235,11 +1241,17 @@ pub fn completion_error_from_anthropic_api( provider, message: error.message, }, + BillingError => Error::PaymentRequired, PermissionError => Error::PermissionError { provider, message: error.message, }, NotFoundError => Error::ApiEndpointNotFound { provider }, + ConflictError => Error::HttpResponseError { + provider, + status_code: StatusCode::CONFLICT, + message: error.message, + }, RequestTooLarge => Error::PromptTooLarge { tokens: language_model_core::parse_prompt_too_long(&error.message), }, @@ -1247,6 +1259,11 @@ pub fn completion_error_from_anthropic_api( provider, retry_after: None, }, + TimeoutError => Error::UpstreamProviderError { + message: error.message, + status: StatusCode::GATEWAY_TIMEOUT, + retry_after: None, + }, ApiError => Error::ApiInternalServerError { provider, message: error.message, @@ -1293,6 +1310,93 @@ mod tests { )); } + #[test] + fn list_models_maps_anthropic_billing_errors_to_payment_required() { + let client = FakeHttpClient::create(|_| async move { + Ok(http::Response::builder() + .status(StatusCode::PAYMENT_REQUIRED) + .body(AsyncBody::from( + r#"{"type":"error","error":{"type":"billing_error","message":"There is a problem with your billing information."},"request_id":"request-id"}"#, + )) + .expect("valid response")) + }); + + let error = futures::executor::block_on(list_models( + client.as_ref(), + ANTHROPIC_API_URL, + "test-key", + &CustomHeaders::default(), + )) + .expect_err("billing error should fail"); + let completion_error: language_model_core::LanguageModelCompletionError = error.into(); + + assert!(matches!( + completion_error, + language_model_core::LanguageModelCompletionError::PaymentRequired + )); + } + + #[test] + fn list_models_maps_anthropic_conflict_errors_to_http_conflict() { + let client = FakeHttpClient::create(|_| async move { + Ok(http::Response::builder() + .status(StatusCode::CONFLICT) + .body(AsyncBody::from( + r#"{"type":"error","error":{"type":"conflict_error","message":"The resource was modified concurrently."},"request_id":"request-id"}"#, + )) + .expect("valid response")) + }); + + let error = futures::executor::block_on(list_models( + client.as_ref(), + ANTHROPIC_API_URL, + "test-key", + &CustomHeaders::default(), + )) + .expect_err("conflict should fail"); + let completion_error: language_model_core::LanguageModelCompletionError = error.into(); + + assert!(matches!( + completion_error, + language_model_core::LanguageModelCompletionError::HttpResponseError { + provider, + status_code: StatusCode::CONFLICT, + message, + } if provider == language_model_core::ANTHROPIC_PROVIDER_NAME + && message == "The resource was modified concurrently." + )); + } + + #[test] + fn list_models_maps_anthropic_timeout_errors_to_gateway_timeout() { + let client = FakeHttpClient::create(|_| async move { + Ok(http::Response::builder() + .status(StatusCode::GATEWAY_TIMEOUT) + .body(AsyncBody::from( + r#"{"type":"error","error":{"type":"timeout_error","message":"The request timed out."},"request_id":"request-id"}"#, + )) + .expect("valid response")) + }); + + let error = futures::executor::block_on(list_models( + client.as_ref(), + ANTHROPIC_API_URL, + "test-key", + &CustomHeaders::default(), + )) + .expect_err("timeout should fail"); + let completion_error: language_model_core::LanguageModelCompletionError = error.into(); + + assert!(matches!( + completion_error, + language_model_core::LanguageModelCompletionError::UpstreamProviderError { + message, + status: StatusCode::GATEWAY_TIMEOUT, + retry_after: None, + } if message == "The request timed out." + )); + } + fn listed_entry(id: &str, capabilities: ModelCapabilities) -> ListModelEntry { ListModelEntry { id: id.to_string(),