Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dto/openai_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ type OpenAIVideoResponse struct {

type InputTokenDetails struct {
CachedTokens int `json:"cached_tokens"`
CachedCreationTokens int `json:"-"`
CachedCreationTokens int `json:"cache_creation_input_tokens"`
TextTokens int `json:"text_tokens"`
AudioTokens int `json:"audio_tokens"`
ImageTokens int `json:"image_tokens"`
Expand Down
36 changes: 36 additions & 0 deletions relay/channel/openai/relay-openai.go
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,19 @@ func applyUsagePostProcessing(info *relaycommon.RelayInfo, usage *dto.Usage, res
usage.PromptTokensDetails.CachedTokens = usage.PromptCacheHitTokens
}
}
default:
if usage.PromptTokensDetails.CachedTokens == 0 {
if usage.PromptCacheHitTokens > 0 {
usage.PromptTokensDetails.CachedTokens = usage.PromptCacheHitTokens
} else if cachedTokens, ok := extractCachedTokensFromBody(responseBody); ok {
usage.PromptTokensDetails.CachedTokens = cachedTokens
}
}
if usage.PromptTokensDetails.CachedCreationTokens == 0 {
if cacheCreationTokens, ok := extractCacheCreationTokensFromBody(responseBody); ok {
usage.PromptTokensDetails.CachedCreationTokens = cacheCreationTokens
}
}
}
}

Expand Down Expand Up @@ -661,6 +674,29 @@ func extractCachedTokensFromBody(body []byte) (int, bool) {
return 0, false
}

func extractCacheCreationTokensFromBody(body []byte) (int, bool) {
if len(body) == 0 {
return 0, false
}

var payload struct {
Usage struct {
PromptTokensDetails struct {
CacheCreationInputTokens *int `json:"cache_creation_input_tokens"`
} `json:"prompt_tokens_details"`
} `json:"usage"`
}

if err := common.Unmarshal(body, &payload); err != nil {
return 0, false
}

if payload.Usage.PromptTokensDetails.CacheCreationInputTokens != nil {
return *payload.Usage.PromptTokensDetails.CacheCreationInputTokens, true
}
return 0, false
}

// extractMoonshotCachedTokensFromBody 从Moonshot的非标准位置提取cached_tokens
// Moonshot的流式响应格式: {"choices":[{"usage":{"cached_tokens":111}}]}
func extractMoonshotCachedTokensFromBody(body []byte) (int, bool) {
Expand Down
115 changes: 115 additions & 0 deletions relay/channel/openai/relay_openai_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package openai

import (
"testing"

"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/dto"
relaycommon "github.com/QuantumNous/new-api/relay/common"
)

func newRelayInfo(channelType int) *relaycommon.RelayInfo {
return &relaycommon.RelayInfo{
ChannelMeta: &relaycommon.ChannelMeta{ChannelType: channelType},
}
}

func TestApplyUsagePostProcessing_DefaultCase_PromptCacheHitTokens(t *testing.T) {
info := newRelayInfo(0)
usage := &dto.Usage{
PromptCacheHitTokens: 200,
}
applyUsagePostProcessing(info, usage, nil)
if usage.PromptTokensDetails.CachedTokens != 200 {
t.Errorf("CachedTokens = %d, want 200", usage.PromptTokensDetails.CachedTokens)
}
}

func TestApplyUsagePostProcessing_DefaultCase_StepFunCachedTokens(t *testing.T) {
body := []byte(`{"usage":{"cached_tokens":150}}`)
info := newRelayInfo(0)
usage := &dto.Usage{}
applyUsagePostProcessing(info, usage, body)
if usage.PromptTokensDetails.CachedTokens != 150 {
t.Errorf("CachedTokens = %d, want 150", usage.PromptTokensDetails.CachedTokens)
}
}

func TestApplyUsagePostProcessing_DefaultCase_PrefersPromptCacheHitTokens(t *testing.T) {
body := []byte(`{"usage":{"cached_tokens":100}}`)
info := newRelayInfo(0)
usage := &dto.Usage{
PromptCacheHitTokens: 200,
}
applyUsagePostProcessing(info, usage, body)
if usage.PromptTokensDetails.CachedTokens != 200 {
t.Errorf("CachedTokens = %d, want 200 (should prefer PromptCacheHitTokens over body)", usage.PromptTokensDetails.CachedTokens)
}
}

func TestApplyUsagePostProcessing_DefaultCase_CacheCreationTokens(t *testing.T) {
body := []byte(`{"usage":{"prompt_tokens_details":{"cache_creation_input_tokens":300}}}`)
info := newRelayInfo(0)
usage := &dto.Usage{}
applyUsagePostProcessing(info, usage, body)
if usage.PromptTokensDetails.CachedCreationTokens != 300 {
t.Errorf("CachedCreationTokens = %d, want 300", usage.PromptTokensDetails.CachedCreationTokens)
}
}

func TestApplyUsagePostProcessing_DefaultCase_AlreadyPopulated(t *testing.T) {
body := []byte(`{"usage":{"cached_tokens":999,"prompt_tokens_details":{"cache_creation_input_tokens":999}}}`)
info := newRelayInfo(0)
usage := &dto.Usage{
PromptTokensDetails: dto.InputTokenDetails{
CachedTokens: 100,
CachedCreationTokens: 50,
},
}
applyUsagePostProcessing(info, usage, body)
if usage.PromptTokensDetails.CachedTokens != 100 {
t.Errorf("CachedTokens = %d, want 100 (should not overwrite)", usage.PromptTokensDetails.CachedTokens)
}
if usage.PromptTokensDetails.CachedCreationTokens != 50 {
t.Errorf("CachedCreationTokens = %d, want 50 (should not overwrite)", usage.PromptTokensDetails.CachedCreationTokens)
}
}

func TestApplyUsagePostProcessing_DeepSeek_Unaffected(t *testing.T) {
info := newRelayInfo(constant.ChannelTypeDeepSeek)
usage := &dto.Usage{
PromptCacheHitTokens: 500,
}
applyUsagePostProcessing(info, usage, nil)
if usage.PromptTokensDetails.CachedTokens != 500 {
t.Errorf("CachedTokens = %d, want 500", usage.PromptTokensDetails.CachedTokens)
}
}

func TestExtractCacheCreationTokensFromBody(t *testing.T) {
body := []byte(`{"usage":{"prompt_tokens_details":{"cache_creation_input_tokens":42}}}`)
tokens, ok := extractCacheCreationTokensFromBody(body)
if !ok {
t.Fatal("expected ok=true")
}
if tokens != 42 {
t.Errorf("tokens = %d, want 42", tokens)
}
}

func TestExtractCacheCreationTokensFromBody_Empty(t *testing.T) {
tokens, ok := extractCacheCreationTokensFromBody(nil)
if ok {
t.Errorf("expected ok=false for nil body, got tokens=%d", tokens)
}

tokens, ok = extractCacheCreationTokensFromBody([]byte(`{}`))
if ok {
t.Errorf("expected ok=false for empty JSON, got tokens=%d", tokens)
}

tokens, ok = extractCacheCreationTokensFromBody([]byte(`invalid`))
if ok {
t.Errorf("expected ok=false for invalid JSON, got tokens=%d", tokens)
}
}