fix(openai): make Codex fingerprint convergence opt-in and cover passthrough

Default codex_fingerprint_mode to off. v0.1.175 treated a missing key as
"session", so upgrading silently rewrote installation/session/thread/turn/
window identifiers for every existing OAuth account that had never configured
this field. The quota regressions in #5555, #5556 and #5582 line up with that
version boundary, with A/B reports that rolling back to v0.1.173 restores
quota. Convergence is now explicit opt-in (#5610).

Only accounts that never set the field change behaviour; explicit off /
device / session / full keep working exactly as configured. That required
flipping the persistence condition in all three account modals from
"!== 'session'" to "!== 'off'": the old rule deleted the key when it equalled
the default, which after the flip would have silently discarded an
administrator's explicit opt-in to session.

Also extend convergence to the passthrough path, which previously left client
identifiers untouched:

- resolve the ids once in forwardOpenAIPassthrough and rewrite
  client_metadata on the raw bytes (gjson extract + sjson splice) because
  passthrough is a hot path that must not fully unmarshal multi-MB bodies;
  a shared core keeps the raw and map variants from drifting
- both request builders apply the staged ids at the same relative position
  (after session isolation, before identity enforcement) so headers and body
  share one id set and turn_id stays consistent
- stage the ids unconditionally, including nil: a failover from a converged
  account to an off account must not leave the previous account's ids behind
This commit is contained in:
shaw
2026-08-15 16:35:26 +08:00
parent 8ae6d8f67e
commit fce41e318f
9 changed files with 392 additions and 45 deletions
@@ -1535,7 +1535,7 @@ const codexCLIOnlyEnabled = ref(false)
const codexCLIOnlyAppServerEnabled = ref(false)
type CodexFingerprintMode = 'off' | 'device' | 'session' | 'full'
const enableCodexFingerprintMode = ref(false)
const codexFingerprintMode = ref<CodexFingerprintMode>('session')
const codexFingerprintMode = ref<CodexFingerprintMode>('off')
const codexFingerprintModeOptions = computed(() => [
{ value: 'off' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintOff') },
{ value: 'device' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintDevice') },
@@ -1829,7 +1829,8 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
if (enableCodexFingerprintMode.value) {
const extra = ensureExtra()
if (codexFingerprintMode.value !== 'session') {
// off = 默认值,清键即可;device/session/full 是显式 opt-in,必须落键(#5610)。
if (codexFingerprintMode.value !== 'off') {
extra.codex_fingerprint_mode = codexFingerprintMode.value
} else {
delete extra.codex_fingerprint_mode
@@ -2082,7 +2083,7 @@ watch(
enableCodexCLIOnly.value = false
enableCodexCLIOnlyAppServer.value = false
enableCodexFingerprintMode.value = false
codexFingerprintMode.value = 'session'
codexFingerprintMode.value = 'off'
enableOpenAICompactMode.value = false
enableOpenAICompactModelMapping.value = false
enableRpmLimit.value = false
@@ -3855,7 +3855,7 @@ const openaiAPIKeyResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OF
const codexCLIOnlyEnabled = ref(false)
const codexCLIOnlyAppServerEnabled = ref(false)
type CodexFingerprintMode = 'off' | 'device' | 'session' | 'full'
const codexFingerprintMode = ref<CodexFingerprintMode>('session')
const codexFingerprintMode = ref<CodexFingerprintMode>('off')
const codexFingerprintModeOptions = computed(() => [
{ value: 'off' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintOff') },
{ value: 'device' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintDevice') },
@@ -4741,7 +4741,7 @@ const resetForm = () => {
openaiAPIKeyResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
codexCLIOnlyEnabled.value = false
codexCLIOnlyAppServerEnabled.value = false
codexFingerprintMode.value = 'session'
codexFingerprintMode.value = 'off'
anthropicPassthroughEnabled.value = false
anthropicAPIKeyAuthScheme.value = 'x_api_key'
webSearchEmulationMode.value = 'default'
@@ -4840,7 +4840,9 @@ const buildOpenAIExtra = (base?: Record<string, unknown>): Record<string, unknow
} else {
delete extra.codex_cli_only_allow_app_server
}
if (codexFingerprintMode.value !== 'session') {
// 收敛是显式 opt-in:off 即默认值,不落键;device/session/full 必须显式写入,
// 否则管理员的选择会被当成默认而丢失(#5610)。
if (codexFingerprintMode.value !== 'off') {
extra.codex_fingerprint_mode = codexFingerprintMode.value
} else {
delete extra.codex_fingerprint_mode
@@ -2980,7 +2980,7 @@ const openaiAPIKeyResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OF
const codexCLIOnlyEnabled = ref(false)
const codexCLIOnlyAppServerEnabled = ref(false)
type CodexFingerprintMode = 'off' | 'device' | 'session' | 'full'
const codexFingerprintMode = ref<CodexFingerprintMode>('session')
const codexFingerprintMode = ref<CodexFingerprintMode>('off')
type CodexImageToolMode = 'inherit' | 'enabled' | 'disabled' | 'block'
const codexImageToolMode = ref<CodexImageToolMode>('inherit')
type AnthropicAPIKeyAuthScheme = 'x_api_key' | 'authorization_bearer'
@@ -3440,7 +3440,7 @@ const syncFormFromAccount = (newAccount: Account | null) => {
openaiAPIKeyResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
codexCLIOnlyEnabled.value = false
codexCLIOnlyAppServerEnabled.value = false
codexFingerprintMode.value = 'session'
codexFingerprintMode.value = 'off'
codexImageToolMode.value = 'inherit'
anthropicPassthroughEnabled.value = false
anthropicAPIKeyAuthScheme.value = 'x_api_key'
@@ -3494,9 +3494,10 @@ const syncFormFromAccount = (newAccount: Account | null) => {
}
if (newAccount.type === 'oauth') {
const fpMode = extra?.codex_fingerprint_mode as string | undefined
// 缺省/非法值按 off 呈现,与后端 GetCodexFingerprintMode 的 opt-in 语义一致(#5610)
codexFingerprintMode.value = (['off', 'device', 'session', 'full'].includes(fpMode || '')
? fpMode as CodexFingerprintMode
: 'session')
: 'off')
}
const credentials = newAccount.credentials as Record<string, unknown> | undefined
const compactMappings = credentials?.compact_model_mapping as Record<string, string> | undefined
@@ -4843,9 +4844,10 @@ const handleSubmit = async () => {
}
}
// 指纹收敛模式:默认 session,不写入;非默认值显式写入(包括 off)
// 指纹收敛模式:默认 off(不写入);device/session/full 是显式 opt-in,
// 必须落键,否则管理员的选择会被后端当作"未设置"而回落到 off(#5610)。
if (props.account.type === 'oauth') {
if (codexFingerprintMode.value !== 'session') {
if (codexFingerprintMode.value !== 'off') {
newExtra.codex_fingerprint_mode = codexFingerprintMode.value
} else {
delete newExtra.codex_fingerprint_mode
@@ -574,10 +574,10 @@ export default {
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)',
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 by default (client identifiers pass through as-is); opt in explicitly when needed. Some accounts reported quota shrinkage after enabling convergence, so choose based on your own measurements.',
codexFingerprintOff: 'Off (passthrough, default)',
codexFingerprintDevice: 'Device only',
codexFingerprintSession: 'Device + Session (recommended)',
codexFingerprintSession: 'Device + Session',
codexFingerprintFull: 'Full convergence',
codexImageTool: 'Codex image bridge policy',
codexImageToolDesc:
@@ -644,10 +644,10 @@ export default {
codexCLIOnlyAppServer: '允许 Codex app-server 客户端',
codexCLIOnlyAppServerDesc: '仅在上方开关开启时生效。开启后本账号额外放行内嵌 Codex 引擎、经 app-server 协议接入的第三方客户端(如 Claude Code 的 codex 插件),仍需通过全局引擎指纹门;与全局 app-server 开关取 OR(任一开即放行)。',
codexFingerprintMode: 'Codex 指纹收敛',
codexFingerprintModeDesc: '多人共享同一 OAuth 账号时,将各用户的设备/会话标识收敛为账号级恒定值,减少上游可见的设备数和会话数。关闭时原样透传客户端标识。',
codexFingerprintOff: '关闭(透传)',
codexFingerprintModeDesc: '多人共享同一 OAuth 账号时,将各用户的设备/会话标识收敛为账号级恒定值,减少上游可见的设备数和会话数。默认关闭(原样透传客户端标识),需要时再显式开启;部分账号开启收敛后出现过额度缩水,请按自己的实测结果选择。',
codexFingerprintOff: '关闭(透传,默认)',
codexFingerprintDevice: '仅设备',
codexFingerprintSession: '设备+会话(推荐)',
codexFingerprintSession: '设备+会话',
codexFingerprintFull: '完全收敛',
codexImageTool: 'Codex 图片桥接策略',
codexImageToolDesc: