jeopi-ai 16.6.3 → 16.6.6

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 CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.6.6] - 2026-10-17
6
+
7
+ ### Fixed
8
+
9
+ - Fixed Google Cloud Code Assist quota failures retrying exhausted credentials or alternate endpoints before existing authentication recovery. Structured quota errors now retain their status across HTTP, SSE, and empty-stream retries, while transient throttling keeps normal retry behavior.
10
+ - Bounded Google error-body inspection so stalled error responses cannot prevent endpoint recovery, and prevented canceled shared work from leaking unhandled rejections.
11
+
5
12
  ## [16.6.2] - 2026-10-03
6
13
 
7
14
  ### Fixed
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "jeopi-ai",
4
- "version": "16.6.3",
4
+ "version": "16.6.6",
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.6.3",
42
- "jeopi-utils": "16.6.3",
43
- "jeopi-wire": "16.6.3",
41
+ "jeopi-catalog": "16.6.6",
42
+ "jeopi-utils": "16.6.6",
43
+ "jeopi-wire": "16.6.6",
44
44
  "arktype": "2.2.3",
45
45
  "zod": "^4"
46
46
  },
@@ -15,6 +15,7 @@ import {
15
15
  } from "jeopi-catalog/wire/gemini-headers";
16
16
  import { extractHttpStatusFromError, fetchWithRetry, readSseJson } from "jeopi-utils";
17
17
  import * as AIError from "../error";
18
+ import { parseRateLimitReason } from "../error/rate-limit";
18
19
  import type {
19
20
  Api,
20
21
  AssistantMessage,
@@ -28,6 +29,7 @@ import type {
28
29
  ToolCall,
29
30
  } from "../types";
30
31
  import { normalizeSystemPrompts } from "../utils";
32
+ import { raceWithSignal } from "../utils/abort";
31
33
  import { AssistantMessageEventStream } from "../utils/event-stream";
32
34
  import { extractGoogleValidationUrl, formatGoogleValidationRequiredMessage } from "../utils/google-validation";
33
35
  import type { RawHttpRequestDump } from "../utils/http-inspector";
@@ -440,51 +442,50 @@ function stringField(record: Record<string, unknown> | undefined, key: string):
440
442
  return typeof value === "string" && value.length > 0 ? value : undefined;
441
443
  }
442
444
 
443
- function extractCloudCodeAssistQuotaDetails(errorBody: string): CloudCodeAssistQuotaDetails | undefined {
444
- if (!/\bquota\b|QUOTA_EXHAUSTED|RESOURCE_EXHAUSTED/i.test(errorBody)) return undefined;
445
+ function parseCloudCodeAssistError(errorBody: string): Record<string, unknown> | undefined {
445
446
  const jsonStart = errorBody.indexOf("{");
446
447
  if (jsonStart === -1) return undefined;
447
-
448
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 };
449
+ return recordOf(recordOf(JSON.parse(errorBody.slice(jsonStart).replace(/\r?\n/g, " ")))?.error);
483
450
  } catch {
484
451
  return undefined;
485
452
  }
486
453
  }
487
454
 
455
+ function extractCloudCodeAssistQuotaDetails(errorBody: string): CloudCodeAssistQuotaDetails | undefined {
456
+ if (!/\bquota\b|QUOTA_EXHAUSTED|RESOURCE_EXHAUSTED/i.test(errorBody)) return undefined;
457
+ const error = parseCloudCodeAssistError(errorBody);
458
+ if (!error) return undefined;
459
+
460
+ const message = stringField(error, "message");
461
+ const status = stringField(error, "status");
462
+ const details = Array.isArray(error.details) ? error.details : [];
463
+ let isQuotaExhausted =
464
+ status === "RESOURCE_EXHAUSTED" && message !== undefined && parseRateLimitReason(message) === "QUOTA_EXHAUSTED";
465
+ let model: string | undefined;
466
+ let quotaResetDelay: string | undefined;
467
+ let quotaResetTimeStamp: string | undefined;
468
+ let retryDelay: string | undefined;
469
+
470
+ for (const rawDetail of details) {
471
+ const detail = recordOf(rawDetail);
472
+ if (!detail) continue;
473
+ const reason = stringField(detail, "reason");
474
+ if (reason === "QUOTA_EXHAUSTED") {
475
+ isQuotaExhausted = true;
476
+ }
477
+ retryDelay ??= stringField(detail, "retryDelay");
478
+ const metadata = recordOf(detail.metadata);
479
+ model ??= stringField(metadata, "model");
480
+ quotaResetDelay ??= stringField(metadata, "quotaResetDelay");
481
+ quotaResetTimeStamp ??=
482
+ stringField(metadata, "quotaResetTimeStamp") ?? stringField(metadata, "quotaResetTimestamp");
483
+ }
484
+
485
+ if (!isQuotaExhausted) return undefined;
486
+ return { message, model, quotaResetDelay, quotaResetTimeStamp, retryDelay };
487
+ }
488
+
488
489
  function sentence(text: string): string {
489
490
  return /[.!?]$/.test(text) ? text : `${text}.`;
490
491
  }
