Skip to content

Initial commit, add base structure, vue frontend#1

Open
maxtaube wants to merge 17 commits into
5.x-devfrom
ID-35-create-AIProviders-plugin
Open

Initial commit, add base structure, vue frontend#1
maxtaube wants to merge 17 commits into
5.x-devfrom
ID-35-create-AIProviders-plugin

Conversation

@maxtaube

@maxtaube maxtaube commented May 20, 2026

Copy link
Copy Markdown
Collaborator

Description:

Adds the AIProviders plugin, which gives Matomo a shared server-side integration layer for AI features. It supports Claude, OpenAI, Gemini, and OpenAI-compatible custom endpoints, with an admin UI for self-managed instances to configure credentials, default provider, model settings, and connection tests.

Other plugins should not call completion endpoints through API.php; those methods are only for the administration UI. Runtime use goes through AIProviderService:

  • complete(new AIRequest($prompt, $callerPlugin)) for prompt-in/text-out or JSON responses.
  • converse(new AIConversationRequest($messages, $callerPlugin)) for multi-turn conversations and tool-calling flows.

Requests use immutable request objects for:

  • caller and feature metadata
  • provider/model hints
  • generation options such as max tokens and temperature
  • JSON mode for simple completions
  • optional site context for completion requests
  • tool catalogs for conversation requests

Conversation messages use a provider-agnostic canonical shape, and each provider translates that shape to its own wire format. Responses expose text/content, provider/model metadata, token usage where available, stop reason, and timing.

Self-managed instances can configure providers in the UI or via server-side configuration. In a managed environment, provider configuration is locked server-side, the admin screen is hidden, and normal plugin requests are routed through the managed default provider. Provider/model selection is only honored for selected trusted server-side callers.

A custom provider can either be configured through the built-in Custom Provider option in the UI, using an OpenAI-compatible base URL plus an optional API key, or registered by another plugin via the AIProviders.addAIProviders event with a custom AIProvider class.

Includes unit and integration coverage for configuration resolution, managed-environment behavior, provider registration, completion requests, JSON responses, conversation support, and provider-specific request/response translation.

Review

  • Functional review done
  • Potential edge cases thought about (behavior of the code with strange input, with strange internal state or possible interactions with other Matomo subsystems)
  • Usability review done (is anything maybe unclear or think about anything that would cause people to reach out to support)
  • Security review done see checklist
  • Code review done
  • Tests were added if useful/possible
  • Reviewed for breaking changes
  • Developer changelog updated if needed
  • Documentation added if needed
  • Existing documentation updated if needed

@maxtaube maxtaube marked this pull request as ready for review June 9, 2026 07:48
maxtaube and others added 3 commits June 10, 2026 13:07
Support JSON completion requests across providers, expose decoded JSON responses, prevent duplicate provider registrations, and hide AI provider settings in managed environments.
@maxtaube maxtaube requested a review from a team June 18, 2026 00:00
maxtaube added 2 commits June 18, 2026 11:03
Add AIProvider exception hierarchy, properly implemented test connection function, fixed custom provider model implementation, dropped rawReponse key from the AIProviderResponse
Add converse() methods for plugins that need conversations with tool calling. Added canonical message shapes
Comment thread lang/en.json Outdated
"DefaultProviderHelp": "Connect providers and pick a default for Matomo to use. Individual features may override this and choose their preferred connected provider.",
"DefaultsTitle": "Change provider settings and default values",
"Disconnect": "Disconnect",
"Disconnecting": "Disconnecting...",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Disconnecting": "Disconnecting...",
"Disconnecting": "Disconnecting",

Same for TestingConnection, having a single character ellipsis usually looks nicer that separate dots.

Comment thread .gitignore
tests/System/processed/*xml
/vue/dist/demo.html
/vue/dist/*.common.js
/vue/dist/*.map

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of the ignored files have already been committed and should be removed to prevent them from not being updated.

Comment thread plugin.json Outdated
{
"name": "AIProviders",
"description": "Configure AI provider connections and default model settings used by Matomo AI features.",
"version": "0.1.0",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"version": "0.1.0",
"version": "5.0.0",

As this is intended to be integrated into Core anyway this is more nit-picky than relevant. Plugins usually share the major version with the support core major, but after a full integration it will bump itself to the core version anyway.

Comment thread API.php Outdated

private function getConfiguration(): Configuration
{
return StaticContainer::get(Configuration::class);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be done using DI in the constructor. Same for the Controller and Menu.

I would only keep the one in the AIProvidersList unless that class is also going to be handled with a container lookup.

Comment thread AIProviders.php
*
* Provider IDs are unique and cannot be overwritten: the first registration
* for an ID wins (see {@link AIProvidersList::addProvider()}). This protects
* the built-in providers, and a provider that is centrally forced in a

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes-ish. Event registration supports before and after for grouping, but otherwise should be treated as random. The actual order is generated by the plugin loading (PluginList#107) but I would not rely on that.

So the strong guarantee is that a duplicated registration will raise a warning. Everthing else depends on plugin sorting and how the individual events are tagged.

{{ translate('General_Cancel') }}
</button>
<SaveButton
:disabled="!hasUnsavedChanges"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just me doing intentionally weird things, but if you have a fully unconfigured plugin, and then only switch the capability level, you can save.

But the then still empty defaultProviderId only generates an error because Matomo does not like the combination of a required parameter and an empty string:

Please specify a value for 'defaultProviderId'.

#0 /core/API/Proxy.php(217): Piwik\\API\\Proxy-&gt;getSanitizedRequestParametersArray(Array, Array)
#1 /core/Context.php(29): Piwik\\API\\Proxy-&gt;Piwik\\API\\{closure}()
#2 /core/API/Proxy.php(386): Piwik\\Context::executeWithQueryParameters(Array, Object(Closure))

method: 'AIProviders.saveSettings',
},
{
defaultProviderId: defaultProviderId.value,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird/interesting edge case:

If I have no model configured, the missing defaultProviderId exception prevents me from saving invalid API keys. After connecting at least one provider I can then save any invalid API key I like.

Saving invalid API keys is imho not a bad thing, because it might still let you save the the settings of one provider if any other provider's key has been revoked. But maybe the current behaviour is also intentional, configuring the first provider without a valid key does not feel like a regular use case.

Comment thread README.md
openaiEndpointUrl = "..." ; or env MATOMO_AIPROVIDERS_OPENAI_ENDPOINT_URL
```

The config key is the provider ID verbatim plus the field suffix; the environment variable upper-cases the ID and replaces `-` with `_`. Credentials supplied this way never appear in the UI as secret values and cannot be edited or removed there.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't know how known that structure is, but the config file can change per tenant in a multi-tenant setup. Doing that with environment variables would be possible but tricky.

Not worth to be pointed out in the README until someone complains, just wanted to mention the multi-tenant config file :)

$responseBody,
&$capturedUrl,
&$capturedBody
): void {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
): void {
): void {

Just in case you want to set up a CI configuration that would catch this.

Comment thread Provider/AIProvider.php Outdated
$payload = [
'model' => $model,
'messages' => $messages,
'max_tokens' => $request->getMaxTokens(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a commit in the history that changed this to max_completion_tokens. A the following commit has changed that value right back to max_tokens.

The max_completion_tokens change would have nicely broken a lot of tests. But there is nothing that would run those tests.

There will be automated testing, right? Including a happy-path UI test to ensure it works?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants