Skip to content

Commit 143663f

Browse files
committed
Add regression tests for SSE comment lines from OpenRouter (#2349)
The OpenAI Go SDK (v3.30.0) already handles SSE comment lines correctly, so no code fix is needed. Add regression tests for both ChatCompletions and Responses API paths to ensure SSE comments (e.g. ': OPENROUTER PROCESSING') don't cause 'unexpected end of JSON input' errors. Closes #2349 Assisted-By: docker-agent
1 parent 5b0355b commit 143663f

1 file changed

Lines changed: 194 additions & 0 deletions

File tree

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package openai
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
13+
"github.com/docker/docker-agent/pkg/chat"
14+
"github.com/docker/docker-agent/pkg/config/latest"
15+
"github.com/docker/docker-agent/pkg/environment"
16+
)
17+
18+
// writeSSEWithComments writes an SSE response prefixed with comment lines
19+
// (starting with ':'), as sent by providers like OpenRouter during initial
20+
// processing. Per the SSE spec, comment lines must be ignored by clients.
21+
// This is used to verify the fix for https://github.com/docker/docker-agent/issues/2349.
22+
func writeSSEWithComments(w http.ResponseWriter, sseLines []string) {
23+
w.Header().Set("Content-Type", "text/event-stream")
24+
flusher, _ := w.(http.Flusher)
25+
26+
// Comment lines like OpenRouter sends during processing
27+
_, _ = fmt.Fprint(w, ": OPENROUTER PROCESSING\n")
28+
_, _ = fmt.Fprint(w, ": OPENROUTER PROCESSING\n")
29+
30+
for _, line := range sseLines {
31+
_, _ = fmt.Fprint(w, line+"\n")
32+
}
33+
flusher.Flush()
34+
}
35+
36+
// TestCustomProvider_SSECommentLines_ChatCompletions is a regression test for
37+
// https://github.com/docker/docker-agent/issues/2349
38+
//
39+
// OpenRouter sends SSE comment lines (": OPENROUTER PROCESSING") before the
40+
// actual data events. This test verifies those comments don't cause
41+
// "unexpected end of JSON input" errors during streaming.
42+
func TestCustomProvider_SSECommentLines_ChatCompletions(t *testing.T) {
43+
t.Parallel()
44+
45+
chunks := []map[string]any{
46+
{
47+
"id": "gen-123", "object": "chat.completion.chunk", "model": "test",
48+
"choices": []map[string]any{{"index": 0, "delta": map[string]any{"role": "assistant", "content": ""}, "finish_reason": nil}},
49+
},
50+
{
51+
"id": "gen-123", "object": "chat.completion.chunk", "model": "test",
52+
"choices": []map[string]any{{"index": 0, "delta": map[string]any{"content": "hello"}, "finish_reason": nil}},
53+
},
54+
{
55+
"id": "gen-123", "object": "chat.completion.chunk", "model": "test",
56+
"choices": []map[string]any{{"index": 0, "delta": map[string]any{}, "finish_reason": "stop"}},
57+
},
58+
{
59+
"id": "gen-123", "object": "chat.completion.chunk", "model": "test",
60+
"choices": []map[string]any{}, "usage": map[string]any{"prompt_tokens": 10, "completion_tokens": 1, "total_tokens": 11},
61+
},
62+
}
63+
64+
var sseLines []string
65+
for _, chunk := range chunks {
66+
data, _ := json.Marshal(chunk)
67+
sseLines = append(sseLines, "data: "+string(data), "")
68+
}
69+
sseLines = append(sseLines, "data: [DONE]", "")
70+
71+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
72+
writeSSEWithComments(w, sseLines)
73+
}))
74+
defer server.Close()
75+
76+
cfg := &latest.ModelConfig{
77+
Provider: "openrouter",
78+
Model: "test-model",
79+
BaseURL: server.URL,
80+
TokenKey: "OPENROUTER_API_KEY",
81+
ProviderOpts: map[string]any{
82+
"api_type": "openai_chatcompletions",
83+
},
84+
}
85+
env := environment.NewMapEnvProvider(map[string]string{
86+
"OPENROUTER_API_KEY": "test-key",
87+
})
88+
89+
client, err := NewClient(t.Context(), cfg, env)
90+
require.NoError(t, err)
91+
92+
stream, err := client.CreateChatCompletionStream(
93+
t.Context(),
94+
[]chat.Message{{Role: chat.MessageRoleUser, Content: "hello"}},
95+
nil,
96+
)
97+
require.NoError(t, err)
98+
defer stream.Close()
99+
100+
var content string
101+
for {
102+
chunk, err := stream.Recv()
103+
if err != nil {
104+
break
105+
}
106+
for _, choice := range chunk.Choices {
107+
content += choice.Delta.Content
108+
}
109+
}
110+
111+
assert.Equal(t, "hello", content)
112+
}
113+
114+
// TestCustomProvider_SSECommentLines_Responses is a regression test for
115+
// https://github.com/docker/docker-agent/issues/2349 using the Responses API
116+
// path (api_type: openai_responses), which is exactly what the issue reporter
117+
// was using with OpenRouter.
118+
func TestCustomProvider_SSECommentLines_Responses(t *testing.T) {
119+
t.Parallel()
120+
121+
events := []map[string]any{
122+
{"type": "response.output_text.delta", "delta": "hello", "item_id": "item-1"},
123+
{
124+
"type": "response.completed",
125+
"response": map[string]any{
126+
"id": "resp-123",
127+
"status": "completed",
128+
"output": []map[string]any{
129+
{"type": "message", "id": "item-1"},
130+
},
131+
"usage": map[string]any{
132+
"input_tokens": 10,
133+
"output_tokens": 1,
134+
"total_tokens": 11,
135+
"input_tokens_details": map[string]any{
136+
"cached_tokens": 0,
137+
},
138+
"output_tokens_details": map[string]any{
139+
"reasoning_tokens": 0,
140+
},
141+
},
142+
},
143+
},
144+
}
145+
146+
var sseLines []string
147+
for _, event := range events {
148+
data, _ := json.Marshal(event)
149+
eventType := event["type"].(string)
150+
sseLines = append(sseLines, "event: "+eventType, "data: "+string(data), "")
151+
}
152+
153+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
154+
writeSSEWithComments(w, sseLines)
155+
}))
156+
defer server.Close()
157+
158+
cfg := &latest.ModelConfig{
159+
Provider: "openrouter",
160+
Model: "test-model",
161+
BaseURL: server.URL,
162+
TokenKey: "OPENROUTER_API_KEY",
163+
ProviderOpts: map[string]any{
164+
"api_type": "openai_responses",
165+
},
166+
}
167+
env := environment.NewMapEnvProvider(map[string]string{
168+
"OPENROUTER_API_KEY": "test-key",
169+
})
170+
171+
client, err := NewClient(t.Context(), cfg, env)
172+
require.NoError(t, err)
173+
174+
stream, err := client.CreateChatCompletionStream(
175+
t.Context(),
176+
[]chat.Message{{Role: chat.MessageRoleUser, Content: "hello"}},
177+
nil,
178+
)
179+
require.NoError(t, err)
180+
defer stream.Close()
181+
182+
var content string
183+
for {
184+
chunk, err := stream.Recv()
185+
if err != nil {
186+
break
187+
}
188+
for _, choice := range chunk.Choices {
189+
content += choice.Delta.Content
190+
}
191+
}
192+
193+
assert.Equal(t, "hello", content)
194+
}

0 commit comments

Comments
 (0)