Log Anthropic rate limit context
This commit is contained in:
@@ -23,6 +23,7 @@ pub enum ApiError {
|
||||
request_id: Option<String>,
|
||||
retry_after: Option<Duration>,
|
||||
rate_limit_reset_after: Option<Duration>,
|
||||
rate_limit_summary: Option<String>,
|
||||
},
|
||||
RetriesExhausted {
|
||||
attempts: u32,
|
||||
@@ -100,15 +101,30 @@ impl Display for ApiError {
|
||||
request_id,
|
||||
retry_after,
|
||||
rate_limit_reset_after,
|
||||
rate_limit_summary,
|
||||
..
|
||||
} => match (error_type, message) {
|
||||
(Some(error_type), Some(message)) => {
|
||||
write!(f, "api returned {status} ({error_type}): {message}")?;
|
||||
write_api_error_hints(f, *status, request_id, *retry_after, *rate_limit_reset_after)
|
||||
write_api_error_hints(
|
||||
f,
|
||||
*status,
|
||||
request_id,
|
||||
*retry_after,
|
||||
*rate_limit_reset_after,
|
||||
rate_limit_summary.as_deref(),
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
write!(f, "api returned {status}: {body}")?;
|
||||
write_api_error_hints(f, *status, request_id, *retry_after, *rate_limit_reset_after)
|
||||
write_api_error_hints(
|
||||
f,
|
||||
*status,
|
||||
request_id,
|
||||
*retry_after,
|
||||
*rate_limit_reset_after,
|
||||
rate_limit_summary.as_deref(),
|
||||
)
|
||||
}
|
||||
},
|
||||
Self::RetriesExhausted {
|
||||
@@ -135,6 +151,7 @@ fn write_api_error_hints(
|
||||
request_id: &Option<String>,
|
||||
retry_after: Option<Duration>,
|
||||
rate_limit_reset_after: Option<Duration>,
|
||||
rate_limit_summary: Option<&str>,
|
||||
) -> std::fmt::Result {
|
||||
if let Some(request_id) = request_id {
|
||||
write!(f, " [request id {request_id}]")?;
|
||||
@@ -145,6 +162,9 @@ fn write_api_error_hints(
|
||||
} else if let Some(reset_after) = rate_limit_reset_after {
|
||||
write!(f, " [quota resets in ~{}s]", reset_after.as_secs())?;
|
||||
}
|
||||
if let Some(rate_limit_summary) = rate_limit_summary {
|
||||
write!(f, " [rate limits {rate_limit_summary}]")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -26,6 +26,23 @@ const RETRY_AFTER_HEADER: &str = "retry-after";
|
||||
const RATE_LIMIT_RESET_HEADER: &str = "anthropic-ratelimit-unified-reset";
|
||||
const RATE_LIMIT_5H_RESET_HEADER: &str = "anthropic-ratelimit-unified-5h-reset";
|
||||
const RATE_LIMIT_7D_RESET_HEADER: &str = "anthropic-ratelimit-unified-7d-reset";
|
||||
const RATE_LIMIT_SUMMARY_HEADERS: &[&str] = &[
|
||||
"anthropic-ratelimit-unified-status",
|
||||
"anthropic-ratelimit-unified-5h-status",
|
||||
"anthropic-ratelimit-unified-5h-utilization",
|
||||
"anthropic-ratelimit-unified-5h-reset",
|
||||
"anthropic-ratelimit-unified-7d-status",
|
||||
"anthropic-ratelimit-unified-7d-utilization",
|
||||
"anthropic-ratelimit-unified-7d-reset",
|
||||
"anthropic-ratelimit-unified-7d_sonnet-status",
|
||||
"anthropic-ratelimit-unified-7d_sonnet-utilization",
|
||||
"anthropic-ratelimit-unified-7d_sonnet-reset",
|
||||
"anthropic-ratelimit-unified-overage-status",
|
||||
"anthropic-ratelimit-unified-overage-utilization",
|
||||
"anthropic-ratelimit-unified-overage-reset",
|
||||
"anthropic-ratelimit-unified-representative-claim",
|
||||
"anthropic-ratelimit-unified-fallback-percentage",
|
||||
];
|
||||
const DEFAULT_INITIAL_BACKOFF: Duration = Duration::from_millis(200);
|
||||
const DEFAULT_MAX_BACKOFF: Duration = Duration::from_secs(2);
|
||||
const DEFAULT_MAX_RETRIES: u32 = 2;
|
||||
@@ -838,6 +855,7 @@ async fn expect_success(response: reqwest::Response) -> Result<reqwest::Response
|
||||
let request_id = request_id_from_headers(response.headers());
|
||||
let retry_after = retry_after_from_headers(response.headers());
|
||||
let rate_limit_reset_after = rate_limit_reset_after_from_headers(response.headers());
|
||||
let rate_limit_summary = rate_limit_summary_from_headers(response.headers());
|
||||
let body = response.text().await.unwrap_or_else(|_| String::new());
|
||||
let parsed_error = serde_json::from_str::<AnthropicErrorEnvelope>(&body).ok();
|
||||
let retryable = is_retryable_status(status);
|
||||
@@ -855,6 +873,7 @@ async fn expect_success(response: reqwest::Response) -> Result<reqwest::Response
|
||||
request_id,
|
||||
retry_after,
|
||||
rate_limit_reset_after,
|
||||
rate_limit_summary,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -883,6 +902,23 @@ fn rate_limit_reset_after_from_headers(headers: &HeaderMap) -> Option<Duration>
|
||||
.map(|epoch| Duration::from_secs(epoch - now))
|
||||
}
|
||||
|
||||
fn rate_limit_summary_from_headers(headers: &HeaderMap) -> Option<String> {
|
||||
let parts = RATE_LIMIT_SUMMARY_HEADERS
|
||||
.iter()
|
||||
.filter_map(|name| {
|
||||
headers
|
||||
.get(*name)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.map(|value| format!("{name}={value}"))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if parts.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(parts.join(", "))
|
||||
}
|
||||
}
|
||||
|
||||
const fn is_retryable_status(status: reqwest::StatusCode) -> bool {
|
||||
matches!(status.as_u16(), 408 | 409 | 429 | 500 | 502 | 503 | 504)
|
||||
}
|
||||
@@ -1320,6 +1356,25 @@ mod tests {
|
||||
assert!(reset_after.as_secs() <= 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rate_limit_summary_collects_present_headers() {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert(
|
||||
"anthropic-ratelimit-unified-status",
|
||||
"limited".parse().expect("header"),
|
||||
);
|
||||
headers.insert(
|
||||
"anthropic-ratelimit-unified-representative-claim",
|
||||
"five_hour".parse().expect("header"),
|
||||
);
|
||||
let summary =
|
||||
super::rate_limit_summary_from_headers(&headers).expect("summary should exist");
|
||||
assert!(summary.contains("anthropic-ratelimit-unified-status=limited"));
|
||||
assert!(summary.contains(
|
||||
"anthropic-ratelimit-unified-representative-claim=five_hour"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_source_applies_headers() {
|
||||
let auth = AuthSource::ApiKeyAndBearer {
|
||||
|
||||
@@ -921,6 +921,7 @@ async fn expect_success(response: reqwest::Response) -> Result<reqwest::Response
|
||||
request_id: None,
|
||||
retry_after: None,
|
||||
rate_limit_reset_after: None,
|
||||
rate_limit_summary: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -270,6 +270,8 @@ async fn post_turn(
|
||||
let runtime = state.runtime.clone();
|
||||
let cancel_flag = turn_state.cancel_flag.clone();
|
||||
let prompt = request.prompt.clone();
|
||||
let profile_id = state.config.profile_id.clone();
|
||||
let model = state.config.model.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
if let Err(error) = runtime.run_turn(
|
||||
&session_path,
|
||||
@@ -278,6 +280,11 @@ async fn post_turn(
|
||||
cancel_flag,
|
||||
runtime_tx.clone(),
|
||||
) {
|
||||
eprintln!(
|
||||
"worker turn failed: profile_id={} model={} error={error}",
|
||||
profile_id,
|
||||
model,
|
||||
);
|
||||
let _ = runtime_tx.send(RuntimeEvent::Failed {
|
||||
message: error.to_string(),
|
||||
});
|
||||
|
||||
@@ -588,6 +588,11 @@ impl TelegramGateway {
|
||||
break;
|
||||
}
|
||||
WorkerTurnEvent::Failed { message } => {
|
||||
eprintln!(
|
||||
"gateway turn failed: profile_id={} chat_id={} error={message}",
|
||||
profile_id,
|
||||
chat_id,
|
||||
);
|
||||
if let Some(message_handle) = &mut status_message {
|
||||
let text = format!("Request failed: {message}");
|
||||
let _ = api
|
||||
|
||||
Reference in New Issue
Block a user