Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions apps/llm-proxy/_modules/cli-proxy-api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# LLM Proxy
- [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI)
- [Caddy](https://caddyserver.com/)

## First boot

Retrieve the generated client key:

```sh
sudo cat /var/lib/cliproxyapi/api-key
```

Test authentication locally on the server:

```sh
API_KEY="$(sudo cat /var/lib/cliproxyapi/api-key)"
curl -fsS -H "Authorization: Bearer $API_KEY" http://127.0.0.1:8317/v1/models
unset API_KEY
```

## Provider login

Provider callback ports remain closed in the Lightsail firewall, thus the login must be done through an SSH tunnel.

```sh
sudo -u cliproxy -H /opt/cliproxyapi/cli-proxy-api --config /etc/cliproxyapi/config.yaml -no-browser --codex-login
```

Replace `--codex-login` with `--claude-login` or `--antigravity-login` when needed. Credentials are written to `/var/lib/cliproxyapi/auth` and detected by the running service.
170 changes: 170 additions & 0 deletions apps/llm-proxy/_modules/cli-proxy-api/bootstrap.sh.tftpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
#!/usr/bin/env bash
set -euo pipefail

exec > >(tee -a /var/log/cliproxy-bootstrap.log | logger -t cliproxy-bootstrap -s 2>/dev/console) 2>&1

export DEBIAN_FRONTEND=noninteractive

if ! swapon --show=NAME --noheadings | grep -qx '/swapfile'; then
if [ ! -f /swapfile ]; then
fallocate -l 1G /swapfile
chmod 600 /swapfile
mkswap /swapfile
fi
swapon /swapfile
fi
grep -q '^/swapfile ' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab

timedatectl set-timezone America/Vancouver

apt-get update
apt-get install -y --no-install-recommends apt-transport-https ca-certificates curl debian-archive-keyring debian-keyring gnupg openssl

curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
| gpg --dearmor --yes -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
> /etc/apt/sources.list.d/caddy-stable.list
chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg /etc/apt/sources.list.d/caddy-stable.list
apt-get update
apt-get install -y --no-install-recommends caddy

if ! id cliproxy >/dev/null 2>&1; then
useradd --system --home-dir /var/lib/cliproxyapi --create-home --shell /usr/sbin/nologin cliproxy
fi

install -d -o root -g root -m 0755 /opt/cliproxyapi
install -d -o root -g cliproxy -m 0750 /etc/cliproxyapi
install -d -o cliproxy -g cliproxy -m 0750 /var/lib/cliproxyapi/auth

cat > /usr/local/sbin/update-cliproxyapi <<'UPDATE_SCRIPT'
#!/usr/bin/env bash
set -euo pipefail

if [ "$#" -ne 1 ]; then
echo "Usage: $0 VERSION_WITHOUT_LEADING_V" >&2
exit 2
fi

VERSION="$${1#v}"
case "$(uname -m)" in
x86_64) ARCH="amd64" ;;
aarch64|arm64) ARCH="aarch64" ;;
*) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;;
esac

ARCHIVE="CLIProxyAPI_$${VERSION}_linux_$${ARCH}.tar.gz"
RELEASE_URL="https://github.com/router-for-me/CLIProxyAPI/releases/download/v$${VERSION}"
WORK_DIR="$(mktemp -d)"
trap 'rm -rf "$${WORK_DIR}"' EXIT

curl -fsSL "$${RELEASE_URL}/checksums.txt" -o "$${WORK_DIR}/checksums.txt"
curl -fsSL "$${RELEASE_URL}/$${ARCHIVE}" -o "$${WORK_DIR}/$${ARCHIVE}"
(
cd "$${WORK_DIR}"
grep " $${ARCHIVE}$" checksums.txt | sha256sum --check --strict -
tar -xzf "$${ARCHIVE}"
)

install -o root -g root -m 0755 "$${WORK_DIR}/cli-proxy-api" /opt/cliproxyapi/cli-proxy-api.new
mv -f /opt/cliproxyapi/cli-proxy-api.new /opt/cliproxyapi/cli-proxy-api

