|
|
@@ -227,21 +227,31 @@ func buildClaudeUsageFromOpenAIUsage(oaiUsage *dto.Usage) *dto.ClaudeUsage {
|
|
|
if oaiUsage == nil {
|
|
|
return nil
|
|
|
}
|
|
|
+ cacheCreation5m, cacheCreation1h := NormalizeCacheCreationSplit(
|
|
|
+ oaiUsage.PromptTokensDetails.CachedCreationTokens,
|
|
|
+ oaiUsage.ClaudeCacheCreation5mTokens,
|
|
|
+ oaiUsage.ClaudeCacheCreation1hTokens,
|
|
|
+ )
|
|
|
usage := &dto.ClaudeUsage{
|
|
|
InputTokens: oaiUsage.PromptTokens,
|
|
|
OutputTokens: oaiUsage.CompletionTokens,
|
|
|
CacheCreationInputTokens: oaiUsage.PromptTokensDetails.CachedCreationTokens,
|
|
|
CacheReadInputTokens: oaiUsage.PromptTokensDetails.CachedTokens,
|
|
|
}
|
|
|
- if oaiUsage.ClaudeCacheCreation5mTokens > 0 || oaiUsage.ClaudeCacheCreation1hTokens > 0 {
|
|
|
+ if cacheCreation5m > 0 || cacheCreation1h > 0 {
|
|
|
usage.CacheCreation = &dto.ClaudeCacheCreationUsage{
|
|
|
- Ephemeral5mInputTokens: oaiUsage.ClaudeCacheCreation5mTokens,
|
|
|
- Ephemeral1hInputTokens: oaiUsage.ClaudeCacheCreation1hTokens,
|
|
|
+ Ephemeral5mInputTokens: cacheCreation5m,
|
|
|
+ Ephemeral1hInputTokens: cacheCreation1h,
|
|
|
}
|
|
|
}
|
|
|
return usage
|
|
|
}
|
|
|
|
|
|
+func NormalizeCacheCreationSplit(totalTokens int, tokens5m int, tokens1h int) (int, int) {
|
|
|
+ remainder := lo.Max([]int{totalTokens - tokens5m - tokens1h, 0})
|
|
|
+ return tokens5m + remainder, tokens1h
|
|
|
+}
|
|
|
+
|
|
|
func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamResponse, info *relaycommon.RelayInfo) []*dto.ClaudeResponse {
|
|
|
if info.ClaudeConvertInfo.Done {
|
|
|
return nil
|
|
|
@@ -426,23 +436,28 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
|
|
|
}
|
|
|
|
|
|
if len(openAIResponse.Choices) == 0 {
|
|
|
- // no choices
|
|
|
- // 可能为非标准的 OpenAI 响应,判断是否已经完成
|
|
|
- if info.ClaudeConvertInfo.Done {
|
|
|
+ // Some OpenAI-compatible upstreams end with a usage-only SSE chunk.
|
|
|
+ oaiUsage := openAIResponse.Usage
|
|
|
+ if oaiUsage == nil {
|
|
|
+ oaiUsage = info.ClaudeConvertInfo.Usage
|
|
|
+ }
|
|
|
+ if oaiUsage != nil {
|
|
|
stopOpenBlocks()
|
|
|
- oaiUsage := info.ClaudeConvertInfo.Usage
|
|
|
- if oaiUsage != nil {
|
|
|
- claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
|
|
|
- Type: "message_delta",
|
|
|
- Usage: buildClaudeUsageFromOpenAIUsage(oaiUsage),
|
|
|
- Delta: &dto.ClaudeMediaMessage{
|
|
|
- StopReason: common.GetPointer[string](stopReasonOpenAI2Claude(info.FinishReason)),
|
|
|
- },
|
|
|
- })
|
|
|
+ stopReason := stopReasonOpenAI2Claude(info.FinishReason)
|
|
|
+ if stopReason == "" {
|
|
|
+ stopReason = "end_turn"
|
|
|
}
|
|
|
+ claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
|
|
|
+ Type: "message_delta",
|
|
|
+ Usage: buildClaudeUsageFromOpenAIUsage(oaiUsage),
|
|
|
+ Delta: &dto.ClaudeMediaMessage{
|
|
|
+ StopReason: common.GetPointer[string](stopReason),
|
|
|
+ },
|
|
|
+ })
|
|
|
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
|
|
|
Type: "message_stop",
|
|
|
})
|
|
|
+ info.ClaudeConvertInfo.Done = true
|
|
|
}
|
|
|
return claudeResponses
|
|
|
} else {
|
|
|
@@ -450,6 +465,13 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
|
|
|
doneChunk := chosenChoice.FinishReason != nil && *chosenChoice.FinishReason != ""
|
|
|
if doneChunk {
|
|
|
info.FinishReason = *chosenChoice.FinishReason
|
|
|
+ oaiUsage := openAIResponse.Usage
|
|
|
+ if oaiUsage == nil {
|
|
|
+ oaiUsage = info.ClaudeConvertInfo.Usage
|
|
|
+ // Some upstreams emit finish_reason first, then send a final usage-only chunk.
|
|
|
+ // Defer closing until usage is available so the final message_delta carries it.
|
|
|
+ return claudeResponses
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
var claudeResponse dto.ClaudeResponse
|