openai_compact_body_signal.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. package service
  2. import (
  3. "net/http"
  4. "strings"
  5. "github.com/gin-gonic/gin"
  6. "github.com/tidwall/gjson"
  7. )
  8. // openAINativeCompactionV2Key 标记本请求是原生 remote compaction v2
  9. // (裸 /responses + stream:true + compaction_trigger),由 handler 在判定后
  10. // 写入,供上游请求构造时补注协商头。
  11. const openAINativeCompactionV2Key = "openai_native_compaction_v2"
  12. const openAIRemoteCompactionV2Feature = "remote_compaction_v2"
  13. // MarkOpenAINativeCompactionV2 由 handler 在识别出原生 v2 压缩请求时调用。
  14. func MarkOpenAINativeCompactionV2(c *gin.Context) {
  15. if c != nil {
  16. c.Set(openAINativeCompactionV2Key, true)
  17. }
  18. }
  19. // NormalizeCompactionTriggerInputOrder keeps a single compaction trigger as
  20. // the final Responses input item, as required by the upstream v2 wire format.
  21. func NormalizeCompactionTriggerInputOrder(body []byte) ([]byte, bool, error) {
  22. if len(body) == 0 {
  23. return body, false, nil
  24. }
  25. var payload map[string]any
  26. if err := decodeOpenAIJSONUseNumber(body, &payload); err != nil {
  27. return body, false, err
  28. }
  29. input, ok := payload["input"].([]any)
  30. if !ok || len(input) == 0 {
  31. return body, false, nil
  32. }
  33. triggerCount := 0
  34. normalized := make([]any, 0, len(input))
  35. for _, raw := range input {
  36. item, itemOK := raw.(map[string]any)
  37. if itemOK && item["type"] == "compaction_trigger" {
  38. triggerCount++
  39. continue
  40. }
  41. normalized = append(normalized, raw)
  42. }
  43. if triggerCount == 0 {
  44. return body, false, nil
  45. }
  46. if triggerCount == 1 {
  47. if last, ok := input[len(input)-1].(map[string]any); ok && last["type"] == "compaction_trigger" {
  48. return body, false, nil
  49. }
  50. }
  51. normalized = append(normalized, map[string]any{"type": "compaction_trigger"})
  52. payload["input"] = normalized
  53. encoded, err := marshalOpenAIUpstreamJSON(payload)
  54. if err != nil {
  55. return body, false, err
  56. }
  57. return encoded, true, nil
  58. }
  59. func isOpenAINativeCompactionV2(c *gin.Context) bool {
  60. if c == nil {
  61. return false
  62. }
  63. return c.GetBool(openAINativeCompactionV2Key)
  64. }
  65. // IsOpenAINativeCompactionV2 reports whether the handler identified this
  66. // request as the native remote compaction v2 wire. It exposes only the
  67. // request-scoped boolean marker; no request payload is retained.
  68. func IsOpenAINativeCompactionV2(c *gin.Context) bool {
  69. return isOpenAINativeCompactionV2(c)
  70. }
  71. // ensureOpenAIRemoteCompactionV2BetaFeature 确保出站 x-codex-beta-features
  72. // 头包含 remote_compaction_v2。真实 Codex 发送 compaction_trigger 时总会同时
  73. // 携带该协商头(codex-rs build_model_client_beta_features_header 对该 feature
  74. // 特判 advertise);上游或下游网关链剥掉它后,请求会在依赖该头做门控的
  75. // 环节被降级(#5586)。这里在原生 v2 请求出站前补齐,使线型与真实 Codex
  76. // 一致。已存在时保持原样,不重复追加。
  77. func ensureOpenAIRemoteCompactionV2BetaFeature(h http.Header) {
  78. if h == nil {
  79. return
  80. }
  81. tokens := make([]string, 0, 4)
  82. for _, value := range h.Values("x-codex-beta-features") {
  83. for _, token := range strings.Split(value, ",") {
  84. token = strings.TrimSpace(token)
  85. if token == "" {
  86. continue
  87. }
  88. if token == openAIRemoteCompactionV2Feature {
  89. return
  90. }
  91. tokens = append(tokens, token)
  92. }
  93. }
  94. tokens = append(tokens, openAIRemoteCompactionV2Feature)
  95. h.Set("x-codex-beta-features", strings.Join(tokens, ","))
  96. }
  97. // hasOpenAICodexBetaFeaturesHeader 报告出站头里是否已存在非空的
  98. // x-codex-beta-features(即客户端自己声明过能力集)。
  99. func hasOpenAICodexBetaFeaturesHeader(h http.Header) bool {
  100. if h == nil {
  101. return false
  102. }
  103. for _, value := range h.Values("x-codex-beta-features") {
  104. if strings.TrimSpace(value) != "" {
  105. return true
  106. }
  107. }
  108. return false
  109. }
  110. // applyOpenAICodexBetaFeatures 按真实 Codex 的会话级行为补注
  111. // x-codex-beta-features。
  112. //
  113. // codex 侧规则(codex-rs:session/mod.rs build_model_client_beta_features_header
  114. // 组装、client.rs build_responses_headers 附加):该头是**会话级常量**,挂在
  115. // /responses、WS 握手、/responses/compact 三处的**每一个**请求上,而不是只挂
  116. // 压缩回合。其内容是"已启用且需要 advertise 的 feature 列表",RemoteCompactionV2
  117. // 被特判 advertise;实测默认安装下没有任何 Experimental 特性默认开启,
  118. // 因此默认 Codex 的头值恰好就是单个 "remote_compaction_v2"。
  119. //
  120. // 网关据此对齐:
  121. // - 原生 v2 压缩回合(body 带 compaction_trigger 实锤):无论账号类型都确保
  122. // v2 在列,覆盖中间网关裁剪 token 的情形(#5586);
  123. // - ChatGPT codex 上游(OAuth)的其余请求:客户端**未**声明该头时补成默认
  124. // Codex 形态,消除"仅压缩回合才带该头"这种真实 Codex 不会产生的模式;
  125. // - 客户端已声明该头:原样保留。非空但不含 v2 表示用户显式关闭了该特性,
  126. // 网关不得替其改写能力声明;
  127. // - 非 OAuth 上游(API Key/第三方兼容网关):不做会话级注入,只保留压缩回合
  128. // 的那一条,避免向非 Codex 后端撒 Codex 专属头。
  129. //
  130. // 已知无解的歧义:用户关掉 v2 且无其他特性时,真实 Codex 同样不发该头,与"老
  131. // 客户端"在线型上不可区分,此时按默认形态补注。该用户的 legacy 压缩端点本就
  132. // 已被上游下线(404),不存在可回退的正确行为。
  133. func applyOpenAICodexBetaFeatures(c *gin.Context, account *Account, h http.Header) {
  134. if h == nil {
  135. return
  136. }
  137. if isOpenAINativeCompactionV2(c) {
  138. ensureOpenAIRemoteCompactionV2BetaFeature(h)
  139. return
  140. }
  141. if account == nil || !account.IsOpenAIOAuthLike() {
  142. return
  143. }
  144. if hasOpenAICodexBetaFeaturesHeader(h) {
  145. return
  146. }
  147. h.Set("x-codex-beta-features", openAIRemoteCompactionV2Feature)
  148. }
  149. // HasCompactionTriggerInInput detects an input item with
  150. // type="compaction_trigger". The handler combines this body signal with the
  151. // request path and stream flag to distinguish the native remote compaction v2
  152. // wire from the legacy /responses/compact bridge.
  153. func HasCompactionTriggerInInput(body []byte) bool {
  154. if len(body) == 0 {
  155. return false
  156. }
  157. input := gjson.GetBytes(body, "input")
  158. if !input.IsArray() {
  159. return false
  160. }
  161. found := false
  162. input.ForEach(func(_, item gjson.Result) bool {
  163. if item.Get("type").String() == "compaction_trigger" {
  164. found = true
  165. return false
  166. }
  167. return true
  168. })
  169. return found
  170. }