mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-01 15:34:29 -07:00
fix: handle request retries and model fallback correctly (#11624)
This commit is contained in:
@@ -0,0 +1,305 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview
|
||||
* This file contains types and functions for parsing structured Google API errors.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Based on google/rpc/error_details.proto
|
||||
*/
|
||||
|
||||
export interface ErrorInfo {
|
||||
'@type': 'type.googleapis.com/google.rpc.ErrorInfo';
|
||||
reason: string;
|
||||
domain: string;
|
||||
metadata: { [key: string]: string };
|
||||
}
|
||||
|
||||
export interface RetryInfo {
|
||||
'@type': 'type.googleapis.com/google.rpc.RetryInfo';
|
||||
retryDelay: string; // e.g. "51820.638305887s"
|
||||
}
|
||||
|
||||
export interface DebugInfo {
|
||||
'@type': 'type.googleapis.com/google.rpc.DebugInfo';
|
||||
stackEntries: string[];
|
||||
detail: string;
|
||||
}
|
||||
|
||||
export interface QuotaFailure {
|
||||
'@type': 'type.googleapis.com/google.rpc.QuotaFailure';
|
||||
violations: Array<{
|
||||
subject?: string;
|
||||
description?: string;
|
||||
apiService?: string;
|
||||
quotaMetric?: string;
|
||||
quotaId?: string;
|
||||
quotaDimensions?: { [key: string]: string };
|
||||
quotaValue?: string | number;
|
||||
futureQuotaValue?: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface PreconditionFailure {
|
||||
'@type': 'type.googleapis.com/google.rpc.PreconditionFailure';
|
||||
violations: Array<{
|
||||
type: string;
|
||||
subject: string;
|
||||
description: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface LocalizedMessage {
|
||||
'@type': 'type.googleapis.com/google.rpc.LocalizedMessage';
|
||||
locale: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface BadRequest {
|
||||
'@type': 'type.googleapis.com/google.rpc.BadRequest';
|
||||
fieldViolations: Array<{
|
||||
field: string;
|
||||
description: string;
|
||||
reason?: string;
|
||||
localizedMessage?: LocalizedMessage;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface RequestInfo {
|
||||
'@type': 'type.googleapis.com/google.rpc.RequestInfo';
|
||||
requestId: string;
|
||||
servingData: string;
|
||||
}
|
||||
|
||||
export interface ResourceInfo {
|
||||
'@type': 'type.googleapis.com/google.rpc.ResourceInfo';
|
||||
resourceType: string;
|
||||
resourceName: string;
|
||||
owner: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface Help {
|
||||
'@type': 'type.googleapis.com/google.rpc.Help';
|
||||
links: Array<{
|
||||
description: string;
|
||||
url: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export type GoogleApiErrorDetail =
|
||||
| ErrorInfo
|
||||
| RetryInfo
|
||||
| DebugInfo
|
||||
| QuotaFailure
|
||||
| PreconditionFailure
|
||||
| BadRequest
|
||||
| RequestInfo
|
||||
| ResourceInfo
|
||||
| Help
|
||||
| LocalizedMessage;
|
||||
|
||||
export interface GoogleApiError {
|
||||
code: number;
|
||||
message: string;
|
||||
details: GoogleApiErrorDetail[];
|
||||
}
|
||||
|
||||
type ErrorShape = {
|
||||
message?: string;
|
||||
details?: unknown[];
|
||||
code?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses an error object to check if it's a structured Google API error
|
||||
* and extracts all details.
|
||||
*
|
||||
* This function can handle two formats:
|
||||
* 1. Standard Google API errors where `details` is a top-level field.
|
||||
* 2. Errors where the entire structured error object is stringified inside
|
||||
* the `message` field of a wrapper error.
|
||||
*
|
||||
* @param error The error object to inspect.
|
||||
* @returns A GoogleApiError object if the error matches, otherwise null.
|
||||
*/
|
||||
export function parseGoogleApiError(error: unknown): GoogleApiError | null {
|
||||
if (!error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let errorObj: unknown = error;
|
||||
|
||||
// If error is a string, try to parse it.
|
||||
if (typeof errorObj === 'string') {
|
||||
try {
|
||||
errorObj = JSON.parse(errorObj);
|
||||
} catch (_) {
|
||||
// Not a JSON string, can't parse.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(errorObj) && errorObj.length > 0) {
|
||||
errorObj = errorObj[0];
|
||||
}
|
||||
|
||||
if (typeof errorObj !== 'object' || errorObj === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let currentError: ErrorShape | undefined =
|
||||
fromGaxiosError(errorObj) ?? fromApiError(errorObj);
|
||||
|
||||
let depth = 0;
|
||||
const maxDepth = 10;
|
||||
// Handle cases where the actual error object is stringified inside the message
|
||||
// by drilling down until we find an error that doesn't have a stringified message.
|
||||
while (
|
||||
currentError &&
|
||||
typeof currentError.message === 'string' &&
|
||||
depth < maxDepth
|
||||
) {
|
||||
try {
|
||||
const parsedMessage = JSON.parse(
|
||||
currentError.message.replace(/\u00A0/g, '').replace(/\n/g, ' '),
|
||||
);
|
||||
if (parsedMessage.error) {
|
||||
currentError = parsedMessage.error;
|
||||
depth++;
|
||||
} else {
|
||||
// The message is a JSON string, but not a nested error object.
|
||||
break;
|
||||
}
|
||||
} catch (_error) {
|
||||
// It wasn't a JSON string, so we've drilled down as far as we can.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const code = currentError.code;
|
||||
const message = currentError.message;
|
||||
const errorDetails = currentError.details;
|
||||
|
||||
if (Array.isArray(errorDetails) && code && message) {
|
||||
const details: GoogleApiErrorDetail[] = [];
|
||||
for (const detail of errorDetails) {
|
||||
if (detail && typeof detail === 'object') {
|
||||
const detailObj = detail as Record<string, unknown>;
|
||||
const typeKey = Object.keys(detailObj).find(
|
||||
(key) => key.trim() === '@type',
|
||||
);
|
||||
if (typeKey) {
|
||||
if (typeKey !== '@type') {
|
||||
detailObj['@type'] = detailObj[typeKey];
|
||||
delete detailObj[typeKey];
|
||||
}
|
||||
// We can just cast it; the consumer will have to switch on @type
|
||||
details.push(detailObj as unknown as GoogleApiErrorDetail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (details.length > 0) {
|
||||
return {
|
||||
code,
|
||||
message,
|
||||
details,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function fromGaxiosError(errorObj: object): ErrorShape | undefined {
|
||||
const gaxiosError = errorObj as {
|
||||
response?: {
|
||||
status?: number;
|
||||
data?:
|
||||
| {
|
||||
error?: ErrorShape;
|
||||
}
|
||||
| string;
|
||||
};
|
||||
error?: ErrorShape;
|
||||
code?: number;
|
||||
};
|
||||
|
||||
let outerError: ErrorShape | undefined;
|
||||
if (gaxiosError.response?.data) {
|
||||
let data = gaxiosError.response.data;
|
||||
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (_) {
|
||||
// Not a JSON string, can't parse.
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
data = data[0];
|
||||
}
|
||||
|
||||
if (typeof data === 'object' && data !== null) {
|
||||
if ('error' in data) {
|
||||
outerError = (data as { error: ErrorShape }).error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!outerError) {
|
||||
// If the gaxios structure isn't there, check for a top-level `error` property.
|
||||
if (gaxiosError.error) {
|
||||
outerError = gaxiosError.error;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return outerError;
|
||||
}
|
||||
|
||||
function fromApiError(errorObj: object): ErrorShape | undefined {
|
||||
const apiError = errorObj as {
|
||||
message?:
|
||||
| {
|
||||
error?: ErrorShape;
|
||||
}
|
||||
| string;
|
||||
code?: number;
|
||||
};
|
||||
|
||||
let outerError: ErrorShape | undefined;
|
||||
if (apiError.message) {
|
||||
let data = apiError.message;
|
||||
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (_) {
|
||||
// Not a JSON string, can't parse.
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
data = data[0];
|
||||
}
|
||||
|
||||
if (typeof data === 'object' && data !== null) {
|
||||
if ('error' in data) {
|
||||
outerError = (data as { error: ErrorShape }).error;
|
||||
}
|
||||
}
|
||||
}
|
||||
return outerError;
|
||||
}
|
||||
Reference in New Issue
Block a user