From 4a63c1746f0f65b9ce05ed669c938663edb41492 Mon Sep 17 00:00:00 2001 From: "Jakub A. W" Date: Tue, 25 Aug 2026 23:59:45 +0200 Subject: [PATCH 1/7] build(go): update toolchain to Go 1.27 --- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 9 ++++++--- Dockerfile | 2 +- Makefile | 2 +- docs/TESTING_STRATEGY.md | 2 +- docs/guides/prometheus-metrics.mdx | 2 +- go.mod | 2 +- 7 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8a0b29e19..9acaef3c3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -155,7 +155,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v7 with: - go-version: 1.26.6 + go-version: 1.27.0 cache: true - name: Run GoReleaser diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 58c0377e9..7d936b099 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ concurrency: cancel-in-progress: true env: - GO_VERSION: "1.26.6" + GO_VERSION: "1.27.0" # Restrict token permissions to minimum required permissions: @@ -31,10 +31,13 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v9 with: - # v2.12+ ships a staticcheck that understands Go 1.26 control flow. + # Build the linter with Go 1.27 until upstream publishes binaries built + # with Go 1.27; older toolchains reject modules targeting newer Go. + install-mode: goinstall + # v2.12+ ships a staticcheck that understands modern Go control flow. # v2.10's staticcheck mis-analyzed `if x == nil { t.Fatal() }` guards # under GO_VERSION 1.26.4 and emitted false-positive SA5011 warnings. - version: v2.12 + version: v2.12.2 # The shared analysis cache went bad on 2026-08-25: staticcheck lost # t.Fatal no-return facts and every run flagged a different set of # SA5011 false positives in untouched files, on PR branches and main diff --git a/Dockerfile b/Dockerfile index e0dcb410b..e98f949de 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build stage — run on the build host's native arch for speed, cross-compile for target -FROM --platform=$BUILDPLATFORM golang:1.26.6-alpine3.23 AS builder +FROM --platform=$BUILDPLATFORM golang:1.27.0-alpine3.23 AS builder ARG TARGETOS ARG TARGETARCH diff --git a/Makefile b/Makefile index 5504adfd8..92340c284 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ LDFLAGS := -X "github.com/enterpilot/gomodel/internal/version.Version=$(VERSION) -X "github.com/enterpilot/gomodel/internal/version.Date=$(DATE)" install-tools: - @command -v golangci-lint > /dev/null 2>&1 || (echo "Installing golangci-lint..." && go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.10) + @command -v golangci-lint > /dev/null 2>&1 || (echo "Installing golangci-lint..." && go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2) @command -v pre-commit > /dev/null 2>&1 || (echo "Installing pre-commit..." && pip install pre-commit==4.5.1) @echo "All tools are ready" diff --git a/docs/TESTING_STRATEGY.md b/docs/TESTING_STRATEGY.md index 935bb55cf..dc6855746 100644 --- a/docs/TESTING_STRATEGY.md +++ b/docs/TESTING_STRATEGY.md @@ -209,7 +209,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v6 with: - go-version: "1.26.4" + go-version: "1.27.0" # Unit + E2E (no external dependencies) - run: make test-all diff --git a/docs/guides/prometheus-metrics.mdx b/docs/guides/prometheus-metrics.mdx index bd299cdd3..6a8c24e95 100644 --- a/docs/guides/prometheus-metrics.mdx +++ b/docs/guides/prometheus-metrics.mdx @@ -37,7 +37,7 @@ docker run --rm -p 8080:8080 \ ``` ```bash Binary (make build) -# Requires Go 1.26.4+; produces bin/gomodel. +# Requires Go 1.27.0+; produces bin/gomodel. make build export METRICS_ENABLED=true diff --git a/go.mod b/go.mod index 9a1133a0f..184979767 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/enterpilot/gomodel -go 1.26.6 +go 1.27.0 require ( github.com/andybalholm/brotli v1.2.2 From 089d24fc331cc18190133692186db69bd832f271 Mon Sep 17 00:00:00 2001 From: "Jakub A. W" Date: Wed, 26 Aug 2026 00:00:19 +0200 Subject: [PATCH 2/7] docs(benchmarks): remove retired benchmark scripts --- docs/.mintignore | 1 - docs/2026-03-23_benchmark_scripts/.gitignore | 2 - docs/2026-03-23_benchmark_scripts/README.md | 82 ---- .../gateway-comparison/.gitignore | 4 - .../configs/gomodel-config.yaml | 47 -- .../configs/litellm-config.yaml | 15 - .../gateway-comparison/mock-backend/main.go | 264 ---------- .../gateway-comparison/run-benchmark.sh | 289 ----------- .../gateway-comparison/stream-bench/main.go | 253 ---------- .../generate_benchmark_artifacts.py | 450 ------------------ docs/2026-03-23_benchmark_scripts/run.sh | 33 -- .../test_generate_benchmark_artifacts.py | 67 --- 12 files changed, 1507 deletions(-) delete mode 100644 docs/2026-03-23_benchmark_scripts/.gitignore delete mode 100644 docs/2026-03-23_benchmark_scripts/README.md delete mode 100644 docs/2026-03-23_benchmark_scripts/gateway-comparison/.gitignore delete mode 100644 docs/2026-03-23_benchmark_scripts/gateway-comparison/configs/gomodel-config.yaml delete mode 100644 docs/2026-03-23_benchmark_scripts/gateway-comparison/configs/litellm-config.yaml delete mode 100644 docs/2026-03-23_benchmark_scripts/gateway-comparison/mock-backend/main.go delete mode 100755 docs/2026-03-23_benchmark_scripts/gateway-comparison/run-benchmark.sh delete mode 100644 docs/2026-03-23_benchmark_scripts/gateway-comparison/stream-bench/main.go delete mode 100755 docs/2026-03-23_benchmark_scripts/generate_benchmark_artifacts.py delete mode 100755 docs/2026-03-23_benchmark_scripts/run.sh delete mode 100644 docs/2026-03-23_benchmark_scripts/test_generate_benchmark_artifacts.py diff --git a/docs/.mintignore b/docs/.mintignore index 08818b09f..29df20508 100644 --- a/docs/.mintignore +++ b/docs/.mintignore @@ -1,6 +1,5 @@ # Internal engineering material is kept in the repository but is not part of # the public product documentation, search index, or AI context. -2026-03-23_benchmark_scripts/ 2026-04-09_CODEBASE_SNAPSHOT.md 2026-07-21_bifrost_reproducible_benchmark/ DEVELOPMENT.md diff --git a/docs/2026-03-23_benchmark_scripts/.gitignore b/docs/2026-03-23_benchmark_scripts/.gitignore deleted file mode 100644 index bacca8e16..000000000 --- a/docs/2026-03-23_benchmark_scripts/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -__pycache__/ -output/ diff --git a/docs/2026-03-23_benchmark_scripts/README.md b/docs/2026-03-23_benchmark_scripts/README.md deleted file mode 100644 index 87299090c..000000000 --- a/docs/2026-03-23_benchmark_scripts/README.md +++ /dev/null @@ -1,82 +0,0 @@ -# March 23, 2026 benchmark scripts - -This directory is the reproducible entry point for the March 23, 2026 -GoModel vs LiteLLM benchmark refresh. - -It is built around the benchmark workspace in -`docs/2026-03-23_benchmark_scripts/gateway-comparison/`, then adds: - -- a tested normalization step for the raw `hey` and streaming outputs, -- chart generation for the blog assets, -- a stable wrapper command for rerunning the benchmark and rebuilding the - article artifacts. - -## What this benchmark measures - -This run uses the same localhost mock backend for both gateways, so the numbers -measure gateway overhead rather than upstream model latency. - -Workloads covered: - -- `/v1/chat/completions` non-streaming -- `/v1/chat/completions` streaming -- `/v1/responses` non-streaming -- `/v1/responses` streaming - -The raw benchmark runner also records a direct baseline for chat traffic with no -gateway in the middle. - -## Prerequisites - -- Go 1.26.3+ -- Python 3.10+ -- `hey` -- `litellm` -- Python packages: `matplotlib`, `numpy` - -Install Python packages if needed: - -```bash -python3 -m pip install matplotlib numpy -``` - -## Quick start - -Run the raw benchmark and generate normalized artifacts: - -```bash -RUN_BENCHMARK=1 bash docs/2026-03-23_benchmark_scripts/run.sh -``` - -If you already have a benchmark result directory, point the wrapper at it: - -```bash -RESULTS_DIR=/path/to/results bash docs/2026-03-23_benchmark_scripts/run.sh -``` - -Copy the generated chart assets into the sibling Enterpilot blog repo: - -```bash -BLOG_PUBLIC_DIR=../enterpilot.io/blog/public/charts \ - bash docs/2026-03-23_benchmark_scripts/run.sh -``` - -## Outputs - -By default, generated artifacts land in `docs/2026-03-23_benchmark_scripts/output/`: - -- `benchmark_summary.json`: normalized machine-readable metrics -- `charts/gomodel-vs-litellm-march-2026-dashboard.png` -- `charts/gomodel-vs-litellm-march-2026-throughput.png` -- `charts/gomodel-vs-litellm-march-2026-latency.png` -- `charts/gomodel-vs-litellm-march-2026-memory.png` -- `charts/gomodel-vs-litellm-march-2026-speedup.png` - -## Notes - -- The raw benchmark runner lives in `docs/2026-03-23_benchmark_scripts/gateway-comparison/run-benchmark.sh`. -- The normalization step exists because raw shell summaries are easy to drift or - misparse; the parser in this directory is covered by unit tests with inline - sample fixtures, so the repo does not need to carry benchmark result dumps. -- These results are a point-in-time localhost benchmark, not a universal claim - about every deployment shape. diff --git a/docs/2026-03-23_benchmark_scripts/gateway-comparison/.gitignore b/docs/2026-03-23_benchmark_scripts/gateway-comparison/.gitignore deleted file mode 100644 index fa52c9464..000000000 --- a/docs/2026-03-23_benchmark_scripts/gateway-comparison/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -gomodel-bin -mock-backend/mock-server -stream-bench/stream-bench -results/ diff --git a/docs/2026-03-23_benchmark_scripts/gateway-comparison/configs/gomodel-config.yaml b/docs/2026-03-23_benchmark_scripts/gateway-comparison/configs/gomodel-config.yaml deleted file mode 100644 index 2cd4c350b..000000000 --- a/docs/2026-03-23_benchmark_scripts/gateway-comparison/configs/gomodel-config.yaml +++ /dev/null @@ -1,47 +0,0 @@ -server: - port: "8081" - master_key: "" - body_size_limit: "10M" - swagger_enabled: false - pprof_enabled: false - enable_passthrough_routes: false - -cache: - model: - refresh_interval: 86400 - local: - cache_dir: "/tmp/gomodel-bench-cache" - -storage: - type: "sqlite" - sqlite: - path: "/tmp/gomodel-bench.db" - -logging: - enabled: false - -usage: - enabled: false - -metrics: - enabled: false - -admin: - endpoints_enabled: false - ui_enabled: false - -http: - timeout: 60 - response_header_timeout: 60 - -resilience: - retry: - max_retries: 0 - circuit_breaker: - failure_threshold: 999 - -providers: - openai: - type: openai - api_key: "sk-bench-test-key" - base_url: "http://localhost:9999/v1" diff --git a/docs/2026-03-23_benchmark_scripts/gateway-comparison/configs/litellm-config.yaml b/docs/2026-03-23_benchmark_scripts/gateway-comparison/configs/litellm-config.yaml deleted file mode 100644 index 0089f9d51..000000000 --- a/docs/2026-03-23_benchmark_scripts/gateway-comparison/configs/litellm-config.yaml +++ /dev/null @@ -1,15 +0,0 @@ -model_list: - - model_name: "gpt-4o-mini" - litellm_params: - model: "openai/gpt-4o-mini" - api_key: "sk-bench-test-key" - api_base: "http://localhost:9999/v1" - -general_settings: - master_key: null - disable_spend_logs: true - -litellm_settings: - num_retries: 0 - request_timeout: 60 - drop_params: true diff --git a/docs/2026-03-23_benchmark_scripts/gateway-comparison/mock-backend/main.go b/docs/2026-03-23_benchmark_scripts/gateway-comparison/mock-backend/main.go deleted file mode 100644 index 69ab46b92..000000000 --- a/docs/2026-03-23_benchmark_scripts/gateway-comparison/mock-backend/main.go +++ /dev/null @@ -1,264 +0,0 @@ -// Mock OpenAI-compatible backend server for benchmarking AI gateways. -// Responds instantly with deterministic payloads so benchmarks measure -// pure gateway overhead, not provider latency. -package main - -import ( - "encoding/json" - "fmt" - "log" - "net/http" - "os" - "strings" - "time" -) - -func main() { - port := "9999" - if p := os.Getenv("MOCK_PORT"); p != "" { - port = p - } - - mux := http.NewServeMux() - mux.HandleFunc("/v1/chat/completions", handleChatCompletions) - mux.HandleFunc("/chat/completions", handleChatCompletions) // some gateways strip /v1 - mux.HandleFunc("/v1/responses", handleResponses) - mux.HandleFunc("/responses", handleResponses) - mux.HandleFunc("/v1/models", handleModels) - mux.HandleFunc("/models", handleModels) - mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) { - writeJSONBytes(w, http.StatusOK, []byte(`{"status":"ok"}`)) - }) - - log.Printf("Mock OpenAI backend listening on :%s", port) - if err := http.ListenAndServe(":"+port, mux); err != nil { - log.Fatal(err) - } -} - -// ---------- Chat Completions ---------- - -func handleChatCompletions(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - return - } - - var body struct { - Stream bool `json:"stream"` - } - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - http.Error(w, "invalid request body", http.StatusBadRequest) - return - } - - if body.Stream { - streamChatCompletion(w) - } else { - nonStreamChatCompletion(w) - } -} - -func nonStreamChatCompletion(w http.ResponseWriter) { - now := time.Now().Unix() - resp := map[string]any{ - "id": "chatcmpl-bench-001", - "object": "chat.completion", - "created": now, - "model": "gpt-4o-mini", - "choices": []map[string]any{ - { - "index": 0, - "message": map[string]any{ - "role": "assistant", - "content": "This is a benchmark response from the mock backend server. It contains enough text to be representative of a typical short AI response that would be returned in production use cases.", - }, - "finish_reason": "stop", - }, - }, - "usage": map[string]any{ - "prompt_tokens": 25, - "completion_tokens": 35, - "total_tokens": 60, - }, - } - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(resp); err != nil { - log.Printf("encode chat response: %v", err) - } -} - -var streamChunks = []string{ - "This ", "is ", "a ", "benchmark ", "response ", "from ", "the ", "mock ", - "backend ", "server. ", "It ", "contains ", "enough ", "text ", "to ", "be ", - "representative ", "of ", "a ", "typical ", "short ", "AI ", "response ", - "that ", "would ", "be ", "returned ", "in ", "production ", "use ", "cases.", -} - -func streamChatCompletion(w http.ResponseWriter) { - w.Header().Set("Content-Type", "text/event-stream") - w.Header().Set("Cache-Control", "no-cache") - w.Header().Set("Connection", "keep-alive") - - flusher, ok := w.(http.Flusher) - if !ok { - http.Error(w, "streaming not supported", 500) - return - } - - now := time.Now().Unix() - - // First chunk with role - chunk := fmt.Sprintf(`{"id":"chatcmpl-bench-001","object":"chat.completion.chunk","created":%d,"model":"gpt-4o-mini","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}`, now) - fmt.Fprintf(w, "data: %s\n\n", chunk) - flusher.Flush() - - // Content chunks - for _, token := range streamChunks { - chunk = fmt.Sprintf(`{"id":"chatcmpl-bench-001","object":"chat.completion.chunk","created":%d,"model":"gpt-4o-mini","choices":[{"index":0,"delta":{"content":"%s"},"finish_reason":null}]}`, now, token) - fmt.Fprintf(w, "data: %s\n\n", chunk) - flusher.Flush() - } - - // Final chunk - chunk = fmt.Sprintf(`{"id":"chatcmpl-bench-001","object":"chat.completion.chunk","created":%d,"model":"gpt-4o-mini","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":25,"completion_tokens":35,"total_tokens":60}}`, now) - fmt.Fprintf(w, "data: %s\n\n", chunk) - fmt.Fprintf(w, "data: [DONE]\n\n") - flusher.Flush() -} - -// ---------- Responses API ---------- - -func handleResponses(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - return - } - - var body struct { - Stream bool `json:"stream"` - } - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - http.Error(w, "invalid request body", http.StatusBadRequest) - return - } - - if body.Stream { - streamResponses(w) - } else { - nonStreamResponses(w) - } -} - -func nonStreamResponses(w http.ResponseWriter) { - now := time.Now().Unix() - resp := map[string]any{ - "id": "resp-bench-001", - "object": "response", - "created_at": now, - "model": "gpt-4o-mini", - "status": "completed", - "output": []map[string]any{ - { - "type": "message", - "id": "msg-bench-001", - "role": "assistant", - "content": []map[string]any{ - { - "type": "output_text", - "text": "This is a benchmark response from the mock backend server. It contains enough text to be representative of a typical short AI response.", - }, - }, - }, - }, - "usage": map[string]any{ - "input_tokens": 25, - "output_tokens": 35, - "total_tokens": 60, - }, - } - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(resp); err != nil { - log.Printf("encode responses response: %v", err) - } -} - -func streamResponses(w http.ResponseWriter) { - w.Header().Set("Content-Type", "text/event-stream") - w.Header().Set("Cache-Control", "no-cache") - w.Header().Set("Connection", "keep-alive") - - flusher, ok := w.(http.Flusher) - if !ok { - http.Error(w, "streaming not supported", 500) - return - } - - now := time.Now().Unix() - fullText := strings.Join(streamChunks, "") - - // response.created - fmt.Fprintf(w, "event: response.created\ndata: %s\n\n", - mustJSON(map[string]any{"id": "resp-bench-001", "object": "response", "created_at": now, "model": "gpt-4o-mini", "status": "in_progress", "output": []any{}})) - flusher.Flush() - - // response.output_item.added - fmt.Fprintf(w, "event: response.output_item.added\ndata: %s\n\n", - mustJSON(map[string]any{"type": "message", "id": "msg-bench-001", "role": "assistant", "content": []any{}})) - flusher.Flush() - - // response.content_part.added - fmt.Fprintf(w, "event: response.content_part.added\ndata: %s\n\n", - mustJSON(map[string]any{"type": "output_text", "text": ""})) - flusher.Flush() - - // text deltas - for _, token := range streamChunks { - fmt.Fprintf(w, "event: response.output_text.delta\ndata: %s\n\n", - mustJSON(map[string]any{"type": "response.output_text.delta", "delta": token})) - flusher.Flush() - } - - // response.output_text.done - fmt.Fprintf(w, "event: response.output_text.done\ndata: %s\n\n", - mustJSON(map[string]any{"type": "response.output_text.done", "text": fullText})) - flusher.Flush() - - // response.completed - fmt.Fprintf(w, "event: response.completed\ndata: %s\n\n", - mustJSON(map[string]any{ - "id": "resp-bench-001", "object": "response", "status": "completed", - "output": []map[string]any{{"type": "message", "id": "msg-bench-001", "role": "assistant", - "content": []map[string]any{{"type": "output_text", "text": fullText}}}}, - "usage": map[string]any{"input_tokens": 25, "output_tokens": 35, "total_tokens": 60}, - })) - flusher.Flush() -} - -// ---------- Models ---------- - -func handleModels(w http.ResponseWriter, _ *http.Request) { - resp := map[string]any{ - "object": "list", - "data": []map[string]any{ - {"id": "gpt-4o-mini", "object": "model", "owned_by": "openai", "created": time.Now().Unix()}, - }, - } - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(resp); err != nil { - log.Printf("encode models response: %v", err) - } -} - -func mustJSON(v any) string { - b, _ := json.Marshal(v) - return string(b) -} - -func writeJSONBytes(w http.ResponseWriter, status int, payload []byte) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - if _, err := w.Write(payload); err != nil { - log.Printf("write response: %v", err) - } -} diff --git a/docs/2026-03-23_benchmark_scripts/gateway-comparison/run-benchmark.sh b/docs/2026-03-23_benchmark_scripts/gateway-comparison/run-benchmark.sh deleted file mode 100755 index 9e4b97c95..000000000 --- a/docs/2026-03-23_benchmark_scripts/gateway-comparison/run-benchmark.sh +++ /dev/null @@ -1,289 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# ── Configuration ────────────────────────────────────────────────── -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" -RESULTS_DIR="$SCRIPT_DIR/results" -MOCK_PORT=9999 -GOMODEL_PORT=8081 -LITELLM_PORT=8082 - -N_REQUESTS=1000 # total requests per test -CONCURRENCY=50 # concurrent connections - -rm -rf "$RESULTS_DIR" -mkdir -p "$RESULTS_DIR" - -# ── Helpers ──────────────────────────────────────────────────────── -log() { printf "\n\033[1;34m>>> %s\033[0m\n" "$1"; } -err() { printf "\033[1;31mERROR: %s\033[0m\n" "$1" >&2; } - -timestamp_ms() { - python3 - <<'PY' -import time -print(int(time.time() * 1000)) -PY -} - -parse_hey_percentile() { - local hey_file=$1 percentile=$2 - awk -v pct="${percentile}%%" '$1 == pct && $2 == "in" { print $3; exit }' "$hey_file" -} - -GW_PID="" -MON_PID="" - -cleanup() { - log "Cleaning up..." - kill "$MOCK_PID" 2>/dev/null || true - [[ -n "$GW_PID" ]] && kill "$GW_PID" 2>/dev/null || true - [[ -n "$MON_PID" ]] && kill "$MON_PID" 2>/dev/null || true - pkill -f "litellm.*${LITELLM_PORT}" 2>/dev/null || true - rm -f /tmp/gomodel-bench.db - rm -rf /tmp/gomodel-bench-cache -} -trap cleanup EXIT - -wait_for_server() { - local url="$1" name="$2" timeout="${3:-15}" - for i in $(seq 1 "$timeout"); do - if curl -sf "$url" >/dev/null 2>&1; then - return 0 - fi - sleep 1 - done - err "$name did not start within ${timeout}s" - return 1 -} - -# Resource monitor: samples RSS (KB) and CPU% every 0.5s -start_monitor() { - local pid=$1 outfile=$2 - ( - echo "timestamp_ms,rss_kb,cpu_pct" > "$outfile" - while kill -0 "$pid" 2>/dev/null; do - read rss cpu <<< "$(ps -o rss=,pcpu= -p "$pid" 2>/dev/null || echo "0 0")" - echo "$(timestamp_ms),${rss// /},${cpu// /}" >> "$outfile" - sleep 0.5 - done - ) & - MON_PID=$! -} - -stop_monitor() { - [[ -n "$MON_PID" ]] && kill "$MON_PID" 2>/dev/null || true - [[ -n "$MON_PID" ]] && wait "$MON_PID" 2>/dev/null || true - MON_PID="" -} - -summarize_resources() { - local file=$1 - if [[ ! -f "$file" ]] || [[ $(wc -l < "$file") -le 1 ]]; then - echo '{"peak_rss_mb":0,"avg_rss_mb":0,"avg_cpu_pct":0}' - return - fi - awk -F, 'NR>1 && $2>0 { - sum_rss+=$2; sum_cpu+=$3; count++; - if($2>max_rss) max_rss=$2 - } END { - if(count>0) - printf "{\"peak_rss_mb\":%.1f,\"avg_rss_mb\":%.1f,\"avg_cpu_pct\":%.1f}\n", - max_rss/1024, sum_rss/count/1024, sum_cpu/count - else - print "{\"peak_rss_mb\":0,\"avg_rss_mb\":0,\"avg_cpu_pct\":0}" - }' "$file" -} - -# ── Generic benchmark functions ──────────────────────────────────── -run_nonstream_bench() { - local name=$1 port=$2 endpoint=$3 gw_pid=$4 model=$5 - local url="http://localhost:${port}${endpoint}" - - start_monitor "$gw_pid" "$RESULTS_DIR/${name}_resources.csv" - - log " Non-streaming: $url (model=$model)" - local body - if [[ "$endpoint" == *responses* ]]; then - body="{\"model\":\"${model}\",\"stream\":false,\"input\":\"Say hello for a benchmark test.\"}" - else - body="{\"model\":\"${model}\",\"stream\":false,\"messages\":[{\"role\":\"user\",\"content\":\"Say hello for a benchmark test.\"}]}" - fi - - hey -n "$N_REQUESTS" -c "$CONCURRENCY" \ - -m POST \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer sk-bench-test-key" \ - -d "$body" \ - "$url" > "$RESULTS_DIR/${name}_hey.txt" 2>&1 - - stop_monitor - - local hey_file="$RESULTS_DIR/${name}_hey.txt" - local rps avg p50 p95 p99 - rps=$(grep "Requests/sec:" "$hey_file" | awk '{print $2}' || echo "N/A") - avg=$(grep "Average:" "$hey_file" | head -1 | awk '{print $2}' || echo "N/A") - p50=$(parse_hey_percentile "$hey_file" "50" || echo "N/A") - p95=$(parse_hey_percentile "$hey_file" "95" || echo "N/A") - p99=$(parse_hey_percentile "$hey_file" "99" || echo "N/A") - - local resources - resources=$(summarize_resources "$RESULTS_DIR/${name}_resources.csv") - - echo "{\"name\":\"$name\",\"rps\":\"$rps\",\"avg\":\"$avg\",\"p50\":\"$p50\",\"p95\":\"$p95\",\"p99\":\"$p99\",\"resources\":$resources}" > "$RESULTS_DIR/${name}_summary.json" - - echo " RPS: $rps | Avg: $avg | p50: $p50 | p95: $p95 | p99: $p99" - echo " Resources: $resources" -} - -run_stream_bench() { - local name=$1 port=$2 endpoint_type=$3 gw_pid=$4 model=$5 - local url - if [[ "$endpoint_type" == "responses" ]]; then - url="http://localhost:${port}/v1/responses" - else - url="http://localhost:${port}/v1/chat/completions" - fi - - start_monitor "$gw_pid" "$RESULTS_DIR/${name}_resources.csv" - - log " Streaming: $url (model=$model)" - "$SCRIPT_DIR/stream-bench/stream-bench" \ - -url "$url" \ - -n "$N_REQUESTS" \ - -c "$CONCURRENCY" \ - -endpoint "$endpoint_type" \ - -model "$model" \ - -json "$RESULTS_DIR/${name}_stream.json" \ - 2>&1 | tee "$RESULTS_DIR/${name}_stream.txt" - - stop_monitor - - local resources - resources=$(summarize_resources "$RESULTS_DIR/${name}_resources.csv") - echo " Resources: $resources" - - if [[ -f "$RESULTS_DIR/${name}_stream.json" ]]; then - python3 -c " -import json -with open('$RESULTS_DIR/${name}_stream.json') as f: - d = json.load(f) -d['resources'] = $resources -with open('$RESULTS_DIR/${name}_stream.json', 'w') as f: - json.dump(d, f, indent=2) -" 2>/dev/null || true - fi -} - -# ── Skip build if binaries already exist ─────────────────────────── -if [[ ! -f "$SCRIPT_DIR/mock-backend/mock-server" ]]; then - log "Building mock backend..." - (cd "$SCRIPT_DIR/mock-backend" && go build -o mock-server .) -fi - -if [[ ! -f "$SCRIPT_DIR/stream-bench/stream-bench" ]]; then - log "Building streaming benchmark tool..." - (cd "$SCRIPT_DIR/stream-bench" && go build -o stream-bench .) -fi - -if [[ ! -f "$SCRIPT_DIR/gomodel-bin" ]]; then - log "Building GoModel..." - (cd "$PROJECT_ROOT" && go build -o "$SCRIPT_DIR/gomodel-bin" ./cmd/gomodel) -fi - -# ── Start mock backend ───────────────────────────────────────────── -log "Starting mock backend on :${MOCK_PORT}..." -MOCK_PORT=$MOCK_PORT "$SCRIPT_DIR/mock-backend/mock-server" & -MOCK_PID=$! -wait_for_server "http://localhost:${MOCK_PORT}/health" "Mock backend" - -# ══════════════════════════════════════════════════════════════════ -# BASELINE: Direct to mock (no gateway) -# ══════════════════════════════════════════════════════════════════ -log "=== BASELINE (direct to mock, no gateway) ===" - -hey -n "$N_REQUESTS" -c "$CONCURRENCY" \ - -m POST \ - -H "Content-Type: application/json" \ - -d '{"model":"gpt-4o-mini","stream":false,"messages":[{"role":"user","content":"Say hello for a benchmark test."}]}' \ - "http://localhost:${MOCK_PORT}/v1/chat/completions" > "$RESULTS_DIR/baseline_chat_nonstream_hey.txt" 2>&1 - -echo " Baseline non-stream:" -grep "Requests/sec:" "$RESULTS_DIR/baseline_chat_nonstream_hey.txt" -awk '/Latency distribution/,0' "$RESULTS_DIR/baseline_chat_nonstream_hey.txt" | head -8 - -"$SCRIPT_DIR/stream-bench/stream-bench" \ - -url "http://localhost:${MOCK_PORT}/v1/chat/completions" \ - -n "$N_REQUESTS" -c "$CONCURRENCY" -endpoint chat -model "gpt-4o-mini" \ - -json "$RESULTS_DIR/baseline_chat_stream.json" \ - 2>&1 | tee "$RESULTS_DIR/baseline_chat_stream.txt" - -# ══════════════════════════════════════════════════════════════════ -# GATEWAY 1: GoModel -# ══════════════════════════════════════════════════════════════════ -log "=== BENCHMARKING GoModel ===" - -OPENAI_API_KEY="sk-bench-test-key" \ -OPENAI_BASE_URL="http://localhost:${MOCK_PORT}/v1" \ -PORT=$GOMODEL_PORT \ -GOMODEL_MASTER_KEY="" \ -LOGGING_ENABLED=false \ -USAGE_ENABLED=false \ -STORAGE_TYPE=sqlite \ -SQLITE_PATH="/tmp/gomodel-bench.db" \ -GOMODEL_CACHE_DIR="/tmp/gomodel-bench-cache" \ -ADMIN_ENDPOINTS_ENABLED=false \ -ADMIN_UI_ENABLED=false \ -SWAGGER_ENABLED=false \ -"$SCRIPT_DIR/gomodel-bin" > "$RESULTS_DIR/gomodel_server.log" 2>&1 & -GW_PID=$! -wait_for_server "http://localhost:${GOMODEL_PORT}/health" "GoModel" -sleep 1 - -# Warm up -hey -n 100 -c 10 -m POST \ - -H "Content-Type: application/json" \ - -d '{"model":"gpt-4o-mini","stream":false,"messages":[{"role":"user","content":"warmup"}]}' \ - "http://localhost:${GOMODEL_PORT}/v1/chat/completions" >/dev/null 2>&1 - -run_nonstream_bench "gomodel_chat_nonstream" $GOMODEL_PORT "/v1/chat/completions" $GW_PID "gpt-4o-mini" -run_stream_bench "gomodel_chat_stream" $GOMODEL_PORT "chat" $GW_PID "gpt-4o-mini" -run_nonstream_bench "gomodel_resp_nonstream" $GOMODEL_PORT "/v1/responses" $GW_PID "gpt-4o-mini" -run_stream_bench "gomodel_resp_stream" $GOMODEL_PORT "responses" $GW_PID "gpt-4o-mini" - -kill $GW_PID 2>/dev/null || true; wait $GW_PID 2>/dev/null || true -GW_PID="" - -# ══════════════════════════════════════════════════════════════════ -# GATEWAY 2: LiteLLM -# ══════════════════════════════════════════════════════════════════ -log "=== BENCHMARKING LiteLLM ===" - -litellm --config "$SCRIPT_DIR/configs/litellm-config.yaml" \ - --port $LITELLM_PORT \ - --num_workers 4 \ - > "$RESULTS_DIR/litellm_server.log" 2>&1 & -GW_PID=$! -wait_for_server "http://localhost:${LITELLM_PORT}/health/liveliness" "LiteLLM" 30 -sleep 3 - -# Warm up -hey -n 50 -c 5 -m POST \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer sk-bench-test-key" \ - -d '{"model":"gpt-4o-mini","stream":false,"messages":[{"role":"user","content":"warmup"}]}' \ - "http://localhost:${LITELLM_PORT}/v1/chat/completions" >/dev/null 2>&1 - -run_nonstream_bench "litellm_chat_nonstream" $LITELLM_PORT "/v1/chat/completions" $GW_PID "gpt-4o-mini" -run_stream_bench "litellm_chat_stream" $LITELLM_PORT "chat" $GW_PID "gpt-4o-mini" -run_nonstream_bench "litellm_resp_nonstream" $LITELLM_PORT "/v1/responses" $GW_PID "gpt-4o-mini" -run_stream_bench "litellm_resp_stream" $LITELLM_PORT "responses" $GW_PID "gpt-4o-mini" - -kill $GW_PID 2>/dev/null || true; wait $GW_PID 2>/dev/null || true -pkill -f "litellm.*${LITELLM_PORT}" 2>/dev/null || true -GW_PID="" - -# ══════════════════════════════════════════════════════════════════ -log "=== ALL BENCHMARKS COMPLETE ===" -log "Results in: $RESULTS_DIR/" -ls -la "$RESULTS_DIR/" diff --git a/docs/2026-03-23_benchmark_scripts/gateway-comparison/stream-bench/main.go b/docs/2026-03-23_benchmark_scripts/gateway-comparison/stream-bench/main.go deleted file mode 100644 index cb5ba3657..000000000 --- a/docs/2026-03-23_benchmark_scripts/gateway-comparison/stream-bench/main.go +++ /dev/null @@ -1,253 +0,0 @@ -// Streaming SSE benchmark tool. -// Sends concurrent streaming requests and measures TTFB + total latency. -package main - -import ( - "bufio" - "bytes" - "context" - "encoding/json" - "flag" - "fmt" - "io" - "math" - "net/http" - "os" - "sort" - "strings" - "sync" - "time" -) - -type result struct { - TTFB time.Duration - Total time.Duration - Chunks int - HasError bool - Error string -} - -func main() { - url := flag.String("url", "", "Target URL") - n := flag.Int("n", 200, "Total requests") - c := flag.Int("c", 50, "Concurrency") - endpoint := flag.String("endpoint", "chat", "Endpoint: chat or responses") - model := flag.String("model", "gpt-4o-mini", "Model name to use in requests") - jsonOut := flag.String("json", "", "Write JSON results to file") - flag.Parse() - - if *url == "" { - fmt.Fprintln(os.Stderr, "Usage: stream-bench -url [-n 200] [-c 50] [-endpoint chat|responses] [-model gpt-4o-mini]") - os.Exit(1) - } - - body := buildRequestBody(*endpoint, *model) - results := make([]result, *n) - var wg sync.WaitGroup - sem := make(chan struct{}, *c) - - start := time.Now() - for i := range *n { - wg.Add(1) - sem <- struct{}{} - go func(idx int) { - defer wg.Done() - defer func() { <-sem }() - results[idx] = doStreamRequest(*url, body) - }(i) - } - wg.Wait() - wallTime := time.Since(start) - - // Collect stats - var ttfbs, totals []float64 - errors := 0 - totalChunks := 0 - for _, r := range results { - if r.HasError { - errors++ - continue - } - ttfbs = append(ttfbs, float64(r.TTFB.Microseconds())) - totals = append(totals, float64(r.Total.Microseconds())) - totalChunks += r.Chunks - } - - successful := len(ttfbs) - if successful == 0 { - fmt.Println("All requests failed!") - for i, r := range results { - if r.HasError && i < 5 { - fmt.Printf(" Error %d: %s\n", i, r.Error) - } - } - os.Exit(1) - } - - sort.Float64s(ttfbs) - sort.Float64s(totals) - - rps := float64(successful) / wallTime.Seconds() - - fmt.Printf("\n=== Streaming Benchmark Results ===\n") - fmt.Printf("URL: %s\n", *url) - fmt.Printf("Endpoint: %s\n", *endpoint) - fmt.Printf("Requests: %d total, %d successful, %d failed\n", *n, successful, errors) - fmt.Printf("Concurrency: %d\n", *c) - fmt.Printf("Wall time: %s\n", wallTime.Round(time.Millisecond)) - fmt.Printf("Throughput: %.2f req/s\n\n", rps) - - fmt.Printf(" TTFB (time to first byte):\n") - printPercentiles(" ", ttfbs) - - fmt.Printf("\n Total latency:\n") - printPercentiles(" ", totals) - - fmt.Printf("\n Avg chunks/response: %d\n", totalChunks/successful) - - if *jsonOut != "" { - writeJSON(*jsonOut, *endpoint, *n, *c, successful, errors, wallTime, rps, ttfbs, totals, totalChunks) - } -} - -func buildRequestBody(endpoint, model string) []byte { - var req any - if endpoint == "responses" { - req = map[string]any{ - "model": model, - "stream": true, - "input": "Say hello for a benchmark test.", - } - } else { - req = map[string]any{ - "model": model, - "stream": true, - "messages": []map[string]any{ - {"role": "user", "content": "Say hello for a benchmark test."}, - }, - } - } - b, _ := json.Marshal(req) - return b -} - -func doStreamRequest(url string, body []byte) result { - reqStart := time.Now() - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - req, _ := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer sk-bench-test-key") - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return result{HasError: true, Error: err.Error()} - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - b, _ := io.ReadAll(resp.Body) - return result{HasError: true, Error: fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(b[:min(len(b), 200)]))} - } - - var ttfb time.Duration - ttfbRecorded := false - chunks := 0 - - scanner := bufio.NewScanner(resp.Body) - for scanner.Scan() { - line := scanner.Text() - if len(line) > 6 && line[:6] == "data: " { - if !ttfbRecorded { - ttfb = time.Since(reqStart) - ttfbRecorded = true - } - chunks++ - // Detect end-of-stream markers - payload := line[6:] - if payload == "[DONE]" { - break - } - // For responses API: detect response.completed or empty-type final chunk - if strings.Contains(payload, `"response.completed"`) || - strings.Contains(payload, `"response.output_text.done"`) { - break - } - } - } - - total := time.Since(reqStart) - if !ttfbRecorded { - ttfb = total - } - return result{TTFB: ttfb, Total: total, Chunks: chunks} -} - -func percentile(sorted []float64, p float64) float64 { - if len(sorted) == 0 { - return 0 - } - idx := p / 100 * float64(len(sorted)-1) - lower := int(math.Floor(idx)) - upper := int(math.Ceil(idx)) - if lower == upper { - return sorted[lower] - } - frac := idx - float64(lower) - return sorted[lower]*(1-frac) + sorted[upper]*frac -} - -func printPercentiles(prefix string, data []float64) { - fmt.Printf("%sp50: %s\n", prefix, fmtUs(percentile(data, 50))) - fmt.Printf("%sp95: %s\n", prefix, fmtUs(percentile(data, 95))) - fmt.Printf("%sp99: %s\n", prefix, fmtUs(percentile(data, 99))) - fmt.Printf("%smin: %s\n", prefix, fmtUs(data[0])) - fmt.Printf("%smax: %s\n", prefix, fmtUs(data[len(data)-1])) - avg := 0.0 - for _, v := range data { - avg += v - } - avg /= float64(len(data)) - fmt.Printf("%savg: %s\n", prefix, fmtUs(avg)) -} - -func fmtUs(us float64) string { - if us < 1000 { - return fmt.Sprintf("%.0fus", us) - } - return fmt.Sprintf("%.2fms", us/1000) -} - -func writeJSON(path, endpoint string, n, c, ok, errs int, wall time.Duration, rps float64, ttfbs, totals []float64, chunks int) { - data := map[string]any{ - "endpoint": endpoint, - "requests": n, - "concurrency": c, - "successful": ok, - "errors": errs, - "wall_time_ms": wall.Milliseconds(), - "rps": rps, - "ttfb": map[string]any{ - "p50_us": percentile(ttfbs, 50), - "p95_us": percentile(ttfbs, 95), - "p99_us": percentile(ttfbs, 99), - "min_us": ttfbs[0], - "max_us": ttfbs[len(ttfbs)-1], - }, - "total_latency": map[string]any{ - "p50_us": percentile(totals, 50), - "p95_us": percentile(totals, 95), - "p99_us": percentile(totals, 99), - "min_us": totals[0], - "max_us": totals[len(totals)-1], - }, - "avg_chunks": chunks / max(ok, 1), - } - b, _ := json.MarshalIndent(data, "", " ") - if err := os.WriteFile(path, b, 0o644); err != nil { - fmt.Fprintf(os.Stderr, "write JSON output %s: %v\n", path, err) - os.Exit(1) - } -} diff --git a/docs/2026-03-23_benchmark_scripts/generate_benchmark_artifacts.py b/docs/2026-03-23_benchmark_scripts/generate_benchmark_artifacts.py deleted file mode 100755 index 8d393cb15..000000000 --- a/docs/2026-03-23_benchmark_scripts/generate_benchmark_artifacts.py +++ /dev/null @@ -1,450 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import csv -import json -import re -import shutil -from pathlib import Path - -import matplotlib.pyplot as plt -import numpy as np - - -WORKLOADS = [ - { - "key": "chat_nonstream", - "label": "Chat non-stream", - "endpoint": "chat", - "mode": "nonstream", - "gates": { - "baseline": {"hey": "baseline_chat_nonstream_hey.txt"}, - "gomodel": { - "hey": "gomodel_chat_nonstream_hey.txt", - "resources": "gomodel_chat_nonstream_resources.csv", - }, - "litellm": { - "hey": "litellm_chat_nonstream_hey.txt", - "resources": "litellm_chat_nonstream_resources.csv", - }, - }, - }, - { - "key": "chat_stream", - "label": "Chat stream", - "endpoint": "chat", - "mode": "stream", - "gates": { - "baseline": {"stream": "baseline_chat_stream.json"}, - "gomodel": { - "stream": "gomodel_chat_stream_stream.json", - "resources": "gomodel_chat_stream_resources.csv", - }, - "litellm": { - "stream": "litellm_chat_stream_stream.json", - "resources": "litellm_chat_stream_resources.csv", - }, - }, - }, - { - "key": "responses_nonstream", - "label": "Responses non-stream", - "endpoint": "responses", - "mode": "nonstream", - "gates": { - "gomodel": { - "hey": "gomodel_resp_nonstream_hey.txt", - "resources": "gomodel_resp_nonstream_resources.csv", - }, - "litellm": { - "hey": "litellm_resp_nonstream_hey.txt", - "resources": "litellm_resp_nonstream_resources.csv", - }, - }, - }, - { - "key": "responses_stream", - "label": "Responses stream", - "endpoint": "responses", - "mode": "stream", - "gates": { - "gomodel": { - "stream": "gomodel_resp_stream_stream.json", - "resources": "gomodel_resp_stream_resources.csv", - }, - "litellm": { - "stream": "litellm_resp_stream_stream.json", - "resources": "litellm_resp_stream_resources.csv", - }, - }, - }, -] - -COLORS = {"gomodel": "#0E7490", "litellm": "#F97316", "baseline": "#64748B"} -BLOG_FILENAMES = { - "dashboard": "gomodel-vs-litellm-march-2026-dashboard.png", - "throughput": "gomodel-vs-litellm-march-2026-throughput.png", - "latency": "gomodel-vs-litellm-march-2026-latency.png", - "memory": "gomodel-vs-litellm-march-2026-memory.png", - "speedup": "gomodel-vs-litellm-march-2026-speedup.png", -} - - -def parse_hey_output(raw: str) -> dict: - requests_per_sec = _extract_float(raw, r"Requests/sec:\s+([0-9.]+)") - average_secs = _extract_float(raw, r"Average:\s+([0-9.]+)\s+secs") - latency_ms = {"avg": round(average_secs * 1000.0, 4)} - for percentile in ("50", "95", "99"): - secs = _extract_float(raw, rf"{percentile}%% in ([0-9.]+) secs") - latency_ms[f"p{percentile}"] = round(secs * 1000.0, 4) - return {"requests_per_sec": requests_per_sec, "latency_ms": latency_ms} - - -def normalize_streaming_result(raw: dict) -> dict: - return { - "requests_per_sec": float(raw["rps"]), - "avg_chunks": int(raw["avg_chunks"]), - "ttfb_ms": _latency_block_to_ms(raw["ttfb"]), - "total_latency_ms": _latency_block_to_ms(raw["total_latency"]), - } - - -def parse_resources_csv(path: Path) -> dict: - if not path.exists(): - return {} - rss_values = [] - cpu_values = [] - with path.open("r", encoding="utf-8", newline="") as handle: - reader = csv.DictReader(handle) - for row in reader: - rss_kb = float(row["rss_kb"]) - cpu_pct = float(row["cpu_pct"]) - if rss_kb > 0: - rss_values.append(rss_kb) - cpu_values.append(cpu_pct) - if not rss_values: - return {} - return { - "peak_rss_mb": round(max(rss_values) / 1024.0, 1), - "avg_rss_mb": round(sum(rss_values) / len(rss_values) / 1024.0, 1), - "avg_cpu_pct": round(sum(cpu_values) / len(cpu_values), 1) if cpu_values else 0.0, - } - - -def load_results(results_dir: Path) -> dict: - normalized = {} - for workload in WORKLOADS: - rows = {} - for gateway, files in workload["gates"].items(): - row = { - "gateway": gateway, - "workload_key": workload["key"], - "workload_label": workload["label"], - "endpoint": workload["endpoint"], - "mode": workload["mode"], - } - if "hey" in files: - raw = (results_dir / files["hey"]).read_text(encoding="utf-8") - row.update(parse_hey_output(raw)) - if "stream" in files: - raw = json.loads((results_dir / files["stream"]).read_text(encoding="utf-8")) - row.update(normalize_streaming_result(raw)) - if "resources" in files: - row["resources"] = parse_resources_csv(results_dir / files["resources"]) - rows[gateway] = row - normalized[workload["key"]] = rows - return normalized - - -def build_summary(dataset: dict) -> dict: - comparisons = {} - for workload_key, rows in dataset.items(): - gomodel = rows.get("gomodel") - litellm = rows.get("litellm") - baseline = rows.get("baseline") - if gomodel and litellm: - if gomodel["mode"] == "nonstream": - gomodel_latency = gomodel["latency_ms"]["p50"] - litellm_latency = litellm["latency_ms"]["p50"] - else: - gomodel_latency = gomodel["ttfb_ms"]["p50"] - litellm_latency = litellm["ttfb_ms"]["p50"] - comparisons[workload_key] = { - "throughput_speedup_vs_litellm": round( - gomodel["requests_per_sec"] / litellm["requests_per_sec"], 2 - ), - "latency_advantage_vs_litellm": round(litellm_latency / gomodel_latency, 2), - "memory_advantage_vs_litellm": round( - litellm["resources"]["peak_rss_mb"] / gomodel["resources"]["peak_rss_mb"], 2 - ), - } - if baseline and "latency_ms" in baseline and gomodel: - comparisons.setdefault(workload_key, {}) - comparisons[workload_key]["gomodel_added_latency_vs_baseline_ms"] = round( - gomodel["latency_ms"]["p50"] - baseline["latency_ms"]["p50"], 2 - ) - comparisons[workload_key]["litellm_added_latency_vs_baseline_ms"] = round( - litellm["latency_ms"]["p50"] - baseline["latency_ms"]["p50"], 2 - ) - if baseline and "ttfb_ms" in baseline and gomodel: - comparisons.setdefault(workload_key, {}) - comparisons[workload_key]["gomodel_added_ttfb_vs_baseline_ms"] = round( - gomodel["ttfb_ms"]["p50"] - baseline["ttfb_ms"]["p50"], 2 - ) - comparisons[workload_key]["litellm_added_ttfb_vs_baseline_ms"] = round( - litellm["ttfb_ms"]["p50"] - baseline["ttfb_ms"]["p50"], 2 - ) - - return {"results": dataset, "comparisons": comparisons} - - -def render_charts(summary: dict, output_dir: Path) -> dict: - charts_dir = output_dir / "charts" - charts_dir.mkdir(parents=True, exist_ok=True) - plt.style.use("seaborn-v0_8-whitegrid") - - throughput_path = charts_dir / BLOG_FILENAMES["throughput"] - latency_path = charts_dir / BLOG_FILENAMES["latency"] - memory_path = charts_dir / BLOG_FILENAMES["memory"] - speedup_path = charts_dir / BLOG_FILENAMES["speedup"] - dashboard_path = charts_dir / BLOG_FILENAMES["dashboard"] - - _plot_grouped_metric( - summary["results"], - throughput_path, - title="Throughput by workload", - value_getter=lambda row: row["requests_per_sec"], - ylabel="Requests / second", - ) - _plot_grouped_metric( - summary["results"], - latency_path, - title="Median latency by workload", - value_getter=lambda row: row["latency_ms"]["p50"] if row["mode"] == "nonstream" else row["ttfb_ms"]["p50"], - ylabel="Milliseconds", - subtitle="Non-stream uses p50 latency. Stream uses p50 TTFB.", - ) - _plot_grouped_metric( - summary["results"], - memory_path, - title="Peak RSS by workload", - value_getter=lambda row: row["resources"]["peak_rss_mb"], - ylabel="MB", - ) - _plot_speedup(summary["comparisons"], speedup_path) - _plot_dashboard(summary, dashboard_path) - - return { - "dashboard": str(dashboard_path), - "throughput": str(throughput_path), - "latency": str(latency_path), - "memory": str(memory_path), - "speedup": str(speedup_path), - } - - -def copy_blog_charts(charts: dict, blog_public_dir: Path) -> None: - blog_public_dir.mkdir(parents=True, exist_ok=True) - for chart_path in charts.values(): - src = Path(chart_path) - shutil.copy2(src, blog_public_dir / src.name) - - -def _extract_float(raw: str, pattern: str) -> float: - match = re.search(pattern, raw) - if not match: - raise ValueError(f"pattern not found: {pattern}") - return float(match.group(1)) - - -def _latency_block_to_ms(block: dict) -> dict: - return { - "p50": round(float(block["p50_us"]) / 1000.0, 5), - "p95": round(float(block["p95_us"]) / 1000.0, 5), - "p99": round(float(block["p99_us"]) / 1000.0, 5), - } - - -def _plot_grouped_metric(results: dict, output_path: Path, title: str, value_getter, ylabel: str, subtitle: str | None = None) -> None: - categories = [] - gomodel_values = [] - litellm_values = [] - baseline_values = [] - baseline_present = False - for workload_key in [item["key"] for item in WORKLOADS]: - rows = results[workload_key] - categories.append(rows["gomodel"]["workload_label"] if "gomodel" in rows else rows["litellm"]["workload_label"]) - gomodel_values.append(value_getter(rows["gomodel"])) - litellm_values.append(value_getter(rows["litellm"])) - if "baseline" in rows: - try: - baseline_value = value_getter(rows["baseline"]) - except KeyError: - baseline_value = np.nan - baseline_values.append(baseline_value) - if not np.isnan(baseline_value): - baseline_present = True - else: - baseline_values.append(np.nan) - - x = np.arange(len(categories)) - width = 0.24 if baseline_present else 0.32 - fig, ax = plt.subplots(figsize=(12, 6.4), constrained_layout=True) - if baseline_present: - baseline_bars = ax.bar(x - width, baseline_values, width, label="Direct baseline", color=COLORS["baseline"]) - _label_bars(ax, baseline_bars) - gomodel_bars = ax.bar(x, gomodel_values, width, label="GoModel", color=COLORS["gomodel"]) - litellm_bars = ax.bar(x + width, litellm_values, width, label="LiteLLM", color=COLORS["litellm"]) - _label_bars(ax, gomodel_bars) - _label_bars(ax, litellm_bars) - - ax.set_title(title, fontsize=16, weight="bold") - if subtitle: - ax.text(0.0, 1.02, subtitle, transform=ax.transAxes, fontsize=10, color="#475569") - ax.set_ylabel(ylabel) - ax.set_xticks(x, categories) - ax.legend(frameon=True) - ax.grid(axis="y", alpha=0.25) - fig.savefig(output_path, dpi=180) - plt.close(fig) - - -def _plot_speedup(comparisons: dict, output_path: Path) -> None: - categories = [item["label"] for item in WORKLOADS] - throughput = [comparisons[item["key"]]["throughput_speedup_vs_litellm"] for item in WORKLOADS] - latency = [comparisons[item["key"]]["latency_advantage_vs_litellm"] for item in WORKLOADS] - - x = np.arange(len(categories)) - width = 0.34 - fig, ax = plt.subplots(figsize=(12, 6.4), constrained_layout=True) - bars_a = ax.bar(x - width / 2, throughput, width, label="Throughput speedup", color="#0891B2") - bars_b = ax.bar(x + width / 2, latency, width, label="Lower-latency factor", color="#14B8A6") - _label_bars(ax, bars_a, suffix="x") - _label_bars(ax, bars_b, suffix="x") - - ax.axhline(1.0, color="#94A3B8", linewidth=1, linestyle="--") - ax.set_title("GoModel advantage vs LiteLLM", fontsize=16, weight="bold") - ax.set_ylabel("Factor") - ax.set_xticks(x, categories) - ax.legend(frameon=True) - ax.grid(axis="y", alpha=0.25) - fig.savefig(output_path, dpi=180) - plt.close(fig) - - -def _plot_dashboard(summary: dict, output_path: Path) -> None: - categories = [item["label"] for item in WORKLOADS] - throughput = [summary["results"][item["key"]]["gomodel"]["requests_per_sec"] for item in WORKLOADS] - throughput_lite = [summary["results"][item["key"]]["litellm"]["requests_per_sec"] for item in WORKLOADS] - latency = [ - summary["results"][item["key"]]["gomodel"]["latency_ms"]["p50"] - if item["mode"] == "nonstream" - else summary["results"][item["key"]]["gomodel"]["ttfb_ms"]["p50"] - for item in WORKLOADS - ] - latency_lite = [ - summary["results"][item["key"]]["litellm"]["latency_ms"]["p50"] - if item["mode"] == "nonstream" - else summary["results"][item["key"]]["litellm"]["ttfb_ms"]["p50"] - for item in WORKLOADS - ] - memory = [summary["results"][item["key"]]["gomodel"]["resources"]["peak_rss_mb"] for item in WORKLOADS] - memory_lite = [summary["results"][item["key"]]["litellm"]["resources"]["peak_rss_mb"] for item in WORKLOADS] - speedup = [summary["comparisons"][item["key"]]["throughput_speedup_vs_litellm"] for item in WORKLOADS] - - x = np.arange(len(categories)) - width = 0.36 - fig, axes = plt.subplots(2, 2, figsize=(15, 10), constrained_layout=True) - fig.suptitle("GoModel vs LiteLLM: March 23, 2026 localhost benchmark", fontsize=18, weight="bold") - - axes[0, 0].bar(x - width / 2, throughput, width, label="GoModel", color=COLORS["gomodel"]) - axes[0, 0].bar(x + width / 2, throughput_lite, width, label="LiteLLM", color=COLORS["litellm"]) - axes[0, 0].set_title("Throughput") - axes[0, 0].set_xticks(x, categories) - axes[0, 0].set_ylabel("Req/s") - axes[0, 0].legend(frameon=True) - - axes[0, 1].bar(x - width / 2, latency, width, label="GoModel", color=COLORS["gomodel"]) - axes[0, 1].bar(x + width / 2, latency_lite, width, label="LiteLLM", color=COLORS["litellm"]) - axes[0, 1].set_title("Median latency / TTFB") - axes[0, 1].set_xticks(x, categories) - axes[0, 1].set_ylabel("ms") - - axes[1, 0].bar(x - width / 2, memory, width, label="GoModel", color=COLORS["gomodel"]) - axes[1, 0].bar(x + width / 2, memory_lite, width, label="LiteLLM", color=COLORS["litellm"]) - axes[1, 0].set_title("Peak RSS") - axes[1, 0].set_xticks(x, categories) - axes[1, 0].set_ylabel("MB") - - speedup_bars = axes[1, 1].bar(x, speedup, width=0.5, color="#14B8A6") - _label_bars(axes[1, 1], speedup_bars, suffix="x") - axes[1, 1].axhline(1.0, color="#94A3B8", linewidth=1, linestyle="--") - axes[1, 1].set_title("Throughput speedup vs LiteLLM") - axes[1, 1].set_xticks(x, categories) - axes[1, 1].set_ylabel("factor") - - for ax in axes.flat: - ax.grid(axis="y", alpha=0.25) - - fig.savefig(output_path, dpi=180) - plt.close(fig) - - -def _label_bars(ax, bars, suffix: str = "") -> None: - for bar in bars: - height = bar.get_height() - if np.isnan(height): - continue - label = f"{height:.1f}{suffix}" if height < 100 else f"{height:,.0f}{suffix}" - ax.annotate( - label, - (bar.get_x() + bar.get_width() / 2, height), - textcoords="offset points", - xytext=(0, 4), - ha="center", - fontsize=8, - ) - - -def main() -> None: - parser = argparse.ArgumentParser(description="Normalize the March 23 benchmark artifacts and generate blog charts.") - parser.add_argument( - "--results-dir", - type=Path, - required=True, - help="Path to docs/2026-03-23_benchmark_scripts/gateway-comparison/results", - ) - parser.add_argument( - "--output-dir", - type=Path, - default=Path(__file__).resolve().parent / "output", - help="Directory for normalized JSON and generated charts", - ) - parser.add_argument( - "--blog-public-dir", - type=Path, - help="Optional blog public charts directory to copy generated chart images into", - ) - args = parser.parse_args() - - results_dir = args.results_dir.resolve() - output_dir = args.output_dir.resolve() - output_dir.mkdir(parents=True, exist_ok=True) - - dataset = load_results(results_dir) - summary = build_summary(dataset) - summary["source_results_dir"] = str(results_dir) - summary_path = output_dir / "benchmark_summary.json" - summary_path.write_text(json.dumps(summary, indent=2), encoding="utf-8") - - charts = render_charts(summary, output_dir) - if args.blog_public_dir: - copy_blog_charts(charts, args.blog_public_dir.resolve()) - - print(f"Wrote normalized summary to {summary_path}") - print(f"Generated charts in {output_dir / 'charts'}") - if args.blog_public_dir: - print(f"Copied blog chart assets to {args.blog_public_dir.resolve()}") - - -if __name__ == "__main__": - main() diff --git a/docs/2026-03-23_benchmark_scripts/run.sh b/docs/2026-03-23_benchmark_scripts/run.sh deleted file mode 100755 index 4f0ba5ca0..000000000 --- a/docs/2026-03-23_benchmark_scripts/run.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -RAW_BENCH_DIR="$SCRIPT_DIR/gateway-comparison" -RESULTS_DIR="${RESULTS_DIR:-$RAW_BENCH_DIR/results}" -OUTPUT_DIR="${OUTPUT_DIR:-$SCRIPT_DIR/output}" - -if [[ "${RUN_BENCHMARK:-0}" == "1" ]]; then - echo "Running raw benchmark suite..." - bash "$RAW_BENCH_DIR/run-benchmark.sh" -fi - -if [[ ! -d "$RESULTS_DIR" ]]; then - echo "Results directory not found: $RESULTS_DIR" >&2 - echo "Run with RUN_BENCHMARK=1 or set RESULTS_DIR to an existing benchmark result directory." >&2 - exit 1 -fi - -echo "Generating normalized benchmark artifacts..." -CMD=( - python3 - "$SCRIPT_DIR/generate_benchmark_artifacts.py" - --results-dir "$RESULTS_DIR" - --output-dir "$OUTPUT_DIR" -) - -if [[ -n "${BLOG_PUBLIC_DIR:-}" ]]; then - CMD+=(--blog-public-dir "$BLOG_PUBLIC_DIR") -fi - -"${CMD[@]}" diff --git a/docs/2026-03-23_benchmark_scripts/test_generate_benchmark_artifacts.py b/docs/2026-03-23_benchmark_scripts/test_generate_benchmark_artifacts.py deleted file mode 100644 index b8141e21b..000000000 --- a/docs/2026-03-23_benchmark_scripts/test_generate_benchmark_artifacts.py +++ /dev/null @@ -1,67 +0,0 @@ -import sys -import unittest -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parent)) - -from generate_benchmark_artifacts import normalize_streaming_result, parse_hey_output - - -HEY_SAMPLE = """ -Summary: - Total:\t0.0414 secs - Slowest:\t0.0084 secs - Fastest:\t0.0002 secs - Average:\t0.0020 secs - Requests/sec:\t24128.7268 - -Latency distribution: - 10%% in 0.0009 secs - 25%% in 0.0015 secs - 50%% in 0.0019 secs - 75%% in 0.0022 secs - 90%% in 0.0027 secs - 95%% in 0.0039 secs - 99%% in 0.0079 secs -""" - -STREAMING_SAMPLE = { - "avg_chunks": 34, - "rps": 3929.187537806888, - "ttfb": { - "p50_us": 12127, - "p95_us": 14262.949999999999, - "p99_us": 17257.29, - }, - "total_latency": { - "p50_us": 12662, - "p95_us": 14870.1, - "p99_us": 17440.979999999996, - }, -} - - -class ParseHeyOutputTests(unittest.TestCase): - def test_parses_numeric_latency_percentiles_from_hey_output(self) -> None: - metrics = parse_hey_output(HEY_SAMPLE) - - self.assertAlmostEqual(metrics["requests_per_sec"], 24128.7268) - self.assertAlmostEqual(metrics["latency_ms"]["avg"], 2.0) - self.assertAlmostEqual(metrics["latency_ms"]["p50"], 1.9) - self.assertAlmostEqual(metrics["latency_ms"]["p95"], 3.9) - self.assertAlmostEqual(metrics["latency_ms"]["p99"], 7.9) - - -class NormalizeStreamingResultTests(unittest.TestCase): - def test_converts_streaming_microseconds_to_milliseconds(self) -> None: - metrics = normalize_streaming_result(STREAMING_SAMPLE) - - self.assertAlmostEqual(metrics["requests_per_sec"], 3929.187537806888) - self.assertAlmostEqual(metrics["ttfb_ms"]["p50"], 12.127) - self.assertAlmostEqual(metrics["ttfb_ms"]["p95"], 14.26295) - self.assertAlmostEqual(metrics["total_latency_ms"]["p99"], 17.44098) - self.assertEqual(metrics["avg_chunks"], 34) - - -if __name__ == "__main__": - unittest.main() From b78a534cb409d39ff04433358f7426596a270e89 Mon Sep 17 00:00:00 2001 From: "Jakub A. W" Date: Wed, 26 Aug 2026 00:00:23 +0200 Subject: [PATCH 3/7] docs(playground): fix Mintlify icon --- docs/features/playground.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/playground.mdx b/docs/features/playground.mdx index 66a1a5b32..e46ea4a9a 100644 --- a/docs/features/playground.mdx +++ b/docs/features/playground.mdx @@ -1,7 +1,7 @@ --- title: "Playground" description: "Try any model through the gateway from the dashboard and inspect the exact request and response JSON." -icon: "flask" +icon: "flask-conical" keywords: ["playground", "dashboard", "chat completions", "responses", "messages", "streaming"] --- From b732e2df81efa7862b5899626e2ff3b76ef93e5e Mon Sep 17 00:00:00 2001 From: "Jakub A. W" Date: Wed, 26 Aug 2026 00:06:41 +0200 Subject: [PATCH 4/7] build(go): align Alpine and lint tooling --- .github/workflows/test.yml | 9 ++------- Dockerfile | 2 +- Makefile | 2 +- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7d936b099..9d3a7acc7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,13 +31,8 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v9 with: - # Build the linter with Go 1.27 until upstream publishes binaries built - # with Go 1.27; older toolchains reject modules targeting newer Go. - install-mode: goinstall - # v2.12+ ships a staticcheck that understands modern Go control flow. - # v2.10's staticcheck mis-analyzed `if x == nil { t.Fatal() }` guards - # under GO_VERSION 1.26.4 and emitted false-positive SA5011 warnings. - version: v2.12.2 + # v2.13+ supports Go 1.27 and bundles a compatible staticcheck. + version: v2.13.1 # The shared analysis cache went bad on 2026-08-25: staticcheck lost # t.Fatal no-return facts and every run flagged a different set of # SA5011 false positives in untouched files, on PR branches and main diff --git a/Dockerfile b/Dockerfile index e98f949de..cc536dfc8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build stage — run on the build host's native arch for speed, cross-compile for target -FROM --platform=$BUILDPLATFORM golang:1.27.0-alpine3.23 AS builder +FROM --platform=$BUILDPLATFORM golang:1.27.0-alpine3.24 AS builder ARG TARGETOS ARG TARGETARCH diff --git a/Makefile b/Makefile index 92340c284..a9a67bb48 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ LDFLAGS := -X "github.com/enterpilot/gomodel/internal/version.Version=$(VERSION) -X "github.com/enterpilot/gomodel/internal/version.Date=$(DATE)" install-tools: - @command -v golangci-lint > /dev/null 2>&1 || (echo "Installing golangci-lint..." && go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2) + @command -v golangci-lint > /dev/null 2>&1 || (echo "Installing golangci-lint..." && go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.13.1) @command -v pre-commit > /dev/null 2>&1 || (echo "Installing pre-commit..." && pip install pre-commit==4.5.1) @echo "All tools are ready" From 0954623d856ff47df95f731e59a9d4fdc36f58f0 Mon Sep 17 00:00:00 2001 From: "Jakub A. W" Date: Wed, 26 Aug 2026 00:09:13 +0200 Subject: [PATCH 5/7] build(tools): enforce pinned golangci-lint --- Makefile | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index a9a67bb48..84d65cbe8 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,8 @@ SWAGGER_ENABLED ?= true # Build tags covering every file the linter and fixers must see. Without these, # tag-gated files (tests/e2e, tests/integration, tests/contract) are skipped. BUILD_TAGS ?= swagger,e2e,integration,contract +GOLANGCI_LINT_VERSION := 2.13.1 +GOLANGCI_LINT ?= $(shell go env GOPATH)/bin/golangci-lint # Linker flags to inject version info LDFLAGS := -X "github.com/enterpilot/gomodel/internal/version.Version=$(VERSION)" \ @@ -20,7 +22,12 @@ LDFLAGS := -X "github.com/enterpilot/gomodel/internal/version.Version=$(VERSION) -X "github.com/enterpilot/gomodel/internal/version.Date=$(DATE)" install-tools: - @command -v golangci-lint > /dev/null 2>&1 || (echo "Installing golangci-lint..." && go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.13.1) + @installed_version="$$($(GOLANGCI_LINT) version 2>/dev/null || true)"; \ + case "$$installed_version" in \ + *"version $(GOLANGCI_LINT_VERSION) "*) ;; \ + *) echo "Installing golangci-lint v$(GOLANGCI_LINT_VERSION)..."; \ + GOBIN="$(dir $(GOLANGCI_LINT))" go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v$(GOLANGCI_LINT_VERSION) ;; \ + esac @command -v pre-commit > /dev/null 2>&1 || (echo "Installing pre-commit..." && pip install pre-commit==4.5.1) @echo "All tools are ready" @@ -142,12 +149,12 @@ docs-openapi: # Run linter lint: - golangci-lint run --build-tags=$(BUILD_TAGS) ./cmd/... ./config/... ./ext/... ./internal/... ./run/... ./tests/... + $(GOLANGCI_LINT) run --build-tags=$(BUILD_TAGS) ./cmd/... ./config/... ./ext/... ./internal/... ./run/... ./tests/... # Run linter with auto-fix. Mirrors `lint`: same tags, same packages, so the # autofix pass cannot silently skip the tag-gated files under tests/. lint-fix: - golangci-lint run --fix --build-tags=$(BUILD_TAGS) ./cmd/... ./config/... ./ext/... ./internal/... ./run/... ./tests/... + $(GOLANGCI_LINT) run --fix --build-tags=$(BUILD_TAGS) ./cmd/... ./config/... ./ext/... ./internal/... ./run/... ./tests/... # Report modernizations go fix would apply, without touching the tree. # Exits non-zero when the tree has drifted; run `make fix` to apply. From fc81cdd236b777fe40c77f3389a1cea2250d5554 Mon Sep 17 00:00:00 2001 From: "Jakub A. W" Date: Wed, 26 Aug 2026 00:29:19 +0200 Subject: [PATCH 6/7] ci(codeql): build analysis with project Go version --- .github/workflows/codeql.yml | 53 ++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..bdcf5151f --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,53 @@ +name: CodeQL + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: "31 3 * * 1" + +permissions: + contents: read + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + permissions: + security-events: write + packages: read + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: go + build-mode: manual + - language: javascript-typescript + build-mode: none + + steps: + - uses: actions/checkout@v6 + + - name: Set up Go + if: matrix.language == 'go' + uses: actions/setup-go@v7 + with: + go-version-file: go.mod + cache: true + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + + - name: Build Go application + if: matrix.language == 'go' + run: go build ./... + + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@v4 From 5b8067ba41ac705ebd8313dad15d85ae1033ea24 Mon Sep 17 00:00:00 2001 From: "Jakub A. W" Date: Wed, 26 Aug 2026 00:32:59 +0200 Subject: [PATCH 7/7] ci(codeql): include quality queries --- .github/workflows/codeql.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index bdcf5151f..b763a9355 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -44,6 +44,7 @@ jobs: with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} + queries: security-and-quality - name: Build Go application if: matrix.language == 'go'