openai_gateway_response_handling_image_usage_test.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. package service
  2. import (
  3. "testing"
  4. "github.com/stretchr/testify/assert"
  5. "github.com/tidwall/gjson"
  6. )
  7. func TestExtractOpenAIUsageFromJSONBytes_MergesHostedImageGenToolUsage(t *testing.T) {
  8. // SSE response.completed event with response.usage + response.tool_usage.image_gen
  9. body := []byte(`{
  10. "type": "response.completed",
  11. "response": {
  12. "usage": {
  13. "input_tokens": 43792,
  14. "output_tokens": 1005,
  15. "total_tokens": 44797
  16. },
  17. "tool_usage": {
  18. "image_gen": {
  19. "input_tokens": 7918,
  20. "input_tokens_details": {"image_tokens": 7620, "text_tokens": 298},
  21. "output_tokens": 186,
  22. "output_tokens_details": {"image_tokens": 186, "text_tokens": 0},
  23. "total_tokens": 8104
  24. }
  25. }
  26. }
  27. }`)
  28. usage, ok := extractOpenAIUsageFromJSONBytes(body)
  29. assert.True(t, ok)
  30. assert.Equal(t, 43792, usage.InputTokens, "input_tokens from response.usage")
  31. assert.Equal(t, 1005, usage.OutputTokens, "output_tokens from response.usage")
  32. assert.Equal(t, 186, usage.ImageOutputTokens, "image output tokens merged from tool_usage.image_gen")
  33. assert.Equal(t, 7620, usage.ImageInputTokens, "image input tokens merged from tool_usage.image_gen")
  34. }
  35. func TestExtractOpenAIUsageFromJSONBytes_NonStreamingMergesImageGen(t *testing.T) {
  36. // Non-streaming response with top-level usage + tool_usage
  37. body := []byte(`{
  38. "id": "resp_abc123",
  39. "object": "response",
  40. "usage": {
  41. "input_tokens": 5000,
  42. "output_tokens": 200
  43. },
  44. "tool_usage": {
  45. "image_gen": {
  46. "input_tokens": 3000,
  47. "input_tokens_details": {"image_tokens": 2800, "text_tokens": 200},
  48. "output_tokens": 150,
  49. "output_tokens_details": {"image_tokens": 150, "text_tokens": 0},
  50. "total_tokens": 3150
  51. }
  52. }
  53. }`)
  54. usage, ok := extractOpenAIUsageFromJSONBytes(body)
  55. assert.True(t, ok)
  56. assert.Equal(t, 5000, usage.InputTokens)
  57. assert.Equal(t, 200, usage.OutputTokens)
  58. assert.Equal(t, 150, usage.ImageOutputTokens, "image output tokens from tool_usage.image_gen")
  59. assert.Equal(t, 2800, usage.ImageInputTokens, "image input tokens from tool_usage.image_gen")
  60. }
  61. func TestExtractOpenAIUsageFromJSONBytes_NoToolUsageUnchanged(t *testing.T) {
  62. // Standard response without tool_usage — behavior should be unchanged
  63. body := []byte(`{
  64. "usage": {
  65. "input_tokens": 100,
  66. "output_tokens": 50
  67. }
  68. }`)
  69. usage, ok := extractOpenAIUsageFromJSONBytes(body)
  70. assert.True(t, ok)
  71. assert.Equal(t, 100, usage.InputTokens)
  72. assert.Equal(t, 50, usage.OutputTokens)
  73. assert.Equal(t, 0, usage.ImageOutputTokens, "no image tokens without tool_usage")
  74. assert.Equal(t, 0, usage.ImageInputTokens, "no image tokens without tool_usage")
  75. }
  76. func TestExtractOpenAIUsageFromJSONBytes_BaseUsageHasImageTokensNoOverride(t *testing.T) {
  77. // If base usage already has image tokens (e.g. from output_tokens_details),
  78. // tool_usage should NOT override them.
  79. body := []byte(`{
  80. "usage": {
  81. "input_tokens": 100,
  82. "output_tokens": 50,
  83. "output_tokens_details": {"image_tokens": 30}
  84. },
  85. "tool_usage": {
  86. "image_gen": {
  87. "input_tokens": 200,
  88. "output_tokens": 100,
  89. "output_tokens_details": {"image_tokens": 100, "text_tokens": 0},
  90. "total_tokens": 300
  91. }
  92. }
  93. }`)
  94. usage, ok := extractOpenAIUsageFromJSONBytes(body)
  95. assert.True(t, ok)
  96. assert.Equal(t, 30, usage.ImageOutputTokens, "base usage image tokens preserved, not overridden")
  97. }
  98. func TestMergeHostedImageGenToolUsage_EmptyImageGen(t *testing.T) {
  99. tests := []struct {
  100. name string
  101. json string
  102. }{
  103. {"missing", `{}`},
  104. {"null", `{"image_gen": null}`},
  105. {"not object", `{"image_gen": 42}`},
  106. }
  107. for _, tt := range tests {
  108. t.Run(tt.name, func(t *testing.T) {
  109. usage := OpenAIUsage{InputTokens: 100, OutputTokens: 50}
  110. original := usage
  111. body := []byte(tt.json)
  112. mergeHostedImageGenToolUsage(gjson.GetBytes(body, "image_gen"), &usage)
  113. assert.Equal(t, original, usage, "usage should be unchanged")
  114. })
  115. }
  116. }
  117. func TestParseSSEUsageBytes_ResponseCompletedWithImageGen(t *testing.T) {
  118. svc := &OpenAIGatewayService{}
  119. data := []byte(`{
  120. "type": "response.completed",
  121. "response": {
  122. "usage": {
  123. "input_tokens": 10000,
  124. "output_tokens": 500
  125. },
  126. "tool_usage": {
  127. "image_gen": {
  128. "input_tokens": 4000,
  129. "input_tokens_details": {"image_tokens": 3800, "text_tokens": 200},
  130. "output_tokens": 186,
  131. "output_tokens_details": {"image_tokens": 186, "text_tokens": 0},
  132. "total_tokens": 4186
  133. }
  134. }
  135. }
  136. }`)
  137. usage := &OpenAIUsage{}
  138. svc.parseSSEUsageBytes(data, usage)
  139. assert.Equal(t, 10000, usage.InputTokens)
  140. assert.Equal(t, 500, usage.OutputTokens)
  141. assert.Equal(t, 186, usage.ImageOutputTokens, "image output tokens from SSE tool_usage")
  142. assert.Equal(t, 3800, usage.ImageInputTokens, "image input tokens from SSE tool_usage")
  143. }