ferrogw init 只打印一次 master key 且不落盘,需自行保存;服务仅在设置 GATEWAY_CONFIG 时才读取配置文件;用 go install 构建的二进制不带控制台,访问 / 只返回占位页。

ai-gateway
ferro-labsApache-2.0活跃维护Go 编写的高性能 AI 网关,统一接入 30+ 家 LLM 服务商,内置缓存、护栏、A/B 测试和成本控制,可替代 LiteLLM 与 Kong AI Gateway。
Unified AI Gateway for 30+ LLMs (OpenAI, Anthropic, Bedrock, Azure etc) with Caching, Guardrails, A/B test & cost controls. Go-native Fastest & Scalable AI Gateway LiteLLM & Kong AI Gateway alternative.
curl -fsSL https://get.ferrolabs.ai | sh
ferrogw init
export GATEWAY_CONFIG=./config.yaml
export MASTER_KEY=fgw_your-master-key
ferrogw serveREADME 未说明显存或 GPU 要求;它是 Go 单静态二进制,无需外部服务,基础内存约 32 MB,可跑在普通服务器或容器上。
Go 写的 OpenAI 兼容 LLM 网关,用一个 API 路由 30 家模型供应商
Go 编写的高性能 AI 网关,统一接入 30+ 家 LLM 服务商,内置缓存、护栏、A/B 测试和成本控制,可替代 LiteLLM 与 Kong AI Gateway。
核心能力
- 提供 8 种路由策略,含 fallback、负载均衡、成本优化与 A/B 测试
- 内置 11 个护栏插件,覆盖 PII 脱敏、注入防护与限流预算
- 以 OpenAI 兼容接口统一转发 30 家供应商的聊天与流式请求
- 内置 MCP 客户端,可注入工具并驱动 tool_calls 循环
- 编译进二进制的运维控制台,查看流量、花费与请求日志
容易踩的坑
- v1.6.0最新正式版2026-10-30 · 2026-10-30 11:05 UTC
[1.6.0] — 2026-10-30
Added
pkg/cache.Fingerprintexposes versioned, namespace-scoped request keys for exact-match completion caches, using the response-cache plugin's encoder.mcp.NewClientWithOptionssupports a caller-supplied HTTP client, response-size limit and initialization identity. The existingNewClientsignature and defaults remain unchanged. Constructors copy headers and client configuration. A custom client with noCheckRedirectretains the default redirect refusal; an explicitly supplied redirect policy is preserved.
Changed
- Cache fingerprints now use a
v1:prefix and an opaque namespace. Existing in-memory entries may miss after the encoding change; no storage migration is required. TTL, capacity, credential isolation and request hooks are unchanged. - Fingerprints describe completion content.
RoutingMetadata,StreamOptionsandClientStreamOptionsare excluded. Applications must scope caches by routing configuration and handle stream framing and usage options separately. A fingerprint is not an authorization decision or a semantic-similarity score. - Future changes to encoded fields, including newly included request fields, require a new fingerprint version prefix and intentionally invalidate old keys.
Fixed
- Response-cache entries now include routing metadata in their namespace, so conditional routing cannot reuse an answer from a different metadata-selected target for the same prompt and credential. Invalid UTF-8 in routing metadata or credential IDs bypasses caching because JSON encoding would otherwise collapse distinct byte sequences into the same namespace.
- Supplying an MCP HTTP client solely to customize its transport no longer enables automatic redirects. An unset redirect policy refuses redirects, preventing credential replay to redirect targets.
- Requests with values that cannot be JSON encoded bypass the response cache. Previously, serialization error messages were hashed as request content, allowing distinct invalid requests to share a cache entry.
Contributors
Thanks to everyone who shipped this release:
- release: prepare v1.6.0 (#461) — @MitulShah1
Full changelog: https://github.com/ferro-labs/ai-gateway/blob/v1.6.0/CHANGELOG.md
- v1.5.112026-10-27 · 2026-10-27 10:06 UTC
查看更新说明
[1.5.11] — 2026-10-27
Security fixes. No API or configuration changes. Building the gateway, or a module that imports it, now needs Go 1.26.10 or later.
Security
- Go 1.26.9 → 1.26.10, closing GO-2026-6809:
crypto/x509did not distrust a set of compromised intermediate certificates. The gateway verifies a certificate chain on every upstream TLS connection, so a provider endpoint presenting a chain through one of them was accepted. (#458) - Dashboard build and test tooling refreshed in
web/:vitestand@vitest/coverage-v83.2 → 4.1.12 (path traversal in@vitest/mocker; removestinypooland its two prototype-pollution advisories),vite6.4.3 → 6.4.5,body-parser2.3 → 2.4 andfast-uri3.1.9. None of these ship in the bundle; the built dashboard is byte-identical to v1.5.10.
Changed
- The minimum Go version is 1.26.10 (
go.mod, CI, release builds anddeploy/Dockerfile). A module importing the gateway picks it up throughGOTOOLCHAIN=auto, or fails to build on an older toolchain if that is off. - The embedded fallback model catalog (
models/catalog_backup.json), used when the live catalog cannot be fetched, is refreshed from 2,542 to 2,947 models. (#394)
Fixed
- Requests to Responses-only models (
gpt-5-codex,gpt-5.1-codex,codex-miniand the rest of the catalog'sresponsesmode) and to legacy completions models (completionmode, e.g.gpt-3.5-turbo-instruct) are priced again. The model catalog began classifying these models by endpoint contract on 2026-08-22, and the cost calculator had no arm for either mode, so every such request was logged, traced and metered as unpriced at $0 and cost-optimized routing treated the models as unpriced. Both modes now bill the same token fields as chat.
Contributors
Thanks to everyone who shipped this release:
- fix: apply v1.5.11 security fixes (#459) — @MitulShah1
Full changelog: https://github.com/ferro-labs/ai-gateway/blob/v1.5.11/CHANGELOG.md
- Go 1.26.9 → 1.26.10, closing GO-2026-6809:
- v1.5.102026-10-18 · 2026-10-18 08:57 UTC
查看更新说明
[1.5.10] — 2026-10-18
Security fixes and one guardrail fix. No API changes. Building the gateway, or a module that imports it, now needs Go 1.26.9 or later.
Security
- Go 1.25.13 → 1.26.9 and
golang.org/x/netv0.58.0 → v0.60.0, closing GO-2026-6599, GO-2026-6600, GO-2026-6603, GO-2026-6605, GO-2026-6607, GO-2026-6608, GO-2026-6609, GO-2026-6610, GO-2026-6611, GO-2026-6612, GO-2026-6613 and GO-2026-6617 (HTTP/1 and HTTP/2 server and client handling innet/http,crypto/tls,net/textproto,html/template). Go 1.25 is out of support, so these have no 1.25 fix. - OpenTelemetry v1.43/v1.44 → v1.45.0, closing GO-2026-6505 (GHSA-8wmf-6v46-5gfg: exporter config logging could leak endpoint URLs).
- Dashboard build dependencies refreshed in
web/package-lock.json:proxy-addr,source-map-js,undici,js-yaml,ip-address,hono,fast-uri,qs,postcss-selector-parser,brace-expansionand@modelcontextprotocol/sdk. All are build or test tooling and never ship in the bundle; the built dashboard is unchanged.
Changed
- The minimum Go version is 1.26.9 (
go.mod, CI, release builds anddeploy/Dockerfile). A module importing the gateway picks it up throughGOTOOLCHAIN=auto, or fails to build on an older toolchain if that is off.
Fixed
word-filternow refuses to load whenblocked_wordsis present but is not a list of strings: an explicitnull, a scalar written where a list belongs, a map, or a list with a non-string item. Such a value used to load as an empty blocklist, so the guardrail registered and let every request through while startup andferrogw validatereported it healthy. The plugin now implementsValidateConfig, soferrogw validatereports the same error. An absent key still configures no words. Runferrogw validateagainst your config before upgrading. (#447, thanks @shaurya416)deploy/Dockerfilecopies therunpackage, so a source-built image (make docker-build,deploy/compose.dev.yaml,render.yaml) builds again. It had failed with an unresolved import since v1.5.0. Release images are built fromdeploy/gateway.release.Dockerfileand were not affected.
Contributors
Thanks to everyone who shipped this release:
- fix: apply v1.5.10 security fixes and validate word-filter config (#455) — @MitulShah1
Full changelog: https://github.com/ferro-labs/ai-gateway/blob/v1.5.10/CHANGELOG.md
- Go 1.25.13 → 1.26.9 and
- v1.5.92026-09-18 · 2026-09-18 09:48 UTC
查看更新说明
[1.5.9] — 2026-09-18
Added
-
The gateway emits one
gateway.guardrail.matchobservability event per guardrail match, through the sameProvider.RecordEventfanout a request's terminal event uses, whatever action the match resolved to (block, warn, log, redact). A guardrail running in a non-blocking mode is now observable: a host can record what alogorwarnrule would have blocked, which the block-only signal could not show. The event names the plugin, its instanceid, the resolved action, the stage, and whether a guardrail denied the request at that stage — abefore_requestmatch reports how the request fared on the way in, and a guardrail rejecting the response later does not change it, which is why every event names its stage. A rate limiter or a budget denying the same request leaves it true: those are not guardrail verdicts, and it is never a claim that the provider call succeeded. It carries no matched text or offending value, keeping the gateway's no-leak posture. A non-matching plugin emits nothing, and a plugin that matches emits one event per resolved action per run rather than one per message, so a long conversation does not multiply identical events. The events are opt-in, on the same terms as routing-attempt events: an exporter implementsobservability.GuardrailMatchExporter, and an embedder's own provider implementsobservability.GuardrailMatchRecordingProvider. Everything written against one event per request keeps seeing exactly one, which is also why match events are not delivered toGateway.AddHookhooks. -
A plugin config entry accepts an optional
id— an opaque, operator-supplied label for that instance. Several instances of one plugin can be configured (one rule each), and until now a decision named only the plugin type, so an operator could report that "some pii-redact rule fired" but not which one. A block'splugin.RejectionErrornow carries theidof the instance that produced it. The gateway treats the id as opaque and never interprets it, and it stays out of the instance identity used for multi-stage sharing (that is name plus config), so one instance listed across stages carries one id.ferrogw validaterejects anidclaimed by two different instances, and one instance listed across stages under two different ids. An emptyidkeeps today's behaviour. A plugin registered by value rather than by pointer has no identity to key on and carries no id. -
gateway.WithoutEnvExpansion()construction option disables${VAR}substitution for plugin configs and MCP server headers/env. With it set, a${NAME}reaches the plugin or MCP client verbatim and the process environment is never read for substitution. Off by default — expansion stays on — so an existing install is unchanged. It is defence-in-depth for a host that builds gateway instances from config it did not author, where a${...}in that config would otherwise resolve against the host's own environment.
Fixed
ferrogw validateandferrogw doctornow refuse a guardrail whose rules cannot act at its configured stage — for example aregex-guardwhose rules allapply_to: outputlisted atbefore_request, or the reverse. Such a plugin derives its supported stages from its compiled rules, so validate checked them on an instance that had not been initialised and reported the config healthy whileserverefused it at startup. Validate now compiles the rules (when the config block carries no${VAR}reference) before the stage check, so the refusal arrives before the deploy rather than as a failed start. A block carrying a reference keeps today's behaviour, since its rules cannot be compiled before the environment is available.
Contributors
Thanks to everyone who shipped this release:
- feat: observable guardrail decisions, plugin instance ids, and an env-expansion off switch (#446) — @MitulShah1
-
- v1.5.82026-09-16 · 2026-09-16 14:51 UTC
查看更新说明
[1.5.8] — 2026-09-16
Added
piiredact.Detectandpromptshield.Detect— pure functions that report, by name, which built-in PII entity types or injection categories occur in a string. They run the same patterns the plugins enforce and never return matched text, so an embedding policy layer can ask the same question with the same answer.pii-redactacceptsaction: log: detect and record, without denying or rewriting. The observe-only mode every other content guardrail already has.secret-scanreports Anthropic keys under their ownanthropic_keykind instead ofopenai_key. The key was already caught; the audit trail now names the right vendor, andkinds: ["openai_key"]no longer covers a vendor it does not name.secret-scangains seven curated kinds, each matched on structure alone:gitlab_token,npm_token,huggingface_token,sendgrid_key,twilio_key(the API key; an account SID is an identifier and is left alone),azure_storage_keyandslack_webhook.
Fixed
- The least-latency end-to-end check tolerates normal scheduling variance while still failing a strategy that has stopped preferring the fast target.
Contributors
Thanks to everyone who shipped this release:
- feat(plugin): export detectors and add a pii-redact log action (#441) — @MitulShah1
Full changelog: https://github.com/ferro-labs/ai-gateway/blob/v1.5.8/CHANGELOG.md
用户点评
登录用户的真实使用记录,可按版本和硬件筛选还没有人点评。第一个说出真实体验的人最有价值。
High-performance AI gateway in Go. Route LLM requests across 30 providers via a single OpenAI-compatible API.
📖 Documentation: docs.ferrolabs.ai
🔀 30 providers, 2,500+ models — one API ⚡ 13,925 RPS at 1,000 concurrent users (v1.0.0 benchmark) 📦 Single static binary, no external services required, 32 MB base memory
Quick Start
Under two minutes from install to first response.
| Platform / tool | Install |
|---|---|
| macOS, Linux | curl -fsSL https://get.ferrolabs.ai | sh |
| Windows | irm https://get.ferrolabs.ai/install.ps1 | iex |
| Homebrew | brew install ferro-labs/tap/ferrogw |
| Scoop | scoop bucket add ferrolabs https://github.com/ferro-labs/homebrew-tap then scoop install ferrogw |
| npm | npm install -g ferrogw |
| Python | uv tool install ferrogw |
| Docker | docker run -p 8080:8080 ghcr.io/ferro-labs/ai-gateway:latest |
| Go | go install github.com/ferro-labs/ai-gateway/cmd/ferrogw@latest — builds from source, without the dashboard |
| Debian, RPM, Alpine | .deb, .rpm and .apk packages on the releases page |
Then go from nothing to a served request:
export OPENAI_API_KEY=sk-your-key # ferrogw init detects this and writes the matching target
ferrogw init # writes config.yaml, prints your master key
export GATEWAY_CONFIG=./config.yaml # the server reads a config file only when this is set
export MASTER_KEY=fgw_your-master-key # the key ferrogw init printed
ferrogw serve # starts the server on :8080
ferrogw init prints the master key once and never writes it to disk — save
it yourself, in your .env file or a secret manager.
Docker runs the server itself, so there is no ferrogw init to print a master
key — choose your own. Pass both variables by name so their values stay off the
command line, where ps would show them:
export OPENAI_API_KEY=sk-your-key
export MASTER_KEY=fgw-any-strong-secret-you-choose
docker run -p 8080:8080 -e OPENAI_API_KEY -e MASTER_KEY ghcr.io/ferro-labs/ai-gateway:latest
Passing them by name keeps the values out of process arguments only — they are
still readable in the container's environment and through docker inspect, so
use Docker secrets or your platform's secret storage in production.
First request
export MASTER_KEY=fgw_your-master-key # the key ferrogw init printed, in whichever shell you curl from
curl http://localhost:8080/v1/chat/completions \
-H "Authorization: Bearer $MASTER_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "Hello from Ferro Labs AI Gateway"}]
}' | jq
Every release is signed with keyless cosign and ships an SPDX SBOM — verification steps are in SECURITY.md.
Why Ferro Labs AI Gateway
AI gateways differ most under load, in throughput, latency and memory. Ferro Labs AI Gateway is written in Go from the ground up for real-world throughput — a single binary that routes LLM requests with predictable latency and minimal resource usage.
| Feature | Ferro Labs | LiteLLM | Bifrost | Kong AI |
|---|---|---|---|---|
| Language | Go | Python | Go | Go/Lua |
| Single binary | ✅ | ❌ | ✅ | ❌ |
| Providers | 30 | 100+ | 20+ | 10+ |
| MCP support | ✅ | ❌ | ✅ | ❌ |
| Response cache | ✅ | ✅ | ✅ | ❌ (paid) |
| Guardrails | ✅ | ✅ | ❌ | ❌ (paid) |
| OSS license | Apache 2.0 | MIT | Apache 2.0 | Apache 2.0 |
| Managed cloud | Coming Soon | ✅ | ✅ | ✅ |
Features
| Capability | What it does | Reference |
|---|---|---|
| 🔀 Routing | 8 strategies — single, fallback, load balance, least latency, cost-optimized, content-based, A/B test, conditional — with per-target retry, failover, and model aliases | Docs → |
| 🔌 30 providers | Chat and streaming everywhere; embeddings, images, rerank, moderations, speech-to-text, text-to-speech, and batch where the vendor offers them | Docs → |
| 🛡️ Guardrails & plugins | Eleven built in — word filter, regex guard, PII redaction, secret scanning, prompt-injection shield, response schema validation, token/message limits, response cache, rate limiting, per-key budgets, request logging — and the plugin framework is public for writing your own | Docs → |
| 🎯 Capability matrix | One declarative record of which OpenAI parameters each provider forwards, translates, or cannot express, served by GET /v1/capabilities | Docs → |
| 🤖 MCP | Connects to stdio and Streamable HTTP tool servers, injects their tools into chat completions, and drives the agentic tool_calls loop itself | Docs → |
| 📊 Observability | OpenTelemetry tracing and Prometheus metrics, one trace ID across logs and spans — a zero-allocation no-op until enabled | Docs → |
| 🖥️ Dashboard | Operations console compiled into the binary and served at / — traffic, spend, provider health, request logs, audit trail | Docs → |
Dashboard
Every release binary serves a built-in operations console at / — same port as
the API, compiled in with go:embed, no second image and no second origin. Sign
in with your MASTER_KEY or any admin / read-only key and it reads the live
gateway: traffic, spend, provider health, routing, plugins, request logs, and the
audit trail.
The bundle is produced by the release pipeline, so a binary built with
go install is the one exception: it answers / with a placeholder instead.
Any other install method above ships the console.
Overview, Analytics (latency/TTFT/cost percentiles), Providers, Routing, Plugins, a Playground over the real routing path, Tracing, Request Logs, the Audit Trail, Configuration with history/rollback, and scoped API key management.
Run the gateway and open http://localhost:8080. To see it filled like the recording above, bring up the self-contained demo stack:
make up-fullstack # gateway + Postgres + Jaeger + Prometheus + Grafana + mock upstream + load generator
# then open http://localhost:8080
Build and development details are in web/README.md.
Documentation
The root README is the overview; each subsystem keeps its own reference beside its code:
| Reference | Covers |
|---|---|
| providers/README.md | The 30 providers, the per-provider endpoint matrix, and every /v1/* surface |
| config/README.md | Config loading, validation, ${VAR} secrets, declared models, trusted proxies |
| internal/strategies/README.md | All 8 routing strategies and their failure semantics |
| plugin/README.md | The plugin framework and the eleven built-in plugins |
| mcp/README.md | MCP tool servers, transports, the subprocess trust boundary, readiness |
| observability/README.md | Tracing setup, managed backends, emitted attributes, privacy levels, exporters |
| deploy/README.md | Dockerfiles, Compose files, the fullstack demo stack |
| web/README.md | Dashboard development and the embed contract |
| AGENTS.md | The complete operator/developer reference: architecture, every env var, request flow |
| SECURITY.md | Reporting, security posture, release verification |
| CONTRIBUTING.md | Branch strategy, commit conventions, provider/plugin checklists |
Examples
Integration examples for common use cases are in ferro-labs/ai-gateway-examples:
| Example | Description |
|---|---|
| basic | Single chat completion to the first configured provider |
| fallback | Fallback strategy — try providers in order with retries |
| loadbalance | Weighted load balancing across targets (70/30 split) |
| with-guardrails | Built-in word-filter and max-token guardrail plugins |
| with-mcp | Local MCP server with tool-calling integration |
| embedded | Embed the gateway as an HTTP handler inside an existing server |
Configuration
One YAML/JSON file, named by GATEWAY_CONFIG, drives routing, guardrails, MCP
tools, and observability:
strategy:
mode: fallback # 8 modes — see internal/strategies/README.md
targets:
- virtual_key: openai # an allowlist: only listed providers are routable
retry: { attempts: 3 } # per target, honoured under every routing mode
concurrency: { max_concurrency: 32, queue_size: 1000 }
- virtual_key: anthropic
aliases:
fast: gpt-4o-mini
plugins: # guardrails first, then cache — see plugin/README.md
- name: word-filter
type: guardrail
stage: before_request
enabled: true
config: { blocked_words: ["password", "secret"] }
mcp_servers: # tool servers — see mcp/README.md
- name: search
url: https://mcp.example.com/mcp
headers: { Authorization: "Bearer ${SEARCH_TOKEN}" }
${VAR} references (braced form only) resolve when a component is constructed,
never at file load — so secrets are never stored, served by GET /admin/config,
or restored by a rollback. A bare $ is data; an undefined variable is an
error.
The full annotated reference with every option is
config.example.yaml /
config.example.json, and the schema guide is
config/README.md. ferrogw validate checks a file without
starting the server.
Key environment variables
| Variable | Purpose |
|---|---|
MASTER_KEY | Bootstrap and break-glass admin credential (generated by ferrogw init); give each operator their own key from POST /admin/keys for day-to-day use |
GATEWAY_CONFIG | Path to config YAML/JSON |
GATEWAY_ENV | Set to production to enable production-mode safety guards: it refuses to start on ALLOW_UNAUTHENTICATED_PROXY=true or a * entry in CORS_ORIGINS, and warns when per-IP rate limiting is off, pprof is mounted, or the API key store is in-memory |
PORT | Server port (default: 8080) |
ALLOW_UNAUTHENTICATED_PROXY | Set to true to disable proxy-route auth (dev only; blocked when GATEWAY_ENV=production) |
CORS_ORIGINS | Comma-separated allowed CORS origins; cross-origin is denied when unset. Matched literally — there is no wildcard, so list each origin explicitly |
TRUSTED_PROXIES | CIDRs of trusted reverse proxies; forwarded headers are honored only from these (default: loopback). See config/README.md |
<PROVIDER>_BASE_URL | Points a provider at a proxy, self-hosted server, or regional endpoint. It is the API root, used verbatim — write it exactly as the vendor documents it, version segment included (https://api.groq.com/openai/v1); a bare host resolves to the provider's own version segment |
See AGENTS.md for the full environment variable reference including provider API keys, store backends, and OTel settings.
Observability
See everything your gateway does — every request, what it cost, how long it took, which provider served it, and which guardrails ran. Ferro Labs AI Gateway ships first-class OpenTelemetry tracing and Prometheus metrics out of the box, and stays at a zero-allocation no-op until you turn it on. Point it at Jaeger, Grafana, New Relic, LangSmith, Datadog, or Honeycomb — anything that speaks OTLP — and every request emits a gateway.request span carrying GenAI semantic conventions (gen_ai.*) plus ferro.* extensions for cost, routing, MCP tool calls, and stream timings. The same trace ID threads your logs, spans, and the X-Request-ID response header. Requests can name the end user and session they belong to (user, X-User-ID, X-Session-ID, or W3C baggage), and that identity rides the request span, every event, and request-log rows.
📈 Full observability guide → observability/README.md — managed-backend setup, endpoint & transport rules, every emitted attribute, privacy levels, and exporter plugins.
Bring up the gateway wired to a full monitoring stack — Prometheus, Grafana, and Jaeger — driven by generated traffic, in one command:
make up-fullstack # then open Grafana at http://localhost:3000
Enable tracing with one variable (or the observability: config block —
endpoint, protocol, sampling, privacy, headers are all documented in the guide):
export OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317
ferrogw serve
CLI
ferrogw is a single binary — no separate CLI tool required.
| Command | Description |
|---|---|
ferrogw | Start the gateway server (default) |
ferrogw serve | Start the gateway server (explicit) |
ferrogw init | First-run setup — generate master key and config |
ferrogw validate | Validate a config file without starting |
ferrogw doctor | Check environment (API keys, config, connectivity) |
ferrogw status | Show gateway health and provider status |
ferrogw version | Print version, commit, and build info |
ferrogw admin keys list | List API keys |
ferrogw admin keys create --name <name> | Create an API key (--scope, --expires-in) |
ferrogw admin logs stats | Show request log statistics |
ferrogw plugins | List registered plugins |
Global flags available on all subcommands: --gateway-url, --api-key, --format (table/json/yaml).
Deployment
Local development
export OPENAI_API_KEY=sk-your-key
export MASTER_KEY=fgw_your-master-key
export GATEWAY_CONFIG=./config.yaml
make build && ./bin/ferrogw
Railway & Render
The deploy buttons at the top of this README provision either platform: Railway
with SQLite on a volume (point API_KEY_STORE_DSN, CONFIG_STORE_DSN and
REQUEST_LOG_STORE_DSN at paths under /data) or PostgreSQL, and Render from the repo's render.yaml Blueprint,
which generates MASTER_KEY and wires the store DSNs to a managed Postgres
automatically.
Docker Compose
Three Compose files in deploy/ follow the standard override pattern — a shared
base, a dev override that builds from source, and a prod override with a pinned
tag, health check, and resource limits. Run everything from the repository root:
make up # dev: builds from source
IMAGE_TAG=v1.4.5 CORS_ORIGINS=https://your-domain.com make up-prod
make down # tears down either
One container serves both the API and the dashboard — no second image, no second
origin. Provider keys go in a repository-root .env or the environment.
deploy/README.md has the full reference, including a
self-contained PostgreSQL pairing and the fullstack observability stack.
Kubernetes via Helm
helm repo add ferro-labs https://ferro-labs.github.io/helm-charts
helm repo update
helm install ferro-gw ferro-labs/ai-gateway \
--set env.OPENAI_API_KEY=sk-your-key
Helm charts: github.com/ferro-labs/helm-charts | ArtifactHub
Migrate to Ferro Labs AI Gateway
The gateway is OpenAI-compatible, so for a client already using an
OpenAI-compatible SDK, migration — from another gateway or from calling a
provider directly — is a base_url and api_key change: point the SDK at the
gateway and present a gateway-issued credential in place of the provider's own.
A client with its own API, such as LiteLLM's completion() below, also moves to
the OpenAI SDK.
From LiteLLM
Before (LiteLLM):
from litellm import completion
response = completion(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello"}]
)
After (Ferro Labs AI Gateway):
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8080/v1",
api_key="your-ferro-api-key",
)
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello"}],
)
Provider API keys move to environment variables (OPENAI_API_KEY,
ANTHROPIC_API_KEY, …); the model list becomes targets + aliases in
config.yaml.
Why migrate from LiteLLM:
- 14x higher throughput at 150 concurrent users (2,447 vs 175 RPS)
- 23x less memory at peak load (47 MB vs 1,124 MB under streaming)
- Single binary — no Python environment, no pip, no virtualenv
- Predictable latency — p99 stays under 65 ms at 150 VU vs LiteLLM's timeouts at the same concurrency
From Portkey
The code change is the same one line — Ferro Labs uses the standard OpenAI SDK with no custom headers in self-hosted mode.
Why migrate from Portkey:
- Fully open source — no per-request pricing, no log limits
- Self-hosted — the gateway runs in your infrastructure, and prompts reach only the providers you configure
- No vendor lock-in — Apache 2.0 license
- MCP support — Portkey self-hosted lacks native MCP
- FerroCloud (coming soon) for teams that want a managed service
Performance
Every figure in this section comes from one run: Ferro Labs v1.0.0, measured 2026-03-23. Benchmarked against Kong OSS, Bifrost, LiteLLM, and Portkey on GCP n2-standard-8 (8 vCPU, 32 GB RAM) using a 60ms fixed-latency mock upstream — results reflect gateway overhead only. Later releases have not been re-measured; reproduce against the version you intend to run using the commands below.

| VU | RPS | p50 | p99 | Memory |
|---|---|---|---|---|
| 50 | 813 | 61.3ms | 64.1ms | 36 MB |
| 150 | 2,447 | 61.2ms | 63.4ms | 47 MB |
| 300 | 4,890 | 61.2ms | 64.4ms | 72 MB |
| 500 | 8,014 | 61.5ms | 72.9ms | 89 MB |
| 1,000 | 13,925 | 68.1ms | 111.9ms | 135 MB |
At 1,000 VU: 13,925 RPS, p50 overhead 8.1ms, memory 135 MB. Against the live OpenAI API, the gateway itself adds 25 microseconds p50 in a typical plugin configuration, and 2 microseconds with no plugins enabled.
Full methodology, raw results, and flamegraph analysis:
ferro-labs/ai-gateway-performance-benchmarks
(make setup && make bench reproduces it).
FerroCloud
FerroCloud — the managed version of Ferro Labs AI Gateway with multi-tenancy, analytics, and cost governance — is coming soon.
👉 Join the waitlist at ferrolabs.ai
SDKs
Official client libraries for the Ferro Labs AI Gateway — and the standard
OpenAI SDK works unchanged: point base_url at http://your-gateway:8080/v1.
| SDK | Install | Repository |
|---|---|---|
| Python | pip install ferrolabs | ferro-labs/ferrolabs-python-sdk |
| TypeScript | npm install ferrolabs | ferro-labs/ferrolabs-typescript-sdk |
from ferrolabs import FerroClient
client = FerroClient(
base_url="http://localhost:8080/v1",
api_key="your-ferro-api-key",
)
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello"}],
)
import { FerroClient } from "ferrolabs";
const client = new FerroClient({
baseURL: "http://localhost:8080/v1",
apiKey: "your-ferro-api-key",
});
const response = await client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: "Hello" }],
});
Contributing
We welcome contributions. New providers go in this OSS repo only — never in FerroCloud. See CONTRIBUTING.md for branch strategy, commit conventions, and PR guidelines.
Community
- GitHub Discussions
- Discord
- Built with Ferro Labs AI Gateway? Open a PR to add to our showcase.
License
Apache 2.0 — see LICENSE.