Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9792498
feat(quota): expose credential-scoped account and key readings
invalid-email-address Sep 5, 2026
b02ecc4
fix(quota): bound stale readings and contain async refresh failures
invalid-email-address Sep 5, 2026
768e5a0
fix(quota): honor default key auth for Kimi readings
invalid-email-address Sep 5, 2026
d78a02a
feat(gui): unify current and per-account quota views
invalid-email-address Sep 5, 2026
8960ddb
fix(gui): make quota lifecycle fixtures and cleanup compiler-safe
invalid-email-address Sep 5, 2026
ac5311b
Merge branch 'codex/provider-usage-attribution' into codex/provider-a…
invalid-email-address Sep 5, 2026
a4fb9e1
test(quota): assert secure transport for account readers
invalid-email-address Sep 5, 2026
9c280b6
Merge branch 'codex/provider-account-quota-api' into codex/provider-q…
invalid-email-address Sep 5, 2026
140c49a
merge: preserve current dev quota readers in account quota layer
invalid-email-address Sep 5, 2026
c279976
Merge branch 'codex/provider-account-quota-api' into codex/provider-q…
invalid-email-address Sep 5, 2026
7126993
Merge branch 'codex/provider-usage-attribution' into codex/provider-a…
invalid-email-address Sep 5, 2026
209a9e1
Merge branch 'codex/provider-account-quota-api' into codex/provider-q…
invalid-email-address Sep 5, 2026
7e5c264
Merge branch 'codex/provider-usage-attribution' into codex/provider-a…
invalid-email-address Sep 5, 2026
671a071
Merge branch 'codex/provider-account-quota-api' into codex/provider-q…
invalid-email-address Sep 5, 2026
6c70347
Merge branch 'codex/provider-usage-attribution' into codex/provider-a…
invalid-email-address Sep 5, 2026
e676200
Merge branch 'codex/provider-account-quota-api' into codex/provider-q…
invalid-email-address Sep 5, 2026
e76e09a
Merge branch 'codex/provider-usage-attribution' into codex/provider-a…
invalid-email-address Sep 5, 2026
638f2b8
Merge branch 'codex/provider-account-quota-api' into codex/provider-q…
invalid-email-address Sep 5, 2026
772ed02
Merge branch 'codex/provider-usage-attribution' into codex/provider-a…
invalid-email-address Sep 5, 2026
eb18ad7
Merge branch 'codex/provider-account-quota-api' into codex/provider-q…
invalid-email-address Sep 5, 2026
3d1b5f5
Merge branch 'codex/provider-usage-attribution' into codex/provider-a…
invalid-email-address Sep 5, 2026
b8d90fe
Merge branch 'codex/provider-account-quota-api' into codex/provider-q…
invalid-email-address Sep 5, 2026
ae07a49
Merge branch 'codex/provider-usage-attribution' into codex/provider-a…
invalid-email-address Sep 5, 2026
dbf40c2
Merge branch 'codex/provider-account-quota-api' into codex/provider-q…
invalid-email-address Sep 5, 2026
b76390b
Merge branch 'codex/provider-usage-attribution' into codex/provider-a…
invalid-email-address Sep 5, 2026
77b95a8
Merge branch 'codex/provider-account-quota-api' into codex/provider-q…
invalid-email-address Sep 5, 2026
947e8bd
Merge branch 'codex/provider-usage-attribution' into codex/provider-a…
invalid-email-address Sep 5, 2026
86258ce
Merge branch 'codex/provider-account-quota-api' into codex/provider-q…
invalid-email-address Sep 5, 2026
616e5b5
Merge branch 'codex/provider-usage-attribution' into codex/provider-a…
invalid-email-address Sep 5, 2026
c299797
Merge branch 'codex/provider-account-quota-api' into codex/provider-q…
invalid-email-address Sep 5, 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
20 changes: 19 additions & 1 deletion devlog/_plan/260905_provider_usage_quota_parity/000_plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,27 @@ Static source checker and GUI build passed; existing chunk-size warning only. No
Next work-phase: attribution. Open risk: unseen historical upstream model identity cannot be
recovered; we qualify only saved fallback provenance instead of guessing or rewriting rows.

Attribution D: `9a9ad98b8`, PR #3582, CI run33938837845 all four backend test shards,
GUI gates (1371 pass / 0 fail), API usage succeeded; independent Volta PASS. Remaining macOS
jobs are still mandatory at landing, not claimed green. Next quota-api P reverified 020 against
the unchanged quota/management baseline; attribution changes do not alter its signatures.
API layer branch `codex/provider-account-quota-api` starts at the verified attribution head.

