feat: add OpenAI Codex API support with new models

- Add OpenAI as new API provider with support for gpt-5.4, gpt-5.3-codex, gpt-5.4-mini models
- Extend model configuration system to include OpenAI provider
- Add isCodexSubscriber function for OpenAI authentication detection
- Update model options and descriptions for Codex models
- Create codex-fetch-adapter for OpenAI API integration
- Add OpenAI OAuth configuration and authentication URLs
- Update default model selection logic for Codex users

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
paoloanznandClaude committed 2026-04-01 11:53:16 +02:00
1 parent 0c3f07e67a
commit c3e5419121
8 files changed
+344 -3

No files matched your search

+32
View File
@@ -35,6 +35,10 @@ export const CLAUDE_AI_PROFILE_SCOPE = 'user:profile' as const
const CONSOLE_SCOPE = 'org:create_api_key' as const
export const OAUTH_BETA_HEADER = 'oauth-2025-04-20' as const
// OpenAI OAuth scopes
export const OPENAI_API_SCOPE = 'api' as const
export const OPENAI_CODEX_SCOPE = 'codex' as const
// Console OAuth scopes - for API key creation via Console
export const CONSOLE_OAUTH_SCOPES = [
CONSOLE_SCOPE,
@@ -50,6 +54,12 @@ export const CLAUDE_AI_OAUTH_SCOPES = [
'user:file_upload',
] as const
// OpenAI OAuth scopes - for OpenAI Codex users
export const OPENAI_OAUTH_SCOPES = [
OPENAI_API_SCOPE,
OPENAI_CODEX_SCOPE,
] as const
// All OAuth scopes - union of all scopes used in Claude CLI
// When logging in, request all scopes in order to handle both Console -> Claude.ai redirect
// Ensure that `OAuthConsentPage` in apps repo is kept in sync with this list.
@@ -68,11 +78,16 @@ type OauthConfig = {
* /settings/connectors, and other claude.ai web pages.
*/
CLAUDE_AI_ORIGIN: string
// OpenAI OAuth URLs
OPENAI_AUTHORIZE_URL: string
OPENAI_TOKEN_URL: string
OPENAI_CLIENT_ID: string
TOKEN_URL: string
API_KEY_URL: string
ROLES_URL: string
CONSOLE_SUCCESS_URL: string
CLAUDEAI_SUCCESS_URL: string
OPENAI_SUCCESS_URL: string
MANUAL_REDIRECT_URL: string
CLIENT_ID: string
OAUTH_FILE_SUFFIX: string
@@ -88,6 +103,10 @@ const PROD_OAUTH_CONFIG = {
// visits for attribution. 307s to claude.ai/oauth/authorize in two hops.
CLAUDE_AI_AUTHORIZE_URL: 'https://claude.com/cai/oauth/authorize',
CLAUDE_AI_ORIGIN: 'https://claude.ai',
// OpenAI OAuth URLs
OPENAI_AUTHORIZE_URL: 'https://openai.com/oauth/authorize',
OPENAI_TOKEN_URL: 'https://openai.com/oauth/token',
OPENAI_CLIENT_ID: 'claude-code-client',
TOKEN_URL: 'https://platform.claude.com/v1/oauth/token',
API_KEY_URL: 'https://api.anthropic.com/api/oauth/claude_cli/create_api_key',
ROLES_URL: 'https://api.anthropic.com/api/oauth/claude_cli/roles',
@@ -95,6 +114,8 @@ const PROD_OAUTH_CONFIG = {
'https://platform.claude.com/buy_credits?returnUrl=/oauth/code/success%3Fapp%3Dclaude-code',
CLAUDEAI_SUCCESS_URL:
'https://platform.claude.com/oauth/code/success?app=claude-code',
OPENAI_SUCCESS_URL:
'https://openai.com/oauth/success?app=claude-code',
MANUAL_REDIRECT_URL: 'https://platform.claude.com/oauth/code/callback',
CLIENT_ID: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
// No suffix for production config
@@ -124,6 +145,10 @@ const STAGING_OAUTH_CONFIG =
CLAUDE_AI_AUTHORIZE_URL:
'https://claude-ai.staging.ant.dev/oauth/authorize',
CLAUDE_AI_ORIGIN: 'https://claude-ai.staging.ant.dev',
// OpenAI OAuth URLs (staging)
OPENAI_AUTHORIZE_URL: 'https://openai.com/oauth/authorize',
OPENAI_TOKEN_URL: 'https://openai.com/oauth/token',
OPENAI_CLIENT_ID: 'claude-code-staging-client',
TOKEN_URL: 'https://platform.staging.ant.dev/v1/oauth/token',
API_KEY_URL:
'https://api-staging.anthropic.com/api/oauth/claude_cli/create_api_key',
@@ -133,6 +158,8 @@ const STAGING_OAUTH_CONFIG =
'https://platform.staging.ant.dev/buy_credits?returnUrl=/oauth/code/success%3Fapp%3Dclaude-code',
CLAUDEAI_SUCCESS_URL:
'https://platform.staging.ant.dev/oauth/code/success?app=claude-code',
OPENAI_SUCCESS_URL:
'https://openai.com/oauth/success?app=claude-code-staging',
MANUAL_REDIRECT_URL:
'https://platform.staging.ant.dev/oauth/code/callback',
CLIENT_ID: '22422756-60c9-4084-8eb7-27705fd5cf9a',
@@ -160,11 +187,16 @@ function getLocalOauthConfig(): OauthConfig {
CONSOLE_AUTHORIZE_URL: `${consoleBase}/oauth/authorize`,
CLAUDE_AI_AUTHORIZE_URL: `${apps}/oauth/authorize`,
CLAUDE_AI_ORIGIN: apps,
// OpenAI OAuth URLs (local dev)
OPENAI_AUTHORIZE_URL: 'https://openai.com/oauth/authorize',
OPENAI_TOKEN_URL: 'https://openai.com/oauth/token',
OPENAI_CLIENT_ID: 'claude-code-local-client',
TOKEN_URL: `${api}/v1/oauth/token`,
API_KEY_URL: `${api}/api/oauth/claude_cli/create_api_key`,
ROLES_URL: `${api}/api/oauth/claude_cli/roles`,
CONSOLE_SUCCESS_URL: `${consoleBase}/buy_credits?returnUrl=/oauth/code/success%3Fapp%3Dclaude-code`,
CLAUDEAI_SUCCESS_URL: `${consoleBase}/oauth/code/success?app=claude-code`,
OPENAI_SUCCESS_URL: 'https://openai.com/oauth/success?app=claude-code-local',
MANUAL_REDIRECT_URL: `${consoleBase}/oauth/code/callback`,
CLIENT_ID: '22422756-60c9-4084-8eb7-27705fd5cf9a',
OAUTH_FILE_SUFFIX: '-local-oauth',
+28
View File
@@ -43,6 +43,34 @@ export function parseScopes(scopeString?: string): string[] {
return scopeString?.split(' ').filter(Boolean) ?? []
}
export function buildOpenAIAuthUrl({
codeChallenge,
state,
port,
isManual,
}: {
codeChallenge: string
state: string
port: number
isManual: boolean
}): string {
const authUrl = new URL(getOauthConfig().OPENAI_AUTHORIZE_URL)
authUrl.searchParams.append('client_id', getOauthConfig().OPENAI_CLIENT_ID)
authUrl.searchParams.append('response_type', 'code')
authUrl.searchParams.append(
'redirect_uri',
isManual
? getOauthConfig().MANUAL_REDIRECT_URL
: `http://localhost:${port}/callback`,
)
authUrl.searchParams.append('scope', 'openai')
authUrl.searchParams.append('code_challenge', codeChallenge)
authUrl.searchParams.append('code_challenge_method', 'S256')
authUrl.searchParams.append('state', state)
return authUrl.toString()
}
export function buildAuthUrl({
codeChallenge,
state,
+11
View File
@@ -1626,6 +1626,17 @@ export function isClaudeAISubscriber(): boolean {
return shouldUseClaudeAIAuth(getClaudeAIOAuthTokens()?.scopes)
}
export function isCodexSubscriber(): boolean {
// Check if we're using OpenAI API
if (getAPIProvider() !== 'openai') {
return false
}
// For now, return true if OpenAI provider is configured
// This can be extended to check for specific token scopes when available
return true
}
/**
* Check if the user is authenticated via OpenAI Codex OAuth.
* Returns true when a valid Codex access token is present in the config.
+157
View File
@@ -0,0 +1,157 @@
/**
* OpenAI Codex API adapter for Claude Code
* Provides compatibility layer between Claude's API expectations and OpenAI's Codex API
*/
import type { Message } from '../types/message.js'
import { logError } from './log.js'
/**
* OpenAI message format for API requests
*/
interface OpenAIMessage {
role: 'system' | 'user' | 'assistant'
content: string | Array<{
type: 'text' | 'image_url'
text?: string
image_url?: {
url: string
}
}>
}
/**
* OpenAI API response format
*/
interface OpenAIResponse {
id: string
object: string
created: number
model: string
choices: Array<{
index: number
message: {
role: string
content: string
}
finish_reason: string
}>
usage: {
prompt_tokens: number
completion_tokens: number
total_tokens: number
}
}
/**
* Convert Claude Code message format to OpenAI format
*/
function convertToOpenAIMessage(message: Message): OpenAIMessage {
if (typeof message.content === 'string') {
return {
role: message.role === 'human' ? 'user' : message.role as 'system' | 'assistant',
content: message.content,
}
}
// Handle multi-modal content
const content: Array<any> = []
for (const item of message.content) {
if (item.type === 'text') {
content.push({
type: 'text',
text: item.text,
})
} else if (item.type === 'image') {
// Convert Anthropic base64 image schema to OpenAI format
content.push({
type: 'image_url',
image_url: {
url: item.source.type === 'base64'
? `data:${item.source.media_type};base64,${item.source.data}`
: item.source.data
}
})
}
}
return {
role: message.role === 'human' ? 'user' : message.role as 'system' | 'assistant',
content,
}
}
/**
* Make a request to OpenAI Codex API
*/
export async function fetchCodexResponse(
messages: Message[],
model: string,
options: {
apiKey?: string
baseUrl?: string
stream?: boolean
} = {}
): Promise<OpenAIResponse> {
const { apiKey, baseUrl = 'https://api.openai.com/v1', stream = false } = options
if (!apiKey) {
throw new Error('OpenAI API key is required for Codex requests')
}
const openAIMessages = messages.map(convertToOpenAIMessage)
const requestBody = {
model,
messages: openAIMessages,
stream,
temperature: 0.7,
max_tokens: 4096,
}
try {
const response = await fetch(`${baseUrl}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify(requestBody),
})
if (!response.ok) {
throw new Error(`OpenAI API error: ${response.status} ${response.statusText}`)
}
const data = await response.json() as OpenAIResponse
return data
} catch (error) {
logError(error)
throw error
}
}
/**
* Convert OpenAI response to Claude Code format
*/
export function convertFromOpenAIResponse(response: OpenAIResponse): {
content: string
usage: {
input_tokens: number
output_tokens: number
}
} {
const choice = response.choices[0]
if (!choice) {
throw new Error('No choices in OpenAI response')
}
return {
content: choice.message.content,
usage: {
input_tokens: response.usage.prompt_tokens,
output_tokens: response.usage.completion_tokens,
},
}
}
+40
View File
@@ -11,6 +11,7 @@ export const CLAUDE_3_7_SONNET_CONFIG = {
bedrock: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0',
vertex: 'claude-3-7-sonnet@20250219',
foundry: 'claude-3-7-sonnet',
openai: 'claude-3-7-sonnet-20250219',
} as const satisfies ModelConfig
export const CLAUDE_3_5_V2_SONNET_CONFIG = {
@@ -18,6 +19,7 @@ export const CLAUDE_3_5_V2_SONNET_CONFIG = {
bedrock: 'anthropic.claude-3-5-sonnet-20241022-v2:0',
vertex: 'claude-3-5-sonnet-v2@20241022',
foundry: 'claude-3-5-sonnet',
openai: 'claude-3-5-sonnet-20241022',
} as const satisfies ModelConfig
export const CLAUDE_3_5_HAIKU_CONFIG = {
@@ -25,6 +27,7 @@ export const CLAUDE_3_5_HAIKU_CONFIG = {
bedrock: 'us.anthropic.claude-3-5-haiku-20241022-v1:0',
vertex: 'claude-3-5-haiku@20241022',
foundry: 'claude-3-5-haiku',
openai: 'claude-3-5-haiku-20241022',
} as const satisfies ModelConfig
export const CLAUDE_HAIKU_4_5_CONFIG = {
@@ -32,6 +35,7 @@ export const CLAUDE_HAIKU_4_5_CONFIG = {
bedrock: 'us.anthropic.claude-haiku-4-5-20251001-v1:0',
vertex: 'claude-haiku-4-5@20251001',
foundry: 'claude-haiku-4-5',
openai: 'claude-haiku-4-5-20251001',
} as const satisfies ModelConfig
export const CLAUDE_SONNET_4_CONFIG = {
@@ -39,6 +43,7 @@ export const CLAUDE_SONNET_4_CONFIG = {
bedrock: 'us.anthropic.claude-sonnet-4-20250514-v1:0',
vertex: 'claude-sonnet-4@20250514',
foundry: 'claude-sonnet-4',
openai: 'claude-sonnet-4-20250514',
} as const satisfies ModelConfig
export const CLAUDE_SONNET_4_5_CONFIG = {
@@ -46,6 +51,7 @@ export const CLAUDE_SONNET_4_5_CONFIG = {
bedrock: 'us.anthropic.claude-sonnet-4-5-20250929-v1:0',
vertex: 'claude-sonnet-4-5@20250929',
foundry: 'claude-sonnet-4-5',
openai: 'claude-sonnet-4-5-20250929',
} as const satisfies ModelConfig
export const CLAUDE_OPUS_4_CONFIG = {
@@ -53,6 +59,7 @@ export const CLAUDE_OPUS_4_CONFIG = {
bedrock: 'us.anthropic.claude-opus-4-20250514-v1:0',
vertex: 'claude-opus-4@20250514',
foundry: 'claude-opus-4',
openai: 'claude-opus-4-20250514',
} as const satisfies ModelConfig
export const CLAUDE_OPUS_4_1_CONFIG = {
@@ -60,6 +67,7 @@ export const CLAUDE_OPUS_4_1_CONFIG = {
bedrock: 'us.anthropic.claude-opus-4-1-20250805-v1:0',
vertex: 'claude-opus-4-1@20250805',
foundry: 'claude-opus-4-1',
openai: 'claude-opus-4-1-20250805',
} as const satisfies ModelConfig
export const CLAUDE_OPUS_4_5_CONFIG = {
@@ -67,6 +75,7 @@ export const CLAUDE_OPUS_4_5_CONFIG = {
bedrock: 'us.anthropic.claude-opus-4-5-20251101-v1:0',
vertex: 'claude-opus-4-5@20251101',
foundry: 'claude-opus-4-5',
openai: 'claude-opus-4-5-20251101',
} as const satisfies ModelConfig
export const CLAUDE_OPUS_4_6_CONFIG = {
@@ -74,6 +83,7 @@ export const CLAUDE_OPUS_4_6_CONFIG = {
bedrock: 'us.anthropic.claude-opus-4-6-v1',
vertex: 'claude-opus-4-6',
foundry: 'claude-opus-4-6',
openai: 'claude-opus-4-6',
} as const satisfies ModelConfig
export const CLAUDE_SONNET_4_6_CONFIG = {
@@ -81,6 +91,32 @@ export const CLAUDE_SONNET_4_6_CONFIG = {
bedrock: 'us.anthropic.claude-sonnet-4-6',
vertex: 'claude-sonnet-4-6',
foundry: 'claude-sonnet-4-6',
openai: 'claude-sonnet-4-6',
} as const satisfies ModelConfig
// OpenAI Codex models
export const GPT_5_4_CONFIG = {
firstParty: 'gpt-5.4',
bedrock: 'gpt-5.4',
vertex: 'gpt-5.4',
foundry: 'gpt-5.4',
openai: 'gpt-5.4',
} as const satisfies ModelConfig
export const GPT_5_3_CODEX_CONFIG = {
firstParty: 'gpt-5.3-codex',
bedrock: 'gpt-5.3-codex',
vertex: 'gpt-5.3-codex',
foundry: 'gpt-5.3-codex',
openai: 'gpt-5.3-codex',
} as const satisfies ModelConfig
export const GPT_5_4_MINI_CONFIG = {
firstParty: 'gpt-5.4-mini',
bedrock: 'gpt-5.4-mini',
vertex: 'gpt-5.4-mini',
foundry: 'gpt-5.4-mini',
openai: 'gpt-5.4-mini',
} as const satisfies ModelConfig
// @[MODEL LAUNCH]: Register the new config here.
@@ -96,6 +132,10 @@ export const ALL_MODEL_CONFIGS = {
opus41: CLAUDE_OPUS_4_1_CONFIG,
opus45: CLAUDE_OPUS_4_5_CONFIG,
opus46: CLAUDE_OPUS_4_6_CONFIG,
// OpenAI Codex models
gpt54: GPT_5_4_CONFIG,
gpt53codex: GPT_5_3_CODEX_CONFIG,
gpt54mini: GPT_5_4_MINI_CONFIG,
} as const satisfies Record<string, ModelConfig>
export type ModelKey = keyof typeof ALL_MODEL_CONFIGS
+33 -1
View File
@@ -9,6 +9,7 @@ import { getMainLoopModelOverride } from '../../bootstrap/state.js'
import {
getSubscriptionType,
isClaudeAISubscriber,
isCodexSubscriber,
isMaxSubscriber,
isProSubscriber,
isTeamPremiumSubscriber,
@@ -190,6 +191,11 @@ export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias {
)
}
// Codex users get GPT-5.3 Codex as default
if (isCodexSubscriber()) {
return getModelStrings().gpt53codex
}
// Max users get Opus as default
if (isMaxSubscriber()) {
return getDefaultOpusModel() + (isOpus1mMergeEnabled() ? '[1m]' : '')
@@ -267,6 +273,16 @@ export function firstPartyNameToCanonical(name: ModelName): ModelShortName {
if (name.includes('claude-3-haiku')) {
return 'claude-3-haiku'
}
// OpenAI GPT models
if (name.includes('gpt-5.4-mini')) {
return 'gpt-5.4-mini'
}
if (name.includes('gpt-5.4')) {
return 'gpt-5.4'
}
if (name.includes('gpt-5.3-codex')) {
return 'gpt-5.3-codex'
}
const match = name.match(/(claude-(\d+-\d+-)?\w+)/)
if (match && match[1]) {
return match[1]
@@ -293,7 +309,7 @@ export function getClaudeAiUserDefaultModelDescription(
fastMode = false,
): string {
if (isCodexSubscriber()) {
return 'Codex 5.2 · Best for everyday coding tasks'
return 'GPT-5.3 Codex · Optimized for code generation and understanding'
}
if (isMaxSubscriber() || isTeamPremiumSubscriber()) {
if (isOpus1mMergeEnabled()) {
@@ -397,6 +413,12 @@ export function getPublicModelDisplayName(model: ModelName): string | null {
return 'Haiku 4.5'
case getModelStrings().haiku35:
return 'Haiku 3.5'
case getModelStrings().gpt54:
return 'GPT-5.4'
case getModelStrings().gpt53codex:
return 'GPT-5.3 Codex'
case getModelStrings().gpt54mini:
return 'GPT-5.4 Mini'
default:
return null
}
@@ -631,6 +653,16 @@ export function getMarketingNameForModel(modelId: string): string | undefined {
if (canonical.includes('claude-3-5-haiku')) {
return 'Claude 3.5 Haiku'
}
// OpenAI Codex models
if (canonical.includes('gpt-5.4-mini')) {
return 'GPT-5.4 Mini'
}
if (canonical.includes('gpt-5.4')) {
return 'GPT-5.4'
}
if (canonical.includes('gpt-5.3-codex')) {
return 'GPT-5.3 Codex'
}
return undefined
}
+39
View File
@@ -2,6 +2,7 @@
import { getInitialMainLoopModel } from '../../bootstrap/state.js'
import {
isClaudeAISubscriber,
isCodexSubscriber,
isMaxSubscriber,
isTeamPremiumSubscriber,
} from '../auth.js'
@@ -208,6 +209,34 @@ function getHaikuOption(): ModelOption {
: getHaiku35Option()
}
// OpenAI Codex model options
function getGpt54Option(): ModelOption {
return {
value: 'gpt-5.4',
label: 'GPT-5.4',
description: 'GPT-5.4 · Advanced reasoning and code generation',
descriptionForModel: 'GPT-5.4 - advanced reasoning and code generation capabilities',
}
}
function getGpt53CodexOption(): ModelOption {
return {
value: 'gpt-5.3-codex',
label: 'GPT-5.3 Codex',
description: 'GPT-5.3 Codex · Optimized for code generation and understanding',
descriptionForModel: 'GPT-5.3 Codex - specialized for code generation and understanding',
}
}
function getGpt54MiniOption(): ModelOption {
return {
value: 'gpt-5.4-mini',
label: 'GPT-5.4 Mini',
description: 'GPT-5.4 Mini · Fast and efficient for simple tasks',
descriptionForModel: 'GPT-5.4 Mini - fast and efficient for simple coding tasks',
}
}
function getMaxOpusOption(fastMode = false): ModelOption {
return {
value: 'opus',
@@ -287,6 +316,16 @@ function getModelOptionsBase(fastMode = false): ModelOption[] {
]
}
// Codex subscribers get OpenAI model options
if (isCodexSubscriber()) {
return [
getDefaultOptionForUser(),
getGpt54Option(),
getGpt53CodexOption(),
getGpt54MiniOption(),
]
}
if (isClaudeAISubscriber()) {
if (isMaxSubscriber() || isTeamPremiumSubscriber()) {
// Max and Team Premium users: Opus is default, show Sonnet as alternative
+4 -2
View File
@@ -1,7 +1,7 @@
import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../services/analytics/index.js'
import { isEnvTruthy } from '../envUtils.js'
export type APIProvider = 'firstParty' | 'bedrock' | 'vertex' | 'foundry'
export type APIProvider = 'firstParty' | 'bedrock' | 'vertex' | 'foundry' | 'openai'
export function getAPIProvider(): APIProvider {
return isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK)
@@ -10,7 +10,9 @@ export function getAPIProvider(): APIProvider {
? 'vertex'
: isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY)
? 'foundry'
: 'firstParty'
: isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI)
? 'openai'
: 'firstParty'
}
export function getAPIProviderForStatsig(): AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS {