feat: Codex OAuth 设备指纹收敛,减少上游可见的设备数和会话数

多人共享同一 OAuth 账号时,各用户 Codex 客户端携带各自不同的 installation_id/session_id/thread_id,
上游据此判定设备数和会话数并限制配额。本功能将这些标识改写为账号级恒定值。

四档策略(账号级 extra 字段 codex_fingerprint_mode):
- off: 不做任何收敛,原样透传
- device: 仅收敛 installation_id
- session(默认): 收敛 installation_id + session_id,thread_id 按客户端原始 session 派生
- full: 收敛所有标识(installation_id + session_id + thread_id)

改写覆盖 6 个指纹载体:x-codex-turn-metadata 头(JSON 内部字段)、x-codex-window-id、
x-codex-installation-id、x-client-request-id、session-id/session_id/thread-id、
请求体 client_metadata。头和体共享同一份预计算 IDs 确保 turn_id 等随机字段一致。
This commit is contained in:
shaw
2026-08-12 17:57:50 +08:00
parent 4ec9ceec4a
commit c0ab3a00ea
8 changed files with 901 additions and 0 deletions
@@ -0,0 +1,301 @@
package service
import (
"crypto/sha256"
"encoding/binary"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/google/uuid"
)
// codexFingerprintMode 控制 OAuth 账号出站请求的设备指纹收敛强度。
// 多人共享同一 OAuth 账号时,每个用户的 Codex 客户端会携带各自不同的
// installation_id / session_id / thread_id,上游据此判定设备数和会话数。
// 收敛模式将这些标识改写为账号级恒定值,减少上游可见的设备/会话指纹。
type codexFingerprintMode string
const (
// codexFingerprintOff 不做任何收敛,原样透传客户端标识(默认行为)。
codexFingerprintOff codexFingerprintMode = "off"
// codexFingerprintDevice 仅收敛 installation_id 为账号级恒定值。
// 上游看到 1 台设备 + 多会话(每用户各自的 session)。
codexFingerprintDevice codexFingerprintMode = "device"
// codexFingerprintSession 收敛 installation_id + session_id,
// thread_id 按客户端原始 session-id 确定性派生(每个真实 Codex 会话一个独立线程)。
// 上游看到 1 台设备 + 1 会话 + N 线程,最接近正常用户 spawn 子代理的模式。
codexFingerprintSession codexFingerprintMode = "session"
// codexFingerprintFull 收敛所有标识:installation_id + session_id + thread_id。
// 上游看到 1 台设备 + 1 会话 + 1 线程,最激进。
codexFingerprintFull codexFingerprintMode = "full"
)
const codexFingerprintModeExtraKey = "codex_fingerprint_mode"
// GetCodexFingerprintMode 从账号 extra JSON 读取指纹收敛模式。
// 未设置时默认 session(设备+会话收敛),显式设为 "off" 才关闭。
func (a *Account) GetCodexFingerprintMode() codexFingerprintMode {
if a == nil || !a.IsOpenAIOAuth() {
return codexFingerprintOff
}
raw := strings.TrimSpace(a.GetExtraString(codexFingerprintModeExtraKey))
switch codexFingerprintMode(raw) {
case codexFingerprintOff, codexFingerprintDevice, codexFingerprintSession, codexFingerprintFull:
return codexFingerprintMode(raw)
default:
return codexFingerprintSession
}
}
// deriveStableUUIDv4 从种子确定性派生一个 UUIDv4 格式的字符串。
// 同一种子永远返回同一值。
func deriveStableUUIDv4(seed string) string {
h := sha256.Sum256([]byte(seed))
b := h[:16]
b[6] = (b[6] & 0x0f) | 0x40 // version 4
b[8] = (b[8] & 0x3f) | 0x80 // variant 1
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
binary.BigEndian.Uint32(b[0:4]),
binary.BigEndian.Uint16(b[4:6]),
binary.BigEndian.Uint16(b[6:8]),
binary.BigEndian.Uint16(b[8:10]),
b[10:16])
}
// resolveConvergedInstallationID 返回账号级恒定的 installation_id。
// 优先使用管理员配置的真实 device_id,无则从 accountID 确定性派生。
func resolveConvergedInstallationID(account *Account) string {
if account == nil {
return ""
}
if deviceID := account.GetOpenAIDeviceID(); deviceID != "" {
return deviceID
}
return deriveStableUUIDv4(fmt.Sprintf("sub2api:codex-install-id:v1:%d", account.ID))
}
// resolveConvergedSessionID 返回账号级恒定的 session_id。
func resolveConvergedSessionID(account *Account) string {
if account == nil {
return ""
}
return deriveStableUUIDv4(fmt.Sprintf("sub2api:codex-session-id:v1:%d", account.ID))
}
// resolveConvergedThreadID 按客户端原始 session-id 确定性派生 thread_id。
// 每个真实 Codex 会话(不同客户端启动实例)获得一个独立线程,
// 模拟正常用户 spawn 子代理或开多窗口的模式。
func resolveConvergedThreadID(account *Account, clientSessionID string) string {
if account == nil || clientSessionID == "" {
return ""
}
return deriveStableUUIDv4(fmt.Sprintf("sub2api:codex-thread-id:v1:%d:%s", account.ID, clientSessionID))
}
// codexFingerprintIDs 收敛后的完整 ID 集合。
// 由 resolveCodexFingerprintIDs 一次性生成,同一个实例在头改写和体改写之间共享,
// 确保所有载体中的 turn_id 等随机字段一致。
type codexFingerprintIDs struct {
mode codexFingerprintMode
installationID string
sessionID string
threadID string
turnID string
windowID string
}
// resolveCodexFingerprintIDs 按收敛模式计算出站 ID 集合。
// clientSessionID 是客户端原始的 session-id 头值(连字符形式),用于 session 模式下
// 的 thread_id 派生——每个真实 Codex 会话得到一个独立线程。
// 返回 nil 表示 off 模式,不需要改写。
// 注意:包含随机生成的 turn_id,调用方必须只调用一次并共享结果给头改写和体改写。
func resolveCodexFingerprintIDs(account *Account, clientSessionID string, mode codexFingerprintMode) *codexFingerprintIDs {
if mode == codexFingerprintOff {
return nil
}
ids := &codexFingerprintIDs{mode: mode}
ids.installationID = resolveConvergedInstallationID(account)
if ids.installationID == "" {
return nil
}
switch mode {
case codexFingerprintDevice:
return ids
case codexFingerprintSession:
ids.sessionID = resolveConvergedSessionID(account)
ids.threadID = resolveConvergedThreadID(account, clientSessionID)
if ids.threadID == "" {
ids.threadID = ids.sessionID
}
ids.turnID = uuid.Must(uuid.NewV7()).String()
ids.windowID = ids.threadID + ":0"
return ids
case codexFingerprintFull:
ids.sessionID = resolveConvergedSessionID(account)
ids.threadID = ids.sessionID
ids.turnID = uuid.Must(uuid.NewV7()).String()
ids.windowID = ids.threadID + ":0"
return ids
}
return nil
}
// extractClientSessionID 从请求头中提取客户端原始的会话标识。
// 优先取 session-id(连字符形式,Codex CLI 标准),回退到 session_id(下划线形式)。
// 返回的值尚未被 isolateOpenAISessionID 改写,是客户端的真实标识。
func extractClientSessionID(h http.Header) string {
if v := strings.TrimSpace(h.Get("session-id")); v != "" {
return v
}
return strings.TrimSpace(h.Get("session_id"))
}
// resolveCodexFingerprintIDsFromRequest 从客户端原始请求头中提取 session-id,
// 结合账号配置一次性解析收敛 ID 集合。调用方应将返回的 ids 同时传给
// applyCodexFingerprintHeaders 和 applyCodexFingerprintClientMetadata。
func resolveCodexFingerprintIDsFromRequest(account *Account, clientHeaders http.Header) *codexFingerprintIDs {
if account == nil {
return nil
}
mode := account.GetCodexFingerprintMode()
if mode == codexFingerprintOff {
return nil
}
clientSessionID := ""
if clientHeaders != nil {
clientSessionID = extractClientSessionID(clientHeaders)
}
return resolveCodexFingerprintIDs(account, clientSessionID, mode)
}
// applyCodexFingerprintHeaders 按预计算的收敛 ID 改写出站 HTTP 头中的设备指纹。
// 在 buildUpstreamRequest 的白名单透传之后、enforceCodexIdentityHeaders 之前调用。
func applyCodexFingerprintHeaders(h http.Header, ids *codexFingerprintIDs) {
if h == nil || ids == nil {
return
}
// 所有非 off 模式都收敛 installation_id
h.Set("x-codex-installation-id", ids.installationID)
if ids.mode == codexFingerprintDevice {
rewriteCodexTurnMetadataFields(h, map[string]any{
"installation_id": ids.installationID,
})
return
}
// session / full 模式:改写所有相关头
h.Set("x-codex-window-id", ids.windowID)
h.Set("x-client-request-id", ids.threadID)
// 连字符形式和下划线形式都改写,保证一致
h.Set("session-id", ids.sessionID)
h.Set("session_id", ids.sessionID)
h.Set("thread-id", ids.threadID)
rewriteCodexTurnMetadataFields(h, map[string]any{
"installation_id": ids.installationID,
"session_id": ids.sessionID,
"thread_id": ids.threadID,
"turn_id": ids.turnID,
"window_id": ids.windowID,
"turn_started_at_unix_ms": time.Now().UnixMilli(),
})
}
// rewriteCodexTurnMetadataFields 解析 x-codex-turn-metadata 头中的 JSON,
// 替换指定字段后回写。保留未指定字段原样(如 sandbox、thread_source 等)。
func rewriteCodexTurnMetadataFields(h http.Header, fields map[string]any) {
raw := strings.TrimSpace(h.Get("x-codex-turn-metadata"))
if raw == "" {
return
}
var metadata map[string]any
if err := json.Unmarshal([]byte(raw), &metadata); err != nil {
return
}
for k, v := range fields {
metadata[k] = v
}
rebuilt, err := json.Marshal(metadata)
if err != nil {
return
}
h.Set("x-codex-turn-metadata", string(rebuilt))
}
// applyCodexFingerprintClientMetadata 按预计算的收敛 ID 改写请求体中的 client_metadata。
// 使用与头改写相同的 ids 实例,确保 turn_id 等随机字段一致。
func applyCodexFingerprintClientMetadata(reqBody map[string]any, ids *codexFingerprintIDs) bool {
if reqBody == nil || ids == nil {
return false
}
existing, _ := reqBody["client_metadata"].(map[string]any)
if existing == nil {
existing = make(map[string]any)
}
modified := false
if ids.installationID != "" {
existing["x-codex-installation-id"] = ids.installationID
modified = true
}
if ids.mode == codexFingerprintDevice {
rewriteClientMetadataEmbeddedTurnMetadata(existing, map[string]any{
"installation_id": ids.installationID,
})
if modified {
reqBody["client_metadata"] = existing
}
return modified
}
// session / full 模式
existing["session_id"] = ids.sessionID
existing["thread_id"] = ids.threadID
existing["turn_id"] = ids.turnID
existing["x-codex-window-id"] = ids.windowID
rewriteClientMetadataEmbeddedTurnMetadata(existing, map[string]any{
"installation_id": ids.installationID,
"session_id": ids.sessionID,
"thread_id": ids.threadID,
"turn_id": ids.turnID,
"window_id": ids.windowID,
"turn_started_at_unix_ms": time.Now().UnixMilli(),
})
reqBody["client_metadata"] = existing
return true
}
// rewriteClientMetadataEmbeddedTurnMetadata 改写 client_metadata 中内嵌的
// x-codex-turn-metadata JSON 字符串里的指定字段。
func rewriteClientMetadataEmbeddedTurnMetadata(clientMetadata map[string]any, fields map[string]any) {
raw, ok := clientMetadata["x-codex-turn-metadata"].(string)
if !ok || raw == "" {
return
}
var metadata map[string]any
if err := json.Unmarshal([]byte(raw), &metadata); err != nil {
return
}
for k, v := range fields {
metadata[k] = v
}
if rebuilt, err := json.Marshal(metadata); err == nil {
clientMetadata["x-codex-turn-metadata"] = string(rebuilt)
}
}
@@ -0,0 +1,447 @@
package service
import (
"encoding/json"
"net/http"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func newTestOAuthAccount(id int64, extra map[string]any) *Account {
return &Account{
ID: id,
Platform: PlatformOpenAI,
Type: AccountTypeOAuth,
Extra: extra,
}
}
// --- deriveStableUUIDv4 ---
func TestDeriveStableUUIDv4_Deterministic(t *testing.T) {
a := deriveStableUUIDv4("test-seed-1")
b := deriveStableUUIDv4("test-seed-1")
assert.Equal(t, a, b, "同一种子应返回相同结果")
}
func TestDeriveStableUUIDv4_DifferentSeeds(t *testing.T) {
a := deriveStableUUIDv4("seed-a")
b := deriveStableUUIDv4("seed-b")
assert.NotEqual(t, a, b, "不同种子应返回不同结果")
}
func TestDeriveStableUUIDv4_ValidFormat(t *testing.T) {
result := deriveStableUUIDv4("test-seed")
parsed, err := uuid.Parse(result)
require.NoError(t, err, "应返回合法 UUID 格式")
assert.Equal(t, uuid.Version(4), parsed.Version(), "应为 UUIDv4")
assert.Equal(t, uuid.RFC4122, parsed.Variant(), "应为 RFC4122 变体")
}
// --- GetCodexFingerprintMode ---
func TestGetCodexFingerprintMode(t *testing.T) {
tests := []struct {
name string
account *Account
expected codexFingerprintMode
}{
{"nil 账号", nil, codexFingerprintOff},
{"非 OAuth 账号", &Account{Platform: PlatformOpenAI, Type: "api_key"}, codexFingerprintOff},
{"无 extra 默认 session", newTestOAuthAccount(1, nil), codexFingerprintSession},
{"空值默认 session", newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: ""}), codexFingerprintSession},
{"非法值默认 session", newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: "invalid"}), codexFingerprintSession},
{"显式 off", newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: "off"}), codexFingerprintOff},
{"device", newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: "device"}), codexFingerprintDevice},
{"session", newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: "session"}), codexFingerprintSession},
{"full", newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: "full"}), codexFingerprintFull},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, tt.account.GetCodexFingerprintMode())
})
}
}
// --- resolveConvergedInstallationID ---
func TestResolveConvergedInstallationID_UsesDeviceID(t *testing.T) {
account := newTestOAuthAccount(1, map[string]any{"openai_device_id": "real-device-id"})
assert.Equal(t, "real-device-id", resolveConvergedInstallationID(account))
}
func TestResolveConvergedInstallationID_DerivesFromAccountID(t *testing.T) {
account := newTestOAuthAccount(42, nil)
result := resolveConvergedInstallationID(account)
_, err := uuid.Parse(result)
require.NoError(t, err, "派生值应为合法 UUID")
assert.Equal(t, result, resolveConvergedInstallationID(account), "确定性")
}
func TestResolveConvergedInstallationID_DifferentAccounts(t *testing.T) {
a := resolveConvergedInstallationID(newTestOAuthAccount(1, nil))
b := resolveConvergedInstallationID(newTestOAuthAccount(2, nil))
assert.NotEqual(t, a, b)
}
// --- resolveConvergedThreadID ---
func TestResolveConvergedThreadID_PerClientSession(t *testing.T) {
account := newTestOAuthAccount(1, nil)
a := resolveConvergedThreadID(account, "session-aaa")
b := resolveConvergedThreadID(account, "session-bbb")
assert.NotEqual(t, a, b, "不同客户端 session 应得到不同 thread_id")
}
func TestResolveConvergedThreadID_Deterministic(t *testing.T) {
account := newTestOAuthAccount(1, nil)
a := resolveConvergedThreadID(account, "session-aaa")
b := resolveConvergedThreadID(account, "session-aaa")
assert.Equal(t, a, b, "同一客户端 session 应得到相同 thread_id")
}
func TestResolveConvergedThreadID_EmptySession(t *testing.T) {
account := newTestOAuthAccount(1, nil)
assert.Equal(t, "", resolveConvergedThreadID(account, ""))
}
// --- off 模式:resolveCodexFingerprintIDsFromRequest 返回 nil ---
func TestResolveCodexFingerprintIDsFromRequest_ExplicitOff(t *testing.T) {
account := newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: "off"})
ids := resolveCodexFingerprintIDsFromRequest(account, nil)
assert.Nil(t, ids, "显式 off 模式应返回 nil")
}
func TestResolveCodexFingerprintIDsFromRequest_DefaultIsSession(t *testing.T) {
account := newTestOAuthAccount(1, nil)
ids := resolveCodexFingerprintIDsFromRequest(account, nil)
require.NotNil(t, ids, "无 extra 默认 session 模式,应返回非 nil")
assert.Equal(t, codexFingerprintSession, ids.mode)
assert.NotEmpty(t, ids.sessionID)
assert.NotEmpty(t, ids.turnID)
}
// --- applyCodexFingerprintHeaders: off 模式 ---
func TestApplyCodexFingerprintHeaders_OffMode(t *testing.T) {
h := http.Header{}
h.Set("x-codex-installation-id", "original-install-id")
h.Set("x-codex-window-id", "original-window-id")
applyCodexFingerprintHeaders(h, nil)
assert.Equal(t, "original-install-id", h.Get("x-codex-installation-id"), "nil ids 不改写")
assert.Equal(t, "original-window-id", h.Get("x-codex-window-id"), "nil ids 不改写")
}
// --- applyCodexFingerprintHeaders: device 模式 ---
func TestApplyCodexFingerprintHeaders_DeviceMode(t *testing.T) {
account := newTestOAuthAccount(1, map[string]any{
codexFingerprintModeExtraKey: "device",
"openai_device_id": "converged-device",
})
turnMetadata := `{"installation_id":"user-install","session_id":"user-session","sandbox":"seccomp"}`
h := http.Header{}
h.Set("x-codex-installation-id", "user-install")
h.Set("x-codex-window-id", "user-window:0")
h.Set("x-codex-turn-metadata", turnMetadata)
ids := resolveCodexFingerprintIDsFromRequest(account, nil)
applyCodexFingerprintHeaders(h, ids)
assert.Equal(t, "converged-device", h.Get("x-codex-installation-id"), "installation_id 应收敛")
assert.Equal(t, "user-window:0", h.Get("x-codex-window-id"), "device 模式不改写 window_id")
var meta map[string]any
require.NoError(t, json.Unmarshal([]byte(h.Get("x-codex-turn-metadata")), &meta))
assert.Equal(t, "converged-device", meta["installation_id"])
assert.Equal(t, "user-session", meta["session_id"], "device 模式不改写 session_id")
assert.Equal(t, "seccomp", meta["sandbox"], "非指纹字段保留原样")
}
// --- applyCodexFingerprintHeaders: session 模式 ---
func TestApplyCodexFingerprintHeaders_SessionMode(t *testing.T) {
account := newTestOAuthAccount(1, map[string]any{
codexFingerprintModeExtraKey: "session",
})
clientHeaders := http.Header{}
clientHeaders.Set("session-id", "client-session-aaa")
turnMetadata := `{"installation_id":"user-install","session_id":"user-session","thread_id":"user-thread","turn_id":"user-turn","window_id":"user-thread:0","sandbox":"seccomp","thread_source":"user"}`
h := http.Header{}
h.Set("x-codex-installation-id", "user-install")
h.Set("x-codex-window-id", "user-thread:0")
h.Set("x-codex-turn-metadata", turnMetadata)
h.Set("x-client-request-id", "user-thread")
ids := resolveCodexFingerprintIDsFromRequest(account, clientHeaders)
applyCodexFingerprintHeaders(h, ids)
convergedInstall := resolveConvergedInstallationID(account)
convergedSession := resolveConvergedSessionID(account)
convergedThread := resolveConvergedThreadID(account, "client-session-aaa")
assert.Equal(t, convergedInstall, h.Get("x-codex-installation-id"))
assert.Equal(t, convergedSession, h.Get("session-id"))
assert.Equal(t, convergedSession, h.Get("session_id"), "下划线形式也应被改写")
assert.Equal(t, convergedThread, h.Get("thread-id"))
assert.Equal(t, convergedThread, h.Get("x-client-request-id"))
assert.Equal(t, convergedThread+":0", h.Get("x-codex-window-id"))
var meta map[string]any
require.NoError(t, json.Unmarshal([]byte(h.Get("x-codex-turn-metadata")), &meta))
assert.Equal(t, convergedInstall, meta["installation_id"])
assert.Equal(t, convergedSession, meta["session_id"])
assert.Equal(t, convergedThread, meta["thread_id"])
assert.NotEqual(t, "user-turn", meta["turn_id"], "turn_id 应被新生成的值替换")
assert.Equal(t, "seccomp", meta["sandbox"], "sandbox 保留原样")
assert.Equal(t, "user", meta["thread_source"], "thread_source 保留原样")
}
// --- session 模式:不同客户端得到不同 thread ---
func TestApplyCodexFingerprintHeaders_SessionMode_DifferentClients(t *testing.T) {
account := newTestOAuthAccount(1, map[string]any{
codexFingerprintModeExtraKey: "session",
})
makeTurnMeta := func() string {
return `{"installation_id":"x","session_id":"x","thread_id":"x","turn_id":"x","window_id":"x:0"}`
}
clientA := http.Header{}
clientA.Set("session-id", "client-A")
idsA := resolveCodexFingerprintIDsFromRequest(account, clientA)
hA := http.Header{}
hA.Set("x-codex-turn-metadata", makeTurnMeta())
applyCodexFingerprintHeaders(hA, idsA)
clientB := http.Header{}
clientB.Set("session-id", "client-B")
idsB := resolveCodexFingerprintIDsFromRequest(account, clientB)
hB := http.Header{}
hB.Set("x-codex-turn-metadata", makeTurnMeta())
applyCodexFingerprintHeaders(hB, idsB)
assert.Equal(t, hA.Get("session-id"), hB.Get("session-id"), "session_id 应相同")
assert.NotEqual(t, hA.Get("thread-id"), hB.Get("thread-id"), "不同客户端 thread_id 应不同")
assert.NotEqual(t, hA.Get("x-codex-window-id"), hB.Get("x-codex-window-id"), "不同客户端 window_id 应不同")
assert.Equal(t, hA.Get("x-codex-installation-id"), hB.Get("x-codex-installation-id"))
}
// --- full 模式 ---
func TestApplyCodexFingerprintHeaders_FullMode(t *testing.T) {
account := newTestOAuthAccount(1, map[string]any{
codexFingerprintModeExtraKey: "full",
})
convergedSession := resolveConvergedSessionID(account)
clientA := http.Header{}
clientA.Set("session-id", "client-A")
idsA := resolveCodexFingerprintIDsFromRequest(account, clientA)
hA := http.Header{}
hA.Set("x-codex-turn-metadata", `{"installation_id":"x","session_id":"x","thread_id":"x","turn_id":"x","window_id":"x:0"}`)
applyCodexFingerprintHeaders(hA, idsA)
clientB := http.Header{}
clientB.Set("session-id", "client-B")
idsB := resolveCodexFingerprintIDsFromRequest(account, clientB)
hB := http.Header{}
hB.Set("x-codex-turn-metadata", `{"installation_id":"x","session_id":"x","thread_id":"x","turn_id":"x","window_id":"x:0"}`)
applyCodexFingerprintHeaders(hB, idsB)
assert.Equal(t, hA.Get("thread-id"), hB.Get("thread-id"), "full 模式 thread_id 应相同")
assert.Equal(t, convergedSession, hA.Get("thread-id"), "full 模式 thread_id 应等于 session_id")
assert.Equal(t, hA.Get("x-codex-window-id"), hB.Get("x-codex-window-id"), "full 模式 window_id 应相同")
}
// --- H1 修复验证:头和体的 turn_id 一致性 ---
func TestFingerprintIDs_HeaderAndBody_TurnID_Consistent(t *testing.T) {
account := newTestOAuthAccount(1, map[string]any{
codexFingerprintModeExtraKey: "session",
})
clientHeaders := http.Header{}
clientHeaders.Set("session-id", "client-session-xyz")
ids := resolveCodexFingerprintIDsFromRequest(account, clientHeaders)
require.NotNil(t, ids)
// 头改写
h := http.Header{}
h.Set("x-codex-turn-metadata", `{"installation_id":"x","session_id":"x","thread_id":"x","turn_id":"x","window_id":"x:0"}`)
applyCodexFingerprintHeaders(h, ids)
// 体改写(使用同一份 ids)
reqBody := map[string]any{
"client_metadata": map[string]any{
"x-codex-installation-id": "x",
"session_id": "x",
"turn_id": "x",
"x-codex-turn-metadata": `{"installation_id":"x","session_id":"x","thread_id":"x","turn_id":"x","window_id":"x:0"}`,
},
}
applyCodexFingerprintClientMetadata(reqBody, ids)
// 从头 turn-metadata JSON 提取 turn_id
var headerMeta map[string]any
require.NoError(t, json.Unmarshal([]byte(h.Get("x-codex-turn-metadata")), &headerMeta))
headerTurnID := headerMeta["turn_id"].(string)
// 从体 client_metadata 提取 turn_id
cm := reqBody["client_metadata"].(map[string]any)
bodyTurnID := cm["turn_id"].(string)
// 从体内嵌 turn-metadata JSON 提取 turn_id
var bodyMeta map[string]any
require.NoError(t, json.Unmarshal([]byte(cm["x-codex-turn-metadata"].(string)), &bodyMeta))
bodyEmbeddedTurnID := bodyMeta["turn_id"].(string)
assert.Equal(t, headerTurnID, bodyTurnID, "头和体的 turn_id 必须一致")
assert.Equal(t, headerTurnID, bodyEmbeddedTurnID, "头和体内嵌 turn-metadata 的 turn_id 必须一致")
assert.Equal(t, ids.turnID, headerTurnID, "所有 turn_id 都应来自同一份 ids")
}
// --- applyCodexFingerprintClientMetadata ---
func TestApplyCodexFingerprintClientMetadata_OffMode(t *testing.T) {
reqBody := map[string]any{
"client_metadata": map[string]any{
"x-codex-installation-id": "original",
},
}
modified := applyCodexFingerprintClientMetadata(reqBody, nil)
assert.False(t, modified, "nil ids 不改写")
}
func TestApplyCodexFingerprintClientMetadata_DeviceMode(t *testing.T) {
account := newTestOAuthAccount(1, map[string]any{
codexFingerprintModeExtraKey: "device",
"openai_device_id": "converged-device",
})
ids := resolveCodexFingerprintIDsFromRequest(account, nil)
require.NotNil(t, ids)
embeddedMeta := `{"installation_id":"x","session_id":"user-session","sandbox":"seccomp"}`
reqBody := map[string]any{
"client_metadata": map[string]any{
"x-codex-installation-id": "original-install",
"session_id": "user-session",
"x-codex-turn-metadata": embeddedMeta,
},
}
modified := applyCodexFingerprintClientMetadata(reqBody, ids)
require.True(t, modified)
cm := reqBody["client_metadata"].(map[string]any)
assert.Equal(t, "converged-device", cm["x-codex-installation-id"])
assert.Equal(t, "user-session", cm["session_id"], "device 模式不改 session_id")
var meta map[string]any
require.NoError(t, json.Unmarshal([]byte(cm["x-codex-turn-metadata"].(string)), &meta))
assert.Equal(t, "converged-device", meta["installation_id"])
assert.Equal(t, "seccomp", meta["sandbox"], "非指纹字段保留原样")
}
func TestApplyCodexFingerprintClientMetadata_SessionMode(t *testing.T) {
account := newTestOAuthAccount(1, map[string]any{
codexFingerprintModeExtraKey: "session",
})
clientHeaders := http.Header{}
clientHeaders.Set("session-id", "client-session-aaa")
ids := resolveCodexFingerprintIDsFromRequest(account, clientHeaders)
require.NotNil(t, ids)
embeddedMeta := `{"installation_id":"x","session_id":"x","thread_id":"x","turn_id":"x","window_id":"x:0","sandbox":"seccomp"}`
reqBody := map[string]any{
"client_metadata": map[string]any{
"x-codex-installation-id": "original-install",
"session_id": "original-session",
"x-codex-turn-metadata": embeddedMeta,
},
}
modified := applyCodexFingerprintClientMetadata(reqBody, ids)
require.True(t, modified)
cm := reqBody["client_metadata"].(map[string]any)
convergedInstall := resolveConvergedInstallationID(account)
convergedSession := resolveConvergedSessionID(account)
convergedThread := resolveConvergedThreadID(account, "client-session-aaa")
assert.Equal(t, convergedInstall, cm["x-codex-installation-id"])
assert.Equal(t, convergedSession, cm["session_id"])
assert.Equal(t, convergedThread, cm["thread_id"])
assert.Equal(t, convergedThread+":0", cm["x-codex-window-id"])
var meta map[string]any
require.NoError(t, json.Unmarshal([]byte(cm["x-codex-turn-metadata"].(string)), &meta))
assert.Equal(t, convergedInstall, meta["installation_id"])
assert.Equal(t, convergedSession, meta["session_id"])
assert.Equal(t, "seccomp", meta["sandbox"], "非指纹字段保留原样")
}
func TestApplyCodexFingerprintClientMetadata_FullMode(t *testing.T) {
account := newTestOAuthAccount(1, map[string]any{
codexFingerprintModeExtraKey: "full",
})
clientHeaders := http.Header{}
clientHeaders.Set("session-id", "any-client")
ids := resolveCodexFingerprintIDsFromRequest(account, clientHeaders)
require.NotNil(t, ids)
reqBody := map[string]any{
"client_metadata": map[string]any{
"session_id": "x",
"thread_id": "x",
"x-codex-turn-metadata": `{"installation_id":"x","session_id":"x","thread_id":"x","turn_id":"x","window_id":"x:0"}`,
},
}
modified := applyCodexFingerprintClientMetadata(reqBody, ids)
require.True(t, modified)
cm := reqBody["client_metadata"].(map[string]any)
convergedSession := resolveConvergedSessionID(account)
assert.Equal(t, convergedSession, cm["session_id"])
assert.Equal(t, convergedSession, cm["thread_id"], "full 模式 thread_id 应等于 session_id")
}
// --- extractClientSessionID ---
func TestExtractClientSessionID(t *testing.T) {
tests := []struct {
name string
headers http.Header
expected string
}{
{"连字符形式优先", func() http.Header {
h := http.Header{}
h.Set("session-id", "hyphen-form")
h.Set("session_id", "underscore-form")
return h
}(), "hyphen-form"},
{"回退到下划线形式", func() http.Header {
h := http.Header{}
h.Set("session_id", "underscore-form")
return h
}(), "underscore-form"},
{"都没有", http.Header{}, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, extractClientSessionID(tt.headers))
})
}
}
@@ -414,6 +414,24 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
if !isCompactRequest && applyCodexClientMetadata(decoded, account) {
markDecodedModified()
}
// 指纹收敛:一次性解析收敛 ID,请求体和出站头共享同一份 IDs(保证 turn_id 等随机字段一致)。
// fingerprintIDs 在此处解析,后续 buildUpstreamRequest 中使用同一份。
if !isCompactRequest {
var clientHeaders http.Header
if c != nil && c.Request != nil {
clientHeaders = c.Request.Header
}
fpIDs := resolveCodexFingerprintIDsFromRequest(account, clientHeaders)
if fpIDs != nil {
if applyCodexFingerprintClientMetadata(decoded, fpIDs) {
markDecodedModified()
}
}
// 将 fpIDs 存入 gin context,供 buildUpstreamRequest 中头改写使用
if c != nil && fpIDs != nil {
c.Set("codex_fingerprint_ids", fpIDs)
}
}
if codexResult.NormalizedModel != "" {
upstreamModel = codexResult.NormalizedModel
}
@@ -1119,6 +1137,15 @@ func (s *OpenAIGatewayService) buildUpstreamRequest(ctx context.Context, c *gin.
req.Header.Set("user-agent", codexCLIUserAgent)
}
// 指纹收敛:使用 Forward() 中预计算的收敛 ID 改写出站头,与请求体使用同一份 IDs。
if account.Type == AccountTypeOAuth && c != nil {
if fpIDs, ok := c.Get("codex_fingerprint_ids"); ok {
if ids, ok := fpIDs.(*codexFingerprintIDs); ok {
applyCodexFingerprintHeaders(req.Header, ids)
}
}
}
// 终态收口:强制统一 OAuth 出站身份(User-Agent / originator / version 同源自洽)。
// 客户端自报身份不参与构造,浏览器型 UA 也因此不会再到达上游(原浏览器 UA 兜底已被吸收)。
if account.Type == AccountTypeOAuth {
@@ -913,6 +913,24 @@
</div>
</div>
<!-- Codex 指纹收敛模式(仅 OpenAI OAuth) -->
<div v-if="allOpenAIOAuth" class="border-t border-gray-200 pt-4 dark:border-dark-600">
<div class="mb-3 flex items-center justify-between">
<label class="input-label mb-0">{{ t('admin.accounts.openai.codexFingerprintMode') }}</label>
<input
v-model="enableCodexFingerprintMode"
type="checkbox"
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/>
</div>
<div :class="!enableCodexFingerprintMode && 'pointer-events-none opacity-50'">
<p class="mb-2 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.openai.codexFingerprintModeDesc') }}
</p>
<Select v-model="codexFingerprintMode" data-testid="bulk-codex-fingerprint-mode-select" :options="codexFingerprintModeOptions" />
</div>
</div>
<!-- Upstream billing auto probe (any API-key platform) -->
<div v-if="allBillingProbeCapable" class="border-t border-gray-200 pt-4 dark:border-dark-600">
<div class="mb-3 flex items-center justify-between">
@@ -1515,6 +1533,15 @@ const openaiAPIKeyResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OF
const upstreamBillingAutoProbeMode = ref<'enabled' | 'disabled'>('enabled')
const codexCLIOnlyEnabled = ref(false)
const codexCLIOnlyAppServerEnabled = ref(false)
type CodexFingerprintMode = 'off' | 'device' | 'session' | 'full'
const enableCodexFingerprintMode = ref(false)
const codexFingerprintMode = ref<CodexFingerprintMode>('session')
const codexFingerprintModeOptions = computed(() => [
{ value: 'off' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintOff') },
{ value: 'device' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintDevice') },
{ value: 'session' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintSession') },
{ value: 'full' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintFull') },
])
const openAICompactMode = ref<OpenAICompactMode>('auto')
const openAICompactModelMappings = ref<ModelMapping[]>([])
const rpmLimitEnabled = ref(false)
@@ -1800,6 +1827,15 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
extra.codex_cli_only_allow_app_server = codexCLIOnlyAppServerEnabled.value
}
if (enableCodexFingerprintMode.value) {
const extra = ensureExtra()
if (codexFingerprintMode.value !== 'session') {
extra.codex_fingerprint_mode = codexFingerprintMode.value
} else {
delete extra.codex_fingerprint_mode
}
}
if (enableOpenAICompactMode.value) {
const extra = ensureExtra()
extra.openai_compact_mode = openAICompactMode.value
@@ -1910,6 +1946,7 @@ const handleSubmit = async () => {
enableUpstreamBillingAutoProbe.value ||
enableCodexCLIOnly.value ||
enableCodexCLIOnlyAppServer.value ||
enableCodexFingerprintMode.value ||
enableOpenAICompactMode.value ||
enableOpenAICompactModelMapping.value ||
enableRpmLimit.value ||
@@ -2044,6 +2081,8 @@ watch(
enableUpstreamBillingAutoProbe.value = false
enableCodexCLIOnly.value = false
enableCodexCLIOnlyAppServer.value = false
enableCodexFingerprintMode.value = false
codexFingerprintMode.value = 'session'
enableOpenAICompactMode.value = false
enableOpenAICompactModelMapping.value = false
enableRpmLimit.value = false
@@ -3003,6 +3003,24 @@
</div>
</div>
<!-- Codex 指纹收敛模式(仅 OpenAI OAuth) -->
<div
v-if="form.platform === 'openai' && accountCategory === 'oauth-based'"
class="border-t border-gray-200 pt-4 dark:border-dark-600"
>
<div class="flex items-center justify-between gap-4">
<div class="min-w-0">
<label class="input-label mb-0">{{ t('admin.accounts.openai.codexFingerprintMode') }}</label>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.openai.codexFingerprintModeDesc') }}
</p>
</div>
<div class="w-52 flex-shrink-0">
<Select v-model="codexFingerprintMode" data-testid="create-codex-fingerprint-mode-select" :options="codexFingerprintModeOptions" />
</div>
</div>
</div>
<!-- OpenAI Compact 能力配置 -->
<div
v-if="form.platform === 'openai' && (accountCategory === 'oauth-based' || accountCategory === 'apikey')"
@@ -3836,6 +3854,14 @@ const openaiOAuthResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF
const openaiAPIKeyResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
const codexCLIOnlyEnabled = ref(false)
const codexCLIOnlyAppServerEnabled = ref(false)
type CodexFingerprintMode = 'off' | 'device' | 'session' | 'full'
const codexFingerprintMode = ref<CodexFingerprintMode>('session')
const codexFingerprintModeOptions = computed(() => [
{ value: 'off' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintOff') },
{ value: 'device' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintDevice') },
{ value: 'session' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintSession') },
{ value: 'full' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintFull') },
])
type AnthropicAPIKeyAuthScheme = 'x_api_key' | 'authorization_bearer'
const anthropicPassthroughEnabled = ref(false)
const anthropicAPIKeyAuthScheme = ref<AnthropicAPIKeyAuthScheme>('x_api_key')
@@ -4715,6 +4741,7 @@ const resetForm = () => {
openaiAPIKeyResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
codexCLIOnlyEnabled.value = false
codexCLIOnlyAppServerEnabled.value = false
codexFingerprintMode.value = 'session'
anthropicPassthroughEnabled.value = false
anthropicAPIKeyAuthScheme.value = 'x_api_key'
webSearchEmulationMode.value = 'default'
@@ -4813,6 +4840,11 @@ const buildOpenAIExtra = (base?: Record<string, unknown>): Record<string, unknow
} else {
delete extra.codex_cli_only_allow_app_server
}
if (codexFingerprintMode.value !== 'session') {
extra.codex_fingerprint_mode = codexFingerprintMode.value
} else {
delete extra.codex_fingerprint_mode
}
if (openAICompactMode.value !== 'auto') {
extra.openai_compact_mode = openAICompactMode.value
} else {
@@ -2001,6 +2001,24 @@
</div>
</div>
<!-- Codex 指纹收敛模式(仅 OpenAI OAuth) -->
<div
v-if="account?.platform === 'openai' && account?.type === 'oauth'"
class="border-t border-gray-200 pt-4 dark:border-dark-600"
>
<div class="flex items-center justify-between gap-4">
<div class="min-w-0">
<label class="input-label mb-0">{{ t('admin.accounts.openai.codexFingerprintMode') }}</label>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.openai.codexFingerprintModeDesc') }}
</p>
</div>
<div class="w-52 flex-shrink-0">
<Select v-model="codexFingerprintMode" data-testid="edit-codex-fingerprint-mode-select" :options="codexFingerprintModeOptions" />
</div>
</div>
</div>
<!-- OpenAI 订阅档位手动覆盖(Plus/Pro/Free),仅 OAuth 非影子账号 -->
<div
v-if="account?.platform === 'openai' && account?.type === 'oauth' && !isSparkShadow"
@@ -2961,6 +2979,8 @@ const openaiOAuthResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF
const openaiAPIKeyResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
const codexCLIOnlyEnabled = ref(false)
const codexCLIOnlyAppServerEnabled = ref(false)
type CodexFingerprintMode = 'off' | 'device' | 'session' | 'full'
const codexFingerprintMode = ref<CodexFingerprintMode>('session')
type CodexImageToolMode = 'inherit' | 'enabled' | 'disabled' | 'block'
const codexImageToolMode = ref<CodexImageToolMode>('inherit')
type AnthropicAPIKeyAuthScheme = 'x_api_key' | 'authorization_bearer'
@@ -2992,6 +3012,13 @@ const editWeeklyResetMode = ref<'rolling' | 'fixed' | null>(null)
const editWeeklyResetDay = ref<number | null>(null)
const editWeeklyResetHour = ref<number | null>(null)
const editResetTimezone = ref<string | null>(null)
const codexFingerprintModeOptions = computed(() => [
{ value: 'off' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintOff') },
{ value: 'device' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintDevice') },
{ value: 'session' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintSession') },
{ value: 'full' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintFull') },
])
const openAIWSModeOptions = computed(() => [
{ value: OPENAI_WS_MODE_OFF, label: t('admin.accounts.openai.wsModeOff') },
{ value: OPENAI_WS_MODE_CTX_POOL, label: t('admin.accounts.openai.wsModeCtxPool') },
@@ -3413,6 +3440,7 @@ const syncFormFromAccount = (newAccount: Account | null) => {
openaiAPIKeyResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
codexCLIOnlyEnabled.value = false
codexCLIOnlyAppServerEnabled.value = false
codexFingerprintMode.value = 'session'
codexImageToolMode.value = 'inherit'
anthropicPassthroughEnabled.value = false
anthropicAPIKeyAuthScheme.value = 'x_api_key'
@@ -3464,6 +3492,12 @@ const syncFormFromAccount = (newAccount: Account | null) => {
codexCLIOnlyAppServerEnabled.value =
extra?.codex_cli_only_allow_app_server === true
}
if (newAccount.type === 'oauth') {
const fpMode = extra?.codex_fingerprint_mode as string | undefined
codexFingerprintMode.value = (['off', 'device', 'session', 'full'].includes(fpMode || '')
? fpMode as CodexFingerprintMode
: 'session')
}
const credentials = newAccount.credentials as Record<string, unknown> | undefined
const compactMappings = credentials?.compact_model_mapping as Record<string, string> | undefined
if (compactMappings && typeof compactMappings === 'object') {
@@ -4809,6 +4843,15 @@ const handleSubmit = async () => {
}
}
// 指纹收敛模式:默认 session,不写入;非默认值显式写入(包括 off)
if (props.account.type === 'oauth') {
if (codexFingerprintMode.value !== 'session') {
newExtra.codex_fingerprint_mode = codexFingerprintMode.value
} else {
delete newExtra.codex_fingerprint_mode
}
}
updatePayload.extra = newExtra
}
@@ -573,6 +573,12 @@ export default {
codexCLIOnlyAppServer: 'Allow Codex app-server clients',
codexCLIOnlyAppServerDesc:
"Effective only when the switch above is on. When enabled, this account also allows third-party clients that embed the Codex engine over the app-server protocol (e.g. Claude Code's codex plugin); they still pass the global engine-fingerprint gate. OR-combined with the global app-server toggle.",
codexFingerprintMode: 'Codex fingerprint convergence',
codexFingerprintModeDesc: 'When multiple users share the same OAuth account, converge device/session identifiers to account-level stable values to reduce upstream-visible device and session count. Off = pass through client identifiers as-is.',
codexFingerprintOff: 'Off (passthrough)',
codexFingerprintDevice: 'Device only',
codexFingerprintSession: 'Device + Session (recommended)',
codexFingerprintFull: 'Full convergence',
codexImageTool: 'Codex image bridge policy',
codexImageToolDesc:
'Controls the hosted image_generation bridge and client-declared image tools on Codex /responses text requests. Hosted auto-injection applies only to non-Responses Lite requests. Account policy takes precedence over channel and global settings; standalone image-generation endpoints are unaffected.',
@@ -643,6 +643,12 @@ export default {
codexCLIOnlyDesc: '仅对 OpenAI OAuth 生效。开启后仅允许 Codex 官方客户端家族访问;关闭后完全绕过并保持原逻辑。',
codexCLIOnlyAppServer: '允许 Codex app-server 客户端',
codexCLIOnlyAppServerDesc: '仅在上方开关开启时生效。开启后本账号额外放行内嵌 Codex 引擎、经 app-server 协议接入的第三方客户端(如 Claude Code 的 codex 插件),仍需通过全局引擎指纹门;与全局 app-server 开关取 OR(任一开即放行)。',
codexFingerprintMode: 'Codex 指纹收敛',
codexFingerprintModeDesc: '多人共享同一 OAuth 账号时,将各用户的设备/会话标识收敛为账号级恒定值,减少上游可见的设备数和会话数。关闭时原样透传客户端标识。',
codexFingerprintOff: '关闭(透传)',
codexFingerprintDevice: '仅设备',
codexFingerprintSession: '设备+会话(推荐)',
codexFingerprintFull: '完全收敛',
codexImageTool: 'Codex 图片桥接策略',
codexImageToolDesc:
'统一控制 Codex /responses 文本请求的 hosted image_generation 桥接和客户端图片工具声明。hosted 工具自动注入仅适用于非 Responses Lite 请求;账号级策略优先于渠道和全局配置,不影响独立图片生成接口。',