responses_stream_event_wire_test.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. package apicompat
  2. import (
  3. "encoding/json"
  4. "testing"
  5. "github.com/stretchr/testify/require"
  6. )
  7. // marshalEvent marshals through the custom MarshalJSON and returns the decoded
  8. // object plus the set of top-level keys.
  9. func marshalEvent(t *testing.T, e ResponsesStreamEvent) map[string]any {
  10. t.Helper()
  11. b, err := json.Marshal(e)
  12. require.NoError(t, err)
  13. var m map[string]any
  14. require.NoError(t, json.Unmarshal(b, &m))
  15. return m
  16. }
  17. // TestWire_IndexFieldsPresentAtZero guards the omitempty trap: output_index/
  18. // content_index/summary_index must serialize even when 0.
  19. func TestWire_IndexFieldsPresentAtZero(t *testing.T) {
  20. m := marshalEvent(t, ResponsesStreamEvent{
  21. Type: "response.output_text.delta", OutputIndex: 0, ContentIndex: 0, ItemID: "msg_1", Delta: "hi",
  22. })
  23. require.Contains(t, m, "output_index")
  24. require.Contains(t, m, "content_index")
  25. require.EqualValues(t, 0, m["output_index"])
  26. r := marshalEvent(t, ResponsesStreamEvent{
  27. Type: "response.reasoning_summary_text.delta", OutputIndex: 0, SummaryIndex: 0, ItemID: "rs_1", Delta: "think",
  28. })
  29. require.Contains(t, r, "output_index")
  30. require.Contains(t, r, "summary_index")
  31. }
  32. // TestWire_FunctionCallItemAlwaysComplete guards that a function_call item
  33. // always carries call_id/name/arguments, including arguments:"" on .added.
  34. func TestWire_FunctionCallItemAlwaysComplete(t *testing.T) {
  35. added := marshalEvent(t, ResponsesStreamEvent{
  36. Type: "response.output_item.added",
  37. OutputIndex: 1,
  38. Item: &ResponsesOutput{Type: "function_call", ID: "fc_1", CallID: "call_a", Name: "exec", Status: "in_progress"},
  39. })
  40. item, ok := added["item"].(map[string]any)
  41. require.True(t, ok, "item must be an object")
  42. for _, k := range []string{"call_id", "name", "arguments"} {
  43. require.Containsf(t, item, k, "function_call item missing %q", k)
  44. }
  45. require.Equal(t, "", item["arguments"])
  46. }
  47. // TestWire_MessageItemContentAlwaysArray guards content:[] presence.
  48. func TestWire_MessageItemContentAlwaysArray(t *testing.T) {
  49. m := marshalEvent(t, ResponsesStreamEvent{
  50. Type: "response.output_item.added",
  51. OutputIndex: 0,
  52. Item: &ResponsesOutput{Type: "message", ID: "msg_1", Role: "assistant", Status: "in_progress"},
  53. })
  54. item, ok := m["item"].(map[string]any)
  55. require.True(t, ok, "item must be an object")
  56. require.Contains(t, item, "content")
  57. _, ok = item["content"].([]any)
  58. require.True(t, ok, "content must be an array")
  59. }
  60. // TestWire_ReasoningItemSummaryAlwaysArray guards summary:[] presence.
  61. func TestWire_ReasoningItemSummaryAlwaysArray(t *testing.T) {
  62. m := marshalEvent(t, ResponsesStreamEvent{
  63. Type: "response.output_item.added",
  64. OutputIndex: 0,
  65. Item: &ResponsesOutput{Type: "reasoning", ID: "rs_1", Status: "in_progress"},
  66. })
  67. item, ok := m["item"].(map[string]any)
  68. require.True(t, ok, "item must be an object")
  69. require.Contains(t, item, "summary")
  70. _, ok = item["summary"].([]any)
  71. require.True(t, ok, "summary must be an array")
  72. }
  73. // TestWire_ContentPartCarriesAnnotationsLogprobs guards the output_text part shape.
  74. func TestWire_ContentPartCarriesAnnotationsLogprobs(t *testing.T) {
  75. m := marshalEvent(t, ResponsesStreamEvent{
  76. Type: "response.content_part.added", OutputIndex: 0, ContentIndex: 0, ItemID: "msg_1",
  77. Part: &ResponsesContentPart{Type: "output_text", Text: ""},
  78. })
  79. part, ok := m["part"].(map[string]any)
  80. require.True(t, ok, "part must be an object")
  81. require.Equal(t, "output_text", part["type"])
  82. require.Contains(t, part, "text")
  83. require.Contains(t, part, "annotations")
  84. require.Contains(t, part, "logprobs")
  85. }
  86. // TestWire_ArgumentsDonePresentEvenEmpty guards arguments presence on done.
  87. func TestWire_ArgumentsDonePresentEvenEmpty(t *testing.T) {
  88. m := marshalEvent(t, ResponsesStreamEvent{
  89. Type: "response.function_call_arguments.done", OutputIndex: 1, ItemID: "fc_1", CallID: "call_a", Name: "exec", Arguments: "",
  90. })
  91. require.Contains(t, m, "arguments")
  92. require.Equal(t, "", m["arguments"])
  93. }
  94. // TestWire_CustomToolCallInputIndexPresentAtZero guards the omitempty trap for
  95. // custom_tool_call_input.delta/done: output_index must serialize even when 0
  96. // (custom tool call as the first output item).
  97. func TestWire_CustomToolCallInputIndexPresentAtZero(t *testing.T) {
  98. d := marshalEvent(t, ResponsesStreamEvent{
  99. Type: "response.custom_tool_call_input.delta", OutputIndex: 0, ItemID: "ct_1", Delta: "dir",
  100. })
  101. require.Contains(t, d, "output_index")
  102. require.EqualValues(t, 0, d["output_index"])
  103. require.Equal(t, "dir", d["delta"])
  104. done := marshalEvent(t, ResponsesStreamEvent{
  105. Type: "response.custom_tool_call_input.done", OutputIndex: 0, ItemID: "ct_1", CallID: "call_1", Name: "exec", Input: "dir",
  106. })
  107. require.Contains(t, done, "output_index")
  108. require.EqualValues(t, 0, done["output_index"])
  109. require.Equal(t, "dir", done["input"])
  110. require.NotContains(t, done, "delta")
  111. }
  112. // TestWire_UnknownEventFallsBackToDefault ensures non-streamed event types keep
  113. // default marshalling (the response object is preserved).
  114. func TestWire_UnknownEventFallsBackToDefault(t *testing.T) {
  115. m := marshalEvent(t, ResponsesStreamEvent{
  116. Type: "response.completed",
  117. Response: &ResponsesResponse{ID: "resp_1", Object: "response", Status: "completed"},
  118. })
  119. require.Contains(t, m, "response")
  120. }
  121. func TestResponsesOutputUnmarshal_ToolSearchObjectArguments(t *testing.T) {
  122. var item ResponsesOutput
  123. require.NoError(t, json.Unmarshal([]byte(`{
  124. "type":"tool_search_call",
  125. "id":"item_1",
  126. "call_id":"call_1",
  127. "execution":"client",
  128. "arguments":{"query":"gmail","limit":2}
  129. }`), &item))
  130. require.Equal(t, "tool_search_call", item.Type)
  131. require.Equal(t, `{"query":"gmail","limit":2}`, item.Arguments)
  132. wire, err := json.Marshal(item)
  133. require.NoError(t, err)
  134. var decoded map[string]any
  135. require.NoError(t, json.Unmarshal(wire, &decoded))
  136. args, ok := decoded["arguments"].(map[string]any)
  137. require.True(t, ok, "tool_search_call arguments must remain an object")
  138. require.Equal(t, "gmail", args["query"])
  139. }
  140. func TestResponsesResponseUnmarshal_ToolSearchObjectArguments(t *testing.T) {
  141. var response ResponsesResponse
  142. require.NoError(t, json.Unmarshal([]byte(`{
  143. "id":"response_1",
  144. "object":"response",
  145. "status":"completed",
  146. "output":[{
  147. "type":"tool_search_call",
  148. "id":"item_1",
  149. "call_id":"call_1",
  150. "arguments":{"query":"gmail"}
  151. }]
  152. }`), &response))
  153. require.Len(t, response.Output, 1)
  154. require.Equal(t, `{"query":"gmail"}`, response.Output[0].Arguments)
  155. }
  156. func TestResponsesStreamEventUnmarshal_ToolSearchObjectArguments(t *testing.T) {
  157. var event ResponsesStreamEvent
  158. require.NoError(t, json.Unmarshal([]byte(`{
  159. "type":"response.output_item.done",
  160. "item":{
  161. "type":"tool_search_call",
  162. "id":"item_1",
  163. "call_id":"call_1",
  164. "arguments":{"query":"gmail"}
  165. }
  166. }`), &event))
  167. require.NotNil(t, event.Item)
  168. require.Equal(t, `{"query":"gmail"}`, event.Item.Arguments)
  169. }