Skip to content

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.

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:

Terminal window
curl http://localhost:8080/api/providers/catalog

It includes OpenAI, Anthropic, OpenRouter, Google AI Studio, Ollama, and a generic custom OpenAI-compatible entry, each with its default base URL and auth style.

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.0
advertise_models = true

The fields:

  1. name — a unique id you’ll reference everywhere else.

  2. kindremote for an external provider (slot is reserved for local container slots, which register themselves).

  3. url — the base URL (required, non-empty).

  4. 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/openai layer), 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), or none (no auth header, e.g. an unauthenticated local Ollama).

  5. 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.

  6. timeout_seconds — request timeout (default 300).

  7. advertise_models — whether this upstream’s models appear in the aggregated model list (default true).

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 same api.env file through POST /api/secrets/{name}, but it does not check the name against any upstream’s auth_value_env — type the exact env-var name your upstream declares.
  • The API, which binds the credential to the upstream’s declared auth_value_env so you can’t accidentally write a key the upstream won’t read (this is also what the bundled MCP admin tool’s provider_credential_write uses):
Terminal window
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***".

Terminal window
# remote providers only
curl http://localhost:8080/api/providers
# all upstreams (remote + local slots)
curl http://localhost:8080/api/upstreams
# one upstream
curl http://localhost:8080/api/upstreams/openrouter

Each 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.

Terminal window
curl -X POST http://localhost:8080/api/upstreams/openrouter/test

This 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.

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.