package anthropic import ( "encoding/json" "fmt" "strings" "time" "nex/backend/internal/conversion" "nex/backend/internal/conversion/canonical" ) // decodeRequest 将 Anthropic 请求解码为 Canonical 请求 func decodeRequest(body []byte) (*canonical.CanonicalRequest, error) { var req MessagesRequest if err := json.Unmarshal(body, &req); err != nil { return nil, conversion.NewConversionError(conversion.ErrorCodeJSONParseError, "解析 Anthropic 请求失败").WithCause(err) } if strings.TrimSpace(req.Model) == "" { return nil, conversion.NewConversionError(conversion.ErrorCodeInvalidInput, "model 字段不能为空") } if len(req.Messages) == 0 { return nil, conversion.NewConversionError(conversion.ErrorCodeInvalidInput, "messages 字段不能为空") } system := decodeSystem(req.System) var canonicalMsgs []canonical.CanonicalMessage for _, msg := range req.Messages { decoded := decodeMessage(msg) canonicalMsgs = append(canonicalMsgs, decoded...) } tools := decodeTools(req.Tools) toolChoice := decodeToolChoice(req.ToolChoice) params := decodeParameters(&req) thinking := decodeThinking(req.Thinking, req.OutputConfig) outputFormat := decodeOutputFormat(req.OutputConfig) var parallelToolUse *bool if req.DisableParallelToolUse != nil && *req.DisableParallelToolUse { val := false parallelToolUse = &val } var userID string if req.Metadata != nil { userID = req.Metadata.UserID } return &canonical.CanonicalRequest{ Model: req.Model, System: system, Messages: canonicalMsgs, Tools: tools, ToolChoice: toolChoice, Parameters: params, Thinking: thinking, Stream: req.Stream, UserID: userID, OutputFormat: outputFormat, ParallelToolUse: parallelToolUse, }, nil } // decodeSystem 解码系统消息 func decodeSystem(system any) any { if system == nil { return nil } switch v := system.(type) { case string: if v == "" { return nil } return v case []any: var blocks []canonical.SystemBlock for _, item := range v { if m, ok := item.(map[string]any); ok { if text, ok := m["text"].(string); ok { blocks = append(blocks, canonical.SystemBlock{Text: text}) } } } if len(blocks) == 0 { return nil } return blocks default: return fmt.Sprintf("%v", v) } } // decodeMessage 解码 Anthropic 消息 func decodeMessage(msg Message) []canonical.CanonicalMessage { switch msg.Role { case "user": blocks := decodeContentBlocks(msg.Content) var toolResults []canonical.ContentBlock var others []canonical.ContentBlock for _, b := range blocks { if b.Type == "tool_result" { toolResults = append(toolResults, b) } else { others = append(others, b) } } var result []canonical.CanonicalMessage if len(others) > 0 { result = append(result, canonical.CanonicalMessage{Role: canonical.RoleUser, Content: others}) } if len(toolResults) > 0 { result = append(result, canonical.CanonicalMessage{Role: canonical.RoleTool, Content: toolResults}) } if len(result) == 0 { result = append(result, canonical.CanonicalMessage{Role: canonical.RoleUser, Content: []canonical.ContentBlock{canonical.NewTextBlock("")}}) } return result case "assistant": blocks := decodeContentBlocks(msg.Content) if len(blocks) == 0 { blocks = append(blocks, canonical.NewTextBlock("")) } return []canonical.CanonicalMessage{{Role: canonical.RoleAssistant, Content: blocks}} } return nil } // decodeContentBlocks 解码内容块列表 func decodeContentBlocks(content any) []canonical.ContentBlock { switch v := content.(type) { case string: return []canonical.ContentBlock{canonical.NewTextBlock(v)} case []any: var blocks []canonical.ContentBlock for _, item := range v { if m, ok := item.(map[string]any); ok { block := decodeSingleContentBlock(m) if block != nil { blocks = append(blocks, *block) } } } if len(blocks) > 0 { return blocks } return []canonical.ContentBlock{canonical.NewTextBlock("")} case nil: return []canonical.ContentBlock{canonical.NewTextBlock("")} default: return []canonical.ContentBlock{canonical.NewTextBlock(fmt.Sprintf("%v", v))} } } // decodeSingleContentBlock 解码单个内容块 func decodeSingleContentBlock(m map[string]any) *canonical.ContentBlock { t, _ := m["type"].(string) switch t { case "text": text, _ := m["text"].(string) return &canonical.ContentBlock{Type: "text", Text: text} case "tool_use": id, _ := m["id"].(string) name, _ := m["name"].(string) input, _ := json.Marshal(m["input"]) return &canonical.ContentBlock{Type: "tool_use", ID: id, Name: name, Input: input} case "tool_result": toolUseID, _ := m["tool_use_id"].(string) isErr := false if ie, ok := m["is_error"].(bool); ok { isErr = ie } var content json.RawMessage if c, ok := m["content"]; ok { switch cv := c.(type) { case string: content = json.RawMessage(fmt.Sprintf("%q", cv)) default: content, _ = json.Marshal(cv) } } else { content = json.RawMessage(`""`) } return &canonical.ContentBlock{ Type: "tool_result", ToolUseID: toolUseID, Content: content, IsError: &isErr, } case "thinking": thinking, _ := m["thinking"].(string) return &canonical.ContentBlock{Type: "thinking", Thinking: thinking} case "redacted_thinking": // 丢弃 return nil } return nil } // decodeTools 解码工具定义 func decodeTools(tools []Tool) []canonical.CanonicalTool { if len(tools) == 0 { return nil } result := make([]canonical.CanonicalTool, len(tools)) for i, t := range tools { result[i] = canonical.CanonicalTool{ Name: t.Name, Description: t.Description, InputSchema: t.InputSchema, } } return result } // decodeToolChoice 解码工具选择 func decodeToolChoice(toolChoice any) *canonical.ToolChoice { if toolChoice == nil { return nil } switch v := toolChoice.(type) { case string: switch v { case "auto": return canonical.NewToolChoiceAuto() case "none": return canonical.NewToolChoiceNone() case "any": return canonical.NewToolChoiceAny() } case map[string]any: t, _ := v["type"].(string) switch t { case "auto": return canonical.NewToolChoiceAuto() case "none": return canonical.NewToolChoiceNone() case "any": return canonical.NewToolChoiceAny() case "tool": name, _ := v["name"].(string) return canonical.NewToolChoiceNamed(name) } } return nil } // decodeParameters 解码请求参数 func decodeParameters(req *MessagesRequest) canonical.RequestParameters { params := canonical.RequestParameters{ Temperature: req.Temperature, TopP: req.TopP, TopK: req.TopK, } if req.MaxTokens > 0 { val := req.MaxTokens params.MaxTokens = &val } if len(req.StopSequences) > 0 { params.StopSequences = req.StopSequences } return params } // decodeThinking 解码思考配置 func decodeThinking(thinking *ThinkingConfig, outputConfig *OutputConfig) *canonical.ThinkingConfig { if thinking == nil { return nil } cfg := &canonical.ThinkingConfig{ Type: thinking.Type, BudgetTokens: thinking.BudgetTokens, } if outputConfig != nil && outputConfig.Effort != "" { cfg.Effort = outputConfig.Effort } return cfg } // decodeOutputFormat 解码输出格式 func decodeOutputFormat(outputConfig *OutputConfig) *canonical.OutputFormat { if outputConfig == nil || outputConfig.Format == nil { return nil } if outputConfig.Format.Type == "json_schema" && outputConfig.Format.Schema != nil { return &canonical.OutputFormat{ Type: "json_schema", Name: "output", Schema: outputConfig.Format.Schema, Strict: boolPtr(true), } } return nil } // decodeResponse 将 Anthropic 响应解码为 Canonical 响应 func decodeResponse(body []byte) (*canonical.CanonicalResponse, error) { var resp MessagesResponse if err := json.Unmarshal(body, &resp); err != nil { return nil, conversion.NewConversionError(conversion.ErrorCodeJSONParseError, "解析 Anthropic 响应失败").WithCause(err) } var blocks []canonical.ContentBlock for _, block := range resp.Content { switch block.Type { case "text": blocks = append(blocks, canonical.NewTextBlock(block.Text)) case "tool_use": blocks = append(blocks, canonical.NewToolUseBlock(block.ID, block.Name, block.Input)) case "thinking": blocks = append(blocks, canonical.NewThinkingBlock(block.Thinking)) case "redacted_thinking": // 丢弃 } } if len(blocks) == 0 { blocks = append(blocks, canonical.NewTextBlock("")) } sr := mapStopReason(resp.StopReason) usage := canonical.CanonicalUsage{ InputTokens: resp.Usage.InputTokens, OutputTokens: resp.Usage.OutputTokens, } if resp.Usage.CacheReadInputTokens != nil { usage.CacheReadTokens = resp.Usage.CacheReadInputTokens } if resp.Usage.CacheCreationInputTokens != nil { usage.CacheCreationTokens = resp.Usage.CacheCreationInputTokens } return &canonical.CanonicalResponse{ ID: resp.ID, Model: resp.Model, Content: blocks, StopReason: &sr, Usage: usage, }, nil } // mapStopReason 映射停止原因 func mapStopReason(reason string) canonical.StopReason { switch reason { case "end_turn": return canonical.StopReasonEndTurn case "max_tokens": return canonical.StopReasonMaxTokens case "tool_use": return canonical.StopReasonToolUse case "stop_sequence": return canonical.StopReasonStopSequence case "pause_turn": return canonical.StopReason("pause_turn") case "refusal": return canonical.StopReasonRefusal default: return canonical.StopReasonEndTurn } } // decodeModelsResponse 解码模型列表响应 func decodeModelsResponse(body []byte) (*canonical.CanonicalModelList, error) { var resp ModelsResponse if err := json.Unmarshal(body, &resp); err != nil { return nil, err } models := make([]canonical.CanonicalModel, len(resp.Data)) for i, m := range resp.Data { name := m.DisplayName if name == "" { name = m.ID } models[i] = canonical.CanonicalModel{ ID: m.ID, Name: name, Created: parseTimestamp(m.CreatedAt), OwnedBy: "anthropic", } } return &canonical.CanonicalModelList{Models: models}, nil } // decodeModelInfoResponse 解码模型详情响应 func decodeModelInfoResponse(body []byte) (*canonical.CanonicalModelInfo, error) { var resp ModelInfoResponse if err := json.Unmarshal(body, &resp); err != nil { return nil, err } name := resp.DisplayName if name == "" { name = resp.ID } return &canonical.CanonicalModelInfo{ ID: resp.ID, Name: name, Created: parseTimestamp(resp.CreatedAt), OwnedBy: "anthropic", }, nil } // parseTimestamp 解析 RFC 3339 时间戳为 Unix func parseTimestamp(s string) int64 { if s == "" { return 0 } t, err := time.Parse(time.RFC3339, s) if err != nil { return 0 } return t.Unix() } // formatTimestamp 将 Unix 时间戳格式化为 RFC 3339 func formatTimestamp(unix int64) string { if unix == 0 { return time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC).Format(time.RFC3339) } return time.Unix(unix, 0).UTC().Format(time.RFC3339) } // boolPtr 返回 bool 指针 func boolPtr(b bool) *bool { return &b }