The per-pod semaphore was not a cap: 3 replicas x 4 slots advertised 12 concurrent requests while the engine runs 4, and nothing coordinated the replicas. Admission is now gated on the engine's own published gauges, which is global by construction because every replica contends for the same engine. - new internal/proxy/engine_state.go: 2s scrape of the backend /metrics, hand-rolled exposition parse (no new dependency), unhealthy after 3 consecutive failed polls, and a startup self-check that raises observer_degraded rather than falling back to fail-open silently - gate: park while kv_cache_usage_perc >= 0.90 or running + waiting >= ADMISSION_MAX_INFLIGHT, bounded by ADMISSION_MAX_PARK_SECONDS, then 503 + Retry-After: 5. Unknown state fails open on purpose - streaming requests receive SSE keep-alive heartbeats while parked - router: extract only the top-level model field instead of unmarshalling the whole body into a map; body cap from MAX_BODY_BYTES (1 MiB, the model path previously inherited the 100 MB default) - GOMAXPROCS from the cgroup CPU quota: the container is limited to 500m while Go defaulted to the host core count and was CFS-throttled - VLLM_MAX_QUEUE_DEPTH default 20 -> 8 - delete k8s/configmap.yaml: absent from kustomization.yaml, and the live ConfigMap of that name carries an unrelated key - correct the llm-client NetworkPolicy comment; llm-serving-ingress now selects by namespace, not by that label Observers run only for models named in ADMISSION_OBSERVE_MODELS. reasoning is the only vLLM engine here; ornith and qwen-cpu are Ollama and embeddings/rerank are TEI, none of which publish vllm:* series, so observing them would hold observer_degraded at 1 permanently. Companion change (engine flags + dashboard panels) is riotpiao-poimen/homelab#51.
homelab-frontend
Production API gateway for the homelab cluster. Single entry point (api.riotpiao.com) for all services: LLM inference, workflows, queues, memory, and cluster operations.
Status: Live in production. Replaced Kong OSS entirely.
Quick Links
- API Reference: See API.md — how to call every service
- Base URL:
https://api.riotpiao.com - Source:
ssh://git.riotpiao.com:2222/rock/homelab-frontend.git
Architecture
┌─────────────┬──────────────┬────────────┐
│ Browser │ SDK │ CLI │
└──────┬──────┴──────┬───────┴────┬───────┘
│ │ │
└─────────────┼────────────┘
│
HTTPS/TLS
│
┌─────────────┼────────────┐
│ Cloudflare Edge │
│ (DDoS, caching) │
└──────────┬──────────────┘
│
ingress-nginx
(SSL termination)
│
┌─────────────────────────────┐
│ homelab-frontend Gateway │
│ (routing, auth, limits) │
└──────┬───────────────────────┘
│
┌──────┴───────────────────────────────────┐
│ │
/v1/* X-Service header routing /
(LLM) (workflow, sqs, s3, iam, memory) /
│ │ │
▼ ▼ ▼
llm-serving temporal:7233 kmsvc/Kafka, MinIO,
(vLLM, Ollama) (gRPC) Authentik, poimen-memory
(TEI)
Design principles:
- ✅ Single hostname, unified X-Service + X-Resource header routing
- ✅ HTTP REST gateway → gRPC Temporal bridge (via X-Service: workflow)
- ✅ Bearer token auth via Authentik (JWT + RBAC)
- ✅ Streaming unbuffered (SSE, WebSocket, HTTP/2 multiplexing)
- ✅ Per-route timeouts & rate limits
- ✅ No cluster credentials held by gateway
Services & Capabilities
| Service | Method | Upstream | Status |
|---|---|---|---|
| LLM Chat | POST /v1/chat/completions |
llm-serving (vLLM) | ✅ Live |
| Embeddings | POST /v1/embeddings |
llm-serving (TEI) | ✅ Live |
| Reranking | POST /v1/rerank |
llm-serving (TEI) | ✅ Live |
| Workflows | X-Service: workflow + X-Resource: {action} |
Temporal gRPC (7233) | ✅ Live (START, DESCRIBE, SIGNAL, QUERY, etc) |
| Queues | X-Service: sqs + X-Resource: {action} |
kmsvc/Kafka | ✅ Live |
| Memory | X-Service: memory + X-Resource: {action} |
poimen-memory | ✅ Live |
| IAM | X-Service: iam + X-Resource: {action} |
Authentik API | ✅ Live |
| S3 | X-Service: s3 + X-Resource: {action} |
MinIO | ✅ Live |
How to Use
1. Get a token
Human (OIDC device code):
core auth login
export TOKEN=$(cat ~/.cache/talos/authentik_id_token)
Service account (client credentials):
core mwinit login --username sa-name --password secret
export TOKEN=$(cat ~/.talos/.riotpiao-auth)
2. Call any service
Chat:
curl -X POST https://api.riotpiao.com/v1/chat/completions \
-H "Authorization: Bearer $TOKEN" \
-d '{
"model": "reasoning",
"messages": [{"role": "user", "content": "What is 2+2?"}]
}'
Workflow (via X-Service header):
curl -X POST https://api.riotpiao.com/ \
-H "Authorization: Bearer $TOKEN" \
-H "X-Service: workflow" \
-H "X-Resource: start" \
-d '{
"namespace": "default",
"workflow_id": "my-workflow",
"workflow_type": "MyWorkflow",
"task_queue": "default"
}'
Memory:
curl -X GET https://api.riotpiao.com/ \
-H "Authorization: Bearer $TOKEN" \
-H 'X-Service: memory' \
-H 'X-Resource: query' \
-G --data-urlencode 'query=explain machine learning'
Full examples: See API.md
Available Models
LLM (Chat & Reasoning)
reasoning— DeepSeek-R1-Distill-Qwen-32B (8 concurrent slots)ornith:35b— Ollama 35Bqwen2.5:3b-instruct— Qwen 2.5 3B
Embeddings
nomic-ai/nomic-embed-text-v2-moe— Fast, multilingual
Reranking
BAAI/bge-reranker-base— Document relevance scoring
Authentication
All endpoints (except /healthz, /readyz) require:
Authorization: Bearer <jwt-token>
Tokens validated via Authentik JWKS (runtime fetched, cached, auto-rotated).
Capabilities (RBAC):
llm:inference—/v1/*chat/embeddings/rerankworkflow:execute—/workflowoperationsmemory:read/memory:write— Memory operationssqs:access— Queue operationss3:access— S3 operationsiam:admin— User/group management
Error Handling
All errors return RFC 9457 application/problem+json:
{
"type": "https://api.example.com/problems/unknown-model",
"title": "Unknown Model",
"status": 400,
"detail": "Model 'gpt-4' is not available",
"valid_models": ["reasoning", "ornith:35b", ...]
}
Common status codes:
- 200 OK
- 400 Bad Request (validation, unknown model)
- 401 Unauthorized (missing/invalid token)
- 403 Forbidden (insufficient capability)
- 404 Not Found (workflow, resource)
- 429 Too Many Requests (rate limit)
- 503 Service Unavailable (backend down)
Rate Limits
| Endpoint | Limit | Retry-After |
|---|---|---|
/v1/chat/completions |
8 concurrent | Yes |
/v1/embeddings |
10 concurrent | Yes |
/v1/rerank |
10 concurrent | Yes |
/workflow |
100 concurrent | Yes |
Hitting limit returns 429 with Retry-After header.
Timeouts
| Endpoint | Connect | Read | Write |
|---|---|---|---|
/v1/chat |
10s | 1h | 1h |
/v1/embeddings |
10s | 10m | 10m |
/v1/rerank |
10s | 10m | 10m |
/workflow |
10s | 30s | 10s |
Client disconnects cancel upstream request immediately (no orphaned slots).
Local Development
Run without cluster, no credentials needed:
# Build
go build ./cmd/gateway
# Run locally
./gateway
# Test in another terminal
curl http://localhost:8080/healthz
Points upstreams at local stubs if not connected to cluster (see internal/config).
Code Layout
cmd/gateway/ Server entrypoint
internal/
server/ Router, health checks
proxy/ Reverse proxy, streaming, timeouts
temporal/ Workflow handler + gRPC bridge
serviceadapter/ X-Service dispatcher (CRD-driven)
config/ Route + upstream configuration
auth/ Authentik JWT validation
observability/ Metrics, structured logging
k8s/
configmap.yaml Route definitions
rbac.yaml Service account, roles
deployment.yaml Pod spec
networkpolicy.yaml Ingress/egress rules
testdata/ Fixtures for offline tests
Deployment
Deployed to Kubernetes via ArgoCD:
# Check deployment
kubectl -n api get deployment homelab-frontend
# View logs
kubectl -n api logs -l app=homelab-frontend -f
# Restart
kubectl -n api rollout restart deployment/homelab-frontend
Configuration mounted as ConfigMap (k8s/configmap.yaml).
Health Checks
# Liveness (always succeeds)
curl https://api.riotpiao.com/healthz
# Readiness (waits for config + JWKS)
curl https://api.riotpiao.com/readyz
# List available models
curl https://api.riotpiao.com/v1/models
Support
Issues: Check pod logs
kubectl -n api logs deployment/homelab-frontend
Debug config:
kubectl -n api get configmap homelab-frontend-config -o yaml
Restart pod:
kubectl -n api rollout restart deployment/homelab-frontend
Test endpoint directly:
kubectl -n api port-forward svc/homelab-frontend 8080:8080
curl http://localhost:8080/healthz
See Also
- API.md — Complete API reference with examples
internal/— Source code (handlers, routing, auth)k8s/— Kubernetes manifests