v0.3.0 — cost accountability + LLM call audit trail (#7)
Per-call recording of cost and I/O for every LLM probe. New tables prv_call_meta (schema v3) and prv_call_io. New admin pages: Costs (spend drill-down vs $5 cap) and Call Log (paginated + detail drawer). 90-day I/O retention prune via prv_daily_prune cron. P0 security: key never stored; gateway client exposes HTTP status only.
QA APPROVED 2026-06-15 SHA 09dc6db9ce
This commit is contained in:
@@ -33,6 +33,13 @@ declare(strict_types=1);
|
||||
*/
|
||||
class PRV_Gateway_Client {
|
||||
|
||||
/**
|
||||
* HTTP status code from the most recent request (set on permanent error).
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private int $last_http_status = 200;
|
||||
|
||||
/**
|
||||
* Base URL for the Cloudflare AI Gateway (without trailing slash).
|
||||
*/
|
||||
@@ -59,6 +66,9 @@ class PRV_Gateway_Client {
|
||||
* @return array<string, mixed> Decoded JSON response body.
|
||||
*
|
||||
* @throws \RuntimeException On permanent HTTP failure or parse error.
|
||||
* Exception message carries HTTP status code only;
|
||||
* the response body is never included (P0: prevents
|
||||
* secret-key leakage via 401 error responses).
|
||||
*/
|
||||
public function post_to_gateway( string $provider_slug, array $body, string $api_key, string $endpoint = '/chat/completions' ): array {
|
||||
$base_url = $this->build_base_url( $provider_slug );
|
||||
@@ -110,8 +120,9 @@ class PRV_Gateway_Client {
|
||||
}
|
||||
|
||||
if ( $status >= 400 ) {
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- exception message, not HTML output
|
||||
throw new \RuntimeException( sprintf( 'PR Vision gateway HTTP %d: %s', $status, substr( $raw_body, 0, 300 ) ) );
|
||||
$this->last_http_status = $status;
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- exception message, not HTML output; body intentionally omitted (P0: prevents key-leak via 401 body)
|
||||
throw new \RuntimeException( sprintf( 'PR Vision gateway HTTP %d', $status ) );
|
||||
}
|
||||
|
||||
$data = json_decode( $raw_body, true );
|
||||
@@ -129,6 +140,18 @@ class PRV_Gateway_Client {
|
||||
throw new \RuntimeException( sprintf( 'PR Vision: gateway failed after %d attempts. Last: %s', PRV_MAX_RETRIES, $last_error ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HTTP status code from the last failed request.
|
||||
*
|
||||
* Allows callers to record the HTTP status code in error records without
|
||||
* parsing the exception message. Returns 200 when no failure has occurred.
|
||||
*
|
||||
* @return int HTTP status code.
|
||||
*/
|
||||
public function get_last_http_status(): int {
|
||||
return $this->last_http_status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the API base URL (gateway or direct fallback).
|
||||
*
|
||||
|
||||
@@ -164,7 +164,11 @@ class PRV_OpenRouter_Provider implements PRV_Probe_Provider {
|
||||
$cost_usd = (float) $data['usage']['total_tokens'] * 0.000002;
|
||||
}
|
||||
|
||||
return new PRV_Probe_Result( $raw_excerpt, $domains, $cited, $our_position, $cost_usd );
|
||||
// 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 );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -133,7 +133,11 @@ class PRV_Perplexity_Provider implements PRV_Probe_Provider {
|
||||
$cost_usd = (float) $data['usage']['total_tokens'] * 0.000001;
|
||||
}
|
||||
|
||||
return new PRV_Probe_Result( $raw_excerpt, $domains, $cited, $our_position, $cost_usd );
|
||||
// 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 );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user