mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -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.
|
- **Description:** Skip the next speaker check.
|
||||||
- **Default:** `true`
|
- **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`
|
||||||
|
|
||||||
- **`context.fileName`** (string | string[]):
|
- **`context.fileName`** (string | string[]):
|
||||||
|
|||||||
@@ -669,6 +669,7 @@ export async function loadCliConfig(
|
|||||||
recordResponses: argv.recordResponses,
|
recordResponses: argv.recordResponses,
|
||||||
retryFetchErrors: settings.general?.retryFetchErrors ?? false,
|
retryFetchErrors: settings.general?.retryFetchErrors ?? false,
|
||||||
ptyInfo: ptyInfo?.name,
|
ptyInfo: ptyInfo?.name,
|
||||||
|
modelConfigServiceConfig: settings.modelConfigs,
|
||||||
// TODO: loading of hooks based on workspace trust
|
// TODO: loading of hooks based on workspace trust
|
||||||
enableHooks: settings.tools?.enableHooks ?? false,
|
enableHooks: settings.tools?.enableHooks ?? false,
|
||||||
hooks: settings.hooks || {},
|
hooks: settings.hooks || {},
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
|
DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
|
||||||
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
|
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
|
||||||
DEFAULT_GEMINI_MODEL,
|
DEFAULT_GEMINI_MODEL,
|
||||||
|
DEFAULT_MODEL_CONFIGS,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import type { CustomTheme } from '../ui/themes/theme.js';
|
import type { CustomTheme } from '../ui/themes/theme.js';
|
||||||
import type { SessionRetentionSettings } from './settings.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: {
|
context: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
label: 'Context',
|
label: 'Context',
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { RipGrepTool, canUseRipgrep } from '../tools/ripGrep.js';
|
|||||||
import { logRipgrepFallback } from '../telemetry/loggers.js';
|
import { logRipgrepFallback } from '../telemetry/loggers.js';
|
||||||
import { RipgrepFallbackEvent } from '../telemetry/types.js';
|
import { RipgrepFallbackEvent } from '../telemetry/types.js';
|
||||||
import { ToolRegistry } from '../tools/tool-registry.js';
|
import { ToolRegistry } from '../tools/tool-registry.js';
|
||||||
|
import { DEFAULT_MODEL_CONFIGS } from './defaultModelConfigs.js';
|
||||||
|
|
||||||
vi.mock('fs', async (importOriginal) => {
|
vi.mock('fs', async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import('fs')>();
|
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', () => {
|
describe('Config getHooks', () => {
|
||||||
const baseParams: ConfigParameters = {
|
const baseParams: ConfigParameters = {
|
||||||
cwd: '/tmp',
|
cwd: '/tmp',
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ import { RipgrepFallbackEvent } from '../telemetry/types.js';
|
|||||||
import type { FallbackModelHandler } from '../fallback/types.js';
|
import type { FallbackModelHandler } from '../fallback/types.js';
|
||||||
import { ModelRouterService } from '../routing/modelRouterService.js';
|
import { ModelRouterService } from '../routing/modelRouterService.js';
|
||||||
import { OutputFormat } from '../output/types.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
|
// Re-export OAuth config type
|
||||||
export type { MCPOAuthConfig, AnyToolInvocation };
|
export type { MCPOAuthConfig, AnyToolInvocation };
|
||||||
@@ -291,6 +294,7 @@ export interface ConfigParameters {
|
|||||||
recordResponses?: string;
|
recordResponses?: string;
|
||||||
ptyInfo?: string;
|
ptyInfo?: string;
|
||||||
disableYoloMode?: boolean;
|
disableYoloMode?: boolean;
|
||||||
|
modelConfigServiceConfig?: ModelConfigServiceConfig;
|
||||||
enableHooks?: boolean;
|
enableHooks?: boolean;
|
||||||
experiments?: Experiments;
|
experiments?: Experiments;
|
||||||
hooks?: {
|
hooks?: {
|
||||||
@@ -309,6 +313,7 @@ export class Config {
|
|||||||
private fileSystemService: FileSystemService;
|
private fileSystemService: FileSystemService;
|
||||||
private contentGeneratorConfig!: ContentGeneratorConfig;
|
private contentGeneratorConfig!: ContentGeneratorConfig;
|
||||||
private contentGenerator!: ContentGenerator;
|
private contentGenerator!: ContentGenerator;
|
||||||
|
readonly modelConfigService: ModelConfigService;
|
||||||
private readonly embeddingModel: string;
|
private readonly embeddingModel: string;
|
||||||
private readonly sandbox: SandboxConfig | undefined;
|
private readonly sandbox: SandboxConfig | undefined;
|
||||||
private readonly targetDir: string;
|
private readonly targetDir: string;
|
||||||
@@ -560,6 +565,25 @@ export class Config {
|
|||||||
}
|
}
|
||||||
this.geminiClient = new GeminiClient(this);
|
this.geminiClient = new GeminiClient(this);
|
||||||
this.modelRouterService = new ModelRouterService(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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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 config
|
||||||
export * from './config/config.js';
|
export * from './config/config.js';
|
||||||
|
export * from './config/defaultModelConfigs.js';
|
||||||
export * from './output/types.js';
|
export * from './output/types.js';
|
||||||
export * from './output/json-formatter.js';
|
export * from './output/json-formatter.js';
|
||||||
export * from './output/stream-json-formatter.js';
|
export * from './output/stream-json-formatter.js';
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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
|
"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": {
|
"context": {
|
||||||
"title": "Context",
|
"title": "Context",
|
||||||
"description": "Settings for managing context provided to the model.",
|
"description": "Settings for managing context provided to the model.",
|
||||||
|
|||||||
Reference in New Issue
Block a user