Add Codex plan/spark provider support
This commit is contained in:
@@ -1,8 +1,12 @@
|
||||
// @ts-nocheck
|
||||
import { writeFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import {
|
||||
DEFAULT_CODEX_BASE_URL,
|
||||
resolveCodexApiCredentials,
|
||||
} from '../src/services/api/providerConfig.js'
|
||||
|
||||
type ProviderProfile = 'openai' | 'ollama'
|
||||
type ProviderProfile = 'openai' | 'ollama' | 'codex'
|
||||
|
||||
type ProfileFile = {
|
||||
profile: ProviderProfile
|
||||
@@ -10,6 +14,7 @@ type ProfileFile = {
|
||||
OPENAI_BASE_URL?: string
|
||||
OPENAI_MODEL?: string
|
||||
OPENAI_API_KEY?: string
|
||||
CODEX_API_KEY?: string
|
||||
}
|
||||
createdAt: string
|
||||
}
|
||||
@@ -23,7 +28,7 @@ function parseArg(name: string): string | null {
|
||||
|
||||
function parseProviderArg(): ProviderProfile | 'auto' {
|
||||
const p = parseArg('--provider')?.toLowerCase()
|
||||
if (p === 'openai' || p === 'ollama') return p
|
||||
if (p === 'openai' || p === 'ollama' || p === 'codex') return p
|
||||
return 'auto'
|
||||
}
|
||||
|
||||
@@ -69,6 +74,23 @@ async function main(): Promise<void> {
|
||||
env.OPENAI_MODEL = argModel || process.env.OPENAI_MODEL || 'llama3.1:8b'
|
||||
const key = sanitizeApiKey(argApiKey || process.env.OPENAI_API_KEY || null)
|
||||
if (key) env.OPENAI_API_KEY = key
|
||||
} else if (selected === 'codex') {
|
||||
env.OPENAI_BASE_URL =
|
||||
argBaseUrl || process.env.OPENAI_BASE_URL || DEFAULT_CODEX_BASE_URL
|
||||
env.OPENAI_MODEL = argModel || process.env.OPENAI_MODEL || 'codexplan'
|
||||
const key = sanitizeApiKey(argApiKey || process.env.CODEX_API_KEY || null)
|
||||
if (key) {
|
||||
env.CODEX_API_KEY = key
|
||||
} else {
|
||||
const credentials = resolveCodexApiCredentials(process.env)
|
||||
if (!credentials.apiKey) {
|
||||
const authHint = credentials.authPath
|
||||
? ` or make sure ${credentials.authPath} exists`
|
||||
: ''
|
||||
console.error(`Codex profile requires CODEX_API_KEY${authHint}.`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
env.OPENAI_BASE_URL = argBaseUrl || process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1'
|
||||
env.OPENAI_MODEL = argModel || process.env.OPENAI_MODEL || 'gpt-4o'
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
import { spawn } from 'node:child_process'
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import {
|
||||
DEFAULT_CODEX_BASE_URL,
|
||||
resolveCodexApiCredentials,
|
||||
} from '../src/services/api/providerConfig.js'
|
||||
|
||||
type ProviderProfile = 'openai' | 'ollama'
|
||||
type ProviderProfile = 'openai' | 'ollama' | 'codex'
|
||||
|
||||
type ProfileFile = {
|
||||
profile: ProviderProfile
|
||||
@@ -11,6 +15,7 @@ type ProfileFile = {
|
||||
OPENAI_BASE_URL?: string
|
||||
OPENAI_MODEL?: string
|
||||
OPENAI_API_KEY?: string
|
||||
CODEX_API_KEY?: string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +37,7 @@ function parseLaunchOptions(argv: string[]): LaunchOptions {
|
||||
continue
|
||||
}
|
||||
|
||||
if ((lower === 'auto' || lower === 'openai' || lower === 'ollama') && requestedProfile === 'auto') {
|
||||
if ((lower === 'auto' || lower === 'openai' || lower === 'ollama' || lower === 'codex') && requestedProfile === 'auto') {
|
||||
requestedProfile = lower as ProviderProfile | 'auto'
|
||||
continue
|
||||
}
|
||||
@@ -62,7 +67,7 @@ function loadPersistedProfile(): ProfileFile | null {
|
||||
if (!existsSync(path)) return null
|
||||
try {
|
||||
const parsed = JSON.parse(readFileSync(path, 'utf8')) as ProfileFile
|
||||
if (parsed.profile === 'openai' || parsed.profile === 'ollama') {
|
||||
if (parsed.profile === 'openai' || parsed.profile === 'ollama' || parsed.profile === 'codex') {
|
||||
return parsed
|
||||
}
|
||||
return null
|
||||
@@ -115,6 +120,20 @@ function buildEnv(profile: ProviderProfile, persisted: ProfileFile | null): Node
|
||||
return env
|
||||
}
|
||||
|
||||
if (profile === 'codex') {
|
||||
env.OPENAI_BASE_URL =
|
||||
process.env.OPENAI_BASE_URL ||
|
||||
persistedEnv.OPENAI_BASE_URL ||
|
||||
DEFAULT_CODEX_BASE_URL
|
||||
env.OPENAI_MODEL =
|
||||
process.env.OPENAI_MODEL ||
|
||||
persistedEnv.OPENAI_MODEL ||
|
||||
'codexplan'
|
||||
env.CODEX_API_KEY =
|
||||
process.env.CODEX_API_KEY || persistedEnv.CODEX_API_KEY
|
||||
return env
|
||||
}
|
||||
|
||||
env.OPENAI_BASE_URL = process.env.OPENAI_BASE_URL || persistedEnv.OPENAI_BASE_URL || 'https://api.openai.com/v1'
|
||||
env.OPENAI_MODEL = process.env.OPENAI_MODEL || persistedEnv.OPENAI_MODEL || 'gpt-4o'
|
||||
env.OPENAI_API_KEY = process.env.OPENAI_API_KEY || persistedEnv.OPENAI_API_KEY
|
||||
@@ -137,18 +156,22 @@ function quoteArg(arg: string): string {
|
||||
}
|
||||
|
||||
function printSummary(profile: ProviderProfile, env: NodeJS.ProcessEnv): void {
|
||||
const keySet = Boolean(env.OPENAI_API_KEY)
|
||||
const keySet = profile === 'codex'
|
||||
? Boolean(resolveCodexApiCredentials(env).apiKey)
|
||||
: Boolean(env.OPENAI_API_KEY)
|
||||
console.log(`Launching profile: ${profile}`)
|
||||
console.log(`OPENAI_BASE_URL=${env.OPENAI_BASE_URL}`)
|
||||
console.log(`OPENAI_MODEL=${env.OPENAI_MODEL}`)
|
||||
console.log(`OPENAI_API_KEY_SET=${keySet}`)
|
||||
console.log(
|
||||
`${profile === 'codex' ? 'CODEX_API_KEY_SET' : 'OPENAI_API_KEY_SET'}=${keySet}`,
|
||||
)
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const options = parseLaunchOptions(process.argv.slice(2))
|
||||
const requestedProfile = options.requestedProfile
|
||||
if (!requestedProfile) {
|
||||
console.error('Usage: bun run scripts/provider-launch.ts [openai|ollama|auto] [--fast] [-- <cli args>]')
|
||||
console.error('Usage: bun run scripts/provider-launch.ts [openai|ollama|codex|auto] [--fast] [-- <cli args>]')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
@@ -175,6 +198,17 @@ async function main(): Promise<void> {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (profile === 'codex') {
|
||||
const credentials = resolveCodexApiCredentials(env)
|
||||
if (!credentials.apiKey) {
|
||||
const authHint = credentials.authPath
|
||||
? ` or make sure ${credentials.authPath} exists`
|
||||
: ''
|
||||
console.error(`CODEX_API_KEY is required for codex profile${authHint}. Run: bun run profile:init -- --provider codex --model codexplan`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
printSummary(profile, env)
|
||||
|
||||
const doctorCode = await runCommand('bun run scripts/system-check.ts', env)
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import {
|
||||
resolveCodexApiCredentials,
|
||||
resolveProviderRequest,
|
||||
isLocalProviderUrl as isProviderLocalUrl,
|
||||
} from '../src/services/api/providerConfig.js'
|
||||
|
||||
type CheckResult = {
|
||||
ok: boolean
|
||||
@@ -84,12 +89,7 @@ function checkBuildArtifacts(): CheckResult {
|
||||
}
|
||||
|
||||
function isLocalBaseUrl(baseUrl: string): boolean {
|
||||
try {
|
||||
const url = new URL(baseUrl)
|
||||
return url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '::1'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
return isProviderLocalUrl(baseUrl)
|
||||
}
|
||||
|
||||
function currentBaseUrl(): string {
|
||||
@@ -105,23 +105,50 @@ function checkOpenAIEnv(): CheckResult[] {
|
||||
return results
|
||||
}
|
||||
|
||||
const baseUrl = process.env.OPENAI_BASE_URL ?? 'https://api.openai.com/v1'
|
||||
const model = process.env.OPENAI_MODEL
|
||||
const key = process.env.OPENAI_API_KEY
|
||||
const request = resolveProviderRequest({
|
||||
model: process.env.OPENAI_MODEL,
|
||||
baseUrl: process.env.OPENAI_BASE_URL,
|
||||
})
|
||||
|
||||
results.push(pass('Provider mode', 'OpenAI-compatible provider enabled.'))
|
||||
results.push(
|
||||
pass(
|
||||
'Provider mode',
|
||||
request.transport === 'codex_responses'
|
||||
? 'Codex responses backend enabled.'
|
||||
: 'OpenAI-compatible provider enabled.',
|
||||
),
|
||||
)
|
||||
|
||||
if (!model) {
|
||||
if (!process.env.OPENAI_MODEL) {
|
||||
results.push(pass('OPENAI_MODEL', 'Not set. Runtime fallback model will be used.'))
|
||||
} else {
|
||||
results.push(pass('OPENAI_MODEL', model))
|
||||
results.push(pass('OPENAI_MODEL', process.env.OPENAI_MODEL))
|
||||
}
|
||||
|
||||
results.push(pass('OPENAI_BASE_URL', baseUrl))
|
||||
results.push(pass('OPENAI_BASE_URL', request.baseUrl))
|
||||
|
||||
if (request.transport === 'codex_responses') {
|
||||
const credentials = resolveCodexApiCredentials(process.env)
|
||||
if (!credentials.apiKey) {
|
||||
const authHint = credentials.authPath
|
||||
? `Missing CODEX_API_KEY and no usable auth.json at ${credentials.authPath}.`
|
||||
: 'Missing CODEX_API_KEY and auth.json fallback.'
|
||||
results.push(fail('CODEX auth', authHint))
|
||||
} else if (!credentials.accountId) {
|
||||
results.push(fail('CHATGPT_ACCOUNT_ID', 'Missing chatgpt_account_id in Codex auth.'))
|
||||
} else {
|
||||
const detail = credentials.source === 'env'
|
||||
? 'Using CODEX_API_KEY.'
|
||||
: `Using ${credentials.authPath}.`
|
||||
results.push(pass('CODEX auth', detail))
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
const key = process.env.OPENAI_API_KEY
|
||||
if (key === 'SUA_CHAVE') {
|
||||
results.push(fail('OPENAI_API_KEY', 'Placeholder value detected: SUA_CHAVE.'))
|
||||
} else if (!key && !isLocalBaseUrl(baseUrl)) {
|
||||
} else if (!key && !isLocalBaseUrl(request.baseUrl)) {
|
||||
results.push(fail('OPENAI_API_KEY', 'Missing key for non-local provider URL.'))
|
||||
} else if (!key) {
|
||||
results.push(pass('OPENAI_API_KEY', 'Not set (allowed for local providers like Ollama/LM Studio).'))
|
||||
@@ -137,22 +164,53 @@ async function checkBaseUrlReachability(): Promise<CheckResult> {
|
||||
return pass('Provider reachability', 'Skipped (OpenAI-compatible mode disabled).')
|
||||
}
|
||||
|
||||
const baseUrl = currentBaseUrl()
|
||||
const key = process.env.OPENAI_API_KEY
|
||||
const endpoint = `${baseUrl.replace(/\/$/, '')}/models`
|
||||
const request = resolveProviderRequest({
|
||||
model: process.env.OPENAI_MODEL,
|
||||
baseUrl: process.env.OPENAI_BASE_URL,
|
||||
})
|
||||
const endpoint = request.transport === 'codex_responses'
|
||||
? `${request.baseUrl}/responses`
|
||||
: `${request.baseUrl}/models`
|
||||
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), 4000)
|
||||
|
||||
try {
|
||||
const headers: Record<string, string> = {}
|
||||
if (key) {
|
||||
headers.Authorization = `Bearer ${key}`
|
||||
let method = 'GET'
|
||||
let body: string | undefined
|
||||
|
||||
if (request.transport === 'codex_responses') {
|
||||
const credentials = resolveCodexApiCredentials(process.env)
|
||||
if (credentials.apiKey) {
|
||||
headers.Authorization = `Bearer ${credentials.apiKey}`
|
||||
}
|
||||
if (credentials.accountId) {
|
||||
headers['chatgpt-account-id'] = credentials.accountId
|
||||
}
|
||||
headers['Content-Type'] = 'application/json'
|
||||
method = 'POST'
|
||||
body = JSON.stringify({
|
||||
model: request.resolvedModel,
|
||||
instructions: 'Runtime doctor probe.',
|
||||
input: [
|
||||
{
|
||||
type: 'message',
|
||||
role: 'user',
|
||||
content: [{ type: 'input_text', text: 'ping' }],
|
||||
},
|
||||
],
|
||||
store: false,
|
||||
stream: true,
|
||||
})
|
||||
} else if (process.env.OPENAI_API_KEY) {
|
||||
headers.Authorization = `Bearer ${process.env.OPENAI_API_KEY}`
|
||||
}
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'GET',
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
@@ -209,11 +267,16 @@ function checkOllamaProcessorMode(): CheckResult {
|
||||
}
|
||||
|
||||
function serializeSafeEnvSummary(): Record<string, string | boolean> {
|
||||
const request = resolveProviderRequest({
|
||||
model: process.env.OPENAI_MODEL,
|
||||
baseUrl: process.env.OPENAI_BASE_URL,
|
||||
})
|
||||
return {
|
||||
CLAUDE_CODE_USE_OPENAI: isTruthy(process.env.CLAUDE_CODE_USE_OPENAI),
|
||||
OPENAI_MODEL: process.env.OPENAI_MODEL ?? '(unset)',
|
||||
OPENAI_BASE_URL: process.env.OPENAI_BASE_URL ?? 'https://api.openai.com/v1',
|
||||
OPENAI_BASE_URL: request.baseUrl,
|
||||
OPENAI_API_KEY_SET: Boolean(process.env.OPENAI_API_KEY),
|
||||
CODEX_API_KEY_SET: Boolean(resolveCodexApiCredentials(process.env).apiKey),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user