mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
feat: claude-fable-5
This commit is contained in:
@@ -72,6 +72,7 @@ const (
|
||||
// 与前端 useModelWhitelist.ts 中的 antigravityDefaultMappings 保持一致
|
||||
var DefaultAntigravityModelMapping = map[string]string{
|
||||
// Claude 白名单
|
||||
"claude-fable-5": "claude-fable-5", // 官方模型
|
||||
"claude-opus-4-8": "claude-opus-4-8", // 官方模型
|
||||
"claude-opus-4-7": "claude-opus-4-7", // 官方模型
|
||||
"claude-opus-4-6-thinking": "claude-opus-4-6-thinking", // 官方模型
|
||||
@@ -122,6 +123,8 @@ var DefaultAntigravityModelMapping = map[string]string{
|
||||
// 注意:此处的 "us." 前缀仅为默认值,ResolveBedrockModelID 会根据账号配置的
|
||||
// aws_region 自动调整为匹配的区域前缀(如 eu.、apac.、jp. 等)
|
||||
var DefaultBedrockModelMapping = map[string]string{
|
||||
// Claude Fable
|
||||
"claude-fable-5": "anthropic.claude-fable-5",
|
||||
// Claude Opus
|
||||
"claude-opus-4-8": "us.anthropic.claude-opus-4-8-v1",
|
||||
"claude-opus-4-7": "us.anthropic.claude-opus-4-7-v1",
|
||||
|
||||
@@ -25,26 +25,38 @@ func TestDefaultAntigravityModelMapping_ImageCompatibilityAliases(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAntigravityModelMapping_ContainsOpus48(t *testing.T) {
|
||||
func TestDefaultAntigravityModelMapping_ContainsNewClaudeModels(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got, ok := DefaultAntigravityModelMapping["claude-opus-4-8"]
|
||||
if !ok {
|
||||
t.Fatal("expected mapping for claude-opus-4-8 to exist")
|
||||
cases := map[string]string{
|
||||
"claude-fable-5": "claude-fable-5",
|
||||
"claude-opus-4-8": "claude-opus-4-8",
|
||||
}
|
||||
if got != "claude-opus-4-8" {
|
||||
t.Fatalf("unexpected claude-opus-4-8 mapping: got %q", got)
|
||||
for from, want := range cases {
|
||||
got, ok := DefaultAntigravityModelMapping[from]
|
||||
if !ok {
|
||||
t.Fatalf("expected mapping for %q to exist", from)
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("unexpected mapping for %q: got %q want %q", from, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultBedrockModelMapping_ContainsOpus48(t *testing.T) {
|
||||
func TestDefaultBedrockModelMapping_ContainsNewClaudeModels(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got, ok := DefaultBedrockModelMapping["claude-opus-4-8"]
|
||||
if !ok {
|
||||
t.Fatal("expected Bedrock mapping for claude-opus-4-8 to exist")
|
||||
cases := map[string]string{
|
||||
"claude-fable-5": "anthropic.claude-fable-5",
|
||||
"claude-opus-4-8": "us.anthropic.claude-opus-4-8-v1",
|
||||
}
|
||||
if got != "us.anthropic.claude-opus-4-8-v1" {
|
||||
t.Fatalf("unexpected Bedrock claude-opus-4-8 mapping: got %q", got)
|
||||
for from, want := range cases {
|
||||
got, ok := DefaultBedrockModelMapping[from]
|
||||
if !ok {
|
||||
t.Fatalf("expected Bedrock mapping for %q to exist", from)
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("unexpected Bedrock mapping for %q: got %q want %q", from, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +149,7 @@ type modelDef struct {
|
||||
|
||||
// Antigravity 支持的 Claude 模型
|
||||
var claudeModels = []modelDef{
|
||||
{ID: "claude-fable-5", DisplayName: "Claude Fable 5", CreatedAt: "2026-06-09T00:00:00Z"},
|
||||
{ID: "claude-opus-4-5-thinking", DisplayName: "Claude Opus 4.5 Thinking", CreatedAt: "2025-11-01T00:00:00Z"},
|
||||
{ID: "claude-sonnet-4-5", DisplayName: "Claude Sonnet 4.5", CreatedAt: "2025-09-29T00:00:00Z"},
|
||||
{ID: "claude-sonnet-4-5-thinking", DisplayName: "Claude Sonnet 4.5 Thinking", CreatedAt: "2025-09-29T00:00:00Z"},
|
||||
|
||||
@@ -12,6 +12,7 @@ func TestDefaultModels_ContainsNewAndLegacyImageModels(t *testing.T) {
|
||||
}
|
||||
|
||||
requiredIDs := []string{
|
||||
"claude-fable-5",
|
||||
"claude-opus-4-8",
|
||||
"claude-opus-4-6-thinking",
|
||||
"gemini-2.5-flash-image",
|
||||
|
||||
@@ -204,6 +204,7 @@ type modelInfo struct {
|
||||
// 只有在此映射表中的模型才会注入身份提示词
|
||||
// 注意:模型映射逻辑在网关层完成;这里仅用于按模型前缀判断是否注入身份提示词。
|
||||
var modelInfoMap = map[string]modelInfo{
|
||||
"claude-fable-5": {DisplayName: "Claude Fable 5", CanonicalID: "claude-fable-5"},
|
||||
"claude-opus-4-8": {DisplayName: "Claude Opus 4.8", CanonicalID: "claude-opus-4-8"},
|
||||
"claude-opus-4-7": {DisplayName: "Claude Opus 4.7", CanonicalID: "claude-opus-4-7"},
|
||||
"claude-opus-4-5": {DisplayName: "Claude Opus 4.5", CanonicalID: "claude-opus-4-5-20250929"},
|
||||
|
||||
@@ -116,6 +116,12 @@ type Model struct {
|
||||
|
||||
// DefaultModels Claude Code 客户端支持的默认模型列表
|
||||
var DefaultModels = []Model{
|
||||
{
|
||||
ID: "claude-fable-5",
|
||||
Type: "model",
|
||||
DisplayName: "Claude Fable 5",
|
||||
CreatedAt: "2026-06-09T00:00:00Z",
|
||||
},
|
||||
{
|
||||
ID: "claude-opus-4-5-20251101",
|
||||
Type: "model",
|
||||
|
||||
@@ -76,6 +76,12 @@ func TestAntigravityGatewayService_GetMappedModel(t *testing.T) {
|
||||
},
|
||||
|
||||
// 3. 默认映射中的透传(映射到自己)
|
||||
{
|
||||
name: "默认映射透传 - claude-fable-5",
|
||||
requestedModel: "claude-fable-5",
|
||||
accountMapping: nil,
|
||||
expected: "claude-fable-5",
|
||||
},
|
||||
{
|
||||
name: "默认映射透传 - claude-sonnet-4-6",
|
||||
requestedModel: "claude-sonnet-4-6",
|
||||
@@ -218,6 +224,7 @@ func TestAntigravityGatewayService_IsModelSupported(t *testing.T) {
|
||||
expected bool
|
||||
}{
|
||||
// 直接支持
|
||||
{"直接支持 - claude-fable-5", "claude-fable-5", true},
|
||||
{"直接支持 - claude-sonnet-4-5", "claude-sonnet-4-5", true},
|
||||
{"直接支持 - gemini-3-flash", "gemini-3-flash", true},
|
||||
|
||||
|
||||
@@ -348,6 +348,9 @@ var claudeVersionRe = regexp.MustCompile(`claude-(?:haiku|sonnet|opus)-(\d+)[-.]
|
||||
// Claude 4.5+ 支持 cache_control 中的 ttl 字段("5m" 和 "1h")
|
||||
func isBedrockClaude45OrNewer(modelID string) bool {
|
||||
lower := strings.ToLower(modelID)
|
||||
if isBedrockFable5(lower) {
|
||||
return true
|
||||
}
|
||||
matches := claudeVersionRe.FindStringSubmatch(lower)
|
||||
if matches == nil {
|
||||
return false
|
||||
@@ -658,9 +661,14 @@ func isBedrockOpus47OrNewer(modelID string) bool {
|
||||
return major > 4 || (major == 4 && minor >= 7)
|
||||
}
|
||||
|
||||
func isBedrockFable5(modelID string) bool {
|
||||
return strings.Contains(strings.ToLower(modelID), "claude-fable-5")
|
||||
}
|
||||
|
||||
const defaultThinkingBudgetTokens = 10000
|
||||
|
||||
// sanitizeBedrockThinking 修复 thinking 字段的 Bedrock 兼容性问题:
|
||||
// - Fable 5: 仅使用 always-on adaptive thinking,不支持手动 budget_tokens
|
||||
// - Opus 4.7+: 仅支持 "adaptive",将 "enabled" 转换为 "adaptive" 并移除 budget_tokens
|
||||
// - 其他模型: "enabled" 必须带 budget_tokens,缺失时补充默认值
|
||||
func sanitizeBedrockThinking(body []byte, modelID string) []byte {
|
||||
@@ -674,6 +682,16 @@ func sanitizeBedrockThinking(body []byte, modelID string) []byte {
|
||||
return body
|
||||
}
|
||||
|
||||
if isBedrockFable5(modelID) {
|
||||
if thinkingType == "enabled" {
|
||||
body, _ = sjson.SetBytes(body, "thinking.type", "adaptive")
|
||||
}
|
||||
if thinkingType == "enabled" || thinkingType == "adaptive" {
|
||||
body, _ = sjson.DeleteBytes(body, "thinking.budget_tokens")
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
if isBedrockOpus47OrNewer(modelID) {
|
||||
if thinkingType == "enabled" {
|
||||
body, _ = sjson.SetBytes(body, "thinking.type", "adaptive")
|
||||
|
||||
@@ -175,6 +175,7 @@ func TestIsBedrockClaude45OrNewer(t *testing.T) {
|
||||
}{
|
||||
{"us.anthropic.claude-opus-4-6-v1", true},
|
||||
{"us.anthropic.claude-opus-4-8-v1", true},
|
||||
{"anthropic.claude-fable-5", true},
|
||||
{"us.anthropic.claude-sonnet-4-6", true},
|
||||
{"us.anthropic.claude-sonnet-4-5-20250929-v1:0", true},
|
||||
{"us.anthropic.claude-opus-4-5-20251101-v1:0", true},
|
||||
@@ -526,6 +527,20 @@ func TestResolveBedrockModelID(t *testing.T) {
|
||||
assert.Equal(t, "eu.anthropic.claude-opus-4-8-v1", modelID)
|
||||
})
|
||||
|
||||
t.Run("默认 Fable 5 映射使用官方 Bedrock 模型 ID", func(t *testing.T) {
|
||||
account := &Account{
|
||||
Platform: PlatformAnthropic,
|
||||
Type: AccountTypeBedrock,
|
||||
Credentials: map[string]any{
|
||||
"aws_region": "eu-west-1",
|
||||
},
|
||||
}
|
||||
|
||||
modelID, ok := ResolveBedrockModelID(account, "claude-fable-5")
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "anthropic.claude-fable-5", modelID)
|
||||
})
|
||||
|
||||
t.Run("force global rewrites anthropic regional model id", func(t *testing.T) {
|
||||
account := &Account{
|
||||
Platform: PlatformAnthropic,
|
||||
@@ -750,6 +765,20 @@ func TestIsBedrockOpus47OrNewer(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSanitizeBedrockThinking(t *testing.T) {
|
||||
t.Run("Fable 5 将 enabled 转换为 adaptive 并移除预算", func(t *testing.T) {
|
||||
input := `{"thinking":{"type":"enabled","budget_tokens":10000},"messages":[]}`
|
||||
result := sanitizeBedrockThinking([]byte(input), "anthropic.claude-fable-5")
|
||||
assert.Equal(t, "adaptive", gjson.GetBytes(result, "thinking.type").String())
|
||||
assert.False(t, gjson.GetBytes(result, "thinking.budget_tokens").Exists())
|
||||
})
|
||||
|
||||
t.Run("Fable 5 adaptive 移除预算", func(t *testing.T) {
|
||||
input := `{"thinking":{"type":"adaptive","budget_tokens":10000},"messages":[]}`
|
||||
result := sanitizeBedrockThinking([]byte(input), "claude-fable-5")
|
||||
assert.Equal(t, "adaptive", gjson.GetBytes(result, "thinking.type").String())
|
||||
assert.False(t, gjson.GetBytes(result, "thinking.budget_tokens").Exists())
|
||||
})
|
||||
|
||||
t.Run("opus 4.7 converts enabled to adaptive", func(t *testing.T) {
|
||||
input := `{"thinking":{"type":"enabled","budget_tokens":10000},"messages":[]}`
|
||||
result := sanitizeBedrockThinking([]byte(input), "us.anthropic.claude-opus-4-7-v1")
|
||||
|
||||
@@ -220,6 +220,7 @@ const activeModelStatuses = computed<AccountModelStatusItem[]>(() => {
|
||||
const formatScopeName = (scope: string): string => {
|
||||
const aliases: Record<string, string> = {
|
||||
// Claude 系列
|
||||
'claude-fable-5': 'CFable5',
|
||||
'claude-opus-4-6': 'COpus46',
|
||||
'claude-opus-4-6-thinking': 'COpus46T',
|
||||
'claude-opus-4-7': 'COpus47',
|
||||
|
||||
@@ -662,6 +662,7 @@ const antigravity3ImageUsageFromAPI = computed(() =>
|
||||
// Claude from API (all Claude model variants)
|
||||
const antigravityClaudeUsageFromAPI = computed(() =>
|
||||
getAntigravityUsageFromAPI([
|
||||
'claude-fable-5',
|
||||
'claude-sonnet-4-5', 'claude-opus-4-5-thinking',
|
||||
'claude-sonnet-4-6', 'claude-opus-4-6', 'claude-opus-4-6-thinking',
|
||||
'claude-opus-4-7', 'claude-opus-4-8',
|
||||
|
||||
@@ -966,6 +966,22 @@ function generateOpenCodeConfig(platform: string, baseUrl: string, apiKey: strin
|
||||
}
|
||||
}
|
||||
const claudeModels = {
|
||||
'claude-fable-5': {
|
||||
name: 'Claude Fable 5',
|
||||
limit: {
|
||||
context: 1048576,
|
||||
output: 128000
|
||||
},
|
||||
modalities: {
|
||||
input: ['text', 'image', 'pdf'],
|
||||
output: ['text']
|
||||
},
|
||||
options: {
|
||||
thinking: {
|
||||
type: 'adaptive'
|
||||
}
|
||||
}
|
||||
},
|
||||
'claude-opus-4-6-thinking': {
|
||||
name: 'Claude 4.6 Opus (Thinking)',
|
||||
limit: {
|
||||
|
||||
@@ -122,4 +122,46 @@ describe('UseKeyModal', () => {
|
||||
expect(codeBlock.text()).toContain('"name": "GPT-5.4 Mini"')
|
||||
expect(codeBlock.text()).not.toContain('"name": "GPT-5.4 Nano"')
|
||||
})
|
||||
|
||||
it('renders Claude Fable 5 OpenCode config with adaptive thinking', async () => {
|
||||
const wrapper = mount(UseKeyModal, {
|
||||
props: {
|
||||
show: true,
|
||||
apiKey: 'sk-test',
|
||||
baseUrl: 'https://example.com/v1',
|
||||
platform: 'antigravity'
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
BaseDialog: {
|
||||
template: '<div><slot /><slot name="footer" /></div>'
|
||||
},
|
||||
Icon: {
|
||||
template: '<span />'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const opencodeTab = wrapper.findAll('button').find((button) =>
|
||||
button.text().includes('keys.useKeyModal.cliTabs.opencode')
|
||||
)
|
||||
|
||||
expect(opencodeTab).toBeDefined()
|
||||
await opencodeTab!.trigger('click')
|
||||
await nextTick()
|
||||
|
||||
const claudeConfig = wrapper.findAll('pre code')
|
||||
.map((code) => code.text())
|
||||
.find((content) => content.includes('"antigravity-claude"'))
|
||||
|
||||
expect(claudeConfig).toBeDefined()
|
||||
const parsed = JSON.parse(claudeConfig!)
|
||||
const fable = parsed.provider['antigravity-claude'].models['claude-fable-5']
|
||||
|
||||
expect(fable.name).toBe('Claude Fable 5')
|
||||
expect(fable.limit).toEqual({ context: 1048576, output: 128000 })
|
||||
expect(fable.options.thinking).toEqual({ type: 'adaptive' })
|
||||
expect(fable.options.thinking).not.toHaveProperty('budgetTokens')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -35,7 +35,9 @@ describe('useModelWhitelist', () => {
|
||||
expect(models).toContain('gemini-3-pro-image')
|
||||
})
|
||||
|
||||
it('Claude 模型列表包含 Opus 4.8', () => {
|
||||
it('Claude 模型列表包含新发布的 Claude 模型', () => {
|
||||
expect(getModelsByPlatform('claude')).toContain('claude-fable-5')
|
||||
expect(getModelsByPlatform('antigravity')).toContain('claude-fable-5')
|
||||
expect(getModelsByPlatform('claude')).toContain('claude-opus-4-8')
|
||||
expect(getModelsByPlatform('antigravity')).toContain('claude-opus-4-8')
|
||||
})
|
||||
|
||||
@@ -30,7 +30,8 @@ export const claudeModels = [
|
||||
'claude-opus-4-6',
|
||||
'claude-opus-4-7',
|
||||
'claude-opus-4-8',
|
||||
'claude-sonnet-4-6'
|
||||
'claude-sonnet-4-6',
|
||||
'claude-fable-5'
|
||||
]
|
||||
|
||||
// Google Gemini
|
||||
@@ -51,6 +52,7 @@ const geminiModels = [
|
||||
// 基于官方 API 返回的模型列表,只支持 Claude 4.5+ 和 Gemini 2.5+
|
||||
const antigravityModels = [
|
||||
// Claude 4.5+ 系列
|
||||
'claude-fable-5',
|
||||
'claude-opus-4-6',
|
||||
'claude-opus-4-6-thinking',
|
||||
'claude-opus-4-7',
|
||||
@@ -234,6 +236,7 @@ export const allModels = allModelsList.map(m => ({ value: m, label: m }))
|
||||
// =====================
|
||||
|
||||
const anthropicPresetMappings = [
|
||||
{ label: 'Fable 5', from: 'claude-fable-5', to: 'claude-fable-5', color: 'bg-rose-100 text-rose-700 hover:bg-rose-200 dark:bg-rose-900/30 dark:text-rose-400' },
|
||||
{ label: 'Sonnet 4', from: 'claude-sonnet-4-20250514', to: 'claude-sonnet-4-20250514', color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400' },
|
||||
{ label: 'Sonnet 4.5', from: 'claude-sonnet-4-5-20250929', to: 'claude-sonnet-4-5-20250929', color: 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400' },
|
||||
{ label: 'Sonnet 4.6', from: 'claude-sonnet-4-6', to: 'claude-sonnet-4-6', color: 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400' },
|
||||
@@ -274,6 +277,7 @@ const geminiPresetMappings = [
|
||||
const antigravityPresetMappings = [
|
||||
// Claude 通配符映射
|
||||
{ label: 'Claude→Sonnet', from: 'claude-*', to: 'claude-sonnet-4-5', color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400' },
|
||||
{ label: 'Fable 5', from: 'claude-fable-5', to: 'claude-fable-5', color: 'bg-rose-100 text-rose-700 hover:bg-rose-200 dark:bg-rose-900/30 dark:text-rose-400' },
|
||||
{ label: 'Sonnet→Sonnet', from: 'claude-sonnet-*', to: 'claude-sonnet-4-5', color: 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400' },
|
||||
{ label: 'Opus→Opus', from: 'claude-opus-*', to: 'claude-opus-4-6-thinking', color: 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' },
|
||||
{ label: 'Haiku→Sonnet', from: 'claude-haiku-*', to: 'claude-sonnet-4-5', color: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400' },
|
||||
@@ -306,6 +310,7 @@ const antigravityPresetMappings = [
|
||||
|
||||
// Bedrock 预设映射(与后端 DefaultBedrockModelMapping 保持一致)
|
||||
const bedrockPresetMappings = [
|
||||
{ label: 'Fable 5', from: 'claude-fable-5', to: 'anthropic.claude-fable-5', color: 'bg-rose-100 text-rose-700 hover:bg-rose-200 dark:bg-rose-900/30 dark:text-rose-400' },
|
||||
{ label: 'Opus 4.6', from: 'claude-opus-4-6', to: 'us.anthropic.claude-opus-4-6-v1', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400' },
|
||||
{ label: 'Opus 4.7', from: 'claude-opus-4-7', to: 'us.anthropic.claude-opus-4-7-v1', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400' },
|
||||
{ label: 'Opus 4.8', from: 'claude-opus-4-8', to: 'us.anthropic.claude-opus-4-8-v1', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400' },
|
||||
|
||||
Reference in New Issue
Block a user