omnigateway 0.9.0 → 0.9.2
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 +89 -608
- package/bin/omni.js +6347 -2002
- package/gateway.js +7642 -2632
- package/package.json +1 -1
- package/public/assets/{Chip-shturdCv.js → Chip-Did4Zhvj.js} +1 -1
- package/public/assets/{CopyValue-BGt9RRTL.js → CopyValue-Cjg9QXrD.js} +1 -1
- package/public/assets/Lamp-C2Q3t5Mn.js +25 -0
- package/public/assets/Rack-D0_N-xsd.js +476 -0
- package/public/assets/{RequestTable-huSAoNZg.js → RequestTable-i9paP_7e.js} +1 -1
- package/public/assets/{SummaryDeck-BPo2bMSC.js → SummaryDeck-g4yLDeXB.js} +1 -1
- package/public/assets/{Toggle-C84xs_YB.js → Toggle-D-NDlneh.js} +1 -1
- package/public/assets/{TokenBreakdown-CmzXuENj.js → TokenBreakdown-D5ZIngM6.js} +1 -1
- package/public/assets/{WindowChart-BZzVrxYk.js → WindowChart-BsonO5Py.js} +1 -1
- package/public/assets/{_app.accounts-DMmtkwnl.js → _app.accounts-DFhyE62a.js} +1 -1
- package/public/assets/{_app.console-BBz3kCPV.js → _app.console-BfHcfQMp.js} +6 -6
- package/public/assets/{_app.database-ZvtMVgsh.js → _app.database-Cgw9z3rK.js} +1 -1
- package/public/assets/{_app.index-DKouD-rz.js → _app.index-Blw3S4-3.js} +1 -1
- package/public/assets/{_app.keys-BG9-zfh6.js → _app.keys-DrdfnfIk.js} +1 -1
- package/public/assets/{_app.logs-DjinYGYS.js → _app.logs-DUOqlJzB.js} +1 -1
- package/public/assets/{_app.models-BCfQVuoT.js → _app.models-Dm83HRaM.js} +1 -1
- package/public/assets/{_app.plugins._pluginId-Dm48f3wz.js → _app.plugins._pluginId-BmJGDYxw.js} +1 -1
- package/public/assets/{_app.settings-C42ylcNP.js → _app.settings-66rm4vHJ.js} +1 -1
- package/public/assets/{_app.usage-DfPM6eQ-.js → _app.usage-BCufy5XR.js} +1 -1
- package/public/assets/{chevron-right-HI_fK6Gw.js → chevron-right-BZ4QmUDM.js} +1 -1
- package/public/assets/{client-TUf0nuim.js → client-De1_cEH5.js} +1 -1
- package/public/assets/index-PP8vsvw3.js +13 -0
- package/public/assets/{login-w-hVstKf.js → login-CkZlwcdd.js} +1 -1
- package/public/assets/plus-D5AwIXj3.js +1 -0
- package/public/assets/preload-helper-BC9bKUg1.js +1 -0
- package/public/assets/shared-C47BfV_u.js +77 -0
- package/public/assets/{trash-2-Bq4GaXzx.js → trash-2-BLc5o7DN.js} +1 -1
- package/public/index.html +4 -7
- package/public/shared/chunks/{react-dom-BTlfKTOo.js → react-dom-BFxyned6.js} +1 -1
- package/public/shared/dashboard-sdk.js +1 -1
- package/public/shared/react-dom-client.js +3 -3
- package/public/shared/react-dom.js +1 -1
- package/public/shared/react-query.js +1 -1
- package/public/shared/styled-components.js +2 -2
- package/public/assets/Lamp-CVGUT2YK.js +0 -25
- package/public/assets/Rack-BbZyyu2u.js +0 -466
- package/public/assets/_app.plugins._pluginId-B9k5eqAt.js +0 -2
- package/public/assets/index-C66Zn0tX.js +0 -13
- package/public/assets/login-DWgivEPK.js +0 -2
- package/public/assets/plus-BCf2TPqz.js +0 -1
- package/public/assets/preload-helper-Unv7FI91.js +0 -1
- package/public/assets/shared-XRnr7At9.js +0 -77
- package/public/assets/stream-3PafhwhZ.js +0 -1
package/README.md
CHANGED
|
@@ -8,16 +8,24 @@ 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.
|
|
10
10
|
|
|
11
|
-
It runs
|
|
12
|
-
logs the contents of your
|
|
11
|
+
It runs as one process on a local SQLite file by default, scales out onto
|
|
12
|
+
Postgres and Redis when you need a fleet, and never logs the contents of your
|
|
13
|
+
prompts or replies.
|
|
13
14
|
|
|
14
15
|
```bash
|
|
15
16
|
bun install -g omnigateway
|
|
16
17
|
omni start
|
|
17
18
|
```
|
|
18
19
|
|
|
19
|
-
> Status: in use and complete for its scope
|
|
20
|
-
|
|
20
|
+
> Status: in use and complete for its scope — see [Scope](#scope-and-limits).
|
|
21
|
+
|
|
22
|
+
**Further reading**, once the gateway is up:
|
|
23
|
+
|
|
24
|
+
- [docs/client-api.md](docs/client-api.md) — endpoints, authentication, rate-limit headers, and how tools decide where a request can go
|
|
25
|
+
- [docs/operations.md](docs/operations.md) — key limits, logs, recording bodies, snapshots and restore
|
|
26
|
+
- [docs/deploying.md](docs/deploying.md) — systemd, reverse proxies, cluster mode, Docker and Kubernetes
|
|
27
|
+
- [docs/plugins.md](docs/plugins.md) — installing, verifying and removing plugins
|
|
28
|
+
- [ARCHITECTURE.md](ARCHITECTURE.md) — how it is built
|
|
21
29
|
|
|
22
30
|
## What it does
|
|
23
31
|
|
|
@@ -59,85 +67,17 @@ omni start
|
|
|
59
67
|
|
|
60
68
|
## How it is built
|
|
61
69
|
|
|
62
|
-
A Bun workspace monorepo
|
|
63
|
-
package is allowed to import: a provider-neutral core at the bottom, provider
|
|
70
|
+
A Bun workspace monorepo: a provider-neutral core at the bottom, provider
|
|
64
71
|
knowledge in one place, pure routing above that, and every side effect pushed up
|
|
65
|
-
into the gateway process.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
graph TD
|
|
69
|
-
subgraph frontends[Front ends]
|
|
70
|
-
gateway["apps/gateway<br/><i>Elysia server, dispatch, loops</i>"]
|
|
71
|
-
cli["apps/cli<br/><i>the omni binary</i>"]
|
|
72
|
-
dashboard["apps/dashboard<br/><i>React console</i>"]
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
control["@omni/control<br/><i>every operator action;<br/>no HTTP, argv or terminal</i>"]
|
|
76
|
-
router["@omni/router<br/><i>pure ranking;<br/>no I/O, no timers</i>"]
|
|
77
|
-
store["@omni/store<br/><i>SQLite + field encryption</i>"]
|
|
78
|
-
providers["@omni/providers<br/><i>adapters, wire codecs,<br/>catalog, HTTP client</i>"]
|
|
79
|
-
rtk["@omni/rtk<br/><i>tool-result filters</i>"]
|
|
80
|
-
ponytail["@omni/ponytail<br/><i>vendored coding ruleset</i>"]
|
|
81
|
-
ratelimit["@omni/ratelimit<br/><i>key limit arithmetic;<br/>no clock, no state</i>"]
|
|
82
|
-
ir["@omni/ir<br/><i>domain model — depends on nothing</i>"]
|
|
83
|
-
|
|
84
|
-
gateway --> control
|
|
85
|
-
gateway --> rtk
|
|
86
|
-
gateway --> ponytail
|
|
87
|
-
gateway --> ratelimit
|
|
88
|
-
cli --> control
|
|
89
|
-
dashboard -. "types only" .-> ir
|
|
90
|
-
dashboard -. "types only" .-> store
|
|
91
|
-
control --> router
|
|
92
|
-
control --> ponytail
|
|
93
|
-
control --> ratelimit
|
|
94
|
-
router -. "types + 2 pure helpers,<br/>via /types subpath" .-> store
|
|
95
|
-
router --> providers
|
|
96
|
-
store --> rtk
|
|
97
|
-
store --> ponytail
|
|
98
|
-
store --> ratelimit
|
|
99
|
-
providers --> ir
|
|
100
|
-
rtk --> ir
|
|
101
|
-
ponytail --> ir
|
|
102
|
-
store --> ir
|
|
103
|
-
```
|
|
72
|
+
into the gateway process. The gateway and the CLI are two front ends over the
|
|
73
|
+
same `@omni/control` functions — the CLI opens the SQLite file directly, so it
|
|
74
|
+
works when the gateway is down.
|
|
104
75
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
`@omni/ratelimit` is the same shape applied to key limits: it decides whether a
|
|
111
|
-
request is over a ceiling, and it holds no clock and no counters — `now` is a
|
|
112
|
-
parameter and the counts are handed to it, so it never learns whether a number
|
|
113
|
-
came from memory or from SQLite. It imports nothing but its schema validator, not
|
|
114
|
-
even `@omni/ir`. The rings and the in-flight gauge live in the gateway, because
|
|
115
|
-
state is not the package's job, and that split is what lets sliding-window
|
|
116
|
-
arithmetic be tested without a gateway, a store, or a clock.
|
|
117
|
-
|
|
118
|
-
The dashboard's edges are dotted because they are type-level only. It has no edge
|
|
119
|
-
to `@omni/providers` at all: provider names, colours, order and model lists reach
|
|
120
|
-
the console over `GET /api/catalog`, because a provider loaded from
|
|
121
|
-
`<root>/plugins/` exists only at runtime and no build-time import could see one.
|
|
122
|
-
|
|
123
|
-
`@omni/providers/catalog` and `/descriptors` are still deliberately import-free,
|
|
124
|
-
for a different reason than they used to be: the pure router imports
|
|
125
|
-
`descriptors`, and the leaf property is what lets it.
|
|
126
|
-
|
|
127
|
-
The router's dotted edge into `@omni/store` is nearly the same story: its
|
|
128
|
-
package-root imports are all `import type`. Two pure arithmetic helpers,
|
|
129
|
-
`durationFor` and `cacheReadRate`, do run at runtime — imported through the
|
|
130
|
-
`@omni/store/types` leaf subpath, so neither SQLite nor crypto enters the
|
|
131
|
-
router's module graph and the no-I/O rule holds.
|
|
132
|
-
|
|
133
|
-
The gateway and the CLI are two front ends over the same `@omni/control`
|
|
134
|
-
functions. The CLI does not call the running server's API — it opens the same
|
|
135
|
-
SQLite file directly, which is why it still works when the gateway is down.
|
|
136
|
-
|
|
137
|
-
**[ARCHITECTURE.md](ARCHITECTURE.md)** goes a level deeper: a request traced end
|
|
138
|
-
to end, how routing ranks and excludes accounts, the commit point that decides
|
|
139
|
-
whether failover is still possible, the provider adapter shape, the database
|
|
140
|
-
schema and its encryption boundary, and the background loops.
|
|
76
|
+
**[ARCHITECTURE.md](ARCHITECTURE.md)** has the package map and layering rules,
|
|
77
|
+
then goes a level deeper: a request traced end to end, how routing ranks and
|
|
78
|
+
excludes accounts, the commit point that decides whether failover is still
|
|
79
|
+
possible, the provider adapter shape, the database schema and its encryption
|
|
80
|
+
boundary, and the background loops.
|
|
141
81
|
|
|
142
82
|
## Requirements
|
|
143
83
|
|
|
@@ -218,12 +158,9 @@ omni models catalog # what is available
|
|
|
218
158
|
omni models put fast --from-catalog anthropic:claude-sonnet-5
|
|
219
159
|
```
|
|
220
160
|
|
|
221
|
-
Two
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
routers carry no published rate, so the router treats them as unpriced rather
|
|
225
|
-
than free — set a real `costPerMTok` on the saved target to have one ranked.
|
|
226
|
-
Details in [docs/adding-a-provider.md](docs/adding-a-provider.md).
|
|
161
|
+
Two providers price in ways a flat per-target rate cannot express — xAI above
|
|
162
|
+
200K context and Kilo's `kilo-auto/*` routers; the caveats are in
|
|
163
|
+
[docs/adding-a-provider.md](docs/adding-a-provider.md).
|
|
227
164
|
|
|
228
165
|
Mint a key for your client. **It is printed once and stored only as a hash:**
|
|
229
166
|
|
|
@@ -232,45 +169,10 @@ omni keys create --label laptop
|
|
|
232
169
|
```
|
|
233
170
|
|
|
234
171
|
Bound what the key can do with `--limit <dimension>:<window>=<value>`, repeated
|
|
235
|
-
once per pair
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
Every window is *sliding*, so a key cannot spend two windows' allowance either
|
|
242
|
-
side of a clock edge. `requests` and `tokens` take `1m`, `5h`, and `1w`; `spend`
|
|
243
|
-
takes `5h` and `1w`; `concurrency` is not a window at all but a ceiling on
|
|
244
|
-
requests in flight at once.
|
|
245
|
-
|
|
246
|
-
`tokens` and `spend` are debited once a response completes, because an exact
|
|
247
|
-
count exists only then — a key at its ceiling is refused on its *next* request.
|
|
248
|
-
The `5h` and `1w` counts derive from `request_logs`, so a `1w` limit on an
|
|
249
|
-
installation that prunes logs after three days really enforces three days. They
|
|
250
|
-
are cached for thirty seconds and so can read slightly high, never low: refused
|
|
251
|
-
early rather than let past a ceiling you set. If the database cannot answer they
|
|
252
|
-
stop enforcing and the gateway logs it, while `1m` and `concurrency` are held in
|
|
253
|
-
memory and go on enforcing exactly.
|
|
254
|
-
|
|
255
|
-
> **Breaking:** `--rate-limit N` is removed, not aliased. Use
|
|
256
|
-
> `--limit requests:1m=N`. A script still passing the old flag stops with an
|
|
257
|
-
> unknown-flag error rather than quietly taking a deprecated path.
|
|
258
|
-
|
|
259
|
-
Limits are editable after the key exists, unlike `--no-bodies` below. `omni keys
|
|
260
|
-
list` prints a compact summary; the full matrix and what has gone against it
|
|
261
|
-
need one key's id:
|
|
262
|
-
|
|
263
|
-
```bash
|
|
264
|
-
omni keys limits <id>
|
|
265
|
-
omni keys limits <id> --set tokens:1w=50000000
|
|
266
|
-
omni keys limits <id> --unset spend:5h
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
`--unset` names a pair that is actually set, so a typo fails rather than
|
|
270
|
-
reporting a change it did not make. The usage shown counts completed requests
|
|
271
|
-
only, so it reads at or below what the gateway is enforcing, and `concurrency`
|
|
272
|
-
shows no figure — that gauge lives in the process, not the database. The
|
|
273
|
-
console's Keys screen shows and edits the same matrix.
|
|
172
|
+
once per pair — `omni keys create --label ci --limit requests:1m=60`. An unset
|
|
173
|
+
pair is unlimited. Windows slide, `tokens` and `spend` are debited on
|
|
174
|
+
completion, and limits are editable afterwards with `omni keys limits`; the
|
|
175
|
+
semantics are in [docs/operations.md](docs/operations.md#key-limits).
|
|
274
176
|
|
|
275
177
|
Now use it:
|
|
276
178
|
|
|
@@ -303,56 +205,13 @@ x-api-key: <gateway-key>
|
|
|
303
205
|
```
|
|
304
206
|
|
|
305
207
|
Ask for one of your virtual models by name. A bare provider model
|
|
306
|
-
(`claude-sonnet-5`, `gpt-5`) also works if an account can serve it.
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
carries the OpenAI keys (`object`, `created`, `owned_by`) and the Anthropic ones
|
|
310
|
-
(`type`, `display_name`, `created_at`, `max_input_tokens`, `max_tokens`) at
|
|
311
|
-
once.
|
|
312
|
-
|
|
313
|
-
Most tools that accept a custom base URL work unchanged: set it to
|
|
314
|
-
`http://127.0.0.1:9000` and use a gateway key where the provider key goes.
|
|
315
|
-
|
|
316
|
-
### Rate-limit headers
|
|
317
|
-
|
|
318
|
-
Every response carries the limit headers of the surface you asked on, so an SDK
|
|
319
|
-
backs off using the code it already ships — the Anthropic dialect on
|
|
320
|
-
`/v1/messages`, the OpenAI one on `/v1/chat/completions` and `/v1/responses`:
|
|
321
|
-
|
|
322
|
-
```http
|
|
323
|
-
anthropic-ratelimit-requests-limit: 2000 x-ratelimit-limit-requests: 2000
|
|
324
|
-
anthropic-ratelimit-requests-remaining: 1841 x-ratelimit-remaining-requests: 1841
|
|
325
|
-
anthropic-ratelimit-requests-reset: 2026-08-19T14:32:07Z x-ratelimit-reset-requests: 4h51m22s
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
`requests-remaining` counts the request being answered; `tokens-remaining` does
|
|
329
|
-
not and cannot — the response is still being written when the header goes out.
|
|
330
|
-
Where a key has several windows on one dimension, the headers report the one
|
|
331
|
-
**nearest exhaustion**, not the reassuring ones. `spend` and `concurrency`
|
|
332
|
-
render on neither dialect, because no vendor defines a header for them. A
|
|
333
|
-
refusal is `429` with `Retry-After` in seconds, computed from the oldest request
|
|
334
|
-
still inside the window that refused you.
|
|
208
|
+
(`claude-sonnet-5`, `gpt-5`) also works if an account can serve it. Most tools
|
|
209
|
+
that accept a custom base URL work unchanged: set it to `http://127.0.0.1:9000`
|
|
210
|
+
and use a gateway key where the provider key goes.
|
|
335
211
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
request can go.
|
|
340
|
-
|
|
341
|
-
A **custom tool** — a name, a description, and a JSON Schema — is portable. It
|
|
342
|
-
translates to every provider, so a request using one routes across your whole
|
|
343
|
-
pool as usual.
|
|
344
|
-
|
|
345
|
-
An **Anthropic-defined tool** — web search, web fetch, code execution, Bash,
|
|
346
|
-
text editor, computer use, memory, tool search, advisor, or an MCP toolset —
|
|
347
|
-
has a schema Anthropic owns. No other provider can express it, so any request
|
|
348
|
-
declaring one, *or replaying the blocks a previous one produced*, is routed to
|
|
349
|
-
an Anthropic account only. If the virtual model you asked for has no Anthropic
|
|
350
|
-
target, the request fails at routing with the unsupported requirement named,
|
|
351
|
-
rather than quietly losing the tool.
|
|
352
|
-
|
|
353
|
-
The gateway forwards the tool version you send and never upgrades it. Betas
|
|
354
|
-
stay yours: send `anthropic-beta` yourself, as the tool requires — the gateway
|
|
355
|
-
carries the header through but does not add one on your behalf.
|
|
212
|
+
Rate-limit headers, the `/v1/models` listing shape, and why an Anthropic-defined
|
|
213
|
+
tool pins a request to an Anthropic account are in
|
|
214
|
+
[docs/client-api.md](docs/client-api.md).
|
|
356
215
|
|
|
357
216
|
## The CLI
|
|
358
217
|
|
|
@@ -398,78 +257,13 @@ each candidate's score, and every account that was excluded with the reason.
|
|
|
398
257
|
`omni status` is the "is anything wrong" command: process state, per-account
|
|
399
258
|
health, and how much provider quota each account has left.
|
|
400
259
|
|
|
401
|
-
## Running it
|
|
260
|
+
## Running it somewhere other than your laptop
|
|
402
261
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
omni console # reads the journal (or the log file, without systemd)
|
|
409
|
-
```
|
|
410
|
-
|
|
411
|
-
Use `--system` for a system-wide unit (needs root). Without systemd, `omni
|
|
412
|
-
start` supervises the process itself with a pidfile under
|
|
413
|
-
`~/.local/state/omnigateway`. Either way, `omni start` returns only once
|
|
414
|
-
`/health` actually answers.
|
|
415
|
-
|
|
416
|
-
### Restarting and stopping from the console
|
|
417
|
-
|
|
418
|
-
The console can restart and shut down the gateway from the foot of its sidebar.
|
|
419
|
-
Restart works under systemd — the gateway asks the manager rather than
|
|
420
|
-
signalling itself, because a handled SIGTERM exits cleanly, which systemd's
|
|
421
|
-
`Restart=on-failure` reads as success — reports uncertainty in a container,
|
|
422
|
-
whose restart policy cannot be read from inside, and disables itself with no
|
|
423
|
-
supervisor; use `omni restart` from a terminal there. Shutdown is offered in
|
|
424
|
-
every shape. In a container it is a one-way door: bring the process back from
|
|
425
|
-
the host.
|
|
426
|
-
|
|
427
|
-
### Behind a reverse proxy
|
|
428
|
-
|
|
429
|
-
Set `OMNI_BASE_URL` to the public HTTPS origin, or OAuth callbacks come back to
|
|
430
|
-
the wrong host.
|
|
431
|
-
|
|
432
|
-
Beyond that, two things travel badly through a proxy, and both are streams.
|
|
433
|
-
Client responses on `/v1/*` are server-sent events, and the console keeps one
|
|
434
|
-
WebSocket open on `/api/stream`. Neither is optional: buffer the first and every
|
|
435
|
-
token of an agent's reply arrives at once at the end, and drop the second and
|
|
436
|
-
the console silently falls back to polling.
|
|
437
|
-
|
|
438
|
-
Caddy and Cloudflare pass WebSockets and unbuffered responses by default and
|
|
439
|
-
need nothing. nginx needs telling:
|
|
440
|
-
|
|
441
|
-
```nginx
|
|
442
|
-
location / {
|
|
443
|
-
proxy_pass http://127.0.0.1:9000;
|
|
444
|
-
proxy_http_version 1.1;
|
|
445
|
-
|
|
446
|
-
# Without these two the Upgrade handshake never reaches the gateway and the
|
|
447
|
-
# console shows LIVE·POLL instead of LIVE·PUSH. It keeps working — the
|
|
448
|
-
# fallback exists for exactly this — but you paid for a socket you are not
|
|
449
|
-
# getting.
|
|
450
|
-
proxy_set_header Upgrade $http_upgrade;
|
|
451
|
-
proxy_set_header Connection "upgrade";
|
|
452
|
-
|
|
453
|
-
proxy_set_header Host $host;
|
|
454
|
-
proxy_set_header X-Forwarded-Proto $scheme;
|
|
455
|
-
|
|
456
|
-
# The socket's heartbeat is 20s and it gives up on a missed pong at 60s.
|
|
457
|
-
# A read timeout below that closes a healthy connection from the outside,
|
|
458
|
-
# and the console reconnects in a loop that looks like an unstable gateway.
|
|
459
|
-
proxy_read_timeout 300s;
|
|
460
|
-
|
|
461
|
-
# SSE must not be buffered. The gateway already sends
|
|
462
|
-
# `x-accel-buffering: no` on streaming responses, which nginx honours on its
|
|
463
|
-
# own, so this line is belt and braces for a proxy chain where something
|
|
464
|
-
# else strips that header before nginx sees it.
|
|
465
|
-
proxy_buffering off;
|
|
466
|
-
}
|
|
467
|
-
```
|
|
468
|
-
|
|
469
|
-
The gateway sends downstream `: keepalive` comments on streaming responses
|
|
470
|
-
because provider heartbeats are decoded away, so an idle stream still looks
|
|
471
|
-
alive to whatever sits in between. Keep any idle timeout in the proxy above
|
|
472
|
-
your longest expected request.
|
|
262
|
+
`omni service install --enable` writes a systemd unit; behind a reverse proxy,
|
|
263
|
+
set `OMNI_BASE_URL` and pass WebSocket upgrades and unbuffered SSE through.
|
|
264
|
+
Several replicas behind a load balancer are **cluster mode** on Postgres and
|
|
265
|
+
Redis. All of it, with the nginx block and the Kubernetes base, is in
|
|
266
|
+
[docs/deploying.md](docs/deploying.md).
|
|
473
267
|
|
|
474
268
|
## Configuration
|
|
475
269
|
|
|
@@ -485,10 +279,15 @@ Configuration is environment variables, read from the installation's `.env`:
|
|
|
485
279
|
| `OMNI_STATIC_DIR` | No | the console shipped with the server | Serve a different console build |
|
|
486
280
|
| `OMNI_LOG_LEVEL` | No | `info` | Stdout threshold: `debug`, `info`, `warn`, or `error` |
|
|
487
281
|
| `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 |
|
|
488
|
-
| `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) |
|
|
282
|
+
| `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](docs/operations.md#recording-bodies) |
|
|
283
|
+
| `OMNI_METRICS_TOKEN` | No | unset | Registers authenticated `GET /metrics`; unset means no route |
|
|
284
|
+
| `OMNI_METRICS_MAX_SERIES` | No | `5000` | Series cap before new `api_key_id` values fold to `other` |
|
|
285
|
+
| `OMNI_OTLP_ENDPOINT` | No | unset | Enables OTLP/HTTP span export to `<endpoint>/v1/traces` |
|
|
286
|
+
| `OMNI_OTLP_HEADERS` | No | unset | Comma-separated `k=v` headers sent to the collector |
|
|
287
|
+
| `OMNI_TRACE_SAMPLE` | No | `1.0` | Head sampling ratio from `0` to `1`; inbound sampled trace context is honoured |
|
|
489
288
|
| `OMNI_ROOT` | No | the installation in the current directory, else `~/.config/omnigateway` | Which installation the CLI acts on, when `--root` is not passed |
|
|
490
289
|
| `OMNI_PLUGIN_REGISTRY` | No | the public npm registry | Registry `omni plugin install <name>` resolves through; must be `https://` |
|
|
491
|
-
| `OMNI_CLUSTER_MODE` | No | unset | `true` selects [cluster mode](#running-more-than-one-gateway) and requires the two URLs below; unset is one process on SQLite, and then the URLs must be unset too |
|
|
290
|
+
| `OMNI_CLUSTER_MODE` | No | unset | `true` selects [cluster mode](docs/deploying.md#running-more-than-one-gateway) and requires the two URLs below; unset is one process on SQLite, and then the URLs must be unset too |
|
|
492
291
|
| `OMNI_DATABASE_URL` | In cluster mode | — | The shared Postgres store |
|
|
493
292
|
| `OMNI_REDIS_URL` | In cluster mode | — | The coordinator every process of a cluster shares: rate-limit counters, sessions, leases, push fan-out |
|
|
494
293
|
|
|
@@ -503,28 +302,9 @@ version pins — are deliberately left out of this table and documented in `.env
|
|
|
503
302
|
change how the gateway identifies itself to a provider, which is not configuration in the sense
|
|
504
303
|
the rest of this table is.
|
|
505
304
|
|
|
506
|
-
Gateway events are written to stdout as one greppable line each
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
`OMNI_LOG_FILE` *names* where output was captured; it does not redirect it. Setting it alone
|
|
510
|
-
leaves the log empty, because the gateway still writes to stdout. Redirect the output and name
|
|
511
|
-
the same path:
|
|
512
|
-
|
|
513
|
-
```bash
|
|
514
|
-
bun apps/gateway/src/index.ts >> /var/log/omni.log 2>&1
|
|
515
|
-
```
|
|
516
|
-
|
|
517
|
-
`omni start` does both for the gateway it supervises, and under systemd the journal needs no
|
|
518
|
-
setup.
|
|
519
|
-
|
|
520
|
-
In a fleet, capture is per process. Each replica can capture its own — tee its stdout to a file
|
|
521
|
-
inside the container and point `OMNI_LOG_FILE` at the same path — and the Console screen then
|
|
522
|
-
merges every process's tail and lets you pick one. That is worth having for an incident on a
|
|
523
|
-
running pod, and it is not a log stack: the file dies with the container, nothing rotates it, and
|
|
524
|
-
the screen reads one process at a time. Ship stdout to a collector for anything beyond that —
|
|
525
|
-
Elasticsearch and Kibana, Loki and Grafana, or whatever already reads your containers — where the
|
|
526
|
-
lines outlive the process that wrote them and can be searched across all of them at once. The
|
|
527
|
-
Console screen says so itself when it finds a fleet capturing nothing.
|
|
305
|
+
Gateway events are written to stdout as one greppable line each. `OMNI_LOG_FILE` *names* where
|
|
306
|
+
that output was captured; it does not redirect it — see
|
|
307
|
+
[docs/operations.md](docs/operations.md#logs).
|
|
528
308
|
|
|
529
309
|
Everything else lives in the database rather than the environment, so it can be
|
|
530
310
|
changed without a restart: the six routing weights, `maxAttempts`,
|
|
@@ -536,192 +316,16 @@ console. `quotaPollIntervalMs` is the one exception: the poller reads it once at
|
|
|
536
316
|
boot, so a change to it takes a restart. Snapshot retention —
|
|
537
317
|
`snapshotKeepLatest` and `snapshotMaxAgeDays` — is stored alongside them but
|
|
538
318
|
deliberately edited on the Database screen instead; see
|
|
539
|
-
[Snapshots and restore](#snapshots-and-restore) for why.
|
|
540
|
-
|
|
541
|
-
## Running more than one gateway
|
|
542
|
-
|
|
543
|
-
One process on SQLite is the default and is what every command in this document assumes. A
|
|
544
|
-
fleet — several replicas behind a load balancer, on Kubernetes or otherwise — is **cluster
|
|
545
|
-
mode**, switched on by `OMNI_CLUSTER_MODE=true` and needing two things beside the gateway:
|
|
546
|
-
|
|
547
|
-
- **Postgres** as the store, named by `OMNI_DATABASE_URL`. Every replica reads and writes one
|
|
548
|
-
database; there is no SQLite file, no snapshot, no restore, and no `omni db vacuum` — those are
|
|
549
|
-
`pg_dump`'s job now, and the Database screen says so.
|
|
550
|
-
- **Redis** (or Valkey) as the coordinator, named by `OMNI_REDIS_URL`. It holds what a fleet must
|
|
551
|
-
agree on and a database is the wrong shape for: the per-minute request ring and the concurrency
|
|
552
|
-
gauge, the long-window counters, admin sessions, pending OAuth flows, quota-probe cooldowns, the
|
|
553
|
-
leases that make the background loops run once rather than N times, and the fan-out that lets a
|
|
554
|
-
console on one replica hear a change made on another.
|
|
555
|
-
|
|
556
|
-
```bash
|
|
557
|
-
OMNI_CLUSTER_MODE=true
|
|
558
|
-
OMNI_DATABASE_URL=postgres://omni:secret@db.internal:5432/omni
|
|
559
|
-
OMNI_REDIS_URL=redis://cache.internal:6379
|
|
560
|
-
OMNI_ENCRYPTION_KEY=… # the same on every replica
|
|
561
|
-
```
|
|
562
|
-
|
|
563
|
-
Boot refuses the switch without both URLs, and refuses either URL without the switch: a
|
|
564
|
-
replica that believes it is clustered and is not is the failure this variable exists to make
|
|
565
|
-
loud.
|
|
566
|
-
|
|
567
|
-
What holds across the fleet, exactly: every API-key limit at every window and dimension; token
|
|
568
|
-
refresh, which one replica performs while the others wait and reuse the result; a cookie issued
|
|
569
|
-
by one replica, which every other verifies and a password change ends everywhere. What is
|
|
570
|
-
per-replica and says so: the console's **Console** screen shows one process's stdout, so it grows
|
|
571
|
-
a selector when there is more than one, and its default view is every process merged by time.
|
|
572
|
-
What is per-replica and does not say so: the routing `load` weight is one round trip stale
|
|
573
|
-
between replicas, so a burst arriving at once on two of them can stack for that long.
|
|
574
|
-
|
|
575
|
-
**No sticky sessions are needed.** The WebSocket the console holds may land on any replica; the
|
|
576
|
-
ingress only needs to pass upgrades and hold an idle timeout above ten seconds. A rolling deploy
|
|
577
|
-
closes each replica's sockets with `1001`, the console reconnects to a live one and refetches
|
|
578
|
-
once.
|
|
579
|
-
|
|
580
|
-
**When Redis is unreachable**, the request path keeps serving: each replica falls back to its
|
|
581
|
-
own in-memory counters, so limits degrade to N-fold until Redis returns, and one line per thirty
|
|
582
|
-
seconds says so (`coord=redis coordFallback=true`). The console does not: a session that cannot
|
|
583
|
-
be checked against the shared store is refused with `503`, because a session verified locally is
|
|
584
|
-
one a password change on another replica cannot end. `GET /health` reports `mode`, `nodeId` and
|
|
585
|
-
`coord` (`ok` or `fallback`) for a readiness probe to read.
|
|
586
|
-
|
|
587
|
-
Plugins are loaded from each replica's own `<root>/plugins/`; bake them into the image so every
|
|
588
|
-
replica holds the same set. Plugin storage is Postgres in cluster mode, so a plugin's SQL is
|
|
589
|
-
written for it. `POST /api/restart` refuses in cluster mode — roll the deployment instead.
|
|
590
|
-
|
|
591
|
-
Moving an existing SQLite installation onto Postgres:
|
|
592
|
-
|
|
593
|
-
```bash
|
|
594
|
-
omni stop
|
|
595
|
-
omni db migrate --to postgres://omni:secret@db.internal:5432/omni
|
|
596
|
-
```
|
|
597
|
-
|
|
598
|
-
It copies credentials (re-encrypted with the same `OMNI_ENCRYPTION_KEY`), API keys, virtual
|
|
599
|
-
models, settings, both passwords and every completed request log into an **empty** Postgres
|
|
600
|
-
database, rebuilds the rollups, and prints what it did not carry: request bodies, `usage_daily`
|
|
601
|
-
older than the retained logs, quota readings and breaker state (re-measured within a poll
|
|
602
|
-
interval), sessions, and `plugin_*` tables, whose SQL is the source dialect's. It refuses while a
|
|
603
|
-
gateway is running and refuses a target that holds anything.
|
|
604
|
-
|
|
605
|
-
## Recording bodies
|
|
606
|
-
|
|
607
|
-
By default the gateway records no prompts and no responses. For incident
|
|
608
|
-
forensics, capture is opt-in and needs **two independent keys, both required**:
|
|
609
|
-
`OMNI_BODY_LOGGING_ALLOWED=1` read at boot, plus the **Capture request and
|
|
610
|
-
response bodies** setting (console Settings, or `omni settings set
|
|
611
|
-
bodyLoggingEnabled true`). An admin session alone cannot start recording your
|
|
612
|
-
users' prompts; with the variable unset the console says the switch does
|
|
613
|
-
nothing rather than letting you flip it. Capture can be toggled mid-incident;
|
|
614
|
-
turning it off stops new capture and does not delete what was written.
|
|
615
|
-
|
|
616
|
-
A gateway key created with `--no-bodies` is never captured whatever the setting
|
|
617
|
-
says — made at issue time, not reversible afterwards; reissue instead. Raw SSE
|
|
618
|
-
frames are captured separately, under `bodyLoggingCaptureStreamChunks`, and are
|
|
619
|
-
far the larger store.
|
|
620
|
-
|
|
621
|
-
What is captured: what arrived at `/v1/*` and what was returned, plus every
|
|
622
|
-
provider attempt in dispatch order — the client side pre-RTK, attempts post-RTK,
|
|
623
|
-
labelled as such in console and CLI. Headers are never captured, at any layer.
|
|
624
|
-
Read them from the console's Logs screen or:
|
|
319
|
+
[Snapshots and restore](docs/operations.md#snapshots-and-restore) for why.
|
|
625
320
|
|
|
626
|
-
|
|
627
|
-
omni bodies req_550e8400-… # the frame: state, size, one line per attempt
|
|
628
|
-
omni bodies req_550e8400-… --full # the payloads themselves
|
|
629
|
-
omni bodies req_550e8400-… --json # the artifact, for a script
|
|
630
|
-
```
|
|
321
|
+
## Recording bodies, snapshots, and the database
|
|
631
322
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
Artifacts live at `request_bodies/YYYY/MM/DD/<requestId>.json.enc` beside the
|
|
639
|
-
database, AES-256-GCM under `OMNI_ENCRYPTION_KEY`; changing that key invalidates
|
|
640
|
-
every artifact. Bounds: log-retention expiry plus a hard **100,000-row cap**, so
|
|
641
|
-
capture is forensics, not an archive — size a volume against roughly 100 GB
|
|
642
|
-
worst case, though most artifacts are kilobytes.
|
|
643
|
-
|
|
644
|
-
Masking is best-effort — bearer tokens, vendor-prefixed keys, long opaque
|
|
645
|
-
tokens are elided before write — a reduction in exposure, not a guarantee, and
|
|
646
|
-
it costs fidelity. Treat the tree as you would the prompts themselves: encrypted
|
|
647
|
-
at rest, on a volume you control, never pasted into a ticket.
|
|
648
|
-
[ARCHITECTURE.md](ARCHITECTURE.md#body-capture-forensics) documents the storage
|
|
649
|
-
format, structural bounds, and masking rules.
|
|
650
|
-
|
|
651
|
-
## Snapshots and restore
|
|
652
|
-
|
|
653
|
-
The console's Database screen reports what this installation occupies — the
|
|
654
|
-
database file, its write-ahead log, the captured-body tree, the free pages a
|
|
655
|
-
compaction would give back, and every table by size — and takes snapshots.
|
|
656
|
-
`omni db stats` prints the same figures. On Postgres both show the server's own
|
|
657
|
-
size, the `request_bodies` table and the per-table listing instead; there is no
|
|
658
|
-
file, so nothing here compacts, snapshots or restores it — `pg_dump` is the
|
|
659
|
-
backup.
|
|
660
|
-
|
|
661
|
-
**What a snapshot is.** One self-contained SQLite file, written into a
|
|
662
|
-
`snapshots/` directory beside the database. The write-ahead log is folded in, so
|
|
663
|
-
there is nothing else to copy alongside it, and taking one is safe while the
|
|
664
|
-
gateway is running: it reads through SQLite rather than copying bytes off disk.
|
|
665
|
-
|
|
666
|
-
**What it is not.** The sibling `request_bodies/` tree is excluded, always. A
|
|
667
|
-
snapshot is never a prompt corpus, and its size tracks your configuration and
|
|
668
|
-
usage history rather than your traffic. The cost is that a restore leaves the
|
|
669
|
-
captured-body tree out of step with the table: files the restored database has no
|
|
670
|
-
row for are collected by the hourly sweep, and a row whose file is gone reads back
|
|
671
|
-
as `captured, then lost`.
|
|
672
|
-
|
|
673
|
-
**A snapshot does carry secrets** — encrypted provider credentials and gateway
|
|
674
|
-
key hashes — inert only because `OMNI_ENCRYPTION_KEY` is not in the file.
|
|
675
|
-
Anyone holding both the file and the key holds your provider accounts; treat a
|
|
676
|
-
downloaded snapshot as the database itself.
|
|
677
|
-
|
|
678
|
-
**Retention** bounds the directory: at most `keepLatest` snapshots are kept, and
|
|
679
|
-
nothing older than `maxAgeDays` — 5 and 30 by default. Both bounds have to pass,
|
|
680
|
-
so an old snapshot goes even while the count is under the limit, and the newest is
|
|
681
|
-
always kept whatever the numbers say. Pruning runs when a snapshot is taken rather
|
|
682
|
-
than on a timer, so a quiet installation keeps what it already has. **Edit the
|
|
683
|
-
policy on the Database screen**, not on Settings: it is deliberately not part of
|
|
684
|
-
the settings form, so a settings save from a client that has never heard of
|
|
685
|
-
retention leaves your policy alone instead of resetting it.
|
|
686
|
-
|
|
687
|
-
The copy taken automatically on the way into a restore is exempt from retention.
|
|
688
|
-
It is the undo.
|
|
689
|
-
|
|
690
|
-
**Restoring from the console** happens inside the running gateway. Client traffic
|
|
691
|
-
on `/v1/*` is refused with a retryable 503 while the file is replaced; `/api/*`
|
|
692
|
-
and `/health` keep answering. The screen also uploads a database file from
|
|
693
|
-
elsewhere, up to 2 GiB — bring `OMNI_ENCRYPTION_KEY` with it, or the credentials
|
|
694
|
-
in it are unreadable. The file is integrity-checked before anything is touched,
|
|
695
|
-
and a copy of what was there is taken first. A restore ends by rebuilding the
|
|
696
|
-
usage rollup, which briefly blocks even `/api/*`: roughly 0.4 s per 500k
|
|
697
|
-
request-log rows, 1.6 s at 2M, 6.5 s at 8M. A failure is logged rather than
|
|
698
|
-
raised — the database is live either way, and `omni doctor` reports a rollup
|
|
699
|
-
that disagrees with its rows.
|
|
700
|
-
|
|
701
|
-
Before it asks, `restore` prints one row per table with what the snapshot holds
|
|
702
|
-
against what is live, so the confirmation is informed rather than a judgement on
|
|
703
|
-
an id and an mtime. `--dry-run` prints the same table and exits without asking.
|
|
704
|
-
The counts cover the tables the integrity check reads, so they are a floor on
|
|
705
|
-
what a restore replaces rather than the whole of it.
|
|
706
|
-
|
|
707
|
-
**`omni db restore <id>` refuses while a gateway is running** against that
|
|
708
|
-
installation, and there is no override flag. A second process can open its own
|
|
709
|
-
handle but cannot quiesce the gateway's, and moving the file out from under a live
|
|
710
|
-
SQLite connection corrupts the database you were trying to rescue. Run `omni stop`
|
|
711
|
-
first, or restore from the console, which swaps the file behind its own quiesce
|
|
712
|
-
latch.
|
|
713
|
-
|
|
714
|
-
**Compaction.** `omni db vacuum`, or the console's equivalent, rewrites the
|
|
715
|
-
database and reclaims the pages deletion left free. It holds SQLite's write lock
|
|
716
|
-
for the rewrite, so a busy gateway stalls on its writes until it finishes — but
|
|
717
|
-
nothing is lost by running it live, and it reports what it actually gave back to
|
|
718
|
-
the filesystem.
|
|
719
|
-
|
|
720
|
-
**Clearing bodies.** `omni db clear-bodies`, or the Database screen's *Clear
|
|
721
|
-
bodies*, deletes every captured prompt and completion on either engine — the
|
|
722
|
-
files beside a SQLite database, the `request_bodies` rows on Postgres. The
|
|
723
|
-
requests stay in the log with their bodies marked pruned. There is no undo:
|
|
724
|
-
bodies are never in a snapshot.
|
|
323
|
+
By default the gateway records no prompts and no responses; capture is opt-in
|
|
324
|
+
and needs both `OMNI_BODY_LOGGING_ALLOWED=1` at boot and a runtime setting, and
|
|
325
|
+
a key created with `--no-bodies` is never captured. Snapshots are one SQLite
|
|
326
|
+
file each, taken live, never carrying captured bodies, and restorable without
|
|
327
|
+
stopping the gateway. Both, with compaction and clearing bodies, are in
|
|
328
|
+
[docs/operations.md](docs/operations.md).
|
|
725
329
|
|
|
726
330
|
## Docker
|
|
727
331
|
|
|
@@ -735,62 +339,45 @@ docker run --rm \
|
|
|
735
339
|
```
|
|
736
340
|
|
|
737
341
|
The container listens on `0.0.0.0:9000`, serves the console, and keeps its
|
|
738
|
-
database and plugins under `/data`.
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
For a fleet, set `OMNI_CLUSTER_MODE=true` with `OMNI_DATABASE_URL` and `OMNI_REDIS_URL` and
|
|
742
|
-
drop the volume;
|
|
743
|
-
see [Running more than one gateway](#running-more-than-one-gateway). A
|
|
744
|
-
Kubernetes deployment — Deployment, Service, Ingress with the timeouts streaming
|
|
745
|
-
needs, HPA, and an example Secret — is under `k8s/` as a kustomize
|
|
746
|
-
base:
|
|
747
|
-
|
|
748
|
-
```bash
|
|
749
|
-
cp k8s/secret.example.yaml k8s/secret.yaml # edit it
|
|
750
|
-
kubectl apply -f k8s/secret.yaml
|
|
751
|
-
kubectl apply -k k8s
|
|
752
|
-
```
|
|
753
|
-
|
|
754
|
-
Releases deploy by GitOps: a `v*` tag publishes `ghcr.io/harismawan/omnigateway:<version>`
|
|
755
|
-
and the workflow commits that version into `k8s/kustomization.yaml` on `main`, which Argo
|
|
756
|
-
CD syncs. Rolling back is editing `newTag` by hand.
|
|
757
|
-
|
|
758
|
-
Plugins in a fleet are baked into the image so every replica holds the same
|
|
759
|
-
set: `COPY plugins/ /data/plugins/` in a derived Dockerfile. The image is
|
|
760
|
-
public, so no pull secret is configured; a private fork adds
|
|
761
|
-
`imagePullSecrets` to the Deployment.
|
|
762
|
-
|
|
763
|
-
Give the container a restart policy — `--restart unless-stopped` — if you want a
|
|
764
|
-
restart request to bring it back. A container cannot read its own policy, so
|
|
765
|
-
without one an exit is simply the end of the installation until you start it
|
|
766
|
-
again from the host.
|
|
342
|
+
database and plugins under `/data`. Fleets, the Kubernetes base under `k8s/`,
|
|
343
|
+
GitOps releases and restart policy are in
|
|
344
|
+
[docs/deploying.md](docs/deploying.md#docker).
|
|
767
345
|
|
|
768
346
|
## Scope and limits
|
|
769
347
|
|
|
770
348
|
Worth knowing before you deploy it:
|
|
771
349
|
|
|
772
|
-
- **One
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
350
|
+
- **One process on SQLite by default; a fleet is opt-in.** Cluster mode on
|
|
351
|
+
Postgres and Redis shares every limit, session and lease across replicas.
|
|
352
|
+
Without it, the `1m` window and the `concurrency` gauge are counted in the
|
|
353
|
+
gateway process and reset when it restarts; `5h` and `1w` are counted from
|
|
354
|
+
the database and survive one. Two gateways over one SQLite file are not a
|
|
355
|
+
cluster and would not see each other's short-window counts.
|
|
356
|
+
- **One operator, with two narrower views.** An optional read-only console
|
|
357
|
+
password and a per-key client dashboard exist; there is no multi-tenancy —
|
|
358
|
+
every provider account is the operator's, whoever is looking.
|
|
777
359
|
- **Two grains of usage history.** Detailed request logs are pruned after 30
|
|
778
360
|
days by default; a daily rollup is kept for 400 days. A day is your host's
|
|
779
361
|
local midnight, fixed when the row is written.
|
|
780
362
|
- **Body capture is forensics, not an archive.** It is off unless you turn it on
|
|
781
363
|
with both keys, it expires on the request-log window, and it is capped at
|
|
782
|
-
100,000 rows.
|
|
364
|
+
100,000 rows. `omni bodies` reads one request's capture; nothing searches
|
|
365
|
+
across them.
|
|
783
366
|
- **Snapshots are manual, and local.** Nothing takes one on a schedule and there
|
|
784
367
|
is no off-host target; retention bounds what you have taken, and a restore
|
|
785
368
|
always takes one first. Copy them somewhere else yourself if the disk failing
|
|
786
|
-
is what you are guarding against.
|
|
369
|
+
is what you are guarding against. On Postgres there are none; `pg_dump` is
|
|
370
|
+
the backup.
|
|
787
371
|
- **Quota readings come from the providers**, and their usage endpoints are
|
|
788
372
|
undocumented. An account with nothing reported is treated as unknown, never
|
|
789
373
|
as unlimited.
|
|
790
374
|
- **The gateway does not know which model accepts which request shape.** An
|
|
791
375
|
unsupported combination surfaces as the provider's own 400 rather than being
|
|
792
376
|
caught earlier.
|
|
793
|
-
- Not in scope
|
|
377
|
+
- Not in scope: semantic caching, billing.
|
|
378
|
+
|
|
379
|
+
What is missing on purpose, what is missing for now, and what is designed but not
|
|
380
|
+
built: [docs/roadmap.md](docs/roadmap.md).
|
|
794
381
|
|
|
795
382
|
## Security
|
|
796
383
|
|
|
@@ -799,10 +386,10 @@ Worth knowing before you deploy it:
|
|
|
799
386
|
- Prompts and responses are never logged. Request logs hold metadata and token
|
|
800
387
|
counts only, and no body ever reaches stdout, the journal, or the Console
|
|
801
388
|
screen. Bodies are stored only if you opt in to
|
|
802
|
-
[body capture](#recording-bodies), which needs both an environment variable
|
|
389
|
+
[body capture](docs/operations.md#recording-bodies), which needs both an environment variable
|
|
803
390
|
and a setting, encrypts what it writes, and can be refused per key.
|
|
804
391
|
- Gateway keys are stored as hashes. A lost key is reissued, not recovered.
|
|
805
|
-
- A [snapshot](#snapshots-and-restore) carries whatever the database does —
|
|
392
|
+
- A [snapshot](docs/operations.md#snapshots-and-restore) carries whatever the database does —
|
|
806
393
|
encrypted provider credentials, gateway key hashes — and is inert only because
|
|
807
394
|
`OMNI_ENCRYPTION_KEY` is not in it. Captured bodies are excluded, so a snapshot
|
|
808
395
|
is never a prompt corpus.
|
|
@@ -810,10 +397,10 @@ Worth knowing before you deploy it:
|
|
|
810
397
|
reads stdin — so they stay out of your shell history and the process table.
|
|
811
398
|
- Behind a reverse proxy, set `OMNI_BASE_URL` to the public HTTPS origin so
|
|
812
399
|
OAuth callbacks match what the providers have registered.
|
|
813
|
-
- The gateway talks to your providers and to nobody else
|
|
814
|
-
fonts
|
|
815
|
-
own, and `omni plugin verify <id>` shows exactly which ones it
|
|
816
|
-
does its manifest, which is a plain file you can read before installing.
|
|
400
|
+
- The gateway talks to your providers and to nobody else — except your collector, if you name
|
|
401
|
+
one with `OMNI_OTLP_ENDPOINT`. No CDN fonts or undeclared third-party origins. A plugin may
|
|
402
|
+
declare outbound origins of its own, and `omni plugin verify <id>` shows exactly which ones it
|
|
403
|
+
asked for — as does its manifest, which is a plain file you can read before installing.
|
|
817
404
|
- **Plugins run inside the gateway process, with its privileges** — the
|
|
818
405
|
capability context is a guardrail, not a sandbox. Read the
|
|
819
406
|
[security note](#plugins) before installing one you did not write.
|
|
@@ -834,80 +421,15 @@ omni plugin update some-plugin # reinstall from whatever it was installed
|
|
|
834
421
|
omni restart # plugins load at boot, so this is required
|
|
835
422
|
```
|
|
836
423
|
|
|
837
|
-
`update` needs no spec: `install` records the one you typed in
|
|
838
|
-
`.omni-install.json` beside the plugin, so picking up a patch release is the
|
|
839
|
-
plugin's id and nothing else. A bare package name re-resolves to the current
|
|
840
|
-
release; `name@1.2.3` reinstalls that version exactly. A plugin copied in by
|
|
841
|
-
hand has no record and `update` says so rather than guessing.
|
|
842
|
-
|
|
843
424
|
**Nothing in the package is executed by any of these.** There is no dependency
|
|
844
425
|
resolution, no `node_modules`, and no lifecycle script — the installer fetches,
|
|
845
426
|
checks, and unpacks, and the plugin's own code is first imported at the next boot.
|
|
427
|
+
How a spec is resolved, what installing by name refuses, what `list` and
|
|
428
|
+
`verify` report, and what `remove` keeps are in
|
|
429
|
+
[docs/plugins.md](docs/plugins.md).
|
|
846
430
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
package shadow the directory you are standing in and turn `omni plugin install
|
|
850
|
-
some-plugin` into a download nobody asked for.
|
|
851
|
-
|
|
852
|
-
Installing by name refuses more than it accepts, and each refusal happens before
|
|
853
|
-
any bytes are fetched: the tarball must be served from the registry's own host,
|
|
854
|
-
the registry must advertise an integrity hash or a shasum, and only an exact
|
|
855
|
-
version or the registry's `latest` resolves — no ranges, no other dist-tags. Use
|
|
856
|
-
`--registry` (or `OMNI_PLUGIN_REGISTRY`) for a private registry; it must be
|
|
857
|
-
`https://`.
|
|
858
|
-
|
|
859
|
-
A URL you type is different, and the difference is the point: nothing downstream
|
|
860
|
-
has a digest to check it against, so TLS to the host you named is the only
|
|
861
|
-
assurance there is. That is why `http://` is refused rather than upgraded.
|
|
862
|
-
|
|
863
|
-
`omni plugin list` prints what this installation has — id, name, version, the
|
|
864
|
-
plugin API and console SDK it was built against, the capabilities it declared,
|
|
865
|
-
and whether the gateway would load it:
|
|
866
|
-
|
|
867
|
-
```
|
|
868
|
-
ID NAME VERSION API SDK CAPABILITIES STATE
|
|
869
|
-
pokemon Pokémon Companion 1.0.0 2 ^1.0.0 storage,files,net:outbound,… ok
|
|
870
|
-
```
|
|
871
|
-
|
|
872
|
-
The API column is the plugin API generation, and it is matched **exactly** — a
|
|
873
|
-
plugin built against an older one is listed with that as its reason rather than
|
|
874
|
-
loaded.
|
|
875
|
-
|
|
876
|
-
The capabilities a manifest may declare are `storage`, `files`, `net:outbound`,
|
|
877
|
-
`events:request`, `events:limit`, `channels` and `provider`. `channels` is
|
|
878
|
-
namespaced topics on the gateway's push socket, which a plugin owns without ever
|
|
879
|
-
touching a connection. `provider` lets a plugin supply a provider of its own —
|
|
880
|
-
the models, the wire format, and optionally the OAuth flow, so
|
|
881
|
-
`omni connect <plugin-id>` works for it exactly as it does for a built-in.
|
|
882
|
-
Anything a plugin did not declare is absent from what it is handed.
|
|
883
|
-
|
|
884
|
-
`net:outbound` and `provider` each require the manifest to declare `origins`,
|
|
885
|
-
and both are enforced: a plugin's own `fetch` and the requests the gateway makes
|
|
886
|
-
on a provider plugin's behalf are both refused outside them. That is what makes
|
|
887
|
-
`omni plugin verify <id>` worth reading before you install one — where your
|
|
888
|
-
prompts can be sent is in the manifest, not only in the code.
|
|
889
|
-
|
|
890
|
-
A plugin that would *not* load is listed with the reason rather than hidden,
|
|
891
|
-
because a plugin missing from the console is exactly what you are trying to
|
|
892
|
-
explain. For one plugin's full detail — its entry points and the outbound
|
|
893
|
-
origins it declared — use `omni plugin verify <id>`.
|
|
894
|
-
|
|
895
|
-
### Available plugins
|
|
896
|
-
|
|
897
|
-
There is no curated directory to browse, and there is no plan for one. A plugin
|
|
898
|
-
is a directory, a tarball, a URL or a package name you point `omni plugin
|
|
899
|
-
install` at, and you are expected to know where it came from — see the
|
|
900
|
-
[security note](#security) for why that is the model rather than an omission.
|
|
901
|
-
|
|
902
|
-
Resolving a name through npm makes distribution easier; it does not make an
|
|
903
|
-
unknown plugin safer. Integrity checking proves you received the bytes the
|
|
904
|
-
registry advertised, and nothing about who wrote them or what they do once the
|
|
905
|
-
gateway imports them.
|
|
906
|
-
|
|
907
|
-
None ship in this repository, deliberately. The first one did, and moving it out
|
|
908
|
-
is what proved the plugin API actually works from outside: while it built as a
|
|
909
|
-
workspace sibling it could reach internal packages no published plugin can, and
|
|
910
|
-
two bugs hid in exactly that gap.
|
|
431
|
+
There is no curated directory to browse. None ship in this repository; one is
|
|
432
|
+
published:
|
|
911
433
|
|
|
912
434
|
| Plugin | What it does |
|
|
913
435
|
| --- | --- |
|
|
@@ -925,47 +447,6 @@ pokemon` prints back. Those assets are Nintendo and Game Freak intellectual
|
|
|
925
447
|
property, fetched at runtime and never vendored — into that repository, this one,
|
|
926
448
|
or anything either publishes.
|
|
927
449
|
|
|
928
|
-
`verify` is the one to run before restarting a gateway that people are using: it
|
|
929
|
-
reaches the same verdict the next boot will, from the same code, without loading
|
|
930
|
-
the plugin.
|
|
931
|
-
|
|
932
|
-
### Installing on a machine with no checkout
|
|
933
|
-
|
|
934
|
-
A published plugin installs by name — no checkout, no build toolchain:
|
|
935
|
-
|
|
936
|
-
```bash
|
|
937
|
-
omni plugin install omnigateway-plugin-example
|
|
938
|
-
omni plugin verify example && omni restart
|
|
939
|
-
```
|
|
940
|
-
|
|
941
|
-
Building and shipping your own plugin — tarball layout, the manifest-at-root
|
|
942
|
-
rule, why plaintext `http://` stays refused, Docker mounting — is covered in
|
|
943
|
-
[docs/writing-a-plugin.md](docs/writing-a-plugin.md).
|
|
944
|
-
|
|
945
|
-
**In Docker**, mount the plugin at `<root>/plugins/<id>` on a volume — the same
|
|
946
|
-
layout `install` writes — and restart the container; read-write, not `:ro`,
|
|
947
|
-
because a plugin declaring `files` writes its cache inside its own directory.
|
|
948
|
-
See [docs/writing-a-plugin.md](docs/writing-a-plugin.md).
|
|
949
|
-
|
|
950
|
-
Removing one keeps its data:
|
|
951
|
-
|
|
952
|
-
```bash
|
|
953
|
-
omni plugin remove some-plugin # directory goes, database tables stay
|
|
954
|
-
omni plugin remove some-plugin --purge # tables too, after confirming
|
|
955
|
-
```
|
|
956
|
-
|
|
957
|
-
That default is deliberate. A plugin directory can be reinstalled from the
|
|
958
|
-
package it came from; whatever it accumulated in your database cannot be
|
|
959
|
-
reinstalled from anything.
|
|
960
|
-
|
|
961
|
-
Note what "directory goes" includes: a plugin's `data/` directory is removed
|
|
962
|
-
with it. That directory holds cached files a plugin can rebuild — it is excluded
|
|
963
|
-
from [snapshots](#snapshots-and-restore) for that reason, so it has no restore
|
|
964
|
-
path and is not meant to need one. Only the database tables are kept, and only
|
|
965
|
-
those are what `--purge` additionally drops. For the same reason, restoring a snapshot onto an
|
|
966
|
-
installation that no longer has a plugin leaves that plugin's tables in place —
|
|
967
|
-
`omni doctor` reports them, and nothing removes them for you.
|
|
968
|
-
|
|
969
450
|
Read the [security note](#security) on what a plugin can reach before installing
|
|
970
451
|
one you did not write. To write one, see
|
|
971
452
|
[docs/writing-a-plugin.md](docs/writing-a-plugin.md).
|