0 && $cacheable) { $headers['cf-aig-cache-ttl'] = (string) max(60, $ttl); } else { $headers['cf-aig-skip-cache'] = 'true'; } if (empty($settings['gateway_log_payload'])) { $headers['cf-aig-collect-log-payload'] = 'false'; } $metadata = [ 'site' => (string) wp_parse_url(home_url(), PHP_URL_HOST), 'feature' => $task !== '' ? $task : 'direct', 'model' => $modelId, 'user' => (string) get_current_user_id(), 'plugin' => 'aipcf/' . AIPCF_VERSION, ]; $metadata = (array) apply_filters('aipcf_gateway_metadata', $metadata, $task); $metadata = array_slice($metadata, 0, 5, true); $encoded = wp_json_encode($metadata); if (is_string($encoded)) { $headers['cf-aig-metadata'] = $encoded; } return (array) apply_filters('aipcf_gateway_headers', $headers, $task, $modelId); } public static function stripHeaders(array $headers): array { foreach (array_keys($headers) as $name) { if (stripos((string) $name, 'cf-aig-') === 0) { unset($headers[$name]); } } return $headers; } public static function markDown(string $reason): void { set_transient(self::DOWN_TRANSIENT, 1, HOUR_IN_SECONDS); update_option(self::LAST_ERROR_OPTION, [ 'time' => time(), 'reason' => $reason, ], false); do_action('aipcf_gateway_marked_down', $reason); } public static function markUp(): void { delete_transient(self::DOWN_TRANSIENT); delete_option(self::LAST_ERROR_OPTION); } public static function dashboardUrl(): string { return sprintf( 'https://dash.cloudflare.com/%s/ai/ai-gateway/gateways/%s', rawurlencode(get_account_id()), rawurlencode(self::id()) ); } public const LICENCE_OPTION = 'aipcf_licence_gate'; public static function isLicenceGate(?array $body): bool { if (!is_array($body) || !isset($body['errors'][0])) { return false; } $e = $body['errors'][0]; return (int) ($e['code'] ?? 0) === 5016 || stripos((string) ($e['message'] ?? ''), 'Model Agreement') !== false; } public static function recordLicenceGate(string $modelId, string $message): void { $gates = get_option(self::LICENCE_OPTION, []); $gates = is_array($gates) ? $gates : []; $gates[$modelId] = ['time' => time(), 'message' => $message]; update_option(self::LICENCE_OPTION, $gates, false); } public static function clearLicenceGate(string $modelId): void { $gates = get_option(self::LICENCE_OPTION, []); if (is_array($gates) && isset($gates[$modelId])) { unset($gates[$modelId]); $gates === [] ? delete_option(self::LICENCE_OPTION) : update_option(self::LICENCE_OPTION, $gates, false); } } /** * Who owns a gated model's licence, from the model id (@cf//...). * The licence is the publisher's; Cloudflare only enforces it. */ public static function licenceOwner(string $modelId): string { $owners = ['meta' => 'Meta', 'meta-llama' => 'Meta', 'mistral' => 'Mistral AI', 'mistralai' => 'Mistral AI', 'google' => 'Google']; $parts = explode('/', $modelId); $vendor = strtolower($parts[1] ?? ''); return $owners[$vendor] ?? __('the model\'s publisher', 'aipcf-ai-provider-for-cloudflare'); } /** * Meta's Llama 3.2 licence withholds its multimodal (vision) models from EU residents. */ public static function licenceExcludesEu(string $modelId): bool { return self::licenceOwner($modelId) === 'Meta' && stripos($modelId, 'vision') !== false; } public static function licenceInstructions(string $modelId): string { return sprintf( /* translators: 1: model id, 2: license owner, e.g. Meta, 3: settings URL */ __('Cloudflare will not run %1$s on your account until %2$s\'s license for it has been accepted once. This is %2$s\'s license, not this plugin\'s. An administrator can accept it in the Cloudflare AI settings (%3$s).', 'aipcf-ai-provider-for-cloudflare'), $modelId, self::licenceOwner($modelId), admin_url('admin.php?page=cloudflare-workers-ai-provider') ); } public static function modelPageUrl(string $modelId): string { $slug = substr((string) strrchr($modelId, '/'), 1); return 'https://developers.cloudflare.com/workers-ai/models/' . rawurlencode($slug !== '' ? $slug : $modelId) . '/'; } /** * Sends the admin's acceptance of the model publisher's licence (e.g. Meta's * Llama licence) to Cloudflare as the one-time "agree" prompt, on the admin's * explicit request. Cloudflare records the acceptance on the account; the * plugin only relays it. Only models already recorded as gated are accepted. * * @return true|string True when accepted, else the reason it failed. */ public static function acceptLicence(string $modelId) { $gates = get_option(self::LICENCE_OPTION, []); if (!is_array($gates) || !isset($gates[$modelId]) || !preg_match('#^@cf/[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$#', $modelId)) { return __('This model is not waiting for a license acceptance.', 'aipcf-ai-provider-for-cloudflare'); } $account = get_account_id(); $token = get_api_key(); if ($account === '' || $token === '') { return __('The Cloudflare credentials are missing.', 'aipcf-ai-provider-for-cloudflare'); } $response = wp_remote_post( sprintf('https://api.cloudflare.com/client/v4/accounts/%s/ai/run/%s', rawurlencode($account), $modelId), [ 'timeout' => 30, 'headers' => ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'], 'body' => (string) wp_json_encode(['prompt' => 'agree']), ] ); if (is_wp_error($response)) { return $response->get_error_message(); } $status = (int) wp_remote_retrieve_response_code($response); $body = json_decode((string) wp_remote_retrieve_body($response), true); if ($status < 200 || $status >= 300 || self::isLicenceGate(is_array($body) ? $body : null)) { $message = is_array($body) && isset($body['errors'][0]['message']) && is_scalar($body['errors'][0]['message']) ? (string) $body['errors'][0]['message'] : sprintf('HTTP %d', $status); return $message; } self::clearLicenceGate($modelId); /** * Fires after Cloudflare confirmed the admin's acceptance of a model publisher's * licence (e.g. Meta's Llama licence). The licence is the publisher's, not AIPCF's. * * @param string $modelId Model id, e.g. @cf/meta/llama-3.2-11b-vision-instruct. * @param string $owner Licence owner, e.g. "Meta". */ do_action('aipcf_model_licence_accepted', $modelId, self::licenceOwner($modelId)); return true; } public static function isGatewayFailureStatus(int $status): bool { return in_array($status, [401, 403, 404], true); } public static function isGatewayFailure(int $status, ?array $body): bool { if (!self::isGatewayFailureStatus($status)) { return false; } if (is_array($body)) { if (($body['name'] ?? '') === 'AiGatewayError') { return true; } if (isset($body['errors']) && is_array($body['errors']) && $body['errors'] !== []) { return false; // Workers AI / API error passed through. } } // 404 or an unrecognised body on a gateway URL: assume the gateway is the problem. return $status === 404 || $body === null; } }