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
+42 -3
View File
@@ -94,6 +94,23 @@ describe('createClientFromConfig', () => {
});
expect(client).toBeInstanceOf(BedrockClient);
});
it('creates GitHubModelsClient for github provider', () => {
const client = createClientFromConfig({
provider: 'github',
model: 'claude-sonnet-4.5',
});
expect(client).toBeInstanceOf(GitHubModelsClient);
});
it('auto-maps Anthropic model names to GitHub equivalents for github provider', () => {
// User might copy-paste the Anthropic model name into a github fallback block
const client = createClientFromConfig({
provider: 'github',
model: 'claude-sonnet-4-5-20250929',
});
expect(client).toBeInstanceOf(GitHubModelsClient);
});
});
describe('anthropicToGitHubModel', () => {
@@ -101,15 +118,37 @@ describe('anthropicToGitHubModel', () => {
expect(anthropicToGitHubModel('claude-sonnet-4-20250514')).toBe('claude-sonnet-4');
});
it('maps claude-sonnet-4-5-20250929 to claude-sonnet-4.5', () => {
expect(anthropicToGitHubModel('claude-sonnet-4-5-20250929')).toBe('claude-sonnet-4.5');
});
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('maps claude-opus-4-5-20250918 to claude-opus-4.5', () => {
expect(anthropicToGitHubModel('claude-opus-4-5-20250918')).toBe('claude-opus-4.5');
});
it('strips date suffix for unknown versioned models', () => {
it('maps claude-opus-4-6-20250715 to claude-opus-4.6', () => {
expect(anthropicToGitHubModel('claude-opus-4-6-20250715')).toBe('claude-opus-4.6');
});
it('maps claude-3-5-haiku-20241022 to claude-haiku-4.5', () => {
expect(anthropicToGitHubModel('claude-3-5-haiku-20241022')).toBe('claude-haiku-4.5');
});
it('maps claude-haiku-4-5-20251001 to claude-haiku-4.5', () => {
expect(anthropicToGitHubModel('claude-haiku-4-5-20251001')).toBe('claude-haiku-4.5');
});
it('strips date suffix and converts trailing version number with dot for unknown models', () => {
// "claude-sonnet-5-7-20260101" → strip date → "claude-sonnet-5-7" → dot → "claude-sonnet-5.7"
expect(anthropicToGitHubModel('claude-sonnet-5-7-20260101')).toBe('claude-sonnet-5.7');
});
it('strips date suffix for models without sub-version', () => {
// "claude-sonnet-5-20260101" → strip date → "claude-sonnet-5" (no trailing -N to dot-convert)
expect(anthropicToGitHubModel('claude-sonnet-5-20260101')).toBe('claude-sonnet-5');
});
+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;
}
-2
View File
@@ -59,7 +59,6 @@ export class GitHubModelsClient implements ModelClient {
apiKey: apiKey || 'placeholder',
baseURL: this.baseURL,
defaultHeaders: {
'X-GitHub-Api-Version': '2022-11-28',
'Openai-Intent': 'conversation-edits',
},
});
@@ -98,7 +97,6 @@ export class GitHubModelsClient implements ModelClient {
apiKey,
baseURL: this.baseURL,
defaultHeaders: {
'X-GitHub-Api-Version': '2022-11-28',
'Openai-Intent': 'conversation-edits',
},
});