Skip to content

Codex quota UI displays remaining percentage as used percentage #262

@myagizmaktav

Description

@myagizmaktav

Summary

The management panel's Codex quota card appears to display ChatGPT/Codex used_percent backwards. The upstream Codex quota endpoint returns usage as percent used, but the UI transforms it into percent remaining (100 - used_percent) and then displays that value without saying it is remaining quota. This makes a mostly-empty quota look nearly full.

Example observed

A direct Codex usage check returned this payload for one credential:

{
  "email": "foxsnow61@gmail.com",
  "plan_type": "team",
  "rate_limit": {
    "allowed": true,
    "limit_reached": false,
    "primary_window": {
      "used_percent": 3,
      "limit_window_seconds": 18000,
      "reset_after_seconds": 9132,
      "reset_at": 1778075834
    },
    "secondary_window": {
      "used_percent": 0,
      "limit_window_seconds": 604800,
      "reset_after_seconds": 595932,
      "reset_at": 1778662634
    }
  }
}

Expected UI:

  • 5-hour limit: 3% used
  • Weekly limit: 0% used
  • Progress bar nearly empty/low

Actual UI before local patch:

  • 5-hour limit displayed around 97%
  • Weekly limit displayed around 100%
  • Progress bar looked full/high

This is confusing because the credential looks exhausted/full even though the upstream response says it is mostly unused.

Related CLIProxyAPI context

While testing router-for-me/CLIProxyAPI#3240, the backend management API correctly exposed runtime state such as quota and model_states. The incorrect display is in the management panel interpretation of the Codex quota endpoint response.

The management API response itself did not contain quota usage windows; the panel fetched Codex usage through management api-call and received the correct used_percent values.

Root cause

The Codex quota response field is named used_percent, and it already represents usage consumed.

The UI currently behaves like this:

const used = window.usedPercent
const clampedUsed = used === null ? null : Math.max(0, Math.min(100, used))
const remaining = clampedUsed === null ? null : Math.max(0, Math.min(100, 100 - clampedUsed))
const label = remaining === null ? '--' : `${Math.round(remaining)}%`

return (
  <QuotaProgressBar
    percent={remaining}
    highThreshold={70}
    mediumThreshold={30}
  />
)

That is only correct if the UI is explicitly showing remaining quota. But the UI reads like usage and color semantics imply that higher means more consumed. For Codex, it makes 3% used appear as 97%.

Suggested fix option A: display used percentage directly

This is the clearest behavior for fields named used_percent:

type QuotaWindow = {
  id: string
  label: string
  labelKey?: string
  labelParams?: Record<string, unknown>
  usedPercent: number | null
  resetLabel?: string
}

function clampPercent(value: number | null): number | null {
  if (value === null || !Number.isFinite(value)) return null
  return Math.max(0, Math.min(100, value))
}

function renderCodexQuotaWindow(
  window: QuotaWindow,
  t: (key: string, params?: Record<string, unknown>) => string,
  styles: Record<string, string>,
  QuotaProgressBar: React.ComponentType<{
    percent: number | null
    highThreshold: number
    mediumThreshold: number
  }>,
) {
  const used = clampPercent(window.usedPercent)
  const displayPercent = used === null ? '--' : `${Math.round(used)}% used`
  const label = window.labelKey ? t(window.labelKey, window.labelParams) : window.label

  return (
    <div key={window.id} className={styles.quotaRow}>
      <div className={styles.quotaRowHeader}>
        <span className={styles.quotaModel}>{label}</span>
        <div className={styles.quotaMeta}>
          <span className={styles.quotaPercent}>{displayPercent}</span>
          <span className={styles.quotaReset}>{window.resetLabel}</span>
        </div>
      </div>
      <QuotaProgressBar
        percent={used}
        highThreshold={70}
        mediumThreshold={30}
      />
    </div>
  )
}

Suggested fix option B: keep remaining percent, but label it clearly

If the intended UI is to show remaining quota, then labels and colors should make that clear:

const used = clampPercent(window.usedPercent)
const remaining = used === null ? null : 100 - used
const displayPercent = remaining === null ? '--' : `${Math.round(remaining)}% remaining`

<QuotaProgressBar
  percent={remaining}
  highThreshold={70}
  mediumThreshold={30}
/>

In this mode, high percent means high remaining capacity, so the color thresholds should be reviewed to avoid implying danger when the account is actually healthy.

Local workaround tested

I patched the built panel asset locally by replacing the Codex quota render calculation from:

remaining = used === null ? null : Math.max(0, Math.min(100, 100 - used))
label = remaining === null ? `--` : `${Math.round(remaining)}%`

to:

remaining = used
label = used === null ? `--` : `${Math.round(used)}% used`

After rebuilding and serving the patched panel:

  • The served management.html contains % used labels.
  • The old 100 - used_percent display path is gone.
  • The same upstream response now displays low usage as low usage.

Acceptance criteria

  • Given primary_window.used_percent = 3, the UI shows 3% used or clearly shows 97% remaining.
  • Given secondary_window.used_percent = 0, the UI shows 0% used or clearly shows 100% remaining.
  • The progress bar color/threshold semantics match the label semantics.
  • Codex quota cards no longer visually imply that a mostly unused account is exhausted/full.

Why this matters

Operators may disable or rotate away from healthy credentials because the dashboard indicates they are full. This is especially confusing when paired with CLIProxyAPI runtime quota state because the backend may be accurate while the frontend still renders the external Codex quota backwards.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions