| |
| |
| |
| |
| |
|
|
| 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({ |
| |
| temperature: 0.2, |
| |
| topP: 0.9, |
| |
| topK: 40, |
| |
| 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, |
| }); |
|
|
| 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', |
| |
| } as any, |
| }, |
| }, |
| }, |
| }, |
| overrides: [ |
| { |
| match: { model: 'base-safe' }, |
| modelConfig: { |
| generateContentConfig: { |
| safetySettings: { |
| HARM_CATEGORY_HATE_SPEECH: 'BLOCK_NONE', |
| HARM_CATEGORY_SEXUALLY_EXPLICIT: 'BLOCK_MEDIUM_AND_ABOVE', |
| |
| } 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({ |
| |
| HARM_CATEGORY_HARASSMENT: 'BLOCK_ONLY_HIGH', |
| |
| HARM_CATEGORY_HATE_SPEECH: 'BLOCK_NONE', |
| |
| 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' }); |
|
|
| |
| 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); |
|
|
| |
| service.registerRuntimeModelOverride({ |
| match: { model: 'gemini-pro', overrideScope: 'my-agent' }, |
| modelConfig: { generateContentConfig: { temperature: 0.1 } }, |
| }); |
|
|
| |
| service.registerRuntimeModelOverride({ |
| match: { model: 'gemini-pro' }, |
| modelConfig: { generateContentConfig: { temperature: 0.9 } }, |
| }); |
|
|
| const resolved = service.getResolvedConfig({ |
| model: 'gemini-pro', |
| overrideScope: 'my-agent', |
| }); |
|
|
| |
| expect(resolved.generateContentConfig.temperature).toBe(0.1); |
| }); |
|
|
| it('should clear runtime overrides', () => { |
| const config: ModelConfigServiceConfig = { |
| aliases: {}, |
| overrides: [], |
| }; |
| const service = new ModelConfigService(config); |
|
|
| service.registerRuntimeModelOverride({ |
| match: { model: 'gemini-pro' }, |
| modelConfig: { generateContentConfig: { temperature: 0.99 } }, |
| }); |
|
|
| expect( |
| service.getResolvedConfig({ model: 'gemini-pro' }).generateContentConfig |
| .temperature, |
| ).toBe(0.99); |
|
|
| service.clearRuntimeOverrides(); |
|
|
| expect( |
| service.getResolvedConfig({ model: 'gemini-pro' }).generateContentConfig |
| .temperature, |
| ).toBeUndefined(); |
| }); |
| }); |
|
|
| 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, |
| }); |
|
|
| |
| 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: {}, |
| overrides: [ |
| { |
| match: { model: unregisteredModelName }, |
| modelConfig: { |
| generateContentConfig: { |
| temperature: 0.01, |
| }, |
| }, |
| }, |
| ], |
| }; |
| const service = new ModelConfigService(config); |
|
|
| |
| const resolved = service.getResolvedConfig({ |
| model: unregisteredModelName, |
| }); |
|
|
| |
| 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' }); |
|
|
| |
| 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); |
|
|
| |
| const normal = service.getResolvedConfig({ model: 'test-model' }); |
| expect(normal.generateContentConfig.temperature).toBe(0.5); |
|
|
| |
| 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: [ |
| |
| { |
| match: { model: 'test-model' }, |
| modelConfig: { |
| generateContentConfig: { |
| temperature: 0.7, |
| }, |
| }, |
| }, |
| |
| { |
| match: { model: 'test-model', isRetry: true }, |
| modelConfig: { |
| generateContentConfig: { |
| temperature: 1.0, |
| }, |
| }, |
| }, |
| ], |
| }; |
| const service = new ModelConfigService(config); |
|
|
| |
| const normal = service.getResolvedConfig({ model: 'test-model' }); |
| expect(normal.generateContentConfig.temperature).toBe(0.7); |
|
|
| |
| 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); |
|
|
| |
| const normal = service.getResolvedConfig({ model: 'child-alias' }); |
| expect(normal.generateContentConfig.temperature).toBe(0.5); |
|
|
| |
| const retry = service.getResolvedConfig({ |
| model: 'child-alias', |
| isRetry: true, |
| }); |
| expect(retry.generateContentConfig.temperature).toBe(1.0); |
| }); |
| }); |
|
|
| |
| describe('resolveModelId', () => { |
| it('should resolve based on useGemini3_5Flash condition', () => { |
| const config: ModelConfigServiceConfig = { |
| modelIdResolutions: { |
| flash: { |
| default: 'gemini-2.0-flash', |
| contexts: [ |
| { |
| condition: { useGemini3_5Flash: true }, |
| target: 'gemini-3.5-flash', |
| }, |
| ], |
| }, |
| }, |
| }; |
| const service = new ModelConfigService(config); |
|
|
| expect(service.resolveModelId('flash', { useGemini3_5Flash: true })).toBe( |
| 'gemini-3.5-flash', |
| ); |
| expect( |
| service.resolveModelId('flash', { useGemini3_5Flash: false }), |
| ).toBe('gemini-2.0-flash'); |
| expect(service.resolveModelId('flash', {})).toBe('gemini-2.0-flash'); |
| }); |
|
|
| it('should resolve based on complex conditions including useGemini3_5Flash', () => { |
| const config: ModelConfigServiceConfig = { |
| modelIdResolutions: { |
| 'gemini-flash': { |
| default: 'gemini-3-flash-preview', |
| contexts: [ |
| { |
| condition: { |
| useGemini3_5Flash: false, |
| hasAccessToPreview: false, |
| }, |
| target: 'gemini-2.5-flash', |
| }, |
| { |
| condition: { useGemini3_5Flash: true }, |
| target: 'gemini-3.5-flash', |
| }, |
| ], |
| }, |
| }, |
| }; |
| const service = new ModelConfigService(config); |
|
|
| |
| expect( |
| service.resolveModelId('gemini-flash', { useGemini3_5Flash: true }), |
| ).toBe('gemini-3.5-flash'); |
|
|
| |
| expect( |
| service.resolveModelId('gemini-flash', { |
| useGemini3_5Flash: false, |
| hasAccessToPreview: true, |
| }), |
| ).toBe('gemini-3-flash-preview'); |
|
|
| |
| expect( |
| service.resolveModelId('gemini-flash', { |
| useGemini3_5Flash: false, |
| hasAccessToPreview: false, |
| }), |
| ).toBe('gemini-2.5-flash'); |
| }); |
| }); |
|
|
| describe('getAvailableModelOptions', () => { |
| it('should filter out Pro models when hasAccessToProModel is false', () => { |
| const config: ModelConfigServiceConfig = { |
| modelDefinitions: { |
| 'gemini-3-pro': { isVisible: true, tier: 'pro' }, |
| 'gemini-3-flash': { isVisible: true, tier: 'flash' }, |
| }, |
| }; |
| const service = new ModelConfigService(config); |
| const options = service.getAvailableModelOptions({ |
| hasAccessToProModel: false, |
| }); |
|
|
| expect(options.map((o) => o.modelId)).not.toContain('gemini-3-pro'); |
| expect(options.map((o) => o.modelId)).toContain('gemini-3-flash'); |
| }); |
|
|
| it('should include Pro models when hasAccessToProModel is true or undefined', () => { |
| const config: ModelConfigServiceConfig = { |
| modelDefinitions: { |
| 'gemini-3-pro': { isVisible: true, tier: 'pro' }, |
| }, |
| }; |
| const service = new ModelConfigService(config); |
|
|
| const optionsWithTrue = service.getAvailableModelOptions({ |
| hasAccessToProModel: true, |
| }); |
| expect(optionsWithTrue.map((o) => o.modelId)).toContain('gemini-3-pro'); |
|
|
| const optionsWithUndefined = service.getAvailableModelOptions({}); |
| expect(optionsWithUndefined.map((o) => o.modelId)).toContain( |
| 'gemini-3-pro', |
| ); |
| }); |
| }); |
| }); |
|
|