Appearance
AI Writer
Your own AI model for the texts the site's modules write for you: a translation of a post, a meta description, an image's alt text. You choose the provider, paste its API key and set how much a month may be spent; the modules ask, the provider bills you for what they ask, and every question is written down with what it cost.
This is the site asking a model with its own key. Connecting your own agent — Claude, Cursor — to run the site is the other way, AI Agents; the Assistant is a chat in the admin that answers with this key too; Three Ways to Use AI compares them.
| Package | Gives |
|---|---|
kitloom/ai-writer | The model behind TextGenerator, the settings, the usage log; on laravel/ai |
kitloom/ai-writer-filament / kitloom/ai-writer-nova | The AI Writer section: Usage, Settings, How it works |
bash
composer require kitloom/ai-writer kitloom/ai-writer-filament # or kitloom/ai-writer-nova
php artisan migrateVideo lessons
Recorded in the Filament admin; the Nova admin works the same way. The second shows AI Writer at work: the Languages module asks it for a French translation.
Setting it up
- Get an API key from the provider — for Anthropic (Claude) at console.anthropic.com → API keys — and set a spending limit there too: the budget here is a second fence, not the only one.
- AI Writer → Settings:
- Provider — Anthropic (Claude), OpenAI, Google Gemini, Mistral, DeepSeek, xAI or OpenRouter;
- API key — encrypted with the application key before it is saved, never shown again; leave the field empty to keep the saved one;
- Model — empty is the provider's cheapest text model (Claude Haiku 5.5 for Anthropic), plenty for translations and descriptions; or name another;
- Monthly budget — US dollars; empty is no limit.
- Test the key — Filament on Settings, Nova on Usage — asks the model one short question and shows the answer, or the provider's error.
Until a key is saved on the page, the provider, key and model from the server's .env are used (KITLOOM_AI_WRITER_PROVIDER, KITLOOM_AI_WRITER_KEY, KITLOOM_AI_WRITER_MODEL); while the page's budget is empty, KITLOOM_AI_WRITER_MONTHLY_BUDGET is. If the application key changes, a saved key no longer decrypts and has to be entered again.
What it costs
AI Writer → Usage lists every question: when, what for (languages.translate, ai-writer.test), who asked, the model, the tokens in and out, and the cost; a question that failed is there too, with the provider's error. Above the list: what the month has cost against the budget.
- The cost is counted from
kitloom-ai-writer.prices, US dollars per million tokens, by model — for every provider on the list: Claude Haiku 5.5 ($0.10 in / $0.50 out), Sonnet 5.5 ($2 / $10), Opus 5.5 ($4 / $20) and Fable 5.1, and the common models of OpenAI, Gemini, Mistral, DeepSeek, xAI and OpenRouter. Where a provider bills a long prompt at another rate, the model has alongprice too, used above its number of tokens. - A model without a price, while no budget is set, is logged by its tokens only, with no cost. While a budget is set it is not asked at all — its questions could not be counted — so the buttons disappear. The settings page says so either way: add its price to the config, choose a priced model, or clear the budget.
- The month starts at the site's midnight on the first (Settings → General → Timezone). Once its questions cost the budget, the modules get no more answers and their buttons disappear until the next month, or until you raise the budget.
- It is an estimate from the tokens the provider reports; the provider's invoice is what you pay.
For scale: a 1,000-word post is about 1,500 tokens, so translating one with Claude Haiku 5.5 — the post in, its translation out — costs well under a cent.
Who uses it
Modules that write text ask AI Writer through a contract and draw their "with AI" button only while a key is set and the budget not spent:
| Module | What it writes |
|---|---|
| Languages | Translate with AI: a post's or term's translation, saved as a draft; a bulk action for many |
More modules will ask it as they get their buttons; each says so on its own page.
A module checks its own permissions: an editor who may translate a post uses the button without seeing the key. Who sees AI Writer itself is one permission, Manage AI Writer (manage_ai_writer): the key and the budget are your money. Administrators have it.
Where things are kept
| What | Where |
|---|---|
| Usage | table ai_writer_usage (the site's table prefix in front) |
| Settings | options ai_writer_provider, ai_writer_key (encrypted), ai_writer_model, ai_writer_monthly_budget |
| Configuration | config/kitloom-ai-writer.php (php artisan vendor:publish --tag=kitloom-ai-writer-config) |
| Key | Default | Meaning |
|---|---|---|
provider, key, model, monthly_budget | anthropic, —, —, — | Used while nothing is saved on the settings page; from KITLOOM_AI_WRITER_PROVIDER, KITLOOM_AI_WRITER_KEY, KITLOOM_AI_WRITER_MODEL, KITLOOM_AI_WRITER_MONTHLY_BUDGET |
providers | seven providers | What the page offers: laravel/ai driver => label |
prices | models of every provider | US dollars per million input and output tokens, by model; long — the rate above a number of prompt tokens, where the provider bills one |
timeout, max_tokens | 120, 16000 | Seconds to wait for one answer, the most tokens it may have |
filament.register, nova.register | true | The admin sections |
privacy.register | true | A user's questions in WordPress's Export and Erase Personal Data (Privacy): the export lists them, the erasure takes the user off them and keeps them for the budget |
For module developers
A module never requires AI Writer or a provider SDK: it asks Kitloom\WpSchema\Contracts\TextGenerator, which every site has — without AI Writer nothing is available.
php
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Kitloom\WpSchema\Contracts\TextGenerator;
use Kitloom\WpSchema\Text\TextGenerationFailed;
$writer = app(TextGenerator::class);
if ($writer->available()) { // a key is set and the budget not spent: draw the button
try {
$fields = $writer->generate(
'Translate the fields into French (Canada). Keep the HTML.',
json_encode(['title' => $post->title, 'content' => $post->content]),
fn (JsonSchema $schema): array => ['title' => $schema->string()->required(), 'content' => $schema->string()->required()],
purpose: 'languages.translate',
actor: $user,
);
} catch (TextGenerationFailed $e) {
// show $e->getMessage() to the person who pressed the button; nothing was saved
}
}- The instructions and the text go apart — the system prompt and the message — so a text that says "ignore the above" is only text.
- Without a schema the answer is a string; with one, an array of those fields.
- Save the answer for a person to look at — a draft, a field to confirm — not straight onto the live site.
- In tests, fake the provider:
Kitloom\AiWriter\Prompting\TextAgent::fake(['Bonjour']), orStructuredTextAgent::fake([[…]])for a schema.