resolveApiKey(); if ($apiKey === '') { // Translate only after init; model lists can be requested earlier. // No ">" in these messages: they are escaped with esc_html() when thrown, so a // plain-text view would show ">" instead of "Settings > Connectors". $message = did_action('init') ? __('The Cloudflare API token is not set. Add it under Connectors (in the Settings menu).', 'aipcf-ai-provider-for-cloudflare') : 'The Cloudflare API token is not set. Add it under Connectors (in the Settings menu).'; throw new RuntimeException(esc_html($message)); } $accountId = get_account_id(); $defaultModelId = get_default_model_id(); // No Account ID yet - validate the token alone and return the static // catalog. This allows the Connectors screen to confirm the key is valid // before the user has saved their Account ID in the plugin settings. if ($accountId === '') { $this->verifyTokenOnly($apiKey); return $this->indexByDefault($this->buildModels(static_model_catalog()), $defaultModelId); } $liveItems = fetch_available_models(); $models = $this->buildModels($liveItems !== [] ? $liveItems : static_model_catalog()); $live = fetch_catalog(); $tasks = $live !== [] ? $live : static_task_catalog(); $models = array_merge($models, $this->buildTaskModels($tasks)); return $this->indexByDefault($models, $defaultModelId); } private function buildModels(array $items): array { $models = []; foreach ($items as $item) { if (!isset($item['id']) || !is_string($item['id']) || $item['id'] === '') { continue; } $models[] = $this->buildModelMetadata( $item['id'], isset($item['name']) && is_string($item['name']) ? $item['name'] : $item['id'], !empty($item['function_calling']), !empty($item['vision']) ); } return $models; } private function buildTaskModels(array $items): array { $out = []; foreach ($items as $item) { $id = (string) ($item['id'] ?? ''); $task = (string) ($item['task'] ?? ''); $name = (string) ($item['name'] ?? $id); if ($id === '') { continue; } if ($task === 'Text-to-Image') { $out[] = new ModelMetadata( $id, $name, [CapabilityEnum::imageGeneration()], [ new SupportedOption(OptionEnum::systemInstruction()), new SupportedOption(OptionEnum::candidateCount(), [1]), new SupportedOption(OptionEnum::outputFileType(), [FileTypeEnum::inline()]), new SupportedOption(OptionEnum::outputMimeType(), ['image/png', 'image/jpeg']), new SupportedOption(OptionEnum::outputMediaOrientation()), new SupportedOption(OptionEnum::outputMediaAspectRatio(), ['1:1', '4:3', '3:4', '16:9', '9:16', '3:2', '2:3']), new SupportedOption(OptionEnum::customOptions()), new SupportedOption(OptionEnum::inputModalities(), [[ModalityEnum::text()]]), new SupportedOption(OptionEnum::outputModalities(), [[ModalityEnum::image()]]), ] ); } elseif ($task === 'Translation') { $out[] = new ModelMetadata( $id, $name, [CapabilityEnum::textGeneration()], [ new SupportedOption(OptionEnum::customOptions()), new SupportedOption(OptionEnum::inputModalities(), [[ModalityEnum::text()]]), new SupportedOption(OptionEnum::outputModalities(), [[ModalityEnum::text()]]), new SupportedOption(OptionEnum::outputMimeType(), ['text/plain']), ] ); } } return $out; } private function buildModelMetadata(string $id, string $name, bool $functionCalling, bool $vision): ModelMetadata { $capabilities = [ CapabilityEnum::textGeneration(), CapabilityEnum::chatHistory(), ]; $inputModalities = $vision ? [[ModalityEnum::text()], [ModalityEnum::text(), ModalityEnum::image()]] : [[ModalityEnum::text()]]; $options = [ new SupportedOption(OptionEnum::systemInstruction()), new SupportedOption(OptionEnum::maxTokens()), new SupportedOption(OptionEnum::temperature()), new SupportedOption(OptionEnum::topP()), new SupportedOption(OptionEnum::stopSequences()), new SupportedOption(OptionEnum::presencePenalty()), new SupportedOption(OptionEnum::frequencyPenalty()), new SupportedOption(OptionEnum::outputMimeType(), ['text/plain', 'application/json']), new SupportedOption(OptionEnum::outputSchema()), new SupportedOption(OptionEnum::customOptions()), new SupportedOption(OptionEnum::inputModalities(), $inputModalities), new SupportedOption(OptionEnum::outputModalities(), [[ModalityEnum::text()]]), ]; if ($functionCalling) { $options[] = new SupportedOption(OptionEnum::functionDeclarations()); } return new ModelMetadata($id, $name, $capabilities, $options); } private function resolveApiKey(): string { if (class_exists(AiClient::class)) { try { $registry = AiClient::defaultRegistry(); if (method_exists($registry, 'getProviderRequestAuthentication')) { $auth = $registry->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 - fall through. } } return get_api_key(); } private function verifyTokenOnly(string $apiKey): void { $response = wp_remote_get( 'https://api.cloudflare.com/client/v4/user/tokens/verify', [ 'timeout' => 8, 'headers' => [ 'Authorization' => 'Bearer ' . $apiKey, 'Accept' => 'application/json', ], ] ); if (is_wp_error($response)) { throw new RuntimeException(esc_html($response->get_error_message())); } $status = (int) wp_remote_retrieve_response_code($response); if ($status < 200 || $status >= 300) { // No ">" in these messages: they are escaped with esc_html() when thrown, so a // plain-text view would show ">" instead of "Settings > Connectors". $message = did_action('init') /* translators: %d: HTTP status code */ ? sprintf(__('Cloudflare did not accept the API token (HTTP %d). Check the token under Connectors (in the Settings menu).', 'aipcf-ai-provider-for-cloudflare'), (int) $status) : sprintf('Cloudflare did not accept the API token (HTTP %d). Check the token under Connectors (in the Settings menu).', (int) $status); throw new RuntimeException(esc_html($message)); } } private function indexByDefault(array $models, string $defaultModelId): array { $index = []; foreach ($models as $model) { $index[$model->getId()] = $model; } if (isset($index[$defaultModelId])) { $default = $index[$defaultModelId]; unset($index[$defaultModelId]); } else { $default = $this->buildModelMetadata($defaultModelId, $defaultModelId, true, false); } return [$defaultModelId => $default] + $index; } }