README: LiteLLM section + Pulse wiring recipe + dual-endpoint client table
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,27 +1,31 @@
|
|||||||
# homelab-ansible-lxc-meridian
|
# homelab-ansible-lxc-meridian
|
||||||
|
|
||||||
Ansible config for the Meridian LXC (CTID 457 on pve01, `192.168.1.164`).
|
Ansible config for the Meridian + LiteLLM LXC (CTID 457 on pve01, `192.168.1.164`).
|
||||||
|
|
||||||
## What it is
|
## What it is
|
||||||
|
|
||||||
[Meridian](https://github.com/rynfar/meridian) is a local Anthropic API server
|
Two services on one LXC, sharing one Claude Max OAuth subscription:
|
||||||
backed by the Claude Code SDK. It translates `/v1/messages` calls into Claude
|
|
||||||
Code SDK `query()` calls so any Anthropic-compatible client can run against
|
|
||||||
Chuck's Claude Max subscription instead of paid API tokens.
|
|
||||||
|
|
||||||
Primary consumer: HAOS's built-in `anthropic` conversation integration (via a
|
- **Meridian** ([rynfar/meridian](https://github.com/rynfar/meridian), port `3456`) — local Anthropic API server backed by the Claude Code SDK. Translates `/v1/messages` calls into Claude Code SDK `query()` calls. **No auth at this layer** — LAN reachability is the gate.
|
||||||
custom_component fork that adds `CONF_BASE_URL`).
|
- **LiteLLM** ([berriai/litellm](https://docs.litellm.ai/), port `4000`) — OpenAI-compatible proxy that fronts Meridian. Lets clients that only speak OpenAI (Pulse, paperless-ai, etc.) ride the same Max sub. **Master-key auth required.**
|
||||||
|
|
||||||
|
```
|
||||||
|
Anthropic-format client ─────────────────────► :3456 Meridian ─► Claude Max
|
||||||
|
(OAuth)
|
||||||
|
OpenAI-format client ─► :4000 LiteLLM ─► 127.0.0.1:3456 ─────► ↑
|
||||||
|
(master key)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Wired today:** Pulse (Settings → AI → OpenAI provider). **Planned:** paperless-ai, HAOS conversation agent (via custom_component fork that adds `CONF_BASE_URL`).
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
- Debian 12 LXC, Node 22 from NodeSource apt repo
|
- Debian 12 LXC, no Docker (everything native)
|
||||||
- `@rynfar/meridian` installed globally via `npm`
|
- **Meridian**: Node 22 from NodeSource apt + `npm i -g @rynfar/meridian`. systemd unit `meridian.service` as user `meridian`, bound to `0.0.0.0:3456`, `HOME=/opt/meridian`.
|
||||||
- systemd unit `meridian.service` running as user `meridian`, binding to
|
- **LiteLLM**: Python venv at `/opt/litellm/venv` + `pip install 'litellm[proxy]'`. systemd unit `litellm.service` as user `litellm`, bound to `0.0.0.0:4000`. `Requires=meridian.service` so it can't outlive the backend.
|
||||||
`0.0.0.0:3456`
|
- **OAuth credentials** at `/opt/meridian/.claude/` (mode 0700, owned by `meridian`).
|
||||||
- OAuth credentials live at `/opt/meridian/.claude/` (transferred manually
|
- **LITELLM_MASTER_KEY** at `/opt/litellm/litellm.env` (mode 0600, owned by `litellm`). Source of truth in Infisical `/meridian/vault_litellm_master_key`. Pulled by `deploy.sh` on the controller and exported for the playbook to consume.
|
||||||
after first deploy — see Bootstrap below)
|
- **No Caddy, no Cloudflare.** Both ports exposed via the same UDM alias `meridian.lan.balders.ca → .164`.
|
||||||
- **No auth at the Meridian layer.** LAN-only reachability is the entire
|
|
||||||
security model — no Caddy public vhost, no Cloudflare tunnel.
|
|
||||||
|
|
||||||
## Bootstrap
|
## Bootstrap
|
||||||
|
|
||||||
@@ -56,32 +60,61 @@ custom_component fork that adds `CONF_BASE_URL`).
|
|||||||
ssh cbalders@192.168.1.164 \
|
ssh cbalders@192.168.1.164 \
|
||||||
'sudo -u meridian -H /usr/lib/node_modules/@rynfar/meridian/node_modules/@anthropic-ai/claude-code/bin/claude.exe auth status'
|
'sudo -u meridian -H /usr/lib/node_modules/@rynfar/meridian/node_modules/@anthropic-ai/claude-code/bin/claude.exe auth status'
|
||||||
```
|
```
|
||||||
5. Smoke from a LAN host:
|
5. Smoke from a LAN host (Anthropic format, direct):
|
||||||
```bash
|
```bash
|
||||||
curl http://192.168.1.164:3456/v1/messages \
|
curl http://192.168.1.164:3456/v1/messages \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-H 'anthropic-version: 2023-06-01' \
|
-H 'anthropic-version: 2023-06-01' \
|
||||||
-d '{"model":"claude-haiku-4-5","max_tokens":40,"messages":[{"role":"user","content":"reply with the single word: pong"}]}'
|
-d '{"model":"claude-haiku-4-5","max_tokens":40,"messages":[{"role":"user","content":"reply with the single word: pong"}]}'
|
||||||
```
|
```
|
||||||
|
6. Smoke via LiteLLM (OpenAI format, master-key auth):
|
||||||
|
```bash
|
||||||
|
KEY=$(infisical secrets get vault_litellm_master_key --env prod --path /meridian --plain)
|
||||||
|
curl http://192.168.1.164:4000/v1/chat/completions \
|
||||||
|
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
|
||||||
|
-d '{"model":"claude-haiku-4-5","max_tokens":40,"messages":[{"role":"user","content":"reply with the single word: pong"}]}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wiring a client
|
||||||
|
|
||||||
|
| Client type | Endpoint | Auth |
|
||||||
|
|-------------|----------|------|
|
||||||
|
| Anthropic-native (HAOS, Cline, Aider, OpenCode) | `http://meridian.lan.balders.ca:3456` | any `x-api-key` (ignored) |
|
||||||
|
| OpenAI-native (Pulse, paperless-ai, Open WebUI) | `http://meridian.lan.balders.ca:4000/v1` | `Authorization: Bearer $LITELLM_MASTER_KEY` |
|
||||||
|
|
||||||
|
Available model aliases (same on both endpoints, all backed by Claude Max): `claude-haiku-4-5`, `claude-sonnet-4-6`, `claude-opus-4-7`.
|
||||||
|
|
||||||
|
### Pulse (proven 2026-05-19)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PULSE_ADMIN_TOKEN=$(infisical secrets get vault_pulse_admin_token --env prod --path /pulse --plain)
|
||||||
|
LITELLM_KEY=$(infisical secrets get vault_litellm_master_key --env prod --path /meridian --plain)
|
||||||
|
curl -X POST https://pulse.balders.ca/api/settings/ai/update \
|
||||||
|
-H "X-API-Token: $PULSE_ADMIN_TOKEN" -H 'Content-Type: application/json' \
|
||||||
|
-d "{\"provider\":\"openai\",\"openai_api_key\":\"$LITELLM_KEY\",\"openai_base_url\":\"http://meridian.lan.balders.ca:4000/v1\",\"model\":\"claude-haiku-4-5\",\"enabled\":true}"
|
||||||
|
# verify
|
||||||
|
curl -X POST https://pulse.balders.ca/api/ai/test -H "X-API-Token: $PULSE_ADMIN_TOKEN" \
|
||||||
|
-d '{"provider":"openai","model":"claude-haiku-4-5"}' # → {"success":true,...}
|
||||||
|
```
|
||||||
|
|
||||||
## Operations
|
## Operations
|
||||||
|
|
||||||
- **Subsequent deploys**: via Semaphore template "Meridian Deploy" (added to
|
- **Subsequent deploys**: via Semaphore template "Meridian Deploy" (scheduled Sun 02:55 EDT). `LITELLM_MASTER_KEY` is auto-reconciled into Semaphore environment 4 by `homelab-ansible-lxc-semaphore/scripts/sync-semaphore-state.py` (merge-only `ENVIRONMENT_KEYS` step).
|
||||||
the sync-semaphore-state.py manifest).
|
- **Token refresh**: handled automatically by the Claude Code SDK. Manual fallback: `sudo -u meridian /usr/bin/meridian refresh-token`.
|
||||||
- **Token refresh**: handled automatically by the Claude Code SDK; if it ever
|
- **Restart after creds change**: `sudo systemctl restart meridian` (LiteLLM follows automatically via `Requires=`).
|
||||||
fails, `sudo -u meridian /usr/bin/meridian refresh-token` from the LXC.
|
- **Rotate master key**: update `/meridian/vault_litellm_master_key` in Infisical, redeploy, update consumers (Pulse, paperless-ai, etc.).
|
||||||
- **Restart after creds change**: `sudo systemctl restart meridian`.
|
- **Logs**: `journalctl -u meridian -f` / `journalctl -u litellm -f`.
|
||||||
- **Logs**: `journalctl -u meridian -f`.
|
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|
||||||
```
|
```
|
||||||
roles/meridian/ Node 22 install + npm i meridian + systemd unit
|
roles/meridian/ Node 22 + npm i @rynfar/meridian + systemd unit
|
||||||
|
roles/litellm/ Python venv + pip install litellm[proxy] + systemd unit
|
||||||
roles/node_exporter/ Prometheus exporter for fleet metrics
|
roles/node_exporter/ Prometheus exporter for fleet metrics
|
||||||
vars/main.yml base packages, ssh keys, meridian config
|
vars/main.yml base packages, ssh keys, meridian + litellm config
|
||||||
site.yml playbook entrypoint
|
site.yml playbook entrypoint (sanity-check assert on LITELLM_MASTER_KEY)
|
||||||
inventory.ini single host (192.168.1.164)
|
inventory.ini single host (192.168.1.164)
|
||||||
deploy.sh wrapper for local first-run
|
deploy.sh wrapper for local first-run; pulls LITELLM_MASTER_KEY from Infisical
|
||||||
```
|
```
|
||||||
|
|
||||||
## Memory pointers
|
## Memory pointers
|
||||||
|
|||||||
Reference in New Issue
Block a user