feat: Add Codex CLI provider with OAuth authentication (#1273)

Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
This commit is contained in:
Ben Vargas
2025-10-05 14:04:45 -06:00
committed by GitHub
parent 86027f1ee4
commit b43b7ce201
28 changed files with 2496 additions and 78 deletions

View File

@@ -28,6 +28,13 @@ export class BaseAIProvider {
* @type {boolean}
*/
this.needsExplicitJsonSchema = false;
/**
* Whether this provider supports temperature parameter
* Can be overridden by subclasses
* @type {boolean}
*/
this.supportsTemperature = true;
}
/**
@@ -168,7 +175,9 @@ export class BaseAIProvider {
model: client(params.modelId),
messages: params.messages,
...this.prepareTokenParam(params.modelId, params.maxTokens),
temperature: params.temperature
...(this.supportsTemperature && params.temperature !== undefined
? { temperature: params.temperature }
: {})
});
log(
@@ -211,7 +220,9 @@ export class BaseAIProvider {
model: client(params.modelId),
messages: params.messages,
...this.prepareTokenParam(params.modelId, params.maxTokens),
temperature: params.temperature
...(this.supportsTemperature && params.temperature !== undefined
? { temperature: params.temperature }
: {})
});
log(
@@ -249,7 +260,9 @@ export class BaseAIProvider {
schema: zodSchema(params.schema),
mode: params.mode || 'auto',
maxOutputTokens: params.maxTokens,
temperature: params.temperature
...(this.supportsTemperature && params.temperature !== undefined
? { temperature: params.temperature }
: {})
});
log(
@@ -295,7 +308,9 @@ export class BaseAIProvider {
schemaName: params.objectName,
schemaDescription: `Generate a valid JSON object for ${params.objectName}`,
maxTokens: params.maxTokens,
temperature: params.temperature
...(this.supportsTemperature && params.temperature !== undefined
? { temperature: params.temperature }
: {})
});
log(

View File

@@ -34,6 +34,8 @@ export class ClaudeCodeProvider extends BaseAIProvider {
this.supportedModels = ['sonnet', 'opus'];
// Claude Code requires explicit JSON schema mode
this.needsExplicitJsonSchema = true;
// Claude Code does not support temperature parameter
this.supportsTemperature = false;
}
/**

View File

@@ -0,0 +1,106 @@
/**
* src/ai-providers/codex-cli.js
*
* Codex CLI provider implementation using the ai-sdk-provider-codex-cli package.
* This provider uses the local OpenAI Codex CLI with OAuth (preferred) or
* an optional OPENAI_CODEX_API_KEY if provided.
*/
import { createCodexCli } from 'ai-sdk-provider-codex-cli';
import { BaseAIProvider } from './base-provider.js';
import { execSync } from 'child_process';
import { log } from '../../scripts/modules/utils.js';
import { getCodexCliSettingsForCommand } from '../../scripts/modules/config-manager.js';
export class CodexCliProvider extends BaseAIProvider {
constructor() {
super();
this.name = 'Codex CLI';
// Codex CLI has native schema support, no explicit JSON schema mode required
this.needsExplicitJsonSchema = false;
// Codex CLI does not support temperature parameter
this.supportsTemperature = false;
// Restrict to supported models for OAuth subscription usage
this.supportedModels = ['gpt-5', 'gpt-5-codex'];
// CLI availability check cache
this._codexCliChecked = false;
this._codexCliAvailable = null;
}
/**
* Codex CLI does not require an API key when using OAuth via `codex login`.
* @returns {boolean}
*/
isRequiredApiKey() {
return false;
}
/**
* Returns the environment variable name used when an API key is provided.
* Even though the API key is optional for Codex CLI (OAuth-first),
* downstream resolution expects a non-throwing implementation.
* Uses OPENAI_CODEX_API_KEY to avoid conflicts with OpenAI provider.
* @returns {string}
*/
getRequiredApiKeyName() {
return 'OPENAI_CODEX_API_KEY';
}
/**
* Optional CLI availability check; provide helpful guidance if missing.
*/
validateAuth() {
if (process.env.NODE_ENV === 'test') return;
if (!this._codexCliChecked) {
try {
execSync('codex --version', { stdio: 'pipe', timeout: 1000 });
this._codexCliAvailable = true;
} catch (error) {
this._codexCliAvailable = false;
log(
'warn',
'Codex CLI not detected. Install with: npm i -g @openai/codex or enable fallback with allowNpx.'
);
} finally {
this._codexCliChecked = true;
}
}
}
/**
* Creates a Codex CLI client instance
* @param {object} params
* @param {string} [params.commandName] - Command name for settings lookup
* @param {string} [params.apiKey] - Optional API key (injected as OPENAI_API_KEY for Codex CLI)
* @returns {Function}
*/
getClient(params = {}) {
try {
// Merge global + command-specific settings from config
const settings = getCodexCliSettingsForCommand(params.commandName) || {};
// Inject API key only if explicitly provided; OAuth is the primary path
const defaultSettings = {
...settings,
...(params.apiKey
? { env: { ...(settings.env || {}), OPENAI_API_KEY: params.apiKey } }
: {})
};
return createCodexCli({ defaultSettings });
} catch (error) {
const msg = String(error?.message || '');
const code = error?.code;
if (code === 'ENOENT' || /codex/i.test(msg)) {
const enhancedError = new Error(
`Codex CLI not available. Please install Codex CLI first. Original error: ${error.message}`
);
enhancedError.cause = error;
this.handleError('Codex CLI initialization', enhancedError);
} else {
this.handleError('client initialization', error);
}
}
}
}

View File

@@ -17,6 +17,8 @@ export class GeminiCliProvider extends BaseAIProvider {
this.name = 'Gemini CLI';
// Gemini CLI requires explicit JSON schema mode
this.needsExplicitJsonSchema = true;
// Gemini CLI does not support temperature parameter
this.supportsTemperature = false;
}
/**

View File

@@ -13,6 +13,8 @@ export class GrokCliProvider extends BaseAIProvider {
this.name = 'Grok CLI';
// Grok CLI requires explicit JSON schema mode
this.needsExplicitJsonSchema = true;
// Grok CLI does not support temperature parameter
this.supportsTemperature = false;
}
/**

View File

@@ -17,3 +17,4 @@ export { VertexAIProvider } from './google-vertex.js';
export { ClaudeCodeProvider } from './claude-code.js';
export { GeminiCliProvider } from './gemini-cli.js';
export { GrokCliProvider } from './grok-cli.js';
export { CodexCliProvider } from './codex-cli.js';

View File

@@ -24,7 +24,8 @@ export const CUSTOM_PROVIDERS = {
CLAUDE_CODE: 'claude-code',
MCP: 'mcp',
GEMINI_CLI: 'gemini-cli',
GROK_CLI: 'grok-cli'
GROK_CLI: 'grok-cli',
CODEX_CLI: 'codex-cli'
};
// Custom providers array (for backward compatibility and iteration)