@llblab/pi-kit 0.38.2 → 0.38.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +3 -3
- package/node_modules/@llblab/pi-codex-usage/AGENTS.md +2 -2
- package/node_modules/@llblab/pi-codex-usage/CHANGELOG.md +6 -0
- package/node_modules/@llblab/pi-codex-usage/README.md +9 -5
- package/node_modules/@llblab/pi-codex-usage/lib/query.ts +229 -50
- package/node_modules/@llblab/pi-codex-usage/lib/usage.ts +9 -1
- package/node_modules/@llblab/pi-codex-usage/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +11 -673
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +18 -0
- package/node_modules/@llblab/pi-state-flow/README.md +8 -8
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +3 -6
- package/node_modules/@llblab/pi-state-flow/dist/lib/model-interface.js +2 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/semantic-module.d.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +2 -6
- package/node_modules/@llblab/pi-state-flow/dist/package.json +2 -1
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +5 -5
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +1 -1
- package/node_modules/@llblab/pi-state-flow/docs/README.md +7 -7
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +11 -15
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +12 -10
- package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +18 -16
- package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +13 -15
- package/node_modules/@llblab/pi-state-flow/docs/protocol-composition-decision.md +2 -2
- package/node_modules/@llblab/pi-state-flow/docs/public-module-contract.md +16 -6
- package/node_modules/@llblab/pi-state-flow/docs/scope-selection-contract.md +25 -23
- package/node_modules/@llblab/pi-state-flow/docs/semantic-compatibility-decision.md +14 -12
- package/node_modules/@llblab/pi-state-flow/docs/semantic-extensibility-decision.md +2 -2
- package/node_modules/@llblab/pi-state-flow/docs/semantic-handoff-contract.md +133 -15
- package/node_modules/@llblab/pi-state-flow/docs/semantic-memory-evaluation.md +5 -5
- package/node_modules/@llblab/pi-state-flow/docs/semantic-module-contract.md +5 -5
- package/node_modules/@llblab/pi-state-flow/docs/single-scope-interface-contract.md +2 -2
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +33 -23
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +3 -7
- package/node_modules/@llblab/pi-state-flow/lib/model-interface.ts +2 -3
- package/node_modules/@llblab/pi-state-flow/lib/semantic-module.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +2 -6
- package/node_modules/@llblab/pi-state-flow/package.json +2 -1
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +5 -5
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@llblab/pi-kit` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.38.4: Bundled Package Updates (2026-11-08)
|
|
6
|
+
|
|
7
|
+
- `Exact pins`: `@llblab/pi-codex-usage` 0.13.2 → 0.13.4.
|
|
8
|
+
- `Packaging`: Synchronizes the lockfile, bundle aliases and README inventory. Package membership and resource order remain unchanged; packed validation does not certify extension safety or installed-client behavior.
|
|
9
|
+
|
|
10
|
+
## 0.38.3: Bundled Package Updates (2026-11-01)
|
|
11
|
+
|
|
12
|
+
- `Exact pins`: `@llblab/pi-state-flow` 0.27.1 → 0.27.4.
|
|
13
|
+
- `Packaging`: Synchronizes the lockfile, bundle aliases and README inventory. Package membership and resource order remain unchanged; packed validation does not certify extension safety or installed-client behavior.
|
|
14
|
+
|
|
5
15
|
## 0.38.2: Bundled Package Updates (2026-10-30)
|
|
6
16
|
|
|
7
17
|
- `Exact pins`: `@llblab/pi-claude-usage` 0.2.4 → 0.2.5.
|
package/README.md
CHANGED
|
@@ -15,9 +15,9 @@ Package links lead to the owning repositories for usage, documentation, issues,
|
|
|
15
15
|
| [`@llblab/pi-actors`](https://github.com/llblab/pi-actors) | `0.55.0` | Inspectable local Runs, reusable Recipes, persistent tools, and delegation Skills |
|
|
16
16
|
| [`@llblab/pi-claude-usage`](https://github.com/llblab/pi-claude-usage) | `0.2.5` | Claude subscription quota status and shared per-model Fast toggle for Opus, mirrored in Telegram |
|
|
17
17
|
| [`@llblab/pi-clean-room`](https://github.com/llblab/pi-clean-room) | `0.3.1` | Isolated nested Pi TUI with named npm extensions and compatible model selection |
|
|
18
|
-
| [`@llblab/pi-codex-usage`](https://github.com/llblab/pi-codex-usage) | `0.13.
|
|
18
|
+
| [`@llblab/pi-codex-usage`](https://github.com/llblab/pi-codex-usage) | `0.13.4` | OpenAI/legacy Codex subscription quota and Business credit status, with persistent priority Fast toggle mirrored in Telegram |
|
|
19
19
|
| [`@llblab/pi-grow-loop`](https://github.com/llblab/pi-grow-loop) | `0.9.0` | Visible continuation scheduling and bounded worker Skills through compiled, manifest-owned resources |
|
|
20
|
-
| [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.27.
|
|
20
|
+
| [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.27.4` | Scoped context/memory compiler with step-independent current memory, stable Passive/Active modes and visible blocked-state status |
|
|
21
21
|
| [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.58.3` | Telegram companion with per-prompt Steer, queued controls, Reroute/Restore, recoverable connections, previews, follower Threads, files, and voice |
|
|
22
22
|
| [`@llblab/skills`](https://github.com/llblab/skills) | `1.15.0` | Portable workflows for engineering, review, design, context maintenance, and other focused tasks |
|
|
23
23
|
|
|
@@ -25,7 +25,7 @@ Versions are exact by design. An upstream release does not change an installed k
|
|
|
25
25
|
|
|
26
26
|
## Install
|
|
27
27
|
|
|
28
|
-
Requires **Pi 1.0.0+** and **Node.js 22.19.0+**. State Flow requires its canonical checkpoint/tail storage format and does not convert unsupported stores in place. Preserve existing stores and consult the [owning package's storage guidance](https://github.com/llblab/pi-state-flow/blob/v0.27.
|
|
28
|
+
Requires **Pi 1.0.0+** and **Node.js 22.19.0+**. State Flow requires its canonical checkpoint/tail storage format and does not convert unsupported stores in place. Preserve existing stores and consult the [owning package's storage guidance](https://github.com/llblab/pi-state-flow/blob/v0.27.4/docs/usage.md#moving-a-store-and-supported-formats) before changing installations.
|
|
29
29
|
|
|
30
30
|
From NPM:
|
|
31
31
|
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
- `Registry dependency`: Use published `@llblab/pi-command-fast@^0.1.0` with aligned registry lock metadata. Local folder links are only for explicitly requested development tests; rebuild the library's `dist/` after source edits and restore registry dependency/locks before consumer publication.
|
|
4
4
|
- `Pi baseline`: Every declared `@earendil-works/*` peer requires ≥1.0.0. Keep Pi peer lock identities aligned and validate against that host generation.
|
|
5
|
-
- `Domain DAG`: `index.ts` only re-exports the extension and public contracts; `lib/extension.ts` composes the Pi extension. `lib/usage-store.ts` owns persistence/leadership, `lib/query.ts` owns quota I/O, `lib/usage.ts` owns normalized reports, `lib/status.ts` and `lib/status-format.ts` own local UI orchestration and formatting, `lib/telegram.ts` owns optional registration, and `lib/fast.ts` owns Codex Fast semantics/request adaptation. `@llblab/pi-command-fast` owns command arbitration and generic JSONC override editing. Keep dependency direction acyclic; do not import `index.ts` from `lib/` or mix Fast persistence into quota coordination. Place domain tests in `tests/<domain>.test.ts` (or a domain-prefixed focused integration test); use independent names such as `invariants.test.ts` only for cross-domain invariants.
|
|
5
|
+
- `Domain DAG`: `index.ts` only re-exports the extension and public contracts; `lib/extension.ts` composes the Pi extension. `lib/usage-store.ts` owns persistence/leadership, `lib/query.ts` owns auth candidate discovery and quota I/O, `lib/usage.ts` owns normalized reports, `lib/status.ts` and `lib/status-format.ts` own local UI orchestration and formatting, `lib/telegram.ts` owns optional registration, and `lib/fast.ts` owns Codex Fast semantics/request adaptation. `@llblab/pi-command-fast` owns command arbitration and generic JSONC override editing. Keep dependency direction acyclic; do not import `index.ts` from `lib/` or mix Fast persistence into quota coordination. Place domain tests in `tests/<domain>.test.ts` (or a domain-prefixed focused integration test); use independent names such as `invariants.test.ts` only for cross-domain invariants.
|
|
6
6
|
- `Statusline-first scope`: Own usage state + usage mode: keep quota reporting zero-configuration and the optional Fast toggle focused on the existing terminal status.
|
|
7
7
|
- Trigger: Considering new commands, menus, persisted settings, or notification output.
|
|
8
8
|
- Action: Prefer deleting the surface unless it is required for the optimistic TUI status widget, the optional `pi-telegram` `/start` status-line mirror, or the existing argument-free `/fast` command. Register both `openai-codex` and `openai` provider handlers through `registerFastProvider` on session_start and release both on session_shutdown; never register `/fast` directly or filter Fast by model ID/auth. Fast persists solely as `serviceTier: "priority"` in the current provider/model override (never share preferences across providers with the same model ID); OFF deletes only that property. Preserve JSONC/unrelated settings, existing wire tiers, and usage refresh/Telegram. Apply lowercase ` fast` through the final terminal boundary; the optional Telegram row reads the active model's Fast preference at render time and appends plain-text ` fast` (or shows `fast` alone without quota). Toggle redraws without quota fetch or success notification. The library owns session WeakMap/reload arbitration, not Fast state. A Fast config read failure must not block usage status.
|
|
9
|
-
- `OpenAI provider support`: Show the same Codex subscription quota for active `openai-codex` and `openai` models.
|
|
9
|
+
- `OpenAI provider support`: Show the same Codex subscription quota for active `openai-codex` and `openai` models. Recognize both Pi OAuth methods: legacy `openai-codex` credentials and compatible `openai` Sign in with ChatGPT tokens (guarded by `modelRegistry.isUsingOAuth` and a `chatgpt_account_id` claim/header). The newer `openai` token is scoped for `api.openai.com/v1` and rejected by `/wham/usage`, so record it as unavailable instead of sending it. Try legacy Pi auth, compatible new Pi auth, then the local Codex CLI `auth.json` directly before the preserved app-server fallback; never send `openai` API-key credentials to the ChatGPT quota endpoint or label Codex quotas as API billing limits.
|
|
10
10
|
- `Optimistic refresh`: Preserve the last good statusline bar during refresh and transient failures.
|
|
11
11
|
- Trigger: Updating quota polling or error handling.
|
|
12
12
|
- Action: Do not collapse the bar while a request is in flight; only show `n/a` or `error` after repeated failures or no usable quota.
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.4: Direct Pi/Codex Auth Hotfix
|
|
4
|
+
|
|
5
|
+
- Fixed the primary quota request falling back to Codex CLI app-server when Pi uses the newer `openai` Sign in with ChatGPT login. The extension recognizes both Pi OpenAI OAuth methods, records the newer API-scoped token as unavailable for `/wham/usage`, and uses legacy-compatible Pi or local Codex CLI credentials for the direct request before the preserved app-server fallback.
|
|
6
|
+
- Compatible credentials now supply `ChatGPT-Account-Id` from Pi headers or the token claim; API keys, non-ChatGPT CLI credentials, and expired tokens are skipped, and JWTs in endpoint error bodies are redacted. The app-server fallback and Windows executable resolution are unchanged.
|
|
7
|
+
- No configuration changes or cache migration are required; restart Pi to load the updated extension.
|
|
8
|
+
|
|
3
9
|
## 0.13.2: Empty Quota Bar Hotfix
|
|
4
10
|
|
|
5
11
|
- Aligned quota bars with rounded remaining percentages: when either window rounds to `0%`, its final segment disappears and the whole dual bar uses the error background, while the other half retains its quota. The 5-hour reset countdown follows the same zero boundary; near-full quota still rounds to the nearest 5% step.
|
|
@@ -22,7 +22,7 @@ This repository is a minimal fork of [`narumiruna/pi-extensions/extensions/pi-co
|
|
|
22
22
|
- Models under either `openai-codex` or `openai` show the same primary Codex subscription quota and `codex` status label (not OpenAI API billing limits)
|
|
23
23
|
- When `pi-telegram` is available, the same compact value appears as `codex: <value>` in the `/start` menu status text for either supported provider
|
|
24
24
|
- Additional returned quota buckets are ignored
|
|
25
|
-
- Pi
|
|
25
|
+
- Both Pi OpenAI OAuth methods are detected; legacy-compatible Pi/Codex CLI credentials are used for the direct quota request
|
|
26
26
|
- Codex CLI app-server remains available as a fallback
|
|
27
27
|
- Missing auth, subscription, plan, or quota windows are shown as `n/a`, not as an error
|
|
28
28
|
- Successful updates briefly redraw the bar only when a 5% segment changes
|
|
@@ -137,7 +137,7 @@ codex error
|
|
|
137
137
|
|
|
138
138
|
## Shared Refresh
|
|
139
139
|
|
|
140
|
-
[`index.ts`](./index.ts) is a re-export-only Pi entrypoint; [`lib/extension.ts`](./lib/extension.ts) composes the live extension. [`lib/usage-store.ts`](./lib/usage-store.ts) owns cross-instance quota coordination, [`lib/query.ts`](./lib/query.ts) owns provider requests, [`lib/usage.ts`](./lib/usage.ts) owns quota normalization, [`lib/status.ts`](./lib/status.ts) and [`lib/status-format.ts`](./lib/status-format.ts) own lifecycle and terminal display, [`lib/telegram.ts`](./lib/telegram.ts) adapts the optional Telegram row, and [`lib/fast.ts`](./lib/fast.ts) owns Codex Fast semantics and the native payload bridge; the shared library owns JSONC and command arbitration. Domain tests live under [`tests/`](./tests/) with matching names; integration tests name the domain whose boundary they exercise.
|
|
140
|
+
[`index.ts`](./index.ts) is a re-export-only Pi entrypoint; [`lib/extension.ts`](./lib/extension.ts) composes the live extension. [`lib/usage-store.ts`](./lib/usage-store.ts) owns cross-instance quota coordination, [`lib/query.ts`](./lib/query.ts) owns auth candidate discovery and provider requests, [`lib/usage.ts`](./lib/usage.ts) owns quota normalization, [`lib/status.ts`](./lib/status.ts) and [`lib/status-format.ts`](./lib/status-format.ts) own lifecycle and terminal display, [`lib/telegram.ts`](./lib/telegram.ts) adapts the optional Telegram row, and [`lib/fast.ts`](./lib/fast.ts) owns Codex Fast semantics and the native payload bridge; the shared library owns JSONC and command arbitration. Domain tests live under [`tests/`](./tests/) with matching names; integration tests name the domain whose boundary they exercise.
|
|
141
141
|
|
|
142
142
|
All Codex models and instances coordinate through `~/.pi/agent/tmp/pi-codex-usage/usage.json` (quota percentages and timestamps only, no tokens). The Claude extension uses its own independent file at `~/.pi/agent/tmp/pi-claude-usage/usage.json`.
|
|
143
143
|
|
|
@@ -169,12 +169,16 @@ The value is the same compact quota bar plus weekly reset countdown used by the
|
|
|
169
169
|
|
|
170
170
|
The extension tries usage sources in this order:
|
|
171
171
|
|
|
172
|
-
1. Pi's
|
|
173
|
-
2. `
|
|
172
|
+
1. Pi's legacy `openai-codex` subscription OAuth, resolved through `modelRegistry.getApiKeyAndHeaders()`
|
|
173
|
+
2. Pi's newer `openai` Sign in with ChatGPT OAuth, when its token carries a Codex account claim
|
|
174
|
+
3. The local Codex CLI `auth.json` ChatGPT credential, read directly without spawning a process
|
|
175
|
+
4. `codex app-server --listen stdio://`
|
|
176
|
+
|
|
177
|
+
The `/wham/usage` endpoint accepts legacy Codex-shaped OAuth tokens and rejects the newer `openai` Sign in with ChatGPT token: it is scoped for `api.openai.com/v1`, has no `chatgpt_account_id` claim, and returns 401 even when an account header is supplied. The extension detects that token, records it as unavailable, and does not send it to the quota endpoint. Both Pi login methods are recognized; use `/login openai-codex` for a directly usable Pi credential, or keep the Codex CLI logged in for the direct local auth source. Compatible tokens get `ChatGPT-Account-Id` from Pi headers or the token's `https://api.openai.com/auth` claim. The Codex CLI source reads `$CODEX_HOME/auth.json` (default `~/.codex/auth.json`), accepts ChatGPT-mode credentials (`auth_mode: "chatgpt"` or legacy files without a mode), skips expired tokens, and never persists tokens.
|
|
174
178
|
|
|
175
179
|
On Windows, the fallback resolves the native `codex.exe` from PATH or from the npm platform package behind a `codex.cmd` shim, then launches it directly without a shell. If Codex is missing, install it with `npm install -g @openai/codex@latest` and restart Pi so it inherits the updated PATH.
|
|
176
180
|
|
|
177
|
-
Selecting an `openai` model enables the status and Fast toggle
|
|
181
|
+
Selecting an `openai` model enables the status and Fast toggle. The extension uses Pi's resolved auth headers/token, checks `modelRegistry.isUsingOAuth` before using `openai` credentials, and keeps the Codex CLI fallback. OpenAI API keys are not ChatGPT subscription auth and do not expose these quotas; `openai` API-key credentials are skipped, not sent to the ChatGPT quota endpoint. Without subscription auth/quota, the quota status is `n/a` when all sources report an unavailable state. Fast works independently of subscription auth.
|
|
178
182
|
|
|
179
183
|
## License
|
|
180
184
|
|
|
@@ -1,16 +1,31 @@
|
|
|
1
|
-
/** Domain: quota transport. Owns: Pi auth,
|
|
1
|
+
/** Domain: quota transport. Owns: Pi/Codex auth candidates, guarded HTTP/RPC quota requests and errors. Excludes: shared leadership and UI. */
|
|
2
2
|
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
4
5
|
import { join } from "node:path";
|
|
5
6
|
import { createInterface } from "node:readline";
|
|
6
7
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
|
-
import { assertObject, errorMessage, isOpenAICodexModel, normalizeAppServerResponse, normalizeBackendPayload, parseJsonObject, selectUsageSnapshot, type AppServerRateLimitResponse, type CodexUsageModel, type CodexUsageReport, type RateLimitStatusPayload, type UsageQueryError } from "./usage.ts";
|
|
8
|
+
import { assertObject, errorMessage, isOpenAICodexModel, normalizeAppServerResponse, normalizeBackendPayload, parseJsonObject, selectUsageSnapshot, type AppServerRateLimitResponse, type CodexUsageModel, type CodexUsageReport, type RateLimitStatusPayload, type UsageQueryError, type UsageSource } from "./usage.ts";
|
|
8
9
|
const CODEX_USAGE_LIMIT_ID = "codex";
|
|
9
10
|
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
11
|
+
const CODEX_USAGE_ACCOUNT_HEADER = "ChatGPT-Account-Id";
|
|
12
|
+
const OPENAI_AUTH_CLAIM = "https://api.openai.com/auth";
|
|
13
|
+
const LEGACY_PROVIDER = "openai-codex";
|
|
14
|
+
const NEW_PROVIDER = "openai";
|
|
15
|
+
const USER_AGENT = "pi-codex-usage";
|
|
16
|
+
const TOKEN_EXPIRY_MARGIN_MS = 60_000;
|
|
10
17
|
const MAX_ERROR_BODY_CHARS = 600;
|
|
11
18
|
type PiModel = NonNullable<ExtensionContext["model"]>;
|
|
12
19
|
type QueryUsageOptions = { timeoutMs: number };
|
|
13
20
|
export type QueryUsageResult = { ok: true; report: CodexUsageReport } | { ok: false; errors: UsageQueryError[] };
|
|
21
|
+
type QuotaAuthCandidate = {
|
|
22
|
+
source: UsageSource;
|
|
23
|
+
headers: Record<string, string>;
|
|
24
|
+
};
|
|
25
|
+
type QuotaAuthResolution = {
|
|
26
|
+
candidates: QuotaAuthCandidate[];
|
|
27
|
+
errors: UsageQueryError[];
|
|
28
|
+
};
|
|
14
29
|
type RpcResponse = {
|
|
15
30
|
id?: unknown;
|
|
16
31
|
result?: unknown;
|
|
@@ -29,15 +44,15 @@ export async function queryUsage(
|
|
|
29
44
|
mayQuery: () => boolean,
|
|
30
45
|
): Promise<QueryUsageResult | undefined> {
|
|
31
46
|
const errors: UsageQueryError[] = [];
|
|
32
|
-
|
|
47
|
+
if (!mayQuery()) return undefined;
|
|
48
|
+
const auth = await resolveQuotaAuthCandidates(ctx);
|
|
49
|
+
errors.push(...auth.errors);
|
|
50
|
+
if (!mayQuery()) return undefined;
|
|
33
51
|
|
|
34
|
-
for (const
|
|
52
|
+
for (const candidate of auth.candidates) {
|
|
35
53
|
if (!mayQuery()) return undefined;
|
|
36
54
|
try {
|
|
37
|
-
const report =
|
|
38
|
-
source === "pi-auth"
|
|
39
|
-
? await queryViaPiAuth(ctx, options.timeoutMs, mayQuery)
|
|
40
|
-
: await queryViaCodexAppServer(options.timeoutMs, mayQuery);
|
|
55
|
+
const report = await queryViaQuotaAuth(candidate, options.timeoutMs, mayQuery);
|
|
41
56
|
if (!report) return undefined;
|
|
42
57
|
if (
|
|
43
58
|
selectUsageSnapshot(report, CODEX_USAGE_LIMIT_ID) ||
|
|
@@ -46,33 +61,44 @@ export async function queryUsage(
|
|
|
46
61
|
return { ok: true, report };
|
|
47
62
|
}
|
|
48
63
|
errors.push({
|
|
49
|
-
source,
|
|
50
|
-
message: `${source} returned no displayable
|
|
64
|
+
source: candidate.source,
|
|
65
|
+
message: `${candidate.source} returned no displayable rate-limit windows`,
|
|
51
66
|
});
|
|
52
67
|
} catch (cause) {
|
|
53
|
-
errors.push({ source, message: errorMessage(cause), cause });
|
|
68
|
+
errors.push({ source: candidate.source, message: errorMessage(cause), cause });
|
|
54
69
|
}
|
|
55
70
|
}
|
|
56
71
|
|
|
72
|
+
if (!mayQuery()) return undefined;
|
|
73
|
+
try {
|
|
74
|
+
const report = await queryViaCodexAppServer(options.timeoutMs, mayQuery);
|
|
75
|
+
if (!report) return undefined;
|
|
76
|
+
if (
|
|
77
|
+
selectUsageSnapshot(report, CODEX_USAGE_LIMIT_ID) ||
|
|
78
|
+
report.credits
|
|
79
|
+
) {
|
|
80
|
+
return { ok: true, report };
|
|
81
|
+
}
|
|
82
|
+
errors.push({
|
|
83
|
+
source: "codex-app-server",
|
|
84
|
+
message: "codex-app-server returned no displayable rate-limit windows",
|
|
85
|
+
});
|
|
86
|
+
} catch (cause) {
|
|
87
|
+
errors.push({ source: "codex-app-server", message: errorMessage(cause), cause });
|
|
88
|
+
}
|
|
89
|
+
|
|
57
90
|
return { ok: false, errors };
|
|
58
91
|
}
|
|
59
92
|
|
|
60
|
-
async function
|
|
61
|
-
|
|
93
|
+
async function queryViaQuotaAuth(
|
|
94
|
+
candidate: QuotaAuthCandidate,
|
|
62
95
|
timeoutMs: number,
|
|
63
96
|
mayQuery: () => boolean,
|
|
64
97
|
): Promise<CodexUsageReport | undefined> {
|
|
65
|
-
const auth = await resolvePiCodexAuth(ctx);
|
|
66
|
-
if (!auth) {
|
|
67
|
-
throw new Error(
|
|
68
|
-
"No Pi OpenAI Codex subscription auth was available. Use an openai or openai-codex subscription model or run /login for OpenAI ChatGPT.",
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
98
|
if (!mayQuery()) return undefined;
|
|
73
99
|
const response = await fetchWithTimeout(
|
|
74
100
|
CODEX_USAGE_URL,
|
|
75
|
-
{ headers:
|
|
101
|
+
{ headers: candidate.headers },
|
|
76
102
|
timeoutMs,
|
|
77
103
|
);
|
|
78
104
|
const text = await response.text();
|
|
@@ -86,43 +112,93 @@ async function queryViaPiAuth(
|
|
|
86
112
|
return normalizeBackendPayload(
|
|
87
113
|
payload as RateLimitStatusPayload,
|
|
88
114
|
Date.now(),
|
|
89
|
-
|
|
115
|
+
candidate.source,
|
|
90
116
|
);
|
|
91
117
|
}
|
|
92
118
|
|
|
93
|
-
|
|
94
|
-
ctx: ExtensionContext,
|
|
95
|
-
): Promise<{ headers: Record<string, string> } | undefined> {
|
|
96
|
-
const models = codexAuthCandidateModels(ctx);
|
|
97
|
-
const errors: string[] = [];
|
|
119
|
+
// --- Auth Candidates ---
|
|
98
120
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
121
|
+
export async function resolveQuotaAuthCandidates(
|
|
122
|
+
ctx: ExtensionContext,
|
|
123
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
124
|
+
now = Date.now(),
|
|
125
|
+
): Promise<QuotaAuthResolution> {
|
|
126
|
+
const candidates: QuotaAuthCandidate[] = [];
|
|
127
|
+
const errors: UsageQueryError[] = [];
|
|
128
|
+
const seenTokens = new Set<string>();
|
|
129
|
+
|
|
130
|
+
for (const model of orderedCodexAuthModels(ctx)) {
|
|
131
|
+
if (model.provider === NEW_PROVIDER && !ctx.modelRegistry.isUsingOAuth(model)) continue;
|
|
132
|
+
const source: UsageSource = model.provider === NEW_PROVIDER
|
|
133
|
+
? "pi-auth-openai"
|
|
134
|
+
: "pi-auth-openai-codex";
|
|
135
|
+
let auth;
|
|
136
|
+
try {
|
|
137
|
+
auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
138
|
+
} catch (cause) {
|
|
139
|
+
errors.push({ source, message: errorMessage(cause) });
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
102
142
|
if (!auth.ok) {
|
|
103
|
-
errors.push(auth.error);
|
|
143
|
+
errors.push({ source, message: auth.error });
|
|
104
144
|
continue;
|
|
105
145
|
}
|
|
106
146
|
|
|
107
|
-
const headers
|
|
108
|
-
|
|
109
|
-
if (value !== null) headers[name] = value;
|
|
110
|
-
}
|
|
111
|
-
if (!hasHeader(headers, "Authorization") && auth.apiKey) {
|
|
147
|
+
const headers = stringHeaders(auth.headers);
|
|
148
|
+
if (!hasHeader(headers, "Authorization") && auth.apiKey)
|
|
112
149
|
headers.Authorization = `Bearer ${auth.apiKey}`;
|
|
150
|
+
const token = bearerToken(headers);
|
|
151
|
+
if (!token || token.startsWith("sk-")) continue;
|
|
152
|
+
const accountId = headerValue(headers, CODEX_USAGE_ACCOUNT_HEADER) ?? chatgptAccountId(token);
|
|
153
|
+
if (model.provider === NEW_PROVIDER && !accountId) {
|
|
154
|
+
errors.push({
|
|
155
|
+
source,
|
|
156
|
+
message: "New Pi OpenAI OAuth credential is not accepted by the Codex usage endpoint",
|
|
157
|
+
});
|
|
158
|
+
continue;
|
|
113
159
|
}
|
|
114
|
-
if (
|
|
115
|
-
|
|
160
|
+
if (isExpiredToken(token, now)) {
|
|
161
|
+
errors.push({ source, message: "Pi OpenAI subscription token has expired" });
|
|
162
|
+
continue;
|
|
116
163
|
}
|
|
117
|
-
if (hasHeader(headers,
|
|
118
|
-
|
|
164
|
+
if (accountId && !hasHeader(headers, CODEX_USAGE_ACCOUNT_HEADER))
|
|
165
|
+
headers[CODEX_USAGE_ACCOUNT_HEADER] = accountId;
|
|
166
|
+
if (!hasHeader(headers, "User-Agent")) headers["User-Agent"] = USER_AGENT;
|
|
167
|
+
if (!hasHeader(headers, "Accept")) headers.Accept = "application/json";
|
|
168
|
+
if (seenTokens.has(token)) continue;
|
|
169
|
+
seenTokens.add(token);
|
|
170
|
+
candidates.push({ source, headers });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const codexCli = readCodexCliAuth(env, now);
|
|
174
|
+
if (codexCli) {
|
|
175
|
+
const token = bearerToken(codexCli.headers);
|
|
176
|
+
if (token && !seenTokens.has(token)) {
|
|
177
|
+
seenTokens.add(token);
|
|
178
|
+
candidates.push(codexCli);
|
|
119
179
|
}
|
|
180
|
+
} else {
|
|
181
|
+
errors.push({
|
|
182
|
+
source: "codex-cli-auth",
|
|
183
|
+
message: "No usable Codex CLI ChatGPT auth was available",
|
|
184
|
+
});
|
|
120
185
|
}
|
|
121
186
|
|
|
122
|
-
if (errors.length
|
|
123
|
-
|
|
187
|
+
if (candidates.length === 0 && errors.length === 0) {
|
|
188
|
+
errors.push({
|
|
189
|
+
source: "pi-auth",
|
|
190
|
+
message: "No Pi OpenAI Codex subscription auth was available. Use an openai or openai-codex subscription model or run /login for OpenAI ChatGPT.",
|
|
191
|
+
});
|
|
124
192
|
}
|
|
125
|
-
return
|
|
193
|
+
return { candidates, errors };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function orderedCodexAuthModels(ctx: ExtensionContext): PiModel[] {
|
|
197
|
+
const models = codexAuthCandidateModels(ctx);
|
|
198
|
+
return [
|
|
199
|
+
...models.filter((model) => model.provider === LEGACY_PROVIDER),
|
|
200
|
+
...models.filter((model) => model.provider === NEW_PROVIDER),
|
|
201
|
+
];
|
|
126
202
|
}
|
|
127
203
|
|
|
128
204
|
function codexAuthCandidateModels(ctx: ExtensionContext): PiModel[] {
|
|
@@ -142,6 +218,114 @@ function codexAuthCandidateModels(ctx: ExtensionContext): PiModel[] {
|
|
|
142
218
|
return candidates;
|
|
143
219
|
}
|
|
144
220
|
|
|
221
|
+
function readCodexCliAuth(
|
|
222
|
+
env: NodeJS.ProcessEnv,
|
|
223
|
+
now: number,
|
|
224
|
+
): QuotaAuthCandidate | undefined {
|
|
225
|
+
try {
|
|
226
|
+
const payload: unknown = JSON.parse(readFileSync(codexAuthPath(env), "utf8"));
|
|
227
|
+
if (!isRecord(payload)) return undefined;
|
|
228
|
+
if (typeof payload.auth_mode === "string" && payload.auth_mode !== "chatgpt")
|
|
229
|
+
return undefined;
|
|
230
|
+
const tokens = isRecord(payload.tokens) ? payload.tokens : undefined;
|
|
231
|
+
const accessToken = typeof tokens?.access_token === "string"
|
|
232
|
+
? tokens.access_token
|
|
233
|
+
: undefined;
|
|
234
|
+
if (!accessToken || accessToken.startsWith("sk-")) return undefined;
|
|
235
|
+
if (isExpiredToken(accessToken, now)) return undefined;
|
|
236
|
+
const accountId = stringValue(tokens?.account_id)
|
|
237
|
+
?? chatgptAccountId(accessToken)
|
|
238
|
+
?? (typeof tokens?.id_token === "string"
|
|
239
|
+
? chatgptAccountId(tokens.id_token)
|
|
240
|
+
: undefined);
|
|
241
|
+
if (!accountId) return undefined;
|
|
242
|
+
return {
|
|
243
|
+
source: "codex-cli-auth",
|
|
244
|
+
headers: {
|
|
245
|
+
Authorization: `Bearer ${accessToken}`,
|
|
246
|
+
[CODEX_USAGE_ACCOUNT_HEADER]: accountId,
|
|
247
|
+
"User-Agent": USER_AGENT,
|
|
248
|
+
Accept: "application/json",
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
} catch {
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function codexAuthPath(env: NodeJS.ProcessEnv): string {
|
|
257
|
+
const configured = typeof env.CODEX_HOME === "string" ? env.CODEX_HOME.trim() : "";
|
|
258
|
+
if (!configured) return join(homedir(), ".codex", "auth.json");
|
|
259
|
+
const root = configured === "~"
|
|
260
|
+
? homedir()
|
|
261
|
+
: configured.startsWith("~/") || configured.startsWith("~\\")
|
|
262
|
+
? join(homedir(), configured.slice(2))
|
|
263
|
+
: configured;
|
|
264
|
+
return join(root, "auth.json");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function stringHeaders(
|
|
268
|
+
headers: Record<string, string | null> | undefined,
|
|
269
|
+
): Record<string, string> {
|
|
270
|
+
const result: Record<string, string> = {};
|
|
271
|
+
for (const [name, value] of Object.entries(headers ?? {})) {
|
|
272
|
+
if (value !== null) result[name] = value;
|
|
273
|
+
}
|
|
274
|
+
return result;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function hasHeader(headers: Record<string, string>, name: string): boolean {
|
|
278
|
+
return headerValue(headers, name) !== undefined;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function headerValue(
|
|
282
|
+
headers: Record<string, string>,
|
|
283
|
+
name: string,
|
|
284
|
+
): string | undefined {
|
|
285
|
+
const expected = name.toLowerCase();
|
|
286
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
287
|
+
if (key.toLowerCase() === expected && value.trim()) return value;
|
|
288
|
+
}
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function bearerToken(headers: Record<string, string>): string | undefined {
|
|
293
|
+
const value = headerValue(headers, "Authorization");
|
|
294
|
+
return /^Bearer\s+(.+)$/iu.exec(value ?? "")?.[1]?.trim();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function chatgptAccountId(token: string): string | undefined {
|
|
298
|
+
const payload = decodeJwtPayload(token);
|
|
299
|
+
const authClaim = payload?.[OPENAI_AUTH_CLAIM];
|
|
300
|
+
return stringValue(isRecord(authClaim) ? authClaim.chatgpt_account_id : undefined);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function isExpiredToken(token: string, now: number): boolean {
|
|
304
|
+
const payload = decodeJwtPayload(token);
|
|
305
|
+
const exp = payload?.exp;
|
|
306
|
+
if (typeof exp !== "number" || !Number.isFinite(exp)) return false;
|
|
307
|
+
return exp * 1000 <= now + TOKEN_EXPIRY_MARGIN_MS;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function decodeJwtPayload(token: string): Record<string, unknown> | undefined {
|
|
311
|
+
const encoded = token.split(".")[1];
|
|
312
|
+
if (!encoded) return undefined;
|
|
313
|
+
try {
|
|
314
|
+
const parsed: unknown = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
315
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
316
|
+
} catch {
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function stringValue(value: unknown): string | undefined {
|
|
322
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
326
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
327
|
+
}
|
|
328
|
+
|
|
145
329
|
async function fetchWithTimeout(
|
|
146
330
|
url: string,
|
|
147
331
|
init: RequestInit,
|
|
@@ -390,17 +574,12 @@ class CodexAppServerClient {
|
|
|
390
574
|
}
|
|
391
575
|
|
|
392
576
|
|
|
393
|
-
function hasHeader(headers: Record<string, string>, name: string): boolean {
|
|
394
|
-
return Object.keys(headers).some(
|
|
395
|
-
(key) => key.toLowerCase() === name.toLowerCase(),
|
|
396
|
-
);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
577
|
function redactErrorBody(body: string): string {
|
|
400
578
|
return truncateEnd(
|
|
401
579
|
body
|
|
402
580
|
.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer <redacted>")
|
|
403
581
|
.replace(/"access_token"\s*:\s*"[^"]+"/gi, '"access_token":"<redacted>"')
|
|
582
|
+
.replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, "<redacted>")
|
|
404
583
|
.trim(),
|
|
405
584
|
MAX_ERROR_BODY_CHARS,
|
|
406
585
|
);
|
|
@@ -8,7 +8,12 @@ export const OPENAI_USAGE_PROVIDERS = ["openai-codex", "openai"] as const;
|
|
|
8
8
|
export function isOpenAICodexModel(model: Pick<PiModel, "provider"> | undefined): boolean {
|
|
9
9
|
return OPENAI_USAGE_PROVIDERS.some((provider) => model?.provider === provider);
|
|
10
10
|
}
|
|
11
|
-
export type UsageSource =
|
|
11
|
+
export type UsageSource =
|
|
12
|
+
| "pi-auth"
|
|
13
|
+
| "pi-auth-openai"
|
|
14
|
+
| "pi-auth-openai-codex"
|
|
15
|
+
| "codex-cli-auth"
|
|
16
|
+
| "codex-app-server";
|
|
12
17
|
export type UsageQueryError = { source: UsageSource; message: string; cause?: unknown };
|
|
13
18
|
export type CodexUsageReport = {
|
|
14
19
|
snapshots: NormalizedRateLimitSnapshot[];
|
|
@@ -283,6 +288,9 @@ function isUnavailableError(error: UsageQueryError): boolean {
|
|
|
283
288
|
const message = error.message.toLowerCase();
|
|
284
289
|
return (
|
|
285
290
|
message.includes("no pi openai codex subscription auth") ||
|
|
291
|
+
message.includes("not accepted by the codex usage endpoint") ||
|
|
292
|
+
message.includes("no usable codex cli chatgpt auth") ||
|
|
293
|
+
message.includes("subscription token has expired") ||
|
|
286
294
|
message.includes("no displayable rate-limit windows") ||
|
|
287
295
|
message.includes("returned no displayable rate-limit windows") ||
|
|
288
296
|
message.includes("returned 401") ||
|
|
@@ -13,7 +13,7 @@ The [relocation ledger](docs/agent-contract-relocation.md) maps every pre-compac
|
|
|
13
13
|
- Artifacts name exact registered source paths; observe only regular non-symlink files, never discover directories or read unrelated bodies. Preserve hidden per-scope provenance, exact-owner compilation, stable fingerprint checks and separate Skill hashes; unavailable sources are not proof of deletion. See [artifact routing](docs/architecture.md#artifact-routing).
|
|
14
14
|
- Preserve session config/runtime, per-scope metadata, lineage and provenance outside model-patchable state; decode legacy mode evidence read-only, never mix representations or normalize storage eagerly. See [storage and identity](docs/architecture.md#storage-and-identity) and [mode compatibility](docs/compatibility.md#mode-configuration-compatibility).
|
|
15
15
|
- Keep one opaque composed causal lineage, anchored checkpoint/tail materialization, configured hot-history folding and independent owner revisions; never invent earlier history or rebuild discarded offsets. Historical reads are observational, and `effective[n]`/owner materializations select the same causal boundary. See [temporal model](docs/architecture.md#temporal-model) and [model tools](docs/architecture.md#model-tools).
|
|
16
|
-
- Keep the [internal semantic-module contract](docs/semantic-module-contract.md) package-owned and separate from storage/native authority. Module candidates are not accepted publications; immutable means main-agent read-only, not producer-frozen. Preserve the single intent-first engine and current dialect; no runtime discovery
|
|
16
|
+
- Keep the [internal semantic-module contract](docs/semantic-module-contract.md) package-owned and separate from storage/native authority. Module candidates are not accepted publications; immutable means main-agent read-only, not producer-frozen. Preserve the single intent-first engine and current dialect; no runtime discovery, public registration authority or automatic inference follows from an internal module identity.
|
|
17
17
|
- Keep the [public module registration boundary](docs/public-module-contract.md) native-host-scoped and revocable. A registration/catalog grants no source, private or publication authority. Follow its explicit compatibility decision before admitting external stored roots; unavailable-module data must never be dropped through filtering or normalization. Pi remains the loader.
|
|
18
18
|
|
|
19
19
|
## Publication and lifecycle
|