Configuration Reference
Odin reads configuration from config.yml at startup. Environment variable substitution is supported via ${VAR} and ${VAR:-default} syntax.
Discord
discord:
token: ${DISCORD_TOKEN} # Required — bot token
allowed_users: [] # User IDs (empty = all allowed)
channels: [] # Channel IDs (empty = all)
respond_to_bots: false # Reply to other bots
require_mention: true # Only respond when @mentioned
ignore_bot_ids: [] # Bot IDs to never respond toTools & Hosts
tools:
enabled: true
ssh_key_path: /home/service-user/.ssh/id_ed25519
ssh_known_hosts_path: /home/service-user/.ssh/known_hosts
command_timeout_seconds: 300 # Default per-handler-attempt timeout
tool_timeouts: # Per-tool handler-attempt overrides
hosts:
localhost:
address: 127.0.0.1
ssh_user: root
os: linux
my-server:
address: 203.0.113.10
ssh_user: deploy
os: linux
max_tool_iterations_chat: 30 # Tool calls per Discord message
max_tool_iterations_loop: 100 # Tool calls per autonomous loopSSH Configuration
tools:
ssh_retry:
max_retries: 2
base_delay: 0.5
max_delay: 10.0
ssh_pool:
enabled: true
control_persist: 60 # Seconds to keep connections alive
socket_dir: /tmp/odin_ssh_sockets
bulkhead:
ssh_max_concurrent: 10
subprocess_max_concurrent: 20
browser_max_concurrent: 3Disabling Built-in Tools
tools:
disabled_tools: [] # Built-in tool names hidden from the modelOperator-disabled built-ins are removed from the model catalog on every surface (chat, agents, loops, schedules) and rejected at dispatch with a typed tool_disabled result. Manage them from the WebUI Tools page — the switches apply live, no restart. Names are case-sensitive; unknown entries are preserved and ignored so a list survives catalog drift. Disabled names stay reserved: a skill or MCP tool can never shadow a disabled built-in. This leaf is read-only on the generic config route; the Tools management API (GET /api/tools/builtins, POST /api/tools/builtins/{name}/enabled) owns it.
LLM / Codex
openai_codex:
enabled: true
model: gpt-5.6-sol # ChatGPT subscription path; GPT-6 models also selectable where entitled
reasoning_effort: xhigh # none | low | medium | high | xhigh | max
agent_reasoning_effort: auto # spawned agents; "auto" = per-spawn choice, null = inherit
credentials_path: ./data/codex_auth.json
request_timeout_seconds: 3600 # whole-request backstop; long reasoning turns stream past 10 min
stream_stall_timeout_seconds: 180 # fail fast when no stream bytes arrive for this long
retry:
max_retries: 3
base_delay: 1.0
max_delay: 30.0
context_compression:
enabled: true
max_context_chars: null # null = auto (model-derived ceiling); a number only lowers it
keep_recent_iterations: 3
# Per-model usable-input-budget overrides (tokens, 50192-2000000). Empty =
# built-in known-safe floors. Consumed by the context-budget resolver.
context_budget_overrides: {}
# Working-set policy: percent of the effective budget compaction targets
# (30-100). Never reduces budgets at or below 272K tokens.
context_utilization: 60
auxiliary: # cheaper model for background jobs
enabled: true
model: gpt-5.6-terraAgent model selection is provider-neutral and lives under agents, rather than under the Codex provider. A bare model name means Codex; use compat:<model> or ollama:<model> to select an OpenAI-compatible endpoint or Ollama model. agents.model: null inherits the chat model and auto selects per spawn. agents.auto_model_allowlist contains concrete model references that Auto may select. The WebUI keeps unknown entries and the codex-auto-review alias visible, so a catalogue refresh cannot erase policy that the running configuration already accepts.
agents:
model: auto
# Discrete compatible-provider switch, not a Codex reasoning effort.
thinking_mode: null # null | adaptive | enabled | disabled
auto_model_allowlist:
- gpt-5.6-luna
- compat:deepseek-v4-flash
- ollama:qwen3:32bA persisted max_context_chars: 750000 from the pre-campaign default is migrated to auto once (a provenance marker under data/ records it, and one warning names the marker); saving the compression settings afterwards makes any explicit value — including 750000 — stick permanently.
The native Codex selectors and default agent catalogue list gpt-6-astra, gpt-6-sol, gpt-6-luna, gpt-5.6-sol, gpt-5.6-terra, and gpt-5.6-luna in that order. Main and auxiliary defaults are unchanged.
Reasoning effort max is served by both families. GPT-6 Sol and Luna accept all six efforts (none, low, medium, high, xhigh, max) and each has a measured input-budget floor of 921,799 tokens (2026-09-22). GPT-6 Astra rejects none.
The retired gpt-5.5 is no longer selectable. On configuration-file load, explicit main, fixed-agent, and auxiliary selections migrate in memory to gpt-5.6-terra, with a warning. Existing effort selections are preserved. Its context-budget overrides are discarded rather than transferred to a different model; any existing Terra override remains unchanged. YAML and environment placeholders are not rewritten. Live configuration updates and explicit per-call requests for the retired model are rejected, not silently rerouted. Historical usage/provenance records are unchanged. An old image outer_model selection (including a pin) uses the established image successor gpt-6-astra on load instead.
Model entitlement is per ChatGPT account: gpt-6-astra rolled out to Personal/Pro accounts before Team accounts (2026-09-04); an account that does not serve a model answers "not supported when using Codex with a ChatGPT account". Odin refuses a known-incompatible model/effort pair everywhere it can be introduced — config load, the admin API, per-spawn agent overrides, and final request construction — naming the pair and the efforts that model does accept. Unknown model strings pass through unchecked (the server stays the authority).
The auxiliary model is an optional cheaper Codex model that runs the fixed background jobs — compaction, reflection, consolidation, and background follow-up — with automatic fallback to the primary model on error. It shares the main Codex OAuth credentials; only the model differs. Set it live from the Web UI (Auxiliary Model dropdown, "Off" to run those jobs on the primary).
Generate credentials: python3 scripts/codex_login.py
Tokens expire weekly — re-run the script and copy data/codex_auth.json to the deployment.
Agents
agents:
max_nesting_depth: 2 # Sub-agent nesting levels (root = 0)
max_children_per_agent: 3
max_iterations: 120 # LLM turns per interactive spawn
scheduled_max_iterations: 180 # Scheduled workflow spawns
hard_max_iterations: 300 # Ceiling for per-spawn overrides
final_warning_iterations: [20, 10, 5, 1]
iteration_timeout_seconds: 900 # Per-LLM-call backstop (60-86400)
max_lifetime_seconds: 14400 # Hard per-agent deadline (60-86400)OpenAI-compatible presets ship verified full endpoint URLs for DeepSeek, Z.ai GLM, Moonshot, Groq, Together, Fireworks, Mistral, xAI, Cerebras, DashScope, and OpenRouter. URLs are used verbatim: Odin never appends /v1. Local examples are vLLM http://127.0.0.1:8000/v1, llama.cpp :8080/v1, and LM Studio :1234/v1. Profiles declare reasoning dialect plus reasoning-content feedback; the safe default is not to echo provider reasoning into history.
iteration_timeout_seconds bounds each agent LLM call. It is a backstop against a hung call, not a working limit — set it well above a legitimate high-effort generation (5–10+ minutes at high reasoning effort); the streaming transport already fails dead connections fast via stream_stall_timeout_seconds.
max_lifetime_seconds is a hard deadline enforced during LLM and tool waits, not just between iterations. Both values are snapshotted at spawn — changing them live affects newly spawned agents only, never the deadline of an agent already running.
Sessions
sessions:
max_history: 50 # Messages per conversation
max_age_hours: 24 # Auto-expire sessions
persist_directory: ./data/sessions
token_budget: 128000 # Auto-compact when exceeded
adaptive_compaction: trueAutomatic learning
learning:
# enabled: false # Default when omitted; opt in explicitlyAutomatic learning is off by default. An existing explicit learning.enabled: true stays enabled until an administrator switches it off; upgrading does not override that choice. In the WebUI, open Capabilities → Learned and use Automatic learning. This persisted setting applies live, without a restart, including to already-created reflection and prompt services.
When off, Odin neither creates automatic lessons nor adds stored learned entries to model context. Existing entries are retained. Operation, session, compaction and loop reflection are all gated; an in-flight reflection cannot publish while disabled. The loop-reflection setting remains a subordinate switch, not an override of learning.enabled.
The Learned panel and /api/learned inspection, editing and deletion remain available while learning is off. Disabling is not a deletion or migration of the learned store. Re-enabling makes retained lessons eligible for context again.
This switch controls only automatic reflections and learned context. Deliberate persistent memory (memory_manage and memory.json), its system-prompt section, and ordinary conversation compaction are unaffected.
Browser
browser:
enabled: true
cdp_url: "" # Empty = native Playwright launch
default_timeout_ms: 30000
viewport_width: 1920
viewport_height: 1080Leave cdp_url empty to launch a local headless Chromium. Set to ws://host:port?token=secret for remote Browserless.
Run playwright install chromium after installation.
Image Generation
image:
openai:
enabled: true # kill switch for the native wire implementation
outer_model: gpt-6-astra # Responses model hosting the image tool (pinned)
image_model: gpt-image-2.5-flareThe generate_image tool uses the native OpenAI image_generation tool on the Codex ChatGPT OAuth backend, riding the same account Odin uses for chat (no separate auth; subscription-quota-backed, so it draws on that account's usage limit). It is available only while the active provider is codex and native image generation is enabled. The tool accepts only a prompt; output dimensions and aspect ratio are selected by the provider.
outer_model is pinned here rather than following your chat model, so changing the chat model (Sol/Terra/…) never alters image generation. The native backend and route are recorded in the audit log, queryable via search_audit, not shown in the tool's reply.
Web Management UI
Set a strong, private web.api_token before starting or exposing the service. If it is empty and both web.api_tokens and the managed-token store have no entries, the general API authentication gate is disabled: routes relying on it are unauthenticated. A blank legacy token alone does not disable authentication when other token entries exist. Computer observation/evidence routes additionally require an authenticated admin identity; those checks do not protect the rest of a tokenless installation. Restrict web.host to loopback unless deliberately exposing it behind TLS and access controls.
web:
enabled: true
port: 3000
api_token: ${WEB_API_TOKEN} # Required for production
session_timeout_minutes: 0 # 0 = persist until logoutThe management console groups workspace, operations, history, capabilities, and system surfaces. MCP server management lives under Capabilities; credentials remain write-only.
Permissions
permissions:
default_tier: user # admin, user, or guest
tiers:
"123456789012345678": admin # Per-user overridesRuntime overrides persist in data/permissions.json and take precedence.
| Tier | Access |
|---|---|
| admin | All 67 built-in tools |
| user | Eleven tools: get_tool_output, search_history, search_knowledge, web_search, fetch_url, list_schedules, list_tasks, list_skills, list_knowledge, manage_list, parse_time (no shell; manage_list can change list state) |
| guest | Conversation only, no tools |
Webhooks
webhook:
enabled: false
secret: '' # HMAC-SHA256 verification
channel_id: '' # Default notification channelContext Files
Place .md files in data/context/ — they are injected into every LLM prompt as infrastructure context.
context:
directory: ./data/contextLogging
logging:
level: INFO # DEBUG, INFO, WARNING, ERROR
directory: ./data/logsFile Paths (DEB install)
| Purpose | Path |
|---|---|
| Config | /etc/odin/config.yml |
| Secrets | /etc/odin/.env |
| Data | /var/lib/odin/ |
| Logs | /var/log/odin/ |
| Application | /opt/odin/ |
| Systemd | /usr/lib/systemd/system/odin.service |
Restarts (self-update and setup wizard)
The WebUI self-updater (Updates page) requires a git-clone install — .deb installs have no repository and should upgrade via apt instead (the endpoint answers 409 with the same hint).
After a successful update — and after the first-boot setup wizard saves its config — Odin restarts in place by re-executing itself once graceful shutdown completes. Recovery therefore does not depend on the service unit's Restart= policy, Docker restart policy, or any supervisor at all. Restart=always (what the packaged unit ships) is still recommended so the service also recovers from crashes and reboots.
LLM Recovery (capacity outages)
Model-capacity errors (e.g. server_is_overloaded, which arrives inside an HTTP 200 as an SSE error event) are retried with a deadline-based policy shared by chat, agents, and autonomous loops, coordinated by a per-model circuit breaker. Quota handling is unchanged: HTTP 429 still rotates accounts inside the provider client; capacity never does.
llm_recovery:
generation_deadline_seconds: 300 # retry budget per LLM generation (waiting, not the attempt)
backoff_cap_seconds: 45 # full-jitter backoff ceiling between attempts
breaker_generation_threshold: 1 # failed generations before the model breaker opens
breaker_cooldown_base_seconds: 30 # first cooldown; doubles per failed probe
breaker_cooldown_cap_seconds: 300 # cooldown ceilingAll keys are optional (schema defaults shown); the section does not need to exist in config.yml.
Turn State (checkpoints and resume)
Discord chat turns are checkpointed to a durable store so a capacity outage suspends the turn with its work preserved instead of discarding it. A suspended turn auto-resumes when capacity returns (if nothing else has happened in the channel), or the original requester can reply resume within the resumable window. Interrupted tool executions are recorded as outcome-unknown and are never re-run automatically.
turn_state:
enabled: true
db_path: "./data/turn_state/turns.sqlite3"
auto_resume: true
resume_ttl_hours: 24 # resumable window from last real progress
payload_retention_days: 7 # diagnostic payloads, then compacted to tombstones
ledger_retention_days: 90 # side-effect ledger (outcome-unknown rows never expire)All keys are optional; disabling turn_state.enabled restores the previous behavior (capacity exhaustion ends the turn with an error).
MCP Servers
MCP servers are managed from Manage → Capabilities → MCP Servers in the WebUI. That page is the sole editor: Configuration Center deliberately shows only a read-only MCP summary and links to it. Changes are persisted to config.yml, applied live, and report saved configuration separately from the current connection state. A server that cannot connect remains saved so its sanitized error can be inspected and the connection retried.
mcp:
enabled: true
servers:
local_tools:
enabled: true
transport: stdio
command: /usr/local/bin/my-mcp-server
args: ["--flag"]
cwd: /srv/my-mcp-server
env:
API_KEY: "${MCP_API_KEY}"
tool_allowlist: [] # empty = all validated discovered tools
timeout_seconds: 120
remote_tools:
enabled: true
transport: http
url: https://mcp.example.com/mcp
headers:
Authorization: "Bearer ${MCP_HTTP_TOKEN}"
tool_allowlist: [search, lookup]
timeout_seconds: 120The global switch and each server's enabled switch must both be on. A tool is published only while its current configuration generation is connected and a complete discovery result has passed schema, provider, and limit validation. Disabled, stale, disconnected, errored, removed, or blocked servers publish no tools. Servers that exceed a safety limit are blocked, not partially published; use tool_allowlist to narrow discovery when appropriate.
Odin supports MCP tools over stdio and Streamable HTTP in both the deployed sessionful 2025 generation and the stateless 2026-07-28 generation. The supported revisions are 2024-11-05 (stdio only), 2025-03-26, 2025-06-18, 2025-11-25, and 2026-07-28. The deprecated HTTP+SSE transport is not supported.
Authentication is static in v1: HTTP headers and stdio environment variables. Secret values are write-only in the API and WebUI. Reads return key names only; rotations and removals use explicit operations so a masked or blank display value can never overwrite a credential accidentally. Prefer ${ENV_VAR} placeholders in YAML. Interactive OAuth, MCP resources/prompts, sampling, elicitation, roots, and tasks are not supported in v1.
The server panel exposes two different recovery actions:
- Refresh tools re-runs discovery on the existing connection.
- Reconnect retires and rebuilds the transport before discovery.
Server-reported instructions, errors, and stderr are untrusted diagnostic text; the management API scrubs configured credential values and bounds all exposed fields before the WebUI renders them.