@centerforagenticai/pi-multi-account 0.1.1
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/LICENSE +21 -0
- package/NOTICE +29 -0
- package/README.md +999 -0
- package/config/models/pi-multi-account.v1.json +32 -0
- package/config/subscription-plans.v1.json +122 -0
- package/package.json +76 -0
- package/packages/pi-anthropic-oauth/LICENSE +21 -0
- package/packages/pi-anthropic-oauth/package.json +54 -0
- package/packages/pi-anthropic-oauth/src/auth.ts +396 -0
- package/packages/pi-anthropic-oauth/src/context.ts +116 -0
- package/packages/pi-anthropic-oauth/src/convert.ts +303 -0
- package/packages/pi-anthropic-oauth/src/index.ts +37 -0
- package/packages/pi-anthropic-oauth/src/prompt.ts +137 -0
- package/packages/pi-anthropic-oauth/src/stream.ts +476 -0
- package/packages/pi-antigravity/LICENSE +21 -0
- package/packages/pi-antigravity/package.json +77 -0
- package/packages/pi-antigravity/src/auth/index.ts +14 -0
- package/packages/pi-antigravity/src/auth/oauth.ts +442 -0
- package/packages/pi-antigravity/src/client/client.ts +561 -0
- package/packages/pi-antigravity/src/client/index.ts +1 -0
- package/packages/pi-antigravity/src/context.ts +110 -0
- package/packages/pi-antigravity/src/diagnostics/diagnostics.ts +96 -0
- package/packages/pi-antigravity/src/diagnostics/index.ts +1 -0
- package/packages/pi-antigravity/src/image/image.ts +336 -0
- package/packages/pi-antigravity/src/image/index.ts +1 -0
- package/packages/pi-antigravity/src/index.ts +280 -0
- package/packages/pi-antigravity/src/models/discovery.ts +154 -0
- package/packages/pi-antigravity/src/models/grouping.ts +424 -0
- package/packages/pi-antigravity/src/models/index.ts +3 -0
- package/packages/pi-antigravity/src/models/models.ts +500 -0
- package/packages/pi-antigravity/src/stream/index.ts +1 -0
- package/packages/pi-antigravity/src/stream/stream.ts +1478 -0
- package/packages/pi-antigravity/src/types/enums.ts +42 -0
- package/packages/pi-antigravity/src/types/index.ts +2 -0
- package/packages/pi-antigravity/src/types/types.ts +292 -0
- package/packages/pi-antigravity/src/usage/index.ts +1 -0
- package/packages/pi-antigravity/src/usage/usage.ts +416 -0
- package/packages/pi-antigravity/src/utils/http.ts +91 -0
- package/packages/pi-antigravity/src/utils/index.ts +3 -0
- package/packages/pi-antigravity/src/utils/security.ts +73 -0
- package/packages/pi-antigravity/src/utils/util.ts +132 -0
- package/scripts/multi-account.mjs +44 -0
- package/src/account-labels.ts +223 -0
- package/src/account-plan-assignment.ts +340 -0
- package/src/account-rate-history.ts +372 -0
- package/src/anthropic-adaptive-stream.ts +531 -0
- package/src/anthropic-alias-stream.ts +140 -0
- package/src/anthropic-context-compat.ts +80 -0
- package/src/api-pricing.ts +579 -0
- package/src/bounded-file-lines.ts +97 -0
- package/src/catalog-rebinding.ts +177 -0
- package/src/catalog-registration-probe.ts +111 -0
- package/src/codex-adapter.ts +345 -0
- package/src/codex-model-defaults.ts +785 -0
- package/src/command-completions.ts +404 -0
- package/src/commands.ts +2000 -0
- package/src/compaction.ts +14 -0
- package/src/config.ts +1317 -0
- package/src/continuation.ts +569 -0
- package/src/cooldowns.ts +110 -0
- package/src/cost-digest-store.ts +332 -0
- package/src/cost-digest.ts +1044 -0
- package/src/cost-history.ts +251 -0
- package/src/cost-period-closer.ts +160 -0
- package/src/cost-report-json.ts +318 -0
- package/src/cost-report-reader.ts +368 -0
- package/src/cost-report-render.ts +207 -0
- package/src/cost-report.ts +1104 -0
- package/src/coverage-attestation.ts +397 -0
- package/src/credential-lifecycle.ts +169 -0
- package/src/credential-refresh.ts +248 -0
- package/src/declaration-notice-marker.ts +238 -0
- package/src/diagnostic-store.ts +276 -0
- package/src/diagnostics.ts +309 -0
- package/src/discovery.ts +471 -0
- package/src/duration.ts +13 -0
- package/src/error-classification.ts +256 -0
- package/src/fuzzy.ts +15 -0
- package/src/group-policy.ts +81 -0
- package/src/history-store.ts +897 -0
- package/src/index.ts +5572 -0
- package/src/lifecycle.ts +378 -0
- package/src/logical-dispatch.ts +279 -0
- package/src/logical-model-selector.ts +254 -0
- package/src/logical-model-switcher.ts +430 -0
- package/src/logical-provider-attribution.ts +544 -0
- package/src/logical-provider.ts +1237 -0
- package/src/logical-route-indicator.ts +215 -0
- package/src/machine-lease.ts +445 -0
- package/src/model-support.ts +66 -0
- package/src/models-declaration.ts +1091 -0
- package/src/openai-adapter.ts +117 -0
- package/src/openrouter-budget.ts +304 -0
- package/src/openrouter-fallback.ts +146 -0
- package/src/period-boundaries.ts +376 -0
- package/src/pi-anthropic-oauth.d.ts +6 -0
- package/src/preflight.ts +253 -0
- package/src/pricing-cache.ts +235 -0
- package/src/project-identity.ts +100 -0
- package/src/provider-registration.ts +942 -0
- package/src/rate-formula.ts +163 -0
- package/src/recovery-engine.ts +853 -0
- package/src/recovery-output.ts +837 -0
- package/src/recovery-plan.ts +239 -0
- package/src/report-range.ts +203 -0
- package/src/route-resolver.ts +789 -0
- package/src/routing-config-transaction.ts +232 -0
- package/src/routing.ts +1163 -0
- package/src/runtime-state.ts +630 -0
- package/src/session-account-groups.ts +284 -0
- package/src/session-restore.ts +287 -0
- package/src/shared-usage.ts +1392 -0
- package/src/standalone-cli.ts +720 -0
- package/src/status-view.ts +578 -0
- package/src/subscription-plan-catalog.ts +346 -0
- package/src/tier-model-resolver.ts +46 -0
- package/src/upstream-anthropic.ts +315 -0
- package/src/upstream-antigravity.ts +327 -0
- package/src/usage-fetch.ts +1634 -0
- package/src/usage.ts +1026 -0
- package/src/vendor.ts +87 -0
- package/src/warmer.ts +231 -0
- package/src/watchdog.ts +219 -0
- package/src/window-history.ts +270 -0
package/README.md
ADDED
|
@@ -0,0 +1,999 @@
|
|
|
1
|
+
# pi-multi-account
|
|
2
|
+
|
|
3
|
+
`pi-multi-account` keeps Pi working when an Anthropic or OpenAI Codex OAuth
|
|
4
|
+
account reaches a limit or loses authorization. It registers numbered account
|
|
5
|
+
aliases, observes provider health, and moves a settled turn to an eligible
|
|
6
|
+
account. **Failover** means moving that turn after the active account fails.
|
|
7
|
+
|
|
8
|
+
The extension is OAuth-first. Credentials remain in Pi's `AuthStorage`.
|
|
9
|
+
Discovery uses Pi's public credential adapter first and a narrow read-only
|
|
10
|
+
`auth.json` fallback when that adapter is unavailable. Both paths project only
|
|
11
|
+
presence, expiry, a derived fingerprint, and an optional Codex label. Raw values
|
|
12
|
+
do not leave that boundary. The package never writes `auth.json` or puts tokens
|
|
13
|
+
in its own state or diagnostics. OpenRouter is available only as an explicitly
|
|
14
|
+
enabled, budgeted final rung in a non-delegate Pi session. It is off by default
|
|
15
|
+
and blocked inside delegate sessions.
|
|
16
|
+
|
|
17
|
+
## Routing behavior
|
|
18
|
+
|
|
19
|
+
The route order is:
|
|
20
|
+
|
|
21
|
+
1. another healthy subscription account in the turn's starting family;
|
|
22
|
+
2. a same-vendor account that uses the vendor's own pay-per-token API;
|
|
23
|
+
3. an explicitly configured cross-vendor subscription destination;
|
|
24
|
+
4. explicitly enabled OpenRouter with a successful budget reservation;
|
|
25
|
+
5. a bounded parked turn that waits for a permitted managed account to recover.
|
|
26
|
+
|
|
27
|
+
A **parked turn** is held without replaying the failed request. The extension
|
|
28
|
+
re-checks live account state for up to 30 minutes. `/multi-account stop`
|
|
29
|
+
cancels it.
|
|
30
|
+
|
|
31
|
+
Routing has these guarantees:
|
|
32
|
+
|
|
33
|
+
- Same-family subscription failover prefers an eligible account whose catalog
|
|
34
|
+
serves the active model. If none does, routing may choose another eligible
|
|
35
|
+
subscription account and use its catalog head.
|
|
36
|
+
- The owning-vendor API tier stays within the model's vendor. It uses the exact
|
|
37
|
+
requested model ID when the destination catalog contains it, then a matching
|
|
38
|
+
`tierModelMap` entry. If neither is available, that account is skipped. It
|
|
39
|
+
never substitutes the account's catalog head.
|
|
40
|
+
- Cross-family subscription failover is directional and disabled by default.
|
|
41
|
+
It uses the first supported model in `preferredModels` for the destination
|
|
42
|
+
family, then its catalog head if no preference is supported.
|
|
43
|
+
- Parked recovery keeps the same family policy as the failure that created it.
|
|
44
|
+
Polling does not create cooldowns or widen the route.
|
|
45
|
+
- A confirmed switch sends one fixed follow-up message with `deliverAs:
|
|
46
|
+
"followUp"`. It never resubmits the original prompt or provider request.
|
|
47
|
+
- A failed model selection sends no follow-up.
|
|
48
|
+
- Two 429 responses within 15 minutes temporarily invalidate an optimistic usage
|
|
49
|
+
reading. A provider snapshot cannot keep an account selectable while live
|
|
50
|
+
requests prove that it is limited.
|
|
51
|
+
- Authentication refresh is bounded to one forced attempt per provider and
|
|
52
|
+
session. A changed account identity is rejected rather than silently replacing
|
|
53
|
+
the selected account.
|
|
54
|
+
- Watchdogs cancel stalled continuations. Every timeout is retained as a
|
|
55
|
+
diagnostic, while identical operator notices are limited to one per 15
|
|
56
|
+
minutes across continuations.
|
|
57
|
+
|
|
58
|
+
Pi performs its own provider retries before the extension acts. Reactive routing
|
|
59
|
+
runs at `agent_settled`, after Pi's retry and compaction loop has finished. A
|
|
60
|
+
visible pause before failover is normally Pi's backoff, not an extra retry made
|
|
61
|
+
by this extension.
|
|
62
|
+
|
|
63
|
+
## Unified logical model provider
|
|
64
|
+
|
|
65
|
+
The extension can register one extra provider, `unified`, whose models come from
|
|
66
|
+
the managed subscription catalogs. A unified OpenAI model can run on either an
|
|
67
|
+
`openai-codex` subscription or the owning-vendor `openai` API tier. Routing tries
|
|
68
|
+
eligible subscriptions first. It uses the API tier only when the exact model ID
|
|
69
|
+
is in that tier's catalog or `tierModelMap.openai` explicitly maps the unified ID
|
|
70
|
+
to a catalog model. An unresolved API model is skipped; routing never chooses the
|
|
71
|
+
API catalog head as a substitute.
|
|
72
|
+
|
|
73
|
+
Public assistant stream events, including the final result, carry `unified` as
|
|
74
|
+
provider and API and the selected logical model ID. Physical account identities
|
|
75
|
+
remain private to routing and usage attribution. When a logical turn finishes,
|
|
76
|
+
the extension records its token usage and retained provider cost under the
|
|
77
|
+
physical account that served it, never under `unified`. A subscription-tier turn
|
|
78
|
+
records subscription usage and cost; an owning-vendor API-tier turn records
|
|
79
|
+
provider cost only, not subscription usage. It also records normalized rate-limit
|
|
80
|
+
observations when the physical transport exposes response headers. Anthropic
|
|
81
|
+
does. Codex does on SSE, but `openai-codex-responses` does not call `onResponse` on its WebSocket branch.
|
|
82
|
+
Logical Codex header observations are therefore absent on WebSocket; the existing
|
|
83
|
+
five-minute usage poll remains the fallback.
|
|
84
|
+
|
|
85
|
+
While a `unified` model is selected, Pi renders `(unified)` in its model line and
|
|
86
|
+
shows one compact, right-aligned below-editor route widget under model/thinking status.
|
|
87
|
+
It starts at `unified(waiting)`, then shows the exact physical account serving an
|
|
88
|
+
attempt and its live usage, such as `unified(anthropic-account-2 · 75% left)`.
|
|
89
|
+
A configured account label appears last inside the parentheses. A retry replaces
|
|
90
|
+
the account with the new exact route. Selecting any physical or unrelated provider
|
|
91
|
+
removes the widget.
|
|
92
|
+
|
|
93
|
+
The indicator uses the same machine-shared quota observations as the status view.
|
|
94
|
+
Fresh utilization leads; fresh request or token counts are used only when utilization
|
|
95
|
+
is absent. Old quota is shown as `usage stale`, and an account with no quota
|
|
96
|
+
observation is `usage unknown`; stale values never appear as current headroom. An
|
|
97
|
+
optional account label is read from live config on every render and appears last.
|
|
98
|
+
Updates are event-driven by model selection, route attempts, usage observations, and
|
|
99
|
+
terminal settlement. The indicator creates no timer, polling loop, retained record,
|
|
100
|
+
or diagnostic state.
|
|
101
|
+
|
|
102
|
+
Selection is exact. A request for a model ID no account serves is refused, not
|
|
103
|
+
redirected. A cross-tier ID may differ only when the operator records that model
|
|
104
|
+
identity in `tierModelMap`. Substituting a catalog head, a same-family sibling,
|
|
105
|
+
or a vendor default would quietly run a different model than the one that was
|
|
106
|
+
chosen, and the reply would look entirely normal.
|
|
107
|
+
|
|
108
|
+
The provider's own rows are never marked enabled or disabled. Which models
|
|
109
|
+
appear in the picker is Pi's decision, expressed through its `enabledModels`
|
|
110
|
+
setting, and this extension does not write that setting on the operator's
|
|
111
|
+
behalf.
|
|
112
|
+
|
|
113
|
+
A model id served by more than one managed family is left out of the
|
|
114
|
+
declaration and reported. No tie-break can be right without knowing which
|
|
115
|
+
vendor was meant, and guessing would send the request to the wrong one.
|
|
116
|
+
|
|
117
|
+
Two commands manage the declaration in Pi's `models.json`:
|
|
118
|
+
|
|
119
|
+
| Command | Result |
|
|
120
|
+
| --- | --- |
|
|
121
|
+
| `/multi-account models install` | Write the declaration and approved Codex context defaults. Refused if a declaration is already present. |
|
|
122
|
+
| `/multi-account models update` | Refresh the declaration and approved Codex context defaults from the current catalogs. Refused if no declaration is present. |
|
|
123
|
+
| `/multi-account model [id]` | Select an exact logical model, or open the logical model picker when `id` is omitted. |
|
|
124
|
+
| `/multi-account-model [id]` | Deprecated one-release alias for `/multi-account model [id]`; it prints a migration notice and otherwise behaves identically. |
|
|
125
|
+
|
|
126
|
+
Pi checks the declaration once at session start. A stale declaration still
|
|
127
|
+
registers the fresh live projection, so logical routing stays on, while a warning
|
|
128
|
+
and `/multi-account status` banner point to `/multi-account models update`. An
|
|
129
|
+
unreadable declaration keeps logical routing off and shows the same update remedy.
|
|
130
|
+
A declaration that is not installed leaves the logical provider unregistered and
|
|
131
|
+
stays silent at startup; use `/multi-account models install` to add it. Startup
|
|
132
|
+
warnings appear at most once per machine per UTC day. A stale status banner stays
|
|
133
|
+
visible for the session until the declaration is updated and Pi restarts.
|
|
134
|
+
|
|
135
|
+
Both commands show the declaration and Codex override changes, then wait for
|
|
136
|
+
confirmation. The offline-approved defaults supply `contextWindow: 1050000`
|
|
137
|
+
when needed for `gpt-5.4`, `gpt-5.5`, `gpt-5.6-sol`, `gpt-5.6-terra`,
|
|
138
|
+
`gpt-5.6-luna`, and `gpt-6-astra`. During these explicit commands only, other
|
|
139
|
+
lowercase, versioned IDs already in the live Codex catalog can receive the same
|
|
140
|
+
default when that exact model's
|
|
141
|
+
`https://developers.openai.com/api/docs/models/<id>.md` page names the exact ID
|
|
142
|
+
and lists a `1,050,000 context window` under Model details. Documentation reads
|
|
143
|
+
send no credential or prompt, follow no redirects, and have fixed candidate,
|
|
144
|
+
time, and response-size limits. When more than eight valid IDs need evidence, a
|
|
145
|
+
UTC-day rotating window checks at most eight and reports the remainder as skipped.
|
|
146
|
+
Offline, malformed, mismatched, or otherwise unverified pages add no default; the
|
|
147
|
+
confirmation shows only a bounded reason-count summary. Every explicit install or
|
|
148
|
+
update verifies new IDs again; a prior override or `unified` row is not evidence.
|
|
149
|
+
A live catalog window already at or above `1050000` requires no new override. For
|
|
150
|
+
one of the six offline-approved IDs, an update removes a prior transaction-managed
|
|
151
|
+
`contextWindow` so the higher live value wins, preserving every other override
|
|
152
|
+
field and deleting the entry only when nothing remains. Operator-owned overrides
|
|
153
|
+
for IDs outside the offline-approved list are always retained; exact documentation
|
|
154
|
+
can create a missing dynamic override and project the default into `unified`, but
|
|
155
|
+
it does not rewrite an existing one.
|
|
156
|
+
|
|
157
|
+
The current transaction's offline-approved and newly verified ID-to-window map
|
|
158
|
+
powers both physical override changes and generated `unified` rows. An existing
|
|
159
|
+
dynamic override can therefore differ from its `unified` row: verified rows use
|
|
160
|
+
the documented default there, while unverified rows continue to copy the current
|
|
161
|
+
live catalog. Every other catalog field,
|
|
162
|
+
including `maxTokens` and tiered cost metadata, is preserved. This metadata
|
|
163
|
+
preservation does not make the separate API-equivalent estimate tier-aware.
|
|
164
|
+
|
|
165
|
+
Before reading the live catalog or model documentation, the command validates
|
|
166
|
+
every existing `openai-codex.modelOverrides` entry against Pi's supported shape.
|
|
167
|
+
If the container or any entry is malformed, it leaves the file unchanged and
|
|
168
|
+
does not ask for confirmation.
|
|
169
|
+
|
|
170
|
+
A successful install or update removes the old `pi-multi-account` provider entry
|
|
171
|
+
only when its API and reserved base URL identify it as this extension's previous
|
|
172
|
+
declaration. The transaction otherwise owns the `unified` declaration and the
|
|
173
|
+
resolved `contextWindow` fields above. It leaves every other provider, override,
|
|
174
|
+
and field structurally untouched: each value parses back deeply equal to the one
|
|
175
|
+
it replaced. The file is rewritten with standard JSON formatting, so original
|
|
176
|
+
indentation, number spelling, and string escapes are not preserved character for
|
|
177
|
+
character.
|
|
178
|
+
|
|
179
|
+
The write is atomic: the candidate goes
|
|
180
|
+
to an owner-only temporary file beside the target and is renamed into place, so
|
|
181
|
+
no reader ever sees a half-written file. If the file changes between the read
|
|
182
|
+
and the write, the command aborts rather than overwriting the other writer.
|
|
183
|
+
|
|
184
|
+
Before `/multi-account models update`, make a byte-for-byte backup of `models.json`
|
|
185
|
+
and set its mode to owner-only (`0600`). A successful update keeps no extra copy.
|
|
186
|
+
To roll back, restore that backup to the same path and restart Pi. The older
|
|
187
|
+
declaration may remain unavailable until a later update, but physical aliases and
|
|
188
|
+
session history are unchanged.
|
|
189
|
+
|
|
190
|
+
### Switching logical models
|
|
191
|
+
|
|
192
|
+
Use the operator-only `/multi-account model` command to choose a logical model
|
|
193
|
+
without changing account policy:
|
|
194
|
+
|
|
195
|
+
```text
|
|
196
|
+
/multi-account model
|
|
197
|
+
/multi-account model gpt-5.6-luna
|
|
198
|
+
/multi-account model unified/gpt-5.6-luna
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
The deprecated `/multi-account-model [id]` alias remains for one release. It
|
|
202
|
+
prints a one-line notice pointing to `/multi-account model` and then follows the
|
|
203
|
+
same selection path.
|
|
204
|
+
|
|
205
|
+
With no argument, the command opens a searchable terminal picker. It shows only
|
|
206
|
+
currently available `unified` rows, in live catalog order, narrowed by
|
|
207
|
+
`enabledModels` or `--models` when the session has a non-empty scope. An empty
|
|
208
|
+
scope admits every available logical row. Each picker row renders
|
|
209
|
+
`<model-id> [unified]`; selection keeps the canonical bare `<model-id>` as the
|
|
210
|
+
model identity and never changes it to `unified/<model-id>`. Search is an
|
|
211
|
+
order-preserving filter over exactly five fields for each row: `unified`, the
|
|
212
|
+
legacy `pi-multi-account` search alias, `unified/<model-id>`, the bare `<model-id>`,
|
|
213
|
+
and the configured display name.
|
|
214
|
+
|
|
215
|
+
An argument must be one exact bare model ID or one exact
|
|
216
|
+
`unified/<model-id>` reference. Slashes inside a model ID are preserved.
|
|
217
|
+
Partial, fuzzy, duplicate, foreign-provider, unavailable, and out-of-scope
|
|
218
|
+
references are refused. The no-argument picker requires terminal UI; the direct
|
|
219
|
+
form remains deterministic in RPC and print contexts.
|
|
220
|
+
|
|
221
|
+
Cancellation leaves the current model unchanged. If Pi refuses or throws while
|
|
222
|
+
selecting the exact model, the command reports that refusal and leaves the
|
|
223
|
+
extension's active-model projection unchanged. Recovery output distinguishes a
|
|
224
|
+
missing declaration (install it), an unusable declaration (update it), scope,
|
|
225
|
+
availability, and invalid references. A successful `pi.setModel` call may add
|
|
226
|
+
Pi's normal `model_change` and `thinking_level_change` session entries. The
|
|
227
|
+
extension does not write those entries, settings, declarations, credentials,
|
|
228
|
+
retained state, or history, and the command is not available to the
|
|
229
|
+
`multi_account_status` agent tool.
|
|
230
|
+
|
|
231
|
+
## Exact-model route resolver
|
|
232
|
+
|
|
233
|
+
The package root exports `resolveExactModelRoutes`, the request and result
|
|
234
|
+
TypeScript types, and `publishRouteResolver` / `lookupRouteResolver`. The
|
|
235
|
+
resolver is a read-only, credential-free policy boundary for consumers that
|
|
236
|
+
need exact managed routes. It does not select Pi's active model and no current
|
|
237
|
+
foreground or reactive path consumes it.
|
|
238
|
+
|
|
239
|
+
The process-local discovery key is
|
|
240
|
+
`Symbol.for("@caair/pi-multi-account/route-resolver")`. Its stored value is
|
|
241
|
+
an immutable `{ purpose: "exact-model-routing", version: 1, resolve }` service.
|
|
242
|
+
`lookupRouteResolver()` returns exactly one of `absent`, `incompatible`, or
|
|
243
|
+
`available`. It reads only an own data-property descriptor, never invokes a
|
|
244
|
+
registry accessor, and catches hostile structural inspection. Every present
|
|
245
|
+
malformed value maps to `incompatible`. An available service may still return
|
|
246
|
+
`unresolved` with `reason: "no-eligible-routes"`; that is different from absent
|
|
247
|
+
discovery.
|
|
248
|
+
|
|
249
|
+
Version 1 requests contain `purpose`, `version`, and an exact `modelId`, with
|
|
250
|
+
optional `family`, `preferredProviderId`, and `excludedProviderIds`. A bare
|
|
251
|
+
model ID infers its family only when exactly one managed family advertises it.
|
|
252
|
+
A supplied family must advertise it. A model written as
|
|
253
|
+
`provider/model` is physical intent when `provider` is a canonical managed
|
|
254
|
+
provider ID; everything after the first slash remains the exact model ID and
|
|
255
|
+
only that provider may be returned.
|
|
256
|
+
|
|
257
|
+
A resolved result has `reason: "eligible-routes"` and an immutable ordered
|
|
258
|
+
`routes` array. Each route contains only
|
|
259
|
+
`{ providerId, modelId, family }`. An affinity preference can reorder eligible
|
|
260
|
+
routes but cannot admit one. Exclusions, disabled or cleared accounts,
|
|
261
|
+
cooldowns, invalidation, exhaustion, dead credentials, duplicate or malformed
|
|
262
|
+
accounts, unsupported model observations, and missing catalog membership are
|
|
263
|
+
filtered before serialization. Unresolved reasons are closed:
|
|
264
|
+
`invalid-input`, `unsupported-purpose`, `incompatible-version`,
|
|
265
|
+
`unknown-model`, `ambiguous-model-family`, `family-model-mismatch`, and
|
|
266
|
+
`no-eligible-routes`.
|
|
267
|
+
|
|
268
|
+
Each version-1 publication replaces the frozen public facade with a new one, but
|
|
269
|
+
every reload-safe facade — including one a consumer retained before a reload —
|
|
270
|
+
dispatches through a process-local coordinator to the newest live session owner,
|
|
271
|
+
so a consumer does not have to look the service up again. When a session shuts
|
|
272
|
+
down, its owner is revoked before Pi invalidates the extension context. Between
|
|
273
|
+
that revocation and the next publication, and after the final owner is revoked,
|
|
274
|
+
a retained facade returns `unresolved` with `reason: "no-eligible-routes"`
|
|
275
|
+
without reaching any stale context; a fresh `lookupRouteResolver()` normally
|
|
276
|
+
becomes `absent` because the public slot is deleted. A higher version is
|
|
277
|
+
retained only when its purpose matches exactly, its version is finite and
|
|
278
|
+
greater than 1, and its resolver is callable; it remains `incompatible` to this
|
|
279
|
+
version-1 lookup contract. Malformed higher, same-version, older, and
|
|
280
|
+
accessor-backed values are replaced when the registry slot is replaceable.
|
|
281
|
+
Consumers should handle `absent` and `incompatible` as no resolver, and inspect
|
|
282
|
+
the result status separately when the service is available.
|
|
283
|
+
|
|
284
|
+
The first deployment of the coordinator-backed resolver, and any rollback from
|
|
285
|
+
it, requires a full Pi process restart, not only `/reload`: a facade created by
|
|
286
|
+
the older direct-closure build is frozen around the old function and cannot be
|
|
287
|
+
rewritten in place. The reload guarantee also covers only the paths that emit
|
|
288
|
+
`session_shutdown` before invalidation — the pinned reload path and
|
|
289
|
+
`AgentSessionRuntime` replacement or disposal. A direct low-level SDK call to
|
|
290
|
+
`AgentSession.dispose()` emits no such event and stays outside the guarantee; an
|
|
291
|
+
SDK integration that loads this resolver must use `AgentSessionRuntime`, or emit
|
|
292
|
+
and await `session_shutdown`, before disposing directly.
|
|
293
|
+
|
|
294
|
+
## Accounts
|
|
295
|
+
|
|
296
|
+
The managed physical families are:
|
|
297
|
+
|
|
298
|
+
- `anthropic` — an OAuth credential is a subscription; an `api_key` credential
|
|
299
|
+
is the Anthropic owning-vendor API tier;
|
|
300
|
+
- `openai-codex` — the ChatGPT/Codex subscription tier;
|
|
301
|
+
- `openai` — the OpenAI owning-vendor API tier;
|
|
302
|
+
- `google-antigravity` — the Google Antigravity subscription tier. It has no
|
|
303
|
+
owning-vendor API tier: unlike `anthropic`/`openai`, there is no separate
|
|
304
|
+
pay-per-token `google` family, so a `google-antigravity` slot is always an
|
|
305
|
+
OAuth subscription account.
|
|
306
|
+
|
|
307
|
+
Proactive subscription routing, OAuth usage fetching, and the version-1
|
|
308
|
+
exact-model resolver remain limited to `anthropic` subscriptions and
|
|
309
|
+
`openai-codex`. The logical provider itself also routes an owning-vendor API
|
|
310
|
+
tier as a same-vendor fallback — the OpenAI `openai` API tier, and an Anthropic
|
|
311
|
+
`api_key` account as the Tier-2 destination described next. An Anthropic `api_key` account is not a Tier-1 peer and is
|
|
312
|
+
not a Codex-origin cross-vendor destination. It is reachable only as the
|
|
313
|
+
same-vendor Tier-2 destination of a turn that started on an Anthropic
|
|
314
|
+
subscription.
|
|
315
|
+
|
|
316
|
+
Each family has a base provider ID and numbered aliases from `-account-2` up to
|
|
317
|
+
`accountLimit`. With the default limit of four, the Codex IDs are
|
|
318
|
+
`openai-codex`, `openai-codex-account-2`, `openai-codex-account-3`, and
|
|
319
|
+
`openai-codex-account-4`; the canonical `google-antigravity` family follows the
|
|
320
|
+
same `google-antigravity`, `google-antigravity-account-2`, … pattern.
|
|
321
|
+
|
|
322
|
+
`accountLimit` is a whole number from `1` to `32`. `32` is the shared maximum,
|
|
323
|
+
and the limit applies independently to each managed family — `anthropic`,
|
|
324
|
+
`openai-codex`, and `google-antigravity` each get their own `1`..`accountLimit`
|
|
325
|
+
slot range under the one configured number (`MAX_ACCOUNT_LIMIT`). Startup rejects a persisted `accountLimit`
|
|
326
|
+
of `0`, `33`, a fraction, a string, or any unsafe value before it discovers any
|
|
327
|
+
account, and it rejects a canonical numbered `accountLabels` or
|
|
328
|
+
`monthlySubscriptionUsd` key above `32`. To lower the limit, remove or remap any
|
|
329
|
+
above-limit metadata key first, then re-login the account you still need into an
|
|
330
|
+
in-range slot. Stored credentials above the limit are left byte-for-byte
|
|
331
|
+
untouched; the extension never opens, moves, rewrites, or deletes them.
|
|
332
|
+
|
|
333
|
+
Authenticate each slot through Pi's public `/login` flow. The extension delegates
|
|
334
|
+
Anthropic OAuth lifecycle and ordinary requests to the in-tree
|
|
335
|
+
`pi-anthropic-oauth@0.2.5-intel.1` workspace, vendored byte-for-byte from the
|
|
336
|
+
reviewed fork commit `a52b62a05b0990ba3e2b1fa47d794b5f686435a5` before the
|
|
337
|
+
recorded workspace-only additions. For live models whose metadata requires
|
|
338
|
+
adaptive thinking, a selected reasoning level uses the extension-owned adapter.
|
|
339
|
+
Before either stream runs, the selector reconstructs Pi transcript system text
|
|
340
|
+
and active tools in the legacy context form consumed by the vendored code. The
|
|
341
|
+
base provider and numbered aliases share this selector, which sends one request
|
|
342
|
+
and never replays an adapter error. The extension re-asserts that base
|
|
343
|
+
registration once at session start, so another Anthropic OAuth extension that
|
|
344
|
+
loads later cannot restore a different request path. No separate global
|
|
345
|
+
installation is required. Codex aliases use Pi's native OAuth surface and
|
|
346
|
+
`openai-codex-responses` transport.
|
|
347
|
+
|
|
348
|
+
### Google Antigravity
|
|
349
|
+
|
|
350
|
+
`google-antigravity` resolves from the in-tree
|
|
351
|
+
`pi-antigravity@0.7.2-intel.1` workspace. Its public baseline is
|
|
352
|
+
`Rahularya01/pi-antigravity` release `v0.7.2`; the recorded local patch series
|
|
353
|
+
reconstructs the reviewed fork commit
|
|
354
|
+
`254a08d73ff8d21c3d84583587a87c0dc7ebad12`. The series projects current Pi
|
|
355
|
+
transcript system and tool changes before request
|
|
356
|
+
conversion, avoiding the observed `MALFORMED_FUNCTION_CALL` failure. This
|
|
357
|
+
repository imports only the five reviewed public barrels (`src/auth`,
|
|
358
|
+
`src/client`, `src/models`, `src/stream`, `src/usage`) and never invokes the
|
|
359
|
+
vendored root factory. The root factory's base `antigravity` provider, commands,
|
|
360
|
+
image tool, and optional connection prewarm are therefore unreachable through
|
|
361
|
+
this extension. See [`NOTICE`](NOTICE) for upstream attribution and the summary
|
|
362
|
+
of local changes.
|
|
363
|
+
|
|
364
|
+
The current catalog has eight models: `gemini-3.8-flash`, `gemini-3.7-flash`,
|
|
365
|
+
`gemini-3.6-flash`, `gemini-3.5-flash`, `gemini-3.1-pro`, `claude-opus-4-6`,
|
|
366
|
+
`claude-sonnet-4-6`, and `gpt-oss-120b`. `/multi-account add google-antigravity`
|
|
367
|
+
registers the next free numbered slot the same way as the other families, and
|
|
368
|
+
`/multi-account rediscover` picks up a credential added outside the running
|
|
369
|
+
session.
|
|
370
|
+
|
|
371
|
+
Usage fetching honors an `AbortSignal` end to end: `UsageFetcher` forwards it
|
|
372
|
+
through to the pinned fork's `fetchAccountUsage(apiKey?, { signal })`, and a
|
|
373
|
+
bounded per-attempt deadline aborts the raw call and awaits its real
|
|
374
|
+
settlement — never just the deadline — before releasing the shared machine
|
|
375
|
+
usage lease. A lease handed off to a background drain is kept renewed at the
|
|
376
|
+
lease's own interval so a peer process can never acquire it and start a
|
|
377
|
+
duplicate real call while one is still outstanding.
|
|
378
|
+
|
|
379
|
+
Google Antigravity has no supported inner-retry behavior yet, so recovery uses
|
|
380
|
+
the same conservative zero-retry default already used for every non-Anthropic
|
|
381
|
+
family; it is not a product gap specific to this family. Reported model `cost`
|
|
382
|
+
fields from the upstream usage endpoint are API-style rate estimates, not
|
|
383
|
+
retained subscription charges — this repository keeps retained provider cost
|
|
384
|
+
separate from those estimates and reports an unpriced case as `unpriced`
|
|
385
|
+
rather than guessing. The upstream usage result's raw project ID is projected
|
|
386
|
+
to a bounded digest before this repository records status, diagnostics, or
|
|
387
|
+
history; see [Storage and privacy](#storage-and-privacy).
|
|
388
|
+
|
|
389
|
+
Authenticating and exercising a real Google account end to end is an operator
|
|
390
|
+
action, not something automated verification performs: `integration:verify`
|
|
391
|
+
and this package's own tests use only isolated temporary `AuthStorage`,
|
|
392
|
+
synthetic fixture credentials, and blocked or faked network transports. See
|
|
393
|
+
Use the same operator-controlled rollout pattern used for Anthropic and Codex
|
|
394
|
+
enablement.
|
|
395
|
+
|
|
396
|
+
## Install
|
|
397
|
+
|
|
398
|
+
Pi packages run with the user's full system permissions. Review the source
|
|
399
|
+
before installation.
|
|
400
|
+
|
|
401
|
+
Install the public package globally:
|
|
402
|
+
|
|
403
|
+
```sh
|
|
404
|
+
pi install npm:@centerforagenticai/pi-multi-account
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
The package carries both modified provider forks inside its own `packages/`
|
|
408
|
+
directory. It does not fetch the private `-intel.1` package versions from a
|
|
409
|
+
registry; Pi SDK dependencies remain host peers.
|
|
410
|
+
|
|
411
|
+
Install a local checkout instead:
|
|
412
|
+
|
|
413
|
+
```sh
|
|
414
|
+
pi install /absolute/path/to/pi-multi-account
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
For a one-session test from a local checkout without changing settings:
|
|
418
|
+
|
|
419
|
+
```sh
|
|
420
|
+
npm ci
|
|
421
|
+
pi -e ./src/index.ts
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
The package manifest exposes one extension entrypoint:
|
|
425
|
+
|
|
426
|
+
```json
|
|
427
|
+
{
|
|
428
|
+
"pi": {
|
|
429
|
+
"extensions": ["./src/index.ts"]
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
Requirements:
|
|
435
|
+
|
|
436
|
+
- Node.js 22.19 or newer;
|
|
437
|
+
- a current Pi build with `agent_settled`, provider-response observation, dynamic
|
|
438
|
+
provider registration, and the public model-registry credential runtime;
|
|
439
|
+
- OAuth credentials stored through Pi for each managed slot.
|
|
440
|
+
|
|
441
|
+
The development lock used by TypeScript and unit tests pins
|
|
442
|
+
`@earendil-works/pi-ai@0.84.4` and
|
|
443
|
+
`@earendil-works/pi-coding-agent@0.84.4`. The release gate also runs the
|
|
444
|
+
extension in an isolated real Pi child. Start a new Pi process or run `/reload`
|
|
445
|
+
after installing or updating the package. Use `/multi-account rediscover` after
|
|
446
|
+
adding or changing account credentials.
|
|
447
|
+
|
|
448
|
+
## Configuration
|
|
449
|
+
|
|
450
|
+
The extension reads one machine-global file:
|
|
451
|
+
|
|
452
|
+
```text
|
|
453
|
+
$PI_CODING_AGENT_DIR/pi-multi-account/config.json
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
When `PI_CODING_AGENT_DIR` is unset, the root is `~/.pi/agent`. The extension
|
|
457
|
+
does not load project-local config files. Missing config uses conservative
|
|
458
|
+
defaults. A malformed file, including one with an unknown top-level field,
|
|
459
|
+
makes extension session initialization fail closed: Pi records a sanitized
|
|
460
|
+
diagnostic and does not rediscover managed aliases. Pi itself stays running
|
|
461
|
+
because extension event handlers are crash-isolated.
|
|
462
|
+
|
|
463
|
+
```json
|
|
464
|
+
{
|
|
465
|
+
"accountLimit": 4,
|
|
466
|
+
"sameFamilyFailover": true,
|
|
467
|
+
"crossFamilyChainEnabled": true,
|
|
468
|
+
"crossFamilyChains": [
|
|
469
|
+
{ "from": "anthropic", "to": "openai-codex" }
|
|
470
|
+
],
|
|
471
|
+
"preferredModels": {
|
|
472
|
+
"openai-codex": ["replace-with-a-supported-codex-model-id"],
|
|
473
|
+
"anthropic": ["replace-with-a-supported-anthropic-model-id"]
|
|
474
|
+
},
|
|
475
|
+
"tierModelMap": {
|
|
476
|
+
"openrouter": { "replace-with-a-source-model-id": "openrouter/replace-with-a-router-model-id" }
|
|
477
|
+
},
|
|
478
|
+
"watchdogIntervalMs": 30000,
|
|
479
|
+
"cooldownMaxMs": 300000,
|
|
480
|
+
"preemptiveExpiryWindowMs": 120000,
|
|
481
|
+
"usageFetchEnabled": {
|
|
482
|
+
"anthropic": true,
|
|
483
|
+
"openai-codex": true,
|
|
484
|
+
"google-antigravity": true
|
|
485
|
+
},
|
|
486
|
+
"accountLabels": {
|
|
487
|
+
"anthropic-account-2": "team subscription"
|
|
488
|
+
},
|
|
489
|
+
"projectLabels": {
|
|
490
|
+
"project-0123456789abcdef01234567": "example project"
|
|
491
|
+
},
|
|
492
|
+
"monthlySubscriptionUsd": {
|
|
493
|
+
"anthropic-account-2": 100,
|
|
494
|
+
"openai-codex-account-2": 20
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
Replace the two `preferredModels` values with exact model IDs from the current
|
|
500
|
+
Anthropic and OpenAI Codex catalogs. They are destination-family IDs, not
|
|
501
|
+
OpenRouter IDs.
|
|
502
|
+
|
|
503
|
+
Key behavior:
|
|
504
|
+
|
|
505
|
+
| Setting | Default | Purpose |
|
|
506
|
+
| --- | ---: | --- |
|
|
507
|
+
| `accountLimit` | `4` | Maximum slot number per managed family. A whole number from `1` to `32`. |
|
|
508
|
+
| `sameFamilyFailover` | `true` | Permit another account in the same family. |
|
|
509
|
+
| `crossFamilyChainEnabled` | `false` | Enable the directional chains listed in `crossFamilyChains`. |
|
|
510
|
+
| `crossFamilyChains` | `[]` | Allowed cross-family transitions. Six directions are recognized, each its own explicit tuple: both `anthropic`↔`openai-codex` directions, and all four directions between `google-antigravity` and each of its two managed partners (`anthropic`, `openai-codex`). Direction matters — authorizing one direction never authorizes the reverse. |
|
|
511
|
+
| `preferredModels` | `{}` | Ordered destination-family models for cross-family routing. |
|
|
512
|
+
| `tierModelMap` | `{}` | Destination-keyed cross-tier model map (`anthropic`/`openai`/`openrouter` → `{ sourceId: destId }`). The source key is the ID shown by `unified`; the value is the same model's physical destination ID. Owning-vendor API and OpenRouter routing first use an exact catalog match, then this map, and fail closed when neither resolves. IDs are bounded to 256 characters and the map holds at most 256 entries total. The `"*"` source key is rejected. |
|
|
513
|
+
| `watchdogIntervalMs` | `30000` | No-progress interval before cancelling a continuation. |
|
|
514
|
+
| `cooldownMaxMs` | `300000` | Maximum bounded cooldown. |
|
|
515
|
+
| `preemptiveExpiryWindowMs` | `120000` | Prefer a fresher same-family credential before expiry. Set `0` to disable. |
|
|
516
|
+
| `usageFetchEnabled` | all `true` | Enable fail-soft provider usage fetches by family. |
|
|
517
|
+
| `accountLabels` | `{}` | Operator display labels keyed by canonical provider ID. |
|
|
518
|
+
| `projectLabels` | `{}` | Display labels keyed by `project-<24 hex>` digest. |
|
|
519
|
+
| `monthlySubscriptionUsd` | `{}` | Legacy, retroactive monthly price for subscription-value reporting, kept readable for compatibility. Superseded by `accountRateHistory` (see `multi-account account set-plan`); reading or writing either field never converts, deletes, or migrates the other, and neither reporting surface sums them together. |
|
|
520
|
+
|
|
521
|
+
`/multi-account reload` validates and reloads this file. It assigns the complete
|
|
522
|
+
persisted config and rediscovers accounts and provider catalogs. A failed reload
|
|
523
|
+
leaves the last valid in-memory config active and returns a sanitized command
|
|
524
|
+
error.
|
|
525
|
+
|
|
526
|
+
### Configuring cross-family routing
|
|
527
|
+
|
|
528
|
+
`/multi-account configure` is the only production path that writes this file. It
|
|
529
|
+
is interactive and runs in the Pi terminal UI only. Use it to view the current
|
|
530
|
+
policy, authorize one Anthropic↔Codex direction, and set the best-first
|
|
531
|
+
destination model list for that direction.
|
|
532
|
+
|
|
533
|
+
It writes three fields: `crossFamilyChainEnabled`, `crossFamilyChains`, and
|
|
534
|
+
`preferredModels`. Every other field is carried over from a fresh read of the
|
|
535
|
+
file taken at commit time, so an unrelated setting is never lost. `tierModelMap`
|
|
536
|
+
is one of those carried-over fields: `configure` preserves it unchanged and
|
|
537
|
+
shows only a bounded destination/entry-count summary, never a per-entry editor.
|
|
538
|
+
The dialog offers every direction whose two families both have a discovered
|
|
539
|
+
account, so an Antigravity-involving direction (`anthropic`↔`google-antigravity`
|
|
540
|
+
or `google-antigravity`↔`openai-codex`) appears the same way the existing
|
|
541
|
+
Anthropic↔Codex directions do, once both sides of that pair have a
|
|
542
|
+
rediscovered account.
|
|
543
|
+
|
|
544
|
+
The write is short and cooperative:
|
|
545
|
+
|
|
546
|
+
- dialogs hold no lock. The extension takes a `config.lock` lease only after you
|
|
547
|
+
answer, with at most three attempts inside 300 ms;
|
|
548
|
+
- under that lease it re-reads the file and abandons the change if the routing
|
|
549
|
+
policy on disk moved while the dialog was open;
|
|
550
|
+
- the replacement is atomic, uses a `0700` directory, and lands at mode `0600`;
|
|
551
|
+
- the new routing policy reaches the current Pi process immediately, without
|
|
552
|
+
account rediscovery. Other running processes keep their existing policy until
|
|
553
|
+
`/multi-account reload` or a restart.
|
|
554
|
+
|
|
555
|
+
The command refuses to widen policy on its own. It authorizes one direction at a
|
|
556
|
+
time, never the reverse direction, and it rejects a model list that would leave
|
|
557
|
+
any managed destination account without a preferred model. Removing an
|
|
558
|
+
authorization still means editing the file and running `/multi-account reload`.
|
|
559
|
+
|
|
560
|
+
Duplicate `crossFamilyChains` entries are collapsed to their first occurrence
|
|
561
|
+
when the file is read. That does not change which routes are allowed. The first
|
|
562
|
+
explicit write also materializes omitted fields at their existing default
|
|
563
|
+
values.
|
|
564
|
+
|
|
565
|
+
Recover from an interrupted write or an outside edit with `/multi-account
|
|
566
|
+
reload`. If a `config.lock` file is left behind, confirm that no Pi process is
|
|
567
|
+
committing, then remove it.
|
|
568
|
+
|
|
569
|
+
## Optional OpenRouter last resort
|
|
570
|
+
|
|
571
|
+
OpenRouter requires credentials available to Pi plus three extension policy
|
|
572
|
+
variables in the non-delegate Pi session. Authenticate with Pi's OpenRouter
|
|
573
|
+
OAuth `/login` flow or provide `OPENROUTER_API_KEY`. For example:
|
|
574
|
+
|
|
575
|
+
```sh
|
|
576
|
+
export OPENROUTER_API_KEY='<secret>'
|
|
577
|
+
export PI_MULTI_ACCOUNT_OPENROUTER_ENABLED=conversation-egress
|
|
578
|
+
export PI_MULTI_ACCOUNT_OPENROUTER_MODEL='<provider>/<model-id>'
|
|
579
|
+
export PI_MULTI_ACCOUNT_OPENROUTER_DAILY_USD_LIMIT=10
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
After authenticating, run `pi --no-extensions --list-models openrouter`.
|
|
583
|
+
Copy the model column only from a row whose provider column is exactly
|
|
584
|
+
`openrouter`. The extension rejects `~`-prefixed aliases and rows from providers
|
|
585
|
+
such as `openrouter-plus`. Replace the placeholder before starting Pi.
|
|
586
|
+
|
|
587
|
+
The exact enable value records consent to send the conversation outside the two
|
|
588
|
+
managed subscription families. Any missing, malformed, or partial setting keeps
|
|
589
|
+
the rung disabled. The daily limit must be greater than zero and no more than
|
|
590
|
+
`1000`, with at most four decimal places.
|
|
591
|
+
|
|
592
|
+
`PI_MULTI_ACCOUNT_OPENROUTER_MODEL` is the default OpenRouter destination when
|
|
593
|
+
`tierModelMap.openrouter` has no entry for the turn's original model. At runtime,
|
|
594
|
+
the extension applies that default only to the current source model; it does not
|
|
595
|
+
create or accept a `"*"` map entry. A specific map entry wins. The resolved ID
|
|
596
|
+
must exist in the live OpenRouter catalog and have bounded pricing; otherwise the
|
|
597
|
+
turn parks without a reservation.
|
|
598
|
+
|
|
599
|
+
Before each metered turn, the extension reserves a conservative worst-case cost
|
|
600
|
+
from Pi's live model catalog. Reservations are machine-global, per project, and
|
|
601
|
+
per UTC day. They are stored under a bounded `project-<digest>` key in
|
|
602
|
+
`openrouter-budget.json`. A missing file means zero reserved. A malformed file,
|
|
603
|
+
failed lease, unknown price, or uncertain write disables the rung for that turn.
|
|
604
|
+
Reservations are not reduced or refunded after a response. One OpenRouter
|
|
605
|
+
provider failure disables the rung for the rest of the session.
|
|
606
|
+
|
|
607
|
+
OpenRouter is never sticky. Before another metered reservation, the extension
|
|
608
|
+
checks managed accounts again. Any available subscription account still blocks
|
|
609
|
+
OpenRouter as before. An owning-vendor API account blocks it only when that
|
|
610
|
+
account can resolve and serve the turn's original model. Delegate environments
|
|
611
|
+
identified by `PI_DELEGATE_LINEAGE_*` cannot use this rung.
|
|
612
|
+
|
|
613
|
+
## Commands and agent tool
|
|
614
|
+
|
|
615
|
+
`/multi-account` supports:
|
|
616
|
+
|
|
617
|
+
| Command | Result |
|
|
618
|
+
| --- | --- |
|
|
619
|
+
| `status [account-id\|--json]` | Health, active model, utilization, credential freshness, and metered-rung state. |
|
|
620
|
+
| `limits` | Token totals, remaining quotas, and human-readable recovery intervals. |
|
|
621
|
+
| `models [account-id]` | Available and session-rejected models by account. |
|
|
622
|
+
| `model [id]` | Select a unified logical model by exact id; with no id, show the selectable models. Replaces the deprecated `/multi-account-model`. |
|
|
623
|
+
| `cost [day\|week\|month\|quarter\|half-year\|year]` | Retained provider cost and separate API-equivalent estimates. |
|
|
624
|
+
| `log [lines]` | The latest sanitized diagnostics; default 20, maximum 100. |
|
|
625
|
+
| `rediscover` | Refresh account metadata and provider slots. |
|
|
626
|
+
| `add <family> [slot]` | Register a new OAuth login slot. Authentication still uses Pi `/login`. |
|
|
627
|
+
| `remove <account-id>` | Use Pi's public removal API when available; otherwise direct the operator to `/logout`. |
|
|
628
|
+
| `clear <account-id>` | Clear process-local state, disable the slot, and preserve credentials. |
|
|
629
|
+
| `next` | Report the next healthy account without switching. |
|
|
630
|
+
| `switch <account-id>` | Make an explicit operator-requested switch. |
|
|
631
|
+
| `stop` | Cancel parked or queued automatic continuation work. |
|
|
632
|
+
| `reset` | Clear process-local routing, usage, watchdog, continuation, and disabled state. |
|
|
633
|
+
| `reload` | Validate and reload machine-global config. |
|
|
634
|
+
| `configure` | Configure cross-family routing directions and destination models. Interactive TUI only. |
|
|
635
|
+
| `group use <id>` | Bind this session id to one configured account group. |
|
|
636
|
+
| `group reset` | Clear this session override and return to the exact-cwd default, global default, or unrestricted routing. |
|
|
637
|
+
| `group status` | Show the effective group and source plus each member's current eligible or blocked reason. |
|
|
638
|
+
| `enable` | Re-enable managed accounts and reset process-local state. |
|
|
639
|
+
| `disable <family>` | Disable one managed family in the current process. |
|
|
640
|
+
|
|
641
|
+
The registered `multi_account_status` tool exposes only `status`, `limits`,
|
|
642
|
+
`models`, `cost`, and `log`. Every `group` action and every other subcommand remains
|
|
643
|
+
operator-only. An agent can inspect general account state but cannot set or reset a
|
|
644
|
+
session group, edit cwd/global defaults, or change account policy or lifecycle state.
|
|
645
|
+
Automatic failover is a reaction to a classified provider failure;
|
|
646
|
+
discretionary account changes remain operator actions.
|
|
647
|
+
|
|
648
|
+
Account groups are named allow-lists in machine-global config. `accountGroups` maps
|
|
649
|
+
a group id to canonical account ids from any managed subscription family in
|
|
650
|
+
`ALLOWED_FAMILIES`; `accountGroupCwdDefaults` maps an exact absolute directory to
|
|
651
|
+
a configured group; `defaultAccountGroup` is
|
|
652
|
+
the optional machine-wide fallback. Resolution order is session override, exact-cwd
|
|
653
|
+
default, global default, then unrestricted. A selected unknown, empty, exhausted, or
|
|
654
|
+
otherwise ineligible group fails closed rather than widening to another account.
|
|
655
|
+
|
|
656
|
+
## Command autocomplete
|
|
657
|
+
|
|
658
|
+
Both commands offer argument suggestions as you type. Pi calls each command's
|
|
659
|
+
`getArgumentCompletions` callback with the text after the command name and
|
|
660
|
+
replaces the whole argument prefix with the chosen item's `value`. Suggestions
|
|
661
|
+
are read-only: the callback parses the prefix, never runs the command, and never
|
|
662
|
+
reads a credential or a human label. It reads model objects only to filter and
|
|
663
|
+
order logical rows, and emits and retains only named fields (a bare value and a
|
|
664
|
+
display label), never a credential, a human label, or an arbitrary model or
|
|
665
|
+
account property. It returns freshly copied items or `null`, so a failure in one
|
|
666
|
+
source suppresses suggestions rather than leaking anything.
|
|
667
|
+
|
|
668
|
+
### `/multi-account` grammar
|
|
669
|
+
|
|
670
|
+
The `/multi-account` callback walks a finite grammar. Every suggested `value` is
|
|
671
|
+
the full argument prefix to insert, for example `status --json` or
|
|
672
|
+
`add anthropic 3`, not a bare token.
|
|
673
|
+
|
|
674
|
+
- With no argument it lists all 19 subcommands in source order: `status`,
|
|
675
|
+
`limits`, `models`, `model`, `cost`, `log`, `rediscover`, `add`, `remove`,
|
|
676
|
+
`clear`, `next`, `switch`, `stop`, `reset`, `reload`, `configure`, `enable`,
|
|
677
|
+
`disable`, `group`.
|
|
678
|
+
- `status` suggests `--json` and every account ID.
|
|
679
|
+
- `models` suggests `install`, `update`, and every account ID.
|
|
680
|
+
- `model` suggests every selectable unified logical model id, with the same
|
|
681
|
+
labels and bare-id insertion as `/multi-account-model`.
|
|
682
|
+
- `remove`, `clear`, and `switch` suggest every account ID.
|
|
683
|
+
- `group` suggests `use`, `reset`, and `status`; `group use` then suggests every
|
|
684
|
+
configured group id.
|
|
685
|
+
- `cost` suggests exactly `day`, `week`, `month`, `quarter`, `half-year`, `year`.
|
|
686
|
+
- `log` suggests exactly `1`, `20`, `50`, `100`.
|
|
687
|
+
- `add` suggests `anthropic` then `openai-codex`; after a family it suggests the
|
|
688
|
+
ascending, current, free, in-range numbered slots for that family.
|
|
689
|
+
- `disable` suggests `anthropic` then `openai-codex`.
|
|
690
|
+
- `limits`, `rediscover`, `next`, `stop`, `reset`, `reload`, `configure`, and
|
|
691
|
+
`enable` take no argument and suggest nothing.
|
|
692
|
+
|
|
693
|
+
The account IDs offered are only discovered slots within the current limit that
|
|
694
|
+
also back a live model.
|
|
695
|
+
|
|
696
|
+
Add-slot suggestions follow live state. They exclude occupied, spare, and
|
|
697
|
+
out-of-range slots, skip the `openai-codex` family while it is disabled, and
|
|
698
|
+
refresh after `rediscover` or a config change. Selecting a complete prefix such
|
|
699
|
+
as `add anthropic 3` runs that exact slot. A prefix matches a suggestion by
|
|
700
|
+
fuzzy search until it exactly names one; an exact terminal token then closes to
|
|
701
|
+
`null` so a valid, complete command is not re-suggested.
|
|
702
|
+
|
|
703
|
+
### `/multi-account model` logical completion
|
|
704
|
+
|
|
705
|
+
The `/multi-account model` and deprecated `/multi-account-model` callbacks share
|
|
706
|
+
the same logical completion projection. They build a fresh logical inventory each time
|
|
707
|
+
and suggest only currently available `unified` rows, in live catalog order,
|
|
708
|
+
narrowed by session scope. Search is an order-preserving filter over `unified`,
|
|
709
|
+
the legacy `pi-multi-account` search alias, `unified/<model-id>`, the bare
|
|
710
|
+
`<model-id>`, and the configured display name. Completion labels render
|
|
711
|
+
`<model-id> [unified]`; the inserted `value` stays
|
|
712
|
+
the canonical bare `<model-id>` and is never `unified/<model-id>`. Slashes inside
|
|
713
|
+
a model ID are preserved.
|
|
714
|
+
|
|
715
|
+
A duplicate row whose bare ID collides with a prefixed form is omitted, a
|
|
716
|
+
physical (non-logical) row never appears, and a closed inventory returns `null`.
|
|
717
|
+
As with the command grammar, an exact bare ID that names a retained candidate
|
|
718
|
+
closes to `null` rather than re-suggesting the model you already typed. The
|
|
719
|
+
direct switch form still rejects a partial, fuzzy, or foreign reference: it acts
|
|
720
|
+
only on one exact bare ID or one exact `unified/<id>` reference.
|
|
721
|
+
|
|
722
|
+
### Limits
|
|
723
|
+
|
|
724
|
+
Suggestions need Pi's terminal UI. Argument autocomplete is a host non-goal in
|
|
725
|
+
RPC, JSON, and print (`pi -p`) contexts; the callbacks run zero times there and
|
|
726
|
+
handler output is identical with or without them. Re-opening the argument
|
|
727
|
+
picker with Tab after a token is a host behavior this extension does not change.
|
|
728
|
+
The built-in `/model` picker is unchanged. Only the extension-owned
|
|
729
|
+
`/multi-account model` picker rows and completion labels render `[unified]`;
|
|
730
|
+
their search includes the
|
|
731
|
+
`unified` field described above.
|
|
732
|
+
|
|
733
|
+
## Usage and cost intelligence
|
|
734
|
+
|
|
735
|
+
Usage comes from two independent record types:
|
|
736
|
+
|
|
737
|
+
- response records carry token totals and retained provider cost;
|
|
738
|
+
- provider headers and fail-soft usage fetches carry utilization, remaining
|
|
739
|
+
quota, and recovery times.
|
|
740
|
+
|
|
741
|
+
A token record with no utilization does not hide a real utilization reading.
|
|
742
|
+
Fresh local and machine-shared observations inform routing. Stale observations
|
|
743
|
+
remain display evidence only.
|
|
744
|
+
|
|
745
|
+
`/multi-account cost` reports two separate facts:
|
|
746
|
+
|
|
747
|
+
1. Pi's retained provider cost;
|
|
748
|
+
2. an API-equivalent estimate based on a bounded cached public rate snapshot.
|
|
749
|
+
|
|
750
|
+
The estimate is not a bill. Missing, stale, or malformed pricing stays
|
|
751
|
+
`unpriced`; it is never treated as zero or substituted for retained cost.
|
|
752
|
+
|
|
753
|
+
Reported cost and value figures are scoped to this extension's own retained
|
|
754
|
+
provider-response history; they never read or sum a delegate rollup. Joining
|
|
755
|
+
delegate usage remains separate future work.
|
|
756
|
+
|
|
757
|
+
Full-detail utilization-window and response-cost history is retained for 90
|
|
758
|
+
days. The first append on each UTC day removes older recognized records under
|
|
759
|
+
the history lock; a capacity-bound append repeats expiry before refusing the
|
|
760
|
+
write. Closed UTC calendar periods are immutable. Days derive from raw history; ISO weeks and months derive from days;
|
|
761
|
+
quarters, half-years, and years derive from months. A missing child period is a
|
|
762
|
+
coverage gap, not zero usage. Period closure runs on the first observation in a
|
|
763
|
+
new UTC day and before cost rendering under one machine-global lease. It does
|
|
764
|
+
not create one timer per Pi process.
|
|
765
|
+
|
|
766
|
+
## Standalone CLI
|
|
767
|
+
|
|
768
|
+
The package also ships a standalone `multi-account` shell command (`bin` entry
|
|
769
|
+
`multi-account`, `scripts/multi-account.mjs`) that runs independent of any
|
|
770
|
+
running Pi session.
|
|
771
|
+
|
|
772
|
+
### `multi-account cost`
|
|
773
|
+
|
|
774
|
+
```text
|
|
775
|
+
multi-account cost
|
|
776
|
+
multi-account cost --period quarter --timezone America/New_York --format json
|
|
777
|
+
multi-account cost --from 2026-01-01 --to 2026-04-01 --timezone UTC --format json
|
|
778
|
+
multi-account cost --from 2026-01-01T12:00:00-05:00 --to 2026-01-02T12:00:00-05:00 --format json
|
|
779
|
+
multi-account cost --all-history --format text
|
|
780
|
+
multi-account cost refresh-pricing
|
|
781
|
+
multi-account cost close-periods
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
Plain `cost` (default `--period month --format text`) is a strict read-only,
|
|
785
|
+
offline probe. It never contacts a provider, refreshes pricing, closes a
|
|
786
|
+
period, migrates configuration, edits an account rate, or creates a catalog,
|
|
787
|
+
including on an empty first run. It feeds the same pure report projection as
|
|
788
|
+
`/multi-account cost` and the `multi_account_status` agent tool. Its
|
|
789
|
+
tier-aware API-equivalent pricing draws on the same installed-catalog data
|
|
790
|
+
those surfaces use: this package's own pinned `@earendil-works/pi-ai`
|
|
791
|
+
dependency inside the standalone process, and the running session's live
|
|
792
|
+
model registry inside Pi.
|
|
793
|
+
|
|
794
|
+
`--period <day|week|month|quarter|half-year|year>`, paired `--from`/`--to`,
|
|
795
|
+
and `--all-history` are mutually exclusive; `--format` is `text` or `json`
|
|
796
|
+
(one versioned document); `--timezone` is an IANA zone (default `UTC`) that
|
|
797
|
+
sets calendar-period boundaries and local midnight for date-only custom
|
|
798
|
+
bounds. Account-cost allocation always splits at UTC calendar-month
|
|
799
|
+
boundaries regardless of this display timezone. A custom bound is an ISO date
|
|
800
|
+
(`YYYY-MM-DD`) or an RFC 3339 timestamp with an explicit `Z` or numeric
|
|
801
|
+
offset; the range is start-inclusive and end-exclusive.
|
|
802
|
+
|
|
803
|
+
`multi-account cost refresh-pricing` and `multi-account cost close-periods`
|
|
804
|
+
are explicit write actions, kept separate from the read-only report path.
|
|
805
|
+
They reach the same authorized OpenRouter pricing cache and machine-leased
|
|
806
|
+
period closer the existing slash/tool observation-driven closure already uses.
|
|
807
|
+
|
|
808
|
+
Exit codes are stable across every standalone command: `0` success
|
|
809
|
+
(including a declined confirmation or a truthful empty report), `1` an
|
|
810
|
+
unexpected internal failure, `2` a syntax, argument, or domain validation
|
|
811
|
+
failure, `3` when retained data cannot satisfy the requested precision, `4`
|
|
812
|
+
for corrupt retained cost history, and `5` for an explicit
|
|
813
|
+
`refresh-pricing`/`close-periods` action failure. Text or JSON goes to
|
|
814
|
+
standard output; diagnostics go to standard error.
|
|
815
|
+
|
|
816
|
+
### `multi-account account set-plan`
|
|
817
|
+
|
|
818
|
+
```text
|
|
819
|
+
multi-account account set-plan <account-id> --type <preset-id> --effective-from <timestamp> [--monthly-usd <amount>]
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
Assigns a shipped or operator-added catalog preset as an account's new
|
|
823
|
+
effective rate record. `--effective-from` is always required as an RFC 3339
|
|
824
|
+
instant with an explicit `Z` or numeric offset; it is never inferred from a
|
|
825
|
+
history start or renewal boundary. The command resolves the preset, prints a
|
|
826
|
+
preview naming the account, provider, account type, preset, monthly rate,
|
|
827
|
+
effective instant, and catalog version, then asks for a literal `y`/`yes`
|
|
828
|
+
confirmation before writing anything. `--monthly-usd` overrides the preset's
|
|
829
|
+
default rate; an explicit `0` is a valid rate and differs from having no rate
|
|
830
|
+
at all. A later catalog edit never rewrites an existing rate record or a past
|
|
831
|
+
report result.
|
|
832
|
+
|
|
833
|
+
This command never migrates the legacy `monthlySubscriptionUsd` value: it does
|
|
834
|
+
not read, convert, or delete it, and it never invents an `--effective-from`
|
|
835
|
+
from a history start or renewal boundary. Recording rate history for an
|
|
836
|
+
account that already has a legacy value requires this explicit,
|
|
837
|
+
operator-supplied instant; there is no automatic migration path.
|
|
838
|
+
|
|
839
|
+
Run `multi-account --help`, `multi-account cost --help`, or
|
|
840
|
+
`multi-account account set-plan --help` for the exact current grammar.
|
|
841
|
+
|
|
842
|
+
## Storage and privacy
|
|
843
|
+
|
|
844
|
+
All files live below `$PI_CODING_AGENT_DIR/pi-multi-account/` or the equivalent
|
|
845
|
+
`~/.pi/agent/pi-multi-account/` default.
|
|
846
|
+
|
|
847
|
+
| File | Default file bound | Contents |
|
|
848
|
+
| --- | ---: | --- |
|
|
849
|
+
| `config.json` | operator-managed | Configuration; no credential values. |
|
|
850
|
+
| `config.lock` | 2 KiB | Short machine-global lease, mode `0600`, held only while `configure` commits. |
|
|
851
|
+
| `usage.ndjson` | 512 KiB | Per-account token and rate-limit observations shared across Pi processes. |
|
|
852
|
+
| `diagnostics.ndjson` | 1 MiB | Sanitized routing events, mode `0600`; compaction keeps the newest complete records. |
|
|
853
|
+
| `window-history.ndjson` | 128 MiB, 90 days | Utilization-window history. |
|
|
854
|
+
| `cost-history.ndjson` | ≤512 MiB, 90 days | Bounded response-cost observations; the byte cap never exceeds Node's safe decoded-string limit. |
|
|
855
|
+
| `*-history.ndjson.retention` | one integer line | UTC-day marker that limits ordinary expiry compaction to once per store per day. |
|
|
856
|
+
| `cost-period-digest.ndjson` | 64 MiB | Immutable closed-period project/account/model rollups. |
|
|
857
|
+
| `api-pricing.json` | 1,000,000 bytes | Cached public pricing data. |
|
|
858
|
+
| `openrouter-budget.json` | 64 KiB, 512 projects | Per-project UTC-day worst-case OpenRouter reservations; inactive entries expire after seven days. |
|
|
859
|
+
| `declaration-notice.json` | 1 KiB | UTC-day and stale/not-installed condition for the throttled startup warning. |
|
|
860
|
+
|
|
861
|
+
History expiry is append-driven. The first append to each store on a new UTC day
|
|
862
|
+
removes recognized records older than 90 days. A capacity-bound append may repeat
|
|
863
|
+
that check during the same day. An idle store retains its existing bytes until
|
|
864
|
+
its next append.
|
|
865
|
+
|
|
866
|
+
Stop every running Pi process before upgrading across the `usage.ndjson` append-cap
|
|
867
|
+
change, then restart them. Older processes do not take the new usage mutation
|
|
868
|
+
lease. Mixing old and new processes can bypass the cap or race usage compaction.
|
|
869
|
+
|
|
870
|
+
Observation, diagnostic, digest, pricing, and budget stores may contain
|
|
871
|
+
canonical provider IDs, model IDs, token and cost numbers, timestamps, bounded
|
|
872
|
+
error categories, and `project-<digest>` keys. They do not contain OAuth tokens,
|
|
873
|
+
API keys, authorization headers, request or response bodies, prompts, assistant
|
|
874
|
+
text, tool content, request IDs, raw provider errors, or raw project paths.
|
|
875
|
+
Human account and project labels remain in `config.json`; renderers resolve them
|
|
876
|
+
at output time instead of copying them into retained observations.
|
|
877
|
+
|
|
878
|
+
Persistent diagnostics reuse the same sanitizer as `/multi-account log`.
|
|
879
|
+
Oversized, malformed, or unsafe records are rejected. Storage failures are
|
|
880
|
+
fail-soft for managed subscription routing and fail-closed for a metered budget
|
|
881
|
+
reservation.
|
|
882
|
+
|
|
883
|
+
## Architecture
|
|
884
|
+
|
|
885
|
+
| Layer | Modules |
|
|
886
|
+
| --- | --- |
|
|
887
|
+
| Provider integration | `upstream-anthropic.ts`, `anthropic-context-compat.ts`, `anthropic-alias-stream.ts`, `codex-adapter.ts`, `provider-registration.ts`, `catalog-rebinding.ts` |
|
|
888
|
+
| Account discovery and credentials | `discovery.ts`, `credential-lifecycle.ts`, `credential-refresh.ts`, `warmer.ts`, `account-labels.ts` |
|
|
889
|
+
| Routing policy | `runtime-state.ts`, `error-classification.ts`, `cooldowns.ts`, `routing.ts`, `preflight.ts`, `model-support.ts` |
|
|
890
|
+
| Continuation lifecycle | `continuation.ts`, `watchdog.ts`, `compaction.ts`, `lifecycle.ts` |
|
|
891
|
+
| Operator surfaces | `commands.ts`, `command-completions.ts`, `fuzzy.ts`, `status-view.ts`, `logical-route-indicator.ts`, `diagnostics.ts`, `diagnostic-store.ts`, `logical-model-switcher.ts`, `logical-model-selector.ts` |
|
|
892
|
+
| Usage and shared state | `usage.ts`, `shared-usage.ts`, `usage-fetch.ts`, `window-history.ts`, `history-store.ts`, `machine-lease.ts` |
|
|
893
|
+
| Cost intelligence | `cost-history.ts`, `cost-digest.ts`, `cost-digest-store.ts`, `cost-period-closer.ts`, `cost-report*.ts`, `coverage-attestation.ts` |
|
|
894
|
+
| Metered last resort | `openrouter-fallback.ts`, `openrouter-budget.ts`, `pricing-cache.ts`, `api-pricing.ts` |
|
|
895
|
+
| Composition | `index.ts` |
|
|
896
|
+
|
|
897
|
+
The extension does not register `before_provider_request`. The captured in-tree
|
|
898
|
+
Anthropic configuration still owns OAuth callbacks, headers, and ordinary
|
|
899
|
+
requests. A narrow stateless selector sends adaptive requests to the
|
|
900
|
+
MIT-licensed local adapter and leaves every other request on the vendored
|
|
901
|
+
`0.2.5-intel.1` stream. Before invoking either stream, the selector reconstructs current Pi
|
|
902
|
+
transcript system text and active tool declarations in the legacy context form
|
|
903
|
+
that both implementations consume. The adaptive adapter imports the vendored
|
|
904
|
+
prompt, message, and tool helpers and changes only adaptive-thinking fields.
|
|
905
|
+
The base provider and numbered aliases use that same selector; it does not
|
|
906
|
+
replay errors. Because Pi merges a later provider registration over an earlier
|
|
907
|
+
one, the extension registers that base configuration again once at session
|
|
908
|
+
start. This is its only permitted base-provider exception.
|
|
909
|
+
|
|
910
|
+
## Known boundaries
|
|
911
|
+
|
|
912
|
+
- Pi defaults to three retries for a retryable provider error before
|
|
913
|
+
`agent_settled`; Pi settings can change that count. This extension cannot
|
|
914
|
+
suppress the host retry layer.
|
|
915
|
+
- Same-turn replay is intentionally excluded. Recovery uses a fixed continuation
|
|
916
|
+
message after settlement.
|
|
917
|
+
- Provider usage endpoints are not guaranteed to be available. Fetches time out,
|
|
918
|
+
fail soft, and report their bounded state.
|
|
919
|
+
- Only sessions that load this extension can use its failover lifecycle. At this
|
|
920
|
+
release, the `pi-delegate` package may load project extensions in workers, but
|
|
921
|
+
its isolated supervisor and collapse sessions run with extensions disabled.
|
|
922
|
+
Provider errors from those calls are outside this package.
|
|
923
|
+
- The current `pi-fork-delegate` repository's worker runtime can discover
|
|
924
|
+
numbered alias models yet fail to resolve their alias credentials. Use a base
|
|
925
|
+
provider for delegated work until the upstream runtime fix is deployed and
|
|
926
|
+
verified.
|
|
927
|
+
- OpenRouter is a non-delegate-session escape hatch, not a delegate fallback and
|
|
928
|
+
not a replacement for managed OAuth accounts.
|
|
929
|
+
- Cross-family subscription model IDs are not portable. Configure
|
|
930
|
+
`preferredModels` for every enabled subscription destination family or accept
|
|
931
|
+
its catalog head. Owning-vendor API and OpenRouter routes instead use
|
|
932
|
+
`tierModelMap` and fail closed rather than choosing a catalog head.
|
|
933
|
+
- The `config.lock` lease binds cooperating Pi processes. An editor that ignores
|
|
934
|
+
it can still replace the file. `configure` narrows that window by re-reading
|
|
935
|
+
under the lease, but a change landing between that read and the atomic rename
|
|
936
|
+
is not detectable.
|
|
937
|
+
- `google-antigravity` has no supported inner-retry behavior yet, so recovery
|
|
938
|
+
reserves zero inner retries for it, the same conservative default already
|
|
939
|
+
used for `openai` and `openai-codex`.
|
|
940
|
+
- Automated verification never performs a real Google sign-in. Exercising a
|
|
941
|
+
live `google-antigravity` account end to end is an operator action performed
|
|
942
|
+
after `integration:verify` passes, the same deferred pattern already used for
|
|
943
|
+
Anthropic and Codex.
|
|
944
|
+
|
|
945
|
+
## Verification
|
|
946
|
+
|
|
947
|
+
The canonical release gate is `integration:verify`. Its child smoke requires
|
|
948
|
+
the current Pi session path. Run Pi's public `/session` command in this checkout
|
|
949
|
+
and copy its `File:` value:
|
|
950
|
+
|
|
951
|
+
```sh
|
|
952
|
+
npm ci
|
|
953
|
+
PI_BIN="$(command -v pi)" \
|
|
954
|
+
PI_MULTI_ACCOUNT_CURRENT_SESSION='/absolute/path/from-pi-session.jsonl' \
|
|
955
|
+
npm run integration:verify
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
`PI_BIN` is required whenever the Pi executable is not beside `process.execPath`.
|
|
959
|
+
The child smoke looks only next to the running Node binary, so a Homebrew or
|
|
960
|
+
nvm-managed Node needs this override. The session file must be real and have a
|
|
961
|
+
recorded cwd matching this checkout.
|
|
962
|
+
|
|
963
|
+
The gate runs TypeScript, all Vitest tests with file parallelism disabled, the
|
|
964
|
+
credential canary, the isolated child smoke, `upstream:check`, and
|
|
965
|
+
`npm pack --dry-run`. `upstream:check` verifies both exact bundled provider
|
|
966
|
+
dependencies and their workspace links, each package's structured `UPSTREAM.md`,
|
|
967
|
+
and every file in its recorded baseline manifest. It reverse-applies each
|
|
968
|
+
package's recorded patch series, verifies the clean baseline, applies the series
|
|
969
|
+
forward, and compares the result with the vendored package. The check makes no
|
|
970
|
+
network request and fails on an unrecorded changed, added, or removed file,
|
|
971
|
+
including files below nested `node_modules` or `.test-dist` directories. Only
|
|
972
|
+
those generated directories at a package root are excluded.
|
|
973
|
+
`npm run upstream:verify-baseline` is the
|
|
974
|
+
separate network check that fetches each recorded upstream commit; it is not part
|
|
975
|
+
of `check` or the CI gate.
|
|
976
|
+
|
|
977
|
+
The Vitest suite's `actual-delegate-runtime` case requires `PI_DELEGATE_PACKAGE_DIR`
|
|
978
|
+
(the installed `pi-delegate` package root) and is otherwise skipped, so the
|
|
979
|
+
release gate additionally requires that variable to be set, failing closed
|
|
980
|
+
rather than passing green on a silently skipped probe. That case measures the
|
|
981
|
+
delegate worker's own bundled Pi SDK version at runtime instead of assuming it
|
|
982
|
+
matches the host `PI_BIN` — this repository's development lock pins the host to
|
|
983
|
+
`@earendil-works/pi-coding-agent@0.84.4`, but a worker session resolves its own
|
|
984
|
+
installed copy independently, and the two are not assumed equal.
|
|
985
|
+
|
|
986
|
+
The smoke starts real offline Pi child processes with fresh `HOME` and
|
|
987
|
+
`PI_CODING_AGENT_DIR` directories. It blocks external sockets and model prompts,
|
|
988
|
+
checks production-only loading and both coexistence orders, and verifies that
|
|
989
|
+
the selected settings, `AuthStorage`, and invoking session files do not change.
|
|
990
|
+
|
|
991
|
+
Every behavioral change must also pass a mutation control: break production in
|
|
992
|
+
the specific way the defect would occur, prove a named test fails, then restore
|
|
993
|
+
from a backup and rerun the test.
|
|
994
|
+
|
|
995
|
+
## License
|
|
996
|
+
|
|
997
|
+
Our code is available under the [MIT License](LICENSE). This package also
|
|
998
|
+
contains modified MIT-licensed copies of `pi-anthropic-oauth` and
|
|
999
|
+
`pi-antigravity`; see [NOTICE](NOTICE) and each fork's included `LICENSE` file.
|