@@ -517,6 +518,39 @@ function formatCloudCodeAssistQuotaMessage(errorBody: string, email?: string): s
517
518
  return parts.join(" ");
518
519
  }
519
520
 
521
+ async function createCloudCodeAssistHttpError(
522
+ response: Response,
523
+ email?: string,
524
+ signal?: AbortSignal,
525
+ ): Promise<AIError.GeminiCliApiError> {
526
+ let errorText: string;
527
+ try {
528
+ signal?.throwIfAborted();
529
+ errorText = await raceWithSignal(response.text(), signal);
530
+ } catch (error) {
531
+ return new AIError.GeminiCliApiError(
532
+ `Cloud Code Assist API error (${response.status}): Could not read response body`,
533
+ response.status,
534
+ { headers: response.headers, cause: error },
535
+ );
536
+ }
537
+ const quotaMessage = formatCloudCodeAssistQuotaMessage(errorText, email);
538
+ const validationUrl = quotaMessage ? undefined : extractGoogleValidationUrl(errorText);
539
+ const detail =
540
+ quotaMessage ??
541
+ (validationUrl
542
+ ? formatGoogleValidationRequiredMessage(validationUrl, "retry your request", email)
543
+ : (stringField(parseCloudCodeAssistError(errorText), "message") ?? errorText));
544
+ return new AIError.GeminiCliApiError(
545
+ `Cloud Code Assist API error (${response.status}): ${detail}`,
546
+ response.status,
547
+ {
548
+ headers: response.headers,
549
+ code: quotaMessage ? "QUOTA_EXHAUSTED" : undefined,
550
+ },
551
+ );
552
+ }
553
+
520
554
  interface CloudCodeAssistRequest {
521
555
  project: string;
522
556
  model: string;
@@ -873,7 +907,9 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
873
907
  const detail = quotaDetail ?? chunk.error.message ?? chunk.error.status ?? "unknown error";
874
908
  const message = `Cloud Code Assist stream error: ${detail}`;
875
909
  throw typeof chunk.error.code === "number" && chunk.error.code >= 400
876
- ? new AIError.GeminiCliApiError(message, chunk.error.code)
910
+ ? new AIError.GeminiCliApiError(message, chunk.error.code, {
911
+ code: quotaDetail ? "QUOTA_EXHAUSTED" : undefined,
912
+ })
877
913
  : new AIError.ProviderResponseError(message, { provider: model.provider, kind: "runtime" });
878
914
  }
879
915
  const responseData = chunk.response;
@@ -1032,9 +1068,8 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
1032
1068
  started = false;
1033
1069
  resetOutput();
1034
1070
 
1035
- // Per attempt: arm a pre-response (TTFT) timer, cleared the instant
1036
- // headers arrive so it never aborts the actively streaming body —
1037
- // an absolute `AbortSignal.timeout` would (issue #2422).
1071
+ // Guard headers and error-body inspection; clear before reading a
1072
+ // successful SSE body so ongoing output is not cut off (issue #2422).
1038
1073
  const watchdog = armPreResponseTimeout(callerSignal, firstEventTimeoutMs);
1039
1074
  let response: Response;
1040
1075
  try {
@@ -1046,38 +1081,18 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
1046
1081
  maxAttempts: isLastEndpoint ? MAX_RETRIES + 1 : 1,
1047
1082
  defaultDelayMs: attempt => BASE_DELAY_MS * 2 ** attempt,
1048
1083
  maxDelayMs: options?.maxRetryDelayMs ?? RATE_LIMIT_BUDGET_MS,
1084
+ shouldRetryResponse: (response, body) =>
1085
+ response.status !== 429 || extractCloudCodeAssistQuotaDetails(body) === undefined,
1049
1086
  fetch: options?.fetch,
1050
1087
  timeout: false,
1051
1088
  });
