package anthropic import ( "encoding/json" "fmt" "nex/backend/internal/conversion" "nex/backend/internal/conversion/canonical" ) // encodeRequest 将 Canonical 请求编码为 Anthropic 请求 func encodeRequest(req *canonical.CanonicalRequest, provider *conversion.TargetProvider) ([]byte, error) { result := map[string]any{ "model": provider.ModelName, "stream": req.Stream, } // max_tokens 必填 if req.Parameters.MaxTokens != nil { result["max_tokens"] = *req.Parameters.MaxTokens } else { result["max_tokens"] = 4096 } // 系统消息 if req.System != nil { result["system"] = encodeSystem(req.System) } // 消息 result["messages"] = encodeMessages(req.Messages) // 参数 if req.Parameters.Temperature != nil { result["temperature"] = *req.Parameters.Temperature } if req.Parameters.TopP != nil { result["top_p"] = *req.Parameters.TopP } if req.Parameters.TopK != nil { result["top_k"] = *req.Parameters.TopK } if len(req.Parameters.StopSequences) > 0 { result["stop_sequences"] = req.Parameters.StopSequences } // 工具 if len(req.Tools) > 0 { tools := make([]map[string]any, len(req.Tools)) for i, t := range req.Tools { tool := map[string]any{ "name": t.Name, "input_schema": t.InputSchema, } if t.Description != "" { tool["description"] = t.Description } tools[i] = tool } result["tools"] = tools } if req.ToolChoice != nil { result["tool_choice"] = encodeToolChoice(req.ToolChoice) } // 公共字段 if req.UserID != "" { result["metadata"] = map[string]any{"user_id": req.UserID} } if req.ParallelToolUse != nil && !*req.ParallelToolUse { result["disable_parallel_tool_use"] = true } if req.Thinking != nil { result["thinking"] = encodeThinkingConfig(req.Thinking) } // output_config outputConfig := map[string]any{} hasOutputConfig := false if req.OutputFormat != nil { of := encodeOutputFormat(req.OutputFormat) if of != nil { outputConfig["format"] = of hasOutputConfig = true } } if req.Thinking != nil && req.Thinking.Effort != "" { outputConfig["effort"] = req.Thinking.Effort hasOutputConfig = true } if hasOutputConfig { result["output_config"] = outputConfig } body, err := json.Marshal(result) if err != nil { return nil, conversion.NewConversionError(conversion.ErrorCodeEncodingFailure, "编码 Anthropic 请求失败").WithCause(err) } return body, nil } // encodeSystem 编码系统消息 func encodeSystem(system any) any { switch v := system.(type) { case string: return v case []canonical.SystemBlock: blocks := make([]map[string]any, len(v)) for i, b := range v { blocks[i] = map[string]any{"text": b.Text} } return blocks default: return fmt.Sprintf("%v", v) } } // encodeMessages 编码消息列表(含角色约束处理) func encodeMessages(msgs []canonical.CanonicalMessage) []map[string]any { var result []map[string]any for _, msg := range msgs { switch msg.Role { case canonical.RoleUser: result = append(result, map[string]any{ "role": "user", "content": encodeContentBlocks(msg.Content), }) case canonical.RoleAssistant: result = append(result, map[string]any{ "role": "assistant", "content": encodeContentBlocks(msg.Content), }) case canonical.RoleTool: // tool 角色合并到相邻 user 消息 toolResults := filterToolResults(msg.Content) if len(result) > 0 && result[len(result)-1]["role"] == "user" { // 合并到最后一条 user 消息 lastContent, ok := result[len(result)-1]["content"].([]map[string]any) if ok { result[len(result)-1]["content"] = append(lastContent, toolResults...) } else { result[len(result)-1]["content"] = toolResults } } else { result = append(result, map[string]any{ "role": "user", "content": toolResults, }) } } } // 确保首消息为 user if len(result) > 0 && result[0]["role"] != "user" { result = append([]map[string]any{{"role": "user", "content": []map[string]any{}}}, result...) } // 合并连续同角色消息 result = mergeConsecutiveRoles(result) return result } // encodeContentBlocks 编码内容块列表 func encodeContentBlocks(blocks []canonical.ContentBlock) []map[string]any { result := make([]map[string]any, 0, len(blocks)) for _, b := range blocks { switch b.Type { case "text": result = append(result, map[string]any{"type": "text", "text": b.Text}) case "tool_use": m := map[string]any{ "type": "tool_use", "id": b.ID, "name": b.Name, "input": b.Input, } if b.Input == nil { m["input"] = map[string]any{} } result = append(result, m) case "tool_result": m := map[string]any{ "type": "tool_result", "tool_use_id": b.ToolUseID, } if b.Content != nil { var contentStr string if json.Unmarshal(b.Content, &contentStr) == nil { m["content"] = contentStr } else { m["content"] = string(b.Content) } } else { m["content"] = "" } if b.IsError != nil { m["is_error"] = *b.IsError } result = append(result, m) case "thinking": result = append(result, map[string]any{"type": "thinking", "thinking": b.Thinking}) } } if len(result) == 0 { return []map[string]any{{"type": "text", "text": ""}} } return result } // filterToolResults 过滤工具结果 func filterToolResults(blocks []canonical.ContentBlock) []map[string]any { var result []map[string]any for _, b := range blocks { if b.Type == "tool_result" { m := map[string]any{ "type": "tool_result", "tool_use_id": b.ToolUseID, } if b.Content != nil { var contentStr string if json.Unmarshal(b.Content, &contentStr) == nil { m["content"] = contentStr } else { m["content"] = string(b.Content) } } else { m["content"] = "" } if b.IsError != nil { m["is_error"] = *b.IsError } result = append(result, m) } } return result } // encodeToolChoice 编码工具选择 func encodeToolChoice(choice *canonical.ToolChoice) any { switch choice.Type { case "auto": return map[string]any{"type": "auto"} case "none": return map[string]any{"type": "none"} case "any": return map[string]any{"type": "any"} case "tool": return map[string]any{"type": "tool", "name": choice.Name} } return map[string]any{"type": "auto"} } // encodeThinkingConfig 编码思考配置 func encodeThinkingConfig(cfg *canonical.ThinkingConfig) map[string]any { switch cfg.Type { case "enabled": m := map[string]any{"type": "enabled"} if cfg.BudgetTokens != nil { m["budget_tokens"] = *cfg.BudgetTokens } return m case "disabled": return map[string]any{"type": "disabled"} case "adaptive": return map[string]any{"type": "adaptive"} } return map[string]any{"type": "disabled"} } // encodeOutputFormat 编码输出格式 func encodeOutputFormat(format *canonical.OutputFormat) map[string]any { if format == nil { return nil } switch format.Type { case "json_schema": schema := format.Schema if schema == nil { schema = json.RawMessage(`{"type":"object"}`) } return map[string]any{ "type": "json_schema", "schema": schema, } case "json_object": return map[string]any{ "type": "json_schema", "schema": map[string]any{"type": "object"}, } case "text": return nil } return nil } // encodeResponse 将 Canonical 响应编码为 Anthropic 响应 func encodeResponse(resp *canonical.CanonicalResponse) ([]byte, error) { blocks := make([]map[string]any, 0, len(resp.Content)) for _, b := range resp.Content { switch b.Type { case "text": blocks = append(blocks, map[string]any{"type": "text", "text": b.Text}) case "tool_use": m := map[string]any{ "type": "tool_use", "id": b.ID, "name": b.Name, "input": b.Input, } if b.Input == nil { m["input"] = map[string]any{} } blocks = append(blocks, m) case "thinking": blocks = append(blocks, map[string]any{"type": "thinking", "thinking": b.Thinking}) } } sr := "end_turn" if resp.StopReason != nil { sr = mapCanonicalStopReason(*resp.StopReason) } usage := map[string]any{ "input_tokens": resp.Usage.InputTokens, "output_tokens": resp.Usage.OutputTokens, } if resp.Usage.CacheReadTokens != nil { usage["cache_read_input_tokens"] = *resp.Usage.CacheReadTokens } if resp.Usage.CacheCreationTokens != nil { usage["cache_creation_input_tokens"] = *resp.Usage.CacheCreationTokens } result := map[string]any{ "id": resp.ID, "type": "message", "role": "assistant", "model": resp.Model, "content": blocks, "stop_reason": sr, "stop_sequence": nil, "usage": usage, } body, err := json.Marshal(result) if err != nil { return nil, conversion.NewConversionError(conversion.ErrorCodeEncodingFailure, "编码 Anthropic 响应失败").WithCause(err) } return body, nil } // mapCanonicalStopReason 映射 Canonical 停止原因到 Anthropic func mapCanonicalStopReason(reason canonical.StopReason) string { switch reason { case canonical.StopReasonEndTurn, canonical.StopReasonContentFilter: return "end_turn" case canonical.StopReasonMaxTokens: return "max_tokens" case canonical.StopReasonToolUse: return "tool_use" case canonical.StopReasonStopSequence: return "stop_sequence" case canonical.StopReasonRefusal: return "refusal" default: return "end_turn" } } // encodeModelsResponse 编码模型列表响应 func encodeModelsResponse(list *canonical.CanonicalModelList) ([]byte, error) { data := make([]map[string]any, len(list.Models)) for i, m := range list.Models { name := m.Name if name == "" { name = m.ID } data[i] = map[string]any{ "id": m.ID, "type": "model", "display_name": name, "created_at": formatTimestamp(m.Created), } } var firstID, lastID *string if len(list.Models) > 0 { fid := list.Models[0].ID firstID = &fid lid := list.Models[len(list.Models)-1].ID lastID = &lid } return json.Marshal(map[string]any{ "data": data, "has_more": false, "first_id": firstID, "last_id": lastID, }) } // encodeModelInfoResponse 编码模型详情响应 func encodeModelInfoResponse(info *canonical.CanonicalModelInfo) ([]byte, error) { name := info.Name if name == "" { name = info.ID } return json.Marshal(map[string]any{ "id": info.ID, "type": "model", "display_name": name, "created_at": formatTimestamp(info.Created), }) } // mergeConsecutiveRoles 合并连续同角色消息 func mergeConsecutiveRoles(messages []map[string]any) []map[string]any { if len(messages) <= 1 { return messages } var result []map[string]any for _, msg := range messages { if len(result) > 0 { lastRole := result[len(result)-1]["role"] currRole := msg["role"] if lastRole == currRole { // 合并 content lastContent := result[len(result)-1]["content"] currContent := msg["content"] switch lv := lastContent.(type) { case []map[string]any: if cv, ok := currContent.([]map[string]any); ok { result[len(result)-1]["content"] = append(lv, cv...) } case string: if cv, ok := currContent.(string); ok { result[len(result)-1]["content"] = lv + cv } } continue } } result = append(result, msg) } return result }