1
0
Files
lanyuanxiaoyao 1dac347d3b refactor: 实现 ConversionEngine 协议转换引擎,替代旧 protocol 包
引入 Canonical Model 和 ProtocolAdapter 架构,支持 OpenAI/Anthropic 协议间
无缝转换,统一 ProxyHandler 替代分散的 OpenAI/Anthropic Handler,简化
ProviderClient 为协议无关的 HTTP 发送器,Provider 新增 protocol 字段。
2026-04-20 00:36:27 +08:00

428 lines
11 KiB
Go

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
}