1089
+ if (!response.ok) {
1090
+ throw await createCloudCodeAssistHttpError(response, parsedCredentials.email, watchdog.signal);
1091
+ }
1052
1092
  } finally {
1053
1093
  watchdog.clear();
1054
1094
  }
1055
1095
 
1056
- if (!response.ok) {
1057
- if (AIError.isTransientStatus(response.status)) {
1058
- if (!isLastEndpoint) {
1059
- continue;
1060
- }
1061
- }
1062
- const errorText = await response.text();
1063
- const quotaMessage = formatCloudCodeAssistQuotaMessage(errorText, parsedCredentials.email);
1064
- const validationUrl = quotaMessage ? undefined : extractGoogleValidationUrl(errorText);
1065
- const errorMessage =
1066
- quotaMessage ??
1067
- (validationUrl
1068
- ? formatGoogleValidationRequiredMessage(
1069
- validationUrl,
1070
- "retry your request",
1071
- parsedCredentials.email,
1072
- )
1073
- : errorText);
1074
- throw new AIError.GeminiCliApiError(
1075
- `Cloud Code Assist API error (${response.status}): ${errorMessage}`,
1076
- response.status,
1077
- { headers: response.headers },
1078
- );
1079
- }
1080
-
1081
1096
  const requestUrl = response.url;
1082
1097
  let currentResponse = response;
1083
1098
 
@@ -1098,22 +1113,24 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
1098
1113
  throw new AIError.ConfigurationError("Missing request URL");
1099
1114
  }
1100
1115
 
1101
- currentResponse = await (options?.fetch ?? fetch)(requestUrl, {
1102
- method: "POST",
1103
- headers: requestHeaders,
1104
- body: requestBodyJson,
1105
- signal: options?.signal,
1106
- });
1107
-
1108
- if (!currentResponse.ok) {
1109
- const retryErrorText = await currentResponse.text();
1110
- const retryErrorMessage =
1111
- formatCloudCodeAssistQuotaMessage(retryErrorText, parsedCredentials.email) ?? retryErrorText;
1112
- throw new AIError.GeminiCliApiError(
1113
- `Cloud Code Assist API error (${currentResponse.status}): ${retryErrorMessage}`,
1114
- currentResponse.status,
1115
- { headers: currentResponse.headers },
1116
- );
1116
+ const retryWatchdog = armPreResponseTimeout(callerSignal, firstEventTimeoutMs);
1117
+ try {
1118
+ currentResponse = await (options?.fetch ?? fetch)(requestUrl, {
1119
+ method: "POST",
1120
+ headers: requestHeaders,
1121
+ body: requestBodyJson,
1122
+ signal: retryWatchdog.signal,
1123
+ });
1124
+
1125
+ if (!currentResponse.ok) {
1126
+ throw await createCloudCodeAssistHttpError(
1127
+ currentResponse,
1128
+ parsedCredentials.email,
1129
+ retryWatchdog.signal,
1130
+ );
1131
+ }
1132
+ } finally {
1133
+ retryWatchdog.clear();
1117
1134
  }
1118
1135
  }
1119
1136
 
@@ -1162,7 +1179,11 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
1162
1179
  break;
1163
1180
  } catch (error) {
1164
1181
  const status = extractHttpStatusFromError(error);
1165
- if (AIError.isTransientStatus(status)) {
1182
+ if (
1183
+ AIError.isTransientStatus(status) &&
1184
+ !callerSignal?.aborted &&
1185
+ !(error instanceof AIError.GeminiCliApiError && error.code === "QUOTA_EXHAUSTED")
1186
+ ) {
1166
1187
  if (!isLastEndpoint && !started) {
1167
1188
  continue;
1168
1189
  }
@@ -59,7 +59,11 @@ export function createAbortSourceTracker(callerSignal?: AbortSignal): AbortSourc
59
59
  */
60
60
  export function raceWithSignal<T>(promise: Promise<T>, signal: AbortSignal | undefined): Promise<T> {
61
61
  if (!signal) return promise;
62
- if (signal.aborted) return Promise.reject(signal.reason ?? new AIError.AbortError());
62
+ if (signal.aborted) {
63
+ // The supplied work already started; observe its rejection after the caller cancels.
64
+ void promise.catch(() => {});
65
+ return Promise.reject(signal.reason ?? new AIError.AbortError());
66
+ }
63
67
  const { promise: aborted, reject } = Promise.withResolvers<never>();
64
68
  const onAbort = () => reject(signal.reason ?? new AIError.AbortError());
65
69
  signal.addEventListener("abort", onAbort, { once: true });