refactor(codex): clarify rate-limit parsing and refresh package docs #7

Merged
eleith merged 1 commits from refactor/codex-rate-limit-readability into main 2026-11-02 15:13:14 +00:00
6 changed files with 106 additions and 33 deletions

No files matched your search

+1 -1
View File
@@ -5,7 +5,7 @@ a handful of small personal extensions to Pi, only using documented APIs.
| extension | description |
| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| [compact editor chrome](extensions/compact-editor-chrome/README.md) | minimal status details around the prompt input. less is more |
| [context codex](extensions/context-codex/README.md) | extend or restore local compaction windows for supported openai-codex models |
| [codex](extensions/codex/README.md) | local context windows, account usage, and fast-mode controls for Codex |
| [notifications](extensions/notifications/README.md) | notifications on turn end using notify-send (with OSC777 fallback for remote support) |
| [progress](extensions/progress/README.md) | OSC9;4 progress bars (with TMUX wrapping support) while waiting on a turn |
| [title status](extensions/title-status/README.md) | simple terminal titles with icon progress indicators |
+6
View File
@@ -1,5 +1,11 @@
# @eleith-pi/codex
## 0.2.1
### Patch Changes
- Separate Codex Retry-After parsing from HTTP error classification and strengthen rate-limit regression coverage. Correct Codex names, links, installation and command instructions in the READMEs.
## 0.2.0
### Minor Changes
+30 -16
View File
@@ -1,7 +1,7 @@
# context codex
# codex
extend or restore Pi's local compaction window for supported `openai-codex` models.
verified profiles currently cover GPT-6 Sol, GPT-6.1 Sol, and GPT-6 Astra;
context controls, account usage, and fast-mode requests using Pi's existing Codex login.
verified extended-context profiles currently cover GPT-6 Sol, GPT-6.1 Sol, and GPT-6 Astra;
unknown models are not guessed.
## why
@@ -12,34 +12,48 @@ compaction window, not server capacity or account limits. larger context may cos
## install
```sh
pi install npm:@eleith-pi/context-codex
pi install npm:@eleith-pi/codex
```
## use
- `/eleith:context-codex` shows context, compaction, and cache status.
- `/eleith:context-codex extend` selects the verified local window when larger.
- `/eleith:context-codex restore` returns to the catalog baseline, confirming reductions.
- `/eleith:codex` shows the local context and fast-mode dashboard, including compaction and cache status.
- `/eleith:codex context on` selects the verified local window when larger.
- `/eleith:codex context off` returns to the catalog baseline, confirming reductions.
- `/eleith:codex context toggle` switches the saved context preference.
- `/eleith:codex fast on` requests priority processing after confirmation of higher allowance use.
- `/eleith:codex fast off` requests standard processing.
- `/eleith:codex fast toggle` switches the saved fast preference.
- `/eleith:codex usage` fetches account allowance and reset information. in the terminal, the cancel key (Escape by default) aborts the request.
branch preferences persist across reloads. the catalog baseline is not guessed:
bare `context` and `fast` show the same dashboard as the root command, without account or catalog requests.
fast controls require a direct official legacy `openai-codex` selection with Pi OAuth and no provider override.
enabling priority also requires catalog eligibility.
priority is a request, not a scheduling guarantee; reported response tiers are shown separately.
context and fast preferences persist per model on the active branch across reloads. the catalog baseline is not guessed:
missing baselines are reported, and models already at or above the verified window
get a no-extension notice rather than a smaller window.
preference replay only runs while Pi is idle. after tree navigation it waits for
local context preference replay only runs while Pi is idle. after tree navigation it waits for
normal user input, before Pi checks compaction. custom-message-triggered turns use
the current window without replaying pending preferences; explicit extend/restore
commands also require idle.
the current window without replaying pending preferences; explicit context and fast
changes also require idle.
### migrate
run `/eleith:context restore` with the old extension loaded before removing it:
when migrating from `@eleith-pi/context-codex`, run its old `/eleith:context-codex restore`
command with that extension loaded before removing it:
```sh
pi remove npm:@eleith-pi/context-window
pi install npm:@eleith-pi/context-codex
pi remove npm:@eleith-pi/context-codex
pi install npm:@eleith-pi/codex
```
branch preferences are preserved. do not load both packages together.
if migrating from the older `@eleith-pi/context-window` instead, first use its
`/eleith:context restore` command, then remove that package before installing codex.
branch preferences are preserved. do not load the predecessor and codex together.
## config
@@ -49,4 +63,4 @@ set a command prefix in `~/.pi/agent/extensions/eleith.json`:
{ "commandPrefix": "personal" }
```
then use `/personal:context-codex`.
then use `/personal:codex`.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@eleith-pi/codex",
"version": "0.2.0",
"version": "0.2.1",
"private": false,
"description": "Context windows, account usage, and fast-mode controls for Codex in Pi",
"keywords": [
+53 -7
View File
@@ -323,17 +323,63 @@ it.each([401, 403])("reports HTTP %s without exposing the server's error body",
expect(cancel).toHaveBeenCalledOnce();
});
it("returns retry-after information without automatically retrying", async () => {
const f = fixture();
f.fetcher.mockResolvedValue(
new Response("private error", { status: 429, headers: { "Retry-After": "60" } }),
);
await expect(fetchCodexUsage(f.registry, undefined, f.fetcher)).rejects.toMatchObject({
it.each([
{ label: "seconds", header: "60", retryAfterMs: 60_000 },
{ label: "zero seconds", header: "0", retryAfterMs: 0 },
{ label: "fractional seconds", header: "1.5", retryAfterMs: 1500 },
{ label: "numeric exponent notation", header: "1e1", retryAfterMs: 10_000 },
{ label: "a future HTTP date", header: "Wed, 02 Jan 2030 03:05:05 GMT", retryAfterMs: 60_000 },
{ label: "the current HTTP date", header: "Wed, 02 Jan 2030 03:04:05 GMT", retryAfterMs: 0 },
{ label: "a past HTTP date", header: "Wed, 02 Jan 2030 03:03:05 GMT", retryAfterMs: undefined },
{ label: "a malformed value", header: "later", retryAfterMs: undefined },
{ label: "negative seconds", header: "-1", retryAfterMs: undefined },
{ label: "a non-finite value", header: "Infinity", retryAfterMs: undefined },
{ label: "an empty header", header: "", retryAfterMs: undefined },
{ label: "a missing header", header: null, retryAfterMs: undefined },
])(
"reports Retry-After with $label without automatically retrying",
async ({ header, retryAfterMs }) => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2030-01-02T03:04:05Z"));
const account = fixture();
const headers = new Headers();
if (header !== null) headers.set("Retry-After", header);
const response = new Response("private error", { status: 429, headers });
const discard = vi.spyOn(response.body!, "cancel");
account.fetcher.mockResolvedValue(response);
await expect(
fetchCodexUsage(account.registry, undefined, account.fetcher),
).rejects.toMatchObject({
code: "http",
message: "Codex account service is rate limited.",
retryAfterMs,
});
expect(account.fetcher).toHaveBeenCalledOnce();
expect(discard).toHaveBeenCalledOnce();
},
);
it("returns the catalog's HTTP-date retry delay without retrying or exposing its body", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2030-01-02T03:04:05Z"));
const account = fixture();
const response = new Response("private catalog error", {
status: 429,
headers: { "Retry-After": "Wed, 02 Jan 2030 03:05:05 GMT" },
});
const discard = vi.spyOn(response.body!, "cancel");
account.fetcher.mockResolvedValue(response);
await expect(
fetchCodexModelCatalog(account.registry, undefined, account.fetcher),
).rejects.toMatchObject({
code: "http",
message: "Codex account service is rate limited.",
retryAfterMs: 60_000,
});
expect(f.fetcher).toHaveBeenCalledOnce();
expect(account.fetcher).toHaveBeenCalledOnce();
expect(discard).toHaveBeenCalledOnce();
});
it("does not follow or display a redirected endpoint", async () => {
+15 -8
View File
@@ -226,18 +226,25 @@ async function requestAccountJson(
function responseError(response: Response): CodexAccountError {
if (response.status === 401 || response.status === 403) return authError();
if (response.status === 429) {
const header = response.headers.get("retry-after");
let retryAfterMs: number | undefined;
if (header) {
const seconds = Number(header);
const delay = Number.isFinite(seconds) ? seconds * 1000 : Date.parse(header) - Date.now();
if (Number.isFinite(delay) && delay >= 0) retryAfterMs = delay;
}
return new CodexAccountError("http", "Codex account service is rate limited.", retryAfterMs);
return new CodexAccountError(
"http",
"Codex account service is rate limited.",
parseRetryAfterMs(response.headers.get("retry-after")),
);
}
return new CodexAccountError("http", `Codex account request failed (HTTP ${response.status}).`);
}
function parseRetryAfterMs(header: string | null): number | undefined {
if (!header) return undefined;
const seconds = Number(header);
let delayMs: number;
if (Number.isFinite(seconds)) delayMs = seconds * 1000;
else delayMs = Date.parse(header) - Date.now();
if (!Number.isFinite(delayMs) || delayMs < 0) return undefined;
return delayMs;
}
function discardBody(response: Response): void {
void response.body?.cancel().catch(() => {});
}