gateway = $gateway ?? new PRV_Gateway_Client(); $this->detector = $detector ?? new PRV_Citation_Detector(); } /** * {@inheritDoc} * * Resolves the key via PRV_Key_Store (constant → option → none). * * @param string $query The search query to probe. * * @return PRV_Probe_Result * @throws \RuntimeException On permanent API failure or no key configured. */ public function probe( string $query ): PRV_Probe_Result { $api_key = PRV_Key_Store::get_key(); if ( '' === $api_key ) { // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- exception, not HTML throw new \RuntimeException( 'PRV_OPENROUTER_API_KEY is not configured. Set it in wp-config.php or via Settings.' ); } $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'] ) ) { $cost_usd = (float) $data['usage']['total_tokens'] * 0.000001; } // Token counts for per-call attribution. $tokens_in = isset( $data['usage']['prompt_tokens'] ) ? (int) $data['usage']['prompt_tokens'] : null; $tokens_out = isset( $data['usage']['completion_tokens'] ) ? (int) $data['usage']['completion_tokens'] : null; return new PRV_Probe_Result( $raw_excerpt, $domains, $cited, $our_position, $cost_usd, $tokens_in, $tokens_out ); } /** * {@inheritDoc} */ public function get_name(): string { return 'Perplexity sonar'; } /** * {@inheritDoc} * * True when any source (constant or admin option) provides a non-empty key. */ public function is_configured(): bool { return '' !== PRV_Key_Store::get_key(); } }