'array', 'default' => [], 'sanitize_callback' => [ $this, 'sanitize_settings' ], ] ); add_settings_section( self::SECTION_ID, '', '__return_empty_string', self::PAGE_SLUG ); add_settings_field( self::OPTION_NAME . '_host', __( 'Host URL', 'ai-provider-for-lm-studio' ), [ $this, 'render_host_field' ], self::PAGE_SLUG, self::SECTION_ID, [ 'label_for' => self::OPTION_NAME . '-host' ] ); add_settings_field( self::OPTION_NAME . '_model', __( 'Available Models', 'ai-provider-for-lm-studio' ), [ $this, 'render_available_models_field' ], self::PAGE_SLUG, self::SECTION_ID, [ 'label_for' => self::OPTION_NAME . '-model' ] ); add_settings_field( self::OPTION_NAME . '_reasoning', __( 'Reasoning', 'ai-provider-for-lm-studio' ), [ $this, 'render_reasoning_field' ], self::PAGE_SLUG, self::SECTION_ID ); } /** * Registers the settings screen. * * @since 1.0.0 */ public function register_settings_screen(): void { add_options_page( __( 'LM Studio Settings', 'ai-provider-for-lm-studio' ), __( 'LM Studio Settings', 'ai-provider-for-lm-studio' ), 'manage_options', self::PAGE_SLUG, [ $this, 'render_screen' ] ); } /** * Sanitizes the settings array. * * @since 1.0.0 * * @param mixed $value The input value. * @return array The sanitized settings. */ public function sanitize_settings( $value ): array { if ( ! is_array( $value ) ) { return self::get_default_settings(); } $host = isset( $value['host'] ) ? trim( (string) $value['host'] ) : ''; $model = isset( $value[ self::KEY_MODEL ] ) ? sanitize_text_field( (string) $value[ self::KEY_MODEL ] ) : ''; $model = trim( $model ); $reasoning = isset( $value[ self::KEY_REASONING ] ) ? sanitize_text_field( (string) $value[ self::KEY_REASONING ] ) : ''; $reasoning = trim( $reasoning ); if ( '' !== $host ) { $host = rtrim( esc_url_raw( $host ), '/' ); } return [ 'host' => $host, self::KEY_MODEL => $model, self::KEY_REASONING => $reasoning, ]; } /** * Renders the settings screen. * * @since 1.0.0 */ public function render_screen(): void { if ( ! current_user_can( 'manage_options' ) ) { return; } ?>

Connectors%2$s.', 'ai-provider-for-lm-studio' ), '', '' ); ?>

', '' ); ?>

'assets/images/vision.svg', 'tools' => 'assets/images/tools.svg', 'reasoning' => 'assets/images/reasoning.svg', 'error' => 'assets/images/error.svg', 'success' => 'assets/images/success.svg', ]; foreach ( $svg_files as $key => $rel_path ) { $full_path = $plugin_dir . $rel_path; if ( ! file_exists( $full_path ) ) { continue; } $svgs[ $key ] = AI_PROVIDER_FOR_LM_STUDIO_PLUGIN_URL . $rel_path; } wp_cache_set( $cache_key, $svgs, 'ai-provider-for-lm-studio' ); } wp_localize_script( 'ai-provider-for-lm-studio-settings', 'AIProviderForLMStudioSettings', [ 'selectedModel' => self::get_selected_model(), 'selectedReasoning' => self::get_selected_reasoning(), 'svgs' => $svgs, ] ); } /** * Registers the REST API routes. * * @since 1.1.0 */ public function register_rest_routes(): void { register_rest_route( 'ai-provider-for-lm-studio/v1', '/models', [ 'methods' => 'GET', 'callback' => [ $this, 'get_models_endpoint' ], 'permission_callback' => [ $this, 'check_rest_permissions' ], ] ); register_rest_route( 'ai-provider-for-lm-studio/v1', '/capabilities', [ 'methods' => 'GET', 'callback' => [ $this, 'get_capabilities_endpoint' ], 'permission_callback' => [ $this, 'check_rest_permissions' ], ] ); } /** * Checks permissions for the REST API endpoints. * * @since 1.0.0 * * @return bool True if the user has permission, false otherwise. */ public function check_rest_permissions(): bool { return current_user_can( 'manage_options' ); } /** * REST API endpoint to retrieve available models. * * @since 1.0.0 * * @return \WP_REST_Response The REST response. */ public function get_models_endpoint(): \WP_REST_Response { $provider_id = 'lmstudio'; $registry = AiClient::defaultRegistry(); if ( ! $registry->hasProvider( $provider_id ) ) { return new \WP_REST_Response( [ 'message' => __( 'AI provider not found.', 'ai-provider-for-lm-studio' ) ], 404 ); } $provider_classname = $registry->getProviderClassName( $provider_id ); try { // phpcs:ignore Generic.Commenting.DocComment.MissingShort $provider_availability = $provider_classname::availability(); if ( ! $provider_availability->isConfigured() ) { return new \WP_REST_Response( [ 'message' => __( 'AI provider not configured - missing API credentials.', 'ai-provider-for-lm-studio' ) ], 400 ); } // phpcs:ignore Generic.Commenting.DocComment.MissingShort $model_metadata_directory = $provider_classname::modelMetadataDirectory(); $model_metadata_objects = $model_metadata_directory->listModelMetadata(); return new \WP_REST_Response( $model_metadata_objects, 200 ); } catch ( \Throwable $e ) { return new \WP_REST_Response( [ // translators: %s: Error message. 'message' => sprintf( __( 'Could not list models for provider. Error: %s', 'ai-provider-for-lm-studio' ), $e->getMessage() ), ], 500 ); } } /** * Gets settings from the WordPress option. * * @since 1.0.0 * * @return array The settings. */ public static function get_settings(): array { $settings = (array) get_option( self::OPTION_NAME, [] ); return array_merge( self::get_default_settings(), $settings ); } /** * Gets default settings. * * @since 1.0.0 * * @return array Default settings. */ private static function get_default_settings(): array { return [ 'host' => '', self::KEY_MODEL => '', self::KEY_REASONING => '', ]; } /** * Gets selected default model. * * @since 1.0.0 * * @return string Selected model ID, or empty string when unset. */ public static function get_selected_model(): string { $settings = self::get_settings(); if ( ! isset( $settings[ self::KEY_MODEL ] ) ) { return ''; } return trim( (string) $settings[ self::KEY_MODEL ] ); } /** * Renders the reasoning field. * * Shows a hidden input (persisted via form submit) plus a JS-populated * fieldset of radio buttons that appears only when the selected model * reports reasoning capabilities. * * @since 1.0.0 */ public function render_reasoning_field(): void { $settings = self::get_settings(); $current_reasoning = isset( $settings[ self::KEY_REASONING ] ) ? (string) $settings[ self::KEY_REASONING ] : ''; ?>

false, ] ); if ( is_wp_error( $response ) ) { return new \WP_REST_Response( [ 'message' => $response->get_error_message() ], 500 ); } $body = wp_remote_retrieve_body( $response ); $data = json_decode( $body, true ); if ( ! is_array( $data ) || ! isset( $data['models'] ) || ! is_array( $data['models'] ) ) { return new \WP_REST_Response( [ 'message' => __( 'Invalid response from LM Studio.', 'ai-provider-for-lm-studio' ) ], 500 ); } $capabilities_map = []; foreach ( $data['models'] as $model ) { if ( ! is_array( $model ) || ! isset( $model['key'] ) || ! isset( $model['type'] ) || 'llm' !== $model['type'] ) { continue; } $model_key = $model['key']; $reasoning = null; $vision = false; $tool_use = false; if ( isset( $model['capabilities'] ) && is_array( $model['capabilities'] ) ) { $raw_caps = $model['capabilities']; if ( isset( $raw_caps['reasoning'] ) && is_array( $raw_caps['reasoning'] ) && isset( $raw_caps['reasoning']['allowed_options'] ) && is_array( $raw_caps['reasoning']['allowed_options'] ) && ! empty( $raw_caps['reasoning']['allowed_options'] ) ) { $raw = $raw_caps['reasoning']; $allowed = array_values( array_filter( $raw['allowed_options'], 'is_string' ) ); $default = isset( $raw['default'] ) && is_string( $raw['default'] ) ? $raw['default'] : ''; $reasoning = [ 'allowed_options' => $allowed, 'default' => $default, ]; } if ( isset( $raw_caps['vision'] ) ) { $vision = filter_var( $raw_caps['vision'], FILTER_VALIDATE_BOOLEAN ); } if ( isset( $raw_caps['trained_for_tool_use'] ) ) { $tool_use = filter_var( $raw_caps['trained_for_tool_use'], FILTER_VALIDATE_BOOLEAN ); } } $capabilities_map[ $model_key ] = [ 'reasoning' => $reasoning, 'vision' => $vision, 'trained_for_tool_use' => $tool_use, ]; } return new \WP_REST_Response( $capabilities_map, 200 ); } /** * Gets the saved reasoning setting. * * @since 1.0.0 * * @return string Reasoning value (e.g. 'on', 'off'), or empty string when unset. */ public static function get_selected_reasoning(): string { $settings = self::get_settings(); if ( ! isset( $settings[ self::KEY_REASONING ] ) ) { return ''; } return trim( (string) $settings[ self::KEY_REASONING ] ); } }