Meridian handbook
ReferenceConfiguration

engine.yaml: analytics and AI

Every key of engine.yaml's analytics section (cookieless ingest, bot filtering, retention, reporting time zone, PDF reports), its ai section (provider, models, data boundary, budget, features, disclosure) and its translation section, with type, default and meaning.

analytics

Meridian’s analytics are cookieless and first-party: page views and interactions are recorded by the engine itself, with visitor addresses discarded after a salted hash that rotates daily.

KeyTypeDefaultMeaning
analytics.enabledbooleantrueRecord analytics events
analytics.ingest_bufferinteger10000Events buffered in memory before they are written. When it is full, new events are dropped and counted (meridian_analytics_events_dropped_total)
analytics.salt_rotationstringdailyMust be daily: the salt that pseudonymises visitors rotates every UTC day and the old one is destroyed
analytics.raw_retention_daysinteger90Raw events are kept this many days, 30 to 400
analytics.hourly_retention_daysinteger400Hourly rollups are kept this many days. At least 30. Daily and monthly rollups are kept
analytics.search_log_retention_daysinteger400The search query log is kept this many days. At least 30
analytics.reporting_timezonestringEmptyThe IANA time zone days and months are counted in, such as Europe/London, for a site that does not set its own. Empty is UTC
analytics.financial_year_startinteger0The month (1 to 12) the financial year starts in, for reports; 0 takes the default site’s country’s convention
analytics.bot_filteringstringstrictoff keeps only deduplication and the anti-gaming rules; standard adds a request-rate rule; strict adds rules no real browser trips
analytics.dedupe_windowduration30mA repeat of the same event by the same visitor within this window counts once
analytics.respect_dntbooleanfalseDrop requests carrying DNT: 1 or Sec-GPC: 1 before anything is recorded
analytics.exclude_ipsstringEmptyAddresses and CIDR ranges, separated by commas or spaces, whose requests are flagged (your office, your monitoring) and left out of reports
analytics.country_headerstringEmptyThe header a trusted proxy or CDN writes the visitor’s ISO country code into, such as CF-IPCountry. Read only from server.trusted_proxies, which must not be empty; without it the country is not recorded
analytics.statement_timeoutduration10sThe time limit on report and dashboard queries. Above zero and below database.statement_timeout
analytics.pdf.rendererstringautoHow member reports become PDF: browser (a headless Chromium, as in the -vips image), builtin (the engine’s own writer), or auto (the browser when one is found, else built in)
analytics.pdf.browser_binarystringEmptyThe Chromium or Chrome binary to use; empty looks for one on the path
analytics.pdf.timeoutduration1mThe time limit on rendering one PDF

A file of extra user-agent substrings to filter, one per line, can be added at config/analytics/bots.txt; it extends the list shipped with the engine. Rollups run every ten minutes on the scheduler leader; engine-dmo analytics recompute rebuilds them for a range.

ai

The AI features are off unless ai.enabled is true and a provider is named. You bring your own provider account; nothing is sent anywhere else. Every AI output is a suggestion a person accepts, edits or rejects.

ai:
  enabled: ${AI_ENABLED:-false}
  provider: openai_compatible
  openai_compatible:
    base_url: https://api.example-provider.eu/v1
    api_key: ${AI_API_KEY}
    model_text: example-large
    model_vision: example-vision
    model_embedding: example-embed
  budget:
    monthly_cost_cap: "50"
  pricing:
    input_per_million: "0.40"
    output_per_million: "1.60"

Provider

KeyTypeDefaultMeaning
ai.enabledstring"false""true" or "false". A string so it can be written as ${AI_ENABLED:-false}. Enabled with provider: none is still off
ai.providerstringnoneazure_openai, anthropic, openai, mistral, openai_compatible or none. bedrock is reserved and refused
ai.timeoutduration30sEach provider request’s time limit
ai.modelsmap of strings{}A model per feature, overriding the provider block’s text or vision model for that feature alone (for Azure OpenAI, a deployment name). Keys are feature names; duplicate_detection uses the embedding model and cannot be set here
ai.prompts_dirstringconfig/ai/promptsWhere prompt overrides are read from (see AI prompts)

Each endpoint-shaped provider has the same keys under its own name, ai.openai, ai.mistral, ai.anthropic and ai.openai_compatible:

KeyTypeDefaultMeaning
ai.<provider>.base_urlstringhttps://api.openai.com/v1, https://api.mistral.ai/v1, https://api.anthropic.com; empty for openai_compatibleThe API’s base URL. Required for openai_compatible (Ollama, vLLM or any compatible service)
ai.<provider>.api_keysecretEmptyThe API key
ai.<provider>.model_textstringEmptyThe text model. Required
ai.<provider>.model_visionstringEmptyThe model that reads images (alt text drafting)
ai.<provider>.model_embeddingstringEmptyThe embedding model (duplicate detection). The Anthropic adapter has none

Azure OpenAI, in your own tenancy:

