PHPackages                             kalakotra/silverstripe-aigateway - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. kalakotra/silverstripe-aigateway

ActiveSilverstripe-vendormodule[Utility &amp; Helpers](/categories/utility)

kalakotra/silverstripe-aigateway
================================

Standardised AI provider gateway layer for SilverStripe 6.1 — supports OpenAI, Gemini and Anthropic via Strategy pattern.

1.0.4(1mo ago)051proprietaryPHPPHP ^8.2

Since Apr 10Pushed 4w agoCompare

[ Source](https://github.com/kalakotra/silverstripe-aigateway)[ Packagist](https://packagist.org/packages/kalakotra/silverstripe-aigateway)[ RSS](/packages/kalakotra-silverstripe-aigateway/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (2)Versions (7)Used By (1)

AI Gateway for SilverStripe 6
=============================

[](#ai-gateway-for-silverstripe-6)

A standardized AI provider gateway for SilverStripe 6.1+ that abstracts multiple AI providers (OpenAI, Google Gemini, Anthropic Claude, Mittwald Open-LLM) behind a unified interface. Supports separate active providers per generation category (text / image).

Features
--------

[](#features)

- **Multi-provider support**: OpenAI GPT, Google Gemini, Anthropic Claude, Mittwald Open-LLM
- **Category-aware routing**: One active provider per category — a text provider and an image provider can be active simultaneously
- **Unified API**: Single `ask()` call regardless of provider
- **Audit logging**: Every call (success and failure) is persisted to `AILog` with token accounting and latency
- **CMS management**: Create, activate, and test providers from `/admin/ai-gateway/`
- **Connection testing**: GridField button — live API ping for text providers; API key format check for image providers
- **Typed exceptions**: `AIProviderException` carries HTTP status code for upstream error classification
- **Extensible**: Add new providers by implementing `AIProviderInterface`

Installation
------------

[](#installation)

```
composer require kalakotra/silverstripe-aigateway
vendor/bin/sake dev/build flush=all
```

CMS Setup
---------

[](#cms-setup)

1. Go to **CMS → AI Gateway → Providers**
2. Click **Add Provider** and fill in:
    - **Label** — human-readable name, e.g. `OpenAI GPT-4o Production`
    - **Provider** — select from dropdown
    - **Model Name** — exact model ID (see [Supported Providers](#supported-providers))
    - **Category** — `Text generation` or `Image generation`
    - **API Key** — your provider secret key
3. Click **⚡ Test** to verify the connection
4. Check **Set as Active Provider** and save

One provider per category may be active at a time. Activating a provider automatically deactivates any other active provider in the same category. Providers in different categories are independent.

Usage
-----

[](#usage)

### Text generation

[](#text-generation)

```
use Kalakotra\AIGateway\Services\AIGatewayService;
use Kalakotra\AIGateway\Exceptions\AIProviderException;
use SilverStripe\Core\Injector\Injector;

/** @var AIGatewayService $gateway */
$gateway = Injector::inst()->get('AIGateway');

try {
    // Full response DTO
    $response = $gateway->ask('Summarise this invoice in two sentences.', [
        'caller_class'   => self::class,
        'caller_context' => 'invoice-summary',
        'temperature'    => 0.4,
        'max_tokens'     => 256,
    ]);

    echo $response->content;        // Generated text
    echo $response->model;          // Resolved model (e.g. 'gpt-4o-2024-08-06')
    echo $response->totalTokens();  // input + output tokens
    echo $response->latencyMs;      // Request duration in ms

    // String-only shortcut
    $text = $gateway->askText('Write a meta description for: ' . $page->Title);

} catch (AIProviderException $e) {
    // $e->getHttpStatusCode() — 429 = rate limit, 401 = bad key, 0 = no provider configured
    error_log('[AI] ' . $e->getMessage());
}
```

### Image generation

[](#image-generation)

```
$response = $gateway->askImage('A photorealistic red apple on a white background', [
    'caller_class'   => self::class,
    'caller_context' => 'product-image',
]);
```

`askImage()` is a shortcut for `ask($prompt, ['category' => 'image', ...])`. The active image-category provider handles the call.

### Passing category explicitly

[](#passing-category-explicitly)

```
$response = $gateway->ask($prompt, ['category' => 'text']);   // default
$response = $gateway->ask($prompt, ['category' => 'image']);
```

### Available options

[](#available-options)

OptionTypeDefaultDescription`category`string`'text'`Which active provider to use`temperature`floatproviderSampling temperature (0.0 – 2.0)`max_tokens`intproviderMaximum output tokens`system`string—System prompt / persona`caller_class`string`''`Class that triggered the call (for logs)`caller_context`string`''`Context label (for logs)### Dependency injection

[](#dependency-injection)

```
class MySEOService
{
    public function __construct(
        private readonly AIGatewayService $gateway,
    ) {}

    public function generateMetaDescription(string $content): string
    {
        return $this->gateway->askText(
            'Write a 155-character meta description for: ' . $content,
            ['caller_class' => self::class, 'caller_context' => 'meta-description'],
        );
    }
}
```

Wire it in YAML:

```
SilverStripe\Core\Injector\Injector:
  MyApp\Services\MySEOService:
    constructor:
      gateway: '%$AIGateway'
```

YAML Configuration
------------------

[](#yaml-configuration)

The module ships with sensible defaults in `_config/aigateway.yml`. Override per environment:

```
# app/_config/aigateway.yml
---
Name: myproject-aigateway
After: aigateway
---

Kalakotra\AIGateway\Services\AIGatewayService:
  enable_logging: true   # set false in test suites
  http_timeout: 30       # Guzzle timeout in seconds
```

### Registering additional providers

[](#registering-additional-providers)

```
Kalakotra\AIGateway\Services\AIProviderRegistry:
  providers:
    mistral: MyApp\AI\Providers\MistralProvider
```

The slug (`mistral`) must match the `ProviderName` stored in `AIProviderConfig`.

Architecture
------------

[](#architecture)

### Request flow

[](#request-flow)

```
$gateway->ask($prompt, $options)
    │
    ├─ resolveActiveConfig(category)   ← AIProviderConfig::filter(IsActive, Category)
    │
    ├─ AIProviderRegistry::requireProviderClass(providerName)
    │
    ├─ Injector::createWithArgs(providerClass, [apiKey, modelName, timeout])
    │
    ├─ AIProviderInterface::sendPrompt($prompt, $providerOptions)
    │       ├─ buildRequestPayload()   → provider-specific JSON
    │       ├─ Guzzle POST             → upstream API
    │       └─ parseResponse()        → AIResponseDTO
    │
    ├─ AILog::createFromResponse()     (if enable_logging)
    │
    └─ return AIResponseDTO
         ↕ on exception
    AILog::createFromException() + re-throw AIProviderException

```

### Key classes

[](#key-classes)

ClassResponsibility`AIGatewayService`Orchestrator — resolves config, instantiates provider, logs`AIProviderRegistry`YAML-driven slug → class map`AIProviderConfig`DB record — credentials, model, category, active flag`AILog`Immutable audit entry per call`AIResponseDTO`Normalized response (content, model, tokens, latency, raw)`AIProviderException`Typed exception with HTTP status code### AIProviderConfig fields

[](#aiproviderconfig-fields)

FieldTypeNotes`Label`VarcharHuman-readable name`ProviderName`VarcharMust match registry slug`ModelName`VarcharExact model ID passed to the API`Category`Enum`text` (default) or `image``IsActive`BooleanOne active per category`APIKey`TextMasked in CMS; encrypt at rest in production`Notes`TextInternal CMS notesExtending with a custom provider
--------------------------------

[](#extending-with-a-custom-provider)

Extend `AbstractAIProvider` (handles Guzzle, timing, error mapping) and implement the three abstract methods:

```
namespace MyApp\AI\Providers;

use Kalakotra\AIGateway\Exceptions\AIProviderException;
use Kalakotra\AIGateway\Interfaces\AIResponseDTO;
use Kalakotra\AIGateway\Providers\AbstractAIProvider;

class MistralProvider extends AbstractAIProvider
{
    protected function getEndpointUrl(): string
    {
        return 'https://api.mistral.ai/v1/chat/completions';
    }

    protected function getRequestHeaders(): array
    {
        return [
            'Content-Type'  => 'application/json',
            'Accept'        => 'application/json',
            'Authorization' => 'Bearer ' . $this->apiKey,
        ];
    }

    protected function buildRequestPayload(string $prompt, array $options): array
    {
        return [
            'model'    => $this->modelName,
            'messages' => [['role' => 'user', 'content' => $prompt]],
        ];
    }

    protected function parseResponse(array $raw, string $providerSlug, string $model, float $latencyMs): AIResponseDTO
    {
        $content = $raw['choices'][0]['message']['content'] ?? null;

        if (!is_string($content)) {
            throw new AIProviderException(
                message: "[$providerSlug] Unexpected response shape.",
                providerSlug: $providerSlug,
                httpStatusCode: 200,
            );
        }

        return new AIResponseDTO(
            content: $content,
            model: $raw['model'] ?? $model,
            providerSlug: $providerSlug,
            inputTokens: (int) ($raw['usage']['prompt_tokens']     ?? 0),
            outputTokens: (int) ($raw['usage']['completion_tokens'] ?? 0),
            latencyMs: $latencyMs,
            rawResponse: $raw,
        );
    }

    public function getProviderSlug(): string { return 'mistral'; }

    public function validateApiKey(string $apiKey): bool
    {
        return strlen(trim($apiKey)) >= 32;
    }
}
```

Register the slug in YAML (see above), then create an `AIProviderConfig` record in the CMS.

Supported providers
-------------------

[](#supported-providers)

### OpenAI

[](#openai)

- **Models**: `gpt-4o`, `gpt-4o-mini`, `o3`, `o4-mini`
- **Key format**: starts with `sk-`, min 40 chars
- **Notes**: Reasoning models (`o*`) use `max_completion_tokens` automatically

### Google Gemini

[](#google-gemini)

- **Models**: `gemini-1.5-pro`, `gemini-2.0-flash`, `gemini-3-pro-image` (image category)
- **Authentication**: API key as query parameter (no `Authorization` header)
- **Notes**: Image-category providers are validated by key format only — no live generation call is made during the connection test

### Anthropic Claude

[](#anthropic-claude)

- **Models**: `claude-opus-4-7`, `claude-sonnet-4-6`, `claude-haiku-4-5-20251001`
- **Key format**: starts with `sk-ant-`

### Mittwald Open-LLM

[](#mittwald-open-llm)

- **Endpoint**: `https://llm.aihosting.mittwald.de/v1/chat/completions`
- **Protocol**: OpenAI-compatible (reuses `OpenAIProvider` request/response handling)
- **Notes**: Some open-source models return `content: null` on token truncation; the provider handles this gracefully by returning an empty string

Connection test behaviour
-------------------------

[](#connection-test-behaviour)

CategoryTest action`text`Sends `"Reply with exactly one word: pong"` — live API call, verifies key + model + quota`image`Validates API key format only — no generation call, no quota consumedError handling
--------------

[](#error-handling)

All failures throw `AIProviderException`:

```
use Kalakotra\AIGateway\Exceptions\AIProviderException;

try {
    $response = $gateway->ask($prompt);
} catch (AIProviderException $e) {
    $code = $e->getHttpStatusCode();
    // 0   = no active provider configured for the requested category
    // 401 = invalid API key
    // 429 = rate limit / quota exceeded
    // 5xx = upstream server error
}
```

Security
--------

[](#security)

- API keys are rendered via `PasswordField` in the CMS (not visible on screen)
- API keys are **not** exported via the GridField export button (export is disabled)
- For production: encrypt keys at rest using SilverStripe's symmetric encryption helpers or a secrets manager
- All prompts and responses are logged server-side only; never expose `AILog` data to end users
- Validate and sanitize user input before including it in prompts

Requirements
------------

[](#requirements)

- PHP 8.2+
- SilverStripe 6.1+
- Guzzle HTTP Client 7.8+

License
-------

[](#license)

Proprietary.

Changelog
---------

[](#changelog)

### v1.1.0

[](#v110)

- Added `Category` field (`text` / `image`) to `AIProviderConfig`
- One active provider allowed per category (text and image can be active simultaneously)
- Added `askImage()` convenience method
- Connection test is now category-aware: image providers skip live call, validate key format only
- `OpenAIProvider`: graceful handling of `finish_reason: length` with null content (Mittwald compatibility)

### v1.0.0

[](#v100)

- Multi-provider gateway (OpenAI, Gemini, Anthropic, Mittwald Open-LLM)
- `ask()` / `askText()` unified API
- `AILog` audit logging with token accounting
- CMS provider management and connection testing

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance92

Actively maintained with recent releases

Popularity4

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity50

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~6 days

Total

6

Last Release

29d ago

Major Versions

1.0.4 → 5.x-dev2026-05-11

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/3845734?v=4)[Kalakotra](/maintainers/kalakotra)[@kalakotra](https://github.com/kalakotra)

---

Top Contributors

[![kalakotra](https://avatars.githubusercontent.com/u/3845734?v=4)](https://github.com/kalakotra "kalakotra (7 commits)")

### Embed Badge

![Health badge](/badges/kalakotra-silverstripe-aigateway/health.svg)

```
[![Health](https://phpackages.com/badges/kalakotra-silverstripe-aigateway/health.svg)](https://phpackages.com/packages/kalakotra-silverstripe-aigateway)
```

###  Alternatives

[neuron-core/neuron-ai

The PHP Agentic Framework.

1.9k496.1k32](/packages/neuron-core-neuron-ai)[tencentcloud/tencentcloud-sdk-php

TencentCloudApi php sdk

3751.2M45](/packages/tencentcloud-tencentcloud-sdk-php)[civicrm/civicrm-core

Open source constituent relationship management for non-profits, NGOs and advocacy organizations.

744284.3k34](/packages/civicrm-civicrm-core)[roundcube/roundcubemail

The Roundcube Webmail suite

7.0k1.4k3](/packages/roundcube-roundcubemail)[spatie/laravel-export

Create a static site bundle from a Laravel app

670139.5k6](/packages/spatie-laravel-export)[nfse-nacional/nfse-php

This is my package nfse

1493.1k](/packages/nfse-nacional-nfse-php)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
