chatcompletions_responses_bridge_test.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. package apicompat
  2. import (
  3. "encoding/json"
  4. "testing"
  5. "github.com/stretchr/testify/assert"
  6. "github.com/stretchr/testify/require"
  7. )
  8. func TestResponsesInputToChatMessages_DeveloperRoleMapsToSystem(t *testing.T) {
  9. messages, err := responsesInputToChatMessages("", json.RawMessage(`[{"role":"developer","content":"follow project instructions"}]`))
  10. require.NoError(t, err)
  11. require.Len(t, messages, 1)
  12. assert.Equal(t, "system", messages[0].Role)
  13. assert.JSONEq(t, `"follow project instructions"`, string(messages[0].Content))
  14. }
  15. func TestResponsesInputToChatMessages_SkipsInvalidHistoricalFunctionCall(t *testing.T) {
  16. input := json.RawMessage(`[
  17. {"type":"function_call","call_id":"call_bad","name":"exec_command","arguments":"{\"cmd\": \"ssh root@HOST"},
  18. {"type":"function_call_output","call_id":"call_bad","output":"failed to parse function arguments"},
  19. {"type":"function_call","call_id":"call_ok","name":"exec_command","arguments":"{}"},
  20. {"type":"function_call_output","call_id":"call_ok","output":"ok"},
  21. {"role":"user","content":"continue"}
  22. ]`)
  23. messages, err := responsesInputToChatMessages("", input)
  24. require.NoError(t, err)
  25. require.Len(t, messages, 3)
  26. require.Equal(t, "assistant", messages[0].Role)
  27. require.Len(t, messages[0].ToolCalls, 1)
  28. require.Equal(t, "call_ok", messages[0].ToolCalls[0].ID)
  29. require.Equal(t, "tool", messages[1].Role)
  30. require.Equal(t, "call_ok", messages[1].ToolCallID)
  31. require.Equal(t, "user", messages[2].Role)
  32. }
  33. func TestResponsesInputToChatMessages_SkipsInvalidEmptyCallIDOutput(t *testing.T) {
  34. input := json.RawMessage(`[
  35. {"type":"function_call","call_id":"","name":"exec_command","arguments":"{\"cmd\": \"ssh root@HOST"},
  36. {"type":"function_call_output","call_id":"","output":"failed to parse function arguments"},
  37. {"role":"user","content":"continue"}
  38. ]`)
  39. messages, err := responsesInputToChatMessages("", input)
  40. require.NoError(t, err)
  41. require.Len(t, messages, 1)
  42. require.Equal(t, "user", messages[0].Role)
  43. }
  44. func TestChatCompletionsResponseToResponses_SkipsInvalidFunctionArguments(t *testing.T) {
  45. resp := &ChatCompletionsResponse{
  46. Model: "deepseek-v4-flash",
  47. Choices: []ChatChoice{{
  48. Message: ChatMessage{
  49. Role: "assistant",
  50. ToolCalls: []ChatToolCall{
  51. {ID: "call_bad", Type: "function", Function: ChatFunctionCall{Name: "exec_command", Arguments: `{"cmd": "ssh root@HOST`}},
  52. {ID: "call_ok", Type: "function", Function: ChatFunctionCall{Name: "exec_command", Arguments: `{}`}},
  53. },
  54. },
  55. FinishReason: "length",
  56. }},
  57. }
  58. out := ChatCompletionsResponseToResponses(resp, "deepseek-v4-flash", nil, nil, false, nil)
  59. require.Equal(t, "incomplete", out.Status)
  60. require.Len(t, out.Output, 1)
  61. require.Equal(t, "function_call", out.Output[0].Type)
  62. require.Equal(t, "call_ok", out.Output[0].CallID)
  63. require.Equal(t, `{}`, out.Output[0].Arguments)
  64. }
  65. func TestResponsesInputToChatMessages_KeepsChatCompletionRoles(t *testing.T) {
  66. input := json.RawMessage(`[
  67. {"role":"system","content":"system message"},
  68. {"role":"user","content":"user message"},
  69. {"role":"assistant","content":"assistant message"},
  70. {"role":"tool","content":"tool message"}
  71. ]`)
  72. messages, err := responsesInputToChatMessages("", input)
  73. require.NoError(t, err)
  74. require.Len(t, messages, 4)
  75. assert.Equal(t, []string{"system", "user", "assistant", "tool"}, chatMessageRoles(messages))
  76. }
  77. func TestResponsesInputToChatMessages_EmptyRoleFallsBackToUser(t *testing.T) {
  78. messages, err := responsesInputToChatMessages("", json.RawMessage(`[{"role":"","content":"hello"}]`))
  79. require.NoError(t, err)
  80. require.Len(t, messages, 1)
  81. assert.Equal(t, "user", messages[0].Role)
  82. }
  83. func TestResponsesInputToChatMessages_DeveloperRoleTrimAndCaseInsensitive(t *testing.T) {
  84. input := json.RawMessage(`[
  85. {"role":" Developer ","content":"one"},
  86. {"role":"\tDEVELOPER\n","content":"two"}
  87. ]`)
  88. messages, err := responsesInputToChatMessages("", input)
  89. require.NoError(t, err)
  90. require.Len(t, messages, 2)
  91. assert.Equal(t, []string{"system", "system"}, chatMessageRoles(messages))
  92. }
  93. func TestResponsesToChatCompletionsRequest_InstructionsAndInputDeveloperRole(t *testing.T) {
  94. req := &ResponsesRequest{
  95. Model: "gpt-4o",
  96. Instructions: "Use concise answers.",
  97. Input: json.RawMessage(`[
  98. {"role":"developer","content":[{"type":"input_text","text":"Prefer JSON."}]},
  99. {"role":"user","content":"Hello"}
  100. ]`),
  101. }
  102. out, err := ResponsesToChatCompletionsRequest(req)
  103. require.NoError(t, err)
  104. require.Len(t, out.Messages, 3)
  105. assert.Equal(t, []string{"system", "system", "user"}, chatMessageRoles(out.Messages))
  106. assert.JSONEq(t, `"Use concise answers."`, string(out.Messages[0].Content))
  107. assert.JSONEq(t, `"Prefer JSON."`, string(out.Messages[1].Content))
  108. assert.JSONEq(t, `"Hello"`, string(out.Messages[2].Content))
  109. }
  110. func TestResponsesToChatCompletionsRequest_TextFormatJsonObject(t *testing.T) {
  111. req := &ResponsesRequest{
  112. Model: "gpt-4o",
  113. Input: json.RawMessage(`[
  114. {"role":"user","content":"Return JSON"}
  115. ]`),
  116. Text: &ResponsesText{
  117. Format: json.RawMessage(`{"type":"json_object"}`),
  118. },
  119. }
  120. out, err := ResponsesToChatCompletionsRequest(req)
  121. require.NoError(t, err)
  122. assert.JSONEq(t, `{"type":"json_object"}`, string(out.ResponseFormat))
  123. }
  124. func TestResponsesToChatCompletionsRequest_TextFormatJsonSchema(t *testing.T) {
  125. req := &ResponsesRequest{
  126. Model: "gpt-4o",
  127. Input: json.RawMessage(`[
  128. {"role":"user","content":"Return structured JSON"}
  129. ]`),
  130. Text: &ResponsesText{
  131. Format: json.RawMessage(`{
  132. "type":"json_schema",
  133. "name":"answer",
  134. "schema":{
  135. "type":"object",
  136. "properties":{"ok":{"type":"boolean"}},
  137. "required":["ok"],
  138. "additionalProperties":false
  139. },
  140. "strict":true
  141. }`),
  142. },
  143. }
  144. out, err := ResponsesToChatCompletionsRequest(req)
  145. require.NoError(t, err)
  146. assert.JSONEq(t, `{
  147. "type":"json_schema",
  148. "json_schema":{
  149. "name":"answer",
  150. "schema":{
  151. "type":"object",
  152. "properties":{"ok":{"type":"boolean"}},
  153. "required":["ok"],
  154. "additionalProperties":false
  155. },
  156. "strict":true
  157. }
  158. }`, string(out.ResponseFormat))
  159. }
  160. func TestResponsesToChatCompletionsRequest_ParallelToolCalls(t *testing.T) {
  161. parallel := false
  162. req := &ResponsesRequest{
  163. Model: "gpt-4o",
  164. Input: json.RawMessage(`[
  165. {"role":"user","content":"Use tools"}
  166. ]`),
  167. ParallelToolCalls: &parallel,
  168. }
  169. out, err := ResponsesToChatCompletionsRequest(req)
  170. require.NoError(t, err)
  171. require.NotNil(t, out.ParallelToolCalls)
  172. assert.False(t, *out.ParallelToolCalls)
  173. payload, err := json.Marshal(out)
  174. require.NoError(t, err)
  175. assert.Contains(t, string(payload), `"parallel_tool_calls":false`)
  176. }
  177. func chatMessageRoles(messages []ChatMessage) []string {
  178. roles := make([]string, 0, len(messages))
  179. for _, message := range messages {
  180. roles = append(roles, message.Role)
  181. }
  182. return roles
  183. }