From 1fe72150a3af6bd0a02a7b829d97ece0209e76e3 Mon Sep 17 00:00:00 2001 From: Alvin0412 Date: Wed, 12 Aug 2026 02:45:43 +0800 Subject: [PATCH] feat(codex): show per-account usage --- .../pr-screenshots/codex-account-usage.png | Bin 0 -> 101645 bytes .../src/content/docs/guides/web-dashboard.md | 7 + .../content/docs/reference/management-api.md | 10 +- .../components/codex-account-pool-cards.tsx | 12 +- .../codex-account-pool-main-card.tsx | 9 + gui/src/hooks/useCodexAccountPool.ts | 56 ++++++- gui/src/i18n/de.ts | 1 + gui/src/i18n/en.ts | 1 + gui/src/i18n/ja.ts | 1 + gui/src/i18n/ko.ts | 1 + gui/src/i18n/ru.ts | 1 + gui/src/i18n/tr.ts | 1 + gui/src/i18n/zh-TW.ts | 1 + gui/src/i18n/zh.ts | 1 + .../codex-account-pool-behaviour.test.tsx | 38 +++++ .../codex-account-pool-pinned-badge.test.tsx | 19 +++ src/codex/account-label.ts | 15 +- src/codex/auth-api.ts | 5 +- src/server/management/logs-usage-routes.ts | 1 + src/server/request-log.ts | 15 ++ src/server/responses/compact.ts | 3 + src/server/responses/core.ts | 21 ++- src/usage/log.ts | 16 ++ src/usage/summary.ts | 155 +++++++++++++++++- structure/05_gui-and-management-api.md | 4 + tests/api-usage.test.ts | 5 +- tests/codex-auth-api.test.ts | 19 ++- tests/request-log.test.ts | 3 +- tests/responses-account-label.test.ts | 147 +++++++++++++++++ tests/responses-compaction-routing.test.ts | 46 +++++- tests/usage-log.test.ts | 35 ++++ tests/usage-summary.test.ts | 148 +++++++++++++++++ 32 files changed, 775 insertions(+), 22 deletions(-) create mode 100644 docs-site/public/pr-screenshots/codex-account-usage.png create mode 100644 tests/responses-account-label.test.ts diff --git a/docs-site/public/pr-screenshots/codex-account-usage.png b/docs-site/public/pr-screenshots/codex-account-usage.png new file mode 100644 index 0000000000000000000000000000000000000000..5d4cbf87b54ed3a384a8fbcd648e3edd215eb7b6 GIT binary patch literal 101645 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 NV*`s56?$&b{{tZgXA}Sc literal 0 HcmV?d00001 diff --git a/docs-site/src/content/docs/guides/web-dashboard.md b/docs-site/src/content/docs/guides/web-dashboard.md index 896a38bb9d..a32643fbb6 100644 --- a/docs-site/src/content/docs/guides/web-dashboard.md +++ b/docs-site/src/content/docs/guides/web-dashboard.md @@ -131,6 +131,13 @@ and other providers. - **Refresh quotas** re-reads account usage immediately so routing and the account cards use the same values. - Pool request logs use opaque labels such as `p3fa91c`, never account emails. +- Each account card also shows that stable log label, the observed 30-day token total, an approximate + API-equivalent cost using currently configured display pricing, and the fraction of attempts with + measured usage. Active user `modelCosts` overlays take priority over bundled verified catalog and + price fallbacks, and historical usage is re-estimated from the pricing active when the summary is + read. The cost is an estimate for reconciliation, not a ChatGPT Plus/Pro subscription invoice. + Historical bare `openai` rows that predate explicit attribution remain ambiguous rather than being + assigned to the current main account. - **Target a specific Codex account from the model picker** is an explicit opt-in. When enabled, ordinary supported GPT picker rows are replaced by one entry per public account selector. Choosing one locks that conversation to the mapped account: it does not rotate, fall back, or diff --git a/docs-site/src/content/docs/reference/management-api.md b/docs-site/src/content/docs/reference/management-api.md index dde75b2703..fdc6ceedc2 100644 --- a/docs-site/src/content/docs/reference/management-api.md +++ b/docs-site/src/content/docs/reference/management-api.md @@ -124,7 +124,7 @@ See [Combos](/guides/combos/) for target strategies, cooldowns, aliases, and rou | `GET /api/debug/usage-logs` | Read bounded usage-debug entries | — | | `GET /api/debug/injection-logs` | Read bounded guidance-injection debug entries | — | | `GET /api/claude/inbound-debug` | Read Claude inbound debug state and entries | — | -| `GET /api/usage` | Summarize usage by range and client surface | Returns an `error: "read_failed"` summary if storage cannot be read | +| `GET /api/usage` | Summarize usage by range and client surface; Codex responses also include an `accounts` breakdown keyed by stable non-PII log labels | Returns an `error: "read_failed"` summary if storage cannot be read | | `GET /api/storage` | Scan Codex storage usage by bucket | Returns an `error: "scan_failed"` payload on scan failure | | `POST /api/storage/cleanup/preview` | Preview archived-session cleanup and return a binding digest | 400 `invalid_json` or `invalid_percent` | | `POST /api/storage/cleanup` | Quarantine or permanently remove the previewed archived set | 400 invalid input; 409 stale/busy/referenced state; 500 filesystem/database failure | @@ -135,6 +135,14 @@ See [Combos](/guides/combos/) for target strategies, cooldowns, aliases, and rou | `POST /api/storage/cleanup-policy/run` | Start a manual cleanup-policy run | 409 `already_running`; 500 `cleanup_failed` | | `GET /api/storage/cleanup-policy/test-stream` | Test-only policy stream hook | 404 `not_found` when unavailable | +For `GET /api/usage?range=30d&surface=codex`, `accounts` contains one row per observed Codex +pool label. Each row reports `accountLogLabel`, token totals, `usageCoverageRatio`, and an optional +`estimatedCostUsd` based on the currently configured display pricing. Active user `modelCosts` +overlays take priority over bundled verified catalog and price fallbacks, and historical usage is +re-estimated from the pricing active when the summary is read. This is an API-equivalent estimate, +not a subscription charge. New main-pool requests use the reserved `main` label; legacy bare +`openai` rows remain in an ambiguous bucket instead of being reassigned from current configuration. + :::caution Storage cleanup endpoints can move or permanently remove archived session data. Always preview first and submit the returned digest. Prefer quarantine when recovery may be needed. diff --git a/gui/src/components/codex-account-pool-cards.tsx b/gui/src/components/codex-account-pool-cards.tsx index 14a68b9e25..22e8273454 100644 --- a/gui/src/components/codex-account-pool-cards.tsx +++ b/gui/src/components/codex-account-pool-cards.tsx @@ -1,4 +1,4 @@ -import { useT } from "../i18n/shared"; +import { useI18n, useT } from "../i18n/shared"; import { IconAlert, IconPause, IconPlay, IconX } from "../icons"; import { displayAccountId } from "../lib/privacy"; import AccountPriorityControl, { AccountPriorityBadge } from "./AccountPriorityControl"; @@ -15,6 +15,7 @@ import { oauthHealthShowsDoctor, oauthHealthShowsReauth, } from "../oauth-health-display"; +import { formatCostUsd, formatTokenCount } from "../provider-workspace/usage"; export function CodexAccountPoolCards({ pool, @@ -65,6 +66,7 @@ export function CodexAccountPoolCards({ doctorCopyOutcomeFor?: (accountId: string) => "copied" | "unavailable" | null; }) { const t = useT(); + const { locale } = useI18n(); const isNext = (account: CodexAccountEntry) => !account.paused && activeId === account.id; return ( @@ -144,6 +146,14 @@ export function CodexAccountPoolCards({

{a.email}{a.plan ? ` · ${a.plan}` : ""} · {t("prov.accountId")}: {displayAccountId(a.id)}
+ {a.logLabel && ( +
{t("codexAuth.logLabel")}: {a.logLabel}
+ )} + {a.usage30d && ( +
+ {t("usage.card.totalTokens")}: {formatTokenCount(a.usage30d.totalTokens, locale)} · {t("pws.estimatedCost")}: {formatCostUsd(a.usage30d.estimatedCostUsd, locale)} · {t("usage.coverage.measured")}: {Math.round(a.usage30d.usageCoverageRatio * 100)}% +
+ )} {healthSummary && (
{healthSummary}
)} diff --git a/gui/src/components/codex-account-pool-main-card.tsx b/gui/src/components/codex-account-pool-main-card.tsx index 4889844629..65b3b96a79 100644 --- a/gui/src/components/codex-account-pool-main-card.tsx +++ b/gui/src/components/codex-account-pool-main-card.tsx @@ -7,6 +7,7 @@ import type { CodexAccountEntry } from "./codex-account-pool-types"; import type { CodexAccountModeState } from "../codex-multi-state"; import type { TFn } from "../i18n/shared"; import type { NoticeTone } from "../ui"; +import { useI18n } from "../i18n/shared"; import { doctorCopyButtonLabel, formatOAuthHealthLabel, @@ -16,6 +17,7 @@ import { oauthHealthShowsDoctor, oauthHealthShowsReauth, } from "../oauth-health-display"; +import { formatCostUsd, formatTokenCount } from "../provider-workspace/usage"; export function CodexAccountPoolMainCard({ t, @@ -60,6 +62,7 @@ export function CodexAccountPoolMainCard({ onCopyDoctor?: (accountId: string) => void; doctorCopyOutcomeFor?: (accountId: string) => "copied" | "unavailable" | null; }) { + const { locale } = useI18n(); const mainFallbackLabel = t("codexAuth.codexApp"); const mainId = main?.id ?? "__main__"; const mainSwitchEntry: CodexAccountEntry = { @@ -135,6 +138,12 @@ export function CodexAccountPoolMainCard({ {t("codexAuth.appLogin")}
{main?.email || t("codexAuth.appLogin")}{main?.plan ? ` · ${main.plan}` : ""}
+
{t("codexAuth.logLabel")}: {main?.logLabel ?? "main"}
+ {main?.usage30d && ( +
+ {t("usage.card.totalTokens")}: {formatTokenCount(main.usage30d.totalTokens, locale)} · {t("pws.estimatedCost")}: {formatCostUsd(main.usage30d.estimatedCostUsd, locale)} · {t("usage.coverage.measured")}: {Math.round(main.usage30d.usageCoverageRatio * 100)}% +
+ )} {healthSummary && (
{healthSummary}
)} diff --git a/gui/src/hooks/useCodexAccountPool.ts b/gui/src/hooks/useCodexAccountPool.ts index 84e5c2eb76..d894594aef 100644 --- a/gui/src/hooks/useCodexAccountPool.ts +++ b/gui/src/hooks/useCodexAccountPool.ts @@ -1,5 +1,6 @@ -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { normalizeAccountPriority } from "../account-priority"; +import { useKeyedClientResource } from "../client-resource"; import { extractAutoSwitchThresholdPayload } from "../codex-auto-switch"; import type { AccountQuota } from "../codex-quota-utils"; import { accountNeedsReauth } from "../oauth-health-display"; @@ -24,6 +25,8 @@ export interface CodexAccountEntry { id: string; email: string; alias?: string; + /** Stable non-PII identity shared with Logs and per-account usage aggregation. */ + logLabel?: string; plan?: string; /** Required, not optional: the API always distinguishes the app-login row. */ isMain: boolean; @@ -38,6 +41,11 @@ export interface CodexAccountEntry { healthLabel?: string; healthSummary?: string; healthAction?: string; + usage30d?: { + totalTokens: number; + estimatedCostUsd?: number; + usageCoverageRatio: number; + }; } export type CodexAccountLoadState = "loading" | "ready" | "error"; @@ -104,6 +112,18 @@ export interface CodexAccountPoolController { } const REFRESH_INTERVAL_MS = 30_000; +const USAGE_REFRESH_INTERVAL_MS = 60_000; + +interface CodexAccountUsageRow { + accountLogLabel: string; + totalTokens: number; + estimatedCostUsd?: number; + usageCoverageRatio: number; +} + +interface CodexAccountUsageSummary { + accounts?: CodexAccountUsageRow[]; +} /** In-memory last-good snapshot (not sessionStorage — accounts carry emails/ids). */ const lastGoodByBase = new Map(); @@ -111,6 +131,16 @@ const lastGoodByBase = new Map(() => seed?.accounts ?? []); + const usage30d = useKeyedClientResource( + `codex-account-usage-30d:${apiBase}`, + [apiBase], + async (signal) => { + const response = await fetch(`${apiBase}/api/usage?range=30d&surface=codex`, { signal }); + if (!response.ok) throw new Error("account usage load failed"); + return response.json() as Promise; + }, + { enabled, pollMs: USAGE_REFRESH_INTERVAL_MS }, + ); const [activeId, setActiveId] = useState(() => seed?.activeId ?? null); const [loadState, setLoadState] = useState(() => (seed != null ? "ready" : "loading")); const [switchingId, setSwitchingId] = useState(null); @@ -196,10 +226,14 @@ export function useCodexAccountPool(apiBase: string, enabled = true): CodexAccou if (loadGenerationRef.current === generation) { // Selection order is required downstream (badge, select). Normalizing here keeps // a payload without it from rendering a NaN order on every card. - nextAccounts = ((payload.accounts ?? []) as CodexAccountEntry[]).map(account => ({ - ...account, - priority: normalizeAccountPriority(account.priority), - })); + nextAccounts = ((payload.accounts ?? []) as CodexAccountEntry[]).map(account => { + const logLabel = account.isMain ? "main" : account.logLabel; + return { + ...account, + ...(logLabel ? { logLabel } : {}), + priority: normalizeAccountPriority(account.priority), + }; + }); setAccounts(nextAccounts); hasAccountsRef.current = nextAccounts.length > 0; hasLoadedRef.current = true; @@ -518,9 +552,19 @@ export function useCodexAccountPool(apiBase: string, enabled = true): CodexAccou // Include health-only reauth so Providers overview attention matches row CTAs. const activeAccount = activePoolAccount ?? mainAccount; const activeNeedsReauth = !activeAccount?.paused && accountNeedsReauth(activeAccount); + const accountsWithUsage = useMemo(() => { + const usageByLabel = new Map( + (usage30d.data?.accounts ?? []).map(row => [row.accountLogLabel, row] as const), + ); + return accounts.map(account => { + const logLabel = account.isMain ? "main" : account.logLabel; + const accountUsage = logLabel ? usageByLabel.get(logLabel) : undefined; + return accountUsage ? { ...account, usage30d: accountUsage } : account; + }); + }, [accounts, usage30d.data]); return { - accounts, + accounts: accountsWithUsage, activeId, loadState, refreshing: inflightCount > 0, diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 4e422e964c..f59ffde6e3 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -919,6 +919,7 @@ export const de: Record = { "integrations.semantics.kimi": "Zum Anwenden neu starten oder /reload ausführen (v2 überwacht die Datei).", "integrations.semantics.gajae": "Gilt für eine neue Sitzung oder beim Öffnen von /model.", "codexAuth.mainAccount": "Hauptkonto", + "codexAuth.logLabel": "Log-Kennung", "codexAuth.codexApp": "Codex App", "codexAuth.appLogin": "App-Login", "codexAuth.accountPool": "Kontopool", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 897aecbc92..09290a3a91 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -1381,6 +1381,7 @@ export const en = { "integrations.semantics.kimi": "Restart or run /reload to apply it (v2 watches the file).", "integrations.semantics.gajae": "Applies to a new session or when opening /model.", "codexAuth.mainAccount": "Main Account", + "codexAuth.logLabel": "Log label", "codexAuth.codexApp": "Codex App", "codexAuth.appLogin": "App login", "codexAuth.accountPool": "Account Pool", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 7a83c8935b..d2ee008a70 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -1329,6 +1329,7 @@ export const ja: Record = { "integrations.semantics.kimi": "再起動するか /reload を実行すると適用されます(v2 はファイルを監視します)。", "integrations.semantics.gajae": "新しいセッション、または /model を開いたときに適用されます。", "codexAuth.mainAccount": "メインアカウント", + "codexAuth.logLabel": "ログラベル", "codexAuth.codexApp": "Codex App", "codexAuth.appLogin": "アプリログイン", "codexAuth.accountPool": "アカウントプール", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 4820c45f00..7a5fa9b4af 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -943,6 +943,7 @@ export const ko: Record = { "integrations.semantics.kimi": "재시작 또는 /reload 시 적용됩니다 (v2는 파일 변경을 감지합니다).", "integrations.semantics.gajae": "새 세션 또는 /model을 열 때 적용됩니다.", "codexAuth.mainAccount": "메인 계정", + "codexAuth.logLabel": "로그 라벨", "codexAuth.codexApp": "Codex App", "codexAuth.appLogin": "앱 로그인", "codexAuth.accountPool": "계정 풀", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 9370936f15..c4df1a933c 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -1371,6 +1371,7 @@ export const ru: Record = { "integrations.semantics.kimi": "Чтобы применить, перезапустите клиент или выполните /reload (v2 отслеживает файл).", "integrations.semantics.gajae": "Применяется в новом сеансе или при открытии /model.", "codexAuth.mainAccount": "Основной аккаунт", + "codexAuth.logLabel": "Метка журнала", "codexAuth.codexApp": "Codex App", "codexAuth.appLogin": "Вход через приложение", "codexAuth.accountPool": "Пул аккаунтов", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 89f790833e..de51ee0b64 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -1378,6 +1378,7 @@ export const tr: Record = { "integrations.semantics.gajae": "Yeni oturuma uygulanır.", "integrations.semantics.omp": "Kataloğu yüklemek için OMP'yi yeniden başlatın.", "codexAuth.mainAccount": "Ana Hesap", + "codexAuth.logLabel": "Günlük etiketi", "codexAuth.codexApp": "Codex Uygulaması", "codexAuth.appLogin": "Uygulama girişi", "codexAuth.accountPool": "Hesap Havuzu", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index a4f36917df..16e99c0273 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -1032,6 +1032,7 @@ export const zhTW: Record = { "nav.closeMenu": "關閉選單", "codexAuth.mainAccount": "主帳號", "codexAuth.codexApp": "Codex App", + "codexAuth.logLabel": "日誌標籤", "codexAuth.appLogin": "應用登入", "codexAuth.accountPool": "帳號池", "codexAuth.accountModeTitle": "OpenAI 帳號模式", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 953de29cac..792a0029ca 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -936,6 +936,7 @@ export const zh: Record = { "integrations.semantics.kimi": "重启或运行 /reload 以应用(v2 会监视该文件)。", "integrations.semantics.gajae": "在新会话中或打开 /model 时生效。", "codexAuth.mainAccount": "主账号", + "codexAuth.logLabel": "日志标签", "codexAuth.codexApp": "Codex App", "codexAuth.appLogin": "应用登录", "codexAuth.accountPool": "账号池", diff --git a/gui/tests/codex-account-pool-behaviour.test.tsx b/gui/tests/codex-account-pool-behaviour.test.tsx index 2f41d45627..2d8e42037a 100644 --- a/gui/tests/codex-account-pool-behaviour.test.tsx +++ b/gui/tests/codex-account-pool-behaviour.test.tsx @@ -2,6 +2,7 @@ import { afterEach, beforeEach, expect, test } from "bun:test"; import { Window } from "happy-dom"; import { act } from "react"; import type { Root } from "react-dom/client"; +import { clearClientResourceStoresForTests } from "../src/client-resource"; import { useCodexAccountPool, type CodexAccountPoolController } from "../src/hooks/useCodexAccountPool"; /** @@ -21,6 +22,7 @@ let root: Root | null = null; let calls: string[] = []; let originalFetch: typeof globalThis.fetch; let accounts: unknown[] = []; +let usageAccounts: unknown[] = []; let threshold = 80; let nextAccountsResponseGate: Promise | null = null; let pauseResponseActiveId: string | null = null; @@ -65,11 +67,15 @@ beforeEach(() => { activeGetId = null; deleteCatalogRefreshPending = false; accounts = [{ id: "a1", email: "account-one", isMain: true, paused: false, priority: 0, hasCredential: true, quota: null }]; + usageAccounts = []; Object.defineProperty(globalThis, "fetch", { configurable: true, value: async (url: string, init?: RequestInit) => { const path = String(url).split("/api/")[1] ?? String(url); calls.push(`${init?.method ?? "GET"} ${path}`); + if (path.startsWith("usage?")) { + return { ok: true, json: async () => ({ accounts: usageAccounts }) } as unknown as Response; + } if (path === "codex-auth/accounts/priority") { const gate = nextPriorityResponseGate; nextPriorityResponseGate = null; @@ -177,6 +183,7 @@ afterEach(async () => { root = null; } await act(async () => { await new Promise((r) => setTimeout(r, 0)); }); + clearClientResourceStoresForTests(); for (const key of globals) { Object.defineProperty(globalThis, key, { configurable: true, value: previous[key] }); } @@ -211,6 +218,37 @@ test("the controller loads once on mount", async () => { expect(seen.current!.loadState).toBe("ready"); }); +test("the controller joins 30-day usage to accounts by the displayed log label", async () => { + accounts = [ + { id: "main", email: "main", isMain: true, paused: false, priority: 0, hasCredential: true, quota: null }, + { id: "pool", email: "pool", logLabel: "pabc123", isMain: false, paused: false, priority: 0, hasCredential: true, quota: null }, + ]; + usageAccounts = [ + { accountLogLabel: "main", totalTokens: 11, estimatedCostUsd: 0.01, usageCoverageRatio: 1 }, + { accountLogLabel: "pabc123", totalTokens: 22, estimatedCostUsd: 0.02, usageCoverageRatio: 0.5 }, + { accountLogLabel: "legacy-ambiguous", totalTokens: 999, estimatedCostUsd: 9, usageCoverageRatio: 1 }, + ]; + + const seen = await mountController(); + expect(seen.current!.accounts.map(account => ({ + label: account.logLabel, + tokens: account.usage30d?.totalTokens, + }))).toEqual([ + { label: "main", tokens: 11 }, + { label: "pabc123", tokens: 22 }, + ]); +}); + +test("account reloads do not refetch the independently polled usage summary", async () => { + const seen = await mountController(); + expect(calls.filter(call => call.startsWith("GET usage?")).length).toBe(1); + + await act(async () => { await seen.current!.load(); }); + + expect(calls.filter(call => call.startsWith("GET usage?")).length).toBe(1); + expect(calls.filter(call => call.includes("codex-auth/accounts")).length).toBe(2); +}); + test("an inert controller issues no requests at all", async () => { await mountController(false); expect(calls.length).toBe(0); diff --git a/gui/tests/codex-account-pool-pinned-badge.test.tsx b/gui/tests/codex-account-pool-pinned-badge.test.tsx index f31d5e4191..d562492530 100644 --- a/gui/tests/codex-account-pool-pinned-badge.test.tsx +++ b/gui/tests/codex-account-pool-pinned-badge.test.tsx @@ -27,11 +27,17 @@ let originalFetch: typeof globalThis.fetch; const account: CodexAccountEntry = { id: "pool-1", email: "pool@example.test", + logLabel: "pabc123", isMain: false, paused: false, priority: 0, hasCredential: true, quota: null, + usage30d: { + totalTokens: 1_500, + estimatedCostUsd: 0.125, + usageCoverageRatio: 0.75, + }, }; const mainAccount: CodexAccountEntry = { @@ -199,3 +205,16 @@ test("a paused account is never shown as pinned", async () => { expect(hasPinnedBadge(host)).toBe(false); expect(hasPinnedHint(host)).toBe(false); }); + +test("account cards show the same log label and 30-day usage identity", async () => { + await mountPool(makeController()); + + const pooled = cardFor("pool@example.test"); + expect(pooled.textContent).toContain("Log label: pabc123"); + expect(pooled.textContent).toContain("Total tokens: 1.5k"); + expect(pooled.textContent).toContain("Estimated cost: ~$0.1250"); + expect(pooled.textContent).toContain("Measured: 75%"); + + const main = cardFor("main@example.test"); + expect(main.textContent).toContain("Log label: main"); +}); diff --git a/src/codex/account-label.ts b/src/codex/account-label.ts index 4c935354bd..cbbd8cf2f8 100644 --- a/src/codex/account-label.ts +++ b/src/codex/account-label.ts @@ -1,5 +1,7 @@ import { createHash, randomBytes } from "node:crypto"; -import type { CodexAccount } from "../types"; +import type { CodexAccount, OcxConfig } from "../types"; +import type { CodexAuthContext } from "./auth-context"; +import { MAIN_CODEX_ACCOUNT_ID } from "./main-account"; export const CODEX_ACCOUNT_LOG_LABEL_RE = /^p[a-f0-9]{6}$/; @@ -22,6 +24,17 @@ export function codexAccountLogLabel(account: CodexAccount): string { : fallbackCodexAccountLogLabel(account.id); } +/** Effective durable label for a resolved Codex Pool account. */ +export function codexAuthContextLogLabel( + authCtx: CodexAuthContext, + config: Pick, +): "main" | `p${string}` | undefined { + if (authCtx.kind !== "pool" && authCtx.kind !== "main-pool") return undefined; + if (authCtx.accountId === MAIN_CODEX_ACCOUNT_ID) return "main"; + const account = (config.codexAccounts ?? []).find(candidate => candidate.id === authCtx.accountId); + return account ? codexAccountLogLabel(account) as `p${string}` : undefined; +} + export function withCodexAccountLogLabel( account: Omit & Partial>, existingAccounts: readonly CodexAccount[], diff --git a/src/codex/auth-api.ts b/src/codex/auth-api.ts index 145971352a..87c849c638 100644 --- a/src/codex/auth-api.ts +++ b/src/codex/auth-api.ts @@ -5,7 +5,7 @@ import { saveConfigPreservingClaudeCode, withConfigMutationLockSync, } from "../config"; -import { withCodexAccountLogLabel } from "./account-label"; +import { codexAccountLogLabel, withCodexAccountLogLabel } from "./account-label"; import { getCodexAccountCredential, getValidCodexToken, @@ -239,7 +239,7 @@ function poolAccountDto( email: maskEmail(account.email) ?? account.email, ...(account.alias !== undefined ? { alias: account.alias } : {}), ...(plan !== undefined ? { plan } : {}), - ...(account.logLabel !== undefined ? { logLabel: account.logLabel } : {}), + logLabel: codexAccountLogLabel(account), isMain: false, paused, priority, @@ -1228,6 +1228,7 @@ export async function listCodexAuthAccountsSnapshot( id: MAIN_CODEX_ACCOUNT_ID, email: maskEmail(mainInfo.email) ?? "Codex App login", plan: mainInfo.plan, + logLabel: "main", isMain: true, paused: isCodexAccountPaused(runtimeConfig, MAIN_CODEX_ACCOUNT_ID), priority: getCodexAccountPriority(runtimeConfig, MAIN_CODEX_ACCOUNT_ID), diff --git a/src/server/management/logs-usage-routes.ts b/src/server/management/logs-usage-routes.ts index 50a66ac908..5d5cd8bbe1 100644 --- a/src/server/management/logs-usage-routes.ts +++ b/src/server/management/logs-usage-routes.ts @@ -261,6 +261,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise undefined); outcomeCtx = alternate.authCtx; + logCtx.accountLogLabel = codexAuthContextLogLabel(alternate.authCtx, config); try { upstream = await sendCompactAttempt(alternate.provider, alternate.headers, "single"); } catch (err) { diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index 5de97affde..6e7bde1efe 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -95,6 +95,7 @@ import { recordCodexUpstreamOutcome, type CodexUpstreamOutcome, } from "../../codex/routing"; +import { codexAuthContextLogLabel } from "../../codex/account-label"; import { applyUpstreamRecoveryInit, fetchWithResetRetry, @@ -495,6 +496,13 @@ async function retryCodexPoolOnAlternateAccount( retryAuthCtx.accountId, config, ); + logCtx.accountLogLabel = codexAuthContextLogLabel(retryAuthCtx, config); + sealRequestAttemptIdentity( + logCtx.activeAttempt, + logCtx.provider, + retryAdapter.name, + logCtx.accountLogLabel, + ); noteAttemptSend(logCtx.activeAttempt, passthroughEstimate); let upstreamResponse: Response; @@ -1170,6 +1178,7 @@ export async function handleComboResponses( attempt, childLog.provider, childLog.providerAdapter ?? attempt.adapter, + childLog.accountLogLabel, ); finishRequestAttempt(attempt, 499, Date.now() - started, childLog.usage); (logCtx.attempts ??= []).push(attempt); @@ -1224,6 +1233,7 @@ export async function handleComboResponses( attempt, childLog.provider, childLog.providerAdapter ?? attempt.adapter, + childLog.accountLogLabel, ); (logCtx.attempts ??= []).push(attempt); attemptRetained = true; @@ -1269,6 +1279,7 @@ export async function handleComboResponses( attempt, childLog.provider, childLog.providerAdapter ?? attempt.adapter, + childLog.accountLogLabel, ); finishRequestAttempt( attempt, @@ -1666,6 +1677,7 @@ async function handleResponsesInner( logCtx.provider = route.codexAccountNamespace ? `${route.providerName}-${route.codexAccountNamespace}` : formatCodexProviderForLog(route.providerName, codexLogAccountId(authCtx), config); + logCtx.accountLogLabel = codexAuthContextLogLabel(authCtx, config); // Prefer Codex pool account as the Cursor thread namespace when present. Cursor routes without // codexAccountMode still get a credential-derived scope inside the Cursor adapter. const identityScope = codexLogAccountId(authCtx); @@ -1763,6 +1775,7 @@ async function handleResponsesInner( delete route.codexAccountId; delete route.codexAccountNamespace; logCtx.provider = route.providerName; + delete logCtx.accountLogLabel; } const adapter = resolveAdapter(adapterProvider, config.cacheRetention); logCtx.providerAdapter = adapter.name; @@ -1779,7 +1792,7 @@ async function handleResponsesInner( logCtx.activeAttemptStartedAt = Date.now(); (logCtx.attempts ??= []).push(attempt); } - sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, adapter.name); + sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, adapter.name, logCtx.accountLogLabel); // CL-09: attach only the opaque exact route-subject identity to the attempt. // This is best-effort passive metadata: no Lab state is created and failure // must never alter, retry, or delay the upstream request. @@ -3077,7 +3090,7 @@ async function handleResponsesInner( : undefined; if (retryEstimate !== undefined) logCtx.usageLogInputTokens = retryEstimate; logCtx.providerAdapter = activeAdapter.name; - sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name); + sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel); noteAttemptSend(logCtx.activeAttempt, retryEstimate, recovery); try { try { @@ -3229,7 +3242,7 @@ async function handleResponsesInner( resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire), config.cacheRetention, ); - sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name); + sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel); const result = await rebuildAndRefetch("anthropic-oauth-429"); if ("failed" in result) return result.failed; upstreamResponse = result; @@ -3497,7 +3510,7 @@ async function handleResponsesInner( resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire), config.cacheRetention, ); - sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name); + sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel); nextContinuationRecoveryKind = "anthropic-oauth-429"; continue; } catch { diff --git a/src/usage/log.ts b/src/usage/log.ts index 00b750ad03..5256c2af93 100644 --- a/src/usage/log.ts +++ b/src/usage/log.ts @@ -5,8 +5,14 @@ import { recordOwnedConfigPath } from "../lib/config-ownership"; import { usageDisplayTotalTokens } from "./totals"; import type { OcxUsage } from "../types"; import { normalizeRouteDecisionTrace, type RouteDecisionTraceV1 } from "../routing/trace"; +import { CODEX_ACCOUNT_LOG_LABEL_RE } from "../codex/account-label"; export type UsageStatus = "reported" | "unreported" | "unsupported" | "estimated"; +export type CodexUsageAccountLogLabel = "main" | `p${string}`; + +export function isCodexUsageAccountLogLabel(value: unknown): value is CodexUsageAccountLogLabel { + return value === "main" || (typeof value === "string" && CODEX_ACCOUNT_LOG_LABEL_RE.test(value)); +} /** * Recovery kinds recorded per attempt in the usage log; the GUI renders localized labels @@ -33,6 +39,8 @@ export interface PersistedUsageAttempt { sendCount: number; recoveryKinds: AttemptRecoveryKind[]; usageStatus: UsageStatus; + /** Stable non-PII identity for the Codex pool account that served this attempt. */ + accountLogLabel?: CodexUsageAccountLogLabel; inputTokenEstimate?: number; usage?: OcxUsage; totalTokens?: number; @@ -58,6 +66,8 @@ export interface PersistedUsageEntry { admissionKind?: "configured" | "environment" | "loopback"; /** The inbound wire, not the client product — see `surface`. */ inboundProtocol?: "responses" | "chat" | "messages"; + /** Stable non-PII identity for Codex Pool usage; absent for Direct/non-Codex traffic. */ + accountLogLabel?: CodexUsageAccountLogLabel; /** Best-effort chat/session correlation for Logs grouping (#330). */ conversationId?: string; resolvedModel?: string; @@ -271,6 +281,9 @@ function normalizeUsageAttempt(raw: unknown): PersistedUsageAttempt | null { sendCount: attempt.sendCount as number, recoveryKinds, usageStatus: attempt.usageStatus as UsageStatus, + ...(isCodexUsageAccountLogLabel(attempt.accountLogLabel) + ? { accountLogLabel: attempt.accountLogLabel } + : {}), ...(isNonNegativeFiniteNumber(attempt.inputTokenEstimate) ? { inputTokenEstimate: attempt.inputTokenEstimate } : {}), @@ -350,6 +363,9 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry { : {}), ...(isKnownAdmissionKind(entry.admissionKind) ? { admissionKind: entry.admissionKind } : {}), ...(isKnownInboundProtocol(entry.inboundProtocol) ? { inboundProtocol: entry.inboundProtocol } : {}), + ...(isCodexUsageAccountLogLabel(entry.accountLogLabel) + ? { accountLogLabel: entry.accountLogLabel } + : {}), ...(typeof entry.conversationId === "string" && entry.conversationId.trim() ? { conversationId: entry.conversationId.trim().slice(0, 128) } : {}), diff --git a/src/usage/summary.ts b/src/usage/summary.ts index 8337505ed5..65f91be109 100644 --- a/src/usage/summary.ts +++ b/src/usage/summary.ts @@ -1,8 +1,8 @@ import { baseProviderLabel } from "../providers/label"; import { canonicalAntigravityUsageModel } from "../providers/antigravity-models"; import { usageDisplayTotalTokens } from "./totals"; -import type { PersistedUsageEntry, UsageStatus } from "./log"; -import { estimateComboCost, estimateRequestCost, serviceTierContext } from "./cost"; +import { isCodexUsageAccountLogLabel, type PersistedUsageEntry, type UsageStatus } from "./log"; +import { estimateAttemptCost, estimateComboCost, estimateRequestCost, serviceTierContext } from "./cost"; export type UsageRange = "7d" | "30d" | "all"; export type UsageSurface = "all" | "codex" | "claude" | "grok"; @@ -79,6 +79,28 @@ export interface UsageProvider { estimatedCostUsd?: number; } +export interface UsageAccount { + accountLogLabel: string; + ambiguous: boolean; + requests: number; + attemptCount: number; + measuredAttempts: number; + reportedAttempts: number; + estimatedAttempts: number; + unmeteredAttempts: number; + inputTokens: number; + outputTokens: number; + cacheReadInputTokens: number; + cacheCreationInputTokens: number; + reasoningOutputTokens: number; + totalTokens: number; + usageCoverageRatio: number; + estimatedCostUsd?: number; + pricedAttempts: number; + unpricedAttempts: number; + priceCoverageRatio: number; +} + export interface UsageSummary { range: UsageRange; surface: UsageSurface; @@ -88,6 +110,7 @@ export interface UsageSummary { days: UsageDay[]; models: UsageModel[]; providers: UsageProvider[]; + accounts: UsageAccount[]; } const DAY_MS = 86_400_000; @@ -547,6 +570,133 @@ function buildProviders(entries: PersistedUsageEntry[], totalTokens: number): Us return providers.sort((a, b) => b.requests - a.requests); } +const LEGACY_AMBIGUOUS_ACCOUNT_LABEL = "legacy-ambiguous"; + +function legacyCodexAccountLabel(provider: string): string | null { + if (baseProviderLabel(provider) !== "openai") return null; + const suffix = provider.match(/-(main|p[a-f0-9]{6})$/)?.[1]; + return suffix ?? LEGACY_AMBIGUOUS_ACCOUNT_LABEL; +} + +function accountLabelForAttribution(provider: string, explicit: unknown): string | null { + if (isCodexUsageAccountLogLabel(explicit)) return explicit; + return legacyCodexAccountLabel(provider); +} + +function buildAccounts(entries: PersistedUsageEntry[]): UsageAccount[] { + const byLabel = new Map(); + const requestIds = new Map>(); + + const add = (input: { + requestId: string; + provider: string; + accountLogLabel?: string; + usageStatus: UsageStatus; + usage?: PersistedUsageEntry["usage"]; + totalTokens?: number; + estimate: ReturnType; + }): void => { + const label = accountLabelForAttribution(input.provider, input.accountLogLabel); + if (!label) return; + let row = byLabel.get(label); + if (!row) { + row = { + accountLogLabel: label, + ambiguous: label === LEGACY_AMBIGUOUS_ACCOUNT_LABEL, + requests: 0, + attemptCount: 0, + measuredAttempts: 0, + reportedAttempts: 0, + estimatedAttempts: 0, + unmeteredAttempts: 0, + inputTokens: 0, + outputTokens: 0, + cacheReadInputTokens: 0, + cacheCreationInputTokens: 0, + reasoningOutputTokens: 0, + totalTokens: 0, + usageCoverageRatio: 0, + pricedAttempts: 0, + unpricedAttempts: 0, + priceCoverageRatio: 0, + }; + byLabel.set(label, row); + requestIds.set(label, new Set()); + } + requestIds.get(label)!.add(input.requestId); + row.requests = requestIds.get(label)!.size; + row.attemptCount += 1; + const measured = input.usage !== undefined && isMeasuredStatus(input.usageStatus); + if (!measured) { + row.unmeteredAttempts += 1; + return; + } + + row.measuredAttempts += 1; + if (input.usageStatus === "reported") row.reportedAttempts += 1; + else if (input.usageStatus === "estimated") row.estimatedAttempts += 1; + row.inputTokens += input.usage!.inputTokens; + row.outputTokens += input.usage!.outputTokens; + const creation = input.usage!.cacheCreationInputTokens; + const read = typeof input.usage!.cacheReadInputTokens === "number" + ? input.usage!.cacheReadInputTokens + : typeof input.usage!.cachedInputTokens === "number" && typeof creation === "number" + ? Math.max(0, input.usage!.cachedInputTokens - creation) + : input.usage!.cachedInputTokens; + if (typeof read === "number") row.cacheReadInputTokens += read; + if (typeof creation === "number") row.cacheCreationInputTokens += creation; + if (typeof input.usage!.reasoningOutputTokens === "number") { + row.reasoningOutputTokens += input.usage!.reasoningOutputTokens; + } + row.totalTokens += usageDisplayTotalTokens(input.usage, input.totalTokens) ?? 0; + if (input.estimate) { + row.pricedAttempts += 1; + row.estimatedCostUsd = (row.estimatedCostUsd ?? 0) + input.estimate.cost.total; + } else { + row.unpricedAttempts += 1; + } + }; + + for (const entry of entries) { + const tier = serviceTierContext(entry); + if (entry.attempts?.length) { + for (const attempt of entry.attempts) { + add({ + requestId: entry.requestId, + provider: attempt.provider, + ...(attempt.accountLogLabel ? { accountLogLabel: attempt.accountLogLabel } : {}), + usageStatus: attempt.usageStatus, + ...(attempt.usage ? { usage: attempt.usage } : {}), + ...(attempt.totalTokens !== undefined ? { totalTokens: attempt.totalTokens } : {}), + estimate: estimateAttemptCost(attempt, undefined, tier), + }); + } + continue; + } + add({ + requestId: entry.requestId, + provider: entry.provider, + ...(entry.accountLogLabel ? { accountLogLabel: entry.accountLogLabel } : {}), + usageStatus: entry.usageStatus, + ...(entry.usage ? { usage: entry.usage } : {}), + ...(entry.totalTokens !== undefined ? { totalTokens: entry.totalTokens } : {}), + estimate: estimateRequestCost({ + provider: entry.provider, + model: entry.model, + usage: entry.usage, + usageStatus: entry.usageStatus, + serviceTier: tier, + }), + }); + } + + for (const row of byLabel.values()) { + row.usageCoverageRatio = row.attemptCount === 0 ? 0 : row.measuredAttempts / row.attemptCount; + row.priceCoverageRatio = row.measuredAttempts === 0 ? 0 : row.pricedAttempts / row.measuredAttempts; + } + return [...byLabel.values()].sort((a, b) => b.totalTokens - a.totalTokens); +} + export function summarizeUsage( entries: PersistedUsageEntry[], range: UsageRange, @@ -581,5 +731,6 @@ export function summarizeUsage( days: buildDayGrid(range, since, now, filteredEntries), models: buildModels(filteredEntries, totals.totalTokens), providers: buildProviders(filteredEntries, totals.totalTokens), + accounts: buildAccounts(filteredEntries), }; } diff --git a/structure/05_gui-and-management-api.md b/structure/05_gui-and-management-api.md index d9cbea71d4..5867321f0e 100644 --- a/structure/05_gui-and-management-api.md +++ b/structure/05_gui-and-management-api.md @@ -293,6 +293,10 @@ keeps the saved state and renders fixed `ocx sync` guidance without server/accou `src/usage/log.ts` writes append-only JSONL to `~/.opencodex/usage.jsonl` with file mode `0o600`. `src/usage/summary.ts` turns that file into the `/api/usage` shape — totals, daily zero-filled grid, model and provider breakdowns, and `measured / reported / unreported / unsupported / estimated` counts. +A Codex-surface response also includes an `accounts` breakdown keyed by the stable non-PII +`accountLogLabel`; current cards join those rows to the management account DTO and show the 30-day +token total, API-equivalent cost estimate, and measurement coverage. New main-pool rows use `main`, +while legacy bare `openai` rows stay ambiguous rather than being reassigned from current config. A missing `usage.jsonl` returns a zeroed summary with 200, not an error: a fresh install has no usage and must not render as a failure. What the shape must never do is present an unmeasured request as a measured zero — that is what the `measured / reported / unreported / unsupported / diff --git a/tests/api-usage.test.ts b/tests/api-usage.test.ts index 33e12d0869..bc6a98f376 100644 --- a/tests/api-usage.test.ts +++ b/tests/api-usage.test.ts @@ -92,7 +92,7 @@ afterEach(() => { }); describe("GET /api/usage", () => { - test("returns documented shape with summary, days, models, providers", async () => { + test("returns documented shape with summary, days, models, providers, and accounts", async () => { writeFixture(Date.now()); const server = startServer(0); try { @@ -105,10 +105,12 @@ describe("GET /api/usage", () => { expect(body).toHaveProperty("days"); expect(body).toHaveProperty("models"); expect(body).toHaveProperty("providers"); + expect(body).toHaveProperty("accounts"); expect(body).toMatchObject({ historyTruncated: false, truncatedPrefixBytes: 0, entriesTruncated: false, entriesDropped: 0 }); expect(Array.isArray(body.days)).toBe(true); expect(Array.isArray(body.models)).toBe(true); expect(Array.isArray(body.providers)).toBe(true); + expect(Array.isArray(body.accounts)).toBe(true); } finally { await server.stop(true); } @@ -328,6 +330,7 @@ describe("GET /api/usage", () => { const body = await res.json(); expect(body.surface).toBe("claude"); expect(body.summary.requests).toBe(0); + expect(body.accounts).toEqual([]); expect(body.error).toBe("read_failed"); } finally { await server.stop(true); diff --git a/tests/codex-auth-api.test.ts b/tests/codex-auth-api.test.ts index 08e85f4a48..e2d8c00790 100644 --- a/tests/codex-auth-api.test.ts +++ b/tests/codex-auth-api.test.ts @@ -8,7 +8,7 @@ import { getNativeMainProfileRequestCount, resetLifecycleDrainStateForTests, } from "../src/server/lifecycle"; -import { CODEX_ACCOUNT_LOG_LABEL_RE } from "../src/codex/account-label"; +import { CODEX_ACCOUNT_LOG_LABEL_RE, fallbackCodexAccountLogLabel } from "../src/codex/account-label"; import { handleCodexAuthAPI, updateAccountQuota, getAccountQuota, checkAccountIdCollision, getMainChatgptAccountId, @@ -978,7 +978,7 @@ describe("codex-auth API", () => { id: "pool-safe", email: "p***n@example.test", plan: "Plus", - logLabel: "work", + logLabel: fallbackCodexAccountLogLabel("pool-safe"), isMain: false, hasCredential: true, }); @@ -987,6 +987,21 @@ describe("codex-auth API", () => { expect(JSON.stringify(pool)).not.toContain("acct-credential-secret"); }); + test("GET /api/codex-auth/accounts exposes the effective label for legacy pool and main accounts", async () => { + const config = makeConfig(); + seedPoolAccount(config, { id: "legacy-pool", email: "legacy@example.test" }); + updateAccountQuota("legacy-pool", 10); + + const req = new Request("http://localhost/api/codex-auth/accounts", { method: "GET" }); + const resp = await handleCodexAuthAPI(req, new URL(req.url), config); + const data = await resp!.json() as { accounts: CodexAuthAccountDto[] }; + + expect(data.accounts.find(account => account.id === "legacy-pool")?.logLabel) + .toBe(fallbackCodexAccountLogLabel("legacy-pool")); + expect(data.accounts.find(account => account.id === MAIN_CODEX_ACCOUNT_ID)?.logLabel).toBe("main"); + expect(config.codexAccounts?.[0]?.logLabel).toBeUndefined(); + }); + test("POST /api/codex-auth/accounts disables manual import by default before writing credentials", async () => { const req = new Request("http://localhost/api/codex-auth/accounts", { method: "POST", diff --git a/tests/request-log.test.ts b/tests/request-log.test.ts index 347724dcc4..5425c52a1e 100644 --- a/tests/request-log.test.ts +++ b/tests/request-log.test.ts @@ -263,7 +263,7 @@ describe("request log metadata", () => { noteAttemptSend(a, 100); noteAttemptSend(a, 120, "transient-5xx"); noteAttemptSend(a, 120, "transient-5xx"); - sealRequestAttemptIdentity(a, "chatgpt-pabcdef", "openai-responses"); + sealRequestAttemptIdentity(a, "chatgpt-pabcdef", "openai-responses", "pabcdef"); finishRequestAttempt(a, 503, 12); const b = beginRequestAttempt(2, "prov-b", "model-b", "openai-chat"); @@ -278,6 +278,7 @@ describe("request log metadata", () => { expect(a).toMatchObject({ ordinal: 1, provider: "chatgpt-pabcdef", + accountLogLabel: "pabcdef", adapter: "openai-responses", status: 503, sendCount: 3, diff --git a/tests/responses-account-label.test.ts b/tests/responses-account-label.test.ts new file mode 100644 index 0000000000..ebb0f76636 --- /dev/null +++ b/tests/responses-account-label.test.ts @@ -0,0 +1,147 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fallbackCodexAccountLogLabel } from "../src/codex/account-label"; +import { saveCodexAccountCredential } from "../src/codex/account-store"; +import { clearAccountQuota, updateAccountQuota } from "../src/codex/auth-api"; +import { MAIN_CODEX_ACCOUNT_ID } from "../src/codex/main-account"; +import { clearCodexUpstreamHealth, clearThreadAccountMap } from "../src/codex/routing"; +import type { RequestLogContext } from "../src/server/request-log"; +import { handleResponses } from "../src/server/responses"; +import type { OcxConfig } from "../src/types"; + +const originalFetch = globalThis.fetch; + +function poolConfig(accountIds: string[]): OcxConfig { + return { + defaultProvider: "openai", + activeCodexAccountId: accountIds[0], + autoSwitchThreshold: 0, + providers: { + openai: { + adapter: "openai-responses", + baseUrl: "https://chatgpt.com/backend-api/codex", + authMode: "forward", + codexAccountMode: "pool", + }, + }, + codexAccounts: accountIds.map(id => ({ + id, + email: `${id}@example.test`, + isMain: false, + chatgptAccountId: `${id}_chatgpt`, + })), + } as OcxConfig; +} + +function completedResponse(id: string): Response { + return Response.json({ + id, + status: "completed", + output: [], + usage: { input_tokens: 10, output_tokens: 5, total_tokens: 15 }, + }); +} + +function request(): Request { + return new Request("http://localhost/v1/responses", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ model: "gpt-5.6-sol", input: "hello", stream: false }), + }); +} + +function savePoolCredential(id: string): void { + saveCodexAccountCredential(id, { + accessToken: `${id}-access-token`, + refreshToken: `${id}-refresh-token`, + expiresAt: Date.now() + 300_000, + chatgptAccountId: `${id}_chatgpt`, + }); +} + +async function withPoolHome(run: (home: string) => Promise): Promise { + const home = mkdtempSync(join(tmpdir(), "ocx-responses-account-label-")); + const previousOpencodexHome = process.env.OPENCODEX_HOME; + const previousCodexHome = process.env.CODEX_HOME; + process.env.OPENCODEX_HOME = home; + process.env.CODEX_HOME = home; + clearCodexUpstreamHealth(); + clearThreadAccountMap(); + clearAccountQuota(); + try { + return await run(home); + } finally { + globalThis.fetch = originalFetch; + clearCodexUpstreamHealth(); + clearThreadAccountMap(); + clearAccountQuota(); + rmSync(home, { recursive: true, force: true }); + if (previousOpencodexHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = previousOpencodexHome; + if (previousCodexHome === undefined) delete process.env.CODEX_HOME; + else process.env.CODEX_HOME = previousCodexHome; + } +} + +afterEach(() => { + globalThis.fetch = originalFetch; +}); + +describe("Responses account usage attribution", () => { + test("main-pool and legacy added accounts carry their effective labels", async () => { + await withPoolHome(async home => { + writeFileSync(join(home, "auth.json"), JSON.stringify({ + tokens: { access_token: "main-access-token", account_id: "main-account" }, + })); + updateAccountQuota(MAIN_CODEX_ACCOUNT_ID, 0); + globalThis.fetch = (async () => completedResponse("main-response")) as typeof fetch; + + const mainConfig = poolConfig([]); + mainConfig.activeCodexAccountId = MAIN_CODEX_ACCOUNT_ID; + const mainLog: RequestLogContext = { model: "", provider: "" }; + expect((await handleResponses(request(), mainConfig, mainLog, {})).status).toBe(200); + expect(mainLog.accountLogLabel).toBe("main"); + expect(mainLog.activeAttempt?.accountLogLabel).toBe("main"); + + const poolConfigValue = poolConfig(["pool-a"]); + savePoolCredential("pool-a"); + updateAccountQuota("pool-a", 0); + const poolLog: RequestLogContext = { model: "", provider: "" }; + expect((await handleResponses(request(), poolConfigValue, poolLog, {})).status).toBe(200); + expect(poolLog.accountLogLabel).toBe(fallbackCodexAccountLogLabel("pool-a")); + expect(poolLog.activeAttempt?.accountLogLabel).toBe(fallbackCodexAccountLogLabel("pool-a")); + }); + }); + + test("a pre-stream quota retry updates attribution to the serving alternate account", async () => { + await withPoolHome(async () => { + const config = poolConfig(["pool-a", "pool-b"]); + for (const id of ["pool-a", "pool-b"]) { + savePoolCredential(id); + updateAccountQuota(id, id === "pool-a" ? 10 : 20); + } + const bearers: string[] = []; + globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => { + const bearer = new Headers(init?.headers).get("authorization") ?? ""; + bearers.push(bearer); + if (bearers.length === 1) { + return Response.json({ error: { message: "rate limited" } }, { + status: 429, + headers: { "retry-after": "42" }, + }); + } + return completedResponse("pool-b-response"); + }) as typeof fetch; + + const logCtx: RequestLogContext = { model: "", provider: "" }; + const response = await handleResponses(request(), config, logCtx, {}); + + expect(response.status).toBe(200); + expect(bearers).toEqual(["Bearer pool-a-access-token", "Bearer pool-b-access-token"]); + expect(logCtx.accountLogLabel).toBe(fallbackCodexAccountLogLabel("pool-b")); + expect(logCtx.activeAttempt?.accountLogLabel).toBe(fallbackCodexAccountLogLabel("pool-b")); + }); + }); +}); diff --git a/tests/responses-compaction-routing.test.ts b/tests/responses-compaction-routing.test.ts index fdb1830517..98a40b4e15 100644 --- a/tests/responses-compaction-routing.test.ts +++ b/tests/responses-compaction-routing.test.ts @@ -20,6 +20,7 @@ import { } from "../src/codex/routing"; import { clearAccountQuota, updateAccountQuota } from "../src/codex/auth-api"; import { MAIN_CODEX_ACCOUNT_ID } from "../src/codex/main-account"; +import { fallbackCodexAccountLogLabel } from "../src/codex/account-label"; import * as authContextModule from "../src/codex/auth-context"; import { releaseCodexAuthContextProbeLease, @@ -195,6 +196,47 @@ describe("native compact usage reporting", () => { expect(body.usage).toMatchObject({ input_tokens: 10, output_tokens: 5, total_tokens: 15 }); expect(logCtx.usage).toMatchObject({ inputTokens: 10, outputTokens: 5, totalTokens: 15 }); }); + + test("main-pool and legacy added accounts carry their effective usage labels", async () => { + const testDir = mkdtempSync(join(tmpdir(), "ocx-compact-account-label-")); + const previousOpencodexHome = process.env.OPENCODEX_HOME; + const previousCodexHome = process.env.CODEX_HOME; + process.env.OPENCODEX_HOME = testDir; + process.env.CODEX_HOME = testDir; + try { + const mainConfig = nativePoolConfig(); + mainConfig.codexAccounts = []; + mainConfig.activeCodexAccountId = MAIN_CODEX_ACCOUNT_ID; + writeFileSync(join(testDir, "auth.json"), JSON.stringify({ + tokens: { access_token: "main-access-token", account_id: "main-account" }, + })); + updateAccountQuota(MAIN_CODEX_ACCOUNT_ID, 0); + globalThis.fetch = (async () => jsonResponse(completedPayload("main compact"))) as typeof fetch; + const mainLog: RequestLogContext = { model: "", provider: "" }; + expect((await handleResponsesCompact(compactionRequest(baseCompactionBody({})), mainConfig, mainLog)).status).toBe(200); + expect(mainLog.accountLogLabel).toBe("main"); + + const poolConfig = nativePoolConfig(); + saveCodexAccountCredential("pool-a", { + accessToken: "pool-access-token", + refreshToken: "pool-refresh-token", + expiresAt: Date.now() + 300_000, + chatgptAccountId: "pool_acc", + }); + updateAccountQuota("pool-a", 0); + const poolLog: RequestLogContext = { model: "", provider: "" }; + expect((await handleResponsesCompact(compactionRequest(baseCompactionBody({})), poolConfig, poolLog)).status).toBe(200); + expect(poolLog.accountLogLabel).toBe(fallbackCodexAccountLogLabel("pool-a")); + } finally { + globalThis.fetch = originalFetch; + clearAccountQuota(); + rmSync(testDir, { recursive: true, force: true }); + if (previousOpencodexHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = previousOpencodexHome; + if (previousCodexHome === undefined) delete process.env.CODEX_HOME; + else process.env.CODEX_HOME = previousCodexHome; + } + }); }); describe("native Codex pool compaction", () => { @@ -642,15 +684,17 @@ describe("compact alternate-account attempt (#913)", () => { return jsonResponse(completedPayload("alternate compact response")); }) as typeof fetch; + const logCtx: RequestLogContext = { model: "", provider: "" }; const res = await handleResponsesCompact( compactionRequest(baseCompactionBody({})), config, - { model: "", provider: "" }, + logCtx, ); // Two sends, not one and not three: the alternate ran once and did not recurse. expect(bearers).toEqual(["Bearer pool-a-access-token", "Bearer pool-b-access-token"]); expect(res.status).toBe(200); + expect(logCtx.accountLogLabel).toBe(fallbackCodexAccountLogLabel("pool-b")); }); }); diff --git a/tests/usage-log.test.ts b/tests/usage-log.test.ts index 98302aca91..5a4b905043 100644 --- a/tests/usage-log.test.ts +++ b/tests/usage-log.test.ts @@ -53,6 +53,41 @@ describe("usage log", () => { expect(normalized.attempts).toEqual([]); }); + test("preserves only valid non-PII Codex account log labels", () => { + const normalized = normalizeUsageEntryForTest({ + requestId: "ocx-account-label", + timestamp: 1, + provider: "openai-pabc123", + model: "gpt-test", + accountLogLabel: "pabc123", + status: 200, + durationMs: 1, + usageStatus: "reported", + attempts: [{ + ordinal: 1, + provider: "openai-pabc123", + model: "gpt-test", + adapter: "openai-responses", + accountLogLabel: "pabc123", + status: 200, + durationMs: 1, + sendCount: 1, + recoveryKinds: [], + usageStatus: "reported", + }], + }); + expect(normalized.accountLogLabel).toBe("pabc123"); + expect(normalized.attempts?.[0]?.accountLogLabel).toBe("pabc123"); + + const rejected = normalizeUsageEntryForTest({ + ...normalized, + accountLogLabel: "raw-account-id", + attempts: [{ ...normalized.attempts![0]!, accountLogLabel: "person@example.test" }], + }); + expect(rejected.accountLogLabel).toBeUndefined(); + expect(rejected.attempts?.[0]?.accountLogLabel).toBeUndefined(); + }); + test("persists the rate-limit-429 recovery kind on attempts", () => { const entry: PersistedUsageEntry = { requestId: "ocx-ratelimit-kind", diff --git a/tests/usage-summary.test.ts b/tests/usage-summary.test.ts index 2d32106d44..52db5bb0ae 100644 --- a/tests/usage-summary.test.ts +++ b/tests/usage-summary.test.ts @@ -15,6 +15,7 @@ function entry(overrides: Partial & { ts: number }): Persis durationMs: rest.durationMs ?? 10, usageStatus: rest.usageStatus ?? "unreported", ...(rest.surface === "claude" ? { surface: rest.surface } : {}), + ...(rest.accountLogLabel !== undefined ? { accountLogLabel: rest.accountLogLabel } : {}), ...(rest.resolvedModel !== undefined ? { resolvedModel: rest.resolvedModel } : {}), ...(rest.usage ? { usage: rest.usage } : {}), ...(rest.totalTokens !== undefined ? { totalTokens: rest.totalTokens } : {}), @@ -151,6 +152,153 @@ describe("summarizeUsage", () => { expect(sum.summary.outputTokens).toBe(5); }); + test("attributes Codex usage and API-equivalent cost by stable account log label", () => { + const entries: PersistedUsageEntry[] = [ + entry({ + ts: FIXED_NOW - 1_000, + requestId: "added-explicit", + provider: "openai-pabc123", + accountLogLabel: "pabc123", + usageStatus: "reported", + usage: { inputTokens: 100, outputTokens: 10 }, + totalTokens: 110, + }), + entry({ + ts: FIXED_NOW - 2_000, + requestId: "added-legacy", + provider: "openai-pabc123", + usageStatus: "estimated", + usage: { inputTokens: 40, outputTokens: 5, estimated: true }, + totalTokens: 45, + }), + entry({ + ts: FIXED_NOW - 3_000, + requestId: "main-explicit", + provider: "openai", + accountLogLabel: "main", + usageStatus: "reported", + usage: { inputTokens: 20, outputTokens: 2 }, + totalTokens: 22, + }), + entry({ + ts: FIXED_NOW - 4_000, + requestId: "main-legacy", + provider: "openai-main", + usageStatus: "reported", + usage: { inputTokens: 30, outputTokens: 3 }, + totalTokens: 33, + }), + entry({ + ts: FIXED_NOW - 5_000, + requestId: "legacy-bare", + provider: "openai", + usageStatus: "unreported", + }), + entry({ + ts: FIXED_NOW - 6_000, + requestId: "custom-selector-explicit", + provider: "openai-side", + accountLogLabel: "pffffff", + usageStatus: "reported", + usage: { inputTokens: 500, outputTokens: 50 }, + totalTokens: 550, + }), + ]; + + const sum = summarizeUsage(entries, "30d", FIXED_NOW, "codex"); + expect(sum.accounts.map(row => row.accountLogLabel).sort()).toEqual([ + "legacy-ambiguous", + "main", + "pabc123", + "pffffff", + ]); + expect(sum.accounts.find(row => row.accountLogLabel === "pabc123")).toMatchObject({ + requests: 2, + attemptCount: 2, + measuredAttempts: 2, + reportedAttempts: 1, + estimatedAttempts: 1, + totalTokens: 155, + inputTokens: 140, + outputTokens: 15, + usageCoverageRatio: 1, + priceCoverageRatio: 1, + }); + expect(sum.accounts.find(row => row.accountLogLabel === "pabc123")?.estimatedCostUsd) + .toBeCloseTo((140 * 5 + 15 * 30) / 1e6, 9); + expect(sum.accounts.find(row => row.accountLogLabel === "main")).toMatchObject({ + requests: 2, + totalTokens: 55, + ambiguous: false, + }); + expect(sum.accounts.find(row => row.accountLogLabel === "pffffff")).toMatchObject({ + requests: 1, + totalTokens: 550, + ambiguous: false, + }); + expect(sum.accounts.find(row => row.accountLogLabel === "legacy-ambiguous")).toMatchObject({ + requests: 1, + unmeteredAttempts: 1, + totalTokens: 0, + usageCoverageRatio: 0, + ambiguous: true, + }); + }); + + test("attributes combo attempts to the account that physically served each attempt", () => { + const combo = entry({ + ts: FIXED_NOW - 1_000, + requestId: "combo-two-accounts", + provider: "combo", + model: "combo/native", + usageStatus: "reported", + usage: { inputTokens: 33, outputTokens: 3 }, + totalTokens: 36, + attempts: [ + { + ordinal: 1, + provider: "openai-p111111", + model: "gpt-5.5", + adapter: "openai-responses", + accountLogLabel: "p111111", + status: 502, + durationMs: 10, + sendCount: 1, + recoveryKinds: [], + usageStatus: "reported", + usage: { inputTokens: 11, outputTokens: 1 }, + totalTokens: 12, + }, + { + ordinal: 2, + provider: "openai-p222222", + model: "gpt-5.5", + adapter: "openai-responses", + accountLogLabel: "p222222", + status: 200, + durationMs: 20, + sendCount: 1, + recoveryKinds: [], + usageStatus: "reported", + usage: { inputTokens: 22, outputTokens: 2 }, + totalTokens: 24, + }, + ], + }); + + const rows = summarizeUsage([combo], "30d", FIXED_NOW, "codex").accounts; + expect(rows.find(row => row.accountLogLabel === "p111111")).toMatchObject({ + requests: 1, + attemptCount: 1, + totalTokens: 12, + }); + expect(rows.find(row => row.accountLogLabel === "p222222")).toMatchObject({ + requests: 1, + attemptCount: 1, + totalTokens: 24, + }); + }); + test("three OpenAI API Pro selections stay separate from their resolved base models", () => { const entries = ["sol", "terra", "luna"].map((family, index) => entry({ ts: FIXED_NOW - index * 1000,