This commit is contained in:
Your Name
2026-01-06 23:00:13 +00:00
parent ca6cc1ecd3
commit 415070e504
6 changed files with 279 additions and 1 deletions

View File

@@ -57,8 +57,12 @@ import { AgentTerminateMode } from './types.js';
import type { AnyDeclarativeTool, AnyToolInvocation } from '../tools/tools.js';
import { CompressionStatus } from '../core/turn.js';
import { ChatCompressionService } from '../services/chatCompressionService.js';
import type { ModelConfigKey } from '../services/modelConfigService.js';
import type {
ModelConfigKey,
ResolvedModelConfig,
} from '../services/modelConfigService.js';
import { getModelConfigAlias } from './registry.js';
import type { ModelRouterService } from '../routing/modelRouterService.js';
const {
mockSendMessageStream,
@@ -1192,6 +1196,101 @@ describe('LocalAgentExecutor', () => {
});
});
describe('Model Routing', () => {
it('should use model routing when the agent model is "auto"', async () => {
const definition = createTestDefinition();
definition.modelConfig.model = 'auto';
const mockRouter = {
route: vi.fn().mockResolvedValue({
model: 'routed-model',
metadata: { source: 'test', reasoning: 'test' },
}),
};
vi.spyOn(mockConfig, 'getModelRouterService').mockReturnValue(
mockRouter as unknown as ModelRouterService,
);
// Mock resolved config to return 'auto'
vi.spyOn(
mockConfig.modelConfigService,
'getResolvedConfig',
).mockReturnValue({
model: 'auto',
generateContentConfig: {},
} as unknown as ResolvedModelConfig);
const executor = await LocalAgentExecutor.create(
definition,
mockConfig,
onActivity,
);
mockModelResponse([
{
name: TASK_COMPLETE_TOOL_NAME,
args: { finalResult: 'done' },
id: 'call1',
},
]);
await executor.run({ goal: 'test' }, signal);
expect(mockRouter.route).toHaveBeenCalled();
expect(mockSendMessageStream).toHaveBeenCalledWith(
expect.objectContaining({ model: 'routed-model' }),
expect.any(Array),
expect.any(String),
expect.any(AbortSignal),
);
});
it('should NOT use model routing when the agent model is NOT "auto"', async () => {
const definition = createTestDefinition();
definition.modelConfig.model = 'concrete-model';
const mockRouter = {
route: vi.fn(),
};
vi.spyOn(mockConfig, 'getModelRouterService').mockReturnValue(
mockRouter as unknown as ModelRouterService,
);
// Mock resolved config to return 'concrete-model'
vi.spyOn(
mockConfig.modelConfigService,
'getResolvedConfig',
).mockReturnValue({
model: 'concrete-model',
generateContentConfig: {},
} as unknown as ResolvedModelConfig);
const executor = await LocalAgentExecutor.create(
definition,
mockConfig,
onActivity,
);
mockModelResponse([
{
name: TASK_COMPLETE_TOOL_NAME,
args: { finalResult: 'done' },
id: 'call1',
},
]);
await executor.run({ goal: 'test' }, signal);
expect(mockRouter.route).not.toHaveBeenCalled();
expect(mockSendMessageStream).toHaveBeenCalledWith(
expect.objectContaining({ model: 'concrete-model' }),
expect.any(Array),
expect.any(String),
expect.any(AbortSignal),
);
});
});
describe('run (Termination Conditions)', () => {
const mockWorkResponse = (id: string) => {
mockModelResponse([{ name: LS_TOOL_NAME, args: { path: '.' }, id }]);

View File

@@ -244,6 +244,42 @@ describe('AgentRegistry', () => {
});
describe('registration logic', () => {
it('should register runtime overrides when the model is "auto"', async () => {
const autoAgent: LocalAgentDefinition = {
...MOCK_AGENT_V1,
name: 'AutoAgent',
modelConfig: { ...MOCK_AGENT_V1.modelConfig, model: 'auto' },
};
const registerOverrideSpy = vi.spyOn(
mockConfig.modelConfigService,
'registerRuntimeModelOverride',
);
await registry.testRegisterAgent(autoAgent);
// Should register two overrides: one for the alias and one for the agent name (scope)
expect(registerOverrideSpy).toHaveBeenCalledTimes(2);
// Check alias override
expect(registerOverrideSpy).toHaveBeenCalledWith(
expect.objectContaining({
match: { model: getModelConfigAlias(autoAgent) },
modelConfig: expect.objectContaining({ model: 'auto' }),
}),
);
// Check scope override
expect(registerOverrideSpy).toHaveBeenCalledWith(
expect.objectContaining({
match: { overrideScope: autoAgent.name },
modelConfig: expect.objectContaining({
generateContentConfig: expect.any(Object),
}),
}),
);
});
it('should register a valid agent definition', async () => {
await registry.testRegisterAgent(MOCK_AGENT_V1);
expect(registry.getDefinition('MockAgent')).toEqual(MOCK_AGENT_V1);

View File

@@ -281,4 +281,30 @@ describe('ClassifierStrategy', () => {
);
consoleWarnSpy.mockRestore();
});
it('should respect requestedModel from context in resolveClassifierModel', async () => {
const requestedModel = DEFAULT_GEMINI_MODEL; // Pro model
const mockApiResponse = {
reasoning: 'Choice is flash',
model_choice: 'flash',
};
vi.mocked(mockBaseLlmClient.generateJson).mockResolvedValue(
mockApiResponse,
);
const contextWithRequestedModel = {
...mockContext,
requestedModel,
} as RoutingContext;
const decision = await strategy.route(
contextWithRequestedModel,
mockConfig,
mockBaseLlmClient,
);
expect(decision).not.toBeNull();
// Since requestedModel is Pro, and choice is flash, it should resolve to Flash
expect(decision?.model).toBe(DEFAULT_GEMINI_FLASH_MODEL);
});
});

View File

@@ -108,4 +108,25 @@ describe('FallbackStrategy', () => {
// Important: check that it queried snapshot with the RESOLVED model, not 'auto'
expect(mockService.snapshot).toHaveBeenCalledWith(DEFAULT_GEMINI_MODEL);
});
it('should respect requestedModel from context', async () => {
const requestedModel = 'requested-model';
const configModel = 'config-model';
vi.mocked(mockConfig.getModel).mockReturnValue(configModel);
vi.mocked(mockService.snapshot).mockReturnValue({ available: true });
const contextWithRequestedModel = {
requestedModel,
} as RoutingContext;
const decision = await strategy.route(
contextWithRequestedModel,
mockConfig,
mockClient,
);
expect(decision).toBeNull();
// Should check availability of the requested model from context
expect(mockService.snapshot).toHaveBeenCalledWith(requestedModel);
});
});

View File

@@ -56,4 +56,25 @@ describe('OverrideStrategy', () => {
expect(decision).not.toBeNull();
expect(decision?.model).toBe(overrideModel);
});
it('should respect requestedModel from context', async () => {
const requestedModel = 'requested-model';
const configModel = 'config-model';
const mockConfig = {
getModel: () => configModel,
getPreviewFeatures: () => false,
} as Config;
const contextWithRequestedModel = {
requestedModel,
} as RoutingContext;
const decision = await strategy.route(
contextWithRequestedModel,
mockConfig,
mockClient,
);
expect(decision).not.toBeNull();
expect(decision?.model).toBe(requestedModel);
});
});

View File

@@ -577,6 +577,81 @@ describe('ModelConfigService', () => {
});
});
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 = {