diff --git a/mateclaw-server/src/main/java/vip/mate/agent/chatmodel/ClaudeCodeIdentityChatModelDecorator.java b/mateclaw-server/src/main/java/vip/mate/agent/chatmodel/ClaudeCodeIdentityChatModelDecorator.java
index 5c90f095..9d64d422 100644
--- a/mateclaw-server/src/main/java/vip/mate/agent/chatmodel/ClaudeCodeIdentityChatModelDecorator.java
+++ b/mateclaw-server/src/main/java/vip/mate/agent/chatmodel/ClaudeCodeIdentityChatModelDecorator.java
@@ -1,50 +1,67 @@
package vip.mate.agent.chatmodel;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.anthropic.AnthropicChatOptions;
+import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
+import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.chat.model.ToolContext;
+import org.springframework.ai.tool.ToolCallback;
+import org.springframework.ai.tool.definition.DefaultToolDefinition;
+import org.springframework.ai.tool.definition.ToolDefinition;
+import org.springframework.ai.tool.metadata.ToolMetadata;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
+import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Set;
/**
* RFC-062: Claude Code OAuth identity transform applied to every Anthropic
* request when the underlying auth is a Claude Code OAuth token.
*
*
Anthropic's OAuth edge enforces an anti-abuse path that rate-limits
- * (and intermittently 5xxs) requests whose system prompt does NOT claim
- * Claude Code identity. Symptoms include:
+ * (and intermittently 5xxs) requests claiming Claude Code identity but
+ * shaped differently from real Claude Code traffic. Symptoms:
*
*
* - HTTP 429 with {@code rate_limit_error} on quiet accounts that haven't
- * come close to their token budget — the give-away is a body of just
+ * come close to their token budget — give-away is a body of just
* {@code {"type":"error","error":{"type":"rate_limit_error","message":"Error"}}}
- * (genuine quota exhaustion includes a descriptive message).
+ * (genuine quota exhaustion carries a descriptive message).
* - Sporadic 500s on the first call after a long idle period.
*
*
* Reference: hermes-agent {@code anthropic_adapter._build_anthropic_messages_request}
- * lines 1571-1607 — applies the same transforms unconditionally on
+ * lines 1571-1607 — same transforms applied unconditionally on
* {@code is_oauth=True} requests.
*
*
Transforms applied per call
*
- * - Prepend {@code "You are Claude Code, Anthropic's official CLI for Claude."}
- * to the system prompt. If no system message is present we insert one.
- * - Scrub MateClaw branding from system text — replace
- * {@code "MateClaw"} → {@code "Claude Code"} and a few common variants —
- * so server-side content filters don't fire on the spoofed identity.
+ * - System prompt prefix: prepend
+ * {@code "You are Claude Code, Anthropic's official CLI for Claude."}.
+ * Insert a new SystemMessage if none exists.
+ * - Brand scrub: replace {@code "MateClaw"}/{@code "mateclaw"}
+ * in system text with their Claude Code equivalents — Anthropic's
+ * content filter flags identity contradictions.
+ * - Tool {@code mcp_} prefix (outgoing): every tool definition
+ * sent to Anthropic is renamed {@code mcp_} — Claude Code
+ * runs all tools through MCP servers, so real Claude Code traffic
+ * always has the prefix. Mismatch trips anti-abuse.
+ * - History tool_use prefix: previously-issued tool calls in
+ * AssistantMessage history get the prefix re-applied (we strip on
+ * response, so they're stored unprefixed).
+ * - Tool {@code mcp_} prefix (incoming): ChatResponse tool_use
+ * names are stripped of the {@code mcp_} prefix so MateClaw's tool
+ * registry can resolve them.
*
- *
- * Tool-name {@code mcp_} prefix (hermes lines 1593-1607) is a
- * separate concern: it requires bidirectional rewriting (out + back) and
- * touches Spring AI's tool-callback layer. Deferred to a follow-up — current
- * symptom is rate-limit on memory analysis (which uses no tools).
*/
@Slf4j
public class ClaudeCodeIdentityChatModelDecorator implements ChatModel {
@@ -53,6 +70,9 @@ public class ClaudeCodeIdentityChatModelDecorator implements ChatModel {
static final String CLAUDE_CODE_SYSTEM_PREFIX =
"You are Claude Code, Anthropic's official CLI for Claude.";
+ /** Tool-name prefix Claude Code uses for all MCP-routed tools. */
+ static final String MCP_TOOL_PREFIX = "mcp_";
+
private final ChatModel delegate;
public ClaudeCodeIdentityChatModelDecorator(ChatModel delegate) {
@@ -61,12 +81,12 @@ public class ClaudeCodeIdentityChatModelDecorator implements ChatModel {
@Override
public ChatResponse call(Prompt prompt) {
- return delegate.call(transform(prompt));
+ return stripToolPrefixes(delegate.call(transform(prompt)));
}
@Override
public Flux stream(Prompt prompt) {
- return delegate.stream(transform(prompt));
+ return delegate.stream(transform(prompt)).map(this::stripToolPrefixes);
}
@Override
@@ -74,6 +94,10 @@ public class ClaudeCodeIdentityChatModelDecorator implements ChatModel {
return delegate.getDefaultOptions();
}
+ /* ====================================================================== */
+ /* Outbound transform: Prompt → Prompt with identity + tool prefix */
+ /* ====================================================================== */
+
/**
* Build a new {@link Prompt} with the OAuth identity transforms applied.
* Package-private for unit tests.
@@ -88,40 +112,145 @@ public class ClaudeCodeIdentityChatModelDecorator implements ChatModel {
boolean systemSeen = false;
for (Message msg : source) {
if (msg instanceof SystemMessage sm && !systemSeen) {
- // Only mutate the FIRST system message — multiple system messages
- // are rare in practice but preserve the second-onward verbatim.
rewritten.add(new SystemMessage(prependIdentity(sanitizeBranding(sm.getText()))));
systemSeen = true;
+ } else if (msg instanceof AssistantMessage am && am.hasToolCalls()) {
+ // Re-prefix tool_use names in history. We strip on response, so
+ // by the time MateClaw stores the AssistantMessage the names
+ // are unprefixed — must put the prefix back when echoing the
+ // history to Anthropic for it to match its own prior turn.
+ rewritten.add(rebuildAssistantMessage(am, true));
} else {
rewritten.add(msg);
}
}
if (!systemSeen) {
- // No system message at all → insert one with just the identity prefix.
rewritten.add(0, new SystemMessage(CLAUDE_CODE_SYSTEM_PREFIX));
}
- return new Prompt(rewritten, original.getOptions());
+
+ ChatOptions transformedOptions = transformOptions(original.getOptions());
+ return new Prompt(rewritten, transformedOptions);
}
+ /**
+ * Wrap each tool callback in the options so its {@code getToolDefinition().name()}
+ * returns {@code mcp_}. Spring AI sends those names verbatim to Anthropic.
+ * Other tool fields (description, schema) untouched. Returns {@code null} for
+ * non-Anthropic options so we don't accidentally drop them on a custom subclass.
+ */
+ private ChatOptions transformOptions(ChatOptions options) {
+ if (!(options instanceof AnthropicChatOptions anthropicOpts)) {
+ return options;
+ }
+ List originalCallbacks = anthropicOpts.getToolCallbacks();
+ Set originalToolNames = anthropicOpts.getToolNames();
+
+ boolean hasCallbacks = originalCallbacks != null && !originalCallbacks.isEmpty();
+ boolean hasToolNames = originalToolNames != null && !originalToolNames.isEmpty();
+ if (!hasCallbacks && !hasToolNames) {
+ return options;
+ }
+
+ AnthropicChatOptions copy = AnthropicChatOptions.fromOptions(anthropicOpts);
+ if (hasCallbacks) {
+ List wrapped = new ArrayList<>(originalCallbacks.size());
+ for (ToolCallback cb : originalCallbacks) {
+ wrapped.add(cb instanceof PrefixedToolCallback ? cb : new PrefixedToolCallback(cb));
+ }
+ copy.setToolCallbacks(wrapped);
+ }
+ if (hasToolNames) {
+ // toolNames is a set used by Spring AI's tool resolver to filter from
+ // a wider registry. If MateClaw populates it (most paths use callbacks
+ // directly so this is rare), prefix the names so they line up with
+ // the wrapped callbacks above.
+ Set prefixed = new LinkedHashSet<>(originalToolNames.size());
+ for (String n : originalToolNames) {
+ prefixed.add(n.startsWith(MCP_TOOL_PREFIX) ? n : MCP_TOOL_PREFIX + n);
+ }
+ copy.setToolNames(prefixed);
+ }
+ return copy;
+ }
+
+ /* ====================================================================== */
+ /* Inbound transform: ChatResponse → strip tool prefix */
+ /* ====================================================================== */
+
+ ChatResponse stripToolPrefixes(ChatResponse response) {
+ if (response == null) {
+ return null;
+ }
+ List origGens = response.getResults();
+ if (origGens == null || origGens.isEmpty()) {
+ return response;
+ }
+ List rewritten = null;
+ for (int i = 0; i < origGens.size(); i++) {
+ Generation g = origGens.get(i);
+ AssistantMessage am = g.getOutput();
+ if (am == null || !am.hasToolCalls()) continue;
+ boolean changed = false;
+ for (AssistantMessage.ToolCall tc : am.getToolCalls()) {
+ if (tc.name() != null && tc.name().startsWith(MCP_TOOL_PREFIX)) {
+ changed = true;
+ break;
+ }
+ }
+ if (!changed) continue;
+
+ if (rewritten == null) {
+ rewritten = new ArrayList<>(origGens);
+ }
+ AssistantMessage stripped = rebuildAssistantMessage(am, false);
+ ChatGenerationMetadata meta = g.getMetadata();
+ rewritten.set(i, new Generation(stripped, meta));
+ }
+ if (rewritten == null) {
+ return response; // no tool_use blocks needed rewriting
+ }
+ return new ChatResponse(rewritten, response.getMetadata());
+ }
+
+ /**
+ * Rebuild an AssistantMessage with tool_call names prefixed (when
+ * {@code prefix=true}) or stripped (when {@code prefix=false}).
+ */
+ private AssistantMessage rebuildAssistantMessage(AssistantMessage original, boolean prefix) {
+ List rebuilt = new ArrayList<>(original.getToolCalls().size());
+ for (AssistantMessage.ToolCall tc : original.getToolCalls()) {
+ String name = tc.name();
+ String newName;
+ if (prefix) {
+ newName = (name == null || name.startsWith(MCP_TOOL_PREFIX)) ? name : MCP_TOOL_PREFIX + name;
+ } else {
+ newName = (name != null && name.startsWith(MCP_TOOL_PREFIX))
+ ? name.substring(MCP_TOOL_PREFIX.length()) : name;
+ }
+ rebuilt.add(new AssistantMessage.ToolCall(tc.id(), tc.type(), newName, tc.arguments()));
+ }
+ return AssistantMessage.builder()
+ .content(original.getText())
+ .properties(original.getMetadata())
+ .toolCalls(rebuilt)
+ .media(original.getMedia())
+ .build();
+ }
+
+ /* ====================================================================== */
+ /* String helpers (system prompt + branding) */
+ /* ====================================================================== */
+
private static String prependIdentity(String existingSystem) {
if (existingSystem == null || existingSystem.isBlank()) {
return CLAUDE_CODE_SYSTEM_PREFIX;
}
if (existingSystem.startsWith(CLAUDE_CODE_SYSTEM_PREFIX)) {
- // Already prefixed (defensive — protects against double-wrapping
- // if a caller invokes the decorator twice).
return existingSystem;
}
return CLAUDE_CODE_SYSTEM_PREFIX + "\n\n" + existingSystem;
}
- /**
- * Replace MateClaw / agent-specific branding tokens with their Claude Code
- * equivalents. Same idea as hermes-agent's
- * {@code text.replace("Hermes Agent", "Claude Code")} chain — Anthropic's
- * server-side filter flags requests where the spoofed identity contradicts
- * itself ("You are Claude Code … built by MateClaw").
- */
static String sanitizeBranding(String text) {
if (text == null || text.isEmpty()) {
return text;
@@ -131,4 +260,57 @@ public class ClaudeCodeIdentityChatModelDecorator implements ChatModel {
.replace("mateclaw", "claude-code")
.replace("Mate Claw", "Claude Code");
}
+
+ /* ====================================================================== */
+ /* PrefixedToolCallback — wraps a ToolCallback to expose the mcp_ name */
+ /* ====================================================================== */
+
+ /**
+ * Wraps a {@link ToolCallback} so its {@code getToolDefinition().name()}
+ * returns {@code mcp_}, while {@code call(...)} forwards verbatim
+ * to the underlying tool. Anthropic sees the prefixed name on the wire;
+ * MateClaw's tool implementation never sees the prefix.
+ */
+ static final class PrefixedToolCallback implements ToolCallback {
+
+ private final ToolCallback delegate;
+ private final ToolDefinition prefixedDefinition;
+
+ PrefixedToolCallback(ToolCallback delegate) {
+ this.delegate = delegate;
+ ToolDefinition orig = delegate.getToolDefinition();
+ String origName = orig.name();
+ String prefixed = (origName != null && origName.startsWith(MCP_TOOL_PREFIX))
+ ? origName : MCP_TOOL_PREFIX + origName;
+ this.prefixedDefinition = DefaultToolDefinition.builder()
+ .name(prefixed)
+ .description(orig.description())
+ .inputSchema(orig.inputSchema())
+ .build();
+ }
+
+ @Override
+ public ToolDefinition getToolDefinition() {
+ return prefixedDefinition;
+ }
+
+ @Override
+ public ToolMetadata getToolMetadata() {
+ return delegate.getToolMetadata();
+ }
+
+ @Override
+ public String call(String input) {
+ return delegate.call(input);
+ }
+
+ @Override
+ public String call(String input, ToolContext context) {
+ return delegate.call(input, context);
+ }
+
+ ToolCallback unwrap() {
+ return delegate;
+ }
+ }
}
diff --git a/mateclaw-server/src/main/resources/db/data-en.sql b/mateclaw-server/src/main/resources/db/data-en.sql
index edc47c3a..7a672fa5 100644
--- a/mateclaw-server/src/main/resources/db/data-en.sql
+++ b/mateclaw-server/src/main/resources/db/data-en.sql
@@ -264,12 +264,13 @@ MERGE INTO mate_model_config (id, name, provider, model_name, description, tempe
-- Claude 4.7 series (direct Anthropic + OpenRouter). Sonnet/Opus.
-- Note: Claude 4.7 forbids temperature/top_p/top_k — handled in AgentAnthropicChatModelBuilder.
(1000000270, 'Claude Opus 4.7', 'anthropic', 'claude-opus-4-7', 'Anthropic Claude Opus 4.7 (xhigh adaptive thinking)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
-(1000000271, 'Claude Sonnet 4.7', 'anthropic', 'claude-sonnet-4-7', 'Anthropic Claude Sonnet 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
+-- Anthropic only released Opus 4.7 — Sonnet stays at 4.6 until further notice.
+(1000000271, 'Claude Sonnet 4.6', 'anthropic', 'claude-sonnet-4-6', 'Anthropic Claude Sonnet 4.6 (latest Sonnet — 4.7 not yet released)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
(1000000272, 'Claude Opus 4.7', 'openrouter', 'anthropic/claude-opus-4-7', 'Claude Opus 4.7 via OpenRouter', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
-(1000000273, 'Claude Sonnet 4.7', 'openrouter', 'anthropic/claude-sonnet-4-7', 'Claude Sonnet 4.7 via OpenRouter', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
+(1000000273, 'Claude Sonnet 4.6', 'openrouter', 'anthropic/claude-sonnet-4-6', 'Claude Sonnet 4.6 via OpenRouter', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
-- RFC-062: Claude 4.7 via Claude Code OAuth subscription (Pro/Max plan).
(1000000280, 'Claude Opus 4.7', 'anthropic-claude-code', 'claude-opus-4-7', 'Claude Opus 4.7 via Claude Code Pro/Max subscription', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
-(1000000281, 'Claude Sonnet 4.7', 'anthropic-claude-code', 'claude-sonnet-4-7', 'Claude Sonnet 4.7 via Claude Code Pro/Max subscription', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0);
+(1000000281, 'Claude Sonnet 4.6', 'anthropic-claude-code', 'claude-sonnet-4-6', 'Claude Sonnet 4.6 via Claude Code Pro/Max subscription', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0);
-- Default system settings
MERGE INTO mate_system_setting (id, setting_key, setting_value, description, create_time, update_time)
diff --git a/mateclaw-server/src/main/resources/db/data-mysql-en.sql b/mateclaw-server/src/main/resources/db/data-mysql-en.sql
index 29e1a1ef..8260ac4f 100644
--- a/mateclaw-server/src/main/resources/db/data-mysql-en.sql
+++ b/mateclaw-server/src/main/resources/db/data-mysql-en.sql
@@ -302,12 +302,13 @@ VALUES
-- Claude 4.7 series (direct Anthropic + OpenRouter).
-- Note: Claude 4.7 forbids temperature/top_p/top_k — handled in AgentAnthropicChatModelBuilder.
(1000000270, 'Claude Opus 4.7', 'anthropic', 'claude-opus-4-7', 'Anthropic Claude Opus 4.7 (xhigh adaptive thinking)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
-(1000000271, 'Claude Sonnet 4.7', 'anthropic', 'claude-sonnet-4-7', 'Anthropic Claude Sonnet 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
+-- Anthropic only released Opus 4.7 — Sonnet stays at 4.6 until further notice.
+(1000000271, 'Claude Sonnet 4.6', 'anthropic', 'claude-sonnet-4-6', 'Anthropic Claude Sonnet 4.6 (latest Sonnet — 4.7 not yet released)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
(1000000272, 'Claude Opus 4.7', 'openrouter', 'anthropic/claude-opus-4-7', 'Claude Opus 4.7 via OpenRouter', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
-(1000000273, 'Claude Sonnet 4.7', 'openrouter', 'anthropic/claude-sonnet-4-7', 'Claude Sonnet 4.7 via OpenRouter', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
+(1000000273, 'Claude Sonnet 4.6', 'openrouter', 'anthropic/claude-sonnet-4-6', 'Claude Sonnet 4.6 via OpenRouter', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
-- RFC-062: Claude 4.7 via Claude Code OAuth subscription (Pro/Max plan).
(1000000280, 'Claude Opus 4.7', 'anthropic-claude-code', 'claude-opus-4-7', 'Claude Opus 4.7 via Claude Code Pro/Max subscription', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
-(1000000281, 'Claude Sonnet 4.7', 'anthropic-claude-code', 'claude-sonnet-4-7', 'Claude Sonnet 4.7 via Claude Code Pro/Max subscription', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0)
+(1000000281, 'Claude Sonnet 4.6', 'anthropic-claude-code', 'claude-sonnet-4-6', 'Claude Sonnet 4.6 via Claude Code Pro/Max subscription', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), provider=VALUES(provider), model_name=VALUES(model_name), description=VALUES(description), temperature=VALUES(temperature), max_tokens=VALUES(max_tokens), top_p=VALUES(top_p), builtin=VALUES(builtin), enabled=VALUES(enabled), is_default=VALUES(is_default), update_time=VALUES(update_time), deleted=VALUES(deleted);
-- Default system settings
diff --git a/mateclaw-server/src/main/resources/db/data-mysql-zh.sql b/mateclaw-server/src/main/resources/db/data-mysql-zh.sql
index 0fa59483..686fdaa5 100644
--- a/mateclaw-server/src/main/resources/db/data-mysql-zh.sql
+++ b/mateclaw-server/src/main/resources/db/data-mysql-zh.sql
@@ -302,12 +302,13 @@ VALUES
-- Claude 4.7 系列(直连 Anthropic + OpenRouter)
-- 注意:Claude 4.7 禁止 temperature / top_p / top_k 参数,已在 AgentAnthropicChatModelBuilder 中适配
(1000000270, 'Claude Opus 4.7', 'anthropic', 'claude-opus-4-7', 'Anthropic Claude Opus 4.7(xhigh 自适应思考)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
-(1000000271, 'Claude Sonnet 4.7', 'anthropic', 'claude-sonnet-4-7', 'Anthropic Claude Sonnet 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
+-- Anthropic 仅发布了 Opus 4.7,Sonnet 暂时仍是 4.6
+(1000000271, 'Claude Sonnet 4.6', 'anthropic', 'claude-sonnet-4-6', 'Anthropic 最新 Sonnet (Sonnet 4.7 暂未发布)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
(1000000272, 'Claude Opus 4.7', 'openrouter', 'anthropic/claude-opus-4-7', 'OpenRouter 代理 Claude Opus 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
-(1000000273, 'Claude Sonnet 4.7', 'openrouter', 'anthropic/claude-sonnet-4-7', 'OpenRouter 代理 Claude Sonnet 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
+(1000000273, 'Claude Sonnet 4.6', 'openrouter', 'anthropic/claude-sonnet-4-6', 'OpenRouter 代理 Claude Sonnet 4.6', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
-- RFC-062:通过 Claude Code Pro/Max 订阅调用 Claude 4.7
(1000000280, 'Claude Opus 4.7', 'anthropic-claude-code', 'claude-opus-4-7', '通过 Claude Code Pro/Max 订阅调用 Claude Opus 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
-(1000000281, 'Claude Sonnet 4.7', 'anthropic-claude-code', 'claude-sonnet-4-7', '通过 Claude Code Pro/Max 订阅调用 Claude Sonnet 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0)
+(1000000281, 'Claude Sonnet 4.6', 'anthropic-claude-code', 'claude-sonnet-4-6', '通过 Claude Code Pro/Max 订阅调用 Claude Sonnet 4.6', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), provider=VALUES(provider), model_name=VALUES(model_name), description=VALUES(description), temperature=VALUES(temperature), max_tokens=VALUES(max_tokens), top_p=VALUES(top_p), builtin=VALUES(builtin), enabled=VALUES(enabled), is_default=VALUES(is_default), update_time=VALUES(update_time), deleted=VALUES(deleted);
-- 默认系统设置
diff --git a/mateclaw-server/src/main/resources/db/data-zh.sql b/mateclaw-server/src/main/resources/db/data-zh.sql
index 791ece92..3ffda686 100644
--- a/mateclaw-server/src/main/resources/db/data-zh.sql
+++ b/mateclaw-server/src/main/resources/db/data-zh.sql
@@ -268,12 +268,13 @@ MERGE INTO mate_model_config (id, name, provider, model_name, description, tempe
-- Claude 4.7 系列(直连 Anthropic + OpenRouter)
-- 注意:Claude 4.7 禁止 temperature / top_p / top_k 参数,已在 AgentAnthropicChatModelBuilder 中适配
(1000000270, 'Claude Opus 4.7', 'anthropic', 'claude-opus-4-7', 'Anthropic Claude Opus 4.7(xhigh 自适应思考)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
-(1000000271, 'Claude Sonnet 4.7', 'anthropic', 'claude-sonnet-4-7', 'Anthropic Claude Sonnet 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
+-- Anthropic 仅发布了 Opus 4.7,Sonnet 暂时仍是 4.6
+(1000000271, 'Claude Sonnet 4.6', 'anthropic', 'claude-sonnet-4-6', 'Anthropic 最新 Sonnet (Sonnet 4.7 暂未发布)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
(1000000272, 'Claude Opus 4.7', 'openrouter', 'anthropic/claude-opus-4-7', 'OpenRouter 代理 Claude Opus 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
-(1000000273, 'Claude Sonnet 4.7', 'openrouter', 'anthropic/claude-sonnet-4-7', 'OpenRouter 代理 Claude Sonnet 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
+(1000000273, 'Claude Sonnet 4.6', 'openrouter', 'anthropic/claude-sonnet-4-6', 'OpenRouter 代理 Claude Sonnet 4.6', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
-- RFC-062:通过 Claude Code Pro/Max 订阅调用 Claude 4.7
(1000000280, 'Claude Opus 4.7', 'anthropic-claude-code', 'claude-opus-4-7', '通过 Claude Code Pro/Max 订阅调用 Claude Opus 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
-(1000000281, 'Claude Sonnet 4.7', 'anthropic-claude-code', 'claude-sonnet-4-7', '通过 Claude Code Pro/Max 订阅调用 Claude Sonnet 4.7', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0);
+(1000000281, 'Claude Sonnet 4.6', 'anthropic-claude-code', 'claude-sonnet-4-6', '通过 Claude Code Pro/Max 订阅调用 Claude Sonnet 4.6', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0);
-- 默认系统设置
MERGE INTO mate_system_setting (id, setting_key, setting_value, description, create_time, update_time)
diff --git a/mateclaw-server/src/main/resources/db/migration/h2/V44__fix_claude_sonnet_47_does_not_exist.sql b/mateclaw-server/src/main/resources/db/migration/h2/V44__fix_claude_sonnet_47_does_not_exist.sql
new file mode 100644
index 00000000..4cf55e42
--- /dev/null
+++ b/mateclaw-server/src/main/resources/db/migration/h2/V44__fix_claude_sonnet_47_does_not_exist.sql
@@ -0,0 +1,32 @@
+-- Repair migration for V42/V43: Anthropic only released Opus 4.7 — there
+-- is no claude-sonnet-4-7 model. Calls return HTTP 404 with body
+-- {"type":"not_found_error","message":"model: claude-sonnet-4-7"}.
+--
+-- Reference: hermes-agent anthropic_adapter.py _ANTHROPIC_OUTPUT_LIMITS
+-- (lines 65-93) lists claude-opus-4-7 but no claude-sonnet-4-7. The latest
+-- released Sonnet remains claude-sonnet-4-6 (released alongside Opus 4.6).
+--
+-- Strategy: rename in place — preserve ids 1000000271, 1000000273, 1000000281
+-- so user-customised settings (default flag, enabled flag) survive.
+-- When/if Anthropic ships Sonnet 4.7, a future migration can switch back.
+
+UPDATE mate_model_config
+SET name = 'Claude Sonnet 4.6',
+ model_name = 'claude-sonnet-4-6',
+ description = 'Anthropic Claude Sonnet 4.6 (latest Sonnet — 4.7 not yet released)',
+ update_time = NOW()
+WHERE id = 1000000271 AND model_name = 'claude-sonnet-4-7';
+
+UPDATE mate_model_config
+SET name = 'Claude Sonnet 4.6',
+ model_name = 'anthropic/claude-sonnet-4-6',
+ description = 'Claude Sonnet 4.6 via OpenRouter',
+ update_time = NOW()
+WHERE id = 1000000273 AND model_name = 'anthropic/claude-sonnet-4-7';
+
+UPDATE mate_model_config
+SET name = 'Claude Sonnet 4.6',
+ model_name = 'claude-sonnet-4-6',
+ description = 'Claude Sonnet 4.6 via Claude Code Pro/Max subscription',
+ update_time = NOW()
+WHERE id = 1000000281 AND model_name = 'claude-sonnet-4-7';
diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V44__fix_claude_sonnet_47_does_not_exist.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V44__fix_claude_sonnet_47_does_not_exist.sql
new file mode 100644
index 00000000..27be8b1a
--- /dev/null
+++ b/mateclaw-server/src/main/resources/db/migration/mysql/V44__fix_claude_sonnet_47_does_not_exist.sql
@@ -0,0 +1,31 @@
+-- Repair migration for V42/V43: Anthropic only released Opus 4.7 — there
+-- is no claude-sonnet-4-7 model. Calls return HTTP 404 with body
+-- {"type":"not_found_error","message":"model: claude-sonnet-4-7"}.
+--
+-- Reference: hermes-agent anthropic_adapter.py _ANTHROPIC_OUTPUT_LIMITS
+-- (lines 65-93) lists claude-opus-4-7 but no claude-sonnet-4-7. The latest
+-- released Sonnet remains claude-sonnet-4-6 (released alongside Opus 4.6).
+--
+-- Strategy: rename in place — preserve ids 1000000271, 1000000273, 1000000281
+-- so user-customised settings (default flag, enabled flag) survive.
+
+UPDATE mate_model_config
+SET name = 'Claude Sonnet 4.6',
+ model_name = 'claude-sonnet-4-6',
+ description = 'Anthropic Claude Sonnet 4.6 (latest Sonnet — 4.7 not yet released)',
+ update_time = NOW()
+WHERE id = 1000000271 AND model_name = 'claude-sonnet-4-7';
+
+UPDATE mate_model_config
+SET name = 'Claude Sonnet 4.6',
+ model_name = 'anthropic/claude-sonnet-4-6',
+ description = 'Claude Sonnet 4.6 via OpenRouter',
+ update_time = NOW()
+WHERE id = 1000000273 AND model_name = 'anthropic/claude-sonnet-4-7';
+
+UPDATE mate_model_config
+SET name = 'Claude Sonnet 4.6',
+ model_name = 'claude-sonnet-4-6',
+ description = 'Claude Sonnet 4.6 via Claude Code Pro/Max subscription',
+ update_time = NOW()
+WHERE id = 1000000281 AND model_name = 'claude-sonnet-4-7';