feat(llm): add Claude Opus 4.8 + 4.8 Fast model entries

This commit is contained in:
matevip 2026-05-29 06:57:04 +08:00
parent 0141b00270
commit 82538ca3aa
8 changed files with 212 additions and 11 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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.8xhigh 自适应思考)', 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)

View File

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

View File

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

View File

@ -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.
*
* <p>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.</p>
*/
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));
}
}