diff --git a/mateclaw-server/src/main/java/vip/mate/llm/chatmodel/AnthropicChatModelBuilder.java b/mateclaw-server/src/main/java/vip/mate/llm/chatmodel/AnthropicChatModelBuilder.java index 9df1b5a9..fc436b84 100644 --- a/mateclaw-server/src/main/java/vip/mate/llm/chatmodel/AnthropicChatModelBuilder.java +++ b/mateclaw-server/src/main/java/vip/mate/llm/chatmodel/AnthropicChatModelBuilder.java @@ -121,13 +121,36 @@ public class AnthropicChatModelBuilder implements ChatModelBuilder { return lower.contains("4-7") || lower.contains("4.7"); } + /** + * Detect Claude 4.8 model variants (including the {@code -fast} sibling). + * Claude 4.8 inherits 4.7's strict API contract: temperature / top_p / + * top_k must be unset, and the "xhigh" thinking tier is available. + */ + static boolean isClaude48(String modelName) { + if (modelName == null) return false; + String lower = modelName.toLowerCase(); + if (!lower.contains("claude")) return false; + // Matches claude-opus-4-8, claude-opus-4.8, the "-fast" sibling + // (claude-opus-4-8-fast / claude-opus-4.8-fast), and OpenRouter + // prefixed forms (anthropic/claude-opus-4-8...). + return lower.contains("4-8") || lower.contains("4.8"); + } + + /** + * True for any Claude 4.7+ model — the family that drops temperature / + * top_p / top_k and exposes the "xhigh" thinking tier between high and max. + */ + static boolean isClaude47OrLater(String modelName) { + return isClaude47(modelName) || isClaude48(modelName); + } + AnthropicChatOptions buildAnthropicOptions(ModelConfigEntity runtimeModel) { AnthropicChatOptions.Builder builder = AnthropicChatOptions.builder(); String modelName = runtimeModel.getModelName(); if (StringUtils.hasText(modelName)) { builder.model(modelName); } - boolean isClaude47 = isClaude47(modelName); + boolean strictSamplingContract = isClaude47OrLater(modelName); // Extended thinking — request-level depth from ThinkingLevelHolder String thinkingLevel = ThinkingLevelHolder.get(); @@ -136,22 +159,22 @@ public class AnthropicChatModelBuilder implements ChatModelBuilder { if (thinkingEnabled) { // Anthropic thinking-mode constraints (pre-4.7): temperature MUST be 1, // top_p forbidden, max_tokens must accommodate budget_tokens + buffer. - // Claude 4.7 forbids temperature/top_p/top_k entirely (any non-null value + // Claude 4.7+ forbids temperature/top_p/top_k entirely (any non-null value // → HTTP 400) and adds an "xhigh" budget tier between high and max. int budgetTokens = switch (thinkingLevel.toLowerCase()) { case "low" -> 4096; case "medium" -> 8192; case "high" -> 16384; - case "xhigh" -> 24576; // 4.7 only — between high (16k) and max (32k) + case "xhigh" -> 24576; // 4.7+ only — between high (16k) and max (32k) case "max" -> 32768; default -> 16384; }; builder.thinking(AnthropicApi.ThinkingType.ENABLED, budgetTokens); builder.maxTokens(Math.max(budgetTokens + 4096, runtimeModel.getMaxTokens() != null ? runtimeModel.getMaxTokens() : 8192)); - // Claude 4.7: omit temperature entirely. Pre-4.7 thinking mode requires + // Claude 4.7+: omit temperature entirely. Pre-4.7 thinking mode requires // temperature=1 (Anthropic-mandated default for thinking). - if (!isClaude47) { + if (!strictSamplingContract) { builder.temperature(1.0); } } else { @@ -159,14 +182,14 @@ public class AnthropicChatModelBuilder implements ChatModelBuilder { // - Pre-4.7: Anthropic accepts EITHER temperature OR top_p (not both). // - 4.7+: rejects all of temperature/top_p/top_k unless null/default. We // omit them entirely so operators with legacy configs don't 400. - if (!isClaude47) { + if (!strictSamplingContract) { if (runtimeModel.getTemperature() != null) { builder.temperature(runtimeModel.getTemperature()); } else if (runtimeModel.getTopP() != null) { builder.topP(runtimeModel.getTopP()); } } else if (runtimeModel.getTemperature() != null || runtimeModel.getTopP() != null) { - log.debug("Ignoring temperature/top_p for Claude 4.7 model {} (API rejects sampling params)", + log.debug("Ignoring temperature/top_p for Claude 4.7+ model {} (API rejects sampling params)", modelName); } // Anthropic rejects non-positive maxTokens — clamp here so a bad config diff --git a/mateclaw-server/src/main/resources/db/data-en.sql b/mateclaw-server/src/main/resources/db/data-en.sql index ecee62e0..6fdeb8bc 100644 --- a/mateclaw-server/src/main/resources/db/data-en.sql +++ b/mateclaw-server/src/main/resources/db/data-en.sql @@ -339,7 +339,15 @@ MERGE INTO mate_model_config (id, name, provider, model_name, description, tempe (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.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); +(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), +-- Claude 4.8 series (direct Anthropic + OpenRouter, including the -fast variant). +-- Shares 4.7's strict sampling contract (temperature/top_p/top_k must be NULL) +-- and the new xhigh thinking tier — handled in AnthropicChatModelBuilder. +(1000000290, 'Claude Opus 4.8', 'anthropic', 'claude-opus-4-8', 'Anthropic Claude Opus 4.8 (xhigh adaptive thinking)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +(1000000291, 'Claude Opus 4.8 Fast', 'anthropic', 'claude-opus-4-8-fast', 'Claude Opus 4.8 fast variant (higher output speed, 2x pricing)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +(1000000292, 'Claude Opus 4.8', 'openrouter', 'anthropic/claude-opus-4-8', 'Claude Opus 4.8 via OpenRouter', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +(1000000293, 'Claude Opus 4.8 Fast', 'openrouter', 'anthropic/claude-opus-4-8-fast', 'Claude Opus 4.8 fast variant via OpenRouter', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +(1000000294, 'Claude Opus 4.8', 'anthropic-claude-code', 'claude-opus-4-8', 'Claude Opus 4.8 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 2d197d95..1f16ce9f 100644 --- a/mateclaw-server/src/main/resources/db/data-mysql-en.sql +++ b/mateclaw-server/src/main/resources/db/data-mysql-en.sql @@ -387,7 +387,15 @@ VALUES (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.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) +(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), +-- Claude 4.8 series (direct Anthropic + OpenRouter, including the -fast variant). +-- Shares 4.7's strict sampling contract (temperature/top_p/top_k must be NULL) +-- and the new xhigh thinking tier — handled in AnthropicChatModelBuilder. +(1000000290, 'Claude Opus 4.8', 'anthropic', 'claude-opus-4-8', 'Anthropic Claude Opus 4.8 (xhigh adaptive thinking)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +(1000000291, 'Claude Opus 4.8 Fast', 'anthropic', 'claude-opus-4-8-fast', 'Claude Opus 4.8 fast variant (higher output speed, 2x pricing)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +(1000000292, 'Claude Opus 4.8', 'openrouter', 'anthropic/claude-opus-4-8', 'Claude Opus 4.8 via OpenRouter', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +(1000000293, 'Claude Opus 4.8 Fast', 'openrouter', 'anthropic/claude-opus-4-8-fast', 'Claude Opus 4.8 fast variant via OpenRouter', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +(1000000294, 'Claude Opus 4.8', 'anthropic-claude-code', 'claude-opus-4-8', 'Claude Opus 4.8 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 b778b395..d3a98623 100644 --- a/mateclaw-server/src/main/resources/db/data-mysql-zh.sql +++ b/mateclaw-server/src/main/resources/db/data-mysql-zh.sql @@ -384,7 +384,14 @@ VALUES (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.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) +(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), +-- Claude 4.8 系列(直连 Anthropic + OpenRouter,包含 -fast 高速变体) +-- 与 4.7 共享严格采样契约:temperature / top_p / top_k 必须为空,新增 xhigh 思考档位 +(1000000290, 'Claude Opus 4.8', 'anthropic', 'claude-opus-4-8', 'Anthropic Claude Opus 4.8(xhigh 自适应思考)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +(1000000291, 'Claude Opus 4.8 Fast', 'anthropic', 'claude-opus-4-8-fast', 'Claude Opus 4.8 高速变体(输出更快、单价 2x)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +(1000000292, 'Claude Opus 4.8', 'openrouter', 'anthropic/claude-opus-4-8', 'OpenRouter 代理 Claude Opus 4.8', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +(1000000293, 'Claude Opus 4.8 Fast', 'openrouter', 'anthropic/claude-opus-4-8-fast', 'OpenRouter 代理 Claude Opus 4.8 高速变体', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +(1000000294, 'Claude Opus 4.8', 'anthropic-claude-code', 'claude-opus-4-8', '通过 Claude Code Pro/Max 订阅调用 Claude Opus 4.8', 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 a663950d..c68c4109 100644 --- a/mateclaw-server/src/main/resources/db/data-zh.sql +++ b/mateclaw-server/src/main/resources/db/data-zh.sql @@ -341,7 +341,14 @@ MERGE INTO mate_model_config (id, name, provider, model_name, description, tempe (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.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); +(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), +-- Claude 4.8 系列(直连 Anthropic + OpenRouter,包含 -fast 高速变体) +-- 与 4.7 共享严格采样契约:temperature / top_p / top_k 必须为空,新增 xhigh 思考档位 +(1000000290, 'Claude Opus 4.8', 'anthropic', 'claude-opus-4-8', 'Anthropic Claude Opus 4.8(xhigh 自适应思考)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +(1000000291, 'Claude Opus 4.8 Fast', 'anthropic', 'claude-opus-4-8-fast', 'Claude Opus 4.8 高速变体(输出更快、单价 2x)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +(1000000292, 'Claude Opus 4.8', 'openrouter', 'anthropic/claude-opus-4-8', 'OpenRouter 代理 Claude Opus 4.8', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +(1000000293, 'Claude Opus 4.8 Fast', 'openrouter', 'anthropic/claude-opus-4-8-fast', 'OpenRouter 代理 Claude Opus 4.8 高速变体', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +(1000000294, 'Claude Opus 4.8', 'anthropic-claude-code', 'claude-opus-4-8', '通过 Claude Code Pro/Max 订阅调用 Claude Opus 4.8', 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/V131__claude_48_models.sql b/mateclaw-server/src/main/resources/db/migration/h2/V131__claude_48_models.sql new file mode 100644 index 00000000..9705bafc --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/h2/V131__claude_48_models.sql @@ -0,0 +1,24 @@ +-- Add Claude Opus 4.8 (regular + -fast variant) model entries to +-- mate_model_config for existing deployments. New installs pick these up via +-- DatabaseBootstrapRunner from data-{en,zh}.sql; this migration covers +-- operators who already have V1 baseline + earlier versions applied. +-- +-- Claude 4.8 inherits 4.7's strict API contract: temperature / top_p / top_k +-- must be NULL (otherwise HTTP 400), and the "xhigh" thinking tier is +-- available. Both are handled in AnthropicChatModelBuilder via the +-- isClaude47OrLater() detector. +-- +-- MERGE INTO is the H2 idempotent upsert; running this twice is a no-op. +-- Same V number is used in mysql/ for cross-dialect parity. + +MERGE INTO mate_model_config (id, name, provider, model_name, description, temperature, max_tokens, top_p, builtin, enabled, is_default, create_time, update_time, deleted) +KEY (id) +VALUES +-- Direct Anthropic +(1000000290, 'Claude Opus 4.8', 'anthropic', 'claude-opus-4-8', 'Anthropic Claude Opus 4.8 (xhigh adaptive thinking)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +(1000000291, 'Claude Opus 4.8 Fast', 'anthropic', 'claude-opus-4-8-fast', 'Claude Opus 4.8 fast variant (higher output speed, 2x pricing)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +-- OpenRouter passthrough +(1000000292, 'Claude Opus 4.8', 'openrouter', 'anthropic/claude-opus-4-8', 'Claude Opus 4.8 via OpenRouter', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +(1000000293, 'Claude Opus 4.8 Fast', 'openrouter', 'anthropic/claude-opus-4-8-fast', 'Claude Opus 4.8 fast variant via OpenRouter', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +-- Claude Code OAuth (Pro/Max subscription) +(1000000294, 'Claude Opus 4.8', 'anthropic-claude-code', 'claude-opus-4-8', 'Claude Opus 4.8 via Claude Code Pro/Max subscription', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0); diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V131__claude_48_models.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V131__claude_48_models.sql new file mode 100644 index 00000000..4e8bc865 --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/mysql/V131__claude_48_models.sql @@ -0,0 +1,36 @@ +-- Add Claude Opus 4.8 (regular + -fast variant) model entries to +-- mate_model_config for existing deployments. New installs pick these up via +-- DatabaseBootstrapRunner from data-mysql-{en,zh}.sql; this migration covers +-- operators who already have earlier Flyway versions applied. +-- +-- Claude 4.8 inherits 4.7's strict API contract: temperature / top_p / top_k +-- must be NULL (otherwise HTTP 400), and the "xhigh" thinking tier is +-- available. Both are handled in AnthropicChatModelBuilder via the +-- isClaude47OrLater() detector. +-- +-- INSERT ... ON DUPLICATE KEY UPDATE is the MySQL idempotent upsert. +-- Same V number is used in h2/ for cross-dialect parity. + +INSERT INTO mate_model_config (id, name, provider, model_name, description, temperature, max_tokens, top_p, builtin, enabled, is_default, create_time, update_time, deleted) +VALUES +-- Direct Anthropic +(1000000290, 'Claude Opus 4.8', 'anthropic', 'claude-opus-4-8', 'Anthropic Claude Opus 4.8 (xhigh adaptive thinking)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +(1000000291, 'Claude Opus 4.8 Fast', 'anthropic', 'claude-opus-4-8-fast', 'Claude Opus 4.8 fast variant (higher output speed, 2x pricing)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +-- OpenRouter passthrough +(1000000292, 'Claude Opus 4.8', 'openrouter', 'anthropic/claude-opus-4-8', 'Claude Opus 4.8 via OpenRouter', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +(1000000293, 'Claude Opus 4.8 Fast', 'openrouter', 'anthropic/claude-opus-4-8-fast', 'Claude Opus 4.8 fast variant via OpenRouter', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0), +-- Claude Code OAuth (Pro/Max subscription) +(1000000294, 'Claude Opus 4.8', 'anthropic-claude-code', 'claude-opus-4-8', 'Claude Opus 4.8 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); diff --git a/mateclaw-server/src/test/java/vip/mate/llm/chatmodel/AnthropicChatModelBuilderClaude48Test.java b/mateclaw-server/src/test/java/vip/mate/llm/chatmodel/AnthropicChatModelBuilderClaude48Test.java new file mode 100644 index 00000000..c5a8f57e --- /dev/null +++ b/mateclaw-server/src/test/java/vip/mate/llm/chatmodel/AnthropicChatModelBuilderClaude48Test.java @@ -0,0 +1,88 @@ +package vip.mate.llm.chatmodel; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link AnthropicChatModelBuilder#isClaude48} must correctly classify the + * Claude 4.8 model variants we'll see in production — including the + * higher-priced {@code -fast} sibling. + * + *
Claude 4.8 inherits 4.7's strict API contract: temperature / top_p / + * top_k must be unset, and the "xhigh" thinking tier is available. The + * builder uses {@link AnthropicChatModelBuilder#isClaude47OrLater} to share + * the gating logic across both generations.
+ */ +class AnthropicChatModelBuilderClaude48Test { + + @Test + @DisplayName("isClaude48 detects hyphenated direct-API model names") + void detect_hyphenated() { + assertTrue(AnthropicChatModelBuilder.isClaude48("claude-opus-4-8")); + assertTrue(AnthropicChatModelBuilder.isClaude48("claude-opus-4-8-fast")); + } + + @Test + @DisplayName("isClaude48 detects dotted variants (e.g. OpenRouter / mixed dialects)") + void detect_dotted() { + assertTrue(AnthropicChatModelBuilder.isClaude48("claude-opus-4.8")); + assertTrue(AnthropicChatModelBuilder.isClaude48("claude-opus-4.8-fast")); + } + + @Test + @DisplayName("isClaude48 detects OpenRouter-style prefixed model ids") + void detect_openrouterPrefix() { + assertTrue(AnthropicChatModelBuilder.isClaude48("anthropic/claude-opus-4-8")); + assertTrue(AnthropicChatModelBuilder.isClaude48("anthropic/claude-opus-4-8-fast")); + assertTrue(AnthropicChatModelBuilder.isClaude48("anthropic/claude-opus-4.8")); + assertTrue(AnthropicChatModelBuilder.isClaude48("anthropic/claude-opus-4.8-fast")); + } + + @Test + @DisplayName("isClaude48 ignores 4.5 / 4.6 / 4.7 / 3.x and unrelated names") + void detect_negatives() { + assertFalse(AnthropicChatModelBuilder.isClaude48("claude-opus-4-7")); + assertFalse(AnthropicChatModelBuilder.isClaude48("claude-opus-4-6")); + assertFalse(AnthropicChatModelBuilder.isClaude48("claude-sonnet-4-5")); + assertFalse(AnthropicChatModelBuilder.isClaude48("claude-3-7-sonnet")); + // The "claude" prefix guard prevents non-Anthropic models from spuriously + // matching even if they contain "4-8" / "4.8" substrings. + assertFalse(AnthropicChatModelBuilder.isClaude48("gpt-4-8"), + "Non-Claude models must NOT match — claude prefix guard active"); + assertFalse(AnthropicChatModelBuilder.isClaude48("nemotron-4-8-instruct")); + } + + @Test + @DisplayName("isClaude48 null-safe") + void detect_nullSafe() { + assertFalse(AnthropicChatModelBuilder.isClaude48(null)); + assertFalse(AnthropicChatModelBuilder.isClaude48("")); + } + + @Test + @DisplayName("isClaude48 tolerates date-stamped variants") + void detect_dateStamped() { + assertTrue(AnthropicChatModelBuilder.isClaude48("claude-opus-4-8-20260601")); + assertTrue(AnthropicChatModelBuilder.isClaude48("claude-opus-4-8-fast-20260601")); + } + + @Test + @DisplayName("isClaude47OrLater unifies the 4.7 + 4.8 sampling-forbidden contract") + void claude47OrLater_unifiesGenerations() { + // 4.7 still matches via the legacy detector + assertTrue(AnthropicChatModelBuilder.isClaude47OrLater("claude-opus-4-7")); + assertTrue(AnthropicChatModelBuilder.isClaude47OrLater("anthropic/claude-opus-4.7")); + // 4.8 (regular + -fast) matches via the new detector + assertTrue(AnthropicChatModelBuilder.isClaude47OrLater("claude-opus-4-8")); + assertTrue(AnthropicChatModelBuilder.isClaude47OrLater("claude-opus-4-8-fast")); + assertTrue(AnthropicChatModelBuilder.isClaude47OrLater("anthropic/claude-opus-4.8-fast")); + // Older Claude generations fall through + assertFalse(AnthropicChatModelBuilder.isClaude47OrLater("claude-opus-4-6")); + assertFalse(AnthropicChatModelBuilder.isClaude47OrLater("claude-sonnet-4-5")); + assertFalse(AnthropicChatModelBuilder.isClaude47OrLater("claude-3-7-sonnet")); + // Null-safe + assertFalse(AnthropicChatModelBuilder.isClaude47OrLater(null)); + } +}