Skip to content

Quickstart — Meridian in scenarios

Each scenario below is copy-paste runnable and states what you should see. Together they are the 0.9.4 manual-validation recipe — run A–F against the mock stack before tagging; G needs a GPU/Ollama and is optional.

You need: Docker + Docker Compose, free ports 8080, 9001, 9002. Scenarios C–F also use jq (or swap in python3 -m json.tool).

# Scenario Config GPU
A Non-stream chat mock_demo.yaml (default) no
B Streaming SSE mock_demo.yaml no
C Auth + RBAC roles rbac_demo.yaml no
D Budget 429 rbac_demo.yaml no
E Cost / usage export rbac_demo.yaml no
F Telemetry & tiering headers mock_demo.yaml no
G Real backend (Ollama) local_gpu.yaml yes
Phase 3 flagship: isolation, key lifecycle API, canary auto-rollback phase3_demo.yaml no

New here? Skip the table and run the flagship: scripts/demo_phase3.sh brings up its own stack (port 8181) and walks every headline feature with expected output inline.


Start the mock stack (Scenarios A, B, F)

git clone https://github.com/IMV-IN/Meridian.git
cd Meridian
docker compose up --build
Service URL
Meridian gateway http://localhost:8080
Mock backend fast (~50 ms) http://localhost:9001
Mock backend slow (~300 ms) http://localhost:9002

No Kafka, no audit consumer, no auth on the default config (configs/mock_demo.yaml). Wait until the gateway logs it is up (~30s after build).


Scenario A — non-stream chat

curl -i http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"demo-model","messages":[{"role":"user","content":"Hello!"}]}'

Expect: HTTP 200 + JSON reply. Response headers include:

Header Meaning
x-request-id Meridian request id (starts with mrdn-)
x-meridian-backend Which mock served it (fast or slow)

Scenario B — streaming

curl -N http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"demo-model","messages":[{"role":"user","content":"Count to 5"}],"stream":true}'

Expect: a sequence of data: {...} SSE lines ending with data: [DONE].

Dashboard (any time)

Open http://localhost:8080/ui. You get:

  • a stats bar — recent request count, error rate, p50/p95 latency, backends up
  • a backend card per backend with health, inflight, EWMA, and telemetry bars
  • a recent-requests table with org / team / tier columns
  • an API-key control (top right) — needed once auth is on (Scenario C)

Scenario C — auth + RBAC roles

Stop the default stack (Ctrl-C) and bring up the gateway with the RBAC config mounted over the default. The two mock backends still run from compose.

docker compose up --build -d backend-fast backend-slow
docker compose run --rm --service-ports \
  -v "$(pwd)/configs/rbac_demo.yaml:/app/config.yaml:ro" \
  gateway

configs/rbac_demo.yaml defines five demo keys (documented in the file):

