inner = $inner; $this->modelId = $modelId; $this->fallbackNeurons = $fallbackNeurons; $this->neuronsPer1kTokens = $neuronsPer1kTokens; } public function send(Request $request, ?RequestOptions $options = null): Response { $task = TaskContext::current(); $fingerprint = self::fingerprint($request); $repeat = $fingerprint !== null && $this->isRepeat($fingerprint); $cacheKey = $this->cacheKey($request); if ($cacheKey !== null && !$repeat) { $cached = get_transient($cacheKey); if (is_array($cached) && isset($cached['status'], $cached['body'])) { do_action('aipcf_cache_hit', $this->modelId, $task); Receipts::record($this->modelId, 0.0, 0, 0, 0.0, $task, true); if ($fingerprint !== null) { $this->markServed($fingerprint); // a second identical call is a regenerate } return new Response( (int) $cached['status'], ['content-type' => ['application/json'], 'x-aipcf-cache' => ['hit']], (string) $cached['body'] ); } } $viaGateway = Gateway::isGatewayUrl($request->getUri()); if ($repeat && $viaGateway) { // A regenerate must not be answered from the AI Gateway edge cache either. $request = $request->withHeader('cf-aig-skip-cache', 'true'); } $started = microtime(true); try { $response = $this->inner->send($request, $options); } catch (NetworkException $e) { // A timeout means the request may have reached the model and been billed; // re-sending would double the cost and the wait, and says nothing about the gateway. if (!$viaGateway || self::isTimeout($e)) { throw $e; } // DNS / TLS / URL-validation failure reaching the gateway host: bypass it. /* translators: %s: error message */ Gateway::markDown(sprintf(__('Network error: %s', 'aipcf-ai-provider-for-cloudflare'), $e->getMessage())); $response = $this->inner->send($this->directRetry($request), $options); $viaGateway = false; } if ($viaGateway && Gateway::isGatewayFailure($response->getStatusCode(), $response->getData())) { Gateway::markDown($this->describeFailure($response)); $response = $this->inner->send($this->directRetry($request), $options); } if ($response->isSuccessful()) { $data = $response->getData(); $sent = $this->sentModelId($request, $data); Gateway::clearLicenceGate($sent); $edgeHit = strtoupper((string) $response->getHeaderAsString('cf-aig-cache-status')) === 'HIT'; $usage = []; if (is_array($data)) { $usage = isset($data['usage']) && is_array($data['usage']) ? $data['usage'] : (isset($data['result']['usage']) && is_array($data['result']['usage']) ? $data['result']['usage'] : []); } $neurons = $edgeHit ? 0.0 : (float) ($usage['neurons'] ?? 0); $in = (int) ($usage['prompt_tokens'] ?? $usage['input_tokens'] ?? 0); $out = (int) ($usage['completion_tokens'] ?? $usage['output_tokens'] ?? 0); if (!$edgeHit && $neurons <= 0) { // Billed, but the response reports no neurons (raw image bytes, IndicTrans2): // count an estimate so the budget guard sees it. if ($this->neuronsPer1kTokens > 0 && $in + $out > 0) { $neurons = round(($in + $out) / 1000 * $this->neuronsPer1kTokens, 3); } elseif ($this->fallbackNeurons > 0) { $neurons = $this->fallbackNeurons; } } if ($edgeHit) { // Served from the AI Gateway cache: the body replays the original // usage figures but Cloudflare bills nothing. Count the request, not the neurons. UsageTracker::record($sent, 0.0, 0, 0, $task !== '' ? $task : 'direct'); do_action('aipcf_cache_hit', $sent, $task); } else { UsageTracker::record($sent, $neurons, $in, $out, $task); } Receipts::record($sent, $neurons, $in, $out, microtime(true) - $started, $task, $edgeHit); if ($fingerprint !== null) { $this->markServed($fingerprint); } if ($cacheKey !== null && is_array($data) && $response->getBody() !== null) { set_transient( $cacheKey, ['status' => $response->getStatusCode(), 'body' => $response->getBody()], $this->cacheTtl() ); } } return $response; } private static function isTimeout(NetworkException $e): bool { return (bool) preg_match('/timed? ?out|cURL error 28/i', $e->getMessage()); } private function directRetry(Request $request): Request { return new Request( $request->getMethod(), Gateway::directUrl($request->getUri()), Gateway::stripHeaders($request->getHeaders()), $request->getData() ?? $request->getBody(), $request->getOptions() ); } private function sentModelId(Request $request, ?array $responseData): string { $candidates = [ is_array($responseData) && isset($responseData['model']) ? (string) $responseData['model'] : '', is_array($request->getData()) && isset($request->getData()['model']) ? (string) $request->getData()['model'] : '', ]; foreach ($candidates as $c) { if ($c === '' || strpos($c, 'dynamic/') === 0) { continue; } return strpos($c, 'workers-ai/') === 0 ? substr($c, strlen('workers-ai/')) : $c; } return $this->modelId; } private function isRepeat(string $fingerprint): bool { $window = self::regenerateWindow(); if ($window <= 0 || get_current_user_id() === 0) { return false; } return get_transient(self::servedKey($fingerprint)) !== false; } private static function fingerprint(Request $request): ?string { if (!self::isDeterministic($request->getData())) { return null; } $body = $request->getBody() ?? wp_json_encode($request->getData()); return md5(Gateway::directUrl($request->getUri()) . '|' . (string) $body); } private function markServed(string $fingerprint): void { $window = self::regenerateWindow(); if ($window <= 0 || get_current_user_id() === 0) { return; } set_transient(self::servedKey($fingerprint), 1, $window); } private static function servedKey(string $fingerprint): string { return transient_key('aipcf_rs_', get_current_user_id() . '_' . $fingerprint); } public static function regenerateWindow(): int { return max(0, (int) apply_filters('aipcf_regenerate_window', 10 * MINUTE_IN_SECONDS)); } private function cacheTtl(): int { $ttl = (int) (get_plugin_settings()['local_cache_ttl'] ?? 0); return max(0, (int) apply_filters('aipcf_local_cache_ttl', $ttl, $this->modelId)); } private function cacheKey(Request $request): ?string { if ($this->cacheTtl() <= 0) { return null; } if (!self::isDeterministic($request->getData())) { return null; } $body = $request->getBody() ?? wp_json_encode($request->getData()); return transient_key('aipcf_rc_', md5($request->getUri() . '|' . (string) $body)); } public static function isDeterministic(?array $data): bool { if ($data === null) { return false; } if (isset($data['temperature']) && (float) $data['temperature'] > 0) { return false; } if (isset($data['seed'])) { return true; } // Image models are stochastic without a seed. if (isset($data['prompt']) && !isset($data['messages'])) { return false; } return true; } private function describeFailure(Response $response): string { $status = $response->getStatusCode(); $data = $response->getData(); $msg = ''; if (is_array($data)) { if (isset($data['error'][0]['message'])) { $msg = (string) $data['error'][0]['message'] . (($data['name'] ?? '') !== '' ? ' (' . $data['name'] . ')' : ''); } elseif (isset($data['errors'][0]['message'])) { $msg = (string) $data['errors'][0]['message']; } elseif (isset($data['error']['message'])) { $msg = (string) $data['error']['message']; } elseif (isset($data['error']) && is_string($data['error'])) { $msg = $data['error']; } } if ($msg === '') { $msg = $status === 404 ? __('the gateway was not found (check the gateway name)', 'aipcf-ai-provider-for-cloudflare') : __('the gateway refused the request (check its auth token)', 'aipcf-ai-provider-for-cloudflare'); } return sprintf('HTTP %d: %s', $status, $msg); } }