Meridian handbook
Features

AI assistance

How to connect your own AI provider (Azure OpenAI, OpenAI, Anthropic, Mistral or any OpenAI-compatible endpoint), which authoring tasks AI drafts and where, the review queue, budgets and the data boundary, prompt overrides, public disclosure and the ai commands.

Meridian can draft some of the tedious parts of authoring: alt text and captions, structured opening hours from a pasted sentence, tags from your vocabulary, SEO titles and descriptions, summaries, and translations. It runs against a provider you configure, with your credentials; nothing is proxied and nothing is sent anywhere by default.

Three rules hold whatever you configure:

  • No provider, no AI. With ai.enabled false or no provider, every AI control is absent from the admin and the portal, not disabled.
  • A person decides. AI only proposes. Every suggestion waits for a person to accept, edit or reject it, and accepting writes through the same validation as a person’s own edit. Nothing is published by AI.
  • Authoring only. AI runs in the admin and, in the DMO bundle, the member portal. The public site never calls a provider: there is no chatbot and no AI on visitor pages.

Connect a provider

# config/engine.yaml
ai:
  enabled: ${AI_ENABLED:-false}
  provider: ${AI_PROVIDER:-none}
  openai_compatible:
    base_url: ${AI_BASE_URL:-http://localhost:11434/v1}
    api_key: ${AI_API_KEY}
    model_text: ${AI_TEXT_MODEL:-llama3.1}
    model_vision: ${AI_VISION_MODEL:-}
  budget:
    monthly_cost_cap: "20"
    per_user_daily_requests: 100
  pricing:
    input_per_million: "0.15"
    output_per_million: "0.60"

Then confirm it, and give your editors the permission:

engine-dmo check                        # provider keys, models, and which features stay unavailable
engine-dmo ai test                      # one round trip with no content
engine-dmo ai status                    # provider, models, health and this month's spend
# config/access/roles.yaml: a role holding the AI permissions
roles:
  - key: ai-drafter
    name: { en: AI drafting }
    permissions: [ai.use, ai.bulk]
# config/access/grants.yaml: give it to the editors group beside its editor role
grants:
  - { group: administrators, role: owner }
  - { group: editors, role: editor }
  - { group: editors, role: ai-drafter }

Run engine-dmo check --fix to write the role’s id. No built-in role holds the AI permissions except Owner; grant them in roles.yaml.

Providers

Write only the block for the provider you use. API keys are secrets: a bare ${VAR} with no default.

providerBlock keysRequiredNotes
openai_compatiblebase_url (up to and including /v1), api_key, model_text, model_vision, model_embeddingbase_url, model_textOllama, vLLM, a sovereign or air-gapped endpoint. A local endpoint may need no key (set the variable empty).
openaias above; base_url defaults to https://api.openai.com/v1model_text
mistralas above; base_url defaults to https://api.mistral.ai/v1model_text
anthropicapi_key, base_url (default https://api.anthropic.com), model_text, model_visionmodel_textNo embeddings, so duplicate detection is unavailable.
azure_openaiendpoint, api_key, deployment_text, deployment_vision, deployment_embedding, api_version (default 2024-10-21)endpoint, deployment_text
noneThe default.

Alt text drafting uses the vision model, else the text model (which may not accept images; check warns). Duplicate detection needs an embedding model (check warns while there is none). ai.models overrides the model for one feature: models: { alt_text: gpt-4o-mini } (for Azure, a deployment name).

What AI does, and where

Each feature has its own switch under ai.features, and each needs ai.use on the site.

FeatureKey (default)WhereWhat it does
Alt text and captionsalt_text (on)An asset’s page; the alt text queue’s “Draft these with AI”; engine-dmo ai bulk alt-textDrafts alt text or a caption per locale, for the default or one site’s own wording. Held to 125 characters and never “image of”.
Opening hours extractionhours_extraction (on)A document’s assistance page, for an opening_hours field; the member portal (DMO)Turns what a business wrote (“12 til 3, closed Mondays”) into structured hours, shown beside the source text.
Tag suggestiontag_suggestion (on)A document’s assistance pageSuggests tags from your configured vocabulary only; the reviewer ticks which to add.
SEO draftingseo_drafting (on)A document’s assistance page, for an seo fieldDrafts a title and a description within the field’s limits, from the document’s own text.
Summarisationsummarisation (off)A document’s assistance page, for a text field that declares summary_of: <field>Drafts a summary of the source field, held to the field’s max (280 when it has none).
Translation draftingtranslation (off)The side-by-side translation editorDrafts the field being translated, with the glossary and translation memory as context.
Zero-result content briefszero_result_briefs (off)Insight, search (analytics.view too)Turns the site’s commonest searches that found nothing into up to five briefs (a listing, an article, a synonym or a redirect to consider). Writes nothing.
Duplicate detectionduplicate_detection (on)CRM organisations (DMO)Finds likely duplicate organisations by embedding.
Member listing improvementmember_improvement (off)The member portal (DMO)Suggests clearer wording for a member’s own listing text.

A field that summarises another:

fields:
  - name: body
    type: text
    multiline: true
  - name: summary
    type: text
    max: 280
    summary_of: body

ai.features.image_generation: true is refused: there is no image generation. quality_review and ai.translation.quality_signals are accepted and do nothing. There is no Amazon Bedrock adapter; reach a model hosted there through an OpenAI-compatible gateway.

The editor’s SEO advice panel (a title that duplicates another page, internal link suggestions) is computed, not generated, and is shown with or without a provider.

How a suggestion is made and reviewed

A person asks for a draft. Nothing runs in the background on content.
The data boundary is applied (below), the budget is checked and reserved, and the request is built from a versioned prompt with your content sent as delimited data, never as instructions.
The answer is validated by the same rules that validate a person’s input: the field’s type and limits, the select’s options, the vocabulary. An invalid answer is retried once with the problems, then abandoned with a clear message.
The valid answer waits as a suggestion, beside the current value.
A person accepts, edits or rejects it. Accepting saves a normal revision under that person’s name, with an assistance record of how the text was made.

The admin’s AI queue lists every suggestion waiting for review; engine-dmo ai queue does the same. Bulk runs (ai.bulk) print an estimate of the cost before they queue anything, and each draft still waits in the queue. Suggestions that have been decided are kept for retention.ai_suggestions (thirteen months).

engine-dmo ai bulk alt-text --locale cy --dry-run     # count and estimate only
engine-dmo ai bulk alt-text --locale cy --site main   # queue drafts of the site's own wording

Cost control

KeyDefaultMeaning
budget.monthly_cost_cap50A hard stop in the site currency; at the cap every AI feature is unavailable. 0 is no cap.
budget.monthly_tokens5000000A token cap per month.
budget.per_user_daily_requests200Requests per person per day.
budget.per_feature_monthly_requests{}Caps per feature: { alt_text: 2000 }.
budget.sites{}A site’s own monthly cost cap: { main: "10" }.
budget.estimate_before_bulktrueAccepted and not read: a bulk run always prints its estimate first.
budget.estimated_tokens_per_call{}Override the engine’s estimate per feature.
pricing.input_per_million, pricing.output_per_millionYour provider’s prices in the site currency, for the cap and the estimate. The engine never bills.
cache.enabled, cache.ttltrue, 720hThe same question about unchanged content is not sent twice.
circuit_breaker.failures, window, cooldown3, 1m, 2mAfter repeated failures every feature pauses, sending nothing, until the cool-down ends.
timeout30sPer request.
engine-dmo ai budget --period 2026-10     # spend by feature and by user against the caps
engine-dmo ai log --from 2026-10-01       # every request: feature, user, model, tokens, cost, outcome, data sent

The data boundary

KeyDefaultMeaning
data_boundary.allow_contenttrueThe content being assisted may be sent.
data_boundary.allow_personal_datafalseCRM personal fields and phone and email values are stripped. true is a warning in check.
data_boundary.redact_before_sendtrueEmail addresses and phone numbers inside free text are replaced before sending.
data_boundary.allow_analyticsaggregate_onlyaggregate_only or none. Raw events are never sent; none turns off zero-result briefs.
data_boundary.exclude_fields[]Fields never sent: listing.internalNotes, *.bookingNote.

Credentials, session tokens, audit entries and membership records are never sent. engine-dmo ai data-flow-statement writes a data protection annex from your configuration: the provider and where requests go, each feature and what it sends, the boundary, the budget and what is recorded. It needs no database:

engine-dmo ai data-flow-statement --out ai-data-flow.md

Telling visitors

Accepting a suggestion records how the text was made: drafted (accepted as written), edited, extracted, translated, suggested. On public pages, entry.aiAssisted is true when the published text carries a record the disclosure policy covers, and aiDisclosure(entry) prints the disclosure sentence, or nothing:

{{ aiDisclosure(entry) }}

The sentence is the ai.disclosure string in your string catalogue, in the page’s locale, else “Parts of this page were drafted with the help of AI and reviewed by a person before publication.” Reading the records calls no provider.

ai.disclosure.publicDiscloses
neverNothing.
drafted_only (default)Text accepted as drafted without material change, and text written by an agent through the content MCP.
allAny assisted text, edited and extracted included. Tag suggestions are never counted.

ai.disclosure.admin is always and cannot be changed: the admin always shows provenance.

Prompt overrides

Each feature’s prompt can be replaced with your own wording, one file per prompt in ai.prompts_dir (config/ai/prompts):

# config/ai/prompts/alt-text.yaml
key: alt-text
version: 2
system: |
  You write alt text for an image on a public website about an arts centre.
  ...
  Answer with a JSON object: {"alt": "<the alt text>"}.

The keys are alt-text, caption, hours-extraction, tag-suggestion, seo-drafting, summarisation, translation, zero-result-briefs, member-improvement and duplicate-detection. version (1 or more) is recorded on every suggestion the prompt produces; raise it when you change the text. check refuses an unknown key, a missing version and empty text. Keep the answer format the engine’s prompt asks for: the answer is validated against it.

Commands

CommandWhat it does
ai statusProvider, models, capabilities, health and spend this month.
ai testOne round trip with no content: confirms the endpoint, the key and the model.
ai queueSuggestions waiting for review.
ai bulk alt-textQueue alt text drafts for every image missing alt text in a locale (--locale, --site, --dry-run).
ai budgetSpend by feature and user against the caps (--period).
ai logThe activity log (--from, --limit).
ai data-flow-statementThe data protection annex (--out).

All but data-flow-statement need the database.

Permissions

PermissionAllows
ai.useAsking for a draft on a site. Accepting also needs the permission the write itself needs, such as media.edit for alt text or content.edit for a field.
ai.bulkBulk runs and the duplicate scan.
ai.configureThe admin’s AI usage screen: spend by feature and user against the caps.

Only Owner holds them by default.

Configuration reference

The ai: section of config/engine.yaml, beyond the provider blocks and the tables above:

KeyDefaultMeaning
enabledfalseMust read true or false after interpolation.
providernoneazure_openai, anthropic, openai, mistral, openai_compatible or none.
models{}A model per feature. Not duplicate_detection.
features.*see the features tableOne switch per feature.
duplicates.threshold0.9Similarity, 0.5 to 1, at or above which two organisations are offered as duplicates.
translation.memory_examples8Prior translations sent per request, 0 to 50.
translation.length_ratio_bands{}Per language pair, the expected length ratio: { "en:de": [0.9, 1.5] }.
disclosure.publicdrafted_onlynever, drafted_only or all.
prompts_dirconfig/ai/promptsWhere prompt overrides live.