'', 'account_id' => '', 'preferred_model' => '', // 1.2.0 - gateway + budget 'gateway_id' => '', 'gateway_token' => '', 'gateway_cache_ttl' => 0, 'gateway_log_payload' => 1, 'dynamic_route' => '', 'budget_enabled' => 0, 'budget_neurons' => 10000, 'budget_action' => 'stop', 'budget_email' => 1, 'cloudflare_only' => 0, // 1.3.0 - routing + cache 'light_model' => '', 'light_tasks' => TaskContext::DEFAULT_LIGHT_TASKS, 'local_cache_ttl' => 0, // 1.4.0 - images + backfill 'image_model' => '@cf/black-forest-labs/flux-1-schnell', 'vision_model' => '', 'backfill_enabled' => 0, 'backfill_tasks' => ['meta_description'], 'backfill_daily_neurons' => 3000, 'backfill_batch' => 10, ] + EmbeddingSettings::defaults() // 1.9.0 - embeddings, off until the site owner turns them on + FeatureSettings::defaults(); // 1.9.0 - related posts, link assistant, member search, search insights; all off } function get_api_key(): string { $settings = get_plugin_settings(); if (!empty($settings['api_key'])) { return (string) $settings['api_key']; } if (defined('CLOUDFLARE_WORKERS_AI_API_TOKEN') && is_string(\CLOUDFLARE_WORKERS_AI_API_TOKEN)) { return \CLOUDFLARE_WORKERS_AI_API_TOKEN; } $envKey = getenv('CLOUDFLARE_WORKERS_AI_API_TOKEN'); if (is_string($envKey) && $envKey !== '') { return $envKey; } // Fall back to the AI Client registry so keys set via the WordPress // Connectors screen are honoured without reading the option directly. if (class_exists(AiClient::class)) { try { $auth = AiClient::defaultRegistry() ->getProviderRequestAuthentication(CloudflareWorkersAiProvider::class); if ( $auth instanceof ApiKeyRequestAuthentication && method_exists($auth, 'getApiKey') ) { $key = (string) $auth->getApiKey(); if ($key !== '') { return $key; } } } catch (\Throwable $e) { // Registry unavailable or method absent - return empty. } } return ''; } function get_account_id(): string { $settings = get_plugin_settings(); if (!empty($settings['account_id'])) { return (string) $settings['account_id']; } if (defined('CLOUDFLARE_WORKERS_AI_ACCOUNT_ID') && is_string(\CLOUDFLARE_WORKERS_AI_ACCOUNT_ID)) { return \CLOUDFLARE_WORKERS_AI_ACCOUNT_ID; } $envId = getenv('CLOUDFLARE_WORKERS_AI_ACCOUNT_ID'); if (is_string($envId) && $envId !== '') { return $envId; } return ''; } function get_default_model_id(): string { $settings = get_plugin_settings(); if (!empty($settings['preferred_model'])) { return (string) $settings['preferred_model']; } if (defined('CLOUDFLARE_WORKERS_AI_DEFAULT_MODEL') && is_string(\CLOUDFLARE_WORKERS_AI_DEFAULT_MODEL)) { return \CLOUDFLARE_WORKERS_AI_DEFAULT_MODEL; } $envModel = getenv('CLOUDFLARE_WORKERS_AI_DEFAULT_MODEL'); if (is_string($envModel) && $envModel !== '') { return $envModel; } return '@cf/meta/llama-4-scout-17b-16e-instruct'; } /** * Key for a per-request transient (response cache, regenerate marker, receipt). * The keys carry a generation that Reset deletes: with an external object cache * these transients cannot be listed, so rotating the generation is what makes * every old one unreachable. They then simply expire. */ function transient_key(string $prefix, string $rest): string { $gen = get_option(AIPCF_CACHE_GEN_OPTION); if (!is_string($gen) || $gen === '') { $gen = strtolower(wp_generate_password(8, false)); add_option(AIPCF_CACHE_GEN_OPTION, $gen, '', true); $stored = get_option(AIPCF_CACHE_GEN_OPTION); $gen = is_string($stored) && $stored !== '' ? $stored : $gen; } return $prefix . $gen . '_' . $rest; } function fetch_available_models(): array { $text = []; foreach (fetch_catalog() as $m) { if (($m['task'] ?? 'Text Generation') === 'Text Generation') { $text[] = $m; } } return $text; } function fetch_catalog(): array { $cached = get_transient(AIPCF_MODELS_TRANSIENT); if (is_array($cached)) { /** @var list $cached */ return $cached; } $accountId = get_account_id(); $apiKey = get_api_key(); if ($accountId === '' || $apiKey === '') { return []; } // A failed fetch is remembered briefly so a bad token or an API outage does not // add a blocking 15-second call to every AI request and admin page. if (get_transient(AIPCF_MODELS_TRANSIENT . '_failed')) { return []; } $failed = static function (): array { set_transient(AIPCF_MODELS_TRANSIENT . '_failed', 1, 5 * MINUTE_IN_SECONDS); return []; }; $url = sprintf( 'https://api.cloudflare.com/client/v4/accounts/%s/ai/models/search?per_page=100', rawurlencode($accountId) ); $response = wp_remote_get( $url, [ 'timeout' => 15, 'headers' => [ 'Authorization' => 'Bearer ' . $apiKey, 'Accept' => 'application/json', ], ] ); if (is_wp_error($response)) { return $failed(); } $status = (int) wp_remote_retrieve_response_code($response); if ($status < 200 || $status >= 300) { return $failed(); } $body = json_decode((string) wp_remote_retrieve_body($response), true); if (!is_array($body) || !isset($body['result']) || !is_array($body['result'])) { return $failed(); } $models = []; foreach ($body['result'] as $item) { if (!is_array($item) || empty($item['name']) || !is_string($item['name'])) { continue; } // The API returns a UUID in `id` and the @cf/... path identifier in `name`. $modelId = $item['name']; $taskName = isset($item['task']['name']) && is_string($item['task']['name']) ? $item['task']['name'] : ''; if (!in_array($taskName, ['Text Generation', 'Text-to-Image', 'Translation'], true)) { continue; } // Image models needing multipart bodies or an input image are not supported. if ($taskName === 'Text-to-Image' && preg_match('#flux-2|inpainting|img2img#i', $modelId)) { continue; } $props = []; if (isset($item['properties']) && is_array($item['properties'])) { foreach ($item['properties'] as $prop) { if (is_array($prop) && isset($prop['property_id']) && is_string($prop['property_id'])) { $props[$prop['property_id']] = $prop['value'] ?? null; } } } $isTrue = static function ($v): bool { return $v === true || $v === 'true' || $v === 1 || $v === '1'; }; // Adapters / classifiers cannot answer a plain chat request. if ($isTrue($props['lora'] ?? null) && substr($modelId, -5) === '-lora') { continue; } if ($taskName === 'Text Generation' && ($isTrue($props['beta'] ?? null) || strpos($modelId, 'llama-guard') !== false)) { continue; } $priceIn = 0.0; $priceOut = 0.0; if (isset($props['price']) && is_array($props['price'])) { foreach ($props['price'] as $tier) { if (!is_array($tier) || !isset($tier['unit'], $tier['price'])) { continue; } if ($tier['unit'] === 'per M input tokens') { $priceIn = (float) $tier['price']; } elseif ($tier['unit'] === 'per M output tokens') { $priceOut = (float) $tier['price']; } } } $description = isset($item['description']) && is_string($item['description']) ? $item['description'] : ''; $vision = strpos($modelId, 'vision') !== false || stripos($description, 'image') !== false && stripos($description, 'vision') !== false; $models[] = [ 'id' => $modelId, 'name' => pretty_model_name($modelId), 'task' => $taskName, 'context_window' => (int) ($props['context_window'] ?? 0), 'function_calling' => $isTrue($props['function_calling'] ?? null), 'vision' => $vision, 'price_in' => $priceIn, 'price_out' => $priceOut, ]; } usort($models, static function (array $a, array $b): int { return strcmp($a['id'], $b['id']); }); $models = (array) apply_filters('aipcf_available_models', $models); if (!empty($models)) { set_transient(AIPCF_MODELS_TRANSIENT, $models, 12 * HOUR_IN_SECONDS); set_transient(AIPCF_MODELS_TRANSIENT . '_time', time(), 12 * HOUR_IN_SECONDS); } else { $failed(); } return array_values($models); } function catalog_fetched_at(): int { return (int) get_transient(AIPCF_MODELS_TRANSIENT . '_time'); } function static_model_catalog(): array { $rows = [ // id, context, tools, vision, $in, $out ['@cf/meta/llama-4-scout-17b-16e-instruct', 131000, true, false, 0.27, 0.85], ['@cf/meta/llama-3.3-70b-instruct-fp8-fast', 24000, true, false, 0.293, 2.253], ['@cf/meta/llama-3.1-8b-instruct-fp8', 32000, false, false, 0.152, 0.287], ['@cf/meta/llama-3.2-3b-instruct', 80000, false, false, 0.051, 0.335], ['@cf/meta/llama-3.2-1b-instruct', 60000, false, false, 0.027, 0.201], ['@cf/meta/llama-3.2-11b-vision-instruct', 128000, false, true, 0.049, 0.676], ['@cf/openai/gpt-oss-120b', 128000, true, false, 0.35, 0.75], ['@cf/openai/gpt-oss-20b', 128000, true, false, 0.20, 0.30], ['@cf/google/gemma-4-26b-a4b-it', 256000, true, false, 0.10, 0.30], ['@cf/mistralai/mistral-small-3.1-24b-instruct', 128000, true, false, 0.351, 0.555], ['@cf/qwen/qwen3-30b-a3b-fp8', 32768, true, false, 0.051, 0.335], ['@cf/qwen/qwen2.5-coder-32b-instruct', 32768, false, false, 0.66, 1.0], ['@cf/qwen/qwq-32b', 24000, false, false, 0.66, 1.0], ['@cf/deepseek-ai/deepseek-r1-distill-qwen-32b', 80000, false, false, 0.497, 4.881], ['@cf/ibm-granite/granite-4.0-h-micro', 131000, true, false, 0.017, 0.112], ['@cf/zai-org/glm-4.7-flash', 131072, true, false, 0.061, 0.40], ]; $models = []; foreach ($rows as $row) { $models[] = [ 'id' => $row[0], 'name' => pretty_model_name($row[0]), 'task' => 'Text Generation', 'context_window' => $row[1], 'function_calling' => $row[2], 'vision' => $row[3], 'price_in' => $row[4], 'price_out' => $row[5], ]; } return $models; } function static_task_catalog(): array { $rows = [ ['@cf/black-forest-labs/flux-1-schnell', 'Text-to-Image'], ['@cf/bytedance/stable-diffusion-xl-lightning', 'Text-to-Image'], ['@cf/stabilityai/stable-diffusion-xl-base-1.0', 'Text-to-Image'], ['@cf/lykon/dreamshaper-8-lcm', 'Text-to-Image'], ['@cf/leonardo/lucid-origin', 'Text-to-Image'], ['@cf/leonardo/phoenix-1.0', 'Text-to-Image'], ['@cf/meta/m2m100-1.2b', 'Translation'], ['@cf/ai4bharat/indictrans2-en-indic-1B', 'Translation'], ]; $out = []; foreach ($rows as $row) { $out[] = [ 'id' => $row[0], 'name' => pretty_model_name($row[0]), 'task' => $row[1], 'context_window' => 0, 'function_calling' => false, 'vision' => false, 'price_in' => $row[1] === 'Translation' ? 0.342 : 0.0, 'price_out' => $row[1] === 'Translation' ? 0.342 : 0.0, ]; } return $out; } function task_label(string $slug): string { if ($slug === '' || $slug === 'direct') { return __('Other (direct API calls)', 'aipcf-ai-provider-for-cloudflare'); } // The plugin's own features and the AI plugin's tasks, by name. Unknown slugs fall back // to the slug itself below. $names = [ // Workers AI reports no usage for embeddings: the neurons are estimated, and say so. 'embed' => __('Embeddings (estimated)', 'aipcf-ai-provider-for-cloudflare'), 'links' => __('Link assistant', 'aipcf-ai-provider-for-cloudflare'), 'search' => __('Member search', 'aipcf-ai-provider-for-cloudflare'), 'insights' => __('Search insights', 'aipcf-ai-provider-for-cloudflare'), 'chat' => __('Site assistant', 'aipcf-ai-provider-for-cloudflare'), 'chat-public' => __('Site assistant (visitors)', 'aipcf-ai-provider-for-cloudflare'), 'related' => __('Related posts', 'aipcf-ai-provider-for-cloudflare'), 'translation' => __('Translation', 'aipcf-ai-provider-for-cloudflare'), 'gateway-test' => __('AI Gateway test', 'aipcf-ai-provider-for-cloudflare'), 'licence-recheck' => __('License check', 'aipcf-ai-provider-for-cloudflare'), 'backfill-meta-description' => __('Backfill: meta descriptions', 'aipcf-ai-provider-for-cloudflare'), 'backfill-alt-text' => __('Backfill: alt text', 'aipcf-ai-provider-for-cloudflare'), 'meta-description' => __('Meta description', 'aipcf-ai-provider-for-cloudflare'), 'title-generation' => __('Titles', 'aipcf-ai-provider-for-cloudflare'), 'excerpt-generation' => __('Excerpts', 'aipcf-ai-provider-for-cloudflare'), 'slug-generation' => __('Slugs (URLs)', 'aipcf-ai-provider-for-cloudflare'), 'comment-moderation' => __('Comment moderation', 'aipcf-ai-provider-for-cloudflare'), 'content-classification' => __('Categories and tags', 'aipcf-ai-provider-for-cloudflare'), 'alt-text-generation' => __('Alt text', 'aipcf-ai-provider-for-cloudflare'), 'summarization' => __('Summaries', 'aipcf-ai-provider-for-cloudflare'), 'content-resizing' => __('Shorten or expand content', 'aipcf-ai-provider-for-cloudflare'), 'suggest-reply' => __('Suggested comment replies', 'aipcf-ai-provider-for-cloudflare'), 'editorial-notes' => __('Editorial notes', 'aipcf-ai-provider-for-cloudflare'), 'content-translation' => __('Content translation', 'aipcf-ai-provider-for-cloudflare'), 'image-generation' => __('Image generation', 'aipcf-ai-provider-for-cloudflare'), ]; if (isset($names[$slug])) { return $names[$slug]; } return ucfirst(str_replace('-', ' ', $slug)); } function pretty_model_name(string $id): string { $slug = (string) preg_replace('#^@cf/[^/]+/#', '', $id); $words = preg_split('/[-_]+/', $slug) ?: []; $out = []; foreach ($words as $w) { if ($w === '') { continue; } if (preg_match('/^\d+(\.\d+)?[bkm]$/i', $w)) { $out[] = strtoupper($w); } elseif (in_array(strtolower($w), ['fp8', 'it', 'hf', 'a3b', 'a4b', 'a12b', 'v0.1', 'v0.2'], true)) { $out[] = strtoupper($w); } else { $out[] = ucfirst($w); } } return implode(' ', $out); } /** * "Name (N neurons per 1,000 tokens, 128K context, tools, recommended)" for model dropdowns. */ function format_model_label(array $model, bool $recommended = false): string { $label = (string) ($model['name'] ?? $model['id'] ?? ''); $ctx = (int) ($model['context_window'] ?? 0); if ($ctx >= 1000000) { $size = rtrim(rtrim(number_format($ctx / 1000000, 1), '0'), '.') . 'M'; } elseif ($ctx >= 1000) { $size = (int) round($ctx / 1000) . 'K'; } elseif ($ctx > 0) { $size = number_format($ctx); } else { $size = ''; } $in = (float) ($model['price_in'] ?? 0); $out = (float) ($model['price_out'] ?? 0); $price = ''; if ($in > 0 || $out > 0) { $pin = number_format(UsageTracker::neuronsPer1k($in), 1); $pout = number_format(UsageTracker::neuronsPer1k($out), 1); $price = $pin === $pout /* translators: %s: neurons per 1,000 tokens */ ? sprintf(__('%s neurons per 1,000 tokens', 'aipcf-ai-provider-for-cloudflare'), $pin) /* translators: 1: neurons per 1,000 input tokens, 2: neurons per 1,000 output tokens */ : sprintf(__('%1$s neurons per 1,000 input tokens, %2$s per 1,000 output tokens', 'aipcf-ai-provider-for-cloudflare'), $pin, $pout); } $caps = []; if (!empty($model['function_calling'])) { $caps[] = __('tools', 'aipcf-ai-provider-for-cloudflare'); } if (!empty($model['vision'])) { $caps[] = __('vision', 'aipcf-ai-provider-for-cloudflare'); } $parts = []; if ($price !== '') { $parts[] = $price; } if ($size !== '') { /* translators: %s: context window size, e.g. "128K" */ $parts[] = sprintf(__('%s context', 'aipcf-ai-provider-for-cloudflare'), $size); } $parts = array_merge($parts, $caps); if ($recommended) { $parts[] = __('recommended', 'aipcf-ai-provider-for-cloudflare'); } if ($parts === []) { return $label; } /* translators: 1: model name, 2: comma-separated details, e.g. "2.5 neurons per 1,000 tokens, 128K context" */ return sprintf(__('%1$s (%2$s)', 'aipcf-ai-provider-for-cloudflare'), $label, implode(wp_get_list_item_separator(), $parts)); } function flush_model_caches(): void { delete_transient(AIPCF_MODELS_TRANSIENT); delete_transient(AIPCF_MODELS_TRANSIENT . '_time'); delete_transient(AIPCF_MODELS_TRANSIENT . '_failed'); delete_transient('aipcf_model_list'); // 1.0.x key. if (!class_exists(AiClient::class)) { return; } $group = (string) apply_filters('wp_ai_client_cache_group', 'wp_ai_client'); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- reading the AI Client SDK's own filter $key = 'ai_client_' . AiClient::VERSION . '_' . md5(\WordPress\CloudflareWorkersAiProvider\Metadata\CloudflareWorkersAiModelMetadataDirectory::class) . '_models'; wp_cache_delete($key, $group); } function maybe_flush_on_upgrade(): void { if (get_option('aipcf_version') === AIPCF_VERSION) { return; } $previous = (string) get_option('aipcf_version', ''); flush_model_caches(); update_option('aipcf_version', AIPCF_VERSION, false); /** * Runs once after the plugin is updated, downgraded or reactivated. * * @param string $previous The version that ran before ('' after reactivation or a first install). */ do_action('aipcf_version_changed', $previous); } add_action('init', __NAMESPACE__ . '\\maybe_flush_on_upgrade', 4); function ai_plugin_status(): array { $file = 'ai/ai.php'; if (!function_exists('is_plugin_active')) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } if (is_plugin_active($file) || is_plugin_active_for_network($file)) { return ['status' => 'active', 'action_url' => admin_url('admin.php?page=ai'), 'action_label' => __('AI settings', 'aipcf-ai-provider-for-cloudflare')]; } if (file_exists(WP_PLUGIN_DIR . '/' . $file)) { return [ 'status' => 'inactive', 'action_url' => wp_nonce_url(admin_url('plugins.php?action=activate&plugin=' . rawurlencode($file)), 'activate-plugin_' . $file), 'action_label' => __('Activate the AI plugin', 'aipcf-ai-provider-for-cloudflare'), ]; } return [ 'status' => 'missing', 'action_url' => current_user_can('install_plugins') ? admin_url('plugin-install.php?tab=plugin-information&plugin=ai&TB_iframe=true&width=772&height=600') : 'https://wordpress.org/plugins/ai/', 'action_label' => __('Install the AI plugin', 'aipcf-ai-provider-for-cloudflare'), ]; } function is_connector_approved(): bool { if (!class_exists('\WordPress\AI\Connector_Approval\Approvals_Store')) { return true; } if (!defined('AIPCF_FILE')) { return false; } $approvals = get_option('wpai_connector_approvals', []); if (!is_array($approvals)) { return false; } $basename = plugin_basename(\AIPCF_FILE); return !empty($approvals[$basename]['cloudflare-workers-ai']); } function core_connector_is_available(): bool { return function_exists('wp_is_connector_registered') && wp_is_connector_registered('cloudflare-workers-ai'); } function register_provider(): void { if (!class_exists(AiClient::class)) { return; } $registry = AiClient::defaultRegistry(); if (!$registry->hasProvider(CloudflareWorkersAiProvider::class)) { $registry->registerProvider(CloudflareWorkersAiProvider::class); } $apiKey = get_api_key(); if ($apiKey !== '') { $registry->setProviderRequestAuthentication( CloudflareWorkersAiProvider::class, new ApiKeyRequestAuthentication($apiKey) ); } } add_action('init', __NAMESPACE__ . '\\register_provider', 5); function boot_admin(): void { if (!is_admin()) { return; } SettingsPage::boot(); FreeTierSettings::boot(); EmbeddingsSettings::boot(); ContentTools::boot(); \WordPress\CloudflareWorkersAiProvider\Admin\InsightsTab::boot(); \WordPress\CloudflareWorkersAiProvider\Admin\ActivityPage::boot(); \WordPress\CloudflareWorkersAiProvider\Admin\AssistantPage::boot(); Setup::boot(); SiteHealth::boot(); } add_action('plugins_loaded', __NAMESPACE__ . '\\boot_admin'); register_activation_hook(__FILE__, [Setup::class, 'onActivate']); register_deactivation_hook(__FILE__, [Reset::class, 'deactivate']); function boot_services(): void { \WordPress\CloudflareWorkersAiProvider\Activity\ActivityLog::boot(); \WordPress\CloudflareWorkersAiProvider\Activity\Recorder::boot(); UsageTracker::boot(); TaskContext::boot(); Backfill::boot(); EmbeddingQueue::boot(); Related::boot(); \WordPress\CloudflareWorkersAiProvider\Features\Search::boot(); \WordPress\CloudflareWorkersAiProvider\Features\Insights::boot(); \WordPress\CloudflareWorkersAiProvider\Features\Chat::boot(); \WordPress\CloudflareWorkersAiProvider\Admin\LinkAssistant::boot(); EmbeddingCli::register(); \WordPress\CloudflareWorkersAiProvider\Features\Cli::register(); \WordPress\CloudflareWorkersAiProvider\Activity\Cli::register(); Receipts::boot(); Report::boot(); } add_action('plugins_loaded', __NAMESPACE__ . '\\boot_services'); /** * Multisite: a deleted site's tables go with it. * * @param list $tables * @return list */ function drop_site_tables($tables): array { global $wpdb; foreach (['aipcf_index', 'aipcf_terms', 'aipcf_terms_df', 'aipcf_links', 'aipcf_searches', 'aipcf_activity', 'aipcf_activity_items', 'aipcf_chats'] as $t) { $tables[] = $wpdb->prefix . $t; } return (array) $tables; } add_filter('wpmu_drop_tables', __NAMESPACE__ . '\\drop_site_tables'); function filter_preferred_image_models($models): array { if (!is_array($models)) { $models = []; } if (get_api_key() === '' || get_account_id() === '') { return $models; } $entry = [CloudflareWorkersAiProvider::ID, (string) (get_plugin_settings()['image_model'] ?: '@cf/black-forest-labs/flux-1-schnell')]; foreach ($models as $c) { if (is_array($c) && ($c[0] ?? '') === $entry[0] && ($c[1] ?? '') === $entry[1]) { return $models; } } array_unshift($models, $entry); return $models; } add_filter('wpai_preferred_image_models', __NAMESPACE__ . '\\filter_preferred_image_models', 5); /** * The model that reads images (alt text, image prompts): the Models tab setting, * else the first vision model on the account, else Llama 3.2 Vision. */ function vision_model_id(): string { $vision = trim((string) (get_plugin_settings()['vision_model'] ?? '')); if ($vision === '') { foreach (fetch_available_models() as $m) { if (!empty($m['vision'])) { $vision = (string) $m['id']; break; } } } if ($vision === '') { $vision = '@cf/meta/llama-3.2-11b-vision-instruct'; } return (string) apply_filters('aipcf_vision_model', $vision); } function filter_preferred_vision_models($models): array { if (!is_array($models)) { $models = []; } if (get_api_key() === '' || get_account_id() === '') { return $models; } $entry = [CloudflareWorkersAiProvider::ID, vision_model_id()]; foreach ($models as $c) { if (is_array($c) && ($c[0] ?? '') === $entry[0] && ($c[1] ?? '') === $entry[1]) { return $models; } } array_unshift($models, $entry); return $models; } add_filter('wpai_preferred_vision_models', __NAMESPACE__ . '\\filter_preferred_vision_models', 5); function filter_cloudflare_only($models): array { if (!is_array($models) || empty(get_plugin_settings()['cloudflare_only'])) { return is_array($models) ? $models : []; } if (get_api_key() === '' || get_account_id() === '') { return $models; } $kept = []; foreach ($models as $c) { if (is_array($c) && ($c[0] ?? '') === CloudflareWorkersAiProvider::ID) { $kept[] = $c; } } return $kept !== [] ? $kept : $models; } add_filter('wpai_preferred_text_models', __NAMESPACE__ . '\\filter_cloudflare_only', 99); add_filter('wpai_preferred_image_models', __NAMESPACE__ . '\\filter_cloudflare_only', 99); add_filter('wpai_preferred_vision_models', __NAMESPACE__ . '\\filter_cloudflare_only', 99); function translate(string $text, string $to, string $from = 'en', ?string $model = null) { if (!class_exists(AiClient::class)) { return new \WP_Error('aipcf_no_ai_client', __('The WordPress AI Client is not available. It needs WordPress 7.0 or later.', 'aipcf-ai-provider-for-cloudflare')); } if ($model === null) { $indic = CloudflareWorkersAiTranslationModel::languageCode($to, '@cf/ai4bharat/indictrans2-en-indic-1B'); $model = ($indic !== null && strtolower(trim($from)) === 'en') || strtolower(trim($from)) === 'english' ? '@cf/ai4bharat/indictrans2-en-indic-1B' : '@cf/meta/m2m100-1.2b'; if ($indic === null) { $model = '@cf/meta/m2m100-1.2b'; } } try { TaskContext::set('translation'); $m = AiClient::defaultRegistry()->getProviderModel(CloudflareWorkersAiProvider::ID, $model); $config = new \WordPress\AiClient\Providers\Models\DTO\ModelConfig(); $config->setCustomOptions(['target_lang' => $to, 'source_lang' => $from]); $r = AiClient::prompt($text) ->usingModel($m) ->usingModelConfig($config) ->generateTextResult(); TaskContext::clear(); return trim($r->toText()); } catch (\Throwable $e) { TaskContext::clear(); return new \WP_Error('aipcf_translate_failed', $e->getMessage()); } } function filter_preferred_text_models($models): array { if (!is_array($models)) { $models = []; } if (get_api_key() === '' || get_account_id() === '') { return $models; } $entry = [CloudflareWorkersAiProvider::ID, get_default_model_id()]; foreach ($models as $candidate) { if ( is_array($candidate) && isset($candidate[0], $candidate[1]) && $candidate[0] === $entry[0] && $candidate[1] === $entry[1] ) { return $models; } } array_unshift($models, $entry); return $models; } add_filter('wpai_preferred_text_models', __NAMESPACE__ . '\\filter_preferred_text_models', 5);