@oh-my-pi/pi-ai 18.1.10 → 18.1.11
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 +8 -0
- package/dist/types/auth-storage.d.ts +36 -0
- package/package.json +6 -6
- package/src/auth-gateway/server.ts +1 -0
- package/src/auth-storage.ts +189 -6
- package/src/error/flags.ts +1 -1
- package/src/providers/pi-native-client.ts +10 -6
- package/src/registry/oauth/github-copilot.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.1.11] - 2026-09-05
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- GitHub Copilot sign-in now requests only basic profile access, restoring login for Enterprise organizations that reject repository, gist, and Codespaces permissions ([#10656](https://github.com/can1357/oh-my-pi/issues/10656)).
|
|
10
|
+
|
|
5
11
|
## [18.1.9] - 2026-09-04
|
|
6
12
|
|
|
7
13
|
### Added
|
|
@@ -27,6 +33,7 @@
|
|
|
27
33
|
### Fixed
|
|
28
34
|
|
|
29
35
|
- Fixed DeepSeek-family Responses replay (e.g. opencode-go) rejecting a resumed thinking-mode turn with `400 The reasoning_text in the thinking mode must be passed back to the API` when compaction dropped the turn's reasoning; a non-empty placeholder is now synthesized instead of an empty `reasoning_text` ([#10690](https://github.com/can1357/oh-my-pi/issues/10690)).
|
|
36
|
+
- Fixed pi-native streams treating a connection that closed before its terminal event as a successful empty response; incomplete streams and namespaced gateway 5xx failures now remain retryable.
|
|
30
37
|
|
|
31
38
|
## [18.1.6] - 2026-09-03
|
|
32
39
|
|
|
@@ -50,6 +57,7 @@
|
|
|
50
57
|
- Anthropic and OpenRouter credit-exhaustion errors now automatically switch to a sibling account instead of stopping the turn with a retry hint.
|
|
51
58
|
- Fixed OpenCode Go and Zen requests by including the required stable per-conversation session identification.
|
|
52
59
|
- Improved Anthropic prompt caching so explicit cache breakpoints preserve reusable tools and system prompts when the message tail changes.
|
|
60
|
+
- Anthropic and OpenRouter 402 credit-exhaustion errors ("would exceed your available credits", "Insufficient credits") now switch to a sibling account instead of stopping the turn with a retry hint.
|
|
53
61
|
|
|
54
62
|
## [18.1.5] - 2026-09-03
|
|
55
63
|
|
|
@@ -496,10 +496,40 @@ export { isDefinitiveOAuthFailure } from "./error/auth-classify.js";
|
|
|
496
496
|
* multi-hour) retry-after when it is sooner. `retryAtMs` is `undefined` when
|
|
497
497
|
* no sibling credentials exist at all, or when the session has no tracked
|
|
498
498
|
* credential to rotate away from.
|
|
499
|
+
*
|
|
500
|
+
* `blockedUntilMs` (epoch ms) is the just-blocked credential's own unblock
|
|
501
|
+
* deadline — the later of the caller's retry-after and any exhausted window
|
|
502
|
+
* the usage report reveals. Callers that wait the account out (instead of
|
|
503
|
+
* rotating) must sleep until this, not the error-text hint alone.
|
|
504
|
+
*
|
|
505
|
+
* `priorBlockedUntilMs` (epoch ms) is the live block deadline the map already
|
|
506
|
+
* stored for this credential before this call. The merged `blockedUntilMs`
|
|
507
|
+
* masks a pre-existing block shorter than this call's own heuristic
|
|
508
|
+
* fallback (`Math.max` in the mark), so callers that replace that heuristic
|
|
509
|
+
* with an authoritative report window must consult the prior deadline to
|
|
510
|
+
* keep honoring the earlier response's provider-stated block.
|
|
511
|
+
*
|
|
512
|
+
* `priorBlockedUntilTimed` is `true` when that prior deadline came from
|
|
513
|
+
* provider-stated timing (a parsed hint or usage-report reset) rather than
|
|
514
|
+
* another session's heuristic guess — only timed priors may extend a wait
|
|
515
|
+
* past an authoritative report window. Persisted blocks carry no provenance
|
|
516
|
+
* and count as untimed: a stale persisted heuristic must not outrank a
|
|
517
|
+
* fresh complete report (longer persisted deadlines still win through the
|
|
518
|
+
* merged `blockedUntilMs`).
|
|
519
|
+
*
|
|
520
|
+
* `reportResetAtMs` (epoch ms) is present only when the usage report is a
|
|
521
|
+
* complete authority for the wait: every exhausted window carries a future
|
|
522
|
+
* reset, so sleeping until the latest one can actually clear the account. A
|
|
523
|
+
* permanent cap alongside a timed window (or no report at all) leaves it
|
|
524
|
+
* unset, and the heuristic fallback alone must never authorize a wait.
|
|
499
525
|
*/
|
|
500
526
|
export interface UsageLimitMarkResult {
|
|
501
527
|
switched: boolean;
|
|
502
528
|
retryAtMs?: number;
|
|
529
|
+
blockedUntilMs?: number;
|
|
530
|
+
priorBlockedUntilMs?: number;
|
|
531
|
+
priorBlockedUntilTimed?: boolean;
|
|
532
|
+
reportResetAtMs?: number;
|
|
503
533
|
}
|
|
504
534
|
export type ModelUsageHealthState = "healthy" | "reserve" | "depleted" | "unknown";
|
|
505
535
|
export interface ModelUsageAccountHealth {
|
|
@@ -1013,6 +1043,12 @@ export declare class AuthStorage {
|
|
|
1013
1043
|
*/
|
|
1014
1044
|
markUsageLimitReached(provider: string, sessionId: string | undefined, options?: {
|
|
1015
1045
|
retryAfterMs?: number;
|
|
1046
|
+
/**
|
|
1047
|
+
* Whether `retryAfterMs` came from provider-stated timing (a parsed
|
|
1048
|
+
* retry hint) rather than a heuristic/default guess. A report reset
|
|
1049
|
+
* extending the block counts as provider timing regardless.
|
|
1050
|
+
*/
|
|
1051
|
+
providerTimed?: boolean;
|
|
1016
1052
|
baseUrl?: string;
|
|
1017
1053
|
modelId?: string;
|
|
1018
1054
|
apiKey?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-ai",
|
|
3
|
-
"version": "18.1.
|
|
3
|
+
"version": "18.1.11",
|
|
4
4
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -124,11 +124,11 @@
|
|
|
124
124
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
125
125
|
},
|
|
126
126
|
"dependencies": {
|
|
127
|
-
"@oh-my-pi/omptype": "18.1.
|
|
128
|
-
"@oh-my-pi/pi-catalog": "18.1.
|
|
129
|
-
"@oh-my-pi/pi-natives": "18.1.
|
|
130
|
-
"@oh-my-pi/pi-utils": "18.1.
|
|
131
|
-
"@oh-my-pi/pi-wire": "18.1.
|
|
127
|
+
"@oh-my-pi/omptype": "18.1.11",
|
|
128
|
+
"@oh-my-pi/pi-catalog": "18.1.11",
|
|
129
|
+
"@oh-my-pi/pi-natives": "18.1.11",
|
|
130
|
+
"@oh-my-pi/pi-utils": "18.1.11",
|
|
131
|
+
"@oh-my-pi/pi-wire": "18.1.11"
|
|
132
132
|
},
|
|
133
133
|
"devDependencies": {
|
|
134
134
|
"@types/bun": "^1.3.14"
|
|
@@ -251,6 +251,7 @@ async function refreshGatewayApiKeyAfterAuthError(
|
|
|
251
251
|
const retryAfterMs = extractRetryHint(undefined, message);
|
|
252
252
|
const { switched, retryAtMs } = await storage.markUsageLimitReached(provider, sessionId, {
|
|
253
253
|
retryAfterMs,
|
|
254
|
+
providerTimed: retryAfterMs !== undefined,
|
|
254
255
|
baseUrl: model.baseUrl,
|
|
255
256
|
modelId: model.id,
|
|
256
257
|
apiKey: oldKey,
|
package/src/auth-storage.ts
CHANGED
|
@@ -755,10 +755,40 @@ export { isDefinitiveOAuthFailure } from "./error/auth-classify";
|
|
|
755
755
|
* multi-hour) retry-after when it is sooner. `retryAtMs` is `undefined` when
|
|
756
756
|
* no sibling credentials exist at all, or when the session has no tracked
|
|
757
757
|
* credential to rotate away from.
|
|
758
|
+
*
|
|
759
|
+
* `blockedUntilMs` (epoch ms) is the just-blocked credential's own unblock
|
|
760
|
+
* deadline — the later of the caller's retry-after and any exhausted window
|
|
761
|
+
* the usage report reveals. Callers that wait the account out (instead of
|
|
762
|
+
* rotating) must sleep until this, not the error-text hint alone.
|
|
763
|
+
*
|
|
764
|
+
* `priorBlockedUntilMs` (epoch ms) is the live block deadline the map already
|
|
765
|
+
* stored for this credential before this call. The merged `blockedUntilMs`
|
|
766
|
+
* masks a pre-existing block shorter than this call's own heuristic
|
|
767
|
+
* fallback (`Math.max` in the mark), so callers that replace that heuristic
|
|
768
|
+
* with an authoritative report window must consult the prior deadline to
|
|
769
|
+
* keep honoring the earlier response's provider-stated block.
|
|
770
|
+
*
|
|
771
|
+
* `priorBlockedUntilTimed` is `true` when that prior deadline came from
|
|
772
|
+
* provider-stated timing (a parsed hint or usage-report reset) rather than
|
|
773
|
+
* another session's heuristic guess — only timed priors may extend a wait
|
|
774
|
+
* past an authoritative report window. Persisted blocks carry no provenance
|
|
775
|
+
* and count as untimed: a stale persisted heuristic must not outrank a
|
|
776
|
+
* fresh complete report (longer persisted deadlines still win through the
|
|
777
|
+
* merged `blockedUntilMs`).
|
|
778
|
+
*
|
|
779
|
+
* `reportResetAtMs` (epoch ms) is present only when the usage report is a
|
|
780
|
+
* complete authority for the wait: every exhausted window carries a future
|
|
781
|
+
* reset, so sleeping until the latest one can actually clear the account. A
|
|
782
|
+
* permanent cap alongside a timed window (or no report at all) leaves it
|
|
783
|
+
* unset, and the heuristic fallback alone must never authorize a wait.
|
|
758
784
|
*/
|
|
759
785
|
export interface UsageLimitMarkResult {
|
|
760
786
|
switched: boolean;
|
|
761
787
|
retryAtMs?: number;
|
|
788
|
+
blockedUntilMs?: number;
|
|
789
|
+
priorBlockedUntilMs?: number;
|
|
790
|
+
priorBlockedUntilTimed?: boolean;
|
|
791
|
+
reportResetAtMs?: number;
|
|
762
792
|
}
|
|
763
793
|
|
|
764
794
|
export type ModelUsageHealthState = "healthy" | "reserve" | "depleted" | "unknown";
|
|
@@ -1325,6 +1355,14 @@ export class AuthStorage {
|
|
|
1325
1355
|
#oauthBearerFingerprints: Map<string, Map<number, string[]>> = new Map();
|
|
1326
1356
|
/** Maps provider:type -> credentialIndex -> blockedUntilMs for temporary backoff. */
|
|
1327
1357
|
#credentialBackoff: Map<string, Map<number, number>> = new Map();
|
|
1358
|
+
/**
|
|
1359
|
+
* Provenance of each in-memory block: `true` when the deadline came from
|
|
1360
|
+
* provider-stated timing (a parsed retry hint or a usage-report reset),
|
|
1361
|
+
* `false` when it is a heuristic/default guess. Mirrors
|
|
1362
|
+
* {@link AuthStorage.#credentialBackoff} longest-wins semantics, tracking
|
|
1363
|
+
* the flag of whichever deadline currently wins.
|
|
1364
|
+
*/
|
|
1365
|
+
#credentialBackoffProviderTimed: Map<string, Map<number, boolean>> = new Map();
|
|
1328
1366
|
/** Earliest time a freshly-set in-memory block may be cleared by live usage reconciliation. */
|
|
1329
1367
|
#credentialBackoffProbeAfter: Map<string, Map<number, number>> = new Map();
|
|
1330
1368
|
/**
|
|
@@ -1800,6 +1838,10 @@ export class AuthStorage {
|
|
|
1800
1838
|
if (backoffMap.size === 0) {
|
|
1801
1839
|
this.#credentialBackoff.delete(backoffKey);
|
|
1802
1840
|
}
|
|
1841
|
+
this.#credentialBackoffProviderTimed.get(backoffKey)?.delete(credentialIndex);
|
|
1842
|
+
if (this.#credentialBackoffProviderTimed.get(backoffKey)?.size === 0) {
|
|
1843
|
+
this.#credentialBackoffProviderTimed.delete(backoffKey);
|
|
1844
|
+
}
|
|
1803
1845
|
const probeAfterMap = this.#credentialBackoffProbeAfter.get(backoffKey);
|
|
1804
1846
|
probeAfterMap?.delete(credentialIndex);
|
|
1805
1847
|
if (probeAfterMap?.size === 0) this.#credentialBackoffProbeAfter.delete(backoffKey);
|
|
@@ -1906,13 +1948,48 @@ export class AuthStorage {
|
|
|
1906
1948
|
return this.#getCredentialBlockedUntil(provider, providerKey, credentialIndex, blockScope) !== undefined;
|
|
1907
1949
|
}
|
|
1908
1950
|
|
|
1909
|
-
/**
|
|
1951
|
+
/**
|
|
1952
|
+
* Whether the in-memory block currently sitting at exactly `deadline` for
|
|
1953
|
+
* this credential was written with provider-stated timing. Mirrors the
|
|
1954
|
+
* scope enumeration of {@link AuthStorage.#getCredentialBlockedUntil}.
|
|
1955
|
+
* A deadline with no matching in-memory entry came from the persisted
|
|
1956
|
+
* store, which carries no provenance — a stale persisted heuristic guess
|
|
1957
|
+
* (pre-restart hintless response) must not outrank a fresh complete usage
|
|
1958
|
+
* report, so persisted-only deadlines count as untimed. Persisted
|
|
1959
|
+
* deadlines longer than this call's own request still win through the
|
|
1960
|
+
* merged `blockedUntilMs` comparison, which needs no provenance.
|
|
1961
|
+
*/
|
|
1962
|
+
#isProviderTimedBlock(
|
|
1963
|
+
providerKey: string,
|
|
1964
|
+
blockScopeOrScopes: string | readonly string[] | undefined,
|
|
1965
|
+
credentialIndex: number,
|
|
1966
|
+
deadline: number,
|
|
1967
|
+
): boolean {
|
|
1968
|
+
const scopes = (
|
|
1969
|
+
typeof blockScopeOrScopes === "string" ? [blockScopeOrScopes] : (blockScopeOrScopes ?? [])
|
|
1970
|
+
).filter(scope => scope.length > 0);
|
|
1971
|
+
for (const key of [providerKey, ...scopes.map(scope => this.#toScopedBackoffKey(providerKey, scope))]) {
|
|
1972
|
+
const entry = this.#credentialBackoff.get(key)?.get(credentialIndex);
|
|
1973
|
+
if (entry === undefined || entry !== deadline) continue;
|
|
1974
|
+
if (this.#credentialBackoffProviderTimed.get(key)?.get(credentialIndex) === true) return true;
|
|
1975
|
+
}
|
|
1976
|
+
return false;
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
/**
|
|
1980
|
+
* Marks a credential as blocked until the specified time. `providerTimed`
|
|
1981
|
+
* records whether the requested deadline comes from provider-stated
|
|
1982
|
+
* timing (a parsed retry hint or a usage-report reset) rather than a
|
|
1983
|
+
* heuristic/default guess; the stored block keeps the provenance of
|
|
1984
|
+
* whichever deadline wins the longest-wins merge.
|
|
1985
|
+
*/
|
|
1910
1986
|
#markCredentialBlocked(
|
|
1911
1987
|
provider: string,
|
|
1912
1988
|
providerKey: string,
|
|
1913
1989
|
credentialIndex: number,
|
|
1914
1990
|
blockedUntilMs: number,
|
|
1915
1991
|
blockScope: string | undefined = undefined,
|
|
1992
|
+
providerTimed = false,
|
|
1916
1993
|
): void {
|
|
1917
1994
|
const backoffKey = this.#toScopedBackoffKey(providerKey, blockScope);
|
|
1918
1995
|
const backoffMap = this.#credentialBackoff.get(backoffKey) ?? new Map<number, number>();
|
|
@@ -1920,6 +1997,16 @@ export class AuthStorage {
|
|
|
1920
1997
|
const nextBlockedUntil = Math.max(existing, blockedUntilMs);
|
|
1921
1998
|
backoffMap.set(credentialIndex, nextBlockedUntil);
|
|
1922
1999
|
this.#credentialBackoff.set(backoffKey, backoffMap);
|
|
2000
|
+
const timedMap = this.#credentialBackoffProviderTimed.get(backoffKey) ?? new Map<number, boolean>();
|
|
2001
|
+
const existingTimed = timedMap.get(credentialIndex) ?? false;
|
|
2002
|
+
const nextTimed =
|
|
2003
|
+
blockedUntilMs > existing
|
|
2004
|
+
? providerTimed
|
|
2005
|
+
: blockedUntilMs === existing
|
|
2006
|
+
? existingTimed || providerTimed
|
|
2007
|
+
: existingTimed;
|
|
2008
|
+
timedMap.set(credentialIndex, nextTimed);
|
|
2009
|
+
this.#credentialBackoffProviderTimed.set(backoffKey, timedMap);
|
|
1923
2010
|
const probeAfterMap = this.#credentialBackoffProbeAfter.get(backoffKey) ?? new Map<number, number>();
|
|
1924
2011
|
probeAfterMap.set(credentialIndex, Math.min(nextBlockedUntil, Date.now() + USAGE_REPORT_TTL_MS));
|
|
1925
2012
|
this.#credentialBackoffProbeAfter.set(backoffKey, probeAfterMap);
|
|
@@ -2205,6 +2292,7 @@ export class AuthStorage {
|
|
|
2205
2292
|
selection.index,
|
|
2206
2293
|
blockedUntil,
|
|
2207
2294
|
args.blockScope,
|
|
2295
|
+
resetAtMs !== undefined,
|
|
2208
2296
|
);
|
|
2209
2297
|
blocked = true;
|
|
2210
2298
|
}
|
|
@@ -2310,6 +2398,11 @@ export class AuthStorage {
|
|
|
2310
2398
|
this.#credentialBackoff.delete(key);
|
|
2311
2399
|
}
|
|
2312
2400
|
}
|
|
2401
|
+
for (const key of this.#credentialBackoffProviderTimed.keys()) {
|
|
2402
|
+
if (key.startsWith(`${provider}:`)) {
|
|
2403
|
+
this.#credentialBackoffProviderTimed.delete(key);
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2313
2406
|
}
|
|
2314
2407
|
|
|
2315
2408
|
/** Updates credential at index in-place (used for OAuth token refresh) */
|
|
@@ -4600,11 +4693,43 @@ export class AuthStorage {
|
|
|
4600
4693
|
targetIndex: number,
|
|
4601
4694
|
blockedUntil: number,
|
|
4602
4695
|
routing: CredentialBlockRouting,
|
|
4696
|
+
providerTimed: boolean,
|
|
4603
4697
|
): UsageLimitMarkResult {
|
|
4698
|
+
// Snapshot the live block deadline BEFORE this call's mark: the merged
|
|
4699
|
+
// value below masks a pre-existing block shorter than this call's own
|
|
4700
|
+
// request (longest-wins), and recovery that replaces this call's
|
|
4701
|
+
// heuristic fallback with an authoritative report window still needs
|
|
4702
|
+
// the prior response's provider-stated deadline.
|
|
4703
|
+
const priorBlockedUntilMs =
|
|
4704
|
+
targetIndex >= 0
|
|
4705
|
+
? this.#getCredentialBlockedUntil(provider, routing.providerKey, targetIndex, routing.blockScope)
|
|
4706
|
+
: undefined;
|
|
4707
|
+
const priorBlockedUntilTimed =
|
|
4708
|
+
targetIndex >= 0 && priorBlockedUntilMs !== undefined
|
|
4709
|
+
? this.#isProviderTimedBlock(routing.providerKey, routing.blockScope, targetIndex, priorBlockedUntilMs)
|
|
4710
|
+
: false;
|
|
4604
4711
|
if (targetIndex >= 0) {
|
|
4605
|
-
this.#markCredentialBlocked(
|
|
4712
|
+
this.#markCredentialBlocked(
|
|
4713
|
+
provider,
|
|
4714
|
+
routing.providerKey,
|
|
4715
|
+
targetIndex,
|
|
4716
|
+
blockedUntil,
|
|
4717
|
+
routing.blockScope,
|
|
4718
|
+
providerTimed,
|
|
4719
|
+
);
|
|
4606
4720
|
}
|
|
4607
4721
|
|
|
4722
|
+
// Report the merged deadline the block map actually stores, not this
|
|
4723
|
+
// call's input: a sibling session may have established a longer block
|
|
4724
|
+
// for the same credential (out-of-order usage-limit responses), and
|
|
4725
|
+
// #markCredentialBlocked keeps the longest. Waiting on the shorter
|
|
4726
|
+
// value would retry before the credential is actually usable.
|
|
4727
|
+
const mergedBlockedUntil =
|
|
4728
|
+
targetIndex >= 0
|
|
4729
|
+
? (this.#getCredentialBlockedUntil(provider, routing.providerKey, targetIndex, routing.blockScope) ??
|
|
4730
|
+
blockedUntil)
|
|
4731
|
+
: blockedUntil;
|
|
4732
|
+
|
|
4608
4733
|
const remainingCredentials = this.#getCredentialsForProvider(provider)
|
|
4609
4734
|
.map((credential, index) => ({ credential, index }))
|
|
4610
4735
|
.filter(
|
|
@@ -4622,10 +4747,22 @@ export class AuthStorage {
|
|
|
4622
4747
|
candidate.index,
|
|
4623
4748
|
routing.siblingBlockScopes,
|
|
4624
4749
|
);
|
|
4625
|
-
if (candidateBlockedUntil === undefined)
|
|
4750
|
+
if (candidateBlockedUntil === undefined)
|
|
4751
|
+
return {
|
|
4752
|
+
switched: true,
|
|
4753
|
+
blockedUntilMs: mergedBlockedUntil,
|
|
4754
|
+
priorBlockedUntilMs,
|
|
4755
|
+
priorBlockedUntilTimed,
|
|
4756
|
+
};
|
|
4626
4757
|
if (retryAtMs === undefined || candidateBlockedUntil < retryAtMs) retryAtMs = candidateBlockedUntil;
|
|
4627
4758
|
}
|
|
4628
|
-
return {
|
|
4759
|
+
return {
|
|
4760
|
+
switched: false,
|
|
4761
|
+
retryAtMs,
|
|
4762
|
+
blockedUntilMs: mergedBlockedUntil,
|
|
4763
|
+
priorBlockedUntilMs,
|
|
4764
|
+
priorBlockedUntilTimed,
|
|
4765
|
+
};
|
|
4629
4766
|
}
|
|
4630
4767
|
|
|
4631
4768
|
/**
|
|
@@ -4640,6 +4777,12 @@ export class AuthStorage {
|
|
|
4640
4777
|
sessionId: string | undefined,
|
|
4641
4778
|
options?: {
|
|
4642
4779
|
retryAfterMs?: number;
|
|
4780
|
+
/**
|
|
4781
|
+
* Whether `retryAfterMs` came from provider-stated timing (a parsed
|
|
4782
|
+
* retry hint) rather than a heuristic/default guess. A report reset
|
|
4783
|
+
* extending the block counts as provider timing regardless.
|
|
4784
|
+
*/
|
|
4785
|
+
providerTimed?: boolean;
|
|
4643
4786
|
baseUrl?: string;
|
|
4644
4787
|
modelId?: string;
|
|
4645
4788
|
apiKey?: string;
|
|
@@ -4673,7 +4816,17 @@ export class AuthStorage {
|
|
|
4673
4816
|
const routing = this.#credentialBlockRouting(provider, credentialType, options?.modelId);
|
|
4674
4817
|
const now = Date.now();
|
|
4675
4818
|
let blockedUntil = now + (options?.retryAfterMs ?? AuthStorage.#defaultBackoffMs);
|
|
4676
|
-
|
|
4819
|
+
// Heuristic/default fallbacks are guesses; provider-stated hints and
|
|
4820
|
+
// report-derived extensions are timed.
|
|
4821
|
+
let providerTimed = options?.providerTimed === true;
|
|
4822
|
+
|
|
4823
|
+
// Epoch ms of the usage-report-derived reset. Authoritative only when
|
|
4824
|
+
// EVERY exhausted window carries a future reset: a permanent cap
|
|
4825
|
+
// alongside a timed window must not authorize a wait that can never
|
|
4826
|
+
// clear it, and a bare heuristic must not sleep alone. Set
|
|
4827
|
+
// independently of whether the report extends the hint so a shorter
|
|
4828
|
+
// authoritative window still counts as provider timing.
|
|
4829
|
+
let reportResetAtMs: number | undefined;
|
|
4677
4830
|
if (target && routing.strategy) {
|
|
4678
4831
|
const report = await raceUsageWithSignal(
|
|
4679
4832
|
this.#getUsageReport(provider, target.credential, options),
|
|
@@ -4685,6 +4838,15 @@ export class AuthStorage {
|
|
|
4685
4838
|
const resetAtMs = this.#getUsageResetAtMs(scopedLimits, Date.now());
|
|
4686
4839
|
if (resetAtMs && resetAtMs > blockedUntil) {
|
|
4687
4840
|
blockedUntil = resetAtMs;
|
|
4841
|
+
providerTimed = true;
|
|
4842
|
+
}
|
|
4843
|
+
const nowMs = Date.now();
|
|
4844
|
+
const exhaustedLimits = scopedLimits.filter(limit => this.#isUsageLimitExhausted(limit));
|
|
4845
|
+
const futureResets = exhaustedLimits
|
|
4846
|
+
.map(limit => this.#resolveWindowResetAt(limit.window))
|
|
4847
|
+
.filter((reset): reset is number => reset !== undefined && reset > nowMs);
|
|
4848
|
+
if (exhaustedLimits.length > 0 && futureResets.length === exhaustedLimits.length) {
|
|
4849
|
+
reportResetAtMs = Math.max(...futureResets);
|
|
4688
4850
|
}
|
|
4689
4851
|
}
|
|
4690
4852
|
}
|
|
@@ -4696,7 +4858,15 @@ export class AuthStorage {
|
|
|
4696
4858
|
const targetIndex = this.#getStoredCredentials(provider).findIndex(
|
|
4697
4859
|
entry => entry.id === targetCredentialId && entry.credential.type === credentialType,
|
|
4698
4860
|
);
|
|
4699
|
-
|
|
4861
|
+
const rotation = this.#blockCredentialForRotation(
|
|
4862
|
+
provider,
|
|
4863
|
+
credentialType,
|
|
4864
|
+
targetIndex,
|
|
4865
|
+
blockedUntil,
|
|
4866
|
+
routing,
|
|
4867
|
+
providerTimed,
|
|
4868
|
+
);
|
|
4869
|
+
return reportResetAtMs === undefined ? rotation : { ...rotation, reportResetAtMs };
|
|
4700
4870
|
}
|
|
4701
4871
|
|
|
4702
4872
|
#resolveWindowResetAt(window: UsageLimit["window"]): number | undefined {
|
|
@@ -4906,6 +5076,7 @@ export class AuthStorage {
|
|
|
4906
5076
|
selection.index,
|
|
4907
5077
|
blockedUntil,
|
|
4908
5078
|
args.blockScope,
|
|
5079
|
+
resetAtMs !== undefined,
|
|
4909
5080
|
);
|
|
4910
5081
|
blocked = true;
|
|
4911
5082
|
}
|
|
@@ -5562,6 +5733,7 @@ export class AuthStorage {
|
|
|
5562
5733
|
selection.index,
|
|
5563
5734
|
resetAtMs ?? Date.now() + AuthStorage.#defaultBackoffMs,
|
|
5564
5735
|
blockScope,
|
|
5736
|
+
resetAtMs !== undefined,
|
|
5565
5737
|
);
|
|
5566
5738
|
return undefined;
|
|
5567
5739
|
}
|
|
@@ -5642,6 +5814,7 @@ export class AuthStorage {
|
|
|
5642
5814
|
selection.index,
|
|
5643
5815
|
resetAtMs ?? Date.now() + AuthStorage.#defaultBackoffMs,
|
|
5644
5816
|
blockScope,
|
|
5817
|
+
resetAtMs !== undefined,
|
|
5645
5818
|
);
|
|
5646
5819
|
return undefined;
|
|
5647
5820
|
}
|
|
@@ -6335,6 +6508,11 @@ export class AuthStorage {
|
|
|
6335
6508
|
backoffMap.delete(index);
|
|
6336
6509
|
if (backoffMap.size === 0) this.#credentialBackoff.delete(key);
|
|
6337
6510
|
}
|
|
6511
|
+
for (const [key, timedMap] of this.#credentialBackoffProviderTimed) {
|
|
6512
|
+
if (key !== providerKey && !key.startsWith(scopedPrefix)) continue;
|
|
6513
|
+
timedMap.delete(index);
|
|
6514
|
+
if (timedMap.size === 0) this.#credentialBackoffProviderTimed.delete(key);
|
|
6515
|
+
}
|
|
6338
6516
|
for (const [key, probeAfterMap] of this.#credentialBackoffProbeAfter) {
|
|
6339
6517
|
if (key !== providerKey && !key.startsWith(scopedPrefix)) continue;
|
|
6340
6518
|
probeAfterMap.delete(index);
|
|
@@ -6396,6 +6574,9 @@ export class AuthStorage {
|
|
|
6396
6574
|
const backoffMap = this.#credentialBackoff.get(key);
|
|
6397
6575
|
backoffMap?.delete(credentialIndex);
|
|
6398
6576
|
if (backoffMap?.size === 0) this.#credentialBackoff.delete(key);
|
|
6577
|
+
const timedMap = this.#credentialBackoffProviderTimed.get(key);
|
|
6578
|
+
timedMap?.delete(credentialIndex);
|
|
6579
|
+
if (timedMap?.size === 0) this.#credentialBackoffProviderTimed.delete(key);
|
|
6399
6580
|
const probeAfterMap = this.#credentialBackoffProbeAfter.get(key);
|
|
6400
6581
|
probeAfterMap?.delete(credentialIndex);
|
|
6401
6582
|
if (probeAfterMap?.size === 0) this.#credentialBackoffProbeAfter.delete(key);
|
|
@@ -6687,6 +6868,7 @@ export class AuthStorage {
|
|
|
6687
6868
|
return (
|
|
6688
6869
|
await this.markUsageLimitReached(provider, sessionId, {
|
|
6689
6870
|
retryAfterMs,
|
|
6871
|
+
providerTimed: retryAfterMs !== undefined,
|
|
6690
6872
|
modelId: options?.modelId,
|
|
6691
6873
|
apiKey: options?.apiKey,
|
|
6692
6874
|
credentialId: options?.credentialId,
|
|
@@ -6726,6 +6908,7 @@ export class AuthStorage {
|
|
|
6726
6908
|
sessionCredential.index,
|
|
6727
6909
|
Date.now() + AuthStorage.#defaultBackoffMs,
|
|
6728
6910
|
routing,
|
|
6911
|
+
false,
|
|
6729
6912
|
).switched;
|
|
6730
6913
|
}
|
|
6731
6914
|
|
package/src/error/flags.ts
CHANGED
|
@@ -157,7 +157,7 @@ const TRANSIENT_ENVELOPE_PATTERN = /anthropic stream envelope error:/i;
|
|
|
157
157
|
const TRANSIENT_ENVELOPE_TRUNCATION_PATTERN = /before message_(?:start|stop)/i;
|
|
158
158
|
export const STREAM_READ_ERROR_PATTERN = /stream[_ -]?read[_ -]?error/i;
|
|
159
159
|
export const TRANSIENT_TRANSPORT_PATTERN =
|
|
160
|
-
/\b(?:no[_ -]?capacity|(?:high|peak)[ _-]?demand|(?:at|over|insufficient)[ _-]?capacity|capacity[ _-]?(?:exceeded|exhausted)|peak[ _-]?load)\b|overloaded|provider.?returned.?error|rate.?limit|too many requests|\b(?:429|500|502|503|504)\b|service.?unavailable|server.?error|internal.?error|retry your request|network.?error|connection.?error|connection.?refused|unable.?to.?connect\.\s*is the computer able to access the url\?|other side closed|fetch failed|upstream.?connect|upstream.?request.?failed|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay|stream stall|no error details in response|HTTP2(?:StreamReset|RefusedStream|EnhanceYourCalm)|nghttp2_(?:internal_error|refused_stream)|stream closed with error code nghttp2_(?:internal_error|refused_stream)|malformed.?function.?call/i;
|
|
160
|
+
/\b(?:no[_ -]?capacity|(?:high|peak)[ _-]?demand|(?:at|over|insufficient)[ _-]?capacity|capacity[ _-]?(?:exceeded|exhausted)|peak[ _-]?load)\b|overloaded|provider.?returned.?error|rate.?limit|too many requests|auth-gateway\s+5\d{2}(?=[:\s]|$)|\b(?:429|500|502|503|504)\b|service.?unavailable|server.?error|internal.?error|retry your request|network.?error|connection.?error|connection.?refused|unable.?to.?connect\.\s*is the computer able to access the url\?|other side closed|fetch failed|upstream.?connect|upstream.?request.?failed|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay|stream stall|no error details in response|HTTP2(?:StreamReset|RefusedStream|EnhanceYourCalm)|nghttp2_(?:internal_error|refused_stream)|stream closed with error code nghttp2_(?:internal_error|refused_stream)|malformed.?function.?call/i;
|
|
161
161
|
const AUTH_FAILURE_PATTERN =
|
|
162
162
|
/\b(?:401|403|unauthorized|forbidden|authentication|auth[_ ]?unavailable|no auth available|(?:invalid|no)[_ ]?api[_ ]?key)\b/i;
|
|
163
163
|
const MALFORMED_FUNCTION_CALL_PATTERN = /\bmalformed.?function.?call\b/i;
|
|
@@ -236,10 +236,6 @@ export function streamPiNative<TApi extends Api>(
|
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
if (!sawTerminal) {
|
|
239
|
-
// SSE closed before a terminal event reached us — synthesize one
|
|
240
|
-
// so awaiters of `.result()` resolve instead of hanging forever.
|
|
241
|
-
// Matches the gateway's own defensive fallback in
|
|
242
|
-
// `pi-native-server.encodeStream`.
|
|
243
239
|
const aborted = abortTracker.wasCallerAbort();
|
|
244
240
|
const partial = makeSyntheticAssistant(model as Model<Api>);
|
|
245
241
|
if (aborted) {
|
|
@@ -247,8 +243,16 @@ export function streamPiNative<TApi extends Api>(
|
|
|
247
243
|
partial.errorMessage = "stream closed without terminal event";
|
|
248
244
|
stream.push({ type: "error", reason: "aborted", error: partial });
|
|
249
245
|
} else {
|
|
250
|
-
|
|
251
|
-
|
|
246
|
+
stream.fail(
|
|
247
|
+
new AIError.ProviderResponseError(
|
|
248
|
+
"pi-native stream read error: stream closed before a terminal response event",
|
|
249
|
+
{
|
|
250
|
+
provider: model.provider,
|
|
251
|
+
kind: "incomplete-stream",
|
|
252
|
+
},
|
|
253
|
+
),
|
|
254
|
+
);
|
|
255
|
+
return;
|
|
252
256
|
}
|
|
253
257
|
}
|
|
254
258
|
stream.end();
|
|
@@ -16,7 +16,7 @@ import type { FetchImpl } from "../../types";
|
|
|
16
16
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
17
17
|
|
|
18
18
|
const CLIENT_ID = "Ov23ctDVkRmgkPke0Mmm";
|
|
19
|
-
const OAUTH_SCOPE = "read:user
|
|
19
|
+
const OAUTH_SCOPE = "read:user";
|
|
20
20
|
const OAUTH_HEADERS = {
|
|
21
21
|
Accept: "application/json",
|
|
22
22
|
"Content-Type": "application/x-www-form-urlencoded",
|