mirror of
https://github.com/sst/opencode.git
synced 2025-08-04 13:30:52 +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 { Auth } from "./index"
|
||||||
|
import { NamedError } from "../util/error"
|
||||||
|
|
||||||
export namespace AuthGithubCopilot {
|
export namespace AuthGithubCopilot {
|
||||||
const CLIENT_ID = "Iv1.b507a08c87ecfe98"
|
const CLIENT_ID = "Iv1.b507a08c87ecfe98"
|
||||||
|
@ -33,7 +35,7 @@ export namespace AuthGithubCopilot {
|
||||||
const deviceResponse = await fetch(DEVICE_CODE_URL, {
|
const deviceResponse = await fetch(DEVICE_CODE_URL, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Accept": "application/json",
|
Accept: "application/json",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"User-Agent": "GithubCopilot/1.155.0",
|
"User-Agent": "GithubCopilot/1.155.0",
|
||||||
},
|
},
|
||||||
|
@ -42,146 +44,111 @@ export namespace AuthGithubCopilot {
|
||||||
scope: "read:user",
|
scope: "read:user",
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!deviceResponse.ok) {
|
|
||||||
throw new DeviceCodeError("Failed to get device code")
|
|
||||||
}
|
|
||||||
|
|
||||||
const deviceData: DeviceCodeResponse = await deviceResponse.json()
|
const deviceData: DeviceCodeResponse = await deviceResponse.json()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
device_code: deviceData.device_code,
|
device: deviceData.device_code,
|
||||||
user_code: deviceData.user_code,
|
user: deviceData.user_code,
|
||||||
verification_uri: deviceData.verification_uri,
|
verification: deviceData.verification_uri,
|
||||||
interval: deviceData.interval || 5,
|
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) {
|
export async function poll(device_code: string) {
|
||||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
const response = await fetch(ACCESS_TOKEN_URL, {
|
||||||
const response = await fetch(ACCESS_TOKEN_URL, {
|
method: "POST",
|
||||||
method: "POST",
|
headers: {
|
||||||
headers: {
|
Accept: "application/json",
|
||||||
"Accept": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Content-Type": "application/json",
|
"User-Agent": "GithubCopilot/1.155.0",
|
||||||
"User-Agent": "GithubCopilot/1.155.0",
|
},
|
||||||
},
|
body: JSON.stringify({
|
||||||
body: JSON.stringify({
|
client_id: CLIENT_ID,
|
||||||
client_id: CLIENT_ID,
|
device_code,
|
||||||
device_code,
|
grant_type: "urn:ietf:params:oauth:grant-type: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,
|
||||||
})
|
})
|
||||||
|
return "complete"
|
||||||
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}`)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new TokenExchangeError("Polling timeout exceeded")
|
if (data.error === "authorization_pending") return "pending"
|
||||||
|
|
||||||
|
if (data.error) return "failed"
|
||||||
|
|
||||||
|
return "pending"
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCopilotApiToken() {
|
export async function access() {
|
||||||
const oauthInfo = await Auth.get("github-copilot-oauth")
|
const info = await Auth.get("github-copilot")
|
||||||
if (!oauthInfo || oauthInfo.type !== "api") {
|
if (!info || info.type !== "oauth") return
|
||||||
throw new AuthenticationError("No GitHub OAuth token found")
|
if (info.access && info.expires > Date.now())
|
||||||
}
|
return { access: info.access, api: "https://api.githubcopilot.com" }
|
||||||
|
|
||||||
// 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",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get new Copilot API token
|
// Get new Copilot API token
|
||||||
const response = await fetch(COPILOT_API_KEY_URL, {
|
const response = await fetch(COPILOT_API_KEY_URL, {
|
||||||
headers: {
|
headers: {
|
||||||
"Accept": "application/json",
|
Accept: "application/json",
|
||||||
"Authorization": `Bearer ${oauthInfo.key}`,
|
Authorization: `Bearer ${info.refresh}`,
|
||||||
"User-Agent": "GithubCopilot/1.155.0",
|
"User-Agent": "GithubCopilot/1.155.0",
|
||||||
"Editor-Version": "vscode/1.85.1",
|
"Editor-Version": "vscode/1.85.1",
|
||||||
"Editor-Plugin-Version": "copilot/1.155.0",
|
"Editor-Plugin-Version": "copilot/1.155.0",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) return
|
||||||
throw new CopilotTokenError("Failed to get Copilot API token")
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokenData: CopilotTokenResponse = await response.json()
|
const tokenData: CopilotTokenResponse = await response.json()
|
||||||
|
|
||||||
// Store the Copilot API token
|
// Store the Copilot API token
|
||||||
await Auth.set("github-copilot", {
|
await Auth.set("github-copilot", {
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
refresh: "", // GitHub Copilot doesn't use refresh tokens
|
refresh: info.refresh,
|
||||||
access: tokenData.token,
|
access: tokenData.token,
|
||||||
expires: tokenData.expires_at * 1000, // Convert to milliseconds
|
expires: tokenData.expires_at * 1000,
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
token: tokenData.token,
|
access: tokenData.token,
|
||||||
apiEndpoint: tokenData.endpoints.api,
|
api: tokenData.endpoints.api,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function access() {
|
export const DeviceCodeError = NamedError.create(
|
||||||
try {
|
"DeviceCodeError",
|
||||||
const result = await getCopilotApiToken()
|
z.object({}),
|
||||||
return result.token
|
)
|
||||||
} catch (error) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DeviceCodeError extends Error {
|
export const TokenExchangeError = NamedError.create(
|
||||||
constructor(message: string) {
|
"TokenExchangeError",
|
||||||
super(message)
|
z.object({
|
||||||
this.name = "DeviceCodeError"
|
message: z.string(),
|
||||||
}
|
}),
|
||||||
}
|
)
|
||||||
|
|
||||||
export class TokenExchangeError extends Error {
|
export const AuthenticationError = NamedError.create(
|
||||||
constructor(message: string) {
|
"AuthenticationError",
|
||||||
super(message)
|
z.object({
|
||||||
this.name = "TokenExchangeError"
|
message: z.string(),
|
||||||
}
|
}),
|
||||||
}
|
)
|
||||||
|
|
||||||
export class AuthenticationError extends Error {
|
export const CopilotTokenError = NamedError.create(
|
||||||
constructor(message: string) {
|
"CopilotTokenError",
|
||||||
super(message)
|
z.object({
|
||||||
this.name = "AuthenticationError"
|
message: z.string(),
|
||||||
}
|
}),
|
||||||
}
|
)
|
||||||
|
|
||||||
export class CopilotTokenError extends Error {
|
|
||||||
constructor(message: string) {
|
|
||||||
super(message)
|
|
||||||
this.name = "CopilotTokenError"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -156,27 +156,29 @@ export const AuthLoginCommand = cmd({
|
||||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||||
const deviceInfo = await AuthGithubCopilot.authorize()
|
const deviceInfo = await AuthGithubCopilot.authorize()
|
||||||
|
|
||||||
prompts.note(`Please visit: ${deviceInfo.verification_uri}`)
|
prompts.note(
|
||||||
prompts.note(`Enter code: ${deviceInfo.user_code}`)
|
`Please visit: ${deviceInfo.verification}\nEnter code: ${deviceInfo.user}`,
|
||||||
|
)
|
||||||
|
|
||||||
const confirm = await prompts.confirm({
|
const spinner = prompts.spinner()
|
||||||
message: "Press Enter after completing authentication in browser",
|
spinner.start("Waiting for authorization...")
|
||||||
})
|
|
||||||
|
|
||||||
if (prompts.isCancel(confirm)) throw new UI.CancelledError()
|
while (true) {
|
||||||
|
await new Promise((resolve) =>
|
||||||
try {
|
setTimeout(resolve, deviceInfo.interval * 1000),
|
||||||
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)}`,
|
|
||||||
)
|
)
|
||||||
|
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")
|
prompts.outro("Done")
|
||||||
return
|
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) {
|
"github-copilot": async (provider) => {
|
||||||
const tokenResult = await AuthGithubCopilot.getCopilotApiToken()
|
const info = await AuthGithubCopilot.access()
|
||||||
if (!tokenResult) return false
|
if (!info) return false
|
||||||
|
|
||||||
const { apiEndpoint } = tokenResult
|
|
||||||
|
|
||||||
// If provider exists (from models.dev), set costs to 0
|
|
||||||
if (provider && provider.models) {
|
if (provider && provider.models) {
|
||||||
for (const model of Object.values(provider.models)) {
|
for (const model of Object.values(provider.models)) {
|
||||||
model.cost = {
|
model.cost = {
|
||||||
|
@ -86,23 +83,18 @@ export namespace Provider {
|
||||||
return {
|
return {
|
||||||
options: {
|
options: {
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
baseURL: apiEndpoint,
|
baseURL: info.api,
|
||||||
async fetch(input: any, init: any) {
|
async fetch(input: any, init: any) {
|
||||||
const currentToken = await AuthGithubCopilot.access()
|
const token = await AuthGithubCopilot.access()
|
||||||
if (!currentToken) {
|
if (!token) throw new Error("GitHub Copilot authentication expired")
|
||||||
throw new Error("GitHub Copilot authentication expired")
|
|
||||||
}
|
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
...init.headers,
|
...init.headers,
|
||||||
Authorization: `Bearer ${currentToken}`,
|
Authorization: `Bearer ${token.access}`,
|
||||||
"User-Agent": "GithubCopilot/1.155.0",
|
"User-Agent": "GithubCopilot/1.155.0",
|
||||||
"Editor-Version": "vscode/1.85.1",
|
"Editor-Version": "vscode/1.85.1",
|
||||||
"Editor-Plugin-Version": "copilot/1.155.0",
|
"Editor-Plugin-Version": "copilot/1.155.0",
|
||||||
}
|
}
|
||||||
|
|
||||||
delete headers["x-api-key"]
|
delete headers["x-api-key"]
|
||||||
|
|
||||||
return fetch(input, {
|
return fetch(input, {
|
||||||
...init,
|
...init,
|
||||||
headers,
|
headers,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue