| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858 |
- package service
- import (
- "encoding/json"
- "fmt"
- "net/url"
- "regexp"
- "strconv"
- "strings"
- "github.com/Wei-Shaw/sub2api/internal/domain"
- "github.com/Wei-Shaw/sub2api/internal/pkg/claude"
- "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
- "github.com/tidwall/gjson"
- "github.com/tidwall/sjson"
- )
- 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 前缀
- // 参考: https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html
- func BedrockCrossRegionPrefix(region string) string {
- switch {
- case strings.HasPrefix(region, "us-gov"):
- return "us-gov" // GovCloud 使用独立的 us-gov 前缀
- case strings.HasPrefix(region, "us-"):
- return "us"
- case strings.HasPrefix(region, "eu-"):
- return "eu"
- case region == "ap-northeast-1":
- return "jp" // 日本区域使用独立的 jp 前缀(AWS 官方定义)
- case region == "ap-southeast-2":
- return "au" // 澳大利亚区域使用独立的 au 前缀(AWS 官方定义)
- case strings.HasPrefix(region, "ap-"):
- return "apac" // 其余亚太区域使用通用 apac 前缀
- case strings.HasPrefix(region, "ca-"):
- return "us" // 加拿大区域使用 us 前缀的跨区域推理
- case strings.HasPrefix(region, "sa-"):
- return "us" // 南美区域使用 us 前缀的跨区域推理
- default:
- return "us"
- }
- }
- // AdjustBedrockModelRegionPrefix 将模型 ID 的区域前缀替换为与当前 AWS Region 匹配的前缀
- // 例如 region=eu-west-1 时,"us.anthropic.claude-opus-4-6-v1" → "eu.anthropic.claude-opus-4-6-v1"
- // 特殊值 region="global" 强制使用 global. 前缀
- func AdjustBedrockModelRegionPrefix(modelID, region string) string {
- var targetPrefix string
- if region == "global" {
- targetPrefix = "global"
- } else {
- targetPrefix = BedrockCrossRegionPrefix(region)
- }
- for _, p := range bedrockCrossRegionPrefixes {
- if strings.HasPrefix(modelID, p) {
- if p == targetPrefix+"." {
- return modelID // 前缀已匹配,无需替换
- }
- return targetPrefix + "." + modelID[len(p):]
- }
- }
- // 模型 ID 没有已知区域前缀(如 "anthropic.claude-..."),不做修改
- return modelID
- }
- func bedrockRuntimeRegion(account *Account) string {
- if account == nil {
- return defaultBedrockRegion
- }
- if region := account.GetCredential("aws_region"); region != "" {
- return region
- }
- return defaultBedrockRegion
- }
- func shouldForceBedrockGlobal(account *Account) bool {
- return account != nil && account.GetCredential("aws_force_global") == "true"
- }
- func isRegionalBedrockModelID(modelID string) bool {
- for _, prefix := range bedrockCrossRegionPrefixes {
- if strings.HasPrefix(modelID, prefix) {
- return true
- }
- }
- return false
- }
- func isLikelyBedrockModelID(modelID string) bool {
- lower := strings.ToLower(strings.TrimSpace(modelID))
- if lower == "" {
- return false
- }
- if strings.HasPrefix(lower, "arn:") {
- return true
- }
- for _, prefix := range []string{
- "anthropic.",
- "amazon.",
- "meta.",
- "mistral.",
- "cohere.",
- "ai21.",
- "deepseek.",
- "stability.",
- "writer.",
- "nova.",
- } {
- if strings.HasPrefix(lower, prefix) {
- return true
- }
- }
- return isRegionalBedrockModelID(lower)
- }
- func normalizeBedrockModelID(modelID string) (normalized string, shouldAdjustRegion bool, ok bool) {
- modelID = strings.TrimSpace(modelID)
- if modelID == "" {
- return "", false, false
- }
- if mapped, exists := domain.DefaultBedrockModelMapping[modelID]; exists {
- return mapped, true, true
- }
- if isRegionalBedrockModelID(modelID) {
- return modelID, true, true
- }
- if isLikelyBedrockModelID(modelID) {
- return modelID, false, true
- }
- return "", false, false
- }
- // ResolveBedrockModelID resolves a requested Claude model into a Bedrock model ID.
- // It applies account model_mapping first, then default Bedrock aliases, and finally
- // adjusts Anthropic cross-region prefixes to match the account region.
- func ResolveBedrockModelID(account *Account, requestedModel string) (string, bool) {
- if account == nil {
- return "", false
- }
- mappedModel := account.GetMappedModel(requestedModel)
- modelID, shouldAdjustRegion, ok := normalizeBedrockModelID(mappedModel)
- if !ok {
- return "", false
- }
- if shouldAdjustRegion {
- targetRegion := bedrockRuntimeRegion(account)
- if shouldForceBedrockGlobal(account) {
- targetRegion = "global"
- }
- modelID = AdjustBedrockModelRegionPrefix(modelID, targetRegion)
- }
- return modelID, true
- }
- // BuildBedrockURL 构建 Bedrock InvokeModel 的 URL
- // stream=true 时使用 invoke-with-response-stream 端点
- // modelID 中的特殊字符会被 URL 编码(与 litellm 的 urllib.parse.quote(safe="") 对齐)
- func BuildBedrockURL(region, modelID string, stream bool) string {
- if region == "" {
- region = defaultBedrockRegion
- }
- encodedModelID := url.PathEscape(modelID)
- // url.PathEscape 不编码冒号(RFC 允许 path 中出现 ":"),
- // 但 AWS Bedrock 期望模型 ID 中的冒号被编码为 %3A
- encodedModelID = strings.ReplaceAll(encodedModelID, ":", "%3A")
- if stream {
- return fmt.Sprintf("https://bedrock-runtime.%s.amazonaws.com/model/%s/invoke-with-response-stream", region, encodedModelID)
- }
- return fmt.Sprintf("https://bedrock-runtime.%s.amazonaws.com/model/%s/invoke", region, encodedModelID)
- }
- // PrepareBedrockRequestBody 处理请求体以适配 Bedrock API
- // 1. 注入 anthropic_version
- // 2. 注入 anthropic_beta(从客户端 anthropic-beta 头解析)
- // 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 不接受的字符
- // 8. 根据最终 Bedrock beta tokens 剥离不再支持的 beta 字段
- func PrepareBedrockRequestBody(body []byte, modelID string, betaHeader string) ([]byte, error) {
- betaTokens := ResolveBedrockBetaTokens(betaHeader, body, modelID)
- return PrepareBedrockRequestBodyWithTokens(body, modelID, betaTokens, false)
- }
- // PrepareBedrockRequestBodyWithTokens prepares a Bedrock request using pre-resolved beta tokens.
- // ccCompat 启用 CC 兼容模式时额外处理 thinking 类型转换和 tool_use.id 清理。
- func PrepareBedrockRequestBodyWithTokens(body []byte, modelID string, betaTokens []string, ccCompat bool) ([]byte, error) {
- var err error
- betaTokens = filterBedrockBetaTokens(betaTokens)
- body = sanitizeBedrockFieldsForBetaTokens(body, betaTokens)
- // 注入 anthropic_version(Bedrock 要求)
- body, err = sjson.SetBytes(body, "anthropic_version", "bedrock-2023-05-31")
- if err != nil {
- return nil, fmt.Errorf("inject anthropic_version: %w", err)
- }
- // 注入 anthropic_beta(Bedrock Invoke 通过请求体传递 beta 头,而非 HTTP 头)
- // 1. 从客户端 anthropic-beta header 解析
- // 2. 根据请求体内容自动补齐必要的 beta token
- // 参考 litellm: AnthropicModelInfo.get_anthropic_beta_list() + _get_tool_search_beta_header_for_bedrock()
- if len(betaTokens) > 0 {
- body, err = sjson.SetBytes(body, "anthropic_beta", betaTokens)
- if err != nil {
- return nil, fmt.Errorf("inject anthropic_beta: %w", err)
- }
- logger.LegacyPrintf("service.gateway", "[Bedrock] Injected beta tokens: %v (model=%s ccCompat=%v)", betaTokens, modelID, ccCompat)
- } else {
- body, _ = sjson.DeleteBytes(body, "anthropic_beta")
- }
- // 移除 Bedrock 不支持的 Anthropic 直连 API 专有顶层字段
- body, _ = sjson.DeleteBytes(body, "provider")
- body, _ = sjson.DeleteBytes(body, "metadata")
- // 移除 model 字段(Bedrock 通过 URL 指定模型)
- body, err = sjson.DeleteBytes(body, "model")
- if err != nil {
- return nil, fmt.Errorf("remove model field: %w", err)
- }
- // 移除 stream 字段(Bedrock 通过不同端点控制流式,不接受请求体中的 stream 字段)
- body, err = sjson.DeleteBytes(body, "stream")
- if err != nil {
- return nil, fmt.Errorf("remove stream field: %w", err)
- }
- // 转换 output_format(Bedrock Invoke 不支持此字段,但可将 schema 内联到最后一条 user message)
- // 参考 litellm: _convert_output_format_to_inline_schema()
- body = convertOutputFormatToInlineSchema(body)
- // 移除 output_config 字段(Bedrock Invoke 不支持)
- body, err = sjson.DeleteBytes(body, "output_config")
- if err != nil {
- return nil, fmt.Errorf("remove output_config field: %w", err)
- }
- // 移除工具定义中的 custom 字段
- // Claude Code (v2.1.69+) 在 tool 定义中发送 custom: {defer_loading: true},
- // Anthropic API 接受但 Bedrock 会拒绝并报 "Extra inputs are not permitted"
- body = removeCustomFieldFromTools(body)
- // 清理 cache_control 中 Bedrock 不支持的字段
- body = sanitizeBedrockCacheControl(body, modelID)
- // CC 兼容模式:修复 CC 发送的 Bedrock 不兼容字段
- if ccCompat {
- body = sanitizeBedrockThinking(body, modelID)
- body = sanitizeBedrockToolUseIDs(body)
- }
- return body, nil
- }
- // ResolveBedrockBetaTokens computes the final Bedrock beta token list before policy filtering.
- func ResolveBedrockBetaTokens(betaHeader string, body []byte, modelID string) []string {
- betaTokens := parseAnthropicBetaHeader(betaHeader)
- betaTokens = autoInjectBedrockBetaTokens(betaTokens, body, modelID)
- return filterBedrockBetaTokens(betaTokens)
- }
- // convertOutputFormatToInlineSchema 将 output_format 中的 JSON schema 内联到最后一条 user message
- // Bedrock Invoke 不支持 output_format 参数,litellm 的做法是将 schema 追加到用户消息中
- // 参考: litellm AmazonAnthropicClaudeMessagesConfig._convert_output_format_to_inline_schema()
- func convertOutputFormatToInlineSchema(body []byte) []byte {
- outputFormat := gjson.GetBytes(body, "output_format")
- if !outputFormat.Exists() || !outputFormat.IsObject() {
- return body
- }
- // 先从请求体中移除 output_format
- body, _ = sjson.DeleteBytes(body, "output_format")
- schema := outputFormat.Get("schema")
- if !schema.Exists() {
- return body
- }
- // 找到最后一条 user message
- messages := gjson.GetBytes(body, "messages")
- if !messages.Exists() || !messages.IsArray() {
- return body
- }
- msgArr := messages.Array()
- lastUserIdx := -1
- for i := len(msgArr) - 1; i >= 0; i-- {
- if msgArr[i].Get("role").String() == "user" {
- lastUserIdx = i
- break
- }
- }
- if lastUserIdx < 0 {
- return body
- }
- // 将 schema 序列化为 JSON 文本追加到该 message 的 content 数组
- schemaJSON, err := json.Marshal(json.RawMessage(schema.Raw))
- if err != nil {
- return body
- }
- content := msgArr[lastUserIdx].Get("content")
- basePath := fmt.Sprintf("messages.%d.content", lastUserIdx)
- if content.IsArray() {
- // 追加一个 text block 到 content 数组末尾
- idx := len(content.Array())
- body, _ = sjson.SetBytes(body, fmt.Sprintf("%s.%d.type", basePath, idx), "text")
- body, _ = sjson.SetBytes(body, fmt.Sprintf("%s.%d.text", basePath, idx), string(schemaJSON))
- } else if content.Type == gjson.String {
- // content 是纯字符串,转换为数组格式
- originalText := content.String()
- body, _ = sjson.SetBytes(body, basePath, []map[string]string{
- {"type": "text", "text": originalText},
- {"type": "text", "text": string(schemaJSON)},
- })
- }
- return body
- }
- // removeCustomFieldFromTools 移除 tools 数组中每个工具定义的 custom 字段
- func removeCustomFieldFromTools(body []byte) []byte {
- tools := gjson.GetBytes(body, "tools")
- if !tools.Exists() || !tools.IsArray() {
- return body
- }
- var err error
- for i := range tools.Array() {
- body, err = sjson.DeleteBytes(body, fmt.Sprintf("tools.%d.custom", i))
- if err != nil {
- // 删除失败不影响整体流程,跳过
- continue
- }
- }
- return body
- }
- // claudeVersionRe 匹配 Claude 模型 ID 中的版本号部分
- // 支持 claude-{tier}-{major}-{minor} 和 claude-{tier}-{major}.{minor} 格式;
- // minor 可选,用于覆盖只有主版本号的新模型 ID(claude-opus-5 / claude-sonnet-5),
- // 此时 matches[2] 为空串,调用方 strconv.Atoi 得到 minor=0。
- // 缺少可选 minor 时这类 ID 完全不匹配,会被当作"旧模型"降级处理:
- // thinking.enabled 不转 adaptive(Opus 5 上游已移除 budget_tokens,直接 400)、
- // cache_control.ttl 被剥离、tool search 被过滤。
- var claudeVersionRe = regexp.MustCompile(`claude-(?:haiku|sonnet|opus)-(\d+)(?:[-.](\d+))?`)
- // isBedrockClaude45OrNewer 判断 Bedrock 模型 ID 是否为 Claude 4.5 或更新版本
- // 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
- }
- major, _ := strconv.Atoi(matches[1])
- minor, _ := strconv.Atoi(matches[2])
- return major > 4 || (major == 4 && minor >= 5)
- }
- // sanitizeBedrockCacheControl 清理 system 和 messages 中 cache_control 里
- // Bedrock 不支持的字段:
- // - scope:Bedrock 不支持(如 "global" 跨请求缓存)
- // - ttl:仅 Claude 4.5+ 支持 "5m" 和 "1h",旧模型需要移除
- func sanitizeBedrockCacheControl(body []byte, modelID string) []byte {
- isClaude45 := isBedrockClaude45OrNewer(modelID)
- // 清理 system 数组中的 cache_control
- systemArr := gjson.GetBytes(body, "system")
- if systemArr.Exists() && systemArr.IsArray() {
- for i, item := range systemArr.Array() {
- if !item.IsObject() {
- continue
- }
- cc := item.Get("cache_control")
- if !cc.Exists() || !cc.IsObject() {
- continue
- }
- body = deleteCacheControlUnsupportedFields(body, fmt.Sprintf("system.%d.cache_control", i), cc, isClaude45)
- }
- }
- // 清理 messages 中的 cache_control
- messages := gjson.GetBytes(body, "messages")
- if !messages.Exists() || !messages.IsArray() {
- return body
- }
- for mi, msg := range messages.Array() {
- if !msg.IsObject() {
- continue
- }
- content := msg.Get("content")
- if !content.Exists() || !content.IsArray() {
- continue
- }
- for ci, block := range content.Array() {
- if !block.IsObject() {
- continue
- }
- cc := block.Get("cache_control")
- if !cc.Exists() || !cc.IsObject() {
- continue
- }
- body = deleteCacheControlUnsupportedFields(body, fmt.Sprintf("messages.%d.content.%d.cache_control", mi, ci), cc, isClaude45)
- }
- }
- return body
- }
- // deleteCacheControlUnsupportedFields 删除给定 cache_control 路径下 Bedrock 不支持的字段
- func deleteCacheControlUnsupportedFields(body []byte, basePath string, cc gjson.Result, isClaude45 bool) []byte {
- // Bedrock 不支持 scope(如 "global")
- if cc.Get("scope").Exists() {
- body, _ = sjson.DeleteBytes(body, basePath+".scope")
- }
- // ttl:仅 Claude 4.5+ 支持 "5m" 和 "1h",其余情况移除
- ttl := cc.Get("ttl")
- if ttl.Exists() {
- shouldRemove := true
- if isClaude45 {
- v := ttl.String()
- if v == "5m" || v == "1h" {
- shouldRemove = false
- }
- }
- if shouldRemove {
- body, _ = sjson.DeleteBytes(body, basePath+".ttl")
- }
- }
- return body
- }
- // parseAnthropicBetaHeader 解析 anthropic-beta 头的逗号分隔字符串为 token 列表
- func parseAnthropicBetaHeader(header string) []string {
- header = strings.TrimSpace(header)
- if header == "" {
- return nil
- }
- if strings.HasPrefix(header, "[") && strings.HasSuffix(header, "]") {
- var parsed []any
- if err := json.Unmarshal([]byte(header), &parsed); err == nil {
- tokens := make([]string, 0, len(parsed))
- for _, item := range parsed {
- token := strings.TrimSpace(fmt.Sprint(item))
- if token != "" {
- tokens = append(tokens, token)
- }
- }
- return tokens
- }
- }
- var tokens []string
- for _, part := range strings.Split(header, ",") {
- t := strings.TrimSpace(part)
- if t != "" {
- tokens = append(tokens, t)
- }
- }
- return tokens
- }
- // bedrockSupportedBetaTokens 是 Bedrock Invoke 支持的 beta 头白名单
- // 参考: AWS Bedrock 官方文档 + litellm anthropic_beta_headers_config.json
- // 更新策略: 当 AWS Bedrock 新增支持的 beta token 时需同步更新此白名单
- var bedrockSupportedBetaTokens = map[string]bool{
- "computer-use-2025-01-24": true,
- "computer-use-2025-11-24": true,
- "context-1m-2025-08-07": true,
- "context-management-2025-06-27": true, // compaction + clear_thinking,AWS 文档已支持
- "compact-2026-01-12": true, // 官方支持,仅 InvokeModel API(Opus 4.6+)
- "fine-grained-tool-streaming-2025-05-14": true, // AWS Tool Use 文档已支持
- // "interleaved-thinking-2025-05-14": false, // 无官方文档支持
- "tool-search-tool-2025-10-19": true,
- "tool-examples-2025-10-29": true,
- }
- const bedrockContextManagementBetaToken = "context-management-2025-06-27"
- // bedrockBetaTokenTransforms 定义 Bedrock Invoke 特有的 beta 头转换规则
- // Anthropic 直接 API 使用通用头,Bedrock Invoke 需要特定的替代头
- var bedrockBetaTokenTransforms = map[string]string{
- "advanced-tool-use-2025-11-20": "tool-search-tool-2025-10-19",
- }
- // autoInjectBedrockBetaTokens 根据请求体内容自动补齐必要的 beta token
- // 参考 litellm: AnthropicModelInfo.get_anthropic_beta_list() 和
- // AmazonAnthropicClaudeMessagesConfig._get_tool_search_beta_header_for_bedrock()
- //
- // 客户端(特别是非 Claude Code 客户端)可能只在 body 中启用了功能而不在 header 中带对应 beta token,
- // 这里通过检测请求体特征自动补齐,确保 Bedrock Invoke 不会因缺少必要 beta 头而 400。
- func autoInjectBedrockBetaTokens(tokens []string, body []byte, modelID string) []string {
- seen := make(map[string]bool, len(tokens))
- for _, t := range tokens {
- seen[t] = true
- }
- inject := func(token string) {
- if !seen[token] {
- tokens = append(tokens, token)
- seen[token] = true
- }
- }
- // 注意:thinking 字段不再自动注入 interleaved-thinking-2025-05-14
- // 因为该 beta token 未在 AWS Bedrock 官方文档中确认支持
- // 检测 computer_use 工具
- // tools 中有 type="computer_20xxxxxx" 的工具 → 需要 computer-use beta
- tools := gjson.GetBytes(body, "tools")
- if tools.Exists() && tools.IsArray() {
- toolSearchUsed := false
- programmaticToolCallingUsed := false
- inputExamplesUsed := false
- for _, tool := range tools.Array() {
- toolType := tool.Get("type").String()
- if strings.HasPrefix(toolType, "computer_20") {
- inject("computer-use-2025-11-24")
- }
- if isBedrockToolSearchType(toolType) {
- toolSearchUsed = true
- }
- if hasCodeExecutionAllowedCallers(tool) {
- programmaticToolCallingUsed = true
- }
- if hasInputExamples(tool) {
- inputExamplesUsed = true
- }
- }
- if programmaticToolCallingUsed || inputExamplesUsed {
- // programmatic tool calling 和 input examples 需要 advanced-tool-use,
- // 后续 filterBedrockBetaTokens 会将其转换为 Bedrock 特定的 tool-search-tool
- inject("advanced-tool-use-2025-11-20")
- }
- if toolSearchUsed && bedrockModelSupportsToolSearch(modelID) {
- // 纯 tool search(无 programmatic/inputExamples)时直接注入 Bedrock 特定头,
- // 跳过 advanced-tool-use → tool-search-tool 的转换步骤(与 litellm 对齐)
- if !programmaticToolCallingUsed && !inputExamplesUsed {
- inject("tool-search-tool-2025-10-19")
- } else {
- inject("advanced-tool-use-2025-11-20")
- }
- }
- }
- return tokens
- }
- func isBedrockToolSearchType(toolType string) bool {
- return toolType == "tool_search_tool_regex_20251119" || toolType == "tool_search_tool_bm25_20251119"
- }
- func hasCodeExecutionAllowedCallers(tool gjson.Result) bool {
- allowedCallers := tool.Get("allowed_callers")
- if containsStringInJSONArray(allowedCallers, "code_execution_20250825") {
- return true
- }
- return containsStringInJSONArray(tool.Get("function.allowed_callers"), "code_execution_20250825")
- }
- func hasInputExamples(tool gjson.Result) bool {
- if arr := tool.Get("input_examples"); arr.Exists() && arr.IsArray() && len(arr.Array()) > 0 {
- return true
- }
- arr := tool.Get("function.input_examples")
- return arr.Exists() && arr.IsArray() && len(arr.Array()) > 0
- }
- func containsStringInJSONArray(result gjson.Result, target string) bool {
- if !result.Exists() || !result.IsArray() {
- return false
- }
- for _, item := range result.Array() {
- if item.String() == target {
- return true
- }
- }
- return false
- }
- // bedrockModelSupportsToolSearch 判断 Bedrock 模型是否支持 tool search
- // 目前仅 Claude Opus/Sonnet 4.5+ 支持,Haiku 不支持
- func bedrockModelSupportsToolSearch(modelID string) bool {
- lower := strings.ToLower(modelID)
- matches := claudeVersionRe.FindStringSubmatch(lower)
- if matches == nil {
- return false
- }
- // Haiku 不支持 tool search
- if strings.Contains(lower, "haiku") {
- return false
- }
- major, _ := strconv.Atoi(matches[1])
- minor, _ := strconv.Atoi(matches[2])
- return major > 4 || (major == 4 && minor >= 5)
- }
- // filterBedrockBetaTokens 过滤并转换 beta token 列表,仅保留 Bedrock Invoke 支持的 token
- // 1. 应用转换规则(如 advanced-tool-use → tool-search-tool)
- // 2. 过滤掉 Bedrock 不支持的 token(如 output-128k, files-api, structured-outputs 等)
- // 3. 自动关联 tool-examples(当 tool-search-tool 存在时)
- func filterBedrockBetaTokens(tokens []string) []string {
- seen := make(map[string]bool, len(tokens))
- var result []string
- for _, t := range tokens {
- // 应用转换规则
- if replacement, ok := bedrockBetaTokenTransforms[t]; ok {
- t = replacement
- }
- // 只保留白名单中的 token,且去重
- if bedrockSupportedBetaTokens[t] && !seen[t] {
- result = append(result, t)
- seen[t] = true
- }
- }
- // 自动关联: tool-search-tool 存在时,确保 tool-examples 也存在
- if seen["tool-search-tool-2025-10-19"] && !seen["tool-examples-2025-10-29"] {
- result = append(result, "tool-examples-2025-10-29")
- }
- return result
- }
- // sanitizeBedrockFieldsForBetaTokens 与直连路径的 sanitizeAnthropicBodyForBetaTokens
- // 对称:按最终 Bedrock beta tokens 决定是否保留 body 中的 beta 字段。
- // - context_management 缺 context-management beta → strip
- // - fallbacks 缺 server-side-fallback beta → strip
- // - fallback_credit_token 缺 server-side-fallback / 任一 fallback-credit beta → strip
- //
- // 注意:fallback beta token 均不在 bedrockSupportedBetaTokens 白名单内
- // (会被 filterBedrockBetaTokens 过滤掉),因此条件 strip 实际总会剥除——这是预期:
- // Bedrock Invoke 不支持这些 beta 字段。
- func sanitizeBedrockFieldsForBetaTokens(body []byte, betaTokens []string) []byte {
- if !containsBedrockBetaToken(betaTokens, bedrockContextManagementBetaToken) && gjson.GetBytes(body, "context_management").Exists() {
- body, _ = sjson.DeleteBytes(body, "context_management")
- }
- if !containsBedrockBetaToken(betaTokens, claude.BetaServerSideFallback) && gjson.GetBytes(body, "fallbacks").Exists() {
- body, _ = sjson.DeleteBytes(body, "fallbacks")
- }
- if !containsAnyBedrockBetaToken(betaTokens, claude.BetaServerSideFallback, claude.BetaFallbackCredit, claude.BetaFallbackCreditLegacy) &&
- gjson.GetBytes(body, "fallback_credit_token").Exists() {
- body, _ = sjson.DeleteBytes(body, "fallback_credit_token")
- }
- return body
- }
- func containsBedrockBetaToken(tokens []string, target string) bool {
- for _, token := range tokens {
- if token == target {
- return true
- }
- }
- return false
- }
- // containsAnyBedrockBetaToken 判断 tokens 是否包含 targets 中的任意一个 token。
- func containsAnyBedrockBetaToken(tokens []string, targets ...string) bool {
- for _, target := range targets {
- if containsBedrockBetaToken(tokens, target) {
- return true
- }
- }
- return false
- }
- // 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)
- }
- 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 {
- thinking := gjson.GetBytes(body, "thinking")
- if !thinking.Exists() || !thinking.IsObject() {
- return body
- }
- thinkingType := thinking.Get("type").String()
- if thinkingType == "" {
- 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")
- 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
- }
- const defaultCCMaxTokens = 81920
- // sanitizeBedrockCCFields 处理 Claude Code 发送的 Bedrock 不兼容字段:
- // - 移除 service_tier(Anthropic API 专有,Bedrock 不支持)
- // - 移除 interface_geo(Anthropic API 专有,Bedrock 不支持)
- // - 移除 context_management(Anthropic API 专有,Bedrock 不支持,CC v2.1.87+ 默认携带)
- // - 无条件移除 fallbacks / fallback_credit_token(server-side refusal fallback,
- // Anthropic 直连 beta API 专有;Bedrock Invoke 无对应 beta,永不支持)
- // - 注入 max_tokens 默认值 81920(CC 可能省略,Bedrock 要求必须提供)
- // - 注入 anthropic_version(CC 通过 HTTP 头发送,Bedrock 需要放在请求体中)
- func sanitizeBedrockCCFields(body []byte) []byte {
- if gjson.GetBytes(body, "service_tier").Exists() {
- body, _ = sjson.DeleteBytes(body, "service_tier")
- }
- if gjson.GetBytes(body, "interface_geo").Exists() {
- body, _ = sjson.DeleteBytes(body, "interface_geo")
- }
- if gjson.GetBytes(body, "context_management").Exists() {
- body, _ = sjson.DeleteBytes(body, "context_management")
- }
- if gjson.GetBytes(body, "fallbacks").Exists() {
- body, _ = sjson.DeleteBytes(body, "fallbacks")
- }
- if gjson.GetBytes(body, "fallback_credit_token").Exists() {
- body, _ = sjson.DeleteBytes(body, "fallback_credit_token")
- }
- if !gjson.GetBytes(body, "max_tokens").Exists() {
- body, _ = sjson.SetBytes(body, "max_tokens", defaultCCMaxTokens)
- }
- if !gjson.GetBytes(body, "anthropic_version").Exists() {
- body, _ = sjson.SetBytes(body, "anthropic_version", "bedrock-2023-05-31")
- }
- return body
- }
- // sanitizeBedrockCCBetaTokens 清理请求体中的 anthropic_beta 字段,只保留 Bedrock 支持的 beta token
- // CC 可能在请求体中注入了 Bedrock 不支持的 beta token(如 prompt-caching 等),导致 ValidationException
- func sanitizeBedrockCCBetaTokens(body []byte, modelID string) []byte {
- betaField := gjson.GetBytes(body, "anthropic_beta")
- if !betaField.Exists() {
- return body
- }
- var tokens []string
- if betaField.IsArray() {
- for _, t := range betaField.Array() {
- if t.Type == gjson.String {
- tokens = append(tokens, t.String())
- }
- }
- }
- originalTokens := append([]string(nil), tokens...) // 保存原始 tokens 用于日志
- // 复用现有的 Bedrock beta token 过滤逻辑(自动注入 + 白名单过滤 + 转换)
- // 即使 tokens 为空,也要执行自动注入(根据 body 内容补充必要的 beta token)
- tokens = autoInjectBedrockBetaTokens(tokens, body, modelID)
- tokens = filterBedrockBetaTokens(tokens)
- if len(tokens) == 0 {
- // 所有 token 都被过滤掉,删除 anthropic_beta 字段
- body, _ = sjson.DeleteBytes(body, "anthropic_beta")
- logger.LegacyPrintf("service.gateway", "[Bedrock CC Compat] Removed all beta tokens: original=%v", originalTokens)
- } else {
- // 更新为过滤后的 token 列表
- body, _ = sjson.SetBytes(body, "anthropic_beta", tokens)
- if len(originalTokens) > 0 {
- logger.LegacyPrintf("service.gateway", "[Bedrock CC Compat] Filtered beta tokens: original=%v final=%v", originalTokens, tokens)
- } else {
- logger.LegacyPrintf("service.gateway", "[Bedrock CC Compat] Auto-injected beta tokens: %v", tokens)
- }
- }
- return body
- }
|