From 04dc540b2371e487569d3a21967d40cec094b638 Mon Sep 17 00:00:00 2001 From: SamizuHM Date: Tue, 11 Aug 2026 18:15:31 +0800 Subject: [PATCH] fix(openai): parse nested data usage envelopes --- .../openai_gateway_response_handling.go | 23 ++++++++++++++----- .../service/openai_gateway_service_test.go | 11 +++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/backend/internal/service/openai_gateway_response_handling.go b/backend/internal/service/openai_gateway_response_handling.go index 2a1993c463..18c4d0de5a 100644 --- a/backend/internal/service/openai_gateway_response_handling.go +++ b/backend/internal/service/openai_gateway_response_handling.go @@ -979,13 +979,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}。 + // 按优先级尝试原生 OpenAI Responses、兼容层 data 包装和 Responses 包装, + // 避免同步请求能正常返回但用量被静默记录为 0。 + candidates := []struct { + usagePath string + imageUsagePath string + }{ + {usagePath: "usage", imageUsagePath: "tool_usage.image_gen"}, + {usagePath: "data.usage", imageUsagePath: "data.tool_usage.image_gen"}, + {usagePath: "response.usage", imageUsagePath: "response.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 } diff --git a/backend/internal/service/openai_gateway_service_test.go b/backend/internal/service/openai_gateway_service_test.go index d03d48e00f..95a26bc148 100644 --- a/backend/internal/service/openai_gateway_service_test.go +++ b/backend/internal/service/openai_gateway_service_test.go @@ -472,6 +472,17 @@ 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 TestOpenAIGatewayService_BindHTTPResponseAccount(t *testing.T) { gin.SetMode(gin.TestMode) rec := httptest.NewRecorder()