'array', 'sanitize_callback' => [self::class, 'sanitize_settings'], 'default' => [ 'api_key' => '', 'default_model' => '', ], ] ); add_settings_section( 'vercel_ai_gateway_provider_main', __('API credentials', 'ai-provider-for-vercel-ai-gateway'), static function (): void { if (core_connector_is_available()) { $connectorsUrl = admin_url('options-connectors.php'); echo '

'; printf( /* translators: %s: link to the core Connectors admin screen */ esc_html__( 'API keys are managed centrally on the %s screen. Use the field below to pick a default model — any plugin that calls wp_ai_client_prompt() will be able to route requests through Vercel AI Gateway.', 'ai-provider-for-vercel-ai-gateway' ), '' . esc_html__('Settings → Connectors', 'ai-provider-for-vercel-ai-gateway') . '' ); echo '

'; return; } echo '

' . esc_html__( 'Enter your Vercel AI Gateway API key and pick a default model. Any plugin that calls wp_ai_client_prompt() will be able to route requests through Vercel AI Gateway.', 'ai-provider-for-vercel-ai-gateway' ) . '

'; }, self::PAGE_SLUG ); add_settings_field( 'api_key', __('API key', 'ai-provider-for-vercel-ai-gateway'), [self::class, 'render_api_key_field'], self::PAGE_SLUG, 'vercel_ai_gateway_provider_main' ); add_settings_field( 'default_model', __('Default text model', 'ai-provider-for-vercel-ai-gateway'), [self::class, 'render_default_model_field'], self::PAGE_SLUG, 'vercel_ai_gateway_provider_main' ); add_settings_field( 'default_image_model', __('Default image model', 'ai-provider-for-vercel-ai-gateway'), [self::class, 'render_default_image_model_field'], self::PAGE_SLUG, 'vercel_ai_gateway_provider_main' ); } /** * Sanitizes the saved settings array. * * Also clears the cached model list when the API key changes so the next * request re-fetches /models with the new credential. * * @since 1.0.0 * * @param mixed $input Raw posted value. * @return array{api_key: string, default_model: string} */ public static function sanitize_settings($input): array { $current = get_plugin_settings(); if (!is_array($input)) { $input = []; } // When the core Connectors screen owns the API key, the field is not // rendered on this page — preserve whatever is already stored so a // "Save changes" click on this page does not blow it away. if (array_key_exists('api_key', $input)) { $apiKey = sanitize_text_field(trim((string) $input['api_key'])); } else { $apiKey = (string) ($current['api_key'] ?? ''); } $defaultModel = isset($input['default_model']) ? trim((string) $input['default_model']) : ''; $defaultModel = sanitize_text_field($defaultModel); $defaultImageModel = isset($input['default_image_model']) ? trim((string) $input['default_image_model']) : ''; $defaultImageModel = sanitize_text_field($defaultImageModel); // Bust the cached /v1/models list when: // - the API key changes (different keys can have different rosters), or // - either default-model selection changes — the directory augments // its output with user-configured defaults, so the cache must be // rebuilt to surface the new selection to the SDK. $apiKeyChanged = $apiKey !== ($current['api_key'] ?? ''); $textModelChanged = $defaultModel !== ($current['default_model'] ?? ''); $imageModelChanged = $defaultImageModel !== ($current['default_image_model'] ?? ''); if ($apiKeyChanged || $textModelChanged || $imageModelChanged) { clear_models_cache(); } return [ 'api_key' => $apiKey, 'default_model' => $defaultModel, 'default_image_model' => $defaultImageModel, ]; } /** * Renders the API key form field. Stored masked when set. * * When the WordPress core "Connectors" screen is available, this field * is replaced by a read-only summary that defers to that screen, since * core auto-discovers this provider and storing the key in two places * would only confuse the user. * * @since 1.0.0 * * @return void */ public static function render_api_key_field(): void { if (core_connector_is_available()) { self::render_api_key_field_managed_by_core(); return; } $settings = get_plugin_settings(); $value = (string) ($settings['api_key'] ?? ''); $option = VERCEL_AI_GATEWAY_PROVIDER_OPTION; printf( '', esc_attr($option), esc_attr($value) ); echo '

'; printf( /* translators: %s: link to the Vercel AI Gateway dashboard */ esc_html__('Create or copy a key from %s.', 'ai-provider-for-vercel-ai-gateway'), '' . esc_html__('your Vercel AI Gateway dashboard', 'ai-provider-for-vercel-ai-gateway') . '' ); echo '

'; if (defined('AI_GATEWAY_API_KEY') && AI_GATEWAY_API_KEY) { echo '

' . esc_html__( 'A value is also defined in wp-config.php as AI_GATEWAY_API_KEY and will be used as a fallback when this field is empty.', 'ai-provider-for-vercel-ai-gateway' ) . '

'; } } /** * Renders the read-only API key summary used when WP core's Connectors * screen is in charge of the key. * * Shows where the key currently lives (database, env var, constant, or * not configured) without ever revealing the value, and links straight * to Settings → Connectors for editing. * * @since 1.0.0 * * @return void */ private static function render_api_key_field_managed_by_core(): void { $connectorsUrl = admin_url('options-connectors.php'); $status = self::resolve_api_key_status(); echo '

' . esc_html($status['label']) . ''; if ($status['detail'] !== '') { echo ' ' . esc_html($status['detail']) . ''; } echo '

'; echo '

'; printf( /* translators: %s: link to the core Connectors admin screen */ esc_html__('Manage this key on the %s screen.', 'ai-provider-for-vercel-ai-gateway'), '' . esc_html__('Settings → Connectors', 'ai-provider-for-vercel-ai-gateway') . '' ); echo '

'; } /** * Resolves where the active API key is coming from, for display in the * read-only summary on the settings page. Never returns the key itself. * * @since 1.0.0 * * @return array{label: string, detail: string} */ private static function resolve_api_key_status(): array { $envValue = getenv('AI_GATEWAY_API_KEY'); if (is_string($envValue) && $envValue !== '') { return [ 'label' => __('Configured via environment variable', 'ai-provider-for-vercel-ai-gateway'), 'detail' => __('AI_GATEWAY_API_KEY (environment) overrides any saved value.', 'ai-provider-for-vercel-ai-gateway'), ]; } if (defined('AI_GATEWAY_API_KEY') && is_string(AI_GATEWAY_API_KEY) && AI_GATEWAY_API_KEY !== '') { return [ 'label' => __('Configured via wp-config.php constant', 'ai-provider-for-vercel-ai-gateway'), 'detail' => __('AI_GATEWAY_API_KEY (constant) overrides any saved value.', 'ai-provider-for-vercel-ai-gateway'), ]; } $coreKey = (string) get_option(VERCEL_AI_GATEWAY_PROVIDER_CORE_CONNECTOR_OPTION, ''); if ($coreKey !== '') { return [ 'label' => __('Configured', 'ai-provider-for-vercel-ai-gateway'), 'detail' => __('Saved on Settings → Connectors.', 'ai-provider-for-vercel-ai-gateway'), ]; } $settings = get_plugin_settings(); if (!empty($settings['api_key'])) { return [ 'label' => __('Configured (legacy)', 'ai-provider-for-vercel-ai-gateway'), 'detail' => __('A value saved by an older version of this plugin is still being used as a fallback. Re-enter it on Settings → Connectors to migrate.', 'ai-provider-for-vercel-ai-gateway'), ]; } return [ 'label' => __('Not configured', 'ai-provider-for-vercel-ai-gateway'), 'detail' => '', ]; } /** * Renders the default text-generation model form field. * * Renders a with one