This commit refactors the logging mechanism across the application by replacing direct logger calls with a centralized logging approach using the `common` package. Key changes include: - Replaced instances of `logger.SysLog` and `logger.FatalLog` with `common.SysLog` and `common.FatalLog` for consistent logging practices. - Updated resource initialization error handling to utilize the new logging structure, enhancing maintainability and readability. - Minor adjustments to improve code clarity and organization throughout various modules. This change aims to streamline logging and improve the overall architecture of the codebase.
814 lines
26 KiB
Go
814 lines
26 KiB
Go
package claude
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"one-api/common"
|
||
"one-api/dto"
|
||
"one-api/logger"
|
||
"one-api/relay/channel/openrouter"
|
||
relaycommon "one-api/relay/common"
|
||
"one-api/relay/helper"
|
||
"one-api/service"
|
||
"one-api/setting/model_setting"
|
||
"one-api/types"
|
||
"strings"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
const (
|
||
WebSearchMaxUsesLow = 1
|
||
WebSearchMaxUsesMedium = 5
|
||
WebSearchMaxUsesHigh = 10
|
||
)
|
||
|
||
func stopReasonClaude2OpenAI(reason string) string {
|
||
switch reason {
|
||
case "stop_sequence":
|
||
return "stop"
|
||
case "end_turn":
|
||
return "stop"
|
||
case "max_tokens":
|
||
return "max_tokens"
|
||
case "tool_use":
|
||
return "tool_calls"
|
||
default:
|
||
return reason
|
||
}
|
||
}
|
||
|
||
func RequestOpenAI2ClaudeComplete(textRequest dto.GeneralOpenAIRequest) *dto.ClaudeRequest {
|
||
|
||
claudeRequest := dto.ClaudeRequest{
|
||
Model: textRequest.Model,
|
||
Prompt: "",
|
||
StopSequences: nil,
|
||
Temperature: textRequest.Temperature,
|
||
TopP: textRequest.TopP,
|
||
TopK: textRequest.TopK,
|
||
Stream: textRequest.Stream,
|
||
}
|
||
if claudeRequest.MaxTokensToSample == 0 {
|
||
claudeRequest.MaxTokensToSample = 4096
|
||
}
|
||
prompt := ""
|
||
for _, message := range textRequest.Messages {
|
||
if message.Role == "user" {
|
||
prompt += fmt.Sprintf("\n\nHuman: %s", message.StringContent())
|
||
} else if message.Role == "assistant" {
|
||
prompt += fmt.Sprintf("\n\nAssistant: %s", message.StringContent())
|
||
} else if message.Role == "system" {
|
||
if prompt == "" {
|
||
prompt = message.StringContent()
|
||
}
|
||
}
|
||
}
|
||
prompt += "\n\nAssistant:"
|
||
claudeRequest.Prompt = prompt
|
||
return &claudeRequest
|
||
}
|
||
|
||
func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*dto.ClaudeRequest, error) {
|
||
claudeTools := make([]any, 0, len(textRequest.Tools))
|
||
|
||
for _, tool := range textRequest.Tools {
|
||
if params, ok := tool.Function.Parameters.(map[string]any); ok {
|
||
claudeTool := dto.Tool{
|
||
Name: tool.Function.Name,
|
||
Description: tool.Function.Description,
|
||
}
|
||
claudeTool.InputSchema = make(map[string]interface{})
|
||
if params["type"] != nil {
|
||
claudeTool.InputSchema["type"] = params["type"].(string)
|
||
}
|
||
claudeTool.InputSchema["properties"] = params["properties"]
|
||
claudeTool.InputSchema["required"] = params["required"]
|
||
for s, a := range params {
|
||
if s == "type" || s == "properties" || s == "required" {
|
||
continue
|
||
}
|
||
claudeTool.InputSchema[s] = a
|
||
}
|
||
claudeTools = append(claudeTools, &claudeTool)
|
||
}
|
||
}
|
||
|
||
// Web search tool
|
||
// https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/web-search-tool
|
||
if textRequest.WebSearchOptions != nil {
|
||
webSearchTool := dto.ClaudeWebSearchTool{
|
||
Type: "web_search_20250305",
|
||
Name: "web_search",
|
||
}
|
||
|
||
// 处理 user_location
|
||
if textRequest.WebSearchOptions.UserLocation != nil {
|
||
anthropicUserLocation := &dto.ClaudeWebSearchUserLocation{
|
||
Type: "approximate", // 固定为 "approximate"
|
||
}
|
||
|
||
// 解析 UserLocation JSON
|
||
var userLocationMap map[string]interface{}
|
||
if err := json.Unmarshal(textRequest.WebSearchOptions.UserLocation, &userLocationMap); err == nil {
|
||
// 检查是否有 approximate 字段
|
||
if approximateData, ok := userLocationMap["approximate"].(map[string]interface{}); ok {
|
||
if timezone, ok := approximateData["timezone"].(string); ok && timezone != "" {
|
||
anthropicUserLocation.Timezone = timezone
|
||
}
|
||
if country, ok := approximateData["country"].(string); ok && country != "" {
|
||
anthropicUserLocation.Country = country
|
||
}
|
||
if region, ok := approximateData["region"].(string); ok && region != "" {
|
||
anthropicUserLocation.Region = region
|
||
}
|
||
if city, ok := approximateData["city"].(string); ok && city != "" {
|
||
anthropicUserLocation.City = city
|
||
}
|
||
}
|
||
}
|
||
|
||
webSearchTool.UserLocation = anthropicUserLocation
|
||
}
|
||
|
||
// 处理 search_context_size 转换为 max_uses
|
||
if textRequest.WebSearchOptions.SearchContextSize != "" {
|
||
switch textRequest.WebSearchOptions.SearchContextSize {
|
||
case "low":
|
||
webSearchTool.MaxUses = WebSearchMaxUsesLow
|
||
case "medium":
|
||
webSearchTool.MaxUses = WebSearchMaxUsesMedium
|
||
case "high":
|
||
webSearchTool.MaxUses = WebSearchMaxUsesHigh
|
||
}
|
||
}
|
||
|
||
claudeTools = append(claudeTools, &webSearchTool)
|
||
}
|
||
|
||
claudeRequest := dto.ClaudeRequest{
|
||
Model: textRequest.Model,
|
||
MaxTokens: textRequest.GetMaxTokens(),
|
||
StopSequences: nil,
|
||
Temperature: textRequest.Temperature,
|
||
TopP: textRequest.TopP,
|
||
TopK: textRequest.TopK,
|
||
Stream: textRequest.Stream,
|
||
Tools: claudeTools,
|
||
}
|
||
|
||
// 处理 tool_choice 和 parallel_tool_calls
|
||
if textRequest.ToolChoice != nil || textRequest.ParallelTooCalls != nil {
|
||
claudeToolChoice := mapToolChoice(textRequest.ToolChoice, textRequest.ParallelTooCalls)
|
||
if claudeToolChoice != nil {
|
||
claudeRequest.ToolChoice = claudeToolChoice
|
||
}
|
||
}
|
||
|
||
if claudeRequest.MaxTokens == 0 {
|
||
claudeRequest.MaxTokens = uint(model_setting.GetClaudeSettings().GetDefaultMaxTokens(textRequest.Model))
|
||
}
|
||
|
||
if model_setting.GetClaudeSettings().ThinkingAdapterEnabled &&
|
||
strings.HasSuffix(textRequest.Model, "-thinking") {
|
||
|
||
// 因为BudgetTokens 必须大于1024
|
||
if claudeRequest.MaxTokens < 1280 {
|
||
claudeRequest.MaxTokens = 1280
|
||
}
|
||
|
||
// BudgetTokens 为 max_tokens 的 80%
|
||
claudeRequest.Thinking = &dto.Thinking{
|
||
Type: "enabled",
|
||
BudgetTokens: common.GetPointer[int](int(float64(claudeRequest.MaxTokens) * model_setting.GetClaudeSettings().ThinkingAdapterBudgetTokensPercentage)),
|
||
}
|
||
// TODO: 临时处理
|
||
// https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#important-considerations-when-using-extended-thinking
|
||
claudeRequest.TopP = 0
|
||
claudeRequest.Temperature = common.GetPointer[float64](1.0)
|
||
claudeRequest.Model = strings.TrimSuffix(textRequest.Model, "-thinking")
|
||
}
|
||
|
||
if textRequest.ReasoningEffort != "" {
|
||
switch textRequest.ReasoningEffort {
|
||
case "low":
|
||
claudeRequest.Thinking = &dto.Thinking{
|
||
Type: "enabled",
|
||
BudgetTokens: common.GetPointer[int](1280),
|
||
}
|
||
case "medium":
|
||
claudeRequest.Thinking = &dto.Thinking{
|
||
Type: "enabled",
|
||
BudgetTokens: common.GetPointer[int](2048),
|
||
}
|
||
case "high":
|
||
claudeRequest.Thinking = &dto.Thinking{
|
||
Type: "enabled",
|
||
BudgetTokens: common.GetPointer[int](4096),
|
||
}
|
||
}
|
||
}
|
||
|
||
// 指定了 reasoning 参数,覆盖 budgetTokens
|
||
if textRequest.Reasoning != nil {
|
||
var reasoning openrouter.RequestReasoning
|
||
if err := common.Unmarshal(textRequest.Reasoning, &reasoning); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
budgetTokens := reasoning.MaxTokens
|
||
if budgetTokens > 0 {
|
||
claudeRequest.Thinking = &dto.Thinking{
|
||
Type: "enabled",
|
||
BudgetTokens: &budgetTokens,
|
||
}
|
||
}
|
||
}
|
||
|
||
if textRequest.Stop != nil {
|
||
// stop maybe string/array string, convert to array string
|
||
switch textRequest.Stop.(type) {
|
||
case string:
|
||
claudeRequest.StopSequences = []string{textRequest.Stop.(string)}
|
||
case []interface{}:
|
||
stopSequences := make([]string, 0)
|
||
for _, stop := range textRequest.Stop.([]interface{}) {
|
||
stopSequences = append(stopSequences, stop.(string))
|
||
}
|
||
claudeRequest.StopSequences = stopSequences
|
||
}
|
||
}
|
||
formatMessages := make([]dto.Message, 0)
|
||
lastMessage := dto.Message{
|
||
Role: "tool",
|
||
}
|
||
for i, message := range textRequest.Messages {
|
||
if message.Role == "" {
|
||
textRequest.Messages[i].Role = "user"
|
||
}
|
||
fmtMessage := dto.Message{
|
||
Role: message.Role,
|
||
Content: message.Content,
|
||
}
|
||
if message.Role == "tool" {
|
||
fmtMessage.ToolCallId = message.ToolCallId
|
||
}
|
||
if message.Role == "assistant" && message.ToolCalls != nil {
|
||
fmtMessage.ToolCalls = message.ToolCalls
|
||
}
|
||
if lastMessage.Role == message.Role && lastMessage.Role != "tool" {
|
||
if lastMessage.IsStringContent() && message.IsStringContent() {
|
||
fmtMessage.SetStringContent(strings.Trim(fmt.Sprintf("%s %s", lastMessage.StringContent(), message.StringContent()), "\""))
|
||
// delete last message
|
||
formatMessages = formatMessages[:len(formatMessages)-1]
|
||
}
|
||
}
|
||
if fmtMessage.Content == nil {
|
||
fmtMessage.SetStringContent("...")
|
||
}
|
||
formatMessages = append(formatMessages, fmtMessage)
|
||
lastMessage = fmtMessage
|
||
}
|
||
|
||
claudeMessages := make([]dto.ClaudeMessage, 0)
|
||
isFirstMessage := true
|
||
for _, message := range formatMessages {
|
||
if message.Role == "system" {
|
||
if message.IsStringContent() {
|
||
claudeRequest.System = message.StringContent()
|
||
} else {
|
||
contents := message.ParseContent()
|
||
content := ""
|
||
for _, ctx := range contents {
|
||
if ctx.Type == "text" {
|
||
content += ctx.Text
|
||
}
|
||
}
|
||
claudeRequest.System = content
|
||
}
|
||
} else {
|
||
if isFirstMessage {
|
||
isFirstMessage = false
|
||
if message.Role != "user" {
|
||
// fix: first message is assistant, add user message
|
||
claudeMessage := dto.ClaudeMessage{
|
||
Role: "user",
|
||
Content: []dto.ClaudeMediaMessage{
|
||
{
|
||
Type: "text",
|
||
Text: common.GetPointer[string]("..."),
|
||
},
|
||
},
|
||
}
|
||
claudeMessages = append(claudeMessages, claudeMessage)
|
||
}
|
||
}
|
||
claudeMessage := dto.ClaudeMessage{
|
||
Role: message.Role,
|
||
}
|
||
if message.Role == "tool" {
|
||
if len(claudeMessages) > 0 && claudeMessages[len(claudeMessages)-1].Role == "user" {
|
||
lastMessage := claudeMessages[len(claudeMessages)-1]
|
||
if content, ok := lastMessage.Content.(string); ok {
|
||
lastMessage.Content = []dto.ClaudeMediaMessage{
|
||
{
|
||
Type: "text",
|
||
Text: common.GetPointer[string](content),
|
||
},
|
||
}
|
||
}
|
||
lastMessage.Content = append(lastMessage.Content.([]dto.ClaudeMediaMessage), dto.ClaudeMediaMessage{
|
||
Type: "tool_result",
|
||
ToolUseId: message.ToolCallId,
|
||
Content: message.Content,
|
||
})
|
||
claudeMessages[len(claudeMessages)-1] = lastMessage
|
||
continue
|
||
} else {
|
||
claudeMessage.Role = "user"
|
||
claudeMessage.Content = []dto.ClaudeMediaMessage{
|
||
{
|
||
Type: "tool_result",
|
||
ToolUseId: message.ToolCallId,
|
||
Content: message.Content,
|
||
},
|
||
}
|
||
}
|
||
} else if message.IsStringContent() && message.ToolCalls == nil {
|
||
claudeMessage.Content = message.StringContent()
|
||
} else {
|
||
claudeMediaMessages := make([]dto.ClaudeMediaMessage, 0)
|
||
for _, mediaMessage := range message.ParseContent() {
|
||
claudeMediaMessage := dto.ClaudeMediaMessage{
|
||
Type: mediaMessage.Type,
|
||
}
|
||
if mediaMessage.Type == "text" {
|
||
claudeMediaMessage.Text = common.GetPointer[string](mediaMessage.Text)
|
||
} else {
|
||
imageUrl := mediaMessage.GetImageMedia()
|
||
claudeMediaMessage.Type = "image"
|
||
claudeMediaMessage.Source = &dto.ClaudeMessageSource{
|
||
Type: "base64",
|
||
}
|
||
// 判断是否是url
|
||
if strings.HasPrefix(imageUrl.Url, "http") {
|
||
// 是url,获取图片的类型和base64编码的数据
|
||
fileData, err := service.GetFileBase64FromUrl(imageUrl.Url)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("get file base64 from url failed: %s", err.Error())
|
||
}
|
||
claudeMediaMessage.Source.MediaType = fileData.MimeType
|
||
claudeMediaMessage.Source.Data = fileData.Base64Data
|
||
} else {
|
||
_, format, base64String, err := service.DecodeBase64ImageData(imageUrl.Url)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
claudeMediaMessage.Source.MediaType = "image/" + format
|
||
claudeMediaMessage.Source.Data = base64String
|
||
}
|
||
}
|
||
claudeMediaMessages = append(claudeMediaMessages, claudeMediaMessage)
|
||
}
|
||
if message.ToolCalls != nil {
|
||
for _, toolCall := range message.ParseToolCalls() {
|
||
inputObj := make(map[string]any)
|
||
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &inputObj); err != nil {
|
||
common.SysLog("tool call function arguments is not a map[string]any: " + fmt.Sprintf("%v", toolCall.Function.Arguments))
|
||
continue
|
||
}
|
||
claudeMediaMessages = append(claudeMediaMessages, dto.ClaudeMediaMessage{
|
||
Type: "tool_use",
|
||
Id: toolCall.ID,
|
||
Name: toolCall.Function.Name,
|
||
Input: inputObj,
|
||
})
|
||
}
|
||
}
|
||
claudeMessage.Content = claudeMediaMessages
|
||
}
|
||
claudeMessages = append(claudeMessages, claudeMessage)
|
||
}
|
||
}
|
||
claudeRequest.Prompt = ""
|
||
claudeRequest.Messages = claudeMessages
|
||
return &claudeRequest, nil
|
||
}
|
||
|
||
func StreamResponseClaude2OpenAI(reqMode int, claudeResponse *dto.ClaudeResponse) *dto.ChatCompletionsStreamResponse {
|
||
var response dto.ChatCompletionsStreamResponse
|
||
response.Object = "chat.completion.chunk"
|
||
response.Model = claudeResponse.Model
|
||
response.Choices = make([]dto.ChatCompletionsStreamResponseChoice, 0)
|
||
tools := make([]dto.ToolCallResponse, 0)
|
||
fcIdx := 0
|
||
if claudeResponse.Index != nil {
|
||
fcIdx = *claudeResponse.Index - 1
|
||
if fcIdx < 0 {
|
||
fcIdx = 0
|
||
}
|
||
}
|
||
var choice dto.ChatCompletionsStreamResponseChoice
|
||
if reqMode == RequestModeCompletion {
|
||
choice.Delta.SetContentString(claudeResponse.Completion)
|
||
finishReason := stopReasonClaude2OpenAI(claudeResponse.StopReason)
|
||
if finishReason != "null" {
|
||
choice.FinishReason = &finishReason
|
||
}
|
||
} else {
|
||
if claudeResponse.Type == "message_start" {
|
||
response.Id = claudeResponse.Message.Id
|
||
response.Model = claudeResponse.Message.Model
|
||
//claudeUsage = &claudeResponse.Message.Usage
|
||
choice.Delta.SetContentString("")
|
||
choice.Delta.Role = "assistant"
|
||
} else if claudeResponse.Type == "content_block_start" {
|
||
if claudeResponse.ContentBlock != nil {
|
||
//choice.Delta.SetContentString(claudeResponse.ContentBlock.Text)
|
||
if claudeResponse.ContentBlock.Type == "tool_use" {
|
||
tools = append(tools, dto.ToolCallResponse{
|
||
Index: common.GetPointer(fcIdx),
|
||
ID: claudeResponse.ContentBlock.Id,
|
||
Type: "function",
|
||
Function: dto.FunctionResponse{
|
||
Name: claudeResponse.ContentBlock.Name,
|
||
Arguments: "",
|
||
},
|
||
})
|
||
}
|
||
} else {
|
||
return nil
|
||
}
|
||
} else if claudeResponse.Type == "content_block_delta" {
|
||
if claudeResponse.Delta != nil {
|
||
choice.Delta.Content = claudeResponse.Delta.Text
|
||
switch claudeResponse.Delta.Type {
|
||
case "input_json_delta":
|
||
tools = append(tools, dto.ToolCallResponse{
|
||
Type: "function",
|
||
Index: common.GetPointer(fcIdx),
|
||
Function: dto.FunctionResponse{
|
||
Arguments: *claudeResponse.Delta.PartialJson,
|
||
},
|
||
})
|
||
case "signature_delta":
|
||
// 加密的不处理
|
||
signatureContent := "\n"
|
||
choice.Delta.ReasoningContent = &signatureContent
|
||
case "thinking_delta":
|
||
thinkingContent := claudeResponse.Delta.Thinking
|
||
choice.Delta.ReasoningContent = &thinkingContent
|
||
}
|
||
}
|
||
} else if claudeResponse.Type == "message_delta" {
|
||
finishReason := stopReasonClaude2OpenAI(*claudeResponse.Delta.StopReason)
|
||
if finishReason != "null" {
|
||
choice.FinishReason = &finishReason
|
||
}
|
||
//claudeUsage = &claudeResponse.Usage
|
||
} else if claudeResponse.Type == "message_stop" {
|
||
return nil
|
||
} else {
|
||
return nil
|
||
}
|
||
}
|
||
if len(tools) > 0 {
|
||
choice.Delta.Content = nil // compatible with other OpenAI derivative applications, like LobeOpenAICompatibleFactory ...
|
||
choice.Delta.ToolCalls = tools
|
||
}
|
||
response.Choices = append(response.Choices, choice)
|
||
|
||
return &response
|
||
}
|
||
|
||
func ResponseClaude2OpenAI(reqMode int, claudeResponse *dto.ClaudeResponse) *dto.OpenAITextResponse {
|
||
choices := make([]dto.OpenAITextResponseChoice, 0)
|
||
fullTextResponse := dto.OpenAITextResponse{
|
||
Id: fmt.Sprintf("chatcmpl-%s", common.GetUUID()),
|
||
Object: "chat.completion",
|
||
Created: common.GetTimestamp(),
|
||
}
|
||
var responseText string
|
||
var responseThinking string
|
||
if len(claudeResponse.Content) > 0 {
|
||
responseText = claudeResponse.Content[0].GetText()
|
||
responseThinking = claudeResponse.Content[0].Thinking
|
||
}
|
||
tools := make([]dto.ToolCallResponse, 0)
|
||
thinkingContent := ""
|
||
|
||
if reqMode == RequestModeCompletion {
|
||
choice := dto.OpenAITextResponseChoice{
|
||
Index: 0,
|
||
Message: dto.Message{
|
||
Role: "assistant",
|
||
Content: strings.TrimPrefix(claudeResponse.Completion, " "),
|
||
Name: nil,
|
||
},
|
||
FinishReason: stopReasonClaude2OpenAI(claudeResponse.StopReason),
|
||
}
|
||
choices = append(choices, choice)
|
||
} else {
|
||
fullTextResponse.Id = claudeResponse.Id
|
||
for _, message := range claudeResponse.Content {
|
||
switch message.Type {
|
||
case "tool_use":
|
||
args, _ := json.Marshal(message.Input)
|
||
tools = append(tools, dto.ToolCallResponse{
|
||
ID: message.Id,
|
||
Type: "function", // compatible with other OpenAI derivative applications
|
||
Function: dto.FunctionResponse{
|
||
Name: message.Name,
|
||
Arguments: string(args),
|
||
},
|
||
})
|
||
case "thinking":
|
||
// 加密的不管, 只输出明文的推理过程
|
||
thinkingContent = message.Thinking
|
||
case "text":
|
||
responseText = message.GetText()
|
||
}
|
||
}
|
||
}
|
||
choice := dto.OpenAITextResponseChoice{
|
||
Index: 0,
|
||
Message: dto.Message{
|
||
Role: "assistant",
|
||
},
|
||
FinishReason: stopReasonClaude2OpenAI(claudeResponse.StopReason),
|
||
}
|
||
choice.SetStringContent(responseText)
|
||
if len(responseThinking) > 0 {
|
||
choice.ReasoningContent = responseThinking
|
||
}
|
||
if len(tools) > 0 {
|
||
choice.Message.SetToolCalls(tools)
|
||
}
|
||
choice.Message.ReasoningContent = thinkingContent
|
||
fullTextResponse.Model = claudeResponse.Model
|
||
choices = append(choices, choice)
|
||
fullTextResponse.Choices = choices
|
||
return &fullTextResponse
|
||
}
|
||
|
||
type ClaudeResponseInfo struct {
|
||
ResponseId string
|
||
Created int64
|
||
Model string
|
||
ResponseText strings.Builder
|
||
Usage *dto.Usage
|
||
Done bool
|
||
}
|
||
|
||
func FormatClaudeResponseInfo(requestMode int, claudeResponse *dto.ClaudeResponse, oaiResponse *dto.ChatCompletionsStreamResponse, claudeInfo *ClaudeResponseInfo) bool {
|
||
if requestMode == RequestModeCompletion {
|
||
claudeInfo.ResponseText.WriteString(claudeResponse.Completion)
|
||
} else {
|
||
if claudeResponse.Type == "message_start" {
|
||
claudeInfo.ResponseId = claudeResponse.Message.Id
|
||
claudeInfo.Model = claudeResponse.Message.Model
|
||
|
||
// message_start, 获取usage
|
||
claudeInfo.Usage.PromptTokens = claudeResponse.Message.Usage.InputTokens
|
||
claudeInfo.Usage.PromptTokensDetails.CachedTokens = claudeResponse.Message.Usage.CacheReadInputTokens
|
||
claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens = claudeResponse.Message.Usage.CacheCreationInputTokens
|
||
claudeInfo.Usage.CompletionTokens = claudeResponse.Message.Usage.OutputTokens
|
||
} else if claudeResponse.Type == "content_block_delta" {
|
||
if claudeResponse.Delta.Text != nil {
|
||
claudeInfo.ResponseText.WriteString(*claudeResponse.Delta.Text)
|
||
}
|
||
if claudeResponse.Delta.Thinking != "" {
|
||
claudeInfo.ResponseText.WriteString(claudeResponse.Delta.Thinking)
|
||
}
|
||
} else if claudeResponse.Type == "message_delta" {
|
||
// 最终的usage获取
|
||
if claudeResponse.Usage.InputTokens > 0 {
|
||
// 不叠加,只取最新的
|
||
claudeInfo.Usage.PromptTokens = claudeResponse.Usage.InputTokens
|
||
}
|
||
claudeInfo.Usage.CompletionTokens = claudeResponse.Usage.OutputTokens
|
||
claudeInfo.Usage.TotalTokens = claudeInfo.Usage.PromptTokens + claudeInfo.Usage.CompletionTokens
|
||
|
||
// 判断是否完整
|
||
claudeInfo.Done = true
|
||
} else if claudeResponse.Type == "content_block_start" {
|
||
} else {
|
||
return false
|
||
}
|
||
}
|
||
if oaiResponse != nil {
|
||
oaiResponse.Id = claudeInfo.ResponseId
|
||
oaiResponse.Created = claudeInfo.Created
|
||
oaiResponse.Model = claudeInfo.Model
|
||
}
|
||
return true
|
||
}
|
||
|
||
func HandleStreamResponseData(c *gin.Context, info *relaycommon.RelayInfo, claudeInfo *ClaudeResponseInfo, data string, requestMode int) *types.NewAPIError {
|
||
var claudeResponse dto.ClaudeResponse
|
||
err := common.UnmarshalJsonStr(data, &claudeResponse)
|
||
if err != nil {
|
||
common.SysLog("error unmarshalling stream response: " + err.Error())
|
||
return types.NewError(err, types.ErrorCodeBadResponseBody)
|
||
}
|
||
if claudeError := claudeResponse.GetClaudeError(); claudeError != nil && claudeError.Type != "" {
|
||
return types.WithClaudeError(*claudeError, http.StatusInternalServerError)
|
||
}
|
||
if info.RelayFormat == types.RelayFormatClaude {
|
||
FormatClaudeResponseInfo(requestMode, &claudeResponse, nil, claudeInfo)
|
||
|
||
if requestMode == RequestModeCompletion {
|
||
} else {
|
||
if claudeResponse.Type == "message_start" {
|
||
// message_start, 获取usage
|
||
info.UpstreamModelName = claudeResponse.Message.Model
|
||
} else if claudeResponse.Type == "content_block_delta" {
|
||
} else if claudeResponse.Type == "message_delta" {
|
||
}
|
||
}
|
||
helper.ClaudeChunkData(c, claudeResponse, data)
|
||
} else if info.RelayFormat == types.RelayFormatOpenAI {
|
||
response := StreamResponseClaude2OpenAI(requestMode, &claudeResponse)
|
||
|
||
if !FormatClaudeResponseInfo(requestMode, &claudeResponse, response, claudeInfo) {
|
||
return nil
|
||
}
|
||
|
||
err = helper.ObjectData(c, response)
|
||
if err != nil {
|
||
logger.LogError(c, "send_stream_response_failed: "+err.Error())
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func HandleStreamFinalResponse(c *gin.Context, info *relaycommon.RelayInfo, claudeInfo *ClaudeResponseInfo, requestMode int) {
|
||
|
||
if requestMode == RequestModeCompletion {
|
||
claudeInfo.Usage = service.ResponseText2Usage(claudeInfo.ResponseText.String(), info.UpstreamModelName, info.PromptTokens)
|
||
} else {
|
||
if claudeInfo.Usage.PromptTokens == 0 {
|
||
//上游出错
|
||
}
|
||
if claudeInfo.Usage.CompletionTokens == 0 || !claudeInfo.Done {
|
||
if common.DebugEnabled {
|
||
common.SysLog("claude response usage is not complete, maybe upstream error")
|
||
}
|
||
claudeInfo.Usage = service.ResponseText2Usage(claudeInfo.ResponseText.String(), info.UpstreamModelName, claudeInfo.Usage.PromptTokens)
|
||
}
|
||
}
|
||
|
||
if info.RelayFormat == types.RelayFormatClaude {
|
||
//
|
||
} else if info.RelayFormat == types.RelayFormatOpenAI {
|
||
if info.ShouldIncludeUsage {
|
||
response := helper.GenerateFinalUsageResponse(claudeInfo.ResponseId, claudeInfo.Created, info.UpstreamModelName, *claudeInfo.Usage)
|
||
err := helper.ObjectData(c, response)
|
||
if err != nil {
|
||
common.SysLog("send final response failed: " + err.Error())
|
||
}
|
||
}
|
||
helper.Done(c)
|
||
}
|
||
}
|
||
|
||
func ClaudeStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo, requestMode int) (*types.NewAPIError, *dto.Usage) {
|
||
claudeInfo := &ClaudeResponseInfo{
|
||
ResponseId: helper.GetResponseID(c),
|
||
Created: common.GetTimestamp(),
|
||
Model: info.UpstreamModelName,
|
||
ResponseText: strings.Builder{},
|
||
Usage: &dto.Usage{},
|
||
}
|
||
var err *types.NewAPIError
|
||
helper.StreamScannerHandler(c, resp, info, func(data string) bool {
|
||
err = HandleStreamResponseData(c, info, claudeInfo, data, requestMode)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
return true
|
||
})
|
||
if err != nil {
|
||
return err, nil
|
||
}
|
||
|
||
HandleStreamFinalResponse(c, info, claudeInfo, requestMode)
|
||
return nil, claudeInfo.Usage
|
||
}
|
||
|
||
func HandleClaudeResponseData(c *gin.Context, info *relaycommon.RelayInfo, claudeInfo *ClaudeResponseInfo, data []byte, requestMode int) *types.NewAPIError {
|
||
var claudeResponse dto.ClaudeResponse
|
||
err := common.Unmarshal(data, &claudeResponse)
|
||
if err != nil {
|
||
return types.NewError(err, types.ErrorCodeBadResponseBody)
|
||
}
|
||
if claudeError := claudeResponse.GetClaudeError(); claudeError != nil && claudeError.Type != "" {
|
||
return types.WithClaudeError(*claudeError, http.StatusInternalServerError)
|
||
}
|
||
if requestMode == RequestModeCompletion {
|
||
completionTokens := service.CountTextToken(claudeResponse.Completion, info.OriginModelName)
|
||
claudeInfo.Usage.PromptTokens = info.PromptTokens
|
||
claudeInfo.Usage.CompletionTokens = completionTokens
|
||
claudeInfo.Usage.TotalTokens = info.PromptTokens + completionTokens
|
||
} else {
|
||
claudeInfo.Usage.PromptTokens = claudeResponse.Usage.InputTokens
|
||
claudeInfo.Usage.CompletionTokens = claudeResponse.Usage.OutputTokens
|
||
claudeInfo.Usage.TotalTokens = claudeResponse.Usage.InputTokens + claudeResponse.Usage.OutputTokens
|
||
claudeInfo.Usage.PromptTokensDetails.CachedTokens = claudeResponse.Usage.CacheReadInputTokens
|
||
claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens = claudeResponse.Usage.CacheCreationInputTokens
|
||
}
|
||
var responseData []byte
|
||
switch info.RelayFormat {
|
||
case types.RelayFormatOpenAI:
|
||
openaiResponse := ResponseClaude2OpenAI(requestMode, &claudeResponse)
|
||
openaiResponse.Usage = *claudeInfo.Usage
|
||
responseData, err = json.Marshal(openaiResponse)
|
||
if err != nil {
|
||
return types.NewError(err, types.ErrorCodeBadResponseBody)
|
||
}
|
||
case types.RelayFormatClaude:
|
||
responseData = data
|
||
}
|
||
|
||
if claudeResponse.Usage.ServerToolUse != nil && claudeResponse.Usage.ServerToolUse.WebSearchRequests > 0 {
|
||
c.Set("claude_web_search_requests", claudeResponse.Usage.ServerToolUse.WebSearchRequests)
|
||
}
|
||
|
||
service.IOCopyBytesGracefully(c, nil, responseData)
|
||
return nil
|
||
}
|
||
|
||
func ClaudeHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo, requestMode int) (*types.NewAPIError, *dto.Usage) {
|
||
defer service.CloseResponseBodyGracefully(resp)
|
||
|
||
claudeInfo := &ClaudeResponseInfo{
|
||
ResponseId: helper.GetResponseID(c),
|
||
Created: common.GetTimestamp(),
|
||
Model: info.UpstreamModelName,
|
||
ResponseText: strings.Builder{},
|
||
Usage: &dto.Usage{},
|
||
}
|
||
responseBody, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return types.NewError(err, types.ErrorCodeBadResponseBody), nil
|
||
}
|
||
if common.DebugEnabled {
|
||
println("responseBody: ", string(responseBody))
|
||
}
|
||
handleErr := HandleClaudeResponseData(c, info, claudeInfo, responseBody, requestMode)
|
||
if handleErr != nil {
|
||
return handleErr, nil
|
||
}
|
||
return nil, claudeInfo.Usage
|
||
}
|
||
|
||
func mapToolChoice(toolChoice any, parallelToolCalls *bool) *dto.ClaudeToolChoice {
|
||
var claudeToolChoice *dto.ClaudeToolChoice
|
||
|
||
// 处理 tool_choice 字符串值
|
||
if toolChoiceStr, ok := toolChoice.(string); ok {
|
||
switch toolChoiceStr {
|
||
case "auto":
|
||
claudeToolChoice = &dto.ClaudeToolChoice{
|
||
Type: "auto",
|
||
}
|
||
case "required":
|
||
claudeToolChoice = &dto.ClaudeToolChoice{
|
||
Type: "any",
|
||
}
|
||
case "none":
|
||
claudeToolChoice = &dto.ClaudeToolChoice{
|
||
Type: "none",
|
||
}
|
||
}
|
||
} else if toolChoiceMap, ok := toolChoice.(map[string]interface{}); ok {
|
||
// 处理 tool_choice 对象值
|
||
if function, ok := toolChoiceMap["function"].(map[string]interface{}); ok {
|
||
if toolName, ok := function["name"].(string); ok {
|
||
claudeToolChoice = &dto.ClaudeToolChoice{
|
||
Type: "tool",
|
||
Name: toolName,
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理 parallel_tool_calls
|
||
if parallelToolCalls != nil {
|
||
if claudeToolChoice == nil {
|
||
// 如果没有 tool_choice,但有 parallel_tool_calls,创建默认的 auto 类型
|
||
claudeToolChoice = &dto.ClaudeToolChoice{
|
||
Type: "auto",
|
||
}
|
||
}
|
||
|
||
// 设置 disable_parallel_tool_use
|
||
// 如果 parallel_tool_calls 为 true,则 disable_parallel_tool_use 为 false
|
||
claudeToolChoice.DisableParallelToolUse = !*parallelToolCalls
|
||
}
|
||
|
||
return claudeToolChoice
|
||
}
|