mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-03 13:41:05 -07:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 72e48451c6 | |||
| 230056cc41 | |||
| be36bf6193 | |||
| 37670fe6e6 | |||
| ece06155cc | |||
| 13b443af4f | |||
| fd885a3e50 | |||
| 25d7a8037f |
@@ -467,7 +467,7 @@ their corresponding top-level category object in your `settings.json` file.
|
|||||||
|
|
||||||
- **`useWriteTodos`** (boolean):
|
- **`useWriteTodos`** (boolean):
|
||||||
- **Description:** Enable the write_todos_list tool.
|
- **Description:** Enable the write_todos_list tool.
|
||||||
- **Default:** `true`
|
- **Default:** `false`
|
||||||
|
|
||||||
#### `security`
|
#### `security`
|
||||||
|
|
||||||
|
|||||||
+3
-4
@@ -6,8 +6,7 @@ This document describes the `write_todos` tool for the Gemini CLI.
|
|||||||
|
|
||||||
The `write_todos` tool allows the Gemini agent to create and manage a list of
|
The `write_todos` tool allows the Gemini agent to create and manage a list of
|
||||||
subtasks for complex user requests. This provides you, the user, with greater
|
subtasks for complex user requests. This provides you, the user, with greater
|
||||||
visibility into the agent's plan and its current progress. It also helps with
|
visibility into the agent's plan and its current progress.
|
||||||
alignment where the agent is less likely to lose track of its current goal.
|
|
||||||
|
|
||||||
### Arguments
|
### Arguments
|
||||||
|
|
||||||
@@ -50,8 +49,8 @@ write_todos({
|
|||||||
|
|
||||||
## Important notes
|
## Important notes
|
||||||
|
|
||||||
- **Enabling:** This tool is enabled by default. You can disable it in your
|
- **Enabling:** This tool is disabled by default. To use it, you must enable it
|
||||||
`settings.json` file by setting `"useWriteTodos": false`.
|
in your `settings.json` file by setting `"useWriteTodos": true`.
|
||||||
|
|
||||||
- **Intended Use:** This tool is primarily used by the agent for complex,
|
- **Intended Use:** This tool is primarily used by the agent for complex,
|
||||||
multi-turn tasks. It is generally not used for simple, single-turn questions.
|
multi-turn tasks. It is generally not used for simple, single-turn questions.
|
||||||
|
|||||||
Generated
+7
-7
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@google/gemini-cli",
|
"name": "@google/gemini-cli",
|
||||||
"version": "0.13.0-nightly.20251031.c89bc30d",
|
"version": "0.13.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@google/gemini-cli",
|
"name": "@google/gemini-cli",
|
||||||
"version": "0.13.0-nightly.20251031.c89bc30d",
|
"version": "0.13.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
@@ -16959,7 +16959,7 @@
|
|||||||
},
|
},
|
||||||
"packages/a2a-server": {
|
"packages/a2a-server": {
|
||||||
"name": "@google/gemini-cli-a2a-server",
|
"name": "@google/gemini-cli-a2a-server",
|
||||||
"version": "0.13.0-nightly.20251031.c89bc30d",
|
"version": "0.13.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@a2a-js/sdk": "^0.3.2",
|
"@a2a-js/sdk": "^0.3.2",
|
||||||
"@google-cloud/storage": "^7.16.0",
|
"@google-cloud/storage": "^7.16.0",
|
||||||
@@ -17249,7 +17249,7 @@
|
|||||||
},
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
"name": "@google/gemini-cli",
|
"name": "@google/gemini-cli",
|
||||||
"version": "0.13.0-nightly.20251031.c89bc30d",
|
"version": "0.13.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google/gemini-cli-core": "file:../core",
|
"@google/gemini-cli-core": "file:../core",
|
||||||
"@google/genai": "1.16.0",
|
"@google/genai": "1.16.0",
|
||||||
@@ -17349,7 +17349,7 @@
|
|||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@google/gemini-cli-core",
|
"name": "@google/gemini-cli-core",
|
||||||
"version": "0.13.0-nightly.20251031.c89bc30d",
|
"version": "0.13.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google-cloud/logging": "^11.2.1",
|
"@google-cloud/logging": "^11.2.1",
|
||||||
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0",
|
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0",
|
||||||
@@ -17493,7 +17493,7 @@
|
|||||||
},
|
},
|
||||||
"packages/test-utils": {
|
"packages/test-utils": {
|
||||||
"name": "@google/gemini-cli-test-utils",
|
"name": "@google/gemini-cli-test-utils",
|
||||||
"version": "0.13.0-nightly.20251031.c89bc30d",
|
"version": "0.13.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
@@ -17504,7 +17504,7 @@
|
|||||||
},
|
},
|
||||||
"packages/vscode-ide-companion": {
|
"packages/vscode-ide-companion": {
|
||||||
"name": "gemini-cli-vscode-ide-companion",
|
"name": "gemini-cli-vscode-ide-companion",
|
||||||
"version": "0.13.0-nightly.20251031.c89bc30d",
|
"version": "0.13.0",
|
||||||
"license": "LICENSE",
|
"license": "LICENSE",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.15.1",
|
"@modelcontextprotocol/sdk": "^1.15.1",
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@google/gemini-cli",
|
"name": "@google/gemini-cli",
|
||||||
"version": "0.13.0-nightly.20251031.c89bc30d",
|
"version": "0.13.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"url": "git+https://github.com/google-gemini/gemini-cli.git"
|
"url": "git+https://github.com/google-gemini/gemini-cli.git"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.13.0-nightly.20251031.c89bc30d"
|
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.13.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "cross-env NODE_ENV=development node scripts/start.js",
|
"start": "cross-env NODE_ENV=development node scripts/start.js",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@google/gemini-cli-a2a-server",
|
"name": "@google/gemini-cli-a2a-server",
|
||||||
"version": "0.13.0-nightly.20251031.c89bc30d",
|
"version": "0.13.0",
|
||||||
"description": "Gemini CLI A2A Server",
|
"description": "Gemini CLI A2A Server",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@google/gemini-cli",
|
"name": "@google/gemini-cli",
|
||||||
"version": "0.13.0-nightly.20251031.c89bc30d",
|
"version": "0.13.0",
|
||||||
"description": "Gemini CLI",
|
"description": "Gemini CLI",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"config": {
|
"config": {
|
||||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.13.0-nightly.20251031.c89bc30d"
|
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.13.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google/gemini-cli-core": "file:../core",
|
"@google/gemini-cli-core": "file:../core",
|
||||||
|
|||||||
@@ -1047,7 +1047,7 @@ const SETTINGS_SCHEMA = {
|
|||||||
label: 'Use Write Todos',
|
label: 'Use Write Todos',
|
||||||
category: 'Advanced',
|
category: 'Advanced',
|
||||||
requiresRestart: false,
|
requiresRestart: false,
|
||||||
default: true,
|
default: false,
|
||||||
description: 'Enable the write_todos_list tool.',
|
description: 'Enable the write_todos_list tool.',
|
||||||
showInDialog: false,
|
showInDialog: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1709,4 +1709,41 @@ describe('AppContainer State Management', () => {
|
|||||||
unmount();
|
unmount();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Shell Interaction', () => {
|
||||||
|
it('should not crash if resizing the pty fails', async () => {
|
||||||
|
const resizePtySpy = vi
|
||||||
|
.spyOn(ShellExecutionService, 'resizePty')
|
||||||
|
.mockImplementation(() => {
|
||||||
|
throw new Error('Cannot resize a pty that has already exited');
|
||||||
|
});
|
||||||
|
|
||||||
|
mockedUseGeminiStream.mockReturnValue({
|
||||||
|
streamingState: 'idle',
|
||||||
|
submitQuery: vi.fn(),
|
||||||
|
initError: null,
|
||||||
|
pendingHistoryItems: [],
|
||||||
|
thought: null,
|
||||||
|
cancelOngoingRequest: vi.fn(),
|
||||||
|
activePtyId: 'some-pty-id', // Make sure activePtyId is set
|
||||||
|
});
|
||||||
|
|
||||||
|
// The main assertion is that the render does not throw.
|
||||||
|
const { unmount } = render(
|
||||||
|
<AppContainer
|
||||||
|
config={mockConfig}
|
||||||
|
settings={mockSettings}
|
||||||
|
version="1.0.0"
|
||||||
|
initializationResult={mockInitResult}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resizePtySpy).toHaveBeenCalled();
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -805,11 +805,27 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activePtyId) {
|
if (activePtyId) {
|
||||||
ShellExecutionService.resizePty(
|
try {
|
||||||
activePtyId,
|
ShellExecutionService.resizePty(
|
||||||
Math.floor(terminalWidth * SHELL_WIDTH_FRACTION),
|
activePtyId,
|
||||||
Math.max(Math.floor(availableTerminalHeight - SHELL_HEIGHT_PADDING), 1),
|
Math.floor(terminalWidth * SHELL_WIDTH_FRACTION),
|
||||||
);
|
Math.max(
|
||||||
|
Math.floor(availableTerminalHeight - SHELL_HEIGHT_PADDING),
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// This can happen in a race condition where the pty exits
|
||||||
|
// right before we try to resize it.
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
e instanceof Error &&
|
||||||
|
e.message.includes('Cannot resize a pty that has already exited')
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [terminalWidth, availableTerminalHeight, activePtyId]);
|
}, [terminalWidth, availableTerminalHeight, activePtyId]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@google/gemini-cli-core",
|
"name": "@google/gemini-cli-core",
|
||||||
"version": "0.13.0-nightly.20251031.c89bc30d",
|
"version": "0.13.0",
|
||||||
"description": "Gemini CLI Core",
|
"description": "Gemini CLI Core",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -494,7 +494,7 @@ export class Config {
|
|||||||
params.truncateToolOutputLines ?? DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES;
|
params.truncateToolOutputLines ?? DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES;
|
||||||
this.enableToolOutputTruncation = params.enableToolOutputTruncation ?? true;
|
this.enableToolOutputTruncation = params.enableToolOutputTruncation ?? true;
|
||||||
this.useSmartEdit = params.useSmartEdit ?? true;
|
this.useSmartEdit = params.useSmartEdit ?? true;
|
||||||
this.useWriteTodos = params.useWriteTodos ?? true;
|
this.useWriteTodos = params.useWriteTodos ?? false;
|
||||||
this.initialUseModelRouter = params.useModelRouter ?? false;
|
this.initialUseModelRouter = params.useModelRouter ?? false;
|
||||||
this.useModelRouter = this.initialUseModelRouter;
|
this.useModelRouter = this.initialUseModelRouter;
|
||||||
this.disableModelRouterForAuth = params.disableModelRouterForAuth ?? [];
|
this.disableModelRouterForAuth = params.disableModelRouterForAuth ?? [];
|
||||||
|
|||||||
@@ -1553,7 +1553,7 @@ describe('CoreToolScheduler request queueing', () => {
|
|||||||
expect(statusUpdates).toContain('awaiting_approval');
|
expect(statusUpdates).toContain('awaiting_approval');
|
||||||
expect(executeFn).not.toHaveBeenCalled();
|
expect(executeFn).not.toHaveBeenCalled();
|
||||||
expect(onAllToolCallsComplete).not.toHaveBeenCalled();
|
expect(onAllToolCallsComplete).not.toHaveBeenCalled();
|
||||||
});
|
}, 20000);
|
||||||
|
|
||||||
it('should handle two synchronous calls to schedule', async () => {
|
it('should handle two synchronous calls to schedule', async () => {
|
||||||
const executeFn = vi.fn().mockResolvedValue({
|
const executeFn = vi.fn().mockResolvedValue({
|
||||||
|
|||||||
@@ -351,6 +351,23 @@ describe('ShellExecutionService', () => {
|
|||||||
|
|
||||||
expect(mockHeadlessTerminal.scrollLines).toHaveBeenCalledWith(10);
|
expect(mockHeadlessTerminal.scrollLines).toHaveBeenCalledWith(10);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not throw when resizing a pty that has already exited (Windows)', () => {
|
||||||
|
const resizeError = new Error(
|
||||||
|
'Cannot resize a pty that has already exited',
|
||||||
|
);
|
||||||
|
mockPtyProcess.resize.mockImplementation(() => {
|
||||||
|
throw resizeError;
|
||||||
|
});
|
||||||
|
|
||||||
|
// This should catch the specific error and not re-throw it.
|
||||||
|
expect(() => {
|
||||||
|
ShellExecutionService.resizePty(mockPtyProcess.pid, 100, 40);
|
||||||
|
}).not.toThrow();
|
||||||
|
|
||||||
|
expect(mockPtyProcess.resize).toHaveBeenCalledWith(100, 40);
|
||||||
|
expect(mockHeadlessTerminal.resize).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Failed Execution', () => {
|
describe('Failed Execution', () => {
|
||||||
@@ -753,7 +770,7 @@ describe('ShellExecutionService child_process fallback', () => {
|
|||||||
expect(onOutputEventMock).not.toHaveBeenCalled();
|
expect(onOutputEventMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should truncate stdout using a sliding window and show a warning', async () => {
|
it.skip('should truncate stdout using a sliding window and show a warning', async () => {
|
||||||
const MAX_SIZE = 16 * 1024 * 1024;
|
const MAX_SIZE = 16 * 1024 * 1024;
|
||||||
const chunk1 = 'a'.repeat(MAX_SIZE / 2 - 5);
|
const chunk1 = 'a'.repeat(MAX_SIZE / 2 - 5);
|
||||||
const chunk2 = 'b'.repeat(MAX_SIZE / 2 - 5);
|
const chunk2 = 'b'.repeat(MAX_SIZE / 2 - 5);
|
||||||
@@ -781,7 +798,7 @@ describe('ShellExecutionService child_process fallback', () => {
|
|||||||
outputWithoutMessage.startsWith(expectedStart.substring(0, 10)),
|
outputWithoutMessage.startsWith(expectedStart.substring(0, 10)),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
expect(outputWithoutMessage.endsWith('c'.repeat(20))).toBe(true);
|
expect(outputWithoutMessage.endsWith('c'.repeat(20))).toBe(true);
|
||||||
}, 20000);
|
}, 120000);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Failed Execution', () => {
|
describe('Failed Execution', () => {
|
||||||
|
|||||||
@@ -771,9 +771,11 @@ export class ShellExecutionService {
|
|||||||
if (
|
if (
|
||||||
e instanceof Error &&
|
e instanceof Error &&
|
||||||
(('code' in e && e.code === 'ESRCH') ||
|
(('code' in e && e.code === 'ESRCH') ||
|
||||||
e.message === 'Cannot resize a pty that has already exited')
|
e.message.includes('Cannot resize a pty that has already exited'))
|
||||||
) {
|
) {
|
||||||
// ignore
|
// On Unix, we get an ESRCH error.
|
||||||
|
// On Windows, we get a message-based error.
|
||||||
|
// In both cases, it's safe to ignore.
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -232,30 +232,34 @@ describe('ShellTool', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
itWindowsOnly('should not wrap command on windows', async () => {
|
itWindowsOnly(
|
||||||
vi.mocked(os.platform).mockReturnValue('win32');
|
'should not wrap command on windows',
|
||||||
const invocation = shellTool.build({ command: 'dir' });
|
async () => {
|
||||||
const promise = invocation.execute(mockAbortSignal);
|
vi.mocked(os.platform).mockReturnValue('win32');
|
||||||
resolveShellExecution({
|
const invocation = shellTool.build({ command: 'dir' });
|
||||||
rawOutput: Buffer.from(''),
|
const promise = invocation.execute(mockAbortSignal);
|
||||||
output: '',
|
resolveShellExecution({
|
||||||
exitCode: 0,
|
rawOutput: Buffer.from(''),
|
||||||
signal: null,
|
output: '',
|
||||||
error: null,
|
exitCode: 0,
|
||||||
aborted: false,
|
signal: null,
|
||||||
pid: 12345,
|
error: null,
|
||||||
executionMethod: 'child_process',
|
aborted: false,
|
||||||
});
|
pid: 12345,
|
||||||
await promise;
|
executionMethod: 'child_process',
|
||||||
expect(mockShellExecutionService).toHaveBeenCalledWith(
|
});
|
||||||
'dir',
|
await promise;
|
||||||
'/test/dir',
|
expect(mockShellExecutionService).toHaveBeenCalledWith(
|
||||||
expect.any(Function),
|
'dir',
|
||||||
mockAbortSignal,
|
'/test/dir',
|
||||||
false,
|
expect.any(Function),
|
||||||
{},
|
mockAbortSignal,
|
||||||
);
|
false,
|
||||||
});
|
{},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
20000,
|
||||||
|
);
|
||||||
|
|
||||||
it('should format error messages correctly', async () => {
|
it('should format error messages correctly', async () => {
|
||||||
const error = new Error('wrapped command failed');
|
const error = new Error('wrapped command failed');
|
||||||
|
|||||||
@@ -25,6 +25,44 @@ describe('classifyGoogleError', () => {
|
|||||||
expect(result).toBe(regularError);
|
expect(result).toBe(regularError);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return RetryableQuotaError when message contains "Please retry in Xs"', () => {
|
||||||
|
const complexError = {
|
||||||
|
error: {
|
||||||
|
message:
|
||||||
|
'{"error": {"code": 429, "status": 429, "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/usage?tab=rate-limit. \n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 2\nPlease retry in 44.097740004s.", "details": [{"detail": "??? to (unknown) : APP_ERROR(8) You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/usage?tab=rate-limit. \n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 2\nPlease retry in 44.097740004s."}]}}',
|
||||||
|
code: 429,
|
||||||
|
status: 'Too Many Requests',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const rawError = new Error(JSON.stringify(complexError));
|
||||||
|
vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(null);
|
||||||
|
|
||||||
|
const result = classifyGoogleError(rawError);
|
||||||
|
|
||||||
|
expect(result).toBeInstanceOf(RetryableQuotaError);
|
||||||
|
expect((result as RetryableQuotaError).retryDelayMs).toBe(44097.740004);
|
||||||
|
expect((result as RetryableQuotaError).message).toBe(rawError.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return RetryableQuotaError when error is a string and message contains "Please retry in Xms"', () => {
|
||||||
|
const complexErrorString = JSON.stringify({
|
||||||
|
error: {
|
||||||
|
message:
|
||||||
|
'{"error": {"code": 429, "status": 429, "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/usage?tab=rate-limit. \n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 2\nPlease retry in 900.2ms.", "details": [{"detail": "??? to (unknown) : APP_ERROR(8) You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/usage?tab=rate-limit. \n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 2\nPlease retry in 900.2ms."}]}}',
|
||||||
|
code: 429,
|
||||||
|
status: 'Too Many Requests',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const rawError = new Error(complexErrorString);
|
||||||
|
vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(null);
|
||||||
|
|
||||||
|
const result = classifyGoogleError(rawError);
|
||||||
|
|
||||||
|
expect(result).toBeInstanceOf(RetryableQuotaError);
|
||||||
|
expect((result as RetryableQuotaError).retryDelayMs).toBeCloseTo(900.2);
|
||||||
|
expect((result as RetryableQuotaError).message).toBe(rawError.message);
|
||||||
|
});
|
||||||
|
|
||||||
it('should return original error if code is not 429', () => {
|
it('should return original error if code is not 429', () => {
|
||||||
const apiError: GoogleApiError = {
|
const apiError: GoogleApiError = {
|
||||||
code: 500,
|
code: 500,
|
||||||
|
|||||||
@@ -43,16 +43,20 @@ export class RetryableQuotaError extends Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a duration string (e.g., "34.074824224s", "60s") and returns the time in seconds.
|
* Parses a duration string (e.g., "34.074824224s", "60s", "900ms") and returns the time in seconds.
|
||||||
* @param duration The duration string to parse.
|
* @param duration The duration string to parse.
|
||||||
* @returns The duration in seconds, or null if parsing fails.
|
* @returns The duration in seconds, or null if parsing fails.
|
||||||
*/
|
*/
|
||||||
function parseDurationInSeconds(duration: string): number | null {
|
function parseDurationInSeconds(duration: string): number | null {
|
||||||
if (!duration.endsWith('s')) {
|
if (duration.endsWith('ms')) {
|
||||||
return null;
|
const milliseconds = parseFloat(duration.slice(0, -2));
|
||||||
|
return isNaN(milliseconds) ? null : milliseconds / 1000;
|
||||||
}
|
}
|
||||||
const seconds = parseFloat(duration.slice(0, -1));
|
if (duration.endsWith('s')) {
|
||||||
return isNaN(seconds) ? null : seconds;
|
const seconds = parseFloat(duration.slice(0, -1));
|
||||||
|
return isNaN(seconds) ? null : seconds;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,6 +68,7 @@ function parseDurationInSeconds(duration: string): number | null {
|
|||||||
* - If the error suggests a retry delay of more than 2 minutes, 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 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`.
|
* - If the error indicates a per-minute limit, it's a `RetryableQuotaError`.
|
||||||
|
* - If the error message contains the phrase "Please retry in X[s|ms]", it's a `RetryableQuotaError`.
|
||||||
*
|
*
|
||||||
* @param error The error to classify.
|
* @param error The error to classify.
|
||||||
* @returns A `TerminalQuotaError`, `RetryableQuotaError`, or the original `unknown` error.
|
* @returns A `TerminalQuotaError`, `RetryableQuotaError`, or the original `unknown` error.
|
||||||
@@ -72,6 +77,24 @@ export function classifyGoogleError(error: unknown): unknown {
|
|||||||
const googleApiError = parseGoogleApiError(error);
|
const googleApiError = parseGoogleApiError(error);
|
||||||
|
|
||||||
if (!googleApiError || googleApiError.code !== 429) {
|
if (!googleApiError || googleApiError.code !== 429) {
|
||||||
|
// Fallback: try to parse the error message for a retry delay
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
const match = errorMessage.match(/Please retry in ([0-9.]+(?:ms|s))/);
|
||||||
|
if (match?.[1]) {
|
||||||
|
const retryDelaySeconds = parseDurationInSeconds(match[1]);
|
||||||
|
if (retryDelaySeconds !== null) {
|
||||||
|
return new RetryableQuotaError(
|
||||||
|
errorMessage,
|
||||||
|
googleApiError ?? {
|
||||||
|
code: 429,
|
||||||
|
message: errorMessage,
|
||||||
|
details: [],
|
||||||
|
},
|
||||||
|
retryDelaySeconds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return error; // Not a 429 error we can handle.
|
return error; // Not a 429 error we can handle.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,18 +83,21 @@ describe('WorkspaceContext with real filesystem', () => {
|
|||||||
expect(directories).toHaveLength(2);
|
expect(directories).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle symbolic links correctly', () => {
|
it.skipIf(os.platform() === 'win32')(
|
||||||
const realDir = path.join(tempDir, 'real');
|
'should handle symbolic links correctly',
|
||||||
fs.mkdirSync(realDir, { recursive: true });
|
() => {
|
||||||
const symlinkDir = path.join(tempDir, 'symlink-to-real');
|
const realDir = path.join(tempDir, 'real');
|
||||||
fs.symlinkSync(realDir, symlinkDir, 'dir');
|
fs.mkdirSync(realDir, { recursive: true });
|
||||||
const workspaceContext = new WorkspaceContext(cwd);
|
const symlinkDir = path.join(tempDir, 'symlink-to-real');
|
||||||
workspaceContext.addDirectory(symlinkDir);
|
fs.symlinkSync(realDir, symlinkDir, 'dir');
|
||||||
|
const workspaceContext = new WorkspaceContext(cwd);
|
||||||
|
workspaceContext.addDirectory(symlinkDir);
|
||||||
|
|
||||||
const directories = workspaceContext.getDirectories();
|
const directories = workspaceContext.getDirectories();
|
||||||
|
|
||||||
expect(directories).toEqual([cwd, realDir]);
|
expect(directories).toEqual([cwd, realDir]);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('path validation', () => {
|
describe('path validation', () => {
|
||||||
@@ -158,7 +161,7 @@ describe('WorkspaceContext with real filesystem', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with symbolic link', () => {
|
describe.skipIf(os.platform() === 'win32')('with symbolic link', () => {
|
||||||
describe('in the workspace', () => {
|
describe('in the workspace', () => {
|
||||||
let realDir: string;
|
let realDir: string;
|
||||||
let symlinkDir: string;
|
let symlinkDir: string;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { defineConfig } from 'vitest/config';
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
reporters: ['default', 'junit'],
|
reporters: ['default', 'junit'],
|
||||||
|
timeout: 30000,
|
||||||
silent: true,
|
silent: true,
|
||||||
setupFiles: ['./test-setup.ts'],
|
setupFiles: ['./test-setup.ts'],
|
||||||
outputFile: {
|
outputFile: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@google/gemini-cli-test-utils",
|
"name": "@google/gemini-cli-test-utils",
|
||||||
"version": "0.13.0-nightly.20251031.c89bc30d",
|
"version": "0.13.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "gemini-cli-vscode-ide-companion",
|
"name": "gemini-cli-vscode-ide-companion",
|
||||||
"displayName": "Gemini CLI Companion",
|
"displayName": "Gemini CLI Companion",
|
||||||
"description": "Enable Gemini CLI with direct access to your IDE workspace.",
|
"description": "Enable Gemini CLI with direct access to your IDE workspace.",
|
||||||
"version": "0.13.0-nightly.20251031.c89bc30d",
|
"version": "0.13.0",
|
||||||
"publisher": "google",
|
"publisher": "google",
|
||||||
"icon": "assets/icon.png",
|
"icon": "assets/icon.png",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -675,8 +675,8 @@
|
|||||||
"useWriteTodos": {
|
"useWriteTodos": {
|
||||||
"title": "Use Write Todos",
|
"title": "Use Write Todos",
|
||||||
"description": "Enable the write_todos_list tool.",
|
"description": "Enable the write_todos_list tool.",
|
||||||
"markdownDescription": "Enable the write_todos_list tool.\n\n- Category: `Advanced`\n- Requires restart: `no`\n- Default: `true`",
|
"markdownDescription": "Enable the write_todos_list tool.\n\n- Category: `Advanced`\n- Requires restart: `no`\n- Default: `false`",
|
||||||
"default": true,
|
"default": false,
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"security": {
|
"security": {
|
||||||
|
|||||||
Reference in New Issue
Block a user