diff --git a/packages/core/src/utils/googleQuotaErrors.test.ts b/packages/core/src/utils/googleQuotaErrors.test.ts index fdef9beead..97cc37433b 100644 --- a/packages/core/src/utils/googleQuotaErrors.test.ts +++ b/packages/core/src/utils/googleQuotaErrors.test.ts @@ -107,6 +107,63 @@ describe('classifyGoogleError', () => { expect((result as RetryableQuotaError).retryDelayMs).toBe(9000); }); + it('should return TerminalQuotaError for MODEL_CAPACITY_EXHAUSTED when no retry delay is specified', () => { + const apiError: GoogleApiError = { + code: 429, + message: + 'No capacity available for model gemini-3.1-pro-preview on the server', + details: [ + { + '@type': 'type.googleapis.com/google.rpc.ErrorInfo', + reason: 'MODEL_CAPACITY_EXHAUSTED', + domain: 'cloudcode-pa.googleapis.com', + metadata: { model: 'gemini-3.1-pro-preview' }, + }, + ], + }; + vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError); + const result = classifyGoogleError(new Error()); + expect(result).toBeInstanceOf(TerminalQuotaError); + }); + + it('should return TerminalQuotaError for MODEL_CAPACITY_EXHAUSTED even when the domain is not a Cloud Code domain (domain-agnostic)', () => { + const apiError: GoogleApiError = { + code: 429, + message: + 'No capacity available for model gemini-3.1-pro-preview on the server', + details: [ + { + '@type': 'type.googleapis.com/google.rpc.ErrorInfo', + reason: 'MODEL_CAPACITY_EXHAUSTED', + domain: 'other.googleapis.com', + metadata: { model: 'gemini-3.1-pro-preview' }, + }, + ], + }; + vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError); + const result = classifyGoogleError(new Error()); + expect(result).toBeInstanceOf(TerminalQuotaError); + }); + + it('should return TerminalQuotaError for MODEL_CAPACITY_EXCEEDED when no retry delay is specified', () => { + const apiError: GoogleApiError = { + code: 429, + message: + 'No capacity available for model gemini-3.1-pro-preview on the server', + details: [ + { + '@type': 'type.googleapis.com/google.rpc.ErrorInfo', + reason: 'MODEL_CAPACITY_EXCEEDED', + domain: 'cloudcode-pa.googleapis.com', + metadata: { model: 'gemini-3.1-pro-preview' }, + }, + ], + }; + vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError); + const result = classifyGoogleError(new Error()); + expect(result).toBeInstanceOf(TerminalQuotaError); + }); + it('should return original error if code is not 429, 499 or 503', () => { const apiError: GoogleApiError = { code: 500, diff --git a/packages/core/src/utils/googleQuotaErrors.ts b/packages/core/src/utils/googleQuotaErrors.ts index 1efadb7772..9d656964fe 100644 --- a/packages/core/src/utils/googleQuotaErrors.ts +++ b/packages/core/src/utils/googleQuotaErrors.ts @@ -330,6 +330,23 @@ export function classifyGoogleError(error: unknown): unknown { ); } + if ( + errorInfo.reason === 'MODEL_CAPACITY_EXHAUSTED' || + errorInfo.reason === 'MODEL_CAPACITY_EXCEEDED' + ) { + // If no server backoff delay is specified, treat capacity exhaustion as a terminal error + // to trigger immediate model fallback without retrying on the same exhausted model. + if (delaySeconds === undefined) { + return new TerminalQuotaError( + googleApiError.message, + googleApiError, + delaySeconds, + errorInfo.reason, + ); + } + // Otherwise, fall through to RetryableQuotaError to honor the server's requested delay. + } + // New Cloud Code API quota handling if (errorInfo.domain) { if (isCloudCodeDomain(errorInfo.domain)) {