AI Hub

BYOK (bring your own key) broker for Moodle β€” full documentation.

English | PortuguΓͺs

View on GitHub Download .zip

Moodle Plugin CI Moodle License Status

AI Hub is a small BYOK (bring your own key) broker for Moodle. It lets the institution’s own plugins generate text through shared AI API keys, without each plugin reimplementing the HTTP transport, the SSRF guard, the provider ladder or a key store.

Use the sidebar to jump to any section on this page.

Source code: github.com/jeanlucio/moodle-local_aihub


✨ Features

🧩 How consumers use it

A sibling plugin consumes the hub through a runtime guard and keeps its own core_ai fallback:

// Availability: hub keys first, then the consumer's own core_ai fallback.
public static function has_ai(): bool {
    if (class_exists(\local_aihub\ai::class) && \local_aihub\ai::is_available()) {
        return true;                       // BYOK via the hub
    }
    return self::has_core_ai();            // works without the hub
}

// Generation.
$result = \local_aihub\ai::generate_text(
    '',                    // system prompt (optional)
    $prompt,               // user prompt
    false,                 // JSON mode
    'your_frankenstyle',   // 4th arg: caller component, for the usage log
    'Short label'          // 5th arg: what is being generated, shown in the admin report
);
if ($result['success']) {
    // $result['data'] is RAW, untrusted text β€” validate and format_text() it.
}

The returned text is raw and untrusted: validate its structure and pass it through format_text() before displaying or persisting it.

πŸ”— Key resolution (BYOK ladder)

The hub resolves a key tier by tier and stops at the first tier that holds a key:

Tier Source
1 Personal key β€” the user’s own key, when personal keys are enabled and the user has local/aihub:usepersonalkey
2 Site key β€” the admin key set in the hub settings

Within the chosen tier, providers are tried in the order Gemini β†’ Groq β†’ DeepSeek β†’ OpenAI-compatible (first key found is used; if its call fails, the next provider in the same tier is tried). When no tier holds a key, generate_text() returns success = false β€” the hub never falls back to core_ai; that decision belongs to the consumer.

πŸ“¦ Requirements

Component Version
Moodle 4.5+
PHP 8.1+

No bundled third-party libraries. The AI providers are external services, declared in the Privacy provider β€” not in thirdpartylibs.xml.

πŸ› οΈ Installation & Configuration

  1. Download the .zip file or clone this repository.
  2. Extract the folder into your Moodle local/ directory.
  3. Rename the folder to aihub (if necessary). Final path: your-moodle/local/aihub/
  4. Visit Site administration β†’ Notifications to complete installation.

Configuring a provider key is optional β€” the plugin installs and works without one. See Usage below for setting site keys and enabling personal keys.

πŸ“– Usage

  1. Configure site keys (admin): Site administration β†’ Plugins β†’ Local plugins β†’ AI Hub β†’ Settings. Set any of the Gemini, Groq, DeepSeek or OpenAI-compatible keys, and (optionally) enable personal API keys.
  2. Add a personal key (user): users who have the local/aihub:usepersonalkey capability get a My AI keys entry in their preferences, where they store their own key (write-only) and see their recent usage.
  3. Review site-key usage (admin): the AI usage report (under the AI Hub category, capability local/aihub:viewusage) lists every request served by the site keys across all users, with CSV and Excel download.
  4. Consume from a plugin (developer): call \local_aihub\ai::generate_text() behind a class_exists() guard, keeping your own core_ai fallback (see How consumers use it).

πŸ§ͺ Automated Tests

The hub ships with a PHPUnit and Behat suite; every CI push runs against the matrix (Moodle 4.5 β†’ 5.x, PostgreSQL & MariaDB).

PHPUnit β€” Unit & Integration Tests

