Skip to content

Commit aecd9cb

Browse files
author
dray
committed
fix: use dynamic Copilot API endpoint from token response
- Read endpoints.api from /copilot_internal/v2/token response and store in state.copilotApiBaseUrl; individual accounts should use https://api.individual.githubcopilot.com (not the hardcoded https://api.githubcopilot.com) which is why Claude models were missing - Refresh copilotApiBaseUrl on token refresh cycles - Replace narrow claude-sonnet/opus hardcoded normalization with a comprehensive 5-pattern normalizeClaude() that handles all versioned Claude model IDs sent by Claude Code (haiku-4-5, sonnet-4/4.5/4.6, opus-4/4.5/4.6, legacy claude-3-x-family formats)
1 parent 0ea08fe commit aecd9cb

5 files changed

Lines changed: 48 additions & 11 deletions

File tree

src/lib/api-config.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`
1414
const API_VERSION = "2025-04-01"
1515

1616
export const copilotBaseUrl = (state: State) =>
17-
state.accountType === "individual" ?
17+
state.copilotApiBaseUrl ??
18+
(state.accountType === "individual" ?
1819
"https://api.githubcopilot.com"
19-
: `https://api.${state.accountType}.githubcopilot.com`
20+
: `https://api.${state.accountType}.githubcopilot.com`)
2021
export const copilotHeaders = (state: State, vision: boolean = false) => {
2122
const headers: Record<string, string> = {
2223
Authorization: `Bearer ${state.copilotToken}`,

src/lib/state.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface State {
55
copilotToken?: string
66

77
accountType: string
8+
copilotApiBaseUrl?: string
89
models?: ModelsResponse
910
vsCodeVersion?: string
1011

src/lib/token.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@ const writeGithubToken = (token: string) =>
1616
fs.writeFile(PATHS.GITHUB_TOKEN_PATH, token)
1717

1818
export const setupCopilotToken = async () => {
19-
const { token, refresh_in } = await getCopilotToken()
19+
const { token, refresh_in, endpoints } = await getCopilotToken()
2020
state.copilotToken = token
21+
if (endpoints?.api) {
22+
state.copilotApiBaseUrl = endpoints.api
23+
}
2124

2225
// Display the Copilot token to the screen
2326
consola.debug("GitHub Copilot Token fetched successfully!")
@@ -29,8 +32,11 @@ export const setupCopilotToken = async () => {
2932
setInterval(async () => {
3033
consola.debug("Refreshing Copilot token")
3134
try {
32-
const { token } = await getCopilotToken()
35+
const { token, endpoints } = await getCopilotToken()
3336
state.copilotToken = token
37+
if (endpoints?.api) {
38+
state.copilotApiBaseUrl = endpoints.api
39+
}
3440
consola.debug("Copilot token refreshed")
3541
if (state.showToken) {
3642
consola.info("Refreshed Copilot token:", token)

src/routes/messages/non-stream-translation.ts

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,39 @@ export function translateToOpenAI(
4747
}
4848

4949
function translateModelName(model: string): string {
50-
// Subagent requests use a specific model number which Copilot doesn't support
51-
if (model.startsWith("claude-sonnet-4-")) {
52-
return model.replace(/^claude-sonnet-4-.*/, "claude-sonnet-4")
53-
} else if (model.startsWith("claude-opus-")) {
54-
return model.replace(/^claude-opus-4-.*/, "claude-opus-4")
55-
}
56-
return model
50+
// Normalize Claude model IDs to the base format Copilot accepts.
51+
// Claude Code sends versioned IDs (e.g. claude-sonnet-4-20250514, claude-haiku-4-5-20251001)
52+
// but Copilot only recognizes the base form (claude-sonnet-4, claude-haiku-4-5).
53+
const normalized = normalizeClaude(model)
54+
return normalized ?? model
55+
}
56+
57+
function normalizeClaude(model: string): string | undefined {
58+
const lower = model.toLowerCase()
59+
// Strip 8-digit date suffix (e.g. -20250514)
60+
const base = lower.replace(/-\d{8}$/, "")
61+
62+
// Pattern 1: claude-{family}-{major}-{minor} → e.g. claude-haiku-4-5, claude-opus-4-5
63+
const p1 = base.match(/^claude-([a-z]+)-(\d+)-(\d+)$/)
64+
if (p1) return `claude-${p1[1]}-${p1[2]}.${p1[3]}`
65+
66+
// Pattern 2: claude-{major}-{minor}-{family} → e.g. claude-3-5-sonnet, claude-3-5-haiku
67+
const p2 = base.match(/^claude-(\d+)-(\d+)-([a-z]+)$/)
68+
if (p2) return `claude-${p2[3]}-${p2[1]}.${p2[2]}`
69+
70+
// Pattern 3: claude-{family}-{major}.{minor} → e.g. claude-haiku-4.5 (already normalized)
71+
const p3 = base.match(/^claude-([a-z]+)-(\d+)\.(\d+)$/)
72+
if (p3) return `claude-${p3[1]}-${p3[2]}.${p3[3]}`
73+
74+
// Pattern 4: claude-{family}-{major} → e.g. claude-sonnet-4, claude-opus-4
75+
const p4 = base.match(/^claude-([a-z]+)-(\d+)$/)
76+
if (p4) return `claude-${p4[1]}-${p4[2]}`
77+
78+
// Pattern 5: claude-{major}-{family} → e.g. claude-3-opus
79+
const p5 = base.match(/^claude-(\d+)-([a-z]+)$/)
80+
if (p5) return `claude-${p5[2]}-${p5[1]}`
81+
82+
return undefined
5783
}
5884

5985
function translateAnthropicMessagesToOpenAI(

src/services/github/get-copilot-token.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,7 @@ interface GetCopilotTokenResponse {
2020
expires_at: number
2121
refresh_in: number
2222
token: string
23+
endpoints?: {
24+
api?: string
25+
}
2326
}

0 commit comments

Comments
 (0)