ferrogw 1.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +531 -0
  3. package/bin/ferrogw.js +105 -0
  4. package/package.json +44 -0
package/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,531 @@
1
+ <div align="center">
2
+
3
+ <p align="right">
4
+ <a href="README.md">English</a> | <a href="README.zh-CN.md">中文</a>
5
+ </p>
6
+
7
+ <table border="0" cellspacing="0" cellpadding="0"><tr>
8
+ <td rowspan="2"><img src="docs/logo.png" alt="Ferro Labs AI Gateway" width="64" /></td>
9
+ <td align="center"><h1>Ferro Labs AI Gateway</h1></td>
10
+ </tr><tr>
11
+ <td align="center"><strong>Open-Source, OpenAI-Compatible LLM Gateway</strong></td>
12
+ </tr></table>
13
+
14
+ **High-performance AI gateway in Go. Route LLM requests across 30 providers via a single OpenAI-compatible API.**
15
+
16
+ [![Deploy on Railway: SQLite](https://railway.com/button.svg)](https://railway.com/deploy/ferro-labs-ai-sqlite-storage?referralCode=KblxKX&utm_medium=integration&utm_source=template&utm_campaign=generic)
17
+ [![Deploy on Railway: PostgreSQL](https://railway.com/button.svg)](https://railway.com/deploy/ferro-labs-ai-postgresql-storage?referralCode=KblxKX&utm_medium=integration&utm_source=template&utm_campaign=generic)
18
+ [![Deploy to Render: PostgreSQL](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/ferro-labs/ai-gateway)
19
+
20
+ [![Go](https://img.shields.io/badge/go-1.25+-00ADD8.svg)](https://go.dev)
21
+ [![Go Reference](https://pkg.go.dev/badge/github.com/ferro-labs/ai-gateway.svg)](https://pkg.go.dev/github.com/ferro-labs/ai-gateway)
22
+ [![codecov](https://codecov.io/gh/ferro-labs/ai-gateway/branch/main/graph/badge.svg)](https://codecov.io/gh/ferro-labs/ai-gateway)
23
+ [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
24
+ [![GitHub Stars](https://img.shields.io/github/stars/ferro-labs/ai-gateway?style=flat&color=yellow)](https://github.com/ferro-labs/ai-gateway/stargazers)
25
+ [![CI](https://github.com/ferro-labs/ai-gateway/actions/workflows/ci.yml/badge.svg)](https://github.com/ferro-labs/ai-gateway/actions/workflows/ci.yml)
26
+ [![Code Scanning](https://github.com/ferro-labs/ai-gateway/actions/workflows/code-scanning.yml/badge.svg)](https://github.com/ferro-labs/ai-gateway/actions/workflows/code-scanning.yml)
27
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg?url=https%3A%2F%2Fdeepwiki.com%2Fferro-labs%2Fai-gateway)](https://deepwiki.com/ferro-labs/ai-gateway)
28
+ [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/ferro-labs)](https://artifacthub.io/packages/search?org=ferro-labs)
29
+ [![Docs](https://img.shields.io/badge/docs-ferrolabs.ai-2ea44f)](https://docs.ferrolabs.ai)
30
+ [![Discord](https://img.shields.io/badge/Discord-Join%20Us-5865F2?logo=discord&logoColor=white)](https://discord.gg/yCAeYvJeDV)
31
+
32
+ 📖 **Documentation:** [docs.ferrolabs.ai](https://docs.ferrolabs.ai)
33
+
34
+ 🔀 **30 providers, 2,500+ models — one API**<br/>
35
+ ⚡ **13,925 RPS at 1,000 concurrent users** ([v1.0.0 benchmark](#performance))<br/>
36
+ 📦 **Single static binary, no external services required, 32 MB base memory**
37
+
38
+ <img src="docs/architecture.svg" alt="Ferro Labs AI Gateway Architecture" width="100%" />
39
+
40
+ </div>
41
+
42
+ ---
43
+
44
+ ## Quick Start
45
+
46
+ Get from zero to first request in under 2 minutes.
47
+
48
+ ### Option A — Binary (fastest)
49
+
50
+ ```bash
51
+ VER=$(curl -fsSL https://api.github.com/repos/ferro-labs/ai-gateway/releases/latest | grep '"tag_name"' | cut -d'"' -f4)
52
+ curl -fsSL "https://github.com/ferro-labs/ai-gateway/releases/download/${VER}/ferrogw_${VER#v}_linux_amd64.tar.gz" | tar xz
53
+ chmod +x ferrogw
54
+ ./ferrogw init # generates config.yaml + MASTER_KEY
55
+ export GATEWAY_CONFIG=./config.yaml # the server reads a config file only when this is set
56
+ export OPENAI_API_KEY=sk-your-key # providers are registered at startup, so export before starting
57
+ export MASTER_KEY=fgw_your-master-key # the key ferrogw init printed
58
+ ./ferrogw # starts the server
59
+ ```
60
+
61
+ Releases are signed and ship SBOMs — verification steps are in
62
+ [SECURITY.md](SECURITY.md#verifying-releases).
63
+
64
+ ### Option B — Docker
65
+
66
+ ```bash
67
+ docker pull ghcr.io/ferro-labs/ai-gateway:latest
68
+ docker run -p 8080:8080 \
69
+ -e OPENAI_API_KEY=sk-your-key \
70
+ -e MASTER_KEY=fgw_your-master-key \
71
+ ghcr.io/ferro-labs/ai-gateway:latest
72
+ ```
73
+
74
+ ### Option C — Go
75
+
76
+ ```bash
77
+ go install github.com/ferro-labs/ai-gateway/cmd/ferrogw@latest
78
+ ferrogw init # first-run setup
79
+ export GATEWAY_CONFIG=./config.yaml # the server reads a config file only when this is set
80
+ export OPENAI_API_KEY=sk-your-key # providers are registered at startup, so export before starting
81
+ export MASTER_KEY=fgw_your-master-key # the key ferrogw init printed
82
+ ferrogw # start the server
83
+ ```
84
+
85
+ `ferrogw init` generates the master key and writes a minimal `config.yaml`. The
86
+ key is shown **once** and never written to disk — store it in your `.env` file
87
+ or secret manager.
88
+
89
+ <div align="center">
90
+ <img src="docs/demo.gif" alt="Ferro Labs AI Gateway — Quick Start Demo" width="720" />
91
+ </div>
92
+
93
+ ### First request
94
+
95
+ ```bash
96
+ export MASTER_KEY=fgw_your-master-key # the key ferrogw init printed, in whichever shell you curl from
97
+
98
+ curl http://localhost:8080/v1/chat/completions \
99
+ -H "Content-Type: application/json" \
100
+ -H "Authorization: Bearer $MASTER_KEY" \
101
+ -d '{
102
+ "model": "gpt-4o-mini",
103
+ "messages": [{"role": "user", "content": "Hello from Ferro Labs AI Gateway"}]
104
+ }' | jq
105
+ ```
106
+
107
+ ---
108
+
109
+ ## Why Ferro Labs AI Gateway
110
+
111
+ Most AI gateways are Python proxies that crack under load or JavaScript services that eat 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.
112
+
113
+ | Feature | Ferro Labs | LiteLLM | Bifrost | Kong AI |
114
+ |:-----------------|:------------|:--------|:-----------|:------------|
115
+ | Language | Go | Python | Go | Go/Lua |
116
+ | Single binary | ✅ | ❌ | ✅ | ❌ |
117
+ | Providers | 30 | 100+ | 20+ | 10+ |
118
+ | MCP support | ✅ | ❌ | ✅ | ❌ |
119
+ | Response cache | ✅ | ✅ | ✅ | ❌ (paid) |
120
+ | Guardrails | ✅ | ✅ | ❌ | ❌ (paid) |
121
+ | OSS license | Apache 2.0 | MIT | Apache 2.0 | Apache 2.0 |
122
+ | Managed cloud | Coming Soon | ✅ | ✅ | ✅ |
123
+
124
+ ---
125
+
126
+ ## Features
127
+
128
+ | Capability | What it does | Reference |
129
+ |:---|:---|:---|
130
+ | 🔀 **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 →](https://docs.ferrolabs.ai/routing/) |
131
+ | 🔌 **30 providers** | Chat and streaming everywhere; embeddings, images, rerank, moderations, speech-to-text, text-to-speech, and batch where the vendor offers them | [Docs →](https://docs.ferrolabs.ai/providers/) |
132
+ | 🛡️ **Guardrails & plugins** | Six built in — word filter, token/message limits, response cache, rate limiting, per-key budgets, request logging — and the plugin framework is public for writing your own | [Docs →](https://docs.ferrolabs.ai/plugins/) |
133
+ | 🎯 **Capability matrix** | One declarative record of which OpenAI parameters each provider forwards, translates, or cannot express, served by `GET /v1/capabilities` | [Docs →](https://docs.ferrolabs.ai/guides/provider-capabilities/) |
134
+ | 🤖 **MCP** | Connects to stdio and Streamable HTTP tool servers, injects their tools into chat completions, and drives the agentic `tool_calls` loop itself | [Docs →](https://docs.ferrolabs.ai/guides/mcp/) |
135
+ | 📊 **Observability** | OpenTelemetry tracing and Prometheus metrics, one trace ID across logs and spans — a zero-allocation no-op until enabled | [Docs →](https://docs.ferrolabs.ai/guides/observability/) |
136
+ | 🖥️ **Dashboard** | Operations console compiled into the binary and served at `/` — traffic, spend, provider health, request logs, audit trail | [Docs →](https://docs.ferrolabs.ai/guides/dashboard/) |
137
+
138
+ ---
139
+
140
+ ## Dashboard
141
+
142
+ Every gateway binary serves a built-in operations console at `/` — same port as
143
+ the API, compiled in with `go:embed`, no second image and no second origin. Sign
144
+ in with your `MASTER_KEY` or any admin / read-only key and it reads the live
145
+ gateway: traffic, spend, provider health, routing, plugins, request logs, and the
146
+ audit trail.
147
+
148
+ <div align="center">
149
+ <img src="docs/dashboard.gif" alt="Ferro Labs AI Gateway operations console: Overview, Analytics, Providers, Routing Strategies, Plugins, Playground, Tracing, Request Logs, Audit Trail, Configuration, and API Keys" width="100%" />
150
+ </div>
151
+
152
+ Overview, Analytics (latency/TTFT/cost percentiles), Providers, Routing,
153
+ Plugins, a Playground over the real routing path, Tracing, Request Logs, the
154
+ Audit Trail, Configuration with history/rollback, and scoped API key management.
155
+
156
+ Run the gateway and open <http://localhost:8080>. To see it filled like the
157
+ recording above, bring up the self-contained demo stack:
158
+
159
+ ```bash
160
+ make up-fullstack # gateway + Postgres + Jaeger + Prometheus + Grafana + mock upstream + load generator
161
+ # then open http://localhost:8080
162
+ ```
163
+
164
+ Build and development details are in [web/README.md](web/README.md).
165
+
166
+ ---
167
+
168
+ ## Documentation
169
+
170
+ The root README is the overview; each subsystem keeps its own reference beside
171
+ its code:
172
+
173
+ | Reference | Covers |
174
+ |:---|:---|
175
+ | [providers/README.md](providers/README.md) | The 30 providers, the per-provider endpoint matrix, and every `/v1/*` surface |
176
+ | [config/README.md](config/README.md) | Config loading, validation, `${VAR}` secrets, declared models, trusted proxies |
177
+ | [internal/strategies/README.md](internal/strategies/README.md) | All 8 routing strategies and their failure semantics |
178
+ | [plugin/README.md](plugin/README.md) | The plugin framework and the six built-in plugins |
179
+ | [mcp/README.md](mcp/README.md) | MCP tool servers, transports, the subprocess trust boundary, readiness |
180
+ | [observability/README.md](observability/README.md) | Tracing setup, managed backends, emitted attributes, privacy levels, exporters |
181
+ | [deploy/README.md](deploy/README.md) | Dockerfiles, Compose files, the fullstack demo stack |
182
+ | [web/README.md](web/README.md) | Dashboard development and the embed contract |
183
+ | [AGENTS.md](AGENTS.md) | The complete operator/developer reference: architecture, every env var, request flow |
184
+ | [SECURITY.md](SECURITY.md) | Reporting, security posture, release verification |
185
+ | [CONTRIBUTING.md](CONTRIBUTING.md) | Branch strategy, commit conventions, provider/plugin checklists |
186
+
187
+ ---
188
+
189
+ ## Examples
190
+
191
+ Integration examples for common use cases are in [ferro-labs/ai-gateway-examples](https://github.com/ferro-labs/ai-gateway-examples):
192
+
193
+ | Example | Description |
194
+ |:--------|:------------|
195
+ | [basic](https://github.com/ferro-labs/ai-gateway-examples/tree/main/basic) | Single chat completion to the first configured provider |
196
+ | [fallback](https://github.com/ferro-labs/ai-gateway-examples/tree/main/fallback) | Fallback strategy — try providers in order with retries |
197
+ | [loadbalance](https://github.com/ferro-labs/ai-gateway-examples/tree/main/loadbalance) | Weighted load balancing across targets (70/30 split) |
198
+ | [with-guardrails](https://github.com/ferro-labs/ai-gateway-examples/tree/main/with-guardrails) | Built-in word-filter and max-token guardrail plugins |
199
+ | [with-mcp](https://github.com/ferro-labs/ai-gateway-examples/tree/main/with-mcp) | Local MCP server with tool-calling integration |
200
+ | [embedded](https://github.com/ferro-labs/ai-gateway-examples/tree/main/embedded) | Embed the gateway as an HTTP handler inside an existing server |
201
+
202
+ ---
203
+
204
+ ## Configuration
205
+
206
+ One YAML/JSON file, named by `GATEWAY_CONFIG`, drives routing, guardrails, MCP
207
+ tools, and observability:
208
+
209
+ ```yaml
210
+ strategy:
211
+ mode: fallback # 8 modes — see internal/strategies/README.md
212
+
213
+ targets:
214
+ - virtual_key: openai # an allowlist: only listed providers are routable
215
+ retry: { attempts: 3 } # per target, honoured under every routing mode
216
+ concurrency: { max_concurrency: 32, queue_size: 1000 }
217
+ - virtual_key: anthropic
218
+
219
+ aliases:
220
+ fast: gpt-4o-mini
221
+
222
+ plugins: # guardrails first, then cache — see plugin/README.md
223
+ - name: word-filter
224
+ type: guardrail
225
+ stage: before_request
226
+ enabled: true
227
+ config: { blocked_words: ["password", "secret"] }
228
+
229
+ mcp_servers: # tool servers — see mcp/README.md
230
+ - name: search
231
+ url: https://mcp.example.com/mcp
232
+ headers: { Authorization: "Bearer ${SEARCH_TOKEN}" }
233
+ ```
234
+
235
+ `${VAR}` references (braced form only) resolve when a component is constructed,
236
+ never at file load — so secrets are never stored, served by `GET /admin/config`,
237
+ or restored by a rollback. A bare `$` is data; an undefined variable is an
238
+ error.
239
+
240
+ The full annotated reference with every option is
241
+ [config.example.yaml](config.example.yaml) /
242
+ [config.example.json](config.example.json), and the schema guide is
243
+ [config/README.md](config/README.md). `ferrogw validate` checks a file without
244
+ starting the server.
245
+
246
+ ### Key environment variables
247
+
248
+ | Variable | Purpose |
249
+ |----------|---------|
250
+ | `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 |
251
+ | `GATEWAY_CONFIG` | Path to config YAML/JSON |
252
+ | `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 |
253
+ | `PORT` | Server port (default: `8080`) |
254
+ | `ALLOW_UNAUTHENTICATED_PROXY` | Set to `true` to disable proxy-route auth (dev only; blocked when `GATEWAY_ENV=production`) |
255
+ | `CORS_ORIGINS` | Comma-separated allowed CORS origins; cross-origin is denied when unset. Matched **literally** — there is no wildcard, so list each origin explicitly |
256
+ | `TRUSTED_PROXIES` | CIDRs of trusted reverse proxies; forwarded headers are honored only from these (default: loopback). See [config/README.md](config/README.md#trusted-proxies-trusted_proxies) |
257
+ | `<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 |
258
+
259
+ See [AGENTS.md](AGENTS.md) for the full environment variable reference including provider API keys, store backends, and OTel settings.
260
+
261
+ ---
262
+
263
+ ## Observability
264
+
265
+ **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.
266
+
267
+ > 📈 **Full observability guide → [observability/README.md](observability/README.md)** — managed-backend setup, endpoint & transport rules, every emitted attribute, privacy levels, and exporter plugins.
268
+
269
+ Bring up the gateway wired to a full monitoring stack — **Prometheus, Grafana, and Jaeger** — driven by generated traffic, in one command:
270
+
271
+ ```bash
272
+ make up-fullstack # then open Grafana at http://localhost:3000
273
+ ```
274
+
275
+ <p align="center">
276
+ <img src="docs/observability/grafana-dashboard.gif" alt="Grafana dashboard: per-provider request rate, latency percentiles, token cost, and circuit-breaker state" width="100%" />
277
+ <br/>
278
+ <em>Grafana — request rate, latency percentiles, per-provider breakdown, token cost, and circuit-breaker state, all from the gateway's Prometheus metrics.</em>
279
+ </p>
280
+
281
+ <p align="center">
282
+ <img src="docs/observability/jaeger-trace.gif" alt="Jaeger trace: one gateway.request span expanding to show its gen_ai.* and ferro.* attributes" width="100%" />
283
+ <br/>
284
+ <em>Jaeger — one request's <code>gateway.request</code> span, opened to reveal its <code>gen_ai.*</code> and <code>ferro.*</code> attributes.</em>
285
+ </p>
286
+
287
+ Enable tracing with one variable (or the `observability:` config block —
288
+ endpoint, protocol, sampling, privacy, headers are all documented in the guide):
289
+
290
+ ```bash
291
+ export OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317
292
+ ferrogw serve
293
+ ```
294
+
295
+ ---
296
+
297
+ ## CLI
298
+
299
+ `ferrogw` is a single binary — no separate CLI tool required.
300
+
301
+ | Command | Description |
302
+ |:--------|:------------|
303
+ | `ferrogw` | Start the gateway server (default) |
304
+ | `ferrogw serve` | Start the gateway server (explicit) |
305
+ | `ferrogw init` | First-run setup — generate master key and config |
306
+ | `ferrogw validate` | Validate a config file without starting |
307
+ | `ferrogw doctor` | Check environment (API keys, config, connectivity) |
308
+ | `ferrogw status` | Show gateway health and provider status |
309
+ | `ferrogw version` | Print version, commit, and build info |
310
+ | `ferrogw admin keys list` | List API keys |
311
+ | `ferrogw admin keys create --name <name>` | Create an API key (`--scope`, `--expires-in`) |
312
+ | `ferrogw admin logs stats` | Show request log statistics |
313
+ | `ferrogw plugins` | List registered plugins |
314
+
315
+ Global flags available on all subcommands: `--gateway-url`, `--api-key`, `--format` (table/json/yaml).
316
+
317
+ ---
318
+
319
+ ## Deployment
320
+
321
+ ### Local development
322
+
323
+ ```bash
324
+ export OPENAI_API_KEY=sk-your-key
325
+ export MASTER_KEY=fgw_your-master-key
326
+ export GATEWAY_CONFIG=./config.yaml
327
+ make build && ./bin/ferrogw
328
+ ```
329
+
330
+ ### Railway & Render
331
+
332
+ The deploy buttons at the top of this README provision either platform: Railway
333
+ with SQLite on a volume (set the three `*_STORE_DSN` variables to paths under
334
+ `/data`) or PostgreSQL, and Render from the repo's `render.yaml` Blueprint,
335
+ which generates `MASTER_KEY` and wires the store DSNs to a managed Postgres
336
+ automatically.
337
+
338
+ ### Docker Compose
339
+
340
+ Three Compose files in `deploy/` follow the standard override pattern — a shared
341
+ base, a dev override that builds from source, and a prod override with a pinned
342
+ tag, health check, and resource limits. Run everything from the repository root:
343
+
344
+ ```bash
345
+ make up # dev: builds from source
346
+ IMAGE_TAG=v1.4.0 CORS_ORIGINS=https://your-domain.com make up-prod
347
+ make down # tears down either
348
+ ```
349
+
350
+ One container serves both the API and the dashboard — no second image, no second
351
+ origin. Provider keys go in a repository-root `.env` or the environment.
352
+ [deploy/README.md](deploy/README.md) has the full reference, including a
353
+ self-contained PostgreSQL pairing and the fullstack observability stack.
354
+
355
+ ### Kubernetes via Helm
356
+
357
+ ```bash
358
+ helm repo add ferro-labs https://ferro-labs.github.io/helm-charts
359
+ helm repo update
360
+ helm install ferro-gw ferro-labs/ai-gateway \
361
+ --set env.OPENAI_API_KEY=sk-your-key
362
+ ```
363
+
364
+ Helm charts: [github.com/ferro-labs/helm-charts](https://github.com/ferro-labs/helm-charts) | [ArtifactHub](https://artifacthub.io/packages/search?org=ferro-labs)
365
+
366
+ ---
367
+
368
+ ## Migrate to Ferro Labs AI Gateway
369
+
370
+ The gateway is OpenAI-compatible, so migration from any gateway — or from
371
+ calling a provider directly — is a `base_url` change.
372
+
373
+ ### From LiteLLM
374
+
375
+ **Before (LiteLLM):**
376
+
377
+ ```python
378
+ from litellm import completion
379
+
380
+ response = completion(
381
+ model="gpt-4o",
382
+ messages=[{"role": "user", "content": "Hello"}]
383
+ )
384
+ ```
385
+
386
+ **After (Ferro Labs AI Gateway):**
387
+
388
+ ```python
389
+ from openai import OpenAI
390
+
391
+ client = OpenAI(
392
+ base_url="http://localhost:8080/v1",
393
+ api_key="your-ferro-api-key",
394
+ )
395
+
396
+ response = client.chat.completions.create(
397
+ model="gpt-4o",
398
+ messages=[{"role": "user", "content": "Hello"}],
399
+ )
400
+ ```
401
+
402
+ Provider API keys move to environment variables (`OPENAI_API_KEY`,
403
+ `ANTHROPIC_API_KEY`, …); the model list becomes `targets` + `aliases` in
404
+ `config.yaml`.
405
+
406
+ **Why migrate from LiteLLM:**
407
+
408
+ - 14x higher throughput at 150 concurrent users (2,447 vs 175 RPS)
409
+ - 23x less memory at peak load (47 MB vs 1,124 MB under streaming)
410
+ - Single binary — no Python environment, no pip, no virtualenv
411
+ - Predictable latency — p99 stays under 65 ms at 150 VU vs LiteLLM's timeouts at the same concurrency
412
+
413
+ ### From Portkey
414
+
415
+ The code change is the same one line — Ferro Labs uses the standard OpenAI SDK
416
+ with no custom headers in self-hosted mode.
417
+
418
+ **Why migrate from Portkey:**
419
+
420
+ - Fully open source — no per-request pricing, no log limits
421
+ - Self-hosted — your data never leaves your infrastructure
422
+ - No vendor lock-in — Apache 2.0 license
423
+ - MCP support — Portkey self-hosted lacks native MCP
424
+ - FerroCloud (coming soon) for teams that want a managed service
425
+
426
+ ---
427
+
428
+ ## Performance
429
+
430
+ Every figure in this section comes from one run: **Ferro Labs v1.0.0, measured
431
+ 2026-03-23**. Benchmarked against Kong OSS, Bifrost, LiteLLM, and Portkey on
432
+ **GCP n2-standard-8** (8 vCPU, 32 GB RAM) using a **60ms fixed-latency
433
+ mock upstream** — results reflect gateway overhead only. Later releases have not
434
+ been re-measured; reproduce against the version you intend to run using the
435
+ commands below.
436
+
437
+ ![Throughput comparison — Ferro Labs vs Kong, Bifrost, LiteLLM, Portkey across 150–1,000 VU](docs/benchmarks/throughput-comparison.png)
438
+
439
+ | VU | RPS | p50 | p99 | Memory |
440
+ |---:|---:|---:|---:|---:|
441
+ | 50 | 813 | 61.3ms | 64.1ms | 36 MB |
442
+ | 150 | 2,447 | 61.2ms | 63.4ms | 47 MB |
443
+ | 300 | 4,890 | 61.2ms | 64.4ms | 72 MB |
444
+ | 500 | 8,014 | 61.5ms | 72.9ms | 89 MB |
445
+ | 1,000 | 13,925 | 68.1ms | 111.9ms | 135 MB |
446
+
447
+ At 1,000 VU: **13,925 RPS**, p50 overhead **8.1ms**, memory **135 MB**.
448
+ Against the live OpenAI API, the gateway itself adds **25 microseconds** p50 in
449
+ a typical plugin configuration, and **2 microseconds** with no plugins enabled.
450
+
451
+ Full methodology, raw results, and flamegraph analysis:
452
+ [ferro-labs/ai-gateway-performance-benchmarks](https://github.com/ferro-labs/ai-gateway-performance-benchmarks)
453
+ (`make setup && make bench` reproduces it).
454
+
455
+ ---
456
+
457
+ ## FerroCloud
458
+
459
+ FerroCloud — the managed version of Ferro Labs AI Gateway with multi-tenancy, analytics, and cost governance — is coming soon.
460
+
461
+ 👉 **Join the waitlist at [ferrolabs.ai](https://ferrolabs.ai)**
462
+
463
+ ---
464
+
465
+ ## SDKs
466
+
467
+ Official client libraries for the Ferro Labs AI Gateway — and the standard
468
+ OpenAI SDK works unchanged: point `base_url` at `http://your-gateway:8080/v1`.
469
+
470
+ | SDK | Install | Repository |
471
+ |:----|:--------|:-----------|
472
+ | Python | `pip install ferrolabs` | [ferro-labs/ferrolabs-python-sdk](https://github.com/ferro-labs/ferrolabs-python-sdk) |
473
+ | TypeScript | `npm install ferrolabs` | [ferro-labs/ferrolabs-typescript-sdk](https://github.com/ferro-labs/ferrolabs-typescript-sdk) |
474
+
475
+ <details>
476
+ <summary><strong>Python</strong></summary>
477
+
478
+ ```python
479
+ from ferrolabs import FerroClient
480
+
481
+ client = FerroClient(
482
+ base_url="http://localhost:8080/v1",
483
+ api_key="your-ferro-api-key",
484
+ )
485
+
486
+ response = client.chat.completions.create(
487
+ model="gpt-4o",
488
+ messages=[{"role": "user", "content": "Hello"}],
489
+ )
490
+ ```
491
+
492
+ </details>
493
+
494
+ <details>
495
+ <summary><strong>TypeScript</strong></summary>
496
+
497
+ ```typescript
498
+ import { FerroClient } from "ferrolabs";
499
+
500
+ const client = new FerroClient({
501
+ baseURL: "http://localhost:8080/v1",
502
+ apiKey: "your-ferro-api-key",
503
+ });
504
+
505
+ const response = await client.chat.completions.create({
506
+ model: "gpt-4o",
507
+ messages: [{ role: "user", content: "Hello" }],
508
+ });
509
+ ```
510
+
511
+ </details>
512
+
513
+ ---
514
+
515
+ ## Contributing
516
+
517
+ We welcome contributions. New providers go in this OSS repo only — never in FerroCloud. See [CONTRIBUTING.md](CONTRIBUTING.md) for branch strategy, commit conventions, and PR guidelines.
518
+
519
+ ---
520
+
521
+ ## Community
522
+
523
+ - [GitHub Discussions](https://github.com/ferro-labs/ai-gateway/discussions)
524
+ - [Discord](https://discord.gg/yCAeYvJeDV)
525
+ - Built with Ferro Labs AI Gateway? Open a PR to add to our showcase.
526
+
527
+ ---
528
+
529
+ ## License
530
+
531
+ Apache 2.0 — see [LICENSE](LICENSE).
package/bin/ferrogw.js ADDED
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ // The `bin` of the `ferrogw` npm package: find the binary that this host's
3
+ // platform package shipped, then hand the process over to it.
4
+ //
5
+ // Why a shim exists at all: one npm package cannot carry six platform binaries
6
+ // without every install paying for five it will never run. So the binaries live
7
+ // in `@ferro-labs-ai/gateway-<platform>` packages that carry `os`/`cpu` fields —
8
+ // npm installs exactly the one that matches and silently skips the rest — and
9
+ // this file is the single `bin` entry the user actually gets.
10
+ //
11
+ // Deliberately NOT a postinstall downloader. `--ignore-scripts` is increasingly
12
+ // the corporate default, an offline install has nothing to download, and a
13
+ // download records no integrity hash in the lockfile. Optional dependencies are
14
+ // integrity-checked like any other and need no script execution.
15
+
16
+ "use strict";
17
+
18
+ const path = require("node:path");
19
+ const { spawn } = require("node:child_process");
20
+
21
+ // npm's platform vocabulary, not Go's: process.arch says `x64` where the
22
+ // release archive says `amd64`, and process.platform says `win32` where it says
23
+ // `windows`. The package names follow npm, because `os`/`cpu` are matched
24
+ // against these exact values.
25
+ const pkg = `@ferro-labs-ai/gateway-${process.platform}-${process.arch}`;
26
+ const exe = process.platform === "win32" ? "ferrogw.exe" : "ferrogw";
27
+
28
+ let binary;
29
+ try {
30
+ // Resolve the platform package's manifest and walk to the binary beside it,
31
+ // rather than resolving the binary path directly: the POSIX binary has no
32
+ // extension, and leaning on require.resolve's extension search for that is a
33
+ // subtlety with no upside. The platform packages declare no `exports`, so
34
+ // `<pkg>/package.json` is always resolvable.
35
+ binary = path.join(require.resolve(`${pkg}/package.json`), "..", exe);
36
+ } catch {
37
+ console.error(
38
+ `ferrogw: no prebuilt binary for ${process.platform}-${process.arch}.\n` +
39
+ `\n` +
40
+ `The optional dependency ${pkg} is not installed. Either this platform is\n` +
41
+ `not one the gateway publishes binaries for, or the install ran with\n` +
42
+ `--omit=optional / --no-optional and skipped it.\n` +
43
+ `\n` +
44
+ `Other install paths: https://get.ferrolabs.ai`,
45
+ );
46
+ process.exit(1);
47
+ }
48
+
49
+ // spawn, not spawnSync. On POSIX the ideal is to replace this process with the
50
+ // binary outright, but Node exposes no execve, so a parent process survives for
51
+ // the whole life of the server and has to behave like one. spawnSync blocks the
52
+ // event loop, so no handler could ever run: Ctrl-C would still reach the child
53
+ // (a tty signals the entire foreground process group), but a `kill`, a
54
+ // `docker stop` or a systemd unit stop targets the parent alone, and that
55
+ // SIGTERM has to be relayed or the gateway never gets its graceful shutdown.
56
+ const child = spawn(binary, process.argv.slice(2), { stdio: "inherit" });
57
+
58
+ // SIGBREAK does not exist off Windows; Node treats an unknown signal name as an
59
+ // ordinary event that simply never fires, so the list needs no per-OS branch.
60
+ const FORWARDED = ["SIGINT", "SIGTERM", "SIGHUP", "SIGBREAK"];
61
+ for (const signal of FORWARDED) {
62
+ process.on(signal, () => {
63
+ // `child.killed` records only that a signal was ever SENT -- it flips true
64
+ // on the first kill() and stays true while the child keeps running, which
65
+ // is the normal case here, because the gateway catches SIGINT/SIGTERM to
66
+ // shut down gracefully. Guarding on it relays the first signal and swallows
67
+ // every one after it, so a supervisor escalating SIGINT -> SIGTERM ->
68
+ // SIGKILL gets only the SIGINT through and then waits out its whole timeout
69
+ // on a process nobody asked again. exitCode/signalCode are both null
70
+ // exactly while the child is running, and one of them is set the moment it
71
+ // is not, which is the fact this guard actually wants.
72
+ //
73
+ // Wrapped because this runs INSIDE a signal handler, where an uncaught
74
+ // throw takes the shim down and orphans the gateway. On Windows only
75
+ // SIGTERM/SIGKILL/SIGINT terminate a process, and libuv raises EINVAL for
76
+ // the other two names in FORWARDED -- so on the one platform where SIGHUP
77
+ // and SIGBREAK are reachable, relaying them is what would kill the relay.
78
+ if (child.exitCode === null && child.signalCode === null) {
79
+ try {
80
+ child.kill(signal);
81
+ } catch {
82
+ // Nothing useful to do: the child is alive and this platform will not
83
+ // deliver this signal. Staying up keeps the exit-code and re-raise
84
+ // paths below intact, which is the more important guarantee.
85
+ }
86
+ }
87
+ });
88
+ }
89
+
90
+ child.on("error", (err) => {
91
+ console.error(`ferrogw: cannot execute ${binary}: ${err.message}`);
92
+ process.exit(1);
93
+ });
94
+
95
+ child.on("close", (code, signal) => {
96
+ if (signal) {
97
+ // Re-raise so the wait status a supervisor reads says "killed by SIGTERM"
98
+ // rather than "exited 143" — those are different facts and only one of them
99
+ // is true. Handlers come off first or we would trap our own signal.
100
+ for (const s of FORWARDED) process.removeAllListeners(s);
101
+ process.kill(process.pid, signal);
102
+ return;
103
+ }
104
+ process.exit(code ?? 1);
105
+ });
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "ferrogw",
3
+ "version": "1.4.4",
4
+ "description": "Ferro Labs AI Gateway — one OpenAI-compatible API in front of 30+ LLM providers, with routing, guardrails and key management.",
5
+ "license": "Apache-2.0",
6
+ "author": "Ferro Labs <hello@ferrolabs.ai>",
7
+ "homepage": "https://github.com/ferro-labs/ai-gateway",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/ferro-labs/ai-gateway.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/ferro-labs/ai-gateway/issues"
14
+ },
15
+ "keywords": [
16
+ "ai",
17
+ "llm",
18
+ "gateway",
19
+ "openai",
20
+ "anthropic",
21
+ "proxy",
22
+ "routing",
23
+ "ferro",
24
+ "ferrolabs",
25
+ "ai-gateway"
26
+ ],
27
+ "bin": {
28
+ "ferrogw": "bin/ferrogw.js"
29
+ },
30
+ "files": [
31
+ "bin"
32
+ ],
33
+ "engines": {
34
+ "node": ">=18"
35
+ },
36
+ "optionalDependencies": {
37
+ "@ferro-labs-ai/gateway-linux-x64": "1.4.4",
38
+ "@ferro-labs-ai/gateway-linux-arm64": "1.4.4",
39
+ "@ferro-labs-ai/gateway-darwin-x64": "1.4.4",
40
+ "@ferro-labs-ai/gateway-darwin-arm64": "1.4.4",
41
+ "@ferro-labs-ai/gateway-win32-x64": "1.4.4",
42
+ "@ferro-labs-ai/gateway-win32-arm64": "1.4.4"
43
+ }
44
+ }