docs(settings): generate the settings reference from the definitions

The hand-maintained settings page documented 95 of 258 settings and
.env-template 42, and both drifted as fields were added. The field
descriptions now live on the model, so the reference is rendered from it:

  python -m docsgpt.core.settings.reference --write

writes docs/content/Deploying/Settings-Reference.mdx, one section per
settings group with each field's type, default, constraints, aliases and
description. --check reports a stale page, and tests/core/test_settings.py
fails when the checked-in page no longer matches the definitions, so a
new setting cannot land undocumented.

test_settings.py also pins the composition contract: every group field is
a flat Settings attribute, no field is defined twice, every field has a
description, and the secret-normalising validator of every group is
applied (the case that a shared method name would silently drop).

The App Configuration page points at the reference instead of at
settings.py, and the reference is listed in the Deploying navigation.
This commit is contained in:
arc53-machine committed 2026-09-17 11:04:11 +01:00
1 parent c17b23378e
commit 5a5226ebe0
5 files changed
+1943 -10

No files matched your search

+9 -10
View File
@@ -27,13 +27,13 @@ API_KEY=YOUR_OPENAI_API_KEY
LLM_NAME=gpt-4o
```
### 2. Configuration via `settings.py` file (Advanced)
### 2. Configuration in code (Advanced)
For more advanced configurations or if you prefer to manage settings directly in code, you can modify the `settings.py` file. This file is located in the `docsgpt/core` directory of your DocsGPT project.
Every setting is defined in the `docsgpt/core/settings/` package, one module per domain (`auth.py`, `llm.py`, `embeddings.py`, ...). If you prefer to manage defaults directly in code, change them there; the `.env` file and the process environment still override whatever the code says.
While modifying `settings.py` offers more flexibility, it's generally recommended to use the `.env` file for basic settings and reserve `settings.py` for more complex adjustments or when you need to configure settings programmatically.
Using the `.env` file is recommended for day-to-day configuration. Reserve code changes for new settings or for defaults you want every deployment of your fork to share.
**Location of `settings.py`:** `docsgpt/core/settings.py`
The [Settings Reference](/Deploying/Settings-Reference) lists every setting with its type, default and description, generated from those definitions.
## Basic Settings Explained
@@ -289,7 +289,7 @@ DocsGPT includes a JWT (JSON Web Token) based authentication feature for managin
### `AUTH_TYPE` Overview
The `AUTH_TYPE` setting in your `.env` file or `settings.py` determines the authentication method used by DocsGPT. This allows you to control how users authenticate with your DocsGPT instance.
The `AUTH_TYPE` setting in your `.env` file determines the authentication method used by DocsGPT. This allows you to control how users authenticate with your DocsGPT instance.
| Value | Description |
| ------------- | ------------------------------------------------------------------------------------------- |
@@ -300,7 +300,7 @@ The `AUTH_TYPE` setting in your `.env` file or `settings.py` determines the auth
#### How to Configure
Add the following to your `.env` file (or set in `settings.py`):
Add the following to your `.env` file:
```env
# Shared signing key (required in production for every authentication mode)
@@ -547,11 +547,10 @@ recovers.
## Exploring More Settings
These are just the basic settings to get you started. The `settings.py` file contains many more advanced options that you can explore to further customize DocsGPT, such as:
These are just the basic settings to get you started. DocsGPT has many more advanced options, such as:
- Vector store configuration (`VECTOR_STORE`, Qdrant, Milvus, LanceDB settings) If you're looking for an easy way to set up a vector store with pgvector, try [Neon](https://get.neon.com/docsgpt).
- Retriever settings (`RETRIEVERS_ENABLED`)
- Cache settings (`CACHE_REDIS_URL`)
- And many more!
- Sandbox, scheduler, guardrails and event-stream tuning
For a complete list of available settings and their descriptions, refer to the `settings.py` file in `docsgpt/core`. Remember to restart your Docker containers after making changes to your `.env` file or `settings.py` for the changes to take effect.
The [Settings Reference](/Deploying/Settings-Reference) lists every setting with its type, default and description. Remember to restart your Docker containers after making changes to your `.env` file for the changes to take effect.
File diff suppressed because it is too large. Load diff
+4
View File
@@ -3,6 +3,10 @@ export default {
"title": "⚙️ App Configuration",
"href": "/Deploying/DocsGPT-Settings"
},
"Settings-Reference": {
"title": "📖 Settings Reference",
"href": "/Deploying/Settings-Reference"
},
"OIDC-SSO": {
"title": "🔐 SSO with OIDC",
"href": "/Deploying/OIDC-SSO"
+169
View File
@@ -0,0 +1,169 @@
"""Render the settings reference page from the ``Settings`` definitions.
The page under ``docs/content/Deploying/Settings-Reference.mdx`` is generated
from the field types, defaults and descriptions in this package, so the model
is the single source of truth. Regenerate it after changing a setting::
python -m docsgpt.core.settings.reference --write
``--check`` exits non-zero when the checked-in page is stale; the test suite
runs the same comparison.
"""
from __future__ import annotations
import argparse
import inspect
import json
import sys
import types
import typing
from pathlib import Path
from typing import Any, Literal, Optional, Union
from pydantic import AliasChoices
from pydantic.fields import FieldInfo
from docsgpt.core.paths import home_dir
from docsgpt.core.settings import SETTINGS_GROUPS, Settings
REFERENCE_PATH = Path("docs") / "content" / "Deploying" / "Settings-Reference.mdx"
HOME_PLACEHOLDER = "<DOCSGPT_HOME>"
_HEADER = """\
---
title: Settings Reference
description: Every DocsGPT setting, grouped by domain, with its type, default and purpose.
---
{/* GENERATED FILE. Do not edit by hand: run `python -m docsgpt.core.settings.reference --write`. */}
# Settings Reference
Every setting DocsGPT reads, generated from `docsgpt/core/settings/`. Each one is
an environment variable of the same name, set in `.env` or the process
environment; see [App Configuration](/Deploying/DocsGPT-Settings) for how the
file is found and for worked examples. `<DOCSGPT_HOME>` below is the data home
described there.
"""
def _mdx(text: str) -> str:
"""Escape prose for MDX, where braces open expressions and ``<`` opens JSX."""
return text.replace("{", "\\{").replace("}", "\\}").replace("<", "&lt;")
def _type_name(annotation: Any) -> str:
origin = typing.get_origin(annotation)
if origin in (Union, types.UnionType):
args = [a for a in typing.get_args(annotation) if a is not type(None)]
return " | ".join(_type_name(a) for a in args)
if origin is Literal:
return " | ".join(json.dumps(v) for v in typing.get_args(annotation))
if origin is not None:
return getattr(origin, "__name__", str(origin))
return getattr(annotation, "__name__", str(annotation))
def _default_text(field: FieldInfo) -> str:
value = field.default_factory() if field.default_factory is not None else field.default
if value is None:
return "unset"
if isinstance(value, bool):
return "`true`" if value else "`false`"
if isinstance(value, str):
value = value.replace(str(home_dir()), HOME_PLACEHOLDER)
return "`\"\"`" if value == "" else f"`{value}`"
if isinstance(value, (list, dict)):
return f"`{json.dumps(value)}`"
return f"`{value}`"
def _constraints(field: FieldInfo) -> list[str]:
out = []
for item in field.metadata:
for attr, symbol in (("gt", ">"), ("ge", ">="), ("lt", "<"), ("le", "<=")):
if hasattr(item, attr):
out.append(f"{symbol} {getattr(item, attr)}")
return out
def _aliases(name: str, field: FieldInfo) -> list[str]:
alias = field.validation_alias
if isinstance(alias, AliasChoices):
return [str(c) for c in alias.choices if str(c) != name]
if isinstance(alias, str) and alias != name:
return [alias]
return []
def _render_field(name: str, field: FieldInfo) -> str:
facts = [f"Type `{_type_name(field.annotation)}`", f"default {_default_text(field)}"]
constraints = _constraints(field)
if constraints:
facts.append("must be " + " and ".join(constraints))
aliases = _aliases(name, field)
if aliases:
facts.append("also read from " + ", ".join(f"`{a}`" for a in aliases))
# The facts are code spans, which MDX leaves alone; only prose needs escaping.
lines = [f"### `{name}`", "", ", ".join(facts) + "."]
if field.deprecated:
note = field.deprecated if isinstance(field.deprecated, str) else "This setting is deprecated."
lines += ["", f"**Deprecated.** {_mdx(str(note))}"]
if field.description:
lines += ["", _mdx(field.description)]
return "\n".join(lines)
def _group_intro(group: type) -> str:
doc = inspect.getdoc(group) or ""
return doc.split("\n\n", 1)[0].replace("\n", " ").strip()
def render_reference() -> str:
"""The full reference page as MDX text."""
parts = [_HEADER]
for title, group in SETTINGS_GROUPS:
parts.append(f"\n## {title}\n")
intro = _group_intro(group)
if intro:
parts.append(_mdx(intro) + "\n")
for name in group.model_fields:
parts.append(_render_field(name, Settings.model_fields[name]) + "\n")
return "\n".join(parts).rstrip("\n") + "\n"
def reference_path(root: Optional[Path] = None) -> Path:
"""Where the generated page lives in a checkout; ``root`` defaults to the repository root."""
if root is None:
root = Path(__file__).resolve().parents[3]
return root / REFERENCE_PATH
def main(argv: Optional[list[str]] = None) -> int:
parser = argparse.ArgumentParser(description=__doc__.split("\n\n", 1)[0])
action = parser.add_mutually_exclusive_group()
action.add_argument("--write", action="store_true", help="write the page into the docs tree")
action.add_argument("--check", action="store_true", help="exit 1 if the checked-in page is stale")
args = parser.parse_args(argv)
rendered = render_reference()
path = reference_path()
if args.write:
path.write_text(rendered, encoding="utf-8")
print(f"wrote {path}")
return 0
if args.check:
current = path.read_text(encoding="utf-8") if path.exists() else ""
if current != rendered:
print(f"{path} is stale; run: python -m docsgpt.core.settings.reference --write", file=sys.stderr)
return 1
print(f"{path} is up to date")
return 0
sys.stdout.write(rendered)
return 0
if __name__ == "__main__":
sys.exit(main())
+109
View File
@@ -0,0 +1,109 @@
"""Contract tests for ``docsgpt.core.settings``.
``Settings`` is composed from one ``SettingsGroup`` per domain; these tests pin
the properties that composition must keep (flat names, no duplicate fields,
validators from every group applied) and that the generated reference page
tracks the definitions.
"""
from pathlib import Path
import pytest
from docsgpt.core.settings import SETTINGS_GROUPS, Settings, settings
from docsgpt.core.settings.reference import reference_path, render_reference
SECRET_FIELDS = (
"API_KEY",
"OPENAI_API_KEY",
"ANTHROPIC_API_KEY",
"GOOGLE_API_KEY",
"GROQ_API_KEY",
"HUGGINGFACE_API_KEY",
"NOVITA_API_KEY",
"EMBEDDINGS_KEY",
"FALLBACK_LLM_API_KEY",
"QDRANT_API_KEY",
"ELEVENLABS_API_KEY",
"INTERNAL_KEY",
)
@pytest.mark.unit
class TestComposition:
def test_every_group_field_is_a_flat_settings_attribute(self):
for _, group in SETTINGS_GROUPS:
for name in group.model_fields:
assert name in Settings.model_fields, name
assert hasattr(settings, name), name
def test_no_field_is_defined_in_two_groups(self):
owners: dict[str, str] = {}
for title, group in SETTINGS_GROUPS:
for name in group.model_fields:
assert name not in owners, f"{name} is defined in both {owners[name]} and {title}"
owners[name] = title
assert len(owners) == len(Settings.model_fields)
def test_every_field_has_a_description(self):
missing = [name for name, field in Settings.model_fields.items() if not field.description]
assert not missing, f"settings without a description: {missing}"
def test_defaults_load_without_an_env_file(self, monkeypatch):
for name in Settings.model_fields:
monkeypatch.delenv(name, raising=False)
fresh = Settings(_env_file=None)
assert fresh.LLM_PROVIDER == "docsgpt"
assert fresh.VECTOR_STORE == "faiss"
@pytest.mark.unit
class TestValidators:
"""Validators live on the group that owns the field; composition must keep all of them.
Two groups defining a validator under the same method name would silently
keep only one, so this checks every secret field, across every group.
"""
@pytest.mark.parametrize("name", SECRET_FIELDS)
@pytest.mark.parametrize("raw", ["None", "none", "", " "])
def test_unset_secret_spellings_become_none(self, name, raw):
assert getattr(Settings.model_validate({name: raw}), name) is None
@pytest.mark.parametrize("name", SECRET_FIELDS)
def test_secret_is_stripped(self, name):
assert getattr(Settings.model_validate({name: " k3y "}), name) == "k3y"
def test_normalize_api_key_classmethod_is_kept(self):
assert Settings.normalize_api_key("None") is None
assert Settings.normalize_api_key(" x ") == "x"
assert Settings.normalize_api_key(42) == 42
def test_postgres_uris_are_normalized(self):
loaded = Settings.model_validate(
{"POSTGRES_URI": "postgres://u:p@h/db", "PGVECTOR_CONNECTION_STRING": "postgresql+psycopg://u:p@h/v"}
)
assert loaded.POSTGRES_URI.startswith("postgresql+psycopg://")
assert loaded.PGVECTOR_CONNECTION_STRING.startswith("postgresql://")
def test_legacy_docling_ocr_aliases_are_read(self):
loaded = Settings.model_validate({"DOCLING_OCR_ENABLED": "true", "DOCLING_OCR_MIN_CHARS_PER_PAGE": "7"})
assert loaded.OCR_ENABLED is True
assert loaded.OCR_MIN_CHARS_PER_PAGE == 7
@pytest.mark.unit
class TestReference:
def test_reference_lists_every_setting_once(self):
page = render_reference()
for name in Settings.model_fields:
assert page.count(f"### `{name}`") == 1, name
def test_checked_in_reference_is_current(self):
path: Path = reference_path()
if not path.exists():
pytest.skip("docs tree not present (installed package, not a checkout)")
assert path.read_text(encoding="utf-8") == render_reference(), (
"docs/content/Deploying/Settings-Reference.mdx is stale; "
"run: python -m docsgpt.core.settings.reference --write"
)