if systemctl is-active --quiet cliproxyapi.service; then
systemctl restart cliproxyapi.service
fi
UPDATE_SCRIPT
chmod 0755 /usr/local/sbin/update-cliproxyapi
# Terraform expands this template expression before the script runs.
# shellcheck disable=SC2016
/usr/local/sbin/update-cliproxyapi '${cliproxyapi_version}'

umask 077
if [ ! -s /var/lib/cliproxyapi/api-key ]; then
openssl rand -hex 32 > /var/lib/cliproxyapi/api-key
fi
# Terraform converts $${API_KEY} below to a shell expansion in the rendered script.
# shellcheck disable=SC2034
API_KEY="$(cat /var/lib/cliproxyapi/api-key)"

cat > /etc/cliproxyapi/config.yaml <<CONFIG
host: "127.0.0.1"
port: 8317

tls:
enable: false
cert: ""
key: ""

remote-management:
allow-remote: false
secret-key: ""
disable-control-panel: true

auth-dir: "/var/lib/cliproxyapi/auth"

api-keys:
- "$${API_KEY}"

debug: false
commercial-mode: true
logging-to-file: false
usage-statistics-enabled: false

plugins:
enabled: false
dir: "/var/lib/cliproxyapi/plugins"
CONFIG
chown root:cliproxy /etc/cliproxyapi/config.yaml
chmod 0640 /etc/cliproxyapi/config.yaml
chown root:root /var/lib/cliproxyapi/api-key
chmod 0600 /var/lib/cliproxyapi/api-key

cat > /etc/systemd/system/cliproxyapi.service <<'SERVICE'
[Unit]
Description=CLIProxyAPI
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
User=cliproxy
Group=cliproxy
WorkingDirectory=/var/lib/cliproxyapi
ExecStart=/opt/cliproxyapi/cli-proxy-api --config /etc/cliproxyapi/config.yaml
Restart=on-failure
RestartSec=5s
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
ReadWritePaths=/var/lib/cliproxyapi
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6

[Install]
WantedBy=multi-user.target
SERVICE

cat > /etc/caddy/Caddyfile <<'CADDYFILE'
${server_hostname} {
reverse_proxy 127.0.0.1:8317
}
CADDYFILE
chown root:caddy /etc/caddy/Caddyfile
chmod 0640 /etc/caddy/Caddyfile

install -d -m 0755 /etc/systemd/journald.conf.d
cat > /etc/systemd/journald.conf.d/cliproxyapi.conf <<'JOURNALD'
[Journal]
SystemMaxUse=100M
RuntimeMaxUse=50M
MaxRetentionSec=7day
JOURNALD

systemctl daemon-reload
systemctl restart systemd-journald
systemctl enable --now cliproxyapi.service
caddy validate --config /etc/caddy/Caddyfile
systemctl enable caddy.service
systemctl restart caddy.service
69 changes: 69 additions & 0 deletions apps/llm-proxy/_modules/cli-proxy-api/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
resource "aws_lightsail_instance" "proxy" {
name = var.instance_name
availability_zone = var.availability_zone
blueprint_id = var.lightsail_blueprint
bundle_id = var.lightsail_bundle
ip_address_type = "dualstack"

user_data = templatefile("${path.module}/bootstrap.sh.tftpl", {
cliproxyapi_version = var.cliproxyapi_version
server_hostname = var.hostname
})

tags = {
Name = var.instance_name
}
}

resource "aws_lightsail_static_ip" "proxy" {
name = var.instance_name
}

resource "aws_lightsail_static_ip_attachment" "proxy" {
static_ip_name = aws_lightsail_static_ip.proxy.name
instance_name = aws_lightsail_instance.proxy.name
}

resource "aws_lightsail_instance_public_ports" "proxy" {
instance_name = aws_lightsail_instance.proxy.name

port_info {
protocol = "tcp"
from_port = 22
to_port = 22
cidrs = [var.admin_ipv4_cidr]
ipv6_cidrs = []
}

port_info {
protocol = "tcp"
from_port = 80
to_port = 80
cidrs = ["0.0.0.0/0"]
ipv6_cidrs = ["::/0"]
}

port_info {
protocol = "tcp"
from_port = 443
to_port = 443
cidrs = ["0.0.0.0/0"]
ipv6_cidrs = ["::/0"]
}
}

