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; } }