@oh-my-pi/pi-ai 18.3.0 → 18.3.2
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 +20 -0
- package/THIRD-PARTY-NOTICES.txt +2 -2
- package/dist/types/auth/policy.d.ts +8 -1
- package/dist/types/auth/pool.d.ts +8 -0
- package/dist/types/auth/types.d.ts +17 -7
- package/dist/types/auth/usage.d.ts +2 -0
- package/dist/types/auth-broker/discover.d.ts +22 -1
- package/dist/types/auth-storage.d.ts +35 -11
- package/dist/types/error/rate-limit.d.ts +3 -2
- package/dist/types/index.d.ts +1 -0
- package/dist/types/providers/anthropic-slow-mode.d.ts +109 -0
- package/dist/types/providers/anthropic-wire.d.ts +4 -0
- package/dist/types/providers/mock.d.ts +3 -1
- package/dist/types/providers/openai-codex/live-steering.d.ts +77 -0
- package/dist/types/providers/transform-messages.d.ts +9 -1
- package/dist/types/types.d.ts +56 -0
- package/dist/types/usage/xai-oauth.d.ts +6 -1
- package/dist/types/utils/http-inspector.d.ts +6 -0
- package/package.json +6 -6
- package/src/auth/policy.ts +27 -6
- package/src/auth/pool.ts +35 -2
- package/src/auth/refresh.ts +2 -2
- package/src/auth/types.ts +17 -7
- package/src/auth/usage.ts +5 -0
- package/src/auth-broker/discover.ts +57 -27
- package/src/auth-storage.ts +121 -35
- package/src/error/flags.ts +2 -1
- package/src/error/rate-limit.ts +8 -4
- package/src/index.ts +1 -0
- package/src/providers/anthropic-slow-mode.ts +232 -0
- package/src/providers/anthropic-wire.ts +4 -0
- package/src/providers/anthropic.ts +322 -22
- package/src/providers/cowork-fetch.ts +11 -4
- package/src/providers/google-shared.ts +30 -7
- package/src/providers/inference-headers.ts +7 -1
- package/src/providers/mock.ts +4 -0
- package/src/providers/openai-codex/live-steering.ts +237 -0
- package/src/providers/openai-codex-responses.ts +372 -73
- package/src/providers/transform-messages.ts +27 -7
- package/src/stream.ts +32 -2
- package/src/types.ts +59 -0
- package/src/usage/registry.ts +2 -1
- package/src/usage/xai-oauth.ts +31 -1
- package/src/utils/http-inspector.ts +21 -2
- package/src/utils/openrouter-headers.ts +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-ai",
|
|
3
|
-
"version": "18.3.
|
|
3
|
+
"version": "18.3.2",
|
|
4
4
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -155,11 +155,11 @@
|
|
|
155
155
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
156
156
|
},
|
|
157
157
|
"dependencies": {
|
|
158
|
-
"@oh-my-pi/omptype": "18.3.
|
|
159
|
-
"@oh-my-pi/pi-catalog": "18.3.
|
|
160
|
-
"@oh-my-pi/pi-natives": "18.3.
|
|
161
|
-
"@oh-my-pi/pi-utils": "18.3.
|
|
162
|
-
"@oh-my-pi/pi-wire": "18.3.
|
|
158
|
+
"@oh-my-pi/omptype": "18.3.2",
|
|
159
|
+
"@oh-my-pi/pi-catalog": "18.3.2",
|
|
160
|
+
"@oh-my-pi/pi-natives": "18.3.2",
|
|
161
|
+
"@oh-my-pi/pi-utils": "18.3.2",
|
|
162
|
+
"@oh-my-pi/pi-wire": "18.3.2"
|
|
163
163
|
},
|
|
164
164
|
"devDependencies": {
|
|
165
165
|
"@types/bun": "^1.3.14"
|
package/src/auth/policy.ts
CHANGED
|
@@ -22,20 +22,41 @@ export function matchesAuthAccountSelector(selector: AuthAccountSelector, identi
|
|
|
22
22
|
/** Validated per-account routing policies (priority/reserve) plus the global reserve fallback. */
|
|
23
23
|
export class AccountPolicies {
|
|
24
24
|
#accountPolicies: AuthAccountPolicies;
|
|
25
|
-
|
|
25
|
+
#defaultReservePct: number;
|
|
26
26
|
|
|
27
27
|
constructor(policies: AuthAccountPolicies, defaultReservePct: number | undefined) {
|
|
28
|
+
AccountPolicies.#validateAccountPolicyConfiguration(policies);
|
|
28
29
|
this.#accountPolicies = policies;
|
|
29
|
-
this
|
|
30
|
+
this.#defaultReservePct =
|
|
30
31
|
typeof defaultReservePct === "number" && Number.isFinite(defaultReservePct)
|
|
31
32
|
? Math.max(0, Math.min(100, defaultReservePct))
|
|
32
33
|
: DEFAULT_USAGE_RESERVE_PCT;
|
|
33
|
-
this.#validateAccountPolicyConfiguration();
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
/** Global usage reserve (0–100) for accounts without a per-account `reservePct`. */
|
|
37
|
+
get defaultReservePct(): number {
|
|
38
|
+
return this.#defaultReservePct;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Replace the policy set and global reserve in place (live settings change).
|
|
43
|
+
* Validates the configuration and every provider in `storedCredentials` before
|
|
44
|
+
* committing; on error the previous policies stay active.
|
|
45
|
+
*/
|
|
46
|
+
replace(
|
|
47
|
+
policies: AuthAccountPolicies,
|
|
48
|
+
defaultReservePct: number | undefined,
|
|
49
|
+
storedCredentials: ReadonlyMap<string, readonly AuthCredential[]> = new Map(),
|
|
50
|
+
): void {
|
|
51
|
+
const next = new AccountPolicies(policies, defaultReservePct);
|
|
52
|
+
for (const [provider, credentials] of storedCredentials) next.validateFor(provider, credentials);
|
|
53
|
+
this.#accountPolicies = next.#accountPolicies;
|
|
54
|
+
this.#defaultReservePct = next.#defaultReservePct;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static #validateAccountPolicyConfiguration(accountPolicies: AuthAccountPolicies): void {
|
|
58
|
+
for (let index = 0; index < accountPolicies.length; index += 1) {
|
|
59
|
+
const policy = accountPolicies[index]!;
|
|
39
60
|
const path = `auth.accountPolicies[${index}]`;
|
|
40
61
|
if (
|
|
41
62
|
typeof policy.provider !== "string" ||
|
package/src/auth/pool.ts
CHANGED
|
@@ -31,6 +31,23 @@ function fingerprintOAuthBearer(bearer: string): string {
|
|
|
31
31
|
/** One stored credential row as cached in memory. */
|
|
32
32
|
export type StoredCredential = { id: number; credential: AuthCredential };
|
|
33
33
|
|
|
34
|
+
/** {@link CredentialDisabledEvent} for a torn-down row, carrying the account identity it was signed in as. */
|
|
35
|
+
export function credentialDisabledEvent(
|
|
36
|
+
provider: string,
|
|
37
|
+
row: StoredCredential,
|
|
38
|
+
disabledCause: string,
|
|
39
|
+
): CredentialDisabledEvent {
|
|
40
|
+
const event: CredentialDisabledEvent = { provider, disabledCause, credentialId: row.id };
|
|
41
|
+
const { credential } = row;
|
|
42
|
+
if (credential.type === "oauth") {
|
|
43
|
+
if (credential.email) event.email = credential.email;
|
|
44
|
+
if (credential.accountId) event.accountId = credential.accountId;
|
|
45
|
+
if (credential.orgId) event.orgId = credential.orgId;
|
|
46
|
+
if (credential.orgName) event.orgName = credential.orgName;
|
|
47
|
+
}
|
|
48
|
+
return event;
|
|
49
|
+
}
|
|
50
|
+
|
|
34
51
|
/** Credential equality used for snapshot change detection. */
|
|
35
52
|
export function authCredentialEquals(left: AuthCredential, right: AuthCredential): boolean {
|
|
36
53
|
if (left.type !== right.type) return false;
|
|
@@ -189,6 +206,19 @@ export class CredentialPool implements CredentialsApi {
|
|
|
189
206
|
}
|
|
190
207
|
}
|
|
191
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Take over the subscribers, buffered disable events, and generation counter of
|
|
211
|
+
* the pool this one replaces (store swap). Listener sets are shared, so
|
|
212
|
+
* unsubscribe functions handed out by `previous` keep working.
|
|
213
|
+
*/
|
|
214
|
+
adoptSubscribers(previous: CredentialPool): void {
|
|
215
|
+
this.#credentialDisabledListeners = previous.#credentialDisabledListeners;
|
|
216
|
+
this.#generationListeners = previous.#generationListeners;
|
|
217
|
+
this.#pendingDisabledEvents = previous.#pendingDisabledEvents;
|
|
218
|
+
previous.#pendingDisabledEvents = [];
|
|
219
|
+
this.#generation = previous.#generation;
|
|
220
|
+
}
|
|
221
|
+
|
|
192
222
|
onGeneration(listener: (generation: number) => void): () => void {
|
|
193
223
|
this.#generationListeners.add(listener);
|
|
194
224
|
return () => {
|
|
@@ -430,7 +460,7 @@ export class CredentialPool implements CredentialsApi {
|
|
|
430
460
|
const updated = entries.filter((_value, idx) => idx !== index);
|
|
431
461
|
this.replace(provider, updated);
|
|
432
462
|
this.reset(provider);
|
|
433
|
-
this.emitDisabled(
|
|
463
|
+
this.emitDisabled(credentialDisabledEvent(provider, target, disabledCause));
|
|
434
464
|
return true;
|
|
435
465
|
}
|
|
436
466
|
|
|
@@ -481,6 +511,9 @@ export class CredentialPool implements CredentialsApi {
|
|
|
481
511
|
}
|
|
482
512
|
|
|
483
513
|
emitDisabled(event: CredentialDisabledEvent): void {
|
|
514
|
+
// Every automatic disable leaves a log line, with or without subscribers: the event
|
|
515
|
+
// alone left nothing on disk once a session had moved on to a sibling account.
|
|
516
|
+
logger.warn("Auth credential disabled", { ...event });
|
|
484
517
|
if (this.#credentialDisabledListeners.size === 0) {
|
|
485
518
|
// No subscribers — buffer for later replay. Cap the backlog so a process that runs
|
|
486
519
|
// without subscribers for a long time can't grow memory unboundedly; drop oldest
|
|
@@ -721,7 +754,7 @@ export class CredentialPool implements CredentialsApi {
|
|
|
721
754
|
const next = entries.filter((_value, idx) => idx !== index);
|
|
722
755
|
this.replace(provider, next);
|
|
723
756
|
this.reset(provider);
|
|
724
|
-
this.emitDisabled(
|
|
757
|
+
this.emitDisabled(credentialDisabledEvent(provider, entries[index]!, disabledCause));
|
|
725
758
|
return true;
|
|
726
759
|
}
|
|
727
760
|
return false;
|
package/src/auth/refresh.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { getOAuthProvider, normalizeOAuthCredentialExpiry, refreshOAuthToken } f
|
|
|
4
4
|
import type { OAuthCredentials, OAuthProvider } from "../registry/oauth/types";
|
|
5
5
|
import type { Provider } from "../types";
|
|
6
6
|
import { raceSignal } from "./abort";
|
|
7
|
-
import { authCredentialEquals, type CredentialPool } from "./pool";
|
|
7
|
+
import { authCredentialEquals, type CredentialPool, credentialDisabledEvent } from "./pool";
|
|
8
8
|
import type { AccountPolicies } from "./policy";
|
|
9
9
|
import { resolveCredentialIdentityKey, serializeCredential } from "./sqlite-credential-store";
|
|
10
10
|
import { hasRefreshLeases, type AuthCredentialStore } from "./store";
|
|
@@ -226,7 +226,7 @@ export class OAuthRefresher {
|
|
|
226
226
|
})),
|
|
227
227
|
);
|
|
228
228
|
this.#deps.pool.reset(provider);
|
|
229
|
-
this.#deps.pool.emitDisabled(
|
|
229
|
+
this.#deps.pool.emitDisabled(credentialDisabledEvent(provider, row, disabledCause));
|
|
230
230
|
return { credential: undefined, refreshed: false, removed: true };
|
|
231
231
|
}
|
|
232
232
|
await this.#deps.pool.reload();
|
package/src/auth/types.ts
CHANGED
|
@@ -300,9 +300,11 @@ export interface AuthCredentialSnapshot {
|
|
|
300
300
|
/**
|
|
301
301
|
* Event payload describing a credential that was just soft-disabled.
|
|
302
302
|
*
|
|
303
|
-
*
|
|
304
|
-
* (`invalid_grant`, `401/403` not from a network blip, etc.)
|
|
305
|
-
*
|
|
303
|
+
* Fired for automatic disables: a definitive OAuth refresh failure
|
|
304
|
+
* (`invalid_grant`, `401/403` not from a network blip, etc.), an upstream
|
|
305
|
+
* token invalidation, and an auth-broker disable. The disabled_cause string is
|
|
306
|
+
* the verbatim error captured for forensics. Every emission is also logged as
|
|
307
|
+
* a warning.
|
|
306
308
|
*
|
|
307
309
|
* Subscribers can use this to surface a notification, banner, or auto-launch
|
|
308
310
|
* a re-login flow instead of letting the credential silently disappear.
|
|
@@ -310,6 +312,14 @@ export interface AuthCredentialSnapshot {
|
|
|
310
312
|
export interface CredentialDisabledEvent {
|
|
311
313
|
provider: string;
|
|
312
314
|
disabledCause: string;
|
|
315
|
+
/** Database row id of the disabled credential (matches {@link StoredAuthCredential.id}). */
|
|
316
|
+
credentialId?: number;
|
|
317
|
+
/** Account identity recorded on the disabled OAuth credential, when the provider supplied one. */
|
|
318
|
+
email?: string;
|
|
319
|
+
accountId?: string;
|
|
320
|
+
/** Organization/workspace the credential was scoped to (Anthropic/ChatGPT multi-subscription). */
|
|
321
|
+
orgId?: string;
|
|
322
|
+
orgName?: string;
|
|
313
323
|
}
|
|
314
324
|
|
|
315
325
|
/** Configuration supplied when constructing credential storage. */
|
|
@@ -330,10 +340,10 @@ export type AuthStorageOptions = {
|
|
|
330
340
|
configValueResolver?: (config: string) => Promise<string | undefined>;
|
|
331
341
|
/**
|
|
332
342
|
* Optional callback fired when AuthStorage automatically disables a
|
|
333
|
-
* credential because something detected it as no longer usable
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
343
|
+
* credential because something detected it as no longer usable (see
|
|
344
|
+
* {@link CredentialDisabledEvent}). NOT fired for user-initiated `remove()`
|
|
345
|
+
* (the user already knows) or dedup of duplicate credentials
|
|
346
|
+
* (uninteresting hygiene).
|
|
337
347
|
*/
|
|
338
348
|
onCredentialDisabled?: (event: CredentialDisabledEvent) => void | Promise<void>;
|
|
339
349
|
/**
|
package/src/auth/usage.ts
CHANGED
|
@@ -151,6 +151,11 @@ export class UsageService implements UsageApi {
|
|
|
151
151
|
});
|
|
152
152
|
this.#deps.cache.invalidateForProvider(provider);
|
|
153
153
|
}
|
|
154
|
+
/** Carry runtime usage provider overrides over from the service this one replaces (store swap). */
|
|
155
|
+
adoptRuntimeProviders(previous: UsageService): void {
|
|
156
|
+
this.#runtimeUsageProviderOverrides = previous.#runtimeUsageProviderOverrides;
|
|
157
|
+
}
|
|
158
|
+
|
|
154
159
|
/** Remove a runtime usage provider override and restore configured/default resolution. */
|
|
155
160
|
removeProvider(provider: Provider): void {
|
|
156
161
|
if (!this.#runtimeUsageProviderOverrides.has(provider)) return;
|
|
@@ -19,9 +19,11 @@ import {
|
|
|
19
19
|
import { YAML } from "bun";
|
|
20
20
|
import {
|
|
21
21
|
type AuthAccountPolicies,
|
|
22
|
+
type AuthCredentialStore,
|
|
22
23
|
AuthStorage,
|
|
23
24
|
type AuthStorageOptions,
|
|
24
25
|
DEFAULT_USAGE_RESERVE_PCT,
|
|
26
|
+
SqliteAuthCredentialStore,
|
|
25
27
|
} from "../auth-storage";
|
|
26
28
|
import * as AIError from "../error";
|
|
27
29
|
import { AuthBrokerClient, AuthBrokerError } from "./client";
|
|
@@ -363,23 +365,32 @@ export async function resolveAuthBrokerConfig(
|
|
|
363
365
|
return { url, token };
|
|
364
366
|
}
|
|
365
367
|
|
|
368
|
+
export interface OpenAuthCredentialStoreOptions {
|
|
369
|
+
/** Broker to connect to; `null` opens the local SQLite store under `agentDir`. */
|
|
370
|
+
brokerConfig: AuthBrokerClientConfig | null;
|
|
371
|
+
agentDir?: string;
|
|
372
|
+
cachePath?: string;
|
|
373
|
+
sourceLabel?: string;
|
|
374
|
+
/** Programmatic pool for SDK hosts. Takes precedence over the environment file. */
|
|
375
|
+
accountPool?: AuthBrokerAccountPool;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** Credential store opened by {@link openAuthCredentialStore} plus its diagnostics label. */
|
|
379
|
+
export interface OpenedAuthCredentialStore {
|
|
380
|
+
store: AuthCredentialStore;
|
|
381
|
+
sourceLabel: string;
|
|
382
|
+
}
|
|
383
|
+
|
|
366
384
|
/**
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
385
|
+
* Open the credential store {@link discoverAuthStorage} would use for
|
|
386
|
+
* `brokerConfig`: the remote broker store (fails fast when the broker has no
|
|
387
|
+
* usable snapshot) or the local SQLite store. Also feeds
|
|
388
|
+
* {@link AuthStorage.replaceStore} when broker settings change at runtime.
|
|
370
389
|
*/
|
|
371
|
-
export async function
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
configValueResolver: options.configValueResolver,
|
|
376
|
-
});
|
|
377
|
-
const { accountPolicies, defaultReservePct } = await loadAuthAccountPolicyConfig({
|
|
378
|
-
agentDir,
|
|
379
|
-
accountPolicies: options.accountPolicies,
|
|
380
|
-
usageReservePct: options.authStorageOptions?.defaultReservePct,
|
|
381
|
-
});
|
|
382
|
-
|
|
390
|
+
export async function openAuthCredentialStore(
|
|
391
|
+
options: OpenAuthCredentialStoreOptions,
|
|
392
|
+
): Promise<OpenedAuthCredentialStore> {
|
|
393
|
+
const brokerConfig = options.brokerConfig;
|
|
383
394
|
if (brokerConfig) {
|
|
384
395
|
const accountPool = options.accountPool ?? (await loadAuthBrokerAccountPool());
|
|
385
396
|
const client = new AuthBrokerClient({ url: brokerConfig.url, token: brokerConfig.token });
|
|
@@ -437,22 +448,41 @@ export async function discoverAuthStorage(options: DiscoverAuthStorageOptions =
|
|
|
437
448
|
onSnapshot: persist,
|
|
438
449
|
accountPool,
|
|
439
450
|
});
|
|
440
|
-
|
|
441
|
-
...options.authStorageOptions,
|
|
442
|
-
configValueResolver: options.configValueResolver,
|
|
443
|
-
sourceLabel: options.sourceLabel ?? `broker ${brokerConfig.url}`,
|
|
444
|
-
accountPolicies,
|
|
445
|
-
defaultReservePct,
|
|
446
|
-
});
|
|
447
|
-
await storage.credentials.reload();
|
|
448
|
-
return storage;
|
|
451
|
+
return { store, sourceLabel: options.sourceLabel ?? `broker ${brokerConfig.url}` };
|
|
449
452
|
}
|
|
450
453
|
|
|
451
|
-
const dbPath = getAgentDbPath(agentDir);
|
|
452
|
-
const
|
|
454
|
+
const dbPath = getAgentDbPath(options.agentDir ?? getAgentDir());
|
|
455
|
+
const store = await SqliteAuthCredentialStore.open(dbPath);
|
|
456
|
+
return { store, sourceLabel: options.sourceLabel ?? `local ${dbPath}` };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Create an AuthStorage instance, using the broker when configured and falling
|
|
461
|
+
* back to the local SQLite store otherwise. This is the single source of truth
|
|
462
|
+
* for the TUI and the catalog generator.
|
|
463
|
+
*/
|
|
464
|
+
export async function discoverAuthStorage(options: DiscoverAuthStorageOptions = {}): Promise<AuthStorage> {
|
|
465
|
+
const agentDir = options.agentDir ?? getAgentDir();
|
|
466
|
+
const brokerConfig = await resolveAuthBrokerConfig({
|
|
467
|
+
agentDir,
|
|
468
|
+
configValueResolver: options.configValueResolver,
|
|
469
|
+
});
|
|
470
|
+
const { accountPolicies, defaultReservePct } = await loadAuthAccountPolicyConfig({
|
|
471
|
+
agentDir,
|
|
472
|
+
accountPolicies: options.accountPolicies,
|
|
473
|
+
usageReservePct: options.authStorageOptions?.defaultReservePct,
|
|
474
|
+
});
|
|
475
|
+
const { store, sourceLabel } = await openAuthCredentialStore({
|
|
476
|
+
brokerConfig,
|
|
477
|
+
agentDir,
|
|
478
|
+
cachePath: options.cachePath,
|
|
479
|
+
sourceLabel: options.sourceLabel,
|
|
480
|
+
accountPool: options.accountPool,
|
|
481
|
+
});
|
|
482
|
+
const storage = new AuthStorage(store, {
|
|
453
483
|
...options.authStorageOptions,
|
|
454
484
|
configValueResolver: options.configValueResolver,
|
|
455
|
-
sourceLabel
|
|
485
|
+
sourceLabel,
|
|
456
486
|
accountPolicies,
|
|
457
487
|
defaultReservePct,
|
|
458
488
|
});
|
package/src/auth-storage.ts
CHANGED
|
@@ -33,7 +33,9 @@ import { CredentialSelector } from "./auth/select";
|
|
|
33
33
|
import { SqliteAuthCredentialStore } from "./auth/sqlite-credential-store";
|
|
34
34
|
import type { AuthCredentialStore } from "./auth/store";
|
|
35
35
|
import type {
|
|
36
|
+
AuthAccountPolicies,
|
|
36
37
|
AuthApiKeyOptions,
|
|
38
|
+
AuthCredential,
|
|
37
39
|
AuthStorageOptions,
|
|
38
40
|
BlocksApi,
|
|
39
41
|
CredentialsApi,
|
|
@@ -54,36 +56,120 @@ export { isSqliteBusyError, isSqliteCorruptionError, SqliteAuthCredentialStore }
|
|
|
54
56
|
export * from "./auth/store";
|
|
55
57
|
export * from "./auth/types";
|
|
56
58
|
|
|
59
|
+
/** Store-bound credential modules; rebuilt as a unit by {@link AuthStorage.replaceStore}. */
|
|
60
|
+
interface AuthStorageModules {
|
|
61
|
+
pool: CredentialPool;
|
|
62
|
+
keys: KeyCascade;
|
|
63
|
+
oauth: OAuthAccounts;
|
|
64
|
+
sessions: SessionAffinity;
|
|
65
|
+
usage: UsageService;
|
|
66
|
+
health: CredentialHealth;
|
|
67
|
+
limits: RateLimits;
|
|
68
|
+
resets: ResetCredits;
|
|
69
|
+
blocks: CredentialBlocks;
|
|
70
|
+
}
|
|
71
|
+
|
|
57
72
|
/**
|
|
58
73
|
* Credential management over an {@link AuthCredentialStore}: multi-account
|
|
59
74
|
* selection with usage-aware ranking, rate-limit blocks, OAuth refresh, and
|
|
60
75
|
* usage reporting. See the module doc for the namespace layout.
|
|
76
|
+
*
|
|
77
|
+
* Namespaces resolve against the current store on every access, so holders of
|
|
78
|
+
* this instance follow {@link AuthStorage.replaceStore} without re-wiring.
|
|
61
79
|
*/
|
|
62
80
|
export class AuthStorage {
|
|
81
|
+
readonly #options: AuthStorageOptions;
|
|
82
|
+
readonly #overrides: KeyOverrides;
|
|
83
|
+
readonly #policies: AccountPolicies;
|
|
84
|
+
#modules: AuthStorageModules;
|
|
85
|
+
|
|
86
|
+
constructor(store: AuthCredentialStore, options: AuthStorageOptions = {}) {
|
|
87
|
+
this.#options = options;
|
|
88
|
+
this.#overrides = new KeyOverrides(options.configValueResolver);
|
|
89
|
+
this.#policies = new AccountPolicies(options.accountPolicies ?? [], options.defaultReservePct);
|
|
90
|
+
this.#modules = this.#compose(store, options.sourceLabel);
|
|
91
|
+
if (options.onCredentialDisabled) this.#modules.pool.onDisabled(options.onCredentialDisabled);
|
|
92
|
+
}
|
|
93
|
+
|
|
63
94
|
/** Stored credential rows, change/disable events, broker snapshot. */
|
|
64
|
-
|
|
95
|
+
get credentials(): CredentialsApi {
|
|
96
|
+
return this.#modules.pool;
|
|
97
|
+
}
|
|
65
98
|
/** Provider auth cascade and key overrides. */
|
|
66
|
-
|
|
99
|
+
get keys(): KeysApi {
|
|
100
|
+
return this.#modules.keys;
|
|
101
|
+
}
|
|
67
102
|
/** OAuth login, account access, listings, refresh. */
|
|
68
|
-
|
|
103
|
+
get oauth(): OAuthApi {
|
|
104
|
+
return this.#modules.oauth;
|
|
105
|
+
}
|
|
69
106
|
/** Session → account pins. */
|
|
70
|
-
|
|
107
|
+
get sessions(): SessionsApi {
|
|
108
|
+
return this.#modules.sessions;
|
|
109
|
+
}
|
|
71
110
|
/** Usage reports, header ingestion, history. */
|
|
72
|
-
|
|
111
|
+
get usage(): UsageApi {
|
|
112
|
+
return this.#modules.usage;
|
|
113
|
+
}
|
|
73
114
|
/** Model pool health and per-credential probes. */
|
|
74
|
-
|
|
115
|
+
get health(): HealthApi {
|
|
116
|
+
return this.#modules.health;
|
|
117
|
+
}
|
|
75
118
|
/** Usage-limit marking and credential rotation. */
|
|
76
|
-
|
|
119
|
+
get limits(): LimitsApi {
|
|
120
|
+
return this.#modules.limits;
|
|
121
|
+
}
|
|
77
122
|
/** Saved rate-limit resets. */
|
|
78
|
-
|
|
123
|
+
get resets(): ResetsApi {
|
|
124
|
+
return this.#modules.resets;
|
|
125
|
+
}
|
|
79
126
|
/** Persisted rate-limit blocks (auth-broker server seam). */
|
|
80
|
-
|
|
81
|
-
|
|
127
|
+
get blocks(): BlocksApi {
|
|
128
|
+
return this.#modules.blocks;
|
|
129
|
+
}
|
|
82
130
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Apply new account routing policy (live `auth.accountPolicies` /
|
|
133
|
+
* `retry.usageReservePct` change). Throws a configuration error, leaving the
|
|
134
|
+
* active policy untouched, when the policy is malformed or does not match the
|
|
135
|
+
* stored OAuth accounts.
|
|
136
|
+
*/
|
|
137
|
+
setAccountPolicies(config: { accountPolicies: AuthAccountPolicies; defaultReservePct: number }): void {
|
|
138
|
+
const pool = this.#modules.pool;
|
|
139
|
+
const stored = new Map<string, AuthCredential[]>();
|
|
140
|
+
for (const provider of pool.providers()) stored.set(provider, pool.credentials(provider));
|
|
141
|
+
this.#policies.replace(config.accountPolicies, config.defaultReservePct, stored);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Swap the backing credential store in place (live `auth.broker.url` change).
|
|
146
|
+
* Loads `store` into fresh store-bound state — pins, blocks, and usage caches are
|
|
147
|
+
* keyed by the old store's row ids — then closes the previous store. Runtime key
|
|
148
|
+
* overrides, account policies, usage-provider overrides, and credential event
|
|
149
|
+
* subscribers carry over. On a load failure `store` is closed and the current
|
|
150
|
+
* store stays active.
|
|
151
|
+
*/
|
|
152
|
+
async replaceStore(store: AuthCredentialStore, options: { sourceLabel?: string } = {}): Promise<void> {
|
|
153
|
+
const next = this.#compose(store, options.sourceLabel ?? this.#options.sourceLabel);
|
|
154
|
+
try {
|
|
155
|
+
await next.pool.reload();
|
|
156
|
+
} catch (error) {
|
|
157
|
+
next.pool.close();
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
const previous = this.#modules;
|
|
161
|
+
next.pool.adoptSubscribers(previous.pool);
|
|
162
|
+
next.usage.adoptRuntimeProviders(previous.usage);
|
|
163
|
+
this.#modules = next;
|
|
164
|
+
previous.pool.close();
|
|
165
|
+
next.pool.bump("store-replaced");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
#compose(store: AuthCredentialStore, sourceLabel: string | undefined): AuthStorageModules {
|
|
169
|
+
const options = this.#options;
|
|
170
|
+
const overrides = this.#overrides;
|
|
171
|
+
const policies = this.#policies;
|
|
172
|
+
const blockHealth = new BlockStoreHealth(sourceLabel);
|
|
87
173
|
const strategies = options.rankingStrategyResolver ?? defaultRankingStrategy;
|
|
88
174
|
const pool = new CredentialPool(store, {
|
|
89
175
|
policies,
|
|
@@ -134,32 +220,32 @@ export class AuthStorage {
|
|
|
134
220
|
selector,
|
|
135
221
|
affinity,
|
|
136
222
|
rotate: (provider, sessionId, rotateOptions) => limits.rotate(provider, sessionId, rotateOptions),
|
|
137
|
-
sourceLabel
|
|
223
|
+
sourceLabel,
|
|
138
224
|
});
|
|
139
225
|
const oauth = new OAuthAccounts({ pool, overrides, policies, selector, affinity, refresher });
|
|
140
226
|
|
|
141
|
-
|
|
142
|
-
this.credentials = pool;
|
|
143
|
-
this.keys = keys;
|
|
144
|
-
this.oauth = oauth;
|
|
145
|
-
this.sessions = affinity;
|
|
146
|
-
this.usage = usage;
|
|
147
|
-
this.health = new CredentialHealth({
|
|
148
|
-
store,
|
|
227
|
+
return {
|
|
149
228
|
pool,
|
|
150
229
|
keys,
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
affinity,
|
|
230
|
+
oauth,
|
|
231
|
+
sessions: affinity,
|
|
154
232
|
usage,
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
233
|
+
health: new CredentialHealth({
|
|
234
|
+
store,
|
|
235
|
+
pool,
|
|
236
|
+
keys,
|
|
237
|
+
policies,
|
|
238
|
+
blocks,
|
|
239
|
+
affinity,
|
|
240
|
+
usage,
|
|
241
|
+
refresher,
|
|
242
|
+
overrides,
|
|
243
|
+
strategies,
|
|
244
|
+
}),
|
|
245
|
+
limits,
|
|
246
|
+
resets: new ResetCredits({ store, pool, oauth, usage, usageCache, blocks }),
|
|
247
|
+
blocks,
|
|
248
|
+
};
|
|
163
249
|
}
|
|
164
250
|
|
|
165
251
|
/** Open the SQLite store at `dbPath` and wrap it (standalone use, e.g. the pi-ai CLI). */
|
|
@@ -170,7 +256,7 @@ export class AuthStorage {
|
|
|
170
256
|
|
|
171
257
|
/** Close the underlying credential store; the instance must not be reused. */
|
|
172
258
|
close(): void {
|
|
173
|
-
this.#pool.close();
|
|
259
|
+
this.#modules.pool.close();
|
|
174
260
|
}
|
|
175
261
|
|
|
176
262
|
/**
|
package/src/error/flags.ts
CHANGED
|
@@ -659,7 +659,8 @@ export function classify(error: unknown, api?: Api): number {
|
|
|
659
659
|
code === "usage_limit_reached" ||
|
|
660
660
|
(code === "insufficient_quota" && !isDashScopeTokenLimitText(link.message)) ||
|
|
661
661
|
(codeStatus === 402 &&
|
|
662
|
-
(
|
|
662
|
+
(is402BillingCapBody(link.message) ||
|
|
663
|
+
(code !== undefined && !isOpaqueStatusBody(code) && is402BillingCapBody(code))))
|
|
663
664
|
) {
|
|
664
665
|
linkKinds |= Flag.UsageLimit;
|
|
665
666
|
}
|
package/src/error/rate-limit.ts
CHANGED
|
@@ -37,7 +37,10 @@ const CREDITS_EXHAUSTED_PATTERN =
|
|
|
37
37
|
// in unrelated diagnostics ("Failed to fetch usage credits from billing
|
|
38
38
|
// service"), which must not rotate a healthy credential.
|
|
39
39
|
const ANTHROPIC_CREDITS_REQUIRED_PATTERN = /\busage credits are required\b|\bcredits_required\b/i;
|
|
40
|
-
|
|
40
|
+
// Account billing ceilings: Anthropic "monthly spend limit" (#4787) and Google
|
|
41
|
+
// "Your project has exceeded its monthly spending cap" (#13090). The `\b` after
|
|
42
|
+
// `cap` keeps "spending capacity" — a throttle, not a billing ceiling — out.
|
|
43
|
+
const SPEND_LIMIT_PATTERN = /spend(?:ing)?[\s_-]?(?:limit|cap)\b/i;
|
|
41
44
|
const SUBSCRIPTION_CAP_PATTERN =
|
|
42
45
|
/\b(?:subscription|plan|membership)\b[^\n]{0,80}\b(?:rate.?limits?|quota|cap)\b|\b(?:rate.?limits?|quota|cap)\b[^\n]{0,80}\b(?:subscription|plan|membership)\b/i;
|
|
43
46
|
const TRANSIENT_INTERVAL_RATE_LIMIT_PATTERN = /\bper\s+(?:second|minute)\b/i;
|
|
@@ -313,8 +316,9 @@ const USAGE_LIMIT_PATTERN =
|
|
|
313
316
|
* account-local usage cap rather than a bad credential or a transient blip.
|
|
314
317
|
* HTTP 402 Payment Required represents an account-billing cap (xAI
|
|
315
318
|
* Grok Build "usage balance exhausted", DeepSeek "Insufficient Balance",
|
|
316
|
-
* OpenRouter credit exhaustion)
|
|
317
|
-
*
|
|
319
|
+
* OpenCode Go "Insufficient account funds", OpenRouter credit exhaustion)
|
|
320
|
+
* when opaque, payment/deactivation/balance/funds-worded, or
|
|
321
|
+
* QUOTA_EXHAUSTED/CONCURRENT_LIMIT. Informative non-quota 402s (e.g.
|
|
318
322
|
* endpoint subscription requirements) remain non-usage-limits. Always combine
|
|
319
323
|
* with {@link isUsageLimitOutcome} when a message is available.
|
|
320
324
|
*/
|
|
@@ -322,7 +326,7 @@ export function isUsageLimitStatus(status: number | undefined): boolean {
|
|
|
322
326
|
return status === 429 || status === 402;
|
|
323
327
|
}
|
|
324
328
|
const STATUS_402_QUOTA_PATTERN =
|
|
325
|
-
/\b(?:payment(?:\s+is)?[-_.\s]*required|deactivated_workspace|insufficient.?balance)\b/i;
|
|
329
|
+
/\b(?:payment(?:\s+is)?[-_.\s]*required|deactivated_workspace|insufficient.?(?:balance|account.?funds))\b/i;
|
|
326
330
|
|
|
327
331
|
export function is402BillingCapBody(message: string | undefined): boolean {
|
|
328
332
|
if (message === undefined || isOpaqueStatusBody(message)) return true;
|
package/src/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ export * from "./providers/anthropic-identity";
|
|
|
22
22
|
export * from "./providers/anthropic-state";
|
|
23
23
|
export type * from "./providers/anthropic-client";
|
|
24
24
|
export * from "./providers/anthropic-user-profiles";
|
|
25
|
+
export * from "./providers/anthropic-slow-mode";
|
|
25
26
|
export type * from "./providers/apple-foundation-models";
|
|
26
27
|
export type * from "./providers/azure-openai-responses";
|
|
27
28
|
export type * from "./providers/cursor";
|