openai_responses_item_id_test.go 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. package service
  2. import (
  3. "strconv"
  4. "testing"
  5. "github.com/stretchr/testify/require"
  6. "github.com/tidwall/gjson"
  7. )
  8. func TestOpenAIResponsesInputItemIDPrefixUsesObservedOutputContracts(t *testing.T) {
  9. tests := []struct {
  10. itemType string
  11. id string
  12. strip bool
  13. }{
  14. {itemType: "message", id: "msg_123", strip: false},
  15. {itemType: "message", id: "item_123", strip: true},
  16. {itemType: "reasoning", id: "rs_123", strip: false},
  17. {itemType: "reasoning", id: "item_123", strip: true},
  18. {itemType: "function_call", id: "fc_123", strip: false},
  19. {itemType: "function_call", id: "call_123", strip: true},
  20. {itemType: "tool_call", id: "fc_123", strip: false},
  21. {itemType: "local_shell_call", id: "fc_123", strip: false},
  22. {itemType: "mcp_tool_call", id: "fc_123", strip: false},
  23. {itemType: "custom_tool_call", id: "ctc_123", strip: false},
  24. {itemType: "custom_tool_call", id: "fc_123", strip: true},
  25. {itemType: "tool_search_call", id: "tsc_123", strip: false},
  26. {itemType: "tool_search_call", id: "fc_123", strip: true},
  27. {itemType: "web_search_call", id: "ws_123", strip: false},
  28. {itemType: "web_search_call", id: "item_123", strip: true},
  29. {itemType: "custom_tool_call_output", id: "fc_123", strip: false},
  30. {itemType: "custom_tool_call_output", id: "ctco_123", strip: true},
  31. // Do not impose an inferred contract on output types for which there is
  32. // no observed upstream prefix rejection.
  33. {itemType: "function_call_output", id: "fco_123", strip: false},
  34. {itemType: "tool_search_output", id: "tso_123", strip: false},
  35. {itemType: "mcp_tool_call_output", id: "mcpo_123", strip: false},
  36. {itemType: "future_item", id: "item_123", strip: false},
  37. }
  38. for _, tt := range tests {
  39. t.Run(tt.itemType+"/"+tt.id, func(t *testing.T) {
  40. require.Equal(t, tt.strip, shouldStripOpenAIResponsesInputItemID(tt.itemType, tt.id))
  41. })
  42. }
  43. }
  44. func TestSanitizeOpenAIResponsesInputItemIDsDoesNotCascadeAcrossIDNamespaces(t *testing.T) {
  45. body := []byte(`{"input":[
  46. {"type":"function_call","id":"item_bad_call","call_id":"call_valid","name":"lookup","arguments":"{}"},
  47. {"type":"function_call_output","call_id":"call_valid","output":"preserve paired output"},
  48. {"type":"function_call_output","call_id":"item_bad_call","output":"preserve opaque output"},
  49. {"type":"item_reference","id":"item_bad_call"},
  50. {"type":"item_reference","id":"remote_valid"},
  51. {"type":"custom_tool_call","id":"ctc_valid","call_id":"ctco_bad_output","name":"apply_patch","input":"patch"},
  52. {"type":"custom_tool_call_output","id":"ctco_bad_output","call_id":"ctco_bad_output","output":"preserve by call_id"}
  53. ]}`)
  54. sanitized, changed, err := sanitizeOpenAIResponsesInputItemIDs(body)
  55. require.NoError(t, err)
  56. require.True(t, changed)
  57. items := gjson.GetBytes(sanitized, "input").Array()
  58. require.Len(t, items, 7)
  59. require.False(t, items[0].Get("id").Exists())
  60. require.Equal(t, "call_valid", items[0].Get("call_id").String())
  61. require.Equal(t, "preserve paired output", items[1].Get("output").String())
  62. require.Equal(t, "preserve opaque output", items[2].Get("output").String())
  63. require.Equal(t, "item_bad_call", items[3].Get("id").String())
  64. require.Equal(t, "remote_valid", items[4].Get("id").String())
  65. require.Equal(t, "ctc_valid", items[5].Get("id").String())
  66. require.False(t, items[6].Get("id").Exists())
  67. require.Equal(t, "ctco_bad_output", items[6].Get("call_id").String())
  68. }
  69. func TestSanitizeOpenAIResponsesInputItemIDsLeavesUnrelatedReferencesUntouched(t *testing.T) {
  70. body := []byte(`{"previous_response_id":"resp_1","input":[{"type":"item_reference","id":"remote_item"}]}`)
  71. sanitized, changed, err := sanitizeOpenAIResponsesInputItemIDs(body)
  72. require.NoError(t, err)
  73. require.False(t, changed)
  74. require.Equal(t, body, sanitized)
  75. }
  76. func TestSanitizeOpenAIResponsesInputItemIDsPreservesReferenceToDuplicateRetainedID(t *testing.T) {
  77. body := []byte(`{"input":[{"type":"function_call","id":"ctc_shared","call_id":"call_1"},{"type":"custom_tool_call","id":"ctc_shared","call_id":"call_2"},{"type":"item_reference","id":"ctc_shared"}]}`)
  78. sanitized, changed, err := sanitizeOpenAIResponsesInputItemIDs(body)
  79. require.NoError(t, err)
  80. require.True(t, changed)
  81. require.False(t, gjson.GetBytes(sanitized, "input.0.id").Exists())
  82. require.Equal(t, "ctc_shared", gjson.GetBytes(sanitized, "input.1.id").String())
  83. require.Equal(t, "ctc_shared", gjson.GetBytes(sanitized, "input.2.id").String())
  84. }
  85. func TestSanitizeOpenAIResponsesInputItemIDsPreservesOpaqueOutputsAndReferences(t *testing.T) {
  86. body := []byte(`{"input":[
  87. {"type":"function_call","id":"item_shared","call_id":"call_real"},
  88. {"type":"function_call_output","id":"item_shared","call_id":"item_shared","output":"dangling"},
  89. {"type":"item_reference","id":"item_shared"},
  90. {"type":"function_call_output","id":"kept_output","call_id":"call_real","output":"kept"},
  91. {"type":"item_reference","id":"kept_output"}
  92. ]}`)
  93. sanitized, changed, err := sanitizeOpenAIResponsesInputItemIDs(body)
  94. require.NoError(t, err)
  95. require.True(t, changed)
  96. require.Len(t, gjson.GetBytes(sanitized, "input").Array(), 5)
  97. require.False(t, gjson.GetBytes(sanitized, "input.0.id").Exists())
  98. require.Equal(t, "dangling", gjson.GetBytes(sanitized, "input.1.output").String())
  99. require.Equal(t, "item_shared", gjson.GetBytes(sanitized, "input.2.id").String())
  100. require.Equal(t, "kept_output", gjson.GetBytes(sanitized, "input.3.id").String())
  101. require.Equal(t, "kept_output", gjson.GetBytes(sanitized, "input.4.id").String())
  102. second, changedAgain, err := sanitizeOpenAIResponsesInputItemIDs(sanitized)
  103. require.NoError(t, err)
  104. require.False(t, changedAgain)
  105. require.Equal(t, sanitized, second)
  106. }
  107. func TestSanitizeOpenAIResponsesInputItemIDsStripsEmptyKnownIDsOnly(t *testing.T) {
  108. body := []byte(`{"input":[{"type":"message","id":"","content":"hello"},{"type":"future_item","id":""}]}`)
  109. sanitized, changed, err := sanitizeOpenAIResponsesInputItemIDs(body)
  110. require.NoError(t, err)
  111. require.True(t, changed)
  112. require.False(t, gjson.GetBytes(sanitized, "input.0.id").Exists())
  113. require.True(t, gjson.GetBytes(sanitized, "input.1.id").Exists())
  114. }
  115. func TestSanitizeOpenAIResponsesInputItemIDsStripsOnlyNonPairCallIDs(t *testing.T) {
  116. body := []byte(`{"input":[
  117. {"type":"message","call_id":"remove_message","content":"hi"},
  118. {"type":"reasoning","call_id":"remove_reasoning","id":"rs_keep","encrypted_content":"cipher","summary":[]},
  119. {"type":"image_generation_call","call_id":"remove_image","id":"ig_keep","status":"completed"},
  120. {"type":"function_call","call_id":"keep_function","name":"lookup","arguments":"{}"},
  121. {"type":"function_call_output","call_id":"keep_function","output":"ok"},
  122. {"type":"custom_tool_call","call_id":"keep_custom","name":"patch","input":"x"},
  123. {"type":"custom_tool_call_output","call_id":"keep_custom","output":"ok"},
  124. {"type":"tool_search_call","call_id":"keep_search","arguments":"{}"},
  125. {"type":"tool_search_output","call_id":"keep_search","output":"ok"},
  126. {"type":"local_shell_call","call_id":"keep_shell","name":"shell","arguments":"{}"}
  127. ]}`)
  128. sanitized, changed, err := sanitizeOpenAIResponsesInputItemIDs(body)
  129. require.NoError(t, err)
  130. require.True(t, changed)
  131. for i := 0; i < 3; i++ {
  132. require.False(t, gjson.GetBytes(sanitized, "input."+strconv.Itoa(i)+".call_id").Exists())
  133. }
  134. for i := 3; i < 10; i++ {
  135. require.True(t, gjson.GetBytes(sanitized, "input."+strconv.Itoa(i)+".call_id").Exists())
  136. }
  137. }