@acarmisc/backstage-plugin-litellm 0.4.0 → 0.5.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/dist/api.d.ts +11 -1
- package/dist/components/AuditLog.d.ts +7 -0
- package/dist/components/KeysTable.d.ts +4 -0
- package/dist/index.cjs.js +469 -79
- package/dist/index.cjs.js.map +4 -4
- package/dist/index.esm.js +509 -99
- package/dist/index.esm.js.map +4 -4
- package/dist/types.d.ts +32 -0
- package/package.json +1 -1
package/dist/index.cjs.js
CHANGED
|
@@ -5,8 +5,13 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __esm = (fn, res) => function __init() {
|
|
9
|
-
|
|
8
|
+
var __esm = (fn, res, err) => function __init() {
|
|
9
|
+
if (err) throw err[0];
|
|
10
|
+
try {
|
|
11
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
12
|
+
} catch (e) {
|
|
13
|
+
throw err = [e], e;
|
|
14
|
+
}
|
|
10
15
|
};
|
|
11
16
|
var __export = (target, all) => {
|
|
12
17
|
for (var name in all)
|
|
@@ -105,6 +110,29 @@ var init_api = __esm({
|
|
|
105
110
|
async deleteKey(keyId) {
|
|
106
111
|
return this.del(`/keys/${encodeURIComponent(keyId)}`);
|
|
107
112
|
}
|
|
113
|
+
async rotateKey(keyId) {
|
|
114
|
+
return this.post(`/keys/${encodeURIComponent(keyId)}/regenerate`, {});
|
|
115
|
+
}
|
|
116
|
+
async blockKey(keyId) {
|
|
117
|
+
await this.post(`/keys/${encodeURIComponent(keyId)}/block`, {});
|
|
118
|
+
}
|
|
119
|
+
async unblockKey(keyId) {
|
|
120
|
+
await this.post(`/keys/${encodeURIComponent(keyId)}/unblock`, {});
|
|
121
|
+
}
|
|
122
|
+
async resetKeySpend(keyId) {
|
|
123
|
+
await this.post(`/keys/${encodeURIComponent(keyId)}/reset_spend`, {});
|
|
124
|
+
}
|
|
125
|
+
async getAuditLogs(params) {
|
|
126
|
+
const strParams = {};
|
|
127
|
+
if (params.page !== void 0) strParams.page = String(params.page);
|
|
128
|
+
if (params.page_size !== void 0) strParams.page_size = String(params.page_size);
|
|
129
|
+
if (params.start_date) strParams.start_date = params.start_date;
|
|
130
|
+
if (params.end_date) strParams.end_date = params.end_date;
|
|
131
|
+
if (params.action) strParams.action = params.action;
|
|
132
|
+
if (params.table_name) strParams.table_name = params.table_name;
|
|
133
|
+
if (params.changed_by) strParams.changed_by = params.changed_by;
|
|
134
|
+
return this.get("/audit", strParams);
|
|
135
|
+
}
|
|
108
136
|
async listModels() {
|
|
109
137
|
return this.get("/models");
|
|
110
138
|
}
|
|
@@ -151,6 +179,33 @@ var init_DashboardHeader = __esm({
|
|
|
151
179
|
});
|
|
152
180
|
|
|
153
181
|
// src/components/KeysTable.tsx
|
|
182
|
+
function expiryStatus(expiresAt) {
|
|
183
|
+
if (!expiresAt) return null;
|
|
184
|
+
const diff = new Date(expiresAt).getTime() - Date.now();
|
|
185
|
+
const days = diff / (1e3 * 60 * 60 * 24);
|
|
186
|
+
if (days < 0) return "expired";
|
|
187
|
+
if (days < 7) return "soon";
|
|
188
|
+
return "ok";
|
|
189
|
+
}
|
|
190
|
+
function ExpiryChip({ expiresAt }) {
|
|
191
|
+
const status = expiryStatus(expiresAt);
|
|
192
|
+
if (!status) return /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2", color: "text.secondary" }, "-");
|
|
193
|
+
const label = status === "expired" ? "Expired" : status === "soon" ? `${Math.ceil((new Date(expiresAt).getTime() - Date.now()) / 864e5)}d left` : formatDate(expiresAt);
|
|
194
|
+
const color = status === "expired" ? "error" : status === "soon" ? "warning" : "default";
|
|
195
|
+
const icon = status === "expired" || status === "soon" ? /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Warning, { fontSize: "small" }) : void 0;
|
|
196
|
+
return /* @__PURE__ */ import_react2.default.createElement(import_material2.Chip, { label, color, size: "small", icon });
|
|
197
|
+
}
|
|
198
|
+
function BudgetCell({ spend, maxBudget }) {
|
|
199
|
+
if (!maxBudget) return /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2", color: "text.secondary" }, "-");
|
|
200
|
+
const pct = Math.min(100, spend / maxBudget * 100);
|
|
201
|
+
const color = pct >= 100 ? "error" : pct >= 80 ? "warning" : "primary";
|
|
202
|
+
return /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { minWidth: 100 }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", justifyContent: "space-between" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "caption" }, "$", spend.toFixed(2)), /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "caption", color: "text.secondary" }, "$", maxBudget)), /* @__PURE__ */ import_react2.default.createElement(import_material2.LinearProgress, { variant: "determinate", value: pct, color, sx: { height: 5, borderRadius: 1, mt: 0.25 } }));
|
|
203
|
+
}
|
|
204
|
+
function fmtCost(perToken) {
|
|
205
|
+
if (!perToken) return null;
|
|
206
|
+
const per1k = perToken * 1e3;
|
|
207
|
+
return per1k < 0.01 ? `$${(perToken * 1e6).toFixed(2)}/M` : `$${per1k.toFixed(3)}/1K`;
|
|
208
|
+
}
|
|
154
209
|
var import_react2, import_material2, import_icons_material, maskKey, shortKeyId, formatDate, emptyForm, keyToEditForm, KeysTable;
|
|
155
210
|
var init_KeysTable = __esm({
|
|
156
211
|
"src/components/KeysTable.tsx"() {
|
|
@@ -180,6 +235,7 @@ var init_KeysTable = __esm({
|
|
|
180
235
|
duration: "30d",
|
|
181
236
|
max_budget: 100,
|
|
182
237
|
tpm_limit: void 0,
|
|
238
|
+
rpm_limit: void 0,
|
|
183
239
|
team_id: void 0,
|
|
184
240
|
key_type: "llm_api"
|
|
185
241
|
});
|
|
@@ -197,16 +253,34 @@ var init_KeysTable = __esm({
|
|
|
197
253
|
loading,
|
|
198
254
|
onGenerateKey,
|
|
199
255
|
onUpdateKey,
|
|
256
|
+
onRotateKey,
|
|
257
|
+
onBlockKey,
|
|
258
|
+
onUnblockKey,
|
|
259
|
+
onResetKeySpend,
|
|
200
260
|
onDeleteKey
|
|
201
261
|
}) => {
|
|
202
262
|
const [generateModalOpen, setGenerateModalOpen] = (0, import_react2.useState)(false);
|
|
203
263
|
const [newKeyValue, setNewKeyValue] = (0, import_react2.useState)(null);
|
|
204
264
|
const [formData, setFormData] = (0, import_react2.useState)(emptyForm());
|
|
205
265
|
const [submitting, setSubmitting] = (0, import_react2.useState)(false);
|
|
206
|
-
const canGenerate = true;
|
|
207
266
|
const [editingKey, setEditingKey] = (0, import_react2.useState)(null);
|
|
208
267
|
const [editForm, setEditForm] = (0, import_react2.useState)({});
|
|
209
268
|
const [editSubmitting, setEditSubmitting] = (0, import_react2.useState)(false);
|
|
269
|
+
const [rotatingKeyId, setRotatingKeyId] = (0, import_react2.useState)(null);
|
|
270
|
+
const [rotatedKeyValue, setRotatedKeyValue] = (0, import_react2.useState)(null);
|
|
271
|
+
const [blockingKeyId, setBlockingKeyId] = (0, import_react2.useState)(null);
|
|
272
|
+
const [deleteConfirmId, setDeleteConfirmId] = (0, import_react2.useState)(null);
|
|
273
|
+
const [deleteSubmitting, setDeleteSubmitting] = (0, import_react2.useState)(false);
|
|
274
|
+
const [resetSpendConfirm, setResetSpendConfirm] = (0, import_react2.useState)(false);
|
|
275
|
+
const [resetSpendSubmitting, setResetSpendSubmitting] = (0, import_react2.useState)(false);
|
|
276
|
+
const [filterText, setFilterText] = (0, import_react2.useState)("");
|
|
277
|
+
const filteredKeys = (0, import_react2.useMemo)(() => {
|
|
278
|
+
if (!filterText.trim()) return keys;
|
|
279
|
+
const q = filterText.toLowerCase();
|
|
280
|
+
return keys.filter(
|
|
281
|
+
(k) => (k.key_alias ?? "").toLowerCase().includes(q) || k.models?.some((m) => m.toLowerCase().includes(q))
|
|
282
|
+
);
|
|
283
|
+
}, [keys, filterText]);
|
|
210
284
|
const selectedModels = models.filter((m) => (formData.models || []).includes(m.model_name));
|
|
211
285
|
const selectedTeam = teams.find((t) => t.team_id === formData.team_id) ?? null;
|
|
212
286
|
const editSelectedModels = models.filter((m) => (editForm.models || []).includes(m.model_name));
|
|
@@ -234,6 +308,7 @@ var init_KeysTable = __esm({
|
|
|
234
308
|
const handleCloseEdit = () => {
|
|
235
309
|
setEditingKey(null);
|
|
236
310
|
setEditForm({});
|
|
311
|
+
setResetSpendConfirm(false);
|
|
237
312
|
};
|
|
238
313
|
const handleUpdate = async () => {
|
|
239
314
|
if (!editingKey) return;
|
|
@@ -247,10 +322,72 @@ var init_KeysTable = __esm({
|
|
|
247
322
|
setEditSubmitting(false);
|
|
248
323
|
}
|
|
249
324
|
};
|
|
325
|
+
const handleRotate = async (keyId) => {
|
|
326
|
+
setRotatingKeyId(keyId);
|
|
327
|
+
try {
|
|
328
|
+
const response = await onRotateKey(keyId);
|
|
329
|
+
setRotatedKeyValue(response.key);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
console.error("Failed to rotate key:", error);
|
|
332
|
+
} finally {
|
|
333
|
+
setRotatingKeyId(null);
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
const handleToggleBlock = async (key) => {
|
|
337
|
+
const keyId = key.token ?? key.key;
|
|
338
|
+
setBlockingKeyId(keyId);
|
|
339
|
+
try {
|
|
340
|
+
if (key.blocked) {
|
|
341
|
+
await onUnblockKey(keyId);
|
|
342
|
+
} else {
|
|
343
|
+
await onBlockKey(keyId);
|
|
344
|
+
}
|
|
345
|
+
} finally {
|
|
346
|
+
setBlockingKeyId(null);
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
const handleResetSpend = async () => {
|
|
350
|
+
if (!editingKey) return;
|
|
351
|
+
setResetSpendSubmitting(true);
|
|
352
|
+
try {
|
|
353
|
+
await onResetKeySpend(editingKey.token ?? editingKey.key);
|
|
354
|
+
setResetSpendConfirm(false);
|
|
355
|
+
handleCloseEdit();
|
|
356
|
+
} finally {
|
|
357
|
+
setResetSpendSubmitting(false);
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
const handleConfirmDelete = async () => {
|
|
361
|
+
if (!deleteConfirmId) return;
|
|
362
|
+
setDeleteSubmitting(true);
|
|
363
|
+
try {
|
|
364
|
+
await onDeleteKey(deleteConfirmId);
|
|
365
|
+
} finally {
|
|
366
|
+
setDeleteSubmitting(false);
|
|
367
|
+
setDeleteConfirmId(null);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
250
370
|
const copyToClipboard = (text) => {
|
|
251
371
|
navigator.clipboard.writeText(text);
|
|
252
372
|
};
|
|
253
|
-
|
|
373
|
+
const modelOption = (m) => {
|
|
374
|
+
const inCost = fmtCost(m.input_cost_per_token);
|
|
375
|
+
const outCost = fmtCost(m.output_cost_per_token);
|
|
376
|
+
return /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, null, /* @__PURE__ */ import_react2.default.createElement("span", null, m.model_name), m.supports_function_calling && " \u{1F527}", m.supports_vision && " \u{1F441}\uFE0F", (inCost || outCost) && /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, inCost, " in \xB7 ", outCost, " out"));
|
|
377
|
+
};
|
|
378
|
+
return /* @__PURE__ */ import_react2.default.createElement(import_react2.default.Fragment, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Paper, { sx: { mb: 2 } }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", justifyContent: "space-between", alignItems: "center", p: 2, gap: 2 }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "h6" }, "Virtual Keys"), /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", gap: 1, alignItems: "center", flex: 1, justifyContent: "flex-end" }, /* @__PURE__ */ import_react2.default.createElement(
|
|
379
|
+
import_material2.TextField,
|
|
380
|
+
{
|
|
381
|
+
size: "small",
|
|
382
|
+
placeholder: "Filter by alias or model\u2026",
|
|
383
|
+
value: filterText,
|
|
384
|
+
onChange: (e) => setFilterText(e.target.value),
|
|
385
|
+
sx: { minWidth: 240 },
|
|
386
|
+
InputProps: {
|
|
387
|
+
startAdornment: /* @__PURE__ */ import_react2.default.createElement(import_material2.InputAdornment, { position: "start" }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Search, { fontSize: "small" }))
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
), /* @__PURE__ */ import_react2.default.createElement(
|
|
254
391
|
import_material2.Button,
|
|
255
392
|
{
|
|
256
393
|
variant: "contained",
|
|
@@ -259,9 +396,11 @@ var init_KeysTable = __esm({
|
|
|
259
396
|
onClick: () => setGenerateModalOpen(true)
|
|
260
397
|
},
|
|
261
398
|
"Generate New Key"
|
|
262
|
-
)), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableContainer, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Table, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableHead, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Alias"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Key ID"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Created"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "
|
|
399
|
+
))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableContainer, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Table, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableHead, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Alias"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Key ID"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Created"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Expires"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Budget"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "TPM / RPM"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Models"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { align: "right" }, "Actions"))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableBody, null, loading ? /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 24 }))) : filteredKeys.length === 0 ? /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { color: "text.secondary" }, filterText ? "No keys match filter" : "No keys found"))) : filteredKeys.map((key) => {
|
|
263
400
|
const keyId = key.token ?? key.key;
|
|
264
|
-
|
|
401
|
+
const isRotating = rotatingKeyId === keyId;
|
|
402
|
+
const isBlocking = blockingKeyId === keyId;
|
|
403
|
+
return /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, { key: keyId, sx: key.blocked ? { bgcolor: "action.disabledBackground" } : void 0 }, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", alignItems: "center", gap: 0.5 }, key.key_alias || "-", key.blocked && /* @__PURE__ */ import_react2.default.createElement(import_material2.Chip, { label: "Blocked", color: "error", size: "small" }))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", alignItems: "center", gap: 0.5 }, /* @__PURE__ */ import_react2.default.createElement(
|
|
265
404
|
import_material2.Typography,
|
|
266
405
|
{
|
|
267
406
|
variant: "body2",
|
|
@@ -277,7 +416,32 @@ var init_KeysTable = __esm({
|
|
|
277
416
|
}
|
|
278
417
|
},
|
|
279
418
|
shortKeyId(keyId)
|
|
280
|
-
), /* @__PURE__ */ import_react2.default.createElement(import_material2.IconButton, { size: "small", onClick: () => copyToClipboard(keyId), title: "Copy Key ID" }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material.ContentCopy, { fontSize: "small" })))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, formatDate(key.created_at)), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null,
|
|
419
|
+
), /* @__PURE__ */ import_react2.default.createElement(import_material2.IconButton, { size: "small", onClick: () => copyToClipboard(keyId), title: "Copy Key ID" }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material.ContentCopy, { fontSize: "small" })))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, formatDate(key.created_at)), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(ExpiryChip, { expiresAt: key.expires_at })), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(BudgetCell, { spend: key.spend ?? 0, maxBudget: key.max_budget })), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2" }, key.tpm_limit ?? "-", " / ", key.rpm_limit ?? "-")), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", gap: 0.5, flexWrap: "wrap" }, key.models?.slice(0, 2).map((model) => /* @__PURE__ */ import_react2.default.createElement(import_material2.Chip, { key: model, label: model, size: "small" })), (key.models?.length || 0) > 2 && /* @__PURE__ */ import_react2.default.createElement(import_material2.Chip, { label: `+${(key.models?.length || 0) - 2}`, size: "small", variant: "outlined" }))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { align: "right" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.IconButton, { onClick: () => handleOpenEdit(key), title: "Edit key" }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Edit, { fontSize: "small" })), /* @__PURE__ */ import_react2.default.createElement(
|
|
420
|
+
import_material2.IconButton,
|
|
421
|
+
{
|
|
422
|
+
onClick: () => handleRotate(keyId),
|
|
423
|
+
disabled: isRotating,
|
|
424
|
+
title: "Rotate key \u2014 generates a new secret, same settings"
|
|
425
|
+
},
|
|
426
|
+
isRotating ? /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 18 }) : /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Autorenew, { fontSize: "small" })
|
|
427
|
+
), /* @__PURE__ */ import_react2.default.createElement(
|
|
428
|
+
import_material2.IconButton,
|
|
429
|
+
{
|
|
430
|
+
onClick: () => handleToggleBlock(key),
|
|
431
|
+
disabled: isBlocking,
|
|
432
|
+
color: key.blocked ? "warning" : "default",
|
|
433
|
+
title: key.blocked ? "Unblock key" : "Block key \u2014 suspends without revoking"
|
|
434
|
+
},
|
|
435
|
+
isBlocking ? /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 18 }) : key.blocked ? /* @__PURE__ */ import_react2.default.createElement(import_icons_material.LockOpen, { fontSize: "small" }) : /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Lock, { fontSize: "small" })
|
|
436
|
+
), /* @__PURE__ */ import_react2.default.createElement(
|
|
437
|
+
import_material2.IconButton,
|
|
438
|
+
{
|
|
439
|
+
color: "error",
|
|
440
|
+
onClick: () => setDeleteConfirmId(keyId),
|
|
441
|
+
title: "Revoke key"
|
|
442
|
+
},
|
|
443
|
+
/* @__PURE__ */ import_react2.default.createElement(import_icons_material.Delete, null)
|
|
444
|
+
)));
|
|
281
445
|
}))))), /* @__PURE__ */ import_react2.default.createElement(import_material2.Dialog, { open: generateModalOpen, onClose: handleCloseModal, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogTitle, null, newKeyValue ? "Key Generated" : "Generate New Key"), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogContent, null, newKeyValue ? /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2", color: "text.secondary", gutterBottom: true }, "Copy this key now. You won't be able to see it again."), /* @__PURE__ */ import_react2.default.createElement(
|
|
282
446
|
import_material2.Box,
|
|
283
447
|
{
|
|
@@ -352,7 +516,7 @@ var init_KeysTable = __esm({
|
|
|
352
516
|
getOptionLabel: (m) => m.model_name,
|
|
353
517
|
value: selectedModels,
|
|
354
518
|
onChange: (_e, selected) => setFormData({ ...formData, models: selected.map((m) => m.model_name) }),
|
|
355
|
-
renderOption: (props, m) => /* @__PURE__ */ import_react2.default.createElement("li", { ...props }, m
|
|
519
|
+
renderOption: (props, m) => /* @__PURE__ */ import_react2.default.createElement("li", { ...props }, modelOption(m)),
|
|
356
520
|
renderInput: (params) => /* @__PURE__ */ import_react2.default.createElement(import_material2.TextField, { ...params, label: "Models", helperText: "Leave empty to allow all models", fullWidth: true })
|
|
357
521
|
}
|
|
358
522
|
), /* @__PURE__ */ import_react2.default.createElement(
|
|
@@ -374,13 +538,22 @@ var init_KeysTable = __esm({
|
|
|
374
538
|
onChange: (e) => setFormData({ ...formData, tpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
|
|
375
539
|
fullWidth: true
|
|
376
540
|
}
|
|
541
|
+
), /* @__PURE__ */ import_react2.default.createElement(
|
|
542
|
+
import_material2.TextField,
|
|
543
|
+
{
|
|
544
|
+
label: "RPM Limit",
|
|
545
|
+
type: "number",
|
|
546
|
+
value: formData.rpm_limit ?? "",
|
|
547
|
+
onChange: (e) => setFormData({ ...formData, rpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
|
|
548
|
+
fullWidth: true
|
|
549
|
+
}
|
|
377
550
|
))), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogActions, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: handleCloseModal }, newKeyValue ? "Done" : "Cancel"), !newKeyValue && /* @__PURE__ */ import_react2.default.createElement(
|
|
378
551
|
import_material2.Button,
|
|
379
552
|
{
|
|
380
553
|
onClick: handleGenerate,
|
|
381
554
|
variant: "contained",
|
|
382
555
|
color: "primary",
|
|
383
|
-
disabled: submitting
|
|
556
|
+
disabled: submitting
|
|
384
557
|
},
|
|
385
558
|
submitting ? /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 24 }) : "Generate"
|
|
386
559
|
), newKeyValue && /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: handleCloseModal, variant: "contained", color: "success" }, "Done"))), /* @__PURE__ */ import_react2.default.createElement(import_material2.Dialog, { open: !!editingKey, onClose: handleCloseEdit, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogTitle, null, "Edit Key"), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogContent, null, editingKey && /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2", color: "text.secondary" }, /* @__PURE__ */ import_react2.default.createElement("code", { style: { fontFamily: "monospace", color: "inherit" } }, maskKey(editingKey.key))), /* @__PURE__ */ import_react2.default.createElement(
|
|
@@ -400,6 +573,7 @@ var init_KeysTable = __esm({
|
|
|
400
573
|
getOptionLabel: (m) => m.model_name,
|
|
401
574
|
value: editSelectedModels,
|
|
402
575
|
onChange: (_e, selected) => setEditForm({ ...editForm, models: selected.map((m) => m.model_name) }),
|
|
576
|
+
renderOption: (props, m) => /* @__PURE__ */ import_react2.default.createElement("li", { ...props }, modelOption(m)),
|
|
403
577
|
renderInput: (params) => /* @__PURE__ */ import_react2.default.createElement(import_material2.TextField, { ...params, label: "Models", fullWidth: true })
|
|
404
578
|
}
|
|
405
579
|
), /* @__PURE__ */ import_react2.default.createElement(
|
|
@@ -429,7 +603,54 @@ var init_KeysTable = __esm({
|
|
|
429
603
|
onChange: (e) => setEditForm({ ...editForm, rpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
|
|
430
604
|
fullWidth: true
|
|
431
605
|
}
|
|
432
|
-
)
|
|
606
|
+
), /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { mt: 1, pt: 2, borderTop: "1px solid", sx: { borderColor: "divider" } }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "caption", color: "text.secondary", display: "block", mb: 1 }, "Danger Zone"), !resetSpendConfirm ? /* @__PURE__ */ import_react2.default.createElement(
|
|
607
|
+
import_material2.Button,
|
|
608
|
+
{
|
|
609
|
+
size: "small",
|
|
610
|
+
color: "warning",
|
|
611
|
+
variant: "outlined",
|
|
612
|
+
onClick: () => setResetSpendConfirm(true)
|
|
613
|
+
},
|
|
614
|
+
"Reset Spend to $0"
|
|
615
|
+
) : /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2", color: "warning.main" }, "Zero out spend counter?"), /* @__PURE__ */ import_react2.default.createElement(
|
|
616
|
+
import_material2.Button,
|
|
617
|
+
{
|
|
618
|
+
size: "small",
|
|
619
|
+
color: "warning",
|
|
620
|
+
variant: "contained",
|
|
621
|
+
disabled: resetSpendSubmitting,
|
|
622
|
+
onClick: handleResetSpend
|
|
623
|
+
},
|
|
624
|
+
resetSpendSubmitting ? /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 16 }) : "Confirm"
|
|
625
|
+
), /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { size: "small", onClick: () => setResetSpendConfirm(false) }, "Cancel"))))), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogActions, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: handleCloseEdit }, "Cancel"), /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: handleUpdate, variant: "contained", color: "primary", disabled: editSubmitting }, editSubmitting ? /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 24 }) : "Save"))), /* @__PURE__ */ import_react2.default.createElement(import_material2.Dialog, { open: !!rotatedKeyValue, onClose: () => setRotatedKeyValue(null), maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogTitle, null, "Key Rotated"), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogContent, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2", color: "text.secondary", gutterBottom: true }, "The old secret is now invalid. Copy the new one \u2014 you won't see it again."), /* @__PURE__ */ import_react2.default.createElement(
|
|
626
|
+
import_material2.Box,
|
|
627
|
+
{
|
|
628
|
+
display: "flex",
|
|
629
|
+
alignItems: "center",
|
|
630
|
+
gap: 1,
|
|
631
|
+
mt: 2,
|
|
632
|
+
p: 2,
|
|
633
|
+
sx: { backgroundColor: "action.hover", border: "1px solid", borderColor: "divider", borderRadius: 1 }
|
|
634
|
+
},
|
|
635
|
+
/* @__PURE__ */ import_react2.default.createElement(
|
|
636
|
+
import_material2.Typography,
|
|
637
|
+
{
|
|
638
|
+
component: "code",
|
|
639
|
+
sx: { fontFamily: "monospace", wordBreak: "break-all", flex: 1 }
|
|
640
|
+
},
|
|
641
|
+
rotatedKeyValue
|
|
642
|
+
),
|
|
643
|
+
/* @__PURE__ */ import_react2.default.createElement(import_material2.IconButton, { onClick: () => copyToClipboard(rotatedKeyValue) }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material.ContentCopy, null))
|
|
644
|
+
)), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogActions, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: () => setRotatedKeyValue(null), variant: "contained", color: "success" }, "Done"))), /* @__PURE__ */ import_react2.default.createElement(import_material2.Dialog, { open: !!deleteConfirmId, onClose: () => setDeleteConfirmId(null), maxWidth: "xs", fullWidth: true }, /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogTitle, null, "Revoke Key?"), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogContent, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, null, "This will permanently revoke the key. Any integrations using it will stop working immediately.")), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogActions, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: () => setDeleteConfirmId(null), disabled: deleteSubmitting }, "Cancel"), /* @__PURE__ */ import_react2.default.createElement(
|
|
645
|
+
import_material2.Button,
|
|
646
|
+
{
|
|
647
|
+
onClick: handleConfirmDelete,
|
|
648
|
+
variant: "contained",
|
|
649
|
+
color: "error",
|
|
650
|
+
disabled: deleteSubmitting
|
|
651
|
+
},
|
|
652
|
+
deleteSubmitting ? /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 20 }) : "Revoke"
|
|
653
|
+
))));
|
|
433
654
|
};
|
|
434
655
|
}
|
|
435
656
|
});
|
|
@@ -703,6 +924,121 @@ var init_TeamUsage = __esm({
|
|
|
703
924
|
}
|
|
704
925
|
});
|
|
705
926
|
|
|
927
|
+
// src/components/AuditLog.tsx
|
|
928
|
+
function actionColor(action) {
|
|
929
|
+
if (!action) return "default";
|
|
930
|
+
for (const [key, color] of Object.entries(ACTION_COLORS)) {
|
|
931
|
+
if (action.toLowerCase().includes(key)) return color;
|
|
932
|
+
}
|
|
933
|
+
return "info";
|
|
934
|
+
}
|
|
935
|
+
function formatDateTime(iso) {
|
|
936
|
+
try {
|
|
937
|
+
return new Date(iso).toLocaleString();
|
|
938
|
+
} catch {
|
|
939
|
+
return iso;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
var import_react5, import_material5, import_icons_material3, import_react_use, ACTION_COLORS, DetailRow, AuditLog;
|
|
943
|
+
var init_AuditLog = __esm({
|
|
944
|
+
"src/components/AuditLog.tsx"() {
|
|
945
|
+
"use strict";
|
|
946
|
+
import_react5 = __toESM(require("react"));
|
|
947
|
+
import_material5 = require("@mui/material");
|
|
948
|
+
import_icons_material3 = require("@mui/icons-material");
|
|
949
|
+
import_react_use = require("react-use");
|
|
950
|
+
ACTION_COLORS = {
|
|
951
|
+
created: "success",
|
|
952
|
+
deleted: "error",
|
|
953
|
+
updated: "warning",
|
|
954
|
+
blocked: "error",
|
|
955
|
+
unblocked: "success"
|
|
956
|
+
};
|
|
957
|
+
DetailRow = ({ entry }) => {
|
|
958
|
+
const [open, setOpen] = (0, import_react5.useState)(false);
|
|
959
|
+
const hasDetail = entry.before_value || entry.updated_values;
|
|
960
|
+
return /* @__PURE__ */ import_react5.default.createElement(import_react5.default.Fragment, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.TableRow, { hover: true }, /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, { sx: { width: 40, pr: 0 } }, hasDetail && /* @__PURE__ */ import_react5.default.createElement(import_material5.IconButton, { size: "small", onClick: () => setOpen((o) => !o) }, open ? /* @__PURE__ */ import_react5.default.createElement(import_icons_material3.KeyboardArrowUp, { fontSize: "small" }) : /* @__PURE__ */ import_react5.default.createElement(import_icons_material3.KeyboardArrowDown, { fontSize: "small" }))), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, { sx: { whiteSpace: "nowrap" } }, formatDateTime(entry.updated_at)), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, entry.action && /* @__PURE__ */ import_react5.default.createElement(import_material5.Chip, { label: entry.action, color: actionColor(entry.action), size: "small" })), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, entry.table_name ?? "-"), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "body2", component: "code", sx: { fontFamily: "monospace", fontSize: "0.75rem" } }, entry.object_id ? entry.object_id.slice(0, 20) + (entry.object_id.length > 20 ? "\u2026" : "") : "-")), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, entry.changed_by ?? "-")), hasDetail && /* @__PURE__ */ import_react5.default.createElement(import_material5.TableRow, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, { colSpan: 6, sx: { py: 0 } }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Collapse, { in: open, unmountOnExit: true }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { p: 2, display: "flex", gap: 2, flexWrap: "wrap" }, entry.before_value && /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { flex: 1, minWidth: 200 }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "caption", color: "text.secondary", display: "block", mb: 0.5 }, "Before"), /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { component: "pre", variant: "caption", sx: { fontFamily: "monospace", whiteSpace: "pre-wrap", wordBreak: "break-all" } }, JSON.stringify(entry.before_value, null, 2))), entry.updated_values && /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { flex: 1, minWidth: 200 }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "caption", color: "text.secondary", display: "block", mb: 0.5 }, "After"), /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { component: "pre", variant: "caption", sx: { fontFamily: "monospace", whiteSpace: "pre-wrap", wordBreak: "break-all" } }, JSON.stringify(entry.updated_values, null, 2))))))));
|
|
961
|
+
};
|
|
962
|
+
AuditLog = ({ api }) => {
|
|
963
|
+
const [page, setPage] = (0, import_react5.useState)(0);
|
|
964
|
+
const [pageSize, setPageSize] = (0, import_react5.useState)(25);
|
|
965
|
+
const [filters, setFilters] = (0, import_react5.useState)({});
|
|
966
|
+
const fetchParams = (0, import_react5.useCallback)(
|
|
967
|
+
() => ({ page: page + 1, page_size: pageSize, ...filters }),
|
|
968
|
+
[page, pageSize, filters]
|
|
969
|
+
)();
|
|
970
|
+
const { value, loading, error } = (0, import_react_use.useAsync)(
|
|
971
|
+
() => api.getAuditLogs(fetchParams),
|
|
972
|
+
[api, fetchParams]
|
|
973
|
+
);
|
|
974
|
+
const entries = value?.audit_logs ?? [];
|
|
975
|
+
const total = value?.total ?? 0;
|
|
976
|
+
return /* @__PURE__ */ import_react5.default.createElement(import_material5.Paper, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { p: 2, display: "flex", gap: 2, flexWrap: "wrap", alignItems: "center" }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { variant: "h6", sx: { flex: "0 0 auto", mr: 1 } }, "Audit Log"), /* @__PURE__ */ import_react5.default.createElement(
|
|
977
|
+
import_material5.TextField,
|
|
978
|
+
{
|
|
979
|
+
size: "small",
|
|
980
|
+
label: "Action",
|
|
981
|
+
select: true,
|
|
982
|
+
value: filters.action ?? "",
|
|
983
|
+
onChange: (e) => {
|
|
984
|
+
setFilters((f) => ({ ...f, action: e.target.value || void 0 }));
|
|
985
|
+
setPage(0);
|
|
986
|
+
},
|
|
987
|
+
sx: { minWidth: 160 }
|
|
988
|
+
},
|
|
989
|
+
/* @__PURE__ */ import_react5.default.createElement(import_material5.MenuItem, { value: "" }, "All actions"),
|
|
990
|
+
/* @__PURE__ */ import_react5.default.createElement(import_material5.MenuItem, { value: "created" }, "Created"),
|
|
991
|
+
/* @__PURE__ */ import_react5.default.createElement(import_material5.MenuItem, { value: "updated" }, "Updated"),
|
|
992
|
+
/* @__PURE__ */ import_react5.default.createElement(import_material5.MenuItem, { value: "deleted" }, "Deleted"),
|
|
993
|
+
/* @__PURE__ */ import_react5.default.createElement(import_material5.MenuItem, { value: "blocked" }, "Blocked")
|
|
994
|
+
), /* @__PURE__ */ import_react5.default.createElement(
|
|
995
|
+
import_material5.TextField,
|
|
996
|
+
{
|
|
997
|
+
size: "small",
|
|
998
|
+
label: "Table",
|
|
999
|
+
select: true,
|
|
1000
|
+
value: filters.table_name ?? "",
|
|
1001
|
+
onChange: (e) => {
|
|
1002
|
+
setFilters((f) => ({ ...f, table_name: e.target.value || void 0 }));
|
|
1003
|
+
setPage(0);
|
|
1004
|
+
},
|
|
1005
|
+
sx: { minWidth: 160 }
|
|
1006
|
+
},
|
|
1007
|
+
/* @__PURE__ */ import_react5.default.createElement(import_material5.MenuItem, { value: "" }, "All tables"),
|
|
1008
|
+
/* @__PURE__ */ import_react5.default.createElement(import_material5.MenuItem, { value: "LiteLLM_VerificationToken" }, "Key"),
|
|
1009
|
+
/* @__PURE__ */ import_react5.default.createElement(import_material5.MenuItem, { value: "LiteLLM_TeamTable" }, "Team"),
|
|
1010
|
+
/* @__PURE__ */ import_react5.default.createElement(import_material5.MenuItem, { value: "LiteLLM_UserTable" }, "User")
|
|
1011
|
+
), /* @__PURE__ */ import_react5.default.createElement(
|
|
1012
|
+
import_material5.TextField,
|
|
1013
|
+
{
|
|
1014
|
+
size: "small",
|
|
1015
|
+
label: "Changed by",
|
|
1016
|
+
value: filters.changed_by ?? "",
|
|
1017
|
+
onChange: (e) => {
|
|
1018
|
+
setFilters((f) => ({ ...f, changed_by: e.target.value || void 0 }));
|
|
1019
|
+
setPage(0);
|
|
1020
|
+
},
|
|
1021
|
+
sx: { minWidth: 200 }
|
|
1022
|
+
}
|
|
1023
|
+
)), error && /* @__PURE__ */ import_react5.default.createElement(import_material5.Box, { px: 2, pb: 2 }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Alert, { severity: "error" }, error.message)), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableContainer, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.Table, { size: "small" }, /* @__PURE__ */ import_react5.default.createElement(import_material5.TableHead, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.TableRow, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, { sx: { width: 40 } }), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, "Time"), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, "Action"), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, "Table"), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, "Object ID"), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, null, "Changed By"))), /* @__PURE__ */ import_react5.default.createElement(import_material5.TableBody, null, loading ? /* @__PURE__ */ import_react5.default.createElement(import_material5.TableRow, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, { colSpan: 6, align: "center", sx: { py: 4 } }, /* @__PURE__ */ import_react5.default.createElement(import_material5.CircularProgress, { size: 24 }))) : entries.length === 0 ? /* @__PURE__ */ import_react5.default.createElement(import_material5.TableRow, null, /* @__PURE__ */ import_react5.default.createElement(import_material5.TableCell, { colSpan: 6, align: "center", sx: { py: 4 } }, /* @__PURE__ */ import_react5.default.createElement(import_material5.Typography, { color: "text.secondary" }, "No audit events found"))) : entries.map((entry) => /* @__PURE__ */ import_react5.default.createElement(DetailRow, { key: entry.id, entry }))))), /* @__PURE__ */ import_react5.default.createElement(
|
|
1024
|
+
import_material5.TablePagination,
|
|
1025
|
+
{
|
|
1026
|
+
component: "div",
|
|
1027
|
+
count: total,
|
|
1028
|
+
page,
|
|
1029
|
+
onPageChange: (_, p) => setPage(p),
|
|
1030
|
+
rowsPerPage: pageSize,
|
|
1031
|
+
onRowsPerPageChange: (e) => {
|
|
1032
|
+
setPageSize(Number(e.target.value));
|
|
1033
|
+
setPage(0);
|
|
1034
|
+
},
|
|
1035
|
+
rowsPerPageOptions: [10, 25, 50]
|
|
1036
|
+
}
|
|
1037
|
+
));
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
|
|
706
1042
|
// src/components/LiteLLMPage.tsx
|
|
707
1043
|
var LiteLLMPage_exports = {};
|
|
708
1044
|
__export(LiteLLMPage_exports, {
|
|
@@ -722,31 +1058,33 @@ function initDateRange() {
|
|
|
722
1058
|
else start.setDate(start.getDate() - 7);
|
|
723
1059
|
return { start, end };
|
|
724
1060
|
}
|
|
725
|
-
var
|
|
1061
|
+
var import_react6, import_material6, import_react_use2, import_core_plugin_api2, PERIOD_LS_KEY2, LiteLLMPage;
|
|
726
1062
|
var init_LiteLLMPage = __esm({
|
|
727
1063
|
"src/components/LiteLLMPage.tsx"() {
|
|
728
1064
|
"use strict";
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
1065
|
+
import_react6 = __toESM(require("react"));
|
|
1066
|
+
import_material6 = require("@mui/material");
|
|
1067
|
+
import_react_use2 = require("react-use");
|
|
732
1068
|
import_core_plugin_api2 = require("@backstage/core-plugin-api");
|
|
733
1069
|
init_DashboardHeader();
|
|
734
1070
|
init_KeysTable();
|
|
735
1071
|
init_UsageStats();
|
|
736
1072
|
init_TeamUsage();
|
|
1073
|
+
init_AuditLog();
|
|
737
1074
|
init_api();
|
|
738
1075
|
PERIOD_LS_KEY2 = "litellm_usage_period";
|
|
739
1076
|
LiteLLMPage = () => {
|
|
740
1077
|
const api = (0, import_core_plugin_api2.useApi)(liteLlmApiRef);
|
|
741
|
-
const [dateRange, setDateRange] = (0,
|
|
742
|
-
const [
|
|
743
|
-
const [
|
|
744
|
-
const [
|
|
745
|
-
const
|
|
1078
|
+
const [dateRange, setDateRange] = (0, import_react6.useState)(initDateRange);
|
|
1079
|
+
const [activeTab, setActiveTab] = (0, import_react6.useState)("overview");
|
|
1080
|
+
const [snackbar, setSnackbar] = (0, import_react6.useState)(null);
|
|
1081
|
+
const [teamUsageCache, setTeamUsageCache] = (0, import_react6.useState)({});
|
|
1082
|
+
const [teamUsageLoading, setTeamUsageLoading] = (0, import_react6.useState)({});
|
|
1083
|
+
const { value: userInfo, loading: userLoading, error: userError } = (0, import_react_use2.useAsync)(
|
|
746
1084
|
() => api.getUserInfo(),
|
|
747
1085
|
[api]
|
|
748
1086
|
);
|
|
749
|
-
const { value: keys, loading: keysLoading, retry: refreshKeys } = (0,
|
|
1087
|
+
const { value: keys, loading: keysLoading, retry: refreshKeys } = (0, import_react_use2.useAsyncRetry)(
|
|
750
1088
|
async () => {
|
|
751
1089
|
try {
|
|
752
1090
|
return await api.listKeys();
|
|
@@ -757,15 +1095,15 @@ var init_LiteLLMPage = __esm({
|
|
|
757
1095
|
},
|
|
758
1096
|
[api]
|
|
759
1097
|
);
|
|
760
|
-
const { value: allModels, loading: modelsLoading } = (0,
|
|
1098
|
+
const { value: allModels, loading: modelsLoading } = (0, import_react_use2.useAsync)(
|
|
761
1099
|
() => api.listModels().catch(() => []),
|
|
762
1100
|
[api]
|
|
763
1101
|
);
|
|
764
|
-
const { value: allTeams, loading: teamsLoading } = (0,
|
|
1102
|
+
const { value: allTeams, loading: teamsLoading } = (0, import_react_use2.useAsync)(
|
|
765
1103
|
() => api.getTeams().catch(() => []),
|
|
766
1104
|
[api]
|
|
767
1105
|
);
|
|
768
|
-
const teams = (0,
|
|
1106
|
+
const teams = (0, import_react6.useMemo)(() => {
|
|
769
1107
|
if (!allTeams?.length) return [];
|
|
770
1108
|
if (!userInfo) return allTeams;
|
|
771
1109
|
const userId = userInfo.user_id;
|
|
@@ -777,12 +1115,17 @@ var init_LiteLLMPage = __esm({
|
|
|
777
1115
|
);
|
|
778
1116
|
return byMembership.length > 0 ? byMembership : allTeams;
|
|
779
1117
|
}, [allTeams, userInfo]);
|
|
780
|
-
const { value: usage, loading: usageLoading } = (0,
|
|
1118
|
+
const { value: usage, loading: usageLoading } = (0, import_react_use2.useAsync)(async () => {
|
|
781
1119
|
const startDate = dateRange.start.toISOString().split("T")[0];
|
|
782
1120
|
const endDate = dateRange.end.toISOString().split("T")[0];
|
|
783
1121
|
return api.getUsage(startDate, endDate);
|
|
784
1122
|
}, [api, dateRange]);
|
|
785
|
-
const
|
|
1123
|
+
const handleDateRangeChange = (0, import_react6.useCallback)((range) => {
|
|
1124
|
+
setDateRange(range);
|
|
1125
|
+
setTeamUsageCache({});
|
|
1126
|
+
setTeamUsageLoading({});
|
|
1127
|
+
}, [setDateRange]);
|
|
1128
|
+
const loadTeamUsage = (0, import_react6.useCallback)(async (teamId) => {
|
|
786
1129
|
if (teamUsageCache[teamId] !== void 0 || teamUsageLoading[teamId]) return;
|
|
787
1130
|
setTeamUsageLoading((prev) => ({ ...prev, [teamId]: true }));
|
|
788
1131
|
try {
|
|
@@ -796,7 +1139,7 @@ var init_LiteLLMPage = __esm({
|
|
|
796
1139
|
setTeamUsageLoading((prev) => ({ ...prev, [teamId]: false }));
|
|
797
1140
|
}
|
|
798
1141
|
}, [api, dateRange, teamUsageCache, teamUsageLoading]);
|
|
799
|
-
const allowedModels = (0,
|
|
1142
|
+
const allowedModels = (0, import_react6.useMemo)(() => {
|
|
800
1143
|
if (!allModels?.length) return [];
|
|
801
1144
|
const userModels = userInfo?.models;
|
|
802
1145
|
const teamModels = teams?.flatMap((t) => t.models ?? []);
|
|
@@ -809,7 +1152,7 @@ var init_LiteLLMPage = __esm({
|
|
|
809
1152
|
]);
|
|
810
1153
|
return allModels.filter((m) => allowed.has(m.model_name));
|
|
811
1154
|
}, [allModels, userInfo, teams]);
|
|
812
|
-
const handleGenerateKey = (0,
|
|
1155
|
+
const handleGenerateKey = (0, import_react6.useCallback)(
|
|
813
1156
|
async (request) => {
|
|
814
1157
|
const response = await api.generateKey(request);
|
|
815
1158
|
setSnackbar({ message: "Key generated successfully", severity: "success" });
|
|
@@ -818,7 +1161,7 @@ var init_LiteLLMPage = __esm({
|
|
|
818
1161
|
},
|
|
819
1162
|
[api, refreshKeys]
|
|
820
1163
|
);
|
|
821
|
-
const handleUpdateKey = (0,
|
|
1164
|
+
const handleUpdateKey = (0, import_react6.useCallback)(
|
|
822
1165
|
async (keyId, request) => {
|
|
823
1166
|
try {
|
|
824
1167
|
await api.updateKey(keyId, request);
|
|
@@ -831,44 +1174,87 @@ var init_LiteLLMPage = __esm({
|
|
|
831
1174
|
},
|
|
832
1175
|
[api, refreshKeys]
|
|
833
1176
|
);
|
|
834
|
-
const
|
|
1177
|
+
const handleBlockKey = (0, import_react6.useCallback)(
|
|
1178
|
+
async (keyId) => {
|
|
1179
|
+
try {
|
|
1180
|
+
await api.blockKey(keyId);
|
|
1181
|
+
setSnackbar({ message: "Key blocked \u2014 requests will be rejected until unblocked", severity: "warning" });
|
|
1182
|
+
refreshKeys();
|
|
1183
|
+
} catch (e) {
|
|
1184
|
+
setSnackbar({ message: `Failed to block key: ${e.message}`, severity: "error" });
|
|
1185
|
+
}
|
|
1186
|
+
},
|
|
1187
|
+
[api, refreshKeys]
|
|
1188
|
+
);
|
|
1189
|
+
const handleUnblockKey = (0, import_react6.useCallback)(
|
|
1190
|
+
async (keyId) => {
|
|
1191
|
+
try {
|
|
1192
|
+
await api.unblockKey(keyId);
|
|
1193
|
+
setSnackbar({ message: "Key unblocked", severity: "success" });
|
|
1194
|
+
refreshKeys();
|
|
1195
|
+
} catch (e) {
|
|
1196
|
+
setSnackbar({ message: `Failed to unblock key: ${e.message}`, severity: "error" });
|
|
1197
|
+
}
|
|
1198
|
+
},
|
|
1199
|
+
[api, refreshKeys]
|
|
1200
|
+
);
|
|
1201
|
+
const handleResetKeySpend = (0, import_react6.useCallback)(
|
|
1202
|
+
async (keyId) => {
|
|
1203
|
+
try {
|
|
1204
|
+
await api.resetKeySpend(keyId);
|
|
1205
|
+
setSnackbar({ message: "Spend counter reset to $0", severity: "success" });
|
|
1206
|
+
refreshKeys();
|
|
1207
|
+
} catch (e) {
|
|
1208
|
+
setSnackbar({ message: `Failed to reset spend: ${e.message}`, severity: "error" });
|
|
1209
|
+
}
|
|
1210
|
+
},
|
|
1211
|
+
[api, refreshKeys]
|
|
1212
|
+
);
|
|
1213
|
+
const handleRotateKey = (0, import_react6.useCallback)(
|
|
1214
|
+
async (keyId) => {
|
|
1215
|
+
const response = await api.rotateKey(keyId);
|
|
1216
|
+
setSnackbar({ message: "Key rotated \u2014 copy the new secret now", severity: "success" });
|
|
1217
|
+
refreshKeys();
|
|
1218
|
+
return response;
|
|
1219
|
+
},
|
|
1220
|
+
[api, refreshKeys]
|
|
1221
|
+
);
|
|
1222
|
+
const handleDeleteKey = (0, import_react6.useCallback)(
|
|
835
1223
|
async (keyId) => {
|
|
836
1224
|
try {
|
|
837
1225
|
await api.deleteKey(keyId);
|
|
838
|
-
setSnackbar({
|
|
839
|
-
message: "Key revoked successfully",
|
|
840
|
-
severity: "success"
|
|
841
|
-
});
|
|
1226
|
+
setSnackbar({ message: "Key revoked successfully", severity: "success" });
|
|
842
1227
|
refreshKeys();
|
|
843
1228
|
} catch (e) {
|
|
844
1229
|
if (e.body?.success && (e.body?.message?.includes("already deleted") || e.body?.message?.includes("never existed"))) {
|
|
845
|
-
setSnackbar({
|
|
846
|
-
message: "Key was already deleted",
|
|
847
|
-
severity: "warning"
|
|
848
|
-
});
|
|
1230
|
+
setSnackbar({ message: "Key was already deleted", severity: "warning" });
|
|
849
1231
|
refreshKeys();
|
|
850
1232
|
return;
|
|
851
1233
|
}
|
|
852
|
-
setSnackbar({
|
|
853
|
-
message: `Failed to revoke key: ${e.message}`,
|
|
854
|
-
severity: "error"
|
|
855
|
-
});
|
|
856
|
-
} finally {
|
|
857
|
-
refreshKeys();
|
|
1234
|
+
setSnackbar({ message: `Failed to revoke key: ${e.message}`, severity: "error" });
|
|
858
1235
|
}
|
|
859
1236
|
},
|
|
860
1237
|
[api, refreshKeys]
|
|
861
1238
|
);
|
|
862
1239
|
const isInitialLoading = userLoading && !userInfo;
|
|
863
1240
|
if (isInitialLoading) {
|
|
864
|
-
return /* @__PURE__ */
|
|
1241
|
+
return /* @__PURE__ */ import_react6.default.createElement(import_material6.Box, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "50vh" }, /* @__PURE__ */ import_react6.default.createElement(import_material6.CircularProgress, null));
|
|
865
1242
|
}
|
|
866
1243
|
if (userError || !userInfo) {
|
|
867
1244
|
const isProvisioningEnabled = userError?.body?.provisioning === true;
|
|
868
1245
|
const hint = userError?.body?.hint;
|
|
869
|
-
return /* @__PURE__ */
|
|
1246
|
+
return /* @__PURE__ */ import_react6.default.createElement(import_material6.Box, { p: 3 }, /* @__PURE__ */ import_react6.default.createElement(import_material6.Paper, { sx: { p: 3 } }, /* @__PURE__ */ import_react6.default.createElement(import_material6.Typography, { variant: "h6", gutterBottom: true }, "Account not provisioned"), /* @__PURE__ */ import_react6.default.createElement(import_material6.Typography, { color: "text.secondary", paragraph: true }, "Your Backstage account is not linked to a LiteLLM user."), hint ? /* @__PURE__ */ import_react6.default.createElement(import_material6.Typography, { variant: "body2", color: "text.secondary" }, hint) : /* @__PURE__ */ import_react6.default.createElement(import_material6.Typography, { variant: "body2", color: "text.secondary" }, isProvisioningEnabled ? "Auto-provisioning is enabled but failed. Check the backend logs." : "Set litellm.provisioning.enabled: true in app-config.yaml to enable auto-provisioning, or ask your administrator to create the account manually.")));
|
|
870
1247
|
}
|
|
871
|
-
return /* @__PURE__ */
|
|
1248
|
+
return /* @__PURE__ */ import_react6.default.createElement(import_material6.Box, { p: 3 }, userInfo.can_view_audit && /* @__PURE__ */ import_react6.default.createElement(
|
|
1249
|
+
import_material6.Tabs,
|
|
1250
|
+
{
|
|
1251
|
+
value: activeTab,
|
|
1252
|
+
onChange: (_, v) => setActiveTab(v),
|
|
1253
|
+
sx: { mb: 2, borderBottom: 1, borderColor: "divider" }
|
|
1254
|
+
},
|
|
1255
|
+
/* @__PURE__ */ import_react6.default.createElement(import_material6.Tab, { label: "Overview", value: "overview" }),
|
|
1256
|
+
/* @__PURE__ */ import_react6.default.createElement(import_material6.Tab, { label: "Audit Log", value: "audit" })
|
|
1257
|
+
), activeTab === "audit" ? /* @__PURE__ */ import_react6.default.createElement(AuditLog, { api }) : /* @__PURE__ */ import_react6.default.createElement(import_material6.Grid, { container: true, spacing: 2 }, /* @__PURE__ */ import_react6.default.createElement(import_material6.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react6.default.createElement(DashboardHeader, { userInfo, teams: teams ?? [], loading: userLoading || teamsLoading })), /* @__PURE__ */ import_react6.default.createElement(import_material6.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react6.default.createElement(
|
|
872
1258
|
TeamUsage,
|
|
873
1259
|
{
|
|
874
1260
|
teams: teams ?? [],
|
|
@@ -880,7 +1266,17 @@ var init_LiteLLMPage = __esm({
|
|
|
880
1266
|
},
|
|
881
1267
|
getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false
|
|
882
1268
|
}
|
|
883
|
-
)), /* @__PURE__ */
|
|
1269
|
+
)), /* @__PURE__ */ import_react6.default.createElement(import_material6.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react6.default.createElement(
|
|
1270
|
+
UsageStats,
|
|
1271
|
+
{
|
|
1272
|
+
usage: usage ?? null,
|
|
1273
|
+
models: allModels ?? [],
|
|
1274
|
+
dateRange,
|
|
1275
|
+
onDateRangeChange: handleDateRangeChange,
|
|
1276
|
+
loading: usageLoading,
|
|
1277
|
+
userInfo
|
|
1278
|
+
}
|
|
1279
|
+
)), /* @__PURE__ */ import_react6.default.createElement(import_material6.Grid, { item: true, xs: 12 }, /* @__PURE__ */ import_react6.default.createElement(
|
|
884
1280
|
KeysTable,
|
|
885
1281
|
{
|
|
886
1282
|
keys: keys ?? [],
|
|
@@ -889,27 +1285,21 @@ var init_LiteLLMPage = __esm({
|
|
|
889
1285
|
loading: keysLoading || modelsLoading,
|
|
890
1286
|
onGenerateKey: handleGenerateKey,
|
|
891
1287
|
onUpdateKey: handleUpdateKey,
|
|
1288
|
+
onRotateKey: handleRotateKey,
|
|
1289
|
+
onBlockKey: handleBlockKey,
|
|
1290
|
+
onUnblockKey: handleUnblockKey,
|
|
1291
|
+
onResetKeySpend: handleResetKeySpend,
|
|
892
1292
|
onDeleteKey: handleDeleteKey
|
|
893
1293
|
}
|
|
894
|
-
)), /* @__PURE__ */
|
|
895
|
-
|
|
896
|
-
{
|
|
897
|
-
usage: usage ?? null,
|
|
898
|
-
models: allModels ?? [],
|
|
899
|
-
dateRange,
|
|
900
|
-
onDateRangeChange: setDateRange,
|
|
901
|
-
loading: usageLoading,
|
|
902
|
-
userInfo
|
|
903
|
-
}
|
|
904
|
-
))), /* @__PURE__ */ import_react5.default.createElement(
|
|
905
|
-
import_material5.Snackbar,
|
|
1294
|
+
))), /* @__PURE__ */ import_react6.default.createElement(
|
|
1295
|
+
import_material6.Snackbar,
|
|
906
1296
|
{
|
|
907
1297
|
open: !!snackbar,
|
|
908
1298
|
autoHideDuration: 5e3,
|
|
909
1299
|
onClose: () => setSnackbar(null),
|
|
910
1300
|
anchorOrigin: { vertical: "bottom", horizontal: "right" }
|
|
911
1301
|
},
|
|
912
|
-
snackbar ? /* @__PURE__ */
|
|
1302
|
+
snackbar ? /* @__PURE__ */ import_react6.default.createElement(import_material6.Alert, { severity: snackbar.severity, onClose: () => setSnackbar(null) }, snackbar.message) : void 0
|
|
913
1303
|
));
|
|
914
1304
|
};
|
|
915
1305
|
}
|
|
@@ -931,8 +1321,8 @@ __export(index_exports, {
|
|
|
931
1321
|
module.exports = __toCommonJS(index_exports);
|
|
932
1322
|
|
|
933
1323
|
// src/plugin.tsx
|
|
934
|
-
var
|
|
935
|
-
var
|
|
1324
|
+
var import_react7 = __toESM(require("react"));
|
|
1325
|
+
var import_icons_material4 = require("@mui/icons-material");
|
|
936
1326
|
var import_frontend_plugin_api = require("@backstage/frontend-plugin-api");
|
|
937
1327
|
init_api();
|
|
938
1328
|
var liteLlmApi = import_frontend_plugin_api.ApiBlueprint.make({
|
|
@@ -946,10 +1336,10 @@ var liteLlmPage = import_frontend_plugin_api.PageBlueprint.make({
|
|
|
946
1336
|
params: {
|
|
947
1337
|
path: "/litellm",
|
|
948
1338
|
title: "LiteLLM",
|
|
949
|
-
icon: /* @__PURE__ */
|
|
1339
|
+
icon: /* @__PURE__ */ import_react7.default.createElement(import_icons_material4.TrendingUp, null),
|
|
950
1340
|
loader: async () => {
|
|
951
1341
|
const { LiteLLMPage: LiteLLMPage2 } = await Promise.resolve().then(() => (init_LiteLLMPage(), LiteLLMPage_exports));
|
|
952
|
-
return /* @__PURE__ */
|
|
1342
|
+
return /* @__PURE__ */ import_react7.default.createElement(LiteLLMPage2, null);
|
|
953
1343
|
}
|
|
954
1344
|
}
|
|
955
1345
|
});
|
|
@@ -966,8 +1356,8 @@ init_UsageStats();
|
|
|
966
1356
|
init_TeamUsage();
|
|
967
1357
|
|
|
968
1358
|
// src/components/LiteLLMHomeWidget.tsx
|
|
969
|
-
var
|
|
970
|
-
var
|
|
1359
|
+
var import_react8 = __toESM(require("react"));
|
|
1360
|
+
var import_material7 = require("@mui/material");
|
|
971
1361
|
var import_recharts3 = require("recharts");
|
|
972
1362
|
var import_core_plugin_api3 = require("@backstage/core-plugin-api");
|
|
973
1363
|
init_api();
|
|
@@ -985,18 +1375,18 @@ function presetToDateRange(preset) {
|
|
|
985
1375
|
}
|
|
986
1376
|
return { start, end };
|
|
987
1377
|
}
|
|
988
|
-
var Kpi = ({ label, value }) => /* @__PURE__ */
|
|
1378
|
+
var Kpi = ({ label, value }) => /* @__PURE__ */ import_react8.default.createElement(import_material7.Box, null, /* @__PURE__ */ import_react8.default.createElement(import_material7.Typography, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ import_react8.default.createElement(import_material7.Typography, { variant: "subtitle1", fontWeight: 600 }, value));
|
|
989
1379
|
var LiteLLMHomeWidget = ({
|
|
990
1380
|
defaultPeriod = "7d",
|
|
991
1381
|
title = "LiteLLM Usage"
|
|
992
1382
|
}) => {
|
|
993
1383
|
const api = (0, import_core_plugin_api3.useApi)(liteLlmApiRef);
|
|
994
|
-
const [period, setPeriod] = (0,
|
|
995
|
-
const [loading, setLoading] = (0,
|
|
996
|
-
const [error, setError] = (0,
|
|
997
|
-
const [usage, setUsage] = (0,
|
|
998
|
-
const [keys, setKeys] = (0,
|
|
999
|
-
(0,
|
|
1384
|
+
const [period, setPeriod] = (0, import_react8.useState)(defaultPeriod);
|
|
1385
|
+
const [loading, setLoading] = (0, import_react8.useState)(true);
|
|
1386
|
+
const [error, setError] = (0, import_react8.useState)(null);
|
|
1387
|
+
const [usage, setUsage] = (0, import_react8.useState)(null);
|
|
1388
|
+
const [keys, setKeys] = (0, import_react8.useState)([]);
|
|
1389
|
+
(0, import_react8.useEffect)(() => {
|
|
1000
1390
|
let cancelled = false;
|
|
1001
1391
|
setLoading(true);
|
|
1002
1392
|
setError(null);
|
|
@@ -1024,17 +1414,17 @@ var LiteLLMHomeWidget = ({
|
|
|
1024
1414
|
spend: d.spend
|
|
1025
1415
|
}));
|
|
1026
1416
|
const hasSparkline = dailyData.length > 0;
|
|
1027
|
-
return /* @__PURE__ */
|
|
1028
|
-
|
|
1417
|
+
return /* @__PURE__ */ import_react8.default.createElement(import_material7.Paper, { sx: { p: 2 } }, /* @__PURE__ */ import_react8.default.createElement(import_material7.Box, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1.5 }, /* @__PURE__ */ import_react8.default.createElement(import_material7.Typography, { variant: "h6" }, title), /* @__PURE__ */ import_react8.default.createElement(import_material7.FormControl, { size: "small", sx: { minWidth: 90 } }, /* @__PURE__ */ import_react8.default.createElement(
|
|
1418
|
+
import_material7.Select,
|
|
1029
1419
|
{
|
|
1030
1420
|
value: period,
|
|
1031
1421
|
onChange: (e) => setPeriod(e.target.value),
|
|
1032
1422
|
displayEmpty: true
|
|
1033
1423
|
},
|
|
1034
|
-
/* @__PURE__ */
|
|
1035
|
-
/* @__PURE__ */
|
|
1036
|
-
/* @__PURE__ */
|
|
1037
|
-
))), loading && /* @__PURE__ */
|
|
1424
|
+
/* @__PURE__ */ import_react8.default.createElement(import_material7.MenuItem, { value: "today" }, "Today"),
|
|
1425
|
+
/* @__PURE__ */ import_react8.default.createElement(import_material7.MenuItem, { value: "7d" }, "7d"),
|
|
1426
|
+
/* @__PURE__ */ import_react8.default.createElement(import_material7.MenuItem, { value: "30d" }, "30d")
|
|
1427
|
+
))), loading && /* @__PURE__ */ import_react8.default.createElement(import_material7.Box, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ import_react8.default.createElement(import_material7.CircularProgress, { size: 32 })), !loading && error && /* @__PURE__ */ import_react8.default.createElement(import_material7.Alert, { severity: "error", sx: { mt: 1 } }, error), !loading && !error && /* @__PURE__ */ import_react8.default.createElement(import_react8.default.Fragment, null, /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "USD Spent", value: fmtUsd2(usage?.total_spend ?? 0) })), /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "Tokens In", value: fmtInt2(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "Tokens Out", value: fmtInt2(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "Keys", value: fmtInt2(keys.length) }))), hasSparkline && /* @__PURE__ */ import_react8.default.createElement(import_material7.Box, { height: 120 }, /* @__PURE__ */ import_react8.default.createElement(import_recharts3.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react8.default.createElement(import_recharts3.AreaChart, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 } }, /* @__PURE__ */ import_react8.default.createElement(
|
|
1038
1428
|
import_recharts3.Area,
|
|
1039
1429
|
{
|
|
1040
1430
|
type: "monotone",
|