From b322e8f29cac5102c8b53462f330e0026d8167ec Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sat, 7 Feb 2026 14:04:54 -0800 Subject: [PATCH] =?UTF-8?q?fix:=20GitHub=20Copilot=20fallback=20=E2=80=94?= =?UTF-8?q?=20remove=20stale=20API=20version=20header=20and=20fix=20model?= =?UTF-8?q?=20name=20mapping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/daemon/clientFactory.test.ts | 45 +++++++++++++++++++++++++++++--- src/daemon/index.ts | 27 +++++++++++++++---- src/models/github.ts | 2 -- 3 files changed, 64 insertions(+), 10 deletions(-) diff --git a/src/daemon/clientFactory.test.ts b/src/daemon/clientFactory.test.ts index d80f57c..f98cdee 100644 --- a/src/daemon/clientFactory.test.ts +++ b/src/daemon/clientFactory.test.ts @@ -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'); }); diff --git a/src/daemon/index.ts b/src/daemon/index.ts index 276147c..06c37ce 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -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 = { + // 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; } diff --git a/src/models/github.ts b/src/models/github.ts index dd4fe8b..8e9ff65 100644 --- a/src/models/github.ts +++ b/src/models/github.ts @@ -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', }, });