mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-26 22:11:57 -07:00
1022 lines
30 KiB
TypeScript
1022 lines
30 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
ModelConfigService,
|
|
type ModelConfigAlias,
|
|
type ModelConfigServiceConfig,
|
|
} 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.',
|
|
);
|
|
});
|
|
|
|
it('should throw an error if the alias chain is too deep', () => {
|
|
const aliases: Record<string, ModelConfigAlias> = {};
|
|
for (let i = 0; i < 101; i++) {
|
|
aliases[`alias-${i}`] = {
|
|
extends: i === 100 ? undefined : `alias-${i + 1}`,
|
|
modelConfig: i === 100 ? { model: 'gemini-pro' } : {},
|
|
};
|
|
}
|
|
const config: ModelConfigServiceConfig = { aliases };
|
|
const service = new ModelConfigService(config);
|
|
expect(() => service.getResolvedConfig({ model: 'alias-0' })).toThrow(
|
|
'Alias inheritance chain exceeded maximum depth of 100.',
|
|
);
|
|
});
|
|
});
|
|
|
|
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',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('runtime aliases', () => {
|
|
it('should resolve a simple runtime-registered alias', () => {
|
|
const config: ModelConfigServiceConfig = {
|
|
aliases: {},
|
|
overrides: [],
|
|
};
|
|
const service = new ModelConfigService(config);
|
|
|
|
service.registerRuntimeModelConfig('runtime-alias', {
|
|
modelConfig: {
|
|
model: 'gemini-runtime-model',
|
|
generateContentConfig: {
|
|
temperature: 0.123,
|
|
},
|
|
},
|
|
});
|
|
|
|
const resolved = service.getResolvedConfig({ model: 'runtime-alias' });
|
|
|
|
expect(resolved.model).toBe('gemini-runtime-model');
|
|
expect(resolved.generateContentConfig).toEqual({
|
|
temperature: 0.123,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('runtime overrides', () => {
|
|
it('should resolve a simple runtime-registered override', () => {
|
|
const config: ModelConfigServiceConfig = {
|
|
aliases: {},
|
|
overrides: [],
|
|
};
|
|
const service = new ModelConfigService(config);
|
|
|
|
service.registerRuntimeModelOverride({
|
|
match: { model: 'gemini-pro' },
|
|
modelConfig: {
|
|
generateContentConfig: {
|
|
temperature: 0.99,
|
|
},
|
|
},
|
|
});
|
|
|
|
const resolved = service.getResolvedConfig({ model: 'gemini-pro' });
|
|
|
|
expect(resolved.model).toBe('gemini-pro');
|
|
expect(resolved.generateContentConfig.temperature).toBe(0.99);
|
|
});
|
|
|
|
it('should prioritize runtime overrides over default overrides when they have the same specificity', () => {
|
|
const config: ModelConfigServiceConfig = {
|
|
aliases: {},
|
|
overrides: [
|
|
{
|
|
match: { model: 'gemini-pro' },
|
|
modelConfig: { generateContentConfig: { temperature: 0.1 } },
|
|
},
|
|
],
|
|
};
|
|
const service = new ModelConfigService(config);
|
|
|
|
service.registerRuntimeModelOverride({
|
|
match: { model: 'gemini-pro' },
|
|
modelConfig: { generateContentConfig: { temperature: 0.9 } },
|
|
});
|
|
|
|
const resolved = service.getResolvedConfig({ model: 'gemini-pro' });
|
|
|
|
// Runtime overrides are appended after overrides/customOverrides, so they should win.
|
|
expect(resolved.generateContentConfig.temperature).toBe(0.9);
|
|
});
|
|
|
|
it('should still respect specificity with runtime overrides', () => {
|
|
const config: ModelConfigServiceConfig = {
|
|
aliases: {},
|
|
overrides: [],
|
|
};
|
|
const service = new ModelConfigService(config);
|
|
|
|
// Register a more specific runtime override
|
|
service.registerRuntimeModelOverride({
|
|
match: { model: 'gemini-pro', overrideScope: 'my-agent' },
|
|
modelConfig: { generateContentConfig: { temperature: 0.1 } },
|
|
});
|
|
|
|
// Register a less specific runtime override later
|
|
service.registerRuntimeModelOverride({
|
|
match: { model: 'gemini-pro' },
|
|
modelConfig: { generateContentConfig: { temperature: 0.9 } },
|
|
});
|
|
|
|
const resolved = service.getResolvedConfig({
|
|
model: 'gemini-pro',
|
|
overrideScope: 'my-agent',
|
|
});
|
|
|
|
// Specificity should win over order
|
|
expect(resolved.generateContentConfig.temperature).toBe(0.1);
|
|
});
|
|
});
|
|
|
|
describe('custom aliases', () => {
|
|
it('should resolve a custom alias', () => {
|
|
const config: ModelConfigServiceConfig = {
|
|
aliases: {},
|
|
customAliases: {
|
|
'my-custom-alias': {
|
|
modelConfig: {
|
|
model: 'gemini-custom',
|
|
generateContentConfig: {
|
|
temperature: 0.9,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
overrides: [],
|
|
};
|
|
const service = new ModelConfigService(config);
|
|
const resolved = service.getResolvedConfig({ model: 'my-custom-alias' });
|
|
|
|
expect(resolved.model).toBe('gemini-custom');
|
|
expect(resolved.generateContentConfig).toEqual({
|
|
temperature: 0.9,
|
|
});
|
|
});
|
|
|
|
it('should allow custom aliases to override built-in aliases', () => {
|
|
const config: ModelConfigServiceConfig = {
|
|
aliases: {
|
|
'standard-alias': {
|
|
modelConfig: {
|
|
model: 'gemini-standard',
|
|
generateContentConfig: {
|
|
temperature: 0.5,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
customAliases: {
|
|
'standard-alias': {
|
|
modelConfig: {
|
|
model: 'gemini-custom-override',
|
|
generateContentConfig: {
|
|
temperature: 0.1,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
overrides: [],
|
|
};
|
|
const service = new ModelConfigService(config);
|
|
const resolved = service.getResolvedConfig({ model: 'standard-alias' });
|
|
|
|
expect(resolved.model).toBe('gemini-custom-override');
|
|
expect(resolved.generateContentConfig).toEqual({
|
|
temperature: 0.1,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('fallback behavior', () => {
|
|
it('should fallback to chat-base if the requested model is completely unknown', () => {
|
|
const config: ModelConfigServiceConfig = {
|
|
aliases: {
|
|
'chat-base': {
|
|
modelConfig: {
|
|
model: 'default-fallback-model',
|
|
generateContentConfig: {
|
|
temperature: 0.99,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
const service = new ModelConfigService(config);
|
|
const resolved = service.getResolvedConfig({
|
|
model: 'my-custom-model',
|
|
isChatModel: true,
|
|
});
|
|
|
|
// It preserves the requested model name, but inherits the config from chat-base
|
|
expect(resolved.model).toBe('my-custom-model');
|
|
expect(resolved.generateContentConfig).toEqual({
|
|
temperature: 0.99,
|
|
});
|
|
});
|
|
|
|
it('should return empty config if requested model is unknown and chat-base is not defined', () => {
|
|
const config: ModelConfigServiceConfig = {
|
|
aliases: {},
|
|
};
|
|
const service = new ModelConfigService(config);
|
|
const resolved = service.getResolvedConfig({
|
|
model: 'my-custom-model',
|
|
isChatModel: true,
|
|
});
|
|
|
|
expect(resolved.model).toBe('my-custom-model');
|
|
expect(resolved.generateContentConfig).toEqual({});
|
|
});
|
|
|
|
it('should NOT fallback to chat-base if the requested model is completely unknown but isChatModel is false', () => {
|
|
const config: ModelConfigServiceConfig = {
|
|
aliases: {
|
|
'chat-base': {
|
|
modelConfig: {
|
|
model: 'default-fallback-model',
|
|
generateContentConfig: {
|
|
temperature: 0.99,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
const service = new ModelConfigService(config);
|
|
const resolved = service.getResolvedConfig({
|
|
model: 'my-custom-model',
|
|
isChatModel: false,
|
|
});
|
|
|
|
expect(resolved.model).toBe('my-custom-model');
|
|
expect(resolved.generateContentConfig).toEqual({});
|
|
});
|
|
});
|
|
|
|
describe('unrecognized models', () => {
|
|
it('should apply overrides to unrecognized model names', () => {
|
|
const unregisteredModelName = 'my-unregistered-model-v1';
|
|
const config: ModelConfigServiceConfig = {
|
|
aliases: {}, // No aliases defined
|
|
overrides: [
|
|
{
|
|
match: { model: unregisteredModelName },
|
|
modelConfig: {
|
|
generateContentConfig: {
|
|
temperature: 0.01,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
};
|
|
const service = new ModelConfigService(config);
|
|
|
|
// Request the unregistered model directly
|
|
const resolved = service.getResolvedConfig({
|
|
model: unregisteredModelName,
|
|
});
|
|
|
|
// It should preserve the model name and apply the override
|
|
expect(resolved.model).toBe(unregisteredModelName);
|
|
expect(resolved.generateContentConfig).toEqual({
|
|
temperature: 0.01,
|
|
});
|
|
});
|
|
|
|
it('should apply scoped overrides to unrecognized model names', () => {
|
|
const unregisteredModelName = 'my-unregistered-model-v1';
|
|
const config: ModelConfigServiceConfig = {
|
|
aliases: {},
|
|
overrides: [
|
|
{
|
|
match: {
|
|
model: unregisteredModelName,
|
|
overrideScope: 'special-agent',
|
|
},
|
|
modelConfig: {
|
|
generateContentConfig: {
|
|
temperature: 0.99,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
};
|
|
const service = new ModelConfigService(config);
|
|
|
|
const resolved = service.getResolvedConfig({
|
|
model: unregisteredModelName,
|
|
overrideScope: 'special-agent',
|
|
});
|
|
|
|
expect(resolved.model).toBe(unregisteredModelName);
|
|
expect(resolved.generateContentConfig).toEqual({
|
|
temperature: 0.99,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('custom overrides', () => {
|
|
it('should apply custom overrides on top of defaults', () => {
|
|
const config: ModelConfigServiceConfig = {
|
|
aliases: {
|
|
'test-alias': {
|
|
modelConfig: {
|
|
model: 'gemini-test',
|
|
generateContentConfig: { temperature: 0.5 },
|
|
},
|
|
},
|
|
},
|
|
overrides: [
|
|
{
|
|
match: { model: 'test-alias' },
|
|
modelConfig: { generateContentConfig: { temperature: 0.6 } },
|
|
},
|
|
],
|
|
customOverrides: [
|
|
{
|
|
match: { model: 'test-alias' },
|
|
modelConfig: { generateContentConfig: { temperature: 0.7 } },
|
|
},
|
|
],
|
|
};
|
|
const service = new ModelConfigService(config);
|
|
const resolved = service.getResolvedConfig({ model: 'test-alias' });
|
|
|
|
// Custom overrides should be appended to overrides, so they win
|
|
expect(resolved.generateContentConfig.temperature).toBe(0.7);
|
|
});
|
|
});
|
|
|
|
describe('retry behavior', () => {
|
|
it('should apply retry-specific overrides when isRetry is true', () => {
|
|
const config: ModelConfigServiceConfig = {
|
|
aliases: {
|
|
'test-model': {
|
|
modelConfig: {
|
|
model: 'gemini-test',
|
|
generateContentConfig: {
|
|
temperature: 0.5,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
overrides: [
|
|
{
|
|
match: { model: 'test-model', isRetry: true },
|
|
modelConfig: {
|
|
generateContentConfig: {
|
|
temperature: 1.0,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
};
|
|
const service = new ModelConfigService(config);
|
|
|
|
// Normal request
|
|
const normal = service.getResolvedConfig({ model: 'test-model' });
|
|
expect(normal.generateContentConfig.temperature).toBe(0.5);
|
|
|
|
// Retry request
|
|
const retry = service.getResolvedConfig({
|
|
model: 'test-model',
|
|
isRetry: true,
|
|
});
|
|
expect(retry.generateContentConfig.temperature).toBe(1.0);
|
|
});
|
|
|
|
it('should prioritize retry overrides over generic overrides', () => {
|
|
const config: ModelConfigServiceConfig = {
|
|
aliases: {
|
|
'test-model': {
|
|
modelConfig: {
|
|
model: 'gemini-test',
|
|
generateContentConfig: {
|
|
temperature: 0.5,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
overrides: [
|
|
// Generic override for this model
|
|
{
|
|
match: { model: 'test-model' },
|
|
modelConfig: {
|
|
generateContentConfig: {
|
|
temperature: 0.7,
|
|
},
|
|
},
|
|
},
|
|
// Retry-specific override
|
|
{
|
|
match: { model: 'test-model', isRetry: true },
|
|
modelConfig: {
|
|
generateContentConfig: {
|
|
temperature: 1.0,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
};
|
|
const service = new ModelConfigService(config);
|
|
|
|
// Normal request - hits generic override
|
|
const normal = service.getResolvedConfig({ model: 'test-model' });
|
|
expect(normal.generateContentConfig.temperature).toBe(0.7);
|
|
|
|
// Retry request - hits retry override (more specific)
|
|
const retry = service.getResolvedConfig({
|
|
model: 'test-model',
|
|
isRetry: true,
|
|
});
|
|
expect(retry.generateContentConfig.temperature).toBe(1.0);
|
|
});
|
|
|
|
it('should apply overrides to parents in the alias hierarchy', () => {
|
|
const config: ModelConfigServiceConfig = {
|
|
aliases: {
|
|
'base-alias': {
|
|
modelConfig: {
|
|
model: 'gemini-test',
|
|
generateContentConfig: {
|
|
temperature: 0.5,
|
|
},
|
|
},
|
|
},
|
|
'child-alias': {
|
|
extends: 'base-alias',
|
|
modelConfig: {
|
|
generateContentConfig: {
|
|
topP: 0.9,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
overrides: [
|
|
{
|
|
match: { model: 'base-alias', isRetry: true },
|
|
modelConfig: {
|
|
generateContentConfig: {
|
|
temperature: 1.0,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
};
|
|
const service = new ModelConfigService(config);
|
|
|
|
// Normal request
|
|
const normal = service.getResolvedConfig({ model: 'child-alias' });
|
|
expect(normal.generateContentConfig.temperature).toBe(0.5);
|
|
|
|
// Retry request - should match override on parent
|
|
const retry = service.getResolvedConfig({
|
|
model: 'child-alias',
|
|
isRetry: true,
|
|
});
|
|
expect(retry.generateContentConfig.temperature).toBe(1.0);
|
|
});
|
|
});
|
|
});
|