gateway = $gateway ?? new PRV_Gateway_Client(); $this->detector = $detector ?? new PRV_Citation_Detector(); } /** * {@inheritDoc} * * Sends the query as a user message to perplexity/sonar via OpenRouter. * Parses the citations[] array from the OpenRouter response envelope. * * @param string $query The search query to probe. * * @return PRV_Probe_Result * @throws \RuntimeException On permanent API failure. */ public function probe( string $query ): PRV_Probe_Result { $api_key = $this->resolve_api_key(); $body = array( 'model' => self::MODEL, 'messages' => array( array( 'role' => 'user', 'content' => $query, ), ), 'max_tokens' => 512, ); $data = $this->gateway->post_to_gateway( 'openrouter', $body, $api_key ); return $this->parse_response( $data ); } /** * Parse the OpenRouter/Perplexity response into a PRV_Probe_Result. * * Side effects: None. * * @param array $data Decoded API response. * * @return PRV_Probe_Result */ public function parse_response( array $data ): PRV_Probe_Result { // Extract text excerpt from the first choice. $content = ''; if ( isset( $data['choices'][0]['message']['content'] ) ) { $content = (string) $data['choices'][0]['message']['content']; } $raw_excerpt = mb_substr( $content, 0, 500 ); // Perplexity returns citations at the top level of the response envelope. $raw_citations = array(); if ( isset( $data['citations'] ) && is_array( $data['citations'] ) ) { $raw_citations = $data['citations']; } $domains = $this->detector->parse_domains( $raw_citations ); $cited = $this->detector->is_cited( $domains ); $our_position = $this->detector->get_our_position( $domains ); // Cost from usage data when available; fall back to estimate. $cost_usd = self::ESTIMATED_COST_PER_PROBE; if ( isset( $data['usage']['total_tokens'] ) ) { // Sonar: ~$1 per 1M input, $1 per 1M output tokens. Avg ~$0.001/1k. $cost_usd = (float) $data['usage']['total_tokens'] * 0.000001; } return new PRV_Probe_Result( $raw_excerpt, $domains, $cited, $our_position, $cost_usd ); } /** * {@inheritDoc} */ public function get_name(): string { return 'Perplexity sonar'; } /** * {@inheritDoc} */ public function is_configured(): bool { return defined( 'PRV_OPENROUTER_API_KEY' ) && '' !== PRV_OPENROUTER_API_KEY; } /** * Retrieve the OpenRouter API key from wp-config constants. * * @return string The API key. * @throws \RuntimeException When the constant is not defined or empty. */ private function resolve_api_key(): string { if ( ! defined( 'PRV_OPENROUTER_API_KEY' ) || '' === PRV_OPENROUTER_API_KEY ) { // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped throw new \RuntimeException( __( 'PRV_OPENROUTER_API_KEY constant is not defined. Add it to wp-config.php.', 'pr-vision' ) ); } return PRV_OPENROUTER_API_KEY; } }