mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
The tree was a debranded snapshot of upstream taken around 2026-02-20, and upstream has moved about five months since. This replaces it with upstream master, mechanically re-derived: bin/debrand does the rename, and the pipeline and squashed migrations are restored from the shipped tree. Not a merge. The previous import replaced the tree wholesale in a single commit, so there is no shared content history to merge against. Reproducing the rename on the new tree is what actually converges. What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints, compose files, LLM.md, and insights/migrations. The squashed initial migration is load-bearing — the live database records it by name, so upstream's ~918 individual migrations cannot come back. Nine products are dropped as unreferenced. Twenty-one more that we do not run are kept anyway because the core app still imports them; unpicking those is a refactor and does not belong in a rebase. This base does not build yet, and the reasons are not in the rename: upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against our 4.2, and the dependency manifests still name upstream packages that we republish under our own names. Those are decisions, not renames.
640 lines
21 KiB
Python
640 lines
21 KiB
Python
#!/usr/bin/env python3
|
|
"""Benchmark script measuring Insights LLM gateway overhead vs direct provider calls.
|
|
|
|
Measures TTFT, TPS, and total latency for streaming and non-streaming requests
|
|
across multiple models, with sequential and concurrent modes.
|
|
|
|
Usage:
|
|
python benchmark.py # all defaults
|
|
python benchmark.py --iterations 10 # more iterations
|
|
python benchmark.py --concurrency 5 # concurrent requests
|
|
python benchmark.py --models gpt-4.1-mini claude-haiku-4-5-20251001
|
|
python benchmark.py --streaming-only # skip non-streaming
|
|
python benchmark.py --report results/benchmark_YYYYMMDD.json
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import asyncio
|
|
import json
|
|
import math
|
|
import os
|
|
import statistics
|
|
import time
|
|
import uuid
|
|
from dataclasses import asdict, dataclass
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
|
|
from anthropic import AsyncAnthropic
|
|
from dotenv import load_dotenv
|
|
from openai import AsyncOpenAI
|
|
|
|
load_dotenv()
|
|
|
|
DEFAULT_MODELS = [
|
|
{"name": "gpt-4.1-mini", "provider": "openai"},
|
|
{"name": "gpt-4.1-nano", "provider": "openai"},
|
|
{"name": "claude-sonnet-4-5-20250514", "provider": "anthropic"},
|
|
{"name": "claude-haiku-4-5-20251001", "provider": "anthropic"},
|
|
]
|
|
|
|
DEFAULT_PROMPT = "Write exactly 200 words about the history of computing."
|
|
DEFAULT_MAX_TOKENS = 300
|
|
DEFAULT_ITERATIONS = 5
|
|
DEFAULT_WARMUP = 1
|
|
DEFAULT_CONCURRENCY = 1
|
|
DEFAULT_OUTPUT_DIR = "results"
|
|
|
|
|
|
@dataclass
|
|
class BenchmarkResult:
|
|
model: str
|
|
route: str # "direct" | "gateway"
|
|
mode: str # "streaming" | "non_streaming"
|
|
ttft_ms: float | None # only for streaming
|
|
tps: float | None # only for streaming
|
|
total_ms: float
|
|
token_count: int
|
|
|
|
|
|
@dataclass
|
|
class BenchmarkStats:
|
|
mean: float
|
|
median: float
|
|
p95: float
|
|
stddev: float
|
|
|
|
|
|
def compute_stats(values: list[float]) -> BenchmarkStats:
|
|
if not values:
|
|
return BenchmarkStats(mean=0, median=0, p95=0, stddev=0)
|
|
mean = statistics.mean(values)
|
|
median = statistics.median(values)
|
|
stddev = statistics.stdev(values) if len(values) > 1 else 0.0
|
|
sorted_vals = sorted(values)
|
|
p95_idx = math.ceil(len(sorted_vals) * 0.95) - 1
|
|
p95 = sorted_vals[max(0, p95_idx)]
|
|
return BenchmarkStats(mean=mean, median=median, p95=p95, stddev=stddev)
|
|
|
|
|
|
def unique_prompt(prompt: str) -> str:
|
|
return f"[{uuid.uuid4()}] {prompt}"
|
|
|
|
|
|
# -- Streaming benchmarks --
|
|
|
|
|
|
async def bench_openai_stream(
|
|
client: AsyncOpenAI, model: str, prompt: str, max_tokens: int, route: str
|
|
) -> BenchmarkResult:
|
|
start = time.perf_counter()
|
|
ttft: float | None = None
|
|
token_count = 0
|
|
chunk_count = 0
|
|
|
|
stream = await client.chat.completions.create(
|
|
model=model,
|
|
messages=[{"role": "user", "content": unique_prompt(prompt)}],
|
|
max_tokens=max_tokens,
|
|
temperature=0,
|
|
stream=True,
|
|
stream_options={"include_usage": True},
|
|
)
|
|
|
|
async for chunk in stream:
|
|
if chunk.choices and chunk.choices[0].delta.content:
|
|
if ttft is None:
|
|
ttft = (time.perf_counter() - start) * 1000
|
|
chunk_count += 1
|
|
if chunk.usage and chunk.usage.completion_tokens:
|
|
token_count = chunk.usage.completion_tokens
|
|
|
|
if token_count == 0:
|
|
token_count = chunk_count
|
|
|
|
total_ms = (time.perf_counter() - start) * 1000
|
|
ttft_ms = ttft if ttft is not None else total_ms
|
|
|
|
generation_ms = total_ms - ttft_ms
|
|
tps = (token_count / (generation_ms / 1000)) if generation_ms > 0 and token_count > 0 else None
|
|
|
|
return BenchmarkResult(
|
|
model=model,
|
|
route=route,
|
|
mode="streaming",
|
|
ttft_ms=ttft_ms,
|
|
tps=tps,
|
|
total_ms=total_ms,
|
|
token_count=token_count,
|
|
)
|
|
|
|
|
|
async def bench_anthropic_stream(
|
|
client: AsyncAnthropic, model: str, prompt: str, max_tokens: int, route: str
|
|
) -> BenchmarkResult:
|
|
start = time.perf_counter()
|
|
ttft: float | None = None
|
|
chunk_count = 0
|
|
|
|
async with client.messages.stream(
|
|
model=model,
|
|
messages=[{"role": "user", "content": unique_prompt(prompt)}],
|
|
max_tokens=max_tokens,
|
|
temperature=0,
|
|
) as stream:
|
|
async for _text in stream.text_stream:
|
|
if ttft is None:
|
|
ttft = (time.perf_counter() - start) * 1000
|
|
chunk_count += 1
|
|
final_message = await stream.get_final_message()
|
|
|
|
token_count = final_message.usage.output_tokens if final_message else chunk_count
|
|
|
|
total_ms = (time.perf_counter() - start) * 1000
|
|
ttft_ms = ttft if ttft is not None else total_ms
|
|
|
|
generation_ms = total_ms - ttft_ms
|
|
tps = (token_count / (generation_ms / 1000)) if generation_ms > 0 and token_count > 0 else None
|
|
|
|
return BenchmarkResult(
|
|
model=model,
|
|
route=route,
|
|
mode="streaming",
|
|
ttft_ms=ttft_ms,
|
|
tps=tps,
|
|
total_ms=total_ms,
|
|
token_count=token_count,
|
|
)
|
|
|
|
|
|
# -- Non-streaming benchmarks --
|
|
|
|
|
|
async def bench_openai_non_stream(
|
|
client: AsyncOpenAI, model: str, prompt: str, max_tokens: int, route: str
|
|
) -> BenchmarkResult:
|
|
start = time.perf_counter()
|
|
|
|
response = await client.chat.completions.create(
|
|
model=model,
|
|
messages=[{"role": "user", "content": unique_prompt(prompt)}],
|
|
max_tokens=max_tokens,
|
|
temperature=0,
|
|
stream=False,
|
|
)
|
|
|
|
total_ms = (time.perf_counter() - start) * 1000
|
|
token_count = response.usage.completion_tokens if response.usage else 0
|
|
|
|
return BenchmarkResult(
|
|
model=model,
|
|
route=route,
|
|
mode="non_streaming",
|
|
ttft_ms=None,
|
|
tps=None,
|
|
total_ms=total_ms,
|
|
token_count=token_count,
|
|
)
|
|
|
|
|
|
async def bench_anthropic_non_stream(
|
|
client: AsyncAnthropic, model: str, prompt: str, max_tokens: int, route: str
|
|
) -> BenchmarkResult:
|
|
start = time.perf_counter()
|
|
|
|
response = await client.messages.create(
|
|
model=model,
|
|
messages=[{"role": "user", "content": unique_prompt(prompt)}],
|
|
max_tokens=max_tokens,
|
|
temperature=0,
|
|
)
|
|
|
|
total_ms = (time.perf_counter() - start) * 1000
|
|
token_count = response.usage.output_tokens
|
|
|
|
return BenchmarkResult(
|
|
model=model,
|
|
route=route,
|
|
mode="non_streaming",
|
|
ttft_ms=None,
|
|
tps=None,
|
|
total_ms=total_ms,
|
|
token_count=token_count,
|
|
)
|
|
|
|
|
|
# -- Client setup --
|
|
|
|
|
|
def create_clients(
|
|
gateway_url: str,
|
|
) -> dict[str, AsyncOpenAI | AsyncAnthropic]:
|
|
gateway_api_key = os.environ.get("GATEWAY_API_KEY")
|
|
|
|
clients: dict[str, AsyncOpenAI | AsyncAnthropic] = {}
|
|
|
|
if not gateway_api_key:
|
|
print(" Warning: GATEWAY_API_KEY not set, skipping gateway routes")
|
|
|
|
openai_key = os.environ.get("OPENAI_API_KEY")
|
|
if openai_key:
|
|
clients["openai_direct"] = AsyncOpenAI(api_key=openai_key)
|
|
if gateway_api_key:
|
|
clients["openai_gateway"] = AsyncOpenAI(base_url=f"{gateway_url}/v1", api_key=gateway_api_key)
|
|
|
|
anthropic_key = os.environ.get("ANTHROPIC_API_KEY")
|
|
if anthropic_key:
|
|
clients["anthropic_direct"] = AsyncAnthropic(api_key=anthropic_key)
|
|
if gateway_api_key:
|
|
clients["anthropic_gateway"] = AsyncAnthropic(base_url=gateway_url, api_key=gateway_api_key)
|
|
|
|
return clients
|
|
|
|
|
|
def get_bench_fn(provider: str, mode: str, client: AsyncOpenAI | AsyncAnthropic):
|
|
fns = {
|
|
("openai", "streaming"): bench_openai_stream,
|
|
("openai", "non_streaming"): bench_openai_non_stream,
|
|
("anthropic", "streaming"): bench_anthropic_stream,
|
|
("anthropic", "non_streaming"): bench_anthropic_non_stream,
|
|
}
|
|
return fns[(provider, mode)]
|
|
|
|
|
|
# -- Runner --
|
|
|
|
|
|
async def run_single(
|
|
bench_fn,
|
|
client: AsyncOpenAI | AsyncAnthropic,
|
|
model: str,
|
|
prompt: str,
|
|
max_tokens: int,
|
|
route: str,
|
|
) -> BenchmarkResult:
|
|
return await bench_fn(client, model, prompt, max_tokens, route)
|
|
|
|
|
|
async def run_benchmark(
|
|
models: list[dict],
|
|
iterations: int,
|
|
warmup: int,
|
|
concurrency: int,
|
|
prompt: str,
|
|
max_tokens: int,
|
|
modes: list[str],
|
|
gateway_url: str,
|
|
) -> list[BenchmarkResult]:
|
|
clients = create_clients(gateway_url)
|
|
all_results: list[BenchmarkResult] = []
|
|
|
|
for model_cfg in models:
|
|
model_name = model_cfg["name"]
|
|
provider = model_cfg["provider"]
|
|
|
|
direct_key = f"{provider}_direct"
|
|
gateway_key = f"{provider}_gateway"
|
|
|
|
if direct_key not in clients:
|
|
print(f" Skipping {model_name}: no {provider} API key configured")
|
|
continue
|
|
|
|
routes = [(route, key) for route, key in [("direct", direct_key), ("gateway", gateway_key)] if key in clients]
|
|
|
|
for mode in modes:
|
|
# Warmup each route
|
|
for route, client_key in routes:
|
|
client = clients[client_key]
|
|
bench_fn = get_bench_fn(provider, mode, client)
|
|
label = f"{model_name} / {route} / {mode}"
|
|
for i in range(warmup):
|
|
print(f" {label} — warmup {i + 1}/{warmup}")
|
|
try:
|
|
await run_single(bench_fn, client, model_name, prompt, max_tokens, route)
|
|
except Exception as e:
|
|
print(f" warmup error: {e}")
|
|
|
|
for i in range(iterations):
|
|
for route, client_key in routes:
|
|
client = clients[client_key]
|
|
bench_fn = get_bench_fn(provider, mode, client)
|
|
label = f"{model_name} / {route} / {mode}"
|
|
|
|
if concurrency > 1:
|
|
print(f" {label} — iteration {i + 1}/{iterations} (x{concurrency} concurrent)")
|
|
try:
|
|
concurrent_results = await asyncio.gather(
|
|
*[
|
|
run_single(bench_fn, client, model_name, prompt, max_tokens, route)
|
|
for _ in range(concurrency)
|
|
],
|
|
return_exceptions=True,
|
|
)
|
|
for r in concurrent_results:
|
|
if isinstance(r, BaseException):
|
|
print(f" concurrent error: {r}")
|
|
else:
|
|
all_results.append(r)
|
|
except Exception as e:
|
|
print(f" concurrent error: {e}")
|
|
else:
|
|
print(f" {label} — iteration {i + 1}/{iterations}")
|
|
try:
|
|
result = await run_single(bench_fn, client, model_name, prompt, max_tokens, route)
|
|
all_results.append(result)
|
|
except Exception as e:
|
|
print(f" error: {e}")
|
|
|
|
return all_results
|
|
|
|
|
|
# -- Stats aggregation --
|
|
|
|
|
|
def aggregate_stats(
|
|
results: list[BenchmarkResult],
|
|
) -> dict[str, dict[str, dict[str, dict[str, BenchmarkStats]]]]:
|
|
grouped: dict[str, dict[str, dict[str, list[BenchmarkResult]]]] = {}
|
|
|
|
for r in results:
|
|
grouped.setdefault(r.model, {}).setdefault(r.mode, {}).setdefault(r.route, []).append(r)
|
|
|
|
stats: dict[str, dict[str, dict[str, dict[str, BenchmarkStats]]]] = {}
|
|
for model, modes in grouped.items():
|
|
stats[model] = {}
|
|
for mode, routes in modes.items():
|
|
stats[model][mode] = {}
|
|
for route, route_results in routes.items():
|
|
route_stats: dict[str, BenchmarkStats] = {
|
|
"total_ms": compute_stats([r.total_ms for r in route_results]),
|
|
}
|
|
ttft_values = [r.ttft_ms for r in route_results if r.ttft_ms is not None]
|
|
if ttft_values:
|
|
route_stats["ttft_ms"] = compute_stats(ttft_values)
|
|
tps_values = [r.tps for r in route_results if r.tps is not None]
|
|
if tps_values:
|
|
route_stats["tps"] = compute_stats(tps_values)
|
|
stats[model][mode][route] = route_stats
|
|
|
|
return stats
|
|
|
|
|
|
# -- Output formatting --
|
|
|
|
|
|
def fmt_stat(s: BenchmarkStats) -> str:
|
|
return f"{s.mean:.0f} / {s.median:.0f} / {s.p95:.0f}"
|
|
|
|
|
|
def fmt_overhead(direct: BenchmarkStats, gateway: BenchmarkStats, higher_is_better: bool = False) -> str:
|
|
diff = gateway.mean - direct.mean
|
|
pct = (diff / direct.mean * 100) if direct.mean != 0 else 0
|
|
sign = "+" if diff >= 0 else ""
|
|
if higher_is_better:
|
|
quality = " (!)" if diff < 0 and abs(pct) > 20 else ""
|
|
else:
|
|
quality = " (!)" if diff > 0 and pct > 20 else ""
|
|
return f"{sign}{diff:.0f} ({sign}{pct:.1f}%){quality}"
|
|
|
|
|
|
def print_streaming_table(
|
|
stats: dict[str, dict[str, dict[str, dict[str, BenchmarkStats]]]],
|
|
mode: str,
|
|
title: str,
|
|
) -> None:
|
|
models_with_mode = [(m, s) for m, s in stats.items() if mode in s]
|
|
if not models_with_mode:
|
|
return
|
|
|
|
print(f"\n{'=' * 3} {title} {'=' * 3}")
|
|
header = f"{'Model':<30} {'Route':<10} {'TTFT (ms)':<22} {'TPS':<22} {'Total (ms)':<22}"
|
|
subheader = f"{'':<30} {'':<10} {'mean / med / p95':<22} {'mean / med / p95':<22} {'mean / med / p95':<22}"
|
|
print(header)
|
|
print(subheader)
|
|
print("─" * 106)
|
|
|
|
for model, model_stats in models_with_mode:
|
|
mode_stats = model_stats[mode]
|
|
for route in ["direct", "gateway"]:
|
|
if route not in mode_stats:
|
|
continue
|
|
rs = mode_stats[route]
|
|
ttft_str = fmt_stat(rs["ttft_ms"]) if "ttft_ms" in rs else "—"
|
|
tps_str = fmt_stat(rs["tps"]) if "tps" in rs else "—"
|
|
total_str = fmt_stat(rs["total_ms"])
|
|
print(f"{model:<30} {route:<10} {ttft_str:<22} {tps_str:<22} {total_str:<22}")
|
|
|
|
if "direct" in mode_stats and "gateway" in mode_stats:
|
|
d, g = mode_stats["direct"], mode_stats["gateway"]
|
|
parts = [" → overhead", ""]
|
|
parts.append(fmt_overhead(d["ttft_ms"], g["ttft_ms"]) if "ttft_ms" in d and "ttft_ms" in g else "—")
|
|
parts.append(fmt_overhead(d["tps"], g["tps"], higher_is_better=True) if "tps" in d and "tps" in g else "—")
|
|
parts.append(fmt_overhead(d["total_ms"], g["total_ms"]))
|
|
print(f"{parts[0]:<30} {parts[1]:<10} {parts[2]:<22} {parts[3]:<22} {parts[4]:<22}")
|
|
print()
|
|
|
|
|
|
def print_non_streaming_table(
|
|
stats: dict[str, dict[str, dict[str, dict[str, BenchmarkStats]]]],
|
|
mode: str,
|
|
title: str,
|
|
) -> None:
|
|
models_with_mode = [(m, s) for m, s in stats.items() if mode in s]
|
|
if not models_with_mode:
|
|
return
|
|
|
|
print(f"\n{'=' * 3} {title} {'=' * 3}")
|
|
header = f"{'Model':<30} {'Route':<10} {'Total (ms)':<22}"
|
|
subheader = f"{'':<30} {'':<10} {'mean / med / p95':<22}"
|
|
print(header)
|
|
print(subheader)
|
|
print("─" * 62)
|
|
|
|
for model, model_stats in models_with_mode:
|
|
mode_stats = model_stats[mode]
|
|
for route in ["direct", "gateway"]:
|
|
if route not in mode_stats:
|
|
continue
|
|
total_str = fmt_stat(mode_stats[route]["total_ms"])
|
|
print(f"{model:<30} {route:<10} {total_str:<22}")
|
|
|
|
if "direct" in mode_stats and "gateway" in mode_stats:
|
|
overhead = fmt_overhead(mode_stats["direct"]["total_ms"], mode_stats["gateway"]["total_ms"])
|
|
print(f"{' → overhead':<30} {'':<10} {overhead:<22}")
|
|
print()
|
|
|
|
|
|
def print_results(
|
|
stats: dict[str, dict[str, dict[str, dict[str, BenchmarkStats]]]],
|
|
concurrency: int,
|
|
) -> None:
|
|
suffix = f" (x{concurrency} concurrent)" if concurrency > 1 else ""
|
|
print_streaming_table(stats, "streaming", f"Streaming Results{suffix}")
|
|
print_non_streaming_table(stats, "non_streaming", f"Non-streaming Results{suffix}")
|
|
|
|
|
|
# -- Persistence --
|
|
|
|
|
|
def stats_to_dict(
|
|
stats: dict[str, dict[str, dict[str, dict[str, BenchmarkStats]]]],
|
|
) -> dict:
|
|
out: dict = {}
|
|
for model, modes in stats.items():
|
|
out[model] = {}
|
|
for mode, routes in modes.items():
|
|
out[model][mode] = {}
|
|
for route, metrics in routes.items():
|
|
out[model][mode][route] = {k: asdict(v) for k, v in metrics.items()}
|
|
return out
|
|
|
|
|
|
def save_results(
|
|
results: list[BenchmarkResult],
|
|
stats: dict[str, dict[str, dict[str, dict[str, BenchmarkStats]]]],
|
|
metadata: dict,
|
|
output_dir: str,
|
|
) -> str:
|
|
path = Path(output_dir)
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
|
|
timestamp = datetime.now(UTC).strftime("%Y%m%d_%H%M%S")
|
|
filename = path / f"benchmark_{timestamp}.json"
|
|
|
|
data = {
|
|
"metadata": metadata,
|
|
"raw_results": [asdict(r) for r in results],
|
|
"stats": stats_to_dict(stats),
|
|
}
|
|
|
|
with open(filename, "w") as f:
|
|
json.dump(data, f, indent=2)
|
|
|
|
print(f"\nResults saved to {filename}")
|
|
return str(filename)
|
|
|
|
|
|
def load_and_print_report(report_path: str) -> None:
|
|
with open(report_path) as f:
|
|
data = json.load(f)
|
|
|
|
meta = data["metadata"]
|
|
print(f"Report from {meta['timestamp']}")
|
|
print(f" iterations={meta['iterations']}, warmup={meta['warmup']}, concurrency={meta['concurrency']}")
|
|
print(f" max_tokens={meta['max_tokens']}")
|
|
print(f" prompt: {meta['prompt'][:80]}...")
|
|
|
|
raw_stats = data["stats"]
|
|
stats: dict[str, dict[str, dict[str, dict[str, BenchmarkStats]]]] = {}
|
|
for model, modes in raw_stats.items():
|
|
stats[model] = {}
|
|
for mode, routes in modes.items():
|
|
stats[model][mode] = {}
|
|
for route, metrics in routes.items():
|
|
stats[model][mode][route] = {k: BenchmarkStats(**v) for k, v in metrics.items()}
|
|
|
|
print_results(stats, meta["concurrency"])
|
|
|
|
|
|
# -- CLI --
|
|
|
|
|
|
def resolve_models(model_names: list[str] | None) -> list[dict]:
|
|
if not model_names:
|
|
return DEFAULT_MODELS
|
|
|
|
model_lookup = {m["name"]: m for m in DEFAULT_MODELS}
|
|
resolved = []
|
|
for name in model_names:
|
|
if name in model_lookup:
|
|
resolved.append(model_lookup[name])
|
|
elif name.startswith("claude"):
|
|
resolved.append({"name": name, "provider": "anthropic"})
|
|
else:
|
|
resolved.append({"name": name, "provider": "openai"})
|
|
return resolved
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description="Benchmark LLM gateway overhead")
|
|
parser.add_argument("--iterations", type=int, default=DEFAULT_ITERATIONS)
|
|
parser.add_argument("--warmup", type=int, default=DEFAULT_WARMUP)
|
|
parser.add_argument("--concurrency", type=int, default=DEFAULT_CONCURRENCY)
|
|
parser.add_argument("--models", nargs="+", default=None)
|
|
parser.add_argument("--streaming-only", action="store_true")
|
|
parser.add_argument("--non-streaming-only", action="store_true")
|
|
parser.add_argument("--prompt", default=DEFAULT_PROMPT)
|
|
parser.add_argument("--max-tokens", type=int, default=DEFAULT_MAX_TOKENS)
|
|
parser.add_argument("--report", default=None, help="Path to saved JSON to re-print")
|
|
parser.add_argument("--output-dir", default=DEFAULT_OUTPUT_DIR)
|
|
parser.add_argument(
|
|
"--gateway-url",
|
|
default=os.environ.get("GATEWAY_URL", "https://gateway.us.hanzo.ai"),
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
async def main() -> None:
|
|
args = parse_args()
|
|
|
|
if args.report:
|
|
load_and_print_report(args.report)
|
|
return
|
|
|
|
models = resolve_models(args.models)
|
|
|
|
modes: list[str] = []
|
|
if args.streaming_only:
|
|
modes = ["streaming"]
|
|
elif args.non_streaming_only:
|
|
modes = ["non_streaming"]
|
|
else:
|
|
modes = ["streaming", "non_streaming"]
|
|
|
|
print(
|
|
f"Benchmarking {len(models)} models, {args.iterations} iterations, "
|
|
f"{args.warmup} warmup, concurrency={args.concurrency}"
|
|
)
|
|
print(f"Modes: {', '.join(modes)}")
|
|
print(f"Prompt: {args.prompt[:80]}...")
|
|
print(f"Max tokens: {args.max_tokens}")
|
|
print(f"Gateway: {args.gateway_url}")
|
|
print()
|
|
|
|
results = await run_benchmark(
|
|
models=models,
|
|
iterations=args.iterations,
|
|
warmup=args.warmup,
|
|
concurrency=args.concurrency,
|
|
prompt=args.prompt,
|
|
max_tokens=args.max_tokens,
|
|
modes=modes,
|
|
gateway_url=args.gateway_url,
|
|
)
|
|
|
|
if not results:
|
|
print("No results collected. Check API keys and connectivity.")
|
|
return
|
|
|
|
stats = aggregate_stats(results)
|
|
print_results(stats, args.concurrency)
|
|
|
|
metadata = {
|
|
"timestamp": datetime.now(UTC).isoformat(),
|
|
"iterations": args.iterations,
|
|
"warmup": args.warmup,
|
|
"concurrency": args.concurrency,
|
|
"prompt": args.prompt,
|
|
"max_tokens": args.max_tokens,
|
|
"models": [m["name"] for m in models],
|
|
"modes": modes,
|
|
}
|
|
|
|
save_results(results, stats, metadata, args.output_dir)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|