From e1fcfae9284daaa913f2745feaae756bf3f3b8d4 Mon Sep 17 00:00:00 2001 From: moondie Date: Thu, 7 Mar 2024 01:21:07 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20Support=20Claude3=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Claude改用messages API,支持Claude3 * 删除新API不支持的模型 * 忘了改请求地址 * 🐛 fix: fix the problem of return format and completion token not being obtained --------- Co-authored-by: MartialBE --- common/model-ratio.go | 11 +-- modelRatio.json | 5 +- providers/claude/base.go | 16 ++-- providers/claude/chat.go | 109 ++++++++++++++++----------- providers/claude/type.go | 61 +++++++++++---- web/src/views/Channel/type/Config.js | 4 +- 6 files changed, 131 insertions(+), 75 deletions(-) diff --git a/common/model-ratio.go b/common/model-ratio.go index 03c87a63..2dd0b11a 100644 --- a/common/model-ratio.go +++ b/common/model-ratio.go @@ -87,11 +87,12 @@ func init() { "dall-e-3": {[]float64{20, 20}, ChannelTypeOpenAI}, // $0.80/million tokens $2.40/million tokens - "claude-instant-1": {[]float64{0.4, 1.2}, ChannelTypeAnthropic}, + "claude-instant-1.2": {[]float64{0.4, 1.2}, ChannelTypeAnthropic}, // $8.00/million tokens $24.00/million tokens - "claude-2": {[]float64{4, 12}, ChannelTypeAnthropic}, - "claude-2.0": {[]float64{4, 12}, ChannelTypeAnthropic}, - "claude-2.1": {[]float64{4, 12}, ChannelTypeAnthropic}, + "claude-2.0": {[]float64{4, 12}, ChannelTypeAnthropic}, + "claude-2.1": {[]float64{4, 12}, ChannelTypeAnthropic}, + "claude-3-opus-20240229": {[]float64{7.5, 22.5}, ChannelTypeAnthropic}, + "claude-3-sonnet-20240229": {[]float64{1.3, 3.9}, ChannelTypeAnthropic}, // ¥0.012 / 1k tokens ¥0.012 / 1k tokens "ERNIE-Bot": {[]float64{0.8572, 0.8572}, ChannelTypeBaidu}, @@ -291,7 +292,7 @@ func GetCompletionRatio(name string) float64 { } return 2 } - if strings.HasPrefix(name, "claude-instant-1") { + if strings.HasPrefix(name, "claude-instant-1.2") { return 3.38 } if strings.HasPrefix(name, "claude-2") { diff --git a/modelRatio.json b/modelRatio.json index b509e58b..1ae7da87 100644 --- a/modelRatio.json +++ b/modelRatio.json @@ -43,10 +43,11 @@ "text-moderation-latest": [0.1, 0.1], "dall-e-2": [8, 8], "dall-e-3": [20, 20], - "claude-instant-1": [0.4, 1.2], - "claude-2": [4, 12], + "claude-instant-1.2": [0.4, 1.2], "claude-2.0": [4, 12], "claude-2.1": [4, 12], + "claude-3-opus-20240229": [7.5, 22.5], + "claude-3-sonnet-20240229": [1.3, 3.9], "ERNIE-Bot": [0.8572, 0.8572], "ERNIE-Bot-8k": [1.7143, 3.4286], "ERNIE-Bot-turbo": [0.5715, 0.5715], diff --git a/providers/claude/base.go b/providers/claude/base.go index 38524a96..58e1ba8c 100644 --- a/providers/claude/base.go +++ b/providers/claude/base.go @@ -29,13 +29,13 @@ type ClaudeProvider struct { func getConfig() base.ProviderConfig { return base.ProviderConfig{ BaseURL: "https://api.anthropic.com", - ChatCompletions: "/v1/complete", + ChatCompletions: "/v1/messages", } } // 请求错误处理 func requestErrorHandle(resp *http.Response) *types.OpenAIError { - claudeError := &ClaudeResponseError{} + claudeError := &ClaudeError{} err := json.NewDecoder(resp.Body).Decode(claudeError) if err != nil { return nil @@ -45,14 +45,14 @@ func requestErrorHandle(resp *http.Response) *types.OpenAIError { } // 错误处理 -func errorHandle(claudeError *ClaudeResponseError) *types.OpenAIError { - if claudeError.Error.Type == "" { +func errorHandle(claudeError *ClaudeError) *types.OpenAIError { + if claudeError.Type == "" { return nil } return &types.OpenAIError{ - Message: claudeError.Error.Message, - Type: claudeError.Error.Type, - Code: claudeError.Error.Type, + Message: claudeError.Message, + Type: claudeError.Type, + Code: claudeError.Type, } } @@ -73,7 +73,7 @@ func (p *ClaudeProvider) GetRequestHeaders() (headers map[string]string) { func stopReasonClaude2OpenAI(reason string) string { switch reason { - case "stop_sequence": + case "end_turn": return types.FinishReasonStop case "max_tokens": return types.FinishReasonLength diff --git a/providers/claude/chat.go b/providers/claude/chat.go index 752ea264..ace6dafc 100644 --- a/providers/claude/chat.go +++ b/providers/claude/chat.go @@ -83,36 +83,36 @@ func (p *ClaudeProvider) getChatRequest(request *types.ChatCompletionRequest) (* func convertFromChatOpenai(request *types.ChatCompletionRequest) *ClaudeRequest { claudeRequest := ClaudeRequest{ - Model: request.Model, - Prompt: "", - MaxTokensToSample: request.MaxTokens, - StopSequences: nil, - Temperature: request.Temperature, - TopP: request.TopP, - Stream: request.Stream, + Model: request.Model, + Messages: nil, + System: "", + MaxTokens: request.MaxTokens, + StopSequences: nil, + Temperature: request.Temperature, + TopP: request.TopP, + Stream: request.Stream, } - if claudeRequest.MaxTokensToSample == 0 { - claudeRequest.MaxTokensToSample = 1000000 + if claudeRequest.MaxTokens == 0 { + claudeRequest.MaxTokens = 4096 } - prompt := "" + var messages []Message for _, message := range request.Messages { - if message.Role == "user" { - prompt += fmt.Sprintf("\n\nHuman: %s", message.Content) - } else if message.Role == "assistant" { - prompt += fmt.Sprintf("\n\nAssistant: %s", message.Content) - } else if message.Role == "system" { - if prompt == "" { - prompt = message.StringContent() - } + if message.Role != "system" { + messages = append(messages, Message{ + Role: message.Role, + Content: message.Content.(string), + }) + claudeRequest.Messages = messages + } else { + claudeRequest.System = message.Content.(string) } } - prompt += "\n\nAssistant:" - claudeRequest.Prompt = prompt + return &claudeRequest } func (p *ClaudeProvider) convertToChatOpenai(response *ClaudeResponse, request *types.ChatCompletionRequest) (openaiResponse *types.ChatCompletionResponse, errWithCode *types.OpenAIErrorWithStatusCode) { - error := errorHandle(&response.ClaudeResponseError) + error := errorHandle(&response.Error) if error != nil { errWithCode = &types.OpenAIErrorWithStatusCode{ OpenAIError: *error, @@ -125,26 +125,33 @@ func (p *ClaudeProvider) convertToChatOpenai(response *ClaudeResponse, request * Index: 0, Message: types.ChatCompletionMessage{ Role: "assistant", - Content: strings.TrimPrefix(response.Completion, " "), + Content: strings.TrimPrefix(response.Content[0].Text, " "), Name: nil, }, FinishReason: stopReasonClaude2OpenAI(response.StopReason), } openaiResponse = &types.ChatCompletionResponse{ - ID: fmt.Sprintf("chatcmpl-%s", common.GetUUID()), + ID: response.Id, Object: "chat.completion", Created: common.GetTimestamp(), Choices: []types.ChatCompletionChoice{choice}, Model: response.Model, + Usage: &types.Usage{ + CompletionTokens: 0, + PromptTokens: 0, + TotalTokens: 0, + }, } - completionTokens := common.CountTokenText(response.Completion, response.Model) - response.Usage.CompletionTokens = completionTokens - response.Usage.TotalTokens = response.Usage.PromptTokens + completionTokens + completionTokens := response.Usage.OutputTokens - openaiResponse.Usage = response.Usage + promptTokens := response.Usage.InputTokens - *p.Usage = *response.Usage + openaiResponse.Usage.PromptTokens = promptTokens + openaiResponse.Usage.CompletionTokens = completionTokens + openaiResponse.Usage.TotalTokens = promptTokens + completionTokens + + *p.Usage = *openaiResponse.Usage return openaiResponse, nil } @@ -152,7 +159,7 @@ func (p *ClaudeProvider) convertToChatOpenai(response *ClaudeResponse, request * // 转换为OpenAI聊天流式请求体 func (h *claudeStreamHandler) handlerStream(rawLine *[]byte, dataChan chan string, errChan chan error) { // 如果rawLine 前缀不为data:,则直接返回 - if !strings.HasPrefix(string(*rawLine), `data: {"type": "completion"`) { + if !strings.HasPrefix(string(*rawLine), `data: {"type"`) { *rawLine = nil return } @@ -160,43 +167,61 @@ func (h *claudeStreamHandler) handlerStream(rawLine *[]byte, dataChan chan strin // 去除前缀 *rawLine = (*rawLine)[6:] - var claudeResponse *ClaudeResponse - err := json.Unmarshal(*rawLine, claudeResponse) + var claudeResponse ClaudeStreamResponse + err := json.Unmarshal(*rawLine, &claudeResponse) if err != nil { errChan <- common.ErrorToOpenAIError(err) return } - error := errorHandle(&claudeResponse.ClaudeResponseError) + error := errorHandle(&claudeResponse.Error) if error != nil { errChan <- error return } - if claudeResponse.StopReason == "stop_sequence" { + switch claudeResponse.Type { + case "message_start": + h.Usage.PromptTokens = claudeResponse.Message.InputTokens + + case "message_delta": + h.convertToOpenaiStream(&claudeResponse, dataChan, errChan) + h.Usage.CompletionTokens = claudeResponse.Usage.OutputTokens + h.Usage.TotalTokens = h.Usage.PromptTokens + h.Usage.CompletionTokens + + case "content_block_delta": + h.convertToOpenaiStream(&claudeResponse, dataChan, errChan) + + case "message_stop": errChan <- io.EOF *rawLine = requester.StreamClosed + + default: return } - - h.convertToOpenaiStream(claudeResponse, dataChan, errChan) } -func (h *claudeStreamHandler) convertToOpenaiStream(claudeResponse *ClaudeResponse, dataChan chan string, errChan chan error) { - var choice types.ChatCompletionStreamChoice - choice.Delta.Content = claudeResponse.Completion - finishReason := stopReasonClaude2OpenAI(claudeResponse.StopReason) - if finishReason != "null" { +func (h *claudeStreamHandler) convertToOpenaiStream(claudeResponse *ClaudeStreamResponse, dataChan chan string, errChan chan error) { + choice := types.ChatCompletionStreamChoice{ + Index: claudeResponse.Index, + } + + if claudeResponse.Delta.Text != "" { + choice.Delta.Content = claudeResponse.Delta.Text + } + + finishReason := stopReasonClaude2OpenAI(claudeResponse.Delta.StopReason) + if finishReason != "" { choice.FinishReason = &finishReason } chatCompletion := types.ChatCompletionStreamResponse{ + ID: fmt.Sprintf("chatcmpl-%s", common.GetUUID()), Object: "chat.completion.chunk", + Created: common.GetTimestamp(), Model: h.Request.Model, Choices: []types.ChatCompletionStreamChoice{choice}, } responseBody, _ := json.Marshal(chatCompletion) dataChan <- string(responseBody) - - h.Usage.PromptTokens += common.CountTokenText(claudeResponse.Completion, h.Request.Model) } diff --git a/providers/claude/type.go b/providers/claude/type.go index 4cc0eefd..8676c178 100644 --- a/providers/claude/type.go +++ b/providers/claude/type.go @@ -1,7 +1,5 @@ package claude -import "one-api/types" - type ClaudeError struct { Type string `json:"type"` Message string `json:"message"` @@ -11,25 +9,56 @@ type ClaudeMetadata struct { UserId string `json:"user_id"` } +type ResContent struct { + Text string `json:"text"` + Type string `json:"type"` +} + +type Message struct { + Role string `json:"role"` + Content string `json:"content"` +} + type ClaudeRequest struct { - Model string `json:"model"` - Prompt string `json:"prompt"` - MaxTokensToSample int `json:"max_tokens_to_sample"` - StopSequences []string `json:"stop_sequences,omitempty"` - Temperature float64 `json:"temperature,omitempty"` - TopP float64 `json:"top_p,omitempty"` - TopK int `json:"top_k,omitempty"` + Model string `json:"model"` + System string `json:"system,omitempty"` + Messages []Message `json:"messages"` + MaxTokens int `json:"max_tokens"` + StopSequences []string `json:"stop_sequences,omitempty"` + Temperature float64 `json:"temperature,omitempty"` + TopP float64 `json:"top_p,omitempty"` + TopK int `json:"top_k,omitempty"` //ClaudeMetadata `json:"metadata,omitempty"` Stream bool `json:"stream,omitempty"` } -type ClaudeResponseError struct { - Error ClaudeError `json:"error,omitempty"` +type Usage struct { + InputTokens int `json:"input_tokens"` + OutputTokens int `json:"output_tokens,omitempty"` } type ClaudeResponse struct { - Completion string `json:"completion"` - StopReason string `json:"stop_reason"` - Model string `json:"model"` - Usage *types.Usage `json:"usage,omitempty"` - ClaudeResponseError + Content []ResContent `json:"content"` + Id string `json:"id"` + Role string `json:"role"` + StopReason string `json:"stop_reason"` + StopSequence string `json:"stop_sequence,omitempty"` + Model string `json:"model"` + Usage `json:"usage,omitempty"` + Error ClaudeError `json:"error,omitempty"` +} + +type Delta struct { + Type string `json:"type,omitempty"` + Text string `json:"text,omitempty"` + StopReason string `json:"stop_reason,omitempty"` + StopSequence string `json:"stop_sequence,omitempty"` +} + +type ClaudeStreamResponse struct { + Type string `json:"type"` + Message ClaudeResponse `json:"message,omitempty"` + Index int `json:"index,omitempty"` + Delta Delta `json:"delta,omitempty"` + Usage Usage `json:"usage,omitempty"` + Error ClaudeError `json:"error,omitempty"` } diff --git a/web/src/views/Channel/type/Config.js b/web/src/views/Channel/type/Config.js index a54df7f1..6a41d50e 100644 --- a/web/src/views/Channel/type/Config.js +++ b/web/src/views/Channel/type/Config.js @@ -59,8 +59,8 @@ const typeConfig = { }, 14: { input: { - models: ['claude-instant-1', 'claude-2', 'claude-2.0', 'claude-2.1'], - test_model: 'claude-2' + models: ['claude-instant-1.2', 'claude-2.0', 'claude-2.1','claude-3-opus-20240229','claude-3-sonnet-20240229'], + test_model: 'claude-3-sonnet-20240229' }, modelGroup: 'Anthropic' },