openai_responses_input_compat.go 2.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. package service
  2. import (
  3. "strings"
  4. )
  5. const openAIResponsesInputTextMaxChars = 10000000
  6. // sanitizeOpenAIResponsesOrphanToolOutputs removes tool-output items that have
  7. // no matching call or item reference anywhere in the current input. Named
  8. // function outputs without a call ID are standalone inputs, not orphan results.
  9. func sanitizeOpenAIResponsesOrphanToolOutputs(reqBody map[string]any, input []any, hasPreviousResponseID bool) bool {
  10. if len(input) == 0 || hasPreviousResponseID {
  11. return false
  12. }
  13. toolCallIDs := make(map[string]struct{}, len(input))
  14. referenceIDs := make(map[string]struct{}, len(input))
  15. for _, rawItem := range input {
  16. item, ok := rawItem.(map[string]any)
  17. if !ok {
  18. continue
  19. }
  20. itemType := strings.TrimSpace(firstNonEmptyString(item["type"]))
  21. if itemType == "item_reference" {
  22. if id := strings.TrimSpace(firstNonEmptyString(item["id"])); id != "" {
  23. referenceIDs[id] = struct{}{}
  24. }
  25. continue
  26. }
  27. if !isCodexToolCallContextItemType(itemType) {
  28. continue
  29. }
  30. if id := strings.TrimSpace(firstNonEmptyString(item["call_id"], item["id"])); id != "" {
  31. toolCallIDs[id] = struct{}{}
  32. }
  33. }
  34. modified := false
  35. normalized := make([]any, 0, len(input))
  36. for _, rawItem := range input {
  37. item, ok := rawItem.(map[string]any)
  38. if !ok || !isCodexToolCallOutputItemType(strings.TrimSpace(firstNonEmptyString(item["type"]))) {
  39. normalized = append(normalized, rawItem)
  40. continue
  41. }
  42. callID := strings.TrimSpace(firstNonEmptyString(item["call_id"]))
  43. // Codex sends externally supplied inputs (such as task delegation) as
  44. // named function outputs without a preceding function call. Preserve
  45. // their native type, namespace and output instead of silently dropping
  46. // the request that started the turn.
  47. if callID == "" && strings.TrimSpace(firstNonEmptyString(item["type"])) == "function_call_output" &&
  48. strings.TrimSpace(firstNonEmptyString(item["name"])) != "" {
  49. normalized = append(normalized, rawItem)
  50. continue
  51. }
  52. _, hasToolCall := toolCallIDs[callID]
  53. _, hasReference := referenceIDs[callID]
  54. if callID != "" && (hasToolCall || hasReference) {
  55. normalized = append(normalized, rawItem)
  56. continue
  57. }
  58. modified = true
  59. }
  60. if !modified {
  61. return false
  62. }
  63. reqBody["input"] = normalized
  64. return true
  65. }
  66. func truncateOpenAIResponsesInputText(_ map[string]any) bool {
  67. // Do not silently rewrite client or tool output. If an upstream enforces a
  68. // text limit, forwarding the original value preserves its explicit error for
  69. // the client and the normal Ops error pipeline. This compatibility shim is
  70. // retained until the two callers can remove the old mutation hook together.
  71. return false
  72. }
  73. func openAIResponsesInputMayNeedTruncation(_ []byte) bool {
  74. // See truncateOpenAIResponsesInputText. Returning false also avoids decoding
  75. // very large bodies solely for a mutation that must not happen.
  76. return false
  77. }