385 lines
9.8 KiB
PHP
385 lines
9.8 KiB
PHP
<?php
|
|
/**
|
|
* Codex Provider (OpenAI API)
|
|
*
|
|
* Direct integration with OpenAI's API for text generation
|
|
*
|
|
* @package WP_Agentic_Writer
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
class WP_Agentic_Writer_Codex_Provider implements WP_Agentic_Writer_AI_Provider_Interface {
|
|
|
|
/**
|
|
* OpenAI API key
|
|
*
|
|
* @var string
|
|
*/
|
|
private $api_key = '';
|
|
|
|
/**
|
|
* API endpoint
|
|
*
|
|
* @var string
|
|
*/
|
|
private $api_endpoint = 'https://api.openai.com/v1/chat/completions';
|
|
|
|
/**
|
|
* Default model
|
|
*
|
|
* @var string
|
|
*/
|
|
private $model = 'gpt-4o';
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
public function __construct() {
|
|
$settings = get_option( 'wp_agentic_writer_settings', array() );
|
|
$this->api_key = $settings['codex_api_key'] ?? '';
|
|
$this->model = $settings['codex_model'] ?? 'gpt-4o';
|
|
}
|
|
|
|
/**
|
|
* Non-streaming chat completion
|
|
*
|
|
* @param array $messages Array of message objects.
|
|
* @param array $options Optional parameters.
|
|
* @param string $type Task type.
|
|
* @return array|WP_Error Response with content, model, tokens, cost.
|
|
*/
|
|
public function chat( $messages, $options = array(), $type = 'planning' ) {
|
|
if ( ! $this->is_configured() ) {
|
|
return new WP_Error(
|
|
'not_configured',
|
|
__( 'Codex API key not configured.', 'wp-agentic-writer' )
|
|
);
|
|
}
|
|
|
|
$model = $options['model'] ?? $this->model;
|
|
$temperature = $options['temperature'] ?? 0.7;
|
|
$max_tokens = $options['max_tokens'] ?? null;
|
|
|
|
$body = array(
|
|
'model' => $model,
|
|
'messages' => $messages,
|
|
'temperature' => $temperature,
|
|
);
|
|
|
|
if ( $max_tokens ) {
|
|
$body['max_tokens'] = $max_tokens;
|
|
}
|
|
|
|
$start_time = microtime( true );
|
|
|
|
$response = wp_remote_post(
|
|
$this->api_endpoint,
|
|
array(
|
|
'headers' => array(
|
|
'Authorization' => 'Bearer ' . $this->api_key,
|
|
'Content-Type' => 'application/json',
|
|
),
|
|
'body' => wp_json_encode( $body ),
|
|
'timeout' => 120,
|
|
)
|
|
);
|
|
|
|
$generation_time = microtime( true ) - $start_time;
|
|
|
|
if ( is_wp_error( $response ) ) {
|
|
return new WP_Error(
|
|
'connection_failed',
|
|
sprintf(
|
|
/* translators: %s: error message */
|
|
__( 'Codex connection failed: %s', 'wp-agentic-writer' ),
|
|
$response->get_error_message()
|
|
)
|
|
);
|
|
}
|
|
|
|
$code = wp_remote_retrieve_response_code( $response );
|
|
if ( 200 !== $code ) {
|
|
$body_response = wp_remote_retrieve_body( $response );
|
|
$error_data = json_decode( $body_response, true );
|
|
return new WP_Error(
|
|
'api_error',
|
|
sprintf(
|
|
/* translators: %1$d: HTTP status code, %2$s: error message */
|
|
__( 'Codex error (%1$d): %2$s', 'wp-agentic-writer' ),
|
|
$code,
|
|
$error_data['error']['message'] ?? $body_response
|
|
)
|
|
);
|
|
}
|
|
|
|
$response_body = json_decode( wp_remote_retrieve_body( $response ), true );
|
|
|
|
if ( ! isset( $response_body['choices'][0]['message']['content'] ) ) {
|
|
return new WP_Error(
|
|
'invalid_response',
|
|
__( 'Invalid response format from Codex', 'wp-agentic-writer' )
|
|
);
|
|
}
|
|
|
|
$content = $response_body['choices'][0]['message']['content'];
|
|
$usage = $response_body['usage'] ?? array();
|
|
|
|
// Calculate cost based on OpenAI pricing
|
|
$cost = $this->calculate_cost( $model, $usage );
|
|
|
|
return array(
|
|
'content' => $content,
|
|
'model' => $model,
|
|
'input_tokens' => $usage['prompt_tokens'] ?? 0,
|
|
'output_tokens' => $usage['completion_tokens'] ?? 0,
|
|
'total_tokens' => $usage['total_tokens'] ?? 0,
|
|
'cost' => $cost,
|
|
'generation_time' => $generation_time,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Streaming chat completion
|
|
*
|
|
* @param array $messages Array of message objects.
|
|
* @param array $options Optional parameters.
|
|
* @param string $type Task type.
|
|
* @param callable $callback Function to call with each chunk.
|
|
* @return array|WP_Error Response or error.
|
|
*/
|
|
public function chat_stream( $messages, $options = array(), $type = 'planning', $callback = null ) {
|
|
if ( ! $this->is_configured() ) {
|
|
return new WP_Error(
|
|
'not_configured',
|
|
__( 'Codex API key not configured.', 'wp-agentic-writer' )
|
|
);
|
|
}
|
|
|
|
$model = $options['model'] ?? $this->model;
|
|
$temperature = $options['temperature'] ?? 0.7;
|
|
|
|
$body = array(
|
|
'model' => $model,
|
|
'messages' => $messages,
|
|
'temperature' => $temperature,
|
|
'stream' => true,
|
|
);
|
|
|
|
if ( isset( $options['max_tokens'] ) ) {
|
|
$body['max_tokens'] = $options['max_tokens'];
|
|
}
|
|
|
|
$accumulated_content = '';
|
|
$accumulated_usage = array();
|
|
$buffer = '';
|
|
|
|
$accumulating_callback = function( $chunk, $is_complete ) use ( &$accumulated_content, $callback ) {
|
|
if ( ! $is_complete && ! empty( $chunk ) ) {
|
|
$accumulated_content .= $chunk;
|
|
}
|
|
|
|
if ( $callback ) {
|
|
call_user_func( $callback, $chunk, $is_complete, $accumulated_content );
|
|
}
|
|
};
|
|
|
|
$ch = curl_init( $this->api_endpoint );
|
|
|
|
curl_setopt_array( $ch, array(
|
|
CURLOPT_POST => true,
|
|
CURLOPT_RETURNTRANSFER => false,
|
|
CURLOPT_WRITEFUNCTION => function( $curl, $data ) use ( &$buffer, $accumulating_callback, &$accumulated_usage ) {
|
|
$buffer .= $data;
|
|
|
|
while ( true ) {
|
|
$newline_pos = strpos( $buffer, "\n" );
|
|
if ( false === $newline_pos ) {
|
|
break;
|
|
}
|
|
|
|
$line = substr( $buffer, 0, $newline_pos );
|
|
$buffer = substr( $buffer, $newline_pos + 1 );
|
|
|
|
$line = trim( $line );
|
|
if ( empty( $line ) || 0 !== strpos( $line, 'data: ' ) ) {
|
|
continue;
|
|
}
|
|
|
|
$json_str = substr( $line, 6 );
|
|
|
|
if ( '[DONE]' === $json_str ) {
|
|
call_user_func( $accumulating_callback, '', true );
|
|
return strlen( $data );
|
|
}
|
|
|
|
$chunk = json_decode( $json_str, true );
|
|
if ( isset( $chunk['choices'][0]['delta']['content'] ) ) {
|
|
$content = $chunk['choices'][0]['delta']['content'];
|
|
call_user_func( $accumulating_callback, $content, false );
|
|
}
|
|
|
|
if ( isset( $chunk['usage'] ) ) {
|
|
$accumulated_usage = $chunk['usage'];
|
|
}
|
|
}
|
|
|
|
return strlen( $data );
|
|
},
|
|
CURLOPT_HTTPHEADER => array(
|
|
'Authorization: Bearer ' . $this->api_key,
|
|
'Content-Type: application/json',
|
|
),
|
|
CURLOPT_POSTFIELDS => wp_json_encode( $body ),
|
|
CURLOPT_TIMEOUT => 180,
|
|
) );
|
|
|
|
$start_time = microtime( true );
|
|
$result = curl_exec( $ch );
|
|
$http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
|
|
$curl_error = curl_error( $ch );
|
|
curl_close( $ch );
|
|
|
|
if ( false === $result && ! empty( $curl_error ) ) {
|
|
return new WP_Error( 'curl_error', 'cURL error: ' . $curl_error );
|
|
}
|
|
|
|
if ( $http_code >= 400 ) {
|
|
return new WP_Error( 'api_error', sprintf( 'API error (%d)', $http_code ) );
|
|
}
|
|
|
|
$cost = $this->calculate_cost( $model, $accumulated_usage );
|
|
|
|
return array(
|
|
'content' => $accumulated_content,
|
|
'model' => $model,
|
|
'input_tokens' => $accumulated_usage['prompt_tokens'] ?? 0,
|
|
'output_tokens' => $accumulated_usage['completion_tokens'] ?? 0,
|
|
'total_tokens' => $accumulated_usage['total_tokens'] ?? 0,
|
|
'cost' => $cost,
|
|
'generation_time' => microtime( true ) - $start_time,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Generate image (not supported by Codex)
|
|
*
|
|
* @param string $prompt Image prompt.
|
|
* @param string $model Model to use.
|
|
* @param array $options Optional parameters.
|
|
* @return WP_Error Error indicating not supported.
|
|
*/
|
|
public function generate_image( $prompt, $model = null, $options = array() ) {
|
|
return new WP_Error(
|
|
'not_supported',
|
|
__( 'Image generation not supported by Codex. Use OpenRouter for images.', 'wp-agentic-writer' )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if provider is configured
|
|
*
|
|
* @return bool True if API key is set.
|
|
*/
|
|
public function is_configured() {
|
|
return ! empty( $this->api_key );
|
|
}
|
|
|
|
/**
|
|
* Test connection to Codex API
|
|
*
|
|
* @return array|WP_Error Success array or error.
|
|
*/
|
|
public function test_connection() {
|
|
if ( ! $this->is_configured() ) {
|
|
return new WP_Error( 'not_configured', 'Codex API key not configured' );
|
|
}
|
|
|
|
$response = wp_remote_post(
|
|
$this->api_endpoint,
|
|
array(
|
|
'headers' => array(
|
|
'Authorization' => 'Bearer ' . $this->api_key,
|
|
'Content-Type' => 'application/json',
|
|
),
|
|
'body' => wp_json_encode(
|
|
array(
|
|
'model' => $this->model,
|
|
'messages' => array(
|
|
array(
|
|
'role' => 'user',
|
|
'content' => 'Reply with: Test successful',
|
|
),
|
|
),
|
|
'max_tokens' => 10,
|
|
)
|
|
),
|
|
'timeout' => 15,
|
|
)
|
|
);
|
|
|
|
if ( is_wp_error( $response ) ) {
|
|
return $response;
|
|
}
|
|
|
|
$code = wp_remote_retrieve_response_code( $response );
|
|
if ( 200 !== $code ) {
|
|
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
|
return new WP_Error(
|
|
'connection_failed',
|
|
$body['error']['message'] ?? sprintf( 'API returned status %d', $code )
|
|
);
|
|
}
|
|
|
|
return array(
|
|
'success' => true,
|
|
'message' => 'Connected to OpenAI Codex successfully',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if provider supports task type
|
|
*
|
|
* @param string $type Task type.
|
|
* @return bool True if supported (all text tasks).
|
|
*/
|
|
public function supports_task_type( $type ) {
|
|
return in_array(
|
|
$type,
|
|
array( 'chat', 'clarity', 'planning', 'writing', 'refinement' ),
|
|
true
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Calculate cost based on OpenAI pricing
|
|
*
|
|
* @param string $model Model identifier.
|
|
* @param array $usage Usage data with prompt_tokens and completion_tokens.
|
|
* @return float Cost in USD.
|
|
*/
|
|
private function calculate_cost( $model, $usage ) {
|
|
if ( empty( $usage['prompt_tokens'] ) && empty( $usage['completion_tokens'] ) ) {
|
|
return 0;
|
|
}
|
|
|
|
// OpenAI pricing (as of 2024, per 1M tokens)
|
|
$pricing = array(
|
|
'gpt-4o' => array( 'input' => 2.50, 'output' => 10.00 ),
|
|
'gpt-4o-mini' => array( 'input' => 0.15, 'output' => 0.60 ),
|
|
'gpt-4-turbo' => array( 'input' => 10.00, 'output' => 30.00 ),
|
|
'gpt-4' => array( 'input' => 30.00, 'output' => 60.00 ),
|
|
'gpt-3.5-turbo' => array( 'input' => 0.50, 'output' => 1.50 ),
|
|
);
|
|
|
|
$rates = $pricing[ $model ] ?? $pricing['gpt-4o'];
|
|
|
|
$input_cost = ( $usage['prompt_tokens'] ?? 0 ) * ( $rates['input'] / 1000000 );
|
|
$output_cost = ( $usage['completion_tokens'] ?? 0 ) * ( $rates['output'] / 1000000 );
|
|
|
|
return $input_cost + $output_cost;
|
|
}
|
|
}
|