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
- 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.
- Namespace and Naming: Define your provider class as
- Action Processors (e.g.,
process/generate_text.php
):- Structure: Create processor classes extending
\core_ai\process_base
(or yourabstract_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).
- Structure: Create processor classes extending
- 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 tohttps://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.
- API Key: Open Router API key (
- 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); }
- Use
- 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)
- Create
classes/process/generate_text.php
:- Extend Base Processor: Extend
\core_ai\process_base
or your custom abstract processor.
- Extend Base Processor: Extend
- 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.
- Retrieve configuration (API key, endpoint, model) from the provider object (
- 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 yourprocess()
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).
Leave a Reply