fix(responses): include model and usage in translated streams
Ensure response.created and response.completed chunks produced by the OpenAI/Gemini/Claude translators always include required fields (response.model and response.usage) so clients validating Responses SSE do not fail schema validation.
This commit is contained in:
@@ -153,6 +153,7 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
||||
created, _ = sjson.Set(created, "sequence_number", nextSeq())
|
||||
created, _ = sjson.Set(created, "response.id", st.ResponseID)
|
||||
created, _ = sjson.Set(created, "response.created_at", st.Created)
|
||||
created, _ = sjson.Set(created, "response.model", modelName)
|
||||
out = append(out, emitRespEvent("response.created", created))
|
||||
|
||||
inprog := `{"type":"response.in_progress","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"in_progress"}}`
|
||||
@@ -578,19 +579,17 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
||||
if gjson.Get(outputsWrapper, "arr.#").Int() > 0 {
|
||||
completed, _ = sjson.SetRaw(completed, "response.output", gjson.Get(outputsWrapper, "arr").Raw)
|
||||
}
|
||||
if st.UsageSeen {
|
||||
completed, _ = sjson.Set(completed, "response.usage.input_tokens", st.PromptTokens)
|
||||
completed, _ = sjson.Set(completed, "response.usage.input_tokens_details.cached_tokens", st.CachedTokens)
|
||||
completed, _ = sjson.Set(completed, "response.usage.output_tokens", st.CompletionTokens)
|
||||
if st.ReasoningTokens > 0 {
|
||||
completed, _ = sjson.Set(completed, "response.usage.output_tokens_details.reasoning_tokens", st.ReasoningTokens)
|
||||
}
|
||||
total := st.TotalTokens
|
||||
if total == 0 {
|
||||
total = st.PromptTokens + st.CompletionTokens
|
||||
}
|
||||
completed, _ = sjson.Set(completed, "response.usage.total_tokens", total)
|
||||
completed, _ = sjson.Set(completed, "response.usage.input_tokens", st.PromptTokens)
|
||||
completed, _ = sjson.Set(completed, "response.usage.input_tokens_details.cached_tokens", st.CachedTokens)
|
||||
completed, _ = sjson.Set(completed, "response.usage.output_tokens", st.CompletionTokens)
|
||||
if st.ReasoningTokens > 0 {
|
||||
completed, _ = sjson.Set(completed, "response.usage.output_tokens_details.reasoning_tokens", st.ReasoningTokens)
|
||||
}
|
||||
total := st.TotalTokens
|
||||
if total == 0 {
|
||||
total = st.PromptTokens + st.CompletionTokens
|
||||
}
|
||||
completed, _ = sjson.Set(completed, "response.usage.total_tokens", total)
|
||||
out = append(out, emitRespEvent("response.completed", completed))
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package responses
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func parseSSEEvent(t *testing.T, chunk string) (string, gjson.Result) {
|
||||
t.Helper()
|
||||
|
||||
lines := strings.Split(chunk, "\n")
|
||||
if len(lines) < 2 {
|
||||
t.Fatalf("unexpected SSE chunk: %q", chunk)
|
||||
}
|
||||
|
||||
event := strings.TrimSpace(strings.TrimPrefix(lines[0], "event:"))
|
||||
dataLine := strings.TrimSpace(strings.TrimPrefix(lines[1], "data:"))
|
||||
if !gjson.Valid(dataLine) {
|
||||
t.Fatalf("invalid SSE data JSON: %q", dataLine)
|
||||
}
|
||||
return event, gjson.Parse(dataLine)
|
||||
}
|
||||
|
||||
func TestConvertOpenAIChatCompletionsResponseToOpenAIResponses_CreatedHasModelAndCompletedHasUsage(t *testing.T) {
|
||||
in := `data: {"id":"chatcmpl-1","object":"chat.completion.chunk","created":1700000000,"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}`
|
||||
|
||||
var param any
|
||||
out := ConvertOpenAIChatCompletionsResponseToOpenAIResponses(context.Background(), "test-model", nil, nil, []byte(in), ¶m)
|
||||
|
||||
gotCreated := false
|
||||
gotCompleted := false
|
||||
createdModel := ""
|
||||
for _, chunk := range out {
|
||||
ev, data := parseSSEEvent(t, chunk)
|
||||
switch ev {
|
||||
case "response.created":
|
||||
gotCreated = true
|
||||
createdModel = data.Get("response.model").String()
|
||||
case "response.completed":
|
||||
gotCompleted = true
|
||||
if !data.Get("response.usage.input_tokens").Exists() {
|
||||
t.Fatalf("response.completed missing usage.input_tokens: %s", data.Raw)
|
||||
}
|
||||
if !data.Get("response.usage.output_tokens").Exists() {
|
||||
t.Fatalf("response.completed missing usage.output_tokens: %s", data.Raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !gotCreated {
|
||||
t.Fatalf("missing response.created event")
|
||||
}
|
||||
if createdModel != "test-model" {
|
||||
t.Fatalf("unexpected response.created model: got %q", createdModel)
|
||||
}
|
||||
if !gotCompleted {
|
||||
t.Fatalf("missing response.completed event")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user