Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
06ec553
Merge pull request #3678 from lidge-jun/codex/promote-main-243-01a07240
lidge-jun Sep 5, 2026
116c2ac
Merge commit '44ea9576e27c6be8be7f13a86e32bb349368c54d' into codex/re…
invalid-email-address Sep 6, 2026
07b48da
Merge pull request #3785 from lidge-jun/codex/release-244-main-07c0
lidge-jun Sep 6, 2026
bcdf559
chore(release): promote validated 2.45.0 to main [skip ci]
invalid-email-address Sep 6, 2026
b0900e5
chore(release): promote 2.45.0 to main (#3813)
lidge-jun Sep 6, 2026
3970601
chore(release): prepare 2.46.0 stable promotion
invalid-email-address Sep 7, 2026
bba6322
Merge pull request #3851 from lidge-jun/codex/release-246-main
lidge-jun Sep 7, 2026
3d53e5f
release: prepare 2.47.0 from audited regression candidate
invalid-email-address Sep 7, 2026
eda8754
Merge commit '48ab3e1e66cfa6e0c873de2fafa4540ac61d6c7d' into codex/re…
invalid-email-address Sep 7, 2026
f9e3515
Merge commit '57252193b' into codex/release-247-main
invalid-email-address Sep 7, 2026
6f71931
release: promote 2.47.0 to main (#3929)
lidge-jun Sep 7, 2026
9a60256
Merge commit 'd0737cff3' into codex/release-247-main-final
invalid-email-address Sep 7, 2026
9e9b1d3
Merge commit 'f48c322c0' into codex/release-247-main-final
invalid-email-address Sep 7, 2026
947bae9
Merge commit '0d7652ad1' into codex/release-247-main-final
invalid-email-address Sep 7, 2026
f7f890f
release: apply final roster correction to main (#3933)
lidge-jun Sep 7, 2026
544ebee
release: promote 2.48.0 to main
invalid-email-address Sep 8, 2026
d24ff57
release: set main channel version 2.48.0
invalid-email-address Sep 8, 2026
9a27e86
Merge pull request #4011 from lidge-jun/codex/release-248-main
lidge-jun Sep 8, 2026
62849df
release: promote verified 2.49.0 product tree to main
lidge-jun Sep 9, 2026
2f3f736
Merge pull request #4117 from lidge-jun/codex/release-249-main-01a08498
lidge-jun Sep 9, 2026
3a3de88
release: promote verified 2.50.0 product tree to main
lidge-jun Sep 10, 2026
2d4d7a2
Merge pull request #4195 from lidge-jun/codex/release-250-main-01a08a81
lidge-jun Sep 10, 2026
cf456e8
release: promote verified 2.51.0 product tree to main
lidge-jun Sep 11, 2026
c155cc7
Merge pull request #4271 from lidge-jun/codex/release-251-main
lidge-jun Sep 11, 2026
95c4875
release: promote verified 2.52.0 product tree to main
lidge-jun Sep 12, 2026
4d37c35
Merge pull request #4407 from lidge-jun/codex/release-2520-main
lidge-jun Sep 12, 2026
588cb85
fix(codex): preserve cache affinity across model detours
luvs01 Sep 14, 2026
3639161
fix(codex): honor disabled autoSwitchThreshold during cache affinity …
luvs01 Sep 14, 2026
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bitkyc08/opencodex",
"version": "2.53.0",
"version": "2.52.0",
"description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
"type": "module",
"main": "./bin/package-main.mjs",
Expand Down
35 changes: 32 additions & 3 deletions src/codex/routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1505,6 +1505,34 @@ function hasCodexQuotaHeadroom(
return usage < threshold;
}

/**
* Whether quota may retire shared state while cache affinity is active.
*
* New/unbound selection still uses {@link hasCodexQuotaHeadroom}; only preservation of an
* existing shared selection or thread binding gets the exhaustion boundary.
*/
function hasCodexSharedStateQuotaHeadroom(
config: OcxConfig,
accountId: string,
selectionOptions?: CodexAccountUsabilityOptions,
now: number = Date.now(),
): boolean {
if (
config.pool?.cacheAffinity !== true
|| normalizeAccountPoolStrategy(config.accountPoolStrategy) !== "quota"
) {
return hasCodexQuotaHeadroom(config, accountId, selectionOptions, now);
}
const threshold = config.pool?.autoSwitchThreshold;
if (typeof threshold === "number" && threshold <= 0) return true;
const usage = computeCodexUsageScore(
getAccountQuota(accountId),
getPoolAccountPlanForSelection(config, accountId, selectionOptions),
now,
);
return isUnknownUsage(usage) || usage < 100;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor a disabled auto-switch threshold

When accountPoolStrategy is quota, cache affinity is enabled, and autoSwitchThreshold is 0, a cached 100% reading makes this predicate false even though 0 disables usage-driven re-evaluation. During a model-gated detour, the resolver consequently deletes the ordinary thread affinity and may promote the detour account, whereas the normal affinity path deliberately keeps the binding when the threshold is disabled; this also contradicts the documented contract in docs-site/src/content/docs/reference/configuration/providers.md:505. Short-circuit to true when the configured threshold is non-positive so only actual failure evidence retires the shared state in this configuration.

AGENTS.md reference: src/AGENTS.md:L10-L10

Useful? React with 👍 / 👎.

}

/**
* Fill-first: keep selectable active under threshold; otherwise advance to the next
* eligible id in stable sorted order after the current active (wrapping).
Expand Down Expand Up @@ -2028,7 +2056,8 @@ function isHealthySharedCodexSelection(
selectionOptions: CodexAccountUsabilityOptions | undefined,
): boolean {
return isCodexAccountSelectable(config, accountId, now, quotaScope, selectionOptions)
&& hasCodexQuotaHeadroom(config, accountId, selectionOptions, now)
&& hasCodexSharedStateQuotaHeadroom(config, accountId, selectionOptions, now)
&& !hasUnrecoveredCodexQuotaRefusal(accountId, quotaScope)
&& !shouldFailover(config, accountId, now);
}

Expand Down Expand Up @@ -2418,7 +2447,7 @@ export function resolveCodexAccountForThreadDetailed(
// the account has already told this thread it cannot serve it.
const quotaRefused = hasUnrecoveredCodexQuotaRefusal(entry.accountId, quotaScope);
const healthyForSharedAffinity = selectableForSharedState
&& hasCodexQuotaHeadroom(config, entry.accountId, sharedSelectionOptions, now)
&& hasCodexSharedStateQuotaHeadroom(config, entry.accountId, sharedSelectionOptions, now)
&& !quotaRefused
&& !failoverReady;
if (
Expand Down Expand Up @@ -2523,7 +2552,7 @@ export function resolveCodexAccountForThreadDetailed(
sharedSelectionOptions,
);
const activeHealthyForSharedSelection = activeSelectableForSharedState
&& hasCodexQuotaHeadroom(config, active, sharedSelectionOptions, now)
&& hasCodexSharedStateQuotaHeadroom(config, active, sharedSelectionOptions, now)
&& !shouldFailover(config, active, now);
if (!isCodexAccountSelectable(config, active, now, quotaScope, selectionOptions)) {
const fallback = pickLowestUsageCodexAccount(config, active, now, quotaScope, selectionOptions);
Expand Down
7 changes: 5 additions & 2 deletions structure/providers/openai-tiers.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,8 +278,11 @@ has headroom, auth resolution validates the caller bearer's own gated-model rost
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.
clearing the healthy shared main pin. With quota-strategy cache affinity, the same detour preserves an
ordinary added-account binding and shared selection beyond the proactive-switch threshold until genuine
exhaustion; pause, cooldown, reauthentication, quota refusal, and failover evidence still retire shared
state normally. A paused or quota-drained main skips the request-owned credential exception and follows
the ordinary Pool promotion path.

> Decision record: [ADR-0086](../decisions/ADR-0086-public-provider-contract.md)

Expand Down
26 changes: 26 additions & 0 deletions tests/codex-integration/codex-routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2452,6 +2452,32 @@ describe("codex account selection order", () => {
expect(resolveCodexAccountForThread("model-gated-task", config, now + 2, "shared")).toBe("b");
});

test("cache affinity preserves an over-threshold shared binding across a model detour", () => {
const config = orderedConfig({
accountPoolStrategy: "quota",
activeCodexAccountId: "a",
activeCodexAccountPinned: "a",
autoSwitchThreshold: 80,
pool: { cacheAffinity: true },
});
const now = Date.now();
updateAccountQuota("a", 10);
updateAccountQuota("b", 10);

expect(resolveCodexAccountForThread("cache-affine-model-detour", config, now, "shared")).toBe("a");
updateAccountQuota("a", 90);
expect(resolveCodexAccountForThreadDetailed(
"cache-affine-model-detour",
config,
now + 1,
"shared",
{ modelEligibleAccountIds: new Set(["b"]) },
)).toEqual({ status: "selected", accountId: "b" });

expect(getEffectiveActiveCodexAccountId(config)).toBe("a");
expect(resolveCodexAccountForThread("cache-affine-model-detour", config, now + 2, "shared")).toBe("a");
});

test("repeated model-gated round-robin requests reuse a separate detour affinity", () => {
const now = 1_800_000_000_000;
const threadId = "model-detour-affinity";
Expand Down
Loading