Developing a Custom Moodle AI Provider Plugin for Open Router

An abstract digital illustration featuring the letter 'M' in orange and a black microchip labeled 'AI' in bright orange, connected by glowing blue circuit lines against a dark background with geometric patterns.

In-Depth Guide: Developing a Custom Moodle AI Provider Plugin for Open Router

This guide helps you develop a custom Moodle AI provider plugin for Open Router. It explains the required plugin structure, essential methods, and the general workflow to integrate an OpenAI API-compatible service (Open Router) into Moodle’s AI subsystem (introduced in Moodle 4.5). We’ll also discuss managing settings, actions, and advanced customization.


Overview

Moodle’s AI subsystem allows integration with external AI services through provider plugins. Provider plugins act as wrappers around the external API, converting data from Moodle actions into the request format expected by the AI service and processing the API response back into a format suitable for Moodle placements.

For Open Router—an AI provider routing requests to various models via an OpenAI API‑compatible layer—you will create a new provider plugin (e.g., aiprovider_openrouter) implementing the standard provider interface.


Plugin Directory Structure

Your custom provider plugin will reside in the ai/provider directory. A typical directory layout:

moodleroot/
  ai/
    provider/
      aiprovider_openrouter/
        classes/
          provider.php               # Main provider class, extending \core_ai\provider
          abstract_processor.php     # (Optional) Abstract processor for shared logic
          process/                   # Subdirectory for processor classes
            generate_text.php        # Class for handling the generate text action
            summarise_text.php       # Class for handling summarisation (if supported)
        lang/
          en/
            aiprovider_openrouter.php # Language strings for your plugin
        settings.php                # Admin settings for API key, endpoint, model, etc.
        version.php                 # Plugin version and compatibility information
        tests/                      # Automated tests (optional but recommended)

(Note: Placing processors in a process subdirectory within classes is common practice for organization).


Key Components

  1. Main Provider Class (provider.php):
    • Namespace and Naming: Define your provider class as \aiprovider_openrouter\provider and extend \core_ai\provider.
    • Essential Methods:
      • get_action_list(): array: List supported actions (e.g., \core_ai\aiactions\generate_text::class).
      • is_provider_configured(): bool: Check if required settings (API key, endpoint, default model) are configured.
        public function is_provider_configured(): bool {
            // Also check for the defaultmodel setting added below.
            return !empty($this->apikey) && !empty($this->apiendpoint) && !empty($this->defaultmodel);
        }
        
      • is_request_allowed(aiactions\base $action): array|bool (Optional): Implement rate limiting using Moodle’s rate limiter API.
  2. Action Processors (e.g., process/generate_text.php):
    • Structure: Create processor classes extending \core_ai\process_base (or your abstract_processor).
    • process() Method: Handles the core logic: accepting the Moodle action, forming the API request, calling the Open Router API, processing the response, handling errors, and returning a Moodle Action Response object (\core_ai\aiactions\responses\response_base subclass).
  3. Admin Settings (settings.php):
    • Use core_ai\admin\admin_settingspage_provider to create the settings page.
    • Essential Settings:
      • API Key: Open Router API key (aiprovider_openrouter/apikey).
      • API Endpoint: Base URL for Open Router (aiprovider_openrouter/apiendpoint). Defaults to https://openrouter.ai/api/v1.
      • Default Model: The default Open Router model identifier to use (e.g., openai/gpt-4o, anthropic/claude-3-opus) (aiprovider_openrouter/defaultmodel). This could potentially be overridden per action instance later.
      • Optional Rate Limits.
    • Example snippet:
      use core_ai\admin\admin_settingspage_provider;
      defined('MOODLE_INTERNAL') || die(); // Add this line.
      
      if ($hassiteconfig) {
          $settings = new admin_settingspage_provider(
              'aiprovider_openrouter',
              new lang_string('pluginname', 'aiprovider_openrouter'),
              'moodle/site:config',
              true // Requires page commit.
          );
      
          // API Key setting.
          $settings->add(new admin_setting_configpasswordunmask( // Use password field for keys.
              'aiprovider_openrouter/apikey',
              new lang_string('apikey', 'aiprovider_openrouter'),
              new lang_string('apikey_desc', 'aiprovider_openrouter'),
              '' // Default value.
          ));
      
          // API Endpoint setting.
          $settings->add(new admin_setting_configtext(
              'aiprovider_openrouter/apiendpoint',
              new lang_string('apiendpoint', 'aiprovider_openrouter'),
              new lang_string('apiendpoint_desc', 'aiprovider_openrouter'), // Description should mention the default.
              'https://openrouter.ai/api/v1', // Default value.
              PARAM_URL
          ));
      
          // Default Model setting.
          $settings->add(new admin_setting_configtext(
              'aiprovider_openrouter/defaultmodel',
              new lang_string('defaultmodel', 'aiprovider_openrouter'),
              new lang_string('defaultmodel_desc', 'aiprovider_openrouter'), // Description should give examples.
              '', // No default, force admin to choose. Or provide a common one like 'openai/gpt-4o'.
              PARAM_TEXT // Or a more specific type if validating against Open Router models.
          ));
      
          // Add rate limit settings if needed.
      
          $ADMIN->add('ai', $settings);
      }
      
  4. Plugin Version (version.php):
    • Define version, Moodle requirement, and maturity. Crucially, requires Moodle 4.5 or later.
    • Example:
      defined('MOODLE_INTERNAL') || die();
      $plugin->component = 'aiprovider_openrouter';
      $plugin->version = 2025040900; // YYYYMMDDXX format for your plugin version.
      // Requires Moodle 4.5 (using 4.5 stable release date for example).
      $plugin->requires = 2024111800; // Moodle 4.5.0 stable release version number.
      $plugin->maturity = MATURITY_BETA;
      $plugin->release = 'v1.0 Beta';
      