Test file Cases What is covered
tests/local/keys_test.php 6 OpenAI base-URL/model defaults; personal-key get/save/clear roundtrip; personal OpenAI-compatible URL/model roundtrip; personal_keys_allowed honouring the toggle and the capability; key resolution personal β†’ site; has_any_key across personal/site keys
tests/local/client_test.php 9 is_safe_url SSRF cases (http, loopback, private range, public IP); DNS-rebinding branch stubbed via dns_stub_client β€” a resolved private IP is blocked, a resolved public IP passes, no records resolves through; resolve_openai_url appends /chat/completions; personal tier wins over site; provider fall-through within a tier (Gemini β†’ Groq, and Gemini/Groq β†’ DeepSeek); no key β†’ success=false (no real HTTP)
tests/local/usage_log_test.php 3 Record insert (with keysource, empty model nulled); site-scoped readers excluding personal/untagged rows; recent-per-user filtering
tests/local/export_test.php 2 Personal usage export (all rows, every column); site-keys report export including the user column and excluding personal usage
tests/ai_test.php 4 is_available states; a successful generation logs the calling component, the description and the key tier; a failed generation logs nothing; report_usage() writes a log row for a consumer that resolved its own key
tests/privacy_provider_test.php 4 Metadata declaration; context/user discovery; export_user_data (log rows + redacted key value); per-user deletion with isolation
tests/output/mykeys_test.php 2 Per-provider personal-key status (set/unset) with no key value ever placed in the template context; recent usage rows carry the correct provider icon, including the fallback for an unrecognised provider
tests/output/report_test.php 2 Site-keys report rows carry the requesting user’s name and correct provider icon, excluding personal-key rows; empty state when there is no site-keys usage
tests/task/purge_old_logs_test.php 2 Rows older than the configured retention are deleted, newer rows are kept; a retention of 0 keeps every row indefinitely
Total 34 Β 
vendor/bin/phpunit --testsuite local_aihub

Line coverage by class (PHPUnit + Xdebug):

Class Line coverage
ai 85%
local\client 36%
local\export 83%
local\keys 92%
local\usage_log 85%
output\mykeys 100%
output\renderer 0%
output\report 100%
privacy\provider 86%
task\purge_old_logs 86%
Overall 73%

Two classes are intentionally not fully unit-tested, both for the same reason β€” the untested lines are live network calls, not business logic:

Behat β€” Acceptance Tests

Feature file Scenarios What is covered
tests/behat/mykeys.feature 2 A provider starts unconfigured; saving a personal key marks it configured without revealing the stored value
Total 2 Β 
php admin/tool/behat/cli/init.php
vendor/bin/behat --tags=@local_aihub --profile=chrome

πŸ” Security & Compliance

πŸ”Ž Third-party Service Disclosure

The hub transmits prompt text to a third-party AI provider only when a key is configured and a generation is requested.

Is an API key required?

No. The plugin installs and runs without any key β€” it simply reports that no source is available, and consumers fall back to their own core_ai integration. No external request is made until a site or personal key is set.

Supported providers

These services operate under their own terms of service and privacy policies.

Groq model choice

The Groq slot calls a single fixed model, openai/gpt-oss-120b. Groq periodically deprecates specific model IDs (e.g. llama-3.3-70b-versatile, decommissioned 2026-08-16) and expects migration to a named replacement. Among the free-tier options Groq recommended for this migration, openai/gpt-oss-120b was chosen over qwen/qwen3.6-27b for stronger instruction-following and more reliable JSON-mode output across the varied prompts sent by different consumer plugins.

How to obtain an API key

API keys are created directly on the provider’s official website. Gemini, Groq and DeepSeek currently offer free usage tiers or trial credits (pricing policies may change). The hub does not provide API keys.

Where keys are configured

  1. Personal key β€” set by each user in My AI keys (preferences), when personal keys are enabled and the user has the capability.
  2. Site key β€” set by the admin in Site administration β†’ Plugins β†’ Local plugins β†’ AI Hub.

Data Transmission

When a key resolves to a provider, the prompt text is transmitted to that provider’s API to generate the response:

The hub stores a usage log (who requested, which component, a short label of what was generated, provider, model, key tier and time) but does not store prompts or AI responses. All external destinations are declared in the plugin’s Privacy provider.

Demo credentials

Not applicable β€” no credentials are required to install or use the hub. Every AI feature stays inert until a site or personal key is configured.

πŸ“„ License

This project is licensed under the GNU General Public License v3 (GPLv3).

Copyright: 2026 Jean LΓΊcio