fix(anthropic): bidirectional mcp_ tool-name prefix on OAuth requests

This commit is contained in:
matevip 2026-04-26 08:34:10 +08:00
parent 44548e3010
commit ae6467a5dc
7 changed files with 291 additions and 42 deletions

View File

@ -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.
*
* <p>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:
*
* <ul>
* <li>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).</li>
* (genuine quota exhaustion carries a descriptive message).</li>
* <li>Sporadic 500s on the first call after a long idle period.</li>
* </ul>
*
* <p>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.
*
* <h2>Transforms applied per call</h2>
* <ol>
* <li>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.</li>
* <li>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.</li>
* <li><b>System prompt prefix</b>: prepend
* {@code "You are Claude Code, Anthropic's official CLI for Claude."}.
* Insert a new SystemMessage if none exists.</li>
* <li><b>Brand scrub</b>: replace {@code "MateClaw"}/{@code "mateclaw"}
* in system text with their Claude Code equivalents Anthropic's
* content filter flags identity contradictions.</li>
* <li><b>Tool {@code mcp_} prefix (outgoing)</b>: every tool definition
* sent to Anthropic is renamed {@code mcp_<orig>} Claude Code
* runs all tools through MCP servers, so real Claude Code traffic
* always has the prefix. Mismatch trips anti-abuse.</li>
* <li><b>History tool_use prefix</b>: previously-issued tool calls in
* AssistantMessage history get the prefix re-applied (we strip on
* response, so they're stored unprefixed).</li>
* <li><b>Tool {@code mcp_} prefix (incoming)</b>: ChatResponse tool_use
* names are stripped of the {@code mcp_} prefix so MateClaw's tool
* registry can resolve them.</li>
* </ol>
*
* <p><b>Tool-name {@code mcp_} prefix</b> (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<ChatResponse> 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_<orig>}. 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<ToolCallback> originalCallbacks = anthropicOpts.getToolCallbacks();
Set<String> 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<ToolCallback> 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<String> 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<Generation> origGens = response.getResults();
if (origGens == null || origGens.isEmpty()) {
return response;
}
List<Generation> 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<AssistantMessage.ToolCall> 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_<orig>}, 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;
}
}
}

View File

@ -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)

View File

@ -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

View File

@ -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.7xhigh 自适应思考)', 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.7Sonnet 暂时仍是 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);
-- 默认系统设置

View File

@ -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.7xhigh 自适应思考)', 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.7Sonnet 暂时仍是 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)

View File

@ -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';

View File

@ -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';