mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
feat(cli): add OpenAI Codex OAuth support
- Add openai-codex to supported providers list - Implement OAuth authentication flow for OpenAI Codex - Create credentials storage for OpenAI Codex tokens - Add CLI commands: auth openai-codex:login, auth openai-codex:logout, auth openai-codex:status - Update provider settings to handle openai-codex without API key - OAuth uses PKCE flow with local callback server on port 1455
This commit is contained in:
parent
4ee494b08c
commit
6a4d54df83
10 changed files with 617 additions and 4 deletions
|
|
@ -1,3 +1,6 @@
|
|||
export * from "./login.js"
|
||||
export * from "./logout.js"
|
||||
export * from "./status.js"
|
||||
export * from "./openai-codex-login.js"
|
||||
export * from "./openai-codex-logout.js"
|
||||
export * from "./openai-codex-status.js"
|
||||
|
|
|
|||
9
apps/cli/src/commands/auth/openai-codex-login.ts
Normal file
9
apps/cli/src/commands/auth/openai-codex-login.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import {
|
||||
loginOpenAiCodex,
|
||||
type OpenAiCodexLoginOptions,
|
||||
type OpenAiCodexLoginResult,
|
||||
} from "@/lib/auth/openai-codex-oauth.js"
|
||||
|
||||
export async function openaiCodexLogin(options: OpenAiCodexLoginOptions = {}): Promise<OpenAiCodexLoginResult> {
|
||||
return loginOpenAiCodex(options)
|
||||
}
|
||||
6
apps/cli/src/commands/auth/openai-codex-logout.ts
Normal file
6
apps/cli/src/commands/auth/openai-codex-logout.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { logoutOpenAiCodex } from "@/lib/auth/openai-codex-oauth.js"
|
||||
|
||||
export async function openaiCodexLogout(): Promise<void> {
|
||||
await logoutOpenAiCodex()
|
||||
console.log("✓ Successfully logged out of OpenAI Codex")
|
||||
}
|
||||
17
apps/cli/src/commands/auth/openai-codex-status.ts
Normal file
17
apps/cli/src/commands/auth/openai-codex-status.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { isAuthenticatedOpenAiCodex } from "@/lib/auth/openai-codex-oauth.js"
|
||||
import { loadOpenAiCodexCredentials } from "@/lib/storage/openai-codex-credentials.js"
|
||||
|
||||
export async function openaiCodexStatus(): Promise<void> {
|
||||
const authenticated = await isAuthenticatedOpenAiCodex()
|
||||
|
||||
if (authenticated) {
|
||||
const credentials = await loadOpenAiCodexCredentials()
|
||||
console.log("✓ Authenticated with OpenAI Codex")
|
||||
if (credentials?.email) {
|
||||
console.log(` Email: ${credentials.email}`)
|
||||
}
|
||||
} else {
|
||||
console.log("✗ Not authenticated with OpenAI Codex")
|
||||
console.log(" Run 'roo-code auth openai-codex:login' to authenticate")
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import { Command } from "commander"
|
|||
|
||||
import { DEFAULT_FLAGS } from "@/types/constants.js"
|
||||
import { VERSION } from "@/lib/utils/version.js"
|
||||
import { run, login, logout, status } from "@/commands/index.js"
|
||||
import { run, login, logout, status, openaiCodexLogin, openaiCodexLogout, openaiCodexStatus } from "@/commands/index.js"
|
||||
|
||||
const program = new Command()
|
||||
|
||||
|
|
@ -62,4 +62,29 @@ authCommand
|
|||
process.exit(result.authenticated ? 0 : 1)
|
||||
})
|
||||
|
||||
authCommand
|
||||
.command("openai-codex:login")
|
||||
.description("Authenticate with OpenAI Codex (ChatGPT Plus/Pro)")
|
||||
.option("-v, --verbose", "Enable verbose output", false)
|
||||
.action(async (options: { verbose: boolean }) => {
|
||||
const result = await openaiCodexLogin({ verbose: options.verbose })
|
||||
process.exit(result.success ? 0 : 1)
|
||||
})
|
||||
|
||||
authCommand
|
||||
.command("openai-codex:logout")
|
||||
.description("Log out from OpenAI Codex")
|
||||
.action(async () => {
|
||||
await openaiCodexLogout()
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
authCommand
|
||||
.command("openai-codex:status")
|
||||
.description("Show OpenAI Codex authentication status")
|
||||
.action(async () => {
|
||||
await openaiCodexStatus()
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
program.parse()
|
||||
|
|
|
|||
492
apps/cli/src/lib/auth/openai-codex-oauth.ts
Normal file
492
apps/cli/src/lib/auth/openai-codex-oauth.ts
Normal file
|
|
@ -0,0 +1,492 @@
|
|||
import * as crypto from "crypto"
|
||||
import * as http from "http"
|
||||
import { URL } from "url"
|
||||
import { exec } from "child_process"
|
||||
|
||||
import type { OpenAiCodexCredentials } from "../storage/openai-codex-credentials.js"
|
||||
import {
|
||||
saveOpenAiCodexCredentials,
|
||||
loadOpenAiCodexCredentials,
|
||||
clearOpenAiCodexCredentials,
|
||||
} from "../storage/openai-codex-credentials.js"
|
||||
|
||||
/**
|
||||
* OpenAI Codex OAuth Configuration
|
||||
* Based on the OpenAI Codex OAuth implementation
|
||||
*/
|
||||
export const OPENAI_CODEX_OAUTH_CONFIG = {
|
||||
authorizationEndpoint: "https://auth.openai.com/oauth/authorize",
|
||||
tokenEndpoint: "https://auth.openai.com/oauth/token",
|
||||
clientId: "app_EMoamEEZ73f0CkXaXp7hrann",
|
||||
redirectUri: "http://localhost:1455/auth/callback",
|
||||
scopes: "openid profile email offline_access",
|
||||
callbackPort: 1455,
|
||||
} as const
|
||||
|
||||
interface TokenResponse {
|
||||
access_token: string
|
||||
refresh_token?: string
|
||||
id_token?: string
|
||||
expires_in: number
|
||||
email?: string
|
||||
token_type?: string
|
||||
}
|
||||
|
||||
interface IdTokenClaims {
|
||||
chatgpt_account_id?: string
|
||||
organizations?: Array<{ id: string }>
|
||||
email?: string
|
||||
"https://api.openai.com/auth"?: {
|
||||
chatgpt_account_id?: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse JWT claims from a token
|
||||
*/
|
||||
function parseJwtClaims(token: string): IdTokenClaims | undefined {
|
||||
const parts = token.split(".")
|
||||
if (parts.length !== 3 || !parts[1]) return undefined
|
||||
try {
|
||||
// Base64url decode: convert base64url to base64, then decode
|
||||
const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/")
|
||||
const payload = Buffer.from(base64, "base64").toString("utf-8")
|
||||
return JSON.parse(payload) as IdTokenClaims
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract ChatGPT account ID from JWT claims
|
||||
*/
|
||||
function extractAccountIdFromClaims(claims: IdTokenClaims): string | undefined {
|
||||
return (
|
||||
claims.chatgpt_account_id ||
|
||||
claims["https://api.openai.com/auth"]?.chatgpt_account_id ||
|
||||
claims.organizations?.[0]?.id
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract ChatGPT account ID from token response
|
||||
*/
|
||||
function extractAccountId(tokens: { id_token?: string; access_token: string }): string | undefined {
|
||||
if (tokens.id_token) {
|
||||
const claims = parseJwtClaims(tokens.id_token)
|
||||
const accountId = claims && extractAccountIdFromClaims(claims)
|
||||
if (accountId) return accountId
|
||||
}
|
||||
if (tokens.access_token) {
|
||||
const claims = parseJwtClaims(tokens.access_token)
|
||||
return claims ? extractAccountIdFromClaims(claims) : undefined
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a cryptographically random PKCE code verifier
|
||||
*/
|
||||
export function generateCodeVerifier(): string {
|
||||
const buffer = crypto.randomBytes(32)
|
||||
return buffer.toString("base64url")
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the PKCE code challenge from the verifier using S256 method
|
||||
*/
|
||||
export function generateCodeChallenge(verifier: string): string {
|
||||
const hash = crypto.createHash("sha256").update(verifier).digest()
|
||||
return hash.toString("base64url")
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random state parameter for CSRF protection
|
||||
*/
|
||||
export function generateState(): string {
|
||||
return crypto.randomBytes(16).toString("hex")
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the authorization URL for OpenAI Codex OAuth flow
|
||||
*/
|
||||
export function buildAuthorizationUrl(codeChallenge: string, state: string): string {
|
||||
const params = new URLSearchParams({
|
||||
client_id: OPENAI_CODEX_OAUTH_CONFIG.clientId,
|
||||
redirect_uri: OPENAI_CODEX_OAUTH_CONFIG.redirectUri,
|
||||
scope: OPENAI_CODEX_OAUTH_CONFIG.scopes,
|
||||
code_challenge: codeChallenge,
|
||||
code_challenge_method: "S256",
|
||||
response_type: "code",
|
||||
state,
|
||||
// Codex-specific parameters
|
||||
codex_cli_simplified_flow: "true",
|
||||
originator: "roo-code",
|
||||
})
|
||||
|
||||
return `${OPENAI_CODEX_OAUTH_CONFIG.authorizationEndpoint}?${params.toString()}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchanges the authorization code for tokens
|
||||
*/
|
||||
export async function exchangeCodeForTokens(code: string, codeVerifier: string): Promise<OpenAiCodexCredentials> {
|
||||
const body = new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
client_id: OPENAI_CODEX_OAUTH_CONFIG.clientId,
|
||||
code,
|
||||
redirect_uri: OPENAI_CODEX_OAUTH_CONFIG.redirectUri,
|
||||
code_verifier: codeVerifier,
|
||||
})
|
||||
|
||||
const response = await fetch(OPENAI_CODEX_OAUTH_CONFIG.tokenEndpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: body.toString(),
|
||||
signal: AbortSignal.timeout(30000),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`Token exchange failed: ${response.status} ${response.statusText} - ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const tokenResponse = data as TokenResponse
|
||||
|
||||
if (!tokenResponse.access_token || !tokenResponse.refresh_token || !tokenResponse.expires_in) {
|
||||
throw new Error("Invalid token response: missing required fields")
|
||||
}
|
||||
|
||||
const expiresAt = Date.now() + tokenResponse.expires_in * 1000
|
||||
const accountId = extractAccountId({
|
||||
id_token: tokenResponse.id_token,
|
||||
access_token: tokenResponse.access_token,
|
||||
})
|
||||
|
||||
return {
|
||||
type: "openai-codex",
|
||||
access_token: tokenResponse.access_token,
|
||||
refresh_token: tokenResponse.refresh_token,
|
||||
expires: expiresAt,
|
||||
email: tokenResponse.email,
|
||||
accountId,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the access token using the refresh token
|
||||
*/
|
||||
export async function refreshAccessToken(credentials: OpenAiCodexCredentials): Promise<OpenAiCodexCredentials> {
|
||||
const body = new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
client_id: OPENAI_CODEX_OAUTH_CONFIG.clientId,
|
||||
refresh_token: credentials.refresh_token,
|
||||
})
|
||||
|
||||
const response = await fetch(OPENAI_CODEX_OAUTH_CONFIG.tokenEndpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: body.toString(),
|
||||
signal: AbortSignal.timeout(30000),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`Token refresh failed: ${response.status} ${response.statusText} - ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const tokenResponse = data as TokenResponse
|
||||
|
||||
const expiresAt = Date.now() + tokenResponse.expires_in * 1000
|
||||
const newAccountId = extractAccountId({
|
||||
id_token: tokenResponse.id_token,
|
||||
access_token: tokenResponse.access_token,
|
||||
})
|
||||
|
||||
return {
|
||||
type: "openai-codex",
|
||||
access_token: tokenResponse.access_token,
|
||||
refresh_token: tokenResponse.refresh_token ?? credentials.refresh_token,
|
||||
expires: expiresAt,
|
||||
email: tokenResponse.email ?? credentials.email,
|
||||
accountId: newAccountId ?? credentials.accountId,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the credentials are expired (with 5 minute buffer)
|
||||
*/
|
||||
export function isTokenExpired(credentials: OpenAiCodexCredentials): boolean {
|
||||
const bufferMs = 5 * 60 * 1000 // 5 minutes buffer
|
||||
return Date.now() >= credentials.expires - bufferMs
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a valid access token, refreshing if necessary
|
||||
*/
|
||||
export async function getAccessToken(): Promise<string | null> {
|
||||
let credentials = await loadOpenAiCodexCredentials()
|
||||
|
||||
if (!credentials) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Refresh if expired
|
||||
if (isTokenExpired(credentials)) {
|
||||
try {
|
||||
credentials = await refreshAccessToken(credentials)
|
||||
await saveOpenAiCodexCredentials(credentials)
|
||||
} catch (error) {
|
||||
console.error("Failed to refresh OpenAI Codex token:", error)
|
||||
// Clear invalid credentials
|
||||
await clearOpenAiCodexCredentials()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return credentials.access_token
|
||||
}
|
||||
|
||||
/**
|
||||
* Open browser for authentication
|
||||
*/
|
||||
function openBrowser(url: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const platform = process.platform
|
||||
let command: string
|
||||
|
||||
switch (platform) {
|
||||
case "darwin":
|
||||
command = `open "${url}"`
|
||||
break
|
||||
case "win32":
|
||||
command = `start "" "${url}"`
|
||||
break
|
||||
default:
|
||||
command = `xdg-open "${url}"`
|
||||
break
|
||||
}
|
||||
|
||||
exec(command, (error) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export interface OpenAiCodexLoginOptions {
|
||||
timeout?: number
|
||||
verbose?: boolean
|
||||
}
|
||||
|
||||
export interface OpenAiCodexLoginResult {
|
||||
success: boolean
|
||||
error?: string
|
||||
email?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform OpenAI Codex OAuth login flow
|
||||
*/
|
||||
export async function loginOpenAiCodex({
|
||||
timeout = 5 * 60 * 1000,
|
||||
verbose = false,
|
||||
}: OpenAiCodexLoginOptions = {}): Promise<OpenAiCodexLoginResult> {
|
||||
const codeVerifier = generateCodeVerifier()
|
||||
const codeChallenge = generateCodeChallenge(codeVerifier)
|
||||
const state = generateState()
|
||||
|
||||
if (verbose) {
|
||||
console.log(
|
||||
`[OpenAI Codex Auth] Starting local callback server on port ${OPENAI_CODEX_OAUTH_CONFIG.callbackPort}`,
|
||||
)
|
||||
}
|
||||
|
||||
const credentialsPromise = new Promise<OpenAiCodexCredentials>((resolve, reject) => {
|
||||
const server = http.createServer(async (req, res) => {
|
||||
const url = new URL(req.url || "", `http://localhost:${OPENAI_CODEX_OAUTH_CONFIG.callbackPort}`)
|
||||
|
||||
if (url.pathname !== "/auth/callback") {
|
||||
res.writeHead(404)
|
||||
res.end("Not Found")
|
||||
return
|
||||
}
|
||||
|
||||
const code = url.searchParams.get("code")
|
||||
const receivedState = url.searchParams.get("state")
|
||||
const error = url.searchParams.get("error")
|
||||
|
||||
if (error) {
|
||||
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" })
|
||||
res.end(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Authentication Failed</title></head>
|
||||
<body>
|
||||
<h1>Authentication failed: ${error}</h1>
|
||||
<p>You can close this window and return to the CLI.</p>
|
||||
</body>
|
||||
</html>`)
|
||||
res.on("close", () => {
|
||||
server.close()
|
||||
reject(new Error(`OAuth error: ${error}`))
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!code || !receivedState) {
|
||||
res.writeHead(400)
|
||||
res.end("Missing code or state parameter")
|
||||
res.on("close", () => {
|
||||
server.close()
|
||||
reject(new Error("Missing code or state parameter"))
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (receivedState !== state) {
|
||||
res.writeHead(400)
|
||||
res.end("State mismatch - possible CSRF attack")
|
||||
res.on("close", () => {
|
||||
server.close()
|
||||
reject(new Error("State mismatch"))
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const credentials = await exchangeCodeForTokens(code, codeVerifier)
|
||||
await saveOpenAiCodexCredentials(credentials)
|
||||
|
||||
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" })
|
||||
res.end(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Authentication Successful</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
background: linear-gradient(135deg, #10a37f 0%, #0d8f6f 100%);
|
||||
color: white;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
h1 { font-size: 2rem; margin-bottom: 1rem; }
|
||||
p { opacity: 0.9; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>✓ Authentication Successful</h1>
|
||||
<p>You can close this window and return to the CLI.</p>
|
||||
</div>
|
||||
<script>setTimeout(() => window.close(), 3000);</script>
|
||||
</body>
|
||||
</html>`)
|
||||
|
||||
res.on("close", () => {
|
||||
server.close()
|
||||
resolve(credentials)
|
||||
})
|
||||
} catch (exchangeError) {
|
||||
res.writeHead(500)
|
||||
res.end(`Token exchange failed: ${exchangeError}`)
|
||||
res.on("close", () => {
|
||||
server.close()
|
||||
reject(exchangeError)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
server.on("error", (err: NodeJS.ErrnoException) => {
|
||||
if (err.code === "EADDRINUSE") {
|
||||
reject(
|
||||
new Error(
|
||||
`Port ${OPENAI_CODEX_OAUTH_CONFIG.callbackPort} is already in use. ` +
|
||||
`Please close any other applications using this port and try again.`,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
server.close()
|
||||
reject(new Error("Authentication timed out"))
|
||||
}, timeout)
|
||||
|
||||
server.listen(OPENAI_CODEX_OAUTH_CONFIG.callbackPort, "127.0.0.1", () => {
|
||||
if (verbose) {
|
||||
console.log(
|
||||
`[OpenAI Codex Auth] Callback server listening on port ${OPENAI_CODEX_OAUTH_CONFIG.callbackPort}`,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
server.on("close", () => {
|
||||
clearTimeout(timeoutId)
|
||||
if (verbose) {
|
||||
console.log("[OpenAI Codex Auth] Callback server closed")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const authUrl = buildAuthorizationUrl(codeChallenge, state)
|
||||
|
||||
console.log("Opening browser for OpenAI Codex authentication...")
|
||||
console.log(`If the browser doesn't open, visit: ${authUrl}`)
|
||||
|
||||
try {
|
||||
await openBrowser(authUrl)
|
||||
} catch (error) {
|
||||
if (verbose) {
|
||||
console.warn("[OpenAI Codex Auth] Failed to open browser automatically:", error)
|
||||
}
|
||||
console.log("Please open the URL above in your browser manually.")
|
||||
}
|
||||
|
||||
try {
|
||||
const credentials = await credentialsPromise
|
||||
console.log("✓ Successfully authenticated with OpenAI Codex!")
|
||||
if (credentials.email) {
|
||||
console.log(` Email: ${credentials.email}`)
|
||||
}
|
||||
return { success: true, email: credentials.email }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
console.error(`✗ OpenAI Codex authentication failed: ${message}`)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout from OpenAI Codex (clear credentials)
|
||||
*/
|
||||
export async function logoutOpenAiCodex(): Promise<void> {
|
||||
await clearOpenAiCodexCredentials()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if authenticated with OpenAI Codex
|
||||
*/
|
||||
export async function isAuthenticatedOpenAiCodex(): Promise<boolean> {
|
||||
const credentials = await loadOpenAiCodexCredentials()
|
||||
return credentials !== null
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
export * from "./config-dir.js"
|
||||
export * from "./settings.js"
|
||||
export * from "./credentials.js"
|
||||
export * from "./openai-codex-credentials.js"
|
||||
export * from "./ephemeral.js"
|
||||
|
|
|
|||
54
apps/cli/src/lib/storage/openai-codex-credentials.ts
Normal file
54
apps/cli/src/lib/storage/openai-codex-credentials.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
|
||||
import { getConfigDir } from "./index.js"
|
||||
|
||||
const OPENAI_CODEX_CREDENTIALS_FILE = path.join(getConfigDir(), "openai-codex-credentials.json")
|
||||
|
||||
export interface OpenAiCodexCredentials {
|
||||
type: "openai-codex"
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
expires: number
|
||||
email?: string
|
||||
accountId?: string
|
||||
}
|
||||
|
||||
export async function saveOpenAiCodexCredentials(credentials: OpenAiCodexCredentials): Promise<void> {
|
||||
await fs.mkdir(getConfigDir(), { recursive: true })
|
||||
|
||||
await fs.writeFile(OPENAI_CODEX_CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), {
|
||||
mode: 0o600, // Read/write for owner only
|
||||
})
|
||||
}
|
||||
|
||||
export async function loadOpenAiCodexCredentials(): Promise<OpenAiCodexCredentials | null> {
|
||||
try {
|
||||
const data = await fs.readFile(OPENAI_CODEX_CREDENTIALS_FILE, "utf-8")
|
||||
return JSON.parse(data) as OpenAiCodexCredentials
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
return null
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearOpenAiCodexCredentials(): Promise<void> {
|
||||
try {
|
||||
await fs.unlink(OPENAI_CODEX_CREDENTIALS_FILE)
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function hasOpenAiCodexCredentials(): Promise<boolean> {
|
||||
const credentials = await loadOpenAiCodexCredentials()
|
||||
return credentials !== null
|
||||
}
|
||||
|
||||
export function getOpenAiCodexCredentialsPath(): string {
|
||||
return OPENAI_CODEX_CREDENTIALS_FILE
|
||||
}
|
||||
|
|
@ -2,22 +2,23 @@ import { RooCodeSettings } from "@roo-code/types"
|
|||
|
||||
import type { SupportedProvider } from "@/types/index.js"
|
||||
|
||||
const envVarMap: Record<SupportedProvider, string> = {
|
||||
const envVarMap: Record<SupportedProvider, string | null> = {
|
||||
anthropic: "ANTHROPIC_API_KEY",
|
||||
"openai-native": "OPENAI_API_KEY",
|
||||
"openai-codex": null, // Uses OAuth, not API key
|
||||
gemini: "GOOGLE_API_KEY",
|
||||
openrouter: "OPENROUTER_API_KEY",
|
||||
"vercel-ai-gateway": "VERCEL_AI_GATEWAY_API_KEY",
|
||||
roo: "ROO_API_KEY",
|
||||
}
|
||||
|
||||
export function getEnvVarName(provider: SupportedProvider): string {
|
||||
export function getEnvVarName(provider: SupportedProvider): string | null {
|
||||
return envVarMap[provider]
|
||||
}
|
||||
|
||||
export function getApiKeyFromEnv(provider: SupportedProvider): string | undefined {
|
||||
const envVar = getEnvVarName(provider)
|
||||
return process.env[envVar]
|
||||
return envVar ? process.env[envVar] : undefined
|
||||
}
|
||||
|
||||
export function getProviderSettings(
|
||||
|
|
@ -36,6 +37,10 @@ export function getProviderSettings(
|
|||
if (apiKey) config.openAiNativeApiKey = apiKey
|
||||
if (model) config.apiModelId = model
|
||||
break
|
||||
case "openai-codex":
|
||||
// OpenAI Codex uses OAuth, credentials are handled separately
|
||||
if (model) config.apiModelId = model
|
||||
break
|
||||
case "gemini":
|
||||
if (apiKey) config.geminiApiKey = apiKey
|
||||
if (model) config.apiModelId = model
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type { ProviderName, ReasoningEffortExtended } from "@roo-code/types"
|
|||
export const supportedProviders = [
|
||||
"anthropic",
|
||||
"openai-native",
|
||||
"openai-codex",
|
||||
"gemini",
|
||||
"openrouter",
|
||||
"vercel-ai-gateway",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue