Skip to content

Generative AI (Magic AI)

Generative AI (Magic AI) is the core package packages/Webkul/MagicAI. It generates text and images for the admin, and powers image search, review translation and the checkout success message on the storefront. It is built on the Laravel AI SDK (laravel/ai) and picks the provider for each call from the model it is given; this page is the developer's side, and the user guide covers the admin screens.

PieceWhere
Service classWebkul\MagicAI\MagicAI, in src/MagicAI.php
FacadeWebkul\MagicAI\Facades\MagicAI
Helpermagic_ai(), in src/Http/helpers.php
Provider and model registryWebkul\MagicAI\AiProvider, in src/AiProvider.php
One model enum per providerWebkul\MagicAI\Enums\Models\*, each implementing Webkul\MagicAI\Enums\Contracts\AiModelContract
Service providerWebkul\MagicAI\Providers\MagicAIServiceProvider, which only loads the helper
Configuration fieldspackages/Webkul/Admin/src/Config/system.php, under magic_ai

The package has no models, migrations or Concord module.

Providers and Models

AiProvider registers eight providers. Each is a case of the SDK's Laravel\Ai\Enums\Lab enum and has a model enum whose case values are the model identifiers you pass:

ProviderModel enumExample text modelsImage modelsDefault text model
anthropicAnthropicModelclaude-opus-4-6, claude-sonnet-4-6, claude-haiku-4-5-20251001noneclaude-haiku-4-5-20251001
deepseekDeepSeekModeldeepseek-chat, deepseek-reasonernonedeepseek-chat
geminiGeminiModelgemini-2.5-pro, gemini-2.5-flash, gemini-3-flash-previewimagen-4.0-generate-001, imagen-4.0-ultra-generate-001, imagen-4.0-fast-generate-001, imagen-3.0-generate-002gemini-3-flash-preview
groqGroqModelllama-3.3-70b-versatile, openai/gpt-oss-120b, qwen/qwen3-32bnonellama-3.1-8b-instant
mistralMistralModelmistral-large-latest, mistral-small-latest, magistral-medium-2509nonemistral-small-latest
ollamaOllamaModelllama4:scout, llama3.3:70b, qwen3:8b, gemma3:27bnonellama3.2:3b
openaiOpenAiModelgpt-5.2, gpt-5, gpt-4.1, gpt-4.1-minigpt-image-1.5, gpt-image-1gpt-4.1
xaiXAiModelgrok-4, grok-4-1-fast, grok-3grok-imagine-image, grok-2-imagegrok-3

The enum cases in packages/Webkul/MagicAI/src/Enums/Models are the complete list.

Configuration

Every setting is a system configuration field, read with core()->getConfigData(). The storefront fields are channel-based.

KeyTypeUsed for
magic_ai.general.settings.enabledbooleanMaster switch, checked together with each feature's own switch
magic_ai.providers.<provider>.api_keypasswordAPI key, one per provider
magic_ai.providers.ollama.urltextOllama server URL, default http://localhost:11434
magic_ai.admin_features.text_generation.enabledbooleanText generation in the admin's TinyMCE editors
magic_ai.admin_features.text_generation.providersmultiselectProviders whose text models the editor offers
magic_ai.admin_features.image_generation.enabledbooleanImage generation in the admin image uploader
magic_ai.admin_features.image_generation.providersmultiselectProviders whose image models the uploader offers
magic_ai.storefront_features.image_search.enabledbooleanImage search keywords from AI. The storefront shows the image upload only while catalog.products.settings.image_search, on by default, is also on
magic_ai.storefront_features.image_search.modelselectModel for analyzeImage()
magic_ai.storefront_features.review_translation.enabledbooleanTranslation of reviews
magic_ai.storefront_features.review_translation.modelselectModel for translate()
magic_ai.storefront_features.checkout_message.enabledbooleanThe generated message on the order success page
magic_ai.storefront_features.checkout_message.modelselectModel for checkoutMessage()

API keys are saved as configuration values in the database. On each call, Magic AI copies the key of the model's provider into the SDK's ai.providers.<provider>.key configuration, and for Ollama the URL into ai.providers.ollama.url. When a provider's field is empty it copies nothing, and the SDK's own key from .env applies, as read in config/ai.php (for example OPENAI_API_KEY).

Calling It

The helper and the facade both give you the Webkul\MagicAI\MagicAI service:

MethodReturnsModel used
generateContent(string $prompt, ?string $model = null): stringThe generated text, trimmedThe one you pass
generateImage(string $prompt, array $options = [], ?string $model = null): arrayA list of ['url' => 'data:<mime>;base64,...'] itemsThe one you pass
analyzeImage(string $imagePath): stringComma-separated search keywords for the product in a local image filemagic_ai.storefront_features.image_search.model
translate(string $content, string $locale): stringThe content translated into $localemagic_ai.storefront_features.review_translation.model
checkoutMessage(mixed $order): stringPlain text built from the order's items, the customer's name, the current locale and the channel namemagic_ai.storefront_features.checkout_message.model