KeyTypeDefaultMeaning
ai.azure_openai.endpointstringEmptyThe resource endpoint. Required
ai.azure_openai.api_keysecretEmptyThe resource key
ai.azure_openai.deployment_textstringEmptyThe text deployment. Required
ai.azure_openai.deployment_visionstringEmptyThe vision deployment
ai.azure_openai.deployment_embeddingstringEmptyThe embedding deployment
ai.azure_openai.api_versionstring2024-10-21The API version

ai.bedrock.* (region, access_key_id, secret_access_key, model_text, model_vision) is accepted for a provider that is not in this release.

Data boundary

What may leave the install, enforced before a request is built.

KeyTypeDefaultMeaning
ai.data_boundary.allow_contentbooleantrueContent may be sent
ai.data_boundary.allow_personal_databooleanfalseCRM personal fields may be sent. true is a warning; record it in your data protection assessment (engine-dmo ai data-flow-statement writes one)
ai.data_boundary.allow_analyticsstringaggregate_onlyaggregate_only or none. Raw events are never sent
ai.data_boundary.redact_before_sendbooleantrueReplace email addresses and telephone numbers in text before it is sent. While allow_personal_data is false they are replaced whatever this says
ai.data_boundary.exclude_fieldslist of strings[]Fields never sent, as type.field or *.field, such as listing.internalNotes

Budget and pricing

The budget is a hard stop, not a report: a feature that would exceed it is refused.

KeyTypeDefaultMeaning
ai.budget.monthly_cost_capstring"50"The monthly cost cap in the site currency, as a decimal ("12.50"); "0" is no cap
ai.budget.monthly_tokensinteger5000000The monthly token cap
ai.budget.per_user_daily_requestsinteger200Requests one person may make a day
ai.budget.per_feature_monthly_requestsmap of integers{}A monthly request cap per feature
ai.budget.sitesmap of strings{}A monthly cost cap per site key
ai.budget.estimate_before_bulkbooleantrueAccepted; bulk runs always print an estimate first
ai.budget.estimated_tokens_per_callmap of integers{}The tokens one call of a feature is estimated at, for bulk estimates
ai.pricing.input_per_millionstringEmptyWhat your provider charges per million input tokens, in the site currency, so spend can be held against the cap
ai.pricing.output_per_millionstringEmptyThe same for output tokens
ai.cache.enabledbooleantrueAnswer a feature run again on unchanged content, with the same prompt and model, from the cache at no cost
ai.cache.ttlduration720hHow long a cached answer is kept
ai.circuit_breaker.failuresinteger3Failures within window that stop the engine calling the provider
ai.circuit_breaker.windowduration1mThe window failures are counted in
ai.circuit_breaker.cooldownduration2mHow long calls stay stopped

Features

Each feature switches on separately.

KeyTypeDefaultMeaning
ai.features.alt_textbooleantrueDraft alt text for images (needs a vision model)
ai.features.hours_extractionbooleantrueRead opening hours from pasted text
ai.features.tag_suggestionbooleantrueSuggest tags
ai.features.seo_draftingbooleantrueDraft SEO titles and descriptions
ai.features.duplicate_detectionbooleantrueOffer likely duplicate records (needs an embedding model)
ai.features.translationbooleanfalseDraft translations for a translator to review
ai.features.summarisationbooleanfalseDraft summaries
ai.features.zero_result_briefsbooleanfalseDraft content briefs from searches that found nothing
ai.features.member_improvementbooleanfalseSuggestions to members for improving their listings (engine-dmo)
ai.features.quality_reviewbooleanfalseAccepted; no feature reads it in this release
ai.features.image_generationbooleanfalseNot in this release; true is refused
ai.duplicates.thresholdnumber0.9The similarity, 0.5 to 1, at which a pair is offered as a duplicate

Translation drafting and disclosure

KeyTypeDefaultMeaning
ai.translation.memory_examplesinteger8Translation memory units sent with a request as examples, 0 to 50
ai.translation.length_ratio_bandsmap{}Per language pair ("en:de"), the shortest and longest expected length ratio, such as [0.9, 1.5]; a draft outside it is flagged
ai.translation.quality_signalslist of strings[]Accepted; every quality signal runs
ai.disclosure.publicstringdrafted_onlyWhether public pages disclose AI assistance: never, drafted_only (text an AI drafted) or all
ai.disclosure.adminstringalwaysAlways always: the admin always shows what an AI drafted

engine-dmo ai status, ai test and ai budget check a configuration. See AI assistance.

translation

The translator tools that work with no AI provider: the glossary and the translation memory.

KeyTypeDefaultMeaning
translation.glossary.auto_seedlist of strings[organisation, listing, area, tag]Where glossary terms are seeded from automatically: organisation names, listing and area titles, tags
translation.glossary.refresh_on_changebooleantrueRefresh seeded terms when their source changes
translation.memory.enabledbooleantrueKeep a translation memory of approved translations
translation.memory.suggest_exact_matchesbooleantrueOffer an exact memory match as a suggestion
translation.memory.fuzzy_thresholdnumber0.75The similarity from which a near match is offered
translation.memory.fuzzy_matchesinteger3The most near matches offered