mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 01:07:46 +00:00
Merge tag 'v0.1.21' of github.com:google-gemini/gemini-cli into chore/sync-gemini-cli-v0.1.21
This commit is contained in:
375
packages/core/src/utils/errorParsing.test.ts
Normal file
375
packages/core/src/utils/errorParsing.test.ts
Normal file
@@ -0,0 +1,375 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parseAndFormatApiError } from './errorParsing.js';
|
||||
import { isProQuotaExceededError } from './quotaErrorDetection.js';
|
||||
import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js';
|
||||
import { UserTierId } from '../code_assist/types.js';
|
||||
import { AuthType } from '../core/contentGenerator.js';
|
||||
import { StructuredError } from '../core/turn.js';
|
||||
|
||||
describe('parseAndFormatApiError', () => {
|
||||
const vertexMessage = 'request a quota increase through Vertex';
|
||||
const geminiMessage = 'request a quota increase through AI Studio';
|
||||
|
||||
it('should format a valid API error JSON', () => {
|
||||
const errorMessage =
|
||||
'got status: 400 Bad Request. {"error":{"code":400,"message":"API key not valid. Please pass a valid API key.","status":"INVALID_ARGUMENT"}}';
|
||||
const expected =
|
||||
'[API Error: API key not valid. Please pass a valid API key. (Status: INVALID_ARGUMENT)]';
|
||||
expect(parseAndFormatApiError(errorMessage)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should format a 429 API error with the default message', () => {
|
||||
const errorMessage =
|
||||
'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Rate limit exceeded","status":"RESOURCE_EXHAUSTED"}}';
|
||||
const result = parseAndFormatApiError(
|
||||
errorMessage,
|
||||
undefined,
|
||||
undefined,
|
||||
'gemini-2.5-pro',
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
);
|
||||
expect(result).toContain('[API Error: Rate limit exceeded');
|
||||
expect(result).toContain(
|
||||
'Possible quota limitations in place or slow response times detected. Switching to the gemini-2.5-flash model',
|
||||
);
|
||||
});
|
||||
|
||||
it('should format a 429 API error with the personal message', () => {
|
||||
const errorMessage =
|
||||
'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Rate limit exceeded","status":"RESOURCE_EXHAUSTED"}}';
|
||||
const result = parseAndFormatApiError(
|
||||
errorMessage,
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
undefined,
|
||||
'gemini-2.5-pro',
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
);
|
||||
expect(result).toContain('[API Error: Rate limit exceeded');
|
||||
expect(result).toContain(
|
||||
'Possible quota limitations in place or slow response times detected. Switching to the gemini-2.5-flash model',
|
||||
);
|
||||
});
|
||||
|
||||
it('should format a 429 API error with the vertex message', () => {
|
||||
const errorMessage =
|
||||
'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Rate limit exceeded","status":"RESOURCE_EXHAUSTED"}}';
|
||||
const result = parseAndFormatApiError(errorMessage, AuthType.USE_VERTEX_AI);
|
||||
expect(result).toContain('[API Error: Rate limit exceeded');
|
||||
expect(result).toContain(vertexMessage);
|
||||
});
|
||||
|
||||
it('should return the original message if it is not a JSON error', () => {
|
||||
const errorMessage = 'This is a plain old error message';
|
||||
expect(parseAndFormatApiError(errorMessage)).toBe(
|
||||
`[API Error: ${errorMessage}]`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return the original message for malformed JSON', () => {
|
||||
const errorMessage = '[Stream Error: {"error": "malformed}';
|
||||
expect(parseAndFormatApiError(errorMessage)).toBe(
|
||||
`[API Error: ${errorMessage}]`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle JSON that does not match the ApiError structure', () => {
|
||||
const errorMessage = '[Stream Error: {"not_an_error": "some other json"}]';
|
||||
expect(parseAndFormatApiError(errorMessage)).toBe(
|
||||
`[API Error: ${errorMessage}]`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should format a nested API error', () => {
|
||||
const nestedErrorMessage = JSON.stringify({
|
||||
error: {
|
||||
code: 429,
|
||||
message:
|
||||
"Gemini 2.5 Pro Preview doesn't have a free quota tier. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits.",
|
||||
status: 'RESOURCE_EXHAUSTED',
|
||||
},
|
||||
});
|
||||
|
||||
const errorMessage = JSON.stringify({
|
||||
error: {
|
||||
code: 429,
|
||||
message: nestedErrorMessage,
|
||||
status: 'Too Many Requests',
|
||||
},
|
||||
});
|
||||
|
||||
const result = parseAndFormatApiError(errorMessage, AuthType.USE_GEMINI);
|
||||
expect(result).toContain('Gemini 2.5 Pro Preview');
|
||||
expect(result).toContain(geminiMessage);
|
||||
});
|
||||
|
||||
it('should format a StructuredError', () => {
|
||||
const error: StructuredError = {
|
||||
message: 'A structured error occurred',
|
||||
status: 500,
|
||||
};
|
||||
const expected = '[API Error: A structured error occurred]';
|
||||
expect(parseAndFormatApiError(error)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should format a 429 StructuredError with the vertex message', () => {
|
||||
const error: StructuredError = {
|
||||
message: 'Rate limit exceeded',
|
||||
status: 429,
|
||||
};
|
||||
const result = parseAndFormatApiError(error, AuthType.USE_VERTEX_AI);
|
||||
expect(result).toContain('[API Error: Rate limit exceeded]');
|
||||
expect(result).toContain(vertexMessage);
|
||||
});
|
||||
|
||||
it('should handle an unknown error type', () => {
|
||||
const error = 12345;
|
||||
const expected = '[API Error: An unknown error occurred.]';
|
||||
expect(parseAndFormatApiError(error)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should format a 429 API error with Pro quota exceeded message for Google auth (Free tier)', () => {
|
||||
const errorMessage =
|
||||
'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini 2.5 Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}';
|
||||
const result = parseAndFormatApiError(
|
||||
errorMessage,
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
undefined,
|
||||
'gemini-2.5-pro',
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
);
|
||||
expect(result).toContain(
|
||||
"[API Error: Quota exceeded for quota metric 'Gemini 2.5 Pro Requests'",
|
||||
);
|
||||
expect(result).toContain(
|
||||
'You have reached your daily gemini-2.5-pro quota limit',
|
||||
);
|
||||
expect(result).toContain(
|
||||
'upgrade to a Gemini Code Assist Standard or Enterprise plan',
|
||||
);
|
||||
});
|
||||
|
||||
it('should format a regular 429 API error with standard message for Google auth', () => {
|
||||
const errorMessage =
|
||||
'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Rate limit exceeded","status":"RESOURCE_EXHAUSTED"}}';
|
||||
const result = parseAndFormatApiError(
|
||||
errorMessage,
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
undefined,
|
||||
'gemini-2.5-pro',
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
);
|
||||
expect(result).toContain('[API Error: Rate limit exceeded');
|
||||
expect(result).toContain(
|
||||
'Possible quota limitations in place or slow response times detected. Switching to the gemini-2.5-flash model',
|
||||
);
|
||||
expect(result).not.toContain(
|
||||
'You have reached your daily gemini-2.5-pro quota limit',
|
||||
);
|
||||
});
|
||||
|
||||
it('should format a 429 API error with generic quota exceeded message for Google auth', () => {
|
||||
const errorMessage =
|
||||
'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'GenerationRequests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}';
|
||||
const result = parseAndFormatApiError(
|
||||
errorMessage,
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
undefined,
|
||||
'gemini-2.5-pro',
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
);
|
||||
expect(result).toContain(
|
||||
"[API Error: Quota exceeded for quota metric 'GenerationRequests'",
|
||||
);
|
||||
expect(result).toContain('You have reached your daily quota limit');
|
||||
expect(result).not.toContain(
|
||||
'You have reached your daily Gemini 2.5 Pro quota limit',
|
||||
);
|
||||
});
|
||||
|
||||
it('should prioritize Pro quota message over generic quota message for Google auth', () => {
|
||||
const errorMessage =
|
||||
'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini 2.5 Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}';
|
||||
const result = parseAndFormatApiError(
|
||||
errorMessage,
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
undefined,
|
||||
'gemini-2.5-pro',
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
);
|
||||
expect(result).toContain(
|
||||
"[API Error: Quota exceeded for quota metric 'Gemini 2.5 Pro Requests'",
|
||||
);
|
||||
expect(result).toContain(
|
||||
'You have reached your daily gemini-2.5-pro quota limit',
|
||||
);
|
||||
expect(result).not.toContain('You have reached your daily quota limit');
|
||||
});
|
||||
|
||||
it('should format a 429 API error with Pro quota exceeded message for Google auth (Standard tier)', () => {
|
||||
const errorMessage =
|
||||
'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini 2.5 Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}';
|
||||
const result = parseAndFormatApiError(
|
||||
errorMessage,
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
UserTierId.STANDARD,
|
||||
'gemini-2.5-pro',
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
);
|
||||
expect(result).toContain(
|
||||
"[API Error: Quota exceeded for quota metric 'Gemini 2.5 Pro Requests'",
|
||||
);
|
||||
expect(result).toContain(
|
||||
'You have reached your daily gemini-2.5-pro quota limit',
|
||||
);
|
||||
expect(result).toContain(
|
||||
'We appreciate you for choosing Gemini Code Assist and the Gemini CLI',
|
||||
);
|
||||
expect(result).not.toContain(
|
||||
'upgrade to a Gemini Code Assist Standard or Enterprise plan',
|
||||
);
|
||||
});
|
||||
|
||||
it('should format a 429 API error with Pro quota exceeded message for Google auth (Legacy tier)', () => {
|
||||
const errorMessage =
|
||||
'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini 2.5 Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}';
|
||||
const result = parseAndFormatApiError(
|
||||
errorMessage,
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
UserTierId.LEGACY,
|
||||
'gemini-2.5-pro',
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
);
|
||||
expect(result).toContain(
|
||||
"[API Error: Quota exceeded for quota metric 'Gemini 2.5 Pro Requests'",
|
||||
);
|
||||
expect(result).toContain(
|
||||
'You have reached your daily gemini-2.5-pro quota limit',
|
||||
);
|
||||
expect(result).toContain(
|
||||
'We appreciate you for choosing Gemini Code Assist and the Gemini CLI',
|
||||
);
|
||||
expect(result).not.toContain(
|
||||
'upgrade to a Gemini Code Assist Standard or Enterprise plan',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle different Gemini 2.5 version strings in Pro quota exceeded errors', () => {
|
||||
const errorMessage25 =
|
||||
'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini 2.5 Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}';
|
||||
const errorMessagePreview =
|
||||
'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'Gemini 2.5-preview Pro Requests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}';
|
||||
|
||||
const result25 = parseAndFormatApiError(
|
||||
errorMessage25,
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
undefined,
|
||||
'gemini-2.5-pro',
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
);
|
||||
const resultPreview = parseAndFormatApiError(
|
||||
errorMessagePreview,
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
undefined,
|
||||
'gemini-2.5-preview-pro',
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
);
|
||||
|
||||
expect(result25).toContain(
|
||||
'You have reached your daily gemini-2.5-pro quota limit',
|
||||
);
|
||||
expect(resultPreview).toContain(
|
||||
'You have reached your daily gemini-2.5-preview-pro quota limit',
|
||||
);
|
||||
expect(result25).toContain(
|
||||
'upgrade to a Gemini Code Assist Standard or Enterprise plan',
|
||||
);
|
||||
expect(resultPreview).toContain(
|
||||
'upgrade to a Gemini Code Assist Standard or Enterprise plan',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not match non-Pro models with similar version strings', () => {
|
||||
// Test that Flash models with similar version strings don't match
|
||||
expect(
|
||||
isProQuotaExceededError(
|
||||
"Quota exceeded for quota metric 'Gemini 2.5 Flash Requests' and limit",
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isProQuotaExceededError(
|
||||
"Quota exceeded for quota metric 'Gemini 2.5-preview Flash Requests' and limit",
|
||||
),
|
||||
).toBe(false);
|
||||
|
||||
// Test other model types
|
||||
expect(
|
||||
isProQuotaExceededError(
|
||||
"Quota exceeded for quota metric 'Gemini 2.5 Ultra Requests' and limit",
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isProQuotaExceededError(
|
||||
"Quota exceeded for quota metric 'Gemini 2.5 Standard Requests' and limit",
|
||||
),
|
||||
).toBe(false);
|
||||
|
||||
// Test generic quota messages
|
||||
expect(
|
||||
isProQuotaExceededError(
|
||||
"Quota exceeded for quota metric 'GenerationRequests' and limit",
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isProQuotaExceededError(
|
||||
"Quota exceeded for quota metric 'EmbeddingRequests' and limit",
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should format a generic quota exceeded message for Google auth (Standard tier)', () => {
|
||||
const errorMessage =
|
||||
'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Quota exceeded for quota metric \'GenerationRequests\' and limit \'RequestsPerDay\' of service \'generativelanguage.googleapis.com\' for consumer \'project_number:123456789\'.","status":"RESOURCE_EXHAUSTED"}}';
|
||||
const result = parseAndFormatApiError(
|
||||
errorMessage,
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
UserTierId.STANDARD,
|
||||
'gemini-2.5-pro',
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
);
|
||||
expect(result).toContain(
|
||||
"[API Error: Quota exceeded for quota metric 'GenerationRequests'",
|
||||
);
|
||||
expect(result).toContain('You have reached your daily quota limit');
|
||||
expect(result).toContain(
|
||||
'We appreciate you for choosing Gemini Code Assist and the Gemini CLI',
|
||||
);
|
||||
expect(result).not.toContain(
|
||||
'upgrade to a Gemini Code Assist Standard or Enterprise plan',
|
||||
);
|
||||
});
|
||||
|
||||
it('should format a regular 429 API error with standard message for Google auth (Standard tier)', () => {
|
||||
const errorMessage =
|
||||
'got status: 429 Too Many Requests. {"error":{"code":429,"message":"Rate limit exceeded","status":"RESOURCE_EXHAUSTED"}}';
|
||||
const result = parseAndFormatApiError(
|
||||
errorMessage,
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
UserTierId.STANDARD,
|
||||
'gemini-2.5-pro',
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
);
|
||||
expect(result).toContain('[API Error: Rate limit exceeded');
|
||||
expect(result).toContain(
|
||||
'We appreciate you for choosing Gemini Code Assist and the Gemini CLI',
|
||||
);
|
||||
expect(result).not.toContain(
|
||||
'upgrade to a Gemini Code Assist Standard or Enterprise plan',
|
||||
);
|
||||
});
|
||||
});
|
||||
166
packages/core/src/utils/errorParsing.ts
Normal file
166
packages/core/src/utils/errorParsing.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
isProQuotaExceededError,
|
||||
isGenericQuotaExceededError,
|
||||
isApiError,
|
||||
isStructuredError,
|
||||
} from './quotaErrorDetection.js';
|
||||
import {
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
} from '../config/models.js';
|
||||
import { UserTierId } from '../code_assist/types.js';
|
||||
import { AuthType } from '../core/contentGenerator.js';
|
||||
|
||||
// Free Tier message functions
|
||||
const getRateLimitErrorMessageGoogleFree = (
|
||||
fallbackModel: string = DEFAULT_GEMINI_FLASH_MODEL,
|
||||
) =>
|
||||
`\nPossible quota limitations in place or slow response times detected. Switching to the ${fallbackModel} model for the rest of this session.`;
|
||||
|
||||
const getRateLimitErrorMessageGoogleProQuotaFree = (
|
||||
currentModel: string = DEFAULT_GEMINI_MODEL,
|
||||
fallbackModel: string = DEFAULT_GEMINI_FLASH_MODEL,
|
||||
) =>
|
||||
`\nYou have reached your daily ${currentModel} quota limit. You will be switched to the ${fallbackModel} model for the rest of this session. To increase your limits, upgrade to a Gemini Code Assist Standard or Enterprise plan with higher limits at https://goo.gle/set-up-gemini-code-assist, or use /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`;
|
||||
|
||||
const getRateLimitErrorMessageGoogleGenericQuotaFree = () =>
|
||||
`\nYou have reached your daily quota limit. To increase your limits, upgrade to a Gemini Code Assist Standard or Enterprise plan with higher limits at https://goo.gle/set-up-gemini-code-assist, or use /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`;
|
||||
|
||||
// Legacy/Standard Tier message functions
|
||||
const getRateLimitErrorMessageGooglePaid = (
|
||||
fallbackModel: string = DEFAULT_GEMINI_FLASH_MODEL,
|
||||
) =>
|
||||
`\nPossible quota limitations in place or slow response times detected. Switching to the ${fallbackModel} model for the rest of this session. We appreciate you for choosing Gemini Code Assist and the Gemini CLI.`;
|
||||
|
||||
const getRateLimitErrorMessageGoogleProQuotaPaid = (
|
||||
currentModel: string = DEFAULT_GEMINI_MODEL,
|
||||
fallbackModel: string = DEFAULT_GEMINI_FLASH_MODEL,
|
||||
) =>
|
||||
`\nYou have reached your daily ${currentModel} quota limit. You will be switched to the ${fallbackModel} model for the rest of this session. We appreciate you for choosing Gemini Code Assist and the Gemini CLI. To continue accessing the ${currentModel} model today, consider using /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`;
|
||||
|
||||
const getRateLimitErrorMessageGoogleGenericQuotaPaid = (
|
||||
currentModel: string = DEFAULT_GEMINI_MODEL,
|
||||
) =>
|
||||
`\nYou have reached your daily quota limit. We appreciate you for choosing Gemini Code Assist and the Gemini CLI. To continue accessing the ${currentModel} model today, consider using /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`;
|
||||
const RATE_LIMIT_ERROR_MESSAGE_USE_GEMINI =
|
||||
'\nPlease wait and try again later. To increase your limits, request a quota increase through AI Studio, or switch to another /auth method';
|
||||
const RATE_LIMIT_ERROR_MESSAGE_VERTEX =
|
||||
'\nPlease wait and try again later. To increase your limits, request a quota increase through Vertex, or switch to another /auth method';
|
||||
const getRateLimitErrorMessageDefault = (
|
||||
fallbackModel: string = DEFAULT_GEMINI_FLASH_MODEL,
|
||||
) =>
|
||||
`\nPossible quota limitations in place or slow response times detected. Switching to the ${fallbackModel} model for the rest of this session.`;
|
||||
|
||||
function getRateLimitMessage(
|
||||
authType?: AuthType,
|
||||
error?: unknown,
|
||||
userTier?: UserTierId,
|
||||
currentModel?: string,
|
||||
fallbackModel?: string,
|
||||
): string {
|
||||
switch (authType) {
|
||||
case AuthType.LOGIN_WITH_GOOGLE: {
|
||||
// Determine if user is on a paid tier (Legacy or Standard) - default to FREE if not specified
|
||||
const isPaidTier =
|
||||
userTier === UserTierId.LEGACY || userTier === UserTierId.STANDARD;
|
||||
|
||||
if (isProQuotaExceededError(error)) {
|
||||
return isPaidTier
|
||||
? getRateLimitErrorMessageGoogleProQuotaPaid(
|
||||
currentModel || DEFAULT_GEMINI_MODEL,
|
||||
fallbackModel,
|
||||
)
|
||||
: getRateLimitErrorMessageGoogleProQuotaFree(
|
||||
currentModel || DEFAULT_GEMINI_MODEL,
|
||||
fallbackModel,
|
||||
);
|
||||
} else if (isGenericQuotaExceededError(error)) {
|
||||
return isPaidTier
|
||||
? getRateLimitErrorMessageGoogleGenericQuotaPaid(
|
||||
currentModel || DEFAULT_GEMINI_MODEL,
|
||||
)
|
||||
: getRateLimitErrorMessageGoogleGenericQuotaFree();
|
||||
} else {
|
||||
return isPaidTier
|
||||
? getRateLimitErrorMessageGooglePaid(fallbackModel)
|
||||
: getRateLimitErrorMessageGoogleFree(fallbackModel);
|
||||
}
|
||||
}
|
||||
case AuthType.USE_GEMINI:
|
||||
return RATE_LIMIT_ERROR_MESSAGE_USE_GEMINI;
|
||||
case AuthType.USE_VERTEX_AI:
|
||||
return RATE_LIMIT_ERROR_MESSAGE_VERTEX;
|
||||
default:
|
||||
return getRateLimitErrorMessageDefault(fallbackModel);
|
||||
}
|
||||
}
|
||||
|
||||
export function parseAndFormatApiError(
|
||||
error: unknown,
|
||||
authType?: AuthType,
|
||||
userTier?: UserTierId,
|
||||
currentModel?: string,
|
||||
fallbackModel?: string,
|
||||
): string {
|
||||
if (isStructuredError(error)) {
|
||||
let text = `[API Error: ${error.message}]`;
|
||||
if (error.status === 429) {
|
||||
text += getRateLimitMessage(
|
||||
authType,
|
||||
error,
|
||||
userTier,
|
||||
currentModel,
|
||||
fallbackModel,
|
||||
);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
// The error message might be a string containing a JSON object.
|
||||
if (typeof error === 'string') {
|
||||
const jsonStart = error.indexOf('{');
|
||||
if (jsonStart === -1) {
|
||||
return `[API Error: ${error}]`; // Not a JSON error, return as is.
|
||||
}
|
||||
|
||||
const jsonString = error.substring(jsonStart);
|
||||
|
||||
try {
|
||||
const parsedError = JSON.parse(jsonString) as unknown;
|
||||
if (isApiError(parsedError)) {
|
||||
let finalMessage = parsedError.error.message;
|
||||
try {
|
||||
// See if the message is a stringified JSON with another error
|
||||
const nestedError = JSON.parse(finalMessage) as unknown;
|
||||
if (isApiError(nestedError)) {
|
||||
finalMessage = nestedError.error.message;
|
||||
}
|
||||
} catch (_e) {
|
||||
// It's not a nested JSON error, so we just use the message as is.
|
||||
}
|
||||
let text = `[API Error: ${finalMessage} (Status: ${parsedError.error.status})]`;
|
||||
if (parsedError.error.code === 429) {
|
||||
text += getRateLimitMessage(
|
||||
authType,
|
||||
parsedError,
|
||||
userTier,
|
||||
currentModel,
|
||||
fallbackModel,
|
||||
);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
} catch (_e) {
|
||||
// Not a valid JSON, fall through and return the original message.
|
||||
}
|
||||
return `[API Error: ${error}]`;
|
||||
}
|
||||
|
||||
return '[API Error: An unknown error occurred.]';
|
||||
}
|
||||
@@ -289,7 +289,7 @@ export class FileSearch {
|
||||
* Builds the in-memory cache for fast pattern matching.
|
||||
*/
|
||||
private buildResultCache(): void {
|
||||
this.resultCache = new ResultCache(this.allFiles, this.absoluteDir);
|
||||
this.resultCache = new ResultCache(this.allFiles);
|
||||
// The v1 algorithm is much faster since it only looks at the first
|
||||
// occurence of the pattern. We use it for search spaces that have >20k
|
||||
// files, because the v2 algorithm is just too slow in those cases.
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import path from 'node:path';
|
||||
import { test, expect } from 'vitest';
|
||||
import { ResultCache } from './result-cache.js';
|
||||
|
||||
@@ -17,7 +16,7 @@ test('ResultCache basic usage', async () => {
|
||||
'subdir/other.js',
|
||||
'subdir/nested/file.md',
|
||||
];
|
||||
const cache = new ResultCache(files, path.resolve('.'));
|
||||
const cache = new ResultCache(files);
|
||||
const { files: resultFiles, isExactMatch } = await cache.get('*.js');
|
||||
expect(resultFiles).toEqual(files);
|
||||
expect(isExactMatch).toBe(false);
|
||||
@@ -25,7 +24,7 @@ test('ResultCache basic usage', async () => {
|
||||
|
||||
test('ResultCache cache hit/miss', async () => {
|
||||
const files = ['foo.txt', 'bar.js', 'baz.md'];
|
||||
const cache = new ResultCache(files, path.resolve('.'));
|
||||
const cache = new ResultCache(files);
|
||||
// First call: miss
|
||||
const { files: result1Files, isExactMatch: isExactMatch1 } =
|
||||
await cache.get('*.js');
|
||||
@@ -44,7 +43,7 @@ test('ResultCache cache hit/miss', async () => {
|
||||
|
||||
test('ResultCache best base query', async () => {
|
||||
const files = ['foo.txt', 'foobar.js', 'baz.md'];
|
||||
const cache = new ResultCache(files, path.resolve('.'));
|
||||
const cache = new ResultCache(files);
|
||||
|
||||
// Cache a broader query
|
||||
cache.set('foo', ['foo.txt', 'foobar.js']);
|
||||
|
||||
@@ -13,10 +13,7 @@ export class ResultCache {
|
||||
private hits = 0;
|
||||
private misses = 0;
|
||||
|
||||
constructor(
|
||||
private readonly allFiles: string[],
|
||||
private readonly absoluteDir: string,
|
||||
) {
|
||||
constructor(private readonly allFiles: string[]) {
|
||||
this.cache = new Map();
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { marked } from 'marked';
|
||||
import { processImports, validateImportPath } from './memoryImportProcessor.js';
|
||||
|
||||
// Helper function to create platform-agnostic test paths
|
||||
const testPath = (...segments: string[]) => {
|
||||
function testPath(...segments: string[]): string {
|
||||
// Start with the first segment as is (might be an absolute path on Windows)
|
||||
let result = segments[0];
|
||||
|
||||
@@ -27,9 +27,8 @@ const testPath = (...segments: string[]) => {
|
||||
}
|
||||
|
||||
return path.normalize(result);
|
||||
};
|
||||
}
|
||||
|
||||
// Mock fs/promises
|
||||
vi.mock('fs/promises');
|
||||
const mockedFs = vi.mocked(fs);
|
||||
|
||||
@@ -509,21 +508,21 @@ describe('memoryImportProcessor', () => {
|
||||
expect(result.importTree.imports).toHaveLength(2);
|
||||
|
||||
// First import: nested.md
|
||||
// Prefix with underscore to indicate they're intentionally unused
|
||||
const _expectedNestedPath = testPath(projectRoot, 'src', 'nested.md');
|
||||
const _expectedInnerPath = testPath(projectRoot, 'src', 'inner.md');
|
||||
const _expectedSimplePath = testPath(projectRoot, 'src', 'simple.md');
|
||||
|
||||
// Check that the paths match using includes to handle potential absolute/relative differences
|
||||
expect(result.importTree.imports![0].path).toContain('nested.md');
|
||||
const expectedNestedPath = testPath(projectRoot, 'src', 'nested.md');
|
||||
|
||||
expect(result.importTree.imports![0].path).toContain(expectedNestedPath);
|
||||
expect(result.importTree.imports![0].imports).toHaveLength(1);
|
||||
|
||||
const expectedInnerPath = testPath(projectRoot, 'src', 'inner.md');
|
||||
expect(result.importTree.imports![0].imports![0].path).toContain(
|
||||
'inner.md',
|
||||
expectedInnerPath,
|
||||
);
|
||||
expect(result.importTree.imports![0].imports![0].imports).toBeUndefined();
|
||||
|
||||
// Second import: simple.md
|
||||
expect(result.importTree.imports![1].path).toContain('simple.md');
|
||||
const expectedSimplePath = testPath(projectRoot, 'src', 'simple.md');
|
||||
expect(result.importTree.imports![1].path).toContain(expectedSimplePath);
|
||||
expect(result.importTree.imports![1].imports).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -724,21 +723,20 @@ describe('memoryImportProcessor', () => {
|
||||
expect(result.importTree.imports).toHaveLength(2);
|
||||
|
||||
// First import: nested.md
|
||||
// Prefix with underscore to indicate they're intentionally unused
|
||||
const _expectedNestedPath = testPath(projectRoot, 'src', 'nested.md');
|
||||
const _expectedInnerPath = testPath(projectRoot, 'src', 'inner.md');
|
||||
const _expectedSimplePath = testPath(projectRoot, 'src', 'simple.md');
|
||||
const expectedNestedPath = testPath(projectRoot, 'src', 'nested.md');
|
||||
const expectedInnerPath = testPath(projectRoot, 'src', 'inner.md');
|
||||
const expectedSimplePath = testPath(projectRoot, 'src', 'simple.md');
|
||||
|
||||
// Check that the paths match using includes to handle potential absolute/relative differences
|
||||
expect(result.importTree.imports![0].path).toContain('nested.md');
|
||||
expect(result.importTree.imports![0].path).toContain(expectedNestedPath);
|
||||
expect(result.importTree.imports![0].imports).toHaveLength(1);
|
||||
expect(result.importTree.imports![0].imports![0].path).toContain(
|
||||
'inner.md',
|
||||
expectedInnerPath,
|
||||
);
|
||||
expect(result.importTree.imports![0].imports![0].imports).toBeUndefined();
|
||||
|
||||
// Second import: simple.md
|
||||
expect(result.importTree.imports![1].path).toContain('simple.md');
|
||||
expect(result.importTree.imports![1].path).toContain(expectedSimplePath);
|
||||
expect(result.importTree.imports![1].imports).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -899,7 +897,7 @@ describe('memoryImportProcessor', () => {
|
||||
|
||||
// Test relative paths - resolve them against basePath
|
||||
const relativePath = './file.md';
|
||||
const _resolvedRelativePath = path.resolve(basePath, relativePath);
|
||||
path.resolve(basePath, relativePath);
|
||||
expect(validateImportPath(relativePath, basePath, [basePath])).toBe(true);
|
||||
|
||||
// Test parent directory access (should be allowed if parent is in allowed paths)
|
||||
@@ -907,12 +905,12 @@ describe('memoryImportProcessor', () => {
|
||||
if (parentPath !== basePath) {
|
||||
// Only test if parent is different
|
||||
const parentRelativePath = '../file.md';
|
||||
const _resolvedParentPath = path.resolve(basePath, parentRelativePath);
|
||||
path.resolve(basePath, parentRelativePath);
|
||||
expect(
|
||||
validateImportPath(parentRelativePath, basePath, [parentPath]),
|
||||
).toBe(true);
|
||||
|
||||
const _resolvedSubPath = path.resolve(basePath, 'sub');
|
||||
path.resolve(basePath, 'sub');
|
||||
const resultSub = validateImportPath('sub', basePath, [basePath]);
|
||||
expect(resultSub).toBe(true);
|
||||
}
|
||||
|
||||
@@ -261,7 +261,7 @@ export async function processImports(
|
||||
|
||||
// Process imports in reverse order to handle indices correctly
|
||||
for (let i = imports.length - 1; i >= 0; i--) {
|
||||
const { start, _end, path: importPath } = imports[i];
|
||||
const { start, path: importPath } = imports[i];
|
||||
|
||||
// Skip if inside a code region
|
||||
if (
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { StructuredError } from '../core/turn.js';
|
||||
|
||||
export interface ApiError {
|
||||
error: {
|
||||
code: number;
|
||||
@@ -13,11 +15,6 @@ export interface ApiError {
|
||||
};
|
||||
}
|
||||
|
||||
interface StructuredError {
|
||||
message: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export function isApiError(error: unknown): error is ApiError {
|
||||
return (
|
||||
typeof error === 'object' &&
|
||||
|
||||
Reference in New Issue
Block a user