AI editorial assists
louise-toolkit/ai adds optional Workers AI
editorial help: alt text generated from an uploaded image, rewrite/tighten
a passage, and SEO title + description suggestions. Everything is opt-in
and best-effort—with no AI binding, or on any model error, the helpers
return null and never block a save, upload, or publish.
Wiring
Section titled “Wiring”Add the binding, then opt each feature in.
"ai": { "binding": "AI" }Alt text on upload—an accessor on the media route fills each new image’s
alt from the image:
mediaRoute({ table: media, resolveEditor, altText: (env) => env.AI });Rewrite + SEO—mount aiRoute for the editor client to call:
aiRoute({ resolveEditor, ai: (env) => env.AI });// POST /api/louise/ai/rewrite { text, mode? } → { text }// POST /api/louise/ai/seo { content } → { title, description }Both are session-gated, same-origin mutations (each call spends AI budget), and
answer 503 when the binding is absent—so the assist is cleanly optional.
Rewrite takes up to 1,536 characters (REWRITE_MAX_CHARS), a few paragraphs.
The limit is sized from the rewrite’s 512-token output cap, because a rewrite runs
about as long as its input, and a longer passage would come back cut off. A longer
selection gets a 413 before any model runs, and its error tells the editor to
select a shorter passage. The editor toolbar shows that message in the rewrite
menu.
Workers AI is billed in Neurons with a 10,000/day free allocation, then
$0.011 per 1,000 Neurons. In practice each assist is a fraction of a cent
(alt text ≈ $0.0002–0.0004/image; rewrite/SEO ≈ $0.0002–0.0004/call), and the
first few hundred actions each day are free.
AI Gateway—caching, cost caps, fallbacks
Section titled “AI Gateway—caching, cost caps, fallbacks”Route the calls through AI Gateway for response caching (identical prompts are free on repeat), rate limiting (bound request volume and spend), retries + provider fallback, and request logging/analytics—all configured on the gateway, transparent to your code.
- Create a gateway in the Cloudflare dashboard (AI → AI Gateway → Create Gateway) and note its id. Set caching, rate limits, and fallback there.
- Pass the gateway config through—per feature:
// alt text: options flow through the media routemediaRoute({ table: media, resolveEditor, altText: (env) => env.AI, altTextOptions: { gateway: { id: "louise-gw", cacheTtl: 86400 } },});
// rewrite + SEO: a gateway accessor on the routeaiRoute({ resolveEditor, ai: (env) => env.AI, gateway: (env) => ({ id: "louise-gw" }),});Gateway caching already keys on the full request (model + inputs), so identical
calls dedupe automatically. Set cacheKey only to deliberately widen a cache
entry (for example, a content hash) across incidental request variance; cacheTtl: 0
disables caching for a call, and skipCache: true forces a fresh run.
Omit gateway and calls go straight to Workers AI—the gateway is purely
additive.
Troubleshooting
Section titled “Troubleshooting”The assists are best-effort, so a failing model doesn’t throw. The helper returns
null and the route answers 502 ("Rewrite unavailable" or
"Suggestion unavailable"). A 503 means something else: no runner, because
the binding is absent or generation is turned off. A 413 from rewrite means the
selection is longer than REWRITE_MAX_CHARS.
When an assist starts answering 502:
- Check
wrangler tail.runAilogs the underlying error before it returnsnull: a retired model, an unmet JSON schema, or a quota. Search for[louise] degraded ai.run; the line names the model. - Check the model ID against the Workers AI model
catalog. Cloudflare
deprecates models on a schedule, and a retired model fails every call. If
rewrite and SEO both fail, suspect the shared text model rather than one
feature. The helpers take a per-call
modeloption, so you can pin a current one. - Look for
[louise] degraded ai.truncatedinwrangler tail. The helpers refuse an answer that hit itsmax_tokens, because a cut-off rewrite would replace a passage with a fragment and cut-off alt text would read as finished. The line names the model, its finish reason, and the tokens it generated against the cap. If it’s frequent, the model is wordier than the cap allows: raisemaxTokens, or pick a model that answers more briefly.
If you call runAi yourself with JSON mode (response_format of type
json_schema), read the result as an object: Workers AI returns the parsed
JSON under response, not a string. Code that expects text gets nothing back
from a structured call.