mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(anthropic): log anthropic-ratelimit-* headers on 429
This commit is contained in:
parent
aabf2b8c32
commit
84cb442446
@ -135,14 +135,21 @@ public class AgentClaudeCodeChatModelBuilder implements ChatModelBuilder {
|
||||
.defaultHeader(HttpHeaders.USER_AGENT, userAgent)
|
||||
.defaultHeader(HttpHeaders.ACCEPT, "application/json")
|
||||
.defaultHeader("anthropic-dangerous-direct-browser-access", "true")
|
||||
.defaultHeader("x-app", xApp);
|
||||
.defaultHeader("x-app", xApp)
|
||||
// Diagnostic: log Anthropic's rate-limit headers on 429 so we
|
||||
// can tell apart "5h Pro quota exhausted" (tokens-remaining=0,
|
||||
// retry-after huge) from "anti-abuse gate" (tokens-remaining
|
||||
// large, retry-after small) from "burst limit hit" without
|
||||
// staring at SDK internals.
|
||||
.requestInterceptor(new RateLimitDiagnosticInterceptor());
|
||||
|
||||
WebClient.Builder webClientBuilder = webClientBuilderProvider.getIfAvailable(WebClient::builder)
|
||||
.defaultHeader(HttpHeaders.AUTHORIZATION, authHeader)
|
||||
.defaultHeader(HttpHeaders.USER_AGENT, userAgent)
|
||||
.defaultHeader(HttpHeaders.ACCEPT, "application/json")
|
||||
.defaultHeader("anthropic-dangerous-direct-browser-access", "true")
|
||||
.defaultHeader("x-app", xApp);
|
||||
.defaultHeader("x-app", xApp)
|
||||
.filter(new RateLimitDiagnosticExchangeFilter());
|
||||
|
||||
// NoopApiKey.getValue() returns "" → Spring AI's addDefaultHeadersIfMissing
|
||||
// skips x-api-key. The Builder.build() Assert.notNull on apiKey still
|
||||
|
||||
@ -0,0 +1,66 @@
|
||||
package vip.mate.agent.chatmodel;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
|
||||
import org.springframework.web.reactive.function.client.ExchangeFunction;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* RFC-062 diagnostic helper for the streaming (WebClient) path. Mirrors
|
||||
* {@link RateLimitDiagnosticInterceptor} for non-streaming RestClient calls —
|
||||
* see that class's javadoc for how to interpret the logged headers.
|
||||
*
|
||||
* <p>Stream calls are where most chat traffic flows, so without this filter
|
||||
* we wouldn't see rate-limit metadata for the most common 429 path.
|
||||
*/
|
||||
@Slf4j
|
||||
class RateLimitDiagnosticExchangeFilter implements ExchangeFilterFunction {
|
||||
|
||||
/** Anthropic's documented rate-limit response headers. */
|
||||
private static final List<String> RATE_LIMIT_HEADERS = List.of(
|
||||
"anthropic-ratelimit-requests-limit",
|
||||
"anthropic-ratelimit-requests-remaining",
|
||||
"anthropic-ratelimit-requests-reset",
|
||||
"anthropic-ratelimit-tokens-limit",
|
||||
"anthropic-ratelimit-tokens-remaining",
|
||||
"anthropic-ratelimit-tokens-reset",
|
||||
"anthropic-ratelimit-input-tokens-limit",
|
||||
"anthropic-ratelimit-input-tokens-remaining",
|
||||
"anthropic-ratelimit-input-tokens-reset",
|
||||
"anthropic-ratelimit-output-tokens-limit",
|
||||
"anthropic-ratelimit-output-tokens-remaining",
|
||||
"anthropic-ratelimit-output-tokens-reset",
|
||||
"retry-after");
|
||||
|
||||
@Override
|
||||
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
|
||||
return next.exchange(request).doOnNext(response -> {
|
||||
if (response.statusCode().value() == 429) {
|
||||
logHeaders(response.headers().asHttpHeaders());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void logHeaders(HttpHeaders headers) {
|
||||
StringBuilder sb = new StringBuilder("[Anthropic 429] rate-limit headers: ");
|
||||
boolean any = false;
|
||||
for (String name : RATE_LIMIT_HEADERS) {
|
||||
String value = headers.getFirst(name);
|
||||
if (value != null) {
|
||||
if (any) sb.append(", ");
|
||||
sb.append(name).append('=').append(value);
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
if (!any) {
|
||||
log.warn("[Anthropic 429] no rate-limit headers present — likely anti-abuse gate, not real quota");
|
||||
} else {
|
||||
log.warn(sb.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
package vip.mate.agent.chatmodel;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestExecution;
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* RFC-062 diagnostic helper: when Anthropic returns 429 we want to see the
|
||||
* {@code anthropic-ratelimit-*} headers in the log so we can tell apart the
|
||||
* three distinct "rate limit" failure modes that all share the same
|
||||
* {@code {"type":"rate_limit_error","message":"Error"}} body:
|
||||
*
|
||||
* <table>
|
||||
* <caption>How to read the headers</caption>
|
||||
* <tr><th>Failure mode</th><th>tokens-remaining</th><th>retry-after</th></tr>
|
||||
* <tr><td>5h Pro/Max quota exhausted</td><td>0</td><td>thousands of seconds</td></tr>
|
||||
* <tr><td>Anti-abuse fingerprint gate</td><td>large</td><td>tens of seconds</td></tr>
|
||||
* <tr><td>Per-minute burst limit</td><td>large</td><td>single-digit seconds</td></tr>
|
||||
* </table>
|
||||
*
|
||||
* <p>Spring AI logs the body but not the headers, so we'd be debugging blind
|
||||
* without this. Sync (RestClient) variant; the WebFlux equivalent lives in
|
||||
* {@link RateLimitDiagnosticExchangeFilter}.
|
||||
*/
|
||||
@Slf4j
|
||||
class RateLimitDiagnosticInterceptor implements ClientHttpRequestInterceptor {
|
||||
|
||||
/** Anthropic's documented rate-limit response headers. */
|
||||
private static final List<String> RATE_LIMIT_HEADERS = List.of(
|
||||
"anthropic-ratelimit-requests-limit",
|
||||
"anthropic-ratelimit-requests-remaining",
|
||||
"anthropic-ratelimit-requests-reset",
|
||||
"anthropic-ratelimit-tokens-limit",
|
||||
"anthropic-ratelimit-tokens-remaining",
|
||||
"anthropic-ratelimit-tokens-reset",
|
||||
"anthropic-ratelimit-input-tokens-limit",
|
||||
"anthropic-ratelimit-input-tokens-remaining",
|
||||
"anthropic-ratelimit-input-tokens-reset",
|
||||
"anthropic-ratelimit-output-tokens-limit",
|
||||
"anthropic-ratelimit-output-tokens-remaining",
|
||||
"anthropic-ratelimit-output-tokens-reset",
|
||||
"retry-after");
|
||||
|
||||
@Override
|
||||
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
|
||||
ClientHttpRequestExecution execution) throws IOException {
|
||||
ClientHttpResponse response = execution.execute(request, body);
|
||||
if (response.getStatusCode().value() == 429) {
|
||||
logHeaders(response.getHeaders());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
private static void logHeaders(HttpHeaders headers) {
|
||||
StringBuilder sb = new StringBuilder("[Anthropic 429] rate-limit headers: ");
|
||||
boolean any = false;
|
||||
for (String name : RATE_LIMIT_HEADERS) {
|
||||
String value = headers.getFirst(name);
|
||||
if (value != null) {
|
||||
if (any) sb.append(", ");
|
||||
sb.append(name).append('=').append(value);
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
if (!any) {
|
||||
// Anthropic still returned 429 but didn't include any rate-limit
|
||||
// headers — strong signal of the anti-abuse path (it generally
|
||||
// doesn't bother filling them in).
|
||||
log.warn("[Anthropic 429] no rate-limit headers present — likely anti-abuse gate, not real quota");
|
||||
} else {
|
||||
log.warn(sb.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user