AI providers
Setup and editor features are described in AI Assistance.
Provider layer
Drivers implement Renick\Content\Classes\Ai\AiProvider:
interface AiProvider
{
public function chat(array $messages, ?array $schema = null, array $options = []): AiResponse;
public function listModels(): array;
public function testConnection(): ConnectionResult;
}OpenAiDriver (https://api.openai.com/v1) and OpenRouterDriver (https://openrouter.ai/api/v1) share OpenAiCompatibleDriver, built on Laravel's Http client, so tests can use Http::fake(). MockDriver answers offline.
ProviderManager::make($feature)returns the driver with thetranslateordraftmodel. An empty model setting falls back toAiSetting::DEFAULT_MODELSfor the active provider (gpt-5.6-terra, on OpenRouteropenai/gpt-5.6-terra). It throws once the monthly token budget is reached.ProviderManager::recordUsage()logs token counts torenick_content_ai_usage.- When
chat()gets a JSON schema, the driver requests OpenAIjson_schemastructured output. - Reasoning models (GPT-5 family, o-series) get no
temperature, and OpenAI requests sendmax_completion_tokens. If another model rejects the temperature, the request is retried once without it. - Reasoning models also get
reasoning_effortfromrenick.content::limits.ai_reasoning_effort(RENICK_CONTENT_AI_REASONING_EFFORT, defaultlow; empty keeps the model default). A chat request getsai_timeoutseconds in total (RENICK_CONTENT_AI_TIMEOUT, default 300), split over two attempts, before a readable timeout message is shown. On OpenRouter, OpenAI models are routed to OpenAI's standard endpoint (provider.order, flex tier ignored). - Network failures, timeouts, HTTP 5xx and 429 answers become translated messages (
TransportError); the raw cURL or response detail is logged as a warning. Other 4xx answers keep the provider's reason, such as an unknown model. - Over REST,
POST /translateandPOST /drafthave their own limit of 10 requests per minute per token, see REST API.
The beforeRequest event
Right before the HTTP call, chat payloads pass through a halting event. Image and Firecrawl requests do not. Return an array to replace the payload:
Event::listen('renick.content.ai.beforeRequest', function (&$payload) {
Log::info('AI request', ['model' => $payload['model']]);
// optionally: return $modifiedPayload;
});Mock driver
Provider mock needs no key. Translated values come back prefixed "[LANG] ", drafts return one heading block Draft: <brief>, and anything else echoes MOCK: ….
Translator and Drafter
Translator::translateDocument() translates the copy fields of each block's definition: non-empty strings of text, textarea, richeditor, richtext and markdown fields, in repeater items and child blocks too, unless a field is translatable: false or its name marks a technical value (see Authoring blocks). Blocks without a definition fall back to text, html, caption, alt, title, author and label. It sends chunks of 40 values, keeps the original where a translation is missing, and throws if the result is invalid. The form of address is decided once per document (Translator::register()) and repeated in the prompt of every chunk, so German stays with Sie or du throughout.
Drafter::draft($brief, $templateJson, $researchContext, $length, $allowExternalImages, $fileContext, $locale, $context) prompts with a catalog of all registered blocks. It gives the answer fresh ids and validates it against the schema and block rules, with at most 2 repair retries. With $context = 'article' the prompt forbids heroes and top-level sections, and DraftArticlePolicy turns any hero into a heading plus richtext and unwraps top-level sections. It then applies default block settings and strips external image URLs unless they are allowed.
Images and research
ImageGenerator calls the image APIs directly: OpenAI images/generations (default gpt-image-2, plus the Image quality setting unless it is auto, upscale via images/edits) or OpenRouter images (default google/gemini-3.1-flash-image, no upscale). FirecrawlClient wraps https://api.firecrawl.dev/v1 for Research & draft.
Privacy hard gate
Unless Allow outbound research requests is on and a Firecrawl API key is set, every Firecrawl call throws. No content-related request leaves the installation.