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:
+83
-12
@@ -131,6 +131,50 @@ export function createClientFromConfig(cfg: ModelConfig): ModelClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map an Anthropic model identifier to its GitHub Models equivalent.
|
||||
* Returns undefined if no mapping is known.
|
||||
*/
|
||||
export function anthropicToGitHubModel(anthropicModel: string): string | undefined {
|
||||
// Mapping from Anthropic versioned names → GitHub Copilot short names
|
||||
const MAPPINGS: Record<string, string> = {
|
||||
'claude-sonnet-4-20250514': 'claude-sonnet-4',
|
||||
'claude-opus-4-20250514': 'claude-opus-4',
|
||||
'claude-3-5-haiku-20241022': 'claude-haiku-4',
|
||||
};
|
||||
|
||||
if (MAPPINGS[anthropicModel]) return MAPPINGS[anthropicModel];
|
||||
|
||||
// Try stripping date suffix (e.g. "claude-sonnet-4-20260101" → "claude-sonnet-4")
|
||||
const dateMatch = anthropicModel.match(/^(.+)-\d{8}$/);
|
||||
if (dateMatch) return dateMatch[1];
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* For a given tier config using the Anthropic provider, create a GitHub Models
|
||||
* client with the equivalent model as an automatic same-model fallback.
|
||||
* Returns undefined if no mapping exists or the tier isn't Anthropic.
|
||||
*/
|
||||
export function createAutoFallbackClient(tierConfig: { provider: string; model: string }): ModelClient | undefined {
|
||||
if (tierConfig.provider !== 'anthropic') return undefined;
|
||||
|
||||
const githubModel = anthropicToGitHubModel(tierConfig.model);
|
||||
if (!githubModel) return undefined;
|
||||
|
||||
return new GitHubModelsClient({
|
||||
model: githubModel,
|
||||
onLoginRequired: async () => {
|
||||
const { loginGitHub } = await import('../auth/index.js');
|
||||
return loginGitHub((userCode, verificationUri) => {
|
||||
console.log(`GitHub login required. Visit: ${verificationUri}`);
|
||||
console.log(`Enter code: ${userCode}`);
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function createModelRouter(config: Config): ModelRouter {
|
||||
const models = config.models;
|
||||
|
||||
@@ -160,25 +204,52 @@ function createModelRouter(config: Config): ModelRouter {
|
||||
}
|
||||
}
|
||||
|
||||
// Build per-tier fallbacks from inline fallback config
|
||||
// Build per-tier fallbacks from inline fallback config + auto same-model fallbacks.
|
||||
// Auto-fallback: when a tier uses Anthropic, automatically insert a GitHub Models
|
||||
// client for the same model *before* any user-configured inline fallbacks.
|
||||
// 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).
|
||||
const tierFallbacks = new Map<ModelTier, ModelClient[]>();
|
||||
if (models.default.fallback) {
|
||||
tierFallbacks.set('default', [createClientFromConfig(models.default.fallback)]);
|
||||
}
|
||||
if (models.fast?.fallback) {
|
||||
tierFallbacks.set('fast', [createClientFromConfig(models.fast.fallback)]);
|
||||
}
|
||||
if (models.complex?.fallback) {
|
||||
tierFallbacks.set('complex', [createClientFromConfig(models.complex.fallback)]);
|
||||
}
|
||||
if (models.local?.fallback) {
|
||||
tierFallbacks.set('local', [createClientFromConfig(models.local.fallback)]);
|
||||
|
||||
const tierConfigs: { tier: ModelTier; cfg: typeof models.default | undefined }[] = [
|
||||
{ tier: 'default', cfg: models.default },
|
||||
{ tier: 'fast', cfg: models.fast },
|
||||
{ tier: 'complex', cfg: models.complex },
|
||||
{ tier: 'local', cfg: models.local },
|
||||
];
|
||||
|
||||
const autoFallbackTiers: string[] = [];
|
||||
for (const { tier, cfg } of tierConfigs) {
|
||||
if (!cfg) continue;
|
||||
|
||||
const fallbackList: ModelClient[] = [];
|
||||
|
||||
// Auto same-model fallback (only when user hasn't configured an inline fallback)
|
||||
if (!cfg.fallback) {
|
||||
const autoClient = createAutoFallbackClient(cfg);
|
||||
if (autoClient) {
|
||||
fallbackList.push(autoClient);
|
||||
autoFallbackTiers.push(tier);
|
||||
}
|
||||
}
|
||||
|
||||
// User-configured inline fallback
|
||||
if (cfg.fallback) {
|
||||
fallbackList.push(createClientFromConfig(cfg.fallback));
|
||||
}
|
||||
|
||||
if (fallbackList.length > 0) {
|
||||
tierFallbacks.set(tier, fallbackList);
|
||||
}
|
||||
}
|
||||
|
||||
if (tierFallbacks.size > 0) {
|
||||
const tierNames = Array.from(tierFallbacks.keys()).join(', ');
|
||||
console.log(`Per-tier fallbacks configured for: ${tierNames}`);
|
||||
}
|
||||
if (autoFallbackTiers.length > 0) {
|
||||
console.log(`Auto same-model fallback (via GitHub Models) for: ${autoFallbackTiers.join(', ')}`);
|
||||
}
|
||||
|
||||
console.log(`Model router: default=${models.default.provider}/${models.default.model}, ` +
|
||||
`fallback=[${models.fallback_chain.join(', ')}]`);
|
||||
|
||||
Reference in New Issue
Block a user