fetchCopilotApiToken function
Exchanges a GitHub token for a Copilot API token (goal: copilot-proxy-go internal/auth/github_client.go).
Only OAuth tokens (gho_/ghu_ from the Copilot CLI or gh apps)
exchange. A fine-grained PAT (v2, github_pat_…) NEVER hits this
function in PAT mode — the caller checks isFineGrainedGitHubPat and
uses the raw PAT as the Copilot Bearer instead. Classic PATs
(ghp_…) are not supported by Copilot at all.
Throws CopilotAuthException on a non-200 — HTTP 401 means the GitHub token is dead (the caller must trigger re-auth, not a token refresh); every other status carries the response body in the message.
Implementation
Future<CopilotApiToken> fetchCopilotApiToken({
required String githubToken,
http.Client? client,
}) async {
final httpClient = client ?? sharedProviderHttpClient();
final response = await httpClient
.get(
Uri.parse(copilotTokenExchangeUrl),
headers: {
// Bearer, not the legacy `token` scheme (pi oauth/github-copilot.ts
// parity): device-flow OAuth tokens (gho_/ghu_) are Bearer
// credentials, and the exchange 401s enterprise-managed (EMU)
// accounts on the legacy scheme.
'authorization': 'Bearer $githubToken',
'accept': 'application/json',
'editor-version': _copilotEditorVersion,
'editor-plugin-version': _copilotPluginVersion,
'user-agent': _copilotUserAgent,
'copilot-integration-id': 'vscode-chat',
},
)
.timeout(effectiveProviderConnectTimeout);
if (response.statusCode == 401) {
throw const CopilotAuthException(
'GitHub token rejected (401) by the Copilot token exchange — '
're-authorize Copilot (CLI: /provider copilot).',
);
}
if (response.statusCode == 404) {
if (isFineGrainedGitHubPat(githubToken)) {
throw CopilotAuthException(
'Copilot token exchange failed (HTTP 404): GitHub never grants a '
'fine-grained PAT the internal token exchange — fa uses '
'github_pat_… tokens directly against the Copilot API instead '
'(recent fa versions, same as the official Copilot CLI), so a '
'PAT should never be exchanged at all. For the PAT to work it '
'must carry the "Copilot Requests" permission (github.com/'
'settings/personal-access-tokens → edit → Permissions); the '
'GitHub device flow (/provider copilot) mints a working OAuth '
'token. Body: ${response.body.trim()}',
);
}
throw CopilotAuthException(
'Copilot token exchange failed (HTTP 404): GitHub does not grant '
'this token Copilot access. A fine-grained PAT (github_pat_…) needs '
'the "Copilot Requests" permission (github.com/settings/'
'personal-access-tokens → edit → Permissions); classic PATs '
'(ghp_…) are not supported by Copilot at all; or the account has '
'no Copilot seat. The GitHub device flow (/provider copilot) mints '
'a working OAuth token. Body: ${response.body.trim()}',
);
}
if (response.statusCode != 200) {
throw CopilotAuthException(
'Copilot token exchange failed (HTTP ${response.statusCode}): '
'${response.body.trim()}',
);
}
final decoded = jsonDecode(response.body);
if (decoded is! Map<String, dynamic>) {
throw const CopilotAuthException(
'Copilot token exchange returned an unexpected payload.',
);
}
return CopilotApiToken.fromJson(decoded);
}