This commit is contained in:
Dax Raad 2025-06-22 18:22:46 -04:00
parent 63c504f086
commit 5e46d98c86
6 changed files with 134 additions and 187 deletions

View file

@ -1,3 +1,37 @@
{
"$schema": "https://opencode.ai/config.json"
"$schema": "https://opencode.ai/config.json",
"provider": {
"github-copilot": {
"npm": "@ai-sdk/openai-compatible",
"options": {
"baseURL": "https://api.githubcopilot.com"
},
"models": {
"gpt-4o": {
"name": "gpt-4o"
},
"gpt-4.1": {
"name": "gpt-4.1"
},
"claude-sonnet-4": {
"name": "claude-sonnet-4"
},
"claude-3.7-sonnet": {
"name": "claude-3.7-sonnet"
},
"o1": {
"name": "o1"
},
"o3-mini": {
"name": "o3-mini"
},
"gemini-2.5-pro": {
"name": "gemini-2.5-pro"
},
"gemini-2.0-flash-001": {
"name": "gemini-2.0-flash-001"
}
}
}
}
}

View file

@ -1,4 +1,6 @@
import { z } from "zod"
import { Auth } from "./index"
import { NamedError } from "../util/error"
export namespace AuthGithubCopilot {
const CLIENT_ID = "Iv1.b507a08c87ecfe98"
@ -33,7 +35,7 @@ export namespace AuthGithubCopilot {
const deviceResponse = await fetch(DEVICE_CODE_URL, {
method: "POST",
headers: {
"Accept": "application/json",
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent": "GithubCopilot/1.155.0",
},
@ -42,146 +44,111 @@ export namespace AuthGithubCopilot {
scope: "read:user",
}),
})
if (!deviceResponse.ok) {
throw new DeviceCodeError("Failed to get device code")
}
const deviceData: DeviceCodeResponse = await deviceResponse.json()
return {
device_code: deviceData.device_code,
user_code: deviceData.user_code,
verification_uri: deviceData.verification_uri,
device: deviceData.device_code,
user: deviceData.user_code,
verification: deviceData.verification_uri,
interval: deviceData.interval || 5,
expires_in: deviceData.expires_in,
expiry: deviceData.expires_in,
}
}
export async function pollForToken(device_code: string, interval: number = 5, maxAttempts: number = 36) {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const response = await fetch(ACCESS_TOKEN_URL, {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"User-Agent": "GithubCopilot/1.155.0",
},
body: JSON.stringify({
client_id: CLIENT_ID,
device_code,
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
}),
export async function poll(device_code: string) {
const response = await fetch(ACCESS_TOKEN_URL, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent": "GithubCopilot/1.155.0",
},
body: JSON.stringify({
client_id: CLIENT_ID,
device_code,
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
}),
})
if (!response.ok) return "failed"
const data: AccessTokenResponse = await response.json()
if (data.access_token) {
// Store the GitHub OAuth token
await Auth.set("github-copilot", {
type: "oauth",
refresh: data.access_token,
access: "",
expires: 0,
})
if (!response.ok) {
throw new TokenExchangeError("Failed to poll for access token")
}
const data: AccessTokenResponse = await response.json()
if (data.access_token) {
// Store the GitHub OAuth token
await Auth.set("github-copilot-oauth", {
type: "api",
key: data.access_token,
})
return data.access_token
}
if (data.error === "authorization_pending") {
await new Promise(resolve => setTimeout(resolve, interval * 1000))
continue
}
if (data.error) {
throw new TokenExchangeError(`OAuth error: ${data.error}`)
}
return "complete"
}
throw new TokenExchangeError("Polling timeout exceeded")
if (data.error === "authorization_pending") return "pending"
if (data.error) return "failed"
return "pending"
}
export async function getCopilotApiToken() {
const oauthInfo = await Auth.get("github-copilot-oauth")
if (!oauthInfo || oauthInfo.type !== "api") {
throw new AuthenticationError("No GitHub OAuth token found")
}
// Check if we have a cached Copilot API token that's still valid
const copilotInfo = await Auth.get("github-copilot")
if (copilotInfo && copilotInfo.type === "oauth" && copilotInfo.expires > Date.now()) {
return {
token: copilotInfo.access,
apiEndpoint: "https://api.githubcopilot.com",
}
}
export async function access() {
const info = await Auth.get("github-copilot")
if (!info || info.type !== "oauth") return
if (info.access && info.expires > Date.now())
return { access: info.access, api: "https://api.githubcopilot.com" }
// Get new Copilot API token
const response = await fetch(COPILOT_API_KEY_URL, {
headers: {
"Accept": "application/json",
"Authorization": `Bearer ${oauthInfo.key}`,
Accept: "application/json",
Authorization: `Bearer ${info.refresh}`,
"User-Agent": "GithubCopilot/1.155.0",
"Editor-Version": "vscode/1.85.1",
"Editor-Plugin-Version": "copilot/1.155.0",
},
})
if (!response.ok) {
throw new CopilotTokenError("Failed to get Copilot API token")
}
if (!response.ok) return
const tokenData: CopilotTokenResponse = await response.json()
// Store the Copilot API token
await Auth.set("github-copilot", {
type: "oauth",
refresh: "", // GitHub Copilot doesn't use refresh tokens
refresh: info.refresh,
access: tokenData.token,
expires: tokenData.expires_at * 1000, // Convert to milliseconds
expires: tokenData.expires_at * 1000,
})
return {
token: tokenData.token,
apiEndpoint: tokenData.endpoints.api,
access: tokenData.token,
api: tokenData.endpoints.api,
}
}
export async function access() {
try {
const result = await getCopilotApiToken()
return result.token
} catch (error) {
return null
}
}
export const DeviceCodeError = NamedError.create(
"DeviceCodeError",
z.object({}),
)
export class DeviceCodeError extends Error {
constructor(message: string) {
super(message)
this.name = "DeviceCodeError"
}
}
export const TokenExchangeError = NamedError.create(
"TokenExchangeError",
z.object({
message: z.string(),
}),
)
export class TokenExchangeError extends Error {
constructor(message: string) {
super(message)
this.name = "TokenExchangeError"
}
}
export const AuthenticationError = NamedError.create(
"AuthenticationError",
z.object({
message: z.string(),
}),
)
export class AuthenticationError extends Error {
constructor(message: string) {
super(message)
this.name = "AuthenticationError"
}
}
export class CopilotTokenError extends Error {
constructor(message: string) {
super(message)
this.name = "CopilotTokenError"
}
}
export const CopilotTokenError = NamedError.create(
"CopilotTokenError",
z.object({
message: z.string(),
}),
)
}

