'user', 'content' => $prompt, ), ); return self::call_messages( $api_key, $model, $messages, 300, 0.3 ); } /** * Call the OpenRouter API with structured message arrays and optional response_format (JSON schema). * * @param string $api_key The API key. * @param string $model Model ID. * @param array $messages Array of message arrays ('role', 'content'). * @param int $max_tokens Max completion tokens. * @param float $temperature Sampling temperature. * @param array $response_format Optional JSON schema format array. * @return array{content: string|\WP_Error, usage: array, request_id: string, cost: float} */ public static function call_messages( string $api_key, string $model, array $messages, int $max_tokens = 800, float $temperature = 0.3, array $response_format = array() ): array { $result = array( 'content' => '', 'usage' => array(), 'request_id' => '', 'cost' => 0.0, ); $body = array( 'model' => $model, 'messages' => $messages, 'max_tokens' => $max_tokens, 'temperature' => $temperature, ); if ( ! empty( $response_format ) ) { $body['response_format'] = $response_format; } $retries = 0; $backoff = 2; // Initial backoff in seconds. while ( $retries <= self::MAX_RETRIES ) { $response = wp_remote_post( self::get_api_url(), array( 'timeout' => 20, 'headers' => array( 'Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json', 'HTTP-Referer' => home_url(), 'X-Title' => 'Peptide News Aggregator', ), 'body' => wp_json_encode( $body ), ) ); if ( is_wp_error( $response ) ) { $result['content'] = $response; return $result; } $status = wp_remote_retrieve_response_code( $response ); // Handle rate limiting with exponential backoff. if ( 429 === $status && $retries < self::MAX_RETRIES ) { $retry_after = wp_remote_retrieve_header( $response, 'retry-after' ); $wait = $retry_after ? min( (int) $retry_after, 30 ) : $backoff; sleep( $wait ); ++$retries; $backoff *= 2; continue; } break; } $data = json_decode( (string) wp_remote_retrieve_body( $response ), true ); // Extract usage data from response regardless of success/failure. if ( is_array( $data ) && isset( $data['usage'] ) && is_array( $data['usage'] ) ) { $result['usage'] = array( 'prompt_tokens' => (int) ( $data['usage']['prompt_tokens'] ?? 0 ), 'completion_tokens' => (int) ( $data['usage']['completion_tokens'] ?? 0 ), 'total_tokens' => (int) ( $data['usage']['total_tokens'] ?? 0 ), ); } // Extract the request ID for debugging/reconciliation. if ( is_array( $data ) && isset( $data['id'] ) ) { $result['request_id'] = (string) $data['id']; } // OpenRouter may include generation cost in data.usage.total_cost. if ( is_array( $data ) && isset( $data['usage']['total_cost'] ) ) { $result['cost'] = (float) $data['usage']['total_cost']; } if ( $status < 200 || $status >= 300 ) { $error_msg = 'HTTP ' . $status; if ( is_array( $data ) && isset( $data['error'] ) && is_array( $data['error'] ) && isset( $data['error']['message'] ) ) { $error_msg = $data['error']['message']; } $result['content'] = new \WP_Error( 'openrouter_error', $error_msg ); return $result; } // Validate the response structure thoroughly. if ( ! is_array( $data ) ) { $result['content'] = new \WP_Error( 'openrouter_invalid', 'Invalid JSON response from OpenRouter' ); return $result; } if ( ! isset( $data['choices'] ) || ! is_array( $data['choices'] ) || empty( $data['choices'] ) ) { $result['content'] = new \WP_Error( 'openrouter_empty', 'No choices in OpenRouter response' ); return $result; } if ( ! isset( $data['choices'][0]['message'] ) || ! is_array( $data['choices'][0]['message'] ) ) { $result['content'] = new \WP_Error( 'openrouter_empty', 'Malformed choice in OpenRouter response' ); return $result; } if ( ! isset( $data['choices'][0]['message']['content'] ) || '' === trim( $data['choices'][0]['message']['content'] ) ) { $result['content'] = new \WP_Error( 'openrouter_empty', 'Empty content in OpenRouter response' ); return $result; } $result['content'] = trim( $data['choices'][0]['message']['content'] ); return $result; } }