feat: openai auth login via cli

This commit is contained in:
cynicalight 2026-04-29 13:14:35 +08:00
parent e99c567dea
commit f091e8e5a5
22 changed files with 1313 additions and 27 deletions

1
.nvimlog Normal file
View File

@ -0,0 +1 @@
WRN 2026-04-29T11:21:24.001 ?.14533 server_start:199: Failed to start server: operation not permitted: /var/folders/1g/3yj40ngn5b33fs9gg0wzvms00000gn/T/nvim.apple/gwHDxl/nvim.14533.0

View File

@ -20,13 +20,19 @@ import {
import { getOauthProfileFromOauthToken } from '../../services/oauth/getOauthProfile.js'
import { OAuthService } from '../../services/oauth/index.js'
import type { OAuthTokens } from '../../services/oauth/types.js'
import { OpenAIOAuthService } from '../../services/openaiAuth/index.js'
import { getOpenAIOAuthTokens } from '../../services/openaiAuth/storage.js'
import type { OpenAIOAuthTokens } from '../../services/openaiAuth/types.js'
import {
clearStoredClaudeAIOAuthTokens,
clearOAuthTokenCache,
getAnthropicApiKeyWithSource,
getAuthTokenSource,
getOpenAIAuthOverrideWarning,
getOauthAccountInfo,
getSubscriptionType,
isUsing3PServices,
removeApiKey,
saveOAuthTokensIfNeeded,
validateForceLoginOrg,
} from '../../utils/auth.js'
@ -109,17 +115,75 @@ export async function installOAuthTokens(tokens: OAuthTokens): Promise<void> {
await clearAuthRelatedCaches()
}
export async function installOpenAIOAuthTokens(
tokens: OpenAIOAuthTokens,
): Promise<string | null> {
await removeApiKey()
const clearClaudeOauth = clearStoredClaudeAIOAuthTokens()
if (!clearClaudeOauth.success) {
throw new Error(
clearClaudeOauth.warning ?? 'Failed to disable existing Claude auth state',
)
}
saveGlobalConfig(current => ({
...current,
oauthAccount: undefined,
}))
if (!tokens.refreshToken) {
throw new Error('OpenAI OAuth tokens are incomplete.')
}
await clearAuthRelatedCaches()
return getOpenAIAuthOverrideWarning()
}
export async function authLogin({
email,
sso,
console: useConsole,
claudeai,
openai,
}: {
email?: string
sso?: boolean
console?: boolean
claudeai?: boolean
openai?: boolean
}): Promise<void> {
if (openai) {
if (email || sso || useConsole || claudeai) {
process.stderr.write(
'Error: --openai cannot be combined with --email, --sso, --console, or --claudeai.\n',
)
process.exit(1)
}
const openaiOAuthService = new OpenAIOAuthService()
try {
const tokens = await openaiOAuthService.startOAuthFlow(async url => {
process.stdout.write('Opening browser to sign in to OpenAI…\n')
process.stdout.write(`If the browser did not open, visit: ${url}\n`)
})
const warning = await installOpenAIOAuthTokens(tokens)
process.stdout.write('OpenAI login successful.\n')
if (warning) {
process.stdout.write(`${warning}\n`)
}
process.exit(0)
} catch (err) {
logError(err)
process.stderr.write(`OpenAI login failed: ${errorMessage(err)}\n`)
process.exit(1)
} finally {
openaiOAuthService.cleanup()
}
}
if (useConsole && claudeai) {
process.stderr.write(
'Error: --console and --claudeai cannot be used together.\n',
@ -232,7 +296,52 @@ export async function authLogin({
export async function authStatus(opts: {
json?: boolean
text?: boolean
openai?: boolean
}): Promise<void> {
if (opts.openai) {
const openaiTokens = getOpenAIOAuthTokens()
const loggedIn =
!!openaiTokens &&
openaiTokens.refreshToken.length > 0 &&
openaiTokens.expiresAt > 0
if (opts.text) {
if (!loggedIn) {
process.stdout.write(
'Not logged in to OpenAI. Run claude auth login --openai to authenticate.\n',
)
} else {
process.stdout.write('Provider: openai\n')
if (openaiTokens.email) {
process.stdout.write(`Email: ${openaiTokens.email}\n`)
}
if (openaiTokens.accountId) {
process.stdout.write(`Account ID: ${openaiTokens.accountId}\n`)
}
process.stdout.write(
`Expires At: ${new Date(openaiTokens.expiresAt).toISOString()}\n`,
)
}
} else {
process.stdout.write(
jsonStringify(
{
loggedIn,
authMethod: loggedIn ? 'openai_oauth' : 'none',
provider: 'openai',
email: openaiTokens?.email ?? null,
accountId: openaiTokens?.accountId ?? null,
expiresAt: openaiTokens?.expiresAt ?? null,
},
null,
2,
) + '\n',
)
}
process.exit(loggedIn ? 0 : 1)
}
const { source: authTokenSource, hasToken } = getAuthTokenSource()
const { source: apiKeySource } = getAnthropicApiKeyWithSource()
const hasApiKeyEnvVar =
@ -240,12 +349,25 @@ export async function authStatus(opts: {
const oauthAccount = getOauthAccountInfo()
const subscriptionType = getSubscriptionType()
const using3P = isUsing3PServices()
const openaiTokens = getOpenAIOAuthTokens()
const usingOpenAI =
!!openaiTokens?.refreshToken &&
!hasToken &&
apiKeySource === 'none' &&
!hasApiKeyEnvVar &&
!using3P
const loggedIn =
hasToken || apiKeySource !== 'none' || hasApiKeyEnvVar || using3P
hasToken ||
apiKeySource !== 'none' ||
hasApiKeyEnvVar ||
using3P ||
usingOpenAI
// Determine auth method
let authMethod: string = 'none'
if (using3P) {
if (usingOpenAI) {
authMethod = 'openai_oauth'
} else if (using3P) {
authMethod = 'third_party'
} else if (authTokenSource === 'claude.ai') {
authMethod = 'claude.ai'
@ -311,6 +433,11 @@ export async function authStatus(opts: {
output.orgId = oauthAccount?.organizationUuid ?? null
output.orgName = oauthAccount?.organizationName ?? null
output.subscriptionType = subscriptionType ?? null
} else if (authMethod === 'openai_oauth') {
output.provider = 'openai'
output.email = openaiTokens?.email ?? null
output.accountId = openaiTokens?.accountId ?? null
output.subscriptionType = 'ChatGPT Pro/Plus'
}
process.stdout.write(jsonStringify(output, null, 2) + '\n')
@ -318,7 +445,18 @@ export async function authStatus(opts: {
process.exit(loggedIn ? 0 : 1)
}
export async function authLogout(): Promise<void> {
export async function authLogout(opts?: { openai?: boolean }): Promise<void> {
if (opts?.openai) {
const openaiOAuthService = new OpenAIOAuthService()
const success = openaiOAuthService.logout()
if (!success) {
process.stderr.write('Failed to log out from OpenAI.\n')
process.exit(1)
}
process.stdout.write('Successfully logged out from your OpenAI account.\n')
process.exit(0)
}
try {
await performLogout({ clearOnboarding: false })
} catch {

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,124 @@
import React, { useCallback, useEffect, useState } from 'react'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from 'src/services/analytics/index.js'
import { installOpenAIOAuthTokens } from '../cli/handlers/auth.js'
import { Box, Link, Text } from '../ink.js'
import { useKeybinding } from '../keybindings/useKeybinding.js'
import { OpenAIOAuthService } from '../services/openaiAuth/index.js'
import { getOpenAIOAuthTokens } from '../services/openaiAuth/storage.js'
import { logError } from '../utils/log.js'
import { Spinner } from './Spinner.js'
type Props = {
onDone(): void
startingMessage?: string
}
type OpenAILoginStatus =
| { state: 'ready_to_start' }
| { state: 'waiting_for_login'; url: string }
| { state: 'success'; warning?: string | null }
| { state: 'error'; message: string }
export function OpenAILoginFlow({
onDone,
startingMessage,
}: Props): React.ReactNode {
const [oauthService] = useState(() => new OpenAIOAuthService())
const [status, setStatus] = useState<OpenAILoginStatus>({
state: 'ready_to_start',
})
useKeybinding(
'confirm:yes',
() => {
logEvent('tengu_openai_oauth_success', {})
onDone()
},
{
context: 'Confirmation',
isActive: status.state === 'success',
},
)
const startOAuth = useCallback(async () => {
try {
logEvent('tengu_openai_oauth_flow_start', {})
const tokens = await oauthService.startOAuthFlow(async url => {
setStatus({ state: 'waiting_for_login', url })
})
const warning = await installOpenAIOAuthTokens(tokens)
setStatus({ state: 'success', warning })
} catch (error) {
logError(error)
setStatus({
state: 'error',
message: (error as Error).message,
})
logEvent('tengu_openai_oauth_error', {
error: (error as Error)
.message as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
}
}, [oauthService])
useEffect(() => {
if (status.state !== 'ready_to_start') {
return
}
void startOAuth()
}, [startOAuth, status.state])
useEffect(() => {
return () => {
oauthService.cleanup()
}
}, [oauthService])
const account = getOpenAIOAuthTokens()
return (
<Box flexDirection="column" gap={1}>
<Text bold>
{startingMessage ??
'Claude Code can also run through your ChatGPT subscription via OpenAI auth.'}
</Text>
{status.state === 'waiting_for_login' && (
<Box flexDirection="column" gap={1}>
<Box>
<Spinner />
<Text>Opening browser to sign in to OpenAI</Text>
</Box>
<Text dimColor>Use the URL below if your browser did not open:</Text>
<Link url={status.url}>
<Text dimColor>{status.url}</Text>
</Link>
</Box>
)}
{status.state === 'success' && (
<Box flexDirection="column">
{account?.email ? (
<Text dimColor>
Logged in as <Text>{account.email}</Text>
</Text>
) : null}
{status.warning ? (
<Text color="warning">{status.warning}</Text>
) : null}
<Text color="success">
Login successful. Press <Text bold>Enter</Text> to continue
</Text>
</Box>
)}
{status.state === 'error' && (
<Text color="error">OpenAI OAuth error: {status.message}</Text>
)}
</Box>
)
}

View File

@ -0,0 +1,157 @@
import React, { useMemo, useState } from 'react'
import { Box, Link, Text } from '../ink.js'
import { useKeybinding } from '../keybindings/useKeybinding.js'
import { getSettings_DEPRECATED } from '../utils/settings/settings.js'
import { ConsoleOAuthFlow } from './ConsoleOAuthFlow.js'
import { Select } from './CustomSelect/select.js'
import { OpenAILoginFlow } from './OpenAILoginFlow.js'
type Props = {
onDone(): void
startingMessage?: string
}
type LoginSelection = 'claudeai' | 'console' | 'openai' | 'platform' | 'idle'
function PlatformSetupFlow({
onBack,
}: {
onBack(): void
}): React.ReactNode {
useKeybinding(
'confirm:yes',
() => {
onBack()
},
{
context: 'Confirmation',
isActive: true,
},
)
return (
<Box flexDirection="column" gap={1} marginTop={1}>
<Text bold>Using 3rd-party platforms</Text>
<Text>
Claude Code supports Amazon Bedrock, Microsoft Foundry, and Vertex AI.
Set the required environment variables, then restart Claude Code.
</Text>
<Text>
If you are part of an enterprise organization, contact your
administrator for setup instructions.
</Text>
<Box flexDirection="column">
<Text bold>Documentation:</Text>
<Text>
· Amazon Bedrock:{' '}
<Link url="https://code.claude.com/docs/en/amazon-bedrock" />
</Text>
<Text>
· Microsoft Foundry:{' '}
<Link url="https://code.claude.com/docs/en/microsoft-foundry" />
</Text>
<Text>
· Vertex AI:{' '}
<Link url="https://code.claude.com/docs/en/google-vertex-ai" />
</Text>
</Box>
<Text dimColor>
Press <Text bold>Enter</Text> to go back to login options.
</Text>
</Box>
)
}
export function SlashLoginFlow({
onDone,
startingMessage,
}: Props): React.ReactNode {
const settings = getSettings_DEPRECATED() || {}
const forceLoginMethod = settings.forceLoginMethod
const [selection, setSelection] = useState<LoginSelection>(() => {
if (forceLoginMethod === 'claudeai' || forceLoginMethod === 'console') {
return forceLoginMethod
}
return 'idle'
})
const options = useMemo(
() => [
{
label: (
<Text>
Claude account with subscription ·{' '}
<Text dimColor>Pro, Max, Team, or Enterprise</Text>
{'\n'}
</Text>
),
value: 'claudeai' as const,
},
{
label: (
<Text>
Anthropic Console account ·{' '}
<Text dimColor>API usage billing</Text>
{'\n'}
</Text>
),
value: 'console' as const,
},
{
label: (
<Text>
OpenAI account · <Text dimColor>ChatGPT Pro/Plus</Text>
{'\n'}
</Text>
),
value: 'openai' as const,
},
{
label: (
<Text>
3rd-party platform ·{' '}
<Text dimColor>Amazon Bedrock, Microsoft Foundry, or Vertex AI</Text>
{'\n'}
</Text>
),
value: 'platform' as const,
},
],
[],
)
if (selection === 'claudeai' || selection === 'console') {
return (
<ConsoleOAuthFlow
onDone={onDone}
startingMessage={startingMessage}
forceLoginMethod={selection}
/>
)
}
if (selection === 'openai') {
return <OpenAILoginFlow onDone={onDone} startingMessage={startingMessage} />
}
if (selection === 'platform') {
return <PlatformSetupFlow onBack={() => setSelection('idle')} />
}
return (
<Box flexDirection="column" gap={1} marginTop={1}>
<Text bold>
{startingMessage ??
'Claude Code can be used with your Claude subscription, Anthropic Console billing, or your ChatGPT subscription.'}
</Text>
<Text>Select login method:</Text>
<Box>
<Select
options={options}
onChange={value => setSelection(value as LoginSelection)}
/>
</Box>
</Box>
)
}

View File

@ -4141,16 +4141,18 @@ async function run(): Promise<CommanderCommand> {
// claude auth
const auth = program.command('auth').description('Manage authentication').configureHelp(createSortedHelpConfig());
auth.command('login').description('Sign in to your Anthropic account').option('--email <email>', 'Pre-populate email address on the login page').option('--sso', 'Force SSO login flow').option('--console', 'Use Anthropic Console (API usage billing) instead of Claude subscription').option('--claudeai', 'Use Claude subscription (default)').action(async ({
auth.command('login').description('Sign in to your Anthropic or OpenAI account').option('--email <email>', 'Pre-populate email address on the login page').option('--sso', 'Force SSO login flow').option('--console', 'Use Anthropic Console (API usage billing) instead of Claude subscription').option('--claudeai', 'Use Claude subscription (default)').option('--openai', 'Use OpenAI ChatGPT/Codex OAuth login').action(async ({
email,
sso,
console: useConsole,
claudeai
claudeai,
openai,
}: {
email?: string;
sso?: boolean;
console?: boolean;
claudeai?: boolean;
openai?: boolean;
}) => {
const {
authLogin
@ -4159,23 +4161,27 @@ async function run(): Promise<CommanderCommand> {
email,
sso,
console: useConsole,
claudeai
claudeai,
openai,
});
});
auth.command('status').description('Show authentication status').option('--json', 'Output as JSON (default)').option('--text', 'Output as human-readable text').action(async (opts: {
auth.command('status').description('Show authentication status').option('--json', 'Output as JSON (default)').option('--text', 'Output as human-readable text').option('--openai', 'Show OpenAI OAuth status').action(async (opts: {
json?: boolean;
text?: boolean;
openai?: boolean;
}) => {
const {
authStatus
} = await import('./cli/handlers/auth.js');
await authStatus(opts);
});
auth.command('logout').description('Log out from your Anthropic account').action(async () => {
auth.command('logout').description('Log out from your Anthropic or OpenAI account').option('--openai', 'Log out from OpenAI OAuth only').action(async (opts: {
openai?: boolean;
}) => {
const {
authLogout
} = await import('./cli/handlers/auth.js');
await authLogout();
await authLogout(opts);
});
/**

View File

@ -303,10 +303,33 @@ describe('anthropicToOpenaiResponses', () => {
const result = anthropicToOpenaiResponses(req)
expect(result.model).toBe('gpt-4o')
expect(result.instructions).toBe('Be helpful')
expect(result.store).toBe(false)
expect(result.tools).toBeUndefined()
expect(result.max_output_tokens).toBeUndefined()
expect(result.input).toEqual([{ type: 'message', role: 'user', content: 'Hello' }])
})
test('tools conversion uses top-level name', () => {
const req: AnthropicRequest = {
model: 'gpt-4o',
max_tokens: 100,
messages: [{ role: 'user', content: 'Hi' }],
tools: [{
name: 'get_weather',
description: 'Get weather',
input_schema: { type: 'object', properties: { city: { type: 'string' } } },
}],
}
const result = anthropicToOpenaiResponses(req)
expect(result.tools).toHaveLength(1)
expect(result.tools![0]).toEqual({
type: 'function',
name: 'get_weather',
description: 'Get weather',
parameters: { type: 'object', properties: { city: { type: 'string' } } },
})
})
test('tool_use lifted to function_call', () => {
const req: AnthropicRequest = {
model: 'gpt-4o',

View File

@ -10,7 +10,6 @@ import type {
AnthropicMessage,
OpenAIResponsesRequest,
OpenAIResponsesInputItem,
OpenAITool,
OpenAIChatContentPart,
} from './types.js'
@ -29,6 +28,7 @@ export function anthropicToOpenaiResponses(body: AnthropicRequest): OpenAIRespon
model: body.model,
input,
stream: body.stream,
store: false,
}
// system → instructions
@ -51,13 +51,11 @@ export function anthropicToOpenaiResponses(body: AnthropicRequest): OpenAIRespon
if (body.tools && body.tools.length > 0) {
result.tools = body.tools
.filter((t) => t.name !== 'BatchTool')
.map((t): OpenAITool => ({
.map((t) => ({
type: 'function',
function: {
name: t.name,
description: t.description,
parameters: t.input_schema,
},
name: t.name,
description: t.description,
parameters: t.input_schema,
}))
}

View File

@ -110,11 +110,17 @@ export type OpenAIResponsesRequest = {
model: string
input: OpenAIResponsesInputItem[]
instructions?: string
store?: boolean
max_output_tokens?: number
temperature?: number
top_p?: number
stream?: boolean
tools?: OpenAITool[]
tools?: Array<{
type: 'function'
name: string
description?: string
parameters?: Record<string, unknown>
}>
tool_choice?: unknown
reasoning?: { effort?: 'low' | 'medium' | 'high' }
}

View File

@ -22,6 +22,11 @@ import {
getSessionId,
} from '../../bootstrap/state.js'
import { getOauthConfig } from '../../constants/oauth.js'
import {
buildOpenAICodexFetch,
OPENAI_OAUTH_DUMMY_KEY,
shouldUseOpenAICodexAuth,
} from '../openaiAuth/fetch.js'
import { isDebugToStdErr, logForDebugging } from '../../utils/debug.js'
import {
getAWSRegion,
@ -132,11 +137,19 @@ export async function getAnthropicClient({
await checkAndRefreshOAuthTokenIfNeeded()
logForDebugging('[API:auth] OAuth token check complete')
if (!isClaudeAISubscriber()) {
const usingOpenAICodex =
shouldUseOpenAICodexAuth() &&
!isClaudeAISubscriber() &&
!process.env.ANTHROPIC_AUTH_TOKEN &&
!(apiKey || getAnthropicApiKey())
if (!isClaudeAISubscriber() && !usingOpenAICodex) {
await configureApiKeyHeaders(defaultHeaders, getIsNonInteractiveSession())
}
const resolvedFetch = buildFetch(fetchOverride, source)
const resolvedFetch = usingOpenAICodex
? buildOpenAICodexFetch(fetchOverride, source)
: buildFetch(fetchOverride, source)
const ARGS = {
defaultHeaders,
@ -299,7 +312,11 @@ export async function getAnthropicClient({
// Determine authentication method based on available tokens
const clientConfig: ConstructorParameters<typeof Anthropic>[0] = {
apiKey: isClaudeAISubscriber() ? null : apiKey || getAnthropicApiKey(),
apiKey: isClaudeAISubscriber()
? null
: usingOpenAICodex
? OPENAI_OAUTH_DUMMY_KEY
: apiKey || getAnthropicApiKey(),
authToken: isClaudeAISubscriber()
? getClaudeAIOAuthTokens()?.accessToken
: undefined,

View File

@ -0,0 +1,141 @@
import { generateCodeChallenge } from '../oauth/crypto.js'
import type {
OpenAIJwtClaims,
OpenAIOAuthTokenResponse,
OpenAIOAuthTokens,
} from './types.js'
export const OPENAI_AUTH_ISSUER = 'https://auth.openai.com'
export const OPENAI_CODEX_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann'
export const OPENAI_CODEX_API_ENDPOINT =
'https://chatgpt.com/backend-api/codex/responses'
export const OPENAI_CODEX_OAUTH_PORT = 1455
export const OPENAI_CODEX_REDIRECT_PATH = '/auth/callback'
const DEFAULT_TOKEN_LIFETIME_MS = 3600 * 1000
export function buildOpenAIAuthorizeUrl(input: {
redirectUri: string
codeVerifier: string
state: string
}): string {
const params = new URLSearchParams({
response_type: 'code',
client_id: OPENAI_CODEX_CLIENT_ID,
redirect_uri: input.redirectUri,
scope: 'openid profile email offline_access',
code_challenge: generateCodeChallenge(input.codeVerifier),
code_challenge_method: 'S256',
id_token_add_organizations: 'true',
codex_cli_simplified_flow: 'true',
state: input.state,
originator: 'opencode',
})
return `${OPENAI_AUTH_ISSUER}/oauth/authorize?${params.toString()}`
}
export async function exchangeOpenAICodeForTokens(input: {
code: string
redirectUri: string
codeVerifier: string
}): Promise<OpenAIOAuthTokenResponse> {
const response = await fetch(`${OPENAI_AUTH_ISSUER}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: input.code,
redirect_uri: input.redirectUri,
client_id: OPENAI_CODEX_CLIENT_ID,
code_verifier: input.codeVerifier,
}).toString(),
})
if (!response.ok) {
throw new Error(`OpenAI token exchange failed: ${response.status}`)
}
return (await response.json()) as OpenAIOAuthTokenResponse
}
export async function refreshOpenAITokens(
refreshToken: string,
): Promise<OpenAIOAuthTokenResponse> {
const response = await fetch(`${OPENAI_AUTH_ISSUER}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: OPENAI_CODEX_CLIENT_ID,
}).toString(),
})
if (!response.ok) {
throw new Error(`OpenAI token refresh failed: ${response.status}`)
}
return (await response.json()) as OpenAIOAuthTokenResponse
}
export function parseOpenAIJwtClaims(
token?: string,
): OpenAIJwtClaims | undefined {
if (!token) return undefined
const parts = token.split('.')
if (parts.length !== 3) return undefined
try {
return JSON.parse(Buffer.from(parts[1]!, 'base64url').toString())
} catch {
return undefined
}
}
export function extractOpenAIAccountId(
claims?: OpenAIJwtClaims,
): string | undefined {
if (!claims) return undefined
return (
claims.chatgpt_account_id ||
claims['https://api.openai.com/auth']?.chatgpt_account_id ||
claims.organizations?.[0]?.id
)
}
export function normalizeOpenAITokens(
response: OpenAIOAuthTokenResponse,
): OpenAIOAuthTokens {
const claims =
parseOpenAIJwtClaims(response.id_token) ??
parseOpenAIJwtClaims(response.access_token)
return {
accessToken: response.access_token,
refreshToken: response.refresh_token,
expiresAt: Date.now() + (response.expires_in ?? 3600) * 1000,
idToken: response.id_token,
accountId: extractOpenAIAccountId(claims),
email: claims?.email,
}
}
export function isOpenAITokenExpired(expiresAt: number): boolean {
return expiresAt - Date.now() <= 5 * 60 * 1000
}
export function withRefreshedAccessToken(
existing: OpenAIOAuthTokens,
refreshed: OpenAIOAuthTokenResponse,
): OpenAIOAuthTokens {
const next = normalizeOpenAITokens(refreshed)
return {
...next,
accountId: next.accountId ?? existing.accountId,
email: next.email ?? existing.email,
idToken: next.idToken ?? existing.idToken,
}
}

View File

@ -0,0 +1,132 @@
import type { ClientOptions } from '@anthropic-ai/sdk'
import { randomUUID } from 'crypto'
import { OPENAI_CODEX_API_ENDPOINT } from './client.js'
import { ensureFreshOpenAITokens } from './index.js'
import { resolveOpenAICodexModel } from './models.js'
import { getOpenAIOAuthTokens } from './storage.js'
import { anthropicToOpenaiResponses } from '../../server/proxy/transform/anthropicToOpenaiResponses.js'
import { openaiResponsesToAnthropic } from '../../server/proxy/transform/openaiResponsesToAnthropic.js'
import { openaiResponsesStreamToAnthropic } from '../../server/proxy/streaming/openaiResponsesStreamToAnthropic.js'
import type { AnthropicRequest } from '../../server/proxy/transform/types.js'
import { logForDebugging } from '../../utils/debug.js'
export const OPENAI_OAUTH_DUMMY_KEY = 'openai-oauth-dummy-key'
export function shouldUseOpenAICodexAuth(): boolean {
const openaiTokens = getOpenAIOAuthTokens()
return !!openaiTokens?.refreshToken
}
export function buildOpenAICodexFetch(
fetchOverride: ClientOptions['fetch'],
source: string | undefined,
): ClientOptions['fetch'] {
const inner = fetchOverride ?? globalThis.fetch
return async (input, init) => {
const url = input instanceof Request ? new URL(input.url) : new URL(String(input))
if (!url.pathname.endsWith('/v1/messages')) {
return inner(input, init)
}
const originalBody = await readAnthropicBody(input, init)
const mappedModel = resolveOpenAICodexModel(originalBody.model)
const transformedBody = anthropicToOpenaiResponses({
...originalBody,
model: mappedModel,
})
const tokens = await ensureFreshOpenAITokens()
if (!tokens) {
throw new Error(
'OpenAI OAuth token is missing or expired. Run claude auth login --openai again.',
)
}
const headers = new Headers()
headers.set('Content-Type', 'application/json')
headers.set('Authorization', `Bearer ${tokens.accessToken}`)
if (tokens.accountId) {
headers.set('ChatGPT-Account-Id', tokens.accountId)
}
logForDebugging(
`[API REQUEST] ${url.pathname} remapped_to=OpenAI/Codex model=${mappedModel} source=${source ?? 'unknown'} request_id=${randomUUID()}`,
)
const upstream = await inner(OPENAI_CODEX_API_ENDPOINT, {
method: 'POST',
headers,
body: JSON.stringify(transformedBody),
signal: init?.signal,
})
if (!upstream.ok) {
const errorText = await upstream.text().catch(() => '')
return Response.json(
{
type: 'error',
error: {
type: 'api_error',
message: `OpenAI upstream returned HTTP ${upstream.status}: ${errorText.slice(0, 500)}`,
},
},
{ status: upstream.status },
)
}
if (transformedBody.stream) {
if (!upstream.body) {
return Response.json(
{
type: 'error',
error: {
type: 'api_error',
message: 'OpenAI upstream returned no body for stream',
},
},
{ status: 502 },
)
}
return new Response(
openaiResponsesStreamToAnthropic(upstream.body, mappedModel),
{
status: 200,
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
},
)
}
const responseBody = await upstream.json()
return Response.json(
openaiResponsesToAnthropic(responseBody, mappedModel),
)
}
}
async function readAnthropicBody(
input: RequestInfo | URL,
init?: RequestInit,
): Promise<AnthropicRequest> {
const directBody = init?.body
if (typeof directBody === 'string') {
return JSON.parse(directBody) as AnthropicRequest
}
if (directBody instanceof Uint8Array || directBody instanceof ArrayBuffer) {
return JSON.parse(Buffer.from(directBody).toString('utf8')) as AnthropicRequest
}
if (input instanceof Request) {
return (await input.clone().json()) as AnthropicRequest
}
throw new Error('Unable to read Anthropic request body for OpenAI/Codex transformation')
}

View File

@ -0,0 +1,167 @@
import { openBrowser } from '../../utils/browser.js'
import { AuthCodeListener } from '../oauth/auth-code-listener.js'
import { generateCodeVerifier, generateState } from '../oauth/crypto.js'
import {
buildOpenAIAuthorizeUrl,
exchangeOpenAICodeForTokens,
isOpenAITokenExpired,
OPENAI_CODEX_OAUTH_PORT,
OPENAI_CODEX_REDIRECT_PATH,
refreshOpenAITokens,
normalizeOpenAITokens,
withRefreshedAccessToken,
} from './client.js'
import {
clearOpenAIOAuthTokenCache,
deleteOpenAIOAuthTokens,
getOpenAIOAuthTokensAsync,
saveOpenAIOAuthTokens,
} from './storage.js'
import type { OpenAIOAuthTokens } from './types.js'
const HTML_SUCCESS = `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>cc-haha OpenAI Authorization Successful</title>
<style>
body { font-family: system-ui, -apple-system, sans-serif; display:flex; justify-content:center; align-items:center; height:100vh; margin:0; background:#131010; color:#f1ecec; }
.container { text-align:center; padding:2rem; }
p { color:#b7b1b1; }
</style>
</head>
<body>
<div class="container">
<h1>Authorization Successful</h1>
<p>You can close this window and return to Claude Code Haha.</p>
</div>
<script>setTimeout(() => window.close(), 2000)</script>
</body>
</html>`
const HTML_ERROR = `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>cc-haha OpenAI Authorization Failed</title>
</head>
<body>
<h1>Authorization Failed</h1>
<p>You can close this window and return to Claude Code Haha.</p>
</body>
</html>`
export class OpenAIOAuthService {
private codeVerifier: string
private authCodeListener: AuthCodeListener | null = null
private port: number | null = null
constructor() {
this.codeVerifier = generateCodeVerifier()
}
async startOAuthFlow(
authURLHandler: (url: string) => Promise<void>,
): Promise<OpenAIOAuthTokens> {
this.authCodeListener = new AuthCodeListener(OPENAI_CODEX_REDIRECT_PATH)
this.port = await this.authCodeListener.start(OPENAI_CODEX_OAUTH_PORT)
const state = generateState()
const redirectUri = `http://localhost:${this.port}${OPENAI_CODEX_REDIRECT_PATH}`
const authorizeUrl = buildOpenAIAuthorizeUrl({
redirectUri,
codeVerifier: this.codeVerifier,
state,
})
const authorizationCode = await new Promise<string>((resolve, reject) => {
this.authCodeListener
?.waitForAuthorization(state, async () => {
await authURLHandler(authorizeUrl)
await openBrowser(authorizeUrl)
})
.then(resolve)
.catch(reject)
})
try {
const response = await exchangeOpenAICodeForTokens({
code: authorizationCode,
redirectUri,
codeVerifier: this.codeVerifier,
})
const tokens = normalizeOpenAITokens(response)
const storage = saveOpenAIOAuthTokens(tokens)
if (!storage.success) {
throw new Error(storage.warning ?? 'Failed to persist OpenAI OAuth tokens')
}
this.authCodeListener?.handleSuccessRedirect([], (res) => {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
res.end(HTML_SUCCESS)
})
return tokens
} catch (error) {
this.authCodeListener?.handleSuccessRedirect([], (res) => {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
res.end(HTML_ERROR)
})
throw error
} finally {
this.cleanup()
}
}
async ensureFreshTokens(): Promise<OpenAIOAuthTokens | null> {
clearOpenAIOAuthTokenCache()
const tokens = await getOpenAIOAuthTokensAsync()
if (!tokens) return null
if (!isOpenAITokenExpired(tokens.expiresAt)) return tokens
const refreshed = await refreshOpenAITokens(tokens.refreshToken)
const updated = withRefreshedAccessToken(tokens, refreshed)
const storage = saveOpenAIOAuthTokens(updated)
if (!storage.success) {
throw new Error(storage.warning ?? 'Failed to persist refreshed OpenAI tokens')
}
return updated
}
async ensureFreshAccessToken(): Promise<string | null> {
const tokens = await this.ensureFreshTokens()
return tokens?.accessToken ?? null
}
logout(): boolean {
clearOpenAIOAuthTokenCache()
return deleteOpenAIOAuthTokens()
}
cleanup(): void {
this.authCodeListener?.close()
this.authCodeListener = null
this.port = null
}
}
export async function ensureFreshOpenAITokens(): Promise<OpenAIOAuthTokens | null> {
clearOpenAIOAuthTokenCache()
const tokens = await getOpenAIOAuthTokensAsync()
if (!tokens) return null
if (!isOpenAITokenExpired(tokens.expiresAt)) return tokens
try {
const refreshed = await refreshOpenAITokens(tokens.refreshToken)
const updated = withRefreshedAccessToken(tokens, refreshed)
const storage = saveOpenAIOAuthTokens(updated)
if (!storage.success) {
throw new Error(storage.warning ?? 'Failed to persist refreshed OpenAI tokens')
}
return updated
} catch {
return null
}
}

View File

@ -0,0 +1,21 @@
import { describe, expect, test } from 'bun:test'
import {
OPENAI_DEFAULT_MAIN_MODEL,
isOpenAIResponsesModel,
resolveOpenAICodexModel,
} from './models.js'
describe('openai auth model resolution', () => {
test('does not treat opus as an OpenAI Responses model', () => {
expect(isOpenAIResponsesModel('opus')).toBe(false)
})
test('accepts gpt and o-series models', () => {
expect(isOpenAIResponsesModel('gpt-5.4')).toBe(true)
expect(isOpenAIResponsesModel('o3-mini')).toBe(true)
})
test('maps opus aliases to the OpenAI default model', () => {
expect(resolveOpenAICodexModel('opus')).toBe(OPENAI_DEFAULT_MAIN_MODEL)
})
})

View File

@ -0,0 +1,92 @@
export const OPENAI_DEFAULT_MAIN_MODEL = 'gpt-5.3-codex'
export const OPENAI_DEFAULT_SONNET_MODEL = 'gpt-5.4'
export const OPENAI_DEFAULT_HAIKU_MODEL = 'gpt-5.4-mini'
export type OpenAIModelCatalogEntry = {
value: string
label: string
description: string
descriptionForModel?: string
}
export const OPENAI_CODEX_MODEL_CATALOG: OpenAIModelCatalogEntry[] = [
{
value: OPENAI_DEFAULT_MAIN_MODEL,
label: 'GPT-5.3 Codex',
description: 'Best for coding and agentic work',
descriptionForModel: 'GPT-5.3 Codex - best for coding and agentic work',
},
{
value: OPENAI_DEFAULT_SONNET_MODEL,
label: 'GPT-5.4',
description: 'Strong general-purpose model',
descriptionForModel: 'GPT-5.4 - strong general-purpose model',
},
{
value: OPENAI_DEFAULT_HAIKU_MODEL,
label: 'GPT-5.4 Mini',
description: 'Fastest for quick tasks',
descriptionForModel: 'GPT-5.4 Mini - fastest for quick tasks',
},
]
export function isOpenAIResponsesModel(model: string): boolean {
const normalized = model.trim().toLowerCase()
return normalized.startsWith('gpt-') || /^o\d/.test(normalized)
}
export function resolveOpenAICodexModel(model: string): string {
if (process.env.OPENAI_CODEX_MODEL?.trim()) {
return process.env.OPENAI_CODEX_MODEL.trim()
}
const normalized = model.trim().toLowerCase()
if (isOpenAIResponsesModel(normalized)) {
return model
}
if (normalized.includes('haiku')) {
return (
process.env.OPENAI_CODEX_HAIKU_MODEL?.trim() ||
OPENAI_DEFAULT_HAIKU_MODEL
)
}
if (normalized.includes('sonnet')) {
return (
process.env.OPENAI_CODEX_SONNET_MODEL?.trim() ||
OPENAI_DEFAULT_SONNET_MODEL
)
}
if (normalized.includes('opus')) {
return (
process.env.OPENAI_CODEX_OPUS_MODEL?.trim() || OPENAI_DEFAULT_MAIN_MODEL
)
}
return OPENAI_DEFAULT_MAIN_MODEL
}
export function getOpenAIModelDisplayName(model: string): string | null {
switch (model.trim().toLowerCase()) {
case 'gpt-5.3-codex':
return 'GPT-5.3 Codex'
case 'gpt-5.4':
return 'GPT-5.4'
case 'gpt-5.4-mini':
return 'GPT-5.4 Mini'
case 'gpt-5.2':
return 'GPT-5.2'
case 'gpt-5.2-codex':
return 'GPT-5.2 Codex'
case 'gpt-5.1-codex':
return 'GPT-5.1 Codex'
case 'gpt-5.1-codex-max':
return 'GPT-5.1 Codex Max'
case 'gpt-5.1-codex-mini':
return 'GPT-5.1 Codex Mini'
default:
return null
}
}

View File

@ -0,0 +1,71 @@
import memoize from 'lodash-es/memoize.js'
import { getSecureStorage } from '../../utils/secureStorage/index.js'
import { errorMessage } from '../../utils/errors.js'
import { logError } from '../../utils/log.js'
import type { OpenAIOAuthTokens } from './types.js'
const STORAGE_KEY = 'openaiCodexOauth'
type SecureStorageShape = Record<string, unknown> & {
openaiCodexOauth?: OpenAIOAuthTokens
}
export function saveOpenAIOAuthTokens(tokens: OpenAIOAuthTokens): {
success: boolean
warning?: string
} {
try {
const storage = getSecureStorage()
const data = (storage.read() ?? {}) as SecureStorageShape
data[STORAGE_KEY] = tokens
const result = storage.update(data)
clearOpenAIOAuthTokenCache()
return result
} catch (error) {
logError(error)
return {
success: false,
warning: `Failed to save OpenAI OAuth tokens: ${errorMessage(error)}`,
}
}
}
export const getOpenAIOAuthTokens = memoize((): OpenAIOAuthTokens | null => {
try {
const storage = getSecureStorage()
const data = storage.read() as SecureStorageShape | null
return data?.openaiCodexOauth ?? null
} catch (error) {
logError(error)
return null
}
})
export async function getOpenAIOAuthTokensAsync(): Promise<OpenAIOAuthTokens | null> {
try {
const storage = getSecureStorage()
const data = (await storage.readAsync()) as SecureStorageShape | null
return data?.openaiCodexOauth ?? null
} catch (error) {
logError(error)
return null
}
}
export function clearOpenAIOAuthTokenCache(): void {
getOpenAIOAuthTokens.cache?.clear?.()
}
export function deleteOpenAIOAuthTokens(): boolean {
try {
const storage = getSecureStorage()
const data = (storage.read() ?? {}) as SecureStorageShape
delete data[STORAGE_KEY]
const result = storage.update(data)
clearOpenAIOAuthTokenCache()
return result.success
} catch (error) {
logError(error)
return false
}
}

View File

@ -0,0 +1,24 @@
export type OpenAIOAuthTokenResponse = {
access_token: string
refresh_token: string
id_token?: string
expires_in?: number
}
export type OpenAIOAuthTokens = {
accessToken: string
refreshToken: string
expiresAt: number
idToken?: string
accountId?: string
email?: string
}
export type OpenAIJwtClaims = {
chatgpt_account_id?: string
organizations?: Array<{ id: string }>
email?: string
'https://api.openai.com/auth'?: {
chatgpt_account_id?: string
}
}

View File

@ -19,6 +19,7 @@ import {
getMockSubscriptionType,
shouldUseMockSubscription,
} from '../services/mockRateLimits.js'
import { getOpenAIOAuthTokens } from '../services/openaiAuth/storage.js'
import {
isOAuthTokenExpired,
refreshOAuthToken,
@ -1183,6 +1184,27 @@ export async function removeApiKey(): Promise<void> {
clearLegacyApiKeyPrefetch()
}
export function clearStoredClaudeAIOAuthTokens(): {
success: boolean
warning?: string
} {
try {
const secureStorage = getSecureStorage()
const storageData = secureStorage.read() || {}
if ('claudeAiOauth' in storageData) {
delete storageData.claudeAiOauth
}
const result = secureStorage.update(storageData)
clearOAuthTokenCache()
return result
} catch (error) {
logError(error)
return { success: false, warning: 'Failed to clear stored Claude OAuth tokens' }
}
}
async function maybeRemoveApiKeyFromMacOSKeychain(): Promise<void> {
try {
await maybeRemoveApiKeyFromMacOSKeychainThrows()
@ -1856,11 +1878,67 @@ export function isConsumerSubscriber(): boolean {
}
export type UserAccountInfo = {
provider?: 'anthropic' | 'openai'
subscription?: string
tokenSource?: string
apiKeySource?: ApiKeySource
organization?: string
email?: string
accountId?: string
}
export function getOpenAISubscriptionName(): string {
return 'ChatGPT Pro/Plus'
}
export function getOpenAIAuthOverrideWarning(): string | null {
if (isUsing3PServices()) {
return 'OpenAI login is saved, but this shell is configured for a third-party provider.'
}
const { source: authTokenSource } = getAuthTokenSource()
if (authTokenSource !== 'none') {
return `OpenAI login is saved, but this shell is still using ${authTokenSource} for Anthropic-compatible auth.`
}
const { source: apiKeySource } = getAnthropicApiKeyWithSource({
skipRetrievingKeyFromApiKeyHelper: true,
})
if (apiKeySource !== 'none') {
return `OpenAI login is saved, but this shell is still using ${apiKeySource} for Anthropic-compatible auth.`
}
return null
}
export function isOpenAIAuthActive(): boolean {
return getOpenAIAccountInformationIfActive() !== undefined
}
function getOpenAIAccountInformationIfActive(input?: {
authTokenSource?: ReturnType<typeof getAuthTokenSource>['source']
apiKeySource?: ApiKeySource
}): UserAccountInfo | undefined {
const openaiTokens = getOpenAIOAuthTokens()
if (!openaiTokens?.refreshToken || isUsing3PServices()) {
return undefined
}
const authTokenSource = input?.authTokenSource ?? getAuthTokenSource().source
const apiKeySource =
input?.apiKeySource ?? getAnthropicApiKeyWithSource().source
if (authTokenSource !== 'none' || apiKeySource !== 'none') {
return undefined
}
return {
provider: 'openai',
subscription: getOpenAISubscriptionName(),
tokenSource: 'OpenAI OAuth',
email: openaiTokens.email,
accountId: openaiTokens.accountId,
}
}
export function getAccountInformation() {
@ -1870,7 +1948,17 @@ export function getAccountInformation() {
return undefined
}
const { source: authTokenSource } = getAuthTokenSource()
const { key: apiKey, source: apiKeySource } = getAnthropicApiKeyWithSource()
const openAIAccountInfo = getOpenAIAccountInformationIfActive({
authTokenSource,
apiKeySource,
})
if (openAIAccountInfo) {
return openAIAccountInfo
}
const accountInfo: UserAccountInfo = {}
accountInfo.provider = 'anthropic'
if (
authTokenSource === 'CLAUDE_CODE_OAUTH_TOKEN' ||
authTokenSource === 'CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR'
@ -1881,7 +1969,6 @@ export function getAccountInformation() {
} else {
accountInfo.tokenSource = authTokenSource
}
const { key: apiKey, source: apiKeySource } = getAnthropicApiKeyWithSource()
if (apiKey) {
accountInfo.apiKeySource = apiKeySource
}

View File

@ -10,6 +10,7 @@ import {
getSubscriptionType,
isClaudeAISubscriber,
isMaxSubscriber,
isOpenAIAuthActive,
isProSubscriber,
isTeamPremiumSubscriber,
} from '../auth.js'
@ -27,6 +28,10 @@ import { getAPIProvider } from './providers.js'
import { LIGHTNING_BOLT } from '../../constants/figures.js'
import { isModelAllowed } from './modelAllowlist.js'
import { type ModelAlias, isModelAlias } from './aliases.js'
import {
getOpenAIModelDisplayName,
resolveOpenAICodexModel,
} from '../../services/openaiAuth/models.js'
import { capitalize } from '../stringUtils.js'
export type ModelShortName = string
@ -103,6 +108,9 @@ export function getBestModel(): ModelName {
// @[MODEL LAUNCH]: Update the default Opus model (3P providers may lag so keep defaults unchanged).
export function getDefaultOpusModel(): ModelName {
if (isOpenAIAuthActive()) {
return resolveOpenAICodexModel('opus')
}
if (process.env.ANTHROPIC_DEFAULT_OPUS_MODEL) {
return process.env.ANTHROPIC_DEFAULT_OPUS_MODEL
}
@ -117,6 +125,9 @@ export function getDefaultOpusModel(): ModelName {
// @[MODEL LAUNCH]: Update the default Sonnet model (3P providers may lag so keep defaults unchanged).
export function getDefaultSonnetModel(): ModelName {
if (isOpenAIAuthActive()) {
return resolveOpenAICodexModel('sonnet')
}
if (process.env.ANTHROPIC_DEFAULT_SONNET_MODEL) {
return process.env.ANTHROPIC_DEFAULT_SONNET_MODEL
}
@ -129,6 +140,9 @@ export function getDefaultSonnetModel(): ModelName {
// @[MODEL LAUNCH]: Update the default Haiku model (3P providers may lag so keep defaults unchanged).
export function getDefaultHaikuModel(): ModelName {
if (isOpenAIAuthActive()) {
return resolveOpenAICodexModel('haiku')
}
if (process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL) {
return process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL
}
@ -188,6 +202,10 @@ export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias {
)
}
if (isOpenAIAuthActive()) {
return resolveOpenAICodexModel('opus')
}
// Max users get Opus as default
if (isMaxSubscriber()) {
return getDefaultOpusModel() + (isOpus1mMergeEnabled() ? '[1m]' : '')
@ -290,6 +308,12 @@ export function getCanonicalName(fullModelName: ModelName): ModelShortName {
export function getClaudeAiUserDefaultModelDescription(
fastMode = false,
): string {
if (isOpenAIAuthActive()) {
const defaultModel = renderDefaultModelSetting(
getDefaultMainLoopModelSetting(),
)
return `${defaultModel} · OpenAI/Codex default`
}
if (isMaxSubscriber() || isTeamPremiumSubscriber()) {
if (isOpus1mMergeEnabled()) {
return `Opus 4.7 with 1M context · Most capable for complex work${fastMode ? getOpus46PricingSuffix(true) : ''}`
@ -351,6 +375,10 @@ export function renderModelSetting(setting: ModelName | ModelAlias): string {
* if the model is not recognized as a public model.
*/
export function getPublicModelDisplayName(model: ModelName): string | null {
const openAIModelName = getOpenAIModelDisplayName(model)
if (openAIModelName) {
return openAIModelName
}
switch (model) {
case getModelStrings().opus46:
return 'Opus 4.7'
@ -572,6 +600,10 @@ export function modelDisplayString(model: ModelSetting): string {
// @[MODEL LAUNCH]: Add a marketing name mapping for the new model below.
export function getMarketingNameForModel(modelId: string): string | undefined {
const openAIModelName = getOpenAIModelDisplayName(modelId)
if (openAIModelName) {
return openAIModelName
}
if (getAPIProvider() === 'foundry') {
// deployment ID is user-defined in Foundry, so it may have no relation to the actual model
return undefined

View File

@ -3,6 +3,7 @@ import { getInitialMainLoopModel } from '../../bootstrap/state.js'
import {
isClaudeAISubscriber,
isMaxSubscriber,
isOpenAIAuthActive,
isTeamPremiumSubscriber,
} from '../auth.js'
import { getModelStrings } from './modelStrings.js'
@ -32,6 +33,7 @@ import {
} from './model.js'
import { has1mContext } from '../context.js'
import { getGlobalConfig } from '../config.js'
import { OPENAI_CODEX_MODEL_CATALOG } from '../../services/openaiAuth/models.js'
// @[MODEL LAUNCH]: Update all the available and default model option strings below.
@ -55,6 +57,18 @@ export function getDefaultOptionForUser(fastMode = false): ModelOption {
}
}
if (isOpenAIAuthActive()) {
const currentModel = renderDefaultModelSetting(
getDefaultMainLoopModelSetting(),
)
return {
value: null,
label: 'Default (recommended)',
description: `Use the OpenAI/Codex default model (currently ${currentModel})`,
descriptionForModel: `OpenAI/Codex default model (currently ${currentModel})`,
}
}
// Subscribers
if (isClaudeAISubscriber()) {
return {
@ -295,6 +309,18 @@ function getAzureOpenAICodexOptions(): ModelOption[] {
]
}
function getOpenAIModelOptions(): ModelOption[] {
return [
getDefaultOptionForUser(),
...OPENAI_CODEX_MODEL_CATALOG.map(model => ({
value: model.value,
label: model.label,
description: model.description,
descriptionForModel: model.descriptionForModel,
})),
]
}
// @[MODEL LAUNCH]: Update the model picker lists below to include/reorder options for the new model.
// Each user tier (ant, Max/Team Premium, Pro/Team Standard/Enterprise, PAYG 1P, PAYG 3P) has its own list.
function getModelOptionsBase(fastMode = false): ModelOption[] {
@ -320,6 +346,10 @@ function getModelOptionsBase(fastMode = false): ModelOption[] {
]
}
if (isOpenAIAuthActive()) {
return getOpenAIModelOptions()
}
if (isClaudeAISubscriber()) {
if (isMaxSubscriber() || isTeamPremiumSubscriber()) {
// Max and Team Premium users: Opus is default, show Sonnet as alternative

View File

@ -3,6 +3,7 @@ import { MODEL_ALIASES } from './aliases.js'
import { isModelAllowed } from './modelAllowlist.js'
import { getAPIProvider } from './providers.js'
import { sideQuery } from '../sideQuery.js'
import { isOpenAIAuthActive } from '../auth.js'
import {
NotFoundError,
APIError,
@ -10,6 +11,7 @@ import {
AuthenticationError,
} from '@anthropic-ai/sdk'
import { getModelStrings } from './modelStrings.js'
import { isOpenAIResponsesModel } from '../../services/openaiAuth/models.js'
// Cache valid models to avoid repeated API calls
const validModelCache = new Map<string, boolean>()
@ -109,6 +111,11 @@ export async function validateModel(
return { valid: true }
}
if (isOpenAIAuthActive() && isOpenAIResponsesModel(normalizedModel)) {
validModelCache.set(normalizedModel, true)
return { valid: true }
}
// Check cache first
if (validModelCache.has(normalizedModel)) {
return { valid: true }

File diff suppressed because one or more lines are too long