refactor: 实现 ConversionEngine 协议转换引擎,替代旧 protocol 包
引入 Canonical Model 和 ProtocolAdapter 架构,支持 OpenAI/Anthropic 协议间 无缝转换,统一 ProxyHandler 替代分散的 OpenAI/Anthropic Handler,简化 ProviderClient 为协议无关的 HTTP 发送器,Provider 新增 protocol 字段。
This commit is contained in:
427
backend/internal/conversion/anthropic/decoder.go
Normal file
427
backend/internal/conversion/anthropic/decoder.go
Normal file
@@ -0,0 +1,427 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user