diff --git a/backend/internal/service/bedrock_request.go b/backend/internal/service/bedrock_request.go index 2160c13c..35a1be85 100644 --- a/backend/internal/service/bedrock_request.go +++ b/backend/internal/service/bedrock_request.go @@ -15,6 +15,9 @@ import ( const defaultBedrockRegion = "us-east-1" +// featureKeyBedrockCCCompat is the key used in Channel.FeaturesConfig for Bedrock CC compatibility. +const featureKeyBedrockCCCompat = "bedrock_cc_compat" + var bedrockCrossRegionPrefixes = []string{"us.", "eu.", "apac.", "jp.", "au.", "us-gov.", "global."} // BedrockCrossRegionPrefix 根据 AWS Region 返回 Bedrock 跨区域推理的模型 ID 前缀 @@ -179,13 +182,16 @@ func BuildBedrockURL(region, modelID string, stream bool) string { // 3. 移除 Bedrock 不支持的字段(model, stream, output_format, output_config) // 4. 移除工具定义中的 custom 字段(Claude Code 会发送 custom: {defer_loading: true}) // 5. 清理 cache_control 中 Bedrock 不支持的字段(scope, ttl) +// 6. 修复 thinking 字段兼容性(Opus 4.7 仅支持 adaptive,enabled 需要 budget_tokens) +// 7. 清理 tool_use.id / tool_use_id 中 Bedrock 不接受的字符 func PrepareBedrockRequestBody(body []byte, modelID string, betaHeader string) ([]byte, error) { betaTokens := ResolveBedrockBetaTokens(betaHeader, body, modelID) - return PrepareBedrockRequestBodyWithTokens(body, modelID, betaTokens) + return PrepareBedrockRequestBodyWithTokens(body, modelID, betaTokens, false) } // PrepareBedrockRequestBodyWithTokens prepares a Bedrock request using pre-resolved beta tokens. -func PrepareBedrockRequestBodyWithTokens(body []byte, modelID string, betaTokens []string) ([]byte, error) { +// ccCompat 启用 CC 兼容模式时额外处理 thinking 类型转换和 tool_use.id 清理。 +func PrepareBedrockRequestBodyWithTokens(body []byte, modelID string, betaTokens []string, ccCompat bool) ([]byte, error) { var err error // 注入 anthropic_version(Bedrock 要求) @@ -235,6 +241,12 @@ func PrepareBedrockRequestBodyWithTokens(body []byte, modelID string, betaTokens // 清理 cache_control 中 Bedrock 不支持的字段 body = sanitizeBedrockCacheControl(body, modelID) + // CC 兼容模式:修复 CC 发送的 Bedrock 不兼容字段 + if ccCompat { + body = sanitizeBedrockThinking(body, modelID) + body = sanitizeBedrockToolUseIDs(body) + } + return body, nil } @@ -605,3 +617,88 @@ func filterBedrockBetaTokens(tokens []string) []string { return result } + +// bedrockToolUseIDRe 匹配 Bedrock 允许的 tool_use ID 字符(字母、数字、下划线、连字符) +var bedrockToolUseIDRe = regexp.MustCompile(`[^a-zA-Z0-9_-]`) + +// isBedrockOpus47OrNewer 判断 Bedrock 模型 ID 是否为 Claude Opus 4.7 或更新版本 +// Opus 4.7 仅支持 thinking.type: "adaptive",不支持 "enabled" +func isBedrockOpus47OrNewer(modelID string) bool { + lower := strings.ToLower(modelID) + if !strings.Contains(lower, "opus") { + return false + } + matches := claudeVersionRe.FindStringSubmatch(lower) + if matches == nil { + return false + } + major, _ := strconv.Atoi(matches[1]) + minor, _ := strconv.Atoi(matches[2]) + return major > 4 || (major == 4 && minor >= 7) +} + +const defaultThinkingBudgetTokens = 10000 + +// sanitizeBedrockThinking 修复 thinking 字段的 Bedrock 兼容性问题: +// - Opus 4.7+: 仅支持 "adaptive",将 "enabled" 转换为 "adaptive" 并移除 budget_tokens +// - 其他模型: "enabled" 必须带 budget_tokens,缺失时补充默认值 +func sanitizeBedrockThinking(body []byte, modelID string) []byte { + thinking := gjson.GetBytes(body, "thinking") + if !thinking.Exists() || !thinking.IsObject() { + return body + } + + thinkingType := thinking.Get("type").String() + if thinkingType == "" { + return body + } + + if isBedrockOpus47OrNewer(modelID) { + if thinkingType == "enabled" { + body, _ = sjson.SetBytes(body, "thinking.type", "adaptive") + body, _ = sjson.DeleteBytes(body, "thinking.budget_tokens") + } + return body + } + + if thinkingType == "enabled" && !thinking.Get("budget_tokens").Exists() { + body, _ = sjson.SetBytes(body, "thinking.budget_tokens", defaultThinkingBudgetTokens) + } + + return body +} + +// sanitizeBedrockToolUseIDs 清理 messages 中 tool_use.id 和 tool_result.tool_use_id +// 的非法字符。Bedrock 要求 ID 匹配 '^[a-zA-Z0-9_-]+$'。 +func sanitizeBedrockToolUseIDs(body []byte) []byte { + messages := gjson.GetBytes(body, "messages") + if !messages.Exists() || !messages.IsArray() { + return body + } + for mi, msg := range messages.Array() { + content := msg.Get("content") + if !content.Exists() || !content.IsArray() { + continue + } + for ci, block := range content.Array() { + switch block.Get("type").String() { + case "tool_use": + body = sanitizeIDField(body, block.Get("id").String(), fmt.Sprintf("messages.%d.content.%d.id", mi, ci)) + case "tool_result": + body = sanitizeIDField(body, block.Get("tool_use_id").String(), fmt.Sprintf("messages.%d.content.%d.tool_use_id", mi, ci)) + } + } + } + return body +} + +func sanitizeIDField(body []byte, id, path string) []byte { + if id == "" { + return body + } + sanitized := bedrockToolUseIDRe.ReplaceAllString(id, "_") + if sanitized != id { + body, _ = sjson.SetBytes(body, path, sanitized) + } + return body +} diff --git a/backend/internal/service/bedrock_request_test.go b/backend/internal/service/bedrock_request_test.go index 361cafb4..b6252849 100644 --- a/backend/internal/service/bedrock_request_test.go +++ b/backend/internal/service/bedrock_request_test.go @@ -657,3 +657,233 @@ func TestAdjustBedrockModelRegionPrefix(t *testing.T) { }) } } + +func TestIsBedrockOpus47OrNewer(t *testing.T) { + tests := []struct { + modelID string + expect bool + }{ + {"us.anthropic.claude-opus-4-7-v1", 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}, + // Sonnet 4.7 is not Opus → false + {"us.anthropic.claude-sonnet-4-7-v1", false}, + {"us.anthropic.claude-sonnet-4-6", false}, + // Haiku is not Opus + {"us.anthropic.claude-haiku-4-5-20251001-v1:0", false}, + // Non-Claude models + {"amazon.nova-pro-v1", false}, + } + for _, tt := range tests { + t.Run(tt.modelID, func(t *testing.T) { + assert.Equal(t, tt.expect, isBedrockOpus47OrNewer(tt.modelID)) + }) + } +} + +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") + 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") + assert.Equal(t, "adaptive", gjson.GetBytes(result, "thinking.type").String()) + }) + + t.Run("opus 4.6 enabled without budget_tokens gets default", func(t *testing.T) { + input := `{"thinking":{"type":"enabled"},"messages":[]}` + result := sanitizeBedrockThinking([]byte(input), "us.anthropic.claude-opus-4-6-v1") + assert.Equal(t, "enabled", gjson.GetBytes(result, "thinking.type").String()) + assert.Equal(t, int64(defaultThinkingBudgetTokens), gjson.GetBytes(result, "thinking.budget_tokens").Int()) + }) + + t.Run("opus 4.6 enabled with budget_tokens unchanged", func(t *testing.T) { + input := `{"thinking":{"type":"enabled","budget_tokens":20000},"messages":[]}` + result := sanitizeBedrockThinking([]byte(input), "us.anthropic.claude-opus-4-6-v1") + assert.Equal(t, "enabled", gjson.GetBytes(result, "thinking.type").String()) + assert.Equal(t, int64(20000), gjson.GetBytes(result, "thinking.budget_tokens").Int()) + }) + + t.Run("no thinking field unchanged", func(t *testing.T) { + input := `{"messages":[]}` + result := sanitizeBedrockThinking([]byte(input), "us.anthropic.claude-opus-4-7-v1") + assert.JSONEq(t, input, string(result)) + }) + + t.Run("sonnet 4.6 enabled without budget_tokens gets default", func(t *testing.T) { + input := `{"thinking":{"type":"enabled"},"messages":[]}` + result := sanitizeBedrockThinking([]byte(input), "us.anthropic.claude-sonnet-4-6") + assert.Equal(t, "enabled", gjson.GetBytes(result, "thinking.type").String()) + assert.Equal(t, int64(defaultThinkingBudgetTokens), gjson.GetBytes(result, "thinking.budget_tokens").Int()) + }) +} + +func TestSanitizeBedrockToolUseIDs(t *testing.T) { + t.Run("clean IDs unchanged", func(t *testing.T) { + input := `{"messages":[{"role":"assistant","content":[{"type":"tool_use","id":"toolu_01AbCdEf","name":"bash","input":{}}]}]}` + result := sanitizeBedrockToolUseIDs([]byte(input)) + assert.Equal(t, "toolu_01AbCdEf", gjson.GetBytes(result, "messages.0.content.0.id").String()) + }) + + t.Run("dots in tool_use ID replaced with underscores", func(t *testing.T) { + input := `{"messages":[{"role":"assistant","content":[{"type":"tool_use","id":"toolu.01.Ab","name":"bash","input":{}}]}]}` + result := sanitizeBedrockToolUseIDs([]byte(input)) + assert.Equal(t, "toolu_01_Ab", gjson.GetBytes(result, "messages.0.content.0.id").String()) + }) + + t.Run("special chars in tool_use ID sanitized", func(t *testing.T) { + input := `{"messages":[{"role":"assistant","content":[{"type":"tool_use","id":"toolu:01@Ab#Cd","name":"bash","input":{}}]}]}` + result := sanitizeBedrockToolUseIDs([]byte(input)) + id := gjson.GetBytes(result, "messages.0.content.0.id").String() + assert.Regexp(t, `^[a-zA-Z0-9_-]+$`, id) + }) + + t.Run("tool_result tool_use_id sanitized", func(t *testing.T) { + input := `{"messages":[{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu.01.Ab","content":"ok"}]}]}` + result := sanitizeBedrockToolUseIDs([]byte(input)) + assert.Equal(t, "toolu_01_Ab", gjson.GetBytes(result, "messages.0.content.0.tool_use_id").String()) + }) + + t.Run("mixed clean and dirty IDs", func(t *testing.T) { + input := `{"messages":[ + {"role":"assistant","content":[{"type":"tool_use","id":"clean_id-123","name":"a","input":{}}]}, + {"role":"user","content":[{"type":"tool_result","tool_use_id":"dirty.id@456","content":"ok"}]}, + {"role":"assistant","content":[{"type":"tool_use","id":"also.dirty","name":"b","input":{}}]} + ]}` + result := sanitizeBedrockToolUseIDs([]byte(input)) + assert.Equal(t, "clean_id-123", gjson.GetBytes(result, "messages.0.content.0.id").String()) + assert.Equal(t, "dirty_id_456", gjson.GetBytes(result, "messages.1.content.0.tool_use_id").String()) + assert.Equal(t, "also_dirty", gjson.GetBytes(result, "messages.2.content.0.id").String()) + }) + + t.Run("no messages unchanged", func(t *testing.T) { + input := `{"system":[{"type":"text","text":"hi"}]}` + result := sanitizeBedrockToolUseIDs([]byte(input)) + assert.JSONEq(t, input, string(result)) + }) + + t.Run("string content skipped", func(t *testing.T) { + input := `{"messages":[{"role":"user","content":"plain text"}]}` + result := sanitizeBedrockToolUseIDs([]byte(input)) + assert.JSONEq(t, input, string(result)) + }) + + t.Run("empty ID skipped", func(t *testing.T) { + input := `{"messages":[{"role":"assistant","content":[{"type":"tool_use","id":"","name":"a","input":{}}]}]}` + result := sanitizeBedrockToolUseIDs([]byte(input)) + assert.Equal(t, "", gjson.GetBytes(result, "messages.0.content.0.id").String()) + }) +} + +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") + 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") + 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") + 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") + 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") + assert.Equal(t, "adaptive", gjson.GetBytes(result, "thinking.type").String()) + assert.Equal(t, int64(5000), gjson.GetBytes(result, "thinking.budget_tokens").Int()) + }) + + // Forward() passes parsed.Model (standard names like "claude-opus-4-7") + t.Run("standard model name opus 4.7 converts enabled to adaptive", func(t *testing.T) { + input := `{"thinking":{"type":"enabled","budget_tokens":10000},"messages":[]}` + result := sanitizeBedrockThinking([]byte(input), "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("standard model name opus 4.6 keeps enabled", func(t *testing.T) { + input := `{"thinking":{"type":"enabled","budget_tokens":10000},"messages":[]}` + result := sanitizeBedrockThinking([]byte(input), "claude-opus-4-6") + assert.Equal(t, "enabled", gjson.GetBytes(result, "thinking.type").String()) + assert.Equal(t, int64(10000), gjson.GetBytes(result, "thinking.budget_tokens").Int()) + }) +} + +func TestIsBedrockOpus47OrNewer_EdgeCases(t *testing.T) { + tests := []struct { + modelID string + expect bool + }{ + {"anthropic.claude-opus-4-7-v1", true}, + {"us.anthropic.claude-opus-4-7-20270101-v1:0", true}, + {"", false}, + // Forward() passes parsed.Model (standard names), not Bedrock IDs + {"claude-opus-4-7", true}, + {"claude-opus-4-6", false}, + {"claude-sonnet-4-7", false}, + } + for _, tt := range tests { + t.Run(tt.modelID, func(t *testing.T) { + assert.Equal(t, tt.expect, isBedrockOpus47OrNewer(tt.modelID)) + }) + } +} + +func TestPrepareBedrockRequestBodyWithTokens_CCCompat(t *testing.T) { + input := `{ + "model":"claude-opus-4-6", + "stream":true, + "max_tokens":16384, + "thinking":{"type":"enabled"}, + "messages":[ + {"role":"assistant","content":[{"type":"tool_use","id":"toolu.01.Ab","name":"bash","input":{}}]}, + {"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu.01.Ab","content":"ok"}]} + ] + }` + + t.Run("ccCompat=false skips thinking and toolUseID sanitization", func(t *testing.T) { + result, err := PrepareBedrockRequestBodyWithTokens([]byte(input), "us.anthropic.claude-opus-4-6-v1", nil, false) + require.NoError(t, err) + assert.Equal(t, "enabled", gjson.GetBytes(result, "thinking.type").String()) + assert.False(t, gjson.GetBytes(result, "thinking.budget_tokens").Exists()) + assert.Equal(t, "toolu.01.Ab", gjson.GetBytes(result, "messages.0.content.0.id").String()) + }) + + t.Run("ccCompat=true applies thinking fix and toolUseID sanitization (opus 4.6)", func(t *testing.T) { + result, err := PrepareBedrockRequestBodyWithTokens([]byte(input), "us.anthropic.claude-opus-4-6-v1", nil, true) + require.NoError(t, err) + assert.Equal(t, "enabled", gjson.GetBytes(result, "thinking.type").String()) + assert.Equal(t, int64(defaultThinkingBudgetTokens), gjson.GetBytes(result, "thinking.budget_tokens").Int()) + assert.Equal(t, "toolu_01_Ab", gjson.GetBytes(result, "messages.0.content.0.id").String()) + assert.Equal(t, "toolu_01_Ab", gjson.GetBytes(result, "messages.1.content.0.tool_use_id").String()) + }) + + 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) + require.NoError(t, err) + assert.Equal(t, "adaptive", gjson.GetBytes(result, "thinking.type").String()) + assert.False(t, gjson.GetBytes(result, "thinking.budget_tokens").Exists()) + assert.Equal(t, "toolu_01_Ab", gjson.GetBytes(result, "messages.0.content.0.id").String()) + }) +} diff --git a/backend/internal/service/channel.go b/backend/internal/service/channel.go index 760f688d..4e5c9b65 100644 --- a/backend/internal/service/channel.go +++ b/backend/internal/service/channel.go @@ -248,6 +248,19 @@ func (c *Channel) IsWebSearchEmulationEnabled(platform string) bool { return ok && enabled } +// IsBedrockCCCompatEnabled 返回该渠道是否为指定平台启用了 Bedrock CC 兼容模式。 +func (c *Channel) IsBedrockCCCompatEnabled(platform string) bool { + if c == nil || c.FeaturesConfig == nil { + return false + } + bcc, ok := c.FeaturesConfig[featureKeyBedrockCCCompat].(map[string]any) + if !ok { + return false + } + enabled, ok := bcc[platform].(bool) + return ok && enabled +} + // deepCopyFeaturesConfig creates a deep copy of FeaturesConfig to prevent cache pollution. func deepCopyFeaturesConfig(src map[string]any) map[string]any { dst := make(map[string]any, len(src)) diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index 0caff3a7..28e31438 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -4352,6 +4352,13 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A return s.handleWebSearchEmulation(ctx, c, account, parsed) } + // Bedrock CC 兼容:在转发之前对请求体做 CC 兼容处理。 + // 只修改 body 内容(thinking 类型、tool_use ID),不影响后续的透传/Bedrock 转发路径。 + if account != nil && s.isBedrockCCCompatEnabled(ctx, account, parsed.GroupID) { + parsed.Body = sanitizeBedrockThinking(parsed.Body, parsed.Model) + parsed.Body = sanitizeBedrockToolUseIDs(parsed.Body) + } + if account != nil && account.IsAnthropicAPIKeyPassthroughEnabled() { passthroughBody := parsed.Body passthroughModel := parsed.Model @@ -5637,6 +5644,18 @@ func writeAnthropicPassthroughResponseHeaders(dst http.Header, src http.Header, } } +// isBedrockCCCompatEnabled 检查渠道是否启用了 Bedrock CC 兼容模式 +func (s *GatewayService) isBedrockCCCompatEnabled(ctx context.Context, account *Account, groupID *int64) bool { + if groupID == nil || s.channelService == nil { + return false + } + ch, err := s.channelService.GetChannelForGroup(ctx, *groupID) + if err != nil || ch == nil { + return false + } + return ch.IsBedrockCCCompatEnabled(account.Platform) +} + // forwardBedrock 转发请求到 AWS Bedrock func (s *GatewayService) forwardBedrock( ctx context.Context, @@ -5669,7 +5688,7 @@ func (s *GatewayService) forwardBedrock( return nil, err } - bedrockBody, err := PrepareBedrockRequestBodyWithTokens(body, mappedModel, betaTokens) + bedrockBody, err := PrepareBedrockRequestBodyWithTokens(body, mappedModel, betaTokens, false) if err != nil { return nil, fmt.Errorf("prepare bedrock request body: %w", err) } diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index f10ac21e..abe4586c 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -2358,6 +2358,8 @@ export default { webSearchEmulationGlobalDisabled: 'Please enable the global switch first in Settings → Gateway → Web Search Emulation', codexImageGenerationBridge: 'Codex Image Generation Bridge', codexImageGenerationBridgeHint: 'When enabled, Codex /responses text requests in OpenAI groups may be automatically given the image_generation tool. Keep off unless the routed accounts support image generation.', + bedrockCCCompat: 'Bedrock CC Compatibility', + bedrockCCCompatHint: '⚠️ When enabled, requests to Bedrock accounts in this channel will be transformed for Claude Code compatibility (thinking type conversion, tool_use ID sanitization).', basicSettings: 'Basic Settings', addPlatform: 'Add Platform', noPlatforms: 'Click "Add Platform" to start configuring the channel', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 286fc28d..af6c0849 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -2435,6 +2435,8 @@ export default { webSearchEmulationGlobalDisabled: '请先在系统设置 → 网关 → Web Search 模拟中启用全局开关', codexImageGenerationBridge: 'Codex 图片生成桥接', codexImageGenerationBridgeHint: '开启后,OpenAI 分组的 Codex /responses 文本请求可能会被自动注入 image_generation 工具。仅在路由账号支持图片生成时开启。', + bedrockCCCompat: 'Bedrock CC 兼容', + bedrockCCCompatHint: '⚠️ 开启后,该渠道下 Bedrock 账号的请求将进行 Claude Code 兼容处理(thinking 类型转换、tool_use ID 清理)', basicSettings: '基础设置', addPlatform: '添加平台', noPlatforms: '点击"添加平台"开始配置渠道', diff --git a/frontend/src/views/admin/ChannelsView.vue b/frontend/src/views/admin/ChannelsView.vue index 3a4aeb0d..38f470a4 100644 --- a/frontend/src/views/admin/ChannelsView.vue +++ b/frontend/src/views/admin/ChannelsView.vue @@ -354,6 +354,21 @@ + +
+
+
+ +

+ {{ t('admin.channels.form.bedrockCCCompatHint') }} +

+
+ +
+
+
@@ -669,6 +684,7 @@ interface PlatformSection { model_pricing: PricingFormEntry[] web_search_emulation: boolean codex_image_generation_bridge: boolean + bedrock_cc_compat: boolean account_stats_pricing_rules: FormPricingRule[] } @@ -765,6 +781,7 @@ function addPlatformSection(platform: GroupPlatform) { model_pricing: [], web_search_emulation: false, codex_image_generation_bridge: false, + bedrock_cc_compat: false, account_stats_pricing_rules: [], }) } @@ -1125,6 +1142,19 @@ function formToAPI(): { group_ids: number[], model_pricing: ChannelModelPricing[ delete featuresConfig.codex_image_generation_bridge } + const bedrockCCCompat: Record = {} + for (const section of form.platforms) { + if (!section.enabled) continue + if (section.platform === 'anthropic') { + bedrockCCCompat[section.platform] = !!section.bedrock_cc_compat + } + } + if (Object.keys(bedrockCCCompat).length > 0) { + featuresConfig.bedrock_cc_compat = bedrockCCCompat + } else { + delete featuresConfig.bedrock_cc_compat + } + return { group_ids, model_pricing, model_mapping, features_config: featuresConfig } } @@ -1175,6 +1205,8 @@ function apiToForm(channel: Channel): PlatformSection[] { const webSearchEnabled = wsEmulation?.[platform] === true const codexImageGenerationBridge = fc?.codex_image_generation_bridge as Record | undefined const codexImageGenerationBridgeEnabled = codexImageGenerationBridge?.[platform] === true + const bedrockCCCompat = fc?.bedrock_cc_compat as Record | undefined + const bedrockCCCompatEnabled = bedrockCCCompat?.[platform] === true sections.push({ platform, @@ -1185,6 +1217,7 @@ function apiToForm(channel: Channel): PlatformSection[] { model_pricing: pricing, web_search_emulation: webSearchEnabled, codex_image_generation_bridge: codexImageGenerationBridgeEnabled, + bedrock_cc_compat: bedrockCCCompatEnabled, account_stats_pricing_rules: [], }) }