Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
47ce9c5
docs: document firewall rules and sudo password trailing space precau…
sheepdestroyer Jul 8, 2026
ee818d7
feat: resolve console links using request host or BASEURL/BASE_URL en…
sheepdestroyer Jul 8, 2026
f542bd0
Update router/main.py
sheepdestroyer Jul 8, 2026
3292da2
docs: add GitHub CLI auth mapping notes to AGENTS.md
sheepdestroyer Jul 8, 2026
75f4851
feat: conditionally resolve console links to subdomains if on vendeuv…
sheepdestroyer Jul 8, 2026
675a59c
feat: subdirectory routing for litellm, langfuse, and llama.cpp under…
sheepdestroyer Jul 8, 2026
36cbc72
fix: address PR reviews - sanitize external_host and fix credentials …
sheepdestroyer Jul 8, 2026
c2a27e8
docs: document -k intent in verification curl (self-signed cert)
sheepdestroyer Jul 8, 2026
aded670
Update AGENTS.md
sheepdestroyer Jul 8, 2026
49b4f3a
chore: update success banner to show external /llm-routing/v1 endpoin…
sheepdestroyer Jul 8, 2026
e096379
fix: bump LiteLLM readiness probe initial delay to 45s to avoid start…
sheepdestroyer Jul 8, 2026
a702b5c
chore: sync changes from boy prod including local-qwen-3.6 config and…
sheepdestroyer Jul 8, 2026
86442b5
fix: address PR reviews - configurable ROUTING_DOMAIN, improve extern…
sheepdestroyer Jul 8, 2026
0404eff
fix: address review comments on PR #241 (domain validation, dynamic s…
sheepdestroyer Jul 8, 2026
8d67ccf
fix: address review comments on PR #242 (revert pod.yaml host paths, …
sheepdestroyer Jul 8, 2026
855deb3
refactor: replace legacy hardcoded placeholders with generic WORKDIR_…
sheepdestroyer Jul 8, 2026
045284a
fix: normalize PUBLIC_BASE_URL to include https scheme and llm-routin…
sheepdestroyer Jul 8, 2026
60ea4af
docs: sanitize legacy gpav paths in README.md
sheepdestroyer Jul 8, 2026
13ebc2c
refactor: centralize URL resolution to resolve_external_urls helper u…
sheepdestroyer Jul 8, 2026
8331d9b
refactor: consolidate routing domain default, dynamic ports for fallb…
sheepdestroyer Jul 9, 2026
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
62 changes: 60 additions & 2 deletions .agents/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ When working on this project, always refer to the dedicated **NotebookLM Compani
- Local model benchmark metrics and `llama-server` configurations

