openai_responses_item_id.go 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. package service
  2. import (
  3. "fmt"
  4. "strings"
  5. "github.com/tidwall/gjson"
  6. "github.com/tidwall/sjson"
  7. )
  8. func openAIResponsesInputItemIDPrefix(itemType string) (string, bool) {
  9. switch strings.TrimSpace(itemType) {
  10. case "message":
  11. return "msg", true
  12. case "reasoning":
  13. return "rs", true
  14. case "web_search_call":
  15. return "ws", true
  16. case "custom_tool_call":
  17. return openAIResponsesToolCallIDPrefix(itemType), true
  18. case "tool_search_call":
  19. return openAIResponsesToolCallIDPrefix(itemType), true
  20. case "custom_tool_call_output":
  21. // Although custom calls use ctc IDs, OpenAI validates replayed custom
  22. // call output item IDs against the generic fc namespace.
  23. return "fc", true
  24. default:
  25. if isCodexToolCallInputType(itemType) {
  26. return openAIResponsesToolCallIDPrefix(itemType), true
  27. }
  28. return "", false
  29. }
  30. }
  31. func openAIResponsesToolCallIDPrefix(itemType string) string {
  32. switch strings.TrimSpace(itemType) {
  33. case "custom_tool_call", "custom_tool_call_output":
  34. return "ctc"
  35. case "tool_search_call", "tool_search_output":
  36. return "tsc"
  37. default:
  38. return "fc"
  39. }
  40. }
  41. // Invalid replayed IDs are removed rather than rewritten because a fabricated
  42. // ID may point at a different upstream object.
  43. func shouldStripOpenAIResponsesInputItemID(itemType, id string) bool {
  44. prefix, constrained := openAIResponsesInputItemIDPrefix(itemType)
  45. if !constrained {
  46. return false
  47. }
  48. return id == "" || !strings.HasPrefix(id, prefix)
  49. }
  50. func shouldStripOpenAIResponsesNonPairCallID(itemType string) bool {
  51. switch strings.TrimSpace(itemType) {
  52. case "message", "reasoning", "image_generation_call":
  53. return true
  54. default:
  55. return false
  56. }
  57. }
  58. func sanitizeOpenAIResponsesInputItemIDs(body []byte) ([]byte, bool, error) {
  59. input := gjson.GetBytes(body, "input")
  60. if !input.IsArray() {
  61. return body, false, nil
  62. }
  63. type inputItem struct {
  64. body []byte
  65. stripID bool
  66. stripCallID bool
  67. }
  68. items := make([]inputItem, 0)
  69. input.ForEach(func(_, item gjson.Result) bool {
  70. parsed := inputItem{body: []byte(item.Raw)}
  71. if item.IsObject() {
  72. itemType := item.Get("type")
  73. id := item.Get("id")
  74. trimmedItemType := strings.TrimSpace(itemType.String())
  75. parsed.stripCallID = item.Get("call_id").Exists() && shouldStripOpenAIResponsesNonPairCallID(trimmedItemType)
  76. if id.Type == gjson.String {
  77. parsed.stripID = shouldStripOpenAIResponsesInputItemID(trimmedItemType, id.String())
  78. }
  79. }
  80. items = append(items, parsed)
  81. return true
  82. })
  83. hasSanitization := false
  84. for _, item := range items {
  85. if item.stripID || item.stripCallID {
  86. hasSanitization = true
  87. break
  88. }
  89. }
  90. if !hasSanitization {
  91. return body, false, nil
  92. }
  93. rebuiltItems := make([][]byte, 0, len(items))
  94. for index, item := range items {
  95. itemBody := item.body
  96. if item.stripID {
  97. var err error
  98. itemBody, err = sjson.DeleteBytes(itemBody, "id")
  99. if err != nil {
  100. return nil, false, fmt.Errorf("delete input.%d.id: %w", index, err)
  101. }
  102. }
  103. if item.stripCallID {
  104. var err error
  105. itemBody, err = sjson.DeleteBytes(itemBody, "call_id")
  106. if err != nil {
  107. return nil, false, fmt.Errorf("delete input.%d.call_id: %w", index, err)
  108. }
  109. }
  110. rebuiltItems = append(rebuiltItems, itemBody)
  111. }
  112. rebuiltInput := make([]byte, 0, len(input.Raw))
  113. rebuiltInput = append(rebuiltInput, '[')
  114. for i, item := range rebuiltItems {
  115. if i > 0 {
  116. rebuiltInput = append(rebuiltInput, ',')
  117. }
  118. rebuiltInput = append(rebuiltInput, item...)
  119. }
  120. rebuiltInput = append(rebuiltInput, ']')
  121. sanitized, err := sjson.SetRawBytes(body, "input", rebuiltInput)
  122. if err != nil {
  123. return nil, false, fmt.Errorf("replace sanitized input: %w", err)
  124. }
  125. return sanitized, true, nil
  126. }