mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-01 08:28:34 +00:00
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:
@@ -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 请求;账号级策略优先于渠道和全局配置,不影响独立图片生成接口。',
|
||||
|
||||
Reference in New Issue
Block a user