This is the second half of the config schema reference. The first half,
Config schema, covers the file layout, hal0.toml
and slots/<name>.toml. Everything here follows the same conventions: TOML files are
validated with Pydantic models in hal0.config.schema, hardware.json and
manifest.json are JSON, and all writes are atomic.
providers.toml
Root model ProvidersConfig, extra="allow": provider: list[ProviderEntry] (default []).
[[provider]]
extra="forbid".
| Key | Type | Default | Req | Constraints |
|---|---|---|---|---|
catalog_id |
str | — | required | non-empty |
name |
str | "" |
— | display override |
base_url |
str | "" |
— | — |
auth_value_env |
str | "" |
— | name of the env var holding the API key — the key itself is never stored in TOML |
enabled |
bool | true |
— | — |
models |
list[str] | [] |
— | — |
upstreams.toml
Root model UpstreamsConfig, extra="allow": upstream: list[UpstreamEntry]
(default []); names must be unique.
[[upstream]]
extra="forbid".
| Key | Type | Default | Req | Constraints |
|---|---|---|---|---|
name |
str | — | required | non-empty |
kind |
str | "remote" |
— | slot or remote |
url |
str | — | required | non-empty |
auth_style |
str | "bearer" |
— | bearer, anthropic, header, none |
auth_header |
str | "" |
— | required if auth_style = "header" |
auth_value_env |
str | "" |
— | — |
timeout_seconds |
float | 300.0 |
— | > 0.0 |
slot_name |
str | null | null |
— | required if kind = "slot" |
warmup_strategy |
str | "none" |
— | none, ondemand, always; aliases lazy → ondemand, eager → always |
advertise_models |
bool | true |
— | — |
enabled |
bool | true |
— | — |
model_filters |
table | null | null |
— | see below |
hal0_peer |
bool | null | null |
— | null auto-derives from the URL host: private/loopback hosts are treated as another hal0’s dashboard API (pollable for hardware/slot metrics), public hosts never are. Set true to opt a public-hostname hal0 peer in. |
google_query was a fifth auth_style value with no implementation behind
it — Google AI Studio calls dispatched unauthenticated while the UI showed
“key set” (#1513). It is retired from the accepted set; an on-disk
auth_style = "google_query" still loads without error, coerced on read to
"bearer" (the style the endpoint it points at actually accepts), with a
warning logged.
[[upstream]].model_filters
extra="forbid".
| Key | Type | Default | Notes |
|---|---|---|---|
models |
list[str] | [] |
exact id list |
include |
list[str] | [] |
fnmatch globs |
exclude |
list[str] | [] |
fnmatch globs; always wins over include |
Profile reasoning fields
Profiles accept reasoning_effort (default|minimal|low|medium|high|xhigh|max)
and reasoning_budget (-1 through 16777216). Their defaults, default and
-1, are omitted from profile exports. Slots can override them in [tune],
along with context, thinking, vision and speculation. Stack exports retain
the sparse tune table without a schema-version bump.
profiles.toml
Root model ProfilesConfig, extra="forbid": profile: dict[str, ProfileConfig] keyed
by name. Seed profiles are always reapplied from code on load and overlay any on-disk
copy — they can’t be edited or deleted, only cloned.
[profile.<name>]
extra="forbid".
| Key | Type | Default | Constraints |
|---|---|---|---|
flags |
str | "" |
free llama-server flags: KV cache types, batch sizes, sampling, reasoning, --parallel. Screened on write and again at launch (below). |
ctx_size |
int | null | null |
128–16777216. null = the model’s native window, dense-capped at 32768, else 8192. On a halogen-runner profile a value past 262144 also needs HALOGEN_ROPE_YARN in flags (422 profiles.halogen_flag_out_of_range). |
enable_thinking |
bool | null | null |
request-time reasoning default for slots running this profile |
vision |
"auto"|"off" |
"auto" |
auto loads the model’s mmproj when it has one |
speculation |
"auto"|"off"|"ngram" |
"auto" |
auto drafts with the model’s paired drafter when the runner can run it, else with its MTP heads when it has them and the runner supports it; ngram uses n-gram lookup and needs no file (refused on a halogen-runner profile, 422 profiles.speculation_unsupported: that engine takes auto or off); off never speculates. Replaces mtp: a stored or posted mtp is refused (422 profiles.field_renamed over the API) |
runner |
str | null | null |
optional RUNNER_IMAGES registry key; applying the profile sets the slot’s binary to this key and, for a single-backend runner, also derives the slot’s device (profile wins). Validated on write — an unknown key 422s profiles.unknown_runner. null = flags-only tune; halogen is the one seed profile that sets runner. |
cloned_from |
str | null | null |
provenance |
intent |
str | "" |
dashboard card headline |
flags may not set what another layer owns. Every class below except the last is rejected when the profile is saved and refused again at launch, in every spelling llama.cpp accepts (short and alternative forms, --flag=value, and _ for -). --reasoning-format none is rejected on save, but launch removes it from the command instead of refusing:
| Class | Examples | Error code |
|---|---|---|
| Hardware (the slot owns it) | -ngl, -dev, -t |
slot.hardware_flag_denied |
| Managed by hal0 | --model, --host, --port, --alias, --ctx-size |
slot.managed_arg_denied |
| Model facts, derived placement | --jinja, --chat-template*, --mmproj, --no-mmproj, --mmproj-auto, --spec-type (set speculation instead), --spec-draft-device, --mmproj-device |
profiles.model_fact_flag_denied |
| Host-owned | remote model fetches (-hf, --model-url), host file reads and writes (--api-key-file, --log-file, --lora), listener and auth/TLS settings (--rpc, --api-key), llama.cpp’s built-in preset switches (--gpt-oss-20b-default, --spec-default) |
slot.host_owned_flag_denied |
| Bare positional arguments | a value with no flag in front of it | slot.positional_arg_denied |
--reasoning-format none |
— | slot.reasoning_format_none_denied on save; stripped, not refused, at launch |
Profiles no longer carry an image field — image resolution is slot-owned
(slot.image_pin or RUNNER_IMAGES[slot.binary]).
stacks.toml
Root model StacksConfig, extra="forbid": stack: dict[str, StackConfig] keyed by
slug. STACK_SCHEMA_VERSION_CURRENT = 3.
[stack.<slug>]
extra="forbid".
| Key | Type | Default |
|---|---|---|
name |
str | "" |
description |
str | "" |
author |
str | "" |
icon |
str | "" |
tags |
list[str] | [] |
schema_version |
int | 3 |
hal0_version |
str | "" |
slots |
list[table] | [] — see StackSlotEntry below |
profiles |
dict[str, table] | {} — ProfileConfig entries |
models |
dict[str, table] | {} — see StackModelMeta below |
StackSlotEntry
extra="forbid".
| Key | Type | Default | Req | Constraints |
|---|---|---|---|---|
slot |
str | — | required | regex ^[a-z0-9][a-z0-9_-]{0,31}$ |
profile |
str | null | null |
— | — |
model |
str | null | null |
— | — |
device |
str | null | null |
— | must be a valid device id if set |
provider |
str | null | null |
— | — |
capabilities |
list[table] | [] |
— | see StackCapabilityRow below |
A slot entry names a model, a profile and placement. It carries no tuning: the profile owns vision, speculation, enable_thinking and the flags.
StackCapabilityRow
extra="forbid".
| Key | Type | Req | Constraints |
|---|---|---|---|
child |
str | required | e.g. embed, rerank, stt, tts, vision |
device |
str | required | must be a valid device id |
provider |
str | required | — |
model |
str | required | — |
enabled |
bool | — | default true |
StackModelMeta
extra="forbid".
| Key | Type | Default | Req |
|---|---|---|---|
id |
str | — | required, non-empty |
name |
str | "" |
— |
hf_repo |
str | "" |
— |
hf_filename |
str | "" |
— |
size_bytes |
int | 0 |
— |
quant |
str | "" |
— |
capabilities |
list[str] | [] |
— |
backends |
list[str] | [] |
— |
provider |
str | null | null |
— |
mmproj |
str | null | null |
— |
agents/<name>.toml
Root model AgentConfig, extra="allow". AGENT_CONFIG_SCHEMA_VERSION = 1.
| Key | Type | Default | Req | Constraints |
|---|---|---|---|---|
schema_version |
int | 1 |
— | >= 1; future versions rejected |
agent |
table | — | required | [agent], see below |
mcp |
table | {} |
— | [mcp], see below |
[agent]
extra="allow".
| Key | Type | Default | Req | Constraints |
|---|---|---|---|---|
name |
str | — | required | regex ^[a-z0-9][a-z0-9-]{0,31}$ |
display |
str | "" |
— | — |
workspace |
str | "" |
— | empty → canonical path |
[mcp]
extra="allow": servers: dict[str, MCPServerConfig] (default {}).
[mcp.servers.<name>]
extra="allow".
| Key | Type | Default | Constraints |
|---|---|---|---|
url |
str | null | null |
required unless builtin = true |
enabled |
bool | true |
— |
builtin |
bool | false |
— |
auth |
table | {} |
see below |
tools |
table | {} |
see below |
[mcp.servers.<name>.auth]
extra="forbid".
| Key | Type | Default | Constraints |
|---|---|---|---|
kind |
"none"|"bearer-from-env" |
"none" |
— |
env |
str | null | null |
required if kind = "bearer-from-env" |
[mcp.servers.<name>.tools]
extra="forbid".
| Key | Type | Default |
|---|---|---|
allow |
list[str] | [] |
gated |
list[str] | [] |
blocked |
list[str] | [] |
allow, gated, and blocked must be disjoint — validation raises naming the
overlapping tool names.
[hermes] (hermes.toml only)
Model HermesAgentSettings, extra="forbid". Kept outside AgentConfig and
carried by its extra="allow", so a bad value here never breaks MCP allow-list
loading. Writes through hal0 agent command-approvals and
PUT /api/agents/hermes/command-approvals are validated; provisioning reads the
value leniently and falls back to the default if it is invalid.
delegation_slot has no CLI or API writer yet; edit the root-owned file.
| Key | Type | Default | Constraints |
|---|---|---|---|
command_approvals |
"smart"|"manual" |
"smart" |
Rendered to Hermes’ approvals.mode; an approvals.mode in agents/hermes/overrides.yaml wins |
delegation_slot |
str | "agent" |
Slot name, ^[a-z0-9][a-z0-9_-]{0,31}$. The slot whose model delegate_task children run on. Any non-default slot is rendered as delegation.model = "hal0/<slot>", so the request keeps that slot’s lane. Applied at provision or repair, only while the slot is ready |
hardware.json
Root model HardwareInfo, extra="allow" (JSON, not TOML). Written by
hal0 config hardware --refresh.
| Key | Type | Default | Constraints |
|---|---|---|---|
hostname |
str | "" |
— |
uptime_s |
int | 0 |
>= 0 |
kernel |
str | "" |
— |
distro |
str | "" |
— |
cpu_model |
str | "" |
— |
cpu_cores |
int | 0 |
>= 0 |
cpu_threads |
int | 0 |
>= 0 |
ram_mb |
int | 0 |
>= 0 |
ram_available_mb |
int | 0 |
>= 0 |
swap_mb |
int | 0 |
>= 0 |
unified_memory_mb |
int | 0 |
>= 0 — the true UMA pool; use this, not ram_mb + vram_mb, on unified-memory systems |
gpus |
list[table] | [] |
see GPUInfo below |
gpu_group_gids |
dict[str,int] | {} |
resolved render/video group ids |
npu |
table | {} |
see NPUInfo below |
disk_free_mb |
int | 0 |
>= 0 |
cgroup_max_mb |
int | null | null |
>= 0 |
probed_at |
str | "" |
ISO-8601 |
platform |
str | "unknown" |
one of strix-halo, wsl2, proxmox-kvm, kvm, lxc, bare-metal-amd-gpu, bare-metal-nvidia-gpu, bare-metal-intel-igpu, bare-metal-cpu-only, unknown |
extra |
dict | {} |
— |
GPUInfo
extra="allow".
| Key | Type | Default |
|---|---|---|
vendor |
str | "" |
index |
int | 0 |
name |
str | "" |
vram_mb |
int | 0 |
pci_id |
str | "" |
driver |
str | "" |
drm_path |
str | "" |
compute_capable |
bool | false |
vulkan_capable |
bool | false |
NPUInfo
extra="allow".
| Key | Type | Default |
|---|---|---|
present |
bool | false |
vendor |
str | "" |
name |
str | "" |
driver |
str | "" |
accel_path |
str | "" |
render_path |
str | "" |
aie_columns |
int | 0 |
validated |
bool | null | null |
manifest.json / env files
manifest.json carries a runners table: one row per runner key with image (bare
repo), tag, digest and lanes_gated. It is the one place a runner image is named — see
Manage runner images. api.env
and openwebui.env are systemd EnvironmentFile=-format KEY=value files, not part
of the Pydantic schema — see Environment variables for
what lives in api.env (admin/client bearer keys, etc).