requestmodel.go 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. // Package requestmodel 统一从网关请求中提取客户端请求的模型名。
  2. //
  3. // 提取顺序与位置(JSON `model` / `session.model`、multipart `model` / `session`)
  4. // 与合成路由中间件、分组模型白名单中间件共用,保证改写与准入看到同一个模型。
  5. //
  6. // 网关里同时存在三类下游解析器,同一请求体可能被解析出不同模型值:
  7. // - gjson 系(大小写敏感、重复键取第一个);
  8. // - encoding/json 绑定系(大小写不敏感、重复键取最后一个);
  9. // - multipart 表单系(字段可重复,不同 handler 取首个或末个)。
  10. //
  11. // 因此准入侧的 FromBodyCandidates 返回「任一下游解析器可能绑定到的全部模型值」,
  12. // 调用方必须逐一校验;FromBodyForRoute 返回首个候选值,仅供合成路由分发使用。
  13. package requestmodel
  14. import (
  15. "bytes"
  16. "errors"
  17. "io"
  18. "mime"
  19. "mime/multipart"
  20. "net/http"
  21. "strconv"
  22. "strings"
  23. "github.com/Wei-Shaw/sub2api/internal/pkg/httputil"
  24. "github.com/tidwall/gjson"
  25. )
  26. // liveRequestRoutes 是模型位于 session.model(而非顶层 model)的入口,
  27. // 与 Live handler 的解析保持一致。
  28. var liveRequestRoutes = map[string]bool{
  29. "/v1/live": true,
  30. "/live": true,
  31. "/backend-api/codex/realtime/calls": true,
  32. }
  33. // IsLiveRequestRoute 报告该路由模板的 handler 是否只读取 session.model。
  34. func IsLiveRequestRoute(routePath string) bool {
  35. return liveRequestRoutes[strings.TrimSpace(routePath)]
  36. }
  37. // FromBodyCandidates 按入口返回所有可被下游解析器绑定的模型值:
  38. // Live 入口取 session 候选(为空回落顶层候选),其余入口取顶层候选
  39. // (为空回落 session 候选)。候选包含重复键、大小写变体的全部出现。
  40. func FromBodyCandidates(routePath, contentType string, body []byte) []string {
  41. var topLevel, session []string
  42. if isMultipartContentType(contentType) {
  43. // 声明为 multipart 的请求体只做 multipart 解析——gjson 的部分扫描
  44. // 会在 multipart 字节流里误匹配 session JSON 内的 model 字段。
  45. models, sessions := multipartModelCandidates(contentType, body)
  46. topLevel = models
  47. session = sessionModelCandidates(sessions)
  48. } else {
  49. topLevel = jsonModelCandidates(body)
  50. session = jsonSessionModelCandidates(body)
  51. }
  52. if IsLiveRequestRoute(routePath) {
  53. if len(session) > 0 {
  54. return session
  55. }
  56. return topLevel
  57. }
  58. if len(topLevel) > 0 {
  59. return topLevel
  60. }
  61. return session
  62. }
  63. // FromBody 从 JSON 或 multipart 请求体提取客户端请求的模型名(首个候选);
  64. // 提取不到返回空串。顶层 `model` 优先,其次 `session.model`(Live 入口之外
  65. // 的地方兜底)。准入校验请改用 FromBodyCandidates。
  66. func FromBody(contentType string, body []byte) string {
  67. return FromBodyForRoute("", contentType, body)
  68. }
  69. // FromBodyForRoute 按入口选择与 handler 一致的模型提取规则,返回首个候选值,
  70. // 仅供合成路由分发使用;准入校验必须用 FromBodyCandidates 的全部候选。
  71. func FromBodyForRoute(routePath, contentType string, body []byte) string {
  72. candidates := FromBodyCandidates(routePath, contentType, body)
  73. if len(candidates) == 0 {
  74. return ""
  75. }
  76. return candidates[0]
  77. }
  78. // FromJSON 从 JSON 请求体提取模型名:顶层 `model` 优先,其次 `session.model`(Live)。
  79. func FromJSON(body []byte) string {
  80. model, _ := JSONModelPath(body)
  81. return model
  82. }
  83. // JSONModelPath 返回 JSON 请求体中的模型名与其 gjson 路径(用于合成路由改写定位)。
  84. // 只匹配精确大小写的键;大小写变体由 FromBodyCandidates 的候选集覆盖。
  85. func JSONModelPath(body []byte) (string, string) {
  86. return jsonModelWithPaths(body, []string{"model", "session.model"})
  87. }
  88. // JSONModelPathForRoute 按入口返回合成路由改写应写入的模型路径:Live 入口
  89. // session.model 优先,其余入口顶层 model 优先。
  90. func JSONModelPathForRoute(routePath string, body []byte) (string, string) {
  91. if IsLiveRequestRoute(routePath) {
  92. return jsonModelWithPaths(body, []string{"session.model", "model"})
  93. }
  94. return jsonModelWithPaths(body, []string{"model", "session.model"})
  95. }
  96. func jsonModelWithPaths(body []byte, paths []string) (string, string) {
  97. for _, path := range paths {
  98. model := gjson.GetBytes(body, path)
  99. if model.Type != gjson.String {
  100. continue
  101. }
  102. if value := strings.TrimSpace(model.String()); value != "" {
  103. return value, path
  104. }
  105. }
  106. return "", ""
  107. }
  108. // jsonModelCandidates 返回顶层所有键名大小写不敏感等于 "model" 的字符串值,
  109. // 按出现顺序保留重复(覆盖 gjson 首 matches 与 encoding/json 末值绑定两类解析器)。
  110. func jsonModelCandidates(body []byte) []string {
  111. var out []string
  112. gjson.ParseBytes(body).ForEach(func(key, value gjson.Result) bool {
  113. if value.Type == gjson.String && strings.EqualFold(key.String(), "model") {
  114. if trimmed := strings.TrimSpace(value.String()); trimmed != "" {
  115. out = append(out, trimmed)
  116. }
  117. }
  118. return true
  119. })
  120. return out
  121. }
  122. // jsonSessionModelCandidates 返回每个顶层 "session"(大小写不敏感)对象内
  123. // 所有键名大小写不敏感等于 "model" 的字符串值。
  124. func jsonSessionModelCandidates(body []byte) []string {
  125. var out []string
  126. gjson.ParseBytes(body).ForEach(func(key, value gjson.Result) bool {
  127. if !strings.EqualFold(key.String(), "session") {
  128. return true
  129. }
  130. value.ForEach(func(sessionKey, sessionValue gjson.Result) bool {
  131. if sessionValue.Type == gjson.String && strings.EqualFold(sessionKey.String(), "model") {
  132. if trimmed := strings.TrimSpace(sessionValue.String()); trimmed != "" {
  133. out = append(out, trimmed)
  134. }
  135. }
  136. return true
  137. })
  138. return true
  139. })
  140. return out
  141. }
  142. // sessionModelCandidates 对若干 session 字段原始值(JSON 文本)逐个提取模型候选。
  143. func sessionModelCandidates(rawSessions []string) []string {
  144. var out []string
  145. for _, raw := range rawSessions {
  146. out = append(out, jsonModelCandidates([]byte(raw))...)
  147. }
  148. return out
  149. }
  150. // multipartModelCandidates 返回 multipart 表单中全部 "model" 字段值与全部
  151. // "session" 字段原始值,按出现顺序保留重复,覆盖取首个或末个字段的两类 handler。
  152. func multipartModelCandidates(contentType string, body []byte) (models, sessions []string) {
  153. mediaType, params, err := mime.ParseMediaType(strings.TrimSpace(contentType))
  154. if err != nil || !strings.EqualFold(mediaType, "multipart/form-data") {
  155. return nil, nil
  156. }
  157. boundary := strings.TrimSpace(params["boundary"])
  158. if boundary == "" {
  159. return nil, nil
  160. }
  161. reader := multipart.NewReader(bytes.NewReader(body), boundary)
  162. for {
  163. part, err := reader.NextPart()
  164. if errors.Is(err, io.EOF) {
  165. return models, sessions
  166. }
  167. if err != nil {
  168. return models, sessions
  169. }
  170. fieldName := strings.TrimSpace(part.FormName())
  171. if part.FileName() != "" || (fieldName != "model" && fieldName != "session") {
  172. continue
  173. }
  174. data, err := io.ReadAll(part)
  175. if err != nil {
  176. continue
  177. }
  178. if value := strings.TrimSpace(string(data)); value != "" {
  179. if fieldName == "model" {
  180. models = append(models, value)
  181. } else {
  182. sessions = append(sessions, value)
  183. }
  184. }
  185. }
  186. }
  187. // isMultipartContentType 判断请求是否声明为 multipart/form-data。
  188. func isMultipartContentType(contentType string) bool {
  189. mediaType, _, err := mime.ParseMediaType(strings.TrimSpace(contentType))
  190. return err == nil && strings.EqualFold(mediaType, "multipart/form-data")
  191. }
  192. // ResetRequestBody 把已读取(可能已被改写)的请求体回填到请求上,
  193. // 供后续 handler 重新读取。使用 httputil.PrereadBody 回填,后续经
  194. // ReadRequestBodyWithPrealloc 读取时零拷贝。
  195. func ResetRequestBody(req *http.Request, body []byte) {
  196. if req == nil {
  197. return
  198. }
  199. req.Body = httputil.NewPrereadBody(body)
  200. req.ContentLength = int64(len(body))
  201. req.Header.Set("Content-Length", strconv.Itoa(len(body)))
  202. }