mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-16 09:01:17 -07:00
193 lines
5.8 KiB
TypeScript
193 lines
5.8 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import type {
|
|
ErrorInfo,
|
|
GoogleApiError,
|
|
QuotaFailure,
|
|
RetryInfo,
|
|
} from './googleErrors.js';
|
|
import { parseGoogleApiError } from './googleErrors.js';
|
|
|
|
/**
|
|
* A non-retryable error indicating a hard quota limit has been reached (e.g., daily limit).
|
|
*/
|
|
export class TerminalQuotaError extends Error {
|
|
constructor(
|
|
message: string,
|
|
override readonly cause: GoogleApiError,
|
|
) {
|
|
super(message);
|
|
this.name = 'TerminalQuotaError';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A retryable error indicating a temporary quota issue (e.g., per-minute limit).
|
|
*/
|
|
export class RetryableQuotaError extends Error {
|
|
retryDelayMs: number;
|
|
|
|
constructor(
|
|
message: string,
|
|
override readonly cause: GoogleApiError,
|
|
retryDelaySeconds: number,
|
|
) {
|
|
super(message);
|
|
this.name = 'RetryableQuotaError';
|
|
this.retryDelayMs = retryDelaySeconds * 1000;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses a duration string (e.g., "34.074824224s", "60s") and returns the time in seconds.
|
|
* @param duration The duration string to parse.
|
|
* @returns The duration in seconds, or null if parsing fails.
|
|
*/
|
|
function parseDurationInSeconds(duration: string): number | null {
|
|
if (!duration.endsWith('s')) {
|
|
return null;
|
|
}
|
|
const seconds = parseFloat(duration.slice(0, -1));
|
|
return isNaN(seconds) ? null : seconds;
|
|
}
|
|
|
|
/**
|
|
* Analyzes a caught error and classifies it as a specific quota-related error if applicable.
|
|
*
|
|
* It decides whether an error is a `TerminalQuotaError` or a `RetryableQuotaError` based on
|
|
* the following logic:
|
|
* - If the error indicates a daily limit, it's a `TerminalQuotaError`.
|
|
* - If the error suggests a retry delay of more than 2 minutes, it's a `TerminalQuotaError`.
|
|
* - If the error suggests a retry delay of 2 minutes or less, it's a `RetryableQuotaError`.
|
|
* - If the error indicates a per-minute limit, it's a `RetryableQuotaError`.
|
|
*
|
|
* @param error The error to classify.
|
|
* @returns A `TerminalQuotaError`, `RetryableQuotaError`, or the original `unknown` error.
|
|
*/
|
|
export function classifyGoogleError(error: unknown): unknown {
|
|
const googleApiError = parseGoogleApiError(error);
|
|
|
|
if (!googleApiError || googleApiError.code !== 429) {
|
|
return error; // Not a 429 error we can handle.
|
|
}
|
|
|
|
const quotaFailure = googleApiError.details.find(
|
|
(d): d is QuotaFailure =>
|
|
d['@type'] === 'type.googleapis.com/google.rpc.QuotaFailure',
|
|
);
|
|
|
|
const errorInfo = googleApiError.details.find(
|
|
(d): d is ErrorInfo =>
|
|
d['@type'] === 'type.googleapis.com/google.rpc.ErrorInfo',
|
|
);
|
|
|
|
const retryInfo = googleApiError.details.find(
|
|
(d): d is RetryInfo =>
|
|
d['@type'] === 'type.googleapis.com/google.rpc.RetryInfo',
|
|
);
|
|
|
|
// 1. Check for long-term limits in QuotaFailure or ErrorInfo
|
|
if (quotaFailure) {
|
|
for (const violation of quotaFailure.violations) {
|
|
const quotaId = violation.quotaId ?? '';
|
|
if (quotaId.includes('PerDay') || quotaId.includes('Daily')) {
|
|
return new TerminalQuotaError(
|
|
`You have exhausted your daily quota on this model.`,
|
|
googleApiError,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (errorInfo) {
|
|
// New Cloud Code API quota handling
|
|
if (errorInfo.domain) {
|
|
const validDomains = [
|
|
'cloudcode-pa.googleapis.com',
|
|
'staging-cloudcode-pa.googleapis.com',
|
|
'autopush-cloudcode-pa.googleapis.com',
|
|
];
|
|
if (validDomains.includes(errorInfo.domain)) {
|
|
if (errorInfo.reason === 'RATE_LIMIT_EXCEEDED') {
|
|
let delaySeconds = 10; // Default retry of 10s
|
|
if (retryInfo?.retryDelay) {
|
|
const parsedDelay = parseDurationInSeconds(retryInfo.retryDelay);
|
|
if (parsedDelay) {
|
|
delaySeconds = parsedDelay;
|
|
}
|
|
}
|
|
return new RetryableQuotaError(
|
|
`${googleApiError.message}`,
|
|
googleApiError,
|
|
delaySeconds,
|
|
);
|
|
}
|
|
if (errorInfo.reason === 'QUOTA_EXHAUSTED') {
|
|
return new TerminalQuotaError(
|
|
`${googleApiError.message}`,
|
|
googleApiError,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Existing Cloud Code API quota handling
|
|
const quotaLimit = errorInfo.metadata?.['quota_limit'] ?? '';
|
|
if (quotaLimit.includes('PerDay') || quotaLimit.includes('Daily')) {
|
|
return new TerminalQuotaError(
|
|
`You have exhausted your daily quota on this model.`,
|
|
googleApiError,
|
|
);
|
|
}
|
|
}
|
|
|
|
// 2. Check for long delays in RetryInfo
|
|
if (retryInfo?.retryDelay) {
|
|
const delaySeconds = parseDurationInSeconds(retryInfo.retryDelay);
|
|
if (delaySeconds) {
|
|
if (delaySeconds > 120) {
|
|
return new TerminalQuotaError(
|
|
`${googleApiError.message}\nSuggested retry after ${retryInfo.retryDelay}.`,
|
|
googleApiError,
|
|
);
|
|
}
|
|
// This is a retryable error with a specific delay.
|
|
return new RetryableQuotaError(
|
|
`${googleApiError.message}\nSuggested retry after ${retryInfo.retryDelay}.`,
|
|
googleApiError,
|
|
delaySeconds,
|
|
);
|
|
}
|
|
}
|
|
|
|
// 3. Check for short-term limits in QuotaFailure or ErrorInfo
|
|
if (quotaFailure) {
|
|
for (const violation of quotaFailure.violations) {
|
|
const quotaId = violation.quotaId ?? '';
|
|
if (quotaId.includes('PerMinute')) {
|
|
return new RetryableQuotaError(
|
|
`${googleApiError.message}\nSuggested retry after 60s.`,
|
|
googleApiError,
|
|
60,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (errorInfo) {
|
|
const quotaLimit = errorInfo.metadata?.['quota_limit'] ?? '';
|
|
if (quotaLimit.includes('PerMinute')) {
|
|
return new RetryableQuotaError(
|
|
`${errorInfo.reason}\nSuggested retry after 60s.`,
|
|
googleApiError,
|
|
60,
|
|
);
|
|
}
|
|
}
|
|
return error; // Fallback to original error if no specific classification fits.
|
|
}
|