View file

@ -156,27 +156,29 @@ export const AuthLoginCommand = cmd({
await new Promise((resolve) => setTimeout(resolve, 10))
const deviceInfo = await AuthGithubCopilot.authorize()
prompts.note(`Please visit: ${deviceInfo.verification_uri}`)
prompts.note(`Enter code: ${deviceInfo.user_code}`)
prompts.note(
`Please visit: ${deviceInfo.verification}\nEnter code: ${deviceInfo.user}`,
)
const confirm = await prompts.confirm({
message: "Press Enter after completing authentication in browser",
})
const spinner = prompts.spinner()
spinner.start("Waiting for authorization...")
if (prompts.isCancel(confirm)) throw new UI.CancelledError()
try {
await AuthGithubCopilot.pollForToken(
deviceInfo.device_code,
deviceInfo.interval,
)
await AuthGithubCopilot.getCopilotApiToken()
prompts.log.success("GitHub Copilot login successful")
} catch (error) {
prompts.log.error(
`Login failed: ${error instanceof Error ? error.message : String(error)}`,
while (true) {
await new Promise((resolve) =>
setTimeout(resolve, deviceInfo.interval * 1000),
)
const status = await AuthGithubCopilot.poll(deviceInfo.device)
if (status === "pending") continue
if (status === "complete") {
spinner.stop("Login successful")
break
}
if (status === "failed") {
spinner.stop("Failed to authorize", 1)
break
}
}
prompts.outro("Done")
return
}

View file

@ -1,20 +0,0 @@
import { AuthAnthropic } from "../../auth/anthropic"
import { UI } from "../ui"
// Example: https://claude.ai/oauth/authorize?code=true&client_id=9d1c250a-e61b-44d9-88ed-5944d1962f5e&response_type=code&redirect_uri=https%3A%2F%2Fconsole.anthropic.com%2Foauth%2Fcode%2Fcallback&scope=org%3Acreate_api_key+user%3Aprofile+user%3Ainference&code_challenge=MdFtFgFap23AWDSN0oa3-eaKjQRFE4CaEhXx8M9fHZg&code_challenge_method=S256&state=rKLtaDzm88GSwekyEqdi0wXX-YqIr13tSzYymSzpvfs
export const LoginAnthropicCommand = {
command: "anthropic",
describe: "Login to Anthropic",
handler: async () => {
const { url, verifier } = await AuthAnthropic.authorize()
UI.println("Login to Anthropic")
UI.println("Open the following URL in your browser:")
UI.println(url)
UI.println("")
const code = await UI.input("Paste the authorization code here: ")
await AuthAnthropic.exchange(code, verifier)
},
}

View file

@ -1,28 +0,0 @@
import { AuthGithubCopilot } from "../../auth/github-copilot"
import { UI } from "../ui"
export const LoginGithubCopilotCommand = {
command: "github-copilot",
describe: "Login to GitHub Copilot",
handler: async () => {
const deviceInfo = await AuthGithubCopilot.authorize()
UI.println("Login to GitHub Copilot")
UI.println("Open the following URL in your browser:")
UI.println(deviceInfo.verification_uri)
UI.println("")
UI.println(`Enter code: ${deviceInfo.user_code}`)
UI.println("")
await UI.input("Press Enter after completing authentication in browser: ")
UI.println("Waiting for authorization...")
await AuthGithubCopilot.pollForToken(
deviceInfo.device_code,
deviceInfo.interval,
)
await AuthGithubCopilot.getCopilotApiToken()
UI.println("✅ Login successful!")
},
}

View file

@ -67,13 +67,10 @@ export namespace Provider {
},
}
},
async "github-copilot"(provider) {
const tokenResult = await AuthGithubCopilot.getCopilotApiToken()
if (!tokenResult) return false
"github-copilot": async (provider) => {
const info = await AuthGithubCopilot.access()
if (!info) return false
const { apiEndpoint } = tokenResult
// If provider exists (from models.dev), set costs to 0
if (provider && provider.models) {
for (const model of Object.values(provider.models)) {
model.cost = {
@ -86,23 +83,18 @@ export namespace Provider {
return {
options: {
apiKey: "",
baseURL: apiEndpoint,
baseURL: info.api,
async fetch(input: any, init: any) {
const currentToken = await AuthGithubCopilot.access()
if (!currentToken) {
throw new Error("GitHub Copilot authentication expired")
}
const token = await AuthGithubCopilot.access()
if (!token) throw new Error("GitHub Copilot authentication expired")
const headers = {
...init.headers,
Authorization: `Bearer ${currentToken}`,
Authorization: `Bearer ${token.access}`,
"User-Agent": "GithubCopilot/1.155.0",
"Editor-Version": "vscode/1.85.1",
"Editor-Plugin-Version": "copilot/1.155.0",
}
delete headers["x-api-key"]
return fetch(input, {
...init,
headers,