Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 1m40s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 42m16s
Publish Docker image (Multi-arch) / Build & push (arm64) (push) Canceled after 0s
Release (Linux, macOS, Windows) / macOS Release (push) Canceled after 0s
Release (Linux, macOS, Windows) / Windows Release (push) Canceled after 0s
Publish Docker image (Multi-arch) / Create multi-arch manifests (push) Canceled after 0s
Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
425 lines
15 KiB
Go
425 lines
15 KiB
Go
package claude
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/constant"
|
|
"github.com/QuantumNous/new-api/logger"
|
|
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
|
"github.com/QuantumNous/new-api/relay/helper"
|
|
"github.com/QuantumNous/new-api/relaykit/dto"
|
|
"github.com/QuantumNous/new-api/relaykit/relayconvert"
|
|
"github.com/QuantumNous/new-api/relaykit/types"
|
|
"github.com/QuantumNous/new-api/service"
|
|
"github.com/QuantumNous/new-api/setting/model_setting"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func stopReasonClaude2OpenAI(reason string) string {
|
|
return relayconvert.StopReasonClaudeToOpenAI(reason)
|
|
}
|
|
|
|
func maybeMarkClaudeRefusal(c *gin.Context, info *relaycommon.RelayInfo, stopReason string) {
|
|
if c == nil {
|
|
return
|
|
}
|
|
if strings.EqualFold(stopReason, "refusal") {
|
|
info.PerformanceBusinessRejection = true
|
|
common.SetContextKey(c, constant.ContextKeyAdminRejectReason, "claude_stop_reason=refusal")
|
|
}
|
|
}
|
|
|
|
func StreamResponseClaude2OpenAI(claudeResponse *dto.ClaudeResponse) *dto.ChatCompletionsStreamResponse {
|
|
return relayconvert.StreamResponseClaude2OpenAI(claudeResponse)
|
|
}
|
|
|
|
func ResponseClaude2OpenAI(claudeResponse *dto.ClaudeResponse) *dto.OpenAITextResponse {
|
|
return relayconvert.ResponseClaude2OpenAI(claudeResponse)
|
|
}
|
|
|
|
type ClaudeResponseInfo = relayconvert.ClaudeResponseInfo
|
|
|
|
func cacheCreationTokensForOpenAIUsage(usage *dto.Usage) int {
|
|
if usage == nil {
|
|
return 0
|
|
}
|
|
openAIUsage := relayconvert.UsageFromClaudeUsage(usage)
|
|
if openAIUsage == nil {
|
|
return 0
|
|
}
|
|
return openAIUsage.PromptTokens - usage.PromptTokens - usage.PromptTokensDetails.CachedTokens
|
|
}
|
|
|
|
func buildOpenAIStyleUsageFromClaudeUsage(usage *dto.Usage) dto.Usage {
|
|
mapped := relayconvert.UsageFromClaudeUsage(usage)
|
|
if mapped == nil {
|
|
return dto.Usage{}
|
|
}
|
|
return *mapped
|
|
}
|
|
|
|
func buildMessageDeltaPatchUsage(claudeResponse *dto.ClaudeResponse, claudeInfo *ClaudeResponseInfo) *dto.ClaudeUsage {
|
|
return relayconvert.BuildMessageDeltaPatchUsage(claudeResponse, claudeInfo)
|
|
}
|
|
|
|
func shouldSkipClaudeMessageDeltaUsagePatch(info *relaycommon.RelayInfo) bool {
|
|
if model_setting.GetGlobalSettings().PassThroughRequestEnabled {
|
|
return true
|
|
}
|
|
if info == nil {
|
|
return false
|
|
}
|
|
return info.ChannelSetting.PassThroughBodyEnabled
|
|
}
|
|
|
|
func patchClaudeMessageDeltaUsageData(data string, usage *dto.ClaudeUsage) string {
|
|
return relayconvert.PatchClaudeMessageDeltaUsageData(data, usage)
|
|
}
|
|
|
|
func FormatClaudeResponseInfo(claudeResponse *dto.ClaudeResponse, oaiResponse *dto.ChatCompletionsStreamResponse, claudeInfo *ClaudeResponseInfo) bool {
|
|
return relayconvert.FormatClaudeResponseInfo(claudeResponse, oaiResponse, claudeInfo)
|
|
}
|
|
|
|
func HandleStreamResponseData(c *gin.Context, info *relaycommon.RelayInfo, claudeInfo *ClaudeResponseInfo, data string) *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 claudeResponse.Type == "message_start" && claudeResponse.Message != nil {
|
|
info.ObserveResponseModel(claudeResponse.Message.Model)
|
|
}
|
|
if claudeResponse.StopReason != "" {
|
|
maybeMarkClaudeRefusal(c, info, claudeResponse.StopReason)
|
|
}
|
|
if claudeResponse.Delta != nil && claudeResponse.Delta.StopReason != nil {
|
|
maybeMarkClaudeRefusal(c, info, *claudeResponse.Delta.StopReason)
|
|
}
|
|
if claudeResponse.Type == "message_stop" {
|
|
info.StreamStatus.MarkCompleted()
|
|
}
|
|
if info.RelayFormat == types.RelayFormatClaude {
|
|
FormatClaudeResponseInfo(&claudeResponse, nil, claudeInfo)
|
|
|
|
if claudeResponse.Type == "message_start" {
|
|
// message_start, 获取usage
|
|
if claudeResponse.Message != nil {
|
|
info.UpstreamModelName = claudeResponse.Message.Model
|
|
}
|
|
} else if claudeResponse.Type == "message_delta" {
|
|
// 确保 message_delta 的 usage 包含完整的 input_tokens 和 cache 相关字段
|
|
// 解决 AWS Bedrock 等上游返回的 message_delta 缺少这些字段的问题
|
|
if !shouldSkipClaudeMessageDeltaUsagePatch(info) {
|
|
data = patchClaudeMessageDeltaUsageData(data, buildMessageDeltaPatchUsage(&claudeResponse, claudeInfo))
|
|
}
|
|
}
|
|
countClaudeStreamBillableTools(c, info, &claudeResponse)
|
|
helper.ClaudeChunkData(c, claudeResponse, data)
|
|
} else if info.RelayFormat == types.RelayFormatOpenAI {
|
|
state, err := claudeToChatStreamState(info)
|
|
if err != nil {
|
|
return types.NewError(err, types.ErrorCodeBadResponseBody)
|
|
}
|
|
response, err := state.ConvertChunk(&claudeResponse)
|
|
if err != nil {
|
|
return types.NewError(err, types.ErrorCodeBadResponseBody)
|
|
}
|
|
|
|
if !FormatClaudeResponseInfo(&claudeResponse, response, claudeInfo) {
|
|
return nil
|
|
}
|
|
|
|
countClaudeStreamBillableTools(c, info, &claudeResponse)
|
|
|
|
if response == nil {
|
|
return nil
|
|
}
|
|
err = helper.ObjectData(c, response)
|
|
if err != nil {
|
|
logger.LogError(c, "send_stream_response_failed: "+err.Error())
|
|
}
|
|
} else if info.RelayFormat == types.RelayFormatGemini {
|
|
state, err := claudeToGeminiStreamState(info)
|
|
if err != nil {
|
|
return types.NewError(err, types.ErrorCodeBadResponseBody)
|
|
}
|
|
results, err := service.ConvertStreamResponseChunk(c, info, state, &claudeResponse)
|
|
if err != nil {
|
|
return types.NewError(err, types.ErrorCodeBadResponseBody)
|
|
}
|
|
if !FormatClaudeResponseInfo(&claudeResponse, nil, claudeInfo) {
|
|
return nil
|
|
}
|
|
countClaudeStreamBillableTools(c, info, &claudeResponse)
|
|
if sendErr := sendGeminiStreamResults(c, results); sendErr != nil {
|
|
return sendErr
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func claudeToChatStreamState(info *relaycommon.RelayInfo) (*relayconvert.ClaudeToChatStreamState, error) {
|
|
if info != nil && info.ClaudeToChatStreamState != nil {
|
|
state, ok := info.ClaudeToChatStreamState.(*relayconvert.ClaudeToChatStreamState)
|
|
if !ok || state == nil {
|
|
return nil, fmt.Errorf("invalid Claude-to-Chat stream state %T", info.ClaudeToChatStreamState)
|
|
}
|
|
return state, nil
|
|
}
|
|
|
|
state := relayconvert.NewClaudeToChatStreamState()
|
|
if info != nil {
|
|
info.ClaudeToChatStreamState = state
|
|
}
|
|
return state, nil
|
|
}
|
|
|
|
func claudeToGeminiStreamState(info *relaycommon.RelayInfo) (*relayconvert.ResponseStreamState, error) {
|
|
if info != nil && info.ChatToGeminiStreamState != nil {
|
|
state, ok := info.ChatToGeminiStreamState.(*relayconvert.ResponseStreamState)
|
|
if !ok || state == nil {
|
|
return nil, fmt.Errorf("invalid Claude-to-Gemini stream state %T", info.ChatToGeminiStreamState)
|
|
}
|
|
return state, nil
|
|
}
|
|
|
|
state, err := relayconvert.NewResponseStreamState(types.RelayFormatClaude, types.RelayFormatGemini, relayconvert.ResponseStreamOptions{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if info != nil {
|
|
info.ChatToGeminiStreamState = state
|
|
}
|
|
return state, nil
|
|
}
|
|
|
|
func sendGeminiStreamResults(c *gin.Context, results []relayconvert.ResponseResult) *types.NewAPIError {
|
|
for _, result := range results {
|
|
geminiResponse, ok := result.Value.(*dto.GeminiChatResponse)
|
|
if !ok {
|
|
return types.NewError(fmt.Errorf("expected Gemini stream response, got %T", result.Value), types.ErrorCodeBadResponseBody)
|
|
}
|
|
if geminiResponse == nil {
|
|
continue
|
|
}
|
|
data, err := common.Marshal(geminiResponse)
|
|
if err != nil {
|
|
return types.NewError(err, types.ErrorCodeBadResponseBody)
|
|
}
|
|
c.Render(-1, common.CustomEvent{Data: "data: " + string(data)})
|
|
_ = helper.FlushWriter(c)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func countClaudeStreamBillableTools(c *gin.Context, info *relaycommon.RelayInfo, claudeResponse *dto.ClaudeResponse) {
|
|
if claudeResponse == nil {
|
|
return
|
|
}
|
|
if claudeResponse.Type == "content_block_start" &&
|
|
claudeResponse.ContentBlock != nil &&
|
|
claudeResponse.ContentBlock.Type == "tool_use" {
|
|
info.CountBillableToolCall(dto.BuildInCallToolUse, claudeResponse.ContentBlock.Name)
|
|
}
|
|
if claudeResponse.Type == "message_delta" &&
|
|
claudeResponse.Usage != nil &&
|
|
claudeResponse.Usage.ServerToolUse != nil &&
|
|
claudeResponse.Usage.ServerToolUse.WebSearchRequests > 0 {
|
|
c.Set("claude_web_search_requests", claudeResponse.Usage.ServerToolUse.WebSearchRequests)
|
|
}
|
|
}
|
|
|
|
func HandleStreamFinalResponse(c *gin.Context, info *relaycommon.RelayInfo, claudeInfo *ClaudeResponseInfo) {
|
|
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")
|
|
}
|
|
// 只补缺失字段,不整份覆盖——保留 message_start 已拿到的 cache 字段
|
|
fallback := service.ResponseText2Usage(c, claudeInfo.ResponseText.String(), info.UpstreamModelName, info.GetEstimatePromptTokens())
|
|
if claudeInfo.Usage.CompletionTokens == 0 ||
|
|
(!claudeInfo.Done && fallback.CompletionTokens > claudeInfo.Usage.CompletionTokens) {
|
|
claudeInfo.Usage.CompletionTokens = fallback.CompletionTokens
|
|
}
|
|
if claudeInfo.Usage.PromptTokens == 0 {
|
|
claudeInfo.Usage.PromptTokens = fallback.PromptTokens
|
|
}
|
|
claudeInfo.Usage.TotalTokens = claudeInfo.Usage.PromptTokens + claudeInfo.Usage.CompletionTokens
|
|
}
|
|
if claudeInfo.Usage != nil {
|
|
claudeInfo.Usage.UsageSemantic = "anthropic"
|
|
}
|
|
relayconvert.FinalizeClaudeStreamBillingUsage(claudeInfo)
|
|
|
|
if info.RelayFormat == types.RelayFormatClaude {
|
|
//
|
|
} else if info.RelayFormat == types.RelayFormatOpenAI {
|
|
if info.ShouldIncludeUsage {
|
|
openAIUsage := buildOpenAIStyleUsageFromClaudeUsage(claudeInfo.Usage)
|
|
response := helper.GenerateFinalUsageResponse(claudeInfo.ResponseId, claudeInfo.Created, info.UpstreamModelName, openAIUsage)
|
|
err := helper.ObjectData(c, response)
|
|
if err != nil {
|
|
common.SysLog("send final response failed: " + err.Error())
|
|
}
|
|
}
|
|
helper.Done(c)
|
|
} else if info.RelayFormat == types.RelayFormatGemini {
|
|
state, err := claudeToGeminiStreamState(info)
|
|
if err != nil {
|
|
common.SysLog("error creating Gemini stream state: " + err.Error())
|
|
return
|
|
}
|
|
results, err := service.FinalizeStreamResponse(c, info, state)
|
|
if err != nil {
|
|
common.SysLog("error finalizing Gemini stream response: " + err.Error())
|
|
return
|
|
}
|
|
if sendErr := sendGeminiStreamResults(c, results); sendErr != nil {
|
|
common.SysLog("send final Gemini stream response failed: " + sendErr.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
func ClaudeStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.Usage, *types.NewAPIError) {
|
|
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, sr *helper.StreamResult) {
|
|
err = HandleStreamResponseData(c, info, claudeInfo, data)
|
|
if err != nil {
|
|
sr.Stop(err)
|
|
}
|
|
})
|
|
info.StreamStatus.RequireTerminal()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
HandleStreamFinalResponse(c, info, claudeInfo)
|
|
return claudeInfo.Usage, nil
|
|
}
|
|
|
|
func HandleClaudeResponseData(c *gin.Context, info *relaycommon.RelayInfo, claudeInfo *ClaudeResponseInfo, httpResp *http.Response, data []byte) *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)
|
|
}
|
|
info.ObserveResponseModel(claudeResponse.Model)
|
|
maybeMarkClaudeRefusal(c, info, claudeResponse.StopReason)
|
|
if claudeInfo.Usage == nil {
|
|
claudeInfo.Usage = &dto.Usage{}
|
|
}
|
|
if claudeResponse.Usage != nil {
|
|
claudeInfo.Usage.PromptTokens = claudeResponse.Usage.InputTokens
|
|
claudeInfo.Usage.CompletionTokens = claudeResponse.Usage.OutputTokens
|
|
claudeInfo.Usage.TotalTokens = claudeResponse.Usage.InputTokens + claudeResponse.Usage.OutputTokens
|
|
claudeInfo.Usage.UsageSemantic = "anthropic"
|
|
claudeInfo.Usage.BillingUsage = dto.CloneBillingUsage(claudeResponse.Usage.BillingUsage)
|
|
if claudeInfo.Usage.BillingUsage == nil {
|
|
claudeInfo.Usage.BillingUsage = dto.NewClaudeMessagesBillingUsage(claudeResponse.Usage)
|
|
}
|
|
claudeInfo.Usage.PromptTokensDetails.CachedTokens = claudeResponse.Usage.CacheReadInputTokens
|
|
claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens = claudeResponse.Usage.CacheCreationInputTokens
|
|
claudeInfo.Usage.ClaudeCacheCreation5mTokens = claudeResponse.Usage.GetCacheCreation5mTokens()
|
|
claudeInfo.Usage.ClaudeCacheCreation1hTokens = claudeResponse.Usage.GetCacheCreation1hTokens()
|
|
}
|
|
var responseData []byte
|
|
switch info.RelayFormat {
|
|
case types.RelayFormatOpenAI:
|
|
openaiResponse := ResponseClaude2OpenAI(&claudeResponse)
|
|
openaiResponse.Usage = buildOpenAIStyleUsageFromClaudeUsage(claudeInfo.Usage)
|
|
responseData, err = common.Marshal(openaiResponse)
|
|
if err != nil {
|
|
return types.NewError(err, types.ErrorCodeBadResponseBody)
|
|
}
|
|
case types.RelayFormatOpenAIResponses:
|
|
convertResult, err := service.ConvertResponse(c, info, types.RelayFormatOpenAIResponses, &claudeResponse)
|
|
if err != nil {
|
|
return types.NewError(err, types.ErrorCodeBadResponseBody)
|
|
}
|
|
responsesResponse, ok := convertResult.Value.(*dto.OpenAIResponsesResponse)
|
|
if !ok {
|
|
return types.NewError(fmt.Errorf("expected OpenAI Responses response, got %T", convertResult.Value), types.ErrorCodeBadResponseBody)
|
|
}
|
|
if responseID := helper.GetResponseID(c); responseID != "" {
|
|
responsesResponse.ID = responseID
|
|
}
|
|
responseData, err = common.Marshal(responsesResponse)
|
|
if err != nil {
|
|
return types.NewError(err, types.ErrorCodeBadResponseBody)
|
|
}
|
|
case types.RelayFormatClaude:
|
|
responseData = data
|
|
case types.RelayFormatGemini:
|
|
{
|
|
convertResult, convertErr := service.ConvertResponse(c, info, types.RelayFormatGemini, &claudeResponse)
|
|
if convertErr != nil {
|
|
return types.NewError(convertErr, types.ErrorCodeBadResponseBody)
|
|
}
|
|
geminiResponse, ok := convertResult.Value.(*dto.GeminiChatResponse)
|
|
if !ok {
|
|
return types.NewError(fmt.Errorf("expected Gemini generateContent response, got %T", convertResult.Value), types.ErrorCodeBadResponseBody)
|
|
}
|
|
responseData, err = common.Marshal(geminiResponse)
|
|
if err != nil {
|
|
return types.NewError(err, types.ErrorCodeBadResponseBody)
|
|
}
|
|
}
|
|
}
|
|
|
|
if claudeResponse.Usage != nil && claudeResponse.Usage.ServerToolUse != nil && claudeResponse.Usage.ServerToolUse.WebSearchRequests > 0 {
|
|
c.Set("claude_web_search_requests", claudeResponse.Usage.ServerToolUse.WebSearchRequests)
|
|
}
|
|
|
|
for _, block := range claudeResponse.Content {
|
|
if block.Type == "tool_use" {
|
|
info.CountBillableToolCall(dto.BuildInCallToolUse, block.Name)
|
|
}
|
|
}
|
|
|
|
service.IOCopyBytesGracefully(c, httpResp, responseData)
|
|
return nil
|
|
}
|
|
|
|
func ClaudeHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.Usage, *types.NewAPIError) {
|
|
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 nil, types.NewError(err, types.ErrorCodeBadResponseBody)
|
|
}
|
|
logger.LogDebug(c, "responseBody: %s", responseBody)
|
|
handleErr := HandleClaudeResponseData(c, info, claudeInfo, resp, responseBody)
|
|
if handleErr != nil {
|
|
return nil, handleErr
|
|
}
|
|
return claudeInfo.Usage, nil
|
|
}
|