Quota API D: `768e5a004`, PR #3584, run33940065554 all four backend shards/gates/API usage
successful, including explicit credential isolation, passive no-network and post-await age
regressions. Wegener full/interdiff review and Kant omitted-mode delta PASS. No local validation
was executed. Next UI P reverified 030 against that additive row contract; remaining platform
rollups remain mandatory before merge. UI branch `codex/provider-quota-parity` starts here.

## Delegation write map (locked before Build)

User reiterated no local suites during quota-api A. No local test suite has been run;
reported regression results came from GitHub Actions. From this point no further local
typecheck/build/lint/scan commands either: command-based validation is remote CI only.
Source inspection and browser observation remain scoped QA; receipts may only wrap remote
CI result checks. This supersedes the earlier local-static/build verification allowance.

- Attribution cycle: Harvey owns `src/usage`, `src/router.ts`, the identified Chat/Messages error catches, `src/server/management/shared.ts`, and corresponding existing backend regression files. Main owns the provider model annotation/share/caching UI, its GUI regressions, docs, commits and CI. No overlapping writes.
- Quota API cycle: Euclid owns quota readers/key-cache/types and relevant provider regression files. Main owns management route joins, API-route regressions and docs; clarify exact exported signatures before either writes.
- Quota UI cycle: main owns report projection/current section; a bounded worker may own account hook/panel refresh and regression changes only after an explicit P amendment gives disjoint paths.
- Quota UI cycle: main owns shared `types.ts`, `report.ts`, NEW ProviderAccountQuota/ProviderCurrentQuota, ProviderDetails/Overview/Usage, all locale keys, public docs and direct shared-renderer regression. Harvey owns useProviderAccountPools, Providers page refresh coordinator, shell refresh callback epoch, ProviderAuthPanel and focused hook/refresh regressions. Worker may read shared contracts but never edit them. No overlapping writes or local validation.
- Independent Kant audits plans read-only; fresh independent final implementation review remains required. No worker changes FSM, goals, branches, commits or remote state, and no worker runs local tests.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Account quota API build record

Layer: `codex/provider-account-quota-api`, parent `9a9ad98b8` / PR #3582.
Main owns existing management route projection, route-level regression file and test-layout
registration. Euclid owns credential-scoped quota readers, key cache, shared fields and provider
regressions. No local test, typecheck, build, lint or scan commands run in this layer.

Route projection now advertises capability on cheap lists, enriches only opt-in supported
credentials, preserves active selection, clears prior failure flags on success, and checks
private identity guards immediately before serializing safe quota fields. Passive reads remain
cache-only. Internal callbacks and credential identities are never spread into response DTOs.

Kant's scoped route review found one missing activation test: replacing an environment key
inside fetch proves worker rejection but not the final route guard. Accepted and added a
completed non-null quota fixture with `isCurrent() === false`, one callback invocation and
null/unavailable JSON expectation. Same final-projection scenario added for OAuth rows.
Re-review of the key guard delta: PASS, static only. Full independent review and remote CI pending.

C inspection at `979249852` found a new-cache age gap: a failed read refreshes attempt TTL
but preserves the measurement timestamp; a cache hit checked only attempt TTL and could keep
the measurement past the stated last-good age. Accepted root cause; repair only the new key
and explicit-OAuth cache hit paths, with deterministic clock boundary regressions. Existing
three OAuth reader cache behavior stays outside this repair. No local execution authorized.

Remote CI run33939675289 at979249852 failed typecheck (`TS2322`, quota.ts1808): inferred
object return widened unique sentinel symbols to `symbol`. The valid translator-budget type
fixture failed downstream of that source compile error. Repair is an explicit return contract
for `readExplicitAccountQuota`, not changing or weakening the translator-budget regression.

Independent Wegener review confirmed those two blockers and refined the age case: a failure
which settles after the last-good deadline must recheck measurement age after its await,
not just on the next cache hit. Accepted; both new cache paths gain post-await age checks and
an independently reachable delayed-failure clock fixture before this repair is republished.

The same baseline run's test4/4 also exposed `Kimi quota skips usages when OAuth refresh fails`:
the new current-account dispatch returned an asynchronous reader without awaiting it inside its
catch boundary. Add the missing await so credential renewal rejection degrades to unavailable
as before; retain the existing regression unchanged. This is a separate runtime cause from
the type inference failure, not an assumed CI flake.

