omnigateway 0.1.0 → 0.1.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 +8 -8
- package/bin/omni.js +255 -118
- package/gateway.js +212 -45
- package/package.json +2 -2
- package/public/assets/{CopyValue-BwjFnqW2.js → CopyValue-bzKz2tiJ.js} +1 -1
- package/public/assets/{Lamp-CSv_5d5k.js → Lamp-WOotyOTd.js} +4 -4
- package/public/assets/{Modal-1tHwlj0o.js → Modal-Crr8uyWt.js} +1 -1
- package/public/assets/{Rack-DANVhi_U.js → Rack-uDxrjvaz.js} +1 -1
- package/public/assets/{Toggle-pWcL2A-N.js → Toggle-D0e4C94X.js} +1 -1
- package/public/assets/{_app-C5mYmzgk.js → _app-OSvB-Dv9.js} +1 -1
- package/public/assets/_app.accounts-BOE49WyS.js +51 -0
- package/public/assets/_app.index-CTRlqxOq.js +61 -0
- package/public/assets/{_app.keys-DTREQf0P.js → _app.keys-BNEOjtbu.js} +1 -1
- package/public/assets/{_app.logs-Co3QPpc1.js → _app.logs-BxxqeEhM.js} +1 -1
- package/public/assets/{_app.models-DPSKIyeq.js → _app.models-BJi-RGUO.js} +6 -6
- package/public/assets/{_app.settings-15ZMiM5O.js → _app.settings-C1gB4w7c.js} +1 -1
- package/public/assets/_app.usage-B0iHKzmI.js +163 -0
- package/public/assets/catalog-DVRPlBJJ.js +1 -0
- package/public/assets/{index-DnNTc35a.js → index--dA3FFSN.js} +2 -2
- package/public/assets/{login-DNvWG7Nv.js → login-D-FsWNX8.js} +1 -1
- package/public/assets/trash-2-Cep_aVLx.js +1 -0
- package/public/favicon.svg +25 -0
- package/public/index.html +3 -1
- package/public/assets/_app.accounts-e7Mbaxdc.js +0 -47
- package/public/assets/_app.index-CxunMQ_m.js +0 -57
- package/public/assets/_app.usage-BBffCDCD.js +0 -163
- package/public/assets/catalog-DmrbfYQe.js +0 -1
package/README.md
CHANGED
|
@@ -77,7 +77,7 @@ Then set up and start:
|
|
|
77
77
|
```bash
|
|
78
78
|
omni db migrate # create the database
|
|
79
79
|
omni admin set-password # prompts; the console signs in with this
|
|
80
|
-
omni start # serves the API and the console on 127.0.0.1:
|
|
80
|
+
omni start # serves the API and the console on 127.0.0.1:9000
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
Connect an account. The CLI prints a URL to open, and waits:
|
|
@@ -103,14 +103,14 @@ omni keys create --label laptop
|
|
|
103
103
|
Now use it:
|
|
104
104
|
|
|
105
105
|
```bash
|
|
106
|
-
curl http://127.0.0.1:
|
|
106
|
+
curl http://127.0.0.1:9000/v1/chat/completions \
|
|
107
107
|
-H 'content-type: application/json' \
|
|
108
108
|
-H 'authorization: Bearer <gateway-key>' \
|
|
109
109
|
-d '{"model": "fast", "messages": [{"role": "user", "content": "Hello"}]}'
|
|
110
110
|
```
|
|
111
111
|
|
|
112
112
|
Everything above is also available in the browser at
|
|
113
|
-
`http://127.0.0.1:
|
|
113
|
+
`http://127.0.0.1:9000`, which walks the same steps.
|
|
114
114
|
|
|
115
115
|
## Using it from a client
|
|
116
116
|
|
|
@@ -132,7 +132,7 @@ Ask for one of your virtual models by name. A bare provider model
|
|
|
132
132
|
(`claude-sonnet-5`, `gpt-5`) also works if an account can serve it.
|
|
133
133
|
|
|
134
134
|
Most tools that accept a custom base URL work unchanged: set it to
|
|
135
|
-
`http://127.0.0.1:
|
|
135
|
+
`http://127.0.0.1:9000` and use a gateway key where the provider key goes.
|
|
136
136
|
|
|
137
137
|
## The CLI
|
|
138
138
|
|
|
@@ -186,7 +186,7 @@ Configuration is environment variables, read from the installation's `.env`:
|
|
|
186
186
|
| --- | --- | --- | --- |
|
|
187
187
|
| `OMNI_ENCRYPTION_KEY` | Yes | — | Encrypts provider credentials at rest; 16 characters minimum |
|
|
188
188
|
| `OMNI_HOST` | No | `127.0.0.1` | Listener host |
|
|
189
|
-
| `OMNI_PORT` | No | `
|
|
189
|
+
| `OMNI_PORT` | No | `9000` | Listener port |
|
|
190
190
|
| `OMNI_DB_PATH` | No | `./omnigateway.db` | SQLite database path |
|
|
191
191
|
| `OMNI_BASE_URL` | No | derived from host and port | Public origin for OAuth callbacks; set this behind a reverse proxy |
|
|
192
192
|
| `OMNI_STATIC_DIR` | No | the console shipped with the server | Serve a different console build |
|
|
@@ -203,13 +203,13 @@ client-identity overrides.
|
|
|
203
203
|
```bash
|
|
204
204
|
docker build -t omnigateway .
|
|
205
205
|
docker run --rm \
|
|
206
|
-
-p
|
|
206
|
+
-p 9000:9000 \
|
|
207
207
|
-e OMNI_ENCRYPTION_KEY="$(openssl rand -base64 32)" \
|
|
208
208
|
-v omnigateway-data:/data \
|
|
209
209
|
omnigateway
|
|
210
210
|
```
|
|
211
211
|
|
|
212
|
-
The container listens on `0.0.0.0:
|
|
212
|
+
The container listens on `0.0.0.0:9000` and keeps its database at
|
|
213
213
|
`/data/omnigateway.db`. Note that **the image builds the gateway only**: it
|
|
214
214
|
serves the APIs and returns 404 for the console. Use the CLI or the control API
|
|
215
215
|
against it, or install the npm package if you want the console.
|
|
@@ -258,7 +258,7 @@ cd omnigateway
|
|
|
258
258
|
bun install
|
|
259
259
|
cp .env.example .env # then set OMNI_ENCRYPTION_KEY
|
|
260
260
|
bun run build:dashboard # the gateway serves this build
|
|
261
|
-
bun run dev # gateway on
|
|
261
|
+
bun run dev # gateway on 9000, with file watching
|
|
262
262
|
```
|
|
263
263
|
|
|
264
264
|
Releases are published to npm from a `v*` tag, with provenance attesting which
|
package/bin/omni.js
CHANGED
|
@@ -159,7 +159,7 @@ function loadConfig(env) {
|
|
|
159
159
|
throw new Error(`OMNI_ENCRYPTION_KEY must be set to at least ${MIN_KEY_LENGTH} characters. ` + "Generate one with: openssl rand -base64 32");
|
|
160
160
|
}
|
|
161
161
|
const host = optionalText(env.OMNI_HOST, "127.0.0.1");
|
|
162
|
-
const rawPort = env.OMNI_PORT ?? "
|
|
162
|
+
const rawPort = env.OMNI_PORT ?? "9000";
|
|
163
163
|
const port = Number(rawPort);
|
|
164
164
|
if (!DECIMAL_INTEGER.test(rawPort) || !Number.isInteger(port) || port < 1 || port > 65535) {
|
|
165
165
|
throw new Error(`OMNI_PORT must be an integer between 1 and 65535, got "${rawPort}"`);
|
|
@@ -16415,25 +16415,104 @@ function requireDimension(grain, dimension) {
|
|
|
16415
16415
|
}
|
|
16416
16416
|
|
|
16417
16417
|
// packages/control/src/credentials.ts
|
|
16418
|
+
function summarizeCredential(credential) {
|
|
16419
|
+
return {
|
|
16420
|
+
id: credential.id,
|
|
16421
|
+
provider: credential.provider,
|
|
16422
|
+
label: credential.label,
|
|
16423
|
+
authType: credential.authType,
|
|
16424
|
+
enabled: credential.enabled,
|
|
16425
|
+
tier: credential.tier,
|
|
16426
|
+
weight: credential.weight,
|
|
16427
|
+
expiresAt: credential.expiresAt,
|
|
16428
|
+
accountEmail: credential.accountEmail,
|
|
16429
|
+
providerData: credential.providerData,
|
|
16430
|
+
disabledReason: credential.disabledReason,
|
|
16431
|
+
disabledAt: credential.disabledAt,
|
|
16432
|
+
hasRefreshToken: credential.hasRefreshToken,
|
|
16433
|
+
createdAt: credential.createdAt,
|
|
16434
|
+
updatedAt: credential.updatedAt
|
|
16435
|
+
};
|
|
16436
|
+
}
|
|
16418
16437
|
async function listCredentials(store) {
|
|
16419
|
-
|
|
16420
|
-
|
|
16421
|
-
|
|
16422
|
-
|
|
16423
|
-
|
|
16424
|
-
|
|
16425
|
-
|
|
16426
|
-
|
|
16427
|
-
|
|
16428
|
-
|
|
16429
|
-
|
|
16430
|
-
|
|
16431
|
-
|
|
16432
|
-
|
|
16433
|
-
|
|
16434
|
-
|
|
16435
|
-
|
|
16436
|
-
|
|
16438
|
+
return (await store.credentials.list()).map(summarizeCredential);
|
|
16439
|
+
}
|
|
16440
|
+
async function getCredential(store, id) {
|
|
16441
|
+
const credential = await store.credentials.get(id);
|
|
16442
|
+
if (credential === null)
|
|
16443
|
+
throw new GatewayError("BAD_REQUEST", "no such credential");
|
|
16444
|
+
return summarizeCredential(credential);
|
|
16445
|
+
}
|
|
16446
|
+
async function createApiKeyCredential(store, input) {
|
|
16447
|
+
const provider = parseOrThrow(providerIdSchema, input.provider);
|
|
16448
|
+
if (typeof input.apiKey !== "string" || input.apiKey.trim().length === 0) {
|
|
16449
|
+
throw new GatewayError("BAD_REQUEST", "apiKey: must not be empty");
|
|
16450
|
+
}
|
|
16451
|
+
if (input.label !== undefined && typeof input.label !== "string") {
|
|
16452
|
+
throw new GatewayError("BAD_REQUEST", "label: must be a string");
|
|
16453
|
+
}
|
|
16454
|
+
const label = input.label?.trim() || `${provider} api key`;
|
|
16455
|
+
const created = await store.credentials.create({
|
|
16456
|
+
id: crypto.randomUUID(),
|
|
16457
|
+
provider,
|
|
16458
|
+
label,
|
|
16459
|
+
authType: "apiKey",
|
|
16460
|
+
enabled: true,
|
|
16461
|
+
tier: 1,
|
|
16462
|
+
weight: 1,
|
|
16463
|
+
expiresAt: null,
|
|
16464
|
+
accountEmail: null,
|
|
16465
|
+
providerData: {},
|
|
16466
|
+
disabledReason: null,
|
|
16467
|
+
disabledAt: null,
|
|
16468
|
+
accessToken: null,
|
|
16469
|
+
refreshToken: null,
|
|
16470
|
+
apiKey: input.apiKey,
|
|
16471
|
+
idToken: null
|
|
16472
|
+
});
|
|
16473
|
+
return summarizeCredential(created);
|
|
16474
|
+
}
|
|
16475
|
+
async function refreshCredential(deps, id) {
|
|
16476
|
+
const credential = await deps.store.credentials.get(id);
|
|
16477
|
+
if (credential === null)
|
|
16478
|
+
throw new GatewayError("BAD_REQUEST", "no such credential");
|
|
16479
|
+
if (credential.authType !== "oauth") {
|
|
16480
|
+
throw new GatewayError("BAD_REQUEST", `credential "${id}" is an api key and has nothing to refresh`);
|
|
16481
|
+
}
|
|
16482
|
+
await deps.refresh(credential);
|
|
16483
|
+
return getCredential(deps.store, id);
|
|
16484
|
+
}
|
|
16485
|
+
async function credentialHealth(store) {
|
|
16486
|
+
const [health, quota] = await Promise.all([
|
|
16487
|
+
store.credentials.listHealth(),
|
|
16488
|
+
store.credentials.listQuota()
|
|
16489
|
+
]);
|
|
16490
|
+
return { health, quota };
|
|
16491
|
+
}
|
|
16492
|
+
async function credentialStatus(store, options) {
|
|
16493
|
+
const [credentials, quota, adminConfigured] = await Promise.all([
|
|
16494
|
+
listCredentials(store),
|
|
16495
|
+
store.credentials.listQuota(),
|
|
16496
|
+
createAdminAuth(store, { now: options.now, sessionTtlMs: 0 }).isConfigured()
|
|
16497
|
+
]);
|
|
16498
|
+
const byCredential = new Map;
|
|
16499
|
+
for (const row of quota) {
|
|
16500
|
+
const rows = byCredential.get(row.credentialId);
|
|
16501
|
+
if (rows === undefined)
|
|
16502
|
+
byCredential.set(row.credentialId, [row]);
|
|
16503
|
+
else
|
|
16504
|
+
rows.push(row);
|
|
16505
|
+
}
|
|
16506
|
+
return {
|
|
16507
|
+
adminConfigured,
|
|
16508
|
+
credentials: credentials.map(({ id, provider, label, enabled }) => ({
|
|
16509
|
+
id,
|
|
16510
|
+
provider,
|
|
16511
|
+
label,
|
|
16512
|
+
enabled,
|
|
16513
|
+
quota: byCredential.get(id) ?? []
|
|
16514
|
+
}))
|
|
16515
|
+
};
|
|
16437
16516
|
}
|
|
16438
16517
|
async function patchCredential(deps, id, input) {
|
|
16439
16518
|
const patch = parseOrThrow(credentialPatchSchema, input);
|
|
@@ -16461,7 +16540,7 @@ function healthKey(credentialId, model) {
|
|
|
16461
16540
|
}
|
|
16462
16541
|
async function buildSnapshot(store, now) {
|
|
16463
16542
|
const [credentials, healthRows, quotaRows, models, settings] = await Promise.all([
|
|
16464
|
-
store.credentials.
|
|
16543
|
+
store.credentials.listRouting(),
|
|
16465
16544
|
store.credentials.listHealth(),
|
|
16466
16545
|
store.credentials.listQuota(),
|
|
16467
16546
|
store.config.listModels(),
|
|
@@ -16775,7 +16854,7 @@ var DEFAULT_SETTINGS = {
|
|
|
16775
16854
|
// packages/store/src/sqlite/config.ts
|
|
16776
16855
|
var SETTINGS_KEY = "settings";
|
|
16777
16856
|
var ADMIN_HASH_KEY = "adminPasswordHash";
|
|
16778
|
-
function createConfigRepo(db) {
|
|
16857
|
+
function createConfigRepo(db, emit2 = () => {}) {
|
|
16779
16858
|
const readRaw = (key) => db.query("SELECT value FROM settings WHERE key = ?").get(key)?.value ?? null;
|
|
16780
16859
|
const writeRaw = (key, value) => {
|
|
16781
16860
|
db.run("INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value = excluded.value", [key, value]);
|
|
@@ -16795,9 +16874,11 @@ function createConfigRepo(db) {
|
|
|
16795
16874
|
targets = excluded.targets,
|
|
16796
16875
|
strategy = excluded.strategy,
|
|
16797
16876
|
is_alias = excluded.is_alias`, [model.id, JSON.stringify(model.targets), model.strategy, model.isAlias ? 1 : 0]);
|
|
16877
|
+
emit2({ type: "modelsChanged" });
|
|
16798
16878
|
},
|
|
16799
16879
|
async removeModel(id) {
|
|
16800
16880
|
db.run("DELETE FROM virtual_models WHERE id = ?", [id]);
|
|
16881
|
+
emit2({ type: "modelsChanged" });
|
|
16801
16882
|
},
|
|
16802
16883
|
async getSettings() {
|
|
16803
16884
|
const raw = readRaw(SETTINGS_KEY);
|
|
@@ -16818,6 +16899,7 @@ function createConfigRepo(db) {
|
|
|
16818
16899
|
weights: { ...current.weights, ...patch.weights }
|
|
16819
16900
|
};
|
|
16820
16901
|
writeRaw(SETTINGS_KEY, JSON.stringify(next));
|
|
16902
|
+
emit2({ type: "settingsChanged" });
|
|
16821
16903
|
return next;
|
|
16822
16904
|
},
|
|
16823
16905
|
async getAdminPasswordHash() {
|
|
@@ -16833,8 +16915,40 @@ function createConfigRepo(db) {
|
|
|
16833
16915
|
};
|
|
16834
16916
|
}
|
|
16835
16917
|
// packages/store/src/sqlite/credentials.ts
|
|
16836
|
-
function createCredentialRepo(db, key) {
|
|
16837
|
-
const
|
|
16918
|
+
function createCredentialRepo(db, key, emit2 = () => {}) {
|
|
16919
|
+
const open = async (v) => v === null ? null : decrypt(key, v);
|
|
16920
|
+
const secretsFrom = async (row) => ({
|
|
16921
|
+
accessToken: await open(row.access_token),
|
|
16922
|
+
refreshToken: await open(row.refresh_token),
|
|
16923
|
+
apiKey: await open(row.api_key),
|
|
16924
|
+
idToken: await open(row.id_token)
|
|
16925
|
+
});
|
|
16926
|
+
const requiredRow = (row, id) => {
|
|
16927
|
+
if (row === null)
|
|
16928
|
+
throw new Error(`credential ${id} no longer exists`);
|
|
16929
|
+
return row;
|
|
16930
|
+
};
|
|
16931
|
+
const currentSecrets = async (id) => {
|
|
16932
|
+
const row = requiredRow(db.query("SELECT access_token, refresh_token, api_key, id_token FROM credentials WHERE id = ?").get(id), id);
|
|
16933
|
+
return secretsFrom(row);
|
|
16934
|
+
};
|
|
16935
|
+
const currentInferenceSecrets = async (id, authType) => {
|
|
16936
|
+
if (authType === "oauth") {
|
|
16937
|
+
const row2 = requiredRow(db.query("SELECT access_token FROM credentials WHERE id = ?").get(id), id);
|
|
16938
|
+
return { accessToken: await open(row2.access_token), apiKey: null };
|
|
16939
|
+
}
|
|
16940
|
+
const row = requiredRow(db.query("SELECT api_key FROM credentials WHERE id = ?").get(id), id);
|
|
16941
|
+
return { accessToken: null, apiKey: await open(row.api_key) };
|
|
16942
|
+
};
|
|
16943
|
+
const currentRefreshSecrets = async (id) => {
|
|
16944
|
+
const row = requiredRow(db.query("SELECT refresh_token FROM credentials WHERE id = ?").get(id), id);
|
|
16945
|
+
return { refreshToken: await open(row.refresh_token) };
|
|
16946
|
+
};
|
|
16947
|
+
const currentUsageSecrets = async (id) => {
|
|
16948
|
+
const row = requiredRow(db.query("SELECT access_token FROM credentials WHERE id = ?").get(id), id);
|
|
16949
|
+
return { accessToken: await open(row.access_token) };
|
|
16950
|
+
};
|
|
16951
|
+
const view = (row, loadCurrentSecrets = false) => ({
|
|
16838
16952
|
id: row.id,
|
|
16839
16953
|
provider: row.provider,
|
|
16840
16954
|
label: row.label,
|
|
@@ -16850,18 +16964,31 @@ function createCredentialRepo(db, key) {
|
|
|
16850
16964
|
hasRefreshToken: row.refresh_token !== null,
|
|
16851
16965
|
createdAt: row.created_at,
|
|
16852
16966
|
updatedAt: row.updated_at,
|
|
16853
|
-
secrets:
|
|
16854
|
-
|
|
16855
|
-
|
|
16856
|
-
|
|
16857
|
-
|
|
16858
|
-
|
|
16967
|
+
secrets: () => loadCurrentSecrets ? currentSecrets(row.id) : secretsFrom(row),
|
|
16968
|
+
openForInference: async () => {
|
|
16969
|
+
if (loadCurrentSecrets)
|
|
16970
|
+
return currentInferenceSecrets(row.id, row.auth_type);
|
|
16971
|
+
if (row.auth_type === "oauth") {
|
|
16972
|
+
return { accessToken: await open(row.access_token), apiKey: null };
|
|
16973
|
+
}
|
|
16974
|
+
return { accessToken: null, apiKey: await open(row.api_key) };
|
|
16975
|
+
},
|
|
16976
|
+
openForRefresh: async () => loadCurrentSecrets ? currentRefreshSecrets(row.id) : { refreshToken: await open(row.refresh_token) },
|
|
16977
|
+
openForUsage: async () => loadCurrentSecrets ? currentUsageSecrets(row.id) : { accessToken: await open(row.access_token) }
|
|
16859
16978
|
});
|
|
16860
|
-
const open = async (v) => v === null ? null : decrypt(key, v);
|
|
16861
16979
|
const seal = async (v) => v === null || v === undefined ? null : encrypt(key, v);
|
|
16862
16980
|
return {
|
|
16863
16981
|
async list() {
|
|
16864
|
-
return db.query("SELECT * FROM credentials ORDER BY tier, label").all().map(view);
|
|
16982
|
+
return db.query("SELECT * FROM credentials ORDER BY tier, label").all().map((row) => view(row));
|
|
16983
|
+
},
|
|
16984
|
+
async listRouting() {
|
|
16985
|
+
return db.query(`SELECT id, provider, label, auth_type, enabled, tier, weight, expires_at,
|
|
16986
|
+
account_email, provider_data, disabled_reason, disabled_at,
|
|
16987
|
+
NULL AS access_token,
|
|
16988
|
+
CASE WHEN refresh_token IS NULL THEN NULL ELSE 'present' END AS refresh_token,
|
|
16989
|
+
NULL AS api_key, NULL AS id_token, created_at, updated_at
|
|
16990
|
+
FROM credentials
|
|
16991
|
+
ORDER BY tier, label`).all().map((row) => view(row, true));
|
|
16865
16992
|
},
|
|
16866
16993
|
async get(id) {
|
|
16867
16994
|
const row = db.query("SELECT * FROM credentials WHERE id = ?").get(id);
|
|
@@ -16894,6 +17021,7 @@ function createCredentialRepo(db, key) {
|
|
|
16894
17021
|
now
|
|
16895
17022
|
]);
|
|
16896
17023
|
const { accessToken, refreshToken, apiKey, idToken, ...meta3 } = input;
|
|
17024
|
+
emit2({ type: "credentialsChanged" });
|
|
16897
17025
|
return {
|
|
16898
17026
|
...meta3,
|
|
16899
17027
|
hasRefreshToken: refreshToken != null,
|
|
@@ -16930,6 +17058,7 @@ function createCredentialRepo(db, key) {
|
|
|
16930
17058
|
return;
|
|
16931
17059
|
put("updated_at", Date.now());
|
|
16932
17060
|
db.run(`UPDATE credentials SET ${sets.join(", ")} WHERE id = ?`, [...vals, id]);
|
|
17061
|
+
emit2({ type: "credentialsChanged" });
|
|
16933
17062
|
},
|
|
16934
17063
|
async updateSecrets(id, secrets, expiresAt) {
|
|
16935
17064
|
const sets = [];
|
|
@@ -16953,9 +17082,11 @@ function createCredentialRepo(db, key) {
|
|
|
16953
17082
|
sets.push("expires_at = ?", "updated_at = ?");
|
|
16954
17083
|
vals.push(expiresAt, Date.now());
|
|
16955
17084
|
db.run(`UPDATE credentials SET ${sets.join(", ")} WHERE id = ?`, [...vals, id]);
|
|
17085
|
+
emit2({ type: "credentialsChanged" });
|
|
16956
17086
|
},
|
|
16957
17087
|
async remove(id) {
|
|
16958
17088
|
db.run("DELETE FROM credentials WHERE id = ?", [id]);
|
|
17089
|
+
emit2({ type: "credentialsChanged" });
|
|
16959
17090
|
},
|
|
16960
17091
|
async listHealth() {
|
|
16961
17092
|
return db.query("SELECT * FROM credential_health").all().map((r) => ({
|
|
@@ -16986,6 +17117,7 @@ function createCredentialRepo(db, key) {
|
|
|
16986
17117
|
stmt.run(r.credentialId, r.model, r.breakerState, r.consecutiveFailures, r.openedAt, r.rateLimitedUntil, r.ewmaTtftMs, r.lastUsedAt);
|
|
16987
17118
|
}
|
|
16988
17119
|
})();
|
|
17120
|
+
emit2({ type: "healthSaved", rows });
|
|
16989
17121
|
},
|
|
16990
17122
|
async listQuota() {
|
|
16991
17123
|
return db.query("SELECT * FROM quota_windows").all().map((r) => ({
|
|
@@ -17008,11 +17140,24 @@ function createCredentialRepo(db, key) {
|
|
|
17008
17140
|
limit_value = excluded.limit_value,
|
|
17009
17141
|
resets_at = excluded.resets_at,
|
|
17010
17142
|
observed_at = excluded.observed_at`);
|
|
17143
|
+
const prune = db.prepare("DELETE FROM quota_windows WHERE credential_id = ? AND window_type NOT IN (SELECT value FROM json_each(?))");
|
|
17144
|
+
const kept = new Map;
|
|
17145
|
+
for (const r of rows) {
|
|
17146
|
+
const types2 = kept.get(r.credentialId);
|
|
17147
|
+
if (types2 === undefined)
|
|
17148
|
+
kept.set(r.credentialId, [r.windowType]);
|
|
17149
|
+
else
|
|
17150
|
+
types2.push(r.windowType);
|
|
17151
|
+
}
|
|
17011
17152
|
db.transaction(() => {
|
|
17012
17153
|
for (const r of rows) {
|
|
17013
17154
|
stmt.run(r.credentialId, r.windowType, r.startsAt, r.used, r.limit, r.resetsAt, r.observedAt);
|
|
17014
17155
|
}
|
|
17156
|
+
for (const [credentialId, types2] of kept) {
|
|
17157
|
+
prune.run(credentialId, JSON.stringify(types2));
|
|
17158
|
+
}
|
|
17015
17159
|
})();
|
|
17160
|
+
emit2({ type: "quotaSaved", rows });
|
|
17016
17161
|
}
|
|
17017
17162
|
};
|
|
17018
17163
|
}
|
|
@@ -17485,11 +17630,26 @@ function createUsageRepo(db) {
|
|
|
17485
17630
|
// packages/store/src/sqlite/store.ts
|
|
17486
17631
|
async function createStore(opts) {
|
|
17487
17632
|
const db = openDb(opts.path);
|
|
17633
|
+
const listeners = new Set;
|
|
17634
|
+
const emit2 = (change) => {
|
|
17635
|
+
for (const listener of listeners) {
|
|
17636
|
+
try {
|
|
17637
|
+
listener(change);
|
|
17638
|
+
} catch {}
|
|
17639
|
+
}
|
|
17640
|
+
};
|
|
17488
17641
|
return {
|
|
17489
|
-
credentials: createCredentialRepo(db, opts.encryptionKey),
|
|
17490
|
-
config: createConfigRepo(db),
|
|
17642
|
+
credentials: createCredentialRepo(db, opts.encryptionKey, emit2),
|
|
17643
|
+
config: createConfigRepo(db, emit2),
|
|
17491
17644
|
keys: createKeyRepo(db),
|
|
17492
17645
|
usage: createUsageRepo(db),
|
|
17646
|
+
routing: {
|
|
17647
|
+
version: () => db.query("PRAGMA data_version").get()?.data_version ?? 0,
|
|
17648
|
+
subscribe(listener) {
|
|
17649
|
+
listeners.add(listener);
|
|
17650
|
+
return () => listeners.delete(listener);
|
|
17651
|
+
}
|
|
17652
|
+
},
|
|
17493
17653
|
close: () => db.close()
|
|
17494
17654
|
};
|
|
17495
17655
|
}
|
|
@@ -17725,14 +17885,34 @@ function toResult3(token, fallbackRefresh, deps, fallbackAccountId = null) {
|
|
|
17725
17885
|
providerData: { accountId: claims.accountId ?? fallbackAccountId }
|
|
17726
17886
|
};
|
|
17727
17887
|
}
|
|
17888
|
+
function windowTypeOf(value, fallback) {
|
|
17889
|
+
const record2 = recordOf(value);
|
|
17890
|
+
if (record2 === null)
|
|
17891
|
+
return fallback;
|
|
17892
|
+
const seconds = numberOf(record2, [
|
|
17893
|
+
"limit_window_seconds",
|
|
17894
|
+
"limitWindowSeconds",
|
|
17895
|
+
"window_seconds",
|
|
17896
|
+
"windowSeconds"
|
|
17897
|
+
]);
|
|
17898
|
+
if (seconds === null || seconds <= 0)
|
|
17899
|
+
return fallback;
|
|
17900
|
+
if (seconds <= 6 * 60 * 60)
|
|
17901
|
+
return "fiveHour";
|
|
17902
|
+
if (seconds <= 36 * 60 * 60)
|
|
17903
|
+
return "daily";
|
|
17904
|
+
return "weekly";
|
|
17905
|
+
}
|
|
17728
17906
|
function parseOpenAIUsage(value, now) {
|
|
17729
17907
|
const root = recordOf(value);
|
|
17730
17908
|
if (root === null)
|
|
17731
17909
|
return null;
|
|
17732
17910
|
const rateLimit = nestedOf(root, ["rate_limit", "rateLimit"]) ?? root;
|
|
17911
|
+
const primary = rateLimit.primary_window ?? rateLimit.primaryWindow;
|
|
17912
|
+
const secondary = rateLimit.secondary_window ?? rateLimit.secondaryWindow;
|
|
17733
17913
|
return reportFrom([
|
|
17734
|
-
windowFrom(
|
|
17735
|
-
windowFrom(
|
|
17914
|
+
windowFrom(primary, windowTypeOf(primary, "fiveHour"), now),
|
|
17915
|
+
windowFrom(secondary, windowTypeOf(secondary, "weekly"), now)
|
|
17736
17916
|
]);
|
|
17737
17917
|
}
|
|
17738
17918
|
var openaiOAuth = {
|
|
@@ -17797,7 +17977,7 @@ var OAUTH_PROVIDERS = {
|
|
|
17797
17977
|
function createRefresher(deps) {
|
|
17798
17978
|
const inFlight = new Map;
|
|
17799
17979
|
async function run(credential) {
|
|
17800
|
-
const secrets = await credential.
|
|
17980
|
+
const secrets = await credential.openForRefresh();
|
|
17801
17981
|
if (secrets.refreshToken === null) {
|
|
17802
17982
|
throw new GatewayError("AUTH", `credential ${credential.id} has no refresh token`);
|
|
17803
17983
|
}
|
|
@@ -18134,10 +18314,7 @@ var credentialsList = {
|
|
|
18134
18314
|
}
|
|
18135
18315
|
};
|
|
18136
18316
|
async function findCredential(ctx, id) {
|
|
18137
|
-
|
|
18138
|
-
if (credential === null)
|
|
18139
|
-
throw new CliError(`no credential "${id}"`);
|
|
18140
|
-
return credential;
|
|
18317
|
+
return getCredential(await ctx.store(), id);
|
|
18141
18318
|
}
|
|
18142
18319
|
var credentialsShow = {
|
|
18143
18320
|
usage: "credentials show <id>",
|
|
@@ -18236,12 +18413,7 @@ var credentialsRefresh = {
|
|
|
18236
18413
|
async run(args, { ctx, writer }) {
|
|
18237
18414
|
const id = requirePositional(args, 0, "credential id");
|
|
18238
18415
|
const store = await ctx.store();
|
|
18239
|
-
const credential = await
|
|
18240
|
-
if (credential === null)
|
|
18241
|
-
throw new CliError(`no credential "${id}"`);
|
|
18242
|
-
if (credential.authType !== "oauth") {
|
|
18243
|
-
throw new CliError(`credential "${id}" is an api key and has nothing to refresh`);
|
|
18244
|
-
}
|
|
18416
|
+
const credential = await findCredential(ctx, id);
|
|
18245
18417
|
const refresh = createRefresher({
|
|
18246
18418
|
store,
|
|
18247
18419
|
providers: OAUTH_PROVIDERS,
|
|
@@ -18249,9 +18421,8 @@ var credentialsRefresh = {
|
|
|
18249
18421
|
now: ctx.now
|
|
18250
18422
|
});
|
|
18251
18423
|
note(ctx, writer, `refreshing ${credential.provider} credential ${id}\u2026`);
|
|
18252
|
-
await refresh
|
|
18253
|
-
|
|
18254
|
-
emit(ctx, writer, { id, expiresAt: updated?.expiresAt ?? null }, () => `${id} refreshed; expires ${formatTime(updated?.expiresAt ?? null)}`);
|
|
18424
|
+
const updated = await refreshCredential({ store, refresh }, id);
|
|
18425
|
+
emit(ctx, writer, { id, expiresAt: updated.expiresAt }, () => `${id} refreshed; expires ${formatTime(updated.expiresAt)}`);
|
|
18255
18426
|
}
|
|
18256
18427
|
};
|
|
18257
18428
|
var credentialsAddKey = {
|
|
@@ -18266,27 +18437,12 @@ var credentialsAddKey = {
|
|
|
18266
18437
|
const key = await prompt.secret(`${providerId} API key: `);
|
|
18267
18438
|
if (key.length === 0)
|
|
18268
18439
|
throw new CliError("no API key given");
|
|
18269
|
-
const
|
|
18270
|
-
const id = crypto.randomUUID();
|
|
18271
|
-
await store.credentials.create({
|
|
18272
|
-
id,
|
|
18440
|
+
const created = await createApiKeyCredential(await ctx.store(), {
|
|
18273
18441
|
provider: providerId,
|
|
18274
|
-
label: stringFlag(args.values, "label") ?? `${providerId} api key`,
|
|
18275
|
-
authType: "apiKey",
|
|
18276
|
-
enabled: true,
|
|
18277
|
-
tier: 1,
|
|
18278
|
-
weight: 1,
|
|
18279
|
-
expiresAt: null,
|
|
18280
|
-
accountEmail: null,
|
|
18281
|
-
providerData: {},
|
|
18282
|
-
disabledReason: null,
|
|
18283
|
-
disabledAt: null,
|
|
18284
|
-
accessToken: null,
|
|
18285
|
-
refreshToken: null,
|
|
18286
18442
|
apiKey: key,
|
|
18287
|
-
|
|
18443
|
+
label: stringFlag(args.values, "label")
|
|
18288
18444
|
});
|
|
18289
|
-
emit(ctx, writer, { id, provider:
|
|
18445
|
+
emit(ctx, writer, { id: created.id, provider: created.provider }, () => `stored ${created.provider} api key as ${created.id}`);
|
|
18290
18446
|
}
|
|
18291
18447
|
};
|
|
18292
18448
|
var credentialsHealth = {
|
|
@@ -18295,8 +18451,8 @@ var credentialsHealth = {
|
|
|
18295
18451
|
options: { all: { type: "boolean" } },
|
|
18296
18452
|
async run(args, { ctx, writer }) {
|
|
18297
18453
|
const store = await ctx.store();
|
|
18298
|
-
const [rows, credentials] = await Promise.all([
|
|
18299
|
-
store
|
|
18454
|
+
const [{ health: rows }, credentials] = await Promise.all([
|
|
18455
|
+
credentialHealth(store),
|
|
18300
18456
|
listCredentials(store)
|
|
18301
18457
|
]);
|
|
18302
18458
|
const labels = new Map(credentials.map((c) => [c.id, c.label]));
|
|
@@ -19098,24 +19254,29 @@ var settingsSet = {
|
|
|
19098
19254
|
};
|
|
19099
19255
|
|
|
19100
19256
|
// apps/cli/src/commands/status.ts
|
|
19101
|
-
|
|
19102
|
-
|
|
19103
|
-
|
|
19104
|
-
|
|
19105
|
-
|
|
19106
|
-
|
|
19107
|
-
|
|
19108
|
-
|
|
19109
|
-
|
|
19110
|
-
|
|
19257
|
+
var WINDOW_ORDER = {
|
|
19258
|
+
fiveHour: 0,
|
|
19259
|
+
daily: 1,
|
|
19260
|
+
weekly: 2
|
|
19261
|
+
};
|
|
19262
|
+
var WINDOW_LABEL = {
|
|
19263
|
+
fiveHour: "5h",
|
|
19264
|
+
daily: "24h",
|
|
19265
|
+
weekly: "7d"
|
|
19266
|
+
};
|
|
19267
|
+
function reportedWindows(windows) {
|
|
19268
|
+
return windows.filter((window) => window.limit !== null && window.limit > 0).sort((a, b) => WINDOW_ORDER[a.windowType] - WINDOW_ORDER[b.windowType]);
|
|
19111
19269
|
}
|
|
19112
|
-
function quotaCell(ctx, windows,
|
|
19113
|
-
const
|
|
19114
|
-
if (
|
|
19270
|
+
function quotaCell(ctx, windows, now) {
|
|
19271
|
+
const reported = reportedWindows(windows);
|
|
19272
|
+
if (reported.length === 0)
|
|
19115
19273
|
return paint(ctx, "dim", "unknown");
|
|
19116
|
-
const
|
|
19117
|
-
|
|
19118
|
-
|
|
19274
|
+
const parts = reported.map((window) => {
|
|
19275
|
+
const used = Math.round(window.used / window.limit * 100);
|
|
19276
|
+
return state(ctx, used < 90, `${WINDOW_LABEL[window.windowType]} ${used}%`);
|
|
19277
|
+
});
|
|
19278
|
+
const observedAt = Math.min(...reported.map((window) => window.observedAt));
|
|
19279
|
+
return `${parts.join(" \xB7 ")} ${paint(ctx, "dim", `(${formatAge(observedAt, now)} ago)`)}`;
|
|
19119
19280
|
}
|
|
19120
19281
|
var status2 = {
|
|
19121
19282
|
usage: "status",
|
|
@@ -19130,31 +19291,11 @@ var status2 = {
|
|
|
19130
19291
|
} catch (error51) {
|
|
19131
19292
|
storeError = error51 instanceof Error ? error51.message : "could not open the database";
|
|
19132
19293
|
}
|
|
19133
|
-
const
|
|
19134
|
-
const
|
|
19135
|
-
const settings = store === null ? null : await store.config.getSettings();
|
|
19136
|
-
const configured = store === null ? false : await createAdminAuth(store, {
|
|
19137
|
-
now: ctx.now,
|
|
19138
|
-
sessionTtlMs: 0
|
|
19139
|
-
}).isConfigured();
|
|
19140
|
-
const byCredential = new Map;
|
|
19141
|
-
for (const row of quotaRows) {
|
|
19142
|
-
const list = byCredential.get(row.credentialId);
|
|
19143
|
-
if (list === undefined)
|
|
19144
|
-
byCredential.set(row.credentialId, [row]);
|
|
19145
|
-
else
|
|
19146
|
-
list.push(row);
|
|
19147
|
-
}
|
|
19294
|
+
const persistent = store === null ? { adminConfigured: false, credentials: [] } : await credentialStatus(store, { now: ctx.now });
|
|
19295
|
+
const { adminConfigured: configured, credentials } = persistent;
|
|
19148
19296
|
const data = {
|
|
19149
19297
|
process: process3,
|
|
19150
|
-
|
|
19151
|
-
credentials: credentials.map((credential) => ({
|
|
19152
|
-
id: credential.id,
|
|
19153
|
-
provider: credential.provider,
|
|
19154
|
-
label: credential.label,
|
|
19155
|
-
enabled: credential.enabled,
|
|
19156
|
-
quota: byCredential.get(credential.id) ?? []
|
|
19157
|
-
})),
|
|
19298
|
+
...persistent,
|
|
19158
19299
|
storeError
|
|
19159
19300
|
};
|
|
19160
19301
|
emit(ctx, writer, data, () => {
|
|
@@ -19176,16 +19317,12 @@ ${state(ctx, false, storeError)}`;
|
|
|
19176
19317
|
|
|
19177
19318
|
no credentials; add one with: omni connect <provider>`;
|
|
19178
19319
|
}
|
|
19179
|
-
const rows = credentials.map((credential) =>
|
|
19180
|
-
|
|
19181
|
-
|
|
19182
|
-
|
|
19183
|
-
|
|
19184
|
-
|
|
19185
|
-
state(ctx, credential.enabled, credential.enabled ? "enabled" : "disabled"),
|
|
19186
|
-
quotaCell(ctx, windows, headroom, ctx.now())
|
|
19187
|
-
];
|
|
19188
|
-
});
|
|
19320
|
+
const rows = credentials.map((credential) => [
|
|
19321
|
+
credential.label,
|
|
19322
|
+
provider(ctx, credential.provider),
|
|
19323
|
+
state(ctx, credential.enabled, credential.enabled ? "enabled" : "disabled"),
|
|
19324
|
+
quotaCell(ctx, credential.quota, ctx.now())
|
|
19325
|
+
]);
|
|
19189
19326
|
return `${header}
|
|
19190
19327
|
|
|
19191
19328
|
${table([{ header: "ACCOUNT" }, { header: "PROVIDER" }, { header: "STATE" }, { header: "QUOTA" }], rows)}`;
|