Files
pr-vision/includes/providers/class-prv-openrouter-provider.php
peptiderepo 68ae5b9b1a PHPCS compliance: fix pre-existing violations exposed by gating the lint step
All violations were present in v0.1.0 and hidden by continue-on-error.
Now that PHPCS gates the build (P2-B), they must be fixed.

- Add file-level @package doc comment to all PHP files (file doc requirement)
- Add short descriptions to @var and constructor docblocks throughout
- Fix @param type spacing throughout (PHPCS alignment rules)
- Expand phpcs:ignore lines to include NoCaching where DirectQuery is already
  suppressed (uninstall.php, cost-ledger, table-manager, collector)
- Suppress EscapeOutput.ExceptionNotEscaped on RuntimeException messages
  (exception strings are not HTML output)
- Suppress error_log_error_log on the probe-runner failure log
- Replace date() with gmdate() in panel (timezone-aware per WP standard)
- Convert inline associative arrays to multi-line in config (WP array style)
- Fix double-arrow alignment in probe-runner counts and insert arrays
- Fix double-space before ?? in provider constructors
- Expand phpcs:ignore for  reads to include MissingUnslash
- Fix $counts++ to ++$counts (prefix increment per WP standard)
- Fix V1/v1 capitalisation in interface doc long description

Agent-Session: pr-vision rename v0.1.1
2026-06-13 09:42:55 +00:00

174 lines
5.2 KiB
PHP

<?php
/** @package PrVision */
declare(strict_types=1);
/**
* Generic OpenRouter provider — parameterised for GPT-search and Gemini-search.
*
* One class handles any OpenRouter-routed model. The model identifier is set
* at construction time; the runner instantiates one provider per model from
* PRV_Config::get_models().
*
* GPT-search (openai/gpt-4o-search-preview) and Gemini-search
* (google/gemini-2.0-flash-001) do not return a structured citations array
* via OpenRouter — citations are parsed from URLs embedded in the response
* text or from any 'annotations' key the model may include.
*
* Required WP-config constant:
* PRV_OPENROUTER_API_KEY — OpenRouter key (sk-or-…)
*
* Who triggers: PRV_Probe_Runner via PRV_Probe_Provider interface.
* Dependencies: PRV_Gateway_Client, PRV_Citation_Detector, PRV_Probe_Result.
*
* @see interface-prv-probe-provider.php — Interface this implements.
* @see class-prv-gateway-client.php — Shared HTTP + retry logic.
* @see class-prv-citation-detector.php — Domain extraction + detection.
* @see ARCHITECTURE.md — §Provider implementations.
* @package PrVision
*/
class PRV_OpenRouter_Provider implements PRV_Probe_Provider {
/**
* Estimated cost per probe call in USD (conservative cross-model default).
*/
const ESTIMATED_COST_PER_PROBE = 0.003;
/**
* OpenRouter model identifier (e.g. "openai/gpt-4o-search-preview").
*
* @var string
*/
private string $model;
/**
* HTTP gateway client.
*
* @var PRV_Gateway_Client
*/
private PRV_Gateway_Client $gateway;
/**
* Citation domain detector.
*
* @var PRV_Citation_Detector
*/
private PRV_Citation_Detector $detector;
/**
* Constructor.
*
* @param string $model OpenRouter model identifier.
* @param PRV_Gateway_Client|null $gateway Injected for testing.
* @param PRV_Citation_Detector|null $detector Injected for testing.
*/
public function __construct( string $model, ?PRV_Gateway_Client $gateway = null, ?PRV_Citation_Detector $detector = null ) {
$this->model = $model;
$this->gateway = $gateway ?? new PRV_Gateway_Client();
$this->detector = $detector ?? new PRV_Citation_Detector();
}
/**
* {@inheritDoc}
*
* @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' => $this->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 response into a PRV_Probe_Result.
*
* Attempts to extract citations from:
* 1. `annotations` array (some models include structured source references).
* 2. URLs found inline in the response text (regex fallback).
*
* @param array<string, mixed> $data Decoded API response.
*
* @return PRV_Probe_Result
*/
public function parse_response( array $data ): PRV_Probe_Result {
$content = '';
if ( isset( $data['choices'][0]['message']['content'] ) ) {
$content = (string) $data['choices'][0]['message']['content'];
}
$raw_excerpt = mb_substr( $content, 0, 500 );
// 1. Prefer structured annotations when present.
$raw_citations = array();
if ( isset( $data['choices'][0]['message']['annotations'] ) && is_array( $data['choices'][0]['message']['annotations'] ) ) {
foreach ( $data['choices'][0]['message']['annotations'] as $ann ) {
if ( isset( $ann['url_citation']['url'] ) ) {
$raw_citations[] = $ann['url_citation']['url'];
}
}
}
// 2. Regex fallback: extract URLs from response text.
if ( empty( $raw_citations ) && '' !== $content ) {
preg_match_all( '#https?://[^\s\)\]\"\'<>]+#i', $content, $matches );
$raw_citations = $matches[0] ?? array();
}
$domains = $this->detector->parse_domains( $raw_citations );
$cited = $this->detector->is_cited( $domains );
$our_position = $this->detector->get_our_position( $domains );
// Cost estimation from usage data.
$cost_usd = self::ESTIMATED_COST_PER_PROBE;
if ( isset( $data['usage']['total_tokens'] ) ) {
// Rough blended rate ~$2/1M tokens across GPT-4o and Gemini Flash.
$cost_usd = (float) $data['usage']['total_tokens'] * 0.000002;
}
return new PRV_Probe_Result( $raw_excerpt, $domains, $cited, $our_position, $cost_usd );
}
/**
* {@inheritDoc}
*/
public function get_name(): string {
return 'OpenRouter/' . $this->model;
}
/**
* {@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
* @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 ) {
throw new \RuntimeException(
__( 'PRV_OPENROUTER_API_KEY constant is not defined. Add it to wp-config.php.', 'pr-vision' )
);
}
return PRV_OPENROUTER_API_KEY;
}
}