Codex GitHub review3939190788 identified the inherited explicit-key-only Kimi guard as
inconsistent with omitted authMode's documented key default. Accepted for this feature:
the shared key selector's initial normalized auth guard already rejects OAuth/forward/local,
so Kimi now uses that same default and its canonical URL check. Added an omitted-mode
per-key regression plus forward-mode negative. This supersedes 020's preserve-stricter-Kimi
note, without adding destinations or sending OAuth credentials down the key path.
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,48 @@ MODIFY `structure/05_gui-and-management-api.md` usage/quota paragraphs and `docs
## Bypass/residual

UI state is presentation, not credential authorization. Backend fixed-destination account/key readers are the actual boundary. Existing account mutation permissions are unchanged; no new enforcement claim.

## P stale-check and locked refinements at 768e5a004

The backend now emits the row-level mode/availability fields, including Kimi's omitted-key-auth
default. 000's no-local-validation directive supersedes all local command examples above.

1. Main owns a shared `AccountQuotaMode` and `AccountQuotaReading` in existing view types:
`quotaMode?: "probe"|"passive"|"unsupported"`, `quota?: AccountQuota|null`,
`quotaUnavailable?: boolean`, `quotaPending?: boolean`. OAuth/key rows and hook interfaces
reuse it. Pending is GUI-only. Worker reads this contract, does not edit types.ts.
2. Main owns NEW `ProviderAccountQuota.tsx`, shared by all three tabs. Props exactly match
AccountQuotaReading. Unsupported ignores retained data; passive without observation has an
honest unobserved message; only explicit probe pending can show a skeleton. Failed reads
retain last-good numbers with a warning and their actual age. Reuse ProviderCapacityQuota
for windows AND credits-only balances. No summation of different accounts/keys.
3. NEW ProviderCurrentQuota accepts `report`, optional `reading: AccountQuotaReading`, and
optional refresh callback. ProviderDetails supplies the active OAuth/key row only. A present
row with a known quotaMode (including passive with no observation), pending/unavailable,
or an explicit quota overrides the provider report;
never substitute the previous active account's quota. Codex uses report aggregation's current
account. Use the actual quota.updatedAt for current metadata, not aggregate refresh time.
4. `currentAccountQuotaReport` treats present malformed aggregation as unknown, not as a
non-pool current quota. Valid aggregate missing current likewise stays unknown. Tests pin
aggregate20/current70, absent current, malformed aggregation and distinct measurement age.
5. Preserve callback argument order: shell reports `onQuotaRefreshSettled(ok, epoch)` with
captured epoch second. Page resolves matching ticket only; superseded/unmounted tickets
resolve false. Existing one-argument callback consumers still receive a boolean.
6. The account loader merges last-good quota only by surviving credential id, publishes explicit
pending for probe rows, awaits forced enrichment, handles HTTP failure behind generation
fences, and uses a bounded fetch deadline rather than a never-ending spinner. Passive and
unsupported rows never get absence-driven loading. Key subset refresh merges other providers.
7. Refresh outcome text says the quota check completed, not that every upstream value is freshly
measured; passive reloads and a provider-report last-good fallback cannot prove freshness.

New localized keys owned by main: `pws.currentAccountUsage`, `pws.quotaUnsupported`,
`pws.quotaUnobserved`, `pws.quotaCheckCompleted`. Existing error/refresh labels are reused.
Worker's AuthPanel imports ProviderAccountQuota and uses the above keys; no locale edits there.

Source delta verification is remote CI plus existing in-app browser on synthetic data only.

A repair: Kant identified a passive account switch while the old provider report remains cached.
Accepted. Any active passive row is authoritative even with no quota: render unobserved and never
reuse the old report. The direct current-section regression must provide an old report75% plus a
new active passive row lacking quota and assert that75% is absent. Unknown-mode legacy rows may
only use a provider report when no row-specific state/data exists; absence is never confirmation.
44 changes: 44 additions & 0 deletions devlog/_plan/260905_provider_usage_quota_parity/031_ui_build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Unified quota presentation evidence

Base `768e5a004`. Main owns shared reading/current projection and three-tab integration;
Harvey owns account enrichment, refresh epochs and AuthPanel integration. No local test,
typecheck, build, lint or privacy-scan commands were executed in this layer. Remote CI owns
all command-based validation.

Implemented one state/credit/window renderer across Overview, Usage and Accounts/API keys.
Known-mode active rows are authoritative, including empty passive readings. Pool totals are
not current-account fallback. Explicit null from the API remains null during client merges;
only absent data can carry forward. Forced reads settle both account enrichment and the
matching provider-report epoch. Main-owned source slice reviewed PASS by Kant; fresh integrated
review remains in progress.

## Browser evidence

Existing isolated Vite server on127.0.0.1:18184; natural production components mounted from
`gui/.tmp/provider-parity.html` with synthetic responses only. No live tokens, accounts or
upstream requests. The temporary fixture is not a shipped page.

- 031_current_overview.png: current35%/61% below usage statistics.
- 032_current_usage.png: same current reading below provider model usage.
- 033_oauth_accounts.png: two account rows show61% and12% separately.
- 034_api_key_balances.png: two keys show75USD and25USD separately, never summed.
- 035_passive_current_unobserved.png: actually selected the unobserved second passive account;
the previous account's report remains in fixture state but its numbers are absent.
- 036_pool_current.png: aggregate20%, current70%; selected-provider current section shows70%.
- Unsupported configured provider: explicit unsupported message, no current refresh control,
no retained quota numbers.
- 037_tablet_usage.png: actual CSS viewport767px, document width767px; Korean layout observed.
- 039_mobile_current.png: actual CSS viewport390px, document width390px; current section and
refresh control stay inside the viewport; the model table keeps its local horizontal scroll.
Refresh was activated with pointer and Return, displaying check-completed feedback with
visible focus. Raw browser capture avoids the in-app screenshot wrapper's zoom clipping.

Desktop, tablet and mobile captures were read back. Temporary viewport overrides were reset.
The source fixture's active account switch and refresh are synthetic UI state transitions,
not writes to the user's account. Exact-head remote CI and final stack integration are pending.

Remote React Doctor atd78a02a63 reported test-harness render-time global assignments in two
new hook tests, one unused mock parameter, and cleanup-ref capture warnings in the account
loader. Its detail was read from the signed-in GitHub summary using Aside, not by running
the tool locally. Repair keeps assertions: capture test observations in layout effects,
remove the unused parameter and capture the stable cleanup containers inside the effect.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ commit `b37841448816107c856171277dff0464032d282e`, limited to the update-recover
and its numbered record. Retain it as a fourth test-only stack layer. No new production
behavior is planned. Any semantic conflict requires a concrete plan amendment and review.

The UI cascade has one textual conflict in `gui/src/pages/Providers.tsx`: retain this
unit's `useQuotaRefreshCoordinator(apiBase)` and upstream's `fetchConfig:
refreshConfigResult` binding together. Keep the upstream void-returning `fetchConfig`
adapter and result-aware `useProviderModelsNotice` caller unchanged. Existing quota epoch
and registration-notice tests must both execute in the new CI. This composes the two
existing contracts; it does not restore superseded unbounded refresh waiters.

Preserve all original commits; use normal merge commits and fast-forward no-verify pushes,
not rebases or force-pushes. Inspect each integration diff, check that inherited quota,
registration and pricing semantics survive, and obtain independent review before publication.
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions docs-site/src/content/docs/guides/web-dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ vendor's model price alone is not enough to estimate cost; an exact provider or
still applies. Model shares are calculated within the selected provider. Requests for an unknown
reserved `policy/` name now fail before reaching an upstream provider; historical usage is retained.

The selected provider's **Overview** and **Usage** tabs show **Current account usage** below the
usage statistics. **Accounts** and **API keys** show each supported credential's own quota,
including credit balances. The provider-wide overview still shows pooled capacity where available;
it is not substituted for a missing current-account reading. Unsupported lookup, no passive
observation yet, loading, failed lookup with last-known values, and measured zero are separate
states. **Quota check completed** means the read settled—not that a passive observation became
new or that every upstream measurement was refreshed.

## Model visibility

The **Models** switches show final Codex visibility: a routed model is on only when its provider allowlist includes it (or no allowlist is set) and it is not disabled. Turning a model on reconciles both filters atomically; **All on** clears the provider allowlist so newly discovered models are also on.
Expand Down Expand Up @@ -256,6 +264,7 @@ The GUI is a thin client over the proxy's JSON management API. Useful endpoints
| `GET /api/models` · `PUT /api/disabled-models` | List native/routed model rows and update the shared disabled-model set. |
| `GET /api/selected-models` · `PUT /api/model-visibility` | Read provider allowlists and atomically change the final visibility of one model or provider group. |
| `GET /api/key-providers` · `GET /api/oauth/providers` | Read the API-key and OAuth provider catalogs. |
| `GET /api/oauth/accounts?provider=...&quota=1` · `GET /api/providers/keys?name=...&quota=1` | Read each account or key's quota where supported, without changing the active credential. Add `refresh=1` to bypass settled quota cache; an in-flight same-credential read can be shared. Omit `quota=1` for a cheap local list with each row's `quotaMode`: `probe`, `passive`, or `unsupported`. Passive reads return existing observations without a network probe. No reading is not the same as 0% used, and quotas for multiple keys are not summed. |
| `POST /api/oauth/login` · `GET /api/oauth/status` | Start a provider OAuth flow and poll for completion. |
| `GET /api/codex-auth/accounts?refresh=1` | List main and pool accounts, force quota refresh, and report main-account `hasCredential` / terminal `needsReauth` state. |
| `PUT /api/codex-auth/active` · `PUT /api/codex-auth/auto-switch` · `PUT /api/codex-auth/failover` | Select the account for the next request and configure pool routing. |
Expand Down
5 changes: 5 additions & 0 deletions docs-site/src/content/docs/ko/guides/web-dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ bun run dev:gui
**Logs**와 **Usage**의 비용 값은 보고된 토큰으로 계산한 API 정가 환산치입니다. 결제 영수증이나
실제 청구 증거가 아니며, 구독 사용량 또는 프로바이더 크레딧이 대신 적용될 수 있습니다.

프로바이더를 선택하면 **개요**와 **사용량** 탭의 통계 아래에 **현재 계정 사용량**이 표시됩니다.
**계정**과 **API 키** 탭에서는 조회를 지원하는 계정·키마다 할당량과 크레딧 잔액을 볼 수 있습니다.
풀 전체 용량을 현재 계정 값 대신 보여주지 않으며, 조회 미지원·관측값 없음·조회 중·조회 실패·실제 0%를 구별합니다.
**할당량 확인 완료**는 조회가 끝났다는 뜻입니다. 패시브 관측값까지 새 값으로 바뀌었다는 뜻은 아닙니다.

## 모델 노출

**Models** 스위치는 Codex의 최종 노출 상태를 나타냅니다. 라우팅 모델은 프로바이더 allowlist에 포함되거나 allowlist가 없고, 동시에 비활성화되지 않았을 때만 켜집니다. 모델을 켜면 두 필터를 원자적으로 조정하며, **모두 활성화**는 allowlist를 해제해 새로 발견되는 모델도 켭니다.
Expand Down
30 changes: 30 additions & 0 deletions gui/src/components/provider-workspace/ProviderAccountQuota.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useT } from "../../i18n/shared";
import { accountQuotaFromReport } from "../../provider-workspace/report";
import { formatRelativeTime, relativeTimeLabelsFromT } from "../../provider-workspace/usage";
import { ProviderCapacityQuota } from "./ProviderCapacityQuota";
import type { AccountQuotaReading } from "./types";

/** The same reading states and credit/window renderer for current and all-account views. */
export default function ProviderAccountQuota({ quota: rawQuota, quotaMode, quotaUnavailable, quotaPending }: AccountQuotaReading) {
const t = useT();
const quota = accountQuotaFromReport({ quota: rawQuota });
if (quotaMode === "unsupported") {
return <p className="muted" data-quota-state="unsupported">{t("pws.quotaUnsupported")}</p>;
}
const pending = quotaMode === "probe" && quotaPending === true;
const state = quotaUnavailable ? "unavailable" : pending ? "pending" : quota ? "ready" : quotaMode === "passive" ? "unobserved" : "unknown";
return <div data-quota-state={state}>
{quotaUnavailable && <p className="muted pwi-auth-acct-quota-stale">{t("pws.accountQuotaUnavailable")}</p>}
{quota || pending ? (
<ProviderCapacityQuota
report={{ quota, updatedAt: quota?.updatedAt, ...(quotaMode === "passive" ? { observed: true } : {}) }}
pending={pending && !quota}
/>
) : !quotaUnavailable && (
<p className="muted">{t(quotaMode === "passive" ? "pws.quotaUnobserved" : "pws.quotaUnavailable")}</p>
)}
{quotaUnavailable && quota && <p className="muted">
{t("pws.stats.quotaUpdated")}: {formatRelativeTime(quota.updatedAt, relativeTimeLabelsFromT(t))}
</p>}
</div>;
}
Loading
Loading