diff --git a/internal/apischema/openai/openai.go b/internal/apischema/openai/openai.go index 9b0242295a..cdfd9a2c81 100644 --- a/internal/apischema/openai/openai.go +++ b/internal/apischema/openai/openai.go @@ -1477,7 +1477,7 @@ type ChatCompletionResponseChunk struct { ID string `json:"id,omitempty"` // Choices are described in the OpenAI API documentation: // https://platform.openai.com/docs/api-reference/chat/streaming#chat/streaming-choices - Choices []ChatCompletionResponseChunkChoice `json:"choices,omitempty"` + Choices []ChatCompletionResponseChunkChoice `json:"choices"` // Created is the Unix timestamp (in seconds) of when the chat completion was created. Created JSONUNIXTime `json:"created,omitzero"` diff --git a/internal/apischema/openai/openai_test.go b/internal/apischema/openai/openai_test.go index ad4006fd36..63ed3cbd40 100644 --- a/internal/apischema/openai/openai_test.go +++ b/internal/apischema/openai/openai_test.go @@ -1510,6 +1510,22 @@ func TestChatCompletionResponseChunk(t *testing.T) { }, expected: `{"id":"chatcmpl-456","object":"chat.completion.chunk","created":1755137934,"model":"gpt-5-nano","choices":[{"index":0,"delta":{"content":"World"}}]}`, }, + { + name: "usage-only chunk with empty choices must serialize choices as empty array", + chunk: ChatCompletionResponseChunk{ + ID: "chatcmpl-789", + Object: "chat.completion.chunk", + Created: JSONUNIXTime(time.Unix(1755137935, 0)), + Model: "gpt-5-nano", + Choices: []ChatCompletionResponseChunkChoice{}, + Usage: &Usage{ + PromptTokens: 20, + CompletionTokens: 10, + TotalTokens: 30, + }, + }, + expected: `{"id":"chatcmpl-789","object":"chat.completion.chunk","created":1755137935,"model":"gpt-5-nano","choices":[],"usage":{"prompt_tokens":20,"completion_tokens":10,"total_tokens":30}}`, + }, } for _, tc := range testCases { diff --git a/internal/translator/openai_awsbedrock.go b/internal/translator/openai_awsbedrock.go index 90975ea1de..f284af7567 100644 --- a/internal/translator/openai_awsbedrock.go +++ b/internal/translator/openai_awsbedrock.go @@ -860,6 +860,7 @@ func (o *openAIToAWSBedrockTranslatorV1ChatCompletion) convertEvent(event *awsbe chunk := &openai.ChatCompletionResponseChunk{ Object: object, Model: o.requestModel, ID: o.responseID, Created: openai.JSONUNIXTime(time.Now()), + Choices: []openai.ChatCompletionResponseChunkChoice{}, } switch event.EventType { diff --git a/internal/translator/openai_awsbedrock_test.go b/internal/translator/openai_awsbedrock_test.go index 134190f09a..af5fb8742c 100644 --- a/internal/translator/openai_awsbedrock_test.go +++ b/internal/translator/openai_awsbedrock_test.go @@ -1383,7 +1383,7 @@ data: {"id":"123","choices":[{"index":0,"delta":{"role":"assistant","tool_calls" data: {"id":"123","choices":[{"index":0,"delta":{"content":"","role":"assistant"},"finish_reason":"tool_calls"}],"created":1731679200,"model":"claude-sonnet-4","object":"chat.completion.chunk"} -data: {"id":"123","created":1731679200,"model":"claude-sonnet-4","service_tier":"default","object":"chat.completion.chunk","usage":{"prompt_tokens":386,"completion_tokens":75,"total_tokens":461}} +data: {"id":"123","choices":[],"created":1731679200,"model":"claude-sonnet-4","service_tier":"default","object":"chat.completion.chunk","usage":{"prompt_tokens":386,"completion_tokens":75,"total_tokens":461}} data: [DONE] `, string(normalizedResults)) @@ -1958,6 +1958,7 @@ func TestOpenAIToAWSBedrockTranslator_convertEvent(t *testing.T) { Model: "claude-sonnet-4", Created: openai.JSONUNIXTime(time.Unix(releaseDateUnix, 0)), // 0 nanoseconds Object: "chat.completion.chunk", + Choices: []openai.ChatCompletionResponseChunkChoice{}, Usage: &openai.Usage{ TotalTokens: 35, PromptTokens: 15, diff --git a/internal/translator/openai_gcpvertexai_test.go b/internal/translator/openai_gcpvertexai_test.go index d90b113b6a..b70f5f9c6e 100644 --- a/internal/translator/openai_gcpvertexai_test.go +++ b/internal/translator/openai_gcpvertexai_test.go @@ -1021,7 +1021,7 @@ func TestOpenAIToGCPVertexAITranslatorV1ChatCompletion_ResponseBody(t *testing.T wantHeaderMut: nil, wantBodyMut: []byte(`data: {"choices":[{"index":0,"delta":{"content":"Hello","role":"assistant"}}],"object":"chat.completion.chunk"} -data: {"object":"chat.completion.chunk","usage":{"prompt_tokens":5,"completion_tokens":3,"total_tokens":8,"completion_tokens_details":{},"prompt_tokens_details":{}}} +data: {"choices":[],"object":"chat.completion.chunk","usage":{"prompt_tokens":5,"completion_tokens":3,"total_tokens":8,"completion_tokens_details":{},"prompt_tokens_details":{}}} data: [DONE] `), @@ -1232,7 +1232,7 @@ data: {"candidates":[{"content":{"parts":[{"text":"Hello"}]}}],"usageMetadata":{ data: {"choices":[{"index":0,"delta":{"content":"Hello","role":"assistant"}}],"object":"chat.completion.chunk"} -data: {"object":"chat.completion.chunk","usage":{"prompt_tokens":5,"completion_tokens":3,"total_tokens":8,"completion_tokens_details":{},"prompt_tokens_details":{}}} +data: {"choices":[],"object":"chat.completion.chunk","usage":{"prompt_tokens":5,"completion_tokens":3,"total_tokens":8,"completion_tokens_details":{},"prompt_tokens_details":{}}} data: [DONE] `), @@ -1254,7 +1254,7 @@ data: {"candidates":[{"content":{"parts":[{"text":"The answer is 42.", "thoughtS data: {"choices":[{"index":0,"delta":{"content":"The answer is 42.","role":"assistant","reasoning_content":{"signature":"dGVzdHNpZ25hdHVyZQ=="}}}],"object":"chat.completion.chunk"} -data: {"object":"chat.completion.chunk","usage":{"prompt_tokens":10,"completion_tokens":8,"total_tokens":18,"completion_tokens_details":{},"prompt_tokens_details":{}}} +data: {"choices":[],"object":"chat.completion.chunk","usage":{"prompt_tokens":10,"completion_tokens":8,"total_tokens":18,"completion_tokens_details":{},"prompt_tokens_details":{}}} data: [DONE] `), diff --git a/tests/data-plane/testupstream_test.go b/tests/data-plane/testupstream_test.go index cad68cf793..99981bbbce 100644 --- a/tests/data-plane/testupstream_test.go +++ b/tests/data-plane/testupstream_test.go @@ -423,7 +423,7 @@ data: {"id":"2bc5b090-a26c-4007-9467-ce5adc4ffa1d","choices":[{"index":0,"delta" data: {"id":"2bc5b090-a26c-4007-9467-ce5adc4ffa1d","choices":[{"index":0,"delta":{"content":"","role":"assistant"},"finish_reason":"tool_calls"}],"created":123,"model":"something","object":"chat.completion.chunk"} -data: {"id":"2bc5b090-a26c-4007-9467-ce5adc4ffa1d","created":123,"model":"something","object":"chat.completion.chunk","usage":{"prompt_tokens":41,"completion_tokens":36,"total_tokens":77}} +data: {"id":"2bc5b090-a26c-4007-9467-ce5adc4ffa1d","choices":[],"created":123,"model":"something","object":"chat.completion.chunk","usage":{"prompt_tokens":41,"completion_tokens":36,"total_tokens":77}} data: [DONE] `, @@ -607,7 +607,7 @@ data: {"id":"msg_123","choices":[{"index":0,"delta":{"content":" today","role":" data: {"id":"msg_123","choices":[{"index":0,"delta":{"content":"?","role":"assistant"},"finish_reason":"stop"}],"created":123,"model":"gemini-1.5-pro","object":"chat.completion.chunk"} -data: {"id":"msg_123","created":123,"model":"gemini-1.5-pro","object":"chat.completion.chunk","usage":{"prompt_tokens":10,"completion_tokens":7,"total_tokens":17,"completion_tokens_details":{},"prompt_tokens_details":{}}} +data: {"id":"msg_123","choices":[],"created":123,"model":"gemini-1.5-pro","object":"chat.completion.chunk","usage":{"prompt_tokens":10,"completion_tokens":7,"total_tokens":17,"completion_tokens_details":{},"prompt_tokens_details":{}}} data: [DONE] `, @@ -651,7 +651,7 @@ data: {"id":"msg_123","choices":[{"index":0,"delta":{"content":" due to Rayleigh data: {"id":"msg_123","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"created":123,"model":"claude-3-sonnet","object":"chat.completion.chunk"} -data: {"id":"msg_123","created":123,"model":"claude-3-sonnet","object":"chat.completion.chunk","usage":{"prompt_tokens":25,"completion_tokens":12,"total_tokens":37,"prompt_tokens_details":{"cached_tokens":10}}} +data: {"id":"msg_123","choices":[],"created":123,"model":"claude-3-sonnet","object":"chat.completion.chunk","usage":{"prompt_tokens":25,"completion_tokens":12,"total_tokens":37,"prompt_tokens_details":{"cached_tokens":10}}} data: [DONE] @@ -715,7 +715,7 @@ data: {"id":"msg_123","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"i data: {"id":"msg_123","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],"created":123,"model":"claude-3-sonnet","object":"chat.completion.chunk"} -data: {"id":"msg_123","created":123,"model":"claude-3-sonnet","object":"chat.completion.chunk","usage":{"prompt_tokens":50,"completion_tokens":20,"total_tokens":70,"prompt_tokens_details":{}}} +data: {"id":"msg_123","choices":[],"created":123,"model":"claude-3-sonnet","object":"chat.completion.chunk","usage":{"prompt_tokens":50,"completion_tokens":20,"total_tokens":70,"prompt_tokens_details":{}}} data: [DONE]