feat: add runtime provider/model switching via /model <tier> <provider/model>
- ModelRouter: add setClient(), labels map, getLabel(), getAllLabels() - TUI commands: parse /model <tier> <provider/model> syntax with autocompletion - TUI minimal: handle provider switching via createClientFromConfig factory - Daemon: wire initial labels into router config - Fix /model alias mappings (opus=complex, sonnet=default, haiku=fast) - Add design doc and update state.json with feature status
This commit is contained in:
@@ -169,3 +169,149 @@ describe('ModelRouter local client switching', () => {
|
||||
expect(router.getClient('local')).toBe(mockLocal2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setClient and labels', () => {
|
||||
it('setClient replaces an existing tier client', async () => {
|
||||
const mockClient1 = { chat: vi.fn() } as unknown as ModelClient;
|
||||
const mockClient2 = { chat: vi.fn() } as unknown as ModelClient;
|
||||
|
||||
const router = new ModelRouter({
|
||||
default: { chat: vi.fn() } as unknown as ModelClient,
|
||||
fast: mockClient1,
|
||||
fallbackChain: [],
|
||||
});
|
||||
|
||||
await router.chat({ messages: [{ role: 'user', content: 'Test' }] }, 'fast');
|
||||
|
||||
expect(mockClient1.chat).toHaveBeenCalled();
|
||||
expect(mockClient1.chat).toHaveBeenCalledTimes(1);
|
||||
|
||||
router.setClient('fast', mockClient2, 'fast-replaced');
|
||||
|
||||
const newFastClient = router.getClient('fast');
|
||||
expect(newFastClient).toBeDefined();
|
||||
await router.chat({ messages: [{ role: 'user', content: 'Test' }] }, 'fast');
|
||||
|
||||
expect(newFastClient!.chat).toHaveBeenCalled();
|
||||
expect(newFastClient!.chat).toHaveBeenCalledTimes(1);
|
||||
expect(mockClient1.chat).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('setClient adds a new tier client', async () => {
|
||||
const mockClient1 = { chat: vi.fn() } as unknown as ModelClient;
|
||||
const mockClient2 = { chat: vi.fn() } as unknown as ModelClient;
|
||||
|
||||
const router = new ModelRouter({
|
||||
default: mockClient1,
|
||||
fallbackChain: [],
|
||||
});
|
||||
|
||||
expect(router.getClient('complex')).toBeUndefined();
|
||||
|
||||
router.setClient('complex', mockClient2, 'complex-tier');
|
||||
|
||||
const newClient = router.getClient('complex');
|
||||
expect(newClient).toBe(mockClient2);
|
||||
|
||||
await router.chat({ messages: [{ role: 'user', content: 'Test' }] }, 'complex');
|
||||
|
||||
expect(newClient!.chat).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('getLabel returns the label set by setClient', () => {
|
||||
const router = new ModelRouter({
|
||||
default: { chat: vi.fn() } as unknown as ModelClient,
|
||||
fallbackChain: [],
|
||||
});
|
||||
|
||||
expect(router.getLabel('fast')).toBe('unknown');
|
||||
|
||||
router.setClient('fast', { chat: vi.fn() } as unknown as ModelClient, 'fast-tier');
|
||||
|
||||
expect(router.getLabel('fast')).toBe('fast-tier');
|
||||
});
|
||||
|
||||
it('getLabel returns "unknown" for unset tier', () => {
|
||||
const router = new ModelRouter({
|
||||
default: { chat: vi.fn() } as unknown as ModelClient,
|
||||
fallbackChain: [],
|
||||
});
|
||||
|
||||
expect(router.getLabel('fast')).toBe('unknown');
|
||||
expect(router.getLabel('complex')).toBe('unknown');
|
||||
});
|
||||
|
||||
it('getAllLabels returns all tier labels', () => {
|
||||
const router = new ModelRouter({
|
||||
default: { chat: vi.fn() } as unknown as ModelClient,
|
||||
fallbackChain: [],
|
||||
});
|
||||
|
||||
const labels = router.getAllLabels();
|
||||
expect(labels).toEqual({});
|
||||
|
||||
router.setClient('fast', { chat: vi.fn() } as unknown as ModelClient, 'fast-tier');
|
||||
router.setClient('complex', { chat: vi.fn() } as unknown as ModelClient, 'complex-tier');
|
||||
|
||||
const allLabels = router.getAllLabels();
|
||||
expect(allLabels).toEqual({
|
||||
fast: 'fast-tier',
|
||||
complex: 'complex-tier',
|
||||
});
|
||||
});
|
||||
|
||||
it('constructor accepts initial labels', async () => {
|
||||
const mockClient1 = { chat: vi.fn() } as unknown as ModelClient;
|
||||
const mockClient2 = { chat: vi.fn() } as unknown as ModelClient;
|
||||
|
||||
const router = new ModelRouter({
|
||||
default: mockClient1,
|
||||
fast: mockClient2,
|
||||
fallbackChain: [],
|
||||
labels: {
|
||||
default: 'default-tier',
|
||||
fast: 'fast-tier',
|
||||
},
|
||||
});
|
||||
|
||||
expect(router.getClient('default')).toBe(mockClient1);
|
||||
expect(router.getClient('fast')).toBe(mockClient2);
|
||||
expect(router.getLabel('default')).toBe('default-tier');
|
||||
expect(router.getLabel('fast')).toBe('fast-tier');
|
||||
expect(router.getLabel('complex')).toBe('unknown');
|
||||
|
||||
await router.chat({ messages: [{ role: 'user', content: 'Hi' }] }, 'fast');
|
||||
|
||||
expect(mockClient2.chat).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('chat uses the new client after setClient', async () => {
|
||||
const mockClient1 = { chat: vi.fn() } as unknown as ModelClient;
|
||||
const mockClient2 = { chat: vi.fn() } as unknown as ModelClient;
|
||||
|
||||
const router = new ModelRouter({
|
||||
default: mockClient1,
|
||||
fast: { chat: vi.fn() } as unknown as ModelClient,
|
||||
fallbackChain: [],
|
||||
labels: {
|
||||
fast: 'original-fast',
|
||||
},
|
||||
});
|
||||
|
||||
const initialFastClient = router.getClient('fast');
|
||||
expect(initialFastClient).toBeDefined();
|
||||
await router.chat({ messages: [{ role: 'user', content: 'Test' }] }, 'fast');
|
||||
|
||||
expect(initialFastClient!.chat).toHaveBeenCalled();
|
||||
expect(initialFastClient!.chat).toHaveBeenCalledTimes(1);
|
||||
|
||||
router.setClient('fast', mockClient2, 'fast-replaced');
|
||||
|
||||
const newFastClient = router.getClient('fast');
|
||||
await router.chat({ messages: [{ role: 'user', content: 'Test' }] }, 'fast');
|
||||
|
||||
expect(newFastClient!.chat).toHaveBeenCalled();
|
||||
expect(newFastClient!.chat).toHaveBeenCalledTimes(1);
|
||||
expect(initialFastClient!.chat).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user