Skip to content

AI providers ​

Setup and editor features are described in AI Assistance.

Provider layer ​

Drivers implement Renick\Content\Classes\Ai\AiProvider:

php
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 the translate or draft model. An empty model setting falls back to AiSetting::DEFAULT_MODELS for the active provider (gpt-5.6-terra, on OpenRouter openai/gpt-5.6-terra). It throws once the monthly token budget is reached.
  • ProviderManager::recordUsage() logs token counts to renick_content_ai_usage.
  • When chat() gets a JSON schema, the driver requests OpenAI json_schema structured output.
  • Reasoning models (GPT-5 family, o-series) get no temperature, and OpenAI requests send max_completion_tokens. If another model rejects the temperature, the request is retried once without it.
  • Reasoning models also get reasoning_effort from renick.content::limits.ai_reasoning_effort (RENICK_CONTENT_AI_REASONING_EFFORT, default low; empty keeps the model default). A chat request gets ai_timeout seconds 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 /translate and POST /draft have 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:

php
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.