@@ -30,13 +30,17 @@ func TestConvertOpenAIChatCompletionsResponseToOpenAIResponses_ResponseCompleted
3030 request := []byte (`{"model":"gpt-5.4","tool_choice":"auto","parallel_tool_calls":true}` )
3131
3232 tests := []struct {
33- name string
34- in []string
35- doneInputIndex int // Index in tt.in where the terminal [DONE] chunk arrives and response.completed must be emitted.
36- hasUsage bool
37- inputTokens int64
38- outputTokens int64
39- totalTokens int64
33+ name string
34+ in []string
35+ doneInputIndex int // Index in tt.in where the terminal [DONE] chunk arrives and response.completed must be emitted.
36+ hasUsage bool
37+ inputTokens int64
38+ outputTokens int64
39+ totalTokens int64
40+ wantMessageText string
41+ wantFunctionCallID string
42+ wantFunctionCallName string
43+ wantFunctionArguments string
4044 }{
4145 {
4246 // A provider may send finish_reason first and only attach usage in a later chunk (e.g. Vertex AI),
@@ -138,6 +142,85 @@ func TestConvertOpenAIChatCompletionsResponseToOpenAIResponses_ResponseCompleted
138142 }
139143}
140144
145+ func TestConvertOpenAIChatCompletionsResponseToOpenAIResponses_MessageWithoutFinishReasonCompletesOnDone (t * testing.T ) {
146+ t .Parallel ()
147+
148+ request := []byte (`{"model":"gpt-5.4","tool_choice":"auto","parallel_tool_calls":true}` )
149+ in := []string {
150+ `data: {"id":"resp_msg_no_finish","object":"chat.completion.chunk","created":1773896263,"model":"model","choices":[{"index":0,"delta":{"role":"assistant","content":"hello world","reasoning_content":null,"tool_calls":null},"finish_reason":null}]}` ,
151+ `data: [DONE]` ,
152+ }
153+
154+ var param any
155+ completedInputIndex := - 1
156+ var completedData gjson.Result
157+ for i , line := range in {
158+ for _ , chunk := range ConvertOpenAIChatCompletionsResponseToOpenAIResponses (context .Background (), "model" , request , request , []byte (line ), & param ) {
159+ event , data := parseOpenAIResponsesSSEEvent (t , chunk )
160+ if event != "response.completed" {
161+ continue
162+ }
163+ completedInputIndex = i
164+ completedData = data
165+ }
166+ }
167+
168+ if completedInputIndex != 1 {
169+ t .Fatalf ("expected response.completed on terminal [DONE] chunk at input index 1, got %d" , completedInputIndex )
170+ }
171+ if got := completedData .Get ("response.output.0.type" ).String (); got != "message" {
172+ t .Fatalf ("unexpected response.output.0.type: got %q want %q" , got , "message" )
173+ }
174+ if got := completedData .Get ("response.output.0.content.0.text" ).String (); got != "hello world" {
175+ t .Fatalf ("unexpected response.output.0.content.0.text: got %q want %q" , got , "hello world" )
176+ }
177+ }
178+
179+ func TestConvertOpenAIChatCompletionsResponseToOpenAIResponses_FunctionCallWithoutFinishReasonCompletesOnDone (t * testing.T ) {
180+ t .Parallel ()
181+
182+ request := []byte (`{"model":"gpt-5.4","tool_choice":"auto","parallel_tool_calls":true}` )
183+ in := []string {
184+ `data: {"id":"resp_func_no_finish","object":"chat.completion.chunk","created":1773896263,"model":"model","choices":[{"index":0,"delta":{"role":"assistant","content":null,"reasoning_content":null,"tool_calls":[{"index":0,"id":"call_func_no_finish","type":"function","function":{"name":"read","arguments":""}}]},"finish_reason":null}]}` ,
185+ `data: {"id":"resp_func_no_finish","object":"chat.completion.chunk","created":1773896263,"model":"model","choices":[{"index":0,"delta":{"role":null,"content":null,"reasoning_content":null,"tool_calls":[{"index":0,"function":{"arguments":"{\"filePath\":\"C:\\\\repo\\\\README.md\"}"}}]},"finish_reason":null}]}` ,
186+ `data: [DONE]` ,
187+ }
188+
189+ var param any
190+ completedInputIndex := - 1
191+ var completedData gjson.Result
192+ for i , line := range in {
193+ for _ , chunk := range ConvertOpenAIChatCompletionsResponseToOpenAIResponses (context .Background (), "model" , request , request , []byte (line ), & param ) {
194+ event , data := parseOpenAIResponsesSSEEvent (t , chunk )
195+ if event != "response.completed" {
196+ continue
197+ }
198+ completedInputIndex = i
199+ completedData = data
200+ }
201+ }
202+
203+ if completedInputIndex != 2 {
204+ t .Fatalf ("expected response.completed on terminal [DONE] chunk at input index 2, got %d" , completedInputIndex )
205+ }
206+ if got := completedData .Get ("response.output.0.type" ).String (); got != "function_call" {
207+ t .Fatalf ("unexpected response.output.0.type: got %q want %q" , got , "function_call" )
208+ }
209+ if got := completedData .Get ("response.output.0.call_id" ).String (); got != "call_func_no_finish" {
210+ t .Fatalf ("unexpected response.output.0.call_id: got %q want %q" , got , "call_func_no_finish" )
211+ }
212+ if got := completedData .Get ("response.output.0.name" ).String (); got != "read" {
213+ t .Fatalf ("unexpected response.output.0.name: got %q want %q" , got , "read" )
214+ }
215+ args := completedData .Get ("response.output.0.arguments" ).String ()
216+ if ! gjson .Valid (args ) {
217+ t .Fatalf ("expected response.output.0.arguments to be valid JSON, got %q" , args )
218+ }
219+ if got := gjson .Get (args , "filePath" ).String (); got != `C:\repo\README.md` {
220+ t .Fatalf ("unexpected response.output.0.arguments.filePath: got %q want %q" , got , `C:\repo\README.md` )
221+ }
222+ }
223+
141224func TestConvertOpenAIChatCompletionsResponseToOpenAIResponses_MultipleToolCallsRemainSeparate (t * testing.T ) {
142225 in := []string {
143226 `data: {"id":"resp_test","object":"chat.completion.chunk","created":1773896263,"model":"model","choices":[{"index":0,"delta":{"role":"assistant","content":null,"reasoning_content":null,"tool_calls":[{"index":0,"id":"call_read","type":"function","function":{"name":"read","arguments":""}}]},"finish_reason":null}]}` ,
0 commit comments