fix: GitHub Copilot fallback — remove stale API version header and fix model name mapping

Two issues prevented the GitHub Models fallback from working:

1. The X-GitHub-Api-Version: 2022-11-28 header caused '400 invalid
   apiVersion' errors. The Copilot chat completions endpoint does not
   use this header — removed from both constructor and rebuildClient.

2. The anthropicToGitHubModel mapping was incomplete: it only knew
   three models and the generic date-stripping fallback produced wrong
   names (e.g. 'claude-sonnet-4-5' instead of 'claude-sonnet-4.5').
   GitHub Copilot uses dots for sub-versions, not hyphens.

   Updated with explicit mappings for all current models (sonnet 4,
   4.5; opus 4, 4.5, 4.6; haiku 4.5) and a smarter generic fallback
   that converts digit-hyphen-digit to digit.digit at the end.

3. createClientFromConfig now auto-maps Anthropic-style model names
   when the provider is 'github', so users can copy model names from
   their Anthropic config into fallback blocks without manual renaming.
This commit is contained in:
William Valentin
2026-02-07 14:04:54 -08:00
parent e12eb3a0be
commit b322e8f29c
3 changed files with 64 additions and 10 deletions
+22 -5
View File
@@ -115,7 +115,7 @@ export function createClientFromConfig(cfg: ModelConfig): ModelClient {
});
case 'github':
return new GitHubModelsClient({
model: cfg.model,
model: anthropicToGitHubModel(cfg.model) ?? cfg.model,
apiKey: cfg.api_key,
endpoint: cfg.endpoint,
onLoginRequired: async () => {
@@ -134,20 +134,37 @@ export function createClientFromConfig(cfg: ModelConfig): ModelClient {
/**
* Map an Anthropic model identifier to its GitHub Models equivalent.
* Returns undefined if no mapping is known.
*
* Anthropic uses hyphens and date suffixes: claude-sonnet-4-5-20250929
* GitHub Copilot uses dots, no dates: claude-sonnet-4.5
*/
export function anthropicToGitHubModel(anthropicModel: string): string | undefined {
// Mapping from Anthropic versioned names → GitHub Copilot short names
// Explicit mappings for known models
const MAPPINGS: Record<string, string> = {
// Sonnet family
'claude-sonnet-4-20250514': 'claude-sonnet-4',
'claude-sonnet-4-5-20250929': 'claude-sonnet-4.5',
// Opus family
'claude-opus-4-20250514': 'claude-opus-4',
'claude-3-5-haiku-20241022': 'claude-haiku-4',
'claude-opus-4-5-20250918': 'claude-opus-4.5',
'claude-opus-4-6-20250715': 'claude-opus-4.6',
// Haiku family
'claude-3-5-haiku-20241022': 'claude-haiku-4.5',
'claude-haiku-4-5-20251001': 'claude-haiku-4.5',
};
if (MAPPINGS[anthropicModel]) return MAPPINGS[anthropicModel];
// Try stripping date suffix (e.g. "claude-sonnet-4-20260101" → "claude-sonnet-4")
// Generic fallback: strip date suffix, then convert trailing -N to .N
// only when preceded by another digit (i.e. "4-5" → "4.5", not "sonnet-5" → "sonnet.5")
// e.g. "claude-sonnet-4-7-20260301" → "claude-sonnet-4-7" → "claude-sonnet-4.7"
const dateMatch = anthropicModel.match(/^(.+)-\d{8}$/);
if (dateMatch) return dateMatch[1];
if (dateMatch) {
const base = dateMatch[1];
// Convert "claude-sonnet-4-5" → "claude-sonnet-4.5" (digit-hyphen-digit at end)
const dotted = base.replace(/(\d)-(\d+)$/, '$1.$2');
return dotted;
}
return undefined;
}