fix(openai): make Codex convergence identity consistent

This commit is contained in:
Jarl
2026-08-17 19:38:03 +08:00
parent e330c243a8
commit 6793d5ac85
20 changed files with 1834 additions and 160 deletions
+42 -1
View File
@@ -71,6 +71,39 @@ var schedulerNeutralExtraKeys = map[string]struct{}{
const postgresParameterBatchSize = 50000
const codexFingerprintSeedCanonicalPattern = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
const codexFingerprintNilSeed = "00000000-0000-0000-0000-000000000000"
func codexFingerprintSeedValidSQL(extraExpr string) string {
value := "(" + extraExpr + " ->> 'codex_fingerprint_seed')"
return "(" + value + " ~ '" + codexFingerprintSeedCanonicalPattern + "' AND " + value + " <> '" + codexFingerprintNilSeed + "')"
}
func ensureCodexFingerprintSeedSQL(extraExpr string) string {
return "CASE WHEN platform = 'openai' AND type = 'oauth' THEN " +
"jsonb_set(" + extraExpr + ", '{codex_fingerprint_seed}', " +
"CASE WHEN " + codexFingerprintSeedValidSQL("extra") +
" THEN to_jsonb(extra ->> 'codex_fingerprint_seed') ELSE to_jsonb(gen_random_uuid()::text) END, true) " +
"ELSE " + extraExpr + " END"
}
func stripCodexFingerprintSeedFromExtraUpdate(extra map[string]any) map[string]any {
if extra == nil {
return nil
}
if _, exists := extra["codex_fingerprint_seed"]; !exists {
return extra
}
stripped := make(map[string]any, len(extra)-1)
for key, value := range extra {
if key == "codex_fingerprint_seed" {
continue
}
stripped[key] = value
}
return stripped
}
// NewAccountRepository 创建账户仓储实例。
// 这是对外暴露的构造函数,返回接口类型以便于依赖注入。
func NewAccountRepository(client *dbent.Client, sqlDB *sql.DB, schedulerCache service.SchedulerCache) service.AccountRepository {
@@ -2520,6 +2553,7 @@ func (r *accountRepository) AutoPauseExpiredAccounts(ctx context.Context, now ti
}
func (r *accountRepository) UpdateExtra(ctx context.Context, id int64, updates map[string]any) error {
updates = stripCodexFingerprintSeedFromExtraUpdate(updates)
if len(updates) == 0 {
return nil
}
@@ -2552,6 +2586,9 @@ func (r *accountRepository) UpdateExtra(ctx context.Context, id int64, updates m
if clearProbeSnapshot {
extraExpression = "(" + extraExpression + ") - 'upstream_billing_probe'"
}
if service.ShouldEnsureCodexFingerprintSeedForExtraUpdates(updates) {
extraExpression = ensureCodexFingerprintSeedSQL(extraExpression)
}
result, err := client.ExecContext(
ctx,
"UPDATE accounts SET extra = "+extraExpression+", updated_at = NOW() WHERE id = $2 AND deleted_at IS NULL",
@@ -2793,6 +2830,7 @@ func (r *accountRepository) BulkUpdate(ctx context.Context, ids []int64, updates
if len(ids) == 0 {
return 0, nil
}
updates.Extra = stripCodexFingerprintSeedFromExtraUpdate(updates.Extra)
setClauses := make([]string, 0, 8)
args := make([]any, 0, 8)
@@ -2880,7 +2918,7 @@ func (r *accountRepository) BulkUpdate(ctx context.Context, ids []int64, updates
" AND "+ollamaCloudBaseURLMatchesSQL(credentialPlaceholder+"::jsonb ->> 'base_url'")+")")
}
if len(updates.Extra) > 0 || len(ollamaGroupIdentityChanges) > 0 || ollamaProxyIdentityChanged != "" {
if len(updates.Extra) > 0 || len(ollamaGroupIdentityChanges) > 0 || ollamaProxyIdentityChanged != "" || updates.EnsureCodexFingerprintSeed {
extraExpression := "COALESCE(extra, '{}'::jsonb)"
if len(updates.Extra) > 0 {
payload, err := json.Marshal(updates.Extra)
@@ -2919,6 +2957,9 @@ func (r *accountRepository) BulkUpdate(ctx context.Context, ids []int64, updates
} else if snapshotIdentityChanged != "" {
extraExpression = "CASE WHEN " + snapshotIdentityChanged + " THEN (" + extraExpression + ") - 'ollama_cloud_usage_snapshot' ELSE " + extraExpression + " END"
}
if updates.EnsureCodexFingerprintSeed {
extraExpression = ensureCodexFingerprintSeedSQL(extraExpression)
}
setClauses = append(setClauses, "extra = "+extraExpression)
}
@@ -0,0 +1,123 @@
package repository
import (
"context"
"errors"
"regexp"
"testing"
"github.com/DATA-DOG/go-sqlmock"
dbent "github.com/Wei-Shaw/sub2api/ent"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/stretchr/testify/require"
"entgo.io/ent/dialect"
entsql "entgo.io/ent/dialect/sql"
)
func TestBulkUpdateEnsuresCodexFingerprintSeedWithPerRowSQL(t *testing.T) {
exec := &recordingSQLExecutor{result: rowsAffectedResult(0)}
repo := newAccountRepositoryWithSQL(nil, exec, nil)
_, err := repo.BulkUpdate(context.Background(), []int64{27, 28}, service.AccountBulkUpdate{
Extra: map[string]any{
"codex_fingerprint_mode": "session",
"codex_fingerprint_seed": "22222222-2222-4222-8222-222222222222",
},
EnsureCodexFingerprintSeed: true,
})
require.NoError(t, err)
require.NotEmpty(t, exec.execQueries)
query := normalizeSQLWhitespace(exec.execQueries[0])
require.Contains(t, query, "jsonb_set")
require.Contains(t, query, "gen_random_uuid()::text")
require.Contains(t, query, "platform = 'openai' AND type = 'oauth'")
require.Contains(t, query, "to_jsonb(extra ->> 'codex_fingerprint_seed')")
require.Contains(t, query, codexFingerprintSeedCanonicalPattern)
require.NotContains(t, query, "22222222-2222-4222-8222-222222222222")
require.NotEmpty(t, exec.execArgs)
payload, ok := exec.execArgs[0][0].([]byte)
require.True(t, ok)
require.Equal(t, `{"codex_fingerprint_mode":"session"}`, string(payload))
}
func TestUpdateExtraEnsuresCodexFingerprintSeedAtomicallyWhenEnabling(t *testing.T) {
db, mock, err := sqlmock.New()
require.NoError(t, err)
t.Cleanup(func() { _ = db.Close() })
client := dbent.NewClient(dbent.Driver(entsql.OpenDB(dialect.Postgres, db)))
t.Cleanup(func() { _ = client.Close() })
mock.ExpectBegin()
mock.ExpectExec(`(?s)UPDATE accounts SET extra = .*jsonb_set.*gen_random_uuid\(\)::text.*WHERE id = \$2 AND deleted_at IS NULL`).
WithArgs(`{"codex_fingerprint_mode":"device"}`, int64(27)).
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(regexp.QuoteMeta("INSERT INTO scheduler_outbox")).
WithArgs(service.SchedulerOutboxEventAccountChanged, int64(27), nil, nil, sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
repo := newAccountRepositoryWithSQL(client, db, nil)
err = repo.UpdateExtra(context.Background(), 27, map[string]any{
"codex_fingerprint_mode": "device",
"codex_fingerprint_seed": "22222222-2222-4222-8222-222222222222",
})
require.NoError(t, err)
require.NoError(t, mock.ExpectationsWereMet())
}
func TestBulkUpdateCodexFingerprintSeedRollsBackWhenUpdateFails(t *testing.T) {
db, mock, err := sqlmock.New()
require.NoError(t, err)
t.Cleanup(func() { _ = db.Close() })
client := dbent.NewClient(dbent.Driver(entsql.OpenDB(dialect.Postgres, db)))
t.Cleanup(func() { _ = client.Close() })
mock.ExpectBegin()
mock.ExpectExec(`(?s)UPDATE accounts SET extra = .*gen_random_uuid\(\)::text.*WHERE id = ANY\(\$2\)`).
WithArgs(sqlmock.AnyArg(), `{27,28}`).
WillReturnError(errors.New("update failed"))
mock.ExpectRollback()
repo := newAccountRepositoryWithSQL(client, db, nil)
rows, err := repo.BulkUpdate(context.Background(), []int64{27, 28}, service.AccountBulkUpdate{
Extra: map[string]any{
"codex_fingerprint_mode": "session",
},
EnsureCodexFingerprintSeed: true,
})
require.EqualError(t, err, "update failed")
require.Zero(t, rows)
require.NoError(t, mock.ExpectationsWereMet())
}
func TestBulkUpdateCodexFingerprintSeedRollsBackWhenOutboxFails(t *testing.T) {
db, mock, err := sqlmock.New()
require.NoError(t, err)
t.Cleanup(func() { _ = db.Close() })
client := dbent.NewClient(dbent.Driver(entsql.OpenDB(dialect.Postgres, db)))
t.Cleanup(func() { _ = client.Close() })
mock.ExpectBegin()
mock.ExpectExec(`(?s)UPDATE accounts SET extra = .*gen_random_uuid\(\)::text.*WHERE id = ANY\(\$2\)`).
WithArgs(sqlmock.AnyArg(), `{27,28}`).
WillReturnResult(sqlmock.NewResult(0, 2))
mock.ExpectExec(regexp.QuoteMeta("INSERT INTO scheduler_outbox")).
WillReturnError(errors.New("outbox failed"))
mock.ExpectRollback()
repo := newAccountRepositoryWithSQL(client, db, nil)
rows, err := repo.BulkUpdate(context.Background(), []int64{27, 28}, service.AccountBulkUpdate{
Extra: map[string]any{
"codex_fingerprint_mode": "full",
},
EnsureCodexFingerprintSeed: true,
})
require.EqualError(t, err, "outbox failed")
require.Zero(t, rows)
require.NoError(t, mock.ExpectationsWereMet())
}
@@ -0,0 +1,150 @@
//go:build integration
package repository
import (
"context"
"testing"
"github.com/Wei-Shaw/sub2api/internal/service"
dbmigrations "github.com/Wei-Shaw/sub2api/migrations"
"github.com/google/uuid"
"github.com/lib/pq"
"github.com/stretchr/testify/require"
)
func requireCanonicalUUIDString(t *testing.T, value string) {
t.Helper()
parsed, err := uuid.Parse(value)
require.NoError(t, err)
require.NotEqual(t, uuid.Nil, parsed)
require.Equal(t, parsed.String(), value)
}
func TestMigration225BackfillsOnlyEnabledOpenAIOAuthMissingOrMalformedSeeds(t *testing.T) {
tx := testTx(t)
ctx := context.Background()
migrationSQL, err := dbmigrations.FS.ReadFile("225_backfill_codex_fingerprint_seed.sql")
require.NoError(t, err)
var missingID, blankID, malformedID, validID, offID, apiKeyID int64
require.NoError(t, tx.QueryRowContext(ctx, `
INSERT INTO accounts (name, platform, type, extra)
VALUES ('migration-225-missing', 'openai', 'oauth', '{"codex_fingerprint_mode":"session"}'::jsonb)
RETURNING id
`).Scan(&missingID))
require.NoError(t, tx.QueryRowContext(ctx, `
INSERT INTO accounts (name, platform, type, extra)
VALUES ('migration-225-blank', 'openai', 'oauth', '{"codex_fingerprint_mode":"device","codex_fingerprint_seed":""}'::jsonb)
RETURNING id
`).Scan(&blankID))
require.NoError(t, tx.QueryRowContext(ctx, `
INSERT INTO accounts (name, platform, type, extra)
VALUES ('migration-225-malformed', 'openai', 'oauth', '{"codex_fingerprint_mode":"full","codex_fingerprint_seed":"BAD"}'::jsonb)
RETURNING id
`).Scan(&malformedID))
require.NoError(t, tx.QueryRowContext(ctx, `
INSERT INTO accounts (name, platform, type, extra)
VALUES ('migration-225-valid', 'openai', 'oauth', '{"codex_fingerprint_mode":"session","codex_fingerprint_seed":"11111111-1111-4111-8111-111111111111"}'::jsonb)
RETURNING id
`).Scan(&validID))
require.NoError(t, tx.QueryRowContext(ctx, `
INSERT INTO accounts (name, platform, type, extra)
VALUES ('migration-225-off', 'openai', 'oauth', '{"codex_fingerprint_mode":"off"}'::jsonb)
RETURNING id
`).Scan(&offID))
require.NoError(t, tx.QueryRowContext(ctx, `
INSERT INTO accounts (name, platform, type, extra)
VALUES ('migration-225-apikey', 'openai', 'apikey', '{"codex_fingerprint_mode":"session"}'::jsonb)
RETURNING id
`).Scan(&apiKeyID))
_, err = tx.ExecContext(ctx, string(migrationSQL))
require.NoError(t, err)
seedsAfterFirst := map[int64]string{}
for _, id := range []int64{missingID, blankID, malformedID, validID} {
var seed string
require.NoError(t, tx.QueryRowContext(ctx, `SELECT extra->>'codex_fingerprint_seed' FROM accounts WHERE id = $1`, id).Scan(&seed))
requireCanonicalUUIDString(t, seed)
seedsAfterFirst[id] = seed
}
require.Equal(t, "11111111-1111-4111-8111-111111111111", seedsAfterFirst[validID])
for _, id := range []int64{offID, apiKeyID} {
var hasSeed bool
require.NoError(t, tx.QueryRowContext(ctx, `SELECT extra ? 'codex_fingerprint_seed' FROM accounts WHERE id = $1`, id).Scan(&hasSeed))
require.False(t, hasSeed)
}
_, err = tx.ExecContext(ctx, string(migrationSQL))
require.NoError(t, err)
for id, want := range seedsAfterFirst {
var got string
require.NoError(t, tx.QueryRowContext(ctx, `SELECT extra->>'codex_fingerprint_seed' FROM accounts WHERE id = $1`, id).Scan(&got))
require.Equal(t, want, got)
}
}
func TestBulkUpdateGeneratesDistinctStableCodexFingerprintSeedsPerEligibleRow(t *testing.T) {
ctx := context.Background()
testName := "bulk-codex-seed-" + uuid.NewString()
type fixture struct {
name string
accountType string
extra string
}
fixtures := []fixture{
{name: testName + "-missing-a", accountType: service.AccountTypeOAuth, extra: `{}`},
{name: testName + "-missing-b", accountType: service.AccountTypeOAuth, extra: `{"codex_fingerprint_seed":"BAD"}`},
{name: testName + "-valid", accountType: service.AccountTypeOAuth, extra: `{"codex_fingerprint_seed":"11111111-1111-4111-8111-111111111111"}`},
{name: testName + "-apikey", accountType: service.AccountTypeAPIKey, extra: `{}`},
}
ids := make([]int64, 0, len(fixtures))
for _, f := range fixtures {
var id int64
require.NoError(t, integrationDB.QueryRowContext(ctx, `
INSERT INTO accounts (name, platform, type, extra)
VALUES ($1, 'openai', $2, $3::jsonb)
RETURNING id
`, f.name, f.accountType, f.extra).Scan(&id))
ids = append(ids, id)
}
t.Cleanup(func() {
_, _ = integrationDB.ExecContext(context.Background(), `DELETE FROM scheduler_outbox WHERE account_id = ANY($1)`, pq.Array(ids))
_, _ = integrationDB.ExecContext(context.Background(), `DELETE FROM accounts WHERE id = ANY($1)`, pq.Array(ids))
})
repo := newAccountRepositoryWithSQL(testEntClient(t), integrationDB, nil)
updates := service.AccountBulkUpdate{
Extra: map[string]any{
"codex_fingerprint_mode": "session",
},
EnsureCodexFingerprintSeed: true,
}
rows, err := repo.BulkUpdate(ctx, ids, updates)
require.NoError(t, err)
require.Equal(t, int64(len(ids)), rows)
readSeed := func(id int64) string {
t.Helper()
var seed string
require.NoError(t, integrationDB.QueryRowContext(ctx, `SELECT COALESCE(extra->>'codex_fingerprint_seed', '') FROM accounts WHERE id = $1`, id).Scan(&seed))
return seed
}
firstSeeds := []string{readSeed(ids[0]), readSeed(ids[1]), readSeed(ids[2]), readSeed(ids[3])}
requireCanonicalUUIDString(t, firstSeeds[0])
requireCanonicalUUIDString(t, firstSeeds[1])
require.NotEqual(t, firstSeeds[0], firstSeeds[1], "gen_random_uuid must be evaluated per eligible row")
require.Equal(t, "11111111-1111-4111-8111-111111111111", firstSeeds[2])
require.Empty(t, firstSeeds[3], "API-key accounts must not receive a Codex fingerprint seed")
rows, err = repo.BulkUpdate(ctx, ids, updates)
require.NoError(t, err)
require.Equal(t, int64(len(ids)), rows)
for i, want := range firstSeeds {
require.Equal(t, want, readSeed(ids[i]), "retry must not rotate an existing valid seed")
}
}
@@ -1000,6 +1000,8 @@ func filterSchedulerExtra(extra map[string]any) map[string]any {
"openai_ws_force_http",
"openai_responses_mode",
"openai_responses_supported",
"codex_fingerprint_mode",
"codex_fingerprint_seed",
"codex_5h_used_percent",
"codex_7d_used_percent",
"codex_5h_reset_at",
@@ -309,6 +309,8 @@ func TestBuildSchedulerMetadataAccount_KeepsOpenAIWSFlags(t *testing.T) {
"openai_ws_force_http": true,
"openai_responses_mode": "force_chat_completions",
"openai_responses_supported": false,
"codex_fingerprint_mode": "session",
"codex_fingerprint_seed": "11111111-1111-4111-8111-111111111111",
"mixed_scheduling": true,
"unused_large_field": "drop-me",
},
@@ -321,6 +323,8 @@ func TestBuildSchedulerMetadataAccount_KeepsOpenAIWSFlags(t *testing.T) {
require.Equal(t, true, got.Extra["openai_ws_force_http"])
require.Equal(t, "force_chat_completions", got.Extra["openai_responses_mode"])
require.Equal(t, false, got.Extra["openai_responses_supported"])
require.Equal(t, "session", got.Extra["codex_fingerprint_mode"])
require.Equal(t, "11111111-1111-4111-8111-111111111111", got.Extra["codex_fingerprint_seed"])
require.Equal(t, true, got.Extra["mixed_scheduling"])
require.Nil(t, got.Extra["unused_large_field"])
}
@@ -0,0 +1,245 @@
package service
import (
"context"
"testing"
"github.com/stretchr/testify/require"
)
const userSuppliedCodexFingerprintSeed = "22222222-2222-4222-8222-222222222222"
func requireValidCodexFingerprintSeed(t *testing.T, extra map[string]any) string {
t.Helper()
seed, ok := codexFingerprintSeed(extra)
require.True(t, ok, "expected valid canonical Codex fingerprint seed")
return seed
}
func TestAdminCreateAccountStripsUserSeedAndCreatesFreshSeedWhenEnabled(t *testing.T) {
repo := &upstreamBillingProbeAccountRepo{}
svc := &adminServiceImpl{accountRepo: repo}
created, err := svc.CreateAccount(context.Background(), &CreateAccountInput{
Name: "codex-oauth",
Platform: PlatformOpenAI,
Type: AccountTypeOAuth,
SkipDefaultGroupBind: true,
Extra: map[string]any{
codexFingerprintModeExtraKey: "session",
codexFingerprintSeedExtraKey: userSuppliedCodexFingerprintSeed,
},
})
require.NoError(t, err)
seed := requireValidCodexFingerprintSeed(t, created.Extra)
require.NotEqual(t, userSuppliedCodexFingerprintSeed, seed)
require.Equal(t, "session", created.Extra[codexFingerprintModeExtraKey])
}
func TestAdminUpdateAccountPreservesExistingSeedAndStripsUserSeed(t *testing.T) {
accountID := int64(201)
repo := &upstreamBillingProbeAccountRepo{accounts: map[int64]*Account{
accountID: {
ID: accountID,
Name: "before",
Platform: PlatformOpenAI,
Type: AccountTypeOAuth,
Status: StatusActive,
Extra: map[string]any{
codexFingerprintModeExtraKey: "session",
codexFingerprintSeedExtraKey: testCodexFingerprintSeed,
},
},
}}
svc := &adminServiceImpl{accountRepo: repo}
updated, err := svc.UpdateAccount(context.Background(), accountID, &UpdateAccountInput{
Extra: map[string]any{
codexFingerprintModeExtraKey: "full",
codexFingerprintSeedExtraKey: userSuppliedCodexFingerprintSeed,
"custom": "value",
},
})
require.NoError(t, err)
require.Equal(t, testCodexFingerprintSeed, requireValidCodexFingerprintSeed(t, updated.Extra))
require.Equal(t, "full", updated.Extra[codexFingerprintModeExtraKey])
require.Equal(t, "value", updated.Extra["custom"])
}
func TestAdminUpdateAccountInitializesSeedWhenFullEditEnables(t *testing.T) {
accountID := int64(202)
repo := &upstreamBillingProbeAccountRepo{accounts: map[int64]*Account{
accountID: {
ID: accountID,
Name: "before",
Platform: PlatformOpenAI,
Type: AccountTypeOAuth,
Status: StatusActive,
Extra: map[string]any{
codexFingerprintModeExtraKey: "off",
codexFingerprintSeedExtraKey: "not-a-seed",
},
},
}}
updated, err := (&adminServiceImpl{accountRepo: repo}).UpdateAccount(context.Background(), accountID, &UpdateAccountInput{
Extra: map[string]any{codexFingerprintModeExtraKey: "device"},
})
require.NoError(t, err)
require.NotEqual(t, "not-a-seed", requireValidCodexFingerprintSeed(t, updated.Extra))
require.Equal(t, "device", updated.Extra[codexFingerprintModeExtraKey])
}
func TestAdminUpdateAccountDisableReenablePreservesValidSeed(t *testing.T) {
accountID := int64(203)
repo := &upstreamBillingProbeAccountRepo{accounts: map[int64]*Account{
accountID: {
ID: accountID,
Platform: PlatformOpenAI,
Type: AccountTypeOAuth,
Status: StatusActive,
Extra: map[string]any{
codexFingerprintModeExtraKey: "session",
codexFingerprintSeedExtraKey: testCodexFingerprintSeed,
},
},
}}
svc := &adminServiceImpl{accountRepo: repo}
disabled, err := svc.UpdateAccount(context.Background(), accountID, &UpdateAccountInput{
Extra: map[string]any{codexFingerprintModeExtraKey: "off"},
})
require.NoError(t, err)
require.Equal(t, testCodexFingerprintSeed, requireValidCodexFingerprintSeed(t, disabled.Extra))
reenabled, err := svc.UpdateAccount(context.Background(), accountID, &UpdateAccountInput{
Extra: map[string]any{codexFingerprintModeExtraKey: "session"},
})
require.NoError(t, err)
require.Equal(t, testCodexFingerprintSeed, requireValidCodexFingerprintSeed(t, reenabled.Extra))
}
func TestAdminUpdateAccountExtraStripsSeedAndLeavesAtomicEnsureToRepository(t *testing.T) {
accountID := int64(204)
repo := &upstreamBillingProbeAccountRepo{accounts: map[int64]*Account{
accountID: {
ID: accountID,
Platform: PlatformOpenAI,
Type: AccountTypeOAuth,
Extra: map[string]any{},
},
}}
err := (&adminServiceImpl{accountRepo: repo}).UpdateAccountExtra(context.Background(), accountID, map[string]any{
codexFingerprintModeExtraKey: "device",
codexFingerprintSeedExtraKey: userSuppliedCodexFingerprintSeed,
})
require.NoError(t, err)
require.Len(t, repo.updates[accountID], 1)
require.Equal(t, "device", repo.updates[accountID][0][codexFingerprintModeExtraKey])
require.NotContains(t, repo.updates[accountID][0], codexFingerprintSeedExtraKey)
}
func TestBulkUpdateAccountsDoesNotPrewriteCodexSeed(t *testing.T) {
repo := &upstreamBillingProbeAccountRepo{}
result, err := (&adminServiceImpl{accountRepo: repo}).BulkUpdateAccounts(context.Background(), &BulkUpdateAccountsInput{
AccountIDs: []int64{301, 302},
Extra: map[string]any{
codexFingerprintModeExtraKey: "session",
codexFingerprintSeedExtraKey: userSuppliedCodexFingerprintSeed,
},
})
require.NoError(t, err)
require.Equal(t, 2, result.Success)
require.Empty(t, repo.updates, "bulk enable must not loop through UpdateExtra before BulkUpdate")
require.Len(t, repo.bulkUpdates, 1)
require.True(t, repo.bulkUpdates[0].EnsureCodexFingerprintSeed)
require.Equal(t, "session", repo.bulkUpdates[0].Extra[codexFingerprintModeExtraKey])
require.NotContains(t, repo.bulkUpdates[0].Extra, codexFingerprintSeedExtraKey)
}
type codexSeedDuplicateRepo struct {
*upstreamBillingProbeAccountRepo
}
func (r *codexSeedDuplicateRepo) CreateWithAccountGroups(ctx context.Context, account *Account, _ []AccountGroup) error {
return r.Create(ctx, account)
}
func TestDuplicateAccountDoesNotCopyCodexFingerprintSeed(t *testing.T) {
ctx := context.Background()
repo := &codexSeedDuplicateRepo{upstreamBillingProbeAccountRepo: &upstreamBillingProbeAccountRepo{accounts: make(map[int64]*Account)}}
svc := &adminServiceImpl{accountRepo: repo, accountDuplicateRepo: repo}
source := &Account{
Name: "source",
Platform: PlatformOpenAI,
Type: AccountTypeAPIKey,
Extra: map[string]any{
codexFingerprintModeExtraKey: "session",
codexFingerprintSeedExtraKey: testCodexFingerprintSeed,
},
}
require.NoError(t, repo.Create(ctx, source))
duplicate, err := svc.DuplicateAccount(ctx, source.ID, "admin:1", "")
require.NoError(t, err)
require.NotEqual(t, source.ID, duplicate.ID)
require.NotContains(t, duplicate.Extra, codexFingerprintSeedExtraKey)
require.Equal(t, "session", duplicate.Extra[codexFingerprintModeExtraKey])
}
func TestDuplicateCreatePathMintsFreshSeedWhenEligible(t *testing.T) {
extra, err := duplicateAccountExtra(map[string]any{
codexFingerprintModeExtraKey: "session",
codexFingerprintSeedExtraKey: testCodexFingerprintSeed,
})
require.NoError(t, err)
account, err := buildAccountForCreate(&CreateAccountInput{
Name: "eligible-copy",
Platform: PlatformOpenAI,
Type: AccountTypeOAuth,
Extra: extra,
}, extra)
require.NoError(t, err)
require.NotEqual(t, testCodexFingerprintSeed, requireValidCodexFingerprintSeed(t, account.Extra))
require.Equal(t, "session", account.Extra[codexFingerprintModeExtraKey])
}
func TestAccountServiceCreateAndUpdateCodexSeedLifecycle(t *testing.T) {
ctx := context.Background()
repo := &upstreamBillingProbeAccountRepo{accounts: make(map[int64]*Account)}
svc := NewAccountService(repo, nil)
created, err := svc.Create(ctx, CreateAccountRequest{
Name: "legacy-create",
Platform: PlatformOpenAI,
Type: AccountTypeOAuth,
Extra: map[string]any{
codexFingerprintModeExtraKey: "session",
codexFingerprintSeedExtraKey: userSuppliedCodexFingerprintSeed,
},
})
require.NoError(t, err)
createdSeed := requireValidCodexFingerprintSeed(t, created.Extra)
require.NotEqual(t, userSuppliedCodexFingerprintSeed, createdSeed)
updateSeed := userSuppliedCodexFingerprintSeed
updated, err := svc.Update(ctx, created.ID, UpdateAccountRequest{
Extra: &map[string]any{
codexFingerprintModeExtraKey: "full",
codexFingerprintSeedExtraKey: updateSeed,
},
})
require.NoError(t, err)
require.Equal(t, createdSeed, requireValidCodexFingerprintSeed(t, updated.Extra))
require.Equal(t, "full", updated.Extra[codexFingerprintModeExtraKey])
}
+7 -2
View File
@@ -166,6 +166,9 @@ type AccountBulkUpdate struct {
Credentials map[string]any
Extra map[string]any
ProbeEnabled *bool
// EnsureCodexFingerprintSeed asks the repository to atomically preserve an
// existing valid Codex fingerprint seed or create one for eligible rows.
EnsureCodexFingerprintSeed bool
}
// CreateAccountRequest 创建账号请求
@@ -233,7 +236,7 @@ func (s *AccountService) Create(ctx context.Context, req CreateAccountRequest) (
Platform: req.Platform,
Type: req.Type,
Credentials: SanitizeStoredCredentials(req.Platform, req.Credentials),
Extra: req.Extra,
Extra: prepareCodexFingerprintExtraForCreate(req.Platform, req.Type, req.Extra),
ProxyID: req.ProxyID,
Concurrency: req.Concurrency,
Priority: req.Priority,
@@ -336,7 +339,9 @@ func (s *AccountService) Update(ctx context.Context, id int64, req UpdateAccount
delete(extra, OllamaCloudUsageSessionExtraKey)
delete(extra, OllamaCloudUsageAutoRefreshExtraKey)
delete(extra, OllamaCloudUsageSnapshotExtraKey)
account.Extra = extra
account.Extra = prepareCodexFingerprintExtraForUpdate(account, extra)
} else {
account.Extra = prepareCodexFingerprintExtraForUpdate(account, account.Extra)
}
if req.ProxyID != nil {
@@ -284,7 +284,10 @@ func TestAccountTestService_TestAccountConnection_OpenAICompactProbeIdentityMatc
"chatgpt_account_id": "chatgpt-acc",
},
// 收敛是显式 opt-in(#5610),这里显式开启以验证探测身份与真实流量同构。
Extra: map[string]any{"codex_fingerprint_mode": "session"},
Extra: map[string]any{
"codex_fingerprint_mode": "session",
codexFingerprintSeedExtraKey: testCodexFingerprintSeed,
},
}
repo := &snapshotUpdateAccountRepo{
stubOpenAIAccountRepo: stubOpenAIAccountRepo{accounts: []Account{account}},
@@ -304,10 +307,12 @@ func TestAccountTestService_TestAccountConnection_OpenAICompactProbeIdentityMatc
require.NoError(t, svc.TestAccountConnection(c, account.ID, "gpt-5.4", "", AccountTestModeCompact))
// 显式 session 收敛模式:出站身份 = 账号级收敛值
converged := resolveConvergedSessionID(&account)
seed, ok := codexFingerprintSeed(account.Extra)
require.True(t, ok)
converged := resolveConvergedSessionID(seed)
require.Equal(t, converged, upstream.lastReq.Header.Get("session-id"))
require.Equal(t, converged, upstream.lastReq.Header.Get("session_id"))
require.Equal(t, resolveConvergedInstallationID(&account), upstream.lastReq.Header.Get("x-codex-installation-id"),
require.Equal(t, resolveConvergedInstallationID(&account, seed), upstream.lastReq.Header.Get("x-codex-installation-id"),
"真实 Codex 每个请求必带 installation-id,探测不得缺失")
require.NotContains(t, upstream.lastReq.Header.Get("session-id"), "probe_compact",
"探测标识不得是可被上游一眼识别的字面量")
+29 -19
View File
@@ -128,22 +128,24 @@ var duplicateAccountDiscardedExtraKeys = map[string]struct{}{
"drive_storage_limit": {},
"drive_storage_usage": {},
"drive_tier_updated_at": {},
"codex_primary_used_percent": {},
"codex_primary_reset_after_seconds": {},
"codex_primary_window_minutes": {},
"codex_secondary_used_percent": {},
"codex_secondary_reset_after_seconds": {},
"codex_secondary_window_minutes": {},
"codex_primary_over_secondary_percent": {},
"codex_usage_updated_at": {},
"codex_5h_used_percent": {},
"codex_5h_reset_after_seconds": {},
"codex_5h_window_minutes": {},
"codex_5h_reset_at": {},
"codex_7d_used_percent": {},
"codex_7d_reset_after_seconds": {},
"codex_7d_window_minutes": {},
"codex_7d_reset_at": {},
// Codex fingerprint convergence uses a per-account random seed, never copied from another account.
codexFingerprintSeedExtraKey: {},
"codex_primary_used_percent": {},
"codex_primary_reset_after_seconds": {},
"codex_primary_window_minutes": {},
"codex_secondary_used_percent": {},
"codex_secondary_reset_after_seconds": {},
"codex_secondary_window_minutes": {},
"codex_primary_over_secondary_percent": {},
"codex_usage_updated_at": {},
"codex_5h_used_percent": {},
"codex_5h_reset_after_seconds": {},
"codex_5h_window_minutes": {},
"codex_5h_reset_at": {},
"codex_7d_used_percent": {},
"codex_7d_reset_after_seconds": {},
"codex_7d_window_minutes": {},
"codex_7d_reset_at": {},
}
func duplicateAccountExtra(value map[string]any) (map[string]any, error) {
@@ -404,6 +406,7 @@ func buildAccountForCreate(input *CreateAccountInput, accountExtra map[string]an
delete(accountExtra, OllamaCloudUsageSessionExtraKey)
delete(accountExtra, OllamaCloudUsageAutoRefreshExtraKey)
delete(accountExtra, OllamaCloudUsageSnapshotExtraKey)
accountExtra = prepareCodexFingerprintExtraForCreate(input.Platform, input.Type, accountExtra)
account := &Account{
Name: input.Name,
Notes: normalizeAccountNotes(input.Notes),
@@ -651,6 +654,7 @@ func (s *adminServiceImpl) UpdateAccount(ctx context.Context, id int64, input *U
normalizedExtra[key] = v
}
}
normalizedExtra = prepareCodexFingerprintExtraForUpdate(account, normalizedExtra)
account.Extra = normalizedExtra
if account.Platform == PlatformAntigravity && wasOveragesEnabled && !account.IsOveragesEnabled() {
delete(account.Extra, "antigravity_credits_overages") // 清理旧版 overages 运行态
@@ -670,6 +674,9 @@ func (s *adminServiceImpl) UpdateAccount(ctx context.Context, id int64, input *U
ComputeQuotaResetAt(account.Extra)
NormalizeFixedQuotaWindows(account.Extra)
}
if input.Extra == nil {
account.Extra = prepareCodexFingerprintExtraForUpdate(account, account.Extra)
}
if requestedRateSyncEnabledUpdate != nil && *requestedRateSyncEnabledUpdate {
if requestedProbeEnabledUpdate != nil && !*requestedProbeEnabledUpdate {
return nil, infraerrors.BadRequest(
@@ -852,6 +859,7 @@ func (s *adminServiceImpl) UpdateAccount(ctx context.Context, id int64, input *U
// UpdateAccountExtra 仅对 Extra JSONB 做 key 级合并,避免覆盖其它运行态键
// (如 model_rate_limits / passive_usage_* 等)。
func (s *adminServiceImpl) UpdateAccountExtra(ctx context.Context, id int64, updates map[string]any) error {
updates = sanitizedCodexFingerprintExtraUpdates(updates)
delete(updates, UpstreamBillingProbeEnabledExtraKey)
delete(updates, UpstreamBillingRateSyncEnabledExtraKey)
delete(updates, UpstreamBillingProbeExtraKey)
@@ -877,6 +885,7 @@ func (s *adminServiceImpl) UpdateAccountExtra(ctx context.Context, id int64, upd
// It merges credentials/extra keys instead of overwriting the whole object.
func (s *adminServiceImpl) BulkUpdateAccounts(ctx context.Context, input *BulkUpdateAccountsInput) (*BulkUpdateAccountsResult, error) {
// Managed probe/session state may only enter through dedicated typed endpoints.
input.Extra = sanitizedCodexFingerprintExtraUpdates(input.Extra)
delete(input.Extra, UpstreamBillingProbeEnabledExtraKey)
delete(input.Extra, UpstreamBillingRateSyncEnabledExtraKey)
delete(input.Extra, UpstreamBillingProbeExtraKey)
@@ -1027,9 +1036,10 @@ func (s *adminServiceImpl) BulkUpdateAccounts(ctx context.Context, input *BulkUp
// Prepare bulk updates for columns and JSONB fields.
repoUpdates := AccountBulkUpdate{
Credentials: input.Credentials,
Extra: input.Extra,
ProbeEnabled: input.ProbeEnabled,
Credentials: input.Credentials,
Extra: input.Extra,
ProbeEnabled: input.ProbeEnabled,
EnsureCodexFingerprintSeed: ShouldEnsureCodexFingerprintSeedForExtraUpdates(input.Extra),
}
if input.ProbeEnabled != nil {
if repoUpdates.Extra == nil {
@@ -5,6 +5,7 @@ import (
"encoding/binary"
"encoding/json"
"fmt"
"maps"
"net/http"
"strings"
"time"
@@ -30,20 +31,30 @@ func stageCodexFingerprintIDs(c *gin.Context, ids *codexFingerprintIDs) {
}
}
// applyStagedCodexFingerprintHeaders 读取 context 暂存的收敛 ID 并改写出站头。
// 非透传与透传两个请求构造器共用本函数,防止应用语义漂移。仅 OAuth 账号
// 生效(stale 键在账号类型混合 failover 下由该门挡住)。
func applyStagedCodexFingerprintHeaders(c *gin.Context, account *Account, h http.Header) {
func stagedCodexFingerprintIDs(c *gin.Context, account *Account) *codexFingerprintIDs {
if c == nil || account == nil || account.Type != AccountTypeOAuth {
return
return nil
}
value, ok := c.Get(codexFingerprintIDsContextKey)
if !ok {
return
return nil
}
if ids, ok := value.(*codexFingerprintIDs); ok {
applyCodexFingerprintHeaders(h, ids)
ids, ok := value.(*codexFingerprintIDs)
if !ok || ids == nil || ids.accountID != account.ID {
return nil
}
return ids
}
// applyStagedCodexFingerprintHeaders 读取 context 暂存的收敛 ID 并改写出站头。
// 非透传与透传两个请求构造器共用本函数,防止应用语义漂移。仅解析该
// snapshot 的 OAuth 账号可读取,避免 stale context 跨账号 failover 泄漏。
func applyStagedCodexFingerprintHeaders(c *gin.Context, account *Account, h http.Header) {
applyCodexFingerprintHeaders(h, stagedCodexFingerprintIDs(c, account))
}
func applyStagedCodexFingerprintClientMetadata(c *gin.Context, account *Account, reqBody map[string]any) bool {
return applyCodexFingerprintClientMetadata(reqBody, stagedCodexFingerprintIDs(c, account))
}
// codexFingerprintMode 控制 OAuth 账号出站请求的设备指纹收敛强度。
@@ -68,7 +79,117 @@ const (
codexFingerprintFull codexFingerprintMode = "full"
)
const codexFingerprintModeExtraKey = "codex_fingerprint_mode"
const (
codexFingerprintModeExtraKey = "codex_fingerprint_mode"
codexFingerprintSeedExtraKey = "codex_fingerprint_seed"
)
func canonicalCodexFingerprintSeed(value any) (string, bool) {
raw, ok := value.(string)
if !ok {
return "", false
}
trimmed := strings.TrimSpace(raw)
parsed, err := uuid.Parse(trimmed)
if err != nil || parsed == uuid.Nil || trimmed != parsed.String() {
return "", false
}
return trimmed, true
}
func newCodexFingerprintSeed() string {
return uuid.NewString()
}
func stripCodexFingerprintSeed(extra map[string]any) map[string]any {
if extra == nil {
return nil
}
stripped := maps.Clone(extra)
delete(stripped, codexFingerprintSeedExtraKey)
return stripped
}
func codexFingerprintModeFromExtra(extra map[string]any) codexFingerprintMode {
if extra == nil {
return codexFingerprintOff
}
raw, _ := extra[codexFingerprintModeExtraKey].(string)
switch codexFingerprintMode(strings.TrimSpace(raw)) {
case codexFingerprintOff, codexFingerprintDevice, codexFingerprintSession, codexFingerprintFull:
return codexFingerprintMode(strings.TrimSpace(raw))
default:
return codexFingerprintOff
}
}
func codexFingerprintModeRequiresSeed(mode codexFingerprintMode) bool {
switch mode {
case codexFingerprintDevice, codexFingerprintSession, codexFingerprintFull:
return true
default:
return false
}
}
func codexFingerprintSeed(extra map[string]any) (string, bool) {
if extra == nil {
return "", false
}
return canonicalCodexFingerprintSeed(extra[codexFingerprintSeedExtraKey])
}
func prepareCodexFingerprintExtraForCreate(platform, accountType string, extra map[string]any) map[string]any {
prepared := stripCodexFingerprintSeed(extra)
if platform != PlatformOpenAI || accountType != AccountTypeOAuth || !codexFingerprintModeRequiresSeed(codexFingerprintModeFromExtra(prepared)) {
return prepared
}
if prepared == nil {
prepared = make(map[string]any, 1)
}
prepared[codexFingerprintSeedExtraKey] = newCodexFingerprintSeed()
return prepared
}
func prepareCodexFingerprintExtraForUpdate(account *Account, extra map[string]any) map[string]any {
prepared := stripCodexFingerprintSeed(extra)
if account == nil || account.Platform != PlatformOpenAI || account.Type != AccountTypeOAuth {
return prepared
}
if seed, ok := codexFingerprintSeed(account.Extra); ok {
if prepared == nil {
prepared = make(map[string]any, 1)
}
prepared[codexFingerprintSeedExtraKey] = seed
return prepared
}
if codexFingerprintModeRequiresSeed(codexFingerprintModeFromExtra(prepared)) {
if prepared == nil {
prepared = make(map[string]any, 1)
}
prepared[codexFingerprintSeedExtraKey] = newCodexFingerprintSeed()
}
return prepared
}
func sanitizedCodexFingerprintExtraUpdates(updates map[string]any) map[string]any {
if updates == nil {
return nil
}
sanitized := maps.Clone(updates)
delete(sanitized, codexFingerprintSeedExtraKey)
return sanitized
}
// ShouldEnsureCodexFingerprintSeedForExtraUpdates reports whether a JSONB key-level
// extra update is enabling Codex fingerprint convergence and therefore must atomically
// preserve or create the system-managed per-account seed in the repository update.
func ShouldEnsureCodexFingerprintSeedForExtraUpdates(updates map[string]any) bool {
if updates == nil {
return false
}
return codexFingerprintModeRequiresSeed(codexFingerprintModeFromExtra(updates))
}
// GetCodexFingerprintMode 从账号 extra JSON 读取指纹收敛模式。
//
@@ -85,13 +206,7 @@ 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 codexFingerprintOff
}
return codexFingerprintModeFromExtra(a.Extra)
}
// deriveStableUUIDv4 从种子确定性派生一个 UUIDv4 格式的字符串。
@@ -110,45 +225,53 @@ func deriveStableUUIDv4(seed string) string {
}
// resolveConvergedInstallationID 返回账号级恒定的 installation_id。
// 优先使用管理员配置的真实 device_id,无则从 accountID 确定性派生。
func resolveConvergedInstallationID(account *Account) string {
// 优先使用管理员配置的真实 device_id,无则从系统管理的账号随机种子确定性派生。
func resolveConvergedInstallationID(account *Account, seed string) string {
if account == nil {
return ""
}
if deviceID := account.GetOpenAIDeviceID(); deviceID != "" {
return deviceID
}
return deriveStableUUIDv4(fmt.Sprintf("sub2api:codex-install-id:v1:%d", account.ID))
if seed == "" {
return ""
}
return deriveStableUUIDv4("sub2api:codex-install-id:v2:" + seed)
}
// resolveConvergedSessionID 返回账号级恒定的 session_id。
func resolveConvergedSessionID(account *Account) string {
if account == nil {
func resolveConvergedSessionID(seed string) string {
if seed == "" {
return ""
}
return deriveStableUUIDv4(fmt.Sprintf("sub2api:codex-session-id:v1:%d", account.ID))
return deriveStableUUIDv4("sub2api:codex-session-id:v2:" + seed)
}
// resolveConvergedThreadID 按客户端原始 session-id 确定性派生 thread_id。
// 每个真实 Codex 会话(不同客户端启动实例)获得一个独立线程,
// 模拟正常用户 spawn 子代理或开多窗口的模式。
func resolveConvergedThreadID(account *Account, clientSessionID string) string {
if account == nil || clientSessionID == "" {
func resolveConvergedThreadID(seed, clientSessionID string) string {
if seed == "" || clientSessionID == "" {
return ""
}
return deriveStableUUIDv4(fmt.Sprintf("sub2api:codex-thread-id:v1:%d:%s", account.ID, clientSessionID))
return deriveStableUUIDv4("sub2api:codex-thread-id:v2:" + seed + ":" + clientSessionID)
}
// codexFingerprintIDs 收敛后的完整 ID 集合。
// 由 resolveCodexFingerprintIDs 一次性生成,同一个实例在头改写和体改写之间共享,
// 确保所有载体中的 turn_id 等随机字段一致。
// 确保所有载体中的 turn_id 等随机字段一致。体改写时还会补记原始
// client_metadata.session_id,用于识别 root prompt_cache_key 的默认值。
type codexFingerprintIDs struct {
mode codexFingerprintMode
installationID string
sessionID string
threadID string
turnID string
windowID string
accountID int64
mode codexFingerprintMode
installationID string
sessionID string
threadID string
turnID string
windowID string
turnStartedAtUnixMs int64
originalBodySessionID string
originalBodySessionIDCaptured bool
}
// resolveCodexFingerprintIDs 按收敛模式计算出站 ID 集合。
@@ -157,13 +280,21 @@ type codexFingerprintIDs struct {
// 返回 nil 表示 off 模式,不需要改写。
// 注意:包含随机生成的 turn_id,调用方必须只调用一次并共享结果给头改写和体改写。
func resolveCodexFingerprintIDs(account *Account, clientSessionID string, mode codexFingerprintMode) *codexFingerprintIDs {
if mode == codexFingerprintOff {
if account == nil || mode == codexFingerprintOff {
return nil
}
seed, ok := codexFingerprintSeed(account.Extra)
if !ok {
return nil
}
ids := &codexFingerprintIDs{mode: mode}
ids := &codexFingerprintIDs{
accountID: account.ID,
mode: mode,
turnStartedAtUnixMs: time.Now().UnixMilli(),
}
ids.installationID = resolveConvergedInstallationID(account)
ids.installationID = resolveConvergedInstallationID(account, seed)
if ids.installationID == "" {
return nil
}
@@ -173,8 +304,8 @@ func resolveCodexFingerprintIDs(account *Account, clientSessionID string, mode c
return ids
case codexFingerprintSession:
ids.sessionID = resolveConvergedSessionID(account)
ids.threadID = resolveConvergedThreadID(account, clientSessionID)
ids.sessionID = resolveConvergedSessionID(seed)
ids.threadID = resolveConvergedThreadID(seed, clientSessionID)
if ids.threadID == "" {
ids.threadID = ids.sessionID
}
@@ -183,7 +314,7 @@ func resolveCodexFingerprintIDs(account *Account, clientSessionID string, mode c
return ids
case codexFingerprintFull:
ids.sessionID = resolveConvergedSessionID(account)
ids.sessionID = resolveConvergedSessionID(seed)
ids.threadID = ids.sessionID
ids.turnID = uuid.Must(uuid.NewV7()).String()
ids.windowID = ids.threadID + ":0"
@@ -252,20 +383,21 @@ func applyCodexFingerprintHeaders(h http.Header, ids *codexFingerprintIDs) {
"thread_id": ids.threadID,
"turn_id": ids.turnID,
"window_id": ids.windowID,
"turn_started_at_unix_ms": time.Now().UnixMilli(),
"turn_started_at_unix_ms": ids.turnStartedAtUnixMs,
})
}
// rewriteCodexTurnMetadataFields 解析 x-codex-turn-metadata 头中的 JSON
// 替换指定字段后回写。保留未指定字段原样(如 sandbox、thread_source 等)。
// 替换指定字段后回写。合法对象保留未指定字段(如 sandbox、thread_source);
// 非法/非对象值重建为最小合法 metadata,避免 flat 与 embedded identity 分裂。
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
if err := json.Unmarshal([]byte(raw), &metadata); err != nil || metadata == nil {
metadata = make(map[string]any, len(fields))
}
for k, v := range fields {
metadata[k] = v
@@ -284,16 +416,21 @@ func applyCodexFingerprintClientMetadata(reqBody map[string]any, ids *codexFinge
return false
}
captureCodexFingerprintOriginalBodySessionID(ids, reqBody["client_metadata"])
existing, _ := reqBody["client_metadata"].(map[string]any)
if existing == nil {
existing = make(map[string]any)
}
if !applyCodexFingerprintToClientMetadataMap(existing, ids) {
return false
modified := false
if applyCodexFingerprintToClientMetadataMap(existing, ids) {
reqBody["client_metadata"] = existing
modified = true
}
reqBody["client_metadata"] = existing
return true
if applyCodexFingerprintPromptCacheKey(reqBody, ids) {
modified = true
}
return modified
}
// applyCodexFingerprintToClientMetadataMap 是 client_metadata 改写的共享核心,
@@ -330,59 +467,130 @@ func applyCodexFingerprintToClientMetadataMap(existing map[string]any, ids *code
"thread_id": ids.threadID,
"turn_id": ids.turnID,
"window_id": ids.windowID,
"turn_started_at_unix_ms": time.Now().UnixMilli(),
"turn_started_at_unix_ms": ids.turnStartedAtUnixMs,
})
return true
}
func captureCodexFingerprintOriginalBodySessionID(ids *codexFingerprintIDs, clientMetadata any) {
if ids == nil || ids.originalBodySessionIDCaptured {
return
}
ids.originalBodySessionIDCaptured = true
if clientMetadata == nil {
return
}
switch metadata := clientMetadata.(type) {
case map[string]any:
if sessionID, ok := metadata["session_id"].(string); ok {
ids.originalBodySessionID = strings.TrimSpace(sessionID)
}
case map[string]string:
ids.originalBodySessionID = strings.TrimSpace(metadata["session_id"])
}
}
func captureCodexFingerprintOriginalBodySessionIDRaw(ids *codexFingerprintIDs, value gjson.Result) {
if ids == nil || ids.originalBodySessionIDCaptured {
return
}
ids.originalBodySessionIDCaptured = true
if value.Exists() && value.Type == gjson.String {
ids.originalBodySessionID = strings.TrimSpace(value.String())
}
}
func shouldRewriteCodexFingerprintPromptCacheKey(ids *codexFingerprintIDs, promptCacheKey string) bool {
if ids == nil || !ids.originalBodySessionIDCaptured || ids.originalBodySessionID == "" || ids.sessionID == "" {
return false
}
if ids.mode != codexFingerprintSession && ids.mode != codexFingerprintFull {
return false
}
return promptCacheKey == ids.originalBodySessionID
}
func applyCodexFingerprintPromptCacheKey(reqBody map[string]any, ids *codexFingerprintIDs) bool {
if reqBody == nil {
return false
}
promptCacheKey, ok := reqBody["prompt_cache_key"].(string)
if !ok || strings.TrimSpace(promptCacheKey) == "" || !shouldRewriteCodexFingerprintPromptCacheKey(ids, promptCacheKey) {
return false
}
if promptCacheKey == ids.sessionID {
return false
}
reqBody["prompt_cache_key"] = ids.sessionID
return true
}
// applyCodexFingerprintClientMetadataRaw 在原始 JSON 字节上改写 client_metadata
// 供透传路径使用——透传是热路径,禁止对可能高达数十 MB 的 body 做全量
// Unmarshal(见 forwardOpenAIPassthrough 的轻量提取注释)。实现为:gjson 提取
// client_metadata 小对象单独解码,经共享核心改写后 sjson 一次性拼回,body
// 其余字节原样保留。语义与 applyCodexFingerprintClientMetadata 逐点一致
// (含"非对象值整体替换为收敛集合"的行为)。
// 其余字节原样保留root prompt_cache_key 仅在可证明是 body session 默认值时
// 做标量改写。语义与 applyCodexFingerprintClientMetadata 逐点一致(含
// "非对象值整体替换为收敛集合"的行为)。
func applyCodexFingerprintClientMetadataRaw(body []byte, ids *codexFingerprintIDs) ([]byte, bool, error) {
if len(body) == 0 || ids == nil {
return body, false, nil
}
// 非 JSON 对象的 body(数组/标量/畸形)没有 client_metadata 语义,
// sjson 在这类根上写字段会改写整体结构,直接放行保持原样。
if !gjson.ParseBytes(body).IsObject() {
root := gjson.ParseBytes(body)
if !root.IsObject() {
captureCodexFingerprintOriginalBodySessionIDRaw(ids, gjson.Result{})
return body, false, nil
}
existing := map[string]any{}
if cm := gjson.GetBytes(body, "client_metadata"); cm.IsObject() {
captureCodexFingerprintOriginalBodySessionIDRaw(ids, gjson.GetBytes(body, "client_metadata.session_id"))
if err := json.Unmarshal([]byte(cm.Raw), &existing); err != nil {
return body, false, fmt.Errorf("decode client_metadata for fingerprint: %w", err)
}
} else {
captureCodexFingerprintOriginalBodySessionIDRaw(ids, gjson.Result{})
}
if !applyCodexFingerprintToClientMetadataMap(existing, ids) {
return body, false, nil
next := body
modified := false
if applyCodexFingerprintToClientMetadataMap(existing, ids) {
raw, err := json.Marshal(existing)
if err != nil {
return body, false, fmt.Errorf("encode converged client_metadata: %w", err)
}
var setErr error
next, setErr = sjson.SetRawBytes(body, "client_metadata", raw)
if setErr != nil {
return body, false, fmt.Errorf("splice converged client_metadata: %w", setErr)
}
modified = true
}
raw, err := json.Marshal(existing)
if err != nil {
return body, false, fmt.Errorf("encode converged client_metadata: %w", err)
promptCacheKey := gjson.GetBytes(body, "prompt_cache_key")
if promptCacheKey.Exists() && promptCacheKey.Type == gjson.String && strings.TrimSpace(promptCacheKey.String()) != "" && shouldRewriteCodexFingerprintPromptCacheKey(ids, promptCacheKey.String()) {
rewritten, err := sjson.SetBytes(next, "prompt_cache_key", ids.sessionID)
if err != nil {
return body, false, fmt.Errorf("splice converged prompt_cache_key: %w", err)
}
next = rewritten
modified = true
}
next, err := sjson.SetRawBytes(body, "client_metadata", raw)
if err != nil {
return body, false, fmt.Errorf("splice converged client_metadata: %w", err)
}
return next, true, nil
return next, modified, nil
}
// rewriteClientMetadataEmbeddedTurnMetadata 改写 client_metadata 中内嵌的
// x-codex-turn-metadata JSON 字符串里的指定字段。
// x-codex-turn-metadata JSON 字符串里的指定字段。非法/非对象值会重建,
// 避免 flat client_metadata 与 embedded metadata 暴露两套身份。
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
if err := json.Unmarshal([]byte(raw), &metadata); err != nil || metadata == nil {
metadata = make(map[string]any, len(fields))
}
for k, v := range fields {
metadata[k] = v
@@ -13,7 +13,17 @@ import (
"github.com/stretchr/testify/require"
)
const testCodexFingerprintSeed = "11111111-1111-4111-8111-111111111111"
func newTestOAuthAccount(id int64, extra map[string]any) *Account {
if codexFingerprintModeRequiresSeed(codexFingerprintModeFromExtra(extra)) {
if extra == nil {
extra = make(map[string]any)
}
if _, exists := extra[codexFingerprintSeedExtraKey]; !exists {
extra[codexFingerprintSeedExtraKey] = testCodexFingerprintSeed
}
}
return &Account{
ID: id,
Platform: PlatformOpenAI,
@@ -75,42 +85,40 @@ func TestGetCodexFingerprintMode(t *testing.T) {
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))
assert.Equal(t, "real-device-id", resolveConvergedInstallationID(account, testCodexFingerprintSeed))
}
func TestResolveConvergedInstallationID_DerivesFromAccountID(t *testing.T) {
func TestResolveConvergedInstallationID_DerivesFromSeed(t *testing.T) {
account := newTestOAuthAccount(42, nil)
result := resolveConvergedInstallationID(account)
result := resolveConvergedInstallationID(account, testCodexFingerprintSeed)
_, err := uuid.Parse(result)
require.NoError(t, err, "派生值应为合法 UUID")
assert.Equal(t, result, resolveConvergedInstallationID(account), "确定性")
assert.Equal(t, result, resolveConvergedInstallationID(account, testCodexFingerprintSeed), "确定性")
}
func TestResolveConvergedInstallationID_DifferentAccounts(t *testing.T) {
a := resolveConvergedInstallationID(newTestOAuthAccount(1, nil))
b := resolveConvergedInstallationID(newTestOAuthAccount(2, nil))
func TestResolveConvergedInstallationID_DifferentSeeds(t *testing.T) {
account := newTestOAuthAccount(1, nil)
a := resolveConvergedInstallationID(account, testCodexFingerprintSeed)
b := resolveConvergedInstallationID(account, "22222222-2222-4222-8222-222222222222")
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")
a := resolveConvergedThreadID(testCodexFingerprintSeed, "session-aaa")
b := resolveConvergedThreadID(testCodexFingerprintSeed, "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")
a := resolveConvergedThreadID(testCodexFingerprintSeed, "session-aaa")
b := resolveConvergedThreadID(testCodexFingerprintSeed, "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, ""))
assert.Equal(t, "", resolveConvergedThreadID(testCodexFingerprintSeed, ""))
}
// --- off 模式:resolveCodexFingerprintIDsFromRequest 返回 nil ---
@@ -141,6 +149,25 @@ func TestResolveCodexFingerprintIDsFromRequest_ExplicitOptInHonored(t *testing.T
}
}
func TestResolveCodexFingerprintIDsFromRequest_EnabledModesRequireValidSeed(t *testing.T) {
for _, tt := range []struct {
name string
extra map[string]any
}{
{name: "missing", extra: map[string]any{codexFingerprintModeExtraKey: "device"}},
{name: "missing with device override", extra: map[string]any{codexFingerprintModeExtraKey: "device", "openai_device_id": "real-device"}},
{name: "blank", extra: map[string]any{codexFingerprintModeExtraKey: "session", codexFingerprintSeedExtraKey: ""}},
{name: "uppercase", extra: map[string]any{codexFingerprintModeExtraKey: "full", codexFingerprintSeedExtraKey: "11111111-1111-4111-8111-AAAAAAAAAAAA"}},
{name: "nil uuid", extra: map[string]any{codexFingerprintModeExtraKey: "device", codexFingerprintSeedExtraKey: "00000000-0000-0000-0000-000000000000"}},
{name: "non string", extra: map[string]any{codexFingerprintModeExtraKey: "session", codexFingerprintSeedExtraKey: 123}},
} {
t.Run(tt.name, func(t *testing.T) {
account := &Account{ID: 1, Platform: PlatformOpenAI, Type: AccountTypeOAuth, Extra: tt.extra}
require.Nil(t, resolveCodexFingerprintIDsFromRequest(account, nil))
})
}
}
// --- applyCodexFingerprintHeaders: off 模式 ---
func TestApplyCodexFingerprintHeaders_OffMode(t *testing.T) {
@@ -199,9 +226,11 @@ func TestApplyCodexFingerprintHeaders_SessionMode(t *testing.T) {
ids := resolveCodexFingerprintIDsFromRequest(account, clientHeaders)
applyCodexFingerprintHeaders(h, ids)
convergedInstall := resolveConvergedInstallationID(account)
convergedSession := resolveConvergedSessionID(account)
convergedThread := resolveConvergedThreadID(account, "client-session-aaa")
seed, ok := codexFingerprintSeed(account.Extra)
require.True(t, ok)
convergedInstall := resolveConvergedInstallationID(account, seed)
convergedSession := resolveConvergedSessionID(seed)
convergedThread := resolveConvergedThreadID(seed, "client-session-aaa")
assert.Equal(t, convergedInstall, h.Get("x-codex-installation-id"))
assert.Equal(t, convergedSession, h.Get("session-id"))
@@ -257,7 +286,9 @@ func TestApplyCodexFingerprintHeaders_FullMode(t *testing.T) {
account := newTestOAuthAccount(1, map[string]any{
codexFingerprintModeExtraKey: "full",
})
convergedSession := resolveConvergedSessionID(account)
seed, ok := codexFingerprintSeed(account.Extra)
require.True(t, ok)
convergedSession := resolveConvergedSessionID(seed)
clientA := http.Header{}
clientA.Set("session-id", "client-A")
@@ -329,6 +360,41 @@ func TestFingerprintIDs_HeaderAndBody_TurnID_Consistent(t *testing.T) {
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")
assert.Equal(t, headerMeta["turn_started_at_unix_ms"], bodyMeta["turn_started_at_unix_ms"], "头和体的 timestamp 必须一致")
assert.Equal(t, float64(ids.turnStartedAtUnixMs), headerMeta["turn_started_at_unix_ms"])
}
func TestFingerprintIDs_MalformedEmbeddedMetadataRebuiltConsistently(t *testing.T) {
account := newTestOAuthAccount(2, map[string]any{codexFingerprintModeExtraKey: "session"})
clientHeaders := make(http.Header)
clientHeaders.Set("session-id", "client-session-malformed")
ids := resolveCodexFingerprintIDsFromRequest(account, clientHeaders)
require.NotNil(t, ids)
h := make(http.Header)
h.Set("x-codex-turn-metadata", "{malformed")
applyCodexFingerprintHeaders(h, ids)
reqBody := map[string]any{
"client_metadata": map[string]any{
"session_id": "client-session-malformed",
"x-codex-turn-metadata": "[malformed",
},
}
require.True(t, applyCodexFingerprintClientMetadata(reqBody, ids))
var headerMeta map[string]any
require.NoError(t, json.Unmarshal([]byte(h.Get("x-codex-turn-metadata")), &headerMeta))
clientMetadata, ok := reqBody["client_metadata"].(map[string]any)
require.True(t, ok)
bodyRaw, ok := clientMetadata["x-codex-turn-metadata"].(string)
require.True(t, ok)
var bodyMeta map[string]any
require.NoError(t, json.Unmarshal([]byte(bodyRaw), &bodyMeta))
for _, key := range []string{"installation_id", "session_id", "thread_id", "turn_id", "window_id", "turn_started_at_unix_ms"} {
assert.Equal(t, headerMeta[key], bodyMeta[key], "rebuilt metadata field %s must match", key)
}
}
// --- applyCodexFingerprintClientMetadata ---
@@ -400,9 +466,11 @@ func TestApplyCodexFingerprintClientMetadata_SessionMode(t *testing.T) {
cm, ok := reqBody["client_metadata"].(map[string]any)
require.True(t, ok)
convergedInstall := resolveConvergedInstallationID(account)
convergedSession := resolveConvergedSessionID(account)
convergedThread := resolveConvergedThreadID(account, "client-session-aaa")
seed, ok := codexFingerprintSeed(account.Extra)
require.True(t, ok)
convergedInstall := resolveConvergedInstallationID(account, seed)
convergedSession := resolveConvergedSessionID(seed)
convergedThread := resolveConvergedThreadID(seed, "client-session-aaa")
assert.Equal(t, convergedInstall, cm["x-codex-installation-id"])
assert.Equal(t, convergedSession, cm["session_id"])
@@ -441,7 +509,9 @@ func TestApplyCodexFingerprintClientMetadata_FullMode(t *testing.T) {
cm, ok := reqBody["client_metadata"].(map[string]any)
require.True(t, ok)
convergedSession := resolveConvergedSessionID(account)
seed, ok := codexFingerprintSeed(account.Extra)
require.True(t, ok)
convergedSession := resolveConvergedSessionID(seed)
assert.Equal(t, convergedSession, cm["session_id"])
assert.Equal(t, convergedSession, cm["thread_id"], "full 模式 thread_id 应等于 session_id")
@@ -496,6 +566,182 @@ func rawVsMapClientMetadata(t *testing.T, body []byte, ids *codexFingerprintIDs)
return mapCM, rawCM
}
func cloneCodexFingerprintIDsForTest(ids *codexFingerprintIDs) *codexFingerprintIDs {
if ids == nil {
return nil
}
cloned := *ids
cloned.originalBodySessionID = ""
cloned.originalBodySessionIDCaptured = false
return &cloned
}
func applyMapAndRawFingerprintBodiesForTest(t *testing.T, body []byte, ids *codexFingerprintIDs) (map[string]any, map[string]any) {
t.Helper()
mapIDs := cloneCodexFingerprintIDsForTest(ids)
rawIDs := cloneCodexFingerprintIDsForTest(ids)
var decoded map[string]any
require.NoError(t, json.Unmarshal(body, &decoded))
applyCodexFingerprintClientMetadata(decoded, mapIDs)
rawBody, _, err := applyCodexFingerprintClientMetadataRaw(body, rawIDs)
require.NoError(t, err)
var rawDecoded map[string]any
require.NoError(t, json.Unmarshal(rawBody, &rawDecoded))
return decoded, rawDecoded
}
func TestApplyCodexFingerprintPromptCacheKey_MapRawEquivalence(t *testing.T) {
for _, mode := range []codexFingerprintMode{codexFingerprintSession, codexFingerprintFull} {
t.Run(string(mode)+"/default", func(t *testing.T) {
account := newTestOAuthAccount(4300, map[string]any{codexFingerprintModeExtraKey: string(mode)})
ids := resolveCodexFingerprintIDs(account, "header-session", mode)
require.NotNil(t, ids)
body := []byte(`{"model":"gpt-5.6-sol","prompt_cache_key":"body-session","client_metadata":{"session_id":" body-session ","trace":"keep"},"input":[]}`)
mapBody, rawBody := applyMapAndRawFingerprintBodiesForTest(t, body, ids)
require.Equal(t, mapBody["prompt_cache_key"], rawBody["prompt_cache_key"])
require.Equal(t, ids.sessionID, mapBody["prompt_cache_key"])
mapCM, _ := mapBody["client_metadata"].(map[string]any)
rawCM, _ := rawBody["client_metadata"].(map[string]any)
require.Equal(t, ids.sessionID, mapCM["session_id"])
require.Equal(t, mapCM["session_id"], rawCM["session_id"])
require.Equal(t, "keep", rawCM["trace"])
})
}
t.Run("explicit override", func(t *testing.T) {
account := newTestOAuthAccount(4301, map[string]any{codexFingerprintModeExtraKey: "session"})
ids := resolveCodexFingerprintIDs(account, "header-session", codexFingerprintSession)
require.NotNil(t, ids)
body := []byte(`{"model":"gpt-5.6-sol","prompt_cache_key":"explicit-cache","client_metadata":{"session_id":"body-session"},"input":[]}`)
mapBody, rawBody := applyMapAndRawFingerprintBodiesForTest(t, body, ids)
require.Equal(t, "explicit-cache", mapBody["prompt_cache_key"])
require.Equal(t, "explicit-cache", rawBody["prompt_cache_key"])
mapCM, _ := mapBody["client_metadata"].(map[string]any)
rawCM, _ := rawBody["client_metadata"].(map[string]any)
require.Equal(t, ids.sessionID, mapCM["session_id"])
require.Equal(t, ids.sessionID, rawCM["session_id"])
})
}
func TestApplyCodexFingerprintPromptCacheKey_Negatives(t *testing.T) {
sessionAccount := newTestOAuthAccount(4310, map[string]any{codexFingerprintModeExtraKey: "session"})
sessionIDs := resolveCodexFingerprintIDs(sessionAccount, "header-session", codexFingerprintSession)
require.NotNil(t, sessionIDs)
deviceAccount := newTestOAuthAccount(4311, map[string]any{codexFingerprintModeExtraKey: "device"})
deviceIDs := resolveCodexFingerprintIDs(deviceAccount, "header-session", codexFingerprintDevice)
require.NotNil(t, deviceIDs)
tests := []struct {
name string
body []byte
ids *codexFingerprintIDs
wantExists bool
wantCacheKey any
wantRawString string
}{
{
name: "missing key is not injected",
body: []byte(`{"client_metadata":{"session_id":"body-session"}}`),
ids: sessionIDs,
wantExists: false,
},
{
name: "empty key preserved",
body: []byte(`{"prompt_cache_key":"","client_metadata":{"session_id":"body-session"}}`),
ids: sessionIDs,
wantExists: true,
wantCacheKey: "",
},
{
name: "whitespace-different key is an explicit override",
body: []byte(`{"prompt_cache_key":" body-session ","client_metadata":{"session_id":"body-session"}}`),
ids: sessionIDs,
wantExists: true,
wantCacheKey: " body-session ",
},
{
name: "non-string key preserved",
body: []byte(`{"prompt_cache_key":123,"client_metadata":{"session_id":"body-session"}}`),
ids: sessionIDs,
wantExists: true,
wantCacheKey: float64(123),
},
{
name: "missing source metadata preserves key",
body: []byte(`{"prompt_cache_key":"body-session"}`),
ids: sessionIDs,
wantExists: true,
wantCacheKey: "body-session",
},
{
name: "non-string source session preserves key",
body: []byte(`{"prompt_cache_key":"123","client_metadata":{"session_id":123}}`),
ids: sessionIDs,
wantExists: true,
wantCacheKey: "123",
},
{
name: "non-object source metadata preserves key",
body: []byte(`{"prompt_cache_key":"body-session","client_metadata":"bad"}`),
ids: sessionIDs,
wantExists: true,
wantCacheKey: "body-session",
},
{
name: "device mode preserves key",
body: []byte(`{"prompt_cache_key":"body-session","client_metadata":{"session_id":"body-session"}}`),
ids: deviceIDs,
wantExists: true,
wantCacheKey: "body-session",
},
{
name: "off mode preserves body",
body: []byte(`{"prompt_cache_key":"body-session","client_metadata":{"session_id":"body-session"}}`),
ids: nil,
wantExists: true,
wantCacheKey: "body-session",
wantRawString: `{"prompt_cache_key":"body-session","client_metadata":{"session_id":"body-session"}}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var mapBody map[string]any
require.NoError(t, json.Unmarshal(tt.body, &mapBody))
changedMap := applyCodexFingerprintClientMetadata(mapBody, cloneCodexFingerprintIDsForTest(tt.ids))
rawBody, changedRaw, err := applyCodexFingerprintClientMetadataRaw(tt.body, cloneCodexFingerprintIDsForTest(tt.ids))
require.NoError(t, err)
if tt.ids == nil {
require.False(t, changedMap)
require.False(t, changedRaw)
require.JSONEq(t, tt.wantRawString, string(rawBody))
return
}
require.True(t, changedMap)
require.True(t, changedRaw)
rawDecoded := map[string]any{}
require.NoError(t, json.Unmarshal(rawBody, &rawDecoded))
_, mapExists := mapBody["prompt_cache_key"]
_, rawExists := rawDecoded["prompt_cache_key"]
require.Equal(t, tt.wantExists, mapExists)
require.Equal(t, tt.wantExists, rawExists)
if tt.wantExists {
require.Equal(t, tt.wantCacheKey, mapBody["prompt_cache_key"])
require.Equal(t, tt.wantCacheKey, rawDecoded["prompt_cache_key"])
}
})
}
}
func TestApplyCodexFingerprintClientMetadataRaw_MatchesMapVariant(t *testing.T) {
embedded := `{\"installation_id\":\"real-install\",\"session_id\":\"real-session\",\"sandbox\":\"seatbelt\"}`
bodies := map[string]string{
@@ -504,7 +750,7 @@ func TestApplyCodexFingerprintClientMetadataRaw_MatchesMapVariant(t *testing.T)
"non_object_value": `{"model":"gpt-5.6-sol","client_metadata":"bogus","stream":true}`,
}
for _, mode := range []codexFingerprintMode{codexFingerprintDevice, codexFingerprintSession, codexFingerprintFull} {
account := newTestOAuthAccount(4242, nil)
account := newTestOAuthAccount(4242, map[string]any{codexFingerprintModeExtraKey: string(mode)})
ids := resolveCodexFingerprintIDs(account, "client-sess-raw", mode)
require.NotNil(t, ids)
for name, body := range bodies {
@@ -517,7 +763,7 @@ func TestApplyCodexFingerprintClientMetadataRaw_MatchesMapVariant(t *testing.T)
}
func TestApplyCodexFingerprintClientMetadataRaw_PreservesUnrelatedFields(t *testing.T) {
account := newTestOAuthAccount(4243, nil)
account := newTestOAuthAccount(4243, map[string]any{codexFingerprintModeExtraKey: "session"})
ids := resolveCodexFingerprintIDs(account, "client-sess-preserve", codexFingerprintSession)
require.NotNil(t, ids)
@@ -562,7 +808,7 @@ func newFingerprintStageTestContext(t *testing.T) *gin.Context {
func TestStageCodexFingerprintIDs_NilOverwritesPreviousAccount(t *testing.T) {
c := newFingerprintStageTestContext(t)
accountA := newTestOAuthAccount(1001, nil)
accountA := newTestOAuthAccount(1001, map[string]any{codexFingerprintModeExtraKey: "session"})
idsA := resolveCodexFingerprintIDs(accountA, "sess-x", codexFingerprintSession)
require.NotNil(t, idsA)
stageCodexFingerprintIDs(c, idsA)
@@ -578,9 +824,30 @@ func TestStageCodexFingerprintIDs_NilOverwritesPreviousAccount(t *testing.T) {
assert.Empty(t, h.Get("x-codex-installation-id"))
}
func TestApplyStagedCodexFingerprintRejectsDifferentOAuthAccount(t *testing.T) {
c := newFingerprintStageTestContext(t)
accountA := newTestOAuthAccount(1003, map[string]any{codexFingerprintModeExtraKey: "session"})
idsA := resolveCodexFingerprintIDs(accountA, "sess-a", codexFingerprintSession)
require.NotNil(t, idsA)
stageCodexFingerprintIDs(c, idsA)
accountB := newTestOAuthAccount(1004, map[string]any{codexFingerprintModeExtraKey: "session"})
h := make(http.Header)
h.Set("session-id", "account-b-session")
applyStagedCodexFingerprintHeaders(c, accountB, h)
assert.Equal(t, "account-b-session", h.Get("session-id"))
assert.Empty(t, h.Get("x-codex-installation-id"))
body := map[string]any{"client_metadata": map[string]any{"session_id": "account-b-session"}}
assert.False(t, applyStagedCodexFingerprintClientMetadata(c, accountB, body))
clientMetadata, ok := body["client_metadata"].(map[string]any)
require.True(t, ok)
assert.Equal(t, "account-b-session", clientMetadata["session_id"])
}
func TestApplyStagedCodexFingerprintHeaders_SkipsNonOAuthAccount(t *testing.T) {
c := newFingerprintStageTestContext(t)
oauthIDs := resolveCodexFingerprintIDs(newTestOAuthAccount(1003, nil), "sess-y", codexFingerprintSession)
oauthIDs := resolveCodexFingerprintIDs(newTestOAuthAccount(1003, map[string]any{codexFingerprintModeExtraKey: "session"}), "sess-y", codexFingerprintSession)
require.NotNil(t, oauthIDs)
stageCodexFingerprintIDs(c, oauthIDs)
@@ -643,12 +910,12 @@ func TestBuildUpstreamRequestOpenAIPassthrough_OffModeKeepsIsolatedSession(t *te
require.NoError(t, err)
assert.NotEmpty(t, req.Header.Get("session_id"))
assert.NotEqual(t, resolveConvergedSessionID(account), req.Header.Get("session_id"), "off 模式不得收敛 session_id")
assert.NotEqual(t, resolveConvergedSessionID(testCodexFingerprintSeed), req.Header.Get("session_id"), "off 模式不得收敛 session_id")
assert.Empty(t, req.Header.Get("x-codex-window-id"))
}
func TestApplyCodexFingerprintClientMetadataRaw_NonObjectBodyUntouched(t *testing.T) {
account := newTestOAuthAccount(4244, nil)
account := newTestOAuthAccount(4244, map[string]any{codexFingerprintModeExtraKey: "session"})
ids := resolveCodexFingerprintIDs(account, "client-sess-nonobj", codexFingerprintSession)
require.NotNil(t, ids)
@@ -421,6 +421,7 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
if !isCompactRequest && applyCodexClientMetadata(decoded, account) {
markDecodedModified()
}
stageCodexFingerprintIDs(c, nil)
// 指纹收敛:一次性解析收敛 ID,请求体和出站头共享同一份 IDs(保证 turn_id 等随机字段一致)。
// fingerprintIDs 在此处解析,后续 buildUpstreamRequest 中使用同一份。
if !isCompactRequest {
@@ -442,7 +443,9 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
if codexResult.NormalizedModel != "" {
upstreamModel = codexResult.NormalizedModel
}
if codexResult.PromptCacheKey != "" {
if currentPromptCacheKey, ok := decoded["prompt_cache_key"].(string); ok && currentPromptCacheKey != "" {
promptCacheKey = currentPromptCacheKey
} else if codexResult.PromptCacheKey != "" {
promptCacheKey = codexResult.PromptCacheKey
}
}
@@ -81,6 +81,7 @@ func (s *OpenAIGatewayService) forwardOpenAIPassthrough(
}
reqStream = gjson.GetBytes(body, "stream").Bool()
stageCodexFingerprintIDs(c, nil)
// 指纹收敛:与非透传路径同门控(仅 OAuth、legacy compact 形态跳过)。
// 一次性解析收敛 ID:请求体 client_metadata 在此改写(raw 字节外科
// 手术,透传热路径禁全量 Unmarshal),出站头改写由请求构造器读取
@@ -1948,6 +1948,212 @@ func TestOpenAIGatewayService_OAuthPassthrough_CodexTuiIdentityUnified(t *testin
require.Equal(t, codexCLIVersion, upstream.lastReq.Header.Get("version"))
}
func TestOpenAIGatewayService_CodexFingerprintHTTPTransformedHeaderBodyParityAndDefaultCacheKey(t *testing.T) {
gin.SetMode(gin.TestMode)
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewReader(nil))
c.Request.Header.Set("User-Agent", "codex_cli_rs/0.144.1")
c.Request.Header.Set("originator", "codex_cli_rs")
c.Request.Header.Set("session-id", "header-session")
c.Request.Header.Set("x-codex-turn-metadata", `{"installation_id":"header-install","session_id":"header-session","thread_id":"header-thread","turn_id":"header-turn","window_id":"header-window","sandbox":"seatbelt"}`)
body := []byte(`{"model":"gpt-5.2","stream":false,"prompt_cache_key":"body-session","client_metadata":{"session_id":"body-session","x-codex-turn-metadata":"{\"installation_id\":\"body-install\",\"session_id\":\"body-session\",\"thread_id\":\"body-thread\",\"turn_id\":\"body-turn\",\"window_id\":\"body-window\",\"sandbox\":\"seatbelt\"}"},"input":[{"type":"message","role":"user","content":"hi"}]}`)
upstream := &httpUpstreamRecorder{resp: &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"text/event-stream"}, "x-request-id": []string{"rid"}},
Body: io.NopCloser(strings.NewReader("data: [DONE]\n\n")),
}}
svc := &OpenAIGatewayService{
cfg: &config.Config{},
httpUpstream: upstream,
toolCorrector: NewCodexToolCorrector(),
}
account := newTestOAuthAccount(4401, map[string]any{codexFingerprintModeExtraKey: "session"})
account.Name = "oauth-transformed"
account.Status = StatusActive
account.Schedulable = true
account.Concurrency = 1
account.Credentials = map[string]any{"access_token": "oauth-token", "chatgpt_account_id": "chatgpt-acc"}
_, err := svc.Forward(context.Background(), c, account, body)
require.NoError(t, err)
require.NotNil(t, upstream.lastReq)
seed, ok := codexFingerprintSeed(account.Extra)
require.True(t, ok)
wantInstall := resolveConvergedInstallationID(account, seed)
wantSession := resolveConvergedSessionID(seed)
wantThread := resolveConvergedThreadID(seed, "header-session")
require.Equal(t, wantInstall, upstream.lastReq.Header.Get("x-codex-installation-id"))
require.Equal(t, wantSession, upstream.lastReq.Header.Get("session-id"))
require.Equal(t, wantSession, upstream.lastReq.Header.Get("session_id"))
require.Equal(t, wantThread, upstream.lastReq.Header.Get("thread-id"))
require.Equal(t, wantThread, upstream.lastReq.Header.Get("x-client-request-id"))
require.Equal(t, wantThread+":0", upstream.lastReq.Header.Get("x-codex-window-id"))
require.Equal(t, wantSession, gjson.GetBytes(upstream.lastBody, "prompt_cache_key").String())
require.Equal(t, wantInstall, gjson.GetBytes(upstream.lastBody, "client_metadata.x-codex-installation-id").String())
require.Equal(t, wantSession, gjson.GetBytes(upstream.lastBody, "client_metadata.session_id").String())
require.Equal(t, wantThread, gjson.GetBytes(upstream.lastBody, "client_metadata.thread_id").String())
require.Equal(t, wantThread+":0", gjson.GetBytes(upstream.lastBody, "client_metadata.x-codex-window-id").String())
bodyTurnMetadata := gjson.GetBytes(upstream.lastBody, "client_metadata.x-codex-turn-metadata").String()
headerTurnMetadata := upstream.lastReq.Header.Get("x-codex-turn-metadata")
require.Equal(t, wantSession, gjson.Get(bodyTurnMetadata, "session_id").String())
require.Equal(t, wantSession, gjson.Get(headerTurnMetadata, "session_id").String())
require.Equal(t, gjson.Get(bodyTurnMetadata, "turn_id").String(), gjson.Get(headerTurnMetadata, "turn_id").String())
}
func TestOpenAIGatewayService_CodexFingerprintHTTPRawPassthroughHeaderBodyParityAndDefaultCacheKey(t *testing.T) {
gin.SetMode(gin.TestMode)
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewReader(nil))
c.Request.Header.Set("User-Agent", "codex_cli_rs/0.144.1")
c.Request.Header.Set("originator", "codex_cli_rs")
c.Request.Header.Set("session-id", "header-session")
c.Request.Header.Set("x-codex-turn-metadata", `{"installation_id":"header-install","session_id":"header-session","thread_id":"header-thread","turn_id":"header-turn","window_id":"header-window","sandbox":"seatbelt"}`)
body := []byte(`{"model":"gpt-5.6-sol","stream":false,"prompt_cache_key":"body-session","client_metadata":{"session_id":"body-session","x-codex-turn-metadata":"{\"installation_id\":\"body-install\",\"session_id\":\"body-session\",\"thread_id\":\"body-thread\",\"turn_id\":\"body-turn\",\"window_id\":\"body-window\",\"sandbox\":\"seatbelt\"}"},"input":[{"type":"message","role":"user","content":"hi"}]}`)
upstream := &httpUpstreamRecorder{resp: &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"text/event-stream"}, "x-request-id": []string{"rid"}},
Body: io.NopCloser(strings.NewReader("data: [DONE]\n\n")),
}}
svc := &OpenAIGatewayService{
cfg: &config.Config{},
httpUpstream: upstream,
}
account := newTestOAuthAccount(4402, map[string]any{
codexFingerprintModeExtraKey: "session",
"openai_oauth_passthrough": true,
})
account.Name = "oauth-raw"
account.Status = StatusActive
account.Schedulable = true
account.Concurrency = 1
account.Credentials = map[string]any{"access_token": "oauth-token", "chatgpt_account_id": "chatgpt-acc"}
_, err := svc.Forward(context.Background(), c, account, body)
require.NoError(t, err)
require.NotNil(t, upstream.lastReq)
seed, ok := codexFingerprintSeed(account.Extra)
require.True(t, ok)
wantInstall := resolveConvergedInstallationID(account, seed)
wantSession := resolveConvergedSessionID(seed)
wantThread := resolveConvergedThreadID(seed, "header-session")
require.Equal(t, wantInstall, upstream.lastReq.Header.Get("x-codex-installation-id"))
require.Equal(t, wantSession, upstream.lastReq.Header.Get("session-id"))
require.Equal(t, wantSession, upstream.lastReq.Header.Get("session_id"))
require.Equal(t, wantThread, upstream.lastReq.Header.Get("thread-id"))
require.Equal(t, wantThread, upstream.lastReq.Header.Get("x-client-request-id"))
require.Equal(t, wantThread+":0", upstream.lastReq.Header.Get("x-codex-window-id"))
require.Equal(t, wantSession, gjson.GetBytes(upstream.lastBody, "prompt_cache_key").String())
require.Equal(t, wantInstall, gjson.GetBytes(upstream.lastBody, "client_metadata.x-codex-installation-id").String())
require.Equal(t, wantSession, gjson.GetBytes(upstream.lastBody, "client_metadata.session_id").String())
require.Equal(t, wantThread, gjson.GetBytes(upstream.lastBody, "client_metadata.thread_id").String())
require.Equal(t, wantThread+":0", gjson.GetBytes(upstream.lastBody, "client_metadata.x-codex-window-id").String())
bodyTurnMetadata := gjson.GetBytes(upstream.lastBody, "client_metadata.x-codex-turn-metadata").String()
headerTurnMetadata := upstream.lastReq.Header.Get("x-codex-turn-metadata")
require.Equal(t, wantSession, gjson.Get(bodyTurnMetadata, "session_id").String())
require.Equal(t, wantSession, gjson.Get(headerTurnMetadata, "session_id").String())
require.Equal(t, gjson.Get(bodyTurnMetadata, "turn_id").String(), gjson.Get(headerTurnMetadata, "turn_id").String())
}
func TestOpenAIGatewayService_CodexFingerprintCompactDoesNotRewriteBodyCacheKeyOrMetadata(t *testing.T) {
gin.SetMode(gin.TestMode)
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses/compact", bytes.NewReader(nil))
c.Request.Header.Set("User-Agent", "codex_cli_rs/0.144.1")
c.Request.Header.Set("originator", "codex_cli_rs")
c.Request.Header.Set("session-id", "header-session")
body := []byte(`{"model":"gpt-5.4","stream":false,"prompt_cache_key":"body-session","client_metadata":{"session_id":"body-session"},"input":[{"type":"message","role":"user","content":"compress"}]}`)
upstream := &httpUpstreamRecorder{resp: &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"text/event-stream"}, "x-request-id": []string{"rid"}},
Body: io.NopCloser(strings.NewReader(compactProbeSSESuccessBody)),
}}
svc := &OpenAIGatewayService{
cfg: &config.Config{},
httpUpstream: upstream,
toolCorrector: NewCodexToolCorrector(),
}
account := newTestOAuthAccount(4403, map[string]any{codexFingerprintModeExtraKey: "session"})
account.Name = "oauth-compact"
account.Status = StatusActive
account.Schedulable = true
account.Concurrency = 1
account.Credentials = map[string]any{"access_token": "oauth-token", "chatgpt_account_id": "chatgpt-acc"}
staleIDs := resolveCodexFingerprintIDs(account, "stale-session", codexFingerprintSession)
require.NotNil(t, staleIDs)
stageCodexFingerprintIDs(c, staleIDs)
_, err := svc.Forward(context.Background(), c, account, body)
require.NoError(t, err)
require.NotNil(t, upstream.lastReq)
seed, ok := codexFingerprintSeed(account.Extra)
require.True(t, ok)
require.NotEqual(t, resolveConvergedSessionID(seed), gjson.GetBytes(upstream.lastBody, "prompt_cache_key").String())
require.Equal(t, "body-session", gjson.GetBytes(upstream.lastBody, "prompt_cache_key").String())
require.Equal(t, "body-session", gjson.GetBytes(upstream.lastBody, "client_metadata.session_id").String())
require.False(t, gjson.GetBytes(upstream.lastBody, "client_metadata.x-codex-installation-id").Exists())
require.Empty(t, upstream.lastReq.Header.Get("x-codex-window-id"))
}
func TestOpenAIGatewayService_CodexFingerprintMessagesBridgeDoesNotInjectBodyPromptCacheKey(t *testing.T) {
gin.SetMode(gin.TestMode)
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewReader(nil))
c.Request.Header.Set("User-Agent", "codex_cli_rs/0.144.1")
c.Request.Header.Set("originator", "codex_cli_rs")
c.Request.Header.Set("session-id", "header-session")
body := []byte(`{"model":"gpt-5.5","stream":true,"prompt_cache_key":"anthropic-metadata-session-1","client_metadata":{"session_id":"anthropic-metadata-session-1"},"input":[{"type":"message","role":"developer","content":[{"type":"input_text","text":"` + openAICompatClaudeCodeTodoGuardMarker + `"}]},{"type":"message","role":"user","content":"hello"}]}`)
upstream := &httpUpstreamRecorder{resp: &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"text/event-stream"}, "x-request-id": []string{"rid"}},
Body: io.NopCloser(strings.NewReader("data: [DONE]\n\n")),
}}
svc := &OpenAIGatewayService{
cfg: &config.Config{},
httpUpstream: upstream,
toolCorrector: NewCodexToolCorrector(),
}
account := newTestOAuthAccount(4404, map[string]any{codexFingerprintModeExtraKey: "session"})
account.Name = "oauth-messages-bridge"
account.Status = StatusActive
account.Schedulable = true
account.Concurrency = 1
account.Credentials = map[string]any{"access_token": "oauth-token", "chatgpt_account_id": "chatgpt-acc"}
_, err := svc.Forward(context.Background(), c, account, body)
require.NoError(t, err)
require.NotNil(t, upstream.lastReq)
seed, ok := codexFingerprintSeed(account.Extra)
require.True(t, ok)
wantSession := resolveConvergedSessionID(seed)
require.False(t, gjson.GetBytes(upstream.lastBody, "prompt_cache_key").Exists())
require.Equal(t, wantSession, gjson.GetBytes(upstream.lastBody, "client_metadata.session_id").String())
require.Equal(t, wantSession, upstream.lastReq.Header.Get("session_id"))
}
func TestOpenAIGatewayService_CodexCLIOnly_RejectsNonCodexClient(t *testing.T) {
gin.SetMode(gin.TestMode)
@@ -93,7 +93,13 @@ func (s *OpenAIGatewayService) buildOpenAIWSHeaders(
headers.Add("x-codex-beta-features", value)
}
}
for _, name := range [...]string{"x-codex-window-id", "x-codex-installation-id"} {
for _, name := range [...]string{
"x-codex-window-id",
"x-codex-installation-id",
"session-id",
"thread-id",
"x-client-request-id",
} {
if value := c.Request.Header.Get(name); strings.TrimSpace(value) != "" {
headers.Set(name, value)
}
@@ -128,6 +134,7 @@ func (s *OpenAIGatewayService) buildOpenAIWSHeaders(
if metadata := strings.TrimSpace(turnMetadata); metadata != "" {
headers.Set(openAIWSTurnMetadataHeader, metadata)
}
applyStagedCodexFingerprintHeaders(c, account, headers)
if account != nil && account.Type == AccountTypeOAuth {
if err := resolveAndSetOpenAIChatGPTAccountHeaders(ctx, s.accountRepo, headers, account); err != nil {
@@ -400,6 +400,9 @@ func TestOpenAIGatewayService_BuildOpenAIWSHeadersPreservesCodexIdentity(t *test
c.Request.Header.Set("User-Agent", "codex_cli_rs/0.144.1")
c.Request.Header.Set("X-Codex-Window-ID", "window-ws")
c.Request.Header.Set("X-Codex-Installation-ID", "installation-ws")
c.Request.Header.Set("session-id", "session-ws")
c.Request.Header.Set("thread-id", "thread-ws")
c.Request.Header.Set("x-client-request-id", "client-request-ws")
c.Request.Header.Set("X-Test", "blocked")
svc := &OpenAIGatewayService{}
@@ -421,9 +424,53 @@ func TestOpenAIGatewayService_BuildOpenAIWSHeadersPreservesCodexIdentity(t *test
require.NoError(t, err)
require.Equal(t, "window-ws", headers.Get("X-Codex-Window-ID"))
require.Equal(t, "installation-ws", headers.Get("X-Codex-Installation-ID"))
require.Equal(t, "session-ws", headers.Get("session-id"))
require.Equal(t, "thread-ws", headers.Get("thread-id"))
require.Equal(t, "client-request-ws", headers.Get("x-client-request-id"))
require.Empty(t, headers.Get("X-Test"))
}
func TestOpenAIGatewayService_BuildOpenAIWSHeadersDeviceModePreservesClientSessionIdentity(t *testing.T) {
gin.SetMode(gin.TestMode)
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodGet, "/v1/responses", nil)
c.Request.Header.Set("User-Agent", "codex_cli_rs/0.144.1")
c.Request.Header.Set("X-Codex-Installation-ID", "client-installation")
c.Request.Header.Set("X-Codex-Window-ID", "client-window")
c.Request.Header.Set("session-id", "client-session")
c.Request.Header.Set("thread-id", "client-thread")
c.Request.Header.Set("x-client-request-id", "client-request")
account := newTestOAuthAccount(1300, map[string]any{codexFingerprintModeExtraKey: "device"})
ids := resolveCodexFingerprintIDsFromRequest(account, c.Request.Header)
require.NotNil(t, ids)
stageCodexFingerprintIDs(c, ids)
svc := &OpenAIGatewayService{}
headers, _, err := svc.buildOpenAIWSHeaders(
context.Background(),
c,
account,
"token",
OpenAIWSProtocolDecision{Transport: OpenAIUpstreamTransportResponsesWebsocketV2},
true,
"",
"",
"",
"",
"",
)
require.NoError(t, err)
require.Equal(t, ids.installationID, headers.Get("x-codex-installation-id"))
require.NotEqual(t, "client-installation", headers.Get("x-codex-installation-id"))
require.Equal(t, "client-window", headers.Get("x-codex-window-id"))
require.Equal(t, "client-session", headers.Get("session-id"))
require.Equal(t, "client-thread", headers.Get("thread-id"))
require.Equal(t, "client-request", headers.Get("x-client-request-id"))
}
func TestLogOpenAIWSBindResponseAccountWarn(t *testing.T) {
require.NotPanics(t, func() {
logOpenAIWSBindResponseAccountWarn(1, 2, "resp_ok", nil)
@@ -983,6 +1030,96 @@ func TestOpenAIGatewayService_Forward_WSv2_HeaderSessionFallbackFromPromptCacheK
require.True(t, gjson.Get(requestToJSONString(captureConn.lastWrite), "stream").Exists())
}
func TestOpenAIGatewayService_Forward_WSv2_CodexFingerprintHandshakeBodyParityAndDefaultCacheKey(t *testing.T) {
gin.SetMode(gin.TestMode)
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodPost, "/openai/v1/responses", nil)
c.Request.Header.Set("User-Agent", "codex_cli_rs/0.144.1")
c.Request.Header.Set("originator", "codex_cli_rs")
c.Request.Header.Set("session-id", "header-session")
c.Request.Header.Set("x-codex-turn-metadata", `{"installation_id":"header-install","session_id":"header-session","thread_id":"header-thread","turn_id":"header-turn","window_id":"header-window","sandbox":"seatbelt"}`)
cfg := &config.Config{}
cfg.Security.URLAllowlist.Enabled = false
cfg.Security.URLAllowlist.AllowInsecureHTTP = true
cfg.Gateway.OpenAIWS.Enabled = true
cfg.Gateway.OpenAIWS.OAuthEnabled = true
cfg.Gateway.OpenAIWS.APIKeyEnabled = true
cfg.Gateway.OpenAIWS.ResponsesWebsocketsV2 = true
cfg.Gateway.OpenAIWS.MaxConnsPerAccount = 1
cfg.Gateway.OpenAIWS.MinIdlePerAccount = 0
cfg.Gateway.OpenAIWS.MaxIdlePerAccount = 1
captureConn := &openAIWSCaptureConn{
events: [][]byte{
[]byte(`{"type":"response.completed","response":{"id":"resp_ws_fingerprint","model":"gpt-5.2","usage":{"input_tokens":2,"output_tokens":1}}}`),
},
}
captureDialer := &openAIWSCaptureDialer{conn: captureConn}
pool := newOpenAIWSConnPool(cfg)
pool.setClientDialerForTest(captureDialer)
svc := &OpenAIGatewayService{
cfg: cfg,
httpUpstream: &httpUpstreamRecorder{},
cache: &stubGatewayCache{},
openaiWSResolver: NewOpenAIWSProtocolResolver(cfg),
toolCorrector: NewCodexToolCorrector(),
openaiWSPool: pool,
}
account := newTestOAuthAccount(4405, map[string]any{
codexFingerprintModeExtraKey: "session",
"responses_websockets_v2_enabled": true,
})
account.Name = "oauth-ws-fingerprint"
account.Status = StatusActive
account.Schedulable = true
account.Concurrency = 1
account.Credentials = map[string]any{"access_token": "oauth-token", "chatgpt_account_id": "chatgpt-acc"}
body := []byte(`{"model":"gpt-5.2","stream":true,"prompt_cache_key":"body-session","client_metadata":{"session_id":"body-session","x-codex-turn-metadata":"{\"installation_id\":\"body-install\",\"session_id\":\"body-session\",\"thread_id\":\"body-thread\",\"turn_id\":\"body-turn\",\"window_id\":\"body-window\",\"sandbox\":\"seatbelt\"}"},"input":[{"type":"input_text","text":"hi"}]}`)
result, err := svc.Forward(context.Background(), c, account, body)
require.NoError(t, err)
require.NotNil(t, result)
require.Equal(t, "resp_ws_fingerprint", result.RequestID)
require.NotNil(t, captureConn.lastWrite)
seed, ok := codexFingerprintSeed(account.Extra)
require.True(t, ok)
wantInstall := resolveConvergedInstallationID(account, seed)
wantSession := resolveConvergedSessionID(seed)
wantThread := resolveConvergedThreadID(seed, "header-session")
payloadJSON := requestToJSONString(captureConn.lastWrite)
require.Equal(t, wantInstall, captureDialer.lastHeaders.Get("x-codex-installation-id"))
require.Equal(t, wantSession, captureDialer.lastHeaders.Get("session-id"))
require.Equal(t, wantSession, captureDialer.lastHeaders.Get("session_id"))
require.Equal(t, wantThread, captureDialer.lastHeaders.Get("thread-id"))
require.Equal(t, wantThread, captureDialer.lastHeaders.Get("x-client-request-id"))
require.Equal(t, wantThread+":0", captureDialer.lastHeaders.Get("x-codex-window-id"))
require.Equal(t, wantSession, gjson.Get(payloadJSON, "prompt_cache_key").String())
require.Equal(t, wantInstall, gjson.Get(payloadJSON, "client_metadata.x-codex-installation-id").String())
require.Equal(t, wantSession, gjson.Get(payloadJSON, "client_metadata.session_id").String())
require.Equal(t, wantThread, gjson.Get(payloadJSON, "client_metadata.thread_id").String())
require.Equal(t, wantThread+":0", gjson.Get(payloadJSON, "client_metadata.x-codex-window-id").String())
bodyTurnMetadata := gjson.Get(payloadJSON, "client_metadata.x-codex-turn-metadata").String()
headerTurnMetadata := captureDialer.lastHeaders.Get("x-codex-turn-metadata")
require.Equal(t, wantInstall, gjson.Get(bodyTurnMetadata, "installation_id").String())
require.Equal(t, wantSession, gjson.Get(bodyTurnMetadata, "session_id").String())
require.Equal(t, wantThread, gjson.Get(bodyTurnMetadata, "thread_id").String())
require.Equal(t, wantSession, gjson.Get(headerTurnMetadata, "session_id").String())
require.Equal(t, gjson.Get(bodyTurnMetadata, "turn_id").String(), gjson.Get(headerTurnMetadata, "turn_id").String())
require.NotZero(t, gjson.Get(bodyTurnMetadata, "turn_started_at_unix_ms").Int())
require.Equal(t,
gjson.Get(bodyTurnMetadata, "turn_started_at_unix_ms").Int(),
gjson.Get(headerTurnMetadata, "turn_started_at_unix_ms").Int(),
)
}
func TestOpenAIGatewayService_Forward_WSv2_ResponseDoneUsageParsed(t *testing.T) {
gin.SetMode(gin.TestMode)
@@ -62,6 +62,14 @@ func (s *OpenAIGatewayService) forwardOpenAIWSV2(
payload := s.buildOpenAIWSCreatePayload(reqBody, account)
payloadStrategy, removedKeys := applyOpenAIWSRetryPayloadStrategy(payload, attempt)
turnState := ""
turnMetadata := ""
if c != nil && c.Request != nil {
turnState = strings.TrimSpace(c.GetHeader(openAIWSTurnStateHeader))
turnMetadata = strings.TrimSpace(c.GetHeader(openAIWSTurnMetadataHeader))
}
setOpenAIWSTurnMetadata(payload, turnMetadata)
applyStagedCodexFingerprintClientMetadata(c, account, payload)
previousResponseID := openAIWSPayloadString(payload, "previous_response_id")
previousResponseIDKind := ClassifyOpenAIPreviousResponseIDKind(previousResponseID)
promptCacheKey := openAIWSPayloadString(payload, "prompt_cache_key")
@@ -79,13 +87,6 @@ func (s *OpenAIGatewayService) forwardOpenAIWSV2(
if raw, ok := payload["stream"]; ok {
streamValue = normalizeOpenAIWSLogValue(strings.TrimSpace(fmt.Sprintf("%v", raw)))
}
turnState := ""
turnMetadata := ""
if c != nil && c.Request != nil {
turnState = strings.TrimSpace(c.GetHeader(openAIWSTurnStateHeader))
turnMetadata = strings.TrimSpace(c.GetHeader(openAIWSTurnMetadataHeader))
}
setOpenAIWSTurnMetadata(payload, turnMetadata)
payloadEventType := openAIWSPayloadString(payload, "type")
if payloadEventType == "" {
payloadEventType = "response.create"
+71 -33
View File
@@ -77,7 +77,13 @@ type openAIWSAcquireRequest struct {
}
type openAIWSHandshakeCompatibilityKey struct {
betaFeatures string
betaFeatures string
codexInstallationID string
sessionIDHyphen string
sessionIDUnderscore string
threadID string
clientRequestID string
codexWindowID string
}
type openAIWSConnLease struct {
@@ -855,7 +861,7 @@ func (p *openAIWSConnPool) acquire(ctx context.Context, req openAIWSAcquireReque
retryAcquire:
accountID := req.Account.ID
compatibility := normalizeOpenAIWSHandshakeCompatibility(req.Headers)
compatibility := normalizeOpenAIWSHandshakeCompatibility(req.Account, req.Headers)
routingAffinity := normalizeOpenAIWSRoutingAffinity(req.Headers)
effectiveMaxConns := p.effectiveMaxConnsByAccount(req.Account)
if effectiveMaxConns <= 0 {
@@ -1015,37 +1021,39 @@ retryAcquire:
p.ensureTargetIdleAsync(accountID)
return lease, nil
}
for _, conn := range ap.conns {
if conn == nil || conn == best || !conn.matchesHandshakeCompatibility(compatibility) || !conn.matchesRoutingAffinity(routingAffinity) {
continue
}
if conn.tryAcquire() {
connPick := time.Since(pickStartedAt)
p.recordConnPickDuration(connPick)
ap.mu.Unlock()
closeOpenAIWSConns(evicted)
if p.shouldHealthCheckConn(conn) {
if err := conn.pingWithTimeout(openAIWSConnHealthCheckTO); err != nil {
conn.close()
p.evictConn(accountID, conn.id)
if retry < 1 {
return p.acquire(ctx, req, retry+1)
}
return nil, err
}
if routingAffinity == "" || len(ap.conns)+ap.creating >= effectiveMaxConns {
for _, conn := range ap.conns {
if conn == nil || conn == best || !conn.matchesHandshakeCompatibility(compatibility) {
continue
}
if conn.tryAcquire() {
connPick := time.Since(pickStartedAt)
p.recordConnPickDuration(connPick)
ap.mu.Unlock()
closeOpenAIWSConns(evicted)
if p.shouldHealthCheckConn(conn) {
if err := conn.pingWithTimeout(openAIWSConnHealthCheckTO); err != nil {
conn.close()
p.evictConn(accountID, conn.id)
if retry < 1 {
return p.acquire(ctx, req, retry+1)
}
return nil, err
}
}
lease := &openAIWSConnLease{pool: p, accountID: accountID, conn: conn, connPick: connPick, reused: true}
p.metrics.acquireReuseTotal.Add(1)
p.recordLastSuccessfulAcquire(accountID, acquireGeneration, req)
p.ensureTargetIdleAsync(accountID)
return lease, nil
}
lease := &openAIWSConnLease{pool: p, accountID: accountID, conn: conn, connPick: connPick, reused: true}
p.metrics.acquireReuseTotal.Add(1)
p.recordLastSuccessfulAcquire(accountID, acquireGeneration, req)
p.ensureTargetIdleAsync(accountID)
return lease, nil
}
}
}
if !req.ForceNewConn && len(ap.conns)+ap.creating >= effectiveMaxConns {
affine := p.pickLeastBusyConnWithRoutingAffinityLocked(ap, compatibility, routingAffinity)
if idle := p.pickOldestIdleConnWithoutHandshakeCompatibilityOrRoutingAffinityLocked(ap, compatibility, routingAffinity); idle != nil {
if idle := p.pickOldestIdleConnWithoutHandshakeCompatibilityLocked(ap, compatibility); idle != nil {
delete(ap.conns, idle.id)
evicted = append(evicted, idle)
p.metrics.scaleDownTotal.Add(1)
@@ -1241,10 +1249,9 @@ func (p *openAIWSConnPool) pickOldestIdleConnLocked(ap *openAIWSAccountPool) *op
return oldest
}
func (p *openAIWSConnPool) pickOldestIdleConnWithoutHandshakeCompatibilityOrRoutingAffinityLocked(
func (p *openAIWSConnPool) pickOldestIdleConnWithoutHandshakeCompatibilityLocked(
ap *openAIWSAccountPool,
compatibility openAIWSHandshakeCompatibilityKey,
routingAffinity string,
) *openAIWSConn {
if ap == nil || len(ap.conns) == 0 {
return nil
@@ -1252,7 +1259,7 @@ func (p *openAIWSConnPool) pickOldestIdleConnWithoutHandshakeCompatibilityOrRout
var oldest *openAIWSConn
for _, conn := range ap.conns {
if conn == nil ||
(conn.matchesHandshakeCompatibility(compatibility) && conn.matchesRoutingAffinity(routingAffinity)) ||
conn.matchesHandshakeCompatibility(compatibility) ||
conn.isLeased() || conn.waiters.Load() > 0 || p.isConnPinnedLocked(ap, conn.id) {
continue
}
@@ -1800,7 +1807,7 @@ func (p *openAIWSConnPool) dialConn(ctx context.Context, req openAIWSAcquireRequ
}
id := p.nextConnID(req.Account.ID)
pooledConn := newOpenAIWSConn(id, req.Account.ID, conn, handshakeHeaders)
pooledConn.handshakeCompatibility = normalizeOpenAIWSHandshakeCompatibility(req.Headers)
pooledConn.handshakeCompatibility = normalizeOpenAIWSHandshakeCompatibility(req.Account, req.Headers)
pooledConn.routingAffinity = normalizeOpenAIWSRoutingAffinity(req.Headers)
return pooledConn, nil
}
@@ -1983,7 +1990,7 @@ func cloneOpenAIWSAcquireRequestPtr(req *openAIWSAcquireRequest) *openAIWSAcquir
func sameOpenAIWSPrewarmTarget(a, b openAIWSAcquireRequest) bool {
return stringsTrim(a.WSURL) == stringsTrim(b.WSURL) &&
stringsTrim(a.ProxyURL) == stringsTrim(b.ProxyURL) &&
normalizeOpenAIWSHandshakeCompatibility(a.Headers) == normalizeOpenAIWSHandshakeCompatibility(b.Headers)
normalizeOpenAIWSHandshakeCompatibility(a.Account, a.Headers) == normalizeOpenAIWSHandshakeCompatibility(b.Account, b.Headers)
}
func normalizeOpenAIWSBetaFeatures(headers http.Header) string {
@@ -2011,10 +2018,41 @@ func normalizeOpenAIWSBetaFeatures(headers http.Header) string {
return strings.Join(normalized, ",")
}
func normalizeOpenAIWSHandshakeCompatibility(headers http.Header) openAIWSHandshakeCompatibilityKey {
return openAIWSHandshakeCompatibilityKey{
func normalizeOpenAIWSHandshakeCompatibility(account *Account, headers http.Header) openAIWSHandshakeCompatibilityKey {
key := openAIWSHandshakeCompatibilityKey{
betaFeatures: normalizeOpenAIWSBetaFeatures(headers),
}
mode := activeCodexFingerprintMode(account)
if mode == codexFingerprintOff {
return key
}
key.codexInstallationID = normalizeOpenAIWSStableIdentityHeader(headers, "x-codex-installation-id")
if mode == codexFingerprintDevice {
return key
}
key.sessionIDHyphen = normalizeOpenAIWSStableIdentityHeader(headers, "session-id")
key.sessionIDUnderscore = normalizeOpenAIWSStableIdentityHeader(headers, "session_id")
key.threadID = normalizeOpenAIWSStableIdentityHeader(headers, "thread-id")
key.clientRequestID = normalizeOpenAIWSStableIdentityHeader(headers, "x-client-request-id")
key.codexWindowID = normalizeOpenAIWSStableIdentityHeader(headers, "x-codex-window-id")
return key
}
func activeCodexFingerprintMode(account *Account) codexFingerprintMode {
if account == nil || account.GetCodexFingerprintMode() == codexFingerprintOff {
return codexFingerprintOff
}
if _, ok := codexFingerprintSeed(account.Extra); !ok {
return codexFingerprintOff
}
return account.GetCodexFingerprintMode()
}
func normalizeOpenAIWSStableIdentityHeader(headers http.Header, name string) string {
if headers == nil {
return ""
}
return strings.TrimSpace(headers.Get(name))
}
func normalizeOpenAIWSRoutingAffinity(headers http.Header) string {
@@ -623,6 +623,206 @@ func TestOpenAIWSConnPool_AcquireReusesOnlyMatchingBetaFeatures(t *testing.T) {
require.Equal(t, 2, dialer.DialCount())
}
func activeCodexFingerprintPoolAccountForTest(id int64) *Account {
return &Account{
ID: id,
Platform: PlatformOpenAI,
Type: AccountTypeOAuth,
Extra: map[string]any{
codexFingerprintModeExtraKey: "session",
codexFingerprintSeedExtraKey: "11111111-1111-4111-8111-111111111111",
},
}
}
func stableOpenAIWSIdentityHeadersForTest() http.Header {
headers := make(http.Header)
headers.Set("X-Codex-Beta-Features", "remote_compaction_v2,responses_websockets_v2")
headers.Set("X-Codex-Installation-ID", "install-a")
headers.Set("session-id", "session-hyphen-a")
headers.Set("session_id", "session-underscore-a")
headers.Set("thread-id", "thread-a")
headers.Set("x-client-request-id", "client-request-a")
headers.Set("x-codex-window-id", "window-a")
return headers
}
func TestOpenAIWSConnPool_AcquireReusesSameStableIdentityWithDifferentTurnMetadata(t *testing.T) {
cfg := &config.Config{}
cfg.Gateway.OpenAIWS.MaxConnsPerAccount = 1
cfg.Gateway.OpenAIWS.MinIdlePerAccount = 0
cfg.Gateway.OpenAIWS.MaxIdlePerAccount = 1
pool := newOpenAIWSConnPool(cfg)
dialer := &openAIWSCountingDialer{}
pool.setClientDialerForTest(dialer)
account := activeCodexFingerprintPoolAccountForTest(132)
headers := stableOpenAIWSIdentityHeadersForTest()
headers.Set("Authorization", "Bearer token-a")
headers.Set("x-codex-turn-metadata", `{"turn_id":"turn-a"}`)
first, err := pool.Acquire(context.Background(), openAIWSAcquireRequest{
Account: account,
WSURL: "wss://example.com/v1/responses",
Headers: headers,
})
require.NoError(t, err)
firstConnID := first.ConnID()
first.Release()
nextHeaders := stableOpenAIWSIdentityHeadersForTest()
nextHeaders.Set("Authorization", "Bearer token-b")
nextHeaders.Set("x-codex-turn-metadata", `{"turn_id":"turn-b"}`)
nextHeaders.Set(openAICodexRoutingHintHeader, "model=gpt-5.6-codex;tier=priority")
second, err := pool.Acquire(context.Background(), openAIWSAcquireRequest{
Account: account,
WSURL: "wss://example.com/v1/responses",
Headers: nextHeaders,
})
require.NoError(t, err)
require.True(t, second.Reused())
require.Equal(t, firstConnID, second.ConnID())
second.Release()
require.Equal(t, 1, dialer.DialCount(), "stable identity match should ignore auth, turn metadata, and soft routing hints")
}
func TestOpenAIWSConnPool_AcquireDoesNotReuseDifferentStableIdentity(t *testing.T) {
for _, tt := range []struct {
name string
header string
value string
}{
{name: "installation", header: "x-codex-installation-id", value: "install-b"},
{name: "session hyphen", header: "session-id", value: "session-hyphen-b"},
{name: "session underscore", header: "session_id", value: "session-underscore-b"},
{name: "thread", header: "thread-id", value: "thread-b"},
{name: "client request", header: "x-client-request-id", value: "client-request-b"},
{name: "window", header: "x-codex-window-id", value: "window-b"},
} {
t.Run(tt.name, func(t *testing.T) {
cfg := &config.Config{}
cfg.Gateway.OpenAIWS.MaxConnsPerAccount = 2
cfg.Gateway.OpenAIWS.MinIdlePerAccount = 0
cfg.Gateway.OpenAIWS.MaxIdlePerAccount = 2
pool := newOpenAIWSConnPool(cfg)
dialer := &openAIWSCountingDialer{}
pool.setClientDialerForTest(dialer)
account := activeCodexFingerprintPoolAccountForTest(133)
first, err := pool.Acquire(context.Background(), openAIWSAcquireRequest{
Account: account,
WSURL: "wss://example.com/v1/responses",
Headers: stableOpenAIWSIdentityHeadersForTest(),
})
require.NoError(t, err)
firstConnID := first.ConnID()
first.Release()
nextHeaders := stableOpenAIWSIdentityHeadersForTest()
nextHeaders.Set(tt.header, tt.value)
second, err := pool.Acquire(context.Background(), openAIWSAcquireRequest{
Account: account,
WSURL: "wss://example.com/v1/responses",
Headers: nextHeaders,
})
require.NoError(t, err)
require.False(t, second.Reused())
require.NotEqual(t, firstConnID, second.ConnID())
second.Release()
require.Equal(t, 2, dialer.DialCount())
})
}
}
func TestOpenAIWSConnPool_AcquireRoutingHintRemainsSoftAffinity(t *testing.T) {
cfg := &config.Config{}
cfg.Gateway.OpenAIWS.MaxConnsPerAccount = 1
cfg.Gateway.OpenAIWS.MinIdlePerAccount = 0
cfg.Gateway.OpenAIWS.MaxIdlePerAccount = 1
pool := newOpenAIWSConnPool(cfg)
dialer := &openAIWSCountingDialer{}
pool.setClientDialerForTest(dialer)
account := activeCodexFingerprintPoolAccountForTest(134)
firstHeaders := stableOpenAIWSIdentityHeadersForTest()
firstHeaders.Set(openAICodexRoutingHintHeader, "model=gpt-5.6-codex")
first, err := pool.Acquire(context.Background(), openAIWSAcquireRequest{
Account: account,
WSURL: "wss://example.com/v1/responses",
Headers: firstHeaders,
})
require.NoError(t, err)
firstConnID := first.ConnID()
first.Release()
secondHeaders := stableOpenAIWSIdentityHeadersForTest()
secondHeaders.Set(openAICodexRoutingHintHeader, "model=gpt-5.6-codex;tier=priority")
second, err := pool.Acquire(context.Background(), openAIWSAcquireRequest{
Account: account,
WSURL: "wss://example.com/v1/responses",
Headers: secondHeaders,
})
require.NoError(t, err)
require.True(t, second.Reused())
require.Equal(t, firstConnID, second.ConnID())
second.Release()
require.Equal(t, 1, dialer.DialCount())
}
func TestOpenAIWSConnPool_DeviceModeKeysOnlyInstallationIdentity(t *testing.T) {
cfg := &config.Config{}
cfg.Gateway.OpenAIWS.MaxConnsPerAccount = 2
cfg.Gateway.OpenAIWS.MinIdlePerAccount = 0
cfg.Gateway.OpenAIWS.MaxIdlePerAccount = 2
pool := newOpenAIWSConnPool(cfg)
dialer := &openAIWSCountingDialer{}
pool.setClientDialerForTest(dialer)
account := activeCodexFingerprintPoolAccountForTest(135)
account.Extra[codexFingerprintModeExtraKey] = "device"
firstHeaders := stableOpenAIWSIdentityHeadersForTest()
first, err := pool.Acquire(context.Background(), openAIWSAcquireRequest{
Account: account,
WSURL: "wss://example.com/v1/responses",
Headers: firstHeaders,
})
require.NoError(t, err)
firstConnID := first.ConnID()
first.Release()
sessionChanged := stableOpenAIWSIdentityHeadersForTest()
sessionChanged.Set("session-id", "session-hyphen-b")
sessionChanged.Set("session_id", "session-underscore-b")
sessionChanged.Set("thread-id", "thread-b")
sessionChanged.Set("x-client-request-id", "client-request-b")
sessionChanged.Set("x-codex-window-id", "window-b")
second, err := pool.Acquire(context.Background(), openAIWSAcquireRequest{
Account: account,
WSURL: "wss://example.com/v1/responses",
Headers: sessionChanged,
})
require.NoError(t, err)
require.True(t, second.Reused())
require.Equal(t, firstConnID, second.ConnID())
second.Release()
installationChanged := sessionChanged.Clone()
installationChanged.Set("x-codex-installation-id", "install-b")
third, err := pool.Acquire(context.Background(), openAIWSAcquireRequest{
Account: account,
WSURL: "wss://example.com/v1/responses",
Headers: installationChanged,
})
require.NoError(t, err)
require.False(t, third.Reused())
require.NotEqual(t, firstConnID, third.ConnID())
third.Release()
require.Equal(t, 2, dialer.DialCount())
}
func TestOpenAIWSConnPool_AcquireReplacesIdleConnWithDifferentBetaFeatures(t *testing.T) {
cfg := &config.Config{}
cfg.Gateway.OpenAIWS.MaxConnsPerAccount = 1
@@ -0,0 +1,21 @@
-- Backfill system-managed Codex fingerprint seeds for enabled OpenAI OAuth accounts.
-- Idempotent: valid canonical seeds are preserved on rerun.
UPDATE accounts
SET extra = jsonb_set(
COALESCE(extra, '{}'::jsonb),
'{codex_fingerprint_seed}',
to_jsonb(gen_random_uuid()::text),
true
)
WHERE deleted_at IS NULL
AND platform = 'openai'
AND type = 'oauth'
AND COALESCE(extra->>'codex_fingerprint_mode', '') IN ('device', 'session', 'full')
AND (
extra->>'codex_fingerprint_seed' IS NULL
OR btrim(extra->>'codex_fingerprint_seed') = ''
OR NOT (
extra->>'codex_fingerprint_seed' ~ '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
AND extra->>'codex_fingerprint_seed' <> '00000000-0000-0000-0000-000000000000'
)
);