feat: add explicit account fingerprint option
This commit is contained in:
1 parent
5aa4a065f3
commit
6d3b5c0b1b
11 files changed
+47
-19
No files matched your search
@@ -101,6 +101,10 @@ GATEWAY_OPENAI_ACCOUNT_UNIQUE_FINGERPRINT_ENABLED=true
|
||||
它会按账号固定 Codex 设备指纹;单个账号可在 `extra` 中设置
|
||||
`codex_fingerprint_mode: off` 关闭。该开关只影响 OpenAI OAuth/Codex 账号,不影响 API Key。
|
||||
|
||||
账号编辑页的“Codex 指纹收敛”下拉框新增“账号唯一设备(新增,推荐)”选项。选择后会写入
|
||||
`codex_fingerprint_mode: account_device`,只固定该账号的设备指纹,不收敛会话和线程;
|
||||
不选择账号级覆盖时,仍由全局开关提供相同的默认行为。
|
||||
|
||||
### 3. 创建数据目录
|
||||
|
||||
```bash
|
||||
@@ -399,13 +403,13 @@ docker compose \
|
||||
|
||||
如果服务器目录是 `/opt/sub2api`,只需替换第一条路径。升级完成后重新执行“判断功能是否生效”中的镜像、环境变量和日志检查。
|
||||
|
||||
更新后可在管理后台点击刷新,版本应显示为类似 `v0.1.179-overdraft.2`,更新方式应提示源码构建使用 `git pull`。也可以直接检查容器内二进制版本:
|
||||
更新后可在管理后台点击刷新,版本应显示为类似 `v0.1.179-overdraft.3`,更新方式应提示源码构建使用 `git pull`。也可以直接检查容器内二进制版本:
|
||||
|
||||
```bash
|
||||
docker exec sub2api /app/sub2api -version
|
||||
```
|
||||
|
||||
维护者发布新的源码版本时必须递增 `FORK_VERSION`,例如从 `0.1.177-overdraft.3` 改为 `0.1.177-overdraft.4`。同步到新的上游 Sub2API 版本时,使用 `0.1.179-overdraft.2` 这样的版本号。
|
||||
维护者发布新的源码版本时必须递增 `FORK_VERSION`,例如从 `0.1.177-overdraft.3` 改为 `0.1.177-overdraft.4`。同步到新的上游 Sub2API 版本时,使用 `0.1.179-overdraft.3` 这样的版本号。
|
||||
|
||||
## 合并 Sub2API 官方更新
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
- 代码默认值:关闭;`deploy/config.example.yaml` 部署示例默认开启
|
||||
- OpenAI 账号固定指纹开关:`gateway.openai_account_unique_fingerprint_enabled`
|
||||
- 固定指纹开关默认开启;没有显式账号模式时按账号 ID 派生稳定设备指纹,账号 extra 中显式设置 `codex_fingerprint_mode: off` 可单独关闭
|
||||
- 账号编辑页新增 `账号唯一设备(新增,推荐)` 选项,对应 `codex_fingerprint_mode: account_device`;它只固定该账号的设备指纹,不改变会话/线程收敛
|
||||
- Fork 版本文件:`FORK_VERSION`
|
||||
- 更新源:`DeanZFC/sub2api-overdraft` 的 `codex-overdraft` 分支
|
||||
|
||||
@@ -18,7 +19,7 @@
|
||||
|
||||
源码 Docker 构建会读取根目录的 `FORK_VERSION`,并以 `BuildType=source` 写入二进制。后台更新服务通过 GitHub Contents API 读取 Fork 分支上的同名文件,使用语义化版本比较判断是否有新版本。Redis 缓存同时记录仓库和构建类型,因此旧的官方更新缓存不会继续生效。
|
||||
|
||||
源码构建仅显示 `git pull` 更新提示,`PerformUpdate`、指定版本回退和在线回退列表均被禁用,防止官方二进制覆盖透支功能。维护者每次发布 Fork 更新都必须递增 `FORK_VERSION`;当前版本为 `0.1.179-overdraft.2`。
|
||||
源码构建仅显示 `git pull` 更新提示,`PerformUpdate`、指定版本回退和在线回退列表均被禁用,防止官方二进制覆盖透支功能。维护者每次发布 Fork 更新都必须递增 `FORK_VERSION`;当前版本为 `0.1.179-overdraft.3`。
|
||||
|
||||
Sub2API `v0.1.179` 继续保留原生 remote compaction v2 在 `/responses` 路径。本定制同时检查旧 Compact 路径和原生 v2 请求信号,二者都不会开启额度透支调度或注入透支请求形态。
|
||||
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
0.1.179-overdraft.2
|
||||
0.1.179-overdraft.3
|
||||
@@ -88,6 +88,10 @@ const (
|
||||
// 账号 extra 未显式配置模式时,GetCodexFingerprintMode 返回此值;
|
||||
// 出站请求是否按全局开关提升到 device 模式由 resolveCodexFingerprintMode 决定。
|
||||
codexFingerprintOff codexFingerprintMode = "off"
|
||||
// codexFingerprintAccountDevice 使用账号 ID(已有系统种子优先)派生
|
||||
// 唯一且稳定的设备指纹,仅收敛 installation_id。这是全局默认行为的
|
||||
// 显式账号级选项,不要求把系统种子写入账号 extra。
|
||||
codexFingerprintAccountDevice codexFingerprintMode = "account_device"
|
||||
// codexFingerprintDevice 仅收敛 installation_id 为账号级恒定值。
|
||||
// 上游看到 1 台设备 + 多会话(每用户各自的 session)。
|
||||
codexFingerprintDevice codexFingerprintMode = "device"
|
||||
@@ -137,7 +141,7 @@ func codexFingerprintModeFromExtra(extra map[string]any) codexFingerprintMode {
|
||||
}
|
||||
raw, _ := extra[codexFingerprintModeExtraKey].(string)
|
||||
switch codexFingerprintMode(strings.TrimSpace(raw)) {
|
||||
case codexFingerprintOff, codexFingerprintDevice, codexFingerprintSession, codexFingerprintFull:
|
||||
case codexFingerprintOff, codexFingerprintAccountDevice, codexFingerprintDevice, codexFingerprintSession, codexFingerprintFull:
|
||||
return codexFingerprintMode(strings.TrimSpace(raw))
|
||||
default:
|
||||
return codexFingerprintOff
|
||||
@@ -239,7 +243,8 @@ func resolveCodexFingerprintMode(account *Account, enabled bool) (codexFingerpri
|
||||
}
|
||||
if account.Extra != nil {
|
||||
if _, configured := account.Extra[codexFingerprintModeExtraKey]; configured {
|
||||
return codexFingerprintModeFromExtra(account.Extra), false
|
||||
mode := codexFingerprintModeFromExtra(account.Extra)
|
||||
return mode, mode == codexFingerprintAccountDevice
|
||||
}
|
||||
}
|
||||
if enabled {
|
||||
@@ -361,7 +366,7 @@ func resolveCodexFingerprintIDsWithSeed(account *Account, clientSessionID string
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case codexFingerprintDevice:
|
||||
case codexFingerprintAccountDevice, codexFingerprintDevice:
|
||||
return ids
|
||||
|
||||
case codexFingerprintSession:
|
||||
@@ -433,7 +438,7 @@ func applyCodexFingerprintHeaders(h http.Header, ids *codexFingerprintIDs) {
|
||||
// 所有非 off 模式都收敛 installation_id
|
||||
h.Set("x-codex-installation-id", ids.installationID)
|
||||
|
||||
if ids.mode == codexFingerprintDevice {
|
||||
if ids.mode == codexFingerprintAccountDevice || ids.mode == codexFingerprintDevice {
|
||||
rewriteCodexTurnMetadataFields(h, map[string]any{
|
||||
"installation_id": ids.installationID,
|
||||
})
|
||||
@@ -519,7 +524,7 @@ func applyCodexFingerprintToClientMetadataMap(existing map[string]any, ids *code
|
||||
modified = true
|
||||
}
|
||||
|
||||
if ids.mode == codexFingerprintDevice {
|
||||
if ids.mode == codexFingerprintAccountDevice || ids.mode == codexFingerprintDevice {
|
||||
rewriteClientMetadataEmbeddedTurnMetadata(existing, map[string]any{
|
||||
"installation_id": ids.installationID,
|
||||
})
|
||||
|
||||
@@ -70,6 +70,7 @@ func TestGetCodexFingerprintMode(t *testing.T) {
|
||||
{"空值默认 off", newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: ""}), codexFingerprintOff},
|
||||
{"非法值默认 off", newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: "invalid"}), codexFingerprintOff},
|
||||
{"显式 off", newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: "off"}), codexFingerprintOff},
|
||||
{"账号唯一设备", newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: "account_device"}), codexFingerprintAccountDevice},
|
||||
{"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},
|
||||
@@ -81,6 +82,18 @@ func TestGetCodexFingerprintMode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCodexFingerprintIDsFromRequest_AccountDeviceModeUsesStableAccountSeed(t *testing.T) {
|
||||
account := newTestOAuthAccount(904, map[string]any{codexFingerprintModeExtraKey: "account_device"})
|
||||
first := resolveCodexFingerprintIDsFromRequest(account, nil)
|
||||
second := resolveCodexFingerprintIDsFromRequest(account, nil)
|
||||
require.NotNil(t, first)
|
||||
require.NotNil(t, second)
|
||||
assert.Equal(t, codexFingerprintAccountDevice, first.mode)
|
||||
assert.Equal(t, first.installationID, second.installationID)
|
||||
assert.Equal(t, resolveConvergedInstallationID(account, deriveAccountCodexFingerprintSeed(account)), first.installationID)
|
||||
assert.Empty(t, account.Extra[codexFingerprintSeedExtraKey], "账号唯一设备模式不需要写入系统种子")
|
||||
}
|
||||
|
||||
// --- resolveConvergedInstallationID ---
|
||||
|
||||
func TestResolveConvergedInstallationID_UsesDeviceID(t *testing.T) {
|
||||
|
||||
@@ -2040,7 +2040,7 @@ func normalizeOpenAIWSHandshakeCompatibility(account *Account, headers http.Head
|
||||
} else {
|
||||
key.codexInstallationID = normalizeOpenAIWSStableIdentityHeader(headers, "x-codex-installation-id")
|
||||
}
|
||||
if mode == codexFingerprintDevice {
|
||||
if mode == codexFingerprintAccountDevice || mode == codexFingerprintDevice {
|
||||
return key
|
||||
}
|
||||
key.sessionIDHyphen = normalizeOpenAIWSStableIdentityHeader(headers, "session-id")
|
||||
|
||||
@@ -1705,11 +1705,12 @@ 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'
|
||||
type CodexFingerprintMode = 'off' | 'account_device' | 'device' | 'session' | 'full'
|
||||
const enableCodexFingerprintMode = ref(false)
|
||||
const codexFingerprintMode = ref<CodexFingerprintMode>('off')
|
||||
const codexFingerprintModeOptions = computed(() => [
|
||||
{ value: 'off' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintOff') },
|
||||
{ value: 'account_device' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintAccountDevice') },
|
||||
{ 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') },
|
||||
@@ -2089,7 +2090,7 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
|
||||
|
||||
if (enableCodexFingerprintMode.value) {
|
||||
const extra = ensureExtra()
|
||||
// off = 默认值,清键即可;device/session/full 是显式 opt-in,必须落键(#5610)。
|
||||
// off = 默认值,清键即可;account_device/device/session/full 是显式 opt-in,必须落键。
|
||||
if (codexFingerprintMode.value !== 'off') {
|
||||
extra.codex_fingerprint_mode = codexFingerprintMode.value
|
||||
} else {
|
||||
|
||||
@@ -4171,10 +4171,11 @@ 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'
|
||||
type CodexFingerprintMode = 'off' | 'account_device' | 'device' | 'session' | 'full'
|
||||
const codexFingerprintMode = ref<CodexFingerprintMode>('off')
|
||||
const codexFingerprintModeOptions = computed(() => [
|
||||
{ value: 'off' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintOff') },
|
||||
{ value: 'account_device' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintAccountDevice') },
|
||||
{ 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') },
|
||||
@@ -5164,7 +5165,7 @@ const buildOpenAIExtra = (base?: Record<string, unknown>): Record<string, unknow
|
||||
} else {
|
||||
delete extra.codex_cli_only_allow_app_server
|
||||
}
|
||||
// 收敛是显式 opt-in:off 即默认值,不落键;device/session/full 必须显式写入,
|
||||
// 收敛是显式 opt-in:off 即默认值,不落键;account_device/device/session/full 必须显式写入,
|
||||
// 否则管理员的选择会被当成默认而丢失(#5610)。
|
||||
if (codexFingerprintMode.value !== 'off') {
|
||||
extra.codex_fingerprint_mode = codexFingerprintMode.value
|
||||
|
||||
@@ -3168,7 +3168,7 @@ 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'
|
||||
type CodexFingerprintMode = 'off' | 'account_device' | 'device' | 'session' | 'full'
|
||||
const codexFingerprintMode = ref<CodexFingerprintMode>('off')
|
||||
type CodexImageToolMode = 'inherit' | 'enabled' | 'disabled' | 'block'
|
||||
const codexImageToolMode = ref<CodexImageToolMode>('inherit')
|
||||
@@ -3203,6 +3203,7 @@ 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: 'account_device' as CodexFingerprintMode, label: t('admin.accounts.openai.codexFingerprintAccountDevice') },
|
||||
{ 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') },
|
||||
@@ -3698,7 +3699,7 @@ 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 || '')
|
||||
codexFingerprintMode.value = (['off', 'account_device', 'device', 'session', 'full'].includes(fpMode || '')
|
||||
? fpMode as CodexFingerprintMode
|
||||
: 'off')
|
||||
}
|
||||
@@ -5117,7 +5118,7 @@ const handleSubmit = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 指纹收敛模式:默认 off(不写入);device/session/full 是显式 opt-in,
|
||||
// 指纹收敛模式:默认 off(不写入);account_device/device/session/full 是显式 opt-in,
|
||||
// 必须落键,否则管理员的选择会被后端当作"未设置"而回落到 off(#5610)。
|
||||
if (props.account.type === 'oauth') {
|
||||
if (codexFingerprintMode.value !== 'off') {
|
||||
|
||||
@@ -612,8 +612,9 @@ 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 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.',
|
||||
codexFingerprintModeDesc: 'When multiple users share the same OAuth account, converge device/session identifiers to account-level stable values. The global account-unique device fingerprint is enabled by default; use this field for an explicit per-account override.',
|
||||
codexFingerprintOff: 'Off (passthrough, default)',
|
||||
codexFingerprintAccountDevice: 'Account-unique device (new, recommended)',
|
||||
codexFingerprintDevice: 'Device only',
|
||||
codexFingerprintSession: 'Device + Session',
|
||||
codexFingerprintFull: 'Full convergence',
|
||||
|
||||
@@ -682,8 +682,9 @@ export default {
|
||||
codexCLIOnlyAppServer: '允许 Codex app-server 客户端',
|
||||
codexCLIOnlyAppServerDesc: '仅在上方开关开启时生效。开启后本账号额外放行内嵌 Codex 引擎、经 app-server 协议接入的第三方客户端(如 Claude Code 的 codex 插件),仍需通过全局引擎指纹门;与全局 app-server 开关取 OR(任一开即放行)。',
|
||||
codexFingerprintMode: 'Codex 指纹收敛',
|
||||
codexFingerprintModeDesc: '多人共享同一 OAuth 账号时,将各用户的设备/会话标识收敛为账号级恒定值,减少上游可见的设备数和会话数。默认关闭(原样透传客户端标识),需要时再显式开启;部分账号开启收敛后出现过额度缩水,请按自己的实测结果选择。',
|
||||
codexFingerprintModeDesc: '多人共享同一 OAuth 账号时,将各用户的设备/会话标识收敛为账号级恒定值,减少上游可见的设备数和会话数。全局默认已启用账号唯一设备指纹;此处可为单个账号显式覆盖。',
|
||||
codexFingerprintOff: '关闭(透传,默认)',
|
||||
codexFingerprintAccountDevice: '账号唯一设备(新增,推荐)',
|
||||
codexFingerprintDevice: '仅设备',
|
||||
codexFingerprintSession: '设备+会话',
|
||||
codexFingerprintFull: '完全收敛',
|
||||
|
||||
Reference in new issue
Block a user