Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions backend/internal/domain/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,16 +168,18 @@ var DefaultBedrockModelMapping = map[string]string{
"claude-fable-5-1": "anthropic.claude-fable-5-1",
"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",
// 新版模型使用无版本后缀的 Bedrock ID,不能沿用 Opus 4.6 的 -v1。
"claude-opus-5": "us.anthropic.claude-opus-5",
"claude-opus-4-8": "us.anthropic.claude-opus-4-8",
"claude-opus-4-7": "us.anthropic.claude-opus-4-7",
"claude-opus-4-6-thinking": "us.anthropic.claude-opus-4-6-v1",
"claude-opus-4-6": "us.anthropic.claude-opus-4-6-v1",
"claude-opus-4-5-thinking": "us.anthropic.claude-opus-4-5-20251101-v1:0",
"claude-opus-4-5-20251101": "us.anthropic.claude-opus-4-5-20251101-v1:0",
"claude-opus-4-1": "us.anthropic.claude-opus-4-1-20250805-v1:0",
"claude-opus-4-20250514": "us.anthropic.claude-opus-4-20250514-v1:0",
// Claude Sonnet
"claude-sonnet-5": "us.anthropic.claude-sonnet-5-v1",
"claude-sonnet-5": "us.anthropic.claude-sonnet-5",
"claude-sonnet-4-6-thinking": "us.anthropic.claude-sonnet-4-6",
"claude-sonnet-4-6": "us.anthropic.claude-sonnet-4-6",
"claude-sonnet-4-5": "us.anthropic.claude-sonnet-4-5-20250929-v1:0",
Expand Down
8 changes: 7 additions & 1 deletion backend/internal/domain/constants_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,13 @@ func TestDefaultBedrockModelMapping_ContainsNewClaudeModels(t *testing.T) {
cases := map[string]string{
"claude-fable-5-1": "anthropic.claude-fable-5-1",
"claude-fable-5": "anthropic.claude-fable-5",
"claude-opus-4-8": "us.anthropic.claude-opus-4-8-v1",
"claude-opus-5": "us.anthropic.claude-opus-5",
"claude-opus-4-8": "us.anthropic.claude-opus-4-8",
"claude-opus-4-7": "us.anthropic.claude-opus-4-7",
"claude-sonnet-5": "us.anthropic.claude-sonnet-5",
// 旧型号合法的版本及日期后缀必须保留。
"claude-opus-4-6": "us.anthropic.claude-opus-4-6-v1",
"claude-opus-4-5-20251101": "us.anthropic.claude-opus-4-5-20251101-v1:0",
}
for from, want := range cases {
got, ok := DefaultBedrockModelMapping[from]
Expand Down
6 changes: 6 additions & 0 deletions backend/internal/pkg/claude/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,12 @@ var DefaultModels = []Model{
DisplayName: "Claude Opus 4.8",
CreatedAt: "2026-05-29T00:00:00Z",
},
{
ID: "claude-opus-5",
Type: "model",
DisplayName: "Claude Opus 5",
CreatedAt: "2026-07-25T00:00:00Z",
},
{
ID: "claude-sonnet-5",
Type: "model",
Expand Down
9 changes: 9 additions & 0 deletions backend/internal/pkg/claude/constants_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,12 @@ func TestDefaultModels_ContainsClaudeFable51(t *testing.T) {
}
t.Fatal("expected claude-fable-5-1 to be exposed in DefaultModels")
}

func TestDefaultModels_ContainsClaudeOpus5(t *testing.T) {
for _, model := range DefaultModels {
if model.ID == "claude-opus-5" && model.DisplayName == "Claude Opus 5" {
return
}
}
t.Fatal("expected claude-opus-5 to be exposed in DefaultModels")
}
20 changes: 15 additions & 5 deletions backend/internal/service/bedrock_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,8 +348,9 @@ func removeCustomFieldFromTools(body []byte) []byte {
}

// claudeVersionRe 匹配 Claude 模型 ID 中的版本号部分
// 支持 claude-{tier}-{major}-{minor} 和 claude-{tier}-{major}.{minor} 格式
var claudeVersionRe = regexp.MustCompile(`claude-(?:haiku|sonnet|opus)-(\d+)[-.](\d+)`)
// 支持 claude-{tier}-{major}、claude-{tier}-{major}-{minor} 和 claude-{tier}-{major}.{minor} 格式;
// 省略 minor 时按 0 处理,以兼容 Claude 5 这类主版本模型 ID。
var claudeVersionRe = regexp.MustCompile(`claude-(?:haiku|sonnet|opus)-(\d+)(?:[-.](\d+))?`)

// isBedrockClaude45OrNewer 判断 Bedrock 模型 ID 是否为 Claude 4.5 或更新版本
// Claude 4.5+ 支持 cache_control 中的 ttl 字段("5m" 和 "1h")
Expand All @@ -363,7 +364,10 @@ func isBedrockClaude45OrNewer(modelID string) bool {
return false
}
major, _ := strconv.Atoi(matches[1])
minor, _ := strconv.Atoi(matches[2])
minor := 0
if len(matches) > 2 && matches[2] != "" {
minor, _ = strconv.Atoi(matches[2])
}
return major > 4 || (major == 4 && minor >= 5)
}

Expand Down Expand Up @@ -602,7 +606,10 @@ func bedrockModelSupportsToolSearch(modelID string) bool {
return false
}
major, _ := strconv.Atoi(matches[1])
minor, _ := strconv.Atoi(matches[2])
minor := 0
if len(matches) > 2 && matches[2] != "" {
minor, _ = strconv.Atoi(matches[2])
}
return major > 4 || (major == 4 && minor >= 5)
}

Expand Down Expand Up @@ -684,7 +691,10 @@ func isBedrockOpus47OrNewer(modelID string) bool {
return false
}
major, _ := strconv.Atoi(matches[1])
minor, _ := strconv.Atoi(matches[2])
minor := 0
if len(matches) > 2 && matches[2] != "" {
minor, _ = strconv.Atoi(matches[2])
}
return major > 4 || (major == 4 && minor >= 7)
}

