openai_codex_function_call_id_test.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. //go:build unit
  2. package service
  3. import (
  4. "testing"
  5. "github.com/stretchr/testify/require"
  6. )
  7. // TestFilterCodexInput_StripsFunctionCallItemID_WhenPreservingReferences
  8. // verifies that function_call items with non-fc id (e.g. item_*) have their
  9. // id stripped even when PreserveReferences is true. OpenAI upstream requires
  10. // function_call ids to begin with "fc" and rejects item_* with 400:
  11. // "Expected an ID that begins with 'fc'." (#3785)
  12. func TestFilterCodexInput_StripsFunctionCallItemID_WhenPreservingReferences(t *testing.T) {
  13. input := []any{
  14. map[string]any{
  15. "type": "function_call",
  16. "id": "item_A9v0SNfS3VaLrfX0j3y4xhyK",
  17. "call_id": "fc_abc123",
  18. "name": "bash",
  19. },
  20. map[string]any{
  21. "type": "function_call_output",
  22. "call_id": "fc_abc123",
  23. "output": "done",
  24. },
  25. }
  26. filtered := filterCodexInputWithOptions(input, codexInputFilterOptions{
  27. PreserveReferences: true,
  28. })
  29. require.Len(t, filtered, 2)
  30. fc, ok := filtered[0].(map[string]any)
  31. require.True(t, ok)
  32. require.Equal(t, "function_call", fc["type"])
  33. _, hasID := fc["id"]
  34. require.False(t, hasID, "item_* id should be stripped from function_call")
  35. require.Equal(t, "fc_abc123", fc["call_id"], "call_id must be preserved")
  36. require.Equal(t, "bash", fc["name"])
  37. }
  38. // TestFilterCodexInput_KeepsFcID_WhenPreservingReferences
  39. // verifies that function_call items with a valid fc* id are kept when
  40. // PreserveReferences is true.
  41. func TestFilterCodexInput_KeepsFcID_WhenPreservingReferences(t *testing.T) {
  42. input := []any{
  43. map[string]any{
  44. "type": "function_call",
  45. "id": "fc_validID123",
  46. "call_id": "fc_validID123",
  47. "name": "bash",
  48. },
  49. }
  50. filtered := filterCodexInputWithOptions(input, codexInputFilterOptions{
  51. PreserveReferences: true,
  52. })
  53. require.Len(t, filtered, 1)
  54. fc, ok := filtered[0].(map[string]any)
  55. require.True(t, ok)
  56. require.Equal(t, "fc_validID123", fc["id"], "valid fc* id must be preserved")
  57. }
  58. func TestFilterCodexInput_PreservesNativeCustomAndToolSearchIDs(t *testing.T) {
  59. input := []any{
  60. map[string]any{"type": "custom_tool_call", "id": "ctc_valid", "call_id": "call_custom", "name": "apply_patch"},
  61. map[string]any{"type": "tool_search_call", "id": "tsc_valid", "call_id": "call_search"},
  62. }
  63. filtered := filterCodexInputWithOptions(input, codexInputFilterOptions{PreserveReferences: true})
  64. require.Equal(t, "ctc_valid", filtered[0].(map[string]any)["id"])
  65. require.Equal(t, "tsc_valid", filtered[1].(map[string]any)["id"])
  66. }
  67. func TestFilterCodexInput_StripsWrongCustomAndToolSearchIDs(t *testing.T) {
  68. input := []any{
  69. map[string]any{"type": "custom_tool_call", "id": "fc_wrong", "call_id": "call_custom", "name": "apply_patch"},
  70. map[string]any{"type": "tool_search_call", "id": "fc_wrong", "call_id": "call_search"},
  71. }
  72. filtered := filterCodexInputWithOptions(input, codexInputFilterOptions{PreserveReferences: true})
  73. require.NotContains(t, filtered[0].(map[string]any), "id")
  74. require.NotContains(t, filtered[1].(map[string]any), "id")
  75. }
  76. func TestFilterCodexInput_MapsItemReferencesToNativeToolCallPair(t *testing.T) {
  77. input := []any{
  78. map[string]any{"type": "custom_tool_call", "id": "fc_custom", "call_id": "call_custom", "name": "apply_patch"},
  79. map[string]any{"type": "custom_tool_call_output", "call_id": "fc_custom", "output": "done"},
  80. map[string]any{"type": "item_reference", "id": "call_custom"},
  81. map[string]any{"type": "tool_search_call", "id": "fc_search", "call_id": "call_search"},
  82. map[string]any{"type": "tool_search_output", "call_id": "fc_search", "output": "result"},
  83. map[string]any{"type": "item_reference", "id": "call_search"},
  84. }
  85. filtered := filterCodexInputWithOptions(input, codexInputFilterOptions{PreserveReferences: true})
  86. require.Equal(t, "ctc_custom", filtered[0].(map[string]any)["call_id"])
  87. require.Equal(t, "ctc_custom", filtered[1].(map[string]any)["call_id"])
  88. require.Equal(t, "ctc_custom", filtered[2].(map[string]any)["id"])
  89. require.Equal(t, "tsc_search", filtered[3].(map[string]any)["call_id"])
  90. require.Equal(t, "tsc_search", filtered[4].(map[string]any)["call_id"])
  91. require.Equal(t, "tsc_search", filtered[5].(map[string]any)["id"])
  92. }
  93. func TestFilterCodexInput_PreservesAmbiguousItemReference(t *testing.T) {
  94. input := []any{
  95. map[string]any{"type": "custom_tool_call", "call_id": "call_shared", "name": "apply_patch"},
  96. map[string]any{"type": "tool_search_call", "call_id": "call_shared"},
  97. map[string]any{"type": "item_reference", "id": "call_shared"},
  98. }
  99. filtered := filterCodexInputWithOptions(input, codexInputFilterOptions{PreserveReferences: true})
  100. require.Equal(t, "ctc_shared", filtered[0].(map[string]any)["call_id"])
  101. require.Equal(t, "tsc_shared", filtered[1].(map[string]any)["call_id"])
  102. require.Equal(t, "call_shared", filtered[2].(map[string]any)["id"])
  103. }
  104. func TestFilterCodexInput_PreservesNativeItemIDReferenceIndependentlyFromCallID(t *testing.T) {
  105. input := []any{
  106. map[string]any{"type": "custom_tool_call", "id": "ctc_item", "call_id": "call_custom", "name": "apply_patch"},
  107. map[string]any{"type": "item_reference", "id": "ctc_item"},
  108. }
  109. filtered := filterCodexInputWithOptions(input, codexInputFilterOptions{PreserveReferences: true})
  110. require.Equal(t, "ctc_item", filtered[0].(map[string]any)["id"])
  111. require.Equal(t, "ctc_custom", filtered[0].(map[string]any)["call_id"])
  112. require.Equal(t, "ctc_item", filtered[1].(map[string]any)["id"])
  113. }
  114. func TestFilterCodexInput_ExistingItemIDWinsOverLegacyCallIDMapping(t *testing.T) {
  115. input := []any{
  116. map[string]any{"type": "custom_tool_call", "call_id": "call_shared", "name": "apply_patch"},
  117. map[string]any{"type": "function_call_output", "id": "call_shared", "call_id": "call_other", "output": "done"},
  118. map[string]any{"type": "item_reference", "id": "call_shared"},
  119. }
  120. filtered := filterCodexInputWithOptions(input, codexInputFilterOptions{PreserveReferences: true})
  121. require.Equal(t, "ctc_shared", filtered[0].(map[string]any)["call_id"])
  122. require.Equal(t, "call_shared", filtered[1].(map[string]any)["id"])
  123. require.Equal(t, "call_shared", filtered[2].(map[string]any)["id"])
  124. }
  125. func TestFilterCodexInput_NormalizesCrossTurnLegacyCallReference(t *testing.T) {
  126. input := []any{
  127. map[string]any{"type": "item_reference", "id": "call_previous_turn"},
  128. }
  129. filtered := filterCodexInputWithOptions(input, codexInputFilterOptions{PreserveReferences: true})
  130. require.Equal(t, "fc_previous_turn", filtered[0].(map[string]any)["id"])
  131. }
  132. func TestFilterCodexInput_PreservesNativeRemoteItemReferences(t *testing.T) {
  133. for _, id := range []string{"fc_remote", "ctc_remote", "tsc_remote", "msg_remote", "rs_remote", "vendor_remote"} {
  134. t.Run(id, func(t *testing.T) {
  135. input := []any{map[string]any{"type": "item_reference", "id": id}}
  136. filtered := filterCodexInputWithOptions(input, codexInputFilterOptions{PreserveReferences: true})
  137. require.Equal(t, id, filtered[0].(map[string]any)["id"])
  138. })
  139. }
  140. }
  141. // TestFilterCodexInput_StripsItemIDFromAllToolCallInputTypes verifies that
  142. // item_* ids are stripped from all call-input types (not output types).
  143. func TestFilterCodexInput_StripsItemIDFromAllToolCallInputTypes(t *testing.T) {
  144. types := []string{"function_call", "tool_call", "local_shell_call", "custom_tool_call", "mcp_tool_call"}
  145. for _, typ := range types {
  146. input := []any{
  147. map[string]any{
  148. "type": typ,
  149. "id": "item_xyz",
  150. "call_id": "fc_001",
  151. "name": "tool",
  152. },
  153. }
  154. filtered := filterCodexInputWithOptions(input, codexInputFilterOptions{
  155. PreserveReferences: true,
  156. })
  157. require.Len(t, filtered, 1)
  158. item, ok := filtered[0].(map[string]any)
  159. require.True(t, ok)
  160. _, hasID := item["id"]
  161. require.False(t, hasID, "item_* id should be stripped from %s", typ)
  162. }
  163. }
  164. // TestFilterCodexInput_OutputTypeKeepsItemID ensures tool-output items
  165. // (e.g. function_call_output) keep their id — only call-input types have
  166. // the fc* constraint.
  167. func TestFilterCodexInput_OutputTypeKeepsItemID(t *testing.T) {
  168. input := []any{
  169. map[string]any{
  170. "type": "function_call_output",
  171. "id": "o1",
  172. "call_id": "fc_abc",
  173. "output": "done",
  174. },
  175. }
  176. filtered := filterCodexInputWithOptions(input, codexInputFilterOptions{
  177. PreserveReferences: true,
  178. })
  179. require.Len(t, filtered, 1)
  180. out, ok := filtered[0].(map[string]any)
  181. require.True(t, ok)
  182. require.Equal(t, "o1", out["id"], "output item id should be preserved")
  183. }
  184. // TestFilterCodexInput_NonToolCallItemKeepsID ensures items subject to neither
  185. // the fc* (call-input) nor the msg* (message) prefix rule still keep their id
  186. // when PreserveReferences is true.
  187. // message is covered separately in openai_codex_message_item_id_test.go (#3981).
  188. func TestFilterCodexInput_NonToolCallItemKeepsID(t *testing.T) {
  189. input := []any{
  190. map[string]any{
  191. "type": "web_search_call",
  192. "id": "ws_001",
  193. },
  194. }
  195. filtered := filterCodexInputWithOptions(input, codexInputFilterOptions{
  196. PreserveReferences: true,
  197. })
  198. require.Len(t, filtered, 1)
  199. item, ok := filtered[0].(map[string]any)
  200. require.True(t, ok)
  201. require.Equal(t, "ws_001", item["id"], "unconstrained items keep their id in preserve mode")
  202. }