Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ export type ExtensionState = Pick<
taskSyncEnabled: boolean
featureRoomoteControlEnabled: boolean
openAiCodexIsAuthenticated?: boolean
openAiCodexEmail?: string
debug?: boolean
}

Expand Down
84 changes: 47 additions & 37 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import OpenAI from "openai"
import type { ProviderSettings, ModelInfo } from "@roo-code/types"

import { ApiStream } from "./transform/stream"
import { ApiHandlerOptions } from "../shared/api"

import {
AnthropicHandler,
Expand Down Expand Up @@ -122,82 +123,91 @@ export interface ApiHandler {
export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
const { apiProvider, ...options } = configuration

// Inject currentApiConfigName into options if available in configuration
// This ensures handlers (like OpenAiCodexHandler) can access the profile name
// for profile-scoped operations.
const currentApiConfigName = (configuration as any).currentApiConfigName
const handlerOptions: ApiHandlerOptions = {
...options,
currentApiConfigName,
}

switch (apiProvider) {
case "anthropic":
return new AnthropicHandler(options)
return new AnthropicHandler(handlerOptions)
case "openrouter":
return new OpenRouterHandler(options)
return new OpenRouterHandler(handlerOptions)
case "bedrock":
return new AwsBedrockHandler(options)
return new AwsBedrockHandler(handlerOptions)
case "vertex":
return options.apiModelId?.startsWith("claude")
? new AnthropicVertexHandler(options)
: new VertexHandler(options)
? new AnthropicVertexHandler(handlerOptions)
: new VertexHandler(handlerOptions)
case "openai":
return new OpenAiHandler(options)
return new OpenAiHandler(handlerOptions)
case "ollama":
return new NativeOllamaHandler(options)
return new NativeOllamaHandler(handlerOptions)
case "lmstudio":
return new LmStudioHandler(options)
return new LmStudioHandler(handlerOptions)
case "gemini":
return new GeminiHandler(options)
return new GeminiHandler(handlerOptions)
case "openai-codex":
return new OpenAiCodexHandler(options)
return new OpenAiCodexHandler(handlerOptions)
case "openai-native":
return new OpenAiNativeHandler(options)
return new OpenAiNativeHandler(handlerOptions)
case "deepseek":
return new DeepSeekHandler(options)
return new DeepSeekHandler(handlerOptions)
case "doubao":
return new DoubaoHandler(options)
return new DoubaoHandler(handlerOptions)
case "qwen-code":
return new QwenCodeHandler(options)
return new QwenCodeHandler(handlerOptions)
case "moonshot":
return new MoonshotHandler(options)
return new MoonshotHandler(handlerOptions)
case "vscode-lm":
return new VsCodeLmHandler(options)
return new VsCodeLmHandler(handlerOptions)
case "mistral":
return new MistralHandler(options)
return new MistralHandler(handlerOptions)
case "unbound":
return new UnboundHandler(options)
return new UnboundHandler(handlerOptions)
case "requesty":
return new RequestyHandler(options)
return new RequestyHandler(handlerOptions)
case "fake-ai":
return new FakeAIHandler(options)
return new FakeAIHandler(handlerOptions)
case "xai":
return new XAIHandler(options)
return new XAIHandler(handlerOptions)
case "groq":
return new GroqHandler(options)
return new GroqHandler(handlerOptions)
case "deepinfra":
return new DeepInfraHandler(options)
return new DeepInfraHandler(handlerOptions)
case "huggingface":
return new HuggingFaceHandler(options)
return new HuggingFaceHandler(handlerOptions)
case "chutes":
return new ChutesHandler(options)
return new ChutesHandler(handlerOptions)
case "litellm":
return new LiteLLMHandler(options)
return new LiteLLMHandler(handlerOptions)
case "cerebras":
return new CerebrasHandler(options)
return new CerebrasHandler(handlerOptions)
case "sambanova":
return new SambaNovaHandler(options)
return new SambaNovaHandler(handlerOptions)
case "zai":
return new ZAiHandler(options)
return new ZAiHandler(handlerOptions)
case "fireworks":
return new FireworksHandler(options)
return new FireworksHandler(handlerOptions)
case "io-intelligence":
return new IOIntelligenceHandler(options)
return new IOIntelligenceHandler(handlerOptions)
case "roo":
// Never throw exceptions from provider constructors
// The provider-proxy server will handle authentication and return appropriate error codes
return new RooHandler(options)
return new RooHandler(handlerOptions)
case "featherless":
return new FeatherlessHandler(options)
return new FeatherlessHandler(handlerOptions)
case "vercel-ai-gateway":
return new VercelAiGatewayHandler(options)
return new VercelAiGatewayHandler(handlerOptions)
case "minimax":
return new MiniMaxHandler(options)
return new MiniMaxHandler(handlerOptions)
case "baseten":
return new BasetenHandler(options)
return new BasetenHandler(handlerOptions)
default:
return new AnthropicHandler(options)
return new AnthropicHandler(handlerOptions)
}
}
14 changes: 8 additions & 6 deletions src/api/providers/openai-codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
this.pendingToolCallName = undefined

// Get access token from OAuth manager
let accessToken = await openAiCodexOAuthManager.getAccessToken()
const profileId = this.options.currentApiConfigName
let accessToken = await openAiCodexOAuthManager.getAccessToken(profileId)
if (!accessToken) {
throw new Error(
t("common:errors.openAiCodex.notAuthenticated", {
Expand Down Expand Up @@ -183,7 +184,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion

if (attempt === 0 && isAuthFailure) {
// Force refresh the token for retry
const refreshed = await openAiCodexOAuthManager.forceRefreshAccessToken()
const refreshed = await openAiCodexOAuthManager.forceRefreshAccessToken(profileId)
if (!refreshed) {
throw new Error(
t("common:errors.openAiCodex.notAuthenticated", {
Expand Down Expand Up @@ -341,7 +342,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
// is consistent across providers.
try {
// Get ChatGPT account ID for organization subscriptions
const accountId = await openAiCodexOAuthManager.getAccountId()
const accountId = await openAiCodexOAuthManager.getAccountId(this.options.currentApiConfigName)

// Build Codex-specific headers. Authorization is provided by the SDK apiKey.
const codexHeaders: Record<string, string> = {
Expand Down Expand Up @@ -481,7 +482,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
const url = `${CODEX_API_BASE_URL}/responses`

// Get ChatGPT account ID for organization subscriptions
const accountId = await openAiCodexOAuthManager.getAccountId()
const accountId = await openAiCodexOAuthManager.getAccountId(this.options.currentApiConfigName)

// Build headers with required Codex-specific fields
const headers: Record<string, string> = {
Expand Down Expand Up @@ -1008,7 +1009,8 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
const model = this.getModel()

// Get access token
const accessToken = await openAiCodexOAuthManager.getAccessToken()
const profileId = this.options.currentApiConfigName
const accessToken = await openAiCodexOAuthManager.getAccessToken(profileId)
if (!accessToken) {
throw new Error(
t("common:errors.openAiCodex.notAuthenticated", {
Expand Down Expand Up @@ -1043,7 +1045,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
const url = `${CODEX_API_BASE_URL}/responses`

// Get ChatGPT account ID for organization subscriptions
const accountId = await openAiCodexOAuthManager.getAccountId()
const accountId = await openAiCodexOAuthManager.getAccountId(this.options.currentApiConfigName)

// Build headers with required Codex-specific fields
const headers: Record<string, string> = {
Expand Down
14 changes: 13 additions & 1 deletion src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2235,11 +2235,23 @@ export class ClineProvider
openAiCodexIsAuthenticated: await (async () => {
try {
const { openAiCodexOAuthManager } = await import("../../integrations/openai-codex/oauth")
return await openAiCodexOAuthManager.isAuthenticated()
// Ensure we use the latest currentApiConfigName from stateValues
const configName = this.contextProxy.getValue("currentApiConfigName")
return await openAiCodexOAuthManager.isAuthenticated(configName)
} catch {
return false
}
})(),
openAiCodexEmail: await (async () => {
try {
const { openAiCodexOAuthManager } = await import("../../integrations/openai-codex/oauth")
const configName = this.contextProxy.getValue("currentApiConfigName")
const email = await openAiCodexOAuthManager.getEmail(configName)
return email || undefined
} catch {
return undefined
}
})(),
debug: vscode.workspace.getConfiguration(Package.name).get<boolean>("debug", false),
}
}
Expand Down
11 changes: 7 additions & 4 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2385,7 +2385,8 @@ export const webviewMessageHandler = async (
case "openAiCodexSignIn": {
try {
const { openAiCodexOAuthManager } = await import("../../integrations/openai-codex/oauth")
const authUrl = openAiCodexOAuthManager.startAuthorizationFlow()
const profileId = getGlobalState("currentApiConfigName")
const authUrl = openAiCodexOAuthManager.startAuthorizationFlow(profileId)

// Open the authorization URL in the browser
await vscode.env.openExternal(vscode.Uri.parse(authUrl))
Expand All @@ -2412,7 +2413,8 @@ export const webviewMessageHandler = async (
case "openAiCodexSignOut": {
try {
const { openAiCodexOAuthManager } = await import("../../integrations/openai-codex/oauth")
await openAiCodexOAuthManager.clearCredentials()
const profileId = getGlobalState("currentApiConfigName")
await openAiCodexOAuthManager.clearCredentials(profileId)
vscode.window.showInformationMessage("Signed out from OpenAI Codex")
await provider.postStateToWebview()
} catch (error) {
Expand Down Expand Up @@ -3244,7 +3246,8 @@ export const webviewMessageHandler = async (
case "requestOpenAiCodexRateLimits": {
try {
const { openAiCodexOAuthManager } = await import("../../integrations/openai-codex/oauth")
const accessToken = await openAiCodexOAuthManager.getAccessToken()
const profileId = getGlobalState("currentApiConfigName")
const accessToken = await openAiCodexOAuthManager.getAccessToken(profileId)

if (!accessToken) {
provider.postMessageToWebview({
Expand All @@ -3254,7 +3257,7 @@ export const webviewMessageHandler = async (
break
}

const accountId = await openAiCodexOAuthManager.getAccountId()
const accountId = await openAiCodexOAuthManager.getAccountId(profileId)
const { fetchOpenAiCodexRateLimitInfo } = await import("../../integrations/openai-codex/rate-limits")
const rateLimits = await fetchOpenAiCodexRateLimitInfo(accessToken, { accountId })

Expand Down
86 changes: 86 additions & 0 deletions src/integrations/openai-codex/__tests__/oauth.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
import { OpenAiCodexOAuthManager } from "../oauth"

describe("OpenAiCodexOAuthManager profile scoping", () => {
let manager: OpenAiCodexOAuthManager
let mockSecrets: Map<string, string>
let mockContext: any

beforeEach(() => {
mockSecrets = new Map()
mockContext = {
secrets: {
get: vi.fn(async (key: string) => mockSecrets.get(key)),
store: vi.fn(async (key: string, value: string) => {
mockSecrets.set(key, value)
}),
delete: vi.fn(async (key: string) => {
mockSecrets.delete(key)
}),
},
}
manager = new OpenAiCodexOAuthManager()
manager.initialize(mockContext)
})

it("should use different storage keys for different profiles", async () => {
const creds1 = {
type: "openai-codex" as const,
access_token: "token1",
refresh_token: "refresh1",
expires: Date.now() + 3600000,
email: "user1@example.com",
}
const creds2 = {
type: "openai-codex" as const,
access_token: "token2",
refresh_token: "refresh2",
expires: Date.now() + 3600000,
email: "user2@example.com",
}

await manager.saveCredentials(creds1, "profile1")
await manager.saveCredentials(creds2, "profile2")

const loaded1 = await manager.loadCredentials("profile1")
const loaded2 = await manager.loadCredentials("profile2")

expect(loaded1?.email).toBe("user1@example.com")
expect(loaded2?.email).toBe("user2@example.com")
expect(mockSecrets.has("openai-codex-oauth-credentials-profile1")).toBe(true)
expect(mockSecrets.has("openai-codex-oauth-credentials-profile2")).toBe(true)
})

it("should fall back to default key when no profileId is provided", async () => {
const creds = {
type: "openai-codex" as const,
access_token: "default-token",
refresh_token: "default-refresh",
expires: Date.now() + 3600000,
email: "default@example.com",
}

await manager.saveCredentials(creds)
expect(mockSecrets.has("openai-codex-oauth-credentials")).toBe(true)

const loaded = await manager.loadCredentials()
expect(loaded?.email).toBe("default@example.com")
})

it("should clear credentials only for the specified profile", async () => {
const creds = {
type: "openai-codex" as const,
access_token: "token",
refresh_token: "refresh",
expires: Date.now() + 3600000,
}

await manager.saveCredentials(creds, "profile1")
await manager.saveCredentials(creds, "profile2")

await manager.clearCredentials("profile1")

expect(await manager.getCredentials("profile1")).toBeNull()
expect(await manager.loadCredentials("profile2")).not.toBeNull()
})
})
Loading
Loading