返回 DeepSeek-Reasonix
output_calls_wire_test.go
根目录 / internal / provider / responses / output_calls_wire_test.go
1 package responses
2
3 import (
4 "net/http"
5 "net/http/httptest"
6 "testing"
7
8 "reasonix/internal/provider"
9 )
10
11 func chunksOf(t *testing.T, events ...string) []provider.Chunk {
12 t.Helper()
13 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
14 writeEvents(w, events...)
15 }))
16 defer server.Close()
17 return collect(t, New(Config{Name: "compatible", APIKey: "k", BaseURL: server.URL, Model: "m", Mode: "stateless"}),
18 provider.Request{Messages: []provider.Message{{Role: provider.RoleUser, Content: "list files"}}})
19 }
20
21 func toolCallsOf(t *testing.T, events ...string) []provider.ToolCall {
22 t.Helper()
23 var calls []provider.ToolCall
24 for _, chunk := range chunksOf(t, events...) {
25 if chunk.Type == provider.ChunkToolCall && chunk.ToolCall != nil {
26 calls = append(calls, *chunk.ToolCall)
27 }
28 }
29 return calls
30 }
31
32 func TestCompletedOutputClosesACallWhoseDoneEventsNeverCame(t *testing.T) {
33 calls := toolCallsOf(t,
34 `{"type":"response.output_item.added","item":{"id":"fc_1","type":"function_call","call_id":"call_1","name":"ls","arguments":""}}`,
35 `{"type":"response.function_call_arguments.delta","item_id":"fc_1","delta":"{\"path\":"}`,
36 `{"type":"response.function_call_arguments.delta","item_id":"fc_1","delta":"\".\"}"}`,
37 `{"type":"response.completed","response":{"id":"resp_1","output":[{"id":"fc_1","type":"function_call","status":"completed","call_id":"call_1","name":"ls","arguments":"{\"path\":\".\"}"}],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}`,
38 )
39 if len(calls) != 1 || calls[0].ID != "call_1" || calls[0].Name != "ls" || calls[0].Arguments != `{"path":"."}` {
40 t.Fatalf("calls = %#v, want the one call the completed response lists", calls)
41 }
42 }
43
44 func TestCompletedOutputClosesACallWhoseDeltasCarriedNoItemID(t *testing.T) {
45 calls := toolCallsOf(t,
46 `{"type":"response.output_item.added","item":{"id":"fc_1","type":"function_call","call_id":"call_1","name":"ls","arguments":""}}`,
47 `{"type":"response.function_call_arguments.delta","delta":"{}"}`,
48 `{"type":"response.completed","response":{"id":"resp_1","output":[{"id":"fc_1","type":"function_call","call_id":"call_1","name":"ls","arguments":"{}"}],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}`,
49 )
50 if len(calls) != 1 || calls[0].ID != "call_1" || calls[0].Name != "ls" {
51 t.Fatalf("calls = %#v, want exactly the listed call", calls)
52 }
53 }
54
55 func TestCompletedOutputDoesNotRepeatACallTheStreamClosed(t *testing.T) {
56 calls := toolCallsOf(t,
57 `{"type":"response.output_item.added","item":{"id":"fc_1","type":"function_call","call_id":"call_1","name":"ls","arguments":""}}`,
58 `{"type":"response.output_item.done","item":{"id":"fc_2","type":"function_call","status":"completed","call_id":"call_1","name":"ls","arguments":"{}"}}`,
59 `{"type":"response.completed","response":{"id":"resp_1","output":[{"id":"fc_1","type":"function_call","status":"completed","call_id":"call_1","name":"ls","arguments":"{}"}],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}`,
60 )
61 if len(calls) != 1 || calls[0].ID != "call_1" {
62 t.Fatalf("calls = %#v, want call_1 dispatched once", calls)
63 }
64 }
65
66 func TestIncompleteOutputCallIsNotDispatched(t *testing.T) {
67 calls := toolCallsOf(t,
68 `{"type":"response.output_item.added","item":{"id":"fc_1","type":"function_call","call_id":"call_1","name":"write_file","arguments":""}}`,
69 `{"type":"response.function_call_arguments.delta","item_id":"fc_1","delta":"{\"path\":\"a"}`,
70 `{"type":"response.incomplete","response":{"id":"resp_1","incomplete_details":{"reason":"max_output_tokens"},"output":[{"id":"fc_1","type":"function_call","status":"incomplete","call_id":"call_1","name":"write_file","arguments":"{\"path\":\"a"}],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}`,
71 )
72 if len(calls) != 0 {
73 t.Fatalf("calls = %#v, want a truncated call left undispatched", calls)
74 }
75 }
76
77 func TestCompletedOutputSurvivesAnItemItCannotRead(t *testing.T) {
78 chunks := chunksOf(t,
79 `{"type":"response.output_item.added","item":{"id":"fc_1","type":"function_call","call_id":"call_1","name":"ls","arguments":""}}`,
80 `{"type":"response.completed","response":{"id":"resp_1","output":[{"id":"rs_1","type":"reasoning","status":{"phase":"done"}},{"id":"fc_2","type":"function_call","call_id":"call_2","name":"ls","arguments":{"path":"."}},{"id":"fc_1","type":"function_call","call_id":"call_1","name":"ls","arguments":"{}"}],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}`,
81 )
82 var calls []provider.ToolCall
83 done := false
84 for _, chunk := range chunks {
85 switch chunk.Type {
86 case provider.ChunkError:
87 t.Fatalf("error chunk: %v", chunk.Err)
88 case provider.ChunkDone:
89 done = true
90 case provider.ChunkToolCall:
91 calls = append(calls, *chunk.ToolCall)
92 }
93 }
94 if !done {
95 t.Fatalf("chunks = %#v, want the turn to finish", chunks)
96 }
97 if len(calls) != 1 || calls[0].ID != "call_1" {
98 t.Fatalf("calls = %#v, want only the readable call_1", calls)
99 }
100 }
101
102 func TestOutputCallWithoutCallIDIsNotDispatched(t *testing.T) {
103 calls := toolCallsOf(t,
104 `{"type":"response.completed","response":{"id":"resp_1","output":[{"id":"fc_1","type":"function_call","status":"completed","name":"ls","arguments":"{}"}],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}`,
105 )
106 if len(calls) != 0 {
107 t.Fatalf("calls = %#v, want a call with no call id left undispatched", calls)
108 }
109 }
110
111 func TestOutputListingACallTwiceDispatchesItOnce(t *testing.T) {
112 calls := toolCallsOf(t,
113 `{"type":"response.completed","response":{"id":"resp_1","output":[{"id":"fc_1","type":"function_call","call_id":"call_1","name":"ls","arguments":"{}"},{"id":"fc_2","type":"function_call","call_id":"call_1","name":"ls","arguments":"{}"}],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}`,
114 )
115 if len(calls) != 1 || calls[0].ID != "call_1" {
116 t.Fatalf("calls = %#v, want call_1 dispatched once", calls)
117 }
118 }
119
120 func TestArgumentsDoneWithoutItemIDIsNotDispatched(t *testing.T) {
121 calls := toolCallsOf(t,
122 `{"type":"response.function_call_arguments.done","arguments":"{}"}`,
123 `{"type":"response.completed","response":{"id":"resp_1","usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}`,
124 )
125 if len(calls) != 0 {
126 t.Fatalf("calls = %#v, want no call without a name", calls)
127 }
128 }
129
129 lines GO