mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-03 04:43:01 +00:00
Parse OpenAI's native cache_write_tokens (chat prompt_tokens_details / responses input_tokens_details), bill it at the cache-creation ratio, and clamp the uncached prompt remainder at zero since cached + cache-write can exceed prompt_tokens. Propagate the field through chat/responses/claude format conversions and tiered expression billing (cc variable).
401 lines
14 KiB
Go
401 lines
14 KiB
Go
package claudemessages
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/dto"
|
|
"github.com/QuantumNous/new-api/relay/reasonmap"
|
|
sharedclaude "github.com/QuantumNous/new-api/service/relayconvert/internal/shared/claude"
|
|
"github.com/tidwall/gjson"
|
|
"github.com/tidwall/sjson"
|
|
)
|
|
|
|
type ClaudeResponseInfo struct {
|
|
ResponseId string
|
|
Created int64
|
|
Model string
|
|
ResponseText strings.Builder
|
|
Usage *dto.Usage
|
|
Done bool
|
|
}
|
|
|
|
func StopReasonClaudeToOpenAI(reason string) string {
|
|
return reasonmap.ClaudeStopReasonToOpenAIFinishReason(reason)
|
|
}
|
|
|
|
func StreamResponseClaude2OpenAI(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
|
|
}
|
|
var choice dto.ChatCompletionsStreamResponseChoice
|
|
if claudeResponse.Type == "message_start" {
|
|
if claudeResponse.Message != nil {
|
|
response.Id = claudeResponse.Message.Id
|
|
response.Model = claudeResponse.Message.Model
|
|
}
|
|
choice.Delta.SetContentString("")
|
|
choice.Delta.Role = "assistant"
|
|
} else if claudeResponse.Type == "content_block_start" {
|
|
if claudeResponse.ContentBlock != nil {
|
|
if claudeResponse.ContentBlock.Type == "text" && claudeResponse.ContentBlock.Text != 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":
|
|
choice.Delta.ReasoningContent = claudeResponse.Delta.Thinking
|
|
}
|
|
}
|
|
} else if claudeResponse.Type == "message_delta" {
|
|
if claudeResponse.Delta != nil && claudeResponse.Delta.StopReason != nil {
|
|
finishReason := StopReasonClaudeToOpenAI(*claudeResponse.Delta.StopReason)
|
|
if finishReason != "null" {
|
|
choice.FinishReason = &finishReason
|
|
}
|
|
}
|
|
} else if claudeResponse.Type == "message_stop" {
|
|
return nil
|
|
} else {
|
|
return nil
|
|
}
|
|
if len(tools) > 0 {
|
|
choice.Delta.Content = nil
|
|
choice.Delta.ToolCalls = tools
|
|
}
|
|
response.Choices = append(response.Choices, choice)
|
|
|
|
return &response
|
|
}
|
|
|
|
func ResponseClaude2OpenAI(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()
|
|
if claudeResponse.Content[0].Thinking != nil {
|
|
responseThinking = *claudeResponse.Content[0].Thinking
|
|
}
|
|
}
|
|
tools := make([]dto.ToolCallResponse, 0)
|
|
thinkingContent := ""
|
|
|
|
fullTextResponse.Id = claudeResponse.Id
|
|
for _, message := range claudeResponse.Content {
|
|
switch message.Type {
|
|
case "tool_use":
|
|
args, _ := common.Marshal(message.Input)
|
|
tools = append(tools, dto.ToolCallResponse{
|
|
ID: message.Id,
|
|
Type: "function",
|
|
Function: dto.FunctionResponse{
|
|
Name: message.Name,
|
|
Arguments: string(args),
|
|
},
|
|
})
|
|
case "thinking":
|
|
if message.Thinking != nil {
|
|
thinkingContent = *message.Thinking
|
|
}
|
|
case "text":
|
|
responseText = message.GetText()
|
|
}
|
|
}
|
|
choice := dto.OpenAITextResponseChoice{
|
|
Index: 0,
|
|
Message: dto.Message{
|
|
Role: "assistant",
|
|
},
|
|
FinishReason: StopReasonClaudeToOpenAI(claudeResponse.StopReason),
|
|
}
|
|
choice.SetStringContent(responseText)
|
|
if len(responseThinking) > 0 {
|
|
choice.ReasoningContent = &responseThinking
|
|
}
|
|
if len(tools) > 0 {
|
|
choice.Message.SetToolCalls(tools)
|
|
}
|
|
if thinkingContent != "" {
|
|
choice.Message.ReasoningContent = &thinkingContent
|
|
}
|
|
fullTextResponse.Model = claudeResponse.Model
|
|
choices = append(choices, choice)
|
|
fullTextResponse.Choices = choices
|
|
return &fullTextResponse
|
|
}
|
|
|
|
func UsageFromClaudeAPIUsage(usage *dto.ClaudeUsage) *dto.Usage {
|
|
if usage == nil {
|
|
return &dto.Usage{}
|
|
}
|
|
semanticUsage := &dto.Usage{
|
|
PromptTokens: usage.InputTokens,
|
|
CompletionTokens: usage.OutputTokens,
|
|
UsageSemantic: "anthropic",
|
|
UsageSource: "anthropic",
|
|
BillingUsage: dto.CloneBillingUsage(usage.BillingUsage),
|
|
}
|
|
if semanticUsage.BillingUsage == nil {
|
|
semanticUsage.BillingUsage = dto.NewClaudeMessagesBillingUsage(usage)
|
|
}
|
|
semanticUsage.PromptTokensDetails.CachedTokens = usage.CacheReadInputTokens
|
|
semanticUsage.PromptTokensDetails.CachedCreationTokens = usage.CacheCreationInputTokens
|
|
semanticUsage.ClaudeCacheCreation5mTokens = usage.GetCacheCreation5mTokens()
|
|
semanticUsage.ClaudeCacheCreation1hTokens = usage.GetCacheCreation1hTokens()
|
|
return UsageFromClaudeUsage(semanticUsage)
|
|
}
|
|
|
|
func UsageFromClaudeUsage(usage *dto.Usage) *dto.Usage {
|
|
mapped := buildOpenAIStyleUsageFromClaudeUsage(usage)
|
|
return &mapped
|
|
}
|
|
|
|
func cacheCreationTokensForOpenAIUsage(usage *dto.Usage) int {
|
|
if usage == nil {
|
|
return 0
|
|
}
|
|
splitCacheCreationTokens := usage.ClaudeCacheCreation5mTokens + usage.ClaudeCacheCreation1hTokens
|
|
if splitCacheCreationTokens == 0 {
|
|
return usage.PromptTokensDetails.CachedCreationTokens
|
|
}
|
|
if usage.PromptTokensDetails.CachedCreationTokens > splitCacheCreationTokens {
|
|
return usage.PromptTokensDetails.CachedCreationTokens
|
|
}
|
|
return splitCacheCreationTokens
|
|
}
|
|
|
|
func buildOpenAIStyleUsageFromClaudeUsage(usage *dto.Usage) dto.Usage {
|
|
if usage == nil {
|
|
return dto.Usage{}
|
|
}
|
|
clone := *usage
|
|
clone.BillingUsage = dto.CloneBillingUsage(usage.BillingUsage)
|
|
clone.ClaudeCacheCreation5mTokens, clone.ClaudeCacheCreation1hTokens = sharedclaude.NormalizeCacheCreationSplit(
|
|
usage.PromptTokensDetails.CachedCreationTokens,
|
|
usage.ClaudeCacheCreation5mTokens,
|
|
usage.ClaudeCacheCreation1hTokens,
|
|
)
|
|
cacheCreationTokens := cacheCreationTokensForOpenAIUsage(usage)
|
|
// Expose the standard OpenAI cache-write field alongside the legacy
|
|
// cached_creation_tokens so OpenAI-format clients can bill cache writes.
|
|
clone.PromptTokensDetails.CacheWriteTokens = cacheCreationTokens
|
|
totalInputTokens := usage.PromptTokens + usage.PromptTokensDetails.CachedTokens + cacheCreationTokens
|
|
clone.PromptTokens = totalInputTokens
|
|
clone.InputTokens = totalInputTokens
|
|
clone.TotalTokens = totalInputTokens + usage.CompletionTokens
|
|
clone.UsageSemantic = "openai"
|
|
clone.UsageSource = "anthropic"
|
|
return clone
|
|
}
|
|
|
|
func BuildMessageDeltaPatchUsage(claudeResponse *dto.ClaudeResponse, claudeInfo *ClaudeResponseInfo) *dto.ClaudeUsage {
|
|
usage := &dto.ClaudeUsage{}
|
|
if claudeResponse != nil && claudeResponse.Usage != nil {
|
|
*usage = *claudeResponse.Usage
|
|
}
|
|
|
|
if claudeInfo == nil || claudeInfo.Usage == nil {
|
|
return usage
|
|
}
|
|
|
|
if usage.InputTokens == 0 && claudeInfo.Usage.PromptTokens > 0 {
|
|
usage.InputTokens = claudeInfo.Usage.PromptTokens
|
|
}
|
|
if usage.CacheReadInputTokens == 0 && claudeInfo.Usage.PromptTokensDetails.CachedTokens > 0 {
|
|
usage.CacheReadInputTokens = claudeInfo.Usage.PromptTokensDetails.CachedTokens
|
|
}
|
|
if usage.CacheCreationInputTokens == 0 && claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens > 0 {
|
|
usage.CacheCreationInputTokens = claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens
|
|
}
|
|
cacheCreation5m := 0
|
|
cacheCreation1h := 0
|
|
if usage.CacheCreation != nil {
|
|
cacheCreation5m = usage.CacheCreation.Ephemeral5mInputTokens
|
|
cacheCreation1h = usage.CacheCreation.Ephemeral1hInputTokens
|
|
} else {
|
|
cacheCreation5m = claudeInfo.Usage.ClaudeCacheCreation5mTokens
|
|
cacheCreation1h = claudeInfo.Usage.ClaudeCacheCreation1hTokens
|
|
}
|
|
cacheCreation5m, cacheCreation1h = sharedclaude.NormalizeCacheCreationSplit(
|
|
usage.CacheCreationInputTokens,
|
|
cacheCreation5m,
|
|
cacheCreation1h,
|
|
)
|
|
if usage.CacheCreation == nil && (cacheCreation5m > 0 || cacheCreation1h > 0) {
|
|
usage.CacheCreation = &dto.ClaudeCacheCreationUsage{}
|
|
}
|
|
if usage.CacheCreation != nil {
|
|
usage.CacheCreation.Ephemeral5mInputTokens = cacheCreation5m
|
|
usage.CacheCreation.Ephemeral1hInputTokens = cacheCreation1h
|
|
}
|
|
return usage
|
|
}
|
|
|
|
func claudeBillingUsageFromSemanticUsage(usage *dto.Usage) *dto.BillingUsage {
|
|
if usage == nil {
|
|
return nil
|
|
}
|
|
cacheCreation5m, cacheCreation1h := sharedclaude.NormalizeCacheCreationSplit(
|
|
usage.PromptTokensDetails.CachedCreationTokens,
|
|
usage.ClaudeCacheCreation5mTokens,
|
|
usage.ClaudeCacheCreation1hTokens,
|
|
)
|
|
claudeUsage := &dto.ClaudeUsage{
|
|
InputTokens: usage.PromptTokens,
|
|
CacheCreationInputTokens: usage.PromptTokensDetails.CachedCreationTokens,
|
|
CacheReadInputTokens: usage.PromptTokensDetails.CachedTokens,
|
|
OutputTokens: usage.CompletionTokens,
|
|
}
|
|
if cacheCreation5m > 0 || cacheCreation1h > 0 {
|
|
claudeUsage.CacheCreation = &dto.ClaudeCacheCreationUsage{
|
|
Ephemeral5mInputTokens: cacheCreation5m,
|
|
Ephemeral1hInputTokens: cacheCreation1h,
|
|
}
|
|
}
|
|
return dto.NewClaudeMessagesBillingUsage(claudeUsage)
|
|
}
|
|
|
|
func PatchClaudeMessageDeltaUsageData(data string, usage *dto.ClaudeUsage) string {
|
|
if data == "" || usage == nil {
|
|
return data
|
|
}
|
|
|
|
data = setMessageDeltaUsageInt(data, "usage.input_tokens", usage.InputTokens)
|
|
data = setMessageDeltaUsageInt(data, "usage.cache_read_input_tokens", usage.CacheReadInputTokens)
|
|
data = setMessageDeltaUsageInt(data, "usage.cache_creation_input_tokens", usage.CacheCreationInputTokens)
|
|
|
|
if usage.CacheCreation != nil {
|
|
data = setMessageDeltaUsageInt(data, "usage.cache_creation.ephemeral_5m_input_tokens", usage.CacheCreation.Ephemeral5mInputTokens)
|
|
data = setMessageDeltaUsageInt(data, "usage.cache_creation.ephemeral_1h_input_tokens", usage.CacheCreation.Ephemeral1hInputTokens)
|
|
}
|
|
|
|
return data
|
|
}
|
|
|
|
func setMessageDeltaUsageInt(data string, path string, localValue int) string {
|
|
if localValue <= 0 {
|
|
return data
|
|
}
|
|
|
|
upstreamValue := gjson.Get(data, path)
|
|
if upstreamValue.Exists() && upstreamValue.Int() > 0 {
|
|
return data
|
|
}
|
|
|
|
patchedData, err := sjson.Set(data, path, localValue)
|
|
if err != nil {
|
|
return data
|
|
}
|
|
return patchedData
|
|
}
|
|
|
|
func FormatClaudeResponseInfo(claudeResponse *dto.ClaudeResponse, oaiResponse *dto.ChatCompletionsStreamResponse, claudeInfo *ClaudeResponseInfo) bool {
|
|
if claudeInfo == nil {
|
|
return false
|
|
}
|
|
if claudeInfo.Usage == nil {
|
|
claudeInfo.Usage = &dto.Usage{}
|
|
}
|
|
if claudeResponse.Type == "message_start" {
|
|
if claudeResponse.Message != nil {
|
|
claudeInfo.ResponseId = claudeResponse.Message.Id
|
|
claudeInfo.Model = claudeResponse.Message.Model
|
|
}
|
|
|
|
if claudeResponse.Message != nil && claudeResponse.Message.Usage != nil {
|
|
claudeInfo.Usage.PromptTokens = claudeResponse.Message.Usage.InputTokens
|
|
claudeInfo.Usage.UsageSemantic = "anthropic"
|
|
claudeInfo.Usage.PromptTokensDetails.CachedTokens = claudeResponse.Message.Usage.CacheReadInputTokens
|
|
claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens = claudeResponse.Message.Usage.CacheCreationInputTokens
|
|
claudeInfo.Usage.ClaudeCacheCreation5mTokens = claudeResponse.Message.Usage.GetCacheCreation5mTokens()
|
|
claudeInfo.Usage.ClaudeCacheCreation1hTokens = claudeResponse.Message.Usage.GetCacheCreation1hTokens()
|
|
claudeInfo.Usage.CompletionTokens = claudeResponse.Message.Usage.OutputTokens
|
|
claudeInfo.Usage.BillingUsage = claudeBillingUsageFromSemanticUsage(claudeInfo.Usage)
|
|
}
|
|
} else if claudeResponse.Type == "content_block_delta" {
|
|
if claudeResponse.Delta != nil {
|
|
if claudeResponse.Delta.Text != nil {
|
|
claudeInfo.ResponseText.WriteString(*claudeResponse.Delta.Text)
|
|
}
|
|
if claudeResponse.Delta.Thinking != nil {
|
|
claudeInfo.ResponseText.WriteString(*claudeResponse.Delta.Thinking)
|
|
}
|
|
}
|
|
} else if claudeResponse.Type == "message_delta" {
|
|
if claudeResponse.Usage != nil {
|
|
claudeInfo.Usage.UsageSemantic = "anthropic"
|
|
if claudeResponse.Usage.InputTokens > 0 {
|
|
claudeInfo.Usage.PromptTokens = claudeResponse.Usage.InputTokens
|
|
}
|
|
if claudeResponse.Usage.CacheReadInputTokens > 0 {
|
|
claudeInfo.Usage.PromptTokensDetails.CachedTokens = claudeResponse.Usage.CacheReadInputTokens
|
|
}
|
|
if claudeResponse.Usage.CacheCreationInputTokens > 0 {
|
|
claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens = claudeResponse.Usage.CacheCreationInputTokens
|
|
}
|
|
if cacheCreation5m := claudeResponse.Usage.GetCacheCreation5mTokens(); cacheCreation5m > 0 {
|
|
claudeInfo.Usage.ClaudeCacheCreation5mTokens = cacheCreation5m
|
|
}
|
|
if cacheCreation1h := claudeResponse.Usage.GetCacheCreation1hTokens(); cacheCreation1h > 0 {
|
|
claudeInfo.Usage.ClaudeCacheCreation1hTokens = cacheCreation1h
|
|
}
|
|
if claudeResponse.Usage.OutputTokens > 0 {
|
|
claudeInfo.Usage.CompletionTokens = claudeResponse.Usage.OutputTokens
|
|
}
|
|
claudeInfo.Usage.TotalTokens = claudeInfo.Usage.PromptTokens + claudeInfo.Usage.CompletionTokens
|
|
claudeInfo.Usage.BillingUsage = claudeBillingUsageFromSemanticUsage(claudeInfo.Usage)
|
|
}
|
|
|
|
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
|
|
}
|