litellm: add OpenAI→Meridian shim role (venv + systemd, port 4000)

LiteLLM sits in front of Meridian for clients that can't talk Anthropic's
/v1/messages format (Pulse OpenAI provider, paperless-ai, etc.). Routes
OpenAI-shaped requests to localhost:3456 (Meridian) which forwards to the
Max sub.

- New roles/litellm/ — Python venv, pip install litellm[proxy], systemd
- vars/main.yml — model map (haiku/sonnet/opus) + LITELLM_MASTER_KEY env lookup
- site.yml — adds litellm role + sanity-check assert
- deploy.sh — pulls LITELLM_MASTER_KEY from Infisical (/meridian/) on the
  controller and exports it for the playbook
- New Infisical secret /meridian/vault_litellm_master_key

Smoke: Pulse → LiteLLM /v1/chat/completions → Meridian /v1/messages → Max sub
returns "pong" through both the LiteLLM master key auth and the Claude Code
SDK OAuth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-05-19 11:23:52 -04:00
parent 4ab85f0227
commit a6b26c500f
8 changed files with 238 additions and 13 deletions
+9
View File
@@ -0,0 +1,9 @@
---
- name: reload systemd
systemd:
daemon_reload: true
- name: restart litellm
systemd:
name: litellm
state: restarted
+114
View File
@@ -0,0 +1,114 @@
---
# LiteLLM — OpenAI-compatible proxy that fronts Meridian's Anthropic
# /v1/messages endpoint. Lets Anthropic-unfriendly clients (Pulse's
# OpenAI provider, paperless-ai, etc.) talk to the Max sub via Meridian.
- name: Ensure python3-venv is installed (LiteLLM runs in a venv)
apt:
name:
- python3-venv
- python3-pip
state: present
- name: Ensure litellm system user
user:
name: "{{ litellm_user }}"
system: true
home: "{{ litellm_home }}"
shell: /usr/sbin/nologin
create_home: true
state: present
- name: Ensure litellm home perms
file:
path: "{{ litellm_home }}"
state: directory
owner: "{{ litellm_user }}"
group: "{{ litellm_user }}"
mode: '0755'
# Pip install runs as root (LXC filesystem doesn't support the ACL flip
# Ansible uses for become_user). Venv contents end up root-owned, which is
# fine — systemd runs the proxy as the litellm user and only needs read+exec.
- name: Create litellm venv
command: python3 -m venv {{ litellm_venv }}
args:
creates: "{{ litellm_venv }}/bin/python"
- name: Upgrade pip + wheel + setuptools in venv
pip:
name:
- pip
- wheel
- setuptools
state: latest
virtualenv: "{{ litellm_venv }}"
virtualenv_command: python3 -m venv
- name: Install LiteLLM into venv
pip:
name: "{{ litellm_package_spec }}"
virtualenv: "{{ litellm_venv }}"
virtualenv_command: python3 -m venv
notify: restart litellm
- name: Resolve litellm binary
stat:
path: "{{ litellm_venv }}/bin/litellm"
register: litellm_bin
- name: Fail if litellm binary missing
fail:
msg: "litellm not installed at {{ litellm_venv }}/bin/litellm"
when: not litellm_bin.stat.exists
- name: Drop LiteLLM config
template:
src: litellm-config.yaml.j2
dest: "{{ litellm_home }}/config.yaml"
owner: "{{ litellm_user }}"
group: "{{ litellm_user }}"
mode: '0640'
notify: restart litellm
- name: Drop systemd environment file (master key)
template:
src: litellm.env.j2
dest: "{{ litellm_home }}/litellm.env"
owner: "{{ litellm_user }}"
group: "{{ litellm_user }}"
mode: '0600'
notify: restart litellm
no_log: true
- name: Deploy litellm systemd unit
template:
src: litellm.service.j2
dest: /etc/systemd/system/litellm.service
owner: root
group: root
mode: '0644'
notify:
- reload systemd
- restart litellm
- name: Flush handlers (reload systemd before enable)
meta: flush_handlers
- name: Enable + start litellm
systemd:
name: litellm
enabled: true
state: started
daemon_reload: true
- name: Wait for LiteLLM /health
uri:
url: "http://127.0.0.1:{{ litellm_port }}/health/liveliness"
status_code: 200
register: litellm_health
until: litellm_health.status is defined and litellm_health.status == 200
retries: 20
delay: 3
failed_when: false
@@ -0,0 +1,21 @@
# {{ ansible_managed }}
#
# LiteLLM proxy config. Routes OpenAI-shaped requests to Meridian's
# /v1/messages (Anthropic format). Meridian (same host, :3456) ignores the
# upstream API key, so we pass a placeholder.
model_list:
{% for m in litellm_models %}
- model_name: {{ m.name }}
litellm_params:
model: {{ m.backend }}
api_base: http://127.0.0.1:{{ meridian_port }}
api_key: placeholder-meridian-ignores-this
{% endfor %}
general_settings:
master_key: os.environ/LITELLM_MASTER_KEY
litellm_settings:
drop_params: true # tolerate clients sending unsupported params
set_verbose: false
+2
View File
@@ -0,0 +1,2 @@
# {{ ansible_managed }}
LITELLM_MASTER_KEY={{ litellm_master_key }}
@@ -0,0 +1,21 @@
[Unit]
Description=LiteLLM (OpenAI → Meridian shim)
Documentation=https://docs.litellm.ai/
After=network-online.target meridian.service
Wants=network-online.target
Requires=meridian.service
[Service]
Type=simple
User={{ litellm_user }}
Group={{ litellm_user }}
WorkingDirectory={{ litellm_home }}
EnvironmentFile={{ litellm_home }}/litellm.env
ExecStart={{ litellm_venv }}/bin/litellm --config {{ litellm_home }}/config.yaml --host {{ litellm_host }} --port {{ litellm_port }} --num_workers 1
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target