mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-13 07:40:56 +00:00
* feat(relaykit): preserve hosted tools across conversions - add protocol-neutral hosted-tool DTOs, conversion metadata, and loss policies - bridge citations, grounding metadata, and hosted-tool stream lifecycles - document the public conversion behavior and channel policy controls * refactor(relaykit): normalize reasoning and thinking intent - centralize provider-neutral reasoning intent, effort, and budget mappings - parse model suffixes at the host entry boundary while preserving provider-owned tails - keep adaptive Claude thinking and explicit zero-token compatibility consistent * fix(billing): preserve authoritative usage across relay hops - carry native BillingUsage sidecars through direct and streamed protocol bridges - merge partial and terminal usage monotonically with safe fallback settlement - retain cache metadata, penultimate usage, and per-call Gemini tool surcharges * feat(relay): bridge Responses with Claude and Gemini protocols - add direct request, response, and stream converters across supported relay formats - expose Claude count_tokens and Chat-to-Responses compatibility endpoints - carry conversion diagnostics through the host while retaining the curated public goldens * fix(relay): wire relaykit conversions into host channels - connect handlers, adaptors, and channel settings to the standalone conversion layer - keep model mapping, pricing identity, retries, and provider-specific suffix behavior aligned - ignore local audit artifacts and retain focused public regression coverage
565 lines
19 KiB
Go
565 lines
19 KiB
Go
package claudemessages
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"github.com/QuantumNous/new-api/relaykit/dto"
|
|
"github.com/QuantumNous/new-api/relaykit/reasonmap"
|
|
sharedclaude "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/shared/claude"
|
|
kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil"
|
|
"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
|
|
|
|
// Only snapshots synthesized from partial display usage may be refreshed by
|
|
// later display deltas. Serialized BillingUsage always remains authoritative.
|
|
billingUsageSynthesized 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)
|
|
annotations, err := claudeCitationsToChat(claudeResponse.ContentBlock.Citations, *claudeResponse.ContentBlock.Text, 0)
|
|
if err == nil {
|
|
choice.Delta.Annotations, _ = marshalChatAnnotations(annotations)
|
|
}
|
|
}
|
|
if claudeResponse.ContentBlock.Type == "tool_use" {
|
|
tools = append(tools, dto.ToolCallResponse{
|
|
Index: kitutil.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: kitutil.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
|
|
case "citations_delta":
|
|
if len(claudeResponse.Delta.Citation) > 0 {
|
|
raw, _ := kitutil.Marshal([]json.RawMessage{claudeResponse.Delta.Citation})
|
|
annotations, err := claudeCitationsToChat(raw, "", 0)
|
|
if err == nil {
|
|
choice.Delta.Annotations, _ = marshalChatAnnotations(annotations)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} 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
|
|
}
|
|
|
|
// ClaudeToChatStreamState translates Anthropic content block indexes into the
|
|
// independent, dense index space used by Chat Completions tool_calls. Text and
|
|
// thinking blocks therefore do not create holes in the downstream tool array.
|
|
type ClaudeToChatStreamState struct {
|
|
toolIndexByContentBlock map[int]int
|
|
blockTypeByContentBlock map[int]string
|
|
nextToolIndex int
|
|
}
|
|
|
|
func NewClaudeToChatStreamState() *ClaudeToChatStreamState {
|
|
return &ClaudeToChatStreamState{
|
|
toolIndexByContentBlock: make(map[int]int),
|
|
blockTypeByContentBlock: make(map[int]string),
|
|
}
|
|
}
|
|
|
|
func (s *ClaudeToChatStreamState) ConvertChunk(claudeResponse *dto.ClaudeResponse) (*dto.ChatCompletionsStreamResponse, error) {
|
|
if s == nil {
|
|
return nil, fmt.Errorf("Claude-to-Chat stream state is required")
|
|
}
|
|
if claudeResponse == nil {
|
|
return nil, nil
|
|
}
|
|
if s.toolIndexByContentBlock == nil {
|
|
s.toolIndexByContentBlock = make(map[int]int)
|
|
}
|
|
if s.blockTypeByContentBlock == nil {
|
|
s.blockTypeByContentBlock = make(map[int]string)
|
|
}
|
|
|
|
converted := *claudeResponse
|
|
switch claudeResponse.Type {
|
|
case "content_block_start":
|
|
if claudeResponse.ContentBlock == nil {
|
|
break
|
|
}
|
|
blockType := strings.TrimSpace(claudeResponse.ContentBlock.Type)
|
|
if blockType == "" {
|
|
break
|
|
}
|
|
if claudeResponse.Index == nil {
|
|
return nil, fmt.Errorf("Claude content block stream start is missing index")
|
|
}
|
|
contentBlockIndex := *claudeResponse.Index
|
|
s.blockTypeByContentBlock[contentBlockIndex] = blockType
|
|
if blockType != "tool_use" {
|
|
if isClaudeHostedToolStreamBlock(blockType) {
|
|
return nil, nil
|
|
}
|
|
break
|
|
}
|
|
toolIndex, exists := s.toolIndexByContentBlock[contentBlockIndex]
|
|
if !exists {
|
|
toolIndex = s.nextToolIndex
|
|
s.nextToolIndex++
|
|
s.toolIndexByContentBlock[contentBlockIndex] = toolIndex
|
|
}
|
|
converted.Index = kitutil.GetPointer(toolIndex)
|
|
case "content_block_delta":
|
|
if claudeResponse.Delta == nil || claudeResponse.Delta.Type != "input_json_delta" {
|
|
break
|
|
}
|
|
if claudeResponse.Index == nil {
|
|
return nil, fmt.Errorf("Claude tool-use stream delta is missing content block index")
|
|
}
|
|
if claudeResponse.Delta.PartialJson == nil {
|
|
return nil, fmt.Errorf("Claude tool-use stream delta is missing partial JSON")
|
|
}
|
|
contentBlockIndex := *claudeResponse.Index
|
|
toolIndex, exists := s.toolIndexByContentBlock[contentBlockIndex]
|
|
if !exists {
|
|
if isClaudeHostedToolStreamBlock(s.blockTypeByContentBlock[contentBlockIndex]) {
|
|
return nil, nil
|
|
}
|
|
return nil, fmt.Errorf("Claude tool-use stream delta references unknown content block index %d", contentBlockIndex)
|
|
}
|
|
converted.Index = kitutil.GetPointer(toolIndex)
|
|
case "content_block_stop":
|
|
if claudeResponse.Index != nil {
|
|
delete(s.blockTypeByContentBlock, *claudeResponse.Index)
|
|
}
|
|
}
|
|
|
|
return StreamResponseClaude2OpenAI(&converted), nil
|
|
}
|
|
|
|
func isClaudeHostedToolStreamBlock(blockType string) bool {
|
|
switch blockType {
|
|
case "server_tool_use", "mcp_tool_use", "web_search_tool_result", "mcp_tool_result", "code_execution_tool_result", "web_fetch_tool_result":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func ResponseClaude2OpenAI(claudeResponse *dto.ClaudeResponse) *dto.OpenAITextResponse {
|
|
choices := make([]dto.OpenAITextResponseChoice, 0)
|
|
fullTextResponse := dto.OpenAITextResponse{
|
|
Id: fmt.Sprintf("chatcmpl-%s", kitutil.GetUUID()),
|
|
Object: "chat.completion",
|
|
Created: kitutil.GetTimestamp(),
|
|
}
|
|
var responseText strings.Builder
|
|
responseTextOffset := 0
|
|
var responseThinking string
|
|
if len(claudeResponse.Content) > 0 {
|
|
if claudeResponse.Content[0].Thinking != nil {
|
|
responseThinking = *claudeResponse.Content[0].Thinking
|
|
}
|
|
}
|
|
tools := make([]dto.ToolCallResponse, 0)
|
|
thinkingContent := ""
|
|
annotations := make([]any, 0)
|
|
|
|
fullTextResponse.Id = claudeResponse.Id
|
|
for _, message := range claudeResponse.Content {
|
|
switch message.Type {
|
|
case "tool_use":
|
|
args, _ := kitutil.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":
|
|
text := message.GetText()
|
|
offset := responseTextOffset
|
|
responseText.WriteString(text)
|
|
responseTextOffset += utf8.RuneCountInString(text)
|
|
converted, err := claudeCitationsToChat(message.Citations, text, offset)
|
|
if err == nil {
|
|
annotations = append(annotations, converted...)
|
|
}
|
|
}
|
|
}
|
|
choice := dto.OpenAITextResponseChoice{
|
|
Index: 0,
|
|
Message: dto.Message{
|
|
Role: "assistant",
|
|
},
|
|
FinishReason: StopReasonClaudeToOpenAI(claudeResponse.StopReason),
|
|
}
|
|
choice.SetStringContent(responseText.String())
|
|
if encodedAnnotations, err := marshalChatAnnotations(annotations); err == nil && len(encodedAnnotations) > 0 {
|
|
choice.Message.Annotations = encodedAnnotations
|
|
}
|
|
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 updateClaudeStreamBillingUsage(claudeUsage *dto.ClaudeUsage, claudeInfo *ClaudeResponseInfo, terminal bool) {
|
|
if claudeUsage == nil || claudeInfo == nil || claudeInfo.Usage == nil {
|
|
return
|
|
}
|
|
if billingUsage := dto.CloneBillingUsage(claudeUsage.BillingUsage); billingUsage != nil {
|
|
claudeInfo.Usage.BillingUsage = billingUsage
|
|
if terminal || claudeUsage.OutputTokens > 0 {
|
|
claudeInfo.billingUsageSynthesized = false
|
|
return
|
|
}
|
|
claudeInfo.billingUsageSynthesized = true
|
|
return
|
|
}
|
|
if claudeInfo.Usage.BillingUsage != nil && !claudeInfo.billingUsageSynthesized {
|
|
return
|
|
}
|
|
claudeInfo.Usage.BillingUsage = claudeBillingUsageFromSemanticUsage(claudeInfo.Usage)
|
|
claudeInfo.billingUsageSynthesized = claudeInfo.Usage.BillingUsage != nil
|
|
}
|
|
|
|
// FinalizeClaudeStreamBillingUsage refreshes only a locally synthesized
|
|
// snapshot after the host has applied its missing-usage fallback. A snapshot
|
|
// received on the wire remains authoritative and is never rewritten.
|
|
func FinalizeClaudeStreamBillingUsage(claudeInfo *ClaudeResponseInfo) {
|
|
if claudeInfo == nil || claudeInfo.Usage == nil {
|
|
return
|
|
}
|
|
if claudeInfo.Usage.BillingUsage != nil && !claudeInfo.billingUsageSynthesized {
|
|
return
|
|
}
|
|
|
|
billingUsage := claudeBillingUsageFromSemanticUsage(claudeInfo.Usage)
|
|
if billingUsage != nil && !claudeInfo.Done {
|
|
billingUsage.Estimated = true
|
|
}
|
|
claudeInfo.Usage.BillingUsage = billingUsage
|
|
claudeInfo.billingUsageSynthesized = billingUsage != nil
|
|
}
|
|
|
|
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 {
|
|
messageUsage := claudeResponse.Message.Usage
|
|
claudeInfo.Usage.PromptTokens = messageUsage.InputTokens
|
|
claudeInfo.Usage.UsageSemantic = "anthropic"
|
|
claudeInfo.Usage.PromptTokensDetails.CachedTokens = messageUsage.CacheReadInputTokens
|
|
claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens = messageUsage.CacheCreationInputTokens
|
|
claudeInfo.Usage.ClaudeCacheCreation5mTokens = messageUsage.GetCacheCreation5mTokens()
|
|
claudeInfo.Usage.ClaudeCacheCreation1hTokens = messageUsage.GetCacheCreation1hTokens()
|
|
claudeInfo.Usage.CompletionTokens = messageUsage.OutputTokens
|
|
updateClaudeStreamBillingUsage(messageUsage, claudeInfo, false)
|
|
}
|
|
} 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
|
|
updateClaudeStreamBillingUsage(claudeResponse.Usage, claudeInfo, true)
|
|
}
|
|
|
|
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
|
|
}
|