docs: add latency benchmark vs LiteLLM/Portkey
- bench/ reproducible harness: instant mock backend + Apache Bench (ab -k -r) - measures gateway overhead in passthrough mode (apples-to-apples) - BENCHMARKS.md: methodology, caveats, results (cortiq ~57k rps p99 1ms vs Portkey ~5.8k p99 9ms vs LiteLLM 4-workers ~1.2k p99 59ms) - README (EN+RU): Performance section + highlight Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
e015555dff
commit
a5966dd479
@@ -0,0 +1,80 @@
|
|||||||
|
# Gateway latency benchmark
|
||||||
|
|
||||||
|
How much latency does the **gateway itself** add? For an LLM proxy the model call
|
||||||
|
dominates wall-clock time, but the gateway's own overhead is what differs between
|
||||||
|
products — and it compounds across the many calls an agent makes. This benchmark
|
||||||
|
isolates that overhead and compares Cortiq Gateway with **LiteLLM** and **Portkey**.
|
||||||
|
|
||||||
|
> Reproduce on your own hardware: `bash bench/run.sh` (see [Methodology](#methodology)).
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
Every gateway proxies the **same instant mock backend** in passthrough mode, under
|
||||||
|
identical load (`ab -k -r -c 20 -n 5000`). Latency is in milliseconds.
|
||||||
|
|
||||||
|
| Gateway | Throughput (req/s) | p50 | p90 | p99 | mean |
|
||||||
|
|---|--:|--:|--:|--:|--:|
|
||||||
|
| **Cortiq Gateway** (Rust) | **~57,300** | 0 ms | 1 ms | **1 ms** | 0.35 ms |
|
||||||
|
| Portkey Gateway (Node) | ~5,800 | 3 ms | 5 ms | 9 ms | 3.5 ms |
|
||||||
|
| LiteLLM (Python, 4 workers) | ~1,200 | 9 ms | 37 ms | 59 ms | 16.6 ms |
|
||||||
|
| _mock-direct (no gateway)_ | _~29,000_ | _1 ms_ | _1 ms_ | _3 ms_ | _0.7 ms_ |
|
||||||
|
|
||||||
|
On this machine Cortiq adds **sub-millisecond p99** overhead and sustains **~10×**
|
||||||
|
the throughput of Portkey and **~47×** of LiteLLM. The gap is architectural: a Rust
|
||||||
|
/ Tokio gateway uses every core in one process, with no interpreter or GC in the hot
|
||||||
|
path.
|
||||||
|
|
||||||
|
> Absolute numbers depend on hardware; the **ratios** between gateways are the point.
|
||||||
|
> Run `bench/run.sh` to get numbers for your environment.
|
||||||
|
|
||||||
|
## Why this matters
|
||||||
|
|
||||||
|
Agentic workloads make **many** gateway calls per task (plan → sub-tasks → tool calls
|
||||||
|
→ synthesis). Overhead that looks small per call (tens of ms) multiplies across a run
|
||||||
|
and caps the throughput a single instance can serve. Lower, more predictable tail
|
||||||
|
latency (p99) is the developer-facing differentiator when the model time is equal.
|
||||||
|
|
||||||
|
## Methodology
|
||||||
|
|
||||||
|
- **Isolate gateway overhead.** A tiny, dependency-free Node mock ([`bench/mock.js`](bench/mock.js))
|
||||||
|
returns a fixed OpenAI-compatible completion **instantly**. So measured latency is
|
||||||
|
the gateway's own cost, not model time. Every gateway proxies this same mock.
|
||||||
|
- **Apples-to-apples.** Each gateway runs in **passthrough / pinned** mode (no routing
|
||||||
|
or model-selection logic) so we compare raw proxy cost. Cortiq's intelligent routing
|
||||||
|
(`cortiq-auto`) adds one classifier round-trip — that's a feature, measured separately,
|
||||||
|
not folded into this overhead number.
|
||||||
|
- **Load.** Apache Bench, keep-alive, tolerant of length variance:
|
||||||
|
`ab -k -r -c 20 -n 5000 -p bench/body.json -T application/json`.
|
||||||
|
- **Fair to each runtime.** Cortiq = one process (Tokio, multi-core). Portkey = one Node
|
||||||
|
process (event loop). LiteLLM = **4 uvicorn workers** (its recommended multi-process
|
||||||
|
setup; a single Python worker is GIL-bound to one core and scores ~860 req/s).
|
||||||
|
- **Same box, localhost**, warm processes, results are representative of one run.
|
||||||
|
|
||||||
|
### Caveats (read me)
|
||||||
|
|
||||||
|
- This measures **gateway overhead only**. Real requests are dominated by model latency,
|
||||||
|
which is identical regardless of gateway — but overhead and tail latency are not.
|
||||||
|
- Localhost + mock backend removes network/model variance on purpose. Your absolute
|
||||||
|
numbers will differ; reproduce with `bench/run.sh`.
|
||||||
|
- `ab` reports LiteLLM "Failed requests" as a non-zero count — these are **response
|
||||||
|
content-length variations, not HTTP errors** (all responses are `200`, verified).
|
||||||
|
- Tested versions: Cortiq Gateway (this repo, `--release`), LiteLLM proxy via
|
||||||
|
`uvicorn litellm.proxy.proxy_server:app --workers 4`, Portkey `@portkey-ai/gateway`,
|
||||||
|
Apache Bench. Hardware: Apple Silicon, macOS.
|
||||||
|
|
||||||
|
## Reproduce
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# prerequisites: node, ab (Apache Bench), a release build of the gateway
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
|
# optional competitors (the harness auto-detects them):
|
||||||
|
pip install 'litellm[proxy]' uvicorn
|
||||||
|
npm install -g @portkey-ai/gateway
|
||||||
|
|
||||||
|
# run (tune load via env)
|
||||||
|
CONC=20 REQ=5000 bash bench/run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The harness ([`bench/run.sh`](bench/run.sh)) starts the mock, then benchmarks the mock
|
||||||
|
directly and each available gateway, printing a throughput / p50 / p90 / p99 table.
|
||||||
@@ -3,6 +3,13 @@
|
|||||||
All notable changes to this project are documented here.
|
All notable changes to this project are documented here.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Latency benchmark** vs LiteLLM and Portkey: a reproducible harness (`bench/`,
|
||||||
|
instant mock backend + Apache Bench) and `BENCHMARKS.md`. Measured gateway overhead:
|
||||||
|
Cortiq ~57k req/s / p99 1 ms vs Portkey ~5.8k / p99 9 ms vs LiteLLM ~1.2k / p99 59 ms.
|
||||||
|
|
||||||
## [0.2.1] - 2026-06-30
|
## [0.2.1] - 2026-06-30
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ One OpenAI-compatible endpoint → automatic model selection from your pool
|
|||||||
|
|
||||||
## ✨ Highlights
|
## ✨ Highlights
|
||||||
|
|
||||||
|
- **Fast.** Rust/Tokio: **sub-millisecond p99** gateway overhead — ~10× the throughput of Portkey and ~47× of LiteLLM in a like-for-like proxy benchmark ([details](BENCHMARKS.md)).
|
||||||
- **Drop-in OpenAI API + streaming.** Point any OpenAI client at the gateway and send `model: "cortiq-auto"`; `stream: true` is fully supported (SSE).
|
- **Drop-in OpenAI API + streaming.** Point any OpenAI client at the gateway and send `model: "cortiq-auto"`; `stream: true` is fully supported (SSE).
|
||||||
- **Multi-protocol.** OpenAI Chat & Embeddings, **Anthropic Messages** (in/out, streaming), and an **MCP** server (`POST /mcp`) exposing routing as tools for agent orchestrators.
|
- **Multi-protocol.** OpenAI Chat & Embeddings, **Anthropic Messages** (in/out, streaming), and an **MCP** server (`POST /mcp`) exposing routing as tools for agent orchestrators.
|
||||||
- **Intelligent routing.** `complexity.tier` → ordered model pool (`low → local`, `high → cloud`), with fallback and graceful degradation when the router is down.
|
- **Intelligent routing.** `complexity.tier` → ordered model pool (`low → local`, `high → cloud`), with fallback and graceful degradation when the router is down.
|
||||||
@@ -219,6 +220,19 @@ Details — [docs/PROTOCOLS.md](https://github.com/infosave2007/cortiq-gateway/b
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## ⚡ Performance
|
||||||
|
|
||||||
|
Gateway overhead proxying the same instant backend, `ab -k -r -c 20 -n 5000`:
|
||||||
|
|
||||||
|
| Gateway | req/s | p50 | p99 |
|
||||||
|
|---|--:|--:|--:|
|
||||||
|
| **Cortiq Gateway** (Rust) | **~57,300** | 0 ms | **1 ms** |
|
||||||
|
| Portkey (Node) | ~5,800 | 3 ms | 9 ms |
|
||||||
|
| LiteLLM (Python, 4 workers) | ~1,200 | 9 ms | 59 ms |
|
||||||
|
|
||||||
|
Methodology, caveats and a reproducible harness: **[BENCHMARKS.md](BENCHMARKS.md)**
|
||||||
|
(`bash bench/run.sh`). Numbers vary by hardware; the ratios are the point.
|
||||||
|
|
||||||
## Build & test
|
## Build & test
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
+15
-1
@@ -34,7 +34,8 @@
|
|||||||
|
|
||||||
## ✨ Ключевое
|
## ✨ Ключевое
|
||||||
|
|
||||||
- **Drop-in OpenAI API.** Направьте любой OpenAI-клиент на шлюз и шлите `model: "cortiq-auto"`.
|
- **Быстрый.** Rust/Tokio: overhead шлюза с **субмиллисекундным p99** — ~10× к throughput Portkey и ~47× к LiteLLM в сопоставимом бенчмарке ([детали](BENCHMARKS.md)).
|
||||||
|
- **Drop-in OpenAI API + стриминг.** Направьте любой OpenAI-клиент на шлюз и шлите `model: "cortiq-auto"`; `stream: true` поддержан (SSE).
|
||||||
- **Интеллектуальный роутинг.** `complexity.tier` → упорядоченный пул моделей (`low → локально`, `high → облако`), с fallback и мягкой деградацией, если роутер недоступен.
|
- **Интеллектуальный роутинг.** `complexity.tier` → упорядоченный пул моделей (`low → локально`, `high → облако`), с fallback и мягкой деградацией, если роутер недоступен.
|
||||||
- **Встроенная многоязычная панель управления.** Модели, роутинг, протоколы, ключи и секреты — из веб-интерфейса, **без ручного TOML и без рестарта** (горячая перезагрузка). **7 языков** (en, ru, de, fr, es, zh, tr), тёмная/светлая тема.
|
- **Встроенная многоязычная панель управления.** Модели, роутинг, протоколы, ключи и секреты — из веб-интерфейса, **без ручного TOML и без рестарта** (горячая перезагрузка). **7 языков** (en, ru, de, fr, es, zh, tr), тёмная/светлая тема.
|
||||||
- **Живая аналитика.** Статистика по запросам (токены, стоимость, латентность, доля успеха, failover), графики, разбивка по моделям/полосам/задачам и Prometheus `GET /metrics`.
|
- **Живая аналитика.** Статистика по запросам (токены, стоимость, латентность, доля успеха, failover), графики, разбивка по моделям/полосам/задачам и Prometheus `GET /metrics`.
|
||||||
@@ -168,6 +169,19 @@ print(resp.choices[0].message.content)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## ⚡ Производительность
|
||||||
|
|
||||||
|
Overhead шлюза при проксировании одного и того же мгновенного бэкенда, `ab -k -r -c 20 -n 5000`:
|
||||||
|
|
||||||
|
| Шлюз | req/s | p50 | p99 |
|
||||||
|
|---|--:|--:|--:|
|
||||||
|
| **Cortiq Gateway** (Rust) | **~57 300** | 0 ms | **1 ms** |
|
||||||
|
| Portkey (Node) | ~5 800 | 3 ms | 9 ms |
|
||||||
|
| LiteLLM (Python, 4 воркера) | ~1 200 | 9 ms | 59 ms |
|
||||||
|
|
||||||
|
Методология, оговорки и воспроизводимый harness — **[BENCHMARKS.md](BENCHMARKS.md)**
|
||||||
|
(`bash bench/run.sh`). Абсолютные числа зависят от железа; важны соотношения.
|
||||||
|
|
||||||
## Сборка и тесты
|
## Сборка и тесты
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{"model":"bench","messages":[{"role":"user","content":"ping"}],"max_tokens":1}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Benchmark config: one model pinned to the mock upstream. Requests use
|
||||||
|
# model="bench" (a real id) → direct passthrough, no router call — so we measure
|
||||||
|
# pure proxy overhead, comparable to LiteLLM/Portkey passthrough.
|
||||||
|
listen = "0.0.0.0:9100"
|
||||||
|
|
||||||
|
[router]
|
||||||
|
url = "http://127.0.0.1:8100"
|
||||||
|
|
||||||
|
[[models]]
|
||||||
|
id = "bench"
|
||||||
|
provider = "openai"
|
||||||
|
base_url = "http://127.0.0.1:8100/v1"
|
||||||
|
model = "bench-model"
|
||||||
|
|
||||||
|
[routing]
|
||||||
|
default = "bench"
|
||||||
|
|
||||||
|
[protocols]
|
||||||
|
openai_chat = true
|
||||||
|
|
||||||
|
[admin]
|
||||||
|
enabled = false
|
||||||
|
|
||||||
|
[stats]
|
||||||
|
enabled = true
|
||||||
|
file = ""
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// Fast, dependency-free mock upstream for gateway benchmarks.
|
||||||
|
// Responds instantly with a fixed OpenAI-compatible chat completion so the
|
||||||
|
// measured latency reflects GATEWAY overhead, not model time.
|
||||||
|
//
|
||||||
|
// node bench/mock.js [port] # default 8100
|
||||||
|
const http = require("http");
|
||||||
|
const PORT = parseInt(process.argv[2] || "8100", 10);
|
||||||
|
|
||||||
|
const COMPLETION = JSON.stringify({
|
||||||
|
id: "chatcmpl-bench",
|
||||||
|
object: "chat.completion",
|
||||||
|
model: "bench-model",
|
||||||
|
choices: [
|
||||||
|
{ index: 0, message: { role: "assistant", content: "ok" }, finish_reason: "stop" },
|
||||||
|
],
|
||||||
|
usage: { prompt_tokens: 8, completion_tokens: 1, total_tokens: 9 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const ROUTE = JSON.stringify({
|
||||||
|
request_id: "bench",
|
||||||
|
decision: { task_label: "general", complexity: { score: 0.2, tier: "low" } },
|
||||||
|
});
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
if (req.method === "GET") {
|
||||||
|
res.writeHead(200, { "content-type": "text/plain" });
|
||||||
|
res.end("ok");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// drain the request body, then reply
|
||||||
|
req.on("data", () => {});
|
||||||
|
req.on("end", () => {
|
||||||
|
const body = req.url.includes("/route") ? ROUTE : COMPLETION;
|
||||||
|
res.writeHead(200, { "content-type": "application/json" });
|
||||||
|
res.end(body);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.keepAliveTimeout = 60000;
|
||||||
|
server.listen(PORT, "127.0.0.1", () => console.log(`mock upstream on :${PORT}`));
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Gateway latency benchmark: measures pure proxy overhead by pointing every
|
||||||
|
# gateway at the same instant mock upstream and load-testing with Apache Bench.
|
||||||
|
#
|
||||||
|
# CONC=20 REQ=10000 bash bench/run.sh
|
||||||
|
#
|
||||||
|
# Compares (when installed): mock-direct (baseline), cortiq-gateway, LiteLLM, Portkey.
|
||||||
|
set -u
|
||||||
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
ROOT="$(cd "$HERE/.." && pwd)"
|
||||||
|
CONC="${CONC:-20}"
|
||||||
|
REQ="${REQ:-10000}"
|
||||||
|
BODY="$HERE/body.json"
|
||||||
|
MOCK_PORT=8100
|
||||||
|
pids=()
|
||||||
|
cleanup() { for p in "${pids[@]:-}"; do kill "$p" 2>/dev/null; done; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
ab_run() { # name url [extra ab -H args...]
|
||||||
|
local name="$1" url="$2"; shift 2
|
||||||
|
local out rps mean p50 p90 p99
|
||||||
|
out=$(ab -k -c "$CONC" -n "$REQ" -p "$BODY" -T application/json -s 60 "$@" "$url" 2>/dev/null)
|
||||||
|
rps=$(echo "$out" | awk '/Requests per second/{print $4}')
|
||||||
|
mean=$(echo "$out" | awk '/Time per request/{print $4; exit}')
|
||||||
|
p50=$(echo "$out" | awk '/ 50%/{print $2}')
|
||||||
|
p90=$(echo "$out" | awk '/ 90%/{print $2}')
|
||||||
|
p99=$(echo "$out" | awk '/ 99%/{print $2}')
|
||||||
|
printf "%-16s | %9s | %8s | %6s | %6s | %6s\n" \
|
||||||
|
"$name" "${rps:-NA}" "${mean:-NA}" "${p50:-NA}" "${p90:-NA}" "${p99:-NA}"
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_up() { for _ in $(seq 1 150); do curl -s -o /dev/null "$1" && return 0; sleep 0.2; done; return 1; }
|
||||||
|
|
||||||
|
echo "concurrency=$CONC requests=$REQ (latency in ms; ab -k)"
|
||||||
|
printf "%-16s | %9s | %8s | %6s | %6s | %6s\n" "gateway" "rps" "mean" "p50" "p90" "p99"
|
||||||
|
printf -- "-----------------+-----------+----------+--------+--------+--------\n"
|
||||||
|
|
||||||
|
# --- mock baseline -----------------------------------------------------------
|
||||||
|
node "$HERE/mock.js" $MOCK_PORT >/tmp/bench_mock.log 2>&1 & pids+=($!)
|
||||||
|
wait_up "http://127.0.0.1:$MOCK_PORT/healthz" || { echo "mock failed"; exit 1; }
|
||||||
|
ab_run "mock-direct" "http://127.0.0.1:$MOCK_PORT/v1/chat/completions"
|
||||||
|
|
||||||
|
# --- cortiq-gateway ----------------------------------------------------------
|
||||||
|
BIN="$ROOT/target/release/cortiq-gateway"
|
||||||
|
[ -x "$BIN" ] || BIN="$ROOT/target/debug/cortiq-gateway"
|
||||||
|
"$BIN" --config "$HERE/cortiq.bench.toml" >/tmp/bench_cortiq.log 2>&1 & CG=$!; pids+=($CG)
|
||||||
|
if wait_up "http://127.0.0.1:9100/healthz"; then
|
||||||
|
ab_run "cortiq-gateway" "http://127.0.0.1:9100/v1/chat/completions"
|
||||||
|
fi
|
||||||
|
kill $CG 2>/dev/null
|
||||||
|
|
||||||
|
# --- LiteLLM -----------------------------------------------------------------
|
||||||
|
if python3 -c "import litellm" 2>/dev/null; then
|
||||||
|
cat > /tmp/litellm.yaml <<YAML
|
||||||
|
model_list:
|
||||||
|
- model_name: bench
|
||||||
|
litellm_params:
|
||||||
|
model: openai/bench-model
|
||||||
|
api_base: http://127.0.0.1:$MOCK_PORT/v1
|
||||||
|
api_key: sk-bench
|
||||||
|
YAML
|
||||||
|
# the proxy CLI is brittle across versions; run the ASGI app via uvicorn.
|
||||||
|
# 4 workers = LiteLLM's recommended multi-process setup (fair vs a multi-core gateway).
|
||||||
|
CONFIG_FILE_PATH=/tmp/litellm.yaml python3 -m uvicorn litellm.proxy.proxy_server:app \
|
||||||
|
--host 127.0.0.1 --port 4000 --workers 4 >/tmp/bench_litellm.log 2>&1 & LL=$!; pids+=($LL)
|
||||||
|
if wait_up "http://127.0.0.1:4000/health/liveliness"; then
|
||||||
|
# warm up the workers before measuring (cold imports per worker)
|
||||||
|
for _ in $(seq 1 10); do curl -s -o /dev/null -X POST -H 'content-type: application/json' -d @"$BODY" "http://127.0.0.1:4000/v1/chat/completions"; done
|
||||||
|
sleep 2
|
||||||
|
ab_run "litellm" "http://127.0.0.1:4000/v1/chat/completions"
|
||||||
|
fi
|
||||||
|
kill $LL 2>/dev/null
|
||||||
|
else
|
||||||
|
echo "litellm | (not installed — pip install 'litellm[proxy]')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Portkey -----------------------------------------------------------------
|
||||||
|
# resolve relative to the active node (npm root -g may point at a different npm)
|
||||||
|
PK_MAIN="$(dirname "$(command -v node 2>/dev/null)")/../lib/node_modules/@portkey-ai/gateway/build/start-server.js"
|
||||||
|
if [ -f "$PK_MAIN" ]; then
|
||||||
|
PORT=8787 node "$PK_MAIN" >/tmp/bench_portkey.log 2>&1 & PK=$!; pids+=($PK)
|
||||||
|
if wait_up "http://127.0.0.1:8787/v1/health" || wait_up "http://127.0.0.1:8787/"; then
|
||||||
|
sleep 1
|
||||||
|
ab_run "portkey" "http://127.0.0.1:8787/v1/chat/completions" \
|
||||||
|
-H "x-portkey-provider: openai" \
|
||||||
|
-H "x-portkey-custom-host: http://127.0.0.1:$MOCK_PORT/v1" \
|
||||||
|
-H "Authorization: Bearer sk-bench"
|
||||||
|
fi
|
||||||
|
kill $PK 2>/dev/null
|
||||||
|
else
|
||||||
|
echo "portkey | (not installed — npm i -g @portkey-ai/gateway)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Note: numbers are gateway overhead over a zero-latency mock on one machine;"
|
||||||
|
echo "subtract mock-direct to isolate proxy cost. Re-run on your own hardware."
|
||||||
Reference in New Issue
Block a user