Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.
Closed
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
2 changes: 2 additions & 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
openAiCodexAuthenticatedEmail?: string
debug?: boolean
}

Expand Down Expand Up @@ -664,6 +665,7 @@ export interface WebviewMessage {
list?: string[] // For dismissedUpsells response
organizationId?: string | null // For organization switching
useProviderSignup?: boolean // For rooCloudSignIn to use provider signup flow
profileId?: string // For profile-scoped OAuth operations (openAiCodexSignIn, openAiCodexSignOut, requestOpenAiCodexRateLimits)
codeIndexSettings?: {
// Global state settings
codebaseIndexEnabled: boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ describe("OpenAiCodexHandler native tool calls", () => {
})

it("yields tool_call_partial chunks when API returns function_call-only response", async () => {
vi.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token")
vi.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test")
vi.spyOn(openAiCodexOAuthManager, "getAccessTokenForProfile").mockResolvedValue("test-token")
vi.spyOn(openAiCodexOAuthManager, "getAccountIdForProfile").mockResolvedValue("acct_test")

// Mock OpenAI SDK streaming (preferred path).
;(handler as any).client = {
Expand Down
28 changes: 16 additions & 12 deletions src/api/providers/openai-codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
private abortController?: AbortController
// Session ID for the Codex API (persists for the lifetime of the handler)
private readonly sessionId: string
// Profile ID for profile-scoped OAuth credentials
private readonly profileId: string | undefined
/**
* Some Codex/Responses streams emit tool-call argument deltas without stable call id/name.
* Track the last observed tool identity from output_item events so we can still
Expand Down Expand Up @@ -89,6 +91,8 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
this.options = options
// Generate a new session ID for standalone handler usage (fallback)
this.sessionId = uuidv7()
// Store profile ID for profile-scoped OAuth credentials
this.profileId = options.apiConfigurationId
}

private normalizeUsage(usage: any, model: OpenAiCodexModel): ApiStreamUsageChunk | undefined {
Expand Down Expand Up @@ -150,8 +154,8 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
this.pendingToolCallId = undefined
this.pendingToolCallName = undefined

// Get access token from OAuth manager
let accessToken = await openAiCodexOAuthManager.getAccessToken()
// Get access token from OAuth manager (profile-scoped)
let accessToken = await openAiCodexOAuthManager.getAccessTokenForProfile(this.profileId)
if (!accessToken) {
throw new Error(
t("common:errors.openAiCodex.notAuthenticated", {
Expand Down Expand Up @@ -182,8 +186,8 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
const isAuthFailure = /unauthorized|invalid token|not authenticated|authentication|401/i.test(message)

if (attempt === 0 && isAuthFailure) {
// Force refresh the token for retry
const refreshed = await openAiCodexOAuthManager.forceRefreshAccessToken()
// Force refresh the token for retry (profile-scoped)
const refreshed = await openAiCodexOAuthManager.forceRefreshAccessTokenForProfile(this.profileId)
if (!refreshed) {
throw new Error(
t("common:errors.openAiCodex.notAuthenticated", {
Expand Down Expand Up @@ -340,8 +344,8 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
// Prefer OpenAI SDK streaming (same approach as openai-native) so event handling
// is consistent across providers.
try {
// Get ChatGPT account ID for organization subscriptions
const accountId = await openAiCodexOAuthManager.getAccountId()
// Get ChatGPT account ID for organization subscriptions (profile-scoped)
const accountId = await openAiCodexOAuthManager.getAccountIdForProfile(this.profileId)

// Build Codex-specific headers. Authorization is provided by the SDK apiKey.
const codexHeaders: Record<string, string> = {
Expand Down Expand Up @@ -480,8 +484,8 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
// Per the implementation guide: route to Codex backend with Bearer token
const url = `${CODEX_API_BASE_URL}/responses`

// Get ChatGPT account ID for organization subscriptions
const accountId = await openAiCodexOAuthManager.getAccountId()
// Get ChatGPT account ID for organization subscriptions (profile-scoped)
const accountId = await openAiCodexOAuthManager.getAccountIdForProfile(this.profileId)

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

// Get access token
const accessToken = await openAiCodexOAuthManager.getAccessToken()
// Get access token (profile-scoped)
const accessToken = await openAiCodexOAuthManager.getAccessTokenForProfile(this.profileId)
if (!accessToken) {
throw new Error(
t("common:errors.openAiCodex.notAuthenticated", {
Expand Down Expand Up @@ -1042,8 +1046,8 @@ 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()
// Get ChatGPT account ID for organization subscriptions (profile-scoped)
const accountId = await openAiCodexOAuthManager.getAccountIdForProfile(this.profileId)

// Build headers with required Codex-specific fields
const headers: Record<string, string> = {
Expand Down
18 changes: 14 additions & 4 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2232,14 +2232,24 @@ export class ClineProvider
openRouterImageApiKey,
openRouterImageGenerationSelectedModel,
featureRoomoteControlEnabled,
openAiCodexIsAuthenticated: await (async () => {
...(await (async () => {
try {
const { openAiCodexOAuthManager } = await import("../../integrations/openai-codex/oauth")
return await openAiCodexOAuthManager.isAuthenticated()
// Get the current profile ID for profile-scoped OAuth
const profileId = listApiConfigMeta?.find(({ name }) => name === currentApiConfigName)?.id
const isAuthenticated = await openAiCodexOAuthManager.isAuthenticatedForProfile(profileId)
const email = isAuthenticated ? await openAiCodexOAuthManager.getEmailForProfile(profileId) : null
return {
openAiCodexIsAuthenticated: isAuthenticated,
openAiCodexAuthenticatedEmail: email ?? undefined,
}
} catch {
return false
return {
openAiCodexIsAuthenticated: false,
openAiCodexAuthenticatedEmail: undefined,
}
}
})(),
})()),
debug: vscode.workspace.getConfiguration(Package.name).get<boolean>("debug", false),
}
}
Expand Down
12 changes: 8 additions & 4 deletions src/core/webview/__tests__/webviewMessageHandler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ vi.mock("../../../integrations/openai-codex/oauth", () => ({
openAiCodexOAuthManager: {
getAccessToken: vi.fn(),
getAccountId: vi.fn(),
getAccessTokenForProfile: vi.fn(),
getAccountIdForProfile: vi.fn(),
},
}))

Expand All @@ -32,6 +34,8 @@ const { fetchOpenAiCodexRateLimitInfo } = await import("../../../integrations/op
const mockGetModels = getModels as Mock<typeof getModels>
const mockGetAccessToken = vi.mocked(openAiCodexOAuthManager.getAccessToken)
const mockGetAccountId = vi.mocked(openAiCodexOAuthManager.getAccountId)
const mockGetAccessTokenForProfile = vi.mocked(openAiCodexOAuthManager.getAccessTokenForProfile)
const mockGetAccountIdForProfile = vi.mocked(openAiCodexOAuthManager.getAccountIdForProfile)
const mockFetchOpenAiCodexRateLimitInfo = vi.mocked(fetchOpenAiCodexRateLimitInfo)

// Mock ClineProvider
Expand Down Expand Up @@ -599,8 +603,8 @@ describe("webviewMessageHandler - requestRouterModels", () => {
describe("webviewMessageHandler - requestOpenAiCodexRateLimits", () => {
beforeEach(() => {
vi.clearAllMocks()
mockGetAccessToken.mockResolvedValue(null)
mockGetAccountId.mockResolvedValue(null)
mockGetAccessTokenForProfile.mockResolvedValue(null)
mockGetAccountIdForProfile.mockResolvedValue(null)
})

it("posts error when not authenticated", async () => {
Expand All @@ -613,8 +617,8 @@ describe("webviewMessageHandler - requestOpenAiCodexRateLimits", () => {
})

it("posts values when authenticated", async () => {
mockGetAccessToken.mockResolvedValue("token")
mockGetAccountId.mockResolvedValue("acct_123")
mockGetAccessTokenForProfile.mockResolvedValue("token")
mockGetAccountIdForProfile.mockResolvedValue("acct_123")
mockFetchOpenAiCodexRateLimitInfo.mockResolvedValue({
primary: { usedPercent: 10, resetsAt: 1700000000000 },
fetchedAt: 1700000000000,
Expand Down
25 changes: 20 additions & 5 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2385,14 +2385,19 @@ export const webviewMessageHandler = async (
case "openAiCodexSignIn": {
try {
const { openAiCodexOAuthManager } = await import("../../integrations/openai-codex/oauth")
const authUrl = openAiCodexOAuthManager.startAuthorizationFlow()
// Get profile ID from message or current API configuration
const listApiConfigMeta = getGlobalState("listApiConfigMeta")
const currentApiConfigName = getGlobalState("currentApiConfigName")
const profileId =
message.profileId || listApiConfigMeta?.find(({ name }) => name === currentApiConfigName)?.id
const authUrl = openAiCodexOAuthManager.startAuthorizationFlowForProfile(profileId)

// Open the authorization URL in the browser
await vscode.env.openExternal(vscode.Uri.parse(authUrl))

// Wait for the callback in a separate promise (non-blocking)
openAiCodexOAuthManager
.waitForCallback()
.waitForCallbackForProfile()
.then(async () => {
vscode.window.showInformationMessage("Successfully signed in to OpenAI Codex")
await provider.postStateToWebview()
Expand All @@ -2412,7 +2417,12 @@ export const webviewMessageHandler = async (
case "openAiCodexSignOut": {
try {
const { openAiCodexOAuthManager } = await import("../../integrations/openai-codex/oauth")
await openAiCodexOAuthManager.clearCredentials()
// Get profile ID from message or current API configuration
const listApiConfigMeta = getGlobalState("listApiConfigMeta")
const currentApiConfigName = getGlobalState("currentApiConfigName")
const profileId =
message.profileId || listApiConfigMeta?.find(({ name }) => name === currentApiConfigName)?.id
await openAiCodexOAuthManager.clearCredentialsForProfile(profileId)
vscode.window.showInformationMessage("Signed out from OpenAI Codex")
await provider.postStateToWebview()
} catch (error) {
Expand Down Expand Up @@ -3244,7 +3254,12 @@ export const webviewMessageHandler = async (
case "requestOpenAiCodexRateLimits": {
try {
const { openAiCodexOAuthManager } = await import("../../integrations/openai-codex/oauth")
const accessToken = await openAiCodexOAuthManager.getAccessToken()
// Get profile ID from message or current API configuration
const listApiConfigMeta = getGlobalState("listApiConfigMeta")
const currentApiConfigName = getGlobalState("currentApiConfigName")
const profileId =
message.profileId || listApiConfigMeta?.find(({ name }) => name === currentApiConfigName)?.id
const accessToken = await openAiCodexOAuthManager.getAccessTokenForProfile(profileId)

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

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

Expand Down
Loading
Loading