Connect external providers
hal0 can route requests to external upstreams — third-party
OpenAI-compatible APIs like OpenAI, Anthropic, OpenRouter, Google AI
Studio, Ollama, or any custom endpoint. You wire them up by editing
/etc/hal0/upstreams.toml (the source of truth) and then writing the
credential and testing reachability through the API.
See what’s available
Section titled “See what’s available”The integration catalog is a static reference of known provider templates — base URL, auth style, capabilities — intended for a future dashboard “add upstream” form. Today it’s most useful as a copy/paste source for the TOML block below:
curl http://localhost:8080/api/providers/catalogIt includes OpenAI, Anthropic, OpenRouter, Google AI Studio, Ollama, and a generic custom OpenAI-compatible entry, each with its default base URL and auth style.
Define the upstream
Section titled “Define the upstream”Add a [[upstream]] block to /etc/hal0/upstreams.toml:
[[upstream]]name = "openrouter"kind = "remote"url = "https://openrouter.ai/api/v1"auth_style = "bearer"auth_value_env = "OPENROUTER_API_KEY"timeout_seconds = 300.0advertise_models = trueThe fields:
-
name— a unique id you’ll reference everywhere else. -
kind—remotefor an external provider (slotis reserved for local container slots, which register themselves). -
url— the base URL (required, non-empty). -
auth_style— how the key is sent.upstreams.toml’s schema validates exactly three values:bearer(Authorization: Bearer <key>— the default, and what OpenRouter, Ollama, and most OpenAI-compatible endpoints expect, including Google AI Studio’s/v1beta/openailayer),header(a custom header — reserved for a future header-name field; nothing in the current schema lets you name the header, so it has no effect yet), ornone(no auth header, e.g. an unauthenticated local Ollama). -
auth_value_env— the name of the environment variable that holds the secret. The key itself is never written to TOML — only the env-var name lives here. -
timeout_seconds— request timeout (default300). -
advertise_models— whether this upstream’s models appear in the aggregated model list (defaulttrue).
Write the credential
Section titled “Write the credential”The secret lives in /etc/hal0/api.env (a systemd EnvironmentFile),
not in the TOML. There are two ways to write it today:
- The dashboard — Settings → Secrets has presets for
OPENAI_API_KEY,ANTHROPIC_API_KEY,GOOGLE_API_KEY,GROQ_API_KEY(labelled “fallback provider” keys) plus a paired AWS Bedrock entry and a free-form custom name. It writes the sameapi.envfile throughPOST /api/secrets/{name}, but it does not check the name against any upstream’sauth_value_env— type the exact env-var name your upstream declares. - The API, which binds the credential to the upstream’s declared
auth_value_envso you can’t accidentally write a key the upstream won’t read (this is also what the bundled MCP admin tool’sprovider_credential_writeuses):
curl -X POST http://localhost:8080/api/providers/openrouter/credentials \ -H 'content-type: application/json' \ -d '{"key":"OPENROUTER_API_KEY","value":"sk-or-..."}'The key must match the upstream’s auth_value_env exactly and be a
valid ALL_CAPS env-var name — the upstream named in the URL must
already exist (a 404 if it doesn’t). The endpoint writes the value
atomically to api.env with 0600 permissions, updates the running
process environment so the change takes effect immediately, and never
echoes the secret back — the response carries "value": "***REDACTED***".
List configured providers
Section titled “List configured providers”# remote providers onlycurl http://localhost:8080/api/providers
# all upstreams (remote + local slots)curl http://localhost:8080/api/upstreams
# one upstreamcurl http://localhost:8080/api/upstreams/openrouterEach entry reports auth_configured — whether the upstream declares an
auth_value_env at all, not whether the credential has actually been
written yet. It’s true the moment your TOML entry names an env var,
even before you’ve written anything to api.env — check with test
(below) to confirm the credential itself is actually set.
Test the connection
Section titled “Test the connection”curl -X POST http://localhost:8080/api/upstreams/openrouter/testThis probes the upstream’s /models endpoint (appended to the configured
base URL) and returns a reachability report:
{ok, status, latency_ms, models_count, error?}. If auth_value_env is
declared but the env var itself is empty, the probe fails fast with
{"ok": false, "error": "env var $X is not set", "latency_ms": 0.0}
without making an HTTP call. An unhealthy result also fires a dashboard
footer event so the outage is visible.
Picking up changes
Section titled “Picking up changes”Credential writes through either the API or the dashboard’s Settings →
Secrets panel apply to the running process immediately — no restart
needed. Structural edits to upstreams.toml (adding, removing, or
retiming an upstream) are a different story: they’re only read once, at
hal0-api startup. hal0 config reload / POST /api/settings/reload
re-reads hal0.toml, not upstreams.toml — there is no lighter-weight
reload for upstream edits today. Run hal0 config validate to check the
file, then systemctl restart hal0-api to pick up the change.