Key Role May do
mrdn_viewerviewerviewerviewer viewer read dashboard/status/requests
mrdn_operatoroperatoroperator operator viewer + POST /meridian/reload
mrdn_adminadminadminadminadmin admin everything, incl. cross-org usage
mrdn_financefinancefinance cost_admin bool read all-org usage (no role)
mrdn_appkeyappkeyappkeyappkey none /v1/* only, no ops access

C1 — chat needs a key now:

# No key -> 401
curl -s -o /dev/null -w "%{http_code}\n" \
  http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"demo-model","messages":[{"role":"user","content":"hi"}]}'
# -> 401

# App key -> 200
curl -s -o /dev/null -w "%{http_code}\n" \
  http://localhost:8080/v1/chat/completions \
  -H "Authorization: Bearer mrdn_appkeyappkeyappkeyappkey" \
  -H "Content-Type: application/json" \
  -d '{"model":"demo-model","messages":[{"role":"user","content":"hi"}]}'
# -> 200

C2 — ops endpoints enforce view rights:

# No key -> 401
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/meridian/status
# -> 401

# Plain app key has no view right -> 403
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/meridian/status \
  -H "Authorization: Bearer mrdn_appkeyappkeyappkeyappkey"
# -> 403

# Viewer key -> 200
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/meridian/status \
  -H "Authorization: Bearer mrdn_viewerviewerviewerviewer"
# -> 200

C3 — reload requires operator/admin:

# Viewer cannot reload -> 403
curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:8080/meridian/reload \
  -H "Authorization: Bearer mrdn_viewerviewerviewerviewer"
# -> 403

# Operator can -> 200 {"reloaded": true, ...}
curl -s -X POST http://localhost:8080/meridian/reload \
  -H "Authorization: Bearer mrdn_operatoroperatoroperator"
# -> {"reloaded": true, "keys": 5}

C4 — dashboard with auth on: open http://localhost:8080/ui, click Set API key, paste mrdn_viewerviewerviewerviewer. Cards and the request table populate. Clearing the key shows the "Authentication required" banner.

Scenario D — budget 429

rbac_demo.yaml caps org acme at 5 requests/day. Fire six with the app key:

for i in $(seq 1 6); do
  curl -s -o /dev/null -w "req $i -> %{http_code}\n" \
    http://localhost:8080/v1/chat/completions \
    -H "Authorization: Bearer mrdn_appkeyappkeyappkeyappkey" \
    -H "Content-Type: application/json" \
    -d '{"model":"demo-model","messages":[{"role":"user","content":"hi"}]}'
done

Expect: first 5 → 200, the 6th → 429 (with a Retry-After header and "type": "rate_limit_exceeded" in the body).

Scenario E — cost / usage export

# Own-org view with the app key (scoped to acme automatically):
curl -s http://localhost:8080/meridian/usage \
  -H "Authorization: Bearer mrdn_appkeyappkeyappkeyappkey" | jq

# Cross-org view: admin or finance key can pass ?org=
curl -s "http://localhost:8080/meridian/usage?org=acme" \
  -H "Authorization: Bearer mrdn_adminadminadminadminadmin" | jq

# A non-admin asking for another org -> 403
curl -s -o /dev/null -w "%{http_code}\n" "http://localhost:8080/meridian/usage?org=finance" \
  -H "Authorization: Bearer mrdn_appkeyappkeyappkeyappkey"
# -> 403

Expect: JSON rows with org_id, model, token counts and cost; CSV via /meridian/usage.csv. The cross-org request from a plain key is refused.

Scenario F — telemetry & tiering headers

Against the default mock stack, inspect the routing headers:

curl -sD - -o /dev/null http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"demo-model","messages":[{"role":"user","content":"hi"}],"max_tokens":2048}' \
  | grep -i x-meridian

Expect: x-meridian-backend, and — when tiering/affinity are enabled in the config — x-meridian-tier / x-meridian-session-route. Backend telemetry (queue depth, GPU mem, tok/s), when a backend exposes it, shows as bars on /ui. See CONFIGURATION.md to enable tiering and telemetry.

Scenario G — real backend (Ollama, GPU)

ollama pull qwen2.5:0.5b && ollama serve
export MERIDIAN_CONFIG=configs/local_gpu.yaml
uvicorn meridian.api.main:app --host 0.0.0.0 --port 8080

Then repeat Scenario A/B against port 8080. Load / overhead numbers: LOAD.md.


Smoke script

pip install -e ".[dev]"   # once
python scripts/smoke_test.py --url http://localhost:8080 --model demo-model

Stop

docker compose down

Next steps

Want Read
Production deploy DEPLOY.md · OPS_RUNBOOK.md
Auth / budgets / cost / roles CONFIGURATION.md, configs/enterprise_example.yaml
Docs map index.md

Env var: always MERIDIAN_CONFIG (uppercase), e.g. MERIDIAN_CONFIG=configs/local_gpu.yaml uvicorn meridian.api.main:app --port 8080