Merge pull request #1531 from QuantumNous/claude-code
feat: 完善格式转换,修复gemini渠道和openai渠道在claude code中使用的问题
This commit is contained in:
@@ -31,6 +31,9 @@ func UnmarshalBodyReusable(c *gin.Context, v any) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
//if DebugEnabled {
|
||||||
|
// println("UnmarshalBodyReusable request body:", string(requestBody))
|
||||||
|
//}
|
||||||
contentType := c.Request.Header.Get("Content-Type")
|
contentType := c.Request.Header.Get("Content-Type")
|
||||||
if strings.HasPrefix(contentType, "application/json") {
|
if strings.HasPrefix(contentType, "application/json") {
|
||||||
err = Unmarshal(requestBody, &v)
|
err = Unmarshal(requestBody, &v)
|
||||||
|
|||||||
@@ -199,6 +199,18 @@ type ClaudeRequest struct {
|
|||||||
Thinking *Thinking `json:"thinking,omitempty"`
|
Thinking *Thinking `json:"thinking,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ClaudeRequest) SearchToolNameByToolCallId(toolCallId string) string {
|
||||||
|
for _, message := range c.Messages {
|
||||||
|
content, _ := message.ParseContent()
|
||||||
|
for _, mediaMessage := range content {
|
||||||
|
if mediaMessage.Id == toolCallId {
|
||||||
|
return mediaMessage.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// AddTool 添加工具到请求中
|
// AddTool 添加工具到请求中
|
||||||
func (c *ClaudeRequest) AddTool(tool any) {
|
func (c *ClaudeRequest) AddTool(tool any) {
|
||||||
if c.Tools == nil {
|
if c.Tools == nil {
|
||||||
|
|||||||
@@ -143,6 +143,13 @@ type ChatCompletionsStreamResponse struct {
|
|||||||
Usage *Usage `json:"usage"`
|
Usage *Usage `json:"usage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ChatCompletionsStreamResponse) IsFinished() bool {
|
||||||
|
if len(c.Choices) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return c.Choices[0].FinishReason != nil && *c.Choices[0].FinishReason != ""
|
||||||
|
}
|
||||||
|
|
||||||
func (c *ChatCompletionsStreamResponse) IsToolCall() bool {
|
func (c *ChatCompletionsStreamResponse) IsToolCall() bool {
|
||||||
if len(c.Choices) == 0 {
|
if len(c.Choices) == 0 {
|
||||||
return false
|
return false
|
||||||
@@ -157,6 +164,19 @@ func (c *ChatCompletionsStreamResponse) GetFirstToolCall() *ToolCallResponse {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ChatCompletionsStreamResponse) ClearToolCalls() {
|
||||||
|
if !c.IsToolCall() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for choiceIdx := range c.Choices {
|
||||||
|
for callIdx := range c.Choices[choiceIdx].Delta.ToolCalls {
|
||||||
|
c.Choices[choiceIdx].Delta.ToolCalls[callIdx].ID = ""
|
||||||
|
c.Choices[choiceIdx].Delta.ToolCalls[callIdx].Type = nil
|
||||||
|
c.Choices[choiceIdx].Delta.ToolCalls[callIdx].Function.Name = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *ChatCompletionsStreamResponse) Copy() *ChatCompletionsStreamResponse {
|
func (c *ChatCompletionsStreamResponse) Copy() *ChatCompletionsStreamResponse {
|
||||||
choices := make([]ChatCompletionsStreamResponseChoice, len(c.Choices))
|
choices := make([]ChatCompletionsStreamResponseChoice, len(c.Choices))
|
||||||
copy(choices, c.Choices)
|
copy(choices, c.Choices)
|
||||||
|
|||||||
@@ -835,6 +835,7 @@ func streamResponseGeminiChat2OpenAI(geminiResponse *dto.GeminiChatResponse) (*d
|
|||||||
call.SetIndex(len(choice.Delta.ToolCalls))
|
call.SetIndex(len(choice.Delta.ToolCalls))
|
||||||
choice.Delta.ToolCalls = append(choice.Delta.ToolCalls, *call)
|
choice.Delta.ToolCalls = append(choice.Delta.ToolCalls, *call)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if part.Thought {
|
} else if part.Thought {
|
||||||
isThought = true
|
isThought = true
|
||||||
texts = append(texts, part.Text)
|
texts = append(texts, part.Text)
|
||||||
@@ -895,6 +896,7 @@ func GeminiChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *
|
|||||||
responseText := strings.Builder{}
|
responseText := strings.Builder{}
|
||||||
var usage = &dto.Usage{}
|
var usage = &dto.Usage{}
|
||||||
var imageCount int
|
var imageCount int
|
||||||
|
finishReason := constant.FinishReasonStop
|
||||||
|
|
||||||
helper.StreamScannerHandler(c, resp, info, func(data string) bool {
|
helper.StreamScannerHandler(c, resp, info, func(data string) bool {
|
||||||
var geminiResponse dto.GeminiChatResponse
|
var geminiResponse dto.GeminiChatResponse
|
||||||
@@ -936,9 +938,21 @@ func GeminiChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *
|
|||||||
|
|
||||||
if info.SendResponseCount == 0 {
|
if info.SendResponseCount == 0 {
|
||||||
// send first response
|
// send first response
|
||||||
err = handleStream(c, info, helper.GenerateStartEmptyResponse(id, createAt, info.UpstreamModelName, nil))
|
emptyResponse := helper.GenerateStartEmptyResponse(id, createAt, info.UpstreamModelName, nil)
|
||||||
if err != nil {
|
if response.IsToolCall() {
|
||||||
common.LogError(c, err.Error())
|
emptyResponse.Choices[0].Delta.ToolCalls = make([]dto.ToolCallResponse, 1)
|
||||||
|
emptyResponse.Choices[0].Delta.ToolCalls[0] = *response.GetFirstToolCall()
|
||||||
|
emptyResponse.Choices[0].Delta.ToolCalls[0].Function.Arguments = ""
|
||||||
|
finishReason = constant.FinishReasonToolCalls
|
||||||
|
err = handleStream(c, info, emptyResponse)
|
||||||
|
if err != nil {
|
||||||
|
common.LogError(c, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
response.ClearToolCalls()
|
||||||
|
if response.IsFinished() {
|
||||||
|
response.Choices[0].FinishReason = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -947,7 +961,7 @@ func GeminiChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *
|
|||||||
common.LogError(c, err.Error())
|
common.LogError(c, err.Error())
|
||||||
}
|
}
|
||||||
if isStop {
|
if isStop {
|
||||||
_ = handleStream(c, info, helper.GenerateStopResponse(id, createAt, info.UpstreamModelName, constant.FinishReasonStop))
|
_ = handleStream(c, info, helper.GenerateStopResponse(id, createAt, info.UpstreamModelName, finishReason))
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
@@ -1026,13 +1040,26 @@ func GeminiChatHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.R
|
|||||||
}
|
}
|
||||||
|
|
||||||
fullTextResponse.Usage = usage
|
fullTextResponse.Usage = usage
|
||||||
jsonResponse, err := json.Marshal(fullTextResponse)
|
|
||||||
if err != nil {
|
switch info.RelayFormat {
|
||||||
return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
|
case relaycommon.RelayFormatOpenAI:
|
||||||
|
responseBody, err = common.Marshal(fullTextResponse)
|
||||||
|
if err != nil {
|
||||||
|
return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
|
||||||
|
}
|
||||||
|
case relaycommon.RelayFormatClaude:
|
||||||
|
claudeResp := service.ResponseOpenAI2Claude(fullTextResponse, info)
|
||||||
|
claudeRespStr, err := common.Marshal(claudeResp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
|
||||||
|
}
|
||||||
|
responseBody = claudeRespStr
|
||||||
|
case relaycommon.RelayFormatGemini:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
c.Writer.Header().Set("Content-Type", "application/json")
|
|
||||||
c.Writer.WriteHeader(resp.StatusCode)
|
common.IOCopyBytesGracefully(c, resp, responseBody)
|
||||||
c.Writer.Write(jsonResponse)
|
|
||||||
return &usage, nil
|
return &usage, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,10 +63,26 @@ func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayIn
|
|||||||
//if !strings.Contains(request.Model, "claude") {
|
//if !strings.Contains(request.Model, "claude") {
|
||||||
// return nil, fmt.Errorf("you are using openai channel type with path /v1/messages, only claude model supported convert, but got %s", request.Model)
|
// return nil, fmt.Errorf("you are using openai channel type with path /v1/messages, only claude model supported convert, but got %s", request.Model)
|
||||||
//}
|
//}
|
||||||
|
//if common.DebugEnabled {
|
||||||
|
// bodyBytes := []byte(common.GetJsonString(request))
|
||||||
|
// err := os.WriteFile(fmt.Sprintf("claude_request_%s.txt", c.GetString(common.RequestIdKey)), bodyBytes, 0644)
|
||||||
|
// if err != nil {
|
||||||
|
// println(fmt.Sprintf("failed to save request body to file: %v", err))
|
||||||
|
// }
|
||||||
|
//}
|
||||||
aiRequest, err := service.ClaudeToOpenAIRequest(*request, info)
|
aiRequest, err := service.ClaudeToOpenAIRequest(*request, info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
//if common.DebugEnabled {
|
||||||
|
// println(fmt.Sprintf("convert claude to openai request result: %s", common.GetJsonString(aiRequest)))
|
||||||
|
// // Save request body to file for debugging
|
||||||
|
// bodyBytes := []byte(common.GetJsonString(aiRequest))
|
||||||
|
// err = os.WriteFile(fmt.Sprintf("claude_to_openai_request_%s.txt", c.GetString(common.RequestIdKey)), bodyBytes, 0644)
|
||||||
|
// if err != nil {
|
||||||
|
// println(fmt.Sprintf("failed to save request body to file: %v", err))
|
||||||
|
// }
|
||||||
|
//}
|
||||||
if info.SupportStreamOptions && info.IsStream {
|
if info.SupportStreamOptions && info.IsStream {
|
||||||
aiRequest.StreamOptions = &dto.StreamOptions{
|
aiRequest.StreamOptions = &dto.StreamOptions{
|
||||||
IncludeUsage: true,
|
IncludeUsage: true,
|
||||||
|
|||||||
@@ -153,9 +153,13 @@ func ClaudeToOpenAIRequest(claudeRequest dto.ClaudeRequest, info *relaycommon.Re
|
|||||||
toolCalls = append(toolCalls, toolCall)
|
toolCalls = append(toolCalls, toolCall)
|
||||||
case "tool_result":
|
case "tool_result":
|
||||||
// Add tool result as a separate message
|
// Add tool result as a separate message
|
||||||
|
toolName := mediaMsg.Name
|
||||||
|
if toolName == "" {
|
||||||
|
toolName = claudeRequest.SearchToolNameByToolCallId(mediaMsg.ToolUseId)
|
||||||
|
}
|
||||||
oaiToolMessage := dto.Message{
|
oaiToolMessage := dto.Message{
|
||||||
Role: "tool",
|
Role: "tool",
|
||||||
Name: &mediaMsg.Name,
|
Name: &toolName,
|
||||||
ToolCallId: mediaMsg.ToolUseId,
|
ToolCallId: mediaMsg.ToolUseId,
|
||||||
}
|
}
|
||||||
//oaiToolMessage.SetStringContent(*mediaMsg.GetMediaContent().Text)
|
//oaiToolMessage.SetStringContent(*mediaMsg.GetMediaContent().Text)
|
||||||
@@ -218,12 +222,14 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
|
|||||||
// Type: "ping",
|
// Type: "ping",
|
||||||
//})
|
//})
|
||||||
if openAIResponse.IsToolCall() {
|
if openAIResponse.IsToolCall() {
|
||||||
|
info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeTools
|
||||||
resp := &dto.ClaudeResponse{
|
resp := &dto.ClaudeResponse{
|
||||||
Type: "content_block_start",
|
Type: "content_block_start",
|
||||||
ContentBlock: &dto.ClaudeMediaMessage{
|
ContentBlock: &dto.ClaudeMediaMessage{
|
||||||
Id: openAIResponse.GetFirstToolCall().ID,
|
Id: openAIResponse.GetFirstToolCall().ID,
|
||||||
Type: "tool_use",
|
Type: "tool_use",
|
||||||
Name: openAIResponse.GetFirstToolCall().Function.Name,
|
Name: openAIResponse.GetFirstToolCall().Function.Name,
|
||||||
|
Input: map[string]interface{}{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
resp.SetIndex(0)
|
resp.SetIndex(0)
|
||||||
|
|||||||
Reference in New Issue
Block a user