blob: 8dcdc2850a1fd83de04a46f046226375313ecfe3 [file] [view]
# llmao
Toolings **implementation** of the ASF LLM gateway seam at `llm.apache.org`.
**What this app is for:** Apache-facing control plane for **shared, attributed,
limited** access to Foundation-sanctioned inference. You sign in with ASF,
manage **PATs** (and later project envelopes), and browse the model catalog.
**Inference** goes to the **LiteLLM proxy** with a PAT not through a chat UI
here.
| Doc | Role |
|-----|------|
| **`apache/rai-private` `services/llmao/README.md`** | Product design (concepts, policy) |
| **[`docs/STATUS.md`](docs/STATUS.md)** | Build status + **planned UX** backlog |
| **This README** | How to run and use the software |
| [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | Repo structure |
| Infra `p6/modules/llmao` | Production deploy |
```
ASF id ──oauth──► llmao (identity, PAT UX, project governance UI)
│ admin
client tools ──PAT──► LiteLLM proxy ──► models
```
---
## Quickstart (local, asfquart + TLS)
The app is **always asfquart** (Apache OAuth). Local login needs HTTPS and a
host name OAuth will accept same pattern as Apache STeVe: **mkcert** certs
for `localhost.apache.org` (see `certs/README.md`).
Requires [uv](https://docs.astral.sh/uv/) on your `PATH`.
Required on-disk YAML (copy from `*.example`; app and `make proxy` **fail-fast**
if missing same presumption as STeVe-style config):
| File | From | Role |
|------|------|------|
| `config.yaml` | `config.yaml.example` | llmao / asfquart |
| `litellm.yaml` | `litellm.yaml.example` | LiteLLM proxy (`include: model_list.yaml`) |
| `model_list.yaml` | `model_list.yaml.example` | **Model inventory SoT** (routes + UX metadata) |
```bash
make install
cp config.yaml.example config.yaml
cp litellm.yaml.example litellm.yaml
cp model_list.yaml.example model_list.yaml
# generate certs under certs/ (mkcert) — certs/README.md
make run # uv run python main.py
```
Local run is **production-shaped**: LiteLLM + system Postgres (not an in-app
mock mode). Needs **system PostgreSQL** and **prisma** from
`litellm[proxy,extra-proxy]`.
```bash
# Postgres running (e.g. apt install postgresql; service started)
make db # bin/setup_litellm_db.py
# paste printed database_url into litellm.yaml general_settings
./bin/gen-litellm-master-key.sh # print sk-…; paste into BOTH:
# litellm.yaml → general_settings.master_key
# config.yaml → litellm.master_key
# set api keys in model_list.yaml (eyaml in production)
make proxy # litellm --config litellm.yaml
make run
```
Open `https://localhost.apache.org:8443/` (port from `config.yaml`), sign in
with ASF.
PAT metadata lives in LiteLLMs Postgres. Model inventory is **only**
`model_list.yaml` (not DB `STORE_MODEL_IN_DB`). Provider **API keys** in that
file come from eyaml in production; **`api_base` is cleartext** (not shown in UX).
After Puppet/VCS updates model list or litellm config, **restart LiteLLM**
(systemd notify in p6 later). Production secrets are on-disk YAML, not env vars.
ASGI (TLS on the reverse proxy):
```bash
uv run python -m hypercorn main:llmao_app --bind 0.0.0.0:8080
```
```bash
make test # offline seam + model_list tests (no OAuth session automation yet)
```
---
## Using the gateway (after sign-in)
1. **My Keys** create a personal PAT for a project you belong to (purpose optional).
Copy the secret **once**.
2. Point your client at the LiteLLM OpenAI-compatible base URL with that `sk-…` key.
Use a **model id** from **Models** as the `model` parameter.
3. **Other Keys** (PMC / site admin) automation keys; who minted them is recorded as `created_by`.
4. **Models** sanctioned inventory (supply-path details for site admins only).
**Projects** (envelopes, member caps, by-person usage) and **Reports** are product intent see design §6 and the UX backlog in [`docs/STATUS.md`](docs/STATUS.md).
Full status and phased UI plan: **[`docs/STATUS.md`](docs/STATUS.md)**.
Product design: **rai-private** `services/llmao/README.md`.
Ops: Infra **`p6/modules/llmao/README.md`**.
---
## API
Authenticated asfquart session (cookie after OAuth). Not the same thing as a
LiteLLM virtual key (PAT for inference).
```bash
# Per-project budget (members) and activity (PMC admins)
GET /v1/projects/<project>/budget
GET /v1/projects/<project>/usage
GET /healthz
```
Unauthenticated access to protected routes redirects to OAuth (browser) or
fails auth via asfquart. Project membership failures return JSON `403` where
the handler still runs.
---
## Production
1. **asfquart** (dependency via `pyproject.toml`) always provides OAuth at
`/auth` and LDAP-backed sessions see
<https://github.com/apache/infrastructure-asfquart>.
2. **Secrets on disk:** Puppet/hiera/eyaml renders `config.yaml` and
`litellm.yaml` with the **same** `master_key` (`sk-…`) and other secrets.
No production env-var secret channel.
3. **LiteLLM** with Postgres (`database_url` in `litellm.yaml`) and
`litellm --config litellm.yaml`. Model routes live in **`model_list.yaml`**
(included); provider API keys via eyaml in production.
4. **Serve** llmao (`main.py` or Hypercorn). Point client tools at the
**LiteLLM** base URL with PATs, not at llmao for chat.
The token handler in `auth.py` is a stub for calling llmaos own API
non-interactively; inference PATs are LiteLLM virtual keys.
### Self-hosted models via vLLM
The self-host catalog entries are served by **vLLM** one vLLM process per
model, each exposing an OpenAI-compatible endpoint with the litellm proxy in
front for per-PMC budgets (budgets live in litellm; vLLM has none). Each model
runs on its own port; litellm routes to the right one by `model_name`
(Option A), so there is no model-swap latency.
The PoC self-host tier (sized for a single 48GB GPU, e.g. an L40S) is three
Apache-2.0 open-weight models:
| Catalog model | vLLM served name | HF weights | Role |
|---|---|---|---|
| Gemma 4 26B-A4B | `gemma4-26b` | `google/gemma-4-26b-a4b` | general / multimodal / agentic |
| Qwen 3.6-27B | `qwen3.6-27b` | `Qwen/Qwen3.6-27B` | coding |
| Qwen3-8B | `qwen3-8b` | `Qwen/Qwen3-8B` | fast / routine calls |
Each model is served by a vLLM container, for example:
```bash
docker run --rm --gpus all --ipc=host -p 8003:8003 \
-v ~/.cache/huggingface:/root/.cache/huggingface -e HF_TOKEN=$HF_TOKEN \
vllm/vllm-openai:v0.6.6 \
--model Qwen/Qwen3-8B --served-model-name qwen3-8b --port 8003
```
Self-host `api_base` values live in **`model_list.yaml`** (cleartext). The
compose stack under `infra/docker/` is optional reference.
---
## Layout
```
main.py entry: create_app, run_standalone / run_asgi
pages.py HTML + /static
api.py JSON /healthz and /v1/*
templates/ static/ EZT + Bootstrap
bin/fetch-thirdparty.sh vendor Bootstrap/icons
bin/gen-litellm-master-key.sh print sk-… for admin key
config.yaml.example → config.yaml (gitignored)
litellm.yaml.example → litellm.yaml (include model_list.yaml)
model_list.yaml.example → model_list.yaml (inventory SoT; keys from eyaml)
certs/ mkcert PEMs + README
llmao/ seam, auth, models, litellm_client
tests/ offline seam + model_list / LiteLLM metadata tests
```