Просмотр исходного кода

fix: cluade to openai message content and stream reponse (#345)

* fix: cluade to openai message content and stream reponse

* fix: ci lint

* fix: ali anthropic api
zijiren 4 месяцев назад
Родитель
Сommit
f7fded35a1
3 измененных файлов с 143 добавлено и 124 удалено
  1. 7 8
      core/relay/adaptor/ali/adaptor.go
  2. 1 1
      core/relay/adaptor/doubao/main.go
  3. 135 115
      core/relay/adaptor/openai/claude.go

+ 7 - 8
core/relay/adaptor/ali/adaptor.go

@@ -103,7 +103,7 @@ func (a *Adaptor) GetRequestURL(meta *meta.Meta, _ adaptor.Store) (adaptor.Reque
 			URL:    url,
 		}, nil
 	case mode.Anthropic:
-		url, err := url.JoinPath(u, "/api/v2/apps/claude-code-proxy")
+		url, err := url.JoinPath(u, "/api/v2/apps/claude-code-proxy/v1/messages")
 		if err != nil {
 			return adaptor.RequestURL{}, err
 		}
@@ -123,12 +123,7 @@ func (a *Adaptor) SetupRequestHeader(
 	_ *gin.Context,
 	req *http.Request,
 ) error {
-	switch meta.Mode {
-	case mode.Anthropic:
-		req.Header.Set(anthropic.AnthropicTokenHeader, "Bearer "+meta.Channel.Key)
-	default:
-		req.Header.Set("Authorization", "Bearer "+meta.Channel.Key)
-	}
+	req.Header.Set("Authorization", "Bearer "+meta.Channel.Key)
 
 	// req.Header.Set("X-Dashscope-Plugin", meta.Channel.Config.Plugin)
 	return nil
@@ -197,7 +192,11 @@ func (a *Adaptor) DoResponse(
 	case mode.AudioTranscription:
 		return STTDoResponse(meta, c, resp)
 	case mode.Anthropic:
-		return anthropic.Handler(meta, c, resp)
+		if utils.IsStreamResponse(resp) {
+			return anthropic.StreamHandler(meta, c, resp)
+		} else {
+			return anthropic.Handler(meta, c, resp)
+		}
 	default:
 		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			fmt.Sprintf("unsupported mode: %s", meta.Mode),

+ 1 - 1
core/relay/adaptor/doubao/main.go

@@ -18,7 +18,7 @@ import (
 func GetRequestURL(meta *meta.Meta) (adaptor.RequestURL, error) {
 	u := meta.Channel.BaseURL
 	switch meta.Mode {
-	case mode.ChatCompletions:
+	case mode.ChatCompletions, mode.Anthropic:
 		if strings.HasPrefix(meta.ActualModel, "bot-") {
 			url, err := url.JoinPath(u, "/api/v3/bots/chat/completions")
 			if err != nil {

+ 135 - 115
core/relay/adaptor/openai/claude.go

@@ -2,6 +2,7 @@ package openai
 
 import (
 	"bufio"
+	"bytes"
 	"fmt"
 	"net/http"
 	"strconv"
@@ -72,7 +73,7 @@ func ConvertClaudeRequest(
 			"Content-Type":   {"application/json"},
 			"Content-Length": {strconv.Itoa(len(jsonData))},
 		},
-		Body: strings.NewReader(string(jsonData)),
+		Body: bytes.NewReader(jsonData),
 	}, nil
 }
 
@@ -112,68 +113,70 @@ func convertClaudeMessagesToOpenAI(
 		switch msg.Content.(type) {
 		case string:
 			openAIMsg.Content = msg.Content
-		case []map[string]any:
+		case []any:
 			rawBytes, _ := sonic.Marshal(msg.Content)
 
 			var content []relaymodel.ClaudeContent
 
 			_ = sonic.Unmarshal(rawBytes, &content)
-			if len(content) == 1 && content[0].Type == "text" {
-				// Simple text content
-				openAIMsg.Content = content[0].Text
-			} else {
-				// Multi-part content
-				var parts []relaymodel.MessageContent
-				for _, content := range content {
-					switch content.Type {
-					case "text":
-						parts = append(parts, relaymodel.MessageContent{
-							Type: relaymodel.ContentTypeText,
-							Text: content.Text,
-						})
-					case "image":
-						if content.Source != nil {
-							imageURL := relaymodel.ImageURL{}
-							switch content.Source.Type {
-							case "url":
-								imageURL.URL = content.Source.URL
-							case "base64":
-								imageURL.URL = fmt.Sprintf("data:%s;base64,%s",
-									content.Source.MediaType, content.Source.Data)
-							}
-
-							parts = append(parts, relaymodel.MessageContent{
-								Type:     relaymodel.ContentTypeImageURL,
-								ImageURL: &imageURL,
-							})
-						}
-					case "tool_result":
-						// Convert tool result to assistant message with tool calls
-						openAIMsg.Role = "tool"
-						openAIMsg.Content = content.Content
-						openAIMsg.ToolCallID = content.ToolUseID
-					case "tool_use":
-						// This should be handled as tool calls
-						if openAIMsg.ToolCalls == nil {
-							openAIMsg.ToolCalls = []relaymodel.ToolCall{}
+
+			var parts []relaymodel.MessageContent
+			for _, content := range content {
+				switch content.Type {
+				case "text":
+					parts = append(parts, relaymodel.MessageContent{
+						Type: relaymodel.ContentTypeText,
+						Text: content.Text,
+					})
+				case "thinking":
+					parts = append(parts, relaymodel.MessageContent{
+						Type: relaymodel.ContentTypeText,
+						Text: content.Thinking,
+					})
+				case "image":
+					if content.Source != nil {
+						imageURL := relaymodel.ImageURL{}
+						switch content.Source.Type {
+						case "url":
+							imageURL.URL = content.Source.URL
+						case "base64":
+							imageURL.URL = fmt.Sprintf("data:%s;base64,%s",
+								content.Source.MediaType, content.Source.Data)
 						}
 
-						args, _ := sonic.MarshalString(content.Input)
-						openAIMsg.ToolCalls = append(openAIMsg.ToolCalls, relaymodel.ToolCall{
-							ID:   content.ID,
-							Type: "function",
-							Function: relaymodel.Function{
-								Name:      content.Name,
-								Arguments: args,
-							},
+						parts = append(parts, relaymodel.MessageContent{
+							Type:     relaymodel.ContentTypeImageURL,
+							ImageURL: &imageURL,
 						})
 					}
-				}
+				case "tool_result":
+					// Convert tool result to assistant message with tool calls
+					openAIMsg.Role = "tool"
+					openAIMsg.Content = content.Content
+					openAIMsg.ToolCallID = content.ToolUseID
+				case "tool_use":
+					// This should be handled as tool calls
+					if openAIMsg.ToolCalls == nil {
+						openAIMsg.ToolCalls = []relaymodel.ToolCall{}
+					}
 
-				if len(parts) > 0 {
-					openAIMsg.Content = parts
+					args, _ := sonic.MarshalString(content.Input)
+					openAIMsg.ToolCalls = append(openAIMsg.ToolCalls, relaymodel.ToolCall{
+						ID:   content.ID,
+						Type: "function",
+						Function: relaymodel.Function{
+							Name:      content.Name,
+							Arguments: args,
+						},
+					})
+				default:
+					continue
 				}
 			}
+
+			if len(parts) > 0 {
+				openAIMsg.Content = parts
+			}
 		}
 
 		// Handle different content types
@@ -247,8 +250,6 @@ func convertClaudeToolChoice(toolChoice any) any {
 	return "auto"
 }
 
-// File: core/relay/adaptor/openai/claude.go (优化版)
-
 // ClaudeStreamHandler handles OpenAI streaming responses and converts them to Claude format
 func ClaudeStreamHandler(
 	meta *meta.Meta,
@@ -272,17 +273,27 @@ func ClaudeStreamHandler(
 
 	// Initialize Claude response tracking
 	var (
-		messageID             = "msg_" + common.ShortUUID()
-		contentText           strings.Builder
-		usage                 relaymodel.ChatUsage
-		stopReason            string
-		hasToolCalls          = false
-		toolCallsBuffer       = make(map[int]*relaymodel.ClaudeContent)
-		toolCallIndex         = 0
-		sentMessageStart      = false
-		sentContentBlockStart = false
+		messageID           = "msg_" + common.ShortUUID()
+		contentText         strings.Builder
+		thinkingText        strings.Builder
+		usage               relaymodel.ChatUsage
+		stopReason          string
+		currentContentIndex = -1
+		currentContentType  = ""
+		sentMessageStart    = false
+		toolCallsBuffer     = make(map[int]*relaymodel.ClaudeContent)
 	)
 
+	// Helper function to close current content block
+	closeCurrentBlock := func() {
+		if currentContentIndex >= 0 {
+			_ = render.ClaudeObjectData(c, relaymodel.ClaudeStreamResponse{
+				Type:  "content_block_stop",
+				Index: currentContentIndex,
+			})
+		}
+	}
+
 	for scanner.Scan() {
 		data := scanner.Bytes()
 		if !render.IsValidSSEData(data) {
@@ -311,7 +322,9 @@ func ClaudeStreamHandler(
 		// Send message_start event (only once)
 		if !sentMessageStart {
 			sentMessageStart = true
-			_ = render.ClaudeObjectData(c, relaymodel.ClaudeStreamResponse{
+
+			// Include initial usage if available
+			messageStartResp := relaymodel.ClaudeStreamResponse{
 				Type: "message_start",
 				Message: &relaymodel.ClaudeResponse{
 					ID:      messageID,
@@ -320,7 +333,15 @@ func ClaudeStreamHandler(
 					Model:   meta.ActualModel,
 					Content: []relaymodel.ClaudeContent{},
 				},
-			})
+			}
+
+			// Add initial usage if available
+			if openAIResponse.Usage != nil {
+				claudeUsage := openAIResponse.Usage.ToClaudeUsage()
+				messageStartResp.Message.Usage = claudeUsage
+			}
+
+			_ = render.ClaudeObjectData(c, messageStartResp)
 
 			// Send ping event
 			_ = render.ClaudeObjectData(c, relaymodel.ClaudeStreamResponse{Type: "ping"})
@@ -328,75 +349,80 @@ func ClaudeStreamHandler(
 
 		// Process each choice
 		for _, choice := range openAIResponse.Choices {
-			// Handle text content
-			if content, ok := choice.Delta.Content.(string); ok && content != "" {
-				if !sentContentBlockStart && !hasToolCalls {
-					// Send content_block_start for text
+			// Handle reasoning/thinking content
+			if choice.Delta.ReasoningContent != "" {
+				// If we're not in a thinking block, start one
+				if currentContentType != "thinking" {
+					closeCurrentBlock()
+
+					currentContentIndex++
+					currentContentType = "thinking"
+
 					_ = render.ClaudeObjectData(c, relaymodel.ClaudeStreamResponse{
 						Type:  "content_block_start",
-						Index: 0,
+						Index: currentContentIndex,
 						ContentBlock: &relaymodel.ClaudeContent{
-							Type: "text",
-							Text: "",
+							Type:     "thinking",
+							Thinking: "",
 						},
 					})
-					sentContentBlockStart = true
 				}
 
-				contentText.WriteString(content)
+				thinkingText.WriteString(choice.Delta.ReasoningContent)
 
-				// Send content_block_delta
 				_ = render.ClaudeObjectData(c, relaymodel.ClaudeStreamResponse{
 					Type:  "content_block_delta",
-					Index: 0,
+					Index: currentContentIndex,
 					Delta: &relaymodel.ClaudeDelta{
-						Type: "text_delta",
-						Text: content,
+						Type:     "thinking_delta",
+						Thinking: choice.Delta.ReasoningContent,
 					},
 				})
 			}
 
-			if choice.Delta.ReasoningContent != "" {
-				if !sentContentBlockStart && !hasToolCalls {
-					// Send content_block_start for text
+			// Handle text content
+			if content, ok := choice.Delta.Content.(string); ok && content != "" {
+				// If we're not in a text block, start one
+				if currentContentType != "text" {
+					closeCurrentBlock()
+
+					currentContentIndex++
+					currentContentType = "text"
+
 					_ = render.ClaudeObjectData(c, relaymodel.ClaudeStreamResponse{
 						Type:  "content_block_start",
-						Index: 0,
+						Index: currentContentIndex,
 						ContentBlock: &relaymodel.ClaudeContent{
 							Type: "text",
 							Text: "",
 						},
 					})
-					sentContentBlockStart = true
 				}
 
-				contentText.WriteString(choice.Delta.ReasoningContent)
+				contentText.WriteString(content)
+
 				_ = render.ClaudeObjectData(c, relaymodel.ClaudeStreamResponse{
 					Type:  "content_block_delta",
-					Index: 0,
+					Index: currentContentIndex,
 					Delta: &relaymodel.ClaudeDelta{
-						Type: "thinking_delta",
-						Text: choice.Delta.ReasoningContent,
+						Type: "text_delta",
+						Text: content,
 					},
 				})
 			}
 
 			// Handle tool calls
 			if len(choice.Delta.ToolCalls) > 0 {
-				hasToolCalls = true
-
 				for _, toolCall := range choice.Delta.ToolCalls {
 					idx := toolCall.Index
+
 					// Initialize tool call if new
 					if _, exists := toolCallsBuffer[idx]; !exists {
-						if sentContentBlockStart && idx == 0 {
-							// Close text block if it was started
-							_ = render.ClaudeObjectData(c, relaymodel.ClaudeStreamResponse{
-								Type:  "content_block_stop",
-								Index: 0,
-							})
-							toolCallIndex = 1
-						}
+						// Close current block if needed
+						closeCurrentBlock()
+
+						currentContentIndex++
+						currentContentType = "tool_use"
 
 						toolCallsBuffer[idx] = &relaymodel.ClaudeContent{
 							Type:  "tool_use",
@@ -408,7 +434,7 @@ func ClaudeStreamHandler(
 						// Send content_block_start for tool use
 						_ = render.ClaudeObjectData(c, relaymodel.ClaudeStreamResponse{
 							Type:         "content_block_start",
-							Index:        toolCallIndex + idx,
+							Index:        currentContentIndex,
 							ContentBlock: toolCallsBuffer[idx],
 						})
 					}
@@ -417,7 +443,7 @@ func ClaudeStreamHandler(
 					if toolCall.Function.Arguments != "" {
 						_ = render.ClaudeObjectData(c, relaymodel.ClaudeStreamResponse{
 							Type:  "content_block_delta",
-							Index: toolCallIndex + idx,
+							Index: currentContentIndex,
 							Delta: &relaymodel.ClaudeDelta{
 								Type:        "input_json_delta",
 								PartialJSON: toolCall.Function.Arguments,
@@ -438,26 +464,18 @@ func ClaudeStreamHandler(
 		log.Error("error reading stream: " + err.Error())
 	}
 
-	// Close any open content blocks
-	if sentContentBlockStart {
-		_ = render.ClaudeObjectData(c, relaymodel.ClaudeStreamResponse{
-			Type:  "content_block_stop",
-			Index: 0,
-		})
-	}
-
-	// Close tool blocks
-	for idx := range toolCallsBuffer {
-		_ = render.ClaudeObjectData(c, relaymodel.ClaudeStreamResponse{
-			Type:  "content_block_stop",
-			Index: toolCallIndex + idx,
-		})
-	}
+	// Close the last open content block
+	closeCurrentBlock()
 
 	// Calculate final usage if not provided
-	if usage.TotalTokens == 0 && contentText.Len() > 0 {
+	if usage.TotalTokens == 0 && (contentText.Len() > 0 || thinkingText.Len() > 0) {
+		totalText := contentText.String()
+		if thinkingText.Len() > 0 {
+			totalText = thinkingText.String() + "\n" + totalText
+		}
+
 		usage = ResponseText2Usage(
-			contentText.String(),
+			totalText,
 			meta.ActualModel,
 			int64(meta.RequestUsage.InputTokens),
 		)
@@ -469,6 +487,7 @@ func ClaudeStreamHandler(
 		stopReason = claudeStopReasonEndTurn
 	}
 
+	// Send message_delta with final usage
 	_ = render.ClaudeObjectData(c, relaymodel.ClaudeStreamResponse{
 		Type: "message_delta",
 		Delta: &relaymodel.ClaudeDelta{
@@ -477,6 +496,7 @@ func ClaudeStreamHandler(
 		Usage: &claudeUsage,
 	})
 
+	// Send message_stop
 	_ = render.ClaudeObjectData(c, relaymodel.ClaudeStreamResponse{
 		Type: "message_stop",
 	})