openai_mode1_integrity.go 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. package service
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "io"
  6. "reflect"
  7. "strings"
  8. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  9. "github.com/gin-gonic/gin"
  10. )
  11. type mode1RequestSnapshot struct {
  12. accountID int64
  13. body []byte
  14. }
  15. const mode1OriginalRequestKey = "mode1_original_request"
  16. func stageMode1Request(c *gin.Context, a *Account, body []byte) {
  17. if c == nil {
  18. return
  19. }
  20. c.Set(mode1OriginalRequestKey, (*mode1RequestSnapshot)(nil))
  21. if a.RequestIntegrityMode() != "off" {
  22. c.Set(mode1OriginalRequestKey, &mode1RequestSnapshot{a.ID, bytes.Clone(body)})
  23. }
  24. }
  25. // validateMode1RequestIntegrity rejects lossy compatibility transformations.
  26. // Identity metadata and transport control fields are deliberately outside this
  27. // comparison. A transport cannot pretend that stripped context was preserved.
  28. func validateMode1RequestIntegrity(original, forwarded []byte) error {
  29. return validateMode1RequestIntegrityForAccount(nil, original, forwarded)
  30. }
  31. // Compare protocol semantics, not JSON shape. Only the documented Codex
  32. // compatibility conversions are allowed. Never run the full (lossy) transform
  33. // on the snapshot: doing so would conceal dropped tools or reasoning items.
  34. func validateMode1RequestIntegrityForAccount(account *Account, original, forwarded []byte) error {
  35. decode := func(raw []byte) (map[string]any, error) {
  36. var v map[string]any
  37. d := json.NewDecoder(bytes.NewReader(raw))
  38. d.UseNumber()
  39. err := d.Decode(&v)
  40. if err == nil {
  41. var trailing any
  42. if v == nil || d.Decode(&trailing) != io.EOF {
  43. err = io.ErrUnexpectedEOF
  44. }
  45. }
  46. return v, err
  47. }
  48. before, err := decode(original)
  49. if err != nil {
  50. return infraerrors.BadRequest("MODE1_INVALID_JSON", "模式一请求不是有效 JSON")
  51. }
  52. after, err := decode(forwarded)
  53. if err != nil {
  54. return infraerrors.BadRequest("MODE1_INVALID_JSON", "模式一出站请求不是有效 JSON")
  55. }
  56. codex := account != nil && account.UsesOpenAICodexProtocol()
  57. if codex {
  58. hadInstructions := !isInstructionsEmpty(before)
  59. // The internal subscription endpoint does not accept token budgets.
  60. // Absence is an explicit protocol limitation; changing a supported
  61. // budget or losing one on an API-key endpoint is still rejected.
  62. for _, key := range []string{"max_output_tokens", "max_completion_tokens"} {
  63. if _, exists := after[key]; !exists {
  64. delete(before, key)
  65. }
  66. }
  67. if model, ok := before["model"].(string); ok {
  68. if actual, ok := after["model"].(string); ok && strings.TrimSpace(actual) == normalizeOpenAIModelForUpstream(account, account.GetMappedModel(model)) {
  69. before["model"] = actual
  70. }
  71. }
  72. mode1CanonicalizeCodexRequest(before)
  73. mode1CanonicalizeCodexRequest(after)
  74. if !hadInstructions {
  75. // HTTP can inject its default before system-message promotion;
  76. // accept that exact default suffix without relaxing user text.
  77. if expected, ok := before["instructions"].(string); ok {
  78. if actual, ok := after["instructions"].(string); ok {
  79. model, _ := after["model"].(string)
  80. if actual == expected+"\n\n"+defaultCodexSynthInstructions(model) {
  81. after["instructions"] = expected
  82. }
  83. }
  84. }
  85. }
  86. }
  87. for _, key := range []string{"model", "input", "instructions", "reasoning", "tools", "tool_choice", "parallel_tool_calls", "text", "previous_response_id", "max_output_tokens", "max_completion_tokens", "session", "functions", "function_call"} {
  88. if value, exists := before[key]; exists && !reflect.DeepEqual(value, after[key]) {
  89. return infraerrors.BadRequest("MODE1_LOSSY_TRANSFORM", "模式一阻止了会改变请求语义的转换,字段:"+key)
  90. }
  91. }
  92. return nil
  93. }
  94. func validateMode1StagedRequest(c *gin.Context, a *Account, forwarded []byte) error {
  95. if a.RequestIntegrityMode() == "off" || c == nil {
  96. return nil
  97. }
  98. v, _ := c.Get(mode1OriginalRequestKey)
  99. s, _ := v.(*mode1RequestSnapshot)
  100. if s == nil || s.accountID != a.ID {
  101. return nil
  102. }
  103. return checkAccountRequestIntegrity(c, a, s.body, forwarded)
  104. }