feat(llm): add Claude Fable 5 support

This commit is contained in:
matevip 2026-06-10 10:14:51 +08:00
parent c1fd39a1e2
commit 7478491652
5 changed files with 142 additions and 3 deletions

View File

@ -137,11 +137,27 @@ public class AnthropicChatModelBuilder implements ChatModelBuilder {
}
/**
* 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.
* Detect the Claude Fable model line (e.g. {@code claude-fable-5}).
* Fable is a reasoning-first family that follows the same strict API
* contract as Claude 4.7+: temperature / top_p / top_k must be unset
* (any non-default value returns HTTP 400) and the "xhigh" adaptive
* thinking tier is available. Matching on the {@code claude-fable} token
* covers the direct-API id, the OpenRouter-prefixed form
* ({@code anthropic/claude-fable-5}), and future {@code claude-fable-N}
* revisions without a per-version code change.
*/
static boolean isClaudeFable(String modelName) {
if (modelName == null) return false;
return modelName.toLowerCase().contains("claude-fable");
}
/**
* True for any modern Claude model that drops temperature / top_p / top_k
* and exposes the "xhigh" thinking tier between high and max the Claude
* 4.7 / 4.8 generations and the Fable reasoning line.
*/
static boolean isClaude47OrLater(String modelName) {
return isClaude47(modelName) || isClaude48(modelName);
return isClaude47(modelName) || isClaude48(modelName) || isClaudeFable(modelName);
}
AnthropicChatOptions buildAnthropicOptions(ModelConfigEntity runtimeModel) {

View File

@ -86,6 +86,7 @@ public class ModelCapabilityService {
// ===== Anthropic Claude =====
// Vision yes (image), native video no Anthropic's API only accepts images.
m.put("claude-fable", EnumSet.of(Modality.VISION));
m.put("claude-4.7", EnumSet.of(Modality.VISION));
m.put("claude-4.5", EnumSet.of(Modality.VISION));
m.put("claude-4", EnumSet.of(Modality.VISION));

View File

@ -0,0 +1,25 @@
-- Add Claude Fable 5 model entries to mate_model_config. Unlike earlier Claude
-- families, the Fable rows live ONLY here, not in the data-{en,zh}.sql seed:
-- Flyway runs every version (V1..) on a fresh database, so the migration seeds
-- new installs and upgrades existing deployments alike. The trade-off is that
-- the description below is English-only (seed files carry localized copy).
--
-- Fable 5 is a reasoning-first model with a 1M-token context window and native
-- vision input. It follows the same strict API contract as Claude 4.7+:
-- temperature / top_p / top_k must be NULL (otherwise HTTP 400), and the
-- "xhigh" adaptive thinking tier is available. Both are handled in
-- AnthropicChatModelBuilder via the isClaudeFable() / isClaude47OrLater()
-- detectors. Vision capability is resolved in ModelCapabilityService.
--
-- 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
(1000000300, 'Claude Fable 5', 'anthropic', 'claude-fable-5', 'Anthropic Claude Fable 5 (1M context, vision, xhigh adaptive thinking)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
-- OpenRouter passthrough
(1000000301, 'Claude Fable 5', 'openrouter', 'anthropic/claude-fable-5', 'Claude Fable 5 via OpenRouter', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
-- Claude Code OAuth (Pro/Max subscription)
(1000000302, 'Claude Fable 5', 'anthropic-claude-code', 'claude-fable-5', 'Claude Fable 5 via Claude Code Pro/Max subscription', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0);

View File

@ -0,0 +1,37 @@
-- Add Claude Fable 5 model entries to mate_model_config. Unlike earlier Claude
-- families, the Fable rows live ONLY here, not in the data-mysql-{en,zh}.sql
-- seed: Flyway runs every version (V1..) on a fresh database, so the migration
-- seeds new installs and upgrades existing deployments alike. The trade-off is
-- that the description below is English-only (seed files carry localized copy).
--
-- Fable 5 is a reasoning-first model with a 1M-token context window and native
-- vision input. It follows the same strict API contract as Claude 4.7+:
-- temperature / top_p / top_k must be NULL (otherwise HTTP 400), and the
-- "xhigh" adaptive thinking tier is available. Both are handled in
-- AnthropicChatModelBuilder via the isClaudeFable() / isClaude47OrLater()
-- detectors. Vision capability is resolved in ModelCapabilityService.
--
-- 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
(1000000300, 'Claude Fable 5', 'anthropic', 'claude-fable-5', 'Anthropic Claude Fable 5 (1M context, vision, xhigh adaptive thinking)', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
-- OpenRouter passthrough
(1000000301, 'Claude Fable 5', 'openrouter', 'anthropic/claude-fable-5', 'Claude Fable 5 via OpenRouter', NULL, 4096, NULL, TRUE, TRUE, FALSE, NOW(), NOW(), 0),
-- Claude Code OAuth (Pro/Max subscription)
(1000000302, 'Claude Fable 5', 'anthropic-claude-code', 'claude-fable-5', 'Claude Fable 5 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,60 @@
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#isClaudeFable} must classify the Claude
* Fable reasoning line as a modern model so it inherits the strict 4.7+ API
* contract: temperature / top_p / top_k must be unset (any non-default value
* returns HTTP 400) and the "xhigh" adaptive thinking tier is available.
*
* <p>{@link AnthropicChatModelBuilder#isClaude47OrLater} unifies the gating
* logic across the 4.7 / 4.8 generations and the Fable family.</p>
*/
class AnthropicChatModelBuilderFableTest {
@Test
@DisplayName("isClaudeFable detects the direct-API and OpenRouter ids")
void detect_directAndOpenRouter() {
assertTrue(AnthropicChatModelBuilder.isClaudeFable("claude-fable-5"));
assertTrue(AnthropicChatModelBuilder.isClaudeFable("anthropic/claude-fable-5"));
// Case-insensitive
assertTrue(AnthropicChatModelBuilder.isClaudeFable("Claude-Fable-5"));
}
@Test
@DisplayName("isClaudeFable tolerates date-stamped and future revisions")
void detect_futureRevisions() {
assertTrue(AnthropicChatModelBuilder.isClaudeFable("claude-fable-5-20260609"));
assertTrue(AnthropicChatModelBuilder.isClaudeFable("claude-fable-6"));
}
@Test
@DisplayName("isClaudeFable ignores other Claude families and unrelated names")
void detect_negatives() {
assertFalse(AnthropicChatModelBuilder.isClaudeFable("claude-opus-4-8"));
assertFalse(AnthropicChatModelBuilder.isClaudeFable("claude-sonnet-4-6"));
// The "claude-fable" token guard prevents a stray "fable" elsewhere from matching.
assertFalse(AnthropicChatModelBuilder.isClaudeFable("some-fable-model"));
}
@Test
@DisplayName("isClaudeFable null-safe")
void detect_nullSafe() {
assertFalse(AnthropicChatModelBuilder.isClaudeFable(null));
assertFalse(AnthropicChatModelBuilder.isClaudeFable(""));
}
@Test
@DisplayName("isClaude47OrLater routes Fable onto the sampling-forbidden contract")
void claude47OrLater_includesFable() {
assertTrue(AnthropicChatModelBuilder.isClaude47OrLater("claude-fable-5"));
assertTrue(AnthropicChatModelBuilder.isClaude47OrLater("anthropic/claude-fable-5"));
// Sanity: existing generations still classify modern, legacy still falls through.
assertTrue(AnthropicChatModelBuilder.isClaude47OrLater("claude-opus-4-8"));
assertFalse(AnthropicChatModelBuilder.isClaude47OrLater("claude-opus-4-6"));
}
}