forked from ENTERPILOT/GoModel
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconfig.example.yaml
More file actions
577 lines (524 loc) · 22.5 KB
/
Copy pathconfig.example.yaml
File metadata and controls
577 lines (524 loc) · 22.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
# GoModel Configuration (optional)
# Copy to config/config.yaml to customize.
# Environment variables always override values in this file.
# All settings have sensible defaults — no config file is required.
server:
port: "8080"
base_path: "/" # env: BASE_PATH; set to "/g" to serve the gateway under https://example.com/g/
master_key: "your-secret-key"
body_size_limit: "10M"
swagger_enabled: false # env: SWAGGER_ENABLED; requires a binary built with -tags=swagger
pprof_enabled: false # expose /debug/pprof/* for local profiling only
enable_passthrough_routes: true # expose /p/{provider}/{endpoint} passthrough routes
allow_passthrough_v1_alias: true # allow /p/{provider}/v1/... while keeping /p/{provider}/... canonical
user_path_header: "X-GoModel-User-Path" # env: USER_PATH_HEADER; inbound header used for user_path scoping
enabled_passthrough_providers: ["openai", "anthropic", "cohere", "openrouter", "kilo", "zai", "sglang", "vllm", "llmd", "deepseek", "bailian"] # providers enabled on /p/{provider}/...
realtime_enabled: true # env: REALTIME_ENABLED; expose /v1/realtime websocket and /p/{provider}/v1/realtime upgrades (OpenAI only)
pid_file: "data/gomodel.pid" # env: PID_FILE; where the running gateway records its process id so `gomodel --reload` can find it. Set per instance when several gateways share a host; empty writes no pid file and disables --reload; changing it needs a restart, not a reload
models:
enabled_by_default: true # env: MODELS_ENABLED_BY_DEFAULT; when false, models stay unavailable until an access override allows one or more user paths
configured_provider_models_mode: "fallback" # env: CONFIGURED_PROVIDER_MODELS_MODE; "fallback" uses configured lists only when upstream /models is unavailable/empty, "allowlist" exposes only configured models and skips upstream /models for configured lists
# Tagging based on headers: label every request from the listed headers. Labels
# are recorded in usage tracking and audit logs. A header value can carry several
# labels split by `delimiter` (default: ","). `prefix` is trimmed from each
# extracted label only — the header itself is forwarded unchanged unless
# `do_not_pass: true`. An env entry replaces the whole YAML entry with the same
# header name (unset companion vars reset fields to defaults, they do not
# inherit the YAML values); other env entries are appended:
# TAGGING_HEADER_1=X-My-Tags
# TAGGING_HEADER_1_PREFIX=tag- # optional
# TAGGING_HEADER_1_DONOTPASS=true # optional, default false (headers are passed as-is)
# TAGGING_HEADER_1_DELIMITER=";" # optional, default ","
# Entries declared here or via env are read-only in the dashboard; omit this
# section to manage tagging headers only from the UI (Settings → Tagging based on headers).
# tagging:
# headers:
# - header: X-My-Tags
# prefix: "tag-" # optional: "tag-alpha, beta" → labels "alpha", "beta"
# - header: X-Internal-Routing
# do_not_pass: true # stripped before forwarding to the provider
# delimiter: ";"
# Session keeping: identify which requests belong to one client session, for
# sticky virtual-model load balancing and audit-log session grouping. Everything
# below is the default — omit the section entirely for the same behavior. Extra
# headers are merged over the built-in known-tools registry; the optional
# transform "session-uuid" extracts a session_<uuid> value.
# session:
# enabled: true
# auto_detect: true
# builtin_rules: true
# headers:
# - header: X-My-Session
# Virtual models as infrastructure-as-code: redirects, load balancers, and access
# policies. These override admin-store rows with the same source and are read-only
# in the dashboard. The VIRTUAL_MODELS env var (a JSON array) merges over this list
# and wins per source. Omit this section to manage virtual models only from the UI.
# virtual_models:
# - source: regular # a plain alias
# target: anthropic/claude-sonnet-4-6
# slowdown: 0.5 # add 50%; use 0 to disable inherited slowdown (active range: 0.1-10)
# - source: smart # weighted round-robin load balancer
# strategy: round_robin # round_robin (default) | cost | adaptive (uses a routing extension when registered; otherwise falls back to round_robin)
# targets:
# - { model: openai/gpt-4o, weight: 2 }
# - { model: anthropic/claude-sonnet-4-6 }
# - source: cheap # always route to the cheapest available target
# strategy: cost
# targets:
# - { model: openai/gpt-4o }
# - { model: groq/llama-3.3-70b }
# MCP gateway: aggregate upstream MCP (Model Context Protocol) servers behind the
# authenticated /mcp endpoint. Tools/prompts are namespaced as {server}_{name};
# /mcp/{server} exposes one upstream with original names. Servers declared here or
# via the MCP_SERVERS env var (a JSON object merged over this map, winning per name)
# are read-only in the dashboard; the dashboard manages its own additional servers.
# The gateway is the credential boundary: client API keys never reach an upstream,
# and upstream headers support ${ENV} references. `user_paths` limits which callers
# can even discover a server (subtree match, like virtual models).
# mcp:
# enabled: true # env: MCP_ENABLED (default: true; a no-op with no servers)
# servers:
# github:
# url: https://api.githubcopilot.com/mcp
# transport: http # http (streamable HTTP, default) | sse (legacy) | stdio
# headers:
# Authorization: "Bearer ${GITHUB_PAT}"
# description: GitHub tools
# allowed_tools: [] # allowlist of upstream tool names; empty = all
# disallowed_tools: [] # blocklist, applied after the allowlist
# user_paths: [] # restrict visibility to these user-path subtrees; empty = everyone
# tool_timeout: 30s # per tools/call upper bound
# local-files: # stdio servers spawn a subprocess and are declarative-only:
# transport: stdio # the admin API and dashboard reject them by design
# command: npx
# args: ["-y", "@modelcontextprotocol/server-filesystem", "/data"]
# env: # subprocesses inherit only PATH/HOME/TMPDIR/USER/LANG — never the
# SOME_TOKEN: "${SOME_TOKEN}" # gateway's API keys; pass extras here explicitly
cache:
model:
refresh_interval: 3600 # how often to refresh the model registry (seconds, default: 3600)
recheck_interval: 60 # env: PROVIDER_RECHECK_INTERVAL; how often providers whose last refresh failed are re-probed for recovery (seconds, default: 60; 0 disables)
local:
cache_dir: ".cache" # local cache directory
# To use Redis instead of local cache, remove `local` and uncomment:
# redis:
# url: "redis://localhost:6379"
# key: "gomodel:models"
# ttl: 86400 # 24 hours in seconds
# response:
# simple: # omit the whole `simple` key to disable exact-match caching (unless RESPONSE_CACHE_SIMPLE_ENABLED=true)
# enabled: true # default when `simple` is present; set false to disable while keeping the block
# redis:
# url: "redis://localhost:6379"
# key: "gomodel:response:"
# ttl: 3600
# semantic: # omit the whole `semantic` key to disable semantic caching (unless SEMANTIC_CACHE_ENABLED=true)
# enabled: true
# embedder:
# provider: openai # must match a key under `providers` (openai, gemini, groq, …)
# model: text-embedding-3-small # optional; provider-specific defaults apply if omitted
# vector_store:
# type: qdrant # required: qdrant | pgvector | pinecone | weaviate (pick one block below)
# qdrant:
# url: "http://localhost:6333"
# collection: "gomodel_semantic"
# api_key: "" # optional for local Qdrant
# # pgvector:
# # url: "postgres://user:pass@localhost:5432/gomodel"
# # table: "gomodel_semantic_cache" # optional; default if omitted
# # dimension: 1536 # must match embedding model output
# # pinecone:
# # host: "https://your-index.svc.region.pinecone.io" # data-plane host
# # api_key: "..."
# # namespace: "" # optional
# # dimension: 1536 # must match your Pinecone index
# # weaviate:
# # url: "http://localhost:8080"
# # class: "GomodelSemanticCache" # PascalCase recommended (GraphQL)
# # api_key: "" # optional
storage:
type: "sqlite" # "sqlite", "postgresql", or "mongodb"
sqlite:
path: "data/gomodel.db"
postgresql:
url: "postgres://user:pass@localhost/gomodel"
max_conns: 10
mongodb:
url: "mongodb://localhost:27017"
database: "gomodel"
logging:
enabled: true
log_bodies: true # WARNING: may contain sensitive data
log_revision_bodies: true # store rewritten bodies from request rewriters (needs log_bodies)
log_headers: true
buffer_size: 1000
flush_interval: 5 # seconds
retention_days: 30 # 0 = keep forever
only_model_interactions: true
usage:
# Usage actions require USAGE_ENABLED=true (or usage.enabled: true) and a supported
# storage backend; pricing recalculation appears only when both usage tracking and
# pricing_recalculation_enabled are enabled.
enabled: true
pricing_recalculation_enabled: true
enforce_returning_usage_data: true
buffer_size: 1000
flush_interval: 5
retention_days: 90
budgets:
enabled: true # env: BUDGETS_ENABLED; with no configured budgets this has no effect
user_paths:
# Env equivalent:
# SET_BUDGET_USER__PATH__EXAMPLE="daily=10,weekly=50"
- path: "/user/path/example"
limits:
- period: "daily" # hourly, daily, weekly, monthly; stored in DB as period_seconds
amount: 10.00
- period: "weekly"
amount: 50.00
labels:
# Request labels are matched verbatim, so they have no env-var form:
# declare label budgets here or in the dashboard.
- label: "Mobile-App-iOS"
limits:
- period: "monthly"
amount: 500.00
rate_limits:
enabled: true # env: RATE_LIMITS_ENABLED; with no configured rules this has no effect
user_paths:
# Env equivalent:
# SET_RATE_LIMIT_USER__PATH__EXAMPLE="rpm=100,tpm=50000,rpd=10000,concurrent=10"
- path: "/user/path/example"
limits:
- period: "minute" # minute, hour, day, concurrent; stored in DB as period_seconds
max_requests: 100
max_tokens: 50000 # requires usage tracking (USAGE_ENABLED=true)
- period: "day"
max_requests: 10000
- period: "concurrent" # max_requests caps in-flight requests
max_requests: 10
# Provider rules cap one configured provider across all consumers and
# models. Virtual-model load balancing and failover skip a saturated
# provider while another target has capacity; when no target can take the
# request it gets 429. Env equivalent: SET_PROVIDER_RATE_LIMIT_OPENAI="rpm=500"
providers:
- name: "openai"
limits:
- period: "minute"
max_requests: 500
max_tokens: 200000
- period: "concurrent"
max_requests: 50
# Model rules cap one model: "openai/gpt-4o" pins one provider's model,
# a bare id like "gpt-4o" caps the model across every provider. No env
# form; use YAML or the admin API/dashboard.
models:
- model: "openai/gpt-4o"
limits:
- period: "minute"
max_tokens: 90000
metrics:
enabled: false
endpoint: "/metrics"
http:
timeout: 600 # seconds (10 minutes)
response_header_timeout: 600
workflows:
refresh_interval: 1m
# Global resilience settings (applied to all providers by default)
# Individual providers can override any of these values.
resilience:
retry:
max_retries: 3
initial_backoff: 1s
max_backoff: 30s
backoff_factor: 2.0
jitter_factor: 0.1
guardrails:
enabled: false
enable_for_batch_processing: false
rules:
# Each entry is a guardrail instance. Multiple instances of the same type are supported.
# Guardrails with the same "order" run in parallel; different orders run sequentially.
- name: "safety-prompt"
type: "system_prompt"
order: 0
system_prompt:
mode: "decorator" # "inject", "override", or "decorator"
content: "Always be safe and respectful."
# Example: a second system_prompt instance running at the same order (parallel)
# - name: "compliance-prompt"
# type: "system_prompt"
# order: 0
# system_prompt:
# mode: "inject"
# content: "Follow all compliance rules."
# Names support spaces and unicode characters:
# - name: "проверка безопасности"
# type: "system_prompt"
# order: 0
# system_prompt:
# mode: "decorator"
# content: "Соблюдайте все правила безопасности."
# - name: "安全検査"
# type: "system_prompt"
# order: 1
# system_prompt:
# mode: "inject"
# content: "Follow safety guidelines."
# Example: rewrite user messages with an auxiliary model before the main call.
# The default prompt is a LiteLLM-derived anonymization prompt; override it to
# implement other LLM-based rewrites.
# - name: "privacy-rewrite"
# type: "llm_based_altering"
# user_path: "/team/privacy" # optional base path for internal rewrite calls and audit logs
# order: 1
# llm_based_altering:
# model: "gpt-4o-mini"
# roles: ["user"]
# max_tokens: 4096
# skip_content_prefix: "### safe"
# # prompt: "Custom rewrite instructions here."
failover:
enabled: true # env: FAILOVER_ENABLED; default true
manual_rules_path: "config/failover.example.json" # optional JSON map: {"primary-model": ["fallback-1", "provider/model"]}
rules:
"gpt-4o":
- "azure/gpt-4o"
- "gemini/gemini-2.5-pro"
disabled_models:
- "claude-sonnet-4"
default_mode: "manual" # deprecated compatibility field; accepted but ignored at runtime
providers:
openai:
type: openai
api_key: "sk-..."
# Identified sessions stay on one key by default, preserving provider prompt
# cache affinity while still spreading different sessions across keys.
# Equivalent env vars: OPENAI_API_KEY_2, OPENAI_API_KEY_3, ...
# api_keys:
# - "${OPENAI_API_KEY_2}"
# - "${OPENAI_API_KEY_3}"
# Set false for strict per-request round robin (usually worse for prompt caching).
# session_sticky_keys: false
# Per-provider resilience overrides (optional).
# Only specified fields override the global defaults above.
# resilience:
# retry:
# max_retries: 5
anthropic:
type: anthropic
api_key: "sk-ant-..."
cohere:
type: cohere
api_key: "${COHERE_API_KEY}"
bailian:
type: bailian
api_key: "${BAILIAN_API_KEY}"
# base_url: "https://dashscope.aliyuncs.com/compatible-mode/v1"
# Alternative regions (replace {workspace-id} with your workspace):
# Singapore: "https://{workspace-id}.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1"
# Frankfurt: "https://{workspace-id}.eu-central-1.maas.aliyuncs.com/compatible-mode/v1"
# Hong Kong: "https://{workspace-id}.cn-hongkong.maas.aliyuncs.com/compatible-mode/v1"
gemini:
type: gemini
api_key: "..."
# Chat/responses use Gemini's native generateContent API by default.
# Set GEMINI_API_MODE=openai_compatible or api_mode: openai_compatible to use
# Gemini's OpenAI-compatible API instead.
# Native mode accepts image data URLs but does not fetch remote image URLs yet.
vertex:
type: vertex
auth_type: gcp_adc # or gcp_service_account
vertex_project: "my-gcp-project"
vertex_location: "us-central1"
# service_account_file: "/var/run/secrets/google/service-account.json"
# service_account_json: "${VERTEX_SERVICE_ACCOUNT_JSON}"
# service_account_json_base64: "${VERTEX_SERVICE_ACCOUNT_JSON_BASE64}"
# api_mode: native # or openai_compatible
# models:
# - id: "google/gemini-2.5-flash"
xai:
type: xai
api_key: "..."
groq:
type: groq
api_key: "gsk_..."
fireworks:
type: fireworks
api_key: "fw_..."
# models:
# - id: "accounts/fireworks/models/gpt-oss-120b"
chutes:
type: chutes
api_key: "${CHUTES_API_KEY}"
# base_url defaults to "https://llm.chutes.ai/v1".
# Set base_url when using a different compatible endpoint.
elevenlabs:
type: elevenlabs
api_key: "${ELEVENLABS_API_KEY}"
# base_url defaults to "https://api.elevenlabs.io".
# Voice-only provider: text-to-speech and speech-to-text, no chat. The
# OpenAI "voice" field must be an ElevenLabs voice_id.
meta:
type: meta
api_key: "..."
# Muse Spark models are not in the upstream model catalog yet; supply
# metadata here to advertise context window and pricing to the gateway.
# models:
# - id: "muse-spark-1.1"
# metadata:
# context_window: 1048576
# pricing:
# currency: USD
# input_per_mtok: 1.25
# output_per_mtok: 4.25
zai:
type: zai
api_key: "..."
# Optional: use GLM Coding Plan endpoint instead of the general endpoint.
# base_url: "https://api.z.ai/api/coding/paas/v4"
xiaomi:
type: xiaomi
api_key: "..."
opencode_go:
type: opencode_go
api_key: "${OPENCODE_GO_API_KEY}"
ollama:
type: ollama
base_url: "http://localhost:11434/v1"
vllm:
type: vllm
base_url: "http://localhost:8000/v1"
# Optional: set this only when vllm serve was started with --api-key.
# api_key: "token-abc123"
# llm-d Router / Endpoint Picker (EPP)
llmd:
type: llmd
base_url: "http://quickstart-epp.llm-d.svc.cluster.local/v1"
# Optional trusted request class injected as llm-d inference headers.
# inference_objective: "standard-traffic"
# Derive the llm-d fairness ID from GoModel's effective user path (default).
fairness_from_user_path: true
# Declare models when the route does not forward GET /v1/models.
models:
- id: "Qwen/Qwen2.5-0.5B-Instruct"
sglang:
type: sglang
base_url: "http://localhost:30000/v1"
# Optional: set this only when launch_server uses --api-key.
# api_key: "token-abc123"
# Custom OpenAI-compatible provider
# my-provider:
# type: openai
# base_url: "https://api.example.com/v1"
# api_key: "..."
# Example: LM Studio. Despite being "Ollama-like", LM Studio speaks the
# OpenAI-compatible API (/v1/chat/completions, /v1/embeddings) and has NO
# native Ollama API. Configure it as "openai" (or "vllm"), NOT "ollama" —
# the ollama type sends embeddings to Ollama's native /api/embed, which LM
# Studio does not implement.
# lmstudio:
# type: openai
# base_url: "http://localhost:1234/v1"
# api_key: "lm-studio" # any non-empty value; LM Studio ignores it
# Example: Groq (OpenAI-compatible)
# groq:
# type: "openai"
# base_url: "https://api.groq.com/openai/v1"
# api_key: "${GROQ_API_KEY}"
# Example: OpenRouter with an explicit configured model list.
# In fallback mode (default), this list is used only if upstream /models is
# unavailable or empty. In allowlist mode, only these models are exposed and
# upstream /models is skipped for this provider.
# You can also set OPENROUTER_MODELS="openai/gpt-oss-120b,anthropic/claude-sonnet-4".
# openrouter:
# type: "openrouter"
# base_url: "https://openrouter.ai/api/v1"
# api_key: "${OPENROUTER_API_KEY}"
# models:
# - openai/gpt-oss-120b
# - anthropic/claude-sonnet-4
# Example: Kilo AI Gateway. Model IDs use provider/model and are forwarded
# unchanged. You can also set KILO_MODELS as a comma-separated env var.
# kilo:
# type: "kilo"
# base_url: "https://api.kilo.ai/api/gateway"
# api_key: "${KILO_API_KEY}"
# models:
# - anthropic/claude-sonnet-4.5
# - openai/gpt-5.5
# Example: Azure OpenAI
# azure:
# type: "azure"
# base_url: "${AZURE_BASE_URL}"
# api_key: "${AZURE_API_KEY}"
# api_version: "2024-10-21"
# Example: Oracle OpenAI-compatible endpoint
# oracle:
# type: "oracle"
# base_url: "${ORACLE_BASE_URL}"
# api_key: "${ORACLE_API_KEY}"
# # Optional when ORACLE_MODELS is set as a comma-separated env var instead.
# models:
# - openai.gpt-oss-120b
# - xai.grok-3
# Example: DeepSeek. GoModel translates /v1/responses requests to DeepSeek
# chat completions because DeepSeek does not expose a native Responses API.
# deepseek:
# type: "deepseek"
# base_url: "https://api.deepseek.com"
# api_key: "${DEEPSEEK_API_KEY}"
# Example: Amazon Bedrock. The provider has no API key — auth is taken from
# the standard AWS credential chain (env vars, AWS_PROFILE, IAM Identity
# Center, container/instance roles). Set base_url to either an AWS region
# ("us-east-1") or a full https endpoint to enable the provider; without it
# gomodel falls back to AWS_REGION / AWS_DEFAULT_REGION.
# bedrock:
# type: "bedrock"
# base_url: "us-east-1"
# # Optional configured model allowlist (model IDs or inference profiles)
# # models:
# # - anthropic.claude-3-5-haiku-20241022-v1:0
# # - amazon.nova-lite-v1:0
# Example: Amazon Bedrock Mantle. This is the OpenAI-compatible Bedrock
# endpoint and is required for Responses-only models such as GPT-5.6.
# Authenticate with api_key or omit it to use the AWS credential chain.
# bedrock-mantle:
# type: "bedrock-mantle"
# base_url: "us-east-1"
# api_key: "${BEDROCK_MANTLE_API_KEY}"
# api_mode: "auto" # auto, openai, or standard
# models:
# - openai.gpt-5.6-sol
# - openai.gpt-5.6-terra
# - openai.gpt-5.6-luna
# Example: local Ollama server with explicit per-model metadata.
# Use the rich entry form to declare context_window, pricing, capabilities,
# etc. for models that aren't in the upstream ai-model-list registry. Plain
# string entries still work and produce unenriched models as before.
# Declared fields are merged onto any remote-registry entry; operator values
# win per-field.
# nippur:
# type: "ollama"
# base_url: "http://127.0.0.1:8080/v1"
# models:
# - id: GLM-4.7-Flash
# metadata:
# display_name: "GLM 4.7 Flash (local)"
# context_window: 131072
# max_output_tokens: 8192
# modes: ["chat"]
# capabilities:
# tools: true
# pricing:
# currency: USD
# input_per_mtok: 0
# output_per_mtok: 0
# - Gemma4-31B
# Extensions are absent by default. Add a named section only when a custom
# distribution requires it. Core preserves these values without depending on
# the extension's schema; the owning extension strictly validates its section.
# extensions:
# example:
# enabled: true