mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
refactor(anthropic): cleanup — deduplicate diagnostic statics, remove dead cache-options code
This commit is contained in:
parent
dbdb585eed
commit
b9c4f40028
@ -7,8 +7,6 @@ import org.springframework.ai.anthropic.AnthropicChatModel;
|
||||
import org.springframework.ai.anthropic.AnthropicChatOptions;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.ai.anthropic.api.AnthropicCacheOptions;
|
||||
import org.springframework.ai.anthropic.api.AnthropicCacheStrategy;
|
||||
import org.springframework.ai.model.NoopApiKey;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@ -104,17 +102,6 @@ public class AgentClaudeCodeChatModelBuilder implements ChatModelBuilder {
|
||||
// sampling-params handling, thinking-budget mapping, prompt cache.
|
||||
AnthropicChatOptions options = anthropicBuilder.buildAnthropicOptions(model);
|
||||
|
||||
// Enable multi-block system caching so Spring AI serialises system as an
|
||||
// array of content blocks. Anthropic's OAuth anti-abuse gate accepts the
|
||||
// identity prefix as a string ONLY when there is no additional content; as
|
||||
// soon as we append the agent's actual system prompt the gate returns 429.
|
||||
// Two separate array blocks always pass (verified 2026-04-25).
|
||||
AnthropicCacheOptions oauthCacheOptions = AnthropicCacheOptions.builder()
|
||||
.strategy(AnthropicCacheStrategy.SYSTEM_ONLY)
|
||||
.multiBlockSystemCaching(true)
|
||||
.build();
|
||||
options.setCacheOptions(oauthCacheOptions);
|
||||
|
||||
AnthropicChatModel raw = AnthropicChatModel.builder()
|
||||
.anthropicApi(api)
|
||||
.defaultOptions(options)
|
||||
@ -126,7 +113,7 @@ public class AgentClaudeCodeChatModelBuilder implements ChatModelBuilder {
|
||||
// 5xxs requests that don't claim Claude Code identity in the system
|
||||
// prompt — symptom: 429 rate_limit_error with body "Error" on quiet
|
||||
// accounts. See ClaudeCodeIdentityChatModelDecorator javadoc.
|
||||
return new ClaudeCodeIdentityChatModelDecorator(raw, oauthCacheOptions);
|
||||
return new ClaudeCodeIdentityChatModelDecorator(raw);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -2,7 +2,6 @@ package vip.mate.agent.chatmodel;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.anthropic.AnthropicChatOptions;
|
||||
import org.springframework.ai.anthropic.api.AnthropicCacheOptions;
|
||||
import org.springframework.ai.chat.messages.AssistantMessage;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.SystemMessage;
|
||||
@ -76,23 +75,8 @@ public class ClaudeCodeIdentityChatModelDecorator implements ChatModel {
|
||||
|
||||
private final ChatModel delegate;
|
||||
|
||||
/**
|
||||
* Cache options to apply to every transformed prompt so Spring AI serialises
|
||||
* {@code system} as an array of content blocks rather than a plain string.
|
||||
* Anthropic's OAuth anti-abuse gate accepts the string format ONLY when the
|
||||
* content is exactly the identity prefix with nothing else; as soon as we
|
||||
* append the agent's actual system prompt the gate returns 429. Sending the
|
||||
* same two pieces as separate blocks in an array is accepted unconditionally.
|
||||
*/
|
||||
private final AnthropicCacheOptions oauthCacheOptions;
|
||||
|
||||
public ClaudeCodeIdentityChatModelDecorator(ChatModel delegate) {
|
||||
this(delegate, null);
|
||||
}
|
||||
|
||||
public ClaudeCodeIdentityChatModelDecorator(ChatModel delegate, AnthropicCacheOptions oauthCacheOptions) {
|
||||
this.delegate = delegate;
|
||||
this.oauthCacheOptions = oauthCacheOptions;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -176,13 +160,6 @@ public class ClaudeCodeIdentityChatModelDecorator implements ChatModel {
|
||||
}
|
||||
|
||||
AnthropicChatOptions copy = AnthropicChatOptions.fromOptions(anthropicOpts);
|
||||
// Force cacheOptions so Spring AI emits system as an array (multi-block path).
|
||||
// AnthropicChatOptions.fromOptions copies cacheOptions from the runtime options,
|
||||
// which default to DISABLED; that would override defaultOptions at merge time,
|
||||
// so we must override here rather than relying on defaultOptions alone.
|
||||
if (oauthCacheOptions != null) {
|
||||
copy.setCacheOptions(oauthCacheOptions);
|
||||
}
|
||||
if (hasCallbacks) {
|
||||
List<ToolCallback> wrapped = new ArrayList<>(originalCallbacks.size());
|
||||
for (ToolCallback cb : originalCallbacks) {
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
package vip.mate.agent.chatmodel;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
@ -19,17 +17,9 @@ import reactor.core.publisher.Mono;
|
||||
/**
|
||||
* WebClient (streaming) counterpart of {@link ClaudeCodeSystemArrayInterceptor}.
|
||||
*
|
||||
* <p>Collects the full request body from the reactive publisher with
|
||||
* {@code DataBufferUtils.join}, rewrites the {@code system} field from a
|
||||
* plain string to a two-element content-block array, and emits the modified
|
||||
* bytes as a single new {@link DataBuffer}.
|
||||
*
|
||||
* <p>Anthropic's OAuth anti-abuse gate rejects the merged-string format with
|
||||
* a 429; two separate array elements always pass (verified 2026-04-25).
|
||||
* Spring AI's native array path is gated behind {@code @JsonIgnore} cache
|
||||
* options that {@code ModelOptionsUtils.copyToTarget} strips before our
|
||||
* settings can reach {@code buildSystemContent}. This interceptor is immune
|
||||
* to that stripping because it runs at the HTTP transport layer.
|
||||
* <p>Collects the full request body via {@code DataBufferUtils.join}, delegates
|
||||
* the rewrite to {@link ClaudeCodeSystemArrayInterceptor#rewriteSystemField}, and
|
||||
* emits the modified bytes as a single new {@link DataBuffer}.
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@ -44,24 +34,22 @@ class ClaudeCodeSystemArrayExchangeFilter implements ExchangeFilterFunction {
|
||||
new ClientHttpRequestDecorator(outputMessage) {
|
||||
@Override
|
||||
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
|
||||
// Join all chunks so we can parse the complete JSON body.
|
||||
return DataBufferUtils.join(Flux.from(body))
|
||||
.flatMap(joined -> {
|
||||
byte[] original = new byte[joined.readableByteCount()];
|
||||
joined.read(original);
|
||||
DataBufferUtils.release(joined);
|
||||
|
||||
byte[] rewritten = rewriteSystemField(original);
|
||||
byte[] rewritten = ClaudeCodeSystemArrayInterceptor
|
||||
.rewriteSystemField(original, objectMapper);
|
||||
|
||||
// Keep Content-Length consistent if it was set.
|
||||
long declared = getHeaders().getContentLength();
|
||||
if (declared > 0 && declared != rewritten.length) {
|
||||
getHeaders().setContentLength(rewritten.length);
|
||||
}
|
||||
|
||||
DataBuffer newBuf = outputMessage.bufferFactory()
|
||||
.wrap(rewritten);
|
||||
return super.writeWith(Mono.just(newBuf));
|
||||
return super.writeWith(Mono.just(
|
||||
outputMessage.bufferFactory().wrap(rewritten)));
|
||||
});
|
||||
}
|
||||
}, context))
|
||||
@ -69,23 +57,4 @@ class ClaudeCodeSystemArrayExchangeFilter implements ExchangeFilterFunction {
|
||||
|
||||
return next.exchange(intercepted);
|
||||
}
|
||||
|
||||
private byte[] rewriteSystemField(byte[] body) {
|
||||
if (body == null || body.length == 0) return body;
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(body);
|
||||
if (!root.isObject()) return body;
|
||||
JsonNode systemNode = root.get("system");
|
||||
if (systemNode == null || !systemNode.isTextual()) return body;
|
||||
byte[] rewritten = objectMapper.writeValueAsBytes(
|
||||
ClaudeCodeSystemArrayInterceptor.buildRewritten(
|
||||
(ObjectNode) root, systemNode.asText()));
|
||||
log.debug("[ClaudeCodeSystem] rewrote system field from string to array ({} → {} bytes)",
|
||||
body.length, rewritten.length);
|
||||
return rewritten;
|
||||
} catch (Exception e) {
|
||||
log.warn("[ClaudeCodeSystem] body rewrite failed, sending original: {}", e.getMessage());
|
||||
return body;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,15 +19,13 @@ import java.io.IOException;
|
||||
* the wire.
|
||||
*
|
||||
* <p>Anthropic's OAuth anti-abuse gate accepts the Claude Code identity prefix
|
||||
* as a string ONLY when it is the sole content. The moment we append the
|
||||
* agent's actual system prompt the gate returns 429. Sending the same two
|
||||
* pieces as separate array elements always passes (verified 2026-04-25).
|
||||
* as a string ONLY when it is the sole content. Appending any additional text
|
||||
* triggers a 429; two separate array elements always pass (verified 2026-04-25).
|
||||
*
|
||||
* <p>Spring AI's {@code AnthropicChatModel} can emit array format natively
|
||||
* via prompt caching, but {@code ModelOptionsUtils.copyToTarget} (Jackson-
|
||||
* based) drops the {@code @JsonIgnore cacheOptions} field, making it
|
||||
* impossible to activate through the normal options path. This interceptor
|
||||
* works at the HTTP layer and is immune to Spring AI's internal option-merging.
|
||||
* <p>Spring AI's native array path is guarded by {@code @JsonIgnore cacheOptions}
|
||||
* which {@code ModelOptionsUtils.copyToTarget} drops before our settings can
|
||||
* reach {@code buildSystemContent}. This interceptor bypasses that by rewriting
|
||||
* at the HTTP transport layer.
|
||||
*
|
||||
* <p>Sync (RestClient) variant; the WebFlux equivalent is
|
||||
* {@link ClaudeCodeSystemArrayExchangeFilter}.
|
||||
@ -41,7 +39,7 @@ class ClaudeCodeSystemArrayInterceptor implements ClientHttpRequestInterceptor {
|
||||
@Override
|
||||
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
|
||||
ClientHttpRequestExecution execution) throws IOException {
|
||||
return execution.execute(request, rewriteSystemField(body));
|
||||
return execution.execute(request, rewriteSystemField(body, objectMapper));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -51,17 +49,17 @@ class ClaudeCodeSystemArrayInterceptor implements ClientHttpRequestInterceptor {
|
||||
* [ {"type":"text","text":"You are Claude Code..."}, {"type":"text","text":"<rest>"} ]
|
||||
* </pre>
|
||||
* Returns {@code body} unchanged on any error or if rewrite is not needed.
|
||||
* Package-private static so {@link ClaudeCodeSystemArrayExchangeFilter} can reuse.
|
||||
*/
|
||||
byte[] rewriteSystemField(byte[] body) {
|
||||
static byte[] rewriteSystemField(byte[] body, ObjectMapper mapper) {
|
||||
if (body == null || body.length == 0) return body;
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(body);
|
||||
JsonNode root = mapper.readTree(body);
|
||||
if (!root.isObject()) return body;
|
||||
JsonNode systemNode = root.get("system");
|
||||
if (systemNode == null || !systemNode.isTextual()) return body; // absent or already array
|
||||
byte[] rewritten = objectMapper.writeValueAsBytes(
|
||||
buildRewritten((ObjectNode) root, systemNode.asText()));
|
||||
log.debug("[ClaudeCodeSystem] rewrote system field from string to array ({} → {} bytes)",
|
||||
if (systemNode == null || !systemNode.isTextual()) return body;
|
||||
byte[] rewritten = mapper.writeValueAsBytes(buildRewritten((ObjectNode) root, systemNode.asText()));
|
||||
log.debug("[ClaudeCodeSystem] rewrote system field to array ({} → {} bytes)",
|
||||
body.length, rewritten.length);
|
||||
return rewritten;
|
||||
} catch (Exception e) {
|
||||
@ -79,7 +77,6 @@ class ClaudeCodeSystemArrayInterceptor implements ClientHttpRequestInterceptor {
|
||||
identityBlock.put("text", identity);
|
||||
arr.add(identityBlock);
|
||||
|
||||
// Strip the identity prefix and leading newlines to get the rest.
|
||||
if (!systemText.equals(identity) && systemText.startsWith(identity)) {
|
||||
String rest = systemText.substring(identity.length()).replaceFirst("^\n+", "");
|
||||
if (!rest.isBlank()) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user