|
6 | 6 | "encoding/json" |
7 | 7 | "net/http" |
8 | 8 | "net/http/httptest" |
| 9 | + "strings" |
9 | 10 | "testing" |
10 | 11 |
|
11 | 12 | "github.com/gin-gonic/gin" |
@@ -515,6 +516,81 @@ func TestConvertResponsesToChatCompletionResponse(t *testing.T) { |
515 | 516 | } |
516 | 517 | } |
517 | 518 |
|
| 519 | +func TestConvertResponsesToChatCompletionStreamResponseSkipsOutputItemDoneContent(t *testing.T) { |
| 520 | + gin.SetMode(gin.TestMode) |
| 521 | + |
| 522 | + stream := strings.Join([]string{ |
| 523 | + `data: {"type":"response.created","response":{"id":"resp_123","object":"response","created_at":1780731105,"status":"in_progress","model":"gpt-5.1","output":[],"parallel_tool_calls":true,"store":false}}`, |
| 524 | + "", |
| 525 | + `data: {"type":"response.output_item.added","item":{"id":"msg_123","type":"message","role":"assistant","content":[]}}`, |
| 526 | + "", |
| 527 | + `data: {"type":"response.output_text.delta","item_id":"msg_123","output_index":0,"content_index":0,"delta":"Hello! What would you like to discuss or work on?"}`, |
| 528 | + "", |
| 529 | + `data: {"type":"response.output_item.done","item":{"id":"msg_123","type":"message","role":"assistant","content":[{"type":"output_text","text":"Hello! What would you like to discuss or work on?"}]}}`, |
| 530 | + "", |
| 531 | + `data: {"type":"response.completed","response":{"id":"resp_123","object":"response","created_at":1780731105,"status":"completed","model":"gpt-5.1","output":[{"id":"msg_123","type":"message","role":"assistant","content":[{"type":"output_text","text":"Hello! What would you like to discuss or work on?"}]}],"parallel_tool_calls":true,"store":false,"usage":{"input_tokens":7,"output_tokens":22,"total_tokens":29}}}`, |
| 532 | + "", |
| 533 | + `data: [DONE]`, |
| 534 | + "", |
| 535 | + }, "\n") |
| 536 | + |
| 537 | + httpResp := &http.Response{ |
| 538 | + StatusCode: http.StatusOK, |
| 539 | + Body: &mockReadCloser{Reader: bytes.NewReader([]byte(stream))}, |
| 540 | + Header: make(http.Header), |
| 541 | + } |
| 542 | + |
| 543 | + w := httptest.NewRecorder() |
| 544 | + c, _ := gin.CreateTestContext(w) |
| 545 | + c.Request = httptest.NewRequestWithContext( |
| 546 | + t.Context(), |
| 547 | + http.MethodPost, |
| 548 | + "/v1/chat/completions", |
| 549 | + nil, |
| 550 | + ) |
| 551 | + |
| 552 | + m := &meta.Meta{ |
| 553 | + ActualModel: "gpt-5.1", |
| 554 | + } |
| 555 | + |
| 556 | + _, err := openai.ConvertResponsesToChatCompletionStreamResponse(m, c, httpResp) |
| 557 | + require.Nil(t, err) |
| 558 | + |
| 559 | + content := collectChatCompletionStreamContent(t, w.Body.String()) |
| 560 | + assert.Equal(t, "Hello! What would you like to discuss or work on?", content) |
| 561 | + assert.Equal( |
| 562 | + t, |
| 563 | + 1, |
| 564 | + strings.Count(w.Body.String(), "Hello! What would you like to discuss or work on?"), |
| 565 | + ) |
| 566 | +} |
| 567 | + |
| 568 | +func collectChatCompletionStreamContent(t *testing.T, body string) string { |
| 569 | + t.Helper() |
| 570 | + |
| 571 | + var builder strings.Builder |
| 572 | + |
| 573 | + for line := range strings.SplitSeq(body, "\n") { |
| 574 | + line = strings.TrimSpace(line) |
| 575 | + if !strings.HasPrefix(line, "data: ") || line == "data: [DONE]" { |
| 576 | + continue |
| 577 | + } |
| 578 | + |
| 579 | + var chunk relaymodel.ChatCompletionsStreamResponse |
| 580 | + |
| 581 | + err := json.Unmarshal([]byte(strings.TrimPrefix(line, "data: ")), &chunk) |
| 582 | + require.NoError(t, err) |
| 583 | + |
| 584 | + for _, choice := range chunk.Choices { |
| 585 | + if content, ok := choice.Delta.Content.(string); ok { |
| 586 | + builder.WriteString(content) |
| 587 | + } |
| 588 | + } |
| 589 | + } |
| 590 | + |
| 591 | + return builder.String() |
| 592 | +} |
| 593 | + |
518 | 594 | // mockReadCloser is a helper to create a ReadCloser from a Reader |
519 | 595 | type mockReadCloser struct { |
520 | 596 | *bytes.Reader |
|
0 commit comments