Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 1 addition & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,11 @@ quota** and **per-session token usage** — without spending a model turn.
quota bars (5h and weekly windows) and per-model token usage, updating in
real time as tokens stream.

Available manual usage limit resets appear below the quota bars, with
the limits they restore and expiration dates in your local time zone.
Reset information refreshes with quota data (every 90 seconds by default).

```text
┌─ Codex Meter ────────────────────────────────┐
│ 5h quota [████████░░░░░░░░░░░░] 37% │
│ Weekly quota [████████████░░░░░░░░] 62% │
│ │
│ Usage limit resets │
│ 2 available │
│ │
│ Full reset (Weekly + 5 hr) │
│ Expires Oct 4, 8:37 AM │
│ │
│ Full reset (Weekly + 5 hr) │
│ Expires Oct 5, 7:21 AM │
│ │
│ openai/gpt-5.5 (5 msgs) │
│ Input 184,230 │
│ Output 8,491 │
Expand All @@ -50,10 +37,6 @@ quota** and **per-session token usage** — without spending a model turn.

Quota data comes from the ChatGPT backend and may be unavailable (the plugin
keeps working with token totals only). Token totals are always available.
If reset details are unavailable, the plugin still shows the known count.
An unavailable count is shown as unavailable, never as zero. Cached reset
data is labeled stale after a failed quota refresh. The sidebar only displays
resets; use the Codex usage page to redeem one.

## Install

Expand Down Expand Up @@ -136,9 +119,7 @@ All settings are optional environment variables:
`~/.local/share/opencode/auth.json`.
- Never reads, stores, or logs the `refresh` token.
- Never writes to `auth.json` or refreshes OAuth credentials.
- Makes read-only requests to `https://chatgpt.com/backend-api/wham/usage`
and, when resets are available, `/backend-api/wham/rate-limit-reset-credits`
on the same host. Never redeems a reset.
- The only network destination is `https://chatgpt.com/backend-api/wham/usage`.

See [SECURITY.md](./SECURITY.md) for the full security policy.

Expand Down
18 changes: 4 additions & 14 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,6 @@ protects it.
The response contains usage percentages and reset times, not
credentials.

- **Manual reset details** — when usage reports available resets, the plugin
reads `https://chatgpt.com/backend-api/wham/rate-limit-reset-credits`.
It retains only the available count and display details (reset type,
sanitized title, expiration) for available, unexpired, plan-supported resets.
Reset IDs, profile fields, and redemption history are discarded.

### What the plugin never does

- **Never writes to `auth.json`** — the plugin has no auth-write
Expand All @@ -42,10 +36,8 @@ protects it.
- **Never sends telemetry** — no analytics, no usage reporting, no
phone-home.
- **Never makes unexpected network requests** — the only network
destinations are `https://chatgpt.com/backend-api/wham/usage` and
`https://chatgpt.com/backend-api/wham/rate-limit-reset-credits`, and only
when credentials are available. Both use GET; the plugin never redeems
or purchases resets.
destination is `https://chatgpt.com/backend-api/wham/usage`, and only
when credentials are available.
- **Never executes install-time code** — the package has no
`postinstall`, `preinstall`, or other lifecycle scripts.

Expand All @@ -64,17 +56,15 @@ The `src/redact.ts` module provides:

### Unsupported endpoint risk

The ChatGPT backend usage and reset-details endpoints are
**undocumented and unsupported** by OpenAI. They may change shape, move,
The `https://chatgpt.com/backend-api/wham/usage` endpoint is
**undocumented and unsupported** by OpenAI. It may change shape, move,
or disappear without notice. The plugin:

- Validates the response at runtime with a tolerant Zod schema.
- Identifies windows by duration (not response position).
- Preserves unknown windows rather than discarding them.
- Treats any failure (401/403/429/5xx/timeout/malformed) as
non-fatal — session token reporting continues independently.
- Keeps quota and the known reset count if the optional reset-details
request fails, times out, or changes schema.
- Does not cache `unauthenticated` for the full TTL (uses a shorter
30-second negative cache).

Expand Down
1 change: 0 additions & 1 deletion src/quota/cached-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ export class CachedProvider implements QuotaProvider {
weekly: null,
unknownWindows: [],
credits: null,
resetCredits: null,
warningCode: "UNAVAILABLE",
};
}
Expand Down
51 changes: 0 additions & 51 deletions src/quota/reset-credits.ts

This file was deleted.

9 changes: 2 additions & 7 deletions src/quota/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
*/

import { z } from "zod";
import { parseResetCreditsSummary } from "./reset-credits";
import type { CreditsInfo, ResetCreditsInfo, UsageWindow } from "./types";
import type { CreditsInfo, UsageWindow } from "./types";
import { identifyWindow } from "./types";

/**
Expand Down Expand Up @@ -97,7 +96,6 @@ const WhamResponseSchema = z
credits: CreditsSchema.optional(),
extra_usage: CreditsSchema.optional(),
extraUsage: CreditsSchema.optional(),
rate_limit_reset_credits: z.unknown().optional(),
})
.passthrough();

Expand All @@ -109,7 +107,6 @@ export interface WhamParseResult {
readonly windows: UsageWindow[];
readonly planType: string | null;
readonly credits: CreditsInfo | null;
readonly resetCredits: ResetCreditsInfo | null;
}

/**
Expand Down Expand Up @@ -190,14 +187,13 @@ export function parseWhamResponse(raw: unknown): WhamParseResult {
windows: arrayResult,
planType: null,
credits: null,
resetCredits: null,
};
}

// Otherwise parse as an object.
const parsed = WhamResponseSchema.safeParse(raw);
if (!parsed.success) {
return { ok: false, windows: [], planType: null, credits: null, resetCredits: null };
return { ok: false, windows: [], planType: null, credits: null };
}

const data = parsed.data;
Expand Down Expand Up @@ -243,7 +239,6 @@ export function parseWhamResponse(raw: unknown): WhamParseResult {
windows,
planType: normalizedPlanType,
credits,
resetCredits: parseResetCreditsSummary(data.rate_limit_reset_credits),
};
}

Expand Down
16 changes: 0 additions & 16 deletions src/quota/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,6 @@ export interface CreditsInfo {
readonly balance: string | null;
}

/** Display-only details for an available manual usage reset. */
export interface ResetCredit {
readonly resetType: string;
readonly title: string | null;
readonly expiresAt: string | null;
}

