Skip to content

Expose Codex subscription usage limits over the fold-codex transport - #46

Open
PepijnSenders wants to merge 2 commits into
humanlayer:mainfrom
PepijnSenders:expose-codex-usage-limits
Open

PepijnSenders wants to merge 2 commits into
humanlayer:mainfrom
PepijnSenders:expose-codex-usage-limits

Conversation

@PepijnSenders

@PepijnSenders PepijnSenders commented Sep 23, 2026 •

Copy link
Copy Markdown

Downstream consumer: humanlayer/synclayer#2663

Why the change

HumanLayer needs to show Codex subscription usage-limit alerts the way it now does for Claude, but Fold exposed no usage-limit data, so this reads what the ChatGPT backend already returns and surfaces it in a provider-neutral shape.

Special things to note

  • the data was already arriving and being dropped: the codex backend attaches usage state as response headers (x-codex-<primary|secondary>-used-percent / -window-minutes / -reset-at, plus x-codex-credits-*) on every reply, verified against a real 429 on this machine, so no new client, endpoint, or websocket was needed
  • surfaced through a caller-supplied onUsageLimits callback, not a durable EventLog entry: the model descriptor produces a LanguageModel.Service with no EventLog access and the existing onStreamRetry seam is the only precedent; a durable entry would need cross-cutting runtime plumbing and is left as a follow-up
  • reads the default codex limit family only (not per-model rateLimitsByLimitId) and fires on successful acquisition only; the 429 path keeps its existing retry-after handling

Change outline

A provider-neutral, best-effort snapshot type in fold-core, modeled on the existing Usage.

// packages/fold-core/src/EventLog/UsageLimits.ts
UsageLimits = {
  limitId?: string
  limitName?: string
  windows: Record<string, {           // keyed by a neutral id (primary/secondary)
    usedPercent?: number              // 0-100
    windowMinutes?: number            // window length; read this, not the key
    resetsAt?: number                 // epoch ms
  }>
  credits?: { hasCredits?: boolean; unlimited?: boolean; balance?: string }
}
decodeUsageLimits = Schema.decodeUnknownOption(UsageLimits)   // None when it does not conform

The codex client already returns the response at acquisition, so headers are read at that seam and handed to the callback.

 decorateCodexClient (packages/fold-codex/src/CodexModel.ts)
   acquire response over SSE (chatgpt backend /codex/responses)
+    usageLimitsFromCodexHeaders(response.headers)   // seconds -> ms, drop blank/out-of-range
+    onUsageLimits?(limits)                           // new CodexModelOptions callback, mirrors onStreamRetry

Files by responsibility.

 packages/fold-core/src/
+  EventLog/UsageLimits.ts        # provider-neutral UsageLimits schema + decodeUsageLimits
   index.ts                       # export it
 packages/fold-codex/src/
+  RateLimitHeaders.ts            # parse the codex header family into UsageLimits
   CodexModel.ts                  # read response.headers, invoke onUsageLimits
   index.ts                       # export the header parser

Tested: RateLimitHeaders.vi.test.ts (6 cases: primary+secondary parse, seconds-to-ms, partial windows, credits + limit name, no-headers None, unparsable/out-of-range dropped) and UsageLimits.vi.test.ts (4 cases: sparse windows, empty windows, percent bound, non-positive duration). Typecheck, oxlint, and oxfmt clean; the two failing CodexLive tests are pre-existing live-network cases that 429 because this account's limit is reached.


HumanLayer session

Add a provider-neutral usage-limits snapshot to fold-core and populate it
from the Codex rate-limit response headers the ChatGPT backend attaches to
every /responses reply.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
HumanLayer-Session: https://app.humanlayer.com/sessions/01a0bf0d-c4bd-787c-b350-a30c562a74d6
@CLAassistant

CLAassistant commented Sep 23, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Match the comment density of the sibling Usage.ts: the window and snapshot
blocks read as one line each, since the names already carry the intent.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
HumanLayer-Session: https://app.humanlayer.com/sessions/01a0bf0d-c4bd-787c-b350-a30c562a74d6
@PepijnSenders
PepijnSenders marked this pull request as ready for review September 27, 2026 14:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants