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