The last three read their model for the current channel. When none is saved, they use gpt-4.1, the default text model of AiProvider::defaultTextProvider() (openai), which needs an OpenAI key even when the store configured another provider.

php
use Webkul\MagicAI\Facades\MagicAI;

$description = magic_ai()->generateContent(
    'Write a two-sentence description of a brown leather wallet.',
    'gpt-4.1',
);

$summary = MagicAI::generateContent('Summarise our returns policy in one line.', 'claude-haiku-4-5-20251001');

Images

generateImage() takes up to three options:

OptionValuesDefault
nNumber of images1
size1:1, 3:2 or 2:31:1
qualityhigh, medium or lowNot sent
php
$images = magic_ai()->generateImage('A running shoe on a plain white background', [
    'n' => 2,
    'size' => '3:2',
    'quality' => 'high',
], 'gpt-image-1');

$firstImageUrl = $images[0]['url'];

Each image is a separate request to the provider. Every provider receives the size and quality as SDK options; for providers other than OpenAI and Gemini they are also written into the prompt.

How the Core Features Call It

The admin controller is packages/Webkul/Admin/src/Http/Controllers/MagicAIController.php; the storefront controllers are in packages/Webkul/Shop/src/Http/Controllers/.

FeatureEntry pointWhat it does
Admin textMagicAIController::content(), route admin.magic_ai.contentValidates prompt and an optional model, and returns { "content": ... }, or a 500 with the exception message
Admin imagesMagicAIController::image(), route admin.magic_ai.imageValidates prompt, model, n (1 to 10), size and quality, and returns { "images": [...] }
Image searchSearchController::upload(), route shop.search.uploadStores the uploaded image and, when both switches are on, returns keywords from analyzeImage() with engine set to ai. Otherwise, or when the call throws, engine is tensorflow and the page classifies the image in the browser with TensorFlow.js
Review translationAPI\ReviewController::translate(), route shop.api.products.reviews.translateTranslates an approved review into the current locale's name
Checkout messageOnepageController::success()When both switches are on, sets $order->checkout_message for shop::checkout.success; if the call throws, the message is left out

The admin's tinymce and media/images components show their generate option when the master switch and the feature's enabled are both on, and list the models of the feature's providers through AiProvider::modelsForProviders(). The storefront's products/view/reviews.blade.php shows the translate link when review_translation.enabled is on.

Using It from Your Package

A package that generates content should let the merchant choose the model, respect the master switch and cope with a failed call. The example is a package that writes a one-sentence product summary. First, a configuration field offers every text model. System Configuration covers registering the file and its translations:

File: packages/Webkul/ProductSummary/src/Config/system.php

php
<?php

use Webkul\MagicAI\AiProvider;

return [
    [
        'key' => 'product_summary',
        'name' => 'product_summary::app.configuration.title',
        'info' => 'product_summary::app.configuration.info',
        'sort' => 10,
    ], [
        'key' => 'product_summary.general',
        'name' => 'product_summary::app.configuration.general.title',
        'info' => 'product_summary::app.configuration.general.info',
        'icon' => 'settings/settings.svg',
        'sort' => 1,
    ], [
        'key' => 'product_summary.general.settings',
        'name' => 'product_summary::app.configuration.general.settings.title',
        'info' => 'product_summary::app.configuration.general.settings.info',
        'sort' => 1,
        'fields' => [
            [
                'name' => 'model',
                'title' => 'product_summary::app.configuration.general.settings.model',
                'type' => 'select',
                'channel_based' => true,
                'options' => AiProvider::textModelOptions(),
            ],
        ],
    ],
];

Then a class calls Magic AI with the saved model:

File: packages/Webkul/ProductSummary/src/Services/SummaryWriter.php

php
<?php

namespace Webkul\ProductSummary\Services;

use Exception;

class SummaryWriter
{
    /**
     * Summarise a product description in one sentence, or return null when that isn't possible.
     */
    public function summarise(string $description): ?string
    {
        if (! core()->getConfigData('magic_ai.general.settings.enabled')) {
            return null;
        }

        $model = core()->getConfigData('product_summary.general.settings.model');

        if (! $model) {
            return null;
        }

        try {
            return magic_ai()->generateContent(
                "Summarise this product description in one sentence:\n\n{$description}",
                $model,
            );
        } catch (Exception $e) {
            report($e);

            return null;
        }
    }
}

Resolve SummaryWriter from the container in a controller, a listener or a queued job. Each call waits for the provider, so generate in a queued job when you work through many products.

Listing Models in Your Own Interface

AiProvider's static methods return what the admin forms use:

MethodReturns
textModelOptions(), imageModelOptions()Every text or image model as ['title' => 'OpenAI: GPT-4.1', 'value' => 'gpt-4.1']
textProviderOptions(), imageProviderOptions()The providers with at least one text or image model, in the same shape
modelsForProviders(array $enabledProviders, string $type = 'text')The text or image models of the given providers, in the same shape
resolveModel(string $model): ?AiModelContractThe enum case for a model value, or null
defaultTextModel(string $provider), defaultImageModel(string $provider)A provider's recommended model case, or null
isProviderSupported(string $provider): boolWhether the provider is registered
label(string $provider): stringThe provider's display name

