feat(demos): ai-gateway demo with tiered token budgets and span-derived dashboards - #23
Merged
Merged
Conversation
Ladas
force-pushed
the
feat/ai-gateway-demo
branch
15 times, most recently
from
September 8, 2026 12:05
73e8cb0 to
307588c
Compare
Adds demos/ai-gateway: the Praxis experimental gateway on its own KIND
cluster, in front of a real local model (Ollama), enforcing per-tier token
budgets, with dashboards built from both Prometheus counters and Tempo span
metrics. Ports are offset from otel-benchmark so both demos run at once.
Enables the token-rate-limit-filter cargo feature on the experimental
server. This image exists to expose feature-gated upstream work and
previously enabled only one of the four experimental AI filters.
The dashboards are built around a distinction that is easy to get wrong.
Prometheus counters are incremented on every request and are unaffected by
trace sampling; span metrics are derived only from sampled traces. So
Prometheus owns counts, rates and budget state, while span metrics own
latency distributions and the per-filter breakdown. The weighted-cost panel
takes its quantile from span metrics and its request rate from
praxis_http_requests_total -- multiplying two sampled series would
under-report by exactly the sampling factor while looking plausible. A
cross-check panel divides one plane by the other, which should approximate
the configured sampling rate; a drop means spans are being lost rather than
sampled.
Documents four things that are easy to get wrong and produce
plausible-but-wrong results:
- token_rate_limit must be declared before token_count. Response hooks run
in reverse declared order, and it reconciles against the token.total
metadata token_count writes. Declared after, it charges every request at
its flat estimate and the metrics look reasonable.
- Ollama stays on its default loopback binding. Containers reach it via
host.docker.internal, which the container runtime proxies from the host
side, so OLLAMA_HOST=0.0.0.0 needlessly exposes the model server.
- The rate-limit demo uses a small model deliberately. The limiter decides
at admission, before the upstream call, but the burst must land inside
the sliding window. A 27B model takes 74-78s per request under
concurrency, so the budget would age out faster than it was consumed and
the limit would never be reached.
- reserved_tokens is a flat estimate well above real usage, so the tier
throttles at roughly a third of its nominal budget. That is the current
upstream milestone's design, pending configurable estimation (ai#121).
All output in the README was captured from real runs.
Signed-off-by: Ladislav Smola <lsmola@redhat.com>
Ladas
force-pushed
the
feat/ai-gateway-demo
branch
from
September 9, 2026 07:29
307588c to
aef9cc7
Compare
Ladas
marked this pull request as ready for review
September 9, 2026 08:37
Contributor
Author
|
merging this demo, I have followup with podman and tweked quickstart that need image built with this rate limitng feature enabled |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
demos/ai-gateway: the Praxis experimental gateway on its own KINDcluster, in front of a real local model (Ollama), enforcing per-tier token
budgets, with Grafana dashboards built from Prometheus counters and Tempo span
metrics. Also enables the
token-rate-limit-filtercargo feature on theexperimental server — this image exists to expose feature-gated upstream work,
and previously enabled only one of the four experimental AI filters.
What it demonstrates
Token budgets that a real workload actually reaches. Five rules in
configs/token-budget.yaml, evaluated in order, first match wins:premiumX-Tier: premiumagent-daily×3X-Model:one ofqwen3.8:27b,qwen3-coder:30b,deepseek-r1:32bfreescripts/rate-limit-demo.shsends 100 requests per tier and reproduces53 admitted / 47 deniedon free against100 / 0on premium — same load, samemodel, same moment. Denials are decided at admission, before the upstream call,
so a rejected request costs no GPU time.
A daily budget is the same filter with a longer window; that is all a "total
budget" is here.
windowacceptsms/s/m/h, so a day is"24h"and aweek
"168h"—"7d"is rejected and praxis keeps the previous config.Tiering without client cooperation.
model_to_headerpromotes the requestbody's
modelintoX-Model, and rules match on it, so Codex, opencode andClaude Code land on the right budget without sending a custom header — none of
them let you.
Reservations sized from measurement. A bare
curlcosts ~406 tokens; onetrivial Codex turn costs ~9,400, because system prompts and tool schemas
dominate. At
reserved_tokens: 1500that recorded 7,971 tokens of overage overfour turns; at 10,000 a turn settles as
estimated 10000 / actual 9435 / refunded 565 / overage 0.Dashboards
Four dashboards, 31 panels, every one carrying an
itooltip explainingwhat it plots, which telemetry plane it comes from, and what a wrong value looks
like.
They are built around a distinction that is easy to get wrong: Prometheus
counters cover 100% of requests and are sampling-independent; span metrics come
only from sampled traces. Prometheus therefore owns counts, rates and budget
state, while span metrics own latency distributions and the per-filter
breakdown. The Weighted Filter Cost panel takes its quantile from spans and its
rate from
praxis_http_requests_totalfor exactly this reason — multiplying twosampled series would under-report by the sampling factor while looking
plausible. A Sampling Ratio Cross-Check panel divides one plane by the other; a
drop means spans are being lost rather than sampled.
Traps the demo is built to avoid
token_rate_limitmust be declared beforetoken_count. Response hooksrun in reverse declared order, and it reconciles against the
token.totalmetadata
token_countwrites. Declared after, it charges every request theflat estimate and the metrics look reasonable but are wrong — the signature is
actual == estimatedwith zero refunds.host.docker.internal, which the runtime proxies from the host side.OLLAMA_HOST=0.0.0.0is unnecessary and publishes the model server to thelocal network.
admission, so the model is irrelevant to what is shown, but 100 requests must
land inside the 1m window.
qwen3.8:27btakes 74-78s under concurrency, sothe budget would age out faster than it was consumed.
Caveats
token_rate_limitis experimental, behind a cargo feature; its parentproposal is not accepted (ai#796) and the config surface may change.
it, so a real deployment needs an auth filter to set the tier header and strip
client copies (grid#101).
allow_public_adminandallow_private_endpointsare demo-only: the firstexposes
/api/log-leveland/api/kvto the cluster, the second disables SSRFhardening so the gateway can reach the host. Both are called out in the README.
memorybackend is per-process, and a sliding window retains one entry perrequest for the length of the window;
backend.kind: valkeymoves that stateout of process.
Test plan
praxis-forge upcreatesai-gw-local; all five stacks deployhost.docker.internalqwen3.8:27banswers through the gatewayagent-daily, confirmed by the admitted countercargo check --locked,make lint, shellcheck, taplo, markdownlint cleanDepends on praxis-proxy/forge#16 (
extraPortMappings), merged.page@6aa31177458395a3dcd779d1c07cc299.webm