@oh-my-pi/pi-ai 16.4.5 → 16.4.8
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 +11 -0
- package/dist/types/auth-broker/client.d.ts +2 -1
- package/dist/types/auth-broker/remote-store.d.ts +1 -0
- package/dist/types/auth-broker/types.d.ts +4 -0
- package/dist/types/auth-broker/wire-schemas.d.ts +3 -0
- package/dist/types/auth-storage.d.ts +9 -0
- package/package.json +4 -4
- package/src/auth-broker/client.ts +9 -0
- package/src/auth-broker/remote-store.ts +7 -0
- package/src/auth-broker/server.ts +11 -0
- package/src/auth-broker/types.ts +5 -0
- package/src/auth-broker/wire-schemas.ts +5 -0
- package/src/auth-storage.ts +79 -37
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.4.6] - 2026-07-12
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added asynchronous `invalidateUsageCache` method to clear cached usage reports
|
|
10
|
+
- Added support for cross-service usage cache invalidation between AuthStorage and AuthBroker
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Fixed OAuth credential resolution returning "No API key found" when every plan-eligible OpenAI Codex account was rate-limit blocked and the only unblocked account failed the model's plan gate: resolution now runs a last-resort ladder that first yields a plan-fitting account regardless of usage blocks (so callers get real usage-limit retry semantics), then tries every account with the plan filter dropped before reporting no credential
|
|
15
|
+
|
|
5
16
|
## [16.4.5] - 2026-07-11
|
|
6
17
|
|
|
7
18
|
### Fixed
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AuthCredential } from "../auth-storage.js";
|
|
2
|
-
import type { CredentialBlockRequest, CredentialBlockResponse, CredentialBlocksDeleteResponse, CredentialDisableResponse, CredentialRefreshResponse, CredentialUploadResponse, HealthzResponse, SnapshotResponse, SnapshotStreamEvent, UsageResponse } from "./types.js";
|
|
2
|
+
import type { CredentialBlockRequest, CredentialBlockResponse, CredentialBlocksDeleteResponse, CredentialDisableResponse, CredentialRefreshResponse, CredentialUploadResponse, HealthzResponse, SnapshotResponse, SnapshotStreamEvent, UsageResponse, UsageStaleResponse } from "./types.js";
|
|
3
3
|
export interface AuthBrokerClientOptions {
|
|
4
4
|
/** Base URL (e.g. `https://broker.tailnet:8765`). Trailing slashes are trimmed. */
|
|
5
5
|
url: string;
|
|
@@ -60,6 +60,7 @@ export declare class AuthBrokerClient {
|
|
|
60
60
|
signal?: AbortSignal;
|
|
61
61
|
}): AsyncGenerator<SnapshotStreamEvent>;
|
|
62
62
|
fetchUsage(signal?: AbortSignal): Promise<UsageResponse>;
|
|
63
|
+
notifyUsageStale(signal?: AbortSignal): Promise<UsageStaleResponse>;
|
|
63
64
|
refreshCredential(id: number, signal?: AbortSignal): Promise<CredentialRefreshResponse>;
|
|
64
65
|
disableCredential(id: number, cause: string, signal?: AbortSignal): Promise<CredentialDisableResponse>;
|
|
65
66
|
uploadCredential(provider: string, credential: AuthCredential, signal?: AbortSignal): Promise<CredentialUploadResponse>;
|
|
@@ -80,6 +80,7 @@ export declare class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
80
80
|
getCache(key: string): string | null;
|
|
81
81
|
setCache(key: string, value: string, expiresAtSec: number): void;
|
|
82
82
|
cleanExpiredCache(): void;
|
|
83
|
+
invalidateUsageCache(signal?: AbortSignal): Promise<void>;
|
|
83
84
|
/**
|
|
84
85
|
* Store-level hook consumed by `AuthStorage` — routes refresh through the
|
|
85
86
|
* broker so the actual refresh token never leaves the broker host. Returns
|
|
@@ -56,6 +56,10 @@ export interface CredentialBlockResponse {
|
|
|
56
56
|
export interface CredentialBlocksDeleteResponse {
|
|
57
57
|
ok: boolean;
|
|
58
58
|
}
|
|
59
|
+
/** POST /v1/usage/stale response body. */
|
|
60
|
+
export interface UsageStaleResponse {
|
|
61
|
+
ok: boolean;
|
|
62
|
+
}
|
|
59
63
|
/**
|
|
60
64
|
* POST /v1/credential request body. The OAuth `refresh` must be the *real*
|
|
61
65
|
* refresh token (not the sentinel) — the broker is the canonical writer.
|
|
@@ -425,6 +425,9 @@ export declare const credentialBlockResponseSchema: import("arktype/internal/var
|
|
|
425
425
|
export declare const credentialBlocksDeleteResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
426
426
|
ok: boolean;
|
|
427
427
|
}, {}>;
|
|
428
|
+
export declare const usageStaleResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
429
|
+
ok: boolean;
|
|
430
|
+
}, {}>;
|
|
428
431
|
export declare const credentialUploadRequestSchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
429
432
|
provider: string;
|
|
430
433
|
credential: {
|
|
@@ -233,6 +233,8 @@ export interface CredentialRefreshLeaseFence {
|
|
|
233
233
|
}
|
|
234
234
|
export interface AuthCredentialStore {
|
|
235
235
|
close(): void;
|
|
236
|
+
/** Optional hook to notify the underlying store that usage report cache is stale. */
|
|
237
|
+
invalidateUsageCache?(signal?: AbortSignal): Promise<void>;
|
|
236
238
|
listAuthCredentials(provider?: string): StoredAuthCredential[];
|
|
237
239
|
updateAuthCredential(id: number, credential: AuthCredential): void;
|
|
238
240
|
deleteAuthCredential(id: number, disabledCause: string): void;
|
|
@@ -930,6 +932,13 @@ export declare class AuthStorage {
|
|
|
930
932
|
baseUrlResolver?: (provider: string) => string | undefined;
|
|
931
933
|
signal?: AbortSignal;
|
|
932
934
|
}): Promise<ResetCreditRedeemOutcome>;
|
|
935
|
+
/**
|
|
936
|
+
* Force-invalidate cached usage reports so the next fetch retrieves fresh
|
|
937
|
+
* values from upstream providers. If `provider` is specified, only that
|
|
938
|
+
* provider's credentials are invalidated; otherwise, all credentials in the
|
|
939
|
+
* store are invalidated.
|
|
940
|
+
*/
|
|
941
|
+
invalidateUsageCache(provider?: string, signal?: AbortSignal): Promise<void>;
|
|
933
942
|
invalidateCredentialMatching(provider: string, apiKey: string, options?: InvalidateCredentialMatchingOptions): Promise<boolean>;
|
|
934
943
|
invalidateCredentialMatching(provider: string, apiKey: string, signal?: AbortSignal): Promise<boolean>;
|
|
935
944
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "16.4.
|
|
4
|
+
"version": "16.4.8",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bufbuild/protobuf": "^2.12.0",
|
|
41
|
-
"@oh-my-pi/pi-catalog": "16.4.
|
|
42
|
-
"@oh-my-pi/pi-utils": "16.4.
|
|
43
|
-
"@oh-my-pi/pi-wire": "16.4.
|
|
41
|
+
"@oh-my-pi/pi-catalog": "16.4.8",
|
|
42
|
+
"@oh-my-pi/pi-utils": "16.4.8",
|
|
43
|
+
"@oh-my-pi/pi-wire": "16.4.8",
|
|
44
44
|
"arktype": "2.2.2",
|
|
45
45
|
"zod": "^4"
|
|
46
46
|
},
|
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
SnapshotResponse,
|
|
22
22
|
SnapshotStreamEvent,
|
|
23
23
|
UsageResponse,
|
|
24
|
+
UsageStaleResponse,
|
|
24
25
|
} from "./types";
|
|
25
26
|
import {
|
|
26
27
|
credentialBlockResponseSchema,
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
snapshotResponseSchema,
|
|
33
34
|
snapshotStreamEventSchema,
|
|
34
35
|
usageResponseSchema,
|
|
36
|
+
usageStaleResponseSchema,
|
|
35
37
|
} from "./wire-schemas";
|
|
36
38
|
|
|
37
39
|
export interface AuthBrokerClientOptions {
|
|
@@ -242,6 +244,13 @@ export class AuthBrokerClient {
|
|
|
242
244
|
return this.#request<UsageResponse>("GET", "/v1/usage", { schema: usageResponseSchema, signal });
|
|
243
245
|
}
|
|
244
246
|
|
|
247
|
+
notifyUsageStale(signal?: AbortSignal): Promise<UsageStaleResponse> {
|
|
248
|
+
return this.#request<UsageStaleResponse>("POST", "/v1/usage/stale", {
|
|
249
|
+
schema: usageStaleResponseSchema,
|
|
250
|
+
signal,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
245
254
|
async refreshCredential(id: number, signal?: AbortSignal): Promise<CredentialRefreshResponse> {
|
|
246
255
|
return this.#request<CredentialRefreshResponse>("POST", `/v1/credential/${id}/refresh`, {
|
|
247
256
|
schema: credentialRefreshResponseSchema,
|
|
@@ -794,6 +794,13 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
794
794
|
}
|
|
795
795
|
}
|
|
796
796
|
|
|
797
|
+
async invalidateUsageCache(signal?: AbortSignal): Promise<void> {
|
|
798
|
+
this.#invalidateUsageCache();
|
|
799
|
+
await this.#client.notifyUsageStale(signal).catch(err => {
|
|
800
|
+
logger.warn("auth-broker notification of stale usage failed", { error: String(err) });
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
|
|
797
804
|
#invalidateUsageCache(): void {
|
|
798
805
|
this.#usageCache = undefined;
|
|
799
806
|
this.#usageInflight = undefined;
|
|
@@ -608,6 +608,17 @@ export function startAuthBroker(opts: AuthBrokerServerOptions): AuthBrokerServer
|
|
|
608
608
|
return json(502, { error: message });
|
|
609
609
|
}
|
|
610
610
|
}
|
|
611
|
+
if (req.method === "POST" && pathname === "/v1/usage/stale") {
|
|
612
|
+
try {
|
|
613
|
+
opts.storage.invalidateUsageCache?.();
|
|
614
|
+
logger.info("auth-broker usage cache invalidated", { peer });
|
|
615
|
+
return json(200, { ok: true });
|
|
616
|
+
} catch (error) {
|
|
617
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
618
|
+
logger.warn("auth-broker usage cache invalidation failed", { peer, error: message });
|
|
619
|
+
return json(500, { error: message });
|
|
620
|
+
}
|
|
621
|
+
}
|
|
611
622
|
const refreshMatch = req.method === "POST" ? pathname.match(REFRESH_ROUTE) : null;
|
|
612
623
|
if (refreshMatch) {
|
|
613
624
|
const id = Number.parseInt(refreshMatch[1], 10);
|
package/src/auth-broker/types.ts
CHANGED
|
@@ -75,6 +75,11 @@ export interface CredentialBlocksDeleteResponse {
|
|
|
75
75
|
ok: boolean;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/** POST /v1/usage/stale response body. */
|
|
79
|
+
export interface UsageStaleResponse {
|
|
80
|
+
ok: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
78
83
|
/**
|
|
79
84
|
* POST /v1/credential request body. The OAuth `refresh` must be the *real*
|
|
80
85
|
* refresh token (not the sentinel) — the broker is the canonical writer.
|
|
@@ -258,6 +258,11 @@ export const credentialBlocksDeleteResponseSchema = type({
|
|
|
258
258
|
ok: "boolean",
|
|
259
259
|
});
|
|
260
260
|
|
|
261
|
+
export const usageStaleResponseSchema = type({
|
|
262
|
+
"+": "reject",
|
|
263
|
+
ok: "boolean",
|
|
264
|
+
});
|
|
265
|
+
|
|
261
266
|
// ─── Upload ────────────────────────────────────────────────────────────────
|
|
262
267
|
|
|
263
268
|
export const credentialUploadRequestSchema = type({
|
package/src/auth-storage.ts
CHANGED
|
@@ -315,6 +315,8 @@ export interface CredentialRefreshLeaseFence {
|
|
|
315
315
|
|
|
316
316
|
export interface AuthCredentialStore {
|
|
317
317
|
close(): void;
|
|
318
|
+
/** Optional hook to notify the underlying store that usage report cache is stale. */
|
|
319
|
+
invalidateUsageCache?(signal?: AbortSignal): Promise<void>;
|
|
318
320
|
listAuthCredentials(provider?: string): StoredAuthCredential[];
|
|
319
321
|
updateAuthCredential(id: number, credential: AuthCredential): void;
|
|
320
322
|
deleteAuthCredential(id: number, disabledCause: string): void;
|
|
@@ -3739,8 +3741,17 @@ export class AuthStorage {
|
|
|
3739
3741
|
|
|
3740
3742
|
/**
|
|
3741
3743
|
* Resolves an OAuth credential, trying credentials in priority order.
|
|
3742
|
-
*
|
|
3743
|
-
*
|
|
3744
|
+
*
|
|
3745
|
+
* Resolution ladder — a request in hand always beats "no API key":
|
|
3746
|
+
* 1. strict: unblocked credentials only, usage limits respected, plan
|
|
3747
|
+
* filter enforced (when any account is confirmed eligible);
|
|
3748
|
+
* 2. plan-fitting last resort: same plan filter, but blocked/exhausted
|
|
3749
|
+
* accounts are allowed (blocked candidates rank earliest-unblocking
|
|
3750
|
+
* first) so the caller gets real usage-limit semantics from the wire
|
|
3751
|
+
* instead of a missing key;
|
|
3752
|
+
* 3. unfiltered last resort: the plan filter matched nothing usable —
|
|
3753
|
+
* skip it and try every account once; the server is the final arbiter
|
|
3754
|
+
* of model access.
|
|
3744
3755
|
*
|
|
3745
3756
|
* Returns both the API key bytes for outbound requests AND the refreshed
|
|
3746
3757
|
* {@link OAuthCredential} so callers needing identity metadata (account id,
|
|
@@ -3890,42 +3901,34 @@ export class AuthStorage {
|
|
|
3890
3901
|
hasPlanRequirement &&
|
|
3891
3902
|
candidates.some(candidate => getOpenAICodexPlanEligibility(candidate.usage, planRequirement) === true);
|
|
3892
3903
|
|
|
3893
|
-
const
|
|
3904
|
+
const passes: Array<{ allowBlocked: boolean; enforcePlanRequirement: boolean }> = [
|
|
3905
|
+
{ allowBlocked: false, enforcePlanRequirement },
|
|
3906
|
+
{ allowBlocked: true, enforcePlanRequirement },
|
|
3907
|
+
];
|
|
3908
|
+
if (enforcePlanRequirement) passes.push({ allowBlocked: true, enforcePlanRequirement: false });
|
|
3894
3909
|
|
|
3895
|
-
for (const
|
|
3896
|
-
const
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
if (fallback && this.#isCredentialBlocked(provider, providerKey, fallback.selection.index, blockScope)) {
|
|
3918
|
-
return this.#tryOAuthCredential(provider, fallback.selection, providerKey, sessionId, options, {
|
|
3919
|
-
checkUsage,
|
|
3920
|
-
allowBlocked: true,
|
|
3921
|
-
prefetchedUsage: fallback.usage,
|
|
3922
|
-
usagePrechecked: fallback.usageChecked,
|
|
3923
|
-
planRequirement,
|
|
3924
|
-
enforcePlanRequirement,
|
|
3925
|
-
strategy,
|
|
3926
|
-
rankingContext,
|
|
3927
|
-
blockScope,
|
|
3928
|
-
});
|
|
3910
|
+
for (const pass of passes) {
|
|
3911
|
+
for (const candidate of candidates) {
|
|
3912
|
+
const resolved = await this.#tryOAuthCredential(
|
|
3913
|
+
provider,
|
|
3914
|
+
candidate.selection,
|
|
3915
|
+
providerKey,
|
|
3916
|
+
sessionId,
|
|
3917
|
+
options,
|
|
3918
|
+
{
|
|
3919
|
+
checkUsage,
|
|
3920
|
+
allowBlocked: pass.allowBlocked,
|
|
3921
|
+
prefetchedUsage: candidate.usage,
|
|
3922
|
+
usagePrechecked: candidate.usageChecked,
|
|
3923
|
+
planRequirement,
|
|
3924
|
+
enforcePlanRequirement: pass.enforcePlanRequirement,
|
|
3925
|
+
strategy,
|
|
3926
|
+
rankingContext,
|
|
3927
|
+
blockScope,
|
|
3928
|
+
},
|
|
3929
|
+
);
|
|
3930
|
+
if (resolved) return resolved;
|
|
3931
|
+
}
|
|
3929
3932
|
}
|
|
3930
3933
|
|
|
3931
3934
|
return undefined;
|
|
@@ -4666,6 +4669,11 @@ export class AuthStorage {
|
|
|
4666
4669
|
});
|
|
4667
4670
|
if (result.ok) {
|
|
4668
4671
|
this.#invalidateUsageReportCache(provider, baseUrl);
|
|
4672
|
+
if (this.#store.invalidateUsageCache) {
|
|
4673
|
+
await this.#store.invalidateUsageCache(options.signal).catch(err => {
|
|
4674
|
+
logger.debug("Failed to notify store of stale usage", { err });
|
|
4675
|
+
});
|
|
4676
|
+
}
|
|
4669
4677
|
// The window this credential was blocked on (by markUsageLimitReached)
|
|
4670
4678
|
// is now reset, so lift its temporary block — otherwise selection
|
|
4671
4679
|
// keeps skipping/under-ranking the freshly-reset account.
|
|
@@ -4691,6 +4699,40 @@ export class AuthStorage {
|
|
|
4691
4699
|
}
|
|
4692
4700
|
}
|
|
4693
4701
|
|
|
4702
|
+
/**
|
|
4703
|
+
* Force-invalidate cached usage reports so the next fetch retrieves fresh
|
|
4704
|
+
* values from upstream providers. If `provider` is specified, only that
|
|
4705
|
+
* provider's credentials are invalidated; otherwise, all credentials in the
|
|
4706
|
+
* store are invalidated.
|
|
4707
|
+
*/
|
|
4708
|
+
async invalidateUsageCache(provider?: string, signal?: AbortSignal): Promise<void> {
|
|
4709
|
+
if (provider) {
|
|
4710
|
+
this.#invalidateUsageReportCache(provider);
|
|
4711
|
+
} else {
|
|
4712
|
+
this.#usageCacheEpoch += 1;
|
|
4713
|
+
const expired = Date.now() - 1;
|
|
4714
|
+
try {
|
|
4715
|
+
const credentials = this.#store.listAuthCredentials();
|
|
4716
|
+
for (const entry of credentials) {
|
|
4717
|
+
if (entry.credential.type !== "oauth") continue;
|
|
4718
|
+
const cacheKey = this.#buildUsageReportCacheKey(
|
|
4719
|
+
this.#buildUsageRequestForOauth(entry.provider, entry.credential),
|
|
4720
|
+
);
|
|
4721
|
+
const existing = this.#usageCache.getStale<UsageReport | null>(cacheKey);
|
|
4722
|
+
this.#usageCache.set(cacheKey, { value: existing?.value ?? null, expiresAt: expired });
|
|
4723
|
+
}
|
|
4724
|
+
} catch (err) {
|
|
4725
|
+
logger.debug("Failed to list auth credentials for complete usage cache invalidation", { err });
|
|
4726
|
+
}
|
|
4727
|
+
}
|
|
4728
|
+
|
|
4729
|
+
if (this.#store.invalidateUsageCache) {
|
|
4730
|
+
await this.#store.invalidateUsageCache(signal).catch(err => {
|
|
4731
|
+
logger.debug("Failed to notify store of stale usage", { err });
|
|
4732
|
+
});
|
|
4733
|
+
}
|
|
4734
|
+
}
|
|
4735
|
+
|
|
4694
4736
|
#invalidateUsageReportCacheForProviderKey(providerKey: string): void {
|
|
4695
4737
|
const oauthSuffix = ":oauth";
|
|
4696
4738
|
if (!providerKey.endsWith(oauthSuffix)) return;
|