mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-11 09:26:24 -07:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a119e6ff7 | |||
| 1d5077cf0d | |||
| 5f181f96f8 | |||
| 44f9b590eb | |||
| 61096af29c | |||
| e781537294 |
@@ -7,5 +7,8 @@
|
||||
},
|
||||
"general": {
|
||||
"devtools": true
|
||||
},
|
||||
"model": {
|
||||
"gemma4Variant": "gemma-4-31b-it"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ Learn all about Gemini CLI in our [documentation](https://geminicli.com/docs/).
|
||||
|
||||
## 🚀 Why Gemini CLI?
|
||||
|
||||
- **🚀 Gemma 4 Support**: Route requests to Gemma 4 models (`gemma-4-26b-a4b-it`
|
||||
or `gemma-4-31b-it`) using the `model.gemma4Variant` configuration setting.
|
||||
|
||||
- **🎯 Free tier**: 60 requests/min and 1,000 requests/day with personal Google
|
||||
account.
|
||||
- **🧠 Powerful Gemini 3 models**: Access to improved reasoning and 1M token
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# Gemma 4 Routing
|
||||
|
||||
Gemini CLI allows you to seamlessly route your requests to Gemma 4 models. When
|
||||
enabled, requests that would normally be sent to standard Gemini Pro and Flash
|
||||
models are automatically redirected to your chosen Gemma 4 variant.
|
||||
|
||||
## Configuration
|
||||
|
||||
You can enable Gemma 4 routing using the CLI settings or by modifying your
|
||||
`settings.json` file.
|
||||
|
||||
### Via Settings UI
|
||||
|
||||
1. Open the settings dialog by running `/settings`.
|
||||
2. Navigate to the **Model** section.
|
||||
3. Locate the **Gemma 4 Variant** setting.
|
||||
4. Select your preferred model:
|
||||
- `gemma-4-26b-a4b-it` (Gemma 4 26B A4B IT)
|
||||
- `gemma-4-31b-it` (Gemma 4 31B IT)
|
||||
5. Save the settings and restart the CLI if prompted.
|
||||
|
||||
### Via `settings.json`
|
||||
|
||||
You can also set this directly in your `.gemini/settings.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": {
|
||||
"gemma4Variant": "gemma-4-31b-it"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
When a `gemma4Variant` is selected, Gemini CLI intercepts model resolution:
|
||||
|
||||
- Requests for `gemini-pro`, `gemini-flash`, and their associated aliases (like
|
||||
`auto`, `pro`, `flash`) are routed to the selected Gemma 4 model.
|
||||
- The **router model** (`flash-lite`) remains unaffected and continues to use
|
||||
`gemini-2.5-flash-lite`. This ensures fast, lightweight background routing
|
||||
tasks continue to operate optimally.
|
||||
- If you do not have preview model access, the CLI normally falls back to stable
|
||||
models; however, the Gemma 4 variant will still take precedence for Pro and
|
||||
Flash targets.
|
||||
|
||||
To disable Gemma 4 routing, simply remove the `gemma4Variant` configuration from
|
||||
your settings or set it to `undefined`/empty in the UI.
|
||||
@@ -171,6 +171,10 @@
|
||||
"label": "Project context (GEMINI.md)",
|
||||
"slug": "docs/cli/gemini-md"
|
||||
},
|
||||
{
|
||||
"label": "Gemma 4 Routing",
|
||||
"slug": "docs/cli/gemma4-routing"
|
||||
},
|
||||
{ "label": "Settings", "slug": "docs/cli/settings" },
|
||||
{
|
||||
"label": "System prompt override",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I am Gemma 4."}],"role":"model"},"index":0,"finishReason":"STOP"}]}]}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { TestRig } from './test-helper.js';
|
||||
|
||||
describe('gemma4 routing', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rig.cleanup();
|
||||
});
|
||||
|
||||
it('routes gemini-pro to gemma-4-31b-it when configured', async () => {
|
||||
await rig.setup('gemma4-routing', {
|
||||
fakeResponsesPath: join(
|
||||
dirname(fileURLToPath(import.meta.url)),
|
||||
'gemma4-routing.responses',
|
||||
),
|
||||
settings: {
|
||||
model: {
|
||||
gemma4Variant: 'gemma-4-31b-it',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// We don't need real responses since we're just checking the routing/telemetry
|
||||
// But TestRig might require them if it actually tries to call the API.
|
||||
// Let's use a simple prompt.
|
||||
await rig.run({
|
||||
args: ['--model', 'gemini-2.5-pro', 'Hello!'],
|
||||
});
|
||||
|
||||
const hasApiRequestEvent = await rig.waitForTelemetryEvent('api_request');
|
||||
expect(hasApiRequestEvent).toBe(true);
|
||||
|
||||
const lastRequest = rig.readLastApiRequest();
|
||||
|
||||
expect(lastRequest).not.toBeNull();
|
||||
// The telemetry logger records the final requested model ID in the 'model' attribute
|
||||
expect(lastRequest?.attributes?.model).toBe('gemma-4-31b-it');
|
||||
});
|
||||
});
|
||||
@@ -913,6 +913,7 @@ export async function loadCliConfig(
|
||||
debugMode,
|
||||
question,
|
||||
worktreeSettings,
|
||||
gemma4Variant: settings.model?.gemma4Variant,
|
||||
|
||||
coreTools: settings.tools?.core || undefined,
|
||||
allowedTools: allowedTools.length > 0 ? allowedTools : undefined,
|
||||
|
||||
@@ -1047,6 +1047,23 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Skip the next speaker check.',
|
||||
showInDialog: true,
|
||||
},
|
||||
gemma4Variant: {
|
||||
type: 'enum',
|
||||
label: 'Gemma 4 Variant',
|
||||
category: 'Model',
|
||||
requiresRestart: true,
|
||||
default: undefined as
|
||||
| 'gemma-4-26b-a4b-it'
|
||||
| 'gemma-4-31b-it'
|
||||
| undefined,
|
||||
description:
|
||||
'Select which Gemma 4 model variant to use when routing Gemini Pro and Flash requests.',
|
||||
showInDialog: true,
|
||||
options: [
|
||||
{ value: 'gemma-4-26b-a4b-it', label: 'Gemma 4 26B A4B IT' },
|
||||
{ value: 'gemma-4-31b-it', label: 'Gemma 4 31B IT' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ export const CodebaseInvestigatorAgent = (
|
||||
): LocalAgentDefinition<typeof CodebaseInvestigationReportSchema> => {
|
||||
// Use Preview Flash model if the main model supports modern features.
|
||||
// If the main model is not a modern model, use the default pro model.
|
||||
const model = supportsModernFeatures(config.getModel())
|
||||
const model = supportsModernFeatures(config.getModel(), config)
|
||||
? PREVIEW_GEMINI_FLASH_MODEL
|
||||
: DEFAULT_GEMINI_MODEL;
|
||||
|
||||
@@ -97,7 +97,7 @@ export const CodebaseInvestigatorAgent = (
|
||||
generateContentConfig: {
|
||||
temperature: 0.1,
|
||||
topP: 0.95,
|
||||
thinkingConfig: supportsModernFeatures(model)
|
||||
thinkingConfig: supportsModernFeatures(model, config)
|
||||
? {
|
||||
includeThoughts: true,
|
||||
thinkingLevel: ThinkingLevel.HIGH,
|
||||
|
||||
@@ -726,6 +726,7 @@ export interface ConfigParameters {
|
||||
billing?: {
|
||||
overageStrategy?: OverageStrategy;
|
||||
};
|
||||
gemma4Variant?: 'gemma-4-26b-a4b-it' | 'gemma-4-31b-it';
|
||||
}
|
||||
|
||||
export class Config implements McpContext, AgentLoopContext {
|
||||
@@ -759,6 +760,10 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
private readonly debugMode: boolean;
|
||||
private readonly question: string | undefined;
|
||||
private readonly worktreeSettings: WorktreeSettings | undefined;
|
||||
private readonly gemma4Variant:
|
||||
| 'gemma-4-26b-a4b-it'
|
||||
| 'gemma-4-31b-it'
|
||||
| undefined;
|
||||
readonly enableConseca: boolean;
|
||||
|
||||
private readonly coreTools: string[] | undefined;
|
||||
@@ -1027,6 +1032,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
this.debugMode = params.debugMode;
|
||||
this.question = params.question;
|
||||
this.worktreeSettings = params.worktreeSettings;
|
||||
this.gemma4Variant = params.gemma4Variant;
|
||||
|
||||
this.coreTools = params.coreTools;
|
||||
this.mainAgentTools = params.mainAgentTools;
|
||||
@@ -1755,6 +1761,10 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
return this.worktreeSettings;
|
||||
}
|
||||
|
||||
getGemma4Variant(): 'gemma-4-26b-a4b-it' | 'gemma-4-31b-it' | undefined {
|
||||
return this.gemma4Variant;
|
||||
}
|
||||
|
||||
getClientName(): string | undefined {
|
||||
return this.clientName;
|
||||
}
|
||||
|
||||
@@ -89,6 +89,18 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
model: 'gemini-2.5-flash-lite',
|
||||
},
|
||||
},
|
||||
'gemma-4-26b-a4b-it': {
|
||||
extends: 'chat-base',
|
||||
modelConfig: {
|
||||
model: 'gemma-4-26b-a4b-it',
|
||||
},
|
||||
},
|
||||
'gemma-4-31b-it': {
|
||||
extends: 'chat-base',
|
||||
modelConfig: {
|
||||
model: 'gemma-4-31b-it',
|
||||
},
|
||||
},
|
||||
// Bases for the internal model configs.
|
||||
'gemini-2.5-flash-base': {
|
||||
extends: 'base',
|
||||
@@ -317,6 +329,20 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
isVisible: true,
|
||||
features: { thinking: false, multimodalToolUse: false },
|
||||
},
|
||||
'gemma-4-26b-a4b-it': {
|
||||
tier: 'pro',
|
||||
family: 'gemma-4',
|
||||
isPreview: true,
|
||||
isVisible: true,
|
||||
features: { thinking: true, multimodalToolUse: false },
|
||||
},
|
||||
'gemma-4-31b-it': {
|
||||
tier: 'pro',
|
||||
family: 'gemma-4',
|
||||
isPreview: true,
|
||||
isVisible: true,
|
||||
features: { thinking: false, multimodalToolUse: false },
|
||||
},
|
||||
// Aliases
|
||||
auto: {
|
||||
tier: 'auto',
|
||||
@@ -362,9 +388,43 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
},
|
||||
},
|
||||
modelIdResolutions: {
|
||||
'gemini-2.5-flash': {
|
||||
default: 'gemini-2.5-flash',
|
||||
contexts: [
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-26b-a4b-it' },
|
||||
target: 'gemma-4-26b-a4b-it',
|
||||
},
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-31b-it' },
|
||||
target: 'gemma-4-31b-it',
|
||||
},
|
||||
],
|
||||
},
|
||||
'gemini-2.5-pro': {
|
||||
default: 'gemini-2.5-pro',
|
||||
contexts: [
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-26b-a4b-it' },
|
||||
target: 'gemma-4-26b-a4b-it',
|
||||
},
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-31b-it' },
|
||||
target: 'gemma-4-31b-it',
|
||||
},
|
||||
],
|
||||
},
|
||||
'gemini-3.1-pro-preview': {
|
||||
default: 'gemini-3.1-pro-preview',
|
||||
contexts: [
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-26b-a4b-it' },
|
||||
target: 'gemma-4-26b-a4b-it',
|
||||
},
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-31b-it' },
|
||||
target: 'gemma-4-31b-it',
|
||||
},
|
||||
{ condition: { hasAccessToPreview: false }, target: 'gemini-2.5-pro' },
|
||||
{
|
||||
condition: { useCustomTools: true },
|
||||
@@ -375,12 +435,28 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
'gemini-3.1-pro-preview-customtools': {
|
||||
default: 'gemini-3.1-pro-preview-customtools',
|
||||
contexts: [
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-26b-a4b-it' },
|
||||
target: 'gemma-4-26b-a4b-it',
|
||||
},
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-31b-it' },
|
||||
target: 'gemma-4-31b-it',
|
||||
},
|
||||
{ condition: { hasAccessToPreview: false }, target: 'gemini-2.5-pro' },
|
||||
],
|
||||
},
|
||||
'gemini-3-flash-preview': {
|
||||
default: 'gemini-3-flash-preview',
|
||||
contexts: [
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-26b-a4b-it' },
|
||||
target: 'gemma-4-26b-a4b-it',
|
||||
},
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-31b-it' },
|
||||
target: 'gemma-4-31b-it',
|
||||
},
|
||||
{
|
||||
condition: { hasAccessToPreview: false },
|
||||
target: 'gemini-2.5-flash',
|
||||
@@ -390,6 +466,14 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
'gemini-3-pro-preview': {
|
||||
default: 'gemini-3-pro-preview',
|
||||
contexts: [
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-26b-a4b-it' },
|
||||
target: 'gemma-4-26b-a4b-it',
|
||||
},
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-31b-it' },
|
||||
target: 'gemma-4-31b-it',
|
||||
},
|
||||
{ condition: { hasAccessToPreview: false }, target: 'gemini-2.5-pro' },
|
||||
{
|
||||
condition: { useGemini3_1: true, useCustomTools: true },
|
||||
@@ -404,6 +488,14 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
'auto-gemini-3': {
|
||||
default: 'gemini-3-pro-preview',
|
||||
contexts: [
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-26b-a4b-it' },
|
||||
target: 'gemma-4-26b-a4b-it',
|
||||
},
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-31b-it' },
|
||||
target: 'gemma-4-31b-it',
|
||||
},
|
||||
{ condition: { hasAccessToPreview: false }, target: 'gemini-2.5-pro' },
|
||||
{
|
||||
condition: { useGemini3_1: true, useCustomTools: true },
|
||||
@@ -418,6 +510,14 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
auto: {
|
||||
default: 'gemini-3-pro-preview',
|
||||
contexts: [
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-26b-a4b-it' },
|
||||
target: 'gemma-4-26b-a4b-it',
|
||||
},
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-31b-it' },
|
||||
target: 'gemma-4-31b-it',
|
||||
},
|
||||
{ condition: { hasAccessToPreview: false }, target: 'gemini-2.5-pro' },
|
||||
{
|
||||
condition: { useGemini3_1: true, useCustomTools: true },
|
||||
@@ -432,6 +532,14 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
pro: {
|
||||
default: 'gemini-3-pro-preview',
|
||||
contexts: [
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-26b-a4b-it' },
|
||||
target: 'gemma-4-26b-a4b-it',
|
||||
},
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-31b-it' },
|
||||
target: 'gemma-4-31b-it',
|
||||
},
|
||||
{ condition: { hasAccessToPreview: false }, target: 'gemini-2.5-pro' },
|
||||
{
|
||||
condition: { useGemini3_1: true, useCustomTools: true },
|
||||
@@ -445,6 +553,16 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
},
|
||||
'auto-gemini-2.5': {
|
||||
default: 'gemini-2.5-pro',
|
||||
contexts: [
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-26b-a4b-it' },
|
||||
target: 'gemma-4-26b-a4b-it',
|
||||
},
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-31b-it' },
|
||||
target: 'gemma-4-31b-it',
|
||||
},
|
||||
],
|
||||
},
|
||||
'gemini-3.1-flash-lite-preview': {
|
||||
default: 'gemini-3.1-flash-lite-preview',
|
||||
@@ -458,6 +576,14 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
flash: {
|
||||
default: 'gemini-3-flash-preview',
|
||||
contexts: [
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-26b-a4b-it' },
|
||||
target: 'gemma-4-26b-a4b-it',
|
||||
},
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-31b-it' },
|
||||
target: 'gemma-4-31b-it',
|
||||
},
|
||||
{
|
||||
condition: { hasAccessToPreview: false },
|
||||
target: 'gemini-2.5-flash',
|
||||
@@ -478,6 +604,22 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
flash: {
|
||||
default: 'gemini-3-flash-preview',
|
||||
contexts: [
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-26b-a4b-it' },
|
||||
target: 'gemma-4-26b-a4b-it',
|
||||
},
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-31b-it' },
|
||||
target: 'gemma-4-31b-it',
|
||||
},
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-26b-a4b-it' },
|
||||
target: 'gemma-4-26b-a4b-it',
|
||||
},
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-31b-it' },
|
||||
target: 'gemma-4-31b-it',
|
||||
},
|
||||
{
|
||||
condition: { requestedModels: ['auto-gemini-2.5', 'gemini-2.5-pro'] },
|
||||
target: 'gemini-2.5-flash',
|
||||
@@ -493,6 +635,22 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
pro: {
|
||||
default: 'gemini-3-pro-preview',
|
||||
contexts: [
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-26b-a4b-it' },
|
||||
target: 'gemma-4-26b-a4b-it',
|
||||
},
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-31b-it' },
|
||||
target: 'gemma-4-31b-it',
|
||||
},
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-26b-a4b-it' },
|
||||
target: 'gemma-4-26b-a4b-it',
|
||||
},
|
||||
{
|
||||
condition: { gemma4Variant: 'gemma-4-31b-it' },
|
||||
target: 'gemma-4-31b-it',
|
||||
},
|
||||
{
|
||||
condition: { requestedModels: ['auto-gemini-2.5', 'gemini-2.5-pro'] },
|
||||
target: 'gemini-2.5-pro',
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface ModelResolutionContext {
|
||||
useCustomTools?: boolean;
|
||||
hasAccessToPreview?: boolean;
|
||||
requestedModel?: string;
|
||||
gemma4Variant?: 'gemma-4-26b-a4b-it' | 'gemma-4-31b-it';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,6 +49,7 @@ export interface IModelConfigService {
|
||||
export interface ModelCapabilityContext {
|
||||
readonly modelConfigService: IModelConfigService;
|
||||
getExperimentalDynamicModelConfiguration(): boolean;
|
||||
getGemma4Variant?: () => 'gemma-4-26b-a4b-it' | 'gemma-4-31b-it' | undefined;
|
||||
}
|
||||
|
||||
export const PREVIEW_GEMINI_MODEL = 'gemini-3-pro-preview';
|
||||
@@ -60,6 +62,8 @@ export const PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL =
|
||||
export const DEFAULT_GEMINI_MODEL = 'gemini-2.5-pro';
|
||||
export const DEFAULT_GEMINI_FLASH_MODEL = 'gemini-2.5-flash';
|
||||
export const DEFAULT_GEMINI_FLASH_LITE_MODEL = 'gemini-2.5-flash-lite';
|
||||
export const GEMMA_4_26B_IT = 'gemma-4-26b-a4b-it';
|
||||
export const GEMMA_4_31B_IT = 'gemma-4-31b-it';
|
||||
|
||||
export const VALID_GEMINI_MODELS = new Set([
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
@@ -70,6 +74,8 @@ export const VALID_GEMINI_MODELS = new Set([
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||
GEMMA_4_26B_IT,
|
||||
GEMMA_4_31B_IT,
|
||||
]);
|
||||
|
||||
export const PREVIEW_GEMINI_MODEL_AUTO = 'auto-gemini-3';
|
||||
@@ -109,6 +115,7 @@ export function resolveModel(
|
||||
useGemini3_1FlashLite,
|
||||
useCustomTools: useCustomToolModel,
|
||||
hasAccessToPreview,
|
||||
gemma4Variant: config.getGemma4Variant?.(),
|
||||
});
|
||||
|
||||
if (!hasAccessToPreview && isPreviewModel(resolved, config)) {
|
||||
@@ -160,6 +167,17 @@ export function resolveModel(
|
||||
}
|
||||
}
|
||||
|
||||
const variant = config?.getGemma4Variant?.();
|
||||
if (
|
||||
variant &&
|
||||
(resolved === PREVIEW_GEMINI_MODEL ||
|
||||
resolved === DEFAULT_GEMINI_MODEL ||
|
||||
resolved === PREVIEW_GEMINI_FLASH_MODEL ||
|
||||
resolved === DEFAULT_GEMINI_FLASH_MODEL)
|
||||
) {
|
||||
return variant;
|
||||
}
|
||||
|
||||
if (!hasAccessToPreview && isPreviewModel(resolved)) {
|
||||
// Downgrade to stable models if user lacks preview access.
|
||||
switch (resolved) {
|
||||
@@ -214,6 +232,7 @@ export function resolveClassifierModel(
|
||||
useGemini3_1FlashLite,
|
||||
useCustomTools: useCustomToolModel,
|
||||
hasAccessToPreview,
|
||||
gemma4Variant: config.getGemma4Variant?.(),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -378,11 +397,21 @@ export function isCustomModel(
|
||||
* This includes Gemini 3 models and any custom models.
|
||||
*
|
||||
* @param model The model name to check.
|
||||
* @param config Optional config object for dynamic model configuration.
|
||||
* @returns True if the model supports modern features like thoughts.
|
||||
*/
|
||||
export function supportsModernFeatures(model: string): boolean {
|
||||
export function supportsModernFeatures(
|
||||
model: string,
|
||||
config?: ModelCapabilityContext,
|
||||
): boolean {
|
||||
if (config?.getExperimentalDynamicModelConfiguration?.() === true) {
|
||||
const definition = config.modelConfigService.getModelDefinition(model);
|
||||
if (definition?.features?.thinking !== undefined) {
|
||||
return definition.features.thinking;
|
||||
}
|
||||
}
|
||||
if (isGemini3Model(model)) return true;
|
||||
return isCustomModel(model);
|
||||
return isCustomModel(model, config);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -569,7 +569,10 @@ export class GeminiChat {
|
||||
abortSignal,
|
||||
};
|
||||
|
||||
let contentsToUse: Content[] = supportsModernFeatures(modelToUse)
|
||||
let contentsToUse: Content[] = supportsModernFeatures(
|
||||
modelToUse,
|
||||
this.context.config,
|
||||
)
|
||||
? [...contentsForPreviewModel]
|
||||
: [...requestContents];
|
||||
|
||||
@@ -613,7 +616,10 @@ export class GeminiChat {
|
||||
);
|
||||
lastModelToUse = modelToUse;
|
||||
// Re-evaluate contentsToUse based on the new model's feature support
|
||||
contentsToUse = supportsModernFeatures(modelToUse)
|
||||
contentsToUse = supportsModernFeatures(
|
||||
modelToUse,
|
||||
this.context.config,
|
||||
)
|
||||
? [...contentsForPreviewModel]
|
||||
: [...requestContents];
|
||||
}
|
||||
@@ -1072,9 +1078,13 @@ export class GeminiChat {
|
||||
}
|
||||
|
||||
const thoughtPart = content.parts[0];
|
||||
if (thoughtPart.text) {
|
||||
const rawText =
|
||||
typeof thoughtPart.thought === 'string'
|
||||
? thoughtPart.thought
|
||||
: (thoughtPart.text ?? '');
|
||||
|
||||
if (rawText) {
|
||||
// Extract subject and description using the same logic as turn.ts
|
||||
const rawText = thoughtPart.text;
|
||||
const subjectStringMatches = rawText.match(/\*\*(.*?)\*\*/s);
|
||||
const subject = subjectStringMatches
|
||||
? subjectStringMatches[1].trim()
|
||||
|
||||
@@ -733,7 +733,7 @@ describe('Turn', () => {
|
||||
},
|
||||
{
|
||||
description: 'should yield thought events with traceId',
|
||||
part: { text: '[Thought: thinking]', thought: 'thinking' },
|
||||
part: { text: '[Thought: thinking]', thought: true },
|
||||
responseId: 'trace-456',
|
||||
expectedEvent: {
|
||||
type: GeminiEventType.Thought,
|
||||
@@ -774,7 +774,7 @@ describe('Turn', () => {
|
||||
{
|
||||
content: {
|
||||
parts: [
|
||||
{ text: '**Planning** the solution', thought: 'planning' },
|
||||
{ text: '**Planning** the solution', thought: true },
|
||||
{ text: 'I will help you with that.' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -308,7 +308,11 @@ export class Turn {
|
||||
const parts = resp.candidates?.[0]?.content?.parts ?? [];
|
||||
for (const part of parts) {
|
||||
if (part.thought) {
|
||||
const thought = parseThought(part.text ?? '');
|
||||
const rawThought =
|
||||
typeof part.thought === 'string'
|
||||
? part.thought
|
||||
: (part.text ?? '');
|
||||
const thought = parseThought(rawThought);
|
||||
yield {
|
||||
type: GeminiEventType.Thought,
|
||||
value: thought,
|
||||
|
||||
@@ -69,7 +69,7 @@ export class PromptProvider {
|
||||
context.config.getHasAccessToPreviewModel?.() ?? true,
|
||||
context.config,
|
||||
);
|
||||
const isModernModel = supportsModernFeatures(desiredModel);
|
||||
const isModernModel = supportsModernFeatures(desiredModel, context.config);
|
||||
const activeSnippets = isModernModel ? snippets : legacySnippets;
|
||||
const contextFilenames = getAllGeminiMdFilenames();
|
||||
|
||||
@@ -280,7 +280,7 @@ export class PromptProvider {
|
||||
context.config.getHasAccessToPreviewModel?.() ?? true,
|
||||
context.config,
|
||||
);
|
||||
const isModernModel = supportsModernFeatures(desiredModel);
|
||||
const isModernModel = supportsModernFeatures(desiredModel, context.config);
|
||||
const activeSnippets = isModernModel ? snippets : legacySnippets;
|
||||
return activeSnippets.getCompressionPrompt(
|
||||
context.config.getApprovedPlanPath(),
|
||||
|
||||
@@ -383,6 +383,56 @@ describe('ClassifierStrategy', () => {
|
||||
expect(decision?.model).toBe(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
});
|
||||
|
||||
it('should skip classification if both pro and flash resolve to the same model', async () => {
|
||||
// We mock the config to trigger the fast path by returning a specific model
|
||||
// that the router will see as identical for both 'pro' and 'flash' tiers.
|
||||
vi.mocked(mockConfig.getModel).mockReturnValue(DEFAULT_GEMINI_MODEL_AUTO);
|
||||
|
||||
// By overriding the modelConfigService, we can simulate gemma4Variant
|
||||
// or any other scenario where both tiers resolve to the same target model.
|
||||
const mockResolveClassifierModelId = vi
|
||||
.fn()
|
||||
.mockReturnValue('gemma-4-31b-it');
|
||||
Object.defineProperty(
|
||||
mockConfig.modelConfigService,
|
||||
'resolveClassifierModelId',
|
||||
{
|
||||
value: mockResolveClassifierModelId,
|
||||
writable: true,
|
||||
},
|
||||
);
|
||||
|
||||
// We also need to mock config.getExperimentalDynamicModelConfiguration()
|
||||
// if that is what resolveClassifierModel uses. Since resolveClassifierModel
|
||||
// is a standalone function, we can mock its behavior indirectly via config.
|
||||
Object.defineProperty(
|
||||
mockConfig,
|
||||
'getExperimentalDynamicModelConfiguration',
|
||||
{
|
||||
value: vi.fn().mockReturnValue(true),
|
||||
writable: true,
|
||||
},
|
||||
);
|
||||
|
||||
const decision = await strategy.route(
|
||||
mockContext,
|
||||
mockConfig,
|
||||
mockBaseLlmClient,
|
||||
mockLocalLiteRtLmClient,
|
||||
);
|
||||
|
||||
expect(decision).toEqual({
|
||||
model: 'gemma-4-31b-it',
|
||||
metadata: {
|
||||
source: 'classifier',
|
||||
latencyMs: 0,
|
||||
reasoning:
|
||||
'Skipped classification because both tiers resolve to the same model: gemma-4-31b-it',
|
||||
},
|
||||
});
|
||||
expect(mockBaseLlmClient.generateJson).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('Gemini 3.1 and Custom Tools Routing', () => {
|
||||
it('should route to PREVIEW_GEMINI_3_1_MODEL when Gemini 3.1 is launched', async () => {
|
||||
vi.mocked(mockConfig.getGemini31Launched).mockResolvedValue(true);
|
||||
|
||||
@@ -137,6 +137,47 @@ export class ClassifierStrategy implements RoutingStrategy {
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
const model = context.requestedModel ?? config.getModel();
|
||||
|
||||
const [useGemini3_1, useGemini3_1FlashLite, useCustomToolModel] =
|
||||
await Promise.all([
|
||||
config.getGemini31Launched(),
|
||||
config.getGemini31FlashLiteLaunched(),
|
||||
config.getUseCustomToolModel(),
|
||||
]);
|
||||
|
||||
const hasAccessToPreview = config.getHasAccessToPreviewModel?.() ?? true;
|
||||
|
||||
// Check if classification is redundant (i.e., both tiers resolve to the same model)
|
||||
const proModel = resolveClassifierModel(
|
||||
model,
|
||||
'pro',
|
||||
useGemini3_1,
|
||||
useGemini3_1FlashLite,
|
||||
useCustomToolModel,
|
||||
hasAccessToPreview,
|
||||
config,
|
||||
);
|
||||
const flashModel = resolveClassifierModel(
|
||||
model,
|
||||
'flash',
|
||||
useGemini3_1,
|
||||
useGemini3_1FlashLite,
|
||||
useCustomToolModel,
|
||||
hasAccessToPreview,
|
||||
config,
|
||||
);
|
||||
|
||||
if (proModel === flashModel) {
|
||||
return {
|
||||
model: proModel,
|
||||
metadata: {
|
||||
source: this.name,
|
||||
latencyMs: 0,
|
||||
reasoning: `Skipped classification because both tiers resolve to the same model: ${proModel}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
(await config.getNumericalRoutingEnabled()) &&
|
||||
isGemini3Model(model, config)
|
||||
@@ -171,19 +212,13 @@ export class ClassifierStrategy implements RoutingStrategy {
|
||||
|
||||
const reasoning = routerResponse.reasoning;
|
||||
const latencyMs = Date.now() - startTime;
|
||||
const [useGemini3_1, useGemini3_1FlashLite, useCustomToolModel] =
|
||||
await Promise.all([
|
||||
config.getGemini31Launched(),
|
||||
config.getGemini31FlashLiteLaunched(),
|
||||
config.getUseCustomToolModel(),
|
||||
]);
|
||||
const selectedModel = resolveClassifierModel(
|
||||
model,
|
||||
routerResponse.model_choice,
|
||||
useGemini3_1,
|
||||
useGemini3_1FlashLite,
|
||||
useCustomToolModel,
|
||||
config.getHasAccessToPreviewModel?.() ?? true,
|
||||
hasAccessToPreview,
|
||||
config,
|
||||
);
|
||||
|
||||
|
||||
@@ -32,6 +32,9 @@ describe('GemmaClassifierStrategy', () => {
|
||||
mockGenerateJson = vi.fn();
|
||||
|
||||
mockConfig = {
|
||||
modelConfigService: {
|
||||
resolveClassifierModelId: vi.fn(),
|
||||
},
|
||||
getGemmaModelRouterSettings: vi.fn().mockReturnValue({
|
||||
enabled: true,
|
||||
classifier: { model: 'gemma3-1b-gpu-custom' },
|
||||
@@ -83,6 +86,44 @@ describe('GemmaClassifierStrategy', () => {
|
||||
).rejects.toThrow('Only gemma3-1b-gpu-custom has been tested');
|
||||
});
|
||||
|
||||
it('should skip classification if both pro and flash resolve to the same model', async () => {
|
||||
// Setup the mock config to use the dynamic model config service.
|
||||
Object.defineProperty(
|
||||
mockConfig,
|
||||
'getExperimentalDynamicModelConfiguration',
|
||||
{
|
||||
value: vi.fn().mockReturnValue(true),
|
||||
writable: true,
|
||||
},
|
||||
);
|
||||
Object.defineProperty(
|
||||
mockConfig.modelConfigService,
|
||||
'resolveClassifierModelId',
|
||||
{
|
||||
value: vi.fn().mockReturnValue('gemma-4-31b-it'),
|
||||
writable: true,
|
||||
},
|
||||
);
|
||||
|
||||
const decision = await strategy.route(
|
||||
mockContext,
|
||||
mockConfig,
|
||||
mockBaseLlmClient,
|
||||
mockLocalLiteRtLmClient,
|
||||
);
|
||||
|
||||
expect(decision).toEqual({
|
||||
model: 'gemma-4-31b-it',
|
||||
metadata: {
|
||||
source: 'gemma-classifier',
|
||||
latencyMs: 0,
|
||||
reasoning:
|
||||
'Skipped classification because both tiers resolve to the same model: gemma-4-31b-it',
|
||||
},
|
||||
});
|
||||
expect(mockGenerateJson).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call generateJson with the correct parameters', async () => {
|
||||
const mockApiResponse = {
|
||||
reasoning: 'Simple task',
|
||||
|
||||
@@ -178,6 +178,39 @@ ${formattedHistory}
|
||||
return null;
|
||||
}
|
||||
|
||||
const model = context.requestedModel ?? config.getModel();
|
||||
|
||||
// Check if classification is redundant (i.e., both tiers resolve to the same model)
|
||||
const proModel = resolveClassifierModel(
|
||||
model,
|
||||
'pro',
|
||||
false, // useGemini3_1
|
||||
false, // useGemini3_1FlashLite
|
||||
false, // useCustomToolModel
|
||||
true, // hasAccessToPreview
|
||||
config,
|
||||
);
|
||||
const flashModel = resolveClassifierModel(
|
||||
model,
|
||||
'flash',
|
||||
false, // useGemini3_1
|
||||
false, // useGemini3_1FlashLite
|
||||
false, // useCustomToolModel
|
||||
true, // hasAccessToPreview
|
||||
config,
|
||||
);
|
||||
|
||||
if (proModel === flashModel) {
|
||||
return {
|
||||
model: proModel,
|
||||
metadata: {
|
||||
source: this.name,
|
||||
latencyMs: 0,
|
||||
reasoning: `Skipped classification because both tiers resolve to the same model: ${proModel}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Only the gemma3-1b-gpu-custom model has been tested and verified.
|
||||
if (gemmaRouterSettings.classifier?.model !== 'gemma3-1b-gpu-custom') {
|
||||
throw new Error('Only gemma3-1b-gpu-custom has been tested');
|
||||
@@ -210,8 +243,13 @@ ${formattedHistory}
|
||||
const reasoning = routerResponse.reasoning;
|
||||
const latencyMs = Date.now() - startTime;
|
||||
const selectedModel = resolveClassifierModel(
|
||||
context.requestedModel ?? config.getModel(),
|
||||
model,
|
||||
routerResponse.model_choice,
|
||||
false, // useGemini3_1
|
||||
false, // useGemini3_1FlashLite
|
||||
false, // useCustomToolModel
|
||||
true, // hasAccessToPreview
|
||||
config,
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -101,6 +101,7 @@ export interface ResolutionContext {
|
||||
hasAccessToPreview?: boolean;
|
||||
hasAccessToProModel?: boolean;
|
||||
requestedModel?: string;
|
||||
gemma4Variant?: 'gemma-4-26b-a4b-it' | 'gemma-4-31b-it';
|
||||
}
|
||||
|
||||
/** The requirements defined in the registry. */
|
||||
@@ -109,6 +110,7 @@ export interface ResolutionCondition {
|
||||
useGemini3_1FlashLite?: boolean;
|
||||
useCustomTools?: boolean;
|
||||
hasAccessToPreview?: boolean;
|
||||
gemma4Variant?: 'gemma-4-26b-a4b-it' | 'gemma-4-31b-it';
|
||||
/** Matches if the current model is in this list. */
|
||||
requestedModels?: string[];
|
||||
}
|
||||
@@ -252,6 +254,8 @@ export class ModelConfigService {
|
||||
return value === context.useCustomTools;
|
||||
case 'hasAccessToPreview':
|
||||
return value === context.hasAccessToPreview;
|
||||
case 'gemma4Variant':
|
||||
return value === context.gemma4Variant;
|
||||
case 'requestedModels':
|
||||
return (
|
||||
Array.isArray(value) &&
|
||||
|
||||
@@ -97,6 +97,28 @@
|
||||
"topK": 64
|
||||
}
|
||||
},
|
||||
"gemma-4-26b-a4b-it": {
|
||||
"model": "gemma-4-26b-a4b-it",
|
||||
"generateContentConfig": {
|
||||
"temperature": 1,
|
||||
"topP": 0.95,
|
||||
"thinkingConfig": {
|
||||
"includeThoughts": true
|
||||
},
|
||||
"topK": 64
|
||||
}
|
||||
},
|
||||
"gemma-4-31b-it": {
|
||||
"model": "gemma-4-31b-it",
|
||||
"generateContentConfig": {
|
||||
"temperature": 1,
|
||||
"topP": 0.95,
|
||||
"thinkingConfig": {
|
||||
"includeThoughts": true
|
||||
},
|
||||
"topK": 64
|
||||
}
|
||||
},
|
||||
"gemini-2.5-flash-base": {
|
||||
"model": "gemini-2.5-flash",
|
||||
"generateContentConfig": {
|
||||
|
||||
@@ -97,6 +97,28 @@
|
||||
"topK": 64
|
||||
}
|
||||
},
|
||||
"gemma-4-26b-a4b-it": {
|
||||
"model": "gemma-4-26b-a4b-it",
|
||||
"generateContentConfig": {
|
||||
"temperature": 1,
|
||||
"topP": 0.95,
|
||||
"thinkingConfig": {
|
||||
"includeThoughts": true
|
||||
},
|
||||
"topK": 64
|
||||
}
|
||||
},
|
||||
"gemma-4-31b-it": {
|
||||
"model": "gemma-4-31b-it",
|
||||
"generateContentConfig": {
|
||||
"temperature": 1,
|
||||
"topP": 0.95,
|
||||
"thinkingConfig": {
|
||||
"includeThoughts": true
|
||||
},
|
||||
"topK": 64
|
||||
}
|
||||
},
|
||||
"gemini-2.5-flash-base": {
|
||||
"model": "gemini-2.5-flash",
|
||||
"generateContentConfig": {
|
||||
|
||||
Reference in New Issue
Block a user