feat: add model router with fallback chain support
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { ModelRouter } from './router.js';
|
||||
import type { ModelClient, ChatResponse } from './types.js';
|
||||
|
||||
describe('ModelRouter', () => {
|
||||
const createMockClient = (name: string, shouldFail = false): ModelClient => ({
|
||||
chat: vi.fn().mockImplementation(async () => {
|
||||
if (shouldFail) {
|
||||
throw new Error(`${name} failed`);
|
||||
}
|
||||
return {
|
||||
content: `Response from ${name}`,
|
||||
stopReason: 'end_turn',
|
||||
usage: { inputTokens: 10, outputTokens: 5 },
|
||||
} satisfies ChatResponse;
|
||||
}),
|
||||
});
|
||||
|
||||
it('uses default client when available', async () => {
|
||||
const defaultClient = createMockClient('default');
|
||||
const router = new ModelRouter({
|
||||
default: defaultClient,
|
||||
fallbackChain: [],
|
||||
});
|
||||
|
||||
const response = await router.chat({ messages: [{ role: 'user', content: 'Hi' }] });
|
||||
|
||||
expect(response.content).toBe('Response from default');
|
||||
expect(defaultClient.chat).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('falls back to next provider on failure', async () => {
|
||||
const failingClient = createMockClient('primary', true);
|
||||
const fallbackClient = createMockClient('fallback');
|
||||
|
||||
const router = new ModelRouter({
|
||||
default: failingClient,
|
||||
fallbackChain: [fallbackClient],
|
||||
});
|
||||
|
||||
const response = await router.chat({ messages: [{ role: 'user', content: 'Hi' }] });
|
||||
|
||||
expect(response.content).toBe('Response from fallback');
|
||||
expect(failingClient.chat).toHaveBeenCalled();
|
||||
expect(fallbackClient.chat).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws when all providers fail', async () => {
|
||||
const failing1 = createMockClient('primary', true);
|
||||
const failing2 = createMockClient('fallback', true);
|
||||
|
||||
const router = new ModelRouter({
|
||||
default: failing1,
|
||||
fallbackChain: [failing2],
|
||||
});
|
||||
|
||||
await expect(router.chat({ messages: [{ role: 'user', content: 'Hi' }] }))
|
||||
.rejects.toThrow('All model providers failed');
|
||||
});
|
||||
|
||||
it('uses tier-specific client when specified', async () => {
|
||||
const defaultClient = createMockClient('default');
|
||||
const fastClient = createMockClient('fast');
|
||||
|
||||
const router = new ModelRouter({
|
||||
default: defaultClient,
|
||||
fast: fastClient,
|
||||
fallbackChain: [],
|
||||
});
|
||||
|
||||
const response = await router.chat(
|
||||
{ messages: [{ role: 'user', content: 'Hi' }] },
|
||||
'fast'
|
||||
);
|
||||
|
||||
expect(response.content).toBe('Response from fast');
|
||||
expect(fastClient.chat).toHaveBeenCalled();
|
||||
expect(defaultClient.chat).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user