mirror of
https://github.com/sst/opencode.git
synced 2025-07-07 16:14:59 +00:00
cleanup
This commit is contained in:
parent
63c504f086
commit
5e46d98c86
6 changed files with 134 additions and 187 deletions
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
}
|
|
@ -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!")
|
||||
},
|
||||
}
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue