@oh-my-pi/pi-ai 16.1.14 → 16.1.15
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 +7 -0
- package/dist/types/auth-retry.d.ts +6 -2
- package/dist/types/providers/openai-shared.d.ts +3 -0
- package/dist/types/rate-limit-utils.d.ts +26 -0
- package/dist/types/registry/nvidia.d.ts +0 -1
- package/dist/types/registry/registry.d.ts +0 -1
- package/dist/types/registry/types.d.ts +0 -2
- package/package.json +4 -4
- package/src/auth-gateway/server.ts +3 -3
- package/src/auth-retry.ts +12 -7
- package/src/auth-storage.ts +7 -11
- package/src/providers/openai-shared.ts +16 -6
- package/src/rate-limit-utils.ts +49 -1
- package/src/registry/nvidia.ts +0 -1
- package/src/registry/types.ts +0 -2
- package/src/stream.ts +11 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.1.15] - 2026-06-22
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed API-key `/login` providers replacing sibling credentials instead of appending new keys for the same provider. ([#3265](https://github.com/can1357/oh-my-pi/issues/3265))
|
|
10
|
+
- Fixed OpenAI Codex OAuth account rotation for quota failures that surface as bare HTTP 429 or `insufficient_quota`, so pre-content failures temporarily block only the exhausted credential and retry a healthy sibling. The 429 status-only fallback applies only to absent/opaque bodies; informative transient bodies (`Too many requests`, `Service overloaded 529`, `Please retry in 5s`, …) defer to `parseRateLimitReason` and stay in the provider's own backoff layer instead of burning sibling credentials. ([#3231](https://github.com/can1357/oh-my-pi/issues/3231))
|
|
11
|
+
|
|
5
12
|
## [16.1.14] - 2026-06-22
|
|
6
13
|
|
|
7
14
|
### Added
|
|
@@ -48,8 +48,12 @@ export declare function resolveApiKeyOnce(key: ApiKey | undefined, signal?: Abor
|
|
|
48
48
|
export declare function seedApiKeyResolver(seed: string | undefined, resolver: ApiKeyResolver): ApiKeyResolver;
|
|
49
49
|
/**
|
|
50
50
|
* Classifies whether an error should trigger a credential refresh/rotation
|
|
51
|
-
* retry: a hard `401`,
|
|
52
|
-
*
|
|
51
|
+
* retry: a hard `401`, body-classified usage limit (Codex
|
|
52
|
+
* `usage_limit_reached`, Anthropic account rate-limit, Google
|
|
53
|
+
* `resource_exhausted`, OpenAI `insufficient_quota`, …), or a bare `429`
|
|
54
|
+
* whose payload did not preserve a richer quota code. Transient 429s
|
|
55
|
+
* (`Too many requests`, per-minute caps) classify as `RATE_LIMIT_EXCEEDED`
|
|
56
|
+
* via {@link parseRateLimitReason} and stay in the upstream-backoff lane.
|
|
53
57
|
*/
|
|
54
58
|
export declare function isAuthRetryableError(error: unknown): boolean;
|
|
55
59
|
/**
|
|
@@ -475,9 +475,12 @@ export declare function populateResponsesUsageFromResponse(output: AssistantMess
|
|
|
475
475
|
input_tokens_details?: {
|
|
476
476
|
cached_tokens?: number | null;
|
|
477
477
|
cache_write_tokens?: number | null;
|
|
478
|
+
orchestration_input_tokens?: number | null;
|
|
479
|
+
orchestration_input_cached_tokens?: number | null;
|
|
478
480
|
} | null;
|
|
479
481
|
output_tokens_details?: {
|
|
480
482
|
reasoning_tokens?: number | null;
|
|
483
|
+
orchestration_output_tokens?: number | null;
|
|
481
484
|
} | null;
|
|
482
485
|
} | null | undefined): void;
|
|
483
486
|
/**
|
|
@@ -17,4 +17,30 @@ export declare function parseRateLimitReason(errorMessage: string): RateLimitRea
|
|
|
17
17
|
* MODEL_CAPACITY gets jitter to prevent thundering herd.
|
|
18
18
|
*/
|
|
19
19
|
export declare function calculateRateLimitBackoffMs(reason: RateLimitReason): number;
|
|
20
|
+
/**
|
|
21
|
+
* HTTP status codes that, absent richer body classification, represent an
|
|
22
|
+
* account-local usage cap rather than a bad credential or a transient blip.
|
|
23
|
+
* Always combine with {@link isUsageLimitOutcome} when a message is available
|
|
24
|
+
* — a 429 carrying transient rate-limit wording is NOT a usage cap.
|
|
25
|
+
*/
|
|
26
|
+
export declare function isUsageLimitStatus(status: number | undefined): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Returns true for failures that should burn one credential and rotate to a
|
|
29
|
+
* sibling account. Decision tree:
|
|
30
|
+
*
|
|
31
|
+
* 1. Body matches {@link isUsageLimitError} (Codex `usage_limit_reached`,
|
|
32
|
+
* Anthropic account rate-limit, Google `resource_exhausted`, OpenAI
|
|
33
|
+
* `insufficient_quota`, …) → rotate.
|
|
34
|
+
* 2. Status is not 429 → backoff (caller's domain).
|
|
35
|
+
* 3. Body is absent or {@link isOpaqueStatusBody opaque} (just the status,
|
|
36
|
+
* empty JSON, HTTP framing only) → rotate conservatively: the server
|
|
37
|
+
* gave us nothing else to go on.
|
|
38
|
+
* 4. Body has content → defer to {@link parseRateLimitReason}. Only
|
|
39
|
+
* `QUOTA_EXHAUSTED` rotates; `RATE_LIMIT_EXCEEDED` (`Too many requests`,
|
|
40
|
+
* per-minute caps), `MODEL_CAPACITY_EXHAUSTED` (`Service overloaded`),
|
|
41
|
+
* `SERVER_ERROR`, and `UNKNOWN` (`Please retry in 5s`) stay in the
|
|
42
|
+
* provider's own backoff layer so transient 429s don't burn sibling
|
|
43
|
+
* credentials.
|
|
44
|
+
*/
|
|
45
|
+
export declare function isUsageLimitOutcome(status: number | undefined, message: string | undefined): boolean;
|
|
20
46
|
export declare function isUsageLimitError(errorMessage: string): boolean;
|
|
@@ -153,7 +153,6 @@ declare const ALL: ({
|
|
|
153
153
|
readonly id: "nvidia";
|
|
154
154
|
readonly name: "NVIDIA";
|
|
155
155
|
readonly login: (cb: import("./oauth").OAuthLoginCallbacks) => Promise<string>;
|
|
156
|
-
readonly appendApiKeyLogin: true;
|
|
157
156
|
} | {
|
|
158
157
|
readonly id: "ollama";
|
|
159
158
|
readonly name: "Ollama (Local OpenAI-compatible)";
|
|
@@ -40,8 +40,6 @@ export interface ProviderDefinition {
|
|
|
40
40
|
readonly showInLoginList?: boolean;
|
|
41
41
|
readonly envKeys?: KeyResolver;
|
|
42
42
|
readonly login?: (callbacks: OAuthLoginCallbacks) => Promise<OAuthCredentials | string>;
|
|
43
|
-
/** String-returning login appends API keys instead of replacing the provider's existing key. */
|
|
44
|
-
readonly appendApiKeyLogin?: boolean;
|
|
45
43
|
readonly refreshToken?: (credentials: OAuthCredentials) => Promise<OAuthCredentials>;
|
|
46
44
|
readonly getApiKey?: (credentials: OAuthCredentials) => string;
|
|
47
45
|
/** Store OAuth credentials under a different provider id (e.g. `openai-codex-device` ⇒ `openai-codex`). */
|
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.1.
|
|
4
|
+
"version": "16.1.15",
|
|
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.1.
|
|
42
|
-
"@oh-my-pi/pi-utils": "16.1.
|
|
43
|
-
"@oh-my-pi/pi-wire": "16.1.
|
|
41
|
+
"@oh-my-pi/pi-catalog": "16.1.15",
|
|
42
|
+
"@oh-my-pi/pi-utils": "16.1.15",
|
|
43
|
+
"@oh-my-pi/pi-wire": "16.1.15",
|
|
44
44
|
"arktype": "^2.2.0",
|
|
45
45
|
"zod": "^4"
|
|
46
46
|
},
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { Effort } from "@oh-my-pi/pi-catalog/effort";
|
|
22
|
-
import { extractRetryHint, logger } from "@oh-my-pi/pi-utils";
|
|
22
|
+
import { extractHttpStatusFromError, extractRetryHint, logger } from "@oh-my-pi/pi-utils";
|
|
23
23
|
import type { ApiKeyResolver } from "../auth-retry";
|
|
24
24
|
import type { AuthStorage } from "../auth-storage";
|
|
25
25
|
import * as anthropicMessages from "../providers/anthropic-messages-server";
|
|
26
26
|
import * as openaiChat from "../providers/openai-chat-server";
|
|
27
27
|
import * as openaiResponses from "../providers/openai-responses-server";
|
|
28
28
|
import * as piNative from "../providers/pi-native-server";
|
|
29
|
-
import { isUsageLimitError } from "../rate-limit-utils";
|
|
29
|
+
import { isUsageLimitError, isUsageLimitOutcome } from "../rate-limit-utils";
|
|
30
30
|
import { streamSimple } from "../stream";
|
|
31
31
|
import type { Api, AssistantMessageEventStream, Context, Model, SimpleStreamOptions } from "../types";
|
|
32
32
|
import { deterministicUuid } from "../utils/deterministic-id";
|
|
@@ -315,7 +315,7 @@ async function refreshGatewayApiKeyAfterAuthError(
|
|
|
315
315
|
peer: string,
|
|
316
316
|
): Promise<string | undefined> {
|
|
317
317
|
const message = error instanceof Error ? error.message : String(error);
|
|
318
|
-
if (
|
|
318
|
+
if (isUsageLimitOutcome(extractHttpStatusFromError(error), message)) {
|
|
319
319
|
const retryAfterMs = extractRetryHint(undefined, message);
|
|
320
320
|
const { switched, retryAtMs } = await storage.markUsageLimitReached(provider, sessionId, {
|
|
321
321
|
retryAfterMs,
|
package/src/auth-retry.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { extractHttpStatusFromError } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import type { OAuthAccess } from "./auth-storage";
|
|
3
|
-
import {
|
|
3
|
+
import { isUsageLimitOutcome } from "./rate-limit-utils";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Context passed to an {@link ApiKeyResolver} on each resolution attempt.
|
|
@@ -72,15 +72,20 @@ export function seedApiKeyResolver(seed: string | undefined, resolver: ApiKeyRes
|
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
74
|
* Classifies whether an error should trigger a credential refresh/rotation
|
|
75
|
-
* retry: a hard `401`,
|
|
76
|
-
*
|
|
75
|
+
* retry: a hard `401`, body-classified usage limit (Codex
|
|
76
|
+
* `usage_limit_reached`, Anthropic account rate-limit, Google
|
|
77
|
+
* `resource_exhausted`, OpenAI `insufficient_quota`, …), or a bare `429`
|
|
78
|
+
* whose payload did not preserve a richer quota code. Transient 429s
|
|
79
|
+
* (`Too many requests`, per-minute caps) classify as `RATE_LIMIT_EXCEEDED`
|
|
80
|
+
* via {@link parseRateLimitReason} and stay in the upstream-backoff lane.
|
|
77
81
|
*/
|
|
78
82
|
export function isAuthRetryableError(error: unknown): boolean {
|
|
79
|
-
|
|
83
|
+
const status = extractHttpStatusFromError(error);
|
|
84
|
+
if (status === 401) return true;
|
|
80
85
|
const message = error instanceof Error ? error.message : typeof error === "string" ? error : undefined;
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
return
|
|
86
|
+
const embeddedStatus = message ? extractHttpStatusFromError({ message }) : undefined;
|
|
87
|
+
if (embeddedStatus === 401) return true;
|
|
88
|
+
return isUsageLimitOutcome(status ?? embeddedStatus, message);
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
/**
|
package/src/auth-storage.ts
CHANGED
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
import { Database, type Statement } from "bun:sqlite";
|
|
11
11
|
import * as fs from "node:fs/promises";
|
|
12
12
|
import * as path from "node:path";
|
|
13
|
-
import { getAgentDbPath, logger } from "@oh-my-pi/pi-utils";
|
|
13
|
+
import { extractHttpStatusFromError, getAgentDbPath, logger } from "@oh-my-pi/pi-utils";
|
|
14
14
|
import type { ApiKeyResolver } from "./auth-retry";
|
|
15
|
-
import {
|
|
15
|
+
import { isUsageLimitOutcome } from "./rate-limit-utils";
|
|
16
16
|
import { getProviderDefinition } from "./registry";
|
|
17
17
|
import { getOAuthApiKey, getOAuthProvider, refreshOAuthToken } from "./registry/oauth";
|
|
18
18
|
import type { OAuthController, OAuthCredentials, OAuthProvider, OAuthProviderId } from "./registry/oauth/types";
|
|
@@ -1845,14 +1845,9 @@ export class AuthStorage {
|
|
|
1845
1845
|
return;
|
|
1846
1846
|
}
|
|
1847
1847
|
const newCredential: ApiKeyCredential = { type: "api_key", key: result };
|
|
1848
|
-
const
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
? await this.#store.upsertAuthCredentialRemote(provider, newCredential)
|
|
1852
|
-
: this.#store.upsertAuthCredentialForProvider(provider, newCredential)
|
|
1853
|
-
: this.#store.replaceAuthCredentialsRemote
|
|
1854
|
-
? await this.#store.replaceAuthCredentialsRemote(provider, [newCredential])
|
|
1855
|
-
: this.#store.replaceAuthCredentialsForProvider(provider, [newCredential]);
|
|
1848
|
+
const stored = this.#store.upsertAuthCredentialRemote
|
|
1849
|
+
? await this.#store.upsertAuthCredentialRemote(provider, newCredential)
|
|
1850
|
+
: this.#store.upsertAuthCredentialForProvider(provider, newCredential);
|
|
1856
1851
|
this.#setStoredCredentials(
|
|
1857
1852
|
provider,
|
|
1858
1853
|
stored.map(entry => ({ id: entry.id, credential: entry.credential })),
|
|
@@ -4089,8 +4084,9 @@ export class AuthStorage {
|
|
|
4089
4084
|
if (!sessionCredential) return false;
|
|
4090
4085
|
|
|
4091
4086
|
const error = options?.error;
|
|
4087
|
+
const status = extractHttpStatusFromError(error);
|
|
4092
4088
|
const message = error instanceof Error ? error.message : typeof error === "string" ? error : undefined;
|
|
4093
|
-
if (
|
|
4089
|
+
if (isUsageLimitOutcome(status, message)) {
|
|
4094
4090
|
return (
|
|
4095
4091
|
await this.markUsageLimitReached(provider, sessionId, {
|
|
4096
4092
|
modelId: options?.modelId,
|
|
@@ -2401,19 +2401,29 @@ export function populateResponsesUsageFromResponse(
|
|
|
2401
2401
|
input_tokens_details?: {
|
|
2402
2402
|
cached_tokens?: number | null;
|
|
2403
2403
|
cache_write_tokens?: number | null;
|
|
2404
|
+
orchestration_input_tokens?: number | null;
|
|
2405
|
+
orchestration_input_cached_tokens?: number | null;
|
|
2406
|
+
} | null;
|
|
2407
|
+
output_tokens_details?: {
|
|
2408
|
+
reasoning_tokens?: number | null;
|
|
2409
|
+
orchestration_output_tokens?: number | null;
|
|
2404
2410
|
} | null;
|
|
2405
|
-
output_tokens_details?: { reasoning_tokens?: number | null } | null;
|
|
2406
2411
|
}
|
|
2407
2412
|
| null
|
|
2408
2413
|
| undefined,
|
|
2409
2414
|
): void {
|
|
2410
2415
|
if (!usage) return;
|
|
2416
|
+
const details = usage.input_tokens_details;
|
|
2417
|
+
const outputDetails = usage.output_tokens_details;
|
|
2418
|
+
const orchestrationInputTokens = details?.orchestration_input_tokens ?? 0;
|
|
2419
|
+
const orchestrationInputCachedTokens = details?.orchestration_input_cached_tokens ?? 0;
|
|
2420
|
+
const orchestrationOutputTokens = outputDetails?.orchestration_output_tokens ?? 0;
|
|
2411
2421
|
const accounting = calculateOpenAIUsageAccounting({
|
|
2412
|
-
promptTokens: usage.input_tokens ?? 0,
|
|
2413
|
-
outputTokens: usage.output_tokens ?? 0,
|
|
2414
|
-
cachedTokens:
|
|
2415
|
-
reasoningTokens:
|
|
2416
|
-
cacheWriteOpenRouter:
|
|
2422
|
+
promptTokens: (usage.input_tokens ?? 0) + orchestrationInputTokens,
|
|
2423
|
+
outputTokens: (usage.output_tokens ?? 0) + orchestrationOutputTokens,
|
|
2424
|
+
cachedTokens: (details?.cached_tokens ?? usage.prompt_cache_hit_tokens ?? 0) + orchestrationInputCachedTokens,
|
|
2425
|
+
reasoningTokens: outputDetails?.reasoning_tokens ?? 0,
|
|
2426
|
+
cacheWriteOpenRouter: details?.cache_write_tokens ?? undefined,
|
|
2417
2427
|
cacheWriteDeepSeek: usage.prompt_cache_miss_tokens ?? undefined,
|
|
2418
2428
|
hasDeepSeekCacheHitAndMiss:
|
|
2419
2429
|
usage.prompt_cache_hit_tokens !== undefined && usage.prompt_cache_miss_tokens !== undefined,
|
package/src/rate-limit-utils.ts
CHANGED
|
@@ -100,7 +100,55 @@ export function calculateRateLimitBackoffMs(reason: RateLimitReason): number {
|
|
|
100
100
|
|
|
101
101
|
/** Detect usage/quota limit errors in error messages (persistent, requires credential switch). */
|
|
102
102
|
const USAGE_LIMIT_PATTERN =
|
|
103
|
-
/usage.?limit|usage_limit_reached|usage_not_included|limit_reached|quota.?exceeded|quota.?reached|resource.?exhausted|exhausted your capacity|quota will reset|insufficient.?balance/i;
|
|
103
|
+
/usage.?limit|usage_limit_reached|usage_not_included|limit_reached|quota.?exceeded|quota.?reached|resource.?exhausted|exhausted your capacity|quota will reset|insufficient.?(?:balance|quota)/i;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* HTTP status codes that, absent richer body classification, represent an
|
|
107
|
+
* account-local usage cap rather than a bad credential or a transient blip.
|
|
108
|
+
* Always combine with {@link isUsageLimitOutcome} when a message is available
|
|
109
|
+
* — a 429 carrying transient rate-limit wording is NOT a usage cap.
|
|
110
|
+
*/
|
|
111
|
+
export function isUsageLimitStatus(status: number | undefined): boolean {
|
|
112
|
+
return status === 429;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Returns true for failures that should burn one credential and rotate to a
|
|
117
|
+
* sibling account. Decision tree:
|
|
118
|
+
*
|
|
119
|
+
* 1. Body matches {@link isUsageLimitError} (Codex `usage_limit_reached`,
|
|
120
|
+
* Anthropic account rate-limit, Google `resource_exhausted`, OpenAI
|
|
121
|
+
* `insufficient_quota`, …) → rotate.
|
|
122
|
+
* 2. Status is not 429 → backoff (caller's domain).
|
|
123
|
+
* 3. Body is absent or {@link isOpaqueStatusBody opaque} (just the status,
|
|
124
|
+
* empty JSON, HTTP framing only) → rotate conservatively: the server
|
|
125
|
+
* gave us nothing else to go on.
|
|
126
|
+
* 4. Body has content → defer to {@link parseRateLimitReason}. Only
|
|
127
|
+
* `QUOTA_EXHAUSTED` rotates; `RATE_LIMIT_EXCEEDED` (`Too many requests`,
|
|
128
|
+
* per-minute caps), `MODEL_CAPACITY_EXHAUSTED` (`Service overloaded`),
|
|
129
|
+
* `SERVER_ERROR`, and `UNKNOWN` (`Please retry in 5s`) stay in the
|
|
130
|
+
* provider's own backoff layer so transient 429s don't burn sibling
|
|
131
|
+
* credentials.
|
|
132
|
+
*/
|
|
133
|
+
export function isUsageLimitOutcome(status: number | undefined, message: string | undefined): boolean {
|
|
134
|
+
if (message && isUsageLimitError(message)) return true;
|
|
135
|
+
if (!isUsageLimitStatus(status)) return false;
|
|
136
|
+
if (!message || isOpaqueStatusBody(message)) return true;
|
|
137
|
+
return parseRateLimitReason(message) === "QUOTA_EXHAUSTED";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* A 429 body is opaque when it carries no signal beyond the status itself —
|
|
142
|
+
* empty, whitespace-only, the status digits with HTTP/JSON framing, or
|
|
143
|
+
* generic punctuation. Anything else (retry hints, capacity wording, error
|
|
144
|
+
* descriptions) is informative enough to defer to the classifier.
|
|
145
|
+
*/
|
|
146
|
+
function isOpaqueStatusBody(message: string): boolean {
|
|
147
|
+
const cleaned = message
|
|
148
|
+
.replace(/\b429\b/g, "")
|
|
149
|
+
.replace(/\b(?:http|https|status|error|code|response|message)\b/gi, "");
|
|
150
|
+
return !/[a-z\d]{3,}/i.test(cleaned);
|
|
151
|
+
}
|
|
104
152
|
|
|
105
153
|
export function isUsageLimitError(errorMessage: string): boolean {
|
|
106
154
|
return USAGE_LIMIT_PATTERN.test(errorMessage) || ACCOUNT_RATE_LIMIT_PATTERN.test(errorMessage);
|
package/src/registry/nvidia.ts
CHANGED
package/src/registry/types.ts
CHANGED
|
@@ -44,8 +44,6 @@ export interface ProviderDefinition {
|
|
|
44
44
|
readonly envKeys?: KeyResolver;
|
|
45
45
|
// --- interactive login (OAuthProviderInterface-compatible) ---
|
|
46
46
|
readonly login?: (callbacks: OAuthLoginCallbacks) => Promise<OAuthCredentials | string>;
|
|
47
|
-
/** String-returning login appends API keys instead of replacing the provider's existing key. */
|
|
48
|
-
readonly appendApiKeyLogin?: boolean;
|
|
49
47
|
readonly refreshToken?: (credentials: OAuthCredentials) => Promise<OAuthCredentials>;
|
|
50
48
|
readonly getApiKey?: (credentials: OAuthCredentials) => string;
|
|
51
49
|
/** Store OAuth credentials under a different provider id (e.g. `openai-codex-device` ⇒ `openai-codex`). */
|
package/src/stream.ts
CHANGED
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
streamOpenAIResponses,
|
|
49
49
|
} from "./providers/register-builtins";
|
|
50
50
|
import { isSyntheticModel, streamSynthetic } from "./providers/synthetic";
|
|
51
|
-
import {
|
|
51
|
+
import { isUsageLimitOutcome } from "./rate-limit-utils";
|
|
52
52
|
import { PROVIDER_REGISTRY } from "./registry";
|
|
53
53
|
import type {
|
|
54
54
|
Api,
|
|
@@ -384,13 +384,18 @@ function extractStatusFromAssistantError(message: AssistantMessage): number | un
|
|
|
384
384
|
function isRetryableUpstreamError(error: unknown, status: number | undefined, message: string | undefined): boolean {
|
|
385
385
|
// 401 means the credential is bad. Usage-limit phrasing (Codex's
|
|
386
386
|
// "You have hit your ChatGPT usage limit", Anthropic's "usage_limit_reached",
|
|
387
|
-
// Google's "resource_exhausted"
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
// `
|
|
387
|
+
// Google's "resource_exhausted", OpenAI's "insufficient_quota") and 429s
|
|
388
|
+
// without transient rate-limit wording mean this account is parked but a
|
|
389
|
+
// sibling credential can usually pick the request up. Both are rotatable
|
|
390
|
+
// via `onAuthError` — the auth-gateway maps the former to
|
|
391
|
+
// `invalidateCredentialMatching` and the latter to
|
|
392
|
+
// `markUsageLimitReached`. Transient 429s ("Too many requests",
|
|
393
|
+
// per-minute caps) classify as RATE_LIMIT_EXCEEDED in
|
|
394
|
+
// `parseRateLimitReason` and stay in the provider's own backoff layer
|
|
395
|
+
// instead of burning siblings.
|
|
391
396
|
if (status === 401) return true;
|
|
392
397
|
void error;
|
|
393
|
-
return
|
|
398
|
+
return isUsageLimitOutcome(status, message);
|
|
394
399
|
}
|
|
395
400
|
|
|
396
401
|
function createAssistantAuthError(message: AssistantMessage): Error {
|