mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 13:08:03 +08:00
Merge pull request #5531 from SamizuHM/fix/openai-compat-nested-data-usage
fix(openai-compat): parse usage from nested data envelopes
This commit is contained in:
@@ -1037,13 +1037,24 @@ func extractOpenAIUsageFromJSONBytes(body []byte) (OpenAIUsage, bool) {
|
||||
if len(body) == 0 || !gjson.ValidBytes(body) {
|
||||
return OpenAIUsage{}, false
|
||||
}
|
||||
if usage, ok := openAIUsageFromGJSON(gjson.GetBytes(body, "usage")); ok {
|
||||
mergeHostedImageGenToolUsage(gjson.GetBytes(body, "tool_usage.image_gen"), &usage)
|
||||
return usage, true
|
||||
// 部分 OpenAI 兼容上游(例如 Cline API)会将标准响应包在 data 字段中:
|
||||
// {"data":{"choices": [...], "usage": {...}}, "success":true}。
|
||||
// 按优先级先保留原有路径,再尝试兼容层 data 包装,
|
||||
// 避免同步请求能正常返回但用量被静默记录为 0。
|
||||
candidates := []struct {
|
||||
usagePath string
|
||||
imageUsagePath string
|
||||
}{
|
||||
{usagePath: "usage", imageUsagePath: "tool_usage.image_gen"},
|
||||
{usagePath: "response.usage", imageUsagePath: "response.tool_usage.image_gen"},
|
||||
{usagePath: "data.usage", imageUsagePath: "data.tool_usage.image_gen"},
|
||||
{usagePath: "data.response.usage", imageUsagePath: "data.response.tool_usage.image_gen"},
|
||||
}
|
||||
if usage, ok := openAIUsageFromGJSON(gjson.GetBytes(body, "response.usage")); ok {
|
||||
mergeHostedImageGenToolUsage(gjson.GetBytes(body, "response.tool_usage.image_gen"), &usage)
|
||||
return usage, true
|
||||
for _, candidate := range candidates {
|
||||
if usage, ok := openAIUsageFromGJSON(gjson.GetBytes(body, candidate.usagePath)); ok {
|
||||
mergeHostedImageGenToolUsage(gjson.GetBytes(body, candidate.imageUsagePath), &usage)
|
||||
return usage, true
|
||||
}
|
||||
}
|
||||
return OpenAIUsage{}, false
|
||||
}
|
||||
|
||||
@@ -495,6 +495,38 @@ func TestExtractOpenAIUsage_CapturesImageInputTokens(t *testing.T) {
|
||||
require.Zero(t, tu.ImageInputTokens)
|
||||
}
|
||||
|
||||
func TestExtractOpenAIUsage_ReadsClineDataEnvelope(t *testing.T) {
|
||||
body := []byte(`{"data":{"choices":[{"message":{"content":"OK"}}],"usage":{"prompt_tokens":8,"completion_tokens":27,"total_tokens":35,"prompt_tokens_details":{"cached_tokens":4}}},"success":true}`)
|
||||
|
||||
usage, ok := extractOpenAIUsageFromJSONBytes(body)
|
||||
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 8, usage.InputTokens)
|
||||
require.Equal(t, 27, usage.OutputTokens)
|
||||
require.Equal(t, 4, usage.CacheReadInputTokens)
|
||||
}
|
||||
|
||||
func TestExtractOpenAIUsage_ReadsWrappedResponsesDataEnvelope(t *testing.T) {
|
||||
body := []byte(`{"data":{"response":{"usage":{"input_tokens":11,"output_tokens":5,"total_tokens":16,"input_tokens_details":{"cached_tokens":2}}}}}`)
|
||||
|
||||
usage, ok := extractOpenAIUsageFromJSONBytes(body)
|
||||
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 11, usage.InputTokens)
|
||||
require.Equal(t, 5, usage.OutputTokens)
|
||||
require.Equal(t, 2, usage.CacheReadInputTokens)
|
||||
}
|
||||
|
||||
func TestExtractOpenAIUsage_PreservesResponseUsagePriority(t *testing.T) {
|
||||
body := []byte(`{"data":{"usage":{"prompt_tokens":100,"completion_tokens":50}},"response":{"usage":{"input_tokens":11,"output_tokens":5}}}`)
|
||||
|
||||
usage, ok := extractOpenAIUsageFromJSONBytes(body)
|
||||
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 11, usage.InputTokens)
|
||||
require.Equal(t, 5, usage.OutputTokens)
|
||||
}
|
||||
|
||||
func TestOpenAIGatewayService_BindHTTPResponseAccount(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
Reference in New Issue
Block a user