引入 Canonical Model 和 ProtocolAdapter 架构,支持 OpenAI/Anthropic 协议间 无缝转换,统一 ProxyHandler 替代分散的 OpenAI/Anthropic Handler,简化 ProviderClient 为协议无关的 HTTP 发送器,Provider 新增 protocol 字段。
428 lines
11 KiB
Go
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
|
|
}
|