Developing the Action Processor (Example: Generate Text)

  1. Create classes/process/generate_text.php:
    • Extend Base Processor: Extend \core_ai\process_base or your custom abstract processor.
  2. Implement process() Method:
    • Retrieve configuration (API key, endpoint, model) from the provider object ($this->provider).
    • Get action-specific data (e.g., prompt) from the action object ($this->action).
    • Construct the full API URL (base endpoint + specific path like /chat/completions).
    • Format the request payload according to Open Router’s OpenAI-compatible API (Chat Completions format is standard).
    • Use Moodle’s HTTP client (\core\http\Client) for the POST request.
    • Implement robust error handling (HTTP status codes, API errors, exceptions).
    • Parse the successful response and extract the generated text.
    • Populate and return a \core_ai\aiactions\responses\response_generate_text object.
  3. Example Code (process() method):
    namespace aiprovider_openrouter\process;
    
    defined('MOODLE_INTERNAL') || die();
    
    use core_ai\process_base;
    use core_ai\aiactions\generate_text; // Assuming this is the action class.
    use core_ai\aiactions\responses\response_generate_text;
    use core_ai\api_exception;
    use core_ai\configuration_exception;
    use core\http\client as http_client;
    use core\http\exception as http_exception;
    use Throwable; // For broader exception catching.
    
    class generate_text extends process_base {
    
        public function process(): response_generate_text {
            /** @var \aiprovider_openrouter\provider $provider */
            $provider = $this->provider;
            /** @var \core_ai\aiactions\generate_text $action */
            $action = $this->action;
    
            // 1. Check configuration.
            if (!$provider->is_provider_configured()) {
                throw new configuration_exception('Provider not configured');
            }
    
            // 2. Get data from action and settings.
            // Example: Getting prompt - adjust key based on actual action implementation.
            $prompttext = $action->get_prompt(); // Assuming a get_prompt() method exists.
            if (empty($prompttext)) {
                 throw new \invalid_parameter_exception('Prompt text is empty');
            }
    
            // Get model - prefer action-specific model if set, otherwise use provider default.
            $model = $action->get_configuration('model') ?: $provider->defaultmodel;
            $max_tokens = $action->get_configuration('max_tokens') ?: 1000; // Example: Make configurable.
    
            $apiurl = $provider->apiendpoint . '/chat/completions'; // Standard chat endpoint.
            $apikey = $provider->apikey;
    
            // 3. Format the API request payload (Chat Completions format).
            $payload = [
                'model' => $model,
                'messages' => [
                    ['role' => 'user', 'content' => $prompttext]
                    // Add system prompt or previous messages if needed/supported by the action.
                ],
                'max_tokens' => (int) $max_tokens,
                // Add other parameters like temperature, top_p as needed/configured.
            ];
    
            // Add Open Router specific headers if required (e.g., HTTP Referer, X-Title).
            // See Open Router documentation. Usually, Authorization is sufficient.
            $headers = [
                'Authorization' => 'Bearer ' . $apikey,
                'Content-Type' => 'application/json',
                // 'HTTP-Referer' => $CFG->wwwroot, // Example Open Router specific header.
                // 'X-Title' => 'Moodle AI Request', // Example Open Router specific header.
            ];
    
            try {
                // 4. Make the API call using Moodle HTTP client.
                $response = http_client::post($apiurl, [
                    'headers' => $headers,
                    'body' => json_encode($payload),
                    'timeout' => 60 // Set a reasonable timeout (seconds).
                ]);
    
                $statuscode = $response->get_status_code();
                $responsebody = $response->get_body();
    
                // 5. Handle API response and errors.
                if ($statuscode !== 200) {
                    // Try to get error details from response body.
                    $errordetails = json_decode($responsebody);
                    $errormessage = $errordetails->error->message ?? 'Unknown API error';
                    // Include status code for clarity.
                    throw new api_exception("API Error: Status {$statuscode} - {$errormessage}");
                }
    
                $responsecontent = json_decode($responsebody, true);
                if (json_last_error() !== JSON_ERROR_NONE) {
                     throw new api_exception('Error decoding API response: ' . json_last_error_msg());
                }
    
                // 6. Extract the generated text. Structure depends on the API response format.
                // Typical OpenAI format:
                if (!isset($responsecontent['choices'][0]['message']['content'])) {
                     throw new api_exception('Unexpected API response format: Generated text not found.');
                }
                $generatedtext = trim($responsecontent['choices'][0]['message']['content']);
    
                // 7. Create and populate the Moodle response object.
                $result = new response_generate_text();
                // Use the appropriate setter method - name might vary slightly in core_ai.
                // Assuming set_generated_text() or set_content(). Check Moodle core_ai code.
                $result->set_generated_text($generatedtext);
                // Optionally set other data from the response if needed by the action/placement.
                // $result->set_response_data($responsecontent); // If raw data needed downstream.
    
                return $result;
    
            } catch (http_exception $e) {
                // Handle Moodle HTTP client exceptions (network issues, timeouts).
                throw new api_exception('HTTP Request Failed: ' . $e->getMessage(), 0, $e);
            } catch (Throwable $e) {
                // Catch any other unexpected errors during processing.
                // Log the error for debugging.
                debugging("Open Router provider failed: " . $e->getMessage() . "\n" . $e->getTraceAsString(), DEBUG_DEVELOPER);
                // Re-throw as a generic AI exception unless it's already an api_exception/configuration_exception.
                if ($e instanceof api_exception || $e instanceof configuration_exception) {
                    throw $e;
                }
                throw new api_exception('An unexpected error occurred: ' . $e->getMessage(), 0, $e);
            }
        }
    }
    

Testing & Debugging

  • Unit Tests: Write PHPUnit tests for your provider and processor classes (tests directory). Mock API calls.
  • Manual Testing: Configure the provider in Moodle Admin -> Server -> AI Settings. Use AI features (e.g., AI text generator in Atto/TinyMCE, Course creator helper) that trigger the generate_text action to test the integration.
  • Logging: Enable Moodle debugging (Developer level) to see detailed logs, including any messages from debugging(). Check web server error logs. Add specific logging within your process() method if needed.

Additional Resources

  • Moodle Developer Documentation (AI Subsystem): Review the official documentation for the AI subsystem, focusing on the version relevant to your Moodle target (4.5+). Check Moodle Development Resources (URL may slightly change; navigate from the main dev docs).
  • Sample Plugins: Examine core provider plugins like aiprovider_openai (server/ai/provider/openai) for implementation patterns.
  • Open Router Documentation: Consult the Open Router API Documentation for specific endpoint details, required headers, model identifiers, and error codes.
  • Community Support: Moodle developer forums and the Moodle.org AI community forums.

Next Steps

More details on implementing specific Open Router headers, handling different Moodle AI actions (like summarization), or advanced configuration options (like allowing users/courses to select models).

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *