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.
| Key | Type | Default | Meaning |
|---|---|---|---|
analytics.enabled | boolean | true | Record analytics events |
analytics.ingest_buffer | integer | 10000 | Events buffered in memory before they are written. When it is full, new events are dropped and counted (meridian_analytics_events_dropped_total) |
analytics.salt_rotation | string | daily | Must be daily: the salt that pseudonymises visitors rotates every UTC day and the old one is destroyed |
analytics.raw_retention_days | integer | 90 | Raw events are kept this many days, 30 to 400 |
analytics.hourly_retention_days | integer | 400 | Hourly rollups are kept this many days. At least 30. Daily and monthly rollups are kept |
analytics.search_log_retention_days | integer | 400 | The search query log is kept this many days. At least 30 |
analytics.reporting_timezone | string | Empty | The 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_start | integer | 0 | The month (1 to 12) the financial year starts in, for reports; 0 takes the default site’s country’s convention |
analytics.bot_filtering | string | strict | off keeps only deduplication and the anti-gaming rules; standard adds a request-rate rule; strict adds rules no real browser trips |
analytics.dedupe_window | duration | 30m | A repeat of the same event by the same visitor within this window counts once |
analytics.respect_dnt | boolean | false | Drop requests carrying DNT: 1 or Sec-GPC: 1 before anything is recorded |
analytics.exclude_ips | string | Empty | Addresses and CIDR ranges, separated by commas or spaces, whose requests are flagged (your office, your monitoring) and left out of reports |
analytics.country_header | string | Empty | The 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_timeout | duration | 10s | The time limit on report and dashboard queries. Above zero and below database.statement_timeout |
analytics.pdf.renderer | string | auto | How 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_binary | string | Empty | The Chromium or Chrome binary to use; empty looks for one on the path |
analytics.pdf.timeout | duration | 1m | The 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
| Key | Type | Default | Meaning |
|---|---|---|---|
ai.enabled | string | "false" | "true" or "false". A string so it can be written as ${AI_ENABLED:-false}. Enabled with provider: none is still off |
ai.provider | string | none | azure_openai, anthropic, openai, mistral, openai_compatible or none. bedrock is reserved and refused |
ai.timeout | duration | 30s | Each provider request’s time limit |
ai.models | map 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_dir | string | config/ai/prompts | Where 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:
| Key | Type | Default | Meaning |
|---|---|---|---|
ai.<provider>.base_url | string | https://api.openai.com/v1, https://api.mistral.ai/v1, https://api.anthropic.com; empty for openai_compatible | The API’s base URL. Required for openai_compatible (Ollama, vLLM or any compatible service) |
ai.<provider>.api_key | secret | Empty | The API key |
ai.<provider>.model_text | string | Empty | The text model. Required |
ai.<provider>.model_vision | string | Empty | The model that reads images (alt text drafting) |
ai.<provider>.model_embedding | string | Empty | The embedding model (duplicate detection). The Anthropic adapter has none |
Azure OpenAI, in your own tenancy:
| Key | Type | Default | Meaning |
|---|---|---|---|
ai.azure_openai.endpoint | string | Empty | The resource endpoint. Required |
ai.azure_openai.api_key | secret | Empty | The resource key |
ai.azure_openai.deployment_text | string | Empty | The text deployment. Required |
ai.azure_openai.deployment_vision | string | Empty | The vision deployment |
ai.azure_openai.deployment_embedding | string | Empty | The embedding deployment |
ai.azure_openai.api_version | string | 2024-10-21 | The 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.
| Key | Type | Default | Meaning |
|---|---|---|---|
ai.data_boundary.allow_content | boolean | true | Content may be sent |
ai.data_boundary.allow_personal_data | boolean | false | CRM 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_analytics | string | aggregate_only | aggregate_only or none. Raw events are never sent |
ai.data_boundary.redact_before_send | boolean | true | Replace 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_fields | list 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.
| Key | Type | Default | Meaning |
|---|---|---|---|
ai.budget.monthly_cost_cap | string | "50" | The monthly cost cap in the site currency, as a decimal ("12.50"); "0" is no cap |
ai.budget.monthly_tokens | integer | 5000000 | The monthly token cap |
ai.budget.per_user_daily_requests | integer | 200 | Requests one person may make a day |
ai.budget.per_feature_monthly_requests | map of integers | {} | A monthly request cap per feature |
ai.budget.sites | map of strings | {} | A monthly cost cap per site key |
ai.budget.estimate_before_bulk | boolean | true | Accepted; bulk runs always print an estimate first |
ai.budget.estimated_tokens_per_call | map of integers | {} | The tokens one call of a feature is estimated at, for bulk estimates |
ai.pricing.input_per_million | string | Empty | What your provider charges per million input tokens, in the site currency, so spend can be held against the cap |
ai.pricing.output_per_million | string | Empty | The same for output tokens |
ai.cache.enabled | boolean | true | Answer a feature run again on unchanged content, with the same prompt and model, from the cache at no cost |
ai.cache.ttl | duration | 720h | How long a cached answer is kept |
ai.circuit_breaker.failures | integer | 3 | Failures within window that stop the engine calling the provider |
ai.circuit_breaker.window | duration | 1m | The window failures are counted in |
ai.circuit_breaker.cooldown | duration | 2m | How long calls stay stopped |
Features
Each feature switches on separately.
| Key | Type | Default | Meaning |
|---|---|---|---|
ai.features.alt_text | boolean | true | Draft alt text for images (needs a vision model) |
ai.features.hours_extraction | boolean | true | Read opening hours from pasted text |
ai.features.tag_suggestion | boolean | true | Suggest tags |
ai.features.seo_drafting | boolean | true | Draft SEO titles and descriptions |
ai.features.duplicate_detection | boolean | true | Offer likely duplicate records (needs an embedding model) |
ai.features.translation | boolean | false | Draft translations for a translator to review |
ai.features.summarisation | boolean | false | Draft summaries |
ai.features.zero_result_briefs | boolean | false | Draft content briefs from searches that found nothing |
ai.features.member_improvement | boolean | false | Suggestions to members for improving their listings (engine-dmo) |
ai.features.quality_review | boolean | false | Accepted; no feature reads it in this release |
ai.features.image_generation | boolean | false | Not in this release; true is refused |
ai.duplicates.threshold | number | 0.9 | The similarity, 0.5 to 1, at which a pair is offered as a duplicate |
Translation drafting and disclosure
| Key | Type | Default | Meaning |
|---|---|---|---|
ai.translation.memory_examples | integer | 8 | Translation memory units sent with a request as examples, 0 to 50 |
ai.translation.length_ratio_bands | map | {} | 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_signals | list of strings | [] | Accepted; every quality signal runs |
ai.disclosure.public | string | drafted_only | Whether public pages disclose AI assistance: never, drafted_only (text an AI drafted) or all |
ai.disclosure.admin | string | always | Always 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.
| Key | Type | Default | Meaning |
|---|---|---|---|
translation.glossary.auto_seed | list of strings | [organisation, listing, area, tag] | Where glossary terms are seeded from automatically: organisation names, listing and area titles, tags |
translation.glossary.refresh_on_change | boolean | true | Refresh seeded terms when their source changes |
translation.memory.enabled | boolean | true | Keep a translation memory of approved translations |
translation.memory.suggest_exact_matches | boolean | true | Offer an exact memory match as a suggestion |
translation.memory.fuzzy_threshold | number | 0.75 | The similarity from which a near match is offered |
translation.memory.fuzzy_matches | integer | 3 | The most near matches offered |
engine.yaml: mail and newsletter
Every key of engine.yaml's mail section (the transactional mail driver for SMTP, Postmark, Amazon SES or Resend, bounce webhooks, templates, suppression and broadcast sending) and its newsletter section, with type, default and meaning.
engine.yaml: DMO modules
Every key of engine.yaml's claims section (ownership claim methods, signal weights, auto-approval) and membership section (renewal reminders), with type, default and meaning, and where the portal settings live.