omnigateway 0.2.1 → 0.3.0
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.
- package/README.md +426 -13
- package/bin/omni.js +7405 -3907
- package/gateway.js +5564 -1384
- package/package.json +1 -1
- package/public/assets/{Chip-C4X8tf5z.js → Chip-DbKk1ExR.js} +2 -2
- package/public/assets/Confirm-C5DYoI93.js +4 -0
- package/public/assets/CopyValue-CXjietiZ.js +27 -0
- package/public/assets/{Field-Ct4SeDZD.js → Field-B7GOsJf8.js} +8 -8
- package/public/assets/Lamp-QRSv-SqM.js +25 -0
- package/public/assets/{Meter-COdz0dwY.js → Meter-Dngwbpjo.js} +2 -2
- package/public/assets/Modal-DfltaSCz.js +82 -0
- package/public/assets/Rack-D1zBw9SW.js +151 -0
- package/public/assets/{Readout-Bl2kD9Tl.js → Readout--CQnPjoO.js} +4 -4
- package/public/assets/{States-D5Memn8D.js → States-CQFkw1UZ.js} +10 -10
- package/public/assets/{Table-DObRatbW.js → Table-Ba58Wg4-.js} +1 -1
- package/public/assets/Toggle-DWrWc9-9.js +39 -0
- package/public/assets/TokenBreakdown-CyHJv0O5.js +31 -0
- package/public/assets/_app-CTDgfstQ.js +1 -0
- package/public/assets/_app.accounts-DP6T8z7I.js +64 -0
- package/public/assets/{_app.console-BVVpryIB.js → _app.console-DYr7TCAl.js} +10 -10
- package/public/assets/_app.database-BzO1S_VR.js +39 -0
- package/public/assets/_app.index-CBt6b-lA.js +62 -0
- package/public/assets/_app.keys-B8HGsPPS.js +76 -0
- package/public/assets/_app.logs-DKsxa2Su.js +68 -0
- package/public/assets/_app.models-CZD2UpM-.js +148 -0
- package/public/assets/_app.settings-B2C1LQIF.js +46 -0
- package/public/assets/_app.usage-CDWUXtZy.js +83 -0
- package/public/assets/catalog-BBKm8Ih8.js +1 -0
- package/public/assets/dist-IkJ0qg-4.js +1 -0
- package/public/assets/index-ClU3Xqui.js +186 -0
- package/public/assets/login-E3gh2_Cl.js +36 -0
- package/public/assets/plus-DkO4vG-T.js +1 -0
- package/public/assets/queries-BJHk_wso.js +144 -0
- package/public/assets/reasons-Cb8yF4Oy.js +1 -0
- package/public/assets/shared-Bp35cgFf.js +84 -0
- package/public/assets/{trash-2-Da6cLgPn.js → trash-2-DTWWI3vA.js} +1 -1
- package/public/index.html +2 -2
- package/public/assets/Confirm-BnmH8Gn0.js +0 -4
- package/public/assets/CopyValue-B-uR4Js-.js +0 -27
- package/public/assets/Lamp-CBOwqG5K.js +0 -25
- package/public/assets/Modal-DMFUHQ-A.js +0 -82
- package/public/assets/Rack-DAbY9x1I.js +0 -151
- package/public/assets/Toggle-Dkjlvck5.js +0 -39
- package/public/assets/TokenBreakdown-oXP49ZTa.js +0 -31
- package/public/assets/_app-Brc2wQZm.js +0 -1
- package/public/assets/_app.accounts-I9RXUiyi.js +0 -54
- package/public/assets/_app.index-83Ig4FUL.js +0 -62
- package/public/assets/_app.keys-KeP6L4g_.js +0 -39
- package/public/assets/_app.logs-B5oh01Ls.js +0 -32
- package/public/assets/_app.models-BuOFMZUN.js +0 -144
- package/public/assets/_app.settings-BN436vI2.js +0 -38
- package/public/assets/_app.usage-C528VOia.js +0 -166
- package/public/assets/catalog-kJ53n_fc.js +0 -1
- package/public/assets/dist-CnzT-Ut2.js +0 -1
- package/public/assets/index-CE-KQ-ju.js +0 -172
- package/public/assets/login-2LpCZOtB.js +0 -33
- package/public/assets/queries-1zRLkX-Q.js +0 -144
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
One endpoint in front of the AI accounts you already pay for.
|
|
4
4
|
|
|
5
5
|
OmniGateway is a self-hosted gateway that speaks the Anthropic and OpenAI APIs
|
|
6
|
-
and answers them using your own Anthropic, OpenAI,
|
|
6
|
+
and answers them using your own Anthropic, OpenAI, Kimi Coding, Kilo, and xAI
|
|
7
7
|
subscriptions. Point any compatible client at it, ask for a model you defined,
|
|
8
8
|
and the gateway picks an account that can serve it — falling back to another
|
|
9
9
|
when one is rate-limited, expired, or out of quota.
|
|
@@ -29,7 +29,7 @@ omni start
|
|
|
29
29
|
provider dispatch, preserve errors and non-tool-result content, and default off.
|
|
30
30
|
- **Routes across your accounts.** Define a virtual model like `fast` or
|
|
31
31
|
`smart` with several targets; the gateway ranks them by tier, health,
|
|
32
|
-
remaining quota, cost, and
|
|
32
|
+
remaining quota, cost, latency, and current load.
|
|
33
33
|
- **Fails over.** A rate-limited or broken account is skipped, its circuit
|
|
34
34
|
breaker opens, and the next candidate is tried — before the response starts
|
|
35
35
|
streaming.
|
|
@@ -39,18 +39,89 @@ omni start
|
|
|
39
39
|
routes by *pace*: 5% remaining is fine minutes before a reset and urgent with
|
|
40
40
|
six days to run.
|
|
41
41
|
- **Issues its own keys.** Hand out gateway keys with per-key model allowlists
|
|
42
|
-
|
|
42
|
+
instead of sharing provider credentials, and bound each one by requests,
|
|
43
|
+
tokens, dollars, or requests in flight — per minute, per five hours, and per
|
|
44
|
+
week.
|
|
43
45
|
- **Reports usage.** Requests, tokens, and cost by provider, model, key, and
|
|
44
46
|
day — metadata only.
|
|
47
|
+
- **Backs itself up.** Snapshot the database from the console or the CLI, see
|
|
48
|
+
what it occupies, reclaim what deletion left behind, and restore a snapshot
|
|
49
|
+
without stopping the gateway.
|
|
45
50
|
- **Ships an admin console and a CLI.** Both cover the same ground; use
|
|
46
51
|
whichever suits the machine you are on.
|
|
47
52
|
|
|
53
|
+
## How it is built
|
|
54
|
+
|
|
55
|
+
A Bun workspace monorepo. The layering is deliberate and enforced by what each
|
|
56
|
+
package is allowed to import: a provider-neutral core at the bottom, provider
|
|
57
|
+
knowledge in one place, pure routing above that, and every side effect pushed up
|
|
58
|
+
into the gateway process.
|
|
59
|
+
|
|
60
|
+
```mermaid
|
|
61
|
+
graph TD
|
|
62
|
+
subgraph frontends[Front ends]
|
|
63
|
+
gateway["apps/gateway<br/><i>Elysia server, dispatch, loops</i>"]
|
|
64
|
+
cli["apps/cli<br/><i>the omni binary</i>"]
|
|
65
|
+
dashboard["apps/dashboard<br/><i>React console</i>"]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
control["@omni/control<br/><i>every operator action;<br/>no HTTP, argv or terminal</i>"]
|
|
69
|
+
router["@omni/router<br/><i>pure ranking;<br/>no I/O, no timers</i>"]
|
|
70
|
+
store["@omni/store<br/><i>SQLite + field encryption</i>"]
|
|
71
|
+
providers["@omni/providers<br/><i>adapters, wire codecs,<br/>catalog, HTTP client</i>"]
|
|
72
|
+
rtk["@omni/rtk<br/><i>tool-result filters</i>"]
|
|
73
|
+
ratelimit["@omni/ratelimit<br/><i>key limit arithmetic;<br/>no clock, no state</i>"]
|
|
74
|
+
ir["@omni/ir<br/><i>domain model — depends on nothing</i>"]
|
|
75
|
+
|
|
76
|
+
gateway --> control
|
|
77
|
+
gateway --> rtk
|
|
78
|
+
gateway --> ratelimit
|
|
79
|
+
cli --> control
|
|
80
|
+
dashboard -. "types + catalog only" .-> providers
|
|
81
|
+
dashboard -. "types only" .-> store
|
|
82
|
+
control --> router
|
|
83
|
+
control --> ratelimit
|
|
84
|
+
router --> store
|
|
85
|
+
router --> providers
|
|
86
|
+
store --> rtk
|
|
87
|
+
store --> ratelimit
|
|
88
|
+
providers --> ir
|
|
89
|
+
rtk --> ir
|
|
90
|
+
store --> ir
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Arrows read *depends on*, and the direction never reverses. Two rules do most of
|
|
94
|
+
the work: `@omni/ir` is side-effect free and imports nothing, and `@omni/router`
|
|
95
|
+
is a pure function — no network, no database, no timers. Everything that has to
|
|
96
|
+
touch the world is pushed up into the gateway process.
|
|
97
|
+
|
|
98
|
+
`@omni/ratelimit` is the same shape applied to key limits: it decides whether a
|
|
99
|
+
request is over a ceiling, and it holds no clock and no counters — `now` is a
|
|
100
|
+
parameter and the counts are handed to it, so it never learns whether a number
|
|
101
|
+
came from memory or from SQLite. It imports nothing but its schema validator, not
|
|
102
|
+
even `@omni/ir`. The rings and the in-flight gauge live in the gateway, because
|
|
103
|
+
state is not the package's job, and that split is what lets sliding-window
|
|
104
|
+
arithmetic be tested without a gateway, a store, or a clock.
|
|
105
|
+
|
|
106
|
+
The dashboard's edges are dotted because they are type-level only.
|
|
107
|
+
`@omni/providers/catalog` is deliberately kept import-free so model lists can be
|
|
108
|
+
bundled into the browser without dragging in the HTTP client.
|
|
109
|
+
|
|
110
|
+
The gateway and the CLI are two front ends over the same `@omni/control`
|
|
111
|
+
functions. The CLI does not call the running server's API — it opens the same
|
|
112
|
+
SQLite file directly, which is why it still works when the gateway is down.
|
|
113
|
+
|
|
114
|
+
**[ARCHITECTURE.md](ARCHITECTURE.md)** goes a level deeper: a request traced end
|
|
115
|
+
to end, how routing ranks and excludes accounts, the commit point that decides
|
|
116
|
+
whether failover is still possible, the provider adapter shape, the database
|
|
117
|
+
schema and its encryption boundary, and the background loops.
|
|
118
|
+
|
|
48
119
|
## Requirements
|
|
49
120
|
|
|
50
121
|
- [Bun](https://bun.sh/) 1.4 or later. Bun is the runtime, not just the
|
|
51
122
|
installer, so a Node-only machine cannot run OmniGateway.
|
|
52
123
|
- A directory that persists, for the SQLite database.
|
|
53
|
-
- At least one Anthropic, OpenAI,
|
|
124
|
+
- At least one Anthropic, OpenAI, Kimi Coding, Kilo, or xAI account to connect.
|
|
54
125
|
|
|
55
126
|
## Install
|
|
56
127
|
|
|
@@ -86,9 +157,18 @@ omni start # serves the API and the console on 127.0.0.1:9000
|
|
|
86
157
|
Connect an account. The CLI prints a URL to open, and waits:
|
|
87
158
|
|
|
88
159
|
```bash
|
|
89
|
-
omni connect anthropic # or: openai, kimi
|
|
160
|
+
omni connect anthropic # or: openai, kimi, kilo, grok
|
|
90
161
|
```
|
|
91
162
|
|
|
163
|
+
Every provider also takes a plain API key, if that is what you hold rather than
|
|
164
|
+
a subscription. `custom` takes nothing else:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
omni credentials add-key anthropic # prompts for the key, or reads stdin
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The console offers the same choice per provider on the Connect dialog.
|
|
171
|
+
|
|
92
172
|
Define a virtual model your clients will ask for, seeding its pricing and
|
|
93
173
|
capabilities from the built-in catalog:
|
|
94
174
|
|
|
@@ -97,12 +177,70 @@ omni models catalog # what is available
|
|
|
97
177
|
omni models put fast --from-catalog anthropic:claude-sonnet-5
|
|
98
178
|
```
|
|
99
179
|
|
|
180
|
+
**A note on grok pricing.** xAI charges by request size: at or above 200K
|
|
181
|
+
context the rate roughly doubles, and the higher rate applies to *every token
|
|
182
|
+
in the request*, not just the tokens past the mark. A target holds one flat
|
|
183
|
+
price, so the catalog carries xAI's sub-200K figures and long-context traffic
|
|
184
|
+
is reported cheaper than it was billed. Catalog pricing is only the default a
|
|
185
|
+
new target starts from — if you run grok at long context, edit the saved
|
|
186
|
+
target's price to match the tier you are actually paying.
|
|
187
|
+
|
|
188
|
+
**A note on `kilo-auto/*` pricing.** Kilo's `frontier`, `balanced`, and
|
|
189
|
+
`efficient` routers choose an upstream model per request, and Kilo states no
|
|
190
|
+
rate for them. The catalog records zero, which the router reads as *unpriced*
|
|
191
|
+
and leaves out of its cost ranking — the same stored figure `kilo-auto/free`
|
|
192
|
+
carries because it genuinely is free. So a `kilo-auto` target seeded from the
|
|
193
|
+
catalog is not free, it is unranked: cost never counts for or against it. If you
|
|
194
|
+
want one ranked against your other accounts, set a real `costPerMTok` on the
|
|
195
|
+
saved target for the tier you expect it to land in.
|
|
196
|
+
|
|
100
197
|
Mint a key for your client. **It is printed once and stored only as a hash:**
|
|
101
198
|
|
|
102
199
|
```bash
|
|
103
200
|
omni keys create --label laptop
|
|
104
201
|
```
|
|
105
202
|
|
|
203
|
+
Bound what the key can do with `--limit <dimension>:<window>=<value>`, repeated
|
|
204
|
+
once per pair. An unset pair is unlimited:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
omni keys create --label ci --limit requests:1m=60
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Every window is *sliding*, so a key cannot spend two windows' allowance either
|
|
211
|
+
side of a clock edge. `requests` and `tokens` take `1m`, `5h`, and `1w`; `spend`
|
|
212
|
+
takes `5h` and `1w`; `concurrency` is not a window at all but a ceiling on
|
|
213
|
+
requests in flight at once.
|
|
214
|
+
|
|
215
|
+
`tokens` and `spend` are debited once a response completes, because an exact
|
|
216
|
+
count exists only then — a key at its ceiling is refused on its *next* request.
|
|
217
|
+
The `5h` and `1w` counts derive from `request_logs`, so a `1w` limit on an
|
|
218
|
+
installation that prunes logs after three days really enforces three days. They
|
|
219
|
+
are cached for thirty seconds and so can read slightly high, never low: refused
|
|
220
|
+
early rather than let past a ceiling you set. If the database cannot answer they
|
|
221
|
+
stop enforcing and the gateway logs it, while `1m` and `concurrency` are held in
|
|
222
|
+
memory and go on enforcing exactly.
|
|
223
|
+
|
|
224
|
+
> **Breaking:** `--rate-limit N` is removed, not aliased. Use
|
|
225
|
+
> `--limit requests:1m=N`. A script still passing the old flag stops with an
|
|
226
|
+
> unknown-flag error rather than quietly taking a deprecated path.
|
|
227
|
+
|
|
228
|
+
Limits are editable after the key exists, unlike `--no-bodies` below. `omni keys
|
|
229
|
+
list` prints a compact summary; the full matrix and what has gone against it
|
|
230
|
+
need one key's id:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
omni keys limits <id>
|
|
234
|
+
omni keys limits <id> --set tokens:1w=50000000
|
|
235
|
+
omni keys limits <id> --unset spend:5h
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`--unset` names a pair that is actually set, so a typo fails rather than
|
|
239
|
+
reporting a change it did not make. The usage shown counts completed requests
|
|
240
|
+
only, so it reads at or below what the gateway is enforcing, and `concurrency`
|
|
241
|
+
shows no figure — that gauge lives in the process, not the database. The
|
|
242
|
+
console's Keys screen shows and edits the same matrix.
|
|
243
|
+
|
|
106
244
|
Now use it:
|
|
107
245
|
|
|
108
246
|
```bash
|
|
@@ -143,6 +281,36 @@ once.
|
|
|
143
281
|
Most tools that accept a custom base URL work unchanged: set it to
|
|
144
282
|
`http://127.0.0.1:9000` and use a gateway key where the provider key goes.
|
|
145
283
|
|
|
284
|
+
### Rate-limit headers
|
|
285
|
+
|
|
286
|
+
Every response carries the limit headers of the surface you asked on, so an SDK
|
|
287
|
+
backs off using the code it already ships — the Anthropic dialect on
|
|
288
|
+
`/v1/messages`, the OpenAI one on `/v1/chat/completions`:
|
|
289
|
+
|
|
290
|
+
```http
|
|
291
|
+
anthropic-ratelimit-requests-limit: 2000 x-ratelimit-limit-requests: 2000
|
|
292
|
+
anthropic-ratelimit-requests-remaining: 1841 x-ratelimit-remaining-requests: 1841
|
|
293
|
+
anthropic-ratelimit-requests-reset: 2026-08-19T14:32:07Z x-ratelimit-reset-requests: 4h51m22s
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
`requests-remaining` counts the request you are being answered, as both vendors
|
|
297
|
+
define it. `tokens-remaining` does not, and cannot: the response is still being
|
|
298
|
+
written when the header goes out, so its token cost is not yet known and
|
|
299
|
+
subtracting anything would be an invented number rather than a measured one.
|
|
300
|
+
|
|
301
|
+
Where a key has several windows on one dimension, the headers report the one
|
|
302
|
+
**nearest exhaustion** — a key comfortable per-minute but one request from its
|
|
303
|
+
weekly ceiling shows you the weekly figures, not the reassuring ones.
|
|
304
|
+
|
|
305
|
+
`spend` and `concurrency` are rendered on neither dialect, because no vendor
|
|
306
|
+
defines a header for them and a number no client parses is noise in every
|
|
307
|
+
response.
|
|
308
|
+
|
|
309
|
+
A refusal is `429` with `Retry-After` in seconds, alongside the usual error body.
|
|
310
|
+
The wait is computed from the oldest request still inside the window that
|
|
311
|
+
refused you, so a weekly ceiling tells you when a slot actually frees rather than
|
|
312
|
+
parking you for seven days.
|
|
313
|
+
|
|
146
314
|
### Tools and routing
|
|
147
315
|
|
|
148
316
|
Two kinds of tool behave differently, and the difference decides where a
|
|
@@ -170,21 +338,31 @@ carries the header through but does not add one on your behalf.
|
|
|
170
338
|
command takes `--json` for scripting, and `--root <path>` to manage an
|
|
171
339
|
installation other than the default.
|
|
172
340
|
|
|
341
|
+
`--root` names the installation, so an `OMNI_DB_PATH` exported in your shell is
|
|
342
|
+
ignored alongside it and said so on stderr; that root's own `.env` still decides.
|
|
343
|
+
Use `--db <path>` to point one command somewhere else.
|
|
344
|
+
|
|
173
345
|
| | |
|
|
174
346
|
| --- | --- |
|
|
175
347
|
| `omni status` | the gateway, its accounts, and their quota, on one screen |
|
|
176
348
|
| `omni start` / `stop` / `restart` | run the gateway; `--foreground` attaches it to your terminal |
|
|
177
349
|
| `omni doctor` | which installation it resolved, and whether it can act on it |
|
|
178
350
|
| `omni logs` | recent requests as the gateway recorded them |
|
|
351
|
+
| `omni bodies <request-id>` | captured bodies for one request; withheld unless you pass `--full` |
|
|
179
352
|
| `omni console` | the gateway process's own output: boot, refreshes, quota, errors |
|
|
180
353
|
| `omni usage` | spend and tokens, by provider, model, key, or day |
|
|
354
|
+
| `omni quota` | provider quota per window: use, burn rate, and when it runs out |
|
|
181
355
|
| `omni connect <provider>` | authorize an account from the terminal |
|
|
182
356
|
| `omni credentials …` | list, show, enable, disable, retier, refresh, remove |
|
|
183
357
|
| `omni models …` | list, show, put, remove, `dry-run`, `catalog` |
|
|
184
358
|
| `omni keys …` | list, create, revoke |
|
|
185
|
-
| `omni settings get` / `set` | routing weights, retention, deadlines |
|
|
359
|
+
| `omni settings get` / `set` | routing weights, retention, deadlines, and the runtime switches |
|
|
186
360
|
| `omni admin set-password` | change the console password |
|
|
187
361
|
| `omni db migrate` | create or upgrade the database |
|
|
362
|
+
| `omni db stats` | size on disk, free pages, schema version, and what snapshots are held |
|
|
363
|
+
| `omni db backup` / `snapshots` | take a snapshot, and list the ones retention has kept |
|
|
364
|
+
| `omni db restore <id>` | put a snapshot back; asks first, and refuses while the gateway is running |
|
|
365
|
+
| `omni db vacuum` | rewrite the database, reclaiming the pages deletion left free |
|
|
188
366
|
|
|
189
367
|
Two worth knowing:
|
|
190
368
|
|
|
@@ -209,6 +387,29 @@ start` supervises the process itself with a pidfile under
|
|
|
209
387
|
`~/.local/state/omnigateway`. Either way, `omni start` returns only once
|
|
210
388
|
`/health` actually answers.
|
|
211
389
|
|
|
390
|
+
### Restarting and stopping from the console
|
|
391
|
+
|
|
392
|
+
The console's Database screen can restart and shut down the gateway. A restart
|
|
393
|
+
only restarts if something would start the process again, so what the control
|
|
394
|
+
does — and whether it is offered at all — depends on how this installation runs:
|
|
395
|
+
|
|
396
|
+
- **Under systemd**, it works. The gateway asks the manager rather than
|
|
397
|
+
signalling itself — `systemctl [--user] --no-block restart
|
|
398
|
+
omnigateway.service` — because the unit `omni service install` writes sets
|
|
399
|
+
`Restart=on-failure`, and a handled `SIGTERM` exits cleanly, which systemd
|
|
400
|
+
reads as success. A gateway that killed itself would stop and stay stopped.
|
|
401
|
+
- **In a container**, it depends on the container's restart policy, and that
|
|
402
|
+
policy cannot be read from inside the container. The console says so instead
|
|
403
|
+
of promising a restart it has no way to verify. Run with
|
|
404
|
+
`--restart unless-stopped` if you want the control to mean what it says.
|
|
405
|
+
- **With no supervisor** — `omni start` with no unit installed — the control is
|
|
406
|
+
disabled and shows the reason. Nothing is watching the process, so an exit is
|
|
407
|
+
simply the end of it. Use `omni restart` from a terminal on the machine.
|
|
408
|
+
|
|
409
|
+
Shutdown is offered in every shape, because stopping is the point of it. In a
|
|
410
|
+
container it is a one-way door: stopping the only process takes the console that
|
|
411
|
+
would have restarted it, so bringing the gateway back needs access to the host.
|
|
412
|
+
|
|
212
413
|
## Configuration
|
|
213
414
|
|
|
214
415
|
Configuration is environment variables, read from the installation's `.env`:
|
|
@@ -223,6 +424,7 @@ Configuration is environment variables, read from the installation's `.env`:
|
|
|
223
424
|
| `OMNI_STATIC_DIR` | No | the console shipped with the server | Serve a different console build |
|
|
224
425
|
| `OMNI_LOG_LEVEL` | No | `info` | Stdout threshold: `debug`, `info`, `warn`, or `error` |
|
|
225
426
|
| `OMNI_LOG_FILE` | No | the systemd journal, when there is one | Where stdout was already redirected, so the Console screen can read it back. Names a file; does not create one |
|
|
427
|
+
| `OMNI_BODY_LOGGING_ALLOWED` | No | unset | Permits request/response body capture on this installation. Read at boot. Capture also needs the runtime setting; see [Recording bodies](#recording-bodies) |
|
|
226
428
|
|
|
227
429
|
Gateway events are written to stdout as one greppable line each: process lifecycle, OAuth
|
|
228
430
|
refreshes, quota probes, failover, and errors.
|
|
@@ -245,6 +447,193 @@ environment. Edit it with `omni settings set` or in the console.
|
|
|
245
447
|
`.env.example` in the repository documents the optional provider
|
|
246
448
|
client-identity overrides.
|
|
247
449
|
|
|
450
|
+
## Recording bodies
|
|
451
|
+
|
|
452
|
+
By default the gateway records no prompts and no responses. If you need them for
|
|
453
|
+
an incident — to see what a client actually sent, or what a provider actually
|
|
454
|
+
returned — capture is opt-in and takes **two independent keys, both required**:
|
|
455
|
+
|
|
456
|
+
1. `OMNI_BODY_LOGGING_ALLOWED=1` in the installation's `.env`, read at boot.
|
|
457
|
+
2. The **Capture request and response bodies** setting, in the console's
|
|
458
|
+
Settings screen or `omni settings set bodyLoggingEnabled true`. Off by
|
|
459
|
+
default. Raw SSE frames are the separate, far larger
|
|
460
|
+
`bodyLoggingCaptureStreamChunks`.
|
|
461
|
+
|
|
462
|
+
Two keys, because an admin session on its own must not be able to start
|
|
463
|
+
recording your users' prompts. With the environment variable unset the setting
|
|
464
|
+
does nothing at all, and the console says so rather than letting you flip a
|
|
465
|
+
switch that silently no-ops. With the environment variable set you can turn
|
|
466
|
+
capture on and off mid-incident without restarting.
|
|
467
|
+
|
|
468
|
+
Turning capture off stops new capture. It does not delete what was already
|
|
469
|
+
written.
|
|
470
|
+
|
|
471
|
+
**Per-key opt-out.** A gateway key can be created with *Never record this key's
|
|
472
|
+
bodies* — `omni keys create --no-bodies` — and it is then never captured whatever
|
|
473
|
+
the setting says. Use it for a client whose payloads must not be retained. The
|
|
474
|
+
choice is made when the key is issued and cannot be reversed afterwards; reissue
|
|
475
|
+
the key instead. `omni keys list` and the console's Keys screen both show which
|
|
476
|
+
keys are exempt, so an audit does not have to go through the database.
|
|
477
|
+
|
|
478
|
+
**What is captured.** What arrived at `/v1/*` and what was returned, plus the
|
|
479
|
+
request and response of every provider attempt, in dispatch order. The client
|
|
480
|
+
request is the conversation *before* RTK compression and each attempt request is
|
|
481
|
+
the one *after* it, so an artifact is the only place you can read what a filter
|
|
482
|
+
actually removed. The console labels which side is which, and so does the CLI.
|
|
483
|
+
|
|
484
|
+
**Reading them.** Expand a row on the console's Logs screen, or from a terminal:
|
|
485
|
+
|
|
486
|
+
```bash
|
|
487
|
+
omni bodies req_550e8400-… # the frame: state, size, one line per attempt
|
|
488
|
+
omni bodies req_550e8400-… --full # the payloads themselves
|
|
489
|
+
omni bodies req_550e8400-… --json # the artifact, for a script
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
**The bare command withholds the bodies and prints only the frame.** Every other
|
|
493
|
+
CLI read prints everything it has; this one prints conversations, and whoever
|
|
494
|
+
runs it during an incident is usually sharing that screen. Asking costs one
|
|
495
|
+
flag; printing by default costs a prompt corpus in someone's scrollback,
|
|
496
|
+
silently. The frame still gives you the state, capture time, size on disk, any
|
|
497
|
+
truncation, and each attempt's provider and byte counts — labelled `pre-RTK` for
|
|
498
|
+
the client request and `post-RTK` for attempts, because they are not the same
|
|
499
|
+
payload.
|
|
500
|
+
|
|
501
|
+
A request with no artifact is an answer rather than an error, and the three
|
|
502
|
+
answers are different: `not captured` means capture was not running, `captured,
|
|
503
|
+
then lost` means retention or the row cap has been through, and `captured, but
|
|
504
|
+
unreadable` means the file is there but will not decrypt — usually a changed
|
|
505
|
+
`OMNI_ENCRYPTION_KEY`.
|
|
506
|
+
|
|
507
|
+
There is no CLI command to delete a captured body. Retention, the row cap, and
|
|
508
|
+
the orphan sweep are the gateway's; a second path that erases forensic evidence
|
|
509
|
+
on request is a way to lose an incident record.
|
|
510
|
+
|
|
511
|
+
**What is never captured.** Headers, at any layer — every provider authenticates
|
|
512
|
+
through headers, so that is where the tokens are. That one is a guarantee: the
|
|
513
|
+
capture layer is never handed a header list, so no provider, present or future,
|
|
514
|
+
can opt its own in.
|
|
515
|
+
|
|
516
|
+
**Masking is best-effort, and a body corpus is sensitive even after it.** Bodies
|
|
517
|
+
are masked before they are written, replacing bearer tokens, `sk-`/`ak-`/`pk-`
|
|
518
|
+
prefixed keys, the well-known vendor prefixes (`ghp_` and the rest of GitHub's,
|
|
519
|
+
`github_pat_`, `AIza`, `GOCSPX-`, `xai-`), and any long opaque token with elided
|
|
520
|
+
forms. Two things follow, and both matter before you turn capture on:
|
|
521
|
+
|
|
522
|
+
- It costs fidelity. The length rule has no idea what it is looking at, so it
|
|
523
|
+
also elides base64 image data, content hashes, and minified source. That is
|
|
524
|
+
deliberate: a corpus that leaks a live credential is the worse failure.
|
|
525
|
+
- It does not catch everything. The length rule is tuned to base64url, so a
|
|
526
|
+
standard-base64 secret or an AWS secret access key can slip through it on a
|
|
527
|
+
`+` or a `/`, and a credential shorter than forty-one characters or exactly
|
|
528
|
+
forty characters long — an Azure OpenAI key, for instance — is out of its reach
|
|
529
|
+
entirely. The prefix rules exist precisely because the length rule cannot be
|
|
530
|
+
the whole answer, and between them they are a reduction in exposure, not a
|
|
531
|
+
guarantee of none.
|
|
532
|
+
|
|
533
|
+
Treat the artifact tree as you would treat the prompts themselves: it is
|
|
534
|
+
encrypted at rest, it belongs on a volume you control, and it is not something to
|
|
535
|
+
copy into a ticket.
|
|
536
|
+
|
|
537
|
+
Nothing changes about stdout. Prompts and responses never reach the log, the
|
|
538
|
+
journal, or the Console screen; capture is a separate encrypted store.
|
|
539
|
+
|
|
540
|
+
**Where it goes.** `request_bodies/YYYY/MM/DD/<requestId>.json.enc` beside the
|
|
541
|
+
database file, encrypted with AES-256-GCM under `OMNI_ENCRYPTION_KEY`, the same
|
|
542
|
+
key as your provider credentials. Copies taken without the key yield nothing.
|
|
543
|
+
Changing the key invalidates every artifact already written.
|
|
544
|
+
|
|
545
|
+
**Bounds.** Two limits, because either alone fails:
|
|
546
|
+
|
|
547
|
+
- Bodies expire on the same **log retention** window as request rows, swept
|
|
548
|
+
hourly, file and row deleted together.
|
|
549
|
+
- A hard cap of **100,000 body rows**, oldest pruned first. The window is what
|
|
550
|
+
you reason about; the row cap is what actually bounds disk, because a week's
|
|
551
|
+
window over sustained traffic bounds nothing.
|
|
552
|
+
|
|
553
|
+
Individual payloads are bounded structurally rather than by byte offset — strings
|
|
554
|
+
past 64 KB, arrays to their last 24 items, nesting past 6 levels, objects to 80
|
|
555
|
+
keys — so a stored artifact is always valid JSON. An artifact still over 512 KB
|
|
556
|
+
after that has its bodies replaced by a marker recording why.
|
|
557
|
+
|
|
558
|
+
**Sizing.** 512 KB is a *plaintext* cap and encryption emits hex, so one artifact
|
|
559
|
+
can reach ~1 MB on disk: with the 100,000-row cap the corpus worst case is about
|
|
560
|
+
**100 GB**, not 50. Most artifacts are a few kilobytes, but that is the number to
|
|
561
|
+
size a volume against. The same cap applies per body held in memory while a
|
|
562
|
+
request is in flight, one per side per attempt — so ~512 KB × (attempts + 1) per
|
|
563
|
+
captured request. Multiply by your concurrency before enabling this on a small
|
|
564
|
+
box.
|
|
565
|
+
|
|
566
|
+
## Snapshots and restore
|
|
567
|
+
|
|
568
|
+
The console's Database screen reports what this installation occupies — the
|
|
569
|
+
database file, its write-ahead log, the captured-body tree, and the free pages a
|
|
570
|
+
compaction would give back — and takes snapshots. `omni db stats` prints the same
|
|
571
|
+
figures.
|
|
572
|
+
|
|
573
|
+
**What a snapshot is.** One self-contained SQLite file, written into a
|
|
574
|
+
`snapshots/` directory beside the database. The write-ahead log is folded in, so
|
|
575
|
+
there is nothing else to copy alongside it, and taking one is safe while the
|
|
576
|
+
gateway is running: it reads through SQLite rather than copying bytes off disk.
|
|
577
|
+
|
|
578
|
+
**What it is not.** The sibling `request_bodies/` tree is excluded, always. A
|
|
579
|
+
snapshot is never a prompt corpus, and its size tracks your configuration and
|
|
580
|
+
usage history rather than your traffic. The cost is that a restore leaves the
|
|
581
|
+
captured-body tree out of step with the table: files the restored database has no
|
|
582
|
+
row for are collected by the hourly sweep, and a row whose file is gone reads back
|
|
583
|
+
as `captured, then lost`.
|
|
584
|
+
|
|
585
|
+
**A snapshot does carry secrets.** Your provider credentials are in it, encrypted,
|
|
586
|
+
along with your gateway key hashes. They are inert only because
|
|
587
|
+
`OMNI_ENCRYPTION_KEY` is not in the file — it lives in the installation's `.env`.
|
|
588
|
+
So treat a downloaded snapshot exactly as you treat the database itself: anyone
|
|
589
|
+
holding both the file and the key holds your provider accounts. Downloads are
|
|
590
|
+
never cached, and each one is recorded in the gateway's log.
|
|
591
|
+
|
|
592
|
+
**Retention** bounds the directory: at most `keepLatest` snapshots are kept, and
|
|
593
|
+
nothing older than `maxAgeDays` — 5 and 30 by default. Both bounds have to pass,
|
|
594
|
+
so an old snapshot goes even while the count is under the limit, and the newest is
|
|
595
|
+
always kept whatever the numbers say. Pruning runs when a snapshot is taken rather
|
|
596
|
+
than on a timer, so a quiet installation keeps what it already has. **Edit the
|
|
597
|
+
policy on the Database screen**, not on Settings: it is deliberately not part of
|
|
598
|
+
the settings form, so a settings save from a client that has never heard of
|
|
599
|
+
retention leaves your policy alone instead of resetting it.
|
|
600
|
+
|
|
601
|
+
The copy taken automatically on the way into a restore is exempt from retention.
|
|
602
|
+
It is the undo.
|
|
603
|
+
|
|
604
|
+
**Restoring from the console** happens inside the running gateway. Client traffic
|
|
605
|
+
on `/v1/*` is refused with a retryable 503 while the file is replaced; `/api/*`
|
|
606
|
+
and `/health` keep answering, so you can watch the restore and hear how it ended.
|
|
607
|
+
Restoring a database that carries a different console password signs every session
|
|
608
|
+
out; restoring this installation's own snapshot does not. The screen also uploads
|
|
609
|
+
a database file from elsewhere, up to 2 GiB, which is how you move an installation
|
|
610
|
+
to another machine — bring `OMNI_ENCRYPTION_KEY` with it, or the credentials in it
|
|
611
|
+
are unreadable. Either way the file is integrity-checked before anything is
|
|
612
|
+
touched, and a copy of what was there is taken first.
|
|
613
|
+
|
|
614
|
+
**A restore ends by rebuilding the hourly usage rollup, and that step blocks.**
|
|
615
|
+
The rollup is what rate limits count their 5h and 1w windows from, and no file an
|
|
616
|
+
operator hands over says whether its counters agree with its rows — so it is
|
|
617
|
+
recomputed rather than trusted. `bun:sqlite` is synchronous, so the grouped scan
|
|
618
|
+
holds the event loop: roughly 0.4 s per 500k request-log rows, 1.6 s per 2M, and
|
|
619
|
+
about 6.5 s at 8M. `/api/*` and `/health` do not answer during it. It is the last
|
|
620
|
+
thing a restore does, after the swap has already succeeded, and a failure is
|
|
621
|
+
logged rather than raised — the database is live either way, and `omni doctor`
|
|
622
|
+
reports a rollup that disagrees with its rows.
|
|
623
|
+
|
|
624
|
+
**`omni db restore <id>` refuses while a gateway is running** against that
|
|
625
|
+
installation, and there is no override flag. A second process can open its own
|
|
626
|
+
handle but cannot quiesce the gateway's, and moving the file out from under a live
|
|
627
|
+
SQLite connection corrupts the database you were trying to rescue. Run `omni stop`
|
|
628
|
+
first, or restore from the console, which swaps the file behind its own quiesce
|
|
629
|
+
latch.
|
|
630
|
+
|
|
631
|
+
**Compaction.** `omni db vacuum`, or the console's equivalent, rewrites the
|
|
632
|
+
database and reclaims the pages deletion left free. It holds SQLite's write lock
|
|
633
|
+
for the rewrite, so a busy gateway stalls on its writes until it finishes — but
|
|
634
|
+
nothing is lost by running it live, and it reports what it actually gave back to
|
|
635
|
+
the filesystem.
|
|
636
|
+
|
|
248
637
|
## Docker
|
|
249
638
|
|
|
250
639
|
```bash
|
|
@@ -261,31 +650,52 @@ The container listens on `0.0.0.0:9000` and keeps its database at
|
|
|
261
650
|
serves the APIs and returns 404 for the console. Use the CLI or the control API
|
|
262
651
|
against it, or install the npm package if you want the console.
|
|
263
652
|
|
|
653
|
+
Give the container a restart policy — `--restart unless-stopped` — if you want a
|
|
654
|
+
restart request to bring it back. A container cannot read its own policy, so
|
|
655
|
+
without one an exit is simply the end of the installation until you start it
|
|
656
|
+
again from the host.
|
|
657
|
+
|
|
264
658
|
## Scope and limits
|
|
265
659
|
|
|
266
660
|
Worth knowing before you deploy it:
|
|
267
661
|
|
|
268
662
|
- **One machine, one operator.** No multi-tenancy, no clustering, no shared
|
|
269
|
-
state.
|
|
663
|
+
state. The `1m` window and the `concurrency` gauge are counted in the gateway
|
|
664
|
+
process and reset when it restarts; `5h` and `1w` are counted from the database
|
|
665
|
+
and survive one. Two gateways over one database would not see each other's
|
|
666
|
+
short-window counts.
|
|
270
667
|
- **Two grains of usage history.** Detailed request logs are pruned after 30
|
|
271
668
|
days by default; a daily rollup is kept for 400 days. A day is your host's
|
|
272
669
|
local midnight, fixed when the row is written.
|
|
670
|
+
- **Body capture is forensics, not an archive.** It is off unless you turn it on
|
|
671
|
+
with both keys, it expires on the request-log window, and it is capped at
|
|
672
|
+
100,000 rows. It is not a searchable prompt history and there is no CLI for it.
|
|
673
|
+
- **Snapshots are manual, and local.** Nothing takes one on a schedule and there
|
|
674
|
+
is no off-host target; retention bounds what you have taken, and a restore
|
|
675
|
+
always takes one first. Copy them somewhere else yourself if the disk failing
|
|
676
|
+
is what you are guarding against.
|
|
273
677
|
- **Quota readings come from the providers**, and their usage endpoints are
|
|
274
678
|
undocumented. An account with nothing reported is treated as unknown, never
|
|
275
679
|
as unlimited.
|
|
276
680
|
- **The gateway does not know which model accepts which request shape.** An
|
|
277
681
|
unsupported combination surfaces as the provider's own 400 rather than being
|
|
278
682
|
caught earlier.
|
|
279
|
-
- Not in scope for version 1: semantic caching, billing,
|
|
280
|
-
horizontal scaling.
|
|
683
|
+
- Not in scope for version 1: semantic caching, billing, horizontal scaling.
|
|
281
684
|
|
|
282
685
|
## Security
|
|
283
686
|
|
|
284
687
|
- Treat `OMNI_ENCRYPTION_KEY`, gateway keys, and the SQLite file as secrets.
|
|
285
688
|
Anyone with the file *and* the key has your provider credentials.
|
|
286
689
|
- Prompts and responses are never logged. Request logs hold metadata and token
|
|
287
|
-
counts only
|
|
690
|
+
counts only, and no body ever reaches stdout, the journal, or the Console
|
|
691
|
+
screen. Bodies are stored only if you opt in to
|
|
692
|
+
[body capture](#recording-bodies), which needs both an environment variable
|
|
693
|
+
and a setting, encrypts what it writes, and can be refused per key.
|
|
288
694
|
- Gateway keys are stored as hashes. A lost key is reissued, not recovered.
|
|
695
|
+
- A [snapshot](#snapshots-and-restore) carries whatever the database does —
|
|
696
|
+
encrypted provider credentials, gateway key hashes — and is inert only because
|
|
697
|
+
`OMNI_ENCRYPTION_KEY` is not in it. Captured bodies are excluded, so a snapshot
|
|
698
|
+
is never a prompt corpus.
|
|
289
699
|
- Secrets are never accepted on the command line — `omni` prompts for them or
|
|
290
700
|
reads stdin — so they stay out of your shell history and the process table.
|
|
291
701
|
- Behind a reverse proxy, set `OMNI_BASE_URL` to the public HTTPS origin so
|
|
@@ -295,9 +705,12 @@ Worth knowing before you deploy it:
|
|
|
295
705
|
|
|
296
706
|
## Development
|
|
297
707
|
|
|
298
|
-
Contributing, or running from a checkout? See
|
|
299
|
-
|
|
300
|
-
|
|
708
|
+
Contributing, or running from a checkout? See
|
|
709
|
+
[ARCHITECTURE.md](ARCHITECTURE.md) for how the system fits together,
|
|
710
|
+
[CLAUDE.md](CLAUDE.md) for the repository map, architectural boundaries, and
|
|
711
|
+
conventions, [docs/adding-a-provider.md](docs/adding-a-provider.md) for the
|
|
712
|
+
provider checklist, and `docs/superpowers/specs/` for the design documents
|
|
713
|
+
behind each feature.
|
|
301
714
|
|
|
302
715
|
```bash
|
|
303
716
|
git clone https://github.com/harismawan/omnigateway.git
|