feat: auto same-model fallback via GitHub Models when primary Anthropic provider fails

When a tier uses the Anthropic provider and has no user-configured inline
fallback, automatically insert a GitHub Models client for the equivalent
model as a tier fallback. This ensures the same model is tried via an
alternative provider before degrading to the global fallback chain (which
may be a much weaker local model).

Mapping: claude-sonnet-4-20250514 → claude-sonnet-4, etc.
This commit is contained in:
William Valentin
2026-02-07 13:52:53 -08:00
parent d5694649bf
commit 5984c42bfd
2 changed files with 127 additions and 13 deletions
+44 -1
View File
@@ -1,11 +1,12 @@
import { describe, it, expect } from 'vitest';
import { createClientFromConfig } from './index.js';
import { createClientFromConfig, anthropicToGitHubModel, createAutoFallbackClient } from './index.js';
import { AnthropicClient } from '../models/anthropic.js';
import { OpenAIClient } from '../models/openai.js';
import { OllamaClient } from '../models/local/ollama.js';
import { LlamaCppClient } from '../models/local/llamacpp.js';
import { GeminiClient } from '../models/gemini.js';
import { BedrockClient } from '../models/bedrock.js';
import { GitHubModelsClient } from '../models/github.js';
describe('createClientFromConfig', () => {
it('creates AnthropicClient for anthropic provider', () => {
@@ -94,3 +95,45 @@ describe('createClientFromConfig', () => {
expect(client).toBeInstanceOf(BedrockClient);
});
});
describe('anthropicToGitHubModel', () => {
it('maps claude-sonnet-4-20250514 to claude-sonnet-4', () => {
expect(anthropicToGitHubModel('claude-sonnet-4-20250514')).toBe('claude-sonnet-4');
});
it('maps claude-opus-4-20250514 to claude-opus-4', () => {
expect(anthropicToGitHubModel('claude-opus-4-20250514')).toBe('claude-opus-4');
});
it('maps claude-3-5-haiku-20241022 to claude-haiku-4', () => {
expect(anthropicToGitHubModel('claude-3-5-haiku-20241022')).toBe('claude-haiku-4');
});
it('strips date suffix for unknown versioned models', () => {
expect(anthropicToGitHubModel('claude-sonnet-5-20260101')).toBe('claude-sonnet-5');
});
it('returns undefined for models without date suffix', () => {
expect(anthropicToGitHubModel('llama3.2:1b')).toBeUndefined();
});
});
describe('createAutoFallbackClient', () => {
it('creates a GitHubModelsClient for anthropic provider', () => {
const client = createAutoFallbackClient({
provider: 'anthropic',
model: 'claude-sonnet-4-20250514',
});
expect(client).toBeInstanceOf(GitHubModelsClient);
});
it('returns undefined for non-anthropic providers', () => {
expect(createAutoFallbackClient({ provider: 'openai', model: 'gpt-4o' })).toBeUndefined();
expect(createAutoFallbackClient({ provider: 'ollama', model: 'llama3.2:1b' })).toBeUndefined();
expect(createAutoFallbackClient({ provider: 'gemini', model: 'gemini-2.5-pro' })).toBeUndefined();
});
it('returns undefined for unmappable anthropic models', () => {
expect(createAutoFallbackClient({ provider: 'anthropic', model: 'custom-model' })).toBeUndefined();
});
});