This document describes the public API for other Shopware plugins that want to use the merchant’s configured AI providers.
| Symbol | Purpose |
|---|---|
Frosh\AI\Client\AiClient |
Main facade — inject this |
frosh_ai.client |
Stable DI alias (decoration) |
Frosh\AI\Client\PromptBuilder |
Fluent prompt API |
Frosh\AI\Contract\AiProviderInterface |
Implement only if you add a provider |
Frosh\AI\Contract\LlmClientInterface |
Low-level transport (rare) |
Frosh\AI\Contract\ModelCatalogInterface |
Model list / capabilities |
ChatRequest, ChatResponse, ChatMessage, ModelPreference |
Value objects |
Capability, MessageRole, FinishReason |
Enums |
AiException hierarchy |
Errors |
AiChatBeforeEvent, AiChatAfterEvent, AiChatFailedEvent |
Hooks |
Do not depend on: OpenAI HTTP clients, config keys (FroshAI.config.*), device-auth internals, or admin Vue routes.
{
"require": {
"frosh/ai": "^0.1"
}
}<?php declare(strict_types=1);
namespace Acme\Seo;
use Frosh\AI\Client\AiClient;
use Frosh\AI\Exception\NoProviderAvailableException;
final class ProductTitleSuggester
{
public function __construct(
private readonly AiClient $ai,
) {
}
public function suggest(string $description): ?string
{
if (!$this->ai->isAvailable()) {
return null;
}
try {
return $this->ai->prompt($description)
->system('Suggest a concise product title. Return only the title.')
->prefer('openai', 'gpt-4o')
->prefer('anthropic', 'claude-sonnet-4-6')
->temperature(0.3)
->maxTokens(64)
->withMetadata([
'extension' => 'AcmeSeo',
'feature' => 'product-title',
])
->generateText();
} catch (NoProviderAvailableException) {
return null;
}
}
}<service id="Acme\Seo\ProductTitleSuggester">
<argument type="service" id="Frosh\AI\Client\AiClient"/>
<!-- or: id="frosh_ai.client" -->
</service>Order at runtime:
- Explicit
usingProvider()/usingModel()on the request - Ordered
prefer(provider, model)pairs that are configured and match the required capability - Shop default models (Settings → Default models): text vs image
- First configured provider that advertises the capability
generateText() always requires text_generation. Image generation:
use Frosh\AI\Enum\Capability;
$image = $this->ai->prompt('Studio product photo of a red sneaker on white background')
->prefer('openai', 'gpt-image-1')
->imageSize('1024x1024')
->withMetadata([
'extension' => 'AcmeMedia',
'feature' => 'product-image',
])
->generateImage();
$b64 = $image->first()?->b64Json;
// or $image->first()?->url / dataUri()Requires an OpenAI API key (Images API). ChatGPT device login alone cannot generate images; you may keep device auth for chat and still set an API key for images.
If nothing matches → NoProviderAvailableException.
If the provider has no Images transport → ImageGenerationNotSupportedException.
Capability defaults (merchant UI)
| Capability | Config keys |
|---|---|
text_generation |
defaultTextProvider + defaultTextModel (legacy fallback: openAiDefaultModel on OpenAI) |
image_generation |
defaultImageProvider + defaultImageModel |
Provider-level “default model” on OpenAI / compatible endpoints is a text fallback for that provider only.
->withMetadata([
'extension' => 'AcmeSeo',
'feature' => 'meta-description',
'entity' => 'product',
'entityId' => $productId,
])use Frosh\AI\Event\AiChatAfterEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
final class AiUsageSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [AiChatAfterEvent::class => 'onAfter'];
}
public function onAfter(AiChatAfterEvent $event): void
{
$usage = $event->getResponse()->usage;
// log $event->getDurationMs(), tokens, metadata…
}
}AiChatBeforeEvent—setRequest()to inject system guidelinesAiChatAfterEvent— success metricsAiChatFailedEvent— failure metrics (exception is still re-thrown)
Merchants can add any Chat Completions-compatible server in the admin (Providers → Add compatible API):
| Field | Example |
|---|---|
| Name | Ollama |
| Technical id | ollama → provider id compat.ollama |
| Base URL | http://127.0.0.1:11434/v1 |
| API key | optional (empty for many local servers) |
| Default model | llama3.2 (text_generation only) |
Compatible endpoints are treated as text providers. They do not appear in the image default picker.
Extensions prefer them like any other provider:
->prefer('compat.ollama', 'llama3.2')
->prefer('compat.openrouter', 'openai/gpt-4o')
->prefer('openai', 'gpt-4o')<service id="Acme\AI\AnthropicProvider">
<tag name="frosh_ai.provider"/>
</service>Implement AiProviderInterface including getDescriptor() and getAdminStatus().
Optional admin form: register Vue component frosh-ai-provider-{id}.
- Providers — list + per-provider detail
- Playground — exercises the same
AiClientpath as extensions
Admin HTTP (/_action/frosh-ai/*) is for admin UIs. Storefront and other plugins’ business logic should call PHP AiClient, not those routes.
Every chat and image call is logged via AiChat* / AiImage* events into frosh_ai_usage_log:
- provider, model, capability, duration, tokens (chat)
- feature / extension / entity from request metadata
- success vs failed (+ truncated error)
- Prompts and response bodies are not stored
Admin: Settings → Frosh AI → Usage (stats, feature/provider filters, expandable errors, CSV export, purge all / older-than-N).
| Endpoint | Method | Notes |
|---|---|---|
/api/_action/frosh-ai/usage |
GET |
Paginated list. Query: page, limit, status, type, feature, provider. Response includes stats, facets (distinct features/providers), retentionDays. |
/api/_action/frosh-ai/usage/stats |
GET |
Aggregates + facets + retention. |
/api/_action/frosh-ai/usage/export |
GET |
CSV (max 10 000 rows), same filter query params. UTF-8 BOM for Excel. |
/api/_action/frosh-ai/usage |
DELETE |
Purge all, or ?olderThanDays=N for aged rows only. |
Retention: plugin config usageRetentionDays (default 90, 0 = keep forever). Daily scheduled task frosh_ai.cleanup_usage_logs deletes rows older than that.
Run plugin migrations after update so the table exists. Register scheduled tasks after install/update (bin/console scheduled-task:register or plugin lifecycle).
Master switch + per-ability toggles (Settings → Frosh AI → General):
| Id | Controls |
|---|---|
(master) featuresEnabled |
All abilities |
product_description |
Product description generation |
product_title |
Product title (name) generation |
product_seo |
Meta title/description, keywords, Open Graph text |
image_generation |
Image generation (playground + AiClient::generateImage) |
A feature is effective only when master AND the ability flag are on. Missing ability keys default to on.
PHP: Frosh\AI\Feature\FeatureFlags / FeatureId.
Admin API: GET|POST /api/_action/frosh-ai/features (also on status.featureFlags).
Catalog product form + SEO tab show a single Generate with AI button that opens a modal:
- Field checkboxes (title, description, meta title/description, keywords, Open Graph)
- Only fill empty fields (default on); overwrite requires confirm
- Per-field progress (queued / generating / done / skipped / failed)
- Last field selection, tone, and fill-empty preference remembered per screen (
localStorage) - Shared tone presets + optional extra instructions
- Language = product content language (admin language switcher), not the admin UI locale
- Uses Default models → Text
| Flag | API |
|---|---|
product_title |
POST …/product/generate-title → title |
product_description |
POST …/product/generate-description → description (HTML) |
product_seo |
POST …/product/generate-seo → metaTitle, metaDescription, keywords, ogTitle, ogDescription |
Common payload: product facts, tone, languageId / language / languageName, optional extraInstructions.
- Tones:
GET|POST /api/_action/frosh-ai/product/tones, reset via…/tones/reset - Status:
GET …/product/description-status→enabled,titleEnabled,seoEnabled, tones - Admin component:
frosh-ai-product-generate-modal - PHP:
ProductDescriptionGenerator,ProductTitleGenerator,ProductSeoGenerator,ProductCopySupport,ProductDescriptionToneStore
Draft fields are filled in admin only; the merchant must save the product.
Provider id: gemini (models.dev catalog key: google).
| Area | Details |
|---|---|
| Auth | AI Studio API key (FroshAI.config.geminiApiKey) |
| Chat | POST …/v1beta/models/{model}:generateContent |
| Image | Native gemini-*-image via generateContent + responseModalities: [TEXT, IMAGE]; Imagen models via :predict |
| Defaults | geminiDefaultModel (text), geminiDefaultImageModel (image) |
| Admin | Settings → Frosh AI → Providers → Google Gemini |
Example:
$text = $ai->prompt('Write a short tagline for sneakers.')
->prefer('gemini', 'gemini-2.5-flash')
->generateText();
$images = $ai->prefer('gemini', 'gemini-2.5-flash-image')
->generateImage(new \Frosh\AI\Request\ImageRequest(
prompt: 'Product photo of red running shoes on white background',
));Shop-wide: set Default models → Text/Image provider to gemini when it should be the global default.
Edit existing pictures with a text instruction (image + prompt → image).
| Flag | image_edit (FroshAI.config.featureImageEdit) |
| API | Same AiClient::generateImage() when sourceImages is non-empty; alias editImage() |
| Gemini | Native image models: generateContent with inlineData + text + responseModalities: [TEXT, IMAGE] |
| OpenAI | POST /v1/images/edits (multipart; first source image; optional mask) |
| Admin | Playground → Image editing (upload + instruction); Save to media library |
| HTTP | POST /api/_action/frosh-ai/playground/image with sourceImages: [{ dataUri|b64, mimeType? }]; POST …/media/save-image |
use Frosh\AI\Request\ImageInput;
$result = $ai->prompt('Remove background, pure white studio, keep product sharp')
->prefer('gemini', 'gemini-2.5-flash-image')
->withSourceImage($base64Png, 'image/png')
->editImage();
// Or:
$ai->generateImage(new \Frosh\AI\Request\ImageRequest(
prompt: 'Soft lifestyle background',
provider: 'openai',
model: 'gpt-image-1',
sourceImages: [ImageInput::fromBase64($b64, 'image/png')],
));Usage log: type=image, metadata.mode=edit|generate, sourceImageCount — no image bytes stored.
If your plugin must run without Frosh AI installed, define your own interface and bind it only when AiClient exists, or make Frosh AI an optional suggest requirement and guard features with isAvailable().