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:
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
106
src/ai-providers/codex-cli.js
Normal file
106
src/ai-providers/codex-cli.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user