resource "aws_route53_record" "proxy_ipv4" {
zone_id = var.route53_hosted_zone
name = var.hostname
type = "A"
ttl = 60
records = [aws_lightsail_static_ip.proxy.ip_address]
}

resource "aws_route53_record" "proxy_ipv6" {
zone_id = var.route53_hosted_zone
name = var.hostname
type = "AAAA"
ttl = 60
records = [aws_lightsail_instance.proxy.ipv6_addresses[0]]
}
14 changes: 14 additions & 0 deletions apps/llm-proxy/_modules/cli-proxy-api/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
output "static_ipv4_address" {
description = "Static public IPv4 address attached to the Lightsail instance"
value = aws_lightsail_static_ip.proxy.ip_address
}

output "ipv6_address" {
description = "Stable public IPv6 address assigned to the Lightsail instance"
value = aws_lightsail_instance.proxy.ipv6_addresses[0]
}

output "instance_username" {
description = "Default SSH username from the selected Lightsail blueprint"
value = aws_lightsail_instance.proxy.username
}
39 changes: 39 additions & 0 deletions apps/llm-proxy/_modules/cli-proxy-api/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
variable "instance_name" {
description = "Name assigned to the Lightsail instance and static IP"
type = string
}

variable "availability_zone" {
description = "Lightsail availability zone"
type = string
}

variable "lightsail_blueprint" {
description = "Lightsail operating-system blueprint ID"
type = string
}

variable "lightsail_bundle" {
description = "Lightsail compute bundle ID"
type = string
}

variable "admin_ipv4_cidr" {
description = "Trusted administrator IPv4 CIDR allowed to connect over SSH"
type = string
}

variable "cliproxyapi_version" {
description = "Pinned CLIProxyAPI release version without the leading v"
type = string
}

variable "hostname" {
description = "Public hostname served by Caddy"
type = string
}

variable "route53_hosted_zone" {
description = "Route 53 hosted zone ID in which to create the hostname"
type = string
}
8 changes: 8 additions & 0 deletions apps/llm-proxy/_modules/cli-proxy-api/versions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
16 changes: 16 additions & 0 deletions apps/llm-proxy/usw2dev/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
locals {
fqdn = "${var.subdomain_name}.${data.terraform_remote_state.dns.outputs.domain_name}"
}

module "cli_proxy_api" {
source = "../_modules/cli-proxy-api"

instance_name = "${var.project_name}-${var.env_name}"
availability_zone = var.availability_zone
lightsail_blueprint = var.lightsail_blueprint_id
lightsail_bundle = var.lightsail_bundle_id
admin_ipv4_cidr = var.admin_ipv4_cidr
cliproxyapi_version = var.cliproxyapi_version
hostname = local.fqdn
route53_hosted_zone = data.terraform_remote_state.dns.outputs.domain_hosted_zone_id
}
29 changes: 29 additions & 0 deletions apps/llm-proxy/usw2dev/moved.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
moved {
from = aws_lightsail_instance.proxy
to = module.cli_proxy_api.aws_lightsail_instance.proxy
}

moved {
from = aws_lightsail_static_ip.proxy
to = module.cli_proxy_api.aws_lightsail_static_ip.proxy
}

moved {
from = aws_lightsail_static_ip_attachment.proxy
to = module.cli_proxy_api.aws_lightsail_static_ip_attachment.proxy
}

moved {
from = aws_lightsail_instance_public_ports.proxy
to = module.cli_proxy_api.aws_lightsail_instance_public_ports.proxy
}

moved {
from = aws_route53_record.proxy_ipv4
to = module.cli_proxy_api.aws_route53_record.proxy_ipv4
}

moved {
from = aws_route53_record.proxy_ipv6
to = module.cli_proxy_api.aws_route53_record.proxy_ipv6
}
Loading