export interface ResetCreditsInfo {
/** Authoritative server count; the detail list may be incomplete. */
readonly availableCount: number;
/** null means details could not be fetched or validated. */
readonly credits: ResetCredit[] | null;
}

/**
* A complete quota snapshot. This is the normalized representation
* that all report formatters consume.
Expand All @@ -67,7 +53,6 @@ export interface QuotaSnapshot {
readonly weekly: UsageWindow | null;
readonly unknownWindows: UsageWindow[];
readonly credits: CreditsInfo | null;
readonly resetCredits: ResetCreditsInfo | null;
readonly warningCode: string | null;
}

Expand All @@ -88,7 +73,6 @@ export function noQuotaSnapshot(
weekly: null,
unknownWindows: [],
credits: null,
resetCredits: null,
warningCode,
};
}
Expand Down
33 changes: 1 addition & 32 deletions src/quota/wham-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
* ChatGPT backend endpoint.
*
* URL: GET https://chatgpt.com/backend-api/wham/usage
* Reset details: GET https://chatgpt.com/backend-api/wham/rate-limit-reset-credits
* Headers:
* Authorization: Bearer <access-token>
* ChatGPT-Account-Id: <account-id> (only if accountId is present)
Expand All @@ -24,21 +23,18 @@
*/

import type { Credentials } from "./auth-reader";
import { parseResetCreditsDetails } from "./reset-credits";
import { parseWhamResponse } from "./schemas";
import type {
Clock,
HttpResponse,
HttpTransport,
QuotaProvider,
QuotaSnapshot,
ResetCreditsInfo,
UsageWindow,
} from "./types";
import { WarningCode, noQuotaSnapshot } from "./types";

const WHAM_URL = "https://chatgpt.com/backend-api/wham/usage";
const RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";

/**
* Configuration for the wham provider.
Expand Down Expand Up @@ -69,7 +65,6 @@ function buildSnapshot(
windows: UsageWindow[],
planType: string | null,
credits: QuotaSnapshot["credits"],
resetCredits: ResetCreditsInfo | null,
fetchedAt: string,
): QuotaSnapshot {
const fiveHour = windows.find((w) => w.kind === "five-hour") ?? null;
Expand All @@ -85,7 +80,6 @@ function buildSnapshot(
weekly,
unknownWindows,
credits,
resetCredits,
warningCode: null,
};
}
Expand Down Expand Up @@ -188,31 +182,6 @@ export class WhamProvider implements QuotaProvider {

// Build the snapshot.
const fetchedAt = new Date(this.deps.clock.now()).toISOString();
let resetCredits = parsed.resetCredits;
if (resetCredits !== null && resetCredits.availableCount > 0) {
// A failed detail lookup must not discard quota or the known reset count.
resetCredits = (await this.fetchResetCredits(headers)) ?? resetCredits;
}
return buildSnapshot(parsed.windows, parsed.planType, parsed.credits, resetCredits, fetchedAt);
}

private async fetchResetCredits(
headers: Record<string, string>,
): Promise<ResetCreditsInfo | null> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.deps.config.timeoutMs);
try {
const response = await this.deps.transport.fetch(RESET_CREDITS_URL, {
method: "GET",
headers,
signal: controller.signal,
});
if (!response.ok) return null;
return parseResetCreditsDetails(await response.json(), this.deps.clock.now());
} catch {
return null;
} finally {
clearTimeout(timeoutId);
}
return buildSnapshot(parsed.windows, parsed.planType, parsed.credits, fetchedAt);
}
}
6 changes: 0 additions & 6 deletions src/report/detailed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
*/

import type { Report, ReportModel } from "./build";
import { formatResetCredits } from "./reset-credits";

/**
* Format a number with comma-separated thousands.
Expand Down Expand Up @@ -103,11 +102,6 @@ function formatQuotaSection(report: Report): string {
lines.push(` Unknown (${w.windowSeconds}s): ${pct}% used · resets in ${reset}`);
}

lines.push("", "Usage limit resets");
for (const line of formatResetCredits(q.resetCredits, q.status === "stale")) {
lines.push(line ? ` ${line}` : "");
}

return `${lines.join("\n")}\n`;
}

Expand Down
3 changes: 0 additions & 3 deletions src/report/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
* provider; the session usage contains only token counts.
*/

import type { ResetCreditsInfo } from "../quota/types";
import type { Report } from "./build";

/**
Expand Down Expand Up @@ -40,7 +39,6 @@ export interface JsonReport {
readonly weekly: object | null;
readonly unknownWindows: object[];
readonly credits: object | null;
readonly resetCredits: ResetCreditsInfo | null;
readonly warningCode: string | null;
} | null;
readonly isWarning: boolean;
Expand Down Expand Up @@ -76,7 +74,6 @@ export function toJsonReport(report: Report): JsonReport {
weekly: report.quota.weekly,
unknownWindows: report.quota.unknownWindows,
credits: report.quota.credits,
resetCredits: report.quota.resetCredits,
warningCode: report.quota.warningCode,
}
: null,
Expand Down
Loading
Loading