jeopi-ai 16.2.20 → 16.2.22
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 +6 -0
- package/package.json +4 -4
- package/src/error/rate-limit.ts +1 -1
- package/src/providers/google-gemini-cli.ts +112 -10
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.2.21] - 2026-07-02
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed Cloud Code Assist `Individual quota reached` 429 responses to render as concise quota-exhausted messages with the affected model, account, reset delay, and `/login` recovery guidance instead of dumping the raw Google error JSON. The shared retry-hint parser now also understands Cloud Code Assist `Resets in …` text and `quotaResetDelay` metadata.
|
|
10
|
+
|
|
5
11
|
## [16.2.18] - 2026-07-02
|
|
6
12
|
|
|
7
13
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "jeopi-ai",
|
|
4
|
-
"version": "16.2.
|
|
4
|
+
"version": "16.2.22",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://github.com/akillness/jeopi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bufbuild/protobuf": "^2.12.0",
|
|
41
|
-
"jeopi-catalog": "16.2.
|
|
42
|
-
"jeopi-utils": "16.2.
|
|
43
|
-
"jeopi-wire": "16.2.
|
|
41
|
+
"jeopi-catalog": "16.2.22",
|
|
42
|
+
"jeopi-utils": "16.2.22",
|
|
43
|
+
"jeopi-wire": "16.2.22",
|
|
44
44
|
"arktype": "^2.2.0",
|
|
45
45
|
"zod": "^4"
|
|
46
46
|
},
|
package/src/error/rate-limit.ts
CHANGED
|
@@ -100,7 +100,7 @@ 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|quota)/i;
|
|
103
|
+
/usage.?limit|usage_limit_reached|usage_not_included|limit_reached|quota.?exceeded|quota.?reached|quota.?exhaust(ed|ion)?|resource.?exhausted|exhausted your capacity|quota will reset|insufficient.?(?:balance|quota)/i;
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
106
|
* HTTP status codes that, absent richer body classification, represent an
|
|
@@ -423,6 +423,99 @@ export function shouldRefreshGeminiCliCredentials(
|
|
|
423
423
|
const skewMs = isAntigravity ? ANTIGRAVITY_REFRESH_SKEW_MS : GOOGLE_GEMINI_REFRESH_SKEW_MS;
|
|
424
424
|
return nowMs + skewMs >= expiresAt;
|
|
425
425
|
}
|
|
426
|
+
interface CloudCodeAssistQuotaDetails {
|
|
427
|
+
message?: string;
|
|
428
|
+
model?: string;
|
|
429
|
+
quotaResetDelay?: string;
|
|
430
|
+
quotaResetTimeStamp?: string;
|
|
431
|
+
retryDelay?: string;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function recordOf(value: unknown): Record<string, unknown> | undefined {
|
|
435
|
+
return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function stringField(record: Record<string, unknown> | undefined, key: string): string | undefined {
|
|
439
|
+
const value = record?.[key];
|
|
440
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function extractCloudCodeAssistQuotaDetails(errorBody: string): CloudCodeAssistQuotaDetails | undefined {
|
|
444
|
+
if (!/\bquota\b|QUOTA_EXHAUSTED|RESOURCE_EXHAUSTED/i.test(errorBody)) return undefined;
|
|
445
|
+
const jsonStart = errorBody.indexOf("{");
|
|
446
|
+
if (jsonStart === -1) return undefined;
|
|
447
|
+
|
|
448
|
+
try {
|
|
449
|
+
const jsonText = errorBody.slice(jsonStart).replace(/\r?\n/g, " ");
|
|
450
|
+
const parsed = JSON.parse(jsonText);
|
|
451
|
+
const root = recordOf(parsed);
|
|
452
|
+
const error = recordOf(root?.error);
|
|
453
|
+
if (!error) return undefined;
|
|
454
|
+
|
|
455
|
+
const message = stringField(error, "message");
|
|
456
|
+
const status = stringField(error, "status");
|
|
457
|
+
const details = Array.isArray(error.details) ? error.details : [];
|
|
458
|
+
|
|
459
|
+
let isQuotaExhausted =
|
|
460
|
+
status === "RESOURCE_EXHAUSTED" && message !== undefined && /\bquota\b|exhausted/i.test(message);
|
|
461
|
+
let model: string | undefined;
|
|
462
|
+
let quotaResetDelay: string | undefined;
|
|
463
|
+
let quotaResetTimeStamp: string | undefined;
|
|
464
|
+
let retryDelay: string | undefined;
|
|
465
|
+
|
|
466
|
+
for (const rawDetail of details) {
|
|
467
|
+
const detail = recordOf(rawDetail);
|
|
468
|
+
if (!detail) continue;
|
|
469
|
+
const reason = stringField(detail, "reason");
|
|
470
|
+
if (reason === "QUOTA_EXHAUSTED") {
|
|
471
|
+
isQuotaExhausted = true;
|
|
472
|
+
}
|
|
473
|
+
retryDelay ??= stringField(detail, "retryDelay");
|
|
474
|
+
const metadata = recordOf(detail.metadata);
|
|
475
|
+
model ??= stringField(metadata, "model");
|
|
476
|
+
quotaResetDelay ??= stringField(metadata, "quotaResetDelay");
|
|
477
|
+
quotaResetTimeStamp ??=
|
|
478
|
+
stringField(metadata, "quotaResetTimeStamp") ?? stringField(metadata, "quotaResetTimestamp");
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (!isQuotaExhausted) return undefined;
|
|
482
|
+
return { message, model, quotaResetDelay, quotaResetTimeStamp, retryDelay };
|
|
483
|
+
} catch {
|
|
484
|
+
return undefined;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function sentence(text: string): string {
|
|
489
|
+
return /[.!?]$/.test(text) ? text : `${text}.`;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function formatCloudCodeAssistQuotaMessage(errorBody: string, email?: string): string | undefined {
|
|
493
|
+
const details = extractCloudCodeAssistQuotaDetails(errorBody);
|
|
494
|
+
if (!details) return undefined;
|
|
495
|
+
|
|
496
|
+
const target = details.model ? ` for ${details.model}` : "";
|
|
497
|
+
const account = email ? ` on ${email}` : "";
|
|
498
|
+
const parts = [`Cloud Code Assist quota exhausted${target}${account}.`];
|
|
499
|
+
|
|
500
|
+
const providerSummary = details.message
|
|
501
|
+
?.replace(/\s+/g, " ")
|
|
502
|
+
.trim()
|
|
503
|
+
.split(/\s+Resets?\s+in\s+/i)[0];
|
|
504
|
+
if (providerSummary) {
|
|
505
|
+
parts.push(sentence(providerSummary));
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const resetDelay = details.quotaResetDelay ?? details.retryDelay;
|
|
509
|
+
if (resetDelay) {
|
|
510
|
+
const resetTime = details.quotaResetTimeStamp ? ` (${details.quotaResetTimeStamp})` : "";
|
|
511
|
+
parts.push(`Your quota will reset after ${resetDelay}${resetTime}.`);
|
|
512
|
+
} else if (details.quotaResetTimeStamp) {
|
|
513
|
+
parts.push(`Your quota resets at ${details.quotaResetTimeStamp}.`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
parts.push("Add another Google account with /login, switch models/providers, or wait for the reset.");
|
|
517
|
+
return parts.join(" ");
|
|
518
|
+
}
|
|
426
519
|
|
|
427
520
|
interface CloudCodeAssistRequest {
|
|
428
521
|
project: string;
|
|
@@ -759,7 +852,11 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
759
852
|
event => options?.onSseEvent?.({ event: event.event, data: event.data, raw: [...event.raw] }, model),
|
|
760
853
|
)) {
|
|
761
854
|
if (chunk.error) {
|
|
762
|
-
const
|
|
855
|
+
const quotaDetail = formatCloudCodeAssistQuotaMessage(
|
|
856
|
+
JSON.stringify({ error: chunk.error }),
|
|
857
|
+
parsedCredentials.email,
|
|
858
|
+
);
|
|
859
|
+
const detail = quotaDetail ?? chunk.error.message ?? chunk.error.status ?? "unknown error";
|
|
763
860
|
const message = `Cloud Code Assist stream error: ${detail}`;
|
|
764
861
|
throw typeof chunk.error.code === "number" && chunk.error.code >= 400
|
|
765
862
|
? new AIError.GeminiCliApiError(message, chunk.error.code)
|
|
@@ -949,14 +1046,17 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
949
1046
|
}
|
|
950
1047
|
}
|
|
951
1048
|
const errorText = await response.text();
|
|
952
|
-
const
|
|
953
|
-
const
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1049
|
+
const quotaMessage = formatCloudCodeAssistQuotaMessage(errorText, parsedCredentials.email);
|
|
1050
|
+
const validationUrl = quotaMessage ? undefined : extractGoogleValidationUrl(errorText);
|
|
1051
|
+
const errorMessage =
|
|
1052
|
+
quotaMessage ??
|
|
1053
|
+
(validationUrl
|
|
1054
|
+
? formatGoogleValidationRequiredMessage(
|
|
1055
|
+
validationUrl,
|
|
1056
|
+
"retry your request",
|
|
1057
|
+
parsedCredentials.email,
|
|
1058
|
+
)
|
|
1059
|
+
: errorText);
|
|
960
1060
|
throw new AIError.GeminiCliApiError(
|
|
961
1061
|
`Cloud Code Assist API error (${response.status}): ${errorMessage}`,
|
|
962
1062
|
response.status,
|
|
@@ -993,8 +1093,10 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
993
1093
|
|
|
994
1094
|
if (!currentResponse.ok) {
|
|
995
1095
|
const retryErrorText = await currentResponse.text();
|
|
1096
|
+
const retryErrorMessage =
|
|
1097
|
+
formatCloudCodeAssistQuotaMessage(retryErrorText, parsedCredentials.email) ?? retryErrorText;
|
|
996
1098
|
throw new AIError.GeminiCliApiError(
|
|
997
|
-
`Cloud Code Assist API error (${currentResponse.status}): ${
|
|
1099
|
+
`Cloud Code Assist API error (${currentResponse.status}): ${retryErrorMessage}`,
|
|
998
1100
|
currentResponse.status,
|
|
999
1101
|
{ headers: currentResponse.headers },
|
|
1000
1102
|
);
|