mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-30 16:00:41 -07:00
feat(core): Add ModelConfigService. (#12556)
This commit is contained in:
@@ -296,6 +296,20 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Description:** Skip the next speaker check.
|
||||
- **Default:** `true`
|
||||
|
||||
#### `modelConfigs`
|
||||
|
||||
- **`modelConfigs.aliases`** (object):
|
||||
- **Description:** Named presets for model configs. Can be used in place of a
|
||||
model name and can inherit from other aliases using an `extends` property.
|
||||
- **Default:**
|
||||
`{"base":{"modelConfig":{"generateContentConfig":{"temperature":0,"topP":1}}},"chat-base":{"extends":"base","modelConfig":{"generateContentConfig":{"thinkingConfig":{"includeThoughts":true,"thinkingBudget":-1}}}},"gemini-2.5-pro":{"extends":"chat-base","modelConfig":{"model":"gemini-2.5-pro"}},"gemini-2.5-flash":{"extends":"chat-base","modelConfig":{"model":"gemini-2.5-flash"}},"gemini-2.5-flash-lite":{"extends":"chat-base","modelConfig":{"model":"gemini-2.5-flash-lite"}},"classifier":{"extends":"base","modelConfig":{"model":"gemini-2.5-flash-lite","generateContentConfig":{"maxOutputTokens":1024,"thinkingConfig":{"thinkingBudget":512}}}},"prompt-completion":{"extends":"base","modelConfig":{"model":"gemini-2.5-flash-lite","generateContentConfig":{"temperature":0.3,"maxOutputTokens":16000,"thinkingConfig":{"thinkingBudget":0}}}},"edit-corrector":{"extends":"base","modelConfig":{"model":"gemini-2.5-flash-lite","generateContentConfig":{"thinkingConfig":{"thinkingBudget":0}}}},"summarizer-default":{"extends":"base","modelConfig":{"model":"gemini-2.5-flash-lite","generateContentConfig":{"maxOutputTokens":2000}}},"summarizer-shell":{"extends":"base","modelConfig":{"model":"gemini-2.5-flash-lite","generateContentConfig":{"maxOutputTokens":2000}}},"web-search-tool":{"extends":"base","modelConfig":{"model":"gemini-2.5-flash","generateContentConfig":{"tools":[{"googleSearch":{}}]}}},"web-fetch-tool":{"extends":"base","modelConfig":{"model":"gemini-2.5-flash","generateContentConfig":{"tools":[{"urlContext":{}}]}}}}`
|
||||
|
||||
- **`modelConfigs.overrides`** (array):
|
||||
- **Description:** Apply specific configuration overrides based on matches,
|
||||
with a primary key of model (or alias). The most specific match will be
|
||||
used.
|
||||
- **Default:** `[]`
|
||||
|
||||
#### `context`
|
||||
|
||||
- **`context.fileName`** (string | string[]):
|
||||
|
||||
@@ -669,6 +669,7 @@ export async function loadCliConfig(
|
||||
recordResponses: argv.recordResponses,
|
||||
retryFetchErrors: settings.general?.retryFetchErrors ?? false,
|
||||
ptyInfo: ptyInfo?.name,
|
||||
modelConfigServiceConfig: settings.modelConfigs,
|
||||
// TODO: loading of hooks based on workspace trust
|
||||
enableHooks: settings.tools?.enableHooks ?? false,
|
||||
hooks: settings.hooks || {},
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
|
||||
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
DEFAULT_MODEL_CONFIGS,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { CustomTheme } from '../ui/themes/theme.js';
|
||||
import type { SessionRetentionSettings } from './settings.js';
|
||||
@@ -680,6 +681,38 @@ const SETTINGS_SCHEMA = {
|
||||
},
|
||||
},
|
||||
|
||||
modelConfigs: {
|
||||
type: 'object',
|
||||
label: 'Model Configs',
|
||||
category: 'Model',
|
||||
requiresRestart: false,
|
||||
default: DEFAULT_MODEL_CONFIGS,
|
||||
description: 'Model configurations.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
aliases: {
|
||||
type: 'object',
|
||||
label: 'Model Config Aliases',
|
||||
category: 'Model',
|
||||
requiresRestart: false,
|
||||
default: DEFAULT_MODEL_CONFIGS.aliases,
|
||||
description:
|
||||
'Named presets for model configs. Can be used in place of a model name and can inherit from other aliases using an `extends` property.',
|
||||
showInDialog: false,
|
||||
},
|
||||
overrides: {
|
||||
type: 'array',
|
||||
label: 'Model Config Overrides',
|
||||
category: 'Model',
|
||||
requiresRestart: false,
|
||||
default: [],
|
||||
description:
|
||||
'Apply specific configuration overrides based on matches, with a primary key of model (or alias). The most specific match will be used.',
|
||||
showInDialog: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
context: {
|
||||
type: 'object',
|
||||
label: 'Context',
|
||||
|
||||
@@ -31,6 +31,7 @@ import { RipGrepTool, canUseRipgrep } from '../tools/ripGrep.js';
|
||||
import { logRipgrepFallback } from '../telemetry/loggers.js';
|
||||
import { RipgrepFallbackEvent } from '../telemetry/types.js';
|
||||
import { ToolRegistry } from '../tools/tool-registry.js';
|
||||
import { DEFAULT_MODEL_CONFIGS } from './defaultModelConfigs.js';
|
||||
|
||||
vi.mock('fs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('fs')>();
|
||||
@@ -1249,6 +1250,92 @@ describe('BaseLlmClient Lifecycle', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Generation Config Merging (HACK)', () => {
|
||||
const MODEL = 'gemini-pro';
|
||||
const SANDBOX: SandboxConfig = {
|
||||
command: 'docker',
|
||||
image: 'gemini-cli-sandbox',
|
||||
};
|
||||
const TARGET_DIR = '/path/to/target';
|
||||
const DEBUG_MODE = false;
|
||||
const QUESTION = 'test question';
|
||||
const USER_MEMORY = 'Test User Memory';
|
||||
const TELEMETRY_SETTINGS = { enabled: false };
|
||||
const EMBEDDING_MODEL = 'gemini-embedding';
|
||||
const SESSION_ID = 'test-session-id';
|
||||
const baseParams: ConfigParameters = {
|
||||
cwd: '/tmp',
|
||||
embeddingModel: EMBEDDING_MODEL,
|
||||
sandbox: SANDBOX,
|
||||
targetDir: TARGET_DIR,
|
||||
debugMode: DEBUG_MODE,
|
||||
question: QUESTION,
|
||||
userMemory: USER_MEMORY,
|
||||
telemetry: TELEMETRY_SETTINGS,
|
||||
sessionId: SESSION_ID,
|
||||
model: MODEL,
|
||||
usageStatisticsEnabled: false,
|
||||
};
|
||||
|
||||
it('should merge default aliases when user provides only overrides', () => {
|
||||
const userOverrides = [
|
||||
{
|
||||
match: { model: 'test-model' },
|
||||
modelConfig: { generateContentConfig: { temperature: 0.1 } },
|
||||
},
|
||||
];
|
||||
|
||||
const params: ConfigParameters = {
|
||||
...baseParams,
|
||||
modelConfigServiceConfig: {
|
||||
overrides: userOverrides,
|
||||
},
|
||||
};
|
||||
|
||||
const config = new Config(params);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const serviceConfig = (config.modelConfigService as any).config;
|
||||
|
||||
// Assert that the default aliases are present
|
||||
expect(serviceConfig.aliases).toEqual(DEFAULT_MODEL_CONFIGS.aliases);
|
||||
// Assert that the user's overrides are present
|
||||
expect(serviceConfig.overrides).toEqual(userOverrides);
|
||||
});
|
||||
|
||||
it('should use user-provided aliases if they exist', () => {
|
||||
const userAliases = {
|
||||
'my-alias': {
|
||||
modelConfig: { model: 'my-model' },
|
||||
},
|
||||
};
|
||||
|
||||
const params: ConfigParameters = {
|
||||
...baseParams,
|
||||
modelConfigServiceConfig: {
|
||||
aliases: userAliases,
|
||||
},
|
||||
};
|
||||
|
||||
const config = new Config(params);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const serviceConfig = (config.modelConfigService as any).config;
|
||||
|
||||
// Assert that the user's aliases are used, not the defaults
|
||||
expect(serviceConfig.aliases).toEqual(userAliases);
|
||||
});
|
||||
|
||||
it('should use default generation config if none is provided', () => {
|
||||
const params: ConfigParameters = { ...baseParams };
|
||||
|
||||
const config = new Config(params);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const serviceConfig = (config.modelConfigService as any).config;
|
||||
|
||||
// Assert that the full default config is used
|
||||
expect(serviceConfig).toEqual(DEFAULT_MODEL_CONFIGS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Config getHooks', () => {
|
||||
const baseParams: ConfigParameters = {
|
||||
cwd: '/tmp',
|
||||
|
||||
@@ -62,6 +62,9 @@ import { RipgrepFallbackEvent } from '../telemetry/types.js';
|
||||
import type { FallbackModelHandler } from '../fallback/types.js';
|
||||
import { ModelRouterService } from '../routing/modelRouterService.js';
|
||||
import { OutputFormat } from '../output/types.js';
|
||||
import type { ModelConfigServiceConfig } from '../services/modelConfigService.js';
|
||||
import { ModelConfigService } from '../services/modelConfigService.js';
|
||||
import { DEFAULT_MODEL_CONFIGS } from './defaultModelConfigs.js';
|
||||
|
||||
// Re-export OAuth config type
|
||||
export type { MCPOAuthConfig, AnyToolInvocation };
|
||||
@@ -291,6 +294,7 @@ export interface ConfigParameters {
|
||||
recordResponses?: string;
|
||||
ptyInfo?: string;
|
||||
disableYoloMode?: boolean;
|
||||
modelConfigServiceConfig?: ModelConfigServiceConfig;
|
||||
enableHooks?: boolean;
|
||||
experiments?: Experiments;
|
||||
hooks?: {
|
||||
@@ -309,6 +313,7 @@ export class Config {
|
||||
private fileSystemService: FileSystemService;
|
||||
private contentGeneratorConfig!: ContentGeneratorConfig;
|
||||
private contentGenerator!: ContentGenerator;
|
||||
readonly modelConfigService: ModelConfigService;
|
||||
private readonly embeddingModel: string;
|
||||
private readonly sandbox: SandboxConfig | undefined;
|
||||
private readonly targetDir: string;
|
||||
@@ -560,6 +565,25 @@ export class Config {
|
||||
}
|
||||
this.geminiClient = new GeminiClient(this);
|
||||
this.modelRouterService = new ModelRouterService(this);
|
||||
|
||||
// HACK: The settings loading logic doesn't currently merge the default
|
||||
// generation config with the user's settings. This means if a user provides
|
||||
// any `generation` settings (e.g., just `overrides`), the default `aliases`
|
||||
// are lost. This hack manually merges the default aliases back in if they
|
||||
// are missing from the user's config.
|
||||
// TODO(12593): Fix the settings loading logic to properly merge defaults and
|
||||
// remove this hack.
|
||||
let modelConfigServiceConfig = params.modelConfigServiceConfig;
|
||||
if (modelConfigServiceConfig && !modelConfigServiceConfig.aliases) {
|
||||
modelConfigServiceConfig = {
|
||||
...modelConfigServiceConfig,
|
||||
aliases: DEFAULT_MODEL_CONFIGS.aliases,
|
||||
};
|
||||
}
|
||||
|
||||
this.modelConfigService = new ModelConfigService(
|
||||
modelConfigServiceConfig ?? DEFAULT_MODEL_CONFIGS,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
129
packages/core/src/config/defaultModelConfigs.ts
Normal file
129
packages/core/src/config/defaultModelConfigs.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { ModelConfigServiceConfig } from '../services/modelConfigService.js';
|
||||
|
||||
// The default model configs. We use `base` as the parent for all of our model
|
||||
// configs, while `chat-base`, a child of `base`, is the parent of the models
|
||||
// we use in the "chat" experience.
|
||||
export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
aliases: {
|
||||
base: {
|
||||
modelConfig: {
|
||||
generateContentConfig: {
|
||||
temperature: 0,
|
||||
topP: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
'chat-base': {
|
||||
extends: 'base',
|
||||
modelConfig: {
|
||||
generateContentConfig: {
|
||||
thinkingConfig: {
|
||||
includeThoughts: true,
|
||||
thinkingBudget: -1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// Because `gemini-2.5-pro` and related model configs are "user-facing"
|
||||
// today, i.e. they could be passed via `--model`, we have to be careful to
|
||||
// ensure these model configs can be used interactively.
|
||||
// TODO(joshualitt): Introduce internal base configs for the various models,
|
||||
// note: we will have to think carefully about names.
|
||||
'gemini-2.5-pro': {
|
||||
extends: 'chat-base',
|
||||
modelConfig: {
|
||||
model: 'gemini-2.5-pro',
|
||||
},
|
||||
},
|
||||
'gemini-2.5-flash': {
|
||||
extends: 'chat-base',
|
||||
modelConfig: {
|
||||
model: 'gemini-2.5-flash',
|
||||
},
|
||||
},
|
||||
'gemini-2.5-flash-lite': {
|
||||
extends: 'chat-base',
|
||||
modelConfig: {
|
||||
model: 'gemini-2.5-flash-lite',
|
||||
},
|
||||
},
|
||||
classifier: {
|
||||
extends: 'base',
|
||||
modelConfig: {
|
||||
model: 'gemini-2.5-flash-lite',
|
||||
generateContentConfig: {
|
||||
maxOutputTokens: 1024,
|
||||
thinkingConfig: {
|
||||
thinkingBudget: 512,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'prompt-completion': {
|
||||
extends: 'base',
|
||||
modelConfig: {
|
||||
model: 'gemini-2.5-flash-lite',
|
||||
generateContentConfig: {
|
||||
temperature: 0.3,
|
||||
maxOutputTokens: 16000,
|
||||
thinkingConfig: {
|
||||
thinkingBudget: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'edit-corrector': {
|
||||
extends: 'base',
|
||||
modelConfig: {
|
||||
model: 'gemini-2.5-flash-lite',
|
||||
generateContentConfig: {
|
||||
thinkingConfig: {
|
||||
thinkingBudget: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'summarizer-default': {
|
||||
extends: 'base',
|
||||
modelConfig: {
|
||||
model: 'gemini-2.5-flash-lite',
|
||||
generateContentConfig: {
|
||||
maxOutputTokens: 2000,
|
||||
},
|
||||
},
|
||||
},
|
||||
'summarizer-shell': {
|
||||
extends: 'base',
|
||||
modelConfig: {
|
||||
model: 'gemini-2.5-flash-lite',
|
||||
generateContentConfig: {
|
||||
maxOutputTokens: 2000,
|
||||
},
|
||||
},
|
||||
},
|
||||
'web-search-tool': {
|
||||
extends: 'base',
|
||||
modelConfig: {
|
||||
model: 'gemini-2.5-flash',
|
||||
generateContentConfig: {
|
||||
tools: [{ googleSearch: {} }],
|
||||
},
|
||||
},
|
||||
},
|
||||
'web-fetch-tool': {
|
||||
extends: 'base',
|
||||
modelConfig: {
|
||||
model: 'gemini-2.5-flash',
|
||||
generateContentConfig: {
|
||||
tools: [{ urlContext: {} }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
// Export config
|
||||
export * from './config/config.js';
|
||||
export * from './config/defaultModelConfigs.js';
|
||||
export * from './output/types.js';
|
||||
export * from './output/json-formatter.js';
|
||||
export * from './output/stream-json-formatter.js';
|
||||
|
||||
63
packages/core/src/services/modelConfig.golden.test.ts
Normal file
63
packages/core/src/services/modelConfig.golden.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
import { ModelConfigService } from './modelConfigService.js';
|
||||
import { DEFAULT_MODEL_CONFIGS } from '../config/defaultModelConfigs.js';
|
||||
|
||||
const GOLDEN_FILE_PATH = path.resolve(
|
||||
process.cwd(),
|
||||
'src',
|
||||
'services',
|
||||
'test-data',
|
||||
'resolved-aliases.golden.json',
|
||||
);
|
||||
|
||||
describe('ModelConfigService Golden Test', () => {
|
||||
it('should match the golden file for resolved default aliases', async () => {
|
||||
const service = new ModelConfigService(DEFAULT_MODEL_CONFIGS);
|
||||
const aliases = Object.keys(DEFAULT_MODEL_CONFIGS.aliases ?? {});
|
||||
|
||||
const resolvedAliases: Record<string, unknown> = {};
|
||||
for (const alias of aliases) {
|
||||
resolvedAliases[alias] =
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(service as any).internalGetResolvedConfig({ model: alias });
|
||||
}
|
||||
|
||||
if (process.env['UPDATE_GOLDENS']) {
|
||||
await fs.mkdir(path.dirname(GOLDEN_FILE_PATH), { recursive: true });
|
||||
await fs.writeFile(
|
||||
GOLDEN_FILE_PATH,
|
||||
JSON.stringify(resolvedAliases, null, 2),
|
||||
'utf-8',
|
||||
);
|
||||
// In update mode, we pass the test after writing the file.
|
||||
return;
|
||||
}
|
||||
|
||||
let goldenContent: string;
|
||||
try {
|
||||
goldenContent = await fs.readFile(GOLDEN_FILE_PATH, 'utf-8');
|
||||
} catch (e) {
|
||||
if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
throw new Error(
|
||||
'Golden file not found. Run with `UPDATE_GOLDENS=true` to create it.',
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
const goldenData = JSON.parse(goldenContent);
|
||||
|
||||
expect(
|
||||
resolvedAliases,
|
||||
'Golden file mismatch. If the new resolved aliases are correct, run the test with `UPDATE_GOLDENS=true` to regenerate the golden file.',
|
||||
).toEqual(goldenData);
|
||||
});
|
||||
});
|
||||
234
packages/core/src/services/modelConfig.integration.test.ts
Normal file
234
packages/core/src/services/modelConfig.integration.test.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ModelConfigService } from './modelConfigService.js';
|
||||
import type { ModelConfigServiceConfig } from './modelConfigService.js';
|
||||
|
||||
// This test suite is designed to validate the end-to-end logic of the
|
||||
// ModelConfigService with a complex, realistic configuration.
|
||||
// It tests the interplay of global settings, alias inheritance, and overrides
|
||||
// of varying specificities.
|
||||
describe('ModelConfigService Integration', () => {
|
||||
const complexConfig: ModelConfigServiceConfig = {
|
||||
aliases: {
|
||||
// Abstract base with no model
|
||||
base: {
|
||||
modelConfig: {
|
||||
generateContentConfig: {
|
||||
topP: 0.95,
|
||||
topK: 64,
|
||||
},
|
||||
},
|
||||
},
|
||||
'default-text-model': {
|
||||
extends: 'base',
|
||||
modelConfig: {
|
||||
model: 'gemini-1.5-pro-latest',
|
||||
generateContentConfig: {
|
||||
topK: 40, // Override base
|
||||
},
|
||||
},
|
||||
},
|
||||
'creative-writer': {
|
||||
extends: 'default-text-model',
|
||||
modelConfig: {
|
||||
generateContentConfig: {
|
||||
temperature: 0.9, // Override global
|
||||
topK: 50, // Override parent
|
||||
},
|
||||
},
|
||||
},
|
||||
'fast-classifier': {
|
||||
extends: 'base',
|
||||
modelConfig: {
|
||||
model: 'gemini-1.5-flash-latest',
|
||||
generateContentConfig: {
|
||||
temperature: 0.1,
|
||||
candidateCount: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
overrides: [
|
||||
// Broad override for all flash models
|
||||
{
|
||||
match: { model: 'gemini-1.5-flash-latest' },
|
||||
modelConfig: {
|
||||
generateContentConfig: {
|
||||
maxOutputTokens: 2048,
|
||||
},
|
||||
},
|
||||
},
|
||||
// Specific override for the 'core' agent
|
||||
{
|
||||
match: { overrideScope: 'core' },
|
||||
modelConfig: {
|
||||
generateContentConfig: {
|
||||
temperature: 0.5,
|
||||
stopSequences: ['AGENT_STOP'],
|
||||
},
|
||||
},
|
||||
},
|
||||
// Highly specific override for the 'fast-classifier' when used by the 'core' agent
|
||||
{
|
||||
match: { model: 'fast-classifier', overrideScope: 'core' },
|
||||
modelConfig: {
|
||||
generateContentConfig: {
|
||||
temperature: 0.0,
|
||||
maxOutputTokens: 4096,
|
||||
},
|
||||
},
|
||||
},
|
||||
// Override to provide a model for the abstract alias
|
||||
{
|
||||
match: { model: 'base', overrideScope: 'core' },
|
||||
modelConfig: {
|
||||
model: 'gemini-1.5-pro-latest',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const service = new ModelConfigService(complexConfig);
|
||||
|
||||
it('should resolve a simple model, applying core agent defaults', () => {
|
||||
const resolved = service.getResolvedConfig({
|
||||
model: 'gemini-test-model',
|
||||
});
|
||||
|
||||
expect(resolved.model).toBe('gemini-test-model');
|
||||
expect(resolved.generateContentConfig).toEqual({
|
||||
temperature: 0.5, // from agent override
|
||||
stopSequences: ['AGENT_STOP'], // from agent override
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly apply a simple inherited alias and merge with global defaults', () => {
|
||||
const resolved = service.getResolvedConfig({
|
||||
model: 'default-text-model',
|
||||
});
|
||||
|
||||
expect(resolved.model).toBe('gemini-1.5-pro-latest'); // from alias
|
||||
expect(resolved.generateContentConfig).toEqual({
|
||||
temperature: 0.5, // from agent override
|
||||
topP: 0.95, // from base
|
||||
topK: 40, // from alias
|
||||
stopSequences: ['AGENT_STOP'], // from agent override
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve a multi-level inherited alias', () => {
|
||||
const resolved = service.getResolvedConfig({
|
||||
model: 'creative-writer',
|
||||
});
|
||||
|
||||
expect(resolved.model).toBe('gemini-1.5-pro-latest'); // from default-text-model
|
||||
expect(resolved.generateContentConfig).toEqual({
|
||||
temperature: 0.5, // from agent override
|
||||
topP: 0.95, // from base
|
||||
topK: 50, // from alias
|
||||
stopSequences: ['AGENT_STOP'], // from agent override
|
||||
});
|
||||
});
|
||||
|
||||
it('should apply an inherited alias and a broad model-based override', () => {
|
||||
const resolved = service.getResolvedConfig({
|
||||
model: 'fast-classifier',
|
||||
// No agent specified, so it should match core agent-specific rules
|
||||
});
|
||||
|
||||
expect(resolved.model).toBe('gemini-1.5-flash-latest'); // from alias
|
||||
expect(resolved.generateContentConfig).toEqual({
|
||||
topP: 0.95, // from base
|
||||
topK: 64, // from base
|
||||
candidateCount: 4, // from alias
|
||||
stopSequences: ['AGENT_STOP'], // from agent override
|
||||
maxOutputTokens: 4096, // from most specific override
|
||||
temperature: 0.0, // from most specific override
|
||||
});
|
||||
});
|
||||
|
||||
it('should apply settings for an unknown model but a known agent', () => {
|
||||
const resolved = service.getResolvedConfig({
|
||||
model: 'gemini-test-model',
|
||||
overrideScope: 'core',
|
||||
});
|
||||
|
||||
expect(resolved.model).toBe('gemini-test-model');
|
||||
expect(resolved.generateContentConfig).toEqual({
|
||||
temperature: 0.5, // from agent override
|
||||
stopSequences: ['AGENT_STOP'], // from agent override
|
||||
});
|
||||
});
|
||||
|
||||
it('should apply the most specific override for a known inherited alias and agent', () => {
|
||||
const resolved = service.getResolvedConfig({
|
||||
model: 'fast-classifier',
|
||||
overrideScope: 'core',
|
||||
});
|
||||
|
||||
expect(resolved.model).toBe('gemini-1.5-flash-latest');
|
||||
expect(resolved.generateContentConfig).toEqual({
|
||||
// Inherited from 'base'
|
||||
topP: 0.95,
|
||||
topK: 64,
|
||||
// From 'fast-classifier' alias
|
||||
candidateCount: 4,
|
||||
// From 'core' agent override
|
||||
stopSequences: ['AGENT_STOP'],
|
||||
// From most specific override (model+agent)
|
||||
temperature: 0.0,
|
||||
maxOutputTokens: 4096,
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly apply agent override on top of a multi-level inherited alias', () => {
|
||||
const resolved = service.getResolvedConfig({
|
||||
model: 'creative-writer',
|
||||
overrideScope: 'core',
|
||||
});
|
||||
|
||||
expect(resolved.model).toBe('gemini-1.5-pro-latest'); // from default-text-model
|
||||
expect(resolved.generateContentConfig).toEqual({
|
||||
temperature: 0.5, // from agent override (wins over alias)
|
||||
topP: 0.95, // from base
|
||||
topK: 50, // from creative-writer alias
|
||||
stopSequences: ['AGENT_STOP'], // from agent override
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve an abstract alias if a specific override provides the model', () => {
|
||||
const resolved = service.getResolvedConfig({
|
||||
model: 'base',
|
||||
overrideScope: 'core',
|
||||
});
|
||||
|
||||
expect(resolved.model).toBe('gemini-1.5-pro-latest'); // from override
|
||||
expect(resolved.generateContentConfig).toEqual({
|
||||
temperature: 0.5, // from agent override
|
||||
topP: 0.95, // from base alias
|
||||
topK: 64, // from base alias
|
||||
stopSequences: ['AGENT_STOP'], // from agent override
|
||||
});
|
||||
});
|
||||
|
||||
it('should not apply core agent overrides when a different agent is specified', () => {
|
||||
const resolved = service.getResolvedConfig({
|
||||
model: 'fast-classifier',
|
||||
overrideScope: 'non-core-agent',
|
||||
});
|
||||
|
||||
expect(resolved.model).toBe('gemini-1.5-flash-latest');
|
||||
expect(resolved.generateContentConfig).toEqual({
|
||||
candidateCount: 4, // from alias
|
||||
maxOutputTokens: 2048, // from override of model
|
||||
temperature: 0.1, // from alias
|
||||
topK: 64, // from base
|
||||
topP: 0.95, // from base
|
||||
});
|
||||
});
|
||||
});
|
||||
553
packages/core/src/services/modelConfigService.test.ts
Normal file
553
packages/core/src/services/modelConfigService.test.ts
Normal file
@@ -0,0 +1,553 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { ModelConfigServiceConfig } from './modelConfigService.js';
|
||||
import { ModelConfigService } from './modelConfigService.js';
|
||||
|
||||
describe('ModelConfigService', () => {
|
||||
it('should resolve a basic alias to its model and settings', () => {
|
||||
const config: ModelConfigServiceConfig = {
|
||||
aliases: {
|
||||
classifier: {
|
||||
modelConfig: {
|
||||
model: 'gemini-1.5-flash-latest',
|
||||
generateContentConfig: {
|
||||
temperature: 0,
|
||||
topP: 0.9,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
overrides: [],
|
||||
};
|
||||
const service = new ModelConfigService(config);
|
||||
const resolved = service.getResolvedConfig({ model: 'classifier' });
|
||||
|
||||
expect(resolved.model).toBe('gemini-1.5-flash-latest');
|
||||
expect(resolved.generateContentConfig).toEqual({
|
||||
temperature: 0,
|
||||
topP: 0.9,
|
||||
});
|
||||
});
|
||||
|
||||
it('should apply a simple override on top of an alias', () => {
|
||||
const config: ModelConfigServiceConfig = {
|
||||
aliases: {
|
||||
classifier: {
|
||||
modelConfig: {
|
||||
model: 'gemini-1.5-flash-latest',
|
||||
generateContentConfig: {
|
||||
temperature: 0,
|
||||
topP: 0.9,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
match: { model: 'classifier' },
|
||||
modelConfig: {
|
||||
generateContentConfig: {
|
||||
temperature: 0.5,
|
||||
maxOutputTokens: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const service = new ModelConfigService(config);
|
||||
const resolved = service.getResolvedConfig({ model: 'classifier' });
|
||||
|
||||
expect(resolved.model).toBe('gemini-1.5-flash-latest');
|
||||
expect(resolved.generateContentConfig).toEqual({
|
||||
temperature: 0.5,
|
||||
topP: 0.9,
|
||||
maxOutputTokens: 1000,
|
||||
});
|
||||
});
|
||||
|
||||
it('should apply the most specific override rule', () => {
|
||||
const config: ModelConfigServiceConfig = {
|
||||
aliases: {},
|
||||
overrides: [
|
||||
{
|
||||
match: { model: 'gemini-pro' },
|
||||
modelConfig: { generateContentConfig: { temperature: 0.5 } },
|
||||
},
|
||||
{
|
||||
match: { model: 'gemini-pro', overrideScope: 'my-agent' },
|
||||
modelConfig: { generateContentConfig: { temperature: 0.1 } },
|
||||
},
|
||||
],
|
||||
};
|
||||
const service = new ModelConfigService(config);
|
||||
const resolved = service.getResolvedConfig({
|
||||
model: 'gemini-pro',
|
||||
overrideScope: 'my-agent',
|
||||
});
|
||||
|
||||
expect(resolved.model).toBe('gemini-pro');
|
||||
expect(resolved.generateContentConfig).toEqual({ temperature: 0.1 });
|
||||
});
|
||||
|
||||
it('should use the last override in case of a tie in specificity', () => {
|
||||
const config: ModelConfigServiceConfig = {
|
||||
aliases: {},
|
||||
overrides: [
|
||||
{
|
||||
match: { model: 'gemini-pro' },
|
||||
modelConfig: {
|
||||
generateContentConfig: { temperature: 0.5, topP: 0.8 },
|
||||
},
|
||||
},
|
||||
{
|
||||
match: { model: 'gemini-pro' },
|
||||
modelConfig: { generateContentConfig: { temperature: 0.1 } },
|
||||
},
|
||||
],
|
||||
};
|
||||
const service = new ModelConfigService(config);
|
||||
const resolved = service.getResolvedConfig({ model: 'gemini-pro' });
|
||||
|
||||
expect(resolved.model).toBe('gemini-pro');
|
||||
expect(resolved.generateContentConfig).toEqual({
|
||||
temperature: 0.1,
|
||||
topP: 0.8,
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly pass through generation config from an alias', () => {
|
||||
const config: ModelConfigServiceConfig = {
|
||||
aliases: {
|
||||
'thinking-alias': {
|
||||
modelConfig: {
|
||||
model: 'gemini-pro',
|
||||
generateContentConfig: {
|
||||
candidateCount: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
overrides: [],
|
||||
};
|
||||
const service = new ModelConfigService(config);
|
||||
const resolved = service.getResolvedConfig({ model: 'thinking-alias' });
|
||||
|
||||
expect(resolved.generateContentConfig).toEqual({ candidateCount: 500 });
|
||||
});
|
||||
|
||||
it('should let an override generation config win over an alias config', () => {
|
||||
const config: ModelConfigServiceConfig = {
|
||||
aliases: {
|
||||
'thinking-alias': {
|
||||
modelConfig: {
|
||||
model: 'gemini-pro',
|
||||
generateContentConfig: {
|
||||
candidateCount: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
match: { model: 'thinking-alias' },
|
||||
modelConfig: {
|
||||
generateContentConfig: {
|
||||
candidateCount: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const service = new ModelConfigService(config);
|
||||
const resolved = service.getResolvedConfig({ model: 'thinking-alias' });
|
||||
|
||||
expect(resolved.generateContentConfig).toEqual({
|
||||
candidateCount: 1000,
|
||||
});
|
||||
});
|
||||
|
||||
it('should merge settings from global, alias, and multiple matching overrides', () => {
|
||||
const config: ModelConfigServiceConfig = {
|
||||
aliases: {
|
||||
'test-alias': {
|
||||
modelConfig: {
|
||||
model: 'gemini-test-model',
|
||||
generateContentConfig: {
|
||||
topP: 0.9,
|
||||
topK: 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
match: { model: 'gemini-test-model' },
|
||||
modelConfig: {
|
||||
generateContentConfig: {
|
||||
topK: 40,
|
||||
maxOutputTokens: 2048,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
match: { overrideScope: 'test-agent' },
|
||||
modelConfig: {
|
||||
generateContentConfig: {
|
||||
maxOutputTokens: 4096,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
match: { model: 'gemini-test-model', overrideScope: 'test-agent' },
|
||||
modelConfig: {
|
||||
generateContentConfig: {
|
||||
temperature: 0.2,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const service = new ModelConfigService(config);
|
||||
const resolved = service.getResolvedConfig({
|
||||
model: 'test-alias',
|
||||
overrideScope: 'test-agent',
|
||||
});
|
||||
|
||||
expect(resolved.model).toBe('gemini-test-model');
|
||||
expect(resolved.generateContentConfig).toEqual({
|
||||
// From global, overridden by most specific override
|
||||
temperature: 0.2,
|
||||
// From alias, not overridden
|
||||
topP: 0.9,
|
||||
// From alias, overridden by less specific override
|
||||
topK: 40,
|
||||
// From first matching override, overridden by second matching override
|
||||
maxOutputTokens: 4096,
|
||||
});
|
||||
});
|
||||
|
||||
it('should match an agent:core override when agent is undefined', () => {
|
||||
const config: ModelConfigServiceConfig = {
|
||||
aliases: {},
|
||||
overrides: [
|
||||
{
|
||||
match: { overrideScope: 'core' },
|
||||
modelConfig: {
|
||||
generateContentConfig: {
|
||||
temperature: 0.1,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const service = new ModelConfigService(config);
|
||||
const resolved = service.getResolvedConfig({
|
||||
model: 'gemini-pro',
|
||||
overrideScope: undefined, // Explicitly undefined
|
||||
});
|
||||
|
||||
expect(resolved.model).toBe('gemini-pro');
|
||||
expect(resolved.generateContentConfig).toEqual({
|
||||
temperature: 0.1,
|
||||
});
|
||||
});
|
||||
|
||||
describe('alias inheritance', () => {
|
||||
it('should resolve a simple "extends" chain', () => {
|
||||
const config: ModelConfigServiceConfig = {
|
||||
aliases: {
|
||||
base: {
|
||||
modelConfig: {
|
||||
model: 'gemini-1.5-pro-latest',
|
||||
generateContentConfig: {
|
||||
temperature: 0.7,
|
||||
topP: 0.9,
|
||||
},
|
||||
},
|
||||
},
|
||||
'flash-variant': {
|
||||
extends: 'base',
|
||||
modelConfig: {
|
||||
model: 'gemini-1.5-flash-latest',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const service = new ModelConfigService(config);
|
||||
const resolved = service.getResolvedConfig({ model: 'flash-variant' });
|
||||
|
||||
expect(resolved.model).toBe('gemini-1.5-flash-latest');
|
||||
expect(resolved.generateContentConfig).toEqual({
|
||||
temperature: 0.7,
|
||||
topP: 0.9,
|
||||
});
|
||||
});
|
||||
|
||||
it('should override parent properties from child alias', () => {
|
||||
const config: ModelConfigServiceConfig = {
|
||||
aliases: {
|
||||
base: {
|
||||
modelConfig: {
|
||||
model: 'gemini-1.5-pro-latest',
|
||||
generateContentConfig: {
|
||||
temperature: 0.7,
|
||||
topP: 0.9,
|
||||
},
|
||||
},
|
||||
},
|
||||
'flash-variant': {
|
||||
extends: 'base',
|
||||
modelConfig: {
|
||||
model: 'gemini-1.5-flash-latest',
|
||||
generateContentConfig: {
|
||||
temperature: 0.2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const service = new ModelConfigService(config);
|
||||
const resolved = service.getResolvedConfig({ model: 'flash-variant' });
|
||||
|
||||
expect(resolved.model).toBe('gemini-1.5-flash-latest');
|
||||
expect(resolved.generateContentConfig).toEqual({
|
||||
temperature: 0.2,
|
||||
topP: 0.9,
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve a multi-level "extends" chain', () => {
|
||||
const config: ModelConfigServiceConfig = {
|
||||
aliases: {
|
||||
base: {
|
||||
modelConfig: {
|
||||
model: 'gemini-1.5-pro-latest',
|
||||
generateContentConfig: {
|
||||
temperature: 0.7,
|
||||
topP: 0.9,
|
||||
},
|
||||
},
|
||||
},
|
||||
'base-flash': {
|
||||
extends: 'base',
|
||||
modelConfig: {
|
||||
model: 'gemini-1.5-flash-latest',
|
||||
},
|
||||
},
|
||||
'classifier-flash': {
|
||||
extends: 'base-flash',
|
||||
modelConfig: {
|
||||
generateContentConfig: {
|
||||
temperature: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const service = new ModelConfigService(config);
|
||||
const resolved = service.getResolvedConfig({
|
||||
model: 'classifier-flash',
|
||||
});
|
||||
|
||||
expect(resolved.model).toBe('gemini-1.5-flash-latest');
|
||||
expect(resolved.generateContentConfig).toEqual({
|
||||
temperature: 0,
|
||||
topP: 0.9,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error for circular dependencies', () => {
|
||||
const config: ModelConfigServiceConfig = {
|
||||
aliases: {
|
||||
a: { extends: 'b', modelConfig: {} },
|
||||
b: { extends: 'a', modelConfig: {} },
|
||||
},
|
||||
};
|
||||
const service = new ModelConfigService(config);
|
||||
expect(() => service.getResolvedConfig({ model: 'a' })).toThrow(
|
||||
'Circular alias dependency: a -> b -> a',
|
||||
);
|
||||
});
|
||||
|
||||
describe('abstract aliases', () => {
|
||||
it('should allow an alias to extend an abstract alias without a model', () => {
|
||||
const config: ModelConfigServiceConfig = {
|
||||
aliases: {
|
||||
'abstract-base': {
|
||||
modelConfig: {
|
||||
generateContentConfig: {
|
||||
temperature: 0.1,
|
||||
},
|
||||
},
|
||||
},
|
||||
'concrete-child': {
|
||||
extends: 'abstract-base',
|
||||
modelConfig: {
|
||||
model: 'gemini-1.5-pro-latest',
|
||||
generateContentConfig: {
|
||||
topP: 0.9,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const service = new ModelConfigService(config);
|
||||
const resolved = service.getResolvedConfig({ model: 'concrete-child' });
|
||||
|
||||
expect(resolved.model).toBe('gemini-1.5-pro-latest');
|
||||
expect(resolved.generateContentConfig).toEqual({
|
||||
temperature: 0.1,
|
||||
topP: 0.9,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if a resolved alias chain has no model', () => {
|
||||
const config: ModelConfigServiceConfig = {
|
||||
aliases: {
|
||||
'abstract-base': {
|
||||
modelConfig: {
|
||||
generateContentConfig: { temperature: 0.7 },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const service = new ModelConfigService(config);
|
||||
expect(() =>
|
||||
service.getResolvedConfig({ model: 'abstract-base' }),
|
||||
).toThrow(
|
||||
'Could not resolve a model name for alias "abstract-base". Please ensure the alias chain or a matching override specifies a model.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve an abstract alias if an override provides the model', () => {
|
||||
const config: ModelConfigServiceConfig = {
|
||||
aliases: {
|
||||
'abstract-base': {
|
||||
modelConfig: {
|
||||
generateContentConfig: {
|
||||
temperature: 0.1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
match: { model: 'abstract-base' },
|
||||
modelConfig: {
|
||||
model: 'gemini-1.5-flash-latest',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const service = new ModelConfigService(config);
|
||||
const resolved = service.getResolvedConfig({ model: 'abstract-base' });
|
||||
|
||||
expect(resolved.model).toBe('gemini-1.5-flash-latest');
|
||||
expect(resolved.generateContentConfig).toEqual({
|
||||
temperature: 0.1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if an extended alias does not exist', () => {
|
||||
const config: ModelConfigServiceConfig = {
|
||||
aliases: {
|
||||
'bad-alias': {
|
||||
extends: 'non-existent',
|
||||
modelConfig: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
const service = new ModelConfigService(config);
|
||||
expect(() => service.getResolvedConfig({ model: 'bad-alias' })).toThrow(
|
||||
'Alias "non-existent" not found.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deep merging', () => {
|
||||
it('should deep merge nested config objects from aliases and overrides', () => {
|
||||
const config: ModelConfigServiceConfig = {
|
||||
aliases: {
|
||||
'base-safe': {
|
||||
modelConfig: {
|
||||
model: 'gemini-pro',
|
||||
generateContentConfig: {
|
||||
safetySettings: {
|
||||
HARM_CATEGORY_HARASSMENT: 'BLOCK_ONLY_HIGH',
|
||||
HARM_CATEGORY_HATE_SPEECH: 'BLOCK_ONLY_HIGH',
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
match: { model: 'base-safe' },
|
||||
modelConfig: {
|
||||
generateContentConfig: {
|
||||
safetySettings: {
|
||||
HARM_CATEGORY_HATE_SPEECH: 'BLOCK_NONE',
|
||||
HARM_CATEGORY_SEXUALLY_EXPLICIT: 'BLOCK_MEDIUM_AND_ABOVE',
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const service = new ModelConfigService(config);
|
||||
const resolved = service.getResolvedConfig({ model: 'base-safe' });
|
||||
|
||||
expect(resolved.model).toBe('gemini-pro');
|
||||
expect(resolved.generateContentConfig.safetySettings).toEqual({
|
||||
// From alias
|
||||
HARM_CATEGORY_HARASSMENT: 'BLOCK_ONLY_HIGH',
|
||||
// From alias, overridden by override
|
||||
HARM_CATEGORY_HATE_SPEECH: 'BLOCK_NONE',
|
||||
// From override
|
||||
HARM_CATEGORY_SEXUALLY_EXPLICIT: 'BLOCK_MEDIUM_AND_ABOVE',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not deeply merge merge arrays from aliases and overrides', () => {
|
||||
const config: ModelConfigServiceConfig = {
|
||||
aliases: {
|
||||
base: {
|
||||
modelConfig: {
|
||||
model: 'gemini-pro',
|
||||
generateContentConfig: {
|
||||
stopSequences: ['foo'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
match: { model: 'base' },
|
||||
modelConfig: {
|
||||
generateContentConfig: {
|
||||
stopSequences: ['overrideFoo'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const service = new ModelConfigService(config);
|
||||
const resolved = service.getResolvedConfig({ model: 'base' });
|
||||
|
||||
expect(resolved.model).toBe('gemini-pro');
|
||||
expect(resolved.generateContentConfig.stopSequences).toEqual([
|
||||
'overrideFoo',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
248
packages/core/src/services/modelConfigService.ts
Normal file
248
packages/core/src/services/modelConfigService.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { GenerateContentConfig } from '@google/genai';
|
||||
|
||||
// The primary key for the ModelConfig is the model string. However, we also
|
||||
// support a secondary key to limit the override scope, typically an agent name.
|
||||
export interface ModelConfigKey {
|
||||
model: string;
|
||||
|
||||
// In many cases the model (or model config alias) is sufficient to fully
|
||||
// scope an override. However, in some cases, we want additional scoping of
|
||||
// an override. Consider the case of developing a new subagent, perhaps we
|
||||
// want to override the temperature for all model calls made by this subagent.
|
||||
// However, we most certainly do not want to change the temperature for other
|
||||
// subagents, nor do we want to introduce a whole new set of aliases just for
|
||||
// the new subagent. Using the `overrideScope` we can limit our overrides to
|
||||
// model calls made by this specific subagent, and no others, while still
|
||||
// ensuring model configs are fully orthogonal to the agents who use them.
|
||||
overrideScope?: string;
|
||||
}
|
||||
|
||||
export interface ModelConfig {
|
||||
model?: string;
|
||||
generateContentConfig?: GenerateContentConfig;
|
||||
}
|
||||
|
||||
export interface ModelConfigOverride {
|
||||
match: {
|
||||
model?: string; // Can be a model name or an alias
|
||||
overrideScope?: string;
|
||||
};
|
||||
modelConfig: ModelConfig;
|
||||
}
|
||||
|
||||
export interface ModelConfigAlias {
|
||||
extends?: string;
|
||||
modelConfig: ModelConfig;
|
||||
}
|
||||
|
||||
export interface ModelConfigServiceConfig {
|
||||
aliases?: Record<string, ModelConfigAlias>;
|
||||
overrides?: ModelConfigOverride[];
|
||||
}
|
||||
|
||||
export type ResolvedModelConfig = _ResolvedModelConfig & {
|
||||
readonly _brand: unique symbol;
|
||||
};
|
||||
|
||||
export interface _ResolvedModelConfig {
|
||||
model: string; // The actual, resolved model name
|
||||
generateContentConfig: GenerateContentConfig;
|
||||
}
|
||||
|
||||
export class ModelConfigService {
|
||||
// TODO(12597): Process config to build a typed alias hierarchy.
|
||||
constructor(private readonly config: ModelConfigServiceConfig) {}
|
||||
|
||||
private resolveAlias(
|
||||
aliasName: string,
|
||||
aliases: Record<string, ModelConfigAlias>,
|
||||
visited = new Set<string>(),
|
||||
): ModelConfigAlias {
|
||||
if (visited.has(aliasName)) {
|
||||
throw new Error(
|
||||
`Circular alias dependency: ${[...visited, aliasName].join(' -> ')}`,
|
||||
);
|
||||
}
|
||||
visited.add(aliasName);
|
||||
|
||||
const alias = aliases[aliasName];
|
||||
if (!alias) {
|
||||
throw new Error(`Alias "${aliasName}" not found.`);
|
||||
}
|
||||
|
||||
if (!alias.extends) {
|
||||
return alias;
|
||||
}
|
||||
|
||||
const baseAlias = this.resolveAlias(alias.extends, aliases, visited);
|
||||
|
||||
return {
|
||||
modelConfig: {
|
||||
model: alias.modelConfig.model ?? baseAlias.modelConfig.model,
|
||||
generateContentConfig: this.deepMerge(
|
||||
baseAlias.modelConfig.generateContentConfig,
|
||||
alias.modelConfig.generateContentConfig,
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private internalGetResolvedConfig(context: ModelConfigKey): {
|
||||
model: string | undefined;
|
||||
generateContentConfig: GenerateContentConfig;
|
||||
} {
|
||||
const config = this.config || {};
|
||||
const { aliases = {}, overrides = [] } = config;
|
||||
let baseModel: string | undefined = context.model;
|
||||
let resolvedConfig: GenerateContentConfig = {};
|
||||
|
||||
// Step 1: Alias Resolution
|
||||
if (aliases[context.model]) {
|
||||
const resolvedAlias = this.resolveAlias(context.model, aliases);
|
||||
baseModel = resolvedAlias.modelConfig.model; // This can now be undefined
|
||||
resolvedConfig = this.deepMerge(
|
||||
resolvedConfig,
|
||||
resolvedAlias.modelConfig.generateContentConfig,
|
||||
);
|
||||
}
|
||||
|
||||
// If an alias was used but didn't resolve to a model, `baseModel` is undefined.
|
||||
// We still need a model for matching overrides. We'll use the original alias name
|
||||
// for matching if no model is resolved yet.
|
||||
const modelForMatching = baseModel ?? context.model;
|
||||
|
||||
const finalContext = {
|
||||
...context,
|
||||
model: modelForMatching,
|
||||
};
|
||||
|
||||
// Step 2: Override Application
|
||||
const matches = overrides
|
||||
.map((override, index) => {
|
||||
const matchEntries = Object.entries(override.match);
|
||||
if (matchEntries.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isMatch = matchEntries.every(([key, value]) => {
|
||||
if (key === 'model') {
|
||||
return value === context.model || value === finalContext.model;
|
||||
}
|
||||
if (key === 'overrideScope' && value === 'core') {
|
||||
// The 'core' overrideScope is special. It should match if the
|
||||
// overrideScope is explicitly 'core' or if the overrideScope
|
||||
// is not specified.
|
||||
return context.overrideScope === 'core' || !context.overrideScope;
|
||||
}
|
||||
return finalContext[key as keyof ModelConfigKey] === value;
|
||||
});
|
||||
|
||||
if (isMatch) {
|
||||
return {
|
||||
specificity: matchEntries.length,
|
||||
modelConfig: override.modelConfig,
|
||||
index,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((match): match is NonNullable<typeof match> => match !== null);
|
||||
|
||||
// The override application logic is designed to be both simple and powerful.
|
||||
// By first sorting all matching overrides by specificity (and then by their
|
||||
// original order as a tie-breaker), we ensure that as we merge the `config`
|
||||
// objects, the settings from the most specific rules are applied last,
|
||||
// correctly overwriting any values from broader, less-specific rules.
|
||||
// This achieves a per-property override effect without complex per-property logic.
|
||||
matches.sort((a, b) => {
|
||||
if (a.specificity !== b.specificity) {
|
||||
return a.specificity - b.specificity;
|
||||
}
|
||||
return a.index - b.index;
|
||||
});
|
||||
|
||||
// Apply matching overrides
|
||||
for (const match of matches) {
|
||||
if (match.modelConfig.model) {
|
||||
baseModel = match.modelConfig.model;
|
||||
}
|
||||
if (match.modelConfig.generateContentConfig) {
|
||||
resolvedConfig = this.deepMerge(
|
||||
resolvedConfig,
|
||||
match.modelConfig.generateContentConfig,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
model: baseModel,
|
||||
generateContentConfig: resolvedConfig,
|
||||
};
|
||||
}
|
||||
|
||||
getResolvedConfig(context: ModelConfigKey): ResolvedModelConfig {
|
||||
const resolved = this.internalGetResolvedConfig(context);
|
||||
|
||||
if (!resolved.model) {
|
||||
throw new Error(
|
||||
`Could not resolve a model name for alias "${context.model}". Please ensure the alias chain or a matching override specifies a model.`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
model: resolved.model,
|
||||
generateContentConfig: resolved.generateContentConfig,
|
||||
} as ResolvedModelConfig;
|
||||
}
|
||||
|
||||
private isObject(item: unknown): item is Record<string, unknown> {
|
||||
return !!item && typeof item === 'object' && !Array.isArray(item);
|
||||
}
|
||||
|
||||
private deepMerge(
|
||||
config1: GenerateContentConfig | undefined,
|
||||
config2: GenerateContentConfig | undefined,
|
||||
): Record<string, unknown> {
|
||||
return this.genericDeepMerge(
|
||||
config1 as Record<string, unknown> | undefined,
|
||||
config2 as Record<string, unknown> | undefined,
|
||||
);
|
||||
}
|
||||
|
||||
private genericDeepMerge(
|
||||
...objects: Array<Record<string, unknown> | undefined>
|
||||
): Record<string, unknown> {
|
||||
return objects.reduce((acc: Record<string, unknown>, obj) => {
|
||||
if (!obj) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
Object.keys(obj).forEach((key) => {
|
||||
const accValue = acc[key];
|
||||
const objValue = obj[key];
|
||||
|
||||
// For now, we only deep merge objects, and not arrays. This is because
|
||||
// If we deep merge arrays, there is no way for the user to completely
|
||||
// override the base array.
|
||||
// TODO(joshualitt): Consider knobs here, i.e. opt-in to deep merging
|
||||
// arrays on a case-by-case basis.
|
||||
if (this.isObject(accValue) && this.isObject(objValue)) {
|
||||
acc[key] = this.deepMerge(
|
||||
accValue as Record<string, unknown>,
|
||||
objValue as Record<string, unknown>,
|
||||
);
|
||||
} else {
|
||||
acc[key] = objValue;
|
||||
}
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
{
|
||||
"base": {
|
||||
"generateContentConfig": {
|
||||
"temperature": 0,
|
||||
"topP": 1
|
||||
}
|
||||
},
|
||||
"chat-base": {
|
||||
"generateContentConfig": {
|
||||
"temperature": 0,
|
||||
"topP": 1,
|
||||
"thinkingConfig": {
|
||||
"includeThoughts": true,
|
||||
"thinkingBudget": -1
|
||||
}
|
||||
}
|
||||
},
|
||||
"gemini-2.5-pro": {
|
||||
"model": "gemini-2.5-pro",
|
||||
"generateContentConfig": {
|
||||
"temperature": 0,
|
||||
"topP": 1,
|
||||
"thinkingConfig": {
|
||||
"includeThoughts": true,
|
||||
"thinkingBudget": -1
|
||||
}
|
||||
}
|
||||
},
|
||||
"gemini-2.5-flash": {
|
||||
"model": "gemini-2.5-flash",
|
||||
"generateContentConfig": {
|
||||
"temperature": 0,
|
||||
"topP": 1,
|
||||
"thinkingConfig": {
|
||||
"includeThoughts": true,
|
||||
"thinkingBudget": -1
|
||||
}
|
||||
}
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"model": "gemini-2.5-flash-lite",
|
||||
"generateContentConfig": {
|
||||
"temperature": 0,
|
||||
"topP": 1,
|
||||
"thinkingConfig": {
|
||||
"includeThoughts": true,
|
||||
"thinkingBudget": -1
|
||||
}
|
||||
}
|
||||
},
|
||||
"classifier": {
|
||||
"model": "gemini-2.5-flash-lite",
|
||||
"generateContentConfig": {
|
||||
"temperature": 0,
|
||||
"topP": 1,
|
||||
"maxOutputTokens": 1024,
|
||||
"thinkingConfig": {
|
||||
"thinkingBudget": 512
|
||||
}
|
||||
}
|
||||
},
|
||||
"prompt-completion": {
|
||||
"model": "gemini-2.5-flash-lite",
|
||||
"generateContentConfig": {
|
||||
"temperature": 0.3,
|
||||
"topP": 1,
|
||||
"maxOutputTokens": 16000,
|
||||
"thinkingConfig": {
|
||||
"thinkingBudget": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"edit-corrector": {
|
||||
"model": "gemini-2.5-flash-lite",
|
||||
"generateContentConfig": {
|
||||
"temperature": 0,
|
||||
"topP": 1,
|
||||
"thinkingConfig": {
|
||||
"thinkingBudget": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"summarizer-default": {
|
||||
"model": "gemini-2.5-flash-lite",
|
||||
"generateContentConfig": {
|
||||
"temperature": 0,
|
||||
"topP": 1,
|
||||
"maxOutputTokens": 2000
|
||||
}
|
||||
},
|
||||
"summarizer-shell": {
|
||||
"model": "gemini-2.5-flash-lite",
|
||||
"generateContentConfig": {
|
||||
"temperature": 0,
|
||||
"topP": 1,
|
||||
"maxOutputTokens": 2000
|
||||
}
|
||||
},
|
||||
"web-search-tool": {
|
||||
"model": "gemini-2.5-flash",
|
||||
"generateContentConfig": {
|
||||
"temperature": 0,
|
||||
"topP": 1,
|
||||
"tools": [
|
||||
{
|
||||
"googleSearch": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"web-fetch-tool": {
|
||||
"model": "gemini-2.5-flash",
|
||||
"generateContentConfig": {
|
||||
"temperature": 0,
|
||||
"topP": 1,
|
||||
"tools": [
|
||||
{
|
||||
"urlContext": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -412,6 +412,270 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"modelConfigs": {
|
||||
"title": "Model Configs",
|
||||
"description": "Model configurations.",
|
||||
"markdownDescription": "Model configurations.\n\n- Category: `Model`\n- Requires restart: `no`\n- Default: `{\"aliases\":{\"base\":{\"modelConfig\":{\"generateContentConfig\":{\"temperature\":0,\"topP\":1}}},\"chat-base\":{\"extends\":\"base\",\"modelConfig\":{\"generateContentConfig\":{\"thinkingConfig\":{\"includeThoughts\":true,\"thinkingBudget\":-1}}}},\"gemini-2.5-pro\":{\"extends\":\"chat-base\",\"modelConfig\":{\"model\":\"gemini-2.5-pro\"}},\"gemini-2.5-flash\":{\"extends\":\"chat-base\",\"modelConfig\":{\"model\":\"gemini-2.5-flash\"}},\"gemini-2.5-flash-lite\":{\"extends\":\"chat-base\",\"modelConfig\":{\"model\":\"gemini-2.5-flash-lite\"}},\"classifier\":{\"extends\":\"base\",\"modelConfig\":{\"model\":\"gemini-2.5-flash-lite\",\"generateContentConfig\":{\"maxOutputTokens\":1024,\"thinkingConfig\":{\"thinkingBudget\":512}}}},\"prompt-completion\":{\"extends\":\"base\",\"modelConfig\":{\"model\":\"gemini-2.5-flash-lite\",\"generateContentConfig\":{\"temperature\":0.3,\"maxOutputTokens\":16000,\"thinkingConfig\":{\"thinkingBudget\":0}}}},\"edit-corrector\":{\"extends\":\"base\",\"modelConfig\":{\"model\":\"gemini-2.5-flash-lite\",\"generateContentConfig\":{\"thinkingConfig\":{\"thinkingBudget\":0}}}},\"summarizer-default\":{\"extends\":\"base\",\"modelConfig\":{\"model\":\"gemini-2.5-flash-lite\",\"generateContentConfig\":{\"maxOutputTokens\":2000}}},\"summarizer-shell\":{\"extends\":\"base\",\"modelConfig\":{\"model\":\"gemini-2.5-flash-lite\",\"generateContentConfig\":{\"maxOutputTokens\":2000}}},\"web-search-tool\":{\"extends\":\"base\",\"modelConfig\":{\"model\":\"gemini-2.5-flash\",\"generateContentConfig\":{\"tools\":[{\"googleSearch\":{}}]}}},\"web-fetch-tool\":{\"extends\":\"base\",\"modelConfig\":{\"model\":\"gemini-2.5-flash\",\"generateContentConfig\":{\"tools\":[{\"urlContext\":{}}]}}}}}`",
|
||||
"default": {
|
||||
"aliases": {
|
||||
"base": {
|
||||
"modelConfig": {
|
||||
"generateContentConfig": {
|
||||
"temperature": 0,
|
||||
"topP": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat-base": {
|
||||
"extends": "base",
|
||||
"modelConfig": {
|
||||
"generateContentConfig": {
|
||||
"thinkingConfig": {
|
||||
"includeThoughts": true,
|
||||
"thinkingBudget": -1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"gemini-2.5-pro": {
|
||||
"extends": "chat-base",
|
||||
"modelConfig": {
|
||||
"model": "gemini-2.5-pro"
|
||||
}
|
||||
},
|
||||
"gemini-2.5-flash": {
|
||||
"extends": "chat-base",
|
||||
"modelConfig": {
|
||||
"model": "gemini-2.5-flash"
|
||||
}
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"extends": "chat-base",
|
||||
"modelConfig": {
|
||||
"model": "gemini-2.5-flash-lite"
|
||||
}
|
||||
},
|
||||
"classifier": {
|
||||
"extends": "base",
|
||||
"modelConfig": {
|
||||
"model": "gemini-2.5-flash-lite",
|
||||
"generateContentConfig": {
|
||||
"maxOutputTokens": 1024,
|
||||
"thinkingConfig": {
|
||||
"thinkingBudget": 512
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"prompt-completion": {
|
||||
"extends": "base",
|
||||
"modelConfig": {
|
||||
"model": "gemini-2.5-flash-lite",
|
||||
"generateContentConfig": {
|
||||
"temperature": 0.3,
|
||||
"maxOutputTokens": 16000,
|
||||
"thinkingConfig": {
|
||||
"thinkingBudget": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"edit-corrector": {
|
||||
"extends": "base",
|
||||
"modelConfig": {
|
||||
"model": "gemini-2.5-flash-lite",
|
||||
"generateContentConfig": {
|
||||
"thinkingConfig": {
|
||||
"thinkingBudget": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"summarizer-default": {
|
||||
"extends": "base",
|
||||
"modelConfig": {
|
||||
"model": "gemini-2.5-flash-lite",
|
||||
"generateContentConfig": {
|
||||
"maxOutputTokens": 2000
|
||||
}
|
||||
}
|
||||
},
|
||||
"summarizer-shell": {
|
||||
"extends": "base",
|
||||
"modelConfig": {
|
||||
"model": "gemini-2.5-flash-lite",
|
||||
"generateContentConfig": {
|
||||
"maxOutputTokens": 2000
|
||||
}
|
||||
}
|
||||
},
|
||||
"web-search-tool": {
|
||||
"extends": "base",
|
||||
"modelConfig": {
|
||||
"model": "gemini-2.5-flash",
|
||||
"generateContentConfig": {
|
||||
"tools": [
|
||||
{
|
||||
"googleSearch": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"web-fetch-tool": {
|
||||
"extends": "base",
|
||||
"modelConfig": {
|
||||
"model": "gemini-2.5-flash",
|
||||
"generateContentConfig": {
|
||||
"tools": [
|
||||
{
|
||||
"urlContext": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"aliases": {
|
||||
"title": "Model Config Aliases",
|
||||
"description": "Named presets for model configs. Can be used in place of a model name and can inherit from other aliases using an `extends` property.",
|
||||
"markdownDescription": "Named presets for model configs. Can be used in place of a model name and can inherit from other aliases using an `extends` property.\n\n- Category: `Model`\n- Requires restart: `no`\n- Default: `{\"base\":{\"modelConfig\":{\"generateContentConfig\":{\"temperature\":0,\"topP\":1}}},\"chat-base\":{\"extends\":\"base\",\"modelConfig\":{\"generateContentConfig\":{\"thinkingConfig\":{\"includeThoughts\":true,\"thinkingBudget\":-1}}}},\"gemini-2.5-pro\":{\"extends\":\"chat-base\",\"modelConfig\":{\"model\":\"gemini-2.5-pro\"}},\"gemini-2.5-flash\":{\"extends\":\"chat-base\",\"modelConfig\":{\"model\":\"gemini-2.5-flash\"}},\"gemini-2.5-flash-lite\":{\"extends\":\"chat-base\",\"modelConfig\":{\"model\":\"gemini-2.5-flash-lite\"}},\"classifier\":{\"extends\":\"base\",\"modelConfig\":{\"model\":\"gemini-2.5-flash-lite\",\"generateContentConfig\":{\"maxOutputTokens\":1024,\"thinkingConfig\":{\"thinkingBudget\":512}}}},\"prompt-completion\":{\"extends\":\"base\",\"modelConfig\":{\"model\":\"gemini-2.5-flash-lite\",\"generateContentConfig\":{\"temperature\":0.3,\"maxOutputTokens\":16000,\"thinkingConfig\":{\"thinkingBudget\":0}}}},\"edit-corrector\":{\"extends\":\"base\",\"modelConfig\":{\"model\":\"gemini-2.5-flash-lite\",\"generateContentConfig\":{\"thinkingConfig\":{\"thinkingBudget\":0}}}},\"summarizer-default\":{\"extends\":\"base\",\"modelConfig\":{\"model\":\"gemini-2.5-flash-lite\",\"generateContentConfig\":{\"maxOutputTokens\":2000}}},\"summarizer-shell\":{\"extends\":\"base\",\"modelConfig\":{\"model\":\"gemini-2.5-flash-lite\",\"generateContentConfig\":{\"maxOutputTokens\":2000}}},\"web-search-tool\":{\"extends\":\"base\",\"modelConfig\":{\"model\":\"gemini-2.5-flash\",\"generateContentConfig\":{\"tools\":[{\"googleSearch\":{}}]}}},\"web-fetch-tool\":{\"extends\":\"base\",\"modelConfig\":{\"model\":\"gemini-2.5-flash\",\"generateContentConfig\":{\"tools\":[{\"urlContext\":{}}]}}}}`",
|
||||
"default": {
|
||||
"base": {
|
||||
"modelConfig": {
|
||||
"generateContentConfig": {
|
||||
"temperature": 0,
|
||||
"topP": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat-base": {
|
||||
"extends": "base",
|
||||
"modelConfig": {
|
||||
"generateContentConfig": {
|
||||
"thinkingConfig": {
|
||||
"includeThoughts": true,
|
||||
"thinkingBudget": -1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"gemini-2.5-pro": {
|
||||
"extends": "chat-base",
|
||||
"modelConfig": {
|
||||
"model": "gemini-2.5-pro"
|
||||
}
|
||||
},
|
||||
"gemini-2.5-flash": {
|
||||
"extends": "chat-base",
|
||||
"modelConfig": {
|
||||
"model": "gemini-2.5-flash"
|
||||
}
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"extends": "chat-base",
|
||||
"modelConfig": {
|
||||
"model": "gemini-2.5-flash-lite"
|
||||
}
|
||||
},
|
||||
"classifier": {
|
||||
"extends": "base",
|
||||
"modelConfig": {
|
||||
"model": "gemini-2.5-flash-lite",
|
||||
"generateContentConfig": {
|
||||
"maxOutputTokens": 1024,
|
||||
"thinkingConfig": {
|
||||
"thinkingBudget": 512
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"prompt-completion": {
|
||||
"extends": "base",
|
||||
"modelConfig": {
|
||||
"model": "gemini-2.5-flash-lite",
|
||||
"generateContentConfig": {
|
||||
"temperature": 0.3,
|
||||
"maxOutputTokens": 16000,
|
||||
"thinkingConfig": {
|
||||
"thinkingBudget": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"edit-corrector": {
|
||||
"extends": "base",
|
||||
"modelConfig": {
|
||||
"model": "gemini-2.5-flash-lite",
|
||||
"generateContentConfig": {
|
||||
"thinkingConfig": {
|
||||
"thinkingBudget": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"summarizer-default": {
|
||||
"extends": "base",
|
||||
"modelConfig": {
|
||||
"model": "gemini-2.5-flash-lite",
|
||||
"generateContentConfig": {
|
||||
"maxOutputTokens": 2000
|
||||
}
|
||||
}
|
||||
},
|
||||
"summarizer-shell": {
|
||||
"extends": "base",
|
||||
"modelConfig": {
|
||||
"model": "gemini-2.5-flash-lite",
|
||||
"generateContentConfig": {
|
||||
"maxOutputTokens": 2000
|
||||
}
|
||||
}
|
||||
},
|
||||
"web-search-tool": {
|
||||
"extends": "base",
|
||||
"modelConfig": {
|
||||
"model": "gemini-2.5-flash",
|
||||
"generateContentConfig": {
|
||||
"tools": [
|
||||
{
|
||||
"googleSearch": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"web-fetch-tool": {
|
||||
"extends": "base",
|
||||
"modelConfig": {
|
||||
"model": "gemini-2.5-flash",
|
||||
"generateContentConfig": {
|
||||
"tools": [
|
||||
{
|
||||
"urlContext": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"overrides": {
|
||||
"title": "Model Config Overrides",
|
||||
"description": "Apply specific configuration overrides based on matches, with a primary key of model (or alias). The most specific match will be used.",
|
||||
"markdownDescription": "Apply specific configuration overrides based on matches, with a primary key of model (or alias). The most specific match will be used.\n\n- Category: `Model`\n- Requires restart: `no`\n- Default: `[]`",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"context": {
|
||||
"title": "Context",
|
||||
"description": "Settings for managing context provided to the model.",
|
||||
|
||||
Reference in New Issue
Block a user