openai_codex_fingerprint.go 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. package service
  2. import (
  3. "crypto/sha256"
  4. "encoding/binary"
  5. "encoding/json"
  6. "fmt"
  7. "maps"
  8. "net/http"
  9. "strings"
  10. "time"
  11. "github.com/gin-gonic/gin"
  12. "github.com/google/uuid"
  13. "github.com/tidwall/gjson"
  14. "github.com/tidwall/sjson"
  15. )
  16. // codexFingerprintIDsContextKey 是暂存在 gin context 的收敛 ID 集合键。
  17. // 由 Forward(非透传)或 forwardOpenAIPassthrough(透传)解析后写入,请求
  18. // 构造器读取用于出站头改写——请求体与出站头必须共享同一份 IDs,保证
  19. // turn_id 等随机字段一致。
  20. const codexFingerprintIDsContextKey = "codex_fingerprint_ids"
  21. // stageCodexFingerprintIDs 将本 attempt 解析出的收敛 ID 暂存到 gin context。
  22. // 必须无条件覆写(含 nil):failover 从收敛账号切到 off 账号时,上一账号的
  23. // IDs 不得残留并被误应用到新账号的出站头(typed-nil 由应用侧 nil 守卫吸收)。
  24. func stageCodexFingerprintIDs(c *gin.Context, ids *codexFingerprintIDs) {
  25. if c != nil {
  26. c.Set(codexFingerprintIDsContextKey, ids)
  27. }
  28. }
  29. func stagedCodexFingerprintIDs(c *gin.Context, account *Account) *codexFingerprintIDs {
  30. if c != nil && account != nil {
  31. if value, ok := c.Get(codexFingerprintIDsContextKey); ok {
  32. if ids, ok := value.(*codexFingerprintIDs); ok && ids != nil && ids.accountID == account.ID {
  33. return ids
  34. }
  35. }
  36. }
  37. // Native WS ingress may bypass Forward. Device-only mode1 has no per-turn
  38. // random fields, so it can resolve the same persisted identity on every path.
  39. if isMode1ProtectionEnabled(account) || (antiDegradeEnabled(account) && account.GetCodexFingerprintMode() == codexFingerprintDevice) {
  40. return resolveCodexFingerprintIDs(account, "", codexFingerprintDevice)
  41. }
  42. if c == nil || account == nil || !account.UsesOpenAICodexProtocol() {
  43. return nil
  44. }
  45. value, ok := c.Get(codexFingerprintIDsContextKey)
  46. if !ok {
  47. return nil
  48. }
  49. ids, ok := value.(*codexFingerprintIDs)
  50. if !ok || ids == nil || ids.accountID != account.ID {
  51. return nil
  52. }
  53. return ids
  54. }
  55. // applyStagedCodexFingerprintHeaders 读取 context 暂存的收敛 ID 并改写出站头。
  56. // 非透传与透传两个请求构造器共用本函数,防止应用语义漂移。仅解析该
  57. // snapshot 的 OAuth 账号可读取,避免 stale context 跨账号 failover 泄漏。
  58. func applyStagedCodexFingerprintHeaders(c *gin.Context, account *Account, h http.Header) {
  59. applyCodexFingerprintHeaders(h, stagedCodexFingerprintIDs(c, account))
  60. if isMode1ProtectionEnabled(account) && c != nil && c.Request != nil && h != nil {
  61. if session := extractClientSessionID(c.Request.Header); session != "" {
  62. isolated := isolateOpenAIUpstreamSessionID(getAPIKeyIDFromContext(c), codexAccountIdentitySource(c, account), session)
  63. h.Set("session-id", isolated)
  64. h.Set("session_id", isolated)
  65. }
  66. }
  67. }
  68. func applyStagedCodexFingerprintClientMetadata(c *gin.Context, account *Account, reqBody map[string]any) bool {
  69. return applyCodexFingerprintClientMetadata(reqBody, stagedCodexFingerprintIDs(c, account))
  70. }
  71. // codexFingerprintMode 控制 OAuth 账号出站请求的设备指纹收敛强度。
  72. // 多人共享同一 OAuth 账号时,每个用户的 Codex 客户端会携带各自不同的
  73. // installation_id / session_id / thread_id,上游据此判定设备数和会话数。
  74. // 收敛模式将这些标识改写为账号级恒定值,减少上游可见的设备/会话指纹。
  75. type codexFingerprintMode string
  76. const (
  77. // codexFingerprintOff 不做任何收敛,原样透传客户端标识。
  78. // 这是默认值:收敛是显式 opt-in 的(见 GetCodexFingerprintMode)。
  79. codexFingerprintOff codexFingerprintMode = "off"
  80. // codexFingerprintDevice 仅收敛 installation_id 为账号级恒定值。
  81. // 上游看到 1 台设备 + 多会话(每用户各自的 session)。
  82. codexFingerprintDevice codexFingerprintMode = "device"
  83. // codexFingerprintSession 收敛 installation_id + session_id,
  84. // thread_id 按客户端原始 session-id 确定性派生(每个真实 Codex 会话一个独立线程)。
  85. // 上游看到 1 台设备 + 1 会话 + N 线程,最接近正常用户 spawn 子代理的模式。
  86. codexFingerprintSession codexFingerprintMode = "session"
  87. // codexFingerprintFull 收敛所有标识:installation_id + session_id + thread_id。
  88. // 上游看到 1 台设备 + 1 会话 + 1 线程,最激进。
  89. codexFingerprintFull codexFingerprintMode = "full"
  90. )
  91. const (
  92. codexFingerprintModeExtraKey = "codex_fingerprint_mode"
  93. codexFingerprintSeedExtraKey = "codex_fingerprint_seed"
  94. )
  95. func canonicalCodexFingerprintSeed(value any) (string, bool) {
  96. raw, ok := value.(string)
  97. if !ok {
  98. return "", false
  99. }
  100. trimmed := strings.TrimSpace(raw)
  101. parsed, err := uuid.Parse(trimmed)
  102. if err != nil || parsed == uuid.Nil || trimmed != parsed.String() {
  103. return "", false
  104. }
  105. return trimmed, true
  106. }
  107. func newCodexFingerprintSeed() string {
  108. return uuid.NewString()
  109. }
  110. func stripCodexFingerprintSeed(extra map[string]any) map[string]any {
  111. if extra == nil {
  112. return nil
  113. }
  114. stripped := maps.Clone(extra)
  115. delete(stripped, codexFingerprintSeedExtraKey)
  116. return stripped
  117. }
  118. func codexFingerprintModeFromExtra(extra map[string]any) codexFingerprintMode {
  119. if extra == nil {
  120. return codexFingerprintOff
  121. }
  122. raw, _ := extra[codexFingerprintModeExtraKey].(string)
  123. switch codexFingerprintMode(strings.TrimSpace(raw)) {
  124. case codexFingerprintOff, codexFingerprintDevice, codexFingerprintSession, codexFingerprintFull:
  125. return codexFingerprintMode(strings.TrimSpace(raw))
  126. default:
  127. return codexFingerprintOff
  128. }
  129. }
  130. func codexFingerprintModeRequiresSeed(mode codexFingerprintMode) bool {
  131. switch mode {
  132. case codexFingerprintDevice, codexFingerprintSession, codexFingerprintFull:
  133. return true
  134. default:
  135. return false
  136. }
  137. }
  138. func codexFingerprintSeed(extra map[string]any) (string, bool) {
  139. if extra == nil {
  140. return "", false
  141. }
  142. return canonicalCodexFingerprintSeed(extra[codexFingerprintSeedExtraKey])
  143. }
  144. func prepareCodexFingerprintExtraForCreate(platform, accountType string, extra map[string]any) map[string]any {
  145. prepared := stripCodexFingerprintSeed(extra)
  146. if platform != PlatformOpenAI || (accountType != AccountTypeOAuth && accountType != AccountTypeSetupToken) || !codexFingerprintModeRequiresSeed(codexFingerprintModeFromExtra(prepared)) {
  147. return prepared
  148. }
  149. if prepared == nil {
  150. prepared = make(map[string]any, 1)
  151. }
  152. prepared[codexFingerprintSeedExtraKey] = newCodexFingerprintSeed()
  153. return prepared
  154. }
  155. func prepareCodexFingerprintExtraForUpdate(account *Account, extra map[string]any) map[string]any {
  156. prepared := stripCodexFingerprintSeed(extra)
  157. if account == nil || !account.IsOpenAIOAuthLike() {
  158. return prepared
  159. }
  160. if seed, ok := codexFingerprintSeed(account.Extra); ok {
  161. if prepared == nil {
  162. prepared = make(map[string]any, 1)
  163. }
  164. prepared[codexFingerprintSeedExtraKey] = seed
  165. return prepared
  166. }
  167. if codexFingerprintModeRequiresSeed(codexFingerprintModeFromExtra(prepared)) {
  168. if prepared == nil {
  169. prepared = make(map[string]any, 1)
  170. }
  171. prepared[codexFingerprintSeedExtraKey] = newCodexFingerprintSeed()
  172. }
  173. return prepared
  174. }
  175. func sanitizedCodexFingerprintExtraUpdates(updates map[string]any) map[string]any {
  176. if updates == nil {
  177. return nil
  178. }
  179. sanitized := maps.Clone(updates)
  180. delete(sanitized, codexFingerprintSeedExtraKey)
  181. return sanitized
  182. }
  183. // ShouldEnsureCodexFingerprintSeedForExtraUpdates reports whether a JSONB key-level
  184. // extra update is enabling Codex fingerprint convergence and therefore must atomically
  185. // preserve or create the system-managed per-account seed in the repository update.
  186. func ShouldEnsureCodexFingerprintSeedForExtraUpdates(updates map[string]any) bool {
  187. if updates == nil {
  188. return false
  189. }
  190. return codexFingerprintModeRequiresSeed(codexFingerprintModeFromExtra(updates))
  191. }
  192. // GetCodexFingerprintMode 从账号 extra JSON 读取指纹收敛模式。
  193. //
  194. // **收敛是显式 opt-in**:未设置、空值或非法值一律按 off 处理,只有管理员
  195. // 明确配置 device / session / full 才收敛。
  196. //
  197. // 历史:v0.1.175(#5553)把缺省值当作 session,导致升级后存量 OAuth 账号
  198. // (普遍没有这个 extra 键)的每个非透传请求都被静默改写 installation /
  199. // session / thread / turn / window 五类标识;#5555、#5556、#5582 报告的额度
  200. // 缩水都卡在该版本边界,并有"回退 v0.1.173 即恢复"与"新账号开收敛后降额"
  201. // 的 A/B 实测。上游的配额判定策略不可观测,因此这里取兼容安全的一侧:
  202. // 不显式 opt-in 就保持 v0.1.175 之前的客户端身份(#5610)。
  203. func (a *Account) GetCodexFingerprintMode() codexFingerprintMode {
  204. if a == nil || !a.IsOpenAIOAuthLike() {
  205. return codexFingerprintOff
  206. }
  207. return codexFingerprintModeFromExtra(a.Extra)
  208. }
  209. // deriveStableUUIDv4 从种子确定性派生一个 UUIDv4 格式的字符串。
  210. // 同一种子永远返回同一值。
  211. func deriveStableUUIDv4(seed string) string {
  212. h := sha256.Sum256([]byte(seed))
  213. b := h[:16]
  214. b[6] = (b[6] & 0x0f) | 0x40 // version 4
  215. b[8] = (b[8] & 0x3f) | 0x80 // variant 1
  216. return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
  217. binary.BigEndian.Uint32(b[0:4]),
  218. binary.BigEndian.Uint16(b[4:6]),
  219. binary.BigEndian.Uint16(b[6:8]),
  220. binary.BigEndian.Uint16(b[8:10]),
  221. b[10:16])
  222. }
  223. // resolveConvergedInstallationID 返回账号级恒定的 installation_id。
  224. // 优先使用管理员配置的真实 device_id,无则从系统管理的账号随机种子确定性派生。
  225. func resolveConvergedInstallationID(account *Account, seed string) string {
  226. if account == nil {
  227. return ""
  228. }
  229. if isMode1ProtectionEnabled(account) && seed != "" {
  230. return deriveStableUUIDv4("sub2api:mode1-install-id:v2:" + seed)
  231. }
  232. if deviceID := account.GetOpenAIDeviceID(); deviceID != "" {
  233. return deviceID
  234. }
  235. if seed == "" {
  236. return ""
  237. }
  238. return deriveStableUUIDv4("sub2api:codex-install-id:v2:" + seed)
  239. }
  240. // resolveConvergedSessionID 返回账号级恒定的 session_id。
  241. func resolveConvergedSessionID(seed string) string {
  242. if seed == "" {
  243. return ""
  244. }
  245. return deriveStableUUIDv4("sub2api:codex-session-id:v2:" + seed)
  246. }
  247. // resolveConvergedThreadID 按客户端原始 session-id 确定性派生 thread_id。
  248. // 每个真实 Codex 会话(不同客户端启动实例)获得一个独立线程,
  249. // 模拟正常用户 spawn 子代理或开多窗口的模式。
  250. func resolveConvergedThreadID(seed, clientSessionID string) string {
  251. if seed == "" || clientSessionID == "" {
  252. return ""
  253. }
  254. return deriveStableUUIDv4("sub2api:codex-thread-id:v2:" + seed + ":" + clientSessionID)
  255. }
  256. // codexFingerprintIDs 收敛后的完整 ID 集合。
  257. // 由 resolveCodexFingerprintIDs 一次性生成,同一个实例在头改写和体改写之间共享,
  258. // 确保所有载体中的 turn_id 等随机字段一致。体改写时还会补记原始
  259. // client_metadata.session_id,用于识别 root prompt_cache_key 的默认值。
  260. type codexFingerprintIDs struct {
  261. accountID int64
  262. mode codexFingerprintMode
  263. installationID string
  264. sessionID string
  265. threadID string
  266. turnID string
  267. windowID string
  268. turnStartedAtUnixMs int64
  269. originalBodySessionID string
  270. originalBodySessionIDCaptured bool
  271. }
  272. // resolveCodexFingerprintIDs 按收敛模式计算出站 ID 集合。
  273. // clientSessionID 是客户端原始的 session-id 头值(连字符形式),用于 session 模式下
  274. // 的 thread_id 派生——每个真实 Codex 会话得到一个独立线程。
  275. // 返回 nil 表示 off 模式,不需要改写。
  276. // 注意:包含随机生成的 turn_id,调用方必须只调用一次并共享结果给头改写和体改写。
  277. func resolveCodexFingerprintIDs(account *Account, clientSessionID string, mode codexFingerprintMode) *codexFingerprintIDs {
  278. if account == nil || mode == codexFingerprintOff {
  279. return nil
  280. }
  281. seed, ok := codexFingerprintSeed(account.Extra)
  282. if !ok {
  283. return nil
  284. }
  285. ids := &codexFingerprintIDs{
  286. accountID: account.ID,
  287. mode: mode,
  288. turnStartedAtUnixMs: time.Now().UnixMilli(),
  289. }
  290. ids.installationID = resolveConvergedInstallationID(account, seed)
  291. if ids.installationID == "" {
  292. return nil
  293. }
  294. switch mode {
  295. case codexFingerprintDevice:
  296. return ids
  297. case codexFingerprintSession:
  298. ids.sessionID = resolveConvergedSessionID(seed)
  299. ids.threadID = resolveConvergedThreadID(seed, clientSessionID)
  300. if ids.threadID == "" {
  301. ids.threadID = ids.sessionID
  302. }
  303. ids.turnID = uuid.Must(uuid.NewV7()).String()
  304. ids.windowID = ids.threadID + ":0"
  305. return ids
  306. case codexFingerprintFull:
  307. ids.sessionID = resolveConvergedSessionID(seed)
  308. ids.threadID = ids.sessionID
  309. ids.turnID = uuid.Must(uuid.NewV7()).String()
  310. ids.windowID = ids.threadID + ":0"
  311. return ids
  312. }
  313. return nil
  314. }
  315. // extractClientSessionID 从请求头中提取客户端原始的会话标识。
  316. // 优先取 session-id(连字符形式,Codex CLI 标准),回退到 session_id(下划线形式)。
  317. // 返回的值尚未被 isolateOpenAISessionID 改写,是客户端的真实标识。
  318. func extractClientSessionID(h http.Header) string {
  319. if v := strings.TrimSpace(h.Get("session-id")); v != "" {
  320. return v
  321. }
  322. return strings.TrimSpace(h.Get("session_id"))
  323. }
  324. // resolveCodexFingerprintIDsFromRequest 从客户端原始请求头中提取 session-id,
  325. // 结合账号配置一次性解析收敛 ID 集合。调用方应将返回的 ids 同时传给
  326. // applyCodexFingerprintHeaders 和 applyCodexFingerprintClientMetadata。
  327. func resolveCodexFingerprintIDsFromRequest(account *Account, clientHeaders http.Header) *codexFingerprintIDs {
  328. if account == nil {
  329. return nil
  330. }
  331. mode := account.GetCodexFingerprintMode()
  332. if mode == codexFingerprintOff {
  333. return nil
  334. }
  335. clientSessionID := ""
  336. if clientHeaders != nil {
  337. clientSessionID = extractClientSessionID(clientHeaders)
  338. }
  339. return resolveCodexFingerprintIDs(account, clientSessionID, mode)
  340. }
  341. // applyCodexFingerprintHeaders 按预计算的收敛 ID 改写出站 HTTP 头中的设备指纹。
  342. // 在 buildUpstreamRequest 的白名单透传之后、enforceCodexIdentityHeaders 之前调用。
  343. func applyCodexFingerprintHeaders(h http.Header, ids *codexFingerprintIDs) {
  344. if h == nil || ids == nil {
  345. return
  346. }
  347. // 所有非 off 模式都收敛 installation_id
  348. h.Set("x-codex-installation-id", ids.installationID)
  349. if ids.mode == codexFingerprintDevice {
  350. rewriteCodexTurnMetadataFields(h, map[string]any{
  351. "installation_id": ids.installationID,
  352. })
  353. return
  354. }
  355. // session / full 模式:改写所有相关头
  356. h.Set("x-codex-window-id", ids.windowID)
  357. h.Set("x-client-request-id", ids.threadID)
  358. // 连字符形式和下划线形式都改写,保证一致
  359. h.Set("session-id", ids.sessionID)
  360. h.Set("session_id", ids.sessionID)
  361. h.Set("thread-id", ids.threadID)
  362. rewriteCodexTurnMetadataFields(h, map[string]any{
  363. "installation_id": ids.installationID,
  364. "session_id": ids.sessionID,
  365. "thread_id": ids.threadID,
  366. "turn_id": ids.turnID,
  367. "window_id": ids.windowID,
  368. "turn_started_at_unix_ms": ids.turnStartedAtUnixMs,
  369. })
  370. }
  371. // rewriteCodexTurnMetadataFields 解析 x-codex-turn-metadata 头中的 JSON,
  372. // 替换指定字段后回写。合法对象保留未指定字段(如 sandbox、thread_source);
  373. // 非法/非对象值重建为最小合法 metadata,避免 flat 与 embedded identity 分裂。
  374. func rewriteCodexTurnMetadataFields(h http.Header, fields map[string]any) {
  375. raw := strings.TrimSpace(h.Get("x-codex-turn-metadata"))
  376. if raw == "" {
  377. return
  378. }
  379. var metadata map[string]any
  380. if err := json.Unmarshal([]byte(raw), &metadata); err != nil || metadata == nil {
  381. metadata = make(map[string]any, len(fields))
  382. }
  383. for k, v := range fields {
  384. metadata[k] = v
  385. }
  386. rebuilt, err := json.Marshal(metadata)
  387. if err != nil {
  388. return
  389. }
  390. h.Set("x-codex-turn-metadata", string(rebuilt))
  391. }
  392. // applyCodexFingerprintClientMetadata 按预计算的收敛 ID 改写请求体中的 client_metadata。
  393. // 使用与头改写相同的 ids 实例,确保 turn_id 等随机字段一致。
  394. func applyCodexFingerprintClientMetadata(reqBody map[string]any, ids *codexFingerprintIDs) bool {
  395. if reqBody == nil || ids == nil {
  396. return false
  397. }
  398. captureCodexFingerprintOriginalBodySessionID(ids, reqBody["client_metadata"])
  399. existing, _ := reqBody["client_metadata"].(map[string]any)
  400. if existing == nil {
  401. existing = make(map[string]any)
  402. }
  403. modified := false
  404. if applyCodexFingerprintToClientMetadataMap(existing, ids) {
  405. reqBody["client_metadata"] = existing
  406. modified = true
  407. }
  408. if applyCodexFingerprintPromptCacheKey(reqBody, ids) {
  409. modified = true
  410. }
  411. return modified
  412. }
  413. // applyCodexFingerprintToClientMetadataMap 是 client_metadata 改写的共享核心,
  414. // map 版(非透传,body 已解码)与 raw 字节版(透传热路径)都经由它,保证两条
  415. // 路径的收敛语义永不漂移。
  416. func applyCodexFingerprintToClientMetadataMap(existing map[string]any, ids *codexFingerprintIDs) bool {
  417. if existing == nil || ids == nil {
  418. return false
  419. }
  420. modified := false
  421. if ids.installationID != "" {
  422. existing["x-codex-installation-id"] = ids.installationID
  423. if _, exists := existing["installation_id"]; exists {
  424. existing["installation_id"] = ids.installationID
  425. }
  426. modified = true
  427. }
  428. if ids.mode == codexFingerprintDevice {
  429. rewriteClientMetadataEmbeddedTurnMetadata(existing, map[string]any{
  430. "installation_id": ids.installationID,
  431. })
  432. return modified
  433. }
  434. // session / full 模式
  435. existing["session_id"] = ids.sessionID
  436. existing["thread_id"] = ids.threadID
  437. existing["turn_id"] = ids.turnID
  438. existing["x-codex-window-id"] = ids.windowID
  439. for key, value := range map[string]string{"session-id": ids.sessionID, "thread-id": ids.threadID, "turn-id": ids.turnID, "window_id": ids.windowID, "x-client-request-id": ids.threadID} {
  440. if _, present := existing[key]; present {
  441. existing[key] = value
  442. }
  443. }
  444. rewriteClientMetadataEmbeddedTurnMetadata(existing, map[string]any{
  445. "installation_id": ids.installationID,
  446. "session_id": ids.sessionID,
  447. "thread_id": ids.threadID,
  448. "turn_id": ids.turnID,
  449. "window_id": ids.windowID,
  450. "turn_started_at_unix_ms": ids.turnStartedAtUnixMs,
  451. })
  452. return true
  453. }
  454. type protectedWSClientSession struct {
  455. accountID int64
  456. raw string
  457. }
  458. // Native WS shares the same fingerprint projection as HTTP. Keep the first
  459. // client session for this connection while refreshing per-turn identifiers.
  460. func applyCodexAccountAndProtectionIdentityRaw(c *gin.Context, account *Account, body []byte) ([]byte, bool, error) {
  461. apiKeyID := getAPIKeyIDFromContext(c)
  462. next, changed, err := applyCodexAccountIdentityClientMetadataRaw(body, codexAccountIdentitySource(c, account), apiKeyID)
  463. if err != nil {
  464. return body, false, err
  465. }
  466. var headers http.Header
  467. if c != nil && c.Request != nil {
  468. headers = c.Request.Header
  469. }
  470. rawSession := extractClientSessionID(headers)
  471. if rawSession == "" {
  472. rawSession = strings.TrimSpace(gjson.GetBytes(body, "client_metadata.session_id").String())
  473. }
  474. if c != nil && account != nil {
  475. const key = "protection_ws_client_session"
  476. value, _ := c.Get(key)
  477. if saved, ok := value.(protectedWSClientSession); ok && saved.accountID == account.ID {
  478. rawSession = saved.raw
  479. } else {
  480. if rawSession == "" {
  481. rawSession = uuid.NewString()
  482. }
  483. c.Set(key, protectedWSClientSession{account.ID, rawSession})
  484. }
  485. }
  486. ids := resolveCodexFingerprintIDs(account, rawSession, account.GetCodexFingerprintMode())
  487. stageCodexFingerprintIDs(c, ids)
  488. next, projected, err := applyCodexFingerprintClientMetadataRaw(next, ids)
  489. return next, changed || projected, err
  490. }
  491. func captureCodexFingerprintOriginalBodySessionID(ids *codexFingerprintIDs, clientMetadata any) {
  492. if ids == nil || ids.originalBodySessionIDCaptured {
  493. return
  494. }
  495. ids.originalBodySessionIDCaptured = true
  496. if clientMetadata == nil {
  497. return
  498. }
  499. switch metadata := clientMetadata.(type) {
  500. case map[string]any:
  501. if sessionID, ok := metadata["session_id"].(string); ok {
  502. ids.originalBodySessionID = strings.TrimSpace(sessionID)
  503. }
  504. case map[string]string:
  505. ids.originalBodySessionID = strings.TrimSpace(metadata["session_id"])
  506. }
  507. }
  508. func captureCodexFingerprintOriginalBodySessionIDRaw(ids *codexFingerprintIDs, value gjson.Result) {
  509. if ids == nil || ids.originalBodySessionIDCaptured {
  510. return
  511. }
  512. ids.originalBodySessionIDCaptured = true
  513. if value.Exists() && value.Type == gjson.String {
  514. ids.originalBodySessionID = strings.TrimSpace(value.String())
  515. }
  516. }
  517. func shouldRewriteCodexFingerprintPromptCacheKey(ids *codexFingerprintIDs, promptCacheKey string) bool {
  518. if ids == nil || !ids.originalBodySessionIDCaptured || ids.originalBodySessionID == "" || ids.sessionID == "" {
  519. return false
  520. }
  521. if ids.mode != codexFingerprintSession && ids.mode != codexFingerprintFull {
  522. return false
  523. }
  524. return promptCacheKey == ids.originalBodySessionID
  525. }
  526. func applyCodexFingerprintPromptCacheKey(reqBody map[string]any, ids *codexFingerprintIDs) bool {
  527. if reqBody == nil {
  528. return false
  529. }
  530. promptCacheKey, ok := reqBody["prompt_cache_key"].(string)
  531. if !ok || strings.TrimSpace(promptCacheKey) == "" || !shouldRewriteCodexFingerprintPromptCacheKey(ids, promptCacheKey) {
  532. return false
  533. }
  534. if promptCacheKey == ids.sessionID {
  535. return false
  536. }
  537. reqBody["prompt_cache_key"] = ids.sessionID
  538. return true
  539. }
  540. // applyCodexFingerprintClientMetadataRaw 在原始 JSON 字节上改写 client_metadata,
  541. // 供透传路径使用——透传是热路径,禁止对可能高达数十 MB 的 body 做全量
  542. // Unmarshal(见 forwardOpenAIPassthrough 的轻量提取注释)。实现为:gjson 提取
  543. // client_metadata 小对象单独解码,经共享核心改写后 sjson 一次性拼回,body
  544. // 其余字节原样保留;root prompt_cache_key 仅在可证明是 body session 默认值时
  545. // 做标量改写。语义与 applyCodexFingerprintClientMetadata 逐点一致(含
  546. // "非对象值整体替换为收敛集合"的行为)。
  547. func applyCodexFingerprintClientMetadataRaw(body []byte, ids *codexFingerprintIDs) ([]byte, bool, error) {
  548. if len(body) == 0 || ids == nil {
  549. return body, false, nil
  550. }
  551. // 非 JSON 对象的 body(数组/标量/畸形)没有 client_metadata 语义,
  552. // sjson 在这类根上写字段会改写整体结构,直接放行保持原样。
  553. root := gjson.ParseBytes(body)
  554. if !root.IsObject() {
  555. captureCodexFingerprintOriginalBodySessionIDRaw(ids, gjson.Result{})
  556. return body, false, nil
  557. }
  558. existing := map[string]any{}
  559. if cm := gjson.GetBytes(body, "client_metadata"); cm.IsObject() {
  560. captureCodexFingerprintOriginalBodySessionIDRaw(ids, gjson.GetBytes(body, "client_metadata.session_id"))
  561. if err := json.Unmarshal([]byte(cm.Raw), &existing); err != nil {
  562. return body, false, fmt.Errorf("decode client_metadata for fingerprint: %w", err)
  563. }
  564. } else {
  565. captureCodexFingerprintOriginalBodySessionIDRaw(ids, gjson.Result{})
  566. }
  567. next := body
  568. modified := false
  569. if applyCodexFingerprintToClientMetadataMap(existing, ids) {
  570. raw, err := json.Marshal(existing)
  571. if err != nil {
  572. return body, false, fmt.Errorf("encode converged client_metadata: %w", err)
  573. }
  574. var setErr error
  575. next, setErr = sjson.SetRawBytes(body, "client_metadata", raw)
  576. if setErr != nil {
  577. return body, false, fmt.Errorf("splice converged client_metadata: %w", setErr)
  578. }
  579. modified = true
  580. }
  581. promptCacheKey := gjson.GetBytes(body, "prompt_cache_key")
  582. if promptCacheKey.Exists() && promptCacheKey.Type == gjson.String && strings.TrimSpace(promptCacheKey.String()) != "" && shouldRewriteCodexFingerprintPromptCacheKey(ids, promptCacheKey.String()) {
  583. rewritten, err := sjson.SetBytes(next, "prompt_cache_key", ids.sessionID)
  584. if err != nil {
  585. return body, false, fmt.Errorf("splice converged prompt_cache_key: %w", err)
  586. }
  587. next = rewritten
  588. modified = true
  589. }
  590. return next, modified, nil
  591. }
  592. // rewriteClientMetadataEmbeddedTurnMetadata 改写 client_metadata 中内嵌的
  593. // x-codex-turn-metadata JSON 字符串里的指定字段。非法/非对象值会重建,
  594. // 避免 flat client_metadata 与 embedded metadata 暴露两套身份。
  595. func rewriteClientMetadataEmbeddedTurnMetadata(clientMetadata map[string]any, fields map[string]any) {
  596. raw, ok := clientMetadata["x-codex-turn-metadata"].(string)
  597. if !ok || raw == "" {
  598. return
  599. }
  600. var metadata map[string]any
  601. if err := json.Unmarshal([]byte(raw), &metadata); err != nil || metadata == nil {
  602. metadata = make(map[string]any, len(fields))
  603. }
  604. for k, v := range fields {
  605. metadata[k] = v
  606. }
  607. if rebuilt, err := json.Marshal(metadata); err == nil {
  608. clientMetadata["x-codex-turn-metadata"] = string(rebuilt)
  609. }
  610. }