Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 41 additions & 2 deletions src/codex/auth-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import { isNativeMainTrafficBlocked, nativeMainStartupGateSnapshot } from "./nat
import type { NativeMainStartupBlockReason } from "./native-profile-startup";
import {
codexQuotaScopeForModel,
computeCodexUsageScore,
getCodexQuotaHealthSnapshot,
isEffectiveCodexAccountPinned,
releaseCodexQuotaProbeLease,
releaseCodexQuotaScopeProbeLease,
tryAcquireCodexQuotaProbeLease,
Expand All @@ -42,7 +44,7 @@ import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "./catalog/native-models";
import type { CodexCooldownSource, CodexQuotaScope } from "./routing";
import { maskAccountId } from "../lib/privacy";
import { formatErrorResponse } from "../bridge";
import { getAccountQuota } from "./quota";
import { CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota } from "./quota";
import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "../types";
import { FORWARD_HEADERS } from "../adapters/openai-responses";
import { captureConfigGeneration } from "../lib/state-store-sweeper";
Expand All @@ -52,6 +54,21 @@ import { extractAccountId } from "../oauth/chatgpt";
const CODEX_AFFINITY_COMPONENT_MAX_BYTES = 512;
const CODEX_APP_AFFINITY_KEY = randomBytes(32);

/**
* A request-owned bearer cannot inspect the physical main credential for its plan, but cached
* WHAM usage is still valid routing evidence for the same logical main account. Score it with
* the conservative unknown-plan rule: an unobserved governing window preserves the pin, while
* any known weekly/monthly/short value at the threshold releases it through the ordinary Pool
* path. This keeps the keyring boundary intact instead of reading auth.json just to classify a
* request that already brought its own credential (#3157).
*/
function requestOwnedMainPinHasQuotaHeadroom(config: OcxConfig): boolean {
const threshold = config.autoSwitchThreshold ?? 80;
if (threshold <= 0) return true;
const usage = computeCodexUsageScore(getAccountQuota(MAIN_CODEX_ACCOUNT_ID));
return usage >= CODEX_UNKNOWN_USAGE_SCORE || usage < threshold;
}

function boundedCodexAffinityComponent(value: string | null): string | undefined {
const normalized = value?.trim();
if (!normalized) return undefined;
Expand Down Expand Up @@ -372,6 +389,12 @@ export async function resolveCodexAuthContext(
const requestScopedMainCredential = options.requestScopedMainCredential === true
&& hasCallerCodexBearer(headers);
const fixedAccountId = options.accountId;
const preserveRequestOwnedMainPin = requestScopedMainCredential
&& fixedAccountId === undefined
&& config.activeCodexAccountPinned === MAIN_CODEX_ACCOUNT_ID
&& isEffectiveCodexAccountPinned(config)
&& !isCodexAccountPaused(config, MAIN_CODEX_ACCOUNT_ID)
&& requestOwnedMainPinHasQuotaHeadroom(config);
if (fixedAccountId !== undefined && options.excludeAccountId !== undefined) {
throw new Error("Codex auth context cannot select and exclude an account simultaneously");
}
Expand Down Expand Up @@ -425,6 +448,19 @@ export async function resolveCodexAuthContext(
directSelectionAdmission.release();
}
};
// Pool discovery excludes request-owned main credentials by design: they must never be folded
// into stored-account entitlement, affinity, or persistence state. An effective manual main pin
// is the one exception where that exclusion is selection evidence in the opposite direction.
// Validate the caller's own gated-model roster before using it, and fall through to a Pool model
// detour when it lacks the grant. This branch performs no physical-main credential read.
if (preserveRequestOwnedMainPin) {
const callerEntitled = !options.modelId
|| !ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(options.modelId)
|| await (
options.isDirectCallerEntitledToCodexModel ?? isDirectCallerEntitledToCodexModel
)(headers, options.modelId);
if (callerEntitled) return { kind: "main", accountId: null };
}
// An explicit namespace binding is stronger than the provider's default mode. It must use the
// selected stored credential even while the canonical OpenAI provider is globally Direct.
// A request-owned bearer is deliberately not represented as `main-pool`: Pool account ids own
Expand Down Expand Up @@ -473,7 +509,10 @@ export async function resolveCodexAuthContext(
// it. Retained recovery makes main wholly ineligible so pool routing continues.
nativeMainSelectionOnly,
isMainAccountTokenLive: requestScopedMainCredential
? () => false
// Main stays excluded from this request's model roster below. This synthetic liveness is
// consulted only by shared-state preservation, so a caller-owned pin survives a model
// detour without reading or selecting the physical main credential.
? () => preserveRequestOwnedMainPin
: options.isMainAccountTokenLive,
modelEligibleAccountIds,
};
Expand Down
23 changes: 23 additions & 0 deletions structure/08_openai-provider-tiers.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,29 @@ pinned account and the effective active account are different questions, and the
both (`pinned` and `pinnedAccountId`). A surface that marks only the active account loses the pin from
view exactly when it is doing the most work — suppressing every higher tier.

A keyring-backed Codex request can carry its own forwardable ChatGPT bearer while the provider remains
in Pool mode. When the effective manual pin is `__main__`, main is not paused, and its cached quota still
has headroom, auth resolution validates the caller bearer's own gated-model roster and uses that
request-owned credential before stored-Pool selection. The credential never enters Pool persistence,
affinity, entitlement cache, or health state, and this decision never reads the physical main credential.
If the caller lacks the requested model, a stored-account model detour may serve the request without
clearing the healthy shared main pin. A paused or quota-drained main skips this exception and follows the
ordinary Pool promotion path.

[Decision Log]
- 목적과 의도: Keep an explicit healthy main selection from being replaced by an exhausted stored
account merely because the client supplied main through a request-owned keyring bearer.
- 기존 구현 및 제약 조건: Request-owned credentials are deliberately excluded from stored-account
entitlement discovery, but shared-state preservation interpreted that exclusion as a dead main login.
- 검토한 주요 대안: Persist the caller credential, read the physical main token for identity, ignore
the manual pin, or validate the caller independently before stored-Pool selection.
- 선택한 방식: Use only the effective pin, pause state, cached quota, and the caller credential's own
gated-model check; synthesize shared-state liveness only while main stays request-ineligible.
- 다른 대안 대신 이 방식을 선택한 이유: It preserves credential isolation and explicit operator
intent without admitting an unentitled model or binding an ephemeral bearer into durable Pool state.
- 장점, 단점 및 영향: Healthy main pins survive keyring requests and model-only detours; cached quota
remains the only proactive drain evidence available without crossing the physical credential boundary.

```text
gpt-5.6-sol # openai; Pool or Direct follows the provider option
main/gpt-daybreak-blue-latest # openai; observed account-native Daybreak, Sol capability metadata
Expand Down
86 changes: 86 additions & 0 deletions tests/codex-auth-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
handleCodexAuthAPI,
isAccountNeedsReauth,
markAccountNeedsReauth,
setAccountQuotaFromParsed,
} from "../src/codex/auth-api";
import { __resetGuardianState, guardianSweep } from "../src/oauth/token-guardian";
import {
Expand Down Expand Up @@ -1088,6 +1089,91 @@ describe("Codex auth context", () => {
clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
}
});

async function resolveRequestOwnedMainPinCase(options: {
mainWeeklyPercent: number;
poolWeeklyPercent: number;
callerEntitled: boolean;
}): Promise<{
cfg: OcxConfig;
context: Awaited<ReturnType<typeof resolveCodexAuthContext>>;
directEntitlementChecks: number;
}> {
const cfg = config();
cfg.accountPoolStrategy = "quota";
cfg.autoSwitchThreshold = 90;
cfg.activeCodexAccountId = MAIN_CODEX_ACCOUNT_ID;
cfg.activeCodexAccountPinned = MAIN_CODEX_ACCOUNT_ID;
cfg.codexAccountPriorities = {
[MAIN_CODEX_ACCOUNT_ID]: 0,
"pool-a": 0,
};
resetCodexRoutingForManualSelection(MAIN_CODEX_ACCOUNT_ID);
saveCodexAccountCredential("pool-a", {
accessToken: "pool-token",
refreshToken: "pool-refresh",
expiresAt: Date.now() + 5 * 60_000,
chatgptAccountId: "pool-account",
});
setAccountQuotaFromParsed(MAIN_CODEX_ACCOUNT_ID, { weeklyPercent: options.mainWeeklyPercent });
setAccountQuotaFromParsed("pool-a", { weeklyPercent: options.poolWeeklyPercent });
let directEntitlementChecks = 0;
const context = await resolveCodexAuthContext(new Headers({
authorization: "Bearer caller-keyring-token",
"chatgpt-account-id": "caller-keyring-account",
}), cfg, "pool", {
requestScopedMainCredential: true,
modelId: "gpt-5.6-sol",
isDirectCallerEntitledToCodexModel: async () => {
directEntitlementChecks += 1;
return options.callerEntitled;
},
resolveCodexModelEntitlements: async () => ({
modelsByAccount: new Map([["pool-a", new Set(["gpt-5.6-sol"])]]),
clientVersionByAccount: new Map([["pool-a", "0.150.1"]]),
confirmedAccountIds: new Set(["pool-a"]),
credentialIdentities: new Map([["pool-a", "pool:1:pool-account"]]),
}),
});
return { cfg, context, directEntitlementChecks };
}

test("a healthy manual main pin keeps the validated caller bearer ahead of an exhausted pool account (#3157)", async () => {
const { cfg, context, directEntitlementChecks } = await resolveRequestOwnedMainPinCase({
mainWeeklyPercent: 16,
poolWeeklyPercent: 100,
callerEntitled: true,
});
expect(context).toMatchObject({ kind: "main", accountId: null });
expect(directEntitlementChecks).toBe(1);
expect(cfg.activeCodexAccountId).toBe(MAIN_CODEX_ACCOUNT_ID);
expect(cfg.activeCodexAccountPinned).toBe(MAIN_CODEX_ACCOUNT_ID);
});

test("an exhausted request-owned main pin still yields to the healthy Pool account (#3157)", async () => {
const { cfg, context, directEntitlementChecks } = await resolveRequestOwnedMainPinCase({
mainWeeklyPercent: 100,
poolWeeklyPercent: 16,
callerEntitled: true,
});
expect(context).toMatchObject({ kind: "pool", accountId: "pool-a" });
expect(directEntitlementChecks).toBe(0);
expect(cfg.activeCodexAccountId).toBe("pool-a");
expect(cfg.activeCodexAccountPinned).toBeUndefined();
});

test("a caller entitlement miss uses a Pool model detour without clearing the healthy main pin (#3157)", async () => {
const { cfg, context, directEntitlementChecks } = await resolveRequestOwnedMainPinCase({
mainWeeklyPercent: 16,
poolWeeklyPercent: 20,
callerEntitled: false,
});
expect(context).toMatchObject({ kind: "pool", accountId: "pool-a" });
expect(directEntitlementChecks).toBe(1);
expect(cfg.activeCodexAccountId).toBe(MAIN_CODEX_ACCOUNT_ID);
expect(cfg.activeCodexAccountPinned).toBe(MAIN_CODEX_ACCOUNT_ID);
});

test("selects pool auth independently of the routed provider", async () => {
saveCodexAccountCredential("pool-a", {
accessToken: "pool_token",
Expand Down
Loading