diff --git a/backend/internal/domain/constants.go b/backend/internal/domain/constants.go index 844348c024..0d3bf88705 100644 --- a/backend/internal/domain/constants.go +++ b/backend/internal/domain/constants.go @@ -136,6 +136,7 @@ var DefaultBedrockModelMapping = map[string]string{ "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-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", diff --git a/backend/internal/pkg/anthropicfp/dateline.go b/backend/internal/pkg/anthropicfp/dateline.go index 6428433506..ab07765582 100644 --- a/backend/internal/pkg/anthropicfp/dateline.go +++ b/backend/internal/pkg/anthropicfp/dateline.go @@ -28,8 +28,8 @@ import ( // user-authored prose like "Today is foo." or "His date is 2026-06-30." from // being touched. var ( - datelineRegexHyphen = regexp.MustCompile("Today(['’ʼʹ])s date is (\\d{4})-(\\d{2})-(\\d{2})\\.") - datelineRegexSlash = regexp.MustCompile("Today(['’ʼʹ])s date is (\\d{4})/(\\d{2})/(\\d{2})\\.") + datelineRegexHyphen = regexp.MustCompile(`Today(['’ʼʹ])s date is (\d{4})-(\d{2})-(\d{2})\.`) + datelineRegexSlash = regexp.MustCompile(`Today(['’ʼʹ])s date is (\d{4})/(\d{2})/(\d{2})\.`) ) // systemReminderRegex matches a block. The dateline lives in @@ -66,10 +66,10 @@ func apostropheVariant(r rune) string { } type datelineMatch struct { - start, end int - apoRune rune - sep string - year, month, day string + start, end int + apoRune rune + sep string + year, month, day string } func collectMatches(text string, re *regexp.Regexp, sep string) []datelineMatch { @@ -124,8 +124,8 @@ func NormalizeText(text string) (string, []DatelineHit) { // Already canonical: no rewrite, no hit. continue } - b.WriteString(text[prev:m.start]) - b.WriteString(canonical) + _, _ = b.WriteString(text[prev:m.start]) + _, _ = b.WriteString(canonical) prev = m.end changed = true hits = append(hits, DatelineHit{ @@ -136,7 +136,7 @@ func NormalizeText(text string) (string, []DatelineHit) { if !changed { return text, nil } - b.WriteString(text[prev:]) + _, _ = b.WriteString(text[prev:]) return b.String(), hits } @@ -159,20 +159,20 @@ func normalizeSystemReminderScopedText(text string) (string, []DatelineHit) { var hits []DatelineHit changed := false for _, loc := range locs { - b.WriteString(text[prev:loc[0]]) + _, _ = b.WriteString(text[prev:loc[0]]) block := text[loc[0]:loc[1]] normalized, blockHits := NormalizeText(block) if normalized != block { changed = true } - b.WriteString(normalized) + _, _ = b.WriteString(normalized) hits = append(hits, blockHits...) prev = loc[1] } if !changed { return text, nil } - b.WriteString(text[prev:]) + _, _ = b.WriteString(text[prev:]) return b.String(), hits } diff --git a/backend/internal/pkg/claude/constants.go b/backend/internal/pkg/claude/constants.go index 06335aeda9..b159f8f5a5 100644 --- a/backend/internal/pkg/claude/constants.go +++ b/backend/internal/pkg/claude/constants.go @@ -146,6 +146,12 @@ var DefaultModels = []Model{ DisplayName: "Claude Opus 4.8", CreatedAt: "2026-05-29T00:00:00Z", }, + { + ID: "claude-sonnet-5", + Type: "model", + DisplayName: "Claude Sonnet 5", + CreatedAt: "2026-07-01T00:00:00Z", + }, { ID: "claude-sonnet-4-6", Type: "model", diff --git a/backend/internal/service/gateway_beta_test.go b/backend/internal/service/gateway_beta_test.go index 6919c148f3..294b634eb7 100644 --- a/backend/internal/service/gateway_beta_test.go +++ b/backend/internal/service/gateway_beta_test.go @@ -242,3 +242,72 @@ func TestIsCountTokensUnsupported404(t *testing.T) { }) } } + +// TestDefaultBetaPolicy_Context1M_Sonnet5Whitelist 验证默认策略下 context-1m-2025-08-07 的分模型行为: +// - claude-sonnet-5 及后续版本:pass(放行),保留 1M 上下文能力 +// - 其他 sonnet 版本(4.x 及以下)、opus、haiku:filter(过滤),因为上游不支持 +func TestDefaultBetaPolicy_Context1M_Sonnet5Whitelist(t *testing.T) { + settings := DefaultBetaPolicySettings() + + // 找到 context-1m-2025-08-07 规则 + var rule *BetaPolicyRule + for i := range settings.Rules { + if settings.Rules[i].BetaToken == "context-1m-2025-08-07" { + rule = &settings.Rules[i] + break + } + } + require.NotNil(t, rule, "default policy must include context-1m-2025-08-07 rule") + require.Equal(t, BetaPolicyActionPass, rule.Action, "primary action for whitelisted models is pass") + require.Equal(t, BetaPolicyActionFilter, rule.FallbackAction, "non-whitelisted models must be filtered") + require.NotEmpty(t, rule.ModelWhitelist, "context-1m must be scoped to sonnet-5+ via whitelist") + + // 表驱动:模型 → 期望 action + // 覆盖每种上游路径下的模型 ID 变形:直连 Anthropic API、Vertex AI("@YYYYMMDD" 后缀)、 + // AWS Bedrock 跨区域推理(us./eu./apac./jp./au./us-gov./global./anthropic. 前缀)。 + cases := []struct { + model string + wantAction string + desc string + }{ + // —— 直连 Anthropic API —— sonnet-5 系列应放行 + {"claude-sonnet-5", BetaPolicyActionPass, "sonnet-5 canonical"}, + {"claude-sonnet-5-20260701", BetaPolicyActionPass, "sonnet-5 dated variant matches wildcard"}, + {"claude-sonnet-5-thinking", BetaPolicyActionPass, "sonnet-5 thinking variant matches wildcard"}, + // —— Vertex AI 归一化后的 sonnet-5 —— 也应放行 + {"claude-sonnet-5@20260701", BetaPolicyActionPass, "sonnet-5 Vertex-normalized dated form"}, + // —— AWS Bedrock 各跨区域前缀 sonnet-5 —— 也应放行 + {"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"}, + {"jp.anthropic.claude-sonnet-5-v1", BetaPolicyActionPass, "bedrock jp. sonnet-5"}, + {"au.anthropic.claude-sonnet-5-v1", BetaPolicyActionPass, "bedrock au. sonnet-5"}, + {"us-gov.anthropic.claude-sonnet-5-v1", BetaPolicyActionPass, "bedrock us-gov. sonnet-5"}, + {"global.anthropic.claude-sonnet-5-v1", BetaPolicyActionPass, "bedrock global. sonnet-5"}, + {"anthropic.claude-sonnet-5-v1", BetaPolicyActionPass, "bedrock no-region sonnet-5"}, + + // —— sonnet-4.x 及以下必须过滤 —— + {"claude-sonnet-4-6", BetaPolicyActionFilter, "sonnet-4.6 must be filtered"}, + {"claude-sonnet-4-5-20250929", BetaPolicyActionFilter, "sonnet-4.5 dated must be filtered"}, + {"claude-sonnet-4", BetaPolicyActionFilter, "sonnet-4 must be filtered"}, + {"claude-sonnet-4-5@20250929", BetaPolicyActionFilter, "sonnet-4.5 Vertex format must be filtered"}, + {"us.anthropic.claude-sonnet-4-6", BetaPolicyActionFilter, "bedrock us. sonnet-4.6 must be filtered"}, + {"us.anthropic.claude-sonnet-4-5-20250929-v1:0", BetaPolicyActionFilter, "bedrock us. sonnet-4.5 must be filtered"}, + // —— Opus / Haiku 必须过滤(无 1M) —— + {"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-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"}, + // —— 特殊边界:不应把 "claude-sonnet-50" / "claude-sonnet-5.1" 之类意外命名误放行 —— + {"claude-sonnet-50", BetaPolicyActionFilter, "must not over-match a hypothetical sonnet-50"}, + } + + for _, tc := range cases { + t.Run(tc.model, func(t *testing.T) { + action, _ := resolveRuleAction(*rule, tc.model) + require.Equal(t, tc.wantAction, action, tc.desc) + }) + } +} diff --git a/backend/internal/service/settings_view.go b/backend/internal/service/settings_view.go index c31e7976c5..ac225e1b14 100644 --- a/backend/internal/service/settings_view.go +++ b/backend/internal/service/settings_view.go @@ -489,6 +489,23 @@ func DefaultRateLimit429CooldownSettings() *RateLimit429CooldownSettings { } // DefaultBetaPolicySettings 返回默认的 Beta 策略配置 +// +// context-1m-2025-08-07 的默认策略: +// - 仅 claude-sonnet-5 及后续版本(如 claude-sonnet-5-*)在上游默认支持 1M 上下文。 +// - Sonnet 4.x 及以下、Opus、Haiku 上游都不支持该 beta,透传上去会被上游 400 或降级。 +// - 因此默认对 sonnet-5* 放行、其余全部过滤,与上游能力保持一致。 +// +// 白名单需要覆盖每个上游路径的模型 ID 变形: +// - 直连 Anthropic API(OAuth mimic / API Key / SetupToken):模型保持客户端原样 +// (如 "claude-sonnet-5"、"claude-sonnet-5-YYYYMMDD"、"claude-sonnet-5-thinking")。 +// - Vertex AI:normalizeVertexAnthropicModelID 会把 "-YYYYMMDD" 后缀转成 "@YYYYMMDD" +// (如 "claude-sonnet-5@YYYYMMDD")。 +// - AWS Bedrock:ResolveBedrockModelID 会输出带跨区域前缀的模型 ID +// (us./eu./apac./jp./au./us-gov./global. 或无前缀的 "anthropic." 形式)。 +// +// 白名单只用后缀通配符(matchModelPattern 语义),因此每个路径都需要显式列出前缀。 +// 精确匹配 "claude-sonnet-5" + 后缀 "-*" 与 "@*",可覆盖直连/Vertex 场景,同时避免误伤 +// 未来可能出现的 "claude-sonnet-50" 或 "claude-sonnet-5.x" 之类的意外命名。 func DefaultBetaPolicySettings() *BetaPolicySettings { return &BetaPolicySettings{ Rules: []BetaPolicyRule{ @@ -499,8 +516,26 @@ func DefaultBetaPolicySettings() *BetaPolicySettings { }, { BetaToken: "context-1m-2025-08-07", - Action: BetaPolicyActionFilter, + Action: BetaPolicyActionPass, Scope: BetaPolicyScopeAll, + ModelWhitelist: []string{ + // 直连 Anthropic API(客户端请求 model 原样) + "claude-sonnet-5", + "claude-sonnet-5-*", + // Vertex AI 走 normalizeVertexAnthropicModelID 后 "@YYYYMMDD" 格式 + "claude-sonnet-5@*", + // AWS Bedrock cross-region inference profile + "us.anthropic.claude-sonnet-5*", + "eu.anthropic.claude-sonnet-5*", + "apac.anthropic.claude-sonnet-5*", + "jp.anthropic.claude-sonnet-5*", + "au.anthropic.claude-sonnet-5*", + "us-gov.anthropic.claude-sonnet-5*", + "global.anthropic.claude-sonnet-5*", + // AWS Bedrock 无 cross-region 前缀 + "anthropic.claude-sonnet-5*", + }, + FallbackAction: BetaPolicyActionFilter, }, }, } diff --git a/frontend/src/composables/useModelWhitelist.ts b/frontend/src/composables/useModelWhitelist.ts index 3076629be1..eaea4aa2ff 100644 --- a/frontend/src/composables/useModelWhitelist.ts +++ b/frontend/src/composables/useModelWhitelist.ts @@ -31,6 +31,7 @@ export const claudeModels = [ 'claude-opus-4-7', 'claude-opus-4-8', 'claude-sonnet-4-6', + 'claude-sonnet-5', 'claude-fable-5' ] @@ -243,6 +244,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 5', from: 'claude-sonnet-5', to: 'claude-sonnet-5', color: 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-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' }, @@ -328,6 +330,7 @@ const bedrockPresetMappings = [ { 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' }, + { label: 'Sonnet 5', from: 'claude-sonnet-5', to: 'us.anthropic.claude-sonnet-5-v1', 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: 'us.anthropic.claude-sonnet-4-6', color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400' }, { label: 'Opus 4.5', from: 'claude-opus-4-5-thinking', to: 'us.anthropic.claude-opus-4-5-20251101-v1:0', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400' }, { label: 'Sonnet 4.5', from: 'claude-sonnet-4-5', to: 'us.anthropic.claude-sonnet-4-5-20250929-v1:0', color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400' },