### Notebook Details
- **Notebook Name:** `TriageGate-Architect-KB`
- **Notebook Name:** `LLM-Routing-KB`
- **Notebook ID:** `llm-triage-gateway`
- **Notebook URL:** [TriageGate-Architect-KB](https://notebooklm.google.com/notebook/826cbd87-7969-4b0e-a38e-5517b5ab7d28)
- **Notebook URL:** [LLM-Routing-KB](https://notebooklm.google.com/notebook/826cbd87-7969-4b0e-a38e-5517b5ab7d28)

### How to Query
Use the `notebooklm` MCP tools to search or ask questions about this codebase and stack:
Expand All @@ -26,3 +26,61 @@ To prevent directory reorganization regressions, outdated file restorations, or
2. **Directory Rename Safety**: If Git reports conflicts related to moved directories or files, do not manually stage deletions of tracked files from moved directories (e.g., under the old `tests/` or `scripts/` paths) or re-create files at the root level. Resolve conflicts by directing all changes and file operations to the newly refactored paths.
3. **Verify Security Credentials**: Never accept resolutions that overwrite configuration files (`pod.yaml`, `start-stack.sh`) with hardcoded default passwords. Ensure placeholder-based configurations are preserved.
4. **Enforce Test Suite Count**: Run the full unit test suite (`pytest`) after conflict resolution. Verify that the total number of passing tests is equal to or greater than before the resolution.

## Production Deployment Checklist

Note: Throughout this checklist, the production host SSH alias is represented by `<prod-host>` (e.g., `boy`), the deployer home path is represented by `<prod-home>` (e.g., `/mnt/DATA/boy`), and the domain is represented by `<prod-domain>` (e.g., `vendeuvre.lan`).

### One-Time Host Prerequisites
- `net.ipv4.ip_unprivileged_port_start=80` persisted in `/etc/sysctl.d/99-unprivileged-ports.conf`
- Host firewall ports `80/tcp` and `443/tcp` opened in `firewalld` (e.g. `sudo firewall-cmd --zone=public --add-port=80/tcp --permanent && sudo firewall-cmd --zone=public --add-port=443/tcp --permanent && sudo firewall-cmd --reload`)
- SSH host alias configured in `~/.ssh/config` — use `ssh <prod-host>` / `rsync ... <prod-host>:` throughout
- Required mount directories created under `<prod-home>`:
- `<prod-home>/.gemini/`
- `<prod-home>/.local/bin/agy` (copy of the `agy` binary)
- `<prod-home>/.local/share/goose/`
- `<prod-home>/.local/share/keyrings/`
- HAProxy SSL cert: `<prod-home>/haproxy/certs/<prod-domain>.pem`
- HAProxy config: `<prod-home>/haproxy/haproxy.cfg`

### Fresh Deploy Steps (after a PR is merged to master)
```bash
# 1. Clean up old deploy
ssh <prod-host> "rm -rf <prod-home>/LLM-Routing"

# 2. Clone fresh from master
ssh <prod-host> "git clone https://github.com/sheepdestroyer/LLM-Routing.git <prod-home>/LLM-Routing"

# 3. Start the full stack (builds and launches all containers)
ssh <prod-host> "cd <prod-home>/LLM-Routing && ./start-stack.sh --full-rebuild"

# 4. Start (or restart) production HAProxy
ssh <prod-host> "podman rm -f production-haproxy || true"
ssh <prod-host> "podman run -d --name production-haproxy --restart always --net host \
-v <prod-home>/haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro \
-v <prod-home>/haproxy/certs:/usr/local/etc/haproxy/certs:ro \
docker.io/library/haproxy:alpine"

# 5. Start the host-side agy daemon
ssh <prod-host> "pkill -f host_agy_daemon.py || true"
ssh <prod-host> "nohup python3 <prod-home>/LLM-Routing/scripts/host_agy_daemon.py >/tmp/agy-daemon.log 2>&1 </dev/null &"

# 6. Verify end-to-end
# NOTE: -k is intentional — the HAProxy cert is self-signed (local CA).
# Replace the cert with a trusted CA-signed cert to remove -k.
ssh <prod-host> "curl -k -s --resolve <prod-host>.<prod-domain>:443:127.0.0.1 https://<prod-host>.<prod-domain>/llm-routing/dashboard" | head -5
```

### Notes
- The `agy-daemon.service` systemd unit cannot be reloaded via `systemctl --user` from
the agent terminal (DBus is not connected). Start the daemon manually with `nohup` as
shown above, or instruct the user to run it in their own session.
- **Sudo Password Precaution**: Always preserve exact bytes (including trailing spaces or newlines) when reading `~/.sudo_password` (e.g. `'your_password_here '`). Stripping whitespace will cause authentication to fail.
- `start-stack.sh` without `--full-rebuild` will do a fast pod restart (reuses images).
Use `--full-rebuild` after code changes or image updates.
- **GitHub CLI Authentication**: If running `gh` commands fails with a 401 error, ensure that `GITHUB_TOKEN` is exported (e.g., mapped from `GITHUB_MCP_PAT` in `~/.bashrc` via `export GITHUB_TOKEN="$GITHUB_MCP_PAT"`).

## GitHub API & Operations Policy
When interacting with the GitHub API or performing repository/PR metadata operations:
1. **Prefer `gh` CLI**: Always prefer using the GitHub CLI (`gh`) instead of executing raw `curl` commands.
2. **REST API Fallback via `gh api`**: If standard `gh` commands (like `gh pr view`) fail due to missing GraphQL token scopes (e.g., `read:org`), use `gh api` to run REST queries against the endpoint (e.g., `gh api repos/{owner}/{repo}/pulls/{pr_number}/reviews`) as it does not require GraphQL scopes.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ The gateway supports multiple routing modes controlled by the `model` field:
All configurations, automation scripts, and databases are self-contained within this repository directory:

```
/home/gpav/Vrac/LAB/AI/LLM-Routing/
/path/to/LLM-Routing/
├── .env # Environment file for API keys, passwords, and generated secrets (ignored by git)
├── .gitignore # Git ignore policy protecting secrets & database files
├── README.md # In-depth system and operational guide
Expand Down Expand Up @@ -350,7 +350,7 @@ The stack also supports **semantic** (vector-similarity) caching via `vector_sto
- **Embedding Model**: Zero-cost local `nomic-embed-text-v1.5-Q4_K_M` (~137MB GGUF) running on llama-server, loaded as `local-nomic-embed` in LiteLLM. Produces 768-dimension vectors with CLS pooling.
- **Vector Store**: PostgreSQL with pgvector extension stores embeddings in the `litellm_semantic_cache` collection.
- **Cost**: Completely free — no OpenRouter API calls for embedding generation.
- **Configuration**: The nomic-embed model profile in `models.ini` (`/home/gpav/Vrac/LAB/AI/models.ini`) includes `embedding = true`, `pooling = cls`, and `embd-normalize = 2` for proper vector similarity search. llama-server runs with `--models-max 3` to keep the classifier (0.8B), MoE (35B), and embedding model loaded simultaneously.
- **Configuration**: The nomic-embed model profile in `models.ini` (e.g., `/path/to/models.ini`) includes `embedding = true`, `pooling = cls`, and `embd-normalize = 2` for proper vector similarity search. llama-server runs with `--models-max 3` to keep the classifier (0.8B), MoE (35B), and embedding model loaded simultaneously.

---

Expand Down Expand Up @@ -456,7 +456,7 @@ Uvicorn's log level follows the same env var via `${LOG_LEVEL:-warning}` in the
| `PrivateTmp` | `yes` | Isolates `/tmp` namespace for the daemon |
| `PrivateDevices` | `yes` | Restricts access to `/dev` (no raw disk/device access) |
| `ProtectSystem` | `strict` | Makes `/usr` and `/etc` read-only |
| `ProtectHome` | `read-only` | `/home/gpav` is read-only except specific paths |
| `ProtectHome` | `read-only` | The user home directory is read-only except specific paths |
| `ProtectKernelTunables` | `yes` | Makes `/sys` and `/proc/sys` read-only |
| `ProtectKernelModules` | `yes` | Blocks loading or listing kernel modules |
| `ProtectControlGroups` | `yes` | Makes cgroup filesystem read-only |
Expand Down Expand Up @@ -758,7 +758,7 @@ To maximize throughput under concurrent queries, `llama-server` is configured wi
#### 3. Custom Memory Endpoint Proxy & MCP Server
To allow Goose (and other agents) to store, list, and delete persistent preference/factual memories, we implemented a custom memory stack:
* **Triage Router Memory Proxy**: Exposes a catch-all route `@app.api_route("/v1/memory{path:path}", methods=["GET", "POST", "DELETE", "PUT"])` in `router/main.py` that intercepts memory calls and proxies them to the LiteLLM gateway (port 4000) using the securely-loaded `LITELLM_MASTER_KEY` authorization.
* **Memory MCP Bridge Server**: Created a custom stdio MCP server in [memory_mcp.py](file:///home/gpav/Vrac/LAB/AI/LLM-Routing/router/memory_mcp.py) that exposes the `rememberMemory`, `retrieveMemories`, and `removeSpecificMemory` tools. The script proxies these commands directly to `http://localhost:5000/v1/memory`.
* **Memory MCP Bridge Server**: Created a custom stdio MCP server in [memory_mcp.py](router/memory_mcp.py) that exposes the `rememberMemory`, `retrieveMemories`, and `removeSpecificMemory` tools. The script proxies these commands directly to `http://localhost:5000/v1/memory`.
* **Goose Integration**: The built-in memory extension is disabled in `~/.config/goose/config.yaml` and replaced with the `litellm-memory` custom command-line extension running our bridge server.

## 9c. Ollama Proxy Integration (via LiteLLM ollama_chat)
Expand Down
25 changes: 12 additions & 13 deletions litellm/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,29 +38,29 @@ litellm_settings:
- agent-advanced-core
- llm-routing-ollama
- openrouter-auto
# - local-qwen-3.6 # DISABLED: 35B model unloaded (23GB GTT saved)
- local-qwen-3.6
- agent-medium-core:
- agent-complex-core
- agent-reasoning-core
- agent-advanced-core
- llm-routing-ollama
- openrouter-auto
# - local-qwen-3.6 # DISABLED
- local-qwen-3.6
- agent-complex-core:
- agent-reasoning-core
- agent-advanced-core
- llm-routing-ollama
- openrouter-auto
# - local-qwen-3.6 # DISABLED
- local-qwen-3.6
- agent-reasoning-core:
- agent-advanced-core
- llm-routing-ollama
- openrouter-auto
# - local-qwen-3.6 # DISABLED
- local-qwen-3.6
- agent-advanced-core:
- llm-routing-ollama
- openrouter-auto
# - local-qwen-3.6 # DISABLED
- local-qwen-3.6

model_list:
- litellm_params:
Expand Down Expand Up @@ -90,14 +90,13 @@ model_list:
max_input_tokens: 524288
is_public_model_group: true

# DISABLED 2026-06-08 — 20GB on disk, 23GB GTT (system RAM as GPU buffer).
# Uncomment to re-enable once a lighter model replaces it.
#- litellm_params:
# api_base: http://127.0.0.1:8080/v1
# api_key: local-token
# model: openai/qwen-35b-q4ks
# request_timeout: 300
# model_name: local-qwen-3.6
# Re-enabled 2026-07-08 — llama.cpp on port 8081, alias local-qwen-3.6
- litellm_params:
api_base: http://127.0.0.1:8081/v1
api_key: local-token
model: openai/local-qwen-3.6
request_timeout: 300
model_name: local-qwen-3.6
- litellm_params:
api_base: http://127.0.0.1:8080/v1
api_key: local-token
Expand Down
46 changes: 27 additions & 19 deletions pod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ spec:
value: LITELLM_MASTER_KEY_PLACEHOLDER
- name: OLLAMA_API_KEY
value: OLLAMA_API_KEY_PLACEHOLDER
- name: SERVER_ROOT_PATH
value: /llm-routing/litellm
- name: PROXY_BASE_URL
value: PROXY_BASE_URL_PLACEHOLDER
image: ghcr.io/berriai/litellm:v1.91.0
livenessProbe:
exec:
Expand All @@ -62,7 +66,7 @@ spec:
- python3
- -c
- import urllib.request; urllib.request.urlopen('http://localhost:4000/health/readiness')
initialDelaySeconds: 10
initialDelaySeconds: 45
periodSeconds: 10
timeoutSeconds: 5
volumeMounts:
Expand Down Expand Up @@ -91,7 +95,7 @@ spec:
- name: DATABASE_URL
value: postgresql://postgres:POSTGRES_PASSWORD_ENCODED_PLACEHOLDER@127.0.0.1:5432/postgres
- name: DBUS_SESSION_BUS_ADDRESS
value: unix:path=/run/user/1000/bus
value: unix:path=RUN_USER_PLACEHOLDER/bus
- name: LITELLM_MASTER_KEY
value: LITELLM_MASTER_KEY_PLACEHOLDER
- name: LANGFUSE_PUBLIC_KEY
Expand All @@ -102,6 +106,10 @@ spec:
value: http://127.0.0.1:3001
- name: OLLAMA_API_KEY
value: OLLAMA_API_KEY_PLACEHOLDER
- name: PUBLIC_BASE_URL
value: PUBLIC_BASE_URL_PLACEHOLDER
- name: ROUTING_DOMAIN
value: ROUTING_DOMAIN_PLACEHOLDER
- name: LOG_LEVEL
value: info
image: localhost/llm-triage-router:latest
Expand Down Expand Up @@ -140,11 +148,11 @@ spec:
- mountPath: /usr/local/bin/agy
name: agy-bin
subPath: agy
- mountPath: /run/user/1000/bus
- mountPath: RUN_USER_PLACEHOLDER/bus
name: dbus-socket
- mountPath: /root/.local/share/keyrings
name: keyring-store
- mountPath: /run/user/1000
- mountPath: RUN_USER_PLACEHOLDER
name: xdg-runtime
- mountPath: /config/.env
name: env-file
Expand Down Expand Up @@ -427,48 +435,48 @@ spec:
restartPolicy: Always
volumes:
- hostPath:
path: /home/gpav/Vrac/LAB/AI/LLM-Routing/valkey-data
path: WORKDIR_PLACEHOLDER/valkey-data
name: valkey-storage
- hostPath:
path: /home/gpav/Vrac/LAB/AI/LLM-Routing/litellm
path: WORKDIR_PLACEHOLDER/litellm
name: litellm-config
- hostPath:
path: /home/gpav/Vrac/LAB/AI/LLM-Routing/router
path: WORKDIR_PLACEHOLDER/router
name: router-config
- hostPath:
path: /home/gpav/Vrac/LAB/AI/LLM-Routing/postgres-data
path: WORKDIR_PLACEHOLDER/postgres-data
name: postgres-storage
- hostPath:
path: /home/gpav/Vrac/LAB/AI/LLM-Routing/clickhouse-data
path: WORKDIR_PLACEHOLDER/clickhouse-data
name: clickhouse-storage
- hostPath:
path: /home/gpav/Vrac/LAB/AI/LLM-Routing/redis-lf-data
path: WORKDIR_PLACEHOLDER/redis-lf-data
name: redis-lf-storage
- hostPath:
path: /home/gpav/Vrac/LAB/AI/LLM-Routing/minio-data
path: WORKDIR_PLACEHOLDER/minio-data
name: minio-storage
- hostPath:
path: /home/gpav/.gemini
path: HOME_PLACEHOLDER/.gemini
name: gemini-secrets
- hostPath:
path: /home/gpav/.local/share/goose
path: HOME_PLACEHOLDER/.local/share/goose
name: goose-sessions
- hostPath:
path: /home/gpav/.local/bin
path: HOME_PLACEHOLDER/.local/bin
name: agy-bin
- hostPath:
path: /run/user/1000/bus
path: RUN_USER_PLACEHOLDER/bus
name: dbus-socket
- hostPath:
path: /home/gpav/.local/share/keyrings
path: HOME_PLACEHOLDER/.local/share/keyrings
name: keyring-store
- hostPath:
path: /run/user/1000
path: RUN_USER_PLACEHOLDER
name: xdg-runtime
- hostPath:
path: /home/gpav/Vrac/LAB/AI/LLM-Routing
path: WORKDIR_PLACEHOLDER
name: env-file
- hostPath:
path: /home/gpav/Vrac/LAB/AI/LLM-Routing/data
path: WORKDIR_PLACEHOLDER/data
type: DirectoryOrCreate
name: dataset-data
2 changes: 1 addition & 1 deletion router/agy_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ async def save(self) -> None:

logger = logging.getLogger("agy-proxy")

# In container: mounted from host /home/gpav/.local/bin/agy
# In container: mounted from host ~/.local/bin/agy
AGY_BINARY = os.environ.get("AGY_BINARY_PATH", "/usr/local/bin/agy")
if not os.path.exists(AGY_BINARY):
AGY_BINARY = os.path.expanduser("~/.local/bin/agy")
Expand Down
23 changes: 15 additions & 8 deletions router/free_models_roster.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@
"score": 28.0,
"context_length": 256000
},
{
"id": "tencent/hy3:free",
"name": "Tencent: Hy3 (free)",
"score": 25.0,
"context_length": 262144
},
{
"id": "poolside/laguna-xs-2.1:free",
"name": "Poolside: Laguna XS 2.1 (free)",
"score": 25.0,
"context_length": 262144
},
{
"id": "cohere/north-mini-code:free",
"name": "Cohere: North Mini Code (free)",
Expand All @@ -54,12 +66,7 @@
"score": 25.0,
"context_length": 128000
},
{
"id": "openrouter/owl-alpha",
"name": "Owl Alpha",
"score": 25.0,
"context_length": 1048756
},

{
"id": "google/lyria-3-pro-preview",
"name": "Google: Lyria 3 Pro Preview",
Expand Down Expand Up @@ -139,6 +146,6 @@
"context_length": 32768
}
],
"updated_at": "2026-06-24T18:40:15.482014Z",
"count": 23
"updated_at": "2026-07-08T22:55:47.271165Z",
"count": 24
}
Loading