anthropic: Handle additional API error codes (#62984)
Anthropic now returns distinct error types for billing failures, request conflicts, and gateway timeouts. Because the Anthropic client did not recognize these values, they fell through to `LanguageModelCompletionError::Other`, losing their structured status and causing callers to apply generic handling. Recognize the documented `billing_error`, `conflict_error`, and `timeout_error` codes. Billing failures now become `PaymentRequired`, conflicts preserve their HTTP 409 status and provider message, and timeouts become typed upstream HTTP 504 failures. End-to-end tests exercise each mapping through the HTTP response parsing path. Testing: - `cargo fmt --check` - `cargo nextest run -p anthropic` - `./script/clippy -p anthropic` - `git diff --check` Release Notes: - Fixed handling of Anthropic billing, conflict, and timeout errors.
This commit is contained in:
1 parent
bf2d51bcfd
commit
0ac96c12fc
1 file changed
+104
@@ -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(),
|
||||
|
||||
Reference in new issue
Block a user