Changing a Prompt

The facade and the helper resolve Webkul\MagicAI\MagicAI from the service container, so a package can bind a subclass instead of editing core. The methods that build prompts are protected:

File: packages/Webkul/ProductSummary/src/MagicAI/ShortCheckoutMessage.php

php
<?php

namespace Webkul\ProductSummary\MagicAI;

use Webkul\MagicAI\MagicAI;

class ShortCheckoutMessage extends MagicAI
{
    /**
     * Build a shorter checkout success prompt.
     */
    protected function buildCheckoutPrompt(mixed $order): string
    {
        return implode("\n\n", [
            'Write a two-sentence thank-you message for this order. Return plain text only.',
            "Customer: {$order->customer_full_name}",
            'Store: '.core()->getCurrentChannel()->name,
        ]);
    }
}

Bind the subclass in the register() method of your package's existing service provider. The sample shows only the binding, so keep whatever else that method already does, such as merging Config/system.php:

File: packages/Webkul/ProductSummary/src/Providers/ProductSummaryServiceProvider.php

php
<?php

namespace Webkul\ProductSummary\Providers;

use Illuminate\Support\ServiceProvider;
use Webkul\MagicAI\MagicAI;
use Webkul\ProductSummary\MagicAI\ShortCheckoutMessage;

class ProductSummaryServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     */
    public function register(): void
    {
        $this->app->bind(MagicAI::class, ShortCheckoutMessage::class);
    }
}

A protected method isn't a stable interface, so compare your override with packages/Webkul/MagicAI/src/MagicAI.php after each upgrade.

There is no extension point for adding a provider or a model. AiProvider::$providers is private static and the model enums are core files, so anything added there is lost on the next update.

Testing Code That Calls It

The Laravel AI SDK ships fakes, so a test never reaches a provider. generateContent(), translate(), analyzeImage() and checkoutMessage() prompt an anonymous agent through the SDK's agent() helper, which Laravel\Ai\AnonymousAgent::fake() answers. generateImage() goes through Laravel\Ai\Image, which Image::fake() answers with placeholder images:

File: packages/Webkul/ProductSummary/tests/Feature/MagicAITest.php

php
<?php

use Laravel\Ai\AnonymousAgent;
use Laravel\Ai\Image;

it('generates text without calling the provider', function () {
    AnonymousAgent::fake(['A slim brown leather wallet.']);

    expect(magic_ai()->generateContent('Describe a wallet.', 'gpt-4.1'))
        ->toBe('A slim brown leather wallet.');

    AnonymousAgent::assertPrompted('Describe a wallet.');
});

it('generates images without calling the provider', function () {
    Image::fake();

    $images = magic_ai()->generateImage('A running shoe', ['n' => 2], 'gpt-image-1');

    expect($images)->toHaveCount(2);

    Image::assertGenerated(fn ($prompt) => str_contains($prompt->prompt, 'A running shoe'));
});

The tests need a booted application with a database, because Magic AI reads the provider key from configuration; Writing Tests for a Package sets up the test case and the suite. assertPrompted() with a string compares the whole prompt, so for the storefront methods, whose prompts Magic AI builds around your text, pass a closure that inspects $prompt->prompt instead.

Things to Watch

  • Always pass a model from the enums. With no model, or a value no enum contains, Magic AI resolves no provider and copies no key. The SDK then uses its defaults from config/ai.php: ai.default (openai, with OPENAI_API_KEY) for text, and ai.default_for_images (gemini, with GEMINI_API_KEY) for images.
  • Image search needs a model that accepts images. analyzeImage() sends the photo as an attachment, but the model field lists every text model. When the call fails, SearchController reports the exception and the storefront falls back to TensorFlow.js.
  • Check the switches in your own endpoints. If your package exposes a route that calls Magic AI, check magic_ai.general.settings.enabled and your feature's setting in the controller, not only in the view that shows the button.
  • Any signed-in admin can call the core generate routes. admin.magic_ai.content and admin.magic_ai.image are in the unrestricted list of packages/Webkul/User/src/Http/Middleware/Bouncer.php, whatever the admin's role.
  • Calls run inside the request. The response waits for the provider, including on the checkout success page.
  • laravel/ai is pre-1.0. Bagisto 2.5 requires ^0.7.0 where Bagisto 2.4 required ^0.2.2. Magic AI's own code didn't change between them, but code of yours that calls Laravel\Ai directly should be checked again on each upgrade.
  • WebMCP: storefront tools that a browser agent calls, a separate feature that doesn't use Magic AI.
  • System Configuration: registering the configuration fields your package reads.
  • Testing with Pest: the test case and suites for package tests.
  • AI in Bagisto: how the generative and agentic features fit together.

Released under the MIT License.