| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- package service
- import (
- "testing"
- )
- func TestOpenAIImageOutputCounter_TextOnlyMessage(t *testing.T) {
- // Simulate a text-only response from /v1/responses
- // The response.output_item.done event for a text message
- sseBody := `data: {"type":"response.created","response":{"id":"resp_123"}}
- data: {"type":"response.in_progress","response":{"id":"resp_123"}}
- data: {"type":"response.output_item.added","item":{"id":"item_1","type":"message","role":"assistant","status":"in_progress"}}
- data: {"type":"response.output_text.delta","item_id":"item_1","output_index":0,"content_index":0,"delta":"Hello"}
- data: {"type":"response.output_text.done","item_id":"item_1","output_index":0,"content_index":0,"text":"Hello"}
- data: {"type":"response.output_item.done","item":{"id":"item_1","type":"message","role":"assistant","status":"completed","content":[{"type":"output_text","text":"Hello"}]}}
- data: {"type":"response.completed","response":{"id":"resp_123","output":[{"id":"item_1","type":"message","role":"assistant","status":"completed","content":[{"type":"output_text","text":"Hello"}]}],"usage":{"input_tokens":10,"output_tokens":5}}}
- data: [DONE]`
- count := countOpenAIImageOutputsFromSSEBody(sseBody)
- if count != 0 {
- t.Errorf("expected 0 images for text-only message, got %d", count)
- }
- }
- func TestOpenAIImageOutputCounter_NestedSub2API_StandardResponse(t *testing.T) {
- // Simulate what a nested sub2api setup receives
- // When the upstream sub2api returns a standard response, the downstream
- // sub2api receives SSE events. This test verifies that no false positives
- // occur.
- sseBody := `data: {"type":"response.created","response":{"id":"resp_nested_1","object":"response","status":"in_progress"}}
- data: {"type":"response.in_progress","response":{"id":"resp_nested_1","object":"response","status":"in_progress"}}
- data: {"type":"response.output_item.added","item":{"id":"item_nested_1","type":"message","role":"assistant","status":"in_progress"}}
- data: {"type":"response.output_text.delta","item_id":"item_nested_1","output_index":0,"content_index":0,"delta":"你好"}
- data: {"type":"response.output_text.done","item_id":"item_nested_1","output_index":0,"content_index":0,"text":"你好!有什么我可以帮助你的吗?"}
- data: {"type":"response.output_item.done","item":{"id":"item_nested_1","type":"message","role":"assistant","status":"completed","content":[{"type":"output_text","text":"你好!有什么我可以帮助你的吗?"}]}}
- data: {"type":"response.completed","response":{"id":"resp_nested_1","object":"response","status":"completed","output":[{"id":"item_nested_1","type":"message","role":"assistant","status":"completed","content":[{"type":"output_text","text":"你好!有什么我可以帮助你的吗?"}]}],"usage":{"input_tokens":15,"output_tokens":12}}}
- data: [DONE]`
- count := countOpenAIImageOutputsFromSSEBody(sseBody)
- if count != 0 {
- t.Errorf("expected 0 images for nested sub2api text-only response, got %d", count)
- }
- }
- func TestOpenAIImageOutputCounter_JSONResponse_TextOnly(t *testing.T) {
- // Simulate JSON response (non-streaming) for text-only message
- jsonBody := `{
- "id": "resp_json_1",
- "object": "response",
- "status": "completed",
- "output": [
- {
- "id": "item_json_1",
- "type": "message",
- "role": "assistant",
- "status": "completed",
- "content": [
- {
- "type": "output_text",
- "text": "Hello!"
- }
- ]
- }
- ],
- "usage": {
- "input_tokens": 10,
- "output_tokens": 5
- }
- }`
- count := countOpenAIResponseImageOutputsFromJSONBytes([]byte(jsonBody))
- if count != 0 {
- t.Errorf("expected 0 images for text-only JSON response, got %d", count)
- }
- }
- func TestOpenAIImageOutputCounter_DataArray_FalsePositive(t *testing.T) {
- // Test: SSE events in /v1/responses should NOT have a "data" array field,
- // but if one is present with actual image URLs, it should be counted.
- // The bug was that addDataArray counted ALL array elements without checking
- // if they contained actual image output (url/b64_json).
- //
- // Test 1: data array with non-image objects should NOT count
- sseWithNonImageData := `data: {"type":"response.completed","response":{"id":"resp_1","output":[{"id":"item_1","type":"message","role":"assistant","status":"completed","content":[{"type":"output_text","text":"Hello"}]}]},"data":[{"id":"not_an_image","status":"done"}]}
- data: [DONE]`
- count := countOpenAIImageOutputsFromSSEBody(sseWithNonImageData)
- if count != 0 {
- t.Errorf("expected 0 images for data array without image output, got %d", count)
- }
- // Test 2: data array with actual image output should still count correctly
- sseWithImageData := `data: {"type":"response.completed","response":{"id":"resp_1","output":[]},"data":[{"url":"https://example.com/img.png"}]}
- data: [DONE]`
- count2 := countOpenAIImageOutputsFromSSEBody(sseWithImageData)
- if count2 != 1 {
- t.Errorf("expected 1 image for data array with image URL, got %d", count2)
- }
- }
- func TestOpenAIImageOutputCounter_JSONResponse_DataArray_FalsePositive(t *testing.T) {
- // Test: JSON responses in /v1/responses should NOT have a "data" array field.
- // But if one is present with non-image objects, it should NOT cause false counting.
- jsonWithNonImageData := `{
- "id": "resp_1",
- "object": "response",
- "status": "completed",
- "output": [
- {
- "id": "item_1",
- "type": "message",
- "role": "assistant",
- "status": "completed",
- "content": [{"type": "output_text", "text": "Hello"}]
- }
- ],
- "data": [
- {"id": "not_an_image", "status": "done"}
- ],
- "usage": {"input_tokens": 10, "output_tokens": 5}
- }`
- count := countOpenAIResponseImageOutputsFromJSONBytes([]byte(jsonWithNonImageData))
- if count != 0 {
- t.Errorf("expected 0 images for data array without image output, got %d", count)
- }
- // Test: data array with actual image output should still count correctly
- jsonWithImageData := `{
- "id": "resp_1",
- "object": "response",
- "output": [],
- "data": [
- {"url": "https://example.com/img.png"}
- ]
- }`
- count2 := countOpenAIResponseImageOutputsFromJSONBytes([]byte(jsonWithImageData))
- if count2 != 1 {
- t.Errorf("expected 1 image for data array with image URL, got %d", count2)
- }
- }
- func TestOpenAIImageOutputCounter_PassthroughSSEToJSON_Conversion(t *testing.T) {
- // Simulate the scenario in handlePassthroughSSEToJSON where extractCodexFinalResponse
- // successfully extracts the response JSON, then countOpenAIResponseImageOutputsFromJSONBytes
- // is called on the extracted response object.
- //
- // The extracted response is just the "response" field from the "response.done" event.
- // This should NOT have a "data" array.
- extractedResponse := `{
- "id": "resp_extracted_1",
- "object": "response",
- "status": "completed",
- "output": [
- {
- "id": "item_ext_1",
- "type": "message",
- "role": "assistant",
- "status": "completed",
- "content": [{"type": "output_text", "text": "Hello"}]
- }
- ],
- "usage": {"input_tokens": 10, "output_tokens": 5}
- }`
- count := countOpenAIResponseImageOutputsFromJSONBytes([]byte(extractedResponse))
- if count != 0 {
- t.Errorf("expected 0 images for extracted response JSON, got %d", count)
- }
- }
- func TestOpenAIImageOutputCounter_AddDataArray_FromVariousSources(t *testing.T) {
- // Test various JSON payloads that might trigger addDataArray
- tests := []struct {
- name string
- json string
- expected int
- }{
- {
- name: "standard response.done event",
- json: `{"type":"response.done","response":{"id":"r1","output":[{"type":"message","id":"m1","content":[{"type":"output_text","text":"hi"}]}]}}`,
- },
- {
- name: "response with null data field",
- json: `{"type":"response.done","response":{"id":"r1","output":[]},"data":null}`,
- },
- {
- name: "response with empty data array",
- json: `{"type":"response.done","response":{"id":"r1","output":[]},"data":[]}`,
- },
- {
- name: "response with single-element data array",
- json: `{"type":"response.done","response":{"id":"r1","output":[]},"data":[{"url":"https://example.com/img.png"}]}`,
- },
- {
- name: "image_generation.completed event without result",
- json: `{"type":"image_generation.completed","item":{"type":"image_generation.completed","id":"call_1"}}`,
- },
- {
- name: "output_item with empty type",
- json: `{"type":"response.output_item.done","item":{"id":"x1","status":"completed"}}`,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- counter := newOpenAIImageOutputCounter()
- counter.AddSSEData([]byte(tt.json))
- count := counter.Count()
- t.Logf(" count=%d (maxDataCount=%d, count=%d)", count, counter.maxDataCount, counter.count)
- })
- }
- }
- func TestOpenAIImageOutputCounter_AddJSONResponse_Exported(t *testing.T) {
- tests := []struct {
- name string
- json string
- }{
- {
- name: "standard /v1/responses JSON response",
- json: `{"id":"r1","object":"response","output":[{"type":"message","id":"m1","content":[{"type":"output_text","text":"hi"}]}],"usage":{"input_tokens":10,"output_tokens":5}}`,
- },
- {
- name: "response with data field (like /v1/images/generations)",
- json: `{"id":"r1","object":"response","output":[{"type":"message","id":"m1","content":[{"type":"output_text","text":"hi"}]}],"data":[{"url":"https://example.com/img.png"}],"usage":{"input_tokens":10,"output_tokens":5}}`,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- count := countOpenAIResponseImageOutputsFromJSONBytes([]byte(tt.json))
- t.Logf(" count=%d", count)
- })
- }
- }
|