fix(anthropic): drop (external, cli) UA suffix — it's the anti-abuse fingerprint

This commit is contained in:
matevip 2026-04-26 08:34:11 +08:00
parent 84cb442446
commit ed3ff54f0c
3 changed files with 21 additions and 9 deletions

View File

@ -38,9 +38,10 @@ import vip.mate.llm.model.ModelProviderEntity;
* {@code oauth-2025-04-20} or Anthropic's edge intermittently 500s.
* We push these via {@link AnthropicApi.Builder#anthropicBetaFeatures}
* so Spring AI's existing header-merging logic still applies.</li>
* <li>{@code User-Agent: claude-cli/<ver> (external, cli)} and
* {@code x-app: cli} masquerade as the Claude Code CLI Anthropic
* rejects unrecognised UAs on Bearer-auth requests with HTTP 400.</li>
* <li>{@code User-Agent: claude-cli/<ver>} (bare no suffix) and
* {@code x-app: cli} masquerade as the Claude Code CLI. Suffix variants
* like {@code (external, cli)} are anti-abuse fingerprints; see
* {@link ClaudeCodeApiHeaders#userAgent()}.</li>
* </ol>
*
* <h2>Token lifecycle</h2>

View File

@ -20,7 +20,7 @@ import java.util.List;
* <caption>Header set sent on OAuth requests</caption>
* <tr><th>Header</th><th>Value</th><th>Why</th></tr>
* <tr><td>{@code Authorization}</td><td>{@code Bearer <accessToken>}</td><td>OAuth path uses Bearer; non-OAuth uses {@code x-api-key}</td></tr>
* <tr><td>{@code User-Agent}</td><td>{@code claude-cli/<version> (external, cli)}</td><td>Anthropic routes OAuth by UA; spoof identity</td></tr>
* <tr><td>{@code User-Agent}</td><td>{@code claude-cli/<version>}</td><td>bare form suffix triggers anti-abuse fingerprint, see {@link #userAgent()}</td></tr>
* <tr><td>{@code x-app}</td><td>{@code cli}</td><td>Claude Code identity flag</td></tr>
* <tr><td>{@code anthropic-beta}</td><td>(comma-joined list see {@link #allBetas()})</td><td>OAuth-only + common feature betas</td></tr>
* </table>
@ -58,12 +58,20 @@ public class ClaudeCodeApiHeaders {
/**
* User-Agent string Anthropic OAuth infrastructure expects.
* Format: {@code claude-cli/<version> (external, cli)}.
* The {@code (external, cli)} suffix is the canonical hermes / OpenCode /
* Cline identity drop it and Anthropic returns 400.
* Format: {@code claude-cli/<version>} bare, no suffix.
*
* <p><b>History note:</b> we previously appended {@code (external, cli)}
* after hermes-agent's pattern. That turned out to be wrong: Anthropic's
* anti-abuse gate uses the suffix to fingerprint third-party clients
* (hermes / OpenCode / Cline) and rate-limits them harder. Real Claude
* Code (Electron + Node + official Anthropic JS SDK) emits the bare
* {@code claude-cli/<v>} form, which is what openclaw
* ({@code anthropic-transport-stream.ts:30,572}) also uses. Verified by
* reproducing 429 with the suffix and {@code anthropic-ratelimit-*}
* headers absent the diagnostic signature of the anti-abuse path.
*/
public String userAgent() {
return "claude-cli/" + versionDetector.get() + " (external, cli)";
return "claude-cli/" + versionDetector.get();
}
/** {@code x-app} header value. Constant. */

View File

@ -80,8 +80,11 @@ public class ClaudeCodeTokenRefresher {
.uri(endpoint)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
// Bare UA see ClaudeCodeApiHeaders.userAgent() javadoc
// for why the (external, cli) suffix would trip Anthropic's
// anti-abuse fingerprint.
.header(HttpHeaders.USER_AGENT,
"claude-cli/" + versionDetector.get() + " (external, cli)")
"claude-cli/" + versionDetector.get())
.body(body)
.retrieve()
.body(String.class);