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
openAiCodexAccountEmail?: string | null
debug?: boolean
}

Expand Down
28 changes: 22 additions & 6 deletions src/api/providers/openai-codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { isMcpTool } from "../../utils/mcp-name"
import { sanitizeOpenAiCallId } from "../../utils/tool-id"
import { openAiCodexOAuthManager } from "../../integrations/openai-codex/oauth"
import { t } from "../../i18n"
import { ContextProxy } from "../../core/config/ContextProxy"

export type OpenAiCodexModel = ReturnType<OpenAiCodexHandler["getModel"]>

Expand Down Expand Up @@ -64,6 +65,20 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
*/
private pendingToolCallId: string | undefined
private pendingToolCallName: string | undefined
private resolveProfileId(): string | undefined {
try {
const contextProxy = ContextProxy.instance
const currentApiConfigName = contextProxy.getValue("currentApiConfigName")
const listApiConfigMeta = contextProxy.getValue("listApiConfigMeta")
if (!Array.isArray(listApiConfigMeta)) {
return undefined
}
const match = listApiConfigMeta.find((profile) => profile?.name === currentApiConfigName)
return typeof match?.id === "string" ? match.id : undefined
} catch {
return undefined
}
}

// Event types handled by the shared event processor
private readonly coreHandledEventTypes = new Set<string>([
Expand Down Expand Up @@ -151,7 +166,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.resolveProfileId()
let accessToken = await openAiCodexOAuthManager.getAccessToken(profileId)
if (!accessToken) {
throw new Error(
t("common:errors.openAiCodex.notAuthenticated", {
Expand Down Expand Up @@ -183,7 +199,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 +357,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.resolveProfileId())

// Build Codex-specific headers. Authorization is provided by the SDK apiKey.
const codexHeaders: Record<string, string> = {
Expand Down Expand Up @@ -481,7 +497,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.resolveProfileId())

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

// Get access token
const accessToken = await openAiCodexOAuthManager.getAccessToken()
const accessToken = await openAiCodexOAuthManager.getAccessToken(this.resolveProfileId())
if (!accessToken) {
throw new Error(
t("common:errors.openAiCodex.notAuthenticated", {
Expand Down Expand Up @@ -1043,7 +1059,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.resolveProfileId())

// Build headers with required Codex-specific fields
const headers: Record<string, string> = {
Expand Down
25 changes: 17 additions & 8 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2105,6 +2105,21 @@ export class ClineProvider
const currentMode = mode ?? defaultModeSlug
const hasSystemPromptOverride = await this.hasFileBasedSystemPromptOverride(currentMode)

const openAiCodexProfileId = Array.isArray(listApiConfigMeta)
? listApiConfigMeta.find((profile) => profile.name === currentApiConfigName)?.id
: undefined
let openAiCodexIsAuthenticated = false
let openAiCodexAccountEmail: string | null = null

try {
const { openAiCodexOAuthManager } = await import("../../integrations/openai-codex/oauth")
openAiCodexIsAuthenticated = await openAiCodexOAuthManager.isAuthenticated(openAiCodexProfileId)
openAiCodexAccountEmail = await openAiCodexOAuthManager.getEmail(openAiCodexProfileId)
} catch {
openAiCodexIsAuthenticated = false
openAiCodexAccountEmail = null
}

return {
version: this.context.extension?.packageJSON?.version ?? "",
apiConfiguration,
Expand Down Expand Up @@ -2232,14 +2247,8 @@ export class ClineProvider
openRouterImageApiKey,
openRouterImageGenerationSelectedModel,
featureRoomoteControlEnabled,
openAiCodexIsAuthenticated: await (async () => {
try {
const { openAiCodexOAuthManager } = await import("../../integrations/openai-codex/oauth")
return await openAiCodexOAuthManager.isAuthenticated()
} catch {
return false
}
})(),
openAiCodexIsAuthenticated,
openAiCodexAccountEmail,
debug: vscode.workspace.getConfiguration(Package.name).get<boolean>("debug", false),
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/core/webview/__tests__/webviewMessageHandler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,10 @@ describe("webviewMessageHandler - requestRouterModels", () => {
describe("webviewMessageHandler - requestOpenAiCodexRateLimits", () => {
beforeEach(() => {
vi.clearAllMocks()
mockClineProvider.getState = vi.fn().mockResolvedValue({
currentApiConfigName: "test-profile",
listApiConfigMeta: [{ id: "test-profile-id", name: "test-profile" }],
})
mockGetAccessToken.mockResolvedValue(null)
mockGetAccountId.mockResolvedValue(null)
})
Expand Down
16 changes: 11 additions & 5 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2385,14 +2385,16 @@ export const webviewMessageHandler = async (
case "openAiCodexSignIn": {
try {
const { openAiCodexOAuthManager } = await import("../../integrations/openai-codex/oauth")
const authUrl = openAiCodexOAuthManager.startAuthorizationFlow()
const { currentApiConfigName, listApiConfigMeta } = await provider.getState()
const profileId = listApiConfigMeta?.find((profile) => profile.name === currentApiConfigName)?.id
const authUrl = openAiCodexOAuthManager.startAuthorizationFlow(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()
.waitForCallback(profileId)
.then(async () => {
vscode.window.showInformationMessage("Successfully signed in to OpenAI Codex")
await provider.postStateToWebview()
Expand All @@ -2412,7 +2414,9 @@ export const webviewMessageHandler = async (
case "openAiCodexSignOut": {
try {
const { openAiCodexOAuthManager } = await import("../../integrations/openai-codex/oauth")
await openAiCodexOAuthManager.clearCredentials()
const { currentApiConfigName, listApiConfigMeta } = await provider.getState()
const profileId = listApiConfigMeta?.find((profile) => profile.name === currentApiConfigName)?.id
await openAiCodexOAuthManager.clearCredentials(profileId)
vscode.window.showInformationMessage("Signed out from OpenAI Codex")
await provider.postStateToWebview()
} catch (error) {
Expand Down Expand Up @@ -3244,7 +3248,9 @@ export const webviewMessageHandler = async (
case "requestOpenAiCodexRateLimits": {
try {
const { openAiCodexOAuthManager } = await import("../../integrations/openai-codex/oauth")
const accessToken = await openAiCodexOAuthManager.getAccessToken()
const { currentApiConfigName, listApiConfigMeta } = await provider.getState()
const profileId = listApiConfigMeta?.find((profile) => profile.name === currentApiConfigName)?.id
const accessToken = await openAiCodexOAuthManager.getAccessToken(profileId)

if (!accessToken) {
provider.postMessageToWebview({
Expand All @@ -3254,7 +3260,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
Loading
Loading