From 0c745be3644661d5e3c9d1c0afdb6963f62c78d4 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Sat, 8 Aug 2026 21:48:28 +0900 Subject: [PATCH 1/2] fix(routing): keep unbound account quota unknown (#1195) Policy profiles choose a provider and model before the request path resolves Pool/Direct identity, thread affinity, Anthropic session affinity, or round-robin/fill-first selection. Attaching the process-global active account during policy evaluation could therefore score or exclude a candidate using account A's quota and then execute the request on account B. An unbound candidate now stays quota-unknown in both the live route trace and the management dry-run, which is more accurate than inventing an account reference and keeps account selection, cooldowns, and session affinity authoritative. Unknown quota already has an explicit profile policy. Explicit `codexAccountId` and account-ref evidence remains unchanged. Republished from #1195 by luvs01, whose branch was 300 commits behind dev. Rebased onto f5147cbc8 with no conflicts; authorship preserved below. Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> --- .../docs/reference/configuration/routing.md | 20 +++++----- src/router.ts | 20 ---------- .../management/routing-profile-routes.ts | 22 ---------- tests/quota-scoring.test.ts | 40 +++++++++++++++++-- 4 files changed, 48 insertions(+), 54 deletions(-) diff --git a/docs-site/src/content/docs/reference/configuration/routing.md b/docs-site/src/content/docs/reference/configuration/routing.md index 862dd9867c..d51e152adc 100644 --- a/docs-site/src/content/docs/reference/configuration/routing.md +++ b/docs-site/src/content/docs/reference/configuration/routing.md @@ -153,15 +153,17 @@ CLI: `ocx route policy list [--json]`, `ocx route policy show [--json]`, an Dry-run evaluates candidates without sending any upstream request. Quota evidence (`optimize.quota`, `require.minQuotaHeadroom`, `unknownEvidence.quota`) comes from -the local Codex pool and Anthropic account quota caches, which are keyed by account. In **Pool** mode -the canonical `openai` provider preserves its existing account selection, then reads quota for the -selected account; **Direct** mode reads quota only from the current (caller/main) account. For other -providers (e.g. Anthropic), runtime candidates use the provider's active account. Quota evidence -never changes account selection, session affinity, cooldowns, or switching behavior — it only feeds -policy scoring. To see quota-aware behavior in a dry-run, supply account refs through the dry-run/API -candidate evidence: `candidates[].codexAccountId` (Codex pool, provider `openai`) or -`candidates[].accountRef` (Anthropic) derives the matching cached account quota; an explicit -`candidates[].quota` object is echoed as given. +account-keyed Codex and Anthropic quota caches. A runtime candidate receives cached quota only when +the evidence already identifies the account. Unbound canonical `openai` and Anthropic candidates +remain unknown during policy evaluation because Pool selection, Direct caller identity, provider +rotation, and thread affinity are resolved after the policy chooses a provider/model; a process-active +account is not used as a substitute. +Quota evidence never changes account selection, session affinity, cooldowns, or switching behavior — +it only feeds policy scoring. To see quota-aware behavior in an API dry-run, supply account refs in +the candidate evidence sent to `POST /api/routing-profiles/dry-run`: +`candidates[].codexAccountId` (Codex pool, provider `openai`) or `candidates[].accountRef` +(Anthropic) derives the matching cached account quota; an explicit `candidates[].quota` object is +echoed as given. The CLI dry-run cannot supply these per-candidate account fields. ### Combos vs policy profiles diff --git a/src/router.ts b/src/router.ts index 599ec9bb94..ec1d5bb914 100644 --- a/src/router.ts +++ b/src/router.ts @@ -22,8 +22,6 @@ import { import { decodeRoutedModelId, encodeRoutedModelId } from "./providers/slug-codec"; import { getStaleCached } from "./codex/model-cache"; import { codexAccountNamespaceEntries } from "./codex/account-namespaces"; -import { getEffectiveActiveCodexAccountId } from "./codex/routing"; -import { getAccountSet } from "./oauth/store"; import { buildRouteDecisionTrace, type RouteDecisionKind, @@ -513,24 +511,6 @@ function routeModelInternal( quota: quotaEvidenceForCandidate({ provider: candidate.provider, model: candidate.model, - ...(candidate.provider === OPENAI_CODEX_PROVIDER_ID - && providerCodexAccountMode( - OPENAI_CODEX_PROVIDER_ID, - config.providers[OPENAI_CODEX_PROVIDER_ID], - ) === "pool" - ? (() => { - const codexAccountId = getEffectiveActiveCodexAccountId(config); - return { - codexAccountId, - codexAccountPlan: codexAccountId - ? config.codexAccounts?.find(account => account.id === codexAccountId)?.plan - : undefined, - }; - })() - : {}), - accountRef: candidate.provider === "anthropic" - ? getAccountSet("anthropic")?.activeAccountId - : undefined, }), cost: costEvidenceForCandidate({ provider: candidate.provider, diff --git a/src/server/management/routing-profile-routes.ts b/src/server/management/routing-profile-routes.ts index 36687c01b3..08406b003a 100644 --- a/src/server/management/routing-profile-routes.ts +++ b/src/server/management/routing-profile-routes.ts @@ -20,10 +20,6 @@ import { candidateCapabilityEvidence } from "../../routing/capability"; import { policyCandidateHealthEvidence } from "../../routing/health"; import { quotaEvidenceForCandidate } from "../../routing/quota"; import { costEvidenceForCandidate } from "../../routing/cost"; -import { providerCodexAccountMode } from "../../providers/registry"; -import { getEffectiveActiveCodexAccountId } from "../../codex/routing"; -import { getAccountSet } from "../../oauth/store"; -import { OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers"; import { saveConfigPreservingClaudeCode } from "../../config"; import { reconcileLiveStateStores } from "../../lib/state-store-registrations"; import { isPlainRecord } from "./shared"; @@ -115,24 +111,6 @@ function assembleCandidateEvidence( quota: quotaEvidenceForCandidate({ provider: candidate.provider, model: candidate.model, - ...(candidate.provider === OPENAI_CODEX_PROVIDER_ID - && providerCodexAccountMode( - OPENAI_CODEX_PROVIDER_ID, - config.providers[OPENAI_CODEX_PROVIDER_ID], - ) === "pool" - ? (() => { - const codexAccountId = getEffectiveActiveCodexAccountId(config); - return { - codexAccountId, - codexAccountPlan: codexAccountId - ? config.codexAccounts?.find(account => account.id === codexAccountId)?.plan - : undefined, - }; - })() - : {}), - accountRef: candidate.provider === "anthropic" - ? getAccountSet("anthropic")?.activeAccountId - : undefined, }), cost: costEvidenceForCandidate({ provider: candidate.provider, diff --git a/tests/quota-scoring.test.ts b/tests/quota-scoring.test.ts index 30a637d84e..7eae3e474c 100644 --- a/tests/quota-scoring.test.ts +++ b/tests/quota-scoring.test.ts @@ -7,6 +7,7 @@ import { setCachedProviderAccountQuotaForTests, clearAccountQuotaCache } from ". import { quotaEvidenceForCandidate, quotaScore } from "../src/routing/quota"; import { evaluatePolicyProfile, QUOTA_UNKNOWN_PENALTY_SCORE } from "../src/routing/evaluator"; import { routeModel } from "../src/router"; +import { getAccountSet, saveCredential } from "../src/oauth/store"; import { closeRequestHistoryIndex } from "../src/routing/history/indexer"; import type { OcxConfig } from "../src/types"; @@ -195,7 +196,7 @@ describe("quota-aware scoring (RI-07)", () => { expect(penalized.candidates[0]!.score!.components.quota).toBe(QUOTA_UNKNOWN_PENALTY_SCORE); }); - test("execution path passes the active codex account into quota evidence", async () => { + test("execution path does not invent Codex quota evidence from the active pool account", async () => { updateAccountQuota("pool-a", 30, 1_800_000_000_000, 20, 1_900_000_000_000); const cfg = config({ codexAccounts: [{ id: "pool-a", email: "pool-a@example.test", isMain: false }], @@ -205,8 +206,41 @@ describe("quota-aware scoring (RI-07)", () => { }, }); const route = routeModel(cfg, "policy/quotaRoute"); - expect(route.routeDecision!.candidates[0]!.quota?.known).toBe(true); - expect(route.routeDecision!.candidates[0]!.quota?.headroom).toBeCloseTo(0.7, 2); + expect(route.routeDecision!.candidates[0]!.accountRef).toBeUndefined(); + expect(route.routeDecision!.candidates[0]!.quota?.known).toBe(false); + expect(route.routeDecision!.candidates[0]!.quota?.headroom).toBeUndefined(); + }); + + test("execution path does not invent Anthropic quota evidence from the active account", async () => { + await saveCredential("anthropic", { + access: "access-a", + refresh: "refresh-a", + expires: Date.now() + 3_600_000, + accountId: "uuid-a", + email: "a@example.test", + }); + const activeId = getAccountSet("anthropic")!.activeAccountId; + setCachedProviderAccountQuotaForTests("anthropic", activeId, { + fiveHourPercent: 40, + updatedAt: Date.now(), + }); + const cfg = config({ + providers: { + anthropic: { + adapter: "anthropic", + baseUrl: "https://api.anthropic.com", + authMode: "oauth", + models: ["claude-sonnet-5"], + }, + }, + routingProfiles: { + quotaRoute: { candidates: [{ provider: "anthropic", model: "claude-sonnet-5" }] }, + }, + }); + const route = routeModel(cfg, "policy/quotaRoute"); + expect(route.routeDecision!.candidates[0]!.accountRef).toBeUndefined(); + expect(route.routeDecision!.candidates[0]!.quota?.known).toBe(false); + expect(route.routeDecision!.candidates[0]!.quota?.headroom).toBeUndefined(); }); test("exact account selectors and pool strategies remain authoritative", () => { From 3fc962f2cf1414b4997b1e8f3153720faa285d3a Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Sat, 8 Aug 2026 21:48:28 +0900 Subject: [PATCH 2/2] test(routing): prove the management dry-run leaves unbound candidates unknown Maintainer-added coverage for the #1195 republish. The contributor's patch deletes the same block from the live router and the management dry-run path, but only the live path had a regression. The existing dry-run test covers a candidate with an explicitly supplied codexAccountId, which stays known and is unaffected by the fix, so the dry-run half of the parity claim was unproven. These two tests exercise an unbound Codex candidate with an active pool account, and an unbound Anthropic candidate with an active account, and assert both stay quota-unknown with no accountRef. Restoring either deleted block fails them. --- tests/routing-profile.test.ts | 75 +++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tests/routing-profile.test.ts b/tests/routing-profile.test.ts index 53aacdb8e2..99dc892ddf 100644 --- a/tests/routing-profile.test.ts +++ b/tests/routing-profile.test.ts @@ -476,4 +476,79 @@ describe("routing profiles (RI-04)", () => { expect(body.candidates?.[0]?.quota?.known).toBe(true); expect(body.candidates?.[0]?.quota?.headroom).toBeCloseTo(0.7, 2); }); + + test("API dry-run leaves an unbound Codex candidate quota unknown despite an active pool account", async () => { + updateAccountQuota("pool-a", 30, 1_800_000_000_000, 20, 1_900_000_000_000); + const config = baseConfig({ + providers: { + openai: { adapter: "openai-responses", authMode: "forward", baseUrl: "https://chatgpt.com/backend-api/codex" }, + }, + codexAccounts: [{ id: "pool-a", email: "pool-a@example.test", isMain: false }], + activeCodexAccountId: "pool-a", + routingProfiles: { + only: { candidates: [{ provider: "openai", model: "gpt-5.6" }] }, + }, + }); + const req = new ManagementRequest("http://localhost/api/routing-profiles/dry-run", { + method: "POST", + headers: { "content-type": "application/json" }, + // No candidates[] override: the candidate is unbound, so the dry-run must + // not reach for the process-global active pool account. Policy evaluation + // runs before Pool/Direct identity and thread affinity resolve, so an + // account attached here can differ from the one that executes. + body: JSON.stringify({ profile: "only", evidence: {} }), + }); + const response = await handleManagementAPI(req, new URL(req.url), config, { refreshCodexCatalog: async () => {} }); + expect(response).not.toBeNull(); + expect(response!.status).toBe(200); + const body = await response!.json() as { + candidates?: Array<{ accountRef?: string; quota?: { known?: boolean; headroom?: number } }>; + }; + expect(body.candidates?.[0]?.accountRef).toBeUndefined(); + expect(body.candidates?.[0]?.quota?.known).toBe(false); + expect(body.candidates?.[0]?.quota?.headroom).toBeUndefined(); + }); + + test("API dry-run leaves an unbound Anthropic candidate quota unknown despite an active account", async () => { + const { saveCredential, getAccountSet } = await import("../src/oauth/store"); + const { setCachedProviderAccountQuotaForTests } = await import("../src/providers/quota"); + await saveCredential("anthropic", { + access: "access-a", + refresh: "refresh-a", + expires: Date.now() + 3_600_000, + accountId: "uuid-a", + email: "a@example.test", + }); + const activeId = getAccountSet("anthropic")!.activeAccountId; + setCachedProviderAccountQuotaForTests("anthropic", activeId, { + fiveHourPercent: 40, + updatedAt: Date.now(), + }); + const config = baseConfig({ + providers: { + anthropic: { + adapter: "anthropic", + baseUrl: "https://api.anthropic.com", + authMode: "oauth", + models: ["claude-sonnet-5"], + }, + }, + routingProfiles: { + only: { candidates: [{ provider: "anthropic", model: "claude-sonnet-5" }] }, + }, + }); + const req = new ManagementRequest("http://localhost/api/routing-profiles/dry-run", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ profile: "only", evidence: {} }), + }); + const response = await handleManagementAPI(req, new URL(req.url), config, { refreshCodexCatalog: async () => {} }); + expect(response).not.toBeNull(); + expect(response!.status).toBe(200); + const body = await response!.json() as { + candidates?: Array<{ accountRef?: string; quota?: { known?: boolean; headroom?: number } }>; + }; + expect(body.candidates?.[0]?.accountRef).toBeUndefined(); + expect(body.candidates?.[0]?.quota?.known).toBe(false); + }); });