Lab 09 — AI Gateway: Auth, Rate Limits, Budgets, PII Redaction, Tracing, Cost — AI Infrastructure for Real Business Problems
Last updated 10/17/2026
Course › Hands-on track › Lab 09

Lab 09 — AI Gateway: Auth, Rate Limits, Budgets, PII Redaction, Tracing, Cost

AI閘道與可觀測性 · Hands-on track: AI Infrastructure for Real Business Problems

Code: labs/09/ · Every earlier lab can point at it with LLM_PROVIDER=compat

Business Problem — 商業問題

After Labs 03–07 the store has five services and n8n all calling models directly, each with its own API key in its own .env. Three months in, the owner asks:

Nobody can answer.

The fix is one internal AI gateway: every app calls it with its own key, and the gateway handles in one place:

It's OpenAI-compatible, so apps change one URL and one key.

Architecture — 系統架構

flowchart LR
    A1[listing-api] & A2[rag-api] & A3[agent-api] & A4[copilot] & A5[n8n / ai-services] -->|Bearer client-key<br/>X-Feature| GW
    subgraph GW["Gateway :8009 (/v1/chat/completions, /v1/embeddings, /v1/usage)"]
        AU[auth: sha256 key → client] --> RL[token-bucket rate limit per client]
        RL --> BU[daily budget check<br/>USD + tokens]
        BU --> RD[PII → placeholders EMAIL_1, PHONE_1, CARD_1, ADDRESS_1<br/>when upstream is cloud]
        RD --> UP[llmkit → upstream]
        UP --> RS[restore tokens in reply<br/>cards only as last 4]
        UP --> TR[trace JSONL / Langfuse]
        UP --> US[(usage SQLite<br/>tokens, latency, cost)]
    end
    UP --> M[(Ollama / cloud API)]
    US --> REP[report.py]

How each part works:

Prerequisites — 先決條件

Step-by-Step Setup — 逐步設定

cd hands-on/labs/09
uv venv --python 3.12 && source .venv/bin/activate
uv pip install -e ../common -r requirements.txt
cp .env.example .env              # LLM_* = the upstream the gateway calls
uvicorn gateway.main:app --port 8009

Call it like any OpenAI endpoint:

curl -s localhost:8009/v1/chat/completions \
  -H 'authorization: Bearer change-me-n8n-key' -H 'x-feature: review-reply' -H 'content-type: application/json' \
  -d '{"messages":[{"role":"user","content":"Write a 1-line thank-you to chloe.lin@example.com"}]}' | python -m json.tool

The response has an x_gateway block with request_id, client, cost_usd and the list of redacted placeholders. From Python:

from openai import OpenAI
c = OpenAI(base_url="http://localhost:8009/v1", api_key="change-me-n8n-key", default_headers={"X-Feature": "listing-gen"})

Point any lab at it:

LLM_PROVIDER=compat LLM_BASE_URL=http://localhost:8009/v1 LLM_API_KEY=change-me-listing-key uvicorn app.main:app --port 8003

See what happened:

python report.py --days 7          # day × client × feature × model: calls, tokens, cost (or "unpriced")
tail -n 3 var/traces.jsonl         # what the model actually saw (redacted when redaction is on)

Production (Docker secret):

mkdir -p secrets
printf 'n8n:%s,listing-svc:%s' "$(openssl rand -hex 24)" "$(openssl rand -hex 24)" > secrets/gateway_keys
docker compose up -d --build

How to Test — 測試方法

LLM_PROVIDER=mock pytest -q        # 9 tests

The tests cover:

What was run on the build box — 實測紀錄

Cost & Performance Notes — 成本與效能

Common Failures & Fixes — 常見錯誤與修正

Symptom Cause Fix
401 for every app GATEWAY_KEYS format name:key,name2:key2, no spaces/quotes; or the secret file has a trailing newline (it's stripped)
429 storms after a batch job per-client bucket too small raise RATE_LIMIT_PER_MIN for that client's workload or slow the job (LISTING_MAX_WORKERS)
429 "daily budget" budget reached intended. Check report.py for the cause before raising it
cost_usd: null model not in pricing.yaml add it with today's price and checked_on
Model replies "" literally it's a placeholder intended when redaction is on; the gateway restores it before returning
Traces contain raw PII redaction off (local upstream with auto) set REDACT_PII=always, restrict var/ permissions, rotate traces (30-day retention per privacy.md)
Langfuse shows nothing wrong host/keys, or the process exited before flush check LANGFUSE_HOST; the gateway never fails because of tracing

Stretch Exercise — 延伸練習

  1. Add per-client budgets (CLIENT_BUDGETS="listing:500000,rag:2000000" tokens/day) and a test.
  2. Add a response cache for identical, temperature-0 requests (hash of model + messages). Measure the hit rate on a replay of Lab 06 events.

Deliverable Checklist — 交付清單

↑