feat: add per-tier fallback field to model config schema
Each model tier (fast, default, complex, local) can now specify an optional fallback provider config that the router will try before falling through to the global fallback chain. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -89,6 +89,57 @@ describe('configSchema — routing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('configSchema — per-tier fallback', () => {
|
||||
const minimalConfig = {
|
||||
telegram: { bot_token: 'test', allowed_chat_ids: [1] },
|
||||
models: { default: { provider: 'anthropic', model: 'claude-3' } },
|
||||
};
|
||||
|
||||
it('accepts per-tier fallback config', () => {
|
||||
const result = configSchema.parse({
|
||||
...minimalConfig,
|
||||
models: {
|
||||
default: {
|
||||
provider: 'anthropic',
|
||||
model: 'claude-sonnet-4-5-20250929',
|
||||
fallback: { provider: 'github', model: 'claude-sonnet-4-5-20250929' },
|
||||
},
|
||||
fast: {
|
||||
provider: 'anthropic',
|
||||
model: 'claude-haiku-4-5-20251001',
|
||||
fallback: { provider: 'github', model: 'claude-haiku-4-5-20251001' },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result.models.default.fallback?.provider).toBe('github');
|
||||
expect(result.models.fast?.fallback?.provider).toBe('github');
|
||||
});
|
||||
|
||||
it('works without fallback field (backward compat)', () => {
|
||||
const result = configSchema.parse(minimalConfig);
|
||||
expect(result.models.default.fallback).toBeUndefined();
|
||||
});
|
||||
|
||||
it('fallback does not itself accept a nested fallback', () => {
|
||||
const result = configSchema.parse({
|
||||
...minimalConfig,
|
||||
models: {
|
||||
default: {
|
||||
provider: 'anthropic',
|
||||
model: 'claude-3',
|
||||
fallback: {
|
||||
provider: 'github',
|
||||
model: 'claude-3',
|
||||
fallback: { provider: 'ollama', model: 'llama' },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
// Zod strips unknown keys from the base schema, so nested fallback is dropped
|
||||
expect((result.models.default.fallback as Record<string, unknown>)?.fallback).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('configSchema automation', () => {
|
||||
const baseConfig = {
|
||||
telegram: { bot_token: 'test-token', allowed_chat_ids: [123] },
|
||||
|
||||
@@ -18,7 +18,7 @@ const serverSchema = z.object({
|
||||
auth_http: z.boolean().default(true),
|
||||
});
|
||||
|
||||
const modelConfigSchema = z.object({
|
||||
const modelConfigBaseSchema = z.object({
|
||||
provider: z.enum(['anthropic', 'openai', 'gemini', 'ollama', 'llamacpp', 'openrouter', 'bedrock', 'github']),
|
||||
model: z.string(),
|
||||
endpoint: z.string().optional(),
|
||||
@@ -29,6 +29,10 @@ const modelConfigSchema = z.object({
|
||||
context_window: z.number().optional(),
|
||||
});
|
||||
|
||||
const modelConfigSchema = modelConfigBaseSchema.extend({
|
||||
fallback: modelConfigBaseSchema.optional(),
|
||||
});
|
||||
|
||||
const modelsSchema = z.object({
|
||||
local: modelConfigSchema.optional(),
|
||||
fast: modelConfigSchema.optional(),
|
||||
|
||||
Reference in New Issue
Block a user