> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mirobody.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Configuration

> The model key, the files configuration is read from, and the settings a deployment changes.

export const OssVersion = ({lang = "en"}) => <p className="text-sm text-gray-500 dark:text-gray-400">
    {lang === "zh" ? "对应 mirobody " : "Written for mirobody "}
    <a href="https://github.com/thetahealth/mirobody/tree/c1aae297b4f3fad5cb87c5fc5bffc5163f7758c9">
      <code>1.5.3</code>
    </a>
  </p>;

export const OssLink = ({path = "", children}) => {
  const base = "https://github.com/thetahealth/mirobody";
  const commit = "c1aae297b4f3fad5cb87c5fc5bffc5163f7758c9";
  const href = !path ? base + "/tree/" + commit : base + (path.endsWith("/") ? "/tree/" : "/blob/") + commit + "/" + path.replace(/\/$/, "");
  return <a href={href}>{children ?? <code>{path}</code>}</a>;
};

export const Fact = ({k, code = false, sep = ", "}) => {
  const facts = {
    "version": "1.5.3",
    "commit": "c1aae297b4f3fad5cb87c5fc5bffc5163f7758c9",
    "commitShort": "c1aae29",
    "repo": "https://github.com/thetahealth/mirobody",
    "python": "3.12",
    "port": 18060,
    "url": "http://localhost:18060",
    "pgPort": 18062,
    "redisPort": null,
    "account": "you@mirobody.ai",
    "accounts": ["you@mirobody.ai", "mom@mirobody.ai"],
    "code": "111111",
    "mcpUrlTtlDays": 10,
    "llmKeys": ["OPENROUTER_API_KEY", "DASHSCOPE_API_KEY", "GOOGLE_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "DEEPSEEK_API_KEY"],
    "cli": ["serve", "dev", "worker", "doctor", "fetch", "parse", "import", "resolve", "device-bundle", "mcp", "migrate-observations", "migrate-genotypes", "recode"],
    "tools": {
      "mcp": ["query_genetic_data", "query_health_indicators", "query_medications", "query_pharmacogenomics", "resolve_indicator", "convert_unit", "normalize_unit"],
      "gated": ["query_genetic_data", "query_pharmacogenomics", "query_health_indicators", "query_medications"]
    },
    "readme": {
      "en": {
        "tagline": "Self-hosted AI health data engine: every source, one standard, answers that cite their source.",
        "stages": [{
          "mark": "①",
          "name": "Collect",
          "what": "Lab reports, wearables, phone photos, genetic files, all pulled in. The source file is kept as it was, so every indicator points back to the page it was read from."
        }, {
          "mark": "②",
          "name": "Translate",
          "what": "One name to one code, one unit to UCUM, offline and deterministic. `A1c`, `HbA1c` and `Glycated Hemoglobin` become the same test here, and `头疼` and `headache` the same complaint (ICPC-3)."
        }, {
          "mark": "③",
          "name": "Agent",
          "what": "Ask over the coded record. Trend a value by minute, hour, day, week or month; get count, min, max, avg or change over any window in one call; compare across labs and devices, because they share one code. It charts the result in its reply, reads medications and genetic variants too, and names the file every number came from."
        }]
      },
      "zh": {
        "tagline": "自托管的 AI 原生健康数据引擎：任何来源，一套标准，每个答案都有出处。",
        "stages": [{
          "mark": "①",
          "name": "收集 Collect",
          "what": "化验单、穿戴设备、手机照片、基因文件，都收进来。源文件原样留下，每一项指标都能指回它被读出来的那一页。"
        }, {
          "mark": "②",
          "name": "转译 Translate",
          "what": "一个名字解析成一个码，一个单位统一到 UCUM，全程离线、结果确定。`A1c`、`HbA1c`、`糖化血红蛋白` 在这一层变成同一项检查，`头疼` 和 `headache` 也成了同一条主诉（ICPC-3）。"
        }, {
          "mark": "③",
          "name": "智能体 Agent",
          "what": "在编码后的记录上提问。按分钟、小时、天、周、月给出趋势，一次调用就能算出计数、最小值、最大值、均值和变化量；同一个码，跨化验所、跨设备直接比较。图表画在回复里，用药记录和基因型数据也读得了，每个数字都说明出自哪份文件。"
        }]
      }
    },
    "source": {
      "cli": "mirobody/cli.py",
      "tools": "mirobody/agent/tools"
    }
  };
  const value = k.split(".").reduce((o, p) => o == null ? undefined : o[p], facts);
  if (value === undefined) return <span>{"[unknown fact " + k + "]"}</span>;
  const items = Array.isArray(value) ? value : [value];
  return <>
      {items.map((item, i) => <span key={i}>
          {i > 0 ? sep : null}
          {code ? <code>{String(item)}</code> : String(item)}
        </span>)}
    </>;
};

<OssVersion lang="en" />

Configuration is a set of YAML files plus environment variables. The shipped files hold working defaults for a local stack: a deployment puts its secrets and the model key in `.env`, and changes the few settings it needs in one overlay file.

## Configuration files

| File | Holds | Edit it |
| - | - | - |
| `config.yaml` | Server, database, sign-in, extension directories | No: override its values in the overlay |
| `config.llm.yaml` | The model table (`MODELS`) and which model each surface uses | To change a model |
| `config.devices.yaml` | Garmin, Oura and Whoop credentials; Google and Apple sign-in | To turn a provider on |
| `config.{ENV}.yaml` | Your overlay for `config.yaml`. Optional: point `.env`'s `MIROBODY_CONFIG_FILE` at one; `./deploy.sh` only reuses an overlay already present from an earlier release | Yes |
| `.env` | `ENV`, the generated secrets, and the model API key | Yes |

Later sources win: environment variables, then the overlay, then `config.devices.yaml` and `config.llm.yaml`, then `config.yaml`. **A dictionary does not merge across files**: a key set in the overlay replaces the whole value, so an overlay that sets `EMAIL_PREDEFINE_CODES` replaces the demo accounts rather than adding to them.

`ENV` only selects which overlay loads and tags log lines; it carries no behaviour of its own. Production posture is the `PRODUCTION` switch below, not an environment name.

In the overlay, a value whose name contains `_KEY`, `_PASSWORD`, `_PASS`, `_PWD`, `_SECRET`, `_SK` or `_TOKEN` is encrypted with `CONFIG_ENCRYPTION_KEY` from `.env`.

<h2 id="the-model-key">
  The model key
</h2>

One key runs every surface. Put one of these in the `.env` next to `compose.yaml`, then apply it:

<Fact k="llmKeys" code />

```bash theme={null}
docker compose up -d
```

`docker compose restart` does not re-read `.env`; `up -d` does.

Each key selects a model for four surfaces: chat (the model picker), vision (report photos and scanned pages), text (indicator extraction, file titles and summaries) and embeddings (indicator search). The table at the top of <OssLink path="config.llm.yaml" /> lists what each key selects and where each key is issued. DeepSeek and Anthropic serve no embedding model; with one of those keys alone, indicator search uses the lexical index.

`mirobody doctor` prints the keys it found and the model each surface selected, and names the fix for a surface that has none:

```bash theme={null}
docker compose exec mirobody mirobody doctor   # the Docker stack
mirobody doctor                                # a checkout or a pip install
```

The server and the worker log the same report at startup.

### Changing a model

* **Point a surface at another entry.** `UTILS_VISION_MODEL`, `UTILS_TEXT_MODEL` and `UTILS_EMBEDDING_MODEL` each take an entry name, a list of names (the first whose key is present wins), a `provider/model` string, or an inline entry. An environment variable of the same name overrides the file, for example `UTILS_VISION_MODEL=qwen-utils`.
* **Change the chat default.** `DEFAULT_MODEL` names a `MODELS` entry; unset, the first entry whose key is present is the default.
* **Route a key through another gateway.** `<PREFIX>_BASE_URL` in `.env`, where `PREFIX` is the key's name without `_API_KEY`, redirects every entry that reads that key to another OpenAI-compatible endpoint, for example `OPENROUTER_BASE_URL`. `ANTHROPIC_BASE_URL` is also read by Anthropic's own SDK, so a machine that already sets it for another tool redirects Mirobody too; `mirobody doctor` prints the endpoint each surface resolved.
* **Add a vendor.** Add an entry to `MODELS` in `config.llm.yaml` with `llm_type: openai`, its `base_url`, `model`, and `api_key` set to the *name* of the `.env` variable that holds the secret. An entry used for vision must declare `supports_image: true`. Add it to `config.llm.yaml` itself: a `MODELS` block in the overlay would replace the whole table.

Changing the embedding model invalidates every vector already stored: vectors from different models are not comparable.

## Deployment settings

| Key | Default | Change it when |
| - | - | - |
| `PRODUCTION` | `false` | The deployment faces anyone but you. The server then refuses to start while predefined sign-in codes or any `REPLACE_THIS_VALUE_IN_PRODUCTION` placeholder remain, and never seeds demo data. |
| `SEED_DEMO_DATA` | `true` | The deployment will hold real data. Set it to `false` in `.env` before the first start, and neither demo account is created. |
| `EMAIL_PREDEFINE_CODES` | the two demo accounts | Always, before a network deployment: remove it and use real sign-in. |
| `BOOTSTRAP_SCHEMA` | `true` | You provision the schema yourself. With `true`, the schema files are replayed at every boot. |
| `MCP_PUBLIC_URL` | unset | The deployment has a public HTTPS origin; see [Deploy on a Server](/en/deployment/production#public-url-and-https). |
| `MCP_URL_TTL_DAYS` | <Fact k="mcpUrlTtlDays" /> | A personal MCP URL should live shorter or longer, in days. |
| `DEFAULT_TIMEZONE` | `America/Los_Angeles` | Your users are mostly elsewhere. An IANA zone name; a user's own setting takes precedence. |
| `HTTP_HEADERS` | unset | A browser client on another origin needs CORS headers. |
| `REQUEST_RATE_LIMITER` | per-path limits | The sign-in and chat limits, in requests per minute, need tuning. |
| `ENABLE_INDICATOR_EXTRACTION` | `1` | Uploads should be stored without extracting readings (`0`). |

The keys that decide how the agent behaves are described in [The Agent](/en/tools/agents#configuration).

## Secrets

| Secret | Where | Set by |
| - | - | - |
| `PG_PASSWORD`, `PG_ENCRYPTION_KEY`, `CONFIG_ENCRYPTION_KEY`, `LOG_ENCRYPTION_KEY`, `JWT_KEY` | `.env` | `./deploy.sh`, generated with `openssl rand -hex 32` on first run |
| `DATABASE_DECRYPTION_KEY` | `config.devices.yaml`, or `.env` to override it | You. Ships as a `REPLACE_THIS_VALUE_IN_PRODUCTION` placeholder; environment variables take precedence over every config file. |

`PG_ENCRYPTION_KEY` is a separate value from `CONFIG_ENCRYPTION_KEY`: it encrypts stored content such as chat messages and uploaded files. `DATABASE_DECRYPTION_KEY` (hex) encrypts the device credentials the providers store. Do not copy either key between environments. Generating these secrets yourself and moving them to another host is described in [Deploy on a Server](/en/deployment/production#secrets).

## Extension directories

The engine scans three lists of directories at startup. Adding one extends the engine without editing the package.

| Key | Default | Holds |
| - | - | - |
| `MCP_TOOL_DIRS` | `mirobody/agent/tools` | Tool modules; see [Adding Tools](/en/tools/adding-tools) |
| `AGENT_DIRS` | `mirobody/agent` | The agent class; see [The Agent](/en/tools/agents#replacing-the-agent) |
| `PROVIDER_DIRS` | `mirobody/collect/providers` | Device providers; see [Writing a Provider](/en/development/provider-integration) |

An overlay that sets one of these replaces the list, so repeat the default entry beside your own. An installed package can extend the same three through the `mirobody.tools`, `mirobody.agents` and `mirobody.providers` entry points instead.

The complete reference for every configuration key is <OssLink path="mirobody/utils/config/README.md" />.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.