Expand Down
66 changes: 50 additions & 16 deletions backend/internal/service/bedrock_request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ func TestIsBedrockClaude45OrNewer(t *testing.T) {
expect bool
}{
{"us.anthropic.claude-opus-4-6-v1", true},
{"us.anthropic.claude-opus-4-8-v1", true},
{"us.anthropic.claude-opus-4-8", true},
{"anthropic.claude-fable-5", true},
{"us.anthropic.claude-sonnet-4-6", true},
{"us.anthropic.claude-sonnet-4-5-20250929-v1:0", true},
Expand All @@ -185,7 +185,7 @@ func TestIsBedrockClaude45OrNewer(t *testing.T) {
{"anthropic.claude-3-haiku-20240307-v1:0", false},
// 未来版本应自动支持
{"us.anthropic.claude-sonnet-5-0-v1", true},
{"us.anthropic.claude-opus-4-7-v1", true},
{"us.anthropic.claude-opus-4-7", true},
// 旧版本
{"anthropic.claude-opus-4-1-v1", false},
{"anthropic.claude-sonnet-4-0-v1", false},
Expand Down Expand Up @@ -524,7 +524,7 @@ func TestResolveBedrockModelID(t *testing.T) {

modelID, ok := ResolveBedrockModelID(account, "claude-opus-4-8")
require.True(t, ok)
assert.Equal(t, "eu.anthropic.claude-opus-4-8-v1", modelID)
assert.Equal(t, "eu.anthropic.claude-opus-4-8", modelID)
})

t.Run("默认 Fable 5 映射使用官方 Bedrock 模型 ID", func(t *testing.T) {
Expand Down Expand Up @@ -601,6 +601,40 @@ func TestResolveBedrockModelID(t *testing.T) {
})
}

// 默认别名必须生成官方请求地址,且无版本后缀的模型仍保留新版缓存能力。
func TestResolveBedrockModelID_OfficialVersionlessModels(t *testing.T) {
t.Parallel()
for _, model := range []string{"claude-opus-4-7", "claude-opus-4-8", "claude-opus-5", "claude-sonnet-5"} {
t.Run(model, func(t *testing.T) {
for _, scope := range []struct {
name, region, prefix string
forceGlobal bool
}{
{name: "区域推理", region: "eu-west-1", prefix: "eu"},
{name: "全局推理", region: "us-east-1", prefix: "global", forceGlobal: true},
} {
t.Run(scope.name, func(t *testing.T) {
account := &Account{Platform: PlatformAnthropic, Type: AccountTypeBedrock, Credentials: map[string]any{
"aws_region": scope.region,
}}
if scope.forceGlobal {
account.Credentials["aws_force_global"] = "true"
}
modelID, ok := ResolveBedrockModelID(account, model)
require.True(t, ok)
wantID := scope.prefix + ".anthropic." + model
require.Equal(t, wantID, modelID)
require.Equal(t, "https://bedrock-runtime."+scope.region+".amazonaws.com/model/"+wantID+"/invoke", BuildBedrockURL(scope.region, modelID, false))

body, err := PrepareBedrockRequestBody([]byte(`{"system":[{"type":"text","text":"system","cache_control":{"type":"ephemeral","ttl":"1h"}}],"messages":[{"role":"user","content":"hello"}],"max_tokens":16}`), modelID, "")
require.NoError(t, err)
require.Equal(t, "1h", gjson.GetBytes(body, "system.0.cache_control.ttl").String())
})
}
})
}
}

func TestAutoInjectBedrockBetaTokens(t *testing.T) {
t.Run("no auto-inject for thinking (interleaved-thinking not supported)", func(t *testing.T) {
body := []byte(`{"thinking":{"type":"enabled","budget_tokens":10000},"messages":[{"role":"user","content":"hi"}]}`)
Expand Down Expand Up @@ -758,8 +792,8 @@ func TestIsBedrockOpus47OrNewer(t *testing.T) {
modelID string
expect bool
}{
{"us.anthropic.claude-opus-4-8-v1", true},
{"us.anthropic.claude-opus-4-7-v1", true},
{"us.anthropic.claude-opus-4-8", true},
{"us.anthropic.claude-opus-4-7", true},
{"us.anthropic.claude-opus-4-6-v1", false},
{"us.anthropic.claude-opus-4-5-20251101-v1:0", false},
{"us.anthropic.claude-opus-5-0-v1", true},
Expand Down Expand Up @@ -795,14 +829,14 @@ func TestSanitizeBedrockThinking(t *testing.T) {

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")
result := sanitizeBedrockThinking([]byte(input), "us.anthropic.claude-opus-4-7")
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 keeps adaptive unchanged", func(t *testing.T) {
input := `{"thinking":{"type":"adaptive"},"messages":[]}`
result := sanitizeBedrockThinking([]byte(input), "us.anthropic.claude-opus-4-7-v1")
result := sanitizeBedrockThinking([]byte(input), "us.anthropic.claude-opus-4-7")
assert.Equal(t, "adaptive", gjson.GetBytes(result, "thinking.type").String())
})

Expand All @@ -822,7 +856,7 @@ func TestSanitizeBedrockThinking(t *testing.T) {

t.Run("no thinking field unchanged", func(t *testing.T) {
input := `{"messages":[]}`
result := sanitizeBedrockThinking([]byte(input), "us.anthropic.claude-opus-4-7-v1")
result := sanitizeBedrockThinking([]byte(input), "us.anthropic.claude-opus-4-7")
assert.JSONEq(t, input, string(result))
})

Expand Down Expand Up @@ -894,32 +928,32 @@ func TestSanitizeBedrockToolUseIDs(t *testing.T) {
func TestSanitizeBedrockThinking_EdgeCases(t *testing.T) {
t.Run("opus 4.7 enabled without budget_tokens converts to adaptive", func(t *testing.T) {
input := `{"thinking":{"type":"enabled"},"messages":[]}`
result := sanitizeBedrockThinking([]byte(input), "us.anthropic.claude-opus-4-7-v1")
result := sanitizeBedrockThinking([]byte(input), "us.anthropic.claude-opus-4-7")
assert.Equal(t, "adaptive", gjson.GetBytes(result, "thinking.type").String())
assert.False(t, gjson.GetBytes(result, "thinking.budget_tokens").Exists())
})

t.Run("thinking type disabled unchanged", func(t *testing.T) {
input := `{"thinking":{"type":"disabled"},"messages":[]}`
result := sanitizeBedrockThinking([]byte(input), "us.anthropic.claude-opus-4-7-v1")
result := sanitizeBedrockThinking([]byte(input), "us.anthropic.claude-opus-4-7")
assert.Equal(t, "disabled", gjson.GetBytes(result, "thinking.type").String())
})

t.Run("thinking type empty string unchanged", func(t *testing.T) {
input := `{"thinking":{"type":""},"messages":[]}`
result := sanitizeBedrockThinking([]byte(input), "us.anthropic.claude-opus-4-7-v1")
result := sanitizeBedrockThinking([]byte(input), "us.anthropic.claude-opus-4-7")
assert.JSONEq(t, input, string(result))
})

t.Run("thinking is not an object unchanged", func(t *testing.T) {
input := `{"thinking":true,"messages":[]}`
result := sanitizeBedrockThinking([]byte(input), "us.anthropic.claude-opus-4-7-v1")
result := sanitizeBedrockThinking([]byte(input), "us.anthropic.claude-opus-4-7")
assert.JSONEq(t, input, string(result))
})

t.Run("opus 4.7 adaptive with budget_tokens preserved", func(t *testing.T) {
input := `{"thinking":{"type":"adaptive","budget_tokens":5000},"messages":[]}`
result := sanitizeBedrockThinking([]byte(input), "us.anthropic.claude-opus-4-7-v1")
result := sanitizeBedrockThinking([]byte(input), "us.anthropic.claude-opus-4-7")
assert.Equal(t, "adaptive", gjson.GetBytes(result, "thinking.type").String())
assert.Equal(t, int64(5000), gjson.GetBytes(result, "thinking.budget_tokens").Int())
})
Expand All @@ -945,8 +979,8 @@ func TestIsBedrockOpus47OrNewer_EdgeCases(t *testing.T) {
modelID string
expect bool
}{
{"anthropic.claude-opus-4-8-v1", true},
{"anthropic.claude-opus-4-7-v1", true},
{"anthropic.claude-opus-4-8", true},
{"anthropic.claude-opus-4-7", true},
{"us.anthropic.claude-opus-4-7-20270101-v1:0", true},
{"", false},
// Forward() 传入的是 parsed.Model(标准模型名),不一定是 Bedrock 模型 ID。
Expand Down Expand Up @@ -992,7 +1026,7 @@ func TestPrepareBedrockRequestBodyWithTokens_CCCompat(t *testing.T) {
})

t.Run("ccCompat=true converts thinking to adaptive for opus 4.7", func(t *testing.T) {
result, err := PrepareBedrockRequestBodyWithTokens([]byte(input), "us.anthropic.claude-opus-4-7-v1", nil, true)
result, err := PrepareBedrockRequestBodyWithTokens([]byte(input), "us.anthropic.claude-opus-4-7", nil, true)
require.NoError(t, err)
assert.Equal(t, "adaptive", gjson.GetBytes(result, "thinking.type").String())
assert.False(t, gjson.GetBytes(result, "thinking.budget_tokens").Exists())
Expand Down
4 changes: 4 additions & 0 deletions backend/internal/service/billing_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,7 @@ func (s *BillingService) initFallbackPricing() {
SupportsCacheBreakdown: true,
SupportsServiceTier: true,
}
s.fallbackPrices["claude-opus-5"] = s.fallbackPrices["claude-opus-4.8"]

// Claude Fable 5.x 的输入/输出和缓存写入价格相同;5.1 的缓存读取价降为每百万 token 0.25 美元。
s.fallbackPrices["claude-fable-5"] = &ModelPricing{
Expand Down Expand Up @@ -883,6 +884,9 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing {
return s.fallbackPrices["claude-fable-5"]
}
if strings.Contains(modelLower, "opus") {
if strings.Contains(modelLower, "opus-5") || strings.Contains(modelLower, "opus5") {
return s.fallbackPrices["claude-opus-5"]
}
if strings.Contains(modelLower, "4.8") || strings.Contains(modelLower, "4-8") {
return s.fallbackPrices["claude-opus-4.8"]
}
Expand Down
7 changes: 7 additions & 0 deletions backend/internal/service/gateway_beta_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,11 @@ func TestDefaultBetaPolicy_Context1M_Sonnet5Whitelist(t *testing.T) {
// Vertex AI 归一化后的 sonnet-5 也应放行。
{"claude-sonnet-5@20260701", BetaPolicyActionPass, "sonnet-5 Vertex-normalized dated form"},
// AWS Bedrock 各跨区域前缀 sonnet-5 也应放行。
{"us.anthropic.claude-sonnet-5", BetaPolicyActionPass, "bedrock us. canonical sonnet-5"},
{"eu.anthropic.claude-sonnet-5", BetaPolicyActionPass, "bedrock eu. canonical sonnet-5"},
{"au.anthropic.claude-sonnet-5", BetaPolicyActionPass, "bedrock au. canonical sonnet-5"},
{"global.anthropic.claude-sonnet-5", BetaPolicyActionPass, "bedrock global. canonical sonnet-5"},
{"anthropic.claude-sonnet-5", BetaPolicyActionPass, "bedrock canonical sonnet-5 without region"},
{"us.anthropic.claude-sonnet-5-v1", BetaPolicyActionPass, "bedrock us. sonnet-5"},
{"eu.anthropic.claude-sonnet-5-20260701-v1:0", BetaPolicyActionPass, "bedrock eu. sonnet-5 dated"},
{"apac.anthropic.claude-sonnet-5-v1", BetaPolicyActionPass, "bedrock apac. sonnet-5"},
Expand All @@ -293,6 +298,8 @@ func TestDefaultBetaPolicy_Context1M_Sonnet5Whitelist(t *testing.T) {
{"claude-opus-4-8", BetaPolicyActionFilter, "opus must be filtered"},
{"claude-opus-4-7", BetaPolicyActionFilter, "opus 4.7 must be filtered"},
{"us.anthropic.claude-opus-4-8-v1", BetaPolicyActionFilter, "bedrock opus 4.8 must be filtered"},
{"claude-opus-5", BetaPolicyActionFilter, "opus 5 must be filtered"},
{"us.anthropic.claude-opus-5", BetaPolicyActionFilter, "bedrock canonical opus 5 must be filtered"},
{"claude-haiku-4-5", BetaPolicyActionFilter, "haiku must be filtered"},
{"us.anthropic.claude-haiku-4-5-20251001-v1:0", BetaPolicyActionFilter, "bedrock haiku must be filtered"},
{"claude-3-5-sonnet-20241022", BetaPolicyActionFilter, "legacy sonnet 3.5 must be filtered"},
Expand Down
3 changes: 3 additions & 0 deletions backend/internal/service/pricing_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -1324,6 +1324,7 @@ func (s *PricingService) matchByModelFamily(model string) *LiteLLMModelPricing {
// 因子串关系误匹配 "claude-opus-4-7"(opus-4.7 系列)。
// 注意:原 map 实现存在 Go map 迭代随机性导致的同类 bug,此处改为有序切片修复。
families := []modelFamily{
{name: "opus-5", match: []string{"claude-opus-5"}, pricing: []string{"claude-opus-5", "claude-opus-4-8", "claude-opus-4.8"}},
{name: "opus-4.7", match: []string{"claude-opus-4-7", "claude-opus-4.7"}, pricing: []string{"claude-opus-4-7", "claude-opus-4.7", "claude-opus-4-6"}},
{name: "opus-4.6", match: []string{"claude-opus-4-6", "claude-opus-4.6"}},
{name: "opus-4.5", match: []string{"claude-opus-4-5", "claude-opus-4.5"}},
Expand Down Expand Up @@ -1356,6 +1357,8 @@ func (s *PricingService) matchByModelFamily(model string) *LiteLLMModelPricing {
switch {
case strings.Contains(model, "opus"):
switch {
case strings.Contains(model, "opus-5") || strings.Contains(model, "opus5"):
fallbackName = "opus-5"
case strings.Contains(model, "4.7") || strings.Contains(model, "4-7"):
fallbackName = "opus-4.7"
case strings.Contains(model, "4.6") || strings.Contains(model, "4-6"):
Expand Down
6 changes: 6 additions & 0 deletions backend/internal/service/settings_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -691,21 +691,27 @@ func DefaultBetaPolicySettings() *BetaPolicySettings {
// Vertex AI 走 normalizeVertexAnthropicModelID 后为 "@YYYYMMDD" 格式
"claude-sonnet-5@*",
// AWS Bedrock cross-region inference profile
// 当前模型 ID 不带版本后缀,精确项不能只由 -* 或 -v* 覆盖。
"us.anthropic.claude-sonnet-5",
"us.anthropic.claude-sonnet-5-v*",
"us.anthropic.claude-sonnet-5-*",
"eu.anthropic.claude-sonnet-5",
"eu.anthropic.claude-sonnet-5-v*",
"eu.anthropic.claude-sonnet-5-*",
"apac.anthropic.claude-sonnet-5-v*",
"apac.anthropic.claude-sonnet-5-*",
"jp.anthropic.claude-sonnet-5-v*",
"jp.anthropic.claude-sonnet-5-*",
"au.anthropic.claude-sonnet-5",
"au.anthropic.claude-sonnet-5-v*",
"au.anthropic.claude-sonnet-5-*",
"us-gov.anthropic.claude-sonnet-5-v*",
"us-gov.anthropic.claude-sonnet-5-*",
"global.anthropic.claude-sonnet-5",
"global.anthropic.claude-sonnet-5-v*",
"global.anthropic.claude-sonnet-5-*",
// AWS Bedrock 无 cross-region 前缀
"anthropic.claude-sonnet-5",
"anthropic.claude-sonnet-5-v*",
"anthropic.claude-sonnet-5-*",
},
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/account/AccountStatusIndicator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ const formatScopeName = (scope: string): string => {
'claude-opus-4-6-thinking': 'COpus46T',
'claude-opus-4-7': 'COpus47',
'claude-opus-4-8': 'COpus48',
'claude-opus-5': 'COpus5',
'claude-sonnet-4-6': 'CSon46',
'claude-sonnet-4-5': 'CSon45',
'claude-sonnet-4-5-thinking': 'CSon45T',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,30 @@ describe('AccountStatusIndicator', () => {
expect(wrapper.text()).not.toContain('claude-sonnet-5')
})

it('Claude Opus 5 模型限流时显示短别名', () => {
const wrapper = mount(AccountStatusIndicator, {
props: {
account: makeAccount({
extra: {
model_rate_limits: {
'claude-opus-5': {
rate_limited_at: '2026-07-28T00:00:00Z',
rate_limit_reset_at: '2099-07-28T00:00:00Z'
}
}
}
})
},
global: {
stubs: {
Icon: true
}
}
})
expect(wrapper.text()).toContain('COpus5')
expect(wrapper.text()).not.toContain('claude-opus-5')
})

it('Grok 账号额度限流时显示自动恢复时间而非临时不可调度', () => {
const wrapper = mount(AccountStatusIndicator, {
props: {
Expand Down
Loading