refactor(codex): clarify rate-limit parsing and refresh package docs #7
No files matched your search
@@ -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 |
|
||||
|
||||
@@ -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
@@ -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,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": [
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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(() => {});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user