mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-23 00:01:24 -07:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e87af9cf2 | |||
| dfa8394cad | |||
| 7059517a00 | |||
| 9519ba6f0e | |||
| 57f1c6912c | |||
| 0a11ce3e93 | |||
| 8acfe0c4ac | |||
| 5a0bee9016 | |||
| b545258c4c |
@@ -602,6 +602,12 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"model": "gemini-3.1-flash-lite"
|
||||
}
|
||||
},
|
||||
"gemini-3.5-flash": {
|
||||
"extends": "chat-base-3",
|
||||
"modelConfig": {
|
||||
"model": "gemini-3.5-flash"
|
||||
}
|
||||
},
|
||||
"gemma-4-31b-it": {
|
||||
"extends": "chat-base-3",
|
||||
"modelConfig": {
|
||||
@@ -626,6 +632,12 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"model": "gemini-3-flash-preview"
|
||||
}
|
||||
},
|
||||
"gemini-3.5-flash-base": {
|
||||
"extends": "base",
|
||||
"modelConfig": {
|
||||
"model": "gemini-3.5-flash"
|
||||
}
|
||||
},
|
||||
"classifier": {
|
||||
"extends": "base",
|
||||
"modelConfig": {
|
||||
@@ -868,6 +880,16 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"multimodalToolUse": true
|
||||
}
|
||||
},
|
||||
"gemini-3.5-flash": {
|
||||
"tier": "flash",
|
||||
"family": "gemini-3",
|
||||
"isPreview": false,
|
||||
"isVisible": true,
|
||||
"features": {
|
||||
"thinking": false,
|
||||
"multimodalToolUse": true
|
||||
}
|
||||
},
|
||||
"gemini-2.5-pro": {
|
||||
"tier": "pro",
|
||||
"family": "gemini-2.5",
|
||||
@@ -1020,9 +1042,46 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"contexts": [
|
||||
{
|
||||
"condition": {
|
||||
"hasAccessToPreview": false,
|
||||
"useGemini3_5Flash": true
|
||||
},
|
||||
"target": "gemini-3.5-flash"
|
||||
},
|
||||
{
|
||||
"condition": {
|
||||
"hasAccessToPreview": false,
|
||||
"useGemini3_5Flash": false
|
||||
},
|
||||
"target": "gemini-2.5-flash"
|
||||
}
|
||||
]
|
||||
},
|
||||
"gemini-3.5-flash": {
|
||||
"default": "gemini-3.5-flash",
|
||||
"contexts": [
|
||||
{
|
||||
"condition": {
|
||||
"useGemini3_5Flash": false,
|
||||
"hasAccessToPreview": false
|
||||
},
|
||||
"target": "gemini-2.5-flash"
|
||||
},
|
||||
{
|
||||
"condition": {
|
||||
"useGemini3_5Flash": false
|
||||
},
|
||||
"target": "gemini-3-flash-preview"
|
||||
}
|
||||
]
|
||||
},
|
||||
"gemini-2.5-flash": {
|
||||
"default": "gemini-2.5-flash",
|
||||
"contexts": [
|
||||
{
|
||||
"condition": {
|
||||
"useGemini3_5Flash": true
|
||||
},
|
||||
"target": "gemini-3.5-flash"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1104,6 +1163,12 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"flash": {
|
||||
"default": "gemini-3-flash-preview",
|
||||
"contexts": [
|
||||
{
|
||||
"condition": {
|
||||
"useGemini3_5Flash": true
|
||||
},
|
||||
"target": "gemini-3.5-flash"
|
||||
},
|
||||
{
|
||||
"condition": {
|
||||
"hasAccessToPreview": false
|
||||
@@ -1157,6 +1222,12 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"flash": {
|
||||
"default": "gemini-3-flash-preview",
|
||||
"contexts": [
|
||||
{
|
||||
"condition": {
|
||||
"useGemini3_5Flash": true
|
||||
},
|
||||
"target": "gemini-3.5-flash"
|
||||
},
|
||||
{
|
||||
"condition": {
|
||||
"hasAccessToPreview": false
|
||||
|
||||
+24
-4
@@ -76,10 +76,30 @@ export class LLMJudge {
|
||||
|
||||
for (const res of rawResults) {
|
||||
// Remove any punctuation the model might have appended
|
||||
const cleanRes = res.replace(/[^A-Z]/g, '');
|
||||
if (cleanRes.startsWith('YES')) yes++;
|
||||
else if (cleanRes.startsWith('NO')) no++;
|
||||
else other++;
|
||||
const cleanRes = res.replace(/[^A-Z ]/g, '');
|
||||
if (
|
||||
cleanRes.includes('THE ANSWER IS YES') ||
|
||||
cleanRes.includes('ANSWER IS YES') ||
|
||||
cleanRes.endsWith('YES')
|
||||
) {
|
||||
yes++;
|
||||
} else if (
|
||||
cleanRes.includes('THE ANSWER IS NO') ||
|
||||
cleanRes.includes('ANSWER IS NO') ||
|
||||
cleanRes.endsWith('NO')
|
||||
) {
|
||||
no++;
|
||||
} else if (cleanRes.trim() === 'YES') {
|
||||
yes++;
|
||||
} else if (cleanRes.trim() === 'NO') {
|
||||
no++;
|
||||
} else {
|
||||
// Fallback: look for YES or NO as standalone words or at the end
|
||||
const words = cleanRes.split(/\s+/);
|
||||
if (words.includes('YES')) yes++;
|
||||
else if (words.includes('NO')) no++;
|
||||
else other++;
|
||||
}
|
||||
}
|
||||
|
||||
// Pass if YES > NO and YES > OTHER (plurality)
|
||||
|
||||
Generated
+9
-9
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.45.0-nightly.20260521.g854f811be",
|
||||
"version": "0.45.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.45.0-nightly.20260521.g854f811be",
|
||||
"version": "0.45.2",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
@@ -18117,7 +18117,7 @@
|
||||
},
|
||||
"packages/a2a-server": {
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.45.0-nightly.20260521.g854f811be",
|
||||
"version": "0.45.2",
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "0.3.11",
|
||||
"@google-cloud/storage": "^7.19.0",
|
||||
@@ -18246,7 +18246,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.45.0-nightly.20260521.g854f811be",
|
||||
"version": "0.45.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.16.1",
|
||||
@@ -18394,7 +18394,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.45.0-nightly.20260521.g854f811be",
|
||||
"version": "0.45.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "0.3.11",
|
||||
@@ -18674,7 +18674,7 @@
|
||||
},
|
||||
"packages/devtools": {
|
||||
"name": "@google/gemini-cli-devtools",
|
||||
"version": "0.45.0-nightly.20260521.g854f811be",
|
||||
"version": "0.45.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"ws": "^8.16.0"
|
||||
@@ -18689,7 +18689,7 @@
|
||||
},
|
||||
"packages/sdk": {
|
||||
"name": "@google/gemini-cli-sdk",
|
||||
"version": "0.45.0-nightly.20260521.g854f811be",
|
||||
"version": "0.45.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
@@ -18720,7 +18720,7 @@
|
||||
},
|
||||
"packages/test-utils": {
|
||||
"name": "@google/gemini-cli-test-utils",
|
||||
"version": "0.45.0-nightly.20260521.g854f811be",
|
||||
"version": "0.45.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
@@ -18752,7 +18752,7 @@
|
||||
},
|
||||
"packages/vscode-ide-companion": {
|
||||
"name": "gemini-cli-vscode-ide-companion",
|
||||
"version": "0.45.0-nightly.20260521.g854f811be",
|
||||
"version": "0.45.2",
|
||||
"license": "LICENSE",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.23.0",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.45.0-nightly.20260521.g854f811be",
|
||||
"version": "0.45.2",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
@@ -14,7 +14,7 @@
|
||||
"url": "git+https://github.com/google-gemini/gemini-cli.git"
|
||||
},
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.45.0-nightly.20260521.g854f811be"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.45.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env NODE_ENV=development node scripts/start.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.45.0-nightly.20260521.g854f811be",
|
||||
"version": "0.45.2",
|
||||
"description": "Gemini CLI A2A Server",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -21,9 +21,12 @@ import {
|
||||
// Tracking bug: https://github.com/microsoft/node-pty/issues/827
|
||||
process.on('uncaughtException', (error) => {
|
||||
if (error instanceof Error) {
|
||||
const message = error.message || '';
|
||||
const isPtyResizeError =
|
||||
error.message === 'Cannot resize a pty that has already exited';
|
||||
const isEbadfError = error.message.includes('EBADF');
|
||||
message === 'Cannot resize a pty that has already exited';
|
||||
const isEbadfError =
|
||||
message.includes('EBADF') ||
|
||||
(error as { code?: string }).code === 'EBADF';
|
||||
const isFromNodePty =
|
||||
error.stack?.includes('node-pty') || error.stack?.includes('PtyResize');
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.45.0-nightly.20260521.g854f811be",
|
||||
"version": "0.45.2",
|
||||
"description": "Gemini CLI",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
@@ -27,7 +27,7 @@
|
||||
"dist"
|
||||
],
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.45.0-nightly.20260521.g854f811be"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.45.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.16.1",
|
||||
|
||||
@@ -265,6 +265,7 @@ export function buildAvailableModels(
|
||||
const preferredModel = config.getModel() || GEMINI_MODEL_ALIAS_AUTO;
|
||||
const shouldShowPreviewModels = config.getHasAccessToPreviewModel();
|
||||
const useGemini31 = config.getGemini31LaunchedSync?.() ?? false;
|
||||
const useGemini3_5Flash = config.hasGemini35FlashGAAccess?.() ?? false;
|
||||
const selectedAuthType = settings.merged.security.auth.selectedType;
|
||||
const useCustomToolModel =
|
||||
useGemini31 && selectedAuthType === AuthType.USE_GEMINI;
|
||||
@@ -276,6 +277,7 @@ export function buildAvailableModels(
|
||||
) {
|
||||
const options = config.getModelConfigService().getAvailableModelOptions({
|
||||
useGemini3_1: useGemini31,
|
||||
useGemini3_5Flash,
|
||||
useCustomTools: useCustomToolModel,
|
||||
hasAccessToPreview: shouldShowPreviewModels,
|
||||
});
|
||||
@@ -294,6 +296,7 @@ export function buildAvailableModels(
|
||||
description: getAutoModelDescription(
|
||||
shouldShowPreviewModels,
|
||||
useGemini31,
|
||||
useGemini3_5Flash,
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -12,7 +12,12 @@ import { MessageType } from '../types.js';
|
||||
|
||||
describe('helpCommand', () => {
|
||||
let mockContext: CommandContext;
|
||||
const originalEnv = { ...process.env };
|
||||
const originalPlatform = process.platform;
|
||||
const action = helpCommand.action;
|
||||
|
||||
if (!action) {
|
||||
throw new Error('Help command has no action');
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockContext = createMockCommandContext({
|
||||
@@ -23,16 +28,13 @@ describe('helpCommand', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
||||
vi.unstubAllEnvs();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should add a help message to the UI history', async () => {
|
||||
if (!helpCommand.action) {
|
||||
throw new Error('Help command has no action');
|
||||
}
|
||||
|
||||
await helpCommand.action(mockContext, '');
|
||||
it('should add a help message to the UI history by default', async () => {
|
||||
await action(mockContext, '');
|
||||
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -47,4 +49,85 @@ describe('helpCommand', () => {
|
||||
expect(helpCommand.kind).toBe(CommandKind.BUILT_IN);
|
||||
expect(helpCommand.description).toBe('For help on gemini-cli');
|
||||
});
|
||||
|
||||
describe('Antigravity installer commands help', () => {
|
||||
it('should output macOS installation command on darwin platform', async () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
||||
|
||||
await action(mockContext, 'install antigravity cli');
|
||||
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageType.INFO,
|
||||
text: `To install the Antigravity CLI on macOS, run the following command:\n\n'curl -fsSL https://antigravity.google/cli/install.sh | bash'`,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should output Linux installation command on linux platform', async () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'linux' });
|
||||
|
||||
await action(mockContext, 'how do I install antigravity CLI');
|
||||
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageType.INFO,
|
||||
text: `To install the Antigravity CLI on Linux, run the following command:\n\n'curl -fsSL https://antigravity.google/cli/install.sh | bash'`,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should output Windows PowerShell installation command on win32 when PSModulePath is set', async () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
vi.stubEnv('PSModulePath', 'C:\\some\\path');
|
||||
|
||||
await action(mockContext, 'how do I migrate to antigravity CLI');
|
||||
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageType.INFO,
|
||||
text: `To install the Antigravity CLI on Windows (PowerShell), run the following command:\n\n'irm https://antigravity.google/cli/install.ps1 | iex'`,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should output Windows CMD installation command on win32 when PSModulePath is not set', async () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
vi.stubEnv('PSModulePath', '');
|
||||
|
||||
await action(mockContext, 'install antigravity cli');
|
||||
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageType.INFO,
|
||||
text: `To install the Antigravity CLI on Windows (Command Prompt), run the following command:\n\n'curl -fsSL https://antigravity.google/cli/install.cmd -o install.cmd && install.cmd && del install.cmd'`,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should learn more message on unsupported platform', async () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'freebsd' });
|
||||
|
||||
await action(mockContext, 'install antigravity cli');
|
||||
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageType.INFO,
|
||||
text: 'Learn more about Antigravity CLI at https://antigravity.google/docs/cli-getting-started',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should fall back to default help if query does not contain install or migrate', async () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
||||
|
||||
await action(mockContext, 'antigravity cli');
|
||||
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageType.HELP,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,13 +6,36 @@
|
||||
|
||||
import { CommandKind, type SlashCommand } from './types.js';
|
||||
import { MessageType, type HistoryItemHelp } from '../types.js';
|
||||
import { getAntigravityInstallInfo } from '../utils/antigravityUtils.js';
|
||||
|
||||
export const helpCommand: SlashCommand = {
|
||||
name: 'help',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
description: 'For help on gemini-cli',
|
||||
autoExecute: true,
|
||||
action: async (context) => {
|
||||
action: async (context, args) => {
|
||||
const lowerArgs = args?.toLowerCase() || '';
|
||||
const hasAntigravity = lowerArgs.includes('antigravity');
|
||||
const hasInstallOrMigrate =
|
||||
lowerArgs.includes('install') || lowerArgs.includes('migrate');
|
||||
|
||||
if (hasAntigravity && hasInstallOrMigrate) {
|
||||
const info = getAntigravityInstallInfo();
|
||||
|
||||
if (info) {
|
||||
context.ui.addItem({
|
||||
type: MessageType.INFO,
|
||||
text: `To install the Antigravity CLI on ${info.platformName}, run the following command:\n\n'${info.installCmd}'`,
|
||||
});
|
||||
} else {
|
||||
context.ui.addItem({
|
||||
type: MessageType.INFO,
|
||||
text: `Learn more about Antigravity CLI at https://antigravity.google/docs/cli-getting-started`,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const helpItem: Omit<HistoryItemHelp, 'id'> = {
|
||||
type: MessageType.HELP,
|
||||
timestamp: new Date(),
|
||||
|
||||
@@ -67,6 +67,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
|
||||
const shouldShowPreviewModels = config?.getHasAccessToPreviewModel() ?? false;
|
||||
const useGemini31 = config?.getGemini31LaunchedSync?.() ?? false;
|
||||
const useGemini3_5Flash = config?.hasGemini35FlashGAAccess?.() ?? false;
|
||||
const selectedAuthType = settings.merged.security.auth.selectedType;
|
||||
const useCustomToolModel =
|
||||
useGemini31 && selectedAuthType === AuthType.USE_GEMINI;
|
||||
@@ -129,6 +130,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
.getModelConfigService()
|
||||
.getAvailableModelOptions({
|
||||
useGemini3_1: useGemini31,
|
||||
useGemini3_5Flash,
|
||||
useCustomTools: useCustomToolModel,
|
||||
hasAccessToPreview: shouldShowPreviewModels,
|
||||
hasAccessToProModel,
|
||||
@@ -162,6 +164,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
description: getAutoModelDescription(
|
||||
shouldShowPreviewModels,
|
||||
useGemini31,
|
||||
useGemini3_5Flash,
|
||||
),
|
||||
key: GEMINI_MODEL_ALIAS_AUTO,
|
||||
},
|
||||
@@ -181,6 +184,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
shouldShowPreviewModels,
|
||||
manualModelSelected,
|
||||
useGemini31,
|
||||
useGemini3_5Flash,
|
||||
useCustomToolModel,
|
||||
hasAccessToProModel,
|
||||
]);
|
||||
@@ -195,6 +199,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
.getModelConfigService()
|
||||
.getAvailableModelOptions({
|
||||
useGemini3_1: useGemini31,
|
||||
useGemini3_5Flash,
|
||||
useCustomTools: useCustomToolModel,
|
||||
hasAccessToPreview: shouldShowPreviewModels,
|
||||
hasAccessToProModel,
|
||||
@@ -287,6 +292,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
}, [
|
||||
shouldShowPreviewModels,
|
||||
useGemini31,
|
||||
useGemini3_5Flash,
|
||||
useCustomToolModel,
|
||||
hasAccessToProModel,
|
||||
config,
|
||||
|
||||
@@ -353,6 +353,49 @@ describe('<ModelStatsDisplay />', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should resolve gemini-3-flash to gemini-3.5-flash via getDisplayString', async () => {
|
||||
const { lastFrame, unmount } = await renderWithMockedStats({
|
||||
models: {
|
||||
'gemini-3-flash': {
|
||||
api: { totalRequests: 1, totalErrors: 0, totalLatencyMs: 100 },
|
||||
tokens: {
|
||||
input: 5,
|
||||
prompt: 10,
|
||||
candidates: 20,
|
||||
total: 30,
|
||||
cached: 5,
|
||||
thoughts: 2,
|
||||
tool: 1,
|
||||
},
|
||||
roles: {},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
totalCalls: 0,
|
||||
totalSuccess: 0,
|
||||
totalFail: 0,
|
||||
totalDurationMs: 0,
|
||||
totalDecisions: {
|
||||
accept: 0,
|
||||
reject: 0,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
byName: {},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 0,
|
||||
totalLinesRemoved: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('gemini-3.5-flash');
|
||||
expect(output).not.toContain('gemini-3-flash');
|
||||
expect(output).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should handle models with long names (gemini-3-*-preview) without layout breaking', async () => {
|
||||
const { lastFrame, unmount } = await renderWithMockedStats(
|
||||
{
|
||||
|
||||
@@ -299,7 +299,7 @@ export const ModelStatsDisplay: React.FC<ModelStatsDisplayProps> = ({
|
||||
},
|
||||
...modelNames.map((name) => ({
|
||||
key: name,
|
||||
header: name,
|
||||
header: getDisplayString(name),
|
||||
flexGrow: 1,
|
||||
renderCell: (row: StatRowData) => {
|
||||
// Don't render anything for section headers in model columns
|
||||
|
||||
@@ -131,6 +131,33 @@ describe('<StatsDisplay />', () => {
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('resolves gemini-3-flash to gemini-3.5-flash in the model usage table', async () => {
|
||||
const metrics = createTestMetrics({
|
||||
models: {
|
||||
'gemini-3-flash': {
|
||||
api: { totalRequests: 5, totalErrors: 0, totalLatencyMs: 3000 },
|
||||
tokens: {
|
||||
input: 1000,
|
||||
prompt: 2000,
|
||||
candidates: 3000,
|
||||
total: 5000,
|
||||
cached: 500,
|
||||
thoughts: 100,
|
||||
tool: 50,
|
||||
},
|
||||
roles: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { lastFrame } = await renderWithMockedStats(metrics);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('gemini-3.5-flash');
|
||||
expect(output).not.toContain('gemini-3-flash\u0020'); // Avoid matching parts of substrings if not intended
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders role breakdown correctly under models', async () => {
|
||||
const metrics = createTestMetrics({
|
||||
models: {
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
import { computeSessionStats } from '../utils/computeStats.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import type { QuotaStats } from '../types.js';
|
||||
import { LlmRole } from '@google/gemini-cli-core';
|
||||
import { LlmRole, getDisplayString } from '@google/gemini-cli-core';
|
||||
|
||||
// A more flexible and powerful StatRow component
|
||||
interface StatRowProps {
|
||||
@@ -101,7 +101,7 @@ const ModelUsageTable: React.FC<ModelUsageTableProps> = ({ models }) => {
|
||||
Object.entries(models).forEach(([name, metrics]) => {
|
||||
rows.push({
|
||||
name,
|
||||
displayName: name,
|
||||
displayName: getDisplayString(name),
|
||||
requests: metrics.api.totalRequests,
|
||||
cachedTokens: metrics.tokens.cached.toLocaleString(),
|
||||
inputTokens: metrics.tokens.prompt.toLocaleString(),
|
||||
|
||||
@@ -215,3 +215,26 @@ exports[`<ModelStatsDisplay /> > should render "no API calls" message when there
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ModelStatsDisplay /> > should resolve gemini-3-flash to gemini-3.5-flash via getDisplayString 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ Model Stats For Nerds │
|
||||
│ │
|
||||
│ │
|
||||
│ Metric gemini-3.5-flash │
|
||||
│ ────────────────────────────────────────────────────────────────────────────────────────────── │
|
||||
│ API │
|
||||
│ Requests 1 │
|
||||
│ Errors 0 (0.0%) │
|
||||
│ Avg Latency 100ms │
|
||||
│ Tokens │
|
||||
│ Total 30 │
|
||||
│ ↳ Input 5 │
|
||||
│ ↳ Cache Reads 5 (50.0%) │
|
||||
│ ↳ Thoughts 2 │
|
||||
│ ↳ Tool 1 │
|
||||
│ ↳ Output 20 │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -292,3 +292,30 @@ exports[`<StatsDisplay /> > renders role breakdown correctly under models 1`] =
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<StatsDisplay /> > resolves gemini-3-flash to gemini-3.5-flash in the model usage table 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ Session Stats │
|
||||
│ │
|
||||
│ Interaction Summary │
|
||||
│ Session ID: test-session-id │
|
||||
│ Tool Calls: 0 ( ✓ 0 x 0 ) │
|
||||
│ Success Rate: 0.0% │
|
||||
│ │
|
||||
│ Performance │
|
||||
│ Wall Time: 1s │
|
||||
│ Agent Active: 3.0s │
|
||||
│ » API Time: 3.0s (100.0%) │
|
||||
│ » Tool Time: 0s (0.0%) │
|
||||
│ │
|
||||
│ │
|
||||
│ Model Usage │
|
||||
│ Use /model to view model quota information │
|
||||
│ │
|
||||
│ Model Reqs Input Tokens Cache Reads Output Tokens │
|
||||
│ ────────────────────────────────────────────────────────────────────────────────────────────── │
|
||||
│ gemini-3.5-flash 5 2,000 500 3,000 │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -10,12 +10,14 @@ import {
|
||||
expect,
|
||||
vi,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
type MockedFunction,
|
||||
} from 'vitest';
|
||||
import { renderHook } from '../../test-utils/render.js';
|
||||
import { useBanner, _clearSessionBannersForTest } from './useBanner.js';
|
||||
import { persistentState } from '../../utils/persistentState.js';
|
||||
import crypto from 'node:crypto';
|
||||
import chalk from 'chalk';
|
||||
|
||||
vi.mock('../../utils/persistentState.js', () => ({
|
||||
persistentState: {
|
||||
@@ -77,10 +79,26 @@ describe('useBanner', () => {
|
||||
.update(defaultBannerData.defaultText)
|
||||
.digest('hex')]: 5,
|
||||
});
|
||||
});
|
||||
|
||||
const { result } = await renderHook(() => useBanner(defaultBannerData));
|
||||
it('should not hide banner if show count exceeds max limit (Legacy format) if it contains an Antigravity announcement', async () => {
|
||||
const antigravityBannerData = {
|
||||
defaultText: 'Antigravity is coming to town!',
|
||||
warningText: '',
|
||||
};
|
||||
|
||||
expect(result.current.bannerText).toBe('');
|
||||
mockedPersistentStateGet.mockReturnValue({
|
||||
[crypto
|
||||
.createHash('sha256')
|
||||
.update(antigravityBannerData.defaultText)
|
||||
.digest('hex')]: 5,
|
||||
});
|
||||
|
||||
const { result } = await renderHook(() => useBanner(antigravityBannerData));
|
||||
|
||||
expect(result.current.bannerText).toContain(
|
||||
'Antigravity is coming to town!',
|
||||
);
|
||||
});
|
||||
|
||||
it('should increment the persistent count when banner is shown', async () => {
|
||||
@@ -123,4 +141,77 @@ describe('useBanner', () => {
|
||||
|
||||
expect(result.current.bannerText).toBe('Line1\nLine2');
|
||||
});
|
||||
|
||||
describe('Antigravity installation commands', () => {
|
||||
const originalPlatform = process.platform;
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('should append macOS & Linux install command when on darwin', async () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
||||
const data = { defaultText: 'Welcome to Antigravity!', warningText: '' };
|
||||
|
||||
const { result } = await renderHook(() => useBanner(data));
|
||||
|
||||
expect(result.current.bannerText).toBe(
|
||||
`Welcome to Antigravity!\n \nTo install run "${chalk.bold('curl -fsSL https://antigravity.google/cli/install.sh | bash')}"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should append macOS & Linux install command when on linux', async () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'linux' });
|
||||
const data = { defaultText: 'Welcome to Antigravity!', warningText: '' };
|
||||
|
||||
const { result } = await renderHook(() => useBanner(data));
|
||||
|
||||
expect(result.current.bannerText).toBe(
|
||||
`Welcome to Antigravity!\n \nTo install run "${chalk.bold('curl -fsSL https://antigravity.google/cli/install.sh | bash')}"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should append Windows PowerShell install command when on win32 and PSModulePath is set', async () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
vi.stubEnv('PSModulePath', 'C:\\some\\path');
|
||||
const data = { defaultText: 'Welcome to Antigravity!', warningText: '' };
|
||||
|
||||
const { result } = await renderHook(() => useBanner(data));
|
||||
|
||||
expect(result.current.bannerText).toBe(
|
||||
`Welcome to Antigravity!\n \nTo install run "${chalk.bold('irm https://antigravity.google/cli/install.ps1 | iex')}"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should append Windows CMD install command when on win32 and PSModulePath is not set', async () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
vi.stubEnv('PSModulePath', '');
|
||||
const data = { defaultText: 'Welcome to Antigravity!', warningText: '' };
|
||||
|
||||
const { result } = await renderHook(() => useBanner(data));
|
||||
|
||||
expect(result.current.bannerText).toBe(
|
||||
`Welcome to Antigravity!\n \nTo install run "${chalk.bold('curl -fsSL https://antigravity.google/cli/install.cmd -o install.cmd && install.cmd && del install.cmd')}"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not append install command if banner text does not contain Antigravity', async () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
||||
const data = { defaultText: 'Regular Banner', warningText: '' };
|
||||
|
||||
const { result } = await renderHook(() => useBanner(data));
|
||||
|
||||
expect(result.current.bannerText).toBe('Regular Banner');
|
||||
});
|
||||
|
||||
it('should not append install command if process.platform is an unsupported platform', async () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'freebsd' });
|
||||
const data = { defaultText: 'Welcome to Antigravity!', warningText: '' };
|
||||
|
||||
const { result } = await renderHook(() => useBanner(data));
|
||||
|
||||
expect(result.current.bannerText).toBe('Welcome to Antigravity!');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { persistentState } from '../../utils/persistentState.js';
|
||||
import crypto from 'node:crypto';
|
||||
import chalk from 'chalk';
|
||||
import { getAntigravityInstallInfo } from '../utils/antigravityUtils.js';
|
||||
|
||||
const DEFAULT_MAX_BANNER_SHOWN_COUNT = 5;
|
||||
|
||||
@@ -41,10 +43,19 @@ export function useBanner(bannerData: BannerData) {
|
||||
const currentBannerCount = bannerCounts[hashedText] || 0;
|
||||
|
||||
const showBanner =
|
||||
activeText !== '' && currentBannerCount < DEFAULT_MAX_BANNER_SHOWN_COUNT;
|
||||
activeText !== '' &&
|
||||
(currentBannerCount < DEFAULT_MAX_BANNER_SHOWN_COUNT ||
|
||||
activeText.includes('Antigravity'));
|
||||
|
||||
const rawBannerText = showBanner ? activeText : '';
|
||||
const bannerText = rawBannerText.replace(/\\n/g, '\n');
|
||||
let bannerText = rawBannerText.replace(/\\n/g, '\n');
|
||||
|
||||
if (showBanner && activeText.includes('Antigravity')) {
|
||||
const info = getAntigravityInstallInfo();
|
||||
if (info) {
|
||||
bannerText += `\n \nTo install run "${chalk.bold(info.installCmd)}"`;
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (showBanner && activeText) {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { getAntigravityInstallInfo } from './antigravityUtils.js';
|
||||
|
||||
describe('antigravityUtils', () => {
|
||||
const originalPlatform = process.platform;
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('should return macOS installation info on darwin platform', () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
||||
|
||||
const info = getAntigravityInstallInfo();
|
||||
|
||||
expect(info).toEqual({
|
||||
platformName: 'macOS',
|
||||
installCmd: 'curl -fsSL https://antigravity.google/cli/install.sh | bash',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return Linux installation info on linux platform', () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'linux' });
|
||||
|
||||
const info = getAntigravityInstallInfo();
|
||||
|
||||
expect(info).toEqual({
|
||||
platformName: 'Linux',
|
||||
installCmd: 'curl -fsSL https://antigravity.google/cli/install.sh | bash',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return Windows PowerShell installation info on win32 when PSModulePath is set', () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
vi.stubEnv('PSModulePath', 'C:\\some\\path');
|
||||
|
||||
const info = getAntigravityInstallInfo();
|
||||
|
||||
expect(info).toEqual({
|
||||
platformName: 'Windows (PowerShell)',
|
||||
installCmd: 'irm https://antigravity.google/cli/install.ps1 | iex',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return Windows CMD installation info on win32 when PSModulePath is not set', () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
vi.stubEnv('PSModulePath', '');
|
||||
|
||||
const info = getAntigravityInstallInfo();
|
||||
|
||||
expect(info).toEqual({
|
||||
platformName: 'Windows (Command Prompt)',
|
||||
installCmd:
|
||||
'curl -fsSL https://antigravity.google/cli/install.cmd -o install.cmd && install.cmd && del install.cmd',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null on unsupported platform', () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'freebsd' });
|
||||
|
||||
const info = getAntigravityInstallInfo();
|
||||
|
||||
expect(info).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import process from 'node:process';
|
||||
|
||||
const ANTIGRAVITY_SH_INSTALL =
|
||||
'curl -fsSL https://antigravity.google/cli/install.sh | bash';
|
||||
|
||||
export interface AntigravityInstallInfo {
|
||||
platformName: string;
|
||||
installCmd: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the platform-specific installation details for the Antigravity CLI.
|
||||
* Returns null if the current platform is unsupported.
|
||||
*/
|
||||
export function getAntigravityInstallInfo(): AntigravityInstallInfo | null {
|
||||
if (process.platform === 'win32') {
|
||||
if (process.env['PSModulePath']) {
|
||||
return {
|
||||
platformName: 'Windows (PowerShell)',
|
||||
installCmd: 'irm https://antigravity.google/cli/install.ps1 | iex',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
platformName: 'Windows (Command Prompt)',
|
||||
installCmd:
|
||||
'curl -fsSL https://antigravity.google/cli/install.cmd -o install.cmd && install.cmd && del install.cmd',
|
||||
};
|
||||
}
|
||||
} else if (process.platform === 'darwin') {
|
||||
return {
|
||||
platformName: 'macOS',
|
||||
installCmd: ANTIGRAVITY_SH_INSTALL,
|
||||
};
|
||||
} else if (process.platform === 'linux') {
|
||||
return {
|
||||
platformName: 'Linux',
|
||||
installCmd: ANTIGRAVITY_SH_INSTALL,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.45.0-nightly.20260521.g854f811be",
|
||||
"version": "0.45.2",
|
||||
"description": "Gemini CLI Core",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface ModelPolicyOptions {
|
||||
useGemini31?: boolean;
|
||||
useGemini31FlashLite?: boolean;
|
||||
useCustomToolModel?: boolean;
|
||||
useGemini3_5Flash?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_ACTIONS: ModelPolicyActionMap = {
|
||||
@@ -94,6 +95,9 @@ export function getModelPolicyChain(
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
options.useGemini31,
|
||||
options.useCustomToolModel,
|
||||
true,
|
||||
undefined,
|
||||
options.useGemini3_5Flash,
|
||||
);
|
||||
return [
|
||||
definePolicy({
|
||||
|
||||
@@ -54,6 +54,7 @@ export function resolvePolicyChain(
|
||||
const useGemini31 = config.getGemini31LaunchedSync?.() ?? false;
|
||||
const useCustomToolModel = config.getUseCustomToolModelSync?.() ?? false;
|
||||
const hasAccessToPreview = config.getHasAccessToPreviewModel?.() ?? false;
|
||||
const useGemini3_5Flash = config.hasGemini35FlashGAAccess?.() ?? false;
|
||||
|
||||
// Capture the original family intent before any normalization or early downgrade.
|
||||
const isOriginallyGemini3 = isGemini3Model(modelFromConfig, config);
|
||||
@@ -65,6 +66,7 @@ export function resolvePolicyChain(
|
||||
useCustomToolModel,
|
||||
hasAccessToPreview,
|
||||
config,
|
||||
useGemini3_5Flash,
|
||||
),
|
||||
);
|
||||
const isAutoPreferred = normalizedPreferredModel
|
||||
@@ -82,6 +84,7 @@ export function resolvePolicyChain(
|
||||
const context = {
|
||||
useGemini3_1: useGemini31,
|
||||
useCustomTools: useCustomToolModel,
|
||||
useGemini3_5Flash,
|
||||
};
|
||||
|
||||
if (resolvedModel === DEFAULT_GEMINI_FLASH_LITE_MODEL) {
|
||||
@@ -136,6 +139,7 @@ export function resolvePolicyChain(
|
||||
userTier: config.getUserTier(),
|
||||
useGemini31,
|
||||
useCustomToolModel,
|
||||
useGemini3_5Flash,
|
||||
});
|
||||
} else {
|
||||
// User requested Gemini 3 but has no access. Proactively downgrade
|
||||
@@ -146,6 +150,7 @@ export function resolvePolicyChain(
|
||||
userTier: config.getUserTier(),
|
||||
useGemini31,
|
||||
useCustomToolModel,
|
||||
useGemini3_5Flash,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from './codeAssist.js';
|
||||
import type { Config } from '../config/config.js';
|
||||
import { LoggingContentGenerator } from '../core/loggingContentGenerator.js';
|
||||
import { ModelMappingContentGenerator } from '../core/modelMappingContentGenerator.js';
|
||||
import { UserTierId } from './types.js';
|
||||
|
||||
// Mock dependencies
|
||||
@@ -22,11 +23,15 @@ vi.mock('./oauth2.js');
|
||||
vi.mock('./setup.js');
|
||||
vi.mock('./server.js');
|
||||
vi.mock('../core/loggingContentGenerator.js');
|
||||
vi.mock('../core/modelMappingContentGenerator.js');
|
||||
|
||||
const mockedGetOauthClient = vi.mocked(getOauthClient);
|
||||
const mockedSetupUser = vi.mocked(setupUser);
|
||||
const MockedCodeAssistServer = vi.mocked(CodeAssistServer);
|
||||
const MockedLoggingContentGenerator = vi.mocked(LoggingContentGenerator);
|
||||
const MockedModelMappingContentGenerator = vi.mocked(
|
||||
ModelMappingContentGenerator,
|
||||
);
|
||||
|
||||
describe('codeAssist', () => {
|
||||
beforeEach(() => {
|
||||
@@ -178,5 +183,47 @@ describe('codeAssist', () => {
|
||||
const server = getCodeAssistServer(mockConfig);
|
||||
expect(server).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should unwrap and return the server if it is wrapped in a ModelMappingContentGenerator', () => {
|
||||
const mockServer = new MockedCodeAssistServer({} as never, '', {});
|
||||
const mockMapper = new MockedModelMappingContentGenerator(
|
||||
{} as never,
|
||||
{},
|
||||
);
|
||||
vi.spyOn(mockMapper, 'getWrapped').mockReturnValue(mockServer);
|
||||
|
||||
const mockConfig = {
|
||||
getContentGenerator: () => mockMapper,
|
||||
} as unknown as Config;
|
||||
|
||||
const server = getCodeAssistServer(mockConfig);
|
||||
expect(server).toBe(mockServer);
|
||||
expect(mockMapper.getWrapped).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should recursively unwrap multiple layers of LoggingContentGenerator and ModelMappingContentGenerator', () => {
|
||||
const mockServer = new MockedCodeAssistServer({} as never, '', {});
|
||||
const mockLogger = new MockedLoggingContentGenerator(
|
||||
{} as never,
|
||||
{} as never,
|
||||
);
|
||||
const mockMapper = new MockedModelMappingContentGenerator(
|
||||
{} as never,
|
||||
{},
|
||||
);
|
||||
|
||||
// Mapper wraps Logger wraps Server
|
||||
vi.spyOn(mockMapper, 'getWrapped').mockReturnValue(mockLogger);
|
||||
vi.spyOn(mockLogger, 'getWrapped').mockReturnValue(mockServer);
|
||||
|
||||
const mockConfig = {
|
||||
getContentGenerator: () => mockMapper,
|
||||
} as unknown as Config;
|
||||
|
||||
const server = getCodeAssistServer(mockConfig);
|
||||
expect(server).toBe(mockServer);
|
||||
expect(mockMapper.getWrapped).toHaveBeenCalled();
|
||||
expect(mockLogger.getWrapped).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import { setupUser } from './setup.js';
|
||||
import { CodeAssistServer, type HttpOptions } from './server.js';
|
||||
import type { Config } from '../config/config.js';
|
||||
import { LoggingContentGenerator } from '../core/loggingContentGenerator.js';
|
||||
import { ModelMappingContentGenerator } from '../core/modelMappingContentGenerator.js';
|
||||
|
||||
export async function createCodeAssistContentGenerator(
|
||||
httpOptions: HttpOptions,
|
||||
@@ -43,9 +44,15 @@ export function getCodeAssistServer(
|
||||
): CodeAssistServer | undefined {
|
||||
let server = config.getContentGenerator();
|
||||
|
||||
// Unwrap LoggingContentGenerator if present
|
||||
if (server instanceof LoggingContentGenerator) {
|
||||
server = server.getWrapped();
|
||||
// Recursively unwrap LoggingContentGenerator and ModelMappingContentGenerator
|
||||
while (true) {
|
||||
if (server instanceof LoggingContentGenerator) {
|
||||
server = server.getWrapped();
|
||||
} else if (server instanceof ModelMappingContentGenerator) {
|
||||
server = server.getWrapped();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!(server instanceof CodeAssistServer)) {
|
||||
|
||||
@@ -19,6 +19,7 @@ export const ExperimentFlags = {
|
||||
GEMINI_3_1_PRO_LAUNCHED: 45760185,
|
||||
PRO_MODEL_NO_ACCESS: 45768879,
|
||||
DEFAULT_REQUEST_TIMEOUT: 45773134,
|
||||
GEMINI_3_5_FLASH_GA_LAUNCHED: 45780819,
|
||||
} as const;
|
||||
|
||||
export type ExperimentFlagName =
|
||||
|
||||
@@ -69,6 +69,7 @@ import {
|
||||
DEFAULT_GEMINI_MODEL_AUTO,
|
||||
PREVIEW_GEMINI_MODEL_AUTO,
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
} from './models.js';
|
||||
import { Storage } from './storage.js';
|
||||
import type { AgentLoopContext } from './agent-loop-context.js';
|
||||
@@ -4346,3 +4347,57 @@ describe('ADKSettings', () => {
|
||||
expect(config.getAgentSessionNoninteractiveEnabled()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasGemini35FlashGAAccess model setting', () => {
|
||||
const baseParams: ConfigParameters = {
|
||||
sessionId: 'test',
|
||||
targetDir: '.',
|
||||
debugMode: false,
|
||||
model: 'test-model',
|
||||
cwd: '.',
|
||||
};
|
||||
|
||||
it('should set DEFAULT_GEMINI_FLASH_MODEL to gemini-3.5-flash and PREVIEW_GEMINI_FLASH_MODEL to gemini-3-flash-preview if hasGemini35FlashGAAccess returns true and authType is USE_GEMINI', () => {
|
||||
const config = new Config(baseParams);
|
||||
config['contentGeneratorConfig'] = { authType: AuthType.USE_GEMINI };
|
||||
|
||||
// Set experiment to return true for GEMINI_3_5_FLASH_GA_LAUNCHED
|
||||
config.setExperiments({
|
||||
experimentIds: [],
|
||||
flags: {
|
||||
[ExperimentFlags.GEMINI_3_5_FLASH_GA_LAUNCHED]: {
|
||||
boolValue: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Call the method
|
||||
const result = config.hasGemini35FlashGAAccess();
|
||||
expect(result).toBe(true);
|
||||
|
||||
expect(DEFAULT_GEMINI_FLASH_MODEL).toBe('gemini-3.5-flash');
|
||||
expect(PREVIEW_GEMINI_FLASH_MODEL).toBe('gemini-3-flash-preview');
|
||||
});
|
||||
|
||||
it('should set DEFAULT_GEMINI_FLASH_MODEL and PREVIEW_GEMINI_FLASH_MODEL to gemini-3.5-flash if hasGemini35FlashGAAccess returns true and authType is not USE_GEMINI', () => {
|
||||
const config = new Config(baseParams);
|
||||
config['contentGeneratorConfig'] = { authType: AuthType.LOGIN_WITH_GOOGLE };
|
||||
|
||||
// Set experiment to return true for GEMINI_3_5_FLASH_GA_LAUNCHED
|
||||
config.setExperiments({
|
||||
experimentIds: [],
|
||||
flags: {
|
||||
[ExperimentFlags.GEMINI_3_5_FLASH_GA_LAUNCHED]: {
|
||||
boolValue: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Call the method
|
||||
const result = config.hasGemini35FlashGAAccess();
|
||||
expect(result).toBe(true);
|
||||
|
||||
expect(DEFAULT_GEMINI_FLASH_MODEL).toBe('gemini-3.5-flash');
|
||||
expect(PREVIEW_GEMINI_FLASH_MODEL).toBe('gemini-3.5-flash');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -86,6 +86,7 @@ import {
|
||||
isGemini2Model,
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
resolveModel,
|
||||
setFlashModels,
|
||||
} from './models.js';
|
||||
import { shouldAttemptBrowserLaunch } from '../utils/browser.js';
|
||||
import type { MCPOAuthConfig } from '../mcp/oauth-provider.js';
|
||||
@@ -2054,6 +2055,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
this.getUseCustomToolModelSync(),
|
||||
this.getHasAccessToPreviewModel(),
|
||||
this,
|
||||
this.hasGemini35FlashGAAccess(),
|
||||
);
|
||||
|
||||
const isPreview = isPreviewModel(primaryModel, this);
|
||||
@@ -2093,6 +2095,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
this.getUseCustomToolModelSync(),
|
||||
this.getHasAccessToPreviewModel(),
|
||||
this,
|
||||
this.hasGemini35FlashGAAccess(),
|
||||
);
|
||||
return this.modelQuotas.get(primaryModel)?.remaining;
|
||||
}
|
||||
@@ -2108,6 +2111,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
this.getUseCustomToolModelSync(),
|
||||
this.getHasAccessToPreviewModel(),
|
||||
this,
|
||||
this.hasGemini35FlashGAAccess(),
|
||||
);
|
||||
return this.modelQuotas.get(primaryModel)?.limit;
|
||||
}
|
||||
@@ -2123,6 +2127,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
this.getUseCustomToolModelSync(),
|
||||
this.getHasAccessToPreviewModel(),
|
||||
this,
|
||||
this.hasGemini35FlashGAAccess(),
|
||||
);
|
||||
return this.modelQuotas.get(primaryModel)?.resetTime;
|
||||
}
|
||||
@@ -3537,6 +3542,38 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether Gemini 3.5 Flash GA has been launched.
|
||||
*
|
||||
* Note: This method should only be called after startup, once experiments have been loaded.
|
||||
*/
|
||||
hasGemini35FlashGAAccess(): boolean {
|
||||
const authType = this.contentGeneratorConfig?.authType;
|
||||
const hasAccess = (() => {
|
||||
if (this.isGemini31LaunchedForAuthType(authType)) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
this.experiments?.flags[ExperimentFlags.GEMINI_3_5_FLASH_GA_LAUNCHED]
|
||||
?.boolValue ?? false
|
||||
);
|
||||
})();
|
||||
// Used to set default flash models based on access
|
||||
// TODO: Remove once the experiment for 3_5 flash rollut can be cleaned up.
|
||||
if (hasAccess) {
|
||||
// Gemini API key users should have the ability to manually select the
|
||||
// old preview flash model.
|
||||
if (authType === AuthType.USE_GEMINI) {
|
||||
setFlashModels('gemini-3-flash-preview', 'gemini-3.5-flash');
|
||||
} else {
|
||||
setFlashModels('gemini-3.5-flash', 'gemini-3.5-flash');
|
||||
}
|
||||
} else {
|
||||
setFlashModels('gemini-3-flash-preview', 'gemini-2.5-flash');
|
||||
}
|
||||
return hasAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether Gemini 3.1 has been launched.
|
||||
*
|
||||
|
||||
@@ -113,6 +113,12 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
model: 'gemini-3.1-flash-lite',
|
||||
},
|
||||
},
|
||||
'gemini-3.5-flash': {
|
||||
extends: 'chat-base-3',
|
||||
modelConfig: {
|
||||
model: 'gemini-3.5-flash',
|
||||
},
|
||||
},
|
||||
'gemma-4-31b-it': {
|
||||
extends: 'chat-base-3',
|
||||
modelConfig: {
|
||||
@@ -139,6 +145,12 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
model: 'gemini-3-flash-preview',
|
||||
},
|
||||
},
|
||||
'gemini-3.5-flash-base': {
|
||||
extends: 'base',
|
||||
modelConfig: {
|
||||
model: 'gemini-3.5-flash',
|
||||
},
|
||||
},
|
||||
classifier: {
|
||||
extends: 'base',
|
||||
modelConfig: {
|
||||
@@ -346,6 +358,13 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
isVisible: true,
|
||||
features: { thinking: false, multimodalToolUse: true },
|
||||
},
|
||||
'gemini-3.5-flash': {
|
||||
tier: 'flash',
|
||||
family: 'gemini-3',
|
||||
isPreview: false,
|
||||
isVisible: true,
|
||||
features: { thinking: false, multimodalToolUse: true },
|
||||
},
|
||||
'gemini-2.5-pro': {
|
||||
tier: 'pro',
|
||||
family: 'gemini-2.5',
|
||||
@@ -451,11 +470,34 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
default: 'gemini-3-flash-preview',
|
||||
contexts: [
|
||||
{
|
||||
condition: { hasAccessToPreview: false },
|
||||
condition: { hasAccessToPreview: false, useGemini3_5Flash: true },
|
||||
target: 'gemini-3.5-flash',
|
||||
},
|
||||
{
|
||||
condition: { hasAccessToPreview: false, useGemini3_5Flash: false },
|
||||
target: 'gemini-2.5-flash',
|
||||
},
|
||||
],
|
||||
},
|
||||
'gemini-3.5-flash': {
|
||||
default: 'gemini-3.5-flash',
|
||||
contexts: [
|
||||
{
|
||||
condition: { useGemini3_5Flash: false, hasAccessToPreview: false },
|
||||
target: 'gemini-2.5-flash',
|
||||
},
|
||||
{
|
||||
condition: { useGemini3_5Flash: false },
|
||||
target: 'gemini-3-flash-preview',
|
||||
},
|
||||
],
|
||||
},
|
||||
'gemini-2.5-flash': {
|
||||
default: 'gemini-2.5-flash',
|
||||
contexts: [
|
||||
{ condition: { useGemini3_5Flash: true }, target: 'gemini-3.5-flash' },
|
||||
],
|
||||
},
|
||||
'gemini-3-pro-preview': {
|
||||
default: 'gemini-3-pro-preview',
|
||||
contexts: [
|
||||
@@ -504,6 +546,7 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
flash: {
|
||||
default: 'gemini-3-flash-preview',
|
||||
contexts: [
|
||||
{ condition: { useGemini3_5Flash: true }, target: 'gemini-3.5-flash' },
|
||||
{
|
||||
condition: { hasAccessToPreview: false },
|
||||
target: 'gemini-2.5-flash',
|
||||
@@ -535,6 +578,7 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
flash: {
|
||||
default: 'gemini-3-flash-preview',
|
||||
contexts: [
|
||||
{ condition: { useGemini3_5Flash: true }, target: 'gemini-3.5-flash' },
|
||||
{
|
||||
condition: { hasAccessToPreview: false },
|
||||
target: 'gemini-2.5-flash',
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
DEFAULT_GEMINI_3_5_FLASH_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||
supportsMultimodalFunctionResponse,
|
||||
GEMINI_MODEL_ALIAS_PRO,
|
||||
@@ -744,4 +745,308 @@ describe('getAutoModelDescription', () => {
|
||||
expect(desc).toContain('gemini-3.1-pro-preview');
|
||||
expect(desc).toContain('gemini-3-flash-preview');
|
||||
});
|
||||
|
||||
it('should return Gemini 3.5 Flash description when hasAccessToPreview and useGemini3_5Flash are true', () => {
|
||||
const desc = getAutoModelDescription(true, true, true);
|
||||
expect(desc).toContain('gemini-3.1-pro-preview');
|
||||
expect(desc).toContain(DEFAULT_GEMINI_3_5_FLASH_MODEL);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveModel Gemini 3.5 Flash GA', () => {
|
||||
it('should resolve all but preview flash models to DEFAULT_GEMINI_FLASH_MODEL when useGemini3_5Flash is true (legacy)', () => {
|
||||
expect(
|
||||
resolveModel(
|
||||
GEMINI_MODEL_ALIAS_FLASH,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
undefined,
|
||||
true,
|
||||
),
|
||||
).toBe(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
expect(
|
||||
resolveModel(
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
undefined,
|
||||
true,
|
||||
),
|
||||
).toBe(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
expect(
|
||||
resolveModel(
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
undefined,
|
||||
true,
|
||||
),
|
||||
).toBe(PREVIEW_GEMINI_FLASH_MODEL);
|
||||
});
|
||||
|
||||
it('should resolve all but preview flash models to gemini-3.5-flash when useGemini3_5Flash is true (dynamic)', () => {
|
||||
const mockDynamicConfig = {
|
||||
getExperimentalDynamicModelConfiguration: () => true,
|
||||
modelConfigService,
|
||||
} as unknown as Config;
|
||||
|
||||
expect(
|
||||
resolveModel(
|
||||
GEMINI_MODEL_ALIAS_FLASH,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
mockDynamicConfig,
|
||||
true,
|
||||
),
|
||||
).toBe('gemini-3.5-flash');
|
||||
expect(
|
||||
resolveModel(
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
mockDynamicConfig,
|
||||
true,
|
||||
),
|
||||
).toBe('gemini-3.5-flash');
|
||||
expect(
|
||||
resolveModel(
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
mockDynamicConfig,
|
||||
true,
|
||||
),
|
||||
).toBe(PREVIEW_GEMINI_FLASH_MODEL);
|
||||
});
|
||||
|
||||
it('should NOT resolve flash models to DEFAULT_GEMINI_FLASH_MODEL when useGemini3_5Flash is false', () => {
|
||||
expect(
|
||||
resolveModel(
|
||||
GEMINI_MODEL_ALIAS_FLASH,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
undefined,
|
||||
false,
|
||||
),
|
||||
).toBe(PREVIEW_GEMINI_FLASH_MODEL);
|
||||
expect(
|
||||
resolveModel(
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
undefined,
|
||||
false,
|
||||
),
|
||||
).toBe(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
expect(
|
||||
resolveModel(
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
undefined,
|
||||
false,
|
||||
),
|
||||
).toBe(PREVIEW_GEMINI_FLASH_MODEL);
|
||||
});
|
||||
|
||||
it('should resolve to DEFAULT_GEMINI_FLASH_MODEL when GA is false AND preview access is false (dynamic)', () => {
|
||||
const mockDynamicConfig = {
|
||||
getExperimentalDynamicModelConfiguration: () => true,
|
||||
modelConfigService,
|
||||
} as unknown as Config;
|
||||
|
||||
expect(
|
||||
resolveModel(
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
false,
|
||||
false,
|
||||
false, // No preview access
|
||||
mockDynamicConfig,
|
||||
false, // GA false
|
||||
),
|
||||
).toBe('gemini-2.5-flash');
|
||||
});
|
||||
|
||||
it('should resolve auto to DEFAULT_GEMINI_FLASH_MODEL when useGemini3_5Flash is true and classifier selects flash', () => {
|
||||
expect(
|
||||
resolveClassifierModel(
|
||||
GEMINI_MODEL_ALIAS_AUTO,
|
||||
GEMINI_MODEL_ALIAS_FLASH,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
undefined,
|
||||
true,
|
||||
),
|
||||
).toBe(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
});
|
||||
|
||||
it('should resolve auto to gemini-3.5-flash when useGemini3_5Flash is true and classifier selects flash (dynamic)', () => {
|
||||
const mockDynamicConfig = {
|
||||
getExperimentalDynamicModelConfiguration: () => true,
|
||||
modelConfigService,
|
||||
} as unknown as Config;
|
||||
|
||||
expect(
|
||||
resolveClassifierModel(
|
||||
GEMINI_MODEL_ALIAS_AUTO,
|
||||
GEMINI_MODEL_ALIAS_FLASH,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
mockDynamicConfig,
|
||||
true,
|
||||
),
|
||||
).toBe('gemini-3.5-flash');
|
||||
});
|
||||
|
||||
describe('Flash model promotion and manual override routing logic', () => {
|
||||
it('should resolve flash alias to DEFAULT_GEMINI_FLASH_MODEL when useGemini3_5Flash is true (static)', () => {
|
||||
expect(
|
||||
resolveModel(
|
||||
GEMINI_MODEL_ALIAS_FLASH,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
undefined,
|
||||
true,
|
||||
),
|
||||
).toBe(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
});
|
||||
|
||||
it('should resolve flash alias to gemini-3.5-flash when useGemini3_5Flash is true (dynamic)', () => {
|
||||
const mockDynamicConfig = {
|
||||
getExperimentalDynamicModelConfiguration: () => true,
|
||||
modelConfigService,
|
||||
} as unknown as Config;
|
||||
|
||||
expect(
|
||||
resolveModel(
|
||||
GEMINI_MODEL_ALIAS_FLASH,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
mockDynamicConfig,
|
||||
true,
|
||||
),
|
||||
).toBe('gemini-3.5-flash');
|
||||
});
|
||||
|
||||
it('should resolve manual selection of gemini-3-flash-preview to gemini-3-flash-preview when useGemini3_5Flash is true and has preview access (static)', () => {
|
||||
expect(
|
||||
resolveModel(
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
undefined,
|
||||
true,
|
||||
),
|
||||
).toBe('gemini-3-flash-preview');
|
||||
});
|
||||
|
||||
it('should resolve manual selection of gemini-3-flash-preview to gemini-3-flash-preview when useGemini3_5Flash is true and has preview access (dynamic)', () => {
|
||||
const mockDynamicConfig = {
|
||||
getExperimentalDynamicModelConfiguration: () => true,
|
||||
modelConfigService,
|
||||
} as unknown as Config;
|
||||
|
||||
expect(
|
||||
resolveModel(
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
mockDynamicConfig,
|
||||
true,
|
||||
),
|
||||
).toBe('gemini-3-flash-preview');
|
||||
});
|
||||
|
||||
it('should resolve manual selection of gemini-3-flash-preview to DEFAULT_GEMINI_FLASH_MODEL when useGemini3_5Flash is true but lacks preview access (static)', () => {
|
||||
expect(
|
||||
resolveModel(
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
undefined,
|
||||
true,
|
||||
),
|
||||
).toBe(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
});
|
||||
|
||||
it('should resolve manual selection of gemini-3-flash-preview to gemini-3.5-flash when useGemini3_5Flash is true but lacks preview access (dynamic)', () => {
|
||||
const mockDynamicConfig = {
|
||||
getExperimentalDynamicModelConfiguration: () => true,
|
||||
modelConfigService,
|
||||
} as unknown as Config;
|
||||
|
||||
expect(
|
||||
resolveModel(
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
mockDynamicConfig,
|
||||
true,
|
||||
),
|
||||
).toBe('gemini-3.5-flash');
|
||||
});
|
||||
|
||||
it('should resolve classifier-selected flash alias to DEFAULT_GEMINI_FLASH_MODEL when useGemini3_5Flash is true (static)', () => {
|
||||
expect(
|
||||
resolveClassifierModel(
|
||||
GEMINI_MODEL_ALIAS_AUTO,
|
||||
GEMINI_MODEL_ALIAS_FLASH,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
undefined,
|
||||
true,
|
||||
),
|
||||
).toBe(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
});
|
||||
|
||||
it('should resolve classifier-selected flash alias to gemini-3.5-flash when useGemini3_5Flash is true (dynamic)', () => {
|
||||
const mockDynamicConfig = {
|
||||
getExperimentalDynamicModelConfiguration: () => true,
|
||||
modelConfigService,
|
||||
} as unknown as Config;
|
||||
|
||||
expect(
|
||||
resolveClassifierModel(
|
||||
GEMINI_MODEL_ALIAS_AUTO,
|
||||
GEMINI_MODEL_ALIAS_FLASH,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
mockDynamicConfig,
|
||||
true,
|
||||
),
|
||||
).toBe('gemini-3.5-flash');
|
||||
});
|
||||
|
||||
it('should resolve auto to PREVIEW_GEMINI_MODEL when useGemini3_5Flash is true and has preview access', () => {
|
||||
expect(
|
||||
resolveModel(
|
||||
GEMINI_MODEL_ALIAS_AUTO,
|
||||
false,
|
||||
false,
|
||||
true, // hasAccessToPreview
|
||||
undefined,
|
||||
true, // useGemini3_5Flash
|
||||
),
|
||||
).toBe(PREVIEW_GEMINI_MODEL);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
export interface ModelResolutionContext {
|
||||
useGemini3_1?: boolean;
|
||||
useGemini3_5Flash?: boolean;
|
||||
useCustomTools?: boolean;
|
||||
hasAccessToPreview?: boolean;
|
||||
requestedModel?: string;
|
||||
@@ -54,9 +55,29 @@ export const PREVIEW_GEMINI_MODEL = 'gemini-3-pro-preview';
|
||||
export const PREVIEW_GEMINI_3_1_MODEL = 'gemini-3.1-pro-preview';
|
||||
export const PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL =
|
||||
'gemini-3.1-pro-preview-customtools';
|
||||
export const PREVIEW_GEMINI_FLASH_MODEL = 'gemini-3-flash-preview';
|
||||
// TODO: set to none and const once the experiment for 3_5 flash rollut can be
|
||||
// cleaned up.
|
||||
export let PREVIEW_GEMINI_FLASH_MODEL = 'gemini-3-flash-preview';
|
||||
export const DEFAULT_GEMINI_MODEL = 'gemini-2.5-pro';
|
||||
export const DEFAULT_GEMINI_FLASH_MODEL = 'gemini-2.5-flash';
|
||||
// TODO: Set to const and update to 'gemini-3.5-flash' once the experiment for
|
||||
// 3_5 flash rollut can be cleaned up.
|
||||
// This is set to either the same as the DEFAULT_GEMINI_3_5_FLASH_MODEL const
|
||||
// OR the SECONDARY_GEMINI_3_5_FLASH_MODEL depending on which is needed for
|
||||
// the user's backend as determined by hasGemini35FlashGAAccess in
|
||||
// packages/core/src/config/config.ts
|
||||
export let DEFAULT_GEMINI_FLASH_MODEL = 'gemini-2.5-flash';
|
||||
export const DEFAULT_GEMINI_3_5_FLASH_MODEL = 'gemini-3.5-flash';
|
||||
// This is resolved to 3.5 flash in backends where it is used,
|
||||
// however those backends do not expect to see the string gemini-3.5-flash
|
||||
// so we need to provide this model as an alternative name in certain instances.
|
||||
export const SECONDARY_GEMINI_3_5_FLASH_MODEL = 'gemini-3-flash';
|
||||
|
||||
// Used to set default flash models based on access
|
||||
// TODO: Cleanup once the experiment for 3_5 flash rollut can be cleaned up.
|
||||
export function setFlashModels(preview: string, defaultFlash: string) {
|
||||
PREVIEW_GEMINI_FLASH_MODEL = preview;
|
||||
DEFAULT_GEMINI_FLASH_MODEL = defaultFlash;
|
||||
}
|
||||
export const DEFAULT_GEMINI_FLASH_LITE_MODEL = 'gemini-3.1-flash-lite';
|
||||
/** @deprecated Gemini 3.1 Flash Lite is now GA. Use DEFAULT_GEMINI_FLASH_LITE_MODEL. */
|
||||
export const PREVIEW_GEMINI_FLASH_LITE_MODEL = 'none';
|
||||
@@ -72,6 +93,8 @@ export const VALID_GEMINI_MODELS = new Set([
|
||||
PREVIEW_GEMINI_FLASH_LITE_MODEL,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
DEFAULT_GEMINI_3_5_FLASH_MODEL,
|
||||
SECONDARY_GEMINI_3_5_FLASH_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||
|
||||
GEMMA_4_31B_IT_MODEL,
|
||||
@@ -97,6 +120,7 @@ export const DEFAULT_THINKING_MODE = 8192;
|
||||
export function getAutoModelDescription(
|
||||
hasAccessToPreview: boolean,
|
||||
useGemini3_1: boolean = false,
|
||||
useGemini3_5Flash: boolean = false,
|
||||
) {
|
||||
const proModel = hasAccessToPreview
|
||||
? useGemini3_1
|
||||
@@ -104,9 +128,11 @@ export function getAutoModelDescription(
|
||||
: PREVIEW_GEMINI_MODEL
|
||||
: DEFAULT_GEMINI_MODEL;
|
||||
const flashModel = hasAccessToPreview
|
||||
? PREVIEW_GEMINI_FLASH_MODEL
|
||||
? useGemini3_5Flash
|
||||
? DEFAULT_GEMINI_3_5_FLASH_MODEL
|
||||
: PREVIEW_GEMINI_FLASH_MODEL
|
||||
: DEFAULT_GEMINI_FLASH_MODEL;
|
||||
return `Let Gemini CLI decide the best model for the task: ${proModel}, ${flashModel}`;
|
||||
return `Let Gemini CLI decide the best model for the task: ${getDisplayString(proModel)}, ${getDisplayString(flashModel)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,6 +141,7 @@ export function getAutoModelDescription(
|
||||
*
|
||||
* @param requestedModel The model alias or concrete model name requested by the user.
|
||||
* @param useGemini3_1 Whether to use Gemini 3.1 Pro Preview for auto/pro aliases.
|
||||
* @param useGemini3_5Flash Whether to use Gemini 3.5 Flash GA.
|
||||
* @param hasAccessToPreview Whether the user has access to preview models.
|
||||
* @returns The resolved concrete model name.
|
||||
*/
|
||||
@@ -124,6 +151,7 @@ export function resolveModel(
|
||||
useCustomToolModel: boolean = false,
|
||||
hasAccessToPreview: boolean = true,
|
||||
config?: ModelCapabilityContext,
|
||||
useGemini3_5Flash: boolean = false,
|
||||
): string {
|
||||
// Defensive check against non-string inputs at runtime
|
||||
const normalizedModel = Array.isArray(requestedModel)
|
||||
@@ -137,6 +165,7 @@ export function resolveModel(
|
||||
useGemini3_1,
|
||||
useCustomTools: useCustomToolModel,
|
||||
hasAccessToPreview,
|
||||
useGemini3_5Flash,
|
||||
});
|
||||
|
||||
if (!hasAccessToPreview && isPreviewModel(resolved, config)) {
|
||||
@@ -179,7 +208,9 @@ export function resolveModel(
|
||||
break;
|
||||
}
|
||||
case GEMINI_MODEL_ALIAS_FLASH: {
|
||||
resolved = PREVIEW_GEMINI_FLASH_MODEL;
|
||||
resolved = useGemini3_5Flash
|
||||
? DEFAULT_GEMINI_FLASH_MODEL
|
||||
: PREVIEW_GEMINI_FLASH_MODEL;
|
||||
break;
|
||||
}
|
||||
case GEMINI_MODEL_ALIAS_FLASH_LITE: {
|
||||
@@ -196,6 +227,14 @@ export function resolveModel(
|
||||
return DEFAULT_GEMINI_FLASH_LITE_MODEL;
|
||||
}
|
||||
|
||||
if (
|
||||
useGemini3_5Flash &&
|
||||
isFlashModel(resolved) &&
|
||||
normalizedModel !== PREVIEW_GEMINI_FLASH_MODEL
|
||||
) {
|
||||
return DEFAULT_GEMINI_FLASH_MODEL;
|
||||
}
|
||||
|
||||
if (!hasAccessToPreview && isPreviewModel(resolved)) {
|
||||
// Downgrade to stable models if user lacks preview access.
|
||||
switch (resolved) {
|
||||
@@ -220,6 +259,17 @@ export function resolveModel(
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function isFlashModel(model: string): boolean {
|
||||
return (
|
||||
model === DEFAULT_GEMINI_FLASH_MODEL ||
|
||||
model === PREVIEW_GEMINI_FLASH_MODEL ||
|
||||
model === DEFAULT_GEMINI_3_5_FLASH_MODEL ||
|
||||
model === SECONDARY_GEMINI_3_5_FLASH_MODEL ||
|
||||
model === 'flash' ||
|
||||
model.endsWith('flash')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the appropriate model based on the classifier's decision.
|
||||
*
|
||||
@@ -237,6 +287,7 @@ export function resolveClassifierModel(
|
||||
useCustomToolModel: boolean = false,
|
||||
hasAccessToPreview: boolean = true,
|
||||
config?: ModelCapabilityContext,
|
||||
useGemini3_5Flash: boolean = false,
|
||||
): string {
|
||||
if (config?.getExperimentalDynamicModelConfiguration?.() === true) {
|
||||
return config.modelConfigService.resolveClassifierModelId(
|
||||
@@ -246,6 +297,7 @@ export function resolveClassifierModel(
|
||||
useGemini3_1,
|
||||
useCustomTools: useCustomToolModel,
|
||||
hasAccessToPreview,
|
||||
useGemini3_5Flash,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -262,6 +314,9 @@ export function resolveClassifierModel(
|
||||
requestedModel === PREVIEW_GEMINI_MODEL ||
|
||||
requestedModel === GEMINI_MODEL_ALIAS_AUTO
|
||||
) {
|
||||
if (useGemini3_5Flash) {
|
||||
return DEFAULT_GEMINI_FLASH_MODEL;
|
||||
}
|
||||
return hasAccessToPreview
|
||||
? PREVIEW_GEMINI_FLASH_MODEL
|
||||
: DEFAULT_GEMINI_FLASH_MODEL;
|
||||
@@ -271,6 +326,8 @@ export function resolveClassifierModel(
|
||||
false,
|
||||
false,
|
||||
hasAccessToPreview,
|
||||
config,
|
||||
useGemini3_5Flash,
|
||||
);
|
||||
}
|
||||
return resolveModel(
|
||||
@@ -279,6 +336,7 @@ export function resolveClassifierModel(
|
||||
useCustomToolModel,
|
||||
hasAccessToPreview,
|
||||
config,
|
||||
useGemini3_5Flash,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -294,6 +352,8 @@ export function getDisplayString(
|
||||
}
|
||||
|
||||
switch (model) {
|
||||
case 'gemini-3-flash':
|
||||
return DEFAULT_GEMINI_3_5_FLASH_MODEL;
|
||||
case GEMINI_MODEL_ALIAS_AUTO:
|
||||
return 'Auto';
|
||||
case PREVIEW_GEMINI_MODEL_AUTO:
|
||||
@@ -514,3 +574,7 @@ export function isActiveModel(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const CCPA_AI_MODEL_MAPPINGS: Record<string, string> = {
|
||||
[DEFAULT_GEMINI_3_5_FLASH_MODEL]: SECONDARY_GEMINI_3_5_FLASH_MODEL,
|
||||
};
|
||||
|
||||
@@ -607,6 +607,7 @@ export class GeminiClient {
|
||||
false,
|
||||
this.config.getHasAccessToPreviewModel?.() ?? true,
|
||||
this.config,
|
||||
this.config.hasGemini35FlashGAAccess?.() ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,10 +18,13 @@ import { HttpProxyAgent } from 'http-proxy-agent';
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import type { Config } from '../config/config.js';
|
||||
import { LoggingContentGenerator } from './loggingContentGenerator.js';
|
||||
import { ModelMappingContentGenerator } from './modelMappingContentGenerator.js';
|
||||
import { CCPA_AI_MODEL_MAPPINGS } from '../config/models.js';
|
||||
import { loadApiKey } from './apiKeyCredentialStorage.js';
|
||||
import { FakeContentGenerator } from './fakeContentGenerator.js';
|
||||
import { RecordingContentGenerator } from './recordingContentGenerator.js';
|
||||
import { resetVersionCache } from '../utils/version.js';
|
||||
import type { LlmRole } from '../telemetry/llmRole.js';
|
||||
|
||||
vi.mock('../code_assist/codeAssist.js');
|
||||
vi.mock('@google/genai');
|
||||
@@ -36,6 +39,14 @@ const mockConfig = {
|
||||
getProxy: vi.fn().mockReturnValue(undefined),
|
||||
getUsageStatisticsEnabled: vi.fn().mockReturnValue(true),
|
||||
getClientName: vi.fn().mockReturnValue(undefined),
|
||||
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(true),
|
||||
getTelemetryTracesEnabled: vi.fn().mockReturnValue(true),
|
||||
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||
refreshUserQuotaIfStale: vi.fn().mockResolvedValue(undefined),
|
||||
setLatestApiRequest: vi.fn(),
|
||||
getContentGeneratorConfig: vi.fn().mockReturnValue({}),
|
||||
isInteractive: vi.fn().mockReturnValue(false),
|
||||
getExperiments: vi.fn().mockReturnValue(undefined),
|
||||
} as unknown as Config;
|
||||
|
||||
describe('getAuthTypeFromEnv', () => {
|
||||
@@ -142,7 +153,10 @@ describe('createContentGenerator', () => {
|
||||
);
|
||||
expect(createCodeAssistContentGenerator).toHaveBeenCalled();
|
||||
expect(generator).toEqual(
|
||||
new LoggingContentGenerator(mockGenerator, mockConfig),
|
||||
new LoggingContentGenerator(
|
||||
new ModelMappingContentGenerator(mockGenerator, CCPA_AI_MODEL_MAPPINGS),
|
||||
mockConfig,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -159,7 +173,10 @@ describe('createContentGenerator', () => {
|
||||
);
|
||||
expect(createCodeAssistContentGenerator).toHaveBeenCalled();
|
||||
expect(generator).toEqual(
|
||||
new LoggingContentGenerator(mockGenerator, mockConfig),
|
||||
new LoggingContentGenerator(
|
||||
new ModelMappingContentGenerator(mockGenerator, CCPA_AI_MODEL_MAPPINGS),
|
||||
mockConfig,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1095,6 +1112,178 @@ describe('createContentGenerator', () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not apply model mapping for Vertex AI', async () => {
|
||||
const mockModels = {
|
||||
generateContent: vi.fn().mockResolvedValue({}),
|
||||
};
|
||||
const mockGenerator = {
|
||||
models: mockModels,
|
||||
} as unknown as GoogleGenAI;
|
||||
vi.mocked(GoogleGenAI).mockImplementation(() => mockGenerator as never);
|
||||
|
||||
const generator = await createContentGenerator(
|
||||
{
|
||||
apiKey: 'test-api-key',
|
||||
authType: AuthType.USE_VERTEX_AI,
|
||||
vertexai: true,
|
||||
},
|
||||
mockConfig,
|
||||
);
|
||||
|
||||
await generator.generateContent(
|
||||
{
|
||||
model: 'gemini-3-flash',
|
||||
contents: [],
|
||||
},
|
||||
'prompt-id',
|
||||
'user' as LlmRole,
|
||||
);
|
||||
|
||||
expect(mockModels.generateContent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
model: 'gemini-3-flash',
|
||||
}),
|
||||
'prompt-id',
|
||||
'user',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not apply model mapping for Gemini API', async () => {
|
||||
const mockModels = {
|
||||
generateContent: vi.fn().mockResolvedValue({}),
|
||||
};
|
||||
const mockGenerator = {
|
||||
models: mockModels,
|
||||
} as unknown as GoogleGenAI;
|
||||
vi.mocked(GoogleGenAI).mockImplementation(() => mockGenerator as never);
|
||||
|
||||
const generator = await createContentGenerator(
|
||||
{
|
||||
apiKey: 'test-api-key',
|
||||
authType: AuthType.USE_GEMINI,
|
||||
},
|
||||
mockConfig,
|
||||
);
|
||||
|
||||
await generator.generateContent(
|
||||
{
|
||||
model: 'gemini-3-flash',
|
||||
contents: [],
|
||||
},
|
||||
'prompt-id',
|
||||
'user' as LlmRole,
|
||||
);
|
||||
|
||||
expect(mockModels.generateContent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
model: 'gemini-3-flash',
|
||||
}),
|
||||
'prompt-id',
|
||||
'user',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not apply model mapping for GATEWAY', async () => {
|
||||
const mockModels = {
|
||||
generateContent: vi.fn().mockResolvedValue({}),
|
||||
};
|
||||
const mockGenerator = {
|
||||
models: mockModels,
|
||||
} as unknown as GoogleGenAI;
|
||||
vi.mocked(GoogleGenAI).mockImplementation(() => mockGenerator as never);
|
||||
|
||||
const generator = await createContentGenerator(
|
||||
{
|
||||
apiKey: 'test-api-key',
|
||||
authType: AuthType.GATEWAY,
|
||||
},
|
||||
mockConfig,
|
||||
);
|
||||
|
||||
await generator.generateContent(
|
||||
{
|
||||
model: 'gemini-3.5-flash',
|
||||
contents: [],
|
||||
},
|
||||
'prompt-id',
|
||||
'user' as LlmRole,
|
||||
);
|
||||
|
||||
expect(mockModels.generateContent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
model: 'gemini-3.5-flash',
|
||||
}),
|
||||
'prompt-id',
|
||||
'user',
|
||||
);
|
||||
});
|
||||
|
||||
it('should apply model mapping for LOGIN_WITH_GOOGLE', async () => {
|
||||
const mockInnerGenerator = {
|
||||
generateContent: vi.fn().mockResolvedValue({}),
|
||||
} as unknown as ContentGenerator;
|
||||
vi.mocked(createCodeAssistContentGenerator).mockResolvedValue(
|
||||
mockInnerGenerator as never,
|
||||
);
|
||||
|
||||
const generator = await createContentGenerator(
|
||||
{
|
||||
authType: AuthType.LOGIN_WITH_GOOGLE,
|
||||
},
|
||||
mockConfig,
|
||||
);
|
||||
|
||||
await generator.generateContent(
|
||||
{
|
||||
model: 'gemini-3.5-flash',
|
||||
contents: [],
|
||||
},
|
||||
'prompt-id',
|
||||
'user' as LlmRole,
|
||||
);
|
||||
|
||||
expect(mockInnerGenerator.generateContent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
model: 'gemini-3-flash',
|
||||
}),
|
||||
'prompt-id',
|
||||
'user',
|
||||
);
|
||||
});
|
||||
|
||||
it('should apply model mapping for COMPUTE_ADC', async () => {
|
||||
const mockInnerGenerator = {
|
||||
generateContent: vi.fn().mockResolvedValue({}),
|
||||
} as unknown as ContentGenerator;
|
||||
vi.mocked(createCodeAssistContentGenerator).mockResolvedValue(
|
||||
mockInnerGenerator as never,
|
||||
);
|
||||
|
||||
const generator = await createContentGenerator(
|
||||
{
|
||||
authType: AuthType.COMPUTE_ADC,
|
||||
},
|
||||
mockConfig,
|
||||
);
|
||||
|
||||
await generator.generateContent(
|
||||
{
|
||||
model: 'gemini-3.5-flash',
|
||||
contents: [],
|
||||
},
|
||||
'prompt-id',
|
||||
'user' as LlmRole,
|
||||
);
|
||||
|
||||
expect(mockInnerGenerator.generateContent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
model: 'gemini-3-flash',
|
||||
}),
|
||||
'prompt-id',
|
||||
'user',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createContentGeneratorConfig', () => {
|
||||
|
||||
@@ -30,6 +30,8 @@ import { determineSurface } from '../utils/surface.js';
|
||||
import { RecordingContentGenerator } from './recordingContentGenerator.js';
|
||||
import { getVersion, resolveModel } from '../../index.js';
|
||||
import type { LlmRole } from '../telemetry/llmRole.js';
|
||||
import { ModelMappingContentGenerator } from './modelMappingContentGenerator.js';
|
||||
import { CCPA_AI_MODEL_MAPPINGS } from '../config/models.js';
|
||||
|
||||
/**
|
||||
* Interface abstracting the core functionalities for generating content and counting tokens.
|
||||
@@ -221,6 +223,7 @@ export async function createContentGenerator(
|
||||
false,
|
||||
gcConfig.getHasAccessToPreviewModel?.() ?? true,
|
||||
gcConfig,
|
||||
gcConfig.hasGemini35FlashGAAccess?.() ?? false,
|
||||
);
|
||||
const customHeadersEnv =
|
||||
process.env['GEMINI_CLI_CUSTOM_HEADERS'] || undefined;
|
||||
@@ -281,11 +284,14 @@ export async function createContentGenerator(
|
||||
) {
|
||||
const httpOptions = { headers: baseHeaders };
|
||||
return new LoggingContentGenerator(
|
||||
await createCodeAssistContentGenerator(
|
||||
httpOptions,
|
||||
config.authType,
|
||||
gcConfig,
|
||||
sessionId,
|
||||
new ModelMappingContentGenerator(
|
||||
await createCodeAssistContentGenerator(
|
||||
httpOptions,
|
||||
config.authType,
|
||||
gcConfig,
|
||||
sessionId,
|
||||
),
|
||||
CCPA_AI_MODEL_MAPPINGS,
|
||||
),
|
||||
gcConfig,
|
||||
);
|
||||
|
||||
@@ -159,6 +159,7 @@ describe('GeminiChat', () => {
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
getTelemetryTracesEnabled: () => false,
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
hasGemini35FlashGAAccess: vi.fn().mockReturnValue(false),
|
||||
getDebugMode: () => false,
|
||||
getContentGeneratorConfig: vi.fn().mockImplementation(() => ({
|
||||
authType: 'oauth-personal',
|
||||
|
||||
@@ -721,7 +721,6 @@ export class GeminiChat {
|
||||
(await this.context.config.getGemini31Launched?.()) ?? false;
|
||||
const hasAccessToPreview =
|
||||
this.context.config.getHasAccessToPreviewModel?.() ?? true;
|
||||
|
||||
// Default to the last used model (which respects arguments/availability selection)
|
||||
let modelToUse = resolveModel(
|
||||
lastModelToUse,
|
||||
@@ -729,6 +728,7 @@ export class GeminiChat {
|
||||
false,
|
||||
hasAccessToPreview,
|
||||
this.context.config,
|
||||
this.context.config.hasGemini35FlashGAAccess?.() ?? false,
|
||||
);
|
||||
|
||||
// If the active model has changed (e.g. due to a fallback updating the config),
|
||||
@@ -740,6 +740,7 @@ export class GeminiChat {
|
||||
false,
|
||||
hasAccessToPreview,
|
||||
this.context.config,
|
||||
this.context.config.hasGemini35FlashGAAccess?.() ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -802,6 +803,7 @@ export class GeminiChat {
|
||||
false,
|
||||
hasAccessToPreview,
|
||||
this.context.config,
|
||||
this.context.config.hasGemini35FlashGAAccess?.() ?? false,
|
||||
);
|
||||
lastModelToUse = modelToUse;
|
||||
// Re-evaluate contentsToUse based on the new model's feature support
|
||||
|
||||
@@ -98,6 +98,7 @@ describe('GeminiChat Network Retries', () => {
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
getTelemetryTracesEnabled: () => false,
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
hasGemini35FlashGAAccess: vi.fn().mockReturnValue(false),
|
||||
getDebugMode: () => false,
|
||||
getContentGeneratorConfig: vi.fn().mockReturnValue({
|
||||
authType: 'oauth-personal',
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { ModelMappingContentGenerator } from './modelMappingContentGenerator.js';
|
||||
import type { ContentGenerator } from './contentGenerator.js';
|
||||
import { LlmRole } from '../telemetry/llmRole.js';
|
||||
import type { GenerateContentParameters } from '@google/genai';
|
||||
|
||||
describe('ModelMappingContentGenerator', () => {
|
||||
const mockMappings = {
|
||||
'gemini-3.5-flash': 'gemini-3-flash',
|
||||
'gemini-pro': 'gemini-1.5-pro',
|
||||
};
|
||||
|
||||
it('delegates userTier, userTierName, and paidTier properties', () => {
|
||||
const mockWrapped = {
|
||||
userTier: 'free',
|
||||
userTierName: 'Free Tier',
|
||||
paidTier: { id: 'paid' },
|
||||
} as unknown as ContentGenerator;
|
||||
|
||||
const generator = new ModelMappingContentGenerator(
|
||||
mockWrapped,
|
||||
mockMappings,
|
||||
);
|
||||
|
||||
expect(generator.userTier).toBe('free');
|
||||
expect(generator.userTierName).toBe('Free Tier');
|
||||
expect(generator.paidTier).toEqual({ id: 'paid' });
|
||||
});
|
||||
|
||||
it('maps matching model without prefix', async () => {
|
||||
const mockWrapped = {
|
||||
generateContent: vi.fn().mockResolvedValue({}),
|
||||
} as unknown as ContentGenerator;
|
||||
|
||||
const generator = new ModelMappingContentGenerator(
|
||||
mockWrapped,
|
||||
mockMappings,
|
||||
);
|
||||
const req = { model: 'gemini-3.5-flash', contents: [] };
|
||||
|
||||
await generator.generateContent(req, 'prompt-id', LlmRole.MAIN);
|
||||
|
||||
expect(mockWrapped.generateContent).toHaveBeenCalledWith(
|
||||
{ model: 'gemini-3-flash', contents: [] },
|
||||
'prompt-id',
|
||||
LlmRole.MAIN,
|
||||
);
|
||||
});
|
||||
|
||||
it('maps matching model with models/ prefix', async () => {
|
||||
const mockWrapped = {
|
||||
generateContent: vi.fn().mockResolvedValue({}),
|
||||
} as unknown as ContentGenerator;
|
||||
|
||||
const generator = new ModelMappingContentGenerator(
|
||||
mockWrapped,
|
||||
mockMappings,
|
||||
);
|
||||
const req = { model: 'models/gemini-3.5-flash', contents: [] };
|
||||
|
||||
await generator.generateContent(req, 'prompt-id', LlmRole.MAIN);
|
||||
|
||||
expect(mockWrapped.generateContent).toHaveBeenCalledWith(
|
||||
{ model: 'models/gemini-3-flash', contents: [] },
|
||||
'prompt-id',
|
||||
LlmRole.MAIN,
|
||||
);
|
||||
});
|
||||
|
||||
it('leaves unmapped model unchanged', async () => {
|
||||
const mockWrapped = {
|
||||
generateContent: vi.fn().mockResolvedValue({}),
|
||||
} as unknown as ContentGenerator;
|
||||
|
||||
const generator = new ModelMappingContentGenerator(
|
||||
mockWrapped,
|
||||
mockMappings,
|
||||
);
|
||||
const req = { model: 'unknown-model', contents: [] };
|
||||
|
||||
await generator.generateContent(req, 'prompt-id', LlmRole.MAIN);
|
||||
|
||||
expect(mockWrapped.generateContent).toHaveBeenCalledWith(
|
||||
{ model: 'unknown-model', contents: [] },
|
||||
'prompt-id',
|
||||
LlmRole.MAIN,
|
||||
);
|
||||
});
|
||||
|
||||
it('leaves model with prefix unchanged if no match after normalization', async () => {
|
||||
const mockWrapped = {
|
||||
generateContent: vi.fn().mockResolvedValue({}),
|
||||
} as unknown as ContentGenerator;
|
||||
|
||||
const generator = new ModelMappingContentGenerator(
|
||||
mockWrapped,
|
||||
mockMappings,
|
||||
);
|
||||
const req = { model: 'models/unknown-model', contents: [] };
|
||||
|
||||
await generator.generateContent(req, 'prompt-id', LlmRole.MAIN);
|
||||
|
||||
expect(mockWrapped.generateContent).toHaveBeenCalledWith(
|
||||
{ model: 'models/unknown-model', contents: [] },
|
||||
'prompt-id',
|
||||
LlmRole.MAIN,
|
||||
);
|
||||
});
|
||||
|
||||
it('handles missing/undefined model property safely', async () => {
|
||||
const mockWrapped = {
|
||||
generateContent: vi.fn().mockResolvedValue({}),
|
||||
} as unknown as ContentGenerator;
|
||||
|
||||
const generator = new ModelMappingContentGenerator(
|
||||
mockWrapped,
|
||||
mockMappings,
|
||||
);
|
||||
const req = { contents: [] } as unknown as GenerateContentParameters;
|
||||
|
||||
await generator.generateContent(req, 'prompt-id', LlmRole.MAIN);
|
||||
|
||||
expect(mockWrapped.generateContent).toHaveBeenCalledWith(
|
||||
{ contents: [] },
|
||||
'prompt-id',
|
||||
LlmRole.MAIN,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
type CountTokensResponse,
|
||||
type GenerateContentResponse,
|
||||
type GenerateContentParameters,
|
||||
type CountTokensParameters,
|
||||
type EmbedContentResponse,
|
||||
type EmbedContentParameters,
|
||||
} from '@google/genai';
|
||||
import { type ContentGenerator } from './contentGenerator.js';
|
||||
import type { LlmRole } from '../telemetry/llmRole.js';
|
||||
import type { UserTierId, GeminiUserTier } from '../code_assist/types.js';
|
||||
import { normalizeModelId } from '../utils/modelUtils.js';
|
||||
|
||||
export class ModelMappingContentGenerator implements ContentGenerator {
|
||||
constructor(
|
||||
private readonly wrapped: ContentGenerator,
|
||||
private readonly mappings: Record<string, string>,
|
||||
) {}
|
||||
|
||||
getWrapped(): ContentGenerator {
|
||||
return this.wrapped;
|
||||
}
|
||||
|
||||
get userTier(): UserTierId | undefined {
|
||||
return this.wrapped.userTier;
|
||||
}
|
||||
|
||||
get userTierName(): string | undefined {
|
||||
return this.wrapped.userTierName;
|
||||
}
|
||||
|
||||
get paidTier(): GeminiUserTier | undefined {
|
||||
return this.wrapped.paidTier;
|
||||
}
|
||||
|
||||
private mapModel<T extends { model?: string }>(req: T): T {
|
||||
if (req.model) {
|
||||
const normalizedModel = normalizeModelId(req.model);
|
||||
if (this.mappings[normalizedModel]) {
|
||||
return {
|
||||
...req,
|
||||
model: req.model.startsWith('models/')
|
||||
? `models/${this.mappings[normalizedModel]}`
|
||||
: this.mappings[normalizedModel],
|
||||
};
|
||||
}
|
||||
}
|
||||
return req;
|
||||
}
|
||||
|
||||
generateContent(
|
||||
request: GenerateContentParameters,
|
||||
userPromptId: string,
|
||||
role: LlmRole,
|
||||
): Promise<GenerateContentResponse> {
|
||||
return this.wrapped.generateContent(
|
||||
this.mapModel(request),
|
||||
userPromptId,
|
||||
role,
|
||||
);
|
||||
}
|
||||
|
||||
generateContentStream(
|
||||
request: GenerateContentParameters,
|
||||
userPromptId: string,
|
||||
role: LlmRole,
|
||||
): Promise<AsyncGenerator<GenerateContentResponse>> {
|
||||
return this.wrapped.generateContentStream(
|
||||
this.mapModel(request),
|
||||
userPromptId,
|
||||
role,
|
||||
);
|
||||
}
|
||||
|
||||
countTokens(request: CountTokensParameters): Promise<CountTokensResponse> {
|
||||
return this.wrapped.countTokens(this.mapModel(request));
|
||||
}
|
||||
|
||||
embedContent(request: EmbedContentParameters): Promise<EmbedContentResponse> {
|
||||
return this.wrapped.embedContent(this.mapModel(request));
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,7 @@ export class PromptProvider {
|
||||
false,
|
||||
context.config.getHasAccessToPreviewModel?.() ?? true,
|
||||
context.config,
|
||||
context.config.hasGemini35FlashGAAccess?.() ?? false,
|
||||
);
|
||||
const isModernModel = supportsModernFeatures(desiredModel);
|
||||
const activeSnippets = isModernModel ? snippets : legacySnippets;
|
||||
@@ -299,6 +300,7 @@ export class PromptProvider {
|
||||
false,
|
||||
context.config.getHasAccessToPreviewModel?.() ?? true,
|
||||
context.config,
|
||||
context.config.hasGemini35FlashGAAccess?.() ?? false,
|
||||
);
|
||||
const isModernModel = supportsModernFeatures(desiredModel);
|
||||
const activeSnippets = isModernModel ? snippets : legacySnippets;
|
||||
|
||||
@@ -242,4 +242,22 @@ describe('ApprovalModeStrategy', () => {
|
||||
// Should resolve to Preview Flash (3.0) because resolveClassifierModel uses preview variants for Gemini 3
|
||||
expect(decision?.model).toBe(PREVIEW_GEMINI_FLASH_MODEL);
|
||||
});
|
||||
|
||||
it('should route to DEFAULT_GEMINI_FLASH_MODEL when hasGemini35FlashGAAccess is true and plan is approved', async () => {
|
||||
vi.mocked(mockConfig.getModel).mockReturnValue(GEMINI_MODEL_ALIAS_AUTO);
|
||||
mockConfig.hasGemini35FlashGAAccess = vi.fn().mockReturnValue(true);
|
||||
|
||||
vi.mocked(mockConfig.getApprovalMode).mockReturnValue(ApprovalMode.DEFAULT);
|
||||
vi.mocked(mockConfig.getApprovedPlanPath).mockReturnValue(
|
||||
'/path/to/plan.md',
|
||||
);
|
||||
|
||||
const decision = await strategy.route(
|
||||
mockContext,
|
||||
mockConfig,
|
||||
mockBaseLlmClient,
|
||||
);
|
||||
|
||||
expect(decision?.model).toBe(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,6 +54,7 @@ export class ApprovalModeStrategy implements RoutingStrategy {
|
||||
config.getUseCustomToolModel(),
|
||||
config.getHasAccessToPreviewModel(),
|
||||
]);
|
||||
const useGemini3_5Flash = config.hasGemini35FlashGAAccess?.() ?? false;
|
||||
|
||||
// 1. Planning Phase: If ApprovalMode === PLAN, explicitly route to the Pro model.
|
||||
if (approvalMode === ApprovalMode.PLAN) {
|
||||
@@ -64,6 +65,7 @@ export class ApprovalModeStrategy implements RoutingStrategy {
|
||||
useCustomToolModel,
|
||||
hasAccessToPreview,
|
||||
config,
|
||||
useGemini3_5Flash,
|
||||
);
|
||||
return {
|
||||
model: proModel,
|
||||
@@ -82,6 +84,7 @@ export class ApprovalModeStrategy implements RoutingStrategy {
|
||||
useCustomToolModel,
|
||||
hasAccessToPreview,
|
||||
config,
|
||||
useGemini3_5Flash,
|
||||
);
|
||||
return {
|
||||
model: flashModel,
|
||||
|
||||
@@ -522,5 +522,27 @@ describe('ClassifierStrategy', () => {
|
||||
|
||||
expect(decision?.model).toBe(PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL);
|
||||
});
|
||||
|
||||
it('should route to DEFAULT_GEMINI_FLASH_MODEL when hasGemini35FlashGAAccess is true', async () => {
|
||||
mockConfig.hasGemini35FlashGAAccess = vi.fn().mockReturnValue(true);
|
||||
vi.mocked(mockConfig.getModel).mockReturnValue(PREVIEW_GEMINI_MODEL_AUTO);
|
||||
|
||||
const mockApiResponse = {
|
||||
reasoning: 'Simple task',
|
||||
model_choice: 'flash',
|
||||
};
|
||||
vi.mocked(mockBaseLlmClient.generateJson).mockResolvedValue(
|
||||
mockApiResponse,
|
||||
);
|
||||
|
||||
const decision = await strategy.route(
|
||||
mockContext,
|
||||
mockConfig,
|
||||
mockBaseLlmClient,
|
||||
mockLocalLiteRtLmClient,
|
||||
);
|
||||
|
||||
expect(decision?.model).toBe(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -186,6 +186,7 @@ export class ClassifierStrategy implements RoutingStrategy {
|
||||
config.getGemini31Launched(),
|
||||
config.getUseCustomToolModel(),
|
||||
]);
|
||||
const useGemini3_5Flash = config.hasGemini35FlashGAAccess?.() ?? false;
|
||||
const selectedModel = normalizeModelId(
|
||||
resolveClassifierModel(
|
||||
normalizeModelId(model),
|
||||
@@ -194,6 +195,7 @@ export class ClassifierStrategy implements RoutingStrategy {
|
||||
useCustomToolModel,
|
||||
config.getHasAccessToPreviewModel?.() ?? true,
|
||||
config,
|
||||
useGemini3_5Flash,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ export class DefaultStrategy implements TerminalStrategy {
|
||||
false,
|
||||
config.getHasAccessToPreviewModel?.() ?? true,
|
||||
config,
|
||||
config.hasGemini35FlashGAAccess?.() ?? false,
|
||||
);
|
||||
return {
|
||||
model: defaultModel,
|
||||
|
||||
@@ -31,6 +31,7 @@ export class FallbackStrategy implements RoutingStrategy {
|
||||
false,
|
||||
config.getHasAccessToPreviewModel?.() ?? true,
|
||||
config,
|
||||
config.hasGemini35FlashGAAccess?.() ?? false,
|
||||
);
|
||||
const service = config.getModelAvailabilityService();
|
||||
const snapshot = service.snapshot(resolvedModel);
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { BaseLlmClient } from '../../core/baseLlmClient.js';
|
||||
import {
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
PREVIEW_GEMINI_MODEL_AUTO,
|
||||
} from '../../config/models.js';
|
||||
import type { Content } from '@google/genai';
|
||||
import { debugLogger } from '../../utils/debugLogger.js';
|
||||
@@ -323,4 +324,24 @@ second message
|
||||
|
||||
expect(lastTurn!.parts!.at(0)!.text).toEqual(expectedLastTurn);
|
||||
});
|
||||
|
||||
it('should route to DEFAULT_GEMINI_FLASH_MODEL when hasGemini35FlashGAAccess is true', async () => {
|
||||
mockConfig.hasGemini35FlashGAAccess = vi.fn().mockReturnValue(true);
|
||||
mockConfig.getModel = () => PREVIEW_GEMINI_MODEL_AUTO;
|
||||
|
||||
const mockApiResponse = {
|
||||
reasoning: 'Simple task',
|
||||
model_choice: 'flash',
|
||||
};
|
||||
mockGenerateJson.mockResolvedValue(mockApiResponse);
|
||||
|
||||
const decision = await strategy.route(
|
||||
mockContext,
|
||||
mockConfig,
|
||||
mockBaseLlmClient,
|
||||
mockLocalLiteRtLmClient,
|
||||
);
|
||||
|
||||
expect(decision?.model).toBe(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -216,6 +216,7 @@ ${formattedHistory}
|
||||
config.getUseCustomToolModel(),
|
||||
config.getHasAccessToPreviewModel(),
|
||||
]);
|
||||
const useGemini3_5Flash = config.hasGemini35FlashGAAccess?.() ?? false;
|
||||
|
||||
const selectedModel = resolveClassifierModel(
|
||||
context.requestedModel ?? config.getModel(),
|
||||
@@ -224,6 +225,7 @@ ${formattedHistory}
|
||||
useCustomToolModel,
|
||||
hasAccessToPreview,
|
||||
config,
|
||||
useGemini3_5Flash,
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
PREVIEW_GEMINI_MODEL_AUTO,
|
||||
DEFAULT_GEMINI_MODEL_AUTO,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
} from '../../config/models.js';
|
||||
import { promptIdContext } from '../../utils/promptIdContext.js';
|
||||
import type { Content } from '@google/genai';
|
||||
@@ -894,5 +895,27 @@ describe('NumericalClassifierStrategy', () => {
|
||||
|
||||
expect(decision?.model).toBe(PREVIEW_GEMINI_3_1_MODEL);
|
||||
});
|
||||
|
||||
it('should route to DEFAULT_GEMINI_FLASH_MODEL when hasGemini35FlashGAAccess is true', async () => {
|
||||
mockConfig.hasGemini35FlashGAAccess = vi.fn().mockReturnValue(true);
|
||||
vi.mocked(mockConfig.getModel).mockReturnValue(PREVIEW_GEMINI_MODEL_AUTO);
|
||||
|
||||
const mockApiResponse = {
|
||||
complexity_reasoning: 'Simple task',
|
||||
complexity_score: 10,
|
||||
};
|
||||
vi.mocked(mockBaseLlmClient.generateJson).mockResolvedValue(
|
||||
mockApiResponse,
|
||||
);
|
||||
|
||||
const decision = await strategy.route(
|
||||
mockContext,
|
||||
mockConfig,
|
||||
mockBaseLlmClient,
|
||||
mockLocalLiteRtLmClient,
|
||||
);
|
||||
|
||||
expect(decision?.model).toBe(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -184,6 +184,7 @@ export class NumericalClassifierStrategy implements RoutingStrategy {
|
||||
config.getGemini31Launched(),
|
||||
config.getUseCustomToolModel(),
|
||||
]);
|
||||
const useGemini3_5Flash = config.hasGemini35FlashGAAccess?.() ?? false;
|
||||
const selectedModel = normalizeModelId(
|
||||
resolveClassifierModel(
|
||||
normalizeModelId(model),
|
||||
@@ -192,6 +193,7 @@ export class NumericalClassifierStrategy implements RoutingStrategy {
|
||||
useCustomToolModel,
|
||||
config.getHasAccessToPreviewModel?.() ?? true,
|
||||
config,
|
||||
useGemini3_5Flash,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ export class OverrideStrategy implements RoutingStrategy {
|
||||
false,
|
||||
config.getHasAccessToPreviewModel?.() ?? true,
|
||||
config,
|
||||
config.hasGemini35FlashGAAccess?.() ?? false,
|
||||
),
|
||||
metadata: {
|
||||
source: this.name,
|
||||
|
||||
@@ -1044,6 +1044,79 @@ describe('ModelConfigService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Resolves a model ID to a concrete model ID based on the provided context.
|
||||
describe('resolveModelId', () => {
|
||||
it('should resolve based on useGemini3_5Flash condition', () => {
|
||||
const config: ModelConfigServiceConfig = {
|
||||
modelIdResolutions: {
|
||||
flash: {
|
||||
default: 'gemini-2.0-flash',
|
||||
contexts: [
|
||||
{
|
||||
condition: { useGemini3_5Flash: true },
|
||||
target: 'gemini-3.5-flash',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const service = new ModelConfigService(config);
|
||||
|
||||
expect(service.resolveModelId('flash', { useGemini3_5Flash: true })).toBe(
|
||||
'gemini-3.5-flash',
|
||||
);
|
||||
expect(
|
||||
service.resolveModelId('flash', { useGemini3_5Flash: false }),
|
||||
).toBe('gemini-2.0-flash');
|
||||
expect(service.resolveModelId('flash', {})).toBe('gemini-2.0-flash');
|
||||
});
|
||||
|
||||
it('should resolve based on complex conditions including useGemini3_5Flash', () => {
|
||||
const config: ModelConfigServiceConfig = {
|
||||
modelIdResolutions: {
|
||||
'gemini-flash': {
|
||||
default: 'gemini-3-flash-preview',
|
||||
contexts: [
|
||||
{
|
||||
condition: {
|
||||
useGemini3_5Flash: false,
|
||||
hasAccessToPreview: false,
|
||||
},
|
||||
target: 'gemini-2.5-flash',
|
||||
},
|
||||
{
|
||||
condition: { useGemini3_5Flash: true },
|
||||
target: 'gemini-3.5-flash',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const service = new ModelConfigService(config);
|
||||
|
||||
// Case 1: GA Access granted
|
||||
expect(
|
||||
service.resolveModelId('gemini-flash', { useGemini3_5Flash: true }),
|
||||
).toBe('gemini-3.5-flash');
|
||||
|
||||
// Case 2: GA Access denied, but has preview access
|
||||
expect(
|
||||
service.resolveModelId('gemini-flash', {
|
||||
useGemini3_5Flash: false,
|
||||
hasAccessToPreview: true,
|
||||
}),
|
||||
).toBe('gemini-3-flash-preview');
|
||||
|
||||
// Case 3: GA Access denied AND no preview access
|
||||
expect(
|
||||
service.resolveModelId('gemini-flash', {
|
||||
useGemini3_5Flash: false,
|
||||
hasAccessToPreview: false,
|
||||
}),
|
||||
).toBe('gemini-2.5-flash');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAvailableModelOptions', () => {
|
||||
it('should filter out Pro models when hasAccessToProModel is false', () => {
|
||||
const config: ModelConfigServiceConfig = {
|
||||
|
||||
@@ -97,6 +97,7 @@ export interface ModelResolution {
|
||||
export interface ResolutionContext {
|
||||
useGemini3_1?: boolean;
|
||||
useGemini3_1FlashLite?: boolean;
|
||||
useGemini3_5Flash?: boolean;
|
||||
useCustomTools?: boolean;
|
||||
hasAccessToPreview?: boolean;
|
||||
hasAccessToProModel?: boolean;
|
||||
@@ -107,6 +108,7 @@ export interface ResolutionContext {
|
||||
export interface ResolutionCondition {
|
||||
useGemini3_1?: boolean;
|
||||
useGemini3_1FlashLite?: boolean;
|
||||
useGemini3_5Flash?: boolean;
|
||||
useCustomTools?: boolean;
|
||||
hasAccessToPreview?: boolean;
|
||||
/** Matches if the current model is in this list. */
|
||||
@@ -155,6 +157,7 @@ export class ModelConfigService {
|
||||
const definitions = this.config.modelDefinitions ?? {};
|
||||
const shouldShowPreviewModels = context.hasAccessToPreview ?? false;
|
||||
const useGemini31 = context.useGemini3_1 ?? false;
|
||||
const useGemini3_5Flash = context.useGemini3_5Flash ?? false;
|
||||
|
||||
const mainOptions = Object.entries(definitions)
|
||||
.filter(([_, m]) => {
|
||||
@@ -169,6 +172,7 @@ export class ModelConfigService {
|
||||
description = getAutoModelDescription(
|
||||
shouldShowPreviewModels,
|
||||
useGemini31,
|
||||
useGemini3_5Flash,
|
||||
);
|
||||
} else if (id === 'auto-gemini-3' && useGemini31) {
|
||||
description = description.replace('gemini-3-pro', 'gemini-3.1-pro');
|
||||
@@ -250,6 +254,8 @@ export class ModelConfigService {
|
||||
return value === context.useGemini3_1;
|
||||
case 'useGemini3_1FlashLite':
|
||||
return value === context.useGemini3_1FlashLite;
|
||||
case 'useGemini3_5Flash':
|
||||
return value === context.useGemini3_5Flash;
|
||||
case 'useCustomTools':
|
||||
return value === context.useCustomTools;
|
||||
case 'hasAccessToPreview':
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
} from './sandboxManager.js';
|
||||
import type { SandboxConfig } from '../config/config.js';
|
||||
import { killProcessGroup } from '../utils/process-utils.js';
|
||||
import { isNodeError } from '../utils/errors.js';
|
||||
import {
|
||||
ExecutionLifecycleService,
|
||||
type ExecutionHandle,
|
||||
@@ -1507,31 +1508,45 @@ export class ShellExecutionService {
|
||||
}
|
||||
|
||||
const activePty = this.activePtys.get(pid);
|
||||
if (activePty) {
|
||||
try {
|
||||
activePty.ptyProcess.resize(cols, rows);
|
||||
activePty.headlessTerminal.resize(cols, rows);
|
||||
} catch (e) {
|
||||
// Ignore errors if the pty has already exited, which can happen
|
||||
// due to a race condition between the exit event and this call.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const err = e as { code?: string; message?: string };
|
||||
const isEsrch = err.code === 'ESRCH';
|
||||
const isEbadf = err.code === 'EBADF' || err.message?.includes('EBADF');
|
||||
const isWindowsPtyError = err.message?.includes(
|
||||
'Cannot resize a pty that has already exited',
|
||||
);
|
||||
if (!activePty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEsrch || isEbadf || isWindowsPtyError) {
|
||||
// On Unix, we get an ESRCH or EBADF error.
|
||||
// On Windows, we get a message-based error.
|
||||
// In both cases, it's safe to ignore.
|
||||
} else {
|
||||
throw e;
|
||||
// Skip Windows: process.kill(pid, 0) is heavy and native errors are catchable there.
|
||||
if (process.platform !== 'win32') {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
} catch (e) {
|
||||
// Bail only if the process is explicitly confirmed dead (ESRCH).
|
||||
if (isNodeError(e) && e.code === 'ESRCH') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
activePty.ptyProcess.resize(cols, rows);
|
||||
activePty.headlessTerminal.resize(cols, rows);
|
||||
} catch (e) {
|
||||
// Ignore errors if the pty has already exited, which can happen
|
||||
// due to a race condition between the exit event and this call.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const err = e as { code?: string; message?: string };
|
||||
const isEsrch = err.code === 'ESRCH';
|
||||
const isEbadf = err.code === 'EBADF' || err.message?.includes('EBADF');
|
||||
const isWindowsPtyError = err.message?.includes(
|
||||
'Cannot resize a pty that has already exited',
|
||||
);
|
||||
|
||||
if (isEsrch || isEbadf || isWindowsPtyError) {
|
||||
// On Unix, we get an ESRCH or EBADF error.
|
||||
// On Windows, we get a message-based error.
|
||||
// In both cases, it's safe to ignore.
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Force emit the new state after resize
|
||||
if (activePty) {
|
||||
const endLine = activePty.headlessTerminal.buffer.active.length;
|
||||
|
||||
@@ -145,6 +145,18 @@
|
||||
"topK": 64
|
||||
}
|
||||
},
|
||||
"gemini-3.5-flash": {
|
||||
"model": "gemini-3.5-flash",
|
||||
"generateContentConfig": {
|
||||
"temperature": 1,
|
||||
"topP": 0.95,
|
||||
"thinkingConfig": {
|
||||
"includeThoughts": true,
|
||||
"thinkingLevel": "HIGH"
|
||||
},
|
||||
"topK": 64
|
||||
}
|
||||
},
|
||||
"gemma-4-31b-it": {
|
||||
"model": "gemma-4-31b-it",
|
||||
"generateContentConfig": {
|
||||
@@ -183,6 +195,13 @@
|
||||
"topP": 1
|
||||
}
|
||||
},
|
||||
"gemini-3.5-flash-base": {
|
||||
"model": "gemini-3.5-flash",
|
||||
"generateContentConfig": {
|
||||
"temperature": 0,
|
||||
"topP": 1
|
||||
}
|
||||
},
|
||||
"classifier": {
|
||||
"model": "flash-lite",
|
||||
"generateContentConfig": {
|
||||
|
||||
@@ -145,6 +145,18 @@
|
||||
"topK": 64
|
||||
}
|
||||
},
|
||||
"gemini-3.5-flash": {
|
||||
"model": "gemini-3.5-flash",
|
||||
"generateContentConfig": {
|
||||
"temperature": 1,
|
||||
"topP": 0.95,
|
||||
"thinkingConfig": {
|
||||
"includeThoughts": true,
|
||||
"thinkingLevel": "HIGH"
|
||||
},
|
||||
"topK": 64
|
||||
}
|
||||
},
|
||||
"gemma-4-31b-it": {
|
||||
"model": "gemma-4-31b-it",
|
||||
"generateContentConfig": {
|
||||
@@ -183,6 +195,13 @@
|
||||
"topP": 1
|
||||
}
|
||||
},
|
||||
"gemini-3.5-flash-base": {
|
||||
"model": "gemini-3.5-flash",
|
||||
"generateContentConfig": {
|
||||
"temperature": 0,
|
||||
"topP": 1
|
||||
}
|
||||
},
|
||||
"classifier": {
|
||||
"model": "flash-lite",
|
||||
"generateContentConfig": {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: antigravity-support
|
||||
description: Use when the user asks questions, seeks help, or requests instructions related to installing, setting up, or migrating to Antigravity CLI. This skill provides the latest up to date details, requirements, and commands sourced from the official Antigravity CLI documentation.
|
||||
---
|
||||
|
||||
# Antigravity CLI Support
|
||||
|
||||
This skill provides up-to-date information on how to install, configure, use, and migrate to Antigravity CLI, sourced from the official documentation at https://antigravity.google/docs/cli-getting-started.
|
||||
|
||||
## What is Antigravity CLI?
|
||||
|
||||
Antigravity CLI is a next-generation terminal interface for collaborating with autonomous agents on local codebases. It is designed to be highly interactive and agent-driven, launching a Terminal User Interface (TUI) to coordinate code generation, reasoning, and workspace tasks.
|
||||
|
||||
Key Features:
|
||||
- **Autonomous Agent Collaboration:** Work directly with agents within your terminal.
|
||||
- **Interactive TUI:** A full terminal user interface designed for agent workflows.
|
||||
- **Workspace Integration:** Deep understanding of your local workspace structure and context.
|
||||
|
||||
## Installation
|
||||
|
||||
To install the Antigravity CLI on your machine:
|
||||
|
||||
### macOS / Linux (Fast-Path Script)
|
||||
Run the following standard curl command in your terminal:
|
||||
```bash
|
||||
curl -fsSL https://antigravity.google/cli/install.sh | bash
|
||||
```
|
||||
This script downloads, verifies, and installs the latest version of Antigravity, and automatically registers the `agy` binary in your PATH.
|
||||
|
||||
### Windows (PowerShell)
|
||||
For Windows environments, install via the official PowerShell setup command:
|
||||
```powershell
|
||||
irm https://antigravity.google/cli/install.ps1 | iex
|
||||
```
|
||||
|
||||
## Initial Setup & Configuration
|
||||
|
||||
Once installed, navigate to any project or workspace directory and run:
|
||||
```bash
|
||||
agy
|
||||
```
|
||||
This command starts the Antigravity CLI. The first time you launch it, the interactive TUI will guide you through:
|
||||
1. **Workspace Trust Verification:** Confirming trust for the workspace folder to allow secure local command execution and file edits.
|
||||
2. **Visual Theme Configuration:** Setting up your preferred interactive terminal aesthetic and layout.
|
||||
3. **Rendering Modes:** Tailoring TUI performance and drawing behaviors to your terminal capabilities.
|
||||
|
||||
## How to Migrate to Antigravity CLI
|
||||
|
||||
If you are transitioning or migrating from another tool (such as Gemini CLI) to Antigravity CLI, follow these steps:
|
||||
1. **Check Requirements:** Ensure your local environment meets standard requirements (e.g., node, git, shell access) and is running a compatible operating system (macOS, Linux, or Windows).
|
||||
2. **Install Antigravity:** Run the installation script above to make the `agy` command globally available.
|
||||
3. **Verify Installation:** Test the installation by running `agy --version` or launching `agy` in an empty or sample directory.
|
||||
4. **Transition Workspaces:** Run `agy` directly inside your project workspace root. The initial setup assistant will guide you to import or configure trust policies, similar to those you might have used previously.
|
||||
|
||||
## Official Resources and Learning More
|
||||
|
||||
If you need more details or have advanced configuration/migration needs, please visit the official documentation:
|
||||
- **Official Documentation:** https://antigravity.google/docs/cli-getting-started
|
||||
@@ -271,4 +271,19 @@ description: Test sanitization
|
||||
expect(skills).toHaveLength(1);
|
||||
expect(skills[0].name).toBe('gke-prs-troubleshooter');
|
||||
});
|
||||
|
||||
it('should load real built-in antigravity-support skill successfully', async () => {
|
||||
const { fileURLToPath } = await import('node:url');
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const builtinDir = path.resolve(__dirname, 'builtin');
|
||||
const skills = await loadSkillsFromDir(builtinDir);
|
||||
const antigravitySkill = skills.find(
|
||||
(s) => s.name === 'antigravity-support',
|
||||
);
|
||||
expect(antigravitySkill).toBeDefined();
|
||||
expect(antigravitySkill!.description).toContain('Antigravity CLI');
|
||||
expect(antigravitySkill!.body).toContain(
|
||||
'https://antigravity.google/docs/cli-getting-started',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1061,18 +1061,24 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
||||
}
|
||||
signal.removeEventListener('abort', onAbort);
|
||||
timeoutController.signal.removeEventListener('abort', onAbort);
|
||||
if (tempFilePath) {
|
||||
try {
|
||||
await fsPromises.unlink(tempFilePath);
|
||||
} catch {
|
||||
// Ignore errors during unlink
|
||||
|
||||
// Only clean up if NOT running in background.
|
||||
// Background processes need the temp directory and PID file to remain
|
||||
// available until they exit.
|
||||
if (!this.params.is_background) {
|
||||
if (tempFilePath) {
|
||||
try {
|
||||
await fsPromises.unlink(tempFilePath);
|
||||
} catch {
|
||||
// Ignore errors during unlink
|
||||
}
|
||||
}
|
||||
}
|
||||
if (tempDir) {
|
||||
try {
|
||||
await fsPromises.rm(tempDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// Ignore errors during rm
|
||||
if (tempDir) {
|
||||
try {
|
||||
await fsPromises.rm(tempDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// Ignore errors during rm
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-devtools",
|
||||
"version": "0.45.0-nightly.20260521.g854f811be",
|
||||
"version": "0.45.2",
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"main": "dist/src/index.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-sdk",
|
||||
"version": "0.45.0-nightly.20260521.g854f811be",
|
||||
"version": "0.45.2",
|
||||
"description": "Gemini CLI SDK",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-test-utils",
|
||||
"version": "0.45.0-nightly.20260521.g854f811be",
|
||||
"version": "0.45.2",
|
||||
"private": true,
|
||||
"main": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "gemini-cli-vscode-ide-companion",
|
||||
"displayName": "Gemini CLI Companion",
|
||||
"description": "Enable Gemini CLI with direct access to your IDE workspace.",
|
||||
"version": "0.45.0-nightly.20260521.g854f811be",
|
||||
"version": "0.45.2",
|
||||
"publisher": "google",
|
||||
"icon": "assets/icon.png",
|
||||
"repository": {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user