@acarmisc/backstage-plugin-litellm 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -0
- package/dist/components/DashboardHeader.d.ts +3 -1
- package/dist/components/GenerateKeyDialog.d.ts +20 -0
- package/dist/components/KeysTable.d.ts +3 -12
- package/dist/index.cjs.js +634 -514
- package/dist/index.cjs.js.map +4 -4
- package/dist/index.esm.js +639 -506
- package/dist/index.esm.js.map +4 -4
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -153,13 +153,26 @@ var init_api = __esm({
|
|
|
153
153
|
});
|
|
154
154
|
|
|
155
155
|
// src/components/DashboardHeader.tsx
|
|
156
|
-
import React from "react";
|
|
157
|
-
import { Box, Typography, LinearProgress, Paper, Chip } from "@mui/material";
|
|
156
|
+
import React, { useMemo } from "react";
|
|
157
|
+
import { Box, Typography, LinearProgress, Paper, Chip, Button, Stack } from "@mui/material";
|
|
158
|
+
import { Add, Key, Warning, Schedule } from "@mui/icons-material";
|
|
158
159
|
var DashboardHeader;
|
|
159
160
|
var init_DashboardHeader = __esm({
|
|
160
161
|
"src/components/DashboardHeader.tsx"() {
|
|
161
162
|
"use strict";
|
|
162
|
-
|
|
163
|
+
init_api();
|
|
164
|
+
DashboardHeader = ({
|
|
165
|
+
userInfo,
|
|
166
|
+
teams,
|
|
167
|
+
keys,
|
|
168
|
+
loading,
|
|
169
|
+
onGenerateKeyClick
|
|
170
|
+
}) => {
|
|
171
|
+
const counts = useMemo(() => {
|
|
172
|
+
const expired = keys.filter((k) => expiryStatus(k.expires_at) === "expired").length;
|
|
173
|
+
const expiringSoon = keys.filter((k) => expiryStatus(k.expires_at) === "soon").length;
|
|
174
|
+
return { total: keys.length, expired, expiringSoon };
|
|
175
|
+
}, [keys]);
|
|
163
176
|
if (loading) {
|
|
164
177
|
return /* @__PURE__ */ React.createElement(Paper, { sx: { p: 2, mb: 2 } }, /* @__PURE__ */ React.createElement(LinearProgress, null));
|
|
165
178
|
}
|
|
@@ -173,27 +186,38 @@ var init_DashboardHeader = __esm({
|
|
|
173
186
|
variant: "outlined",
|
|
174
187
|
color: "primary"
|
|
175
188
|
}
|
|
176
|
-
))))))
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
189
|
+
)))), /* @__PURE__ */ React.createElement(Box, { display: "flex", alignItems: "center", gap: 2, flexWrap: "wrap" }, /* @__PURE__ */ React.createElement(Stack, { direction: "row", spacing: 1 }, /* @__PURE__ */ React.createElement(Chip, { icon: /* @__PURE__ */ React.createElement(Key, { fontSize: "small" }), label: `${counts.total} keys`, size: "small" }), /* @__PURE__ */ React.createElement(
|
|
190
|
+
Chip,
|
|
191
|
+
{
|
|
192
|
+
icon: /* @__PURE__ */ React.createElement(Warning, { fontSize: "small" }),
|
|
193
|
+
label: `${counts.expired} expired`,
|
|
194
|
+
size: "small",
|
|
195
|
+
color: counts.expired > 0 ? "error" : "default"
|
|
196
|
+
}
|
|
197
|
+
), /* @__PURE__ */ React.createElement(
|
|
198
|
+
Chip,
|
|
199
|
+
{
|
|
200
|
+
icon: /* @__PURE__ */ React.createElement(Schedule, { fontSize: "small" }),
|
|
201
|
+
label: `${counts.expiringSoon} expiring soon`,
|
|
202
|
+
size: "small",
|
|
203
|
+
color: counts.expiringSoon > 0 ? "warning" : "default"
|
|
204
|
+
}
|
|
205
|
+
)), /* @__PURE__ */ React.createElement(
|
|
206
|
+
Button,
|
|
207
|
+
{
|
|
208
|
+
variant: "contained",
|
|
209
|
+
color: "primary",
|
|
210
|
+
startIcon: /* @__PURE__ */ React.createElement(Add, null),
|
|
211
|
+
onClick: onGenerateKeyClick
|
|
212
|
+
},
|
|
213
|
+
"Generate New Key"
|
|
214
|
+
))));
|
|
191
215
|
};
|
|
192
216
|
}
|
|
193
217
|
});
|
|
194
218
|
|
|
195
219
|
// src/components/KeysTable.tsx
|
|
196
|
-
import React2, { useState, useMemo } from "react";
|
|
220
|
+
import React2, { useState, useMemo as useMemo2 } from "react";
|
|
197
221
|
import {
|
|
198
222
|
Paper as Paper2,
|
|
199
223
|
Table,
|
|
@@ -202,7 +226,7 @@ import {
|
|
|
202
226
|
TableContainer,
|
|
203
227
|
TableHead,
|
|
204
228
|
TableRow,
|
|
205
|
-
Button,
|
|
229
|
+
Button as Button2,
|
|
206
230
|
IconButton,
|
|
207
231
|
Box as Box2,
|
|
208
232
|
Typography as Typography2,
|
|
@@ -211,24 +235,19 @@ import {
|
|
|
211
235
|
DialogContent,
|
|
212
236
|
DialogActions,
|
|
213
237
|
TextField,
|
|
214
|
-
MenuItem,
|
|
215
238
|
Chip as Chip2,
|
|
216
239
|
CircularProgress,
|
|
217
240
|
Autocomplete,
|
|
218
241
|
LinearProgress as LinearProgress2,
|
|
219
|
-
InputAdornment
|
|
220
|
-
Checkbox,
|
|
221
|
-
FormControlLabel,
|
|
222
|
-
Tabs,
|
|
223
|
-
Tab
|
|
242
|
+
InputAdornment
|
|
224
243
|
} from "@mui/material";
|
|
225
|
-
import { ContentCopy, Delete, Add, Edit, Autorenew, Search, Warning, Lock, LockOpen
|
|
244
|
+
import { ContentCopy, Delete, Add as Add2, Edit, Autorenew, Search, Warning as Warning2, Lock, LockOpen } from "@mui/icons-material";
|
|
226
245
|
function ExpiryChip({ expiresAt }) {
|
|
227
246
|
const status = expiryStatus(expiresAt);
|
|
228
247
|
if (!status) return /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "text.secondary" }, "-");
|
|
229
248
|
const label = status === "expired" ? "Expired" : status === "soon" ? `${Math.ceil((new Date(expiresAt).getTime() - Date.now()) / 864e5)}d left` : formatDate(expiresAt);
|
|
230
249
|
const color = status === "expired" ? "error" : status === "soon" ? "warning" : "default";
|
|
231
|
-
const icon = status === "expired" || status === "soon" ? /* @__PURE__ */ React2.createElement(
|
|
250
|
+
const icon = status === "expired" || status === "soon" ? /* @__PURE__ */ React2.createElement(Warning2, { fontSize: "small" }) : void 0;
|
|
232
251
|
return /* @__PURE__ */ React2.createElement(Chip2, { label, color, size: "small", icon });
|
|
233
252
|
}
|
|
234
253
|
function BudgetCell({ spend, maxBudget }) {
|
|
@@ -257,48 +276,15 @@ function formatContextWindow(maxInput, maxOutput) {
|
|
|
257
276
|
if (outPart) return `ctx ${outPart} out`;
|
|
258
277
|
return null;
|
|
259
278
|
}
|
|
260
|
-
|
|
261
|
-
return url.replace(/\/+$/, "");
|
|
262
|
-
}
|
|
263
|
-
function buildSnippets(baseUrl, key, model) {
|
|
264
|
-
const base = trimSlash(baseUrl);
|
|
265
|
-
return {
|
|
266
|
-
curl: `curl ${base}/v1/chat/completions \\
|
|
267
|
-
-H "Authorization: Bearer ${key}" \\
|
|
268
|
-
-H "Content-Type: application/json" \\
|
|
269
|
-
-d '{
|
|
270
|
-
"model": "${model}",
|
|
271
|
-
"messages": [{ "role": "user", "content": "Hello!" }]
|
|
272
|
-
}'`,
|
|
273
|
-
openai: `from openai import OpenAI
|
|
274
|
-
|
|
275
|
-
client = OpenAI(
|
|
276
|
-
api_key="${key}",
|
|
277
|
-
base_url="${base}/v1",
|
|
278
|
-
)
|
|
279
|
-
|
|
280
|
-
response = client.chat.completions.create(
|
|
281
|
-
model="${model}",
|
|
282
|
-
messages=[{ "role": "user", "content": "Hello!" }],
|
|
283
|
-
)
|
|
284
|
-
print(response.choices[0].message.content)`
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
var maskKey, generateDefaultAlias, shortKeyId, formatDate, emptyForm, keyToEditForm, KeysTable, SnippetTabs;
|
|
279
|
+
var maskKey, shortKeyId, formatDate, keyToEditForm, KeysTable;
|
|
288
280
|
var init_KeysTable = __esm({
|
|
289
281
|
"src/components/KeysTable.tsx"() {
|
|
290
282
|
"use strict";
|
|
291
283
|
init_api();
|
|
292
|
-
init_format();
|
|
293
284
|
maskKey = (key) => {
|
|
294
285
|
if (key.length <= 8) return "***";
|
|
295
286
|
return `${key.slice(0, 4)}...${key.slice(-4)}`;
|
|
296
287
|
};
|
|
297
|
-
generateDefaultAlias = (username) => {
|
|
298
|
-
const base = (username || "user").split("@")[0].toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "user";
|
|
299
|
-
const hash = Math.random().toString(36).slice(2, 8);
|
|
300
|
-
return `${base}-${hash}`;
|
|
301
|
-
};
|
|
302
288
|
shortKeyId = (token) => {
|
|
303
289
|
if (!token) return "-";
|
|
304
290
|
if (token.length <= 16) return token;
|
|
@@ -311,16 +297,6 @@ var init_KeysTable = __esm({
|
|
|
311
297
|
return dateStr;
|
|
312
298
|
}
|
|
313
299
|
};
|
|
314
|
-
emptyForm = () => ({
|
|
315
|
-
alias: "",
|
|
316
|
-
models: [],
|
|
317
|
-
duration: "30d",
|
|
318
|
-
max_budget: 100,
|
|
319
|
-
tpm_limit: void 0,
|
|
320
|
-
rpm_limit: void 0,
|
|
321
|
-
team_id: void 0,
|
|
322
|
-
key_type: "llm_api"
|
|
323
|
-
});
|
|
324
300
|
keyToEditForm = (k) => ({
|
|
325
301
|
key_alias: k.key_alias ?? "",
|
|
326
302
|
models: k.models ?? [],
|
|
@@ -331,26 +307,15 @@ var init_KeysTable = __esm({
|
|
|
331
307
|
KeysTable = ({
|
|
332
308
|
keys,
|
|
333
309
|
models,
|
|
334
|
-
teams,
|
|
335
|
-
username,
|
|
336
|
-
keyGenerationSettings,
|
|
337
310
|
loading,
|
|
338
|
-
|
|
311
|
+
onGenerateKeyClick,
|
|
339
312
|
onUpdateKey,
|
|
340
313
|
onBlockKey,
|
|
341
314
|
onUnblockKey,
|
|
342
315
|
onResetKeySpend,
|
|
343
316
|
onDeleteKey,
|
|
344
|
-
onPruneExpiredKeys
|
|
345
|
-
onGetConfig
|
|
317
|
+
onPruneExpiredKeys
|
|
346
318
|
}) => {
|
|
347
|
-
const [generateModalOpen, setGenerateModalOpen] = useState(false);
|
|
348
|
-
const [newKeyValue, setNewKeyValue] = useState(null);
|
|
349
|
-
const [newKeySnippets, setNewKeySnippets] = useState(null);
|
|
350
|
-
const [newKeyModel, setNewKeyModel] = useState("");
|
|
351
|
-
const [formData, setFormData] = useState(emptyForm());
|
|
352
|
-
const [unlimitedBudget, setUnlimitedBudget] = useState(false);
|
|
353
|
-
const [submitting, setSubmitting] = useState(false);
|
|
354
319
|
const [editingKey, setEditingKey] = useState(null);
|
|
355
320
|
const [editForm, setEditForm] = useState({});
|
|
356
321
|
const [editSubmitting, setEditSubmitting] = useState(false);
|
|
@@ -360,73 +325,14 @@ var init_KeysTable = __esm({
|
|
|
360
325
|
const [resetSpendConfirm, setResetSpendConfirm] = useState(false);
|
|
361
326
|
const [resetSpendSubmitting, setResetSpendSubmitting] = useState(false);
|
|
362
327
|
const [filterText, setFilterText] = useState("");
|
|
363
|
-
const filteredKeys =
|
|
328
|
+
const filteredKeys = useMemo2(() => {
|
|
364
329
|
if (!filterText.trim()) return keys;
|
|
365
330
|
const q = filterText.toLowerCase();
|
|
366
331
|
return keys.filter(
|
|
367
332
|
(k) => (k.key_alias ?? "").toLowerCase().includes(q) || k.models?.some((m) => m.toLowerCase().includes(q))
|
|
368
333
|
);
|
|
369
334
|
}, [keys, filterText]);
|
|
370
|
-
const allowUnlimitedBudget = keyGenerationSettings?.allowUnlimitedBudget ?? false;
|
|
371
|
-
const teamRequired = keyGenerationSettings?.teamRequired ?? true;
|
|
372
|
-
const selectedTeam = teams.find((t) => t.team_id === formData.team_id) ?? null;
|
|
373
|
-
const availableModels = useMemo(() => {
|
|
374
|
-
const teamModels = selectedTeam?.models;
|
|
375
|
-
if (teamModels && teamModels.length > 0) {
|
|
376
|
-
const allowed = new Set(teamModels);
|
|
377
|
-
return models.filter((m) => allowed.has(m.model_name));
|
|
378
|
-
}
|
|
379
|
-
return models;
|
|
380
|
-
}, [models, selectedTeam]);
|
|
381
|
-
const selectedModels = availableModels.filter((m) => (formData.models || []).includes(m.model_name));
|
|
382
335
|
const editSelectedModels = models.filter((m) => (editForm.models || []).includes(m.model_name));
|
|
383
|
-
const aliasError = !(formData.alias || "").trim();
|
|
384
|
-
const teamError = teamRequired && !formData.team_id;
|
|
385
|
-
const budgetInvalid = !unlimitedBudget && (formData.max_budget === void 0 || formData.max_budget === null || formData.max_budget <= 0);
|
|
386
|
-
const canGenerate = !aliasError && !teamError && !budgetInvalid && !submitting;
|
|
387
|
-
const budgetEstimate = useMemo(() => {
|
|
388
|
-
if (unlimitedBudget || budgetInvalid) return null;
|
|
389
|
-
const inputCosts = selectedModels.map((m) => m.input_cost_per_token).filter((c) => typeof c === "number" && c > 0);
|
|
390
|
-
if (inputCosts.length === 0) return null;
|
|
391
|
-
const pricePerToken = Math.max(...inputCosts);
|
|
392
|
-
return estimateTokensFromBudget(formData.max_budget ?? 0, pricePerToken);
|
|
393
|
-
}, [formData.max_budget, selectedModels, unlimitedBudget, budgetInvalid]);
|
|
394
|
-
const handleGenerate = async () => {
|
|
395
|
-
setSubmitting(true);
|
|
396
|
-
try {
|
|
397
|
-
const request = {
|
|
398
|
-
...formData,
|
|
399
|
-
max_budget: unlimitedBudget ? null : formData.max_budget
|
|
400
|
-
};
|
|
401
|
-
const response = await onGenerateKey(request);
|
|
402
|
-
setNewKeyValue(response.key);
|
|
403
|
-
setNewKeyModel(formData.models?.[0] ?? "");
|
|
404
|
-
setNewKeySnippets(null);
|
|
405
|
-
try {
|
|
406
|
-
const config = await onGetConfig();
|
|
407
|
-
const model = formData.models?.[0] ?? "";
|
|
408
|
-
setNewKeySnippets(buildSnippets(config.baseUrl, response.key, model));
|
|
409
|
-
} catch {
|
|
410
|
-
}
|
|
411
|
-
setFormData(emptyForm());
|
|
412
|
-
setUnlimitedBudget(false);
|
|
413
|
-
} catch (error) {
|
|
414
|
-
console.error("Failed to generate key:", error);
|
|
415
|
-
} finally {
|
|
416
|
-
setSubmitting(false);
|
|
417
|
-
}
|
|
418
|
-
};
|
|
419
|
-
const handleOpenGenerate = () => {
|
|
420
|
-
setFormData({ ...emptyForm(), alias: generateDefaultAlias(username) });
|
|
421
|
-
setGenerateModalOpen(true);
|
|
422
|
-
};
|
|
423
|
-
const handleCloseModal = () => {
|
|
424
|
-
setGenerateModalOpen(false);
|
|
425
|
-
setNewKeyValue(null);
|
|
426
|
-
setNewKeySnippets(null);
|
|
427
|
-
setFormData(emptyForm());
|
|
428
|
-
setUnlimitedBudget(false);
|
|
429
|
-
};
|
|
430
336
|
const handleOpenEdit = (k) => {
|
|
431
337
|
setEditingKey(k);
|
|
432
338
|
setEditForm(keyToEditForm(k));
|
|
@@ -478,117 +384,406 @@ var init_KeysTable = __esm({
|
|
|
478
384
|
try {
|
|
479
385
|
await onDeleteKey(deleteConfirmId);
|
|
480
386
|
} finally {
|
|
481
|
-
setDeleteSubmitting(false);
|
|
482
|
-
setDeleteConfirmId(null);
|
|
387
|
+
setDeleteSubmitting(false);
|
|
388
|
+
setDeleteConfirmId(null);
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
const copyToClipboard = (text) => {
|
|
392
|
+
navigator.clipboard.writeText(text);
|
|
393
|
+
};
|
|
394
|
+
const [pruneConfirmCount, setPruneConfirmCount] = useState(null);
|
|
395
|
+
const [pruneSubmitting, setPruneSubmitting] = useState(false);
|
|
396
|
+
const expiredKeys = useMemo2(() => {
|
|
397
|
+
return keys.filter((k) => expiryStatus(k.expires_at) === "expired");
|
|
398
|
+
}, [keys]);
|
|
399
|
+
const handlePruneExpired = async () => {
|
|
400
|
+
if (pruneConfirmCount === null || pruneConfirmCount === 0) return;
|
|
401
|
+
setPruneSubmitting(true);
|
|
402
|
+
try {
|
|
403
|
+
await onPruneExpiredKeys();
|
|
404
|
+
} catch (error) {
|
|
405
|
+
console.error("Failed to prune expired keys:", error);
|
|
406
|
+
} finally {
|
|
407
|
+
setPruneSubmitting(false);
|
|
408
|
+
setPruneConfirmCount(null);
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
const modelOption = (m) => {
|
|
412
|
+
const inCost = fmtCost(m.input_cost_per_token);
|
|
413
|
+
const outCost = fmtCost(m.output_cost_per_token);
|
|
414
|
+
const ctx = formatContextWindow(m.max_input_tokens, m.max_output_tokens);
|
|
415
|
+
return /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement("span", null, m.model_name), m.supports_function_calling && " \u{1F527}", m.supports_vision && " \u{1F441}\uFE0F", ctx && /* @__PURE__ */ React2.createElement(Typography2, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, ctx), (inCost || outCost) && /* @__PURE__ */ React2.createElement(Typography2, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, inCost, " in \xB7 ", outCost, " out"));
|
|
416
|
+
};
|
|
417
|
+
return /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement(Paper2, { sx: { mb: 2 } }, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", justifyContent: "space-between", alignItems: "center", p: 2, gap: 2 }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "h6" }, "Virtual Keys"), /* @__PURE__ */ React2.createElement(Box2, { display: "flex", gap: 1, alignItems: "center", flex: 1, justifyContent: "flex-end" }, /* @__PURE__ */ React2.createElement(
|
|
418
|
+
TextField,
|
|
419
|
+
{
|
|
420
|
+
size: "small",
|
|
421
|
+
placeholder: "Filter by alias or model\u2026",
|
|
422
|
+
value: filterText,
|
|
423
|
+
onChange: (e) => setFilterText(e.target.value),
|
|
424
|
+
sx: { minWidth: 240 },
|
|
425
|
+
InputProps: {
|
|
426
|
+
startAdornment: /* @__PURE__ */ React2.createElement(InputAdornment, { position: "start" }, /* @__PURE__ */ React2.createElement(Search, { fontSize: "small" }))
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
), expiredKeys.length > 0 && /* @__PURE__ */ React2.createElement(
|
|
430
|
+
Button2,
|
|
431
|
+
{
|
|
432
|
+
variant: "outlined",
|
|
433
|
+
color: "error",
|
|
434
|
+
startIcon: /* @__PURE__ */ React2.createElement(Autorenew, null),
|
|
435
|
+
onClick: () => setPruneConfirmCount(expiredKeys.length)
|
|
436
|
+
},
|
|
437
|
+
"Prune expired (",
|
|
438
|
+
expiredKeys.length,
|
|
439
|
+
")"
|
|
440
|
+
), /* @__PURE__ */ React2.createElement(
|
|
441
|
+
Button2,
|
|
442
|
+
{
|
|
443
|
+
variant: "contained",
|
|
444
|
+
color: "primary",
|
|
445
|
+
startIcon: /* @__PURE__ */ React2.createElement(Add2, null),
|
|
446
|
+
onClick: onGenerateKeyClick
|
|
447
|
+
},
|
|
448
|
+
"Generate New Key"
|
|
449
|
+
))), /* @__PURE__ */ React2.createElement(TableContainer, null, /* @__PURE__ */ React2.createElement(Table, null, /* @__PURE__ */ React2.createElement(TableHead, null, /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, null, "Alias"), /* @__PURE__ */ React2.createElement(TableCell, null, "Key ID"), /* @__PURE__ */ React2.createElement(TableCell, null, "Created"), /* @__PURE__ */ React2.createElement(TableCell, null, "Expires"), /* @__PURE__ */ React2.createElement(TableCell, null, "Budget"), /* @__PURE__ */ React2.createElement(TableCell, null, "TPM / RPM"), /* @__PURE__ */ React2.createElement(TableCell, null, "Models"), /* @__PURE__ */ React2.createElement(TableCell, { align: "right" }, "Actions"))), /* @__PURE__ */ React2.createElement(TableBody, null, loading ? /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ React2.createElement(CircularProgress, { size: 24 }))) : filteredKeys.length === 0 ? /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, { colSpan: 8, align: "center", sx: { py: 4 } }, filterText ? /* @__PURE__ */ React2.createElement(Typography2, { color: "text.secondary" }, "No keys match filter") : /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Typography2, { color: "text.secondary", gutterBottom: true }, "No keys yet \u2014 generate your first key to start calling models."), /* @__PURE__ */ React2.createElement(
|
|
450
|
+
Button2,
|
|
451
|
+
{
|
|
452
|
+
variant: "contained",
|
|
453
|
+
color: "primary",
|
|
454
|
+
size: "small",
|
|
455
|
+
startIcon: /* @__PURE__ */ React2.createElement(Add2, null),
|
|
456
|
+
onClick: onGenerateKeyClick
|
|
457
|
+
},
|
|
458
|
+
"Generate Your First Key"
|
|
459
|
+
)))) : filteredKeys.map((key) => {
|
|
460
|
+
const keyId = key.token ?? key.key;
|
|
461
|
+
const isBlocking = blockingKeyId === keyId;
|
|
462
|
+
return /* @__PURE__ */ React2.createElement(TableRow, { key: keyId, sx: key.blocked ? { bgcolor: "action.disabledBackground" } : void 0 }, /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", alignItems: "center", gap: 0.5 }, key.key_alias || "-", key.blocked && /* @__PURE__ */ React2.createElement(Chip2, { label: "Blocked", color: "error", size: "small" }))), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", alignItems: "center", gap: 0.5 }, /* @__PURE__ */ React2.createElement(
|
|
463
|
+
Typography2,
|
|
464
|
+
{
|
|
465
|
+
variant: "body2",
|
|
466
|
+
component: "code",
|
|
467
|
+
color: "text.secondary",
|
|
468
|
+
title: keyId,
|
|
469
|
+
sx: {
|
|
470
|
+
fontFamily: "monospace",
|
|
471
|
+
backgroundColor: "background.default",
|
|
472
|
+
px: 1,
|
|
473
|
+
py: 0.5,
|
|
474
|
+
borderRadius: 1
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
shortKeyId(keyId)
|
|
478
|
+
), /* @__PURE__ */ React2.createElement(IconButton, { size: "small", onClick: () => copyToClipboard(keyId), title: "Copy Key ID" }, /* @__PURE__ */ React2.createElement(ContentCopy, { fontSize: "small" })))), /* @__PURE__ */ React2.createElement(TableCell, null, formatDate(key.created_at)), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(ExpiryChip, { expiresAt: key.expires_at })), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(BudgetCell, { spend: key.spend ?? 0, maxBudget: key.max_budget })), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2" }, key.tpm_limit ?? "-", " / ", key.rpm_limit ?? "-")), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", gap: 0.5, flexWrap: "wrap" }, key.models?.slice(0, 2).map((model) => /* @__PURE__ */ React2.createElement(Chip2, { key: model, label: model, size: "small" })), (key.models?.length || 0) > 2 && /* @__PURE__ */ React2.createElement(Chip2, { label: `+${(key.models?.length || 0) - 2}`, size: "small", variant: "outlined" }))), /* @__PURE__ */ React2.createElement(TableCell, { align: "right" }, /* @__PURE__ */ React2.createElement(IconButton, { onClick: () => handleOpenEdit(key), title: "Edit key" }, /* @__PURE__ */ React2.createElement(Edit, { fontSize: "small" })), /* @__PURE__ */ React2.createElement(
|
|
479
|
+
IconButton,
|
|
480
|
+
{
|
|
481
|
+
onClick: () => handleToggleBlock(key),
|
|
482
|
+
disabled: isBlocking,
|
|
483
|
+
color: key.blocked ? "warning" : "default",
|
|
484
|
+
title: key.blocked ? "Unblock key" : "Block key \u2014 suspends without revoking"
|
|
485
|
+
},
|
|
486
|
+
isBlocking ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 18 }) : key.blocked ? /* @__PURE__ */ React2.createElement(LockOpen, { fontSize: "small" }) : /* @__PURE__ */ React2.createElement(Lock, { fontSize: "small" })
|
|
487
|
+
), /* @__PURE__ */ React2.createElement(
|
|
488
|
+
IconButton,
|
|
489
|
+
{
|
|
490
|
+
color: "error",
|
|
491
|
+
onClick: () => setDeleteConfirmId(keyId),
|
|
492
|
+
title: "Revoke key"
|
|
493
|
+
},
|
|
494
|
+
/* @__PURE__ */ React2.createElement(Delete, null)
|
|
495
|
+
)));
|
|
496
|
+
}))))), /* @__PURE__ */ React2.createElement(Dialog, { open: !!editingKey, onClose: handleCloseEdit, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ React2.createElement(DialogTitle, null, "Edit Key"), /* @__PURE__ */ React2.createElement(DialogContent, null, editingKey && /* @__PURE__ */ React2.createElement(Box2, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "text.secondary" }, /* @__PURE__ */ React2.createElement("code", { style: { fontFamily: "monospace", color: "inherit" } }, maskKey(editingKey.key))), /* @__PURE__ */ React2.createElement(
|
|
497
|
+
TextField,
|
|
498
|
+
{
|
|
499
|
+
label: "Alias",
|
|
500
|
+
value: editForm.key_alias || "",
|
|
501
|
+
onChange: (e) => setEditForm({ ...editForm, key_alias: e.target.value }),
|
|
502
|
+
fullWidth: true
|
|
503
|
+
}
|
|
504
|
+
), models.length > 0 && /* @__PURE__ */ React2.createElement(
|
|
505
|
+
Autocomplete,
|
|
506
|
+
{
|
|
507
|
+
multiple: true,
|
|
508
|
+
options: models,
|
|
509
|
+
groupBy: (m) => m.mode || "other",
|
|
510
|
+
getOptionLabel: (m) => m.model_name,
|
|
511
|
+
value: editSelectedModels,
|
|
512
|
+
onChange: (_e, selected) => setEditForm({ ...editForm, models: selected.map((m) => m.model_name) }),
|
|
513
|
+
renderOption: (props, m) => /* @__PURE__ */ React2.createElement("li", { ...props }, modelOption(m)),
|
|
514
|
+
renderInput: (params) => /* @__PURE__ */ React2.createElement(TextField, { ...params, label: "Models", fullWidth: true })
|
|
515
|
+
}
|
|
516
|
+
), /* @__PURE__ */ React2.createElement(
|
|
517
|
+
TextField,
|
|
518
|
+
{
|
|
519
|
+
label: "Max Budget (USD)",
|
|
520
|
+
type: "number",
|
|
521
|
+
value: editForm.max_budget ?? "",
|
|
522
|
+
onChange: (e) => setEditForm({ ...editForm, max_budget: e.target.value ? Number(e.target.value) : void 0 }),
|
|
523
|
+
fullWidth: true
|
|
524
|
+
}
|
|
525
|
+
), /* @__PURE__ */ React2.createElement(
|
|
526
|
+
TextField,
|
|
527
|
+
{
|
|
528
|
+
label: "TPM Limit",
|
|
529
|
+
type: "number",
|
|
530
|
+
value: editForm.tpm_limit ?? "",
|
|
531
|
+
onChange: (e) => setEditForm({ ...editForm, tpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
|
|
532
|
+
helperText: "Max tokens per minute this key can consume across all models. Leave blank for no limit.",
|
|
533
|
+
fullWidth: true
|
|
534
|
+
}
|
|
535
|
+
), /* @__PURE__ */ React2.createElement(
|
|
536
|
+
TextField,
|
|
537
|
+
{
|
|
538
|
+
label: "RPM Limit",
|
|
539
|
+
type: "number",
|
|
540
|
+
value: editForm.rpm_limit ?? "",
|
|
541
|
+
onChange: (e) => setEditForm({ ...editForm, rpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
|
|
542
|
+
helperText: "Max requests per minute this key can make across all models. Leave blank for no limit.",
|
|
543
|
+
fullWidth: true
|
|
544
|
+
}
|
|
545
|
+
), /* @__PURE__ */ React2.createElement(Box2, { mt: 1, pt: 2, borderTop: "1px solid", sx: { borderColor: "divider" } }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "caption", color: "text.secondary", display: "block", mb: 1 }, "Danger Zone"), !resetSpendConfirm ? /* @__PURE__ */ React2.createElement(
|
|
546
|
+
Button2,
|
|
547
|
+
{
|
|
548
|
+
size: "small",
|
|
549
|
+
color: "warning",
|
|
550
|
+
variant: "outlined",
|
|
551
|
+
onClick: () => setResetSpendConfirm(true)
|
|
552
|
+
},
|
|
553
|
+
"Reset Spend to $0"
|
|
554
|
+
) : /* @__PURE__ */ React2.createElement(Box2, { display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "warning.main" }, "Zero out spend counter?"), /* @__PURE__ */ React2.createElement(
|
|
555
|
+
Button2,
|
|
556
|
+
{
|
|
557
|
+
size: "small",
|
|
558
|
+
color: "warning",
|
|
559
|
+
variant: "contained",
|
|
560
|
+
disabled: resetSpendSubmitting,
|
|
561
|
+
onClick: handleResetSpend
|
|
562
|
+
},
|
|
563
|
+
resetSpendSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 16 }) : "Confirm"
|
|
564
|
+
), /* @__PURE__ */ React2.createElement(Button2, { size: "small", onClick: () => setResetSpendConfirm(false) }, "Cancel"))))), /* @__PURE__ */ React2.createElement(DialogActions, null, /* @__PURE__ */ React2.createElement(Button2, { onClick: handleCloseEdit }, "Cancel"), /* @__PURE__ */ React2.createElement(Button2, { onClick: handleUpdate, variant: "contained", color: "primary", disabled: editSubmitting }, editSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 24 }) : "Save"))), /* @__PURE__ */ React2.createElement(Dialog, { open: !!deleteConfirmId, onClose: () => setDeleteConfirmId(null), maxWidth: "xs", fullWidth: true }, /* @__PURE__ */ React2.createElement(DialogTitle, null, "Revoke Key?"), /* @__PURE__ */ React2.createElement(DialogContent, null, /* @__PURE__ */ React2.createElement(Typography2, null, "This will permanently revoke the key. Any integrations using it will stop working immediately.")), /* @__PURE__ */ React2.createElement(DialogActions, null, /* @__PURE__ */ React2.createElement(Button2, { onClick: () => setDeleteConfirmId(null), disabled: deleteSubmitting }, "Cancel"), /* @__PURE__ */ React2.createElement(
|
|
565
|
+
Button2,
|
|
566
|
+
{
|
|
567
|
+
onClick: handleConfirmDelete,
|
|
568
|
+
variant: "contained",
|
|
569
|
+
color: "error",
|
|
570
|
+
disabled: deleteSubmitting
|
|
571
|
+
},
|
|
572
|
+
deleteSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 20 }) : "Revoke"
|
|
573
|
+
))), /* @__PURE__ */ React2.createElement(Dialog, { open: pruneConfirmCount !== null, onClose: () => setPruneConfirmCount(null), maxWidth: "xs", fullWidth: true }, /* @__PURE__ */ React2.createElement(DialogTitle, null, "Prune Expired Keys?"), /* @__PURE__ */ React2.createElement(DialogContent, null, /* @__PURE__ */ React2.createElement(Typography2, null, "This will permanently delete ", pruneConfirmCount, " expired key", pruneConfirmCount !== 1 ? "s" : "", " from LiteLLM. Any integrations using them will stop working immediately.")), /* @__PURE__ */ React2.createElement(DialogActions, null, /* @__PURE__ */ React2.createElement(Button2, { onClick: () => setPruneConfirmCount(null), disabled: pruneSubmitting }, "Cancel"), /* @__PURE__ */ React2.createElement(
|
|
574
|
+
Button2,
|
|
575
|
+
{
|
|
576
|
+
onClick: handlePruneExpired,
|
|
577
|
+
variant: "contained",
|
|
578
|
+
color: "error",
|
|
579
|
+
disabled: pruneSubmitting
|
|
580
|
+
},
|
|
581
|
+
pruneSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 20 }) : "Prune"
|
|
582
|
+
))));
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// src/format.ts
|
|
588
|
+
var fmtUsd, fmtInt, estimateTokensFromBudget;
|
|
589
|
+
var init_format = __esm({
|
|
590
|
+
"src/format.ts"() {
|
|
591
|
+
"use strict";
|
|
592
|
+
fmtUsd = (n) => `$${(n ?? 0).toFixed(n < 1 ? 4 : 2)}`;
|
|
593
|
+
fmtInt = (n) => (n ?? 0).toLocaleString();
|
|
594
|
+
estimateTokensFromBudget = (budget, pricePerToken) => {
|
|
595
|
+
if (!pricePerToken || pricePerToken <= 0 || !budget || budget <= 0) return null;
|
|
596
|
+
return Math.floor(budget / pricePerToken);
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
// src/components/GenerateKeyDialog.tsx
|
|
602
|
+
import React3, { useState as useState2, useEffect, useMemo as useMemo3 } from "react";
|
|
603
|
+
import {
|
|
604
|
+
Box as Box3,
|
|
605
|
+
Typography as Typography3,
|
|
606
|
+
Dialog as Dialog2,
|
|
607
|
+
DialogTitle as DialogTitle2,
|
|
608
|
+
DialogContent as DialogContent2,
|
|
609
|
+
DialogActions as DialogActions2,
|
|
610
|
+
TextField as TextField2,
|
|
611
|
+
MenuItem,
|
|
612
|
+
Button as Button3,
|
|
613
|
+
IconButton as IconButton2,
|
|
614
|
+
CircularProgress as CircularProgress2,
|
|
615
|
+
Autocomplete as Autocomplete2,
|
|
616
|
+
Checkbox,
|
|
617
|
+
FormControlLabel,
|
|
618
|
+
Alert,
|
|
619
|
+
Tabs,
|
|
620
|
+
Tab
|
|
621
|
+
} from "@mui/material";
|
|
622
|
+
import { ContentCopy as ContentCopy2, Code } from "@mui/icons-material";
|
|
623
|
+
function trimSlash(url) {
|
|
624
|
+
return url.replace(/\/+$/, "");
|
|
625
|
+
}
|
|
626
|
+
function buildSnippets(baseUrl, key, model) {
|
|
627
|
+
const base = trimSlash(baseUrl);
|
|
628
|
+
return {
|
|
629
|
+
curl: `curl ${base}/v1/chat/completions \\
|
|
630
|
+
-H "Authorization: Bearer ${key}" \\
|
|
631
|
+
-H "Content-Type: application/json" \\
|
|
632
|
+
-d '{
|
|
633
|
+
"model": "${model}",
|
|
634
|
+
"messages": [{ "role": "user", "content": "Hello!" }]
|
|
635
|
+
}'`,
|
|
636
|
+
openai: `from openai import OpenAI
|
|
637
|
+
|
|
638
|
+
client = OpenAI(
|
|
639
|
+
api_key="${key}",
|
|
640
|
+
base_url="${base}/v1",
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
response = client.chat.completions.create(
|
|
644
|
+
model="${model}",
|
|
645
|
+
messages=[{ "role": "user", "content": "Hello!" }],
|
|
646
|
+
)
|
|
647
|
+
print(response.choices[0].message.content)`
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
function fmtCost2(perToken) {
|
|
651
|
+
if (!perToken) return null;
|
|
652
|
+
const per1k = perToken * 1e3;
|
|
653
|
+
return per1k < 0.01 ? `$${(perToken * 1e6).toFixed(2)}/M` : `$${per1k.toFixed(3)}/1K`;
|
|
654
|
+
}
|
|
655
|
+
function formatContextWindow2(maxInput, maxOutput) {
|
|
656
|
+
if (!maxInput && !maxOutput) return null;
|
|
657
|
+
const fmt = (n) => {
|
|
658
|
+
if (!n) return null;
|
|
659
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
660
|
+
if (n >= 1e3) return `${Math.round(n / 1e3)}K`;
|
|
661
|
+
return String(n);
|
|
662
|
+
};
|
|
663
|
+
const inPart = fmt(maxInput);
|
|
664
|
+
const outPart = fmt(maxOutput);
|
|
665
|
+
if (inPart && outPart) return `ctx ${inPart} in / ${outPart} out`;
|
|
666
|
+
if (inPart) return `ctx ${inPart}`;
|
|
667
|
+
if (outPart) return `ctx ${outPart} out`;
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
var generateDefaultAlias, emptyForm, GenerateKeyDialog, SnippetTabs;
|
|
671
|
+
var init_GenerateKeyDialog = __esm({
|
|
672
|
+
"src/components/GenerateKeyDialog.tsx"() {
|
|
673
|
+
"use strict";
|
|
674
|
+
init_format();
|
|
675
|
+
generateDefaultAlias = (username) => {
|
|
676
|
+
const base = (username || "user").split("@")[0].toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "user";
|
|
677
|
+
const hash = Math.random().toString(36).slice(2, 8);
|
|
678
|
+
return `${base}-${hash}`;
|
|
679
|
+
};
|
|
680
|
+
emptyForm = () => ({
|
|
681
|
+
alias: "",
|
|
682
|
+
models: [],
|
|
683
|
+
duration: "30d",
|
|
684
|
+
max_budget: 100,
|
|
685
|
+
tpm_limit: void 0,
|
|
686
|
+
rpm_limit: void 0,
|
|
687
|
+
team_id: void 0,
|
|
688
|
+
key_type: "llm_api"
|
|
689
|
+
});
|
|
690
|
+
GenerateKeyDialog = ({
|
|
691
|
+
open,
|
|
692
|
+
onClose,
|
|
693
|
+
keys,
|
|
694
|
+
models,
|
|
695
|
+
teams,
|
|
696
|
+
username,
|
|
697
|
+
keyGenerationSettings,
|
|
698
|
+
onGenerateKey,
|
|
699
|
+
onGetConfig
|
|
700
|
+
}) => {
|
|
701
|
+
const [formData, setFormData] = useState2(emptyForm());
|
|
702
|
+
const [unlimitedBudget, setUnlimitedBudget] = useState2(false);
|
|
703
|
+
const [submitting, setSubmitting] = useState2(false);
|
|
704
|
+
const [newKeyValue, setNewKeyValue] = useState2(null);
|
|
705
|
+
const [newKeySnippets, setNewKeySnippets] = useState2(null);
|
|
706
|
+
const [newKeyModel, setNewKeyModel] = useState2("");
|
|
707
|
+
const [generateError, setGenerateError] = useState2(null);
|
|
708
|
+
useEffect(() => {
|
|
709
|
+
if (open) {
|
|
710
|
+
setFormData({ ...emptyForm(), alias: generateDefaultAlias(username) });
|
|
711
|
+
setUnlimitedBudget(false);
|
|
712
|
+
setNewKeyValue(null);
|
|
713
|
+
setNewKeySnippets(null);
|
|
714
|
+
setNewKeyModel("");
|
|
715
|
+
setGenerateError(null);
|
|
716
|
+
}
|
|
717
|
+
}, [open, username]);
|
|
718
|
+
const allowUnlimitedBudget = keyGenerationSettings?.allowUnlimitedBudget ?? false;
|
|
719
|
+
const teamRequired = keyGenerationSettings?.teamRequired ?? true;
|
|
720
|
+
const selectedTeam = teams.find((t) => t.team_id === formData.team_id) ?? null;
|
|
721
|
+
const availableModels = useMemo3(() => {
|
|
722
|
+
const teamModels = selectedTeam?.models;
|
|
723
|
+
if (teamModels && teamModels.length > 0) {
|
|
724
|
+
const allowed = new Set(teamModels);
|
|
725
|
+
return models.filter((m) => allowed.has(m.model_name));
|
|
726
|
+
}
|
|
727
|
+
return models;
|
|
728
|
+
}, [models, selectedTeam]);
|
|
729
|
+
const selectedModels = availableModels.filter((m) => (formData.models || []).includes(m.model_name));
|
|
730
|
+
const aliasError = !(formData.alias || "").trim();
|
|
731
|
+
const teamError = teamRequired && !formData.team_id;
|
|
732
|
+
const budgetInvalid = !unlimitedBudget && (formData.max_budget === void 0 || formData.max_budget === null || formData.max_budget <= 0);
|
|
733
|
+
const canGenerate = !aliasError && !teamError && !budgetInvalid && !submitting;
|
|
734
|
+
const aliasDuplicate = !!formData.alias && keys.some((k) => k.key_alias === formData.alias);
|
|
735
|
+
const budgetEstimate = useMemo3(() => {
|
|
736
|
+
if (unlimitedBudget || budgetInvalid) return null;
|
|
737
|
+
const inputCosts = selectedModels.map((m) => m.input_cost_per_token).filter((c) => typeof c === "number" && c > 0);
|
|
738
|
+
if (inputCosts.length === 0) return null;
|
|
739
|
+
const pricePerToken = Math.max(...inputCosts);
|
|
740
|
+
return estimateTokensFromBudget(formData.max_budget ?? 0, pricePerToken);
|
|
741
|
+
}, [formData.max_budget, selectedModels, unlimitedBudget, budgetInvalid]);
|
|
742
|
+
const handleGenerate = async () => {
|
|
743
|
+
setSubmitting(true);
|
|
744
|
+
setGenerateError(null);
|
|
745
|
+
try {
|
|
746
|
+
const request = {
|
|
747
|
+
...formData,
|
|
748
|
+
max_budget: unlimitedBudget ? null : formData.max_budget
|
|
749
|
+
};
|
|
750
|
+
const response = await onGenerateKey(request);
|
|
751
|
+
setNewKeyValue(response.key);
|
|
752
|
+
setNewKeyModel(formData.models?.[0] ?? "");
|
|
753
|
+
setNewKeySnippets(null);
|
|
754
|
+
try {
|
|
755
|
+
const config = await onGetConfig();
|
|
756
|
+
const model = formData.models?.[0] ?? "";
|
|
757
|
+
setNewKeySnippets(buildSnippets(config.baseUrl, response.key, model));
|
|
758
|
+
} catch {
|
|
759
|
+
}
|
|
760
|
+
setFormData(emptyForm());
|
|
761
|
+
setUnlimitedBudget(false);
|
|
762
|
+
} catch (error) {
|
|
763
|
+
setGenerateError(error?.message ?? "Failed to generate key");
|
|
764
|
+
} finally {
|
|
765
|
+
setSubmitting(false);
|
|
483
766
|
}
|
|
484
767
|
};
|
|
768
|
+
const handleClose = () => {
|
|
769
|
+
onClose();
|
|
770
|
+
setGenerateError(null);
|
|
771
|
+
setNewKeyValue(null);
|
|
772
|
+
setNewKeySnippets(null);
|
|
773
|
+
setFormData(emptyForm());
|
|
774
|
+
setUnlimitedBudget(false);
|
|
775
|
+
};
|
|
485
776
|
const copyToClipboard = (text) => {
|
|
486
777
|
navigator.clipboard.writeText(text);
|
|
487
778
|
};
|
|
488
|
-
const [pruneConfirmCount, setPruneConfirmCount] = useState(null);
|
|
489
|
-
const [pruneSubmitting, setPruneSubmitting] = useState(false);
|
|
490
|
-
const expiredKeys = useMemo(() => {
|
|
491
|
-
return keys.filter((k) => expiryStatus(k.expires_at) === "expired");
|
|
492
|
-
}, [keys]);
|
|
493
|
-
const handlePruneExpired = async () => {
|
|
494
|
-
if (pruneConfirmCount === null || pruneConfirmCount === 0) return;
|
|
495
|
-
setPruneSubmitting(true);
|
|
496
|
-
try {
|
|
497
|
-
await onPruneExpiredKeys();
|
|
498
|
-
} catch (error) {
|
|
499
|
-
console.error("Failed to prune expired keys:", error);
|
|
500
|
-
} finally {
|
|
501
|
-
setPruneSubmitting(false);
|
|
502
|
-
setPruneConfirmCount(null);
|
|
503
|
-
}
|
|
504
|
-
};
|
|
505
779
|
const modelOption = (m) => {
|
|
506
|
-
const inCost =
|
|
507
|
-
const outCost =
|
|
508
|
-
const ctx =
|
|
509
|
-
return /* @__PURE__ */
|
|
780
|
+
const inCost = fmtCost2(m.input_cost_per_token);
|
|
781
|
+
const outCost = fmtCost2(m.output_cost_per_token);
|
|
782
|
+
const ctx = formatContextWindow2(m.max_input_tokens, m.max_output_tokens);
|
|
783
|
+
return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement("span", null, m.model_name), m.supports_function_calling && " \u{1F527}", m.supports_vision && " \u{1F441}\uFE0F", ctx && /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, ctx), (inCost || outCost) && /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, inCost, " in \xB7 ", outCost, " out"));
|
|
510
784
|
};
|
|
511
|
-
return /* @__PURE__ */
|
|
512
|
-
|
|
513
|
-
{
|
|
514
|
-
size: "small",
|
|
515
|
-
placeholder: "Filter by alias or model\u2026",
|
|
516
|
-
value: filterText,
|
|
517
|
-
onChange: (e) => setFilterText(e.target.value),
|
|
518
|
-
sx: { minWidth: 240 },
|
|
519
|
-
InputProps: {
|
|
520
|
-
startAdornment: /* @__PURE__ */ React2.createElement(InputAdornment, { position: "start" }, /* @__PURE__ */ React2.createElement(Search, { fontSize: "small" }))
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
), expiredKeys.length > 0 && /* @__PURE__ */ React2.createElement(
|
|
524
|
-
Button,
|
|
525
|
-
{
|
|
526
|
-
variant: "outlined",
|
|
527
|
-
color: "error",
|
|
528
|
-
startIcon: /* @__PURE__ */ React2.createElement(Autorenew, null),
|
|
529
|
-
onClick: () => setPruneConfirmCount(expiredKeys.length)
|
|
530
|
-
},
|
|
531
|
-
"Prune expired (",
|
|
532
|
-
expiredKeys.length,
|
|
533
|
-
")"
|
|
534
|
-
), /* @__PURE__ */ React2.createElement(
|
|
535
|
-
Button,
|
|
536
|
-
{
|
|
537
|
-
variant: "contained",
|
|
538
|
-
color: "primary",
|
|
539
|
-
startIcon: /* @__PURE__ */ React2.createElement(Add, null),
|
|
540
|
-
onClick: handleOpenGenerate
|
|
541
|
-
},
|
|
542
|
-
"Generate New Key"
|
|
543
|
-
))), /* @__PURE__ */ React2.createElement(TableContainer, null, /* @__PURE__ */ React2.createElement(Table, null, /* @__PURE__ */ React2.createElement(TableHead, null, /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, null, "Alias"), /* @__PURE__ */ React2.createElement(TableCell, null, "Key ID"), /* @__PURE__ */ React2.createElement(TableCell, null, "Created"), /* @__PURE__ */ React2.createElement(TableCell, null, "Expires"), /* @__PURE__ */ React2.createElement(TableCell, null, "Budget"), /* @__PURE__ */ React2.createElement(TableCell, null, "TPM / RPM"), /* @__PURE__ */ React2.createElement(TableCell, null, "Models"), /* @__PURE__ */ React2.createElement(TableCell, { align: "right" }, "Actions"))), /* @__PURE__ */ React2.createElement(TableBody, null, loading ? /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ React2.createElement(CircularProgress, { size: 24 }))) : filteredKeys.length === 0 ? /* @__PURE__ */ React2.createElement(TableRow, null, /* @__PURE__ */ React2.createElement(TableCell, { colSpan: 8, align: "center", sx: { py: 4 } }, filterText ? /* @__PURE__ */ React2.createElement(Typography2, { color: "text.secondary" }, "No keys match filter") : /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Typography2, { color: "text.secondary", gutterBottom: true }, "No keys yet \u2014 generate your first key to start calling models."), /* @__PURE__ */ React2.createElement(
|
|
544
|
-
Button,
|
|
545
|
-
{
|
|
546
|
-
variant: "contained",
|
|
547
|
-
color: "primary",
|
|
548
|
-
size: "small",
|
|
549
|
-
startIcon: /* @__PURE__ */ React2.createElement(Add, null),
|
|
550
|
-
onClick: handleOpenGenerate
|
|
551
|
-
},
|
|
552
|
-
"Generate Your First Key"
|
|
553
|
-
)))) : filteredKeys.map((key) => {
|
|
554
|
-
const keyId = key.token ?? key.key;
|
|
555
|
-
const isBlocking = blockingKeyId === keyId;
|
|
556
|
-
return /* @__PURE__ */ React2.createElement(TableRow, { key: keyId, sx: key.blocked ? { bgcolor: "action.disabledBackground" } : void 0 }, /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", alignItems: "center", gap: 0.5 }, key.key_alias || "-", key.blocked && /* @__PURE__ */ React2.createElement(Chip2, { label: "Blocked", color: "error", size: "small" }))), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", alignItems: "center", gap: 0.5 }, /* @__PURE__ */ React2.createElement(
|
|
557
|
-
Typography2,
|
|
558
|
-
{
|
|
559
|
-
variant: "body2",
|
|
560
|
-
component: "code",
|
|
561
|
-
color: "text.secondary",
|
|
562
|
-
title: keyId,
|
|
563
|
-
sx: {
|
|
564
|
-
fontFamily: "monospace",
|
|
565
|
-
backgroundColor: "background.default",
|
|
566
|
-
px: 1,
|
|
567
|
-
py: 0.5,
|
|
568
|
-
borderRadius: 1
|
|
569
|
-
}
|
|
570
|
-
},
|
|
571
|
-
shortKeyId(keyId)
|
|
572
|
-
), /* @__PURE__ */ React2.createElement(IconButton, { size: "small", onClick: () => copyToClipboard(keyId), title: "Copy Key ID" }, /* @__PURE__ */ React2.createElement(ContentCopy, { fontSize: "small" })))), /* @__PURE__ */ React2.createElement(TableCell, null, formatDate(key.created_at)), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(ExpiryChip, { expiresAt: key.expires_at })), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(BudgetCell, { spend: key.spend ?? 0, maxBudget: key.max_budget })), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2" }, key.tpm_limit ?? "-", " / ", key.rpm_limit ?? "-")), /* @__PURE__ */ React2.createElement(TableCell, null, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", gap: 0.5, flexWrap: "wrap" }, key.models?.slice(0, 2).map((model) => /* @__PURE__ */ React2.createElement(Chip2, { key: model, label: model, size: "small" })), (key.models?.length || 0) > 2 && /* @__PURE__ */ React2.createElement(Chip2, { label: `+${(key.models?.length || 0) - 2}`, size: "small", variant: "outlined" }))), /* @__PURE__ */ React2.createElement(TableCell, { align: "right" }, /* @__PURE__ */ React2.createElement(IconButton, { onClick: () => handleOpenEdit(key), title: "Edit key" }, /* @__PURE__ */ React2.createElement(Edit, { fontSize: "small" })), /* @__PURE__ */ React2.createElement(
|
|
573
|
-
IconButton,
|
|
574
|
-
{
|
|
575
|
-
onClick: () => handleToggleBlock(key),
|
|
576
|
-
disabled: isBlocking,
|
|
577
|
-
color: key.blocked ? "warning" : "default",
|
|
578
|
-
title: key.blocked ? "Unblock key" : "Block key \u2014 suspends without revoking"
|
|
579
|
-
},
|
|
580
|
-
isBlocking ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 18 }) : key.blocked ? /* @__PURE__ */ React2.createElement(LockOpen, { fontSize: "small" }) : /* @__PURE__ */ React2.createElement(Lock, { fontSize: "small" })
|
|
581
|
-
), /* @__PURE__ */ React2.createElement(
|
|
582
|
-
IconButton,
|
|
583
|
-
{
|
|
584
|
-
color: "error",
|
|
585
|
-
onClick: () => setDeleteConfirmId(keyId),
|
|
586
|
-
title: "Revoke key"
|
|
587
|
-
},
|
|
588
|
-
/* @__PURE__ */ React2.createElement(Delete, null)
|
|
589
|
-
)));
|
|
590
|
-
}))))), /* @__PURE__ */ React2.createElement(Dialog, { open: generateModalOpen, onClose: handleCloseModal, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ React2.createElement(DialogTitle, null, newKeyValue ? "Key Generated" : "Generate New Key"), /* @__PURE__ */ React2.createElement(DialogContent, null, newKeyValue ? /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "text.secondary", gutterBottom: true }, "Copy this key now. You won't be able to see it again."), /* @__PURE__ */ React2.createElement(
|
|
591
|
-
Box2,
|
|
785
|
+
return /* @__PURE__ */ React3.createElement(Dialog2, { open, onClose: handleClose, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ React3.createElement(DialogTitle2, null, newKeyValue ? "Key Generated" : "Generate New Key"), /* @__PURE__ */ React3.createElement(DialogContent2, null, newKeyValue ? /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Typography3, { variant: "body2", color: "text.secondary", gutterBottom: true }, "Copy this key now. You won't be able to see it again."), /* @__PURE__ */ React3.createElement(
|
|
786
|
+
Box3,
|
|
592
787
|
{
|
|
593
788
|
display: "flex",
|
|
594
789
|
alignItems: "center",
|
|
@@ -602,8 +797,8 @@ var init_KeysTable = __esm({
|
|
|
602
797
|
borderRadius: 1
|
|
603
798
|
}
|
|
604
799
|
},
|
|
605
|
-
/* @__PURE__ */
|
|
606
|
-
|
|
800
|
+
/* @__PURE__ */ React3.createElement(
|
|
801
|
+
Typography3,
|
|
607
802
|
{
|
|
608
803
|
component: "code",
|
|
609
804
|
color: "text.primary",
|
|
@@ -611,20 +806,24 @@ var init_KeysTable = __esm({
|
|
|
611
806
|
},
|
|
612
807
|
newKeyValue
|
|
613
808
|
),
|
|
614
|
-
/* @__PURE__ */
|
|
615
|
-
), newKeySnippets && /* @__PURE__ */
|
|
616
|
-
|
|
809
|
+
/* @__PURE__ */ React3.createElement(IconButton2, { onClick: () => copyToClipboard(newKeyValue) }, /* @__PURE__ */ React3.createElement(ContentCopy2, null))
|
|
810
|
+
), newKeySnippets && /* @__PURE__ */ React3.createElement(Box3, { mt: 3 }, /* @__PURE__ */ React3.createElement(Box3, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React3.createElement(Code, { fontSize: "small", color: "action" }), /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2" }, "Start calling the proxy \u2014 paste and run")), /* @__PURE__ */ React3.createElement(SnippetTabs, { snippets: newKeySnippets, model: newKeyModel, onCopy: copyToClipboard }))) : /* @__PURE__ */ React3.createElement(Box3, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, generateError && /* @__PURE__ */ React3.createElement(Alert, { severity: "error", onClose: () => setGenerateError(null) }, generateError), /* @__PURE__ */ React3.createElement(
|
|
811
|
+
TextField2,
|
|
617
812
|
{
|
|
618
813
|
label: "Alias",
|
|
619
814
|
value: formData.alias || "",
|
|
620
|
-
onChange: (e) =>
|
|
815
|
+
onChange: (e) => {
|
|
816
|
+
setFormData({ ...formData, alias: e.target.value });
|
|
817
|
+
setGenerateError(null);
|
|
818
|
+
},
|
|
621
819
|
error: aliasError,
|
|
622
|
-
|
|
820
|
+
color: aliasDuplicate ? "warning" : void 0,
|
|
821
|
+
helperText: aliasError ? "Alias is required" : aliasDuplicate ? "This alias is already used by one of your keys \u2014 LiteLLM requires aliases to be unique across all keys" : void 0,
|
|
623
822
|
required: true,
|
|
624
823
|
fullWidth: true
|
|
625
824
|
}
|
|
626
|
-
), /* @__PURE__ */
|
|
627
|
-
|
|
825
|
+
), /* @__PURE__ */ React3.createElement(
|
|
826
|
+
TextField2,
|
|
628
827
|
{
|
|
629
828
|
select: true,
|
|
630
829
|
label: "Duration",
|
|
@@ -632,13 +831,13 @@ var init_KeysTable = __esm({
|
|
|
632
831
|
onChange: (e) => setFormData({ ...formData, duration: e.target.value }),
|
|
633
832
|
fullWidth: true
|
|
634
833
|
},
|
|
635
|
-
/* @__PURE__ */
|
|
636
|
-
/* @__PURE__ */
|
|
637
|
-
/* @__PURE__ */
|
|
638
|
-
/* @__PURE__ */
|
|
639
|
-
/* @__PURE__ */
|
|
640
|
-
), teams.length > 0 ? /* @__PURE__ */
|
|
641
|
-
|
|
834
|
+
/* @__PURE__ */ React3.createElement(MenuItem, { value: "1d" }, "1 Day"),
|
|
835
|
+
/* @__PURE__ */ React3.createElement(MenuItem, { value: "7d" }, "7 Days"),
|
|
836
|
+
/* @__PURE__ */ React3.createElement(MenuItem, { value: "30d" }, "30 Days"),
|
|
837
|
+
/* @__PURE__ */ React3.createElement(MenuItem, { value: "90d" }, "90 Days"),
|
|
838
|
+
/* @__PURE__ */ React3.createElement(MenuItem, { value: "1y" }, "1 Year")
|
|
839
|
+
), teams.length > 0 ? /* @__PURE__ */ React3.createElement(
|
|
840
|
+
Autocomplete2,
|
|
642
841
|
{
|
|
643
842
|
options: teams,
|
|
644
843
|
getOptionLabel: (t) => t.team_alias || t.team_id,
|
|
@@ -648,8 +847,8 @@ var init_KeysTable = __esm({
|
|
|
648
847
|
const restrictedModels = teamModels && teamModels.length > 0 ? (formData.models || []).filter((m) => teamModels.includes(m)) : formData.models;
|
|
649
848
|
setFormData({ ...formData, team_id: team?.team_id, models: restrictedModels });
|
|
650
849
|
},
|
|
651
|
-
renderInput: (params) => /* @__PURE__ */
|
|
652
|
-
|
|
850
|
+
renderInput: (params) => /* @__PURE__ */ React3.createElement(
|
|
851
|
+
TextField2,
|
|
653
852
|
{
|
|
654
853
|
...params,
|
|
655
854
|
label: "Team",
|
|
@@ -660,8 +859,8 @@ var init_KeysTable = __esm({
|
|
|
660
859
|
}
|
|
661
860
|
)
|
|
662
861
|
}
|
|
663
|
-
) : teamRequired ? /* @__PURE__ */
|
|
664
|
-
|
|
862
|
+
) : teamRequired ? /* @__PURE__ */ React3.createElement(Typography3, { variant: "body2", color: "error" }, "Team selection is required, but you don't belong to any team yet \u2014 contact your administrator.") : null, availableModels.length > 0 && /* @__PURE__ */ React3.createElement(
|
|
863
|
+
Autocomplete2,
|
|
665
864
|
{
|
|
666
865
|
multiple: true,
|
|
667
866
|
options: availableModels,
|
|
@@ -669,9 +868,9 @@ var init_KeysTable = __esm({
|
|
|
669
868
|
getOptionLabel: (m) => m.model_name,
|
|
670
869
|
value: selectedModels,
|
|
671
870
|
onChange: (_e, selected) => setFormData({ ...formData, models: selected.map((m) => m.model_name) }),
|
|
672
|
-
renderOption: (props, m) => /* @__PURE__ */
|
|
673
|
-
renderInput: (params) => /* @__PURE__ */
|
|
674
|
-
|
|
871
|
+
renderOption: (props, m) => /* @__PURE__ */ React3.createElement("li", { ...props }, modelOption(m)),
|
|
872
|
+
renderInput: (params) => /* @__PURE__ */ React3.createElement(
|
|
873
|
+
TextField2,
|
|
675
874
|
{
|
|
676
875
|
...params,
|
|
677
876
|
label: "Models",
|
|
@@ -680,10 +879,10 @@ var init_KeysTable = __esm({
|
|
|
680
879
|
}
|
|
681
880
|
)
|
|
682
881
|
}
|
|
683
|
-
), allowUnlimitedBudget && /* @__PURE__ */
|
|
882
|
+
), allowUnlimitedBudget && /* @__PURE__ */ React3.createElement(
|
|
684
883
|
FormControlLabel,
|
|
685
884
|
{
|
|
686
|
-
control: /* @__PURE__ */
|
|
885
|
+
control: /* @__PURE__ */ React3.createElement(
|
|
687
886
|
Checkbox,
|
|
688
887
|
{
|
|
689
888
|
checked: unlimitedBudget,
|
|
@@ -699,8 +898,8 @@ var init_KeysTable = __esm({
|
|
|
699
898
|
),
|
|
700
899
|
label: "Unlimited budget"
|
|
701
900
|
}
|
|
702
|
-
), /* @__PURE__ */
|
|
703
|
-
|
|
901
|
+
), /* @__PURE__ */ React3.createElement(
|
|
902
|
+
TextField2,
|
|
704
903
|
{
|
|
705
904
|
label: "Max Budget (USD)",
|
|
706
905
|
type: "number",
|
|
@@ -712,8 +911,8 @@ var init_KeysTable = __esm({
|
|
|
712
911
|
required: true,
|
|
713
912
|
fullWidth: true
|
|
714
913
|
}
|
|
715
|
-
), /* @__PURE__ */
|
|
716
|
-
|
|
914
|
+
), /* @__PURE__ */ React3.createElement(
|
|
915
|
+
TextField2,
|
|
717
916
|
{
|
|
718
917
|
label: "TPM Limit",
|
|
719
918
|
type: "number",
|
|
@@ -722,8 +921,8 @@ var init_KeysTable = __esm({
|
|
|
722
921
|
helperText: "Max tokens per minute this key can consume across all models. Leave blank for no limit.",
|
|
723
922
|
fullWidth: true
|
|
724
923
|
}
|
|
725
|
-
), /* @__PURE__ */
|
|
726
|
-
|
|
924
|
+
), /* @__PURE__ */ React3.createElement(
|
|
925
|
+
TextField2,
|
|
727
926
|
{
|
|
728
927
|
label: "RPM Limit",
|
|
729
928
|
type: "number",
|
|
@@ -732,125 +931,39 @@ var init_KeysTable = __esm({
|
|
|
732
931
|
helperText: "Max requests per minute this key can make across all models. Leave blank for no limit.",
|
|
733
932
|
fullWidth: true
|
|
734
933
|
}
|
|
735
|
-
))), /* @__PURE__ */
|
|
736
|
-
|
|
934
|
+
))), /* @__PURE__ */ React3.createElement(DialogActions2, null, newKeyValue ? /* @__PURE__ */ React3.createElement(Button3, { onClick: handleClose, variant: "contained", color: "success" }, "Done") : /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement(Button3, { onClick: handleClose }, "Cancel"), /* @__PURE__ */ React3.createElement(
|
|
935
|
+
Button3,
|
|
737
936
|
{
|
|
738
937
|
onClick: handleGenerate,
|
|
739
938
|
variant: "contained",
|
|
740
939
|
color: "primary",
|
|
741
940
|
disabled: !canGenerate
|
|
742
941
|
},
|
|
743
|
-
submitting ? /* @__PURE__ */
|
|
744
|
-
)))), /* @__PURE__ */ React2.createElement(Dialog, { open: !!editingKey, onClose: handleCloseEdit, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ React2.createElement(DialogTitle, null, "Edit Key"), /* @__PURE__ */ React2.createElement(DialogContent, null, editingKey && /* @__PURE__ */ React2.createElement(Box2, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "text.secondary" }, /* @__PURE__ */ React2.createElement("code", { style: { fontFamily: "monospace", color: "inherit" } }, maskKey(editingKey.key))), /* @__PURE__ */ React2.createElement(
|
|
745
|
-
TextField,
|
|
746
|
-
{
|
|
747
|
-
label: "Alias",
|
|
748
|
-
value: editForm.key_alias || "",
|
|
749
|
-
onChange: (e) => setEditForm({ ...editForm, key_alias: e.target.value }),
|
|
750
|
-
fullWidth: true
|
|
751
|
-
}
|
|
752
|
-
), models.length > 0 && /* @__PURE__ */ React2.createElement(
|
|
753
|
-
Autocomplete,
|
|
754
|
-
{
|
|
755
|
-
multiple: true,
|
|
756
|
-
options: models,
|
|
757
|
-
groupBy: (m) => m.mode || "other",
|
|
758
|
-
getOptionLabel: (m) => m.model_name,
|
|
759
|
-
value: editSelectedModels,
|
|
760
|
-
onChange: (_e, selected) => setEditForm({ ...editForm, models: selected.map((m) => m.model_name) }),
|
|
761
|
-
renderOption: (props, m) => /* @__PURE__ */ React2.createElement("li", { ...props }, modelOption(m)),
|
|
762
|
-
renderInput: (params) => /* @__PURE__ */ React2.createElement(TextField, { ...params, label: "Models", fullWidth: true })
|
|
763
|
-
}
|
|
764
|
-
), /* @__PURE__ */ React2.createElement(
|
|
765
|
-
TextField,
|
|
766
|
-
{
|
|
767
|
-
label: "Max Budget (USD)",
|
|
768
|
-
type: "number",
|
|
769
|
-
value: editForm.max_budget ?? "",
|
|
770
|
-
onChange: (e) => setEditForm({ ...editForm, max_budget: e.target.value ? Number(e.target.value) : void 0 }),
|
|
771
|
-
fullWidth: true
|
|
772
|
-
}
|
|
773
|
-
), /* @__PURE__ */ React2.createElement(
|
|
774
|
-
TextField,
|
|
775
|
-
{
|
|
776
|
-
label: "TPM Limit",
|
|
777
|
-
type: "number",
|
|
778
|
-
value: editForm.tpm_limit ?? "",
|
|
779
|
-
onChange: (e) => setEditForm({ ...editForm, tpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
|
|
780
|
-
helperText: "Max tokens per minute this key can consume across all models. Leave blank for no limit.",
|
|
781
|
-
fullWidth: true
|
|
782
|
-
}
|
|
783
|
-
), /* @__PURE__ */ React2.createElement(
|
|
784
|
-
TextField,
|
|
785
|
-
{
|
|
786
|
-
label: "RPM Limit",
|
|
787
|
-
type: "number",
|
|
788
|
-
value: editForm.rpm_limit ?? "",
|
|
789
|
-
onChange: (e) => setEditForm({ ...editForm, rpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
|
|
790
|
-
helperText: "Max requests per minute this key can make across all models. Leave blank for no limit.",
|
|
791
|
-
fullWidth: true
|
|
792
|
-
}
|
|
793
|
-
), /* @__PURE__ */ React2.createElement(Box2, { mt: 1, pt: 2, borderTop: "1px solid", sx: { borderColor: "divider" } }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "caption", color: "text.secondary", display: "block", mb: 1 }, "Danger Zone"), !resetSpendConfirm ? /* @__PURE__ */ React2.createElement(
|
|
794
|
-
Button,
|
|
795
|
-
{
|
|
796
|
-
size: "small",
|
|
797
|
-
color: "warning",
|
|
798
|
-
variant: "outlined",
|
|
799
|
-
onClick: () => setResetSpendConfirm(true)
|
|
800
|
-
},
|
|
801
|
-
"Reset Spend to $0"
|
|
802
|
-
) : /* @__PURE__ */ React2.createElement(Box2, { display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "warning.main" }, "Zero out spend counter?"), /* @__PURE__ */ React2.createElement(
|
|
803
|
-
Button,
|
|
804
|
-
{
|
|
805
|
-
size: "small",
|
|
806
|
-
color: "warning",
|
|
807
|
-
variant: "contained",
|
|
808
|
-
disabled: resetSpendSubmitting,
|
|
809
|
-
onClick: handleResetSpend
|
|
810
|
-
},
|
|
811
|
-
resetSpendSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 16 }) : "Confirm"
|
|
812
|
-
), /* @__PURE__ */ React2.createElement(Button, { size: "small", onClick: () => setResetSpendConfirm(false) }, "Cancel"))))), /* @__PURE__ */ React2.createElement(DialogActions, null, /* @__PURE__ */ React2.createElement(Button, { onClick: handleCloseEdit }, "Cancel"), /* @__PURE__ */ React2.createElement(Button, { onClick: handleUpdate, variant: "contained", color: "primary", disabled: editSubmitting }, editSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 24 }) : "Save"))), /* @__PURE__ */ React2.createElement(Dialog, { open: !!deleteConfirmId, onClose: () => setDeleteConfirmId(null), maxWidth: "xs", fullWidth: true }, /* @__PURE__ */ React2.createElement(DialogTitle, null, "Revoke Key?"), /* @__PURE__ */ React2.createElement(DialogContent, null, /* @__PURE__ */ React2.createElement(Typography2, null, "This will permanently revoke the key. Any integrations using it will stop working immediately.")), /* @__PURE__ */ React2.createElement(DialogActions, null, /* @__PURE__ */ React2.createElement(Button, { onClick: () => setDeleteConfirmId(null), disabled: deleteSubmitting }, "Cancel"), /* @__PURE__ */ React2.createElement(
|
|
813
|
-
Button,
|
|
814
|
-
{
|
|
815
|
-
onClick: handleConfirmDelete,
|
|
816
|
-
variant: "contained",
|
|
817
|
-
color: "error",
|
|
818
|
-
disabled: deleteSubmitting
|
|
819
|
-
},
|
|
820
|
-
deleteSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 20 }) : "Revoke"
|
|
821
|
-
))), /* @__PURE__ */ React2.createElement(Dialog, { open: pruneConfirmCount !== null, onClose: () => setPruneConfirmCount(null), maxWidth: "xs", fullWidth: true }, /* @__PURE__ */ React2.createElement(DialogTitle, null, "Prune Expired Keys?"), /* @__PURE__ */ React2.createElement(DialogContent, null, /* @__PURE__ */ React2.createElement(Typography2, null, "This will permanently delete ", pruneConfirmCount, " expired key", pruneConfirmCount !== 1 ? "s" : "", " from LiteLLM. Any integrations using them will stop working immediately.")), /* @__PURE__ */ React2.createElement(DialogActions, null, /* @__PURE__ */ React2.createElement(Button, { onClick: () => setPruneConfirmCount(null), disabled: pruneSubmitting }, "Cancel"), /* @__PURE__ */ React2.createElement(
|
|
822
|
-
Button,
|
|
823
|
-
{
|
|
824
|
-
onClick: handlePruneExpired,
|
|
825
|
-
variant: "contained",
|
|
826
|
-
color: "error",
|
|
827
|
-
disabled: pruneSubmitting
|
|
828
|
-
},
|
|
829
|
-
pruneSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 20 }) : "Prune"
|
|
942
|
+
submitting ? /* @__PURE__ */ React3.createElement(CircularProgress2, { size: 24 }) : "Generate"
|
|
830
943
|
))));
|
|
831
944
|
};
|
|
832
945
|
SnippetTabs = ({ snippets, model, onCopy }) => {
|
|
833
|
-
const [tab, setTab] =
|
|
946
|
+
const [tab, setTab] = useState2("curl");
|
|
834
947
|
const code = tab === "curl" ? snippets.curl : snippets.openai;
|
|
835
|
-
return /* @__PURE__ */
|
|
836
|
-
|
|
948
|
+
return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Tabs, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 1 } }, /* @__PURE__ */ React3.createElement(Tab, { label: "curl", value: "curl" }), /* @__PURE__ */ React3.createElement(Tab, { label: "OpenAI SDK", value: "openai" })), /* @__PURE__ */ React3.createElement(
|
|
949
|
+
Box3,
|
|
837
950
|
{
|
|
838
951
|
position: "relative",
|
|
839
952
|
p: 1.5,
|
|
840
953
|
sx: { backgroundColor: "action.hover", border: "1px solid", borderColor: "divider", borderRadius: 1 }
|
|
841
954
|
},
|
|
842
|
-
/* @__PURE__ */
|
|
843
|
-
|
|
955
|
+
/* @__PURE__ */ React3.createElement(
|
|
956
|
+
IconButton2,
|
|
844
957
|
{
|
|
845
958
|
size: "small",
|
|
846
959
|
onClick: () => onCopy(code),
|
|
847
960
|
title: "Copy snippet",
|
|
848
961
|
sx: { position: "absolute", top: 4, right: 4 }
|
|
849
962
|
},
|
|
850
|
-
/* @__PURE__ */
|
|
963
|
+
/* @__PURE__ */ React3.createElement(ContentCopy2, { fontSize: "small" })
|
|
851
964
|
),
|
|
852
|
-
/* @__PURE__ */
|
|
853
|
-
|
|
965
|
+
/* @__PURE__ */ React3.createElement(
|
|
966
|
+
Typography3,
|
|
854
967
|
{
|
|
855
968
|
component: "pre",
|
|
856
969
|
sx: {
|
|
@@ -864,18 +977,18 @@ var init_KeysTable = __esm({
|
|
|
864
977
|
},
|
|
865
978
|
code
|
|
866
979
|
),
|
|
867
|
-
model && /* @__PURE__ */
|
|
980
|
+
model && /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption", color: "text.secondary", display: "block", mt: 1 }, "Using model \u201C", model, "\u201D \u2014 swap it for any model you have access to.")
|
|
868
981
|
));
|
|
869
982
|
};
|
|
870
983
|
}
|
|
871
984
|
});
|
|
872
985
|
|
|
873
986
|
// src/components/UsageStats.tsx
|
|
874
|
-
import
|
|
987
|
+
import React4, { useMemo as useMemo4, useState as useState3 } from "react";
|
|
875
988
|
import {
|
|
876
989
|
Paper as Paper3,
|
|
877
|
-
Box as
|
|
878
|
-
Typography as
|
|
990
|
+
Box as Box4,
|
|
991
|
+
Typography as Typography4,
|
|
879
992
|
FormControl,
|
|
880
993
|
InputLabel,
|
|
881
994
|
Select,
|
|
@@ -934,12 +1047,12 @@ var init_UsageStats = __esm({
|
|
|
934
1047
|
"#d0ed57"
|
|
935
1048
|
];
|
|
936
1049
|
fmtPct = (n) => `${(n * 100).toFixed(1)}%`;
|
|
937
|
-
KpiCard = ({ label, value, hint }) => /* @__PURE__ */
|
|
938
|
-
ChartSkeleton = ({ height = 260 }) => /* @__PURE__ */
|
|
1050
|
+
KpiCard = ({ label, value, hint }) => /* @__PURE__ */ React4.createElement(Paper3, { variant: "outlined", sx: { p: 2, height: "100%" } }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption", color: "text.secondary" }, label), /* @__PURE__ */ React4.createElement(Typography4, { variant: "h5", sx: { mt: 0.5 } }, value), hint ? /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption", color: "text.secondary" }, hint) : null);
|
|
1051
|
+
ChartSkeleton = ({ height = 260 }) => /* @__PURE__ */ React4.createElement(Skeleton, { variant: "rectangular", height, sx: { borderRadius: 1 } });
|
|
939
1052
|
EmptyChart = ({
|
|
940
1053
|
height = 260,
|
|
941
1054
|
message = "No data for this period"
|
|
942
|
-
}) => /* @__PURE__ */
|
|
1055
|
+
}) => /* @__PURE__ */ React4.createElement(Box4, { height, display: "flex", alignItems: "center", justifyContent: "center" }, /* @__PURE__ */ React4.createElement(Typography4, { color: "text.secondary", variant: "body2" }, message));
|
|
943
1056
|
UsageStats = ({
|
|
944
1057
|
usage,
|
|
945
1058
|
models,
|
|
@@ -949,14 +1062,14 @@ var init_UsageStats = __esm({
|
|
|
949
1062
|
userInfo
|
|
950
1063
|
}) => {
|
|
951
1064
|
const theme = useTheme();
|
|
952
|
-
const [selectedModel, setSelectedModel] =
|
|
953
|
-
const [tab, setTab] =
|
|
1065
|
+
const [selectedModel, setSelectedModel] = useState3("all");
|
|
1066
|
+
const [tab, setTab] = useState3("costs");
|
|
954
1067
|
const gridStroke = theme.palette.divider;
|
|
955
1068
|
const tickFill = theme.palette.text.secondary;
|
|
956
1069
|
const tickStyle = { fontSize: 12, fill: tickFill };
|
|
957
1070
|
const tickStyleSmall = { fontSize: 11, fill: tickFill };
|
|
958
1071
|
const tickStyleTiny = { fontSize: 10, fill: tickFill };
|
|
959
|
-
const selectedPreset =
|
|
1072
|
+
const selectedPreset = useMemo4(() => {
|
|
960
1073
|
if (dateRange.start.toDateString() === dateRange.end.toDateString()) return "today";
|
|
961
1074
|
const diffMs = dateRange.end.getTime() - dateRange.start.getTime();
|
|
962
1075
|
const diffHours = diffMs / (1e3 * 60 * 60);
|
|
@@ -978,7 +1091,7 @@ var init_UsageStats = __esm({
|
|
|
978
1091
|
}
|
|
979
1092
|
onDateRangeChange({ start, end });
|
|
980
1093
|
};
|
|
981
|
-
const dailyData =
|
|
1094
|
+
const dailyData = useMemo4(
|
|
982
1095
|
() => (usage?.daily_usage ?? []).map((d) => ({
|
|
983
1096
|
date: d.date,
|
|
984
1097
|
spend: d.spend,
|
|
@@ -991,21 +1104,21 @@ var init_UsageStats = __esm({
|
|
|
991
1104
|
})),
|
|
992
1105
|
[usage]
|
|
993
1106
|
);
|
|
994
|
-
const cumulativeData =
|
|
1107
|
+
const cumulativeData = useMemo4(() => {
|
|
995
1108
|
let cum = 0;
|
|
996
1109
|
return dailyData.map((d) => {
|
|
997
1110
|
cum += d.spend;
|
|
998
1111
|
return { date: d.date, cumulative: cum };
|
|
999
1112
|
});
|
|
1000
1113
|
}, [dailyData]);
|
|
1001
|
-
const successRateData =
|
|
1114
|
+
const successRateData = useMemo4(
|
|
1002
1115
|
() => dailyData.filter((d) => d.apiRequests > 0).map((d) => ({
|
|
1003
1116
|
date: d.date,
|
|
1004
1117
|
successRate: parseFloat((d.successfulRequests / d.apiRequests * 100).toFixed(1))
|
|
1005
1118
|
})),
|
|
1006
1119
|
[dailyData]
|
|
1007
1120
|
);
|
|
1008
|
-
const { modelSpendByDate, topModels: topSpendModels } =
|
|
1121
|
+
const { modelSpendByDate, topModels: topSpendModels } = useMemo4(() => {
|
|
1009
1122
|
const rows = usage?.daily_by_model ?? [];
|
|
1010
1123
|
const modelTotals = {};
|
|
1011
1124
|
for (const r of rows) modelTotals[r.model] = (modelTotals[r.model] ?? 0) + r.spend;
|
|
@@ -1020,7 +1133,7 @@ var init_UsageStats = __esm({
|
|
|
1020
1133
|
const hasOther = sorted.some((r) => r.Other > 0);
|
|
1021
1134
|
return { modelSpendByDate: sorted, topModels: hasOther ? [...top, "Other"] : top };
|
|
1022
1135
|
}, [usage]);
|
|
1023
|
-
const modelRows =
|
|
1136
|
+
const modelRows = useMemo4(() => {
|
|
1024
1137
|
const entries = Object.entries(usage?.usage_by_model ?? {}).map(([model, d]) => ({
|
|
1025
1138
|
model,
|
|
1026
1139
|
spend: d.total_spend,
|
|
@@ -1034,11 +1147,11 @@ var init_UsageStats = __esm({
|
|
|
1034
1147
|
}));
|
|
1035
1148
|
return selectedModel === "all" ? entries : entries.filter((e) => e.model === selectedModel);
|
|
1036
1149
|
}, [usage, selectedModel]);
|
|
1037
|
-
const topModelSpendBars =
|
|
1150
|
+
const topModelSpendBars = useMemo4(
|
|
1038
1151
|
() => [...modelRows].sort((a, b) => b.spend - a.spend).slice(0, 10),
|
|
1039
1152
|
[modelRows]
|
|
1040
1153
|
);
|
|
1041
|
-
const keyRows =
|
|
1154
|
+
const keyRows = useMemo4(
|
|
1042
1155
|
() => Object.entries(usage?.usage_by_key ?? {}).map(([keyHash, d]) => ({
|
|
1043
1156
|
keyHash,
|
|
1044
1157
|
keyAlias: d.key_alias ?? keyHash.slice(0, 8),
|
|
@@ -1055,7 +1168,7 @@ var init_UsageStats = __esm({
|
|
|
1055
1168
|
})),
|
|
1056
1169
|
[usage]
|
|
1057
1170
|
);
|
|
1058
|
-
const topKeySpendBars =
|
|
1171
|
+
const topKeySpendBars = useMemo4(
|
|
1059
1172
|
() => [...keyRows].sort((a, b) => b.spend - a.spend).slice(0, 10),
|
|
1060
1173
|
[keyRows]
|
|
1061
1174
|
);
|
|
@@ -1063,34 +1176,34 @@ var init_UsageStats = __esm({
|
|
|
1063
1176
|
const overallSuccessRate = totalRequests > 0 ? (usage?.successful_requests ?? 0) / totalRequests : 0;
|
|
1064
1177
|
const maxBudget = userInfo?.max_budget ?? 0;
|
|
1065
1178
|
const totalCumSpend = cumulativeData[cumulativeData.length - 1]?.cumulative ?? 0;
|
|
1066
|
-
return /* @__PURE__ */
|
|
1179
|
+
return /* @__PURE__ */ React4.createElement(Paper3, { sx: { p: 2 } }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2, flexWrap: "wrap", gap: 2 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "h6" }, "Usage Analytics"), /* @__PURE__ */ React4.createElement(Box4, { display: "flex", gap: 2, alignItems: "center", flexWrap: "wrap" }, /* @__PURE__ */ React4.createElement(FormControl, { size: "small", sx: { minWidth: 140 } }, /* @__PURE__ */ React4.createElement(InputLabel, null, "Period"), /* @__PURE__ */ React4.createElement(
|
|
1067
1180
|
Select,
|
|
1068
1181
|
{
|
|
1069
1182
|
value: selectedPreset,
|
|
1070
1183
|
label: "Period",
|
|
1071
1184
|
onChange: (e) => handlePresetChange(e.target.value)
|
|
1072
1185
|
},
|
|
1073
|
-
/* @__PURE__ */
|
|
1074
|
-
/* @__PURE__ */
|
|
1075
|
-
/* @__PURE__ */
|
|
1076
|
-
/* @__PURE__ */
|
|
1077
|
-
)), tab === "models" && /* @__PURE__ */
|
|
1186
|
+
/* @__PURE__ */ React4.createElement(MenuItem2, { value: "today" }, "Today"),
|
|
1187
|
+
/* @__PURE__ */ React4.createElement(MenuItem2, { value: "24h" }, "Last 24h"),
|
|
1188
|
+
/* @__PURE__ */ React4.createElement(MenuItem2, { value: "7d" }, "Last 7 days"),
|
|
1189
|
+
/* @__PURE__ */ React4.createElement(MenuItem2, { value: "30d" }, "Last 30 days")
|
|
1190
|
+
)), tab === "models" && /* @__PURE__ */ React4.createElement(FormControl, { size: "small", sx: { minWidth: 180 } }, /* @__PURE__ */ React4.createElement(InputLabel, null, "Model"), /* @__PURE__ */ React4.createElement(
|
|
1078
1191
|
Select,
|
|
1079
1192
|
{
|
|
1080
1193
|
value: selectedModel,
|
|
1081
1194
|
label: "Model",
|
|
1082
1195
|
onChange: (e) => setSelectedModel(e.target.value)
|
|
1083
1196
|
},
|
|
1084
|
-
/* @__PURE__ */
|
|
1085
|
-
models.map((m) => /* @__PURE__ */
|
|
1086
|
-
)))), /* @__PURE__ */
|
|
1197
|
+
/* @__PURE__ */ React4.createElement(MenuItem2, { value: "all" }, "All Models"),
|
|
1198
|
+
models.map((m) => /* @__PURE__ */ React4.createElement(MenuItem2, { key: m.model_name, value: m.model_name }, m.model_name))
|
|
1199
|
+
)))), /* @__PURE__ */ React4.createElement(Grid, { container: true, spacing: 2, sx: { mb: 2 } }, /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 6, sm: 3 }, loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, { height: 80 }) : /* @__PURE__ */ React4.createElement(KpiCard, { label: "Total Spend", value: fmtUsd(usage?.total_spend ?? 0) })), /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 6, sm: 3 }, loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, { height: 80 }) : /* @__PURE__ */ React4.createElement(KpiCard, { label: "Total Requests", value: fmtInt(totalRequests), hint: `${fmtInt(usage?.failed_requests ?? 0)} failed` })), /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 6, sm: 3 }, loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, { height: 80 }) : /* @__PURE__ */ React4.createElement(KpiCard, { label: "Success Rate", value: totalRequests > 0 ? fmtPct(overallSuccessRate) : "\u2014" })), /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 6, sm: 3 }, loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, { height: 80 }) : /* @__PURE__ */ React4.createElement(
|
|
1087
1200
|
KpiCard,
|
|
1088
1201
|
{
|
|
1089
1202
|
label: "Total Tokens",
|
|
1090
1203
|
value: fmtInt(usage?.total_tokens ?? 0),
|
|
1091
1204
|
hint: `${fmtInt(usage?.prompt_tokens ?? 0)} in \xB7 ${fmtInt(usage?.completion_tokens ?? 0)} out`
|
|
1092
1205
|
}
|
|
1093
|
-
))), /* @__PURE__ */
|
|
1206
|
+
))), /* @__PURE__ */ React4.createElement(Tabs2, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 2 } }, /* @__PURE__ */ React4.createElement(Tab2, { value: "costs", label: "Costs" }), /* @__PURE__ */ React4.createElement(Tab2, { value: "models", label: "Model Activity" }), /* @__PURE__ */ React4.createElement(Tab2, { value: "keys", label: "Key Activity" })), tab === "costs" && /* @__PURE__ */ React4.createElement(Grid, { container: true, spacing: 3 }, /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend by Model"), loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, null) : modelSpendByDate.length === 0 ? /* @__PURE__ */ React4.createElement(EmptyChart, null) : /* @__PURE__ */ React4.createElement(Box4, { height: 260 }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(AreaChart, { data: modelSpendByDate }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React4.createElement(XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ React4.createElement(YAxis, { tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React4.createElement(Tooltip, { formatter: (v) => [`$${v.toFixed(4)}`, void 0] }), /* @__PURE__ */ React4.createElement(Legend, null), topSpendModels.map((m) => /* @__PURE__ */ React4.createElement(
|
|
1094
1207
|
Area,
|
|
1095
1208
|
{
|
|
1096
1209
|
key: m,
|
|
@@ -1101,35 +1214,35 @@ var init_UsageStats = __esm({
|
|
|
1101
1214
|
fill: modelColor(m),
|
|
1102
1215
|
fillOpacity: 0.6
|
|
1103
1216
|
}
|
|
1104
|
-
)))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Token Usage"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : dailyData.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: 260 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(AreaChart, { data: dailyData }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ React3.createElement(YAxis, { tick: tickStyle, tickFormatter: fmtInt }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [fmtInt(v), void 0] }), /* @__PURE__ */ React3.createElement(Legend, null), /* @__PURE__ */ React3.createElement(Area, { type: "monotone", dataKey: "promptTokens", name: "Input (prompt)", stackId: "tok", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.5 }), /* @__PURE__ */ React3.createElement(Area, { type: "monotone", dataKey: "completionTokens", name: "Output (completion)", stackId: "tok", stroke: "#82ca9d", fill: "#82ca9d", fillOpacity: 0.5 }))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Requests"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : dailyData.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: 260 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(BarChart, { data: dailyData }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ React3.createElement(YAxis, { tick: tickStyle }), /* @__PURE__ */ React3.createElement(Tooltip, null), /* @__PURE__ */ React3.createElement(Legend, null), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "successfulRequests", name: "Successful", fill: "#82ca9d", stackId: "r" }), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "failedRequests", name: "Failed", fill: "#e57373", stackId: "r" }))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Success Rate"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : successRateData.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: 260 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(LineChart, { data: successRateData }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ React3.createElement(YAxis, { domain: [0, 100], tick: tickStyle, tickFormatter: (v) => `${v}%` }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [`${v}%`, "Success rate"] }), /* @__PURE__ */ React3.createElement(ReferenceLine, { y: 100, stroke: "#82ca9d", strokeDasharray: "4 2" }), /* @__PURE__ */ React3.createElement(Line, { type: "monotone", dataKey: "successRate", name: "Success rate", stroke: "#8884d8", dot: { r: 3 } }))))), (maxBudget > 0 || cumulativeData.length > 0) && /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Cumulative Spend", maxBudget > 0 ? ` vs Budget (${fmtUsd(maxBudget)})` : "", maxBudget > 0 && /* @__PURE__ */ React3.createElement(Typography3, { component: "span", variant: "caption", color: "text.secondary", sx: { ml: 1 } }, fmtUsd(totalCumSpend), " used \xB7 ", fmtUsd(Math.max(0, maxBudget - totalCumSpend)), " remaining")), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, { height: 180 }) : cumulativeData.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, { height: 180 }) : /* @__PURE__ */ React3.createElement(Box3, { height: 180 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(AreaChart, { data: cumulativeData }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ React3.createElement(YAxis, { tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [`$${v.toFixed(4)}`, "Cumulative spend"] }), maxBudget > 0 && /* @__PURE__ */ React3.createElement(ReferenceLine, { y: maxBudget, stroke: "#e57373", strokeDasharray: "6 3", label: { value: `Budget ${fmtUsd(maxBudget)}`, position: "insideTopRight", fontSize: 11 } }), /* @__PURE__ */ React3.createElement(Area, { type: "monotone", dataKey: "cumulative", name: "Cumulative spend", stroke: "#ffc658", fill: "#ffc658", fillOpacity: 0.3 })))))), tab === "models" && /* @__PURE__ */ React3.createElement(Grid, { container: true, spacing: 3 }, /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Spend by Model"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : topModelSpendBars.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: Math.max(200, topModelSpendBars.length * 36) }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(BarChart, { data: topModelSpendBars, layout: "vertical", margin: { left: 20 } }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React3.createElement(XAxis, { type: "number", tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React3.createElement(YAxis, { type: "category", dataKey: "model", tick: tickStyleSmall, width: 200 }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [fmtUsd(v), "Spend"] }), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "spend", name: "Spend", radius: [0, 4, 4, 0] }, topModelSpendBars.map((r) => /* @__PURE__ */ React3.createElement("rect", { key: r.model, fill: modelColor(r.model) }))))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Tokens by Model"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : modelRows.length === 0 ? /* @__PURE__ */ React3.createElement(EmptyChart, null) : /* @__PURE__ */ React3.createElement(Box3, { height: 260 }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(BarChart, { data: modelRows }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "model", tick: tickStyleTiny, interval: 0, angle: -15, textAnchor: "end", height: 60 }), /* @__PURE__ */ React3.createElement(YAxis, { tick: tickStyle }), /* @__PURE__ */ React3.createElement(Tooltip, null), /* @__PURE__ */ React3.createElement(Legend, null), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "promptTokens", name: "Prompt", fill: "#8884d8", stackId: "t" }), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "completionTokens", name: "Completion", fill: "#82ca9d", stackId: "t" }))))), /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(TableContainer2, { component: Paper3, variant: "outlined" }, /* @__PURE__ */ React3.createElement(Table2, { size: "small" }, /* @__PURE__ */ React3.createElement(TableHead2, null, /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, null, "Model"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Spend"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Requests"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Success"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Failed"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Prompt"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Completion"), /* @__PURE__ */ React3.createElement(TableCell2, { sx: { minWidth: 120 } }, "Success rate"))), /* @__PURE__ */ React3.createElement(TableBody2, null, loading ? /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, { colSpan: 8 }, /* @__PURE__ */ React3.createElement(LinearProgress3, null))) : modelRows.length === 0 ? /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, { colSpan: 8, align: "center" }, "No model activity")) : [...modelRows].sort((a, b) => b.spend - a.spend || b.totalTokens - a.totalTokens).map((r) => /* @__PURE__ */ React3.createElement(TableRow2, { key: r.model }, /* @__PURE__ */ React3.createElement(TableCell2, null, /* @__PURE__ */ React3.createElement(Box3, { display: "flex", alignItems: "center", gap: 0.75 }, /* @__PURE__ */ React3.createElement(Box3, { sx: { width: 10, height: 10, borderRadius: "50%", bgcolor: modelColor(r.model), flexShrink: 0 } }), r.model)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtUsd(r.spend)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.apiRequests)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.successfulRequests)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.failedRequests)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.promptTokens)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.completionTokens)), /* @__PURE__ */ React3.createElement(TableCell2, null, r.apiRequests > 0 ? /* @__PURE__ */ React3.createElement(Box3, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React3.createElement(
|
|
1217
|
+
)))))), /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Token Usage"), loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, null) : dailyData.length === 0 ? /* @__PURE__ */ React4.createElement(EmptyChart, null) : /* @__PURE__ */ React4.createElement(Box4, { height: 260 }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(AreaChart, { data: dailyData }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React4.createElement(XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ React4.createElement(YAxis, { tick: tickStyle, tickFormatter: fmtInt }), /* @__PURE__ */ React4.createElement(Tooltip, { formatter: (v) => [fmtInt(v), void 0] }), /* @__PURE__ */ React4.createElement(Legend, null), /* @__PURE__ */ React4.createElement(Area, { type: "monotone", dataKey: "promptTokens", name: "Input (prompt)", stackId: "tok", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.5 }), /* @__PURE__ */ React4.createElement(Area, { type: "monotone", dataKey: "completionTokens", name: "Output (completion)", stackId: "tok", stroke: "#82ca9d", fill: "#82ca9d", fillOpacity: 0.5 }))))), /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Requests"), loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, null) : dailyData.length === 0 ? /* @__PURE__ */ React4.createElement(EmptyChart, null) : /* @__PURE__ */ React4.createElement(Box4, { height: 260 }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(BarChart, { data: dailyData }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React4.createElement(XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ React4.createElement(YAxis, { tick: tickStyle }), /* @__PURE__ */ React4.createElement(Tooltip, null), /* @__PURE__ */ React4.createElement(Legend, null), /* @__PURE__ */ React4.createElement(Bar, { dataKey: "successfulRequests", name: "Successful", fill: "#82ca9d", stackId: "r" }), /* @__PURE__ */ React4.createElement(Bar, { dataKey: "failedRequests", name: "Failed", fill: "#e57373", stackId: "r" }))))), /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Success Rate"), loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, null) : successRateData.length === 0 ? /* @__PURE__ */ React4.createElement(EmptyChart, null) : /* @__PURE__ */ React4.createElement(Box4, { height: 260 }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(LineChart, { data: successRateData }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React4.createElement(XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ React4.createElement(YAxis, { domain: [0, 100], tick: tickStyle, tickFormatter: (v) => `${v}%` }), /* @__PURE__ */ React4.createElement(Tooltip, { formatter: (v) => [`${v}%`, "Success rate"] }), /* @__PURE__ */ React4.createElement(ReferenceLine, { y: 100, stroke: "#82ca9d", strokeDasharray: "4 2" }), /* @__PURE__ */ React4.createElement(Line, { type: "monotone", dataKey: "successRate", name: "Success rate", stroke: "#8884d8", dot: { r: 3 } }))))), (maxBudget > 0 || cumulativeData.length > 0) && /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Cumulative Spend", maxBudget > 0 ? ` vs Budget (${fmtUsd(maxBudget)})` : "", maxBudget > 0 && /* @__PURE__ */ React4.createElement(Typography4, { component: "span", variant: "caption", color: "text.secondary", sx: { ml: 1 } }, fmtUsd(totalCumSpend), " used \xB7 ", fmtUsd(Math.max(0, maxBudget - totalCumSpend)), " remaining")), loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, { height: 180 }) : cumulativeData.length === 0 ? /* @__PURE__ */ React4.createElement(EmptyChart, { height: 180 }) : /* @__PURE__ */ React4.createElement(Box4, { height: 180 }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(AreaChart, { data: cumulativeData }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React4.createElement(XAxis, { dataKey: "date", tick: tickStyle }), /* @__PURE__ */ React4.createElement(YAxis, { tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React4.createElement(Tooltip, { formatter: (v) => [`$${v.toFixed(4)}`, "Cumulative spend"] }), maxBudget > 0 && /* @__PURE__ */ React4.createElement(ReferenceLine, { y: maxBudget, stroke: "#e57373", strokeDasharray: "6 3", label: { value: `Budget ${fmtUsd(maxBudget)}`, position: "insideTopRight", fontSize: 11 } }), /* @__PURE__ */ React4.createElement(Area, { type: "monotone", dataKey: "cumulative", name: "Cumulative spend", stroke: "#ffc658", fill: "#ffc658", fillOpacity: 0.3 })))))), tab === "models" && /* @__PURE__ */ React4.createElement(Grid, { container: true, spacing: 3 }, /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Spend by Model"), loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, null) : topModelSpendBars.length === 0 ? /* @__PURE__ */ React4.createElement(EmptyChart, null) : /* @__PURE__ */ React4.createElement(Box4, { height: Math.max(200, topModelSpendBars.length * 36) }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(BarChart, { data: topModelSpendBars, layout: "vertical", margin: { left: 20 } }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React4.createElement(XAxis, { type: "number", tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React4.createElement(YAxis, { type: "category", dataKey: "model", tick: tickStyleSmall, width: 200 }), /* @__PURE__ */ React4.createElement(Tooltip, { formatter: (v) => [fmtUsd(v), "Spend"] }), /* @__PURE__ */ React4.createElement(Bar, { dataKey: "spend", name: "Spend", radius: [0, 4, 4, 0] }, topModelSpendBars.map((r) => /* @__PURE__ */ React4.createElement("rect", { key: r.model, fill: modelColor(r.model) }))))))), /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Tokens by Model"), loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, null) : modelRows.length === 0 ? /* @__PURE__ */ React4.createElement(EmptyChart, null) : /* @__PURE__ */ React4.createElement(Box4, { height: 260 }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(BarChart, { data: modelRows }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React4.createElement(XAxis, { dataKey: "model", tick: tickStyleTiny, interval: 0, angle: -15, textAnchor: "end", height: 60 }), /* @__PURE__ */ React4.createElement(YAxis, { tick: tickStyle }), /* @__PURE__ */ React4.createElement(Tooltip, null), /* @__PURE__ */ React4.createElement(Legend, null), /* @__PURE__ */ React4.createElement(Bar, { dataKey: "promptTokens", name: "Prompt", fill: "#8884d8", stackId: "t" }), /* @__PURE__ */ React4.createElement(Bar, { dataKey: "completionTokens", name: "Completion", fill: "#82ca9d", stackId: "t" }))))), /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React4.createElement(TableContainer2, { component: Paper3, variant: "outlined" }, /* @__PURE__ */ React4.createElement(Table2, { size: "small" }, /* @__PURE__ */ React4.createElement(TableHead2, null, /* @__PURE__ */ React4.createElement(TableRow2, null, /* @__PURE__ */ React4.createElement(TableCell2, null, "Model"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Spend"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Requests"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Success"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Failed"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Prompt"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Completion"), /* @__PURE__ */ React4.createElement(TableCell2, { sx: { minWidth: 120 } }, "Success rate"))), /* @__PURE__ */ React4.createElement(TableBody2, null, loading ? /* @__PURE__ */ React4.createElement(TableRow2, null, /* @__PURE__ */ React4.createElement(TableCell2, { colSpan: 8 }, /* @__PURE__ */ React4.createElement(LinearProgress3, null))) : modelRows.length === 0 ? /* @__PURE__ */ React4.createElement(TableRow2, null, /* @__PURE__ */ React4.createElement(TableCell2, { colSpan: 8, align: "center" }, "No model activity")) : [...modelRows].sort((a, b) => b.spend - a.spend || b.totalTokens - a.totalTokens).map((r) => /* @__PURE__ */ React4.createElement(TableRow2, { key: r.model }, /* @__PURE__ */ React4.createElement(TableCell2, null, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 0.75 }, /* @__PURE__ */ React4.createElement(Box4, { sx: { width: 10, height: 10, borderRadius: "50%", bgcolor: modelColor(r.model), flexShrink: 0 } }), r.model)), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtUsd(r.spend)), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtInt(r.apiRequests)), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtInt(r.successfulRequests)), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtInt(r.failedRequests)), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtInt(r.promptTokens)), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtInt(r.completionTokens)), /* @__PURE__ */ React4.createElement(TableCell2, null, r.apiRequests > 0 ? /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React4.createElement(
|
|
1105
1218
|
LinearProgress3,
|
|
1106
1219
|
{
|
|
1107
1220
|
variant: "determinate",
|
|
1108
1221
|
value: r.successRate * 100,
|
|
1109
1222
|
sx: { flex: 1, height: 6, borderRadius: 3 }
|
|
1110
1223
|
}
|
|
1111
|
-
), /* @__PURE__ */
|
|
1224
|
+
), /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption" }, fmtPct(r.successRate))) : "\u2014")))))))), tab === "keys" && /* @__PURE__ */ React4.createElement(Grid, { container: true, spacing: 3 }, (loading || topKeySpendBars.length > 0) && /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Spend by Key"), loading ? /* @__PURE__ */ React4.createElement(ChartSkeleton, null) : /* @__PURE__ */ React4.createElement(Box4, { height: Math.max(160, topKeySpendBars.length * 36) }, /* @__PURE__ */ React4.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(BarChart, { data: topKeySpendBars, layout: "vertical", margin: { left: 20 } }, /* @__PURE__ */ React4.createElement(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React4.createElement(XAxis, { type: "number", tick: tickStyle, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React4.createElement(YAxis, { type: "category", dataKey: "keyAlias", tick: tickStyleSmall, width: 160 }), /* @__PURE__ */ React4.createElement(Tooltip, { formatter: (v) => [fmtUsd(v), "Spend"] }), /* @__PURE__ */ React4.createElement(Bar, { dataKey: "spend", name: "Spend", fill: "#8884d8", radius: [0, 4, 4, 0] }))))), /* @__PURE__ */ React4.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React4.createElement(TableContainer2, { component: Paper3, variant: "outlined" }, /* @__PURE__ */ React4.createElement(Table2, { size: "small" }, /* @__PURE__ */ React4.createElement(TableHead2, null, /* @__PURE__ */ React4.createElement(TableRow2, null, /* @__PURE__ */ React4.createElement(TableCell2, null, "Key"), /* @__PURE__ */ React4.createElement(TableCell2, null, "Models"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Spend"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Requests"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Tokens"), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, "Failed"), /* @__PURE__ */ React4.createElement(TableCell2, { sx: { minWidth: 120 } }, "Success rate"))), /* @__PURE__ */ React4.createElement(TableBody2, null, loading ? /* @__PURE__ */ React4.createElement(TableRow2, null, /* @__PURE__ */ React4.createElement(TableCell2, { colSpan: 7 }, /* @__PURE__ */ React4.createElement(LinearProgress3, null))) : keyRows.length === 0 ? /* @__PURE__ */ React4.createElement(TableRow2, null, /* @__PURE__ */ React4.createElement(TableCell2, { colSpan: 7, align: "center" }, "No key activity")) : [...keyRows].sort((a, b) => b.spend - a.spend || b.apiRequests - a.apiRequests).map((r) => /* @__PURE__ */ React4.createElement(TableRow2, { key: r.keyHash }, /* @__PURE__ */ React4.createElement(TableCell2, null, /* @__PURE__ */ React4.createElement(Typography4, { variant: "body2" }, r.keyAlias), r.teamId ? /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption", color: "text.secondary" }, "team: ", r.teamId) : null), /* @__PURE__ */ React4.createElement(TableCell2, null, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", gap: 0.5, flexWrap: "wrap" }, r.models.map((m) => /* @__PURE__ */ React4.createElement(Chip3, { key: m, label: m, size: "small", variant: "outlined" })))), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtUsd(r.spend)), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtInt(r.apiRequests)), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtInt(r.totalTokens)), /* @__PURE__ */ React4.createElement(TableCell2, { align: "right" }, fmtInt(r.failedRequests)), /* @__PURE__ */ React4.createElement(TableCell2, null, r.apiRequests > 0 ? /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React4.createElement(
|
|
1112
1225
|
LinearProgress3,
|
|
1113
1226
|
{
|
|
1114
1227
|
variant: "determinate",
|
|
1115
1228
|
value: r.successRate * 100,
|
|
1116
1229
|
sx: { flex: 1, height: 6, borderRadius: 3 }
|
|
1117
1230
|
}
|
|
1118
|
-
), /* @__PURE__ */
|
|
1231
|
+
), /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption" }, fmtPct(r.successRate))) : "\u2014")))))))));
|
|
1119
1232
|
};
|
|
1120
1233
|
}
|
|
1121
1234
|
});
|
|
1122
1235
|
|
|
1123
1236
|
// src/components/TeamUsage.tsx
|
|
1124
|
-
import
|
|
1237
|
+
import React5, { useState as useState4 } from "react";
|
|
1125
1238
|
import {
|
|
1126
1239
|
Paper as Paper4,
|
|
1127
|
-
Box as
|
|
1128
|
-
Typography as
|
|
1240
|
+
Box as Box5,
|
|
1241
|
+
Typography as Typography5,
|
|
1129
1242
|
LinearProgress as LinearProgress4,
|
|
1130
1243
|
Chip as Chip4,
|
|
1131
1244
|
Divider,
|
|
1132
|
-
Stack,
|
|
1245
|
+
Stack as Stack2,
|
|
1133
1246
|
Table as Table3,
|
|
1134
1247
|
TableBody as TableBody3,
|
|
1135
1248
|
TableCell as TableCell3,
|
|
@@ -1137,8 +1250,8 @@ import {
|
|
|
1137
1250
|
TableRow as TableRow3,
|
|
1138
1251
|
TableContainer as TableContainer3,
|
|
1139
1252
|
Collapse,
|
|
1140
|
-
IconButton as
|
|
1141
|
-
CircularProgress as
|
|
1253
|
+
IconButton as IconButton3,
|
|
1254
|
+
CircularProgress as CircularProgress3,
|
|
1142
1255
|
useTheme as useTheme2
|
|
1143
1256
|
} from "@mui/material";
|
|
1144
1257
|
import { ExpandMore, ExpandLess, Group, Speed, Memory } from "@mui/icons-material";
|
|
@@ -1155,9 +1268,9 @@ var Stat, TeamCard, TeamUsage;
|
|
|
1155
1268
|
var init_TeamUsage = __esm({
|
|
1156
1269
|
"src/components/TeamUsage.tsx"() {
|
|
1157
1270
|
"use strict";
|
|
1158
|
-
Stat = ({ label, value }) => /* @__PURE__ */
|
|
1271
|
+
Stat = ({ label, value }) => /* @__PURE__ */ React5.createElement(Box5, null, /* @__PURE__ */ React5.createElement(Typography5, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2", fontWeight: 600, sx: { fontFamily: "monospace" } }, value));
|
|
1159
1272
|
TeamCard = ({ team, usage, usageLoading }) => {
|
|
1160
|
-
const [expanded, setExpanded] =
|
|
1273
|
+
const [expanded, setExpanded] = useState4(false);
|
|
1161
1274
|
const theme = useTheme2();
|
|
1162
1275
|
const gridStroke = theme.palette.divider;
|
|
1163
1276
|
const tickFill = theme.palette.text.secondary;
|
|
@@ -1167,43 +1280,43 @@ var init_TeamUsage = __esm({
|
|
|
1167
1280
|
const isOver = budget > 0 && spend >= budget;
|
|
1168
1281
|
const isNear = budget > 0 && spend >= budget * 0.8 && !isOver;
|
|
1169
1282
|
const dailyData = usage?.daily_usage?.map((d) => ({ date: d.date, spend: d.spend })) ?? [];
|
|
1170
|
-
return /* @__PURE__ */
|
|
1283
|
+
return /* @__PURE__ */ React5.createElement(Paper4, { variant: "outlined", sx: { p: 2, mb: 2 } }, /* @__PURE__ */ React5.createElement(Box5, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React5.createElement(Group, { color: "action" }), /* @__PURE__ */ React5.createElement(Box5, { flexGrow: 1 }, /* @__PURE__ */ React5.createElement(Typography5, { variant: "subtitle1", fontWeight: 600 }, team.team_alias || "Untitled team"), /* @__PURE__ */ React5.createElement(Typography5, { variant: "caption", color: "text.secondary", sx: { fontFamily: "monospace" } }, team.team_id)), /* @__PURE__ */ React5.createElement(IconButton3, { size: "small", onClick: () => setExpanded((e) => !e), "aria-label": "toggle details" }, expanded ? /* @__PURE__ */ React5.createElement(ExpandLess, null) : /* @__PURE__ */ React5.createElement(ExpandMore, null))), /* @__PURE__ */ React5.createElement(Stack2, { direction: "row", spacing: 3, mt: 1.5, flexWrap: "wrap", useFlexGap: true, gap: 1.5 }, /* @__PURE__ */ React5.createElement(
|
|
1171
1284
|
Stat,
|
|
1172
1285
|
{
|
|
1173
1286
|
label: "Members",
|
|
1174
1287
|
value: team.members_with_roles?.length ? String(team.members_with_roles.length) : "\u2014"
|
|
1175
1288
|
}
|
|
1176
|
-
), /* @__PURE__ */
|
|
1289
|
+
), /* @__PURE__ */ React5.createElement(
|
|
1177
1290
|
Stat,
|
|
1178
1291
|
{
|
|
1179
1292
|
label: "Models",
|
|
1180
1293
|
value: team.models?.length ? String(team.models.length) : "All"
|
|
1181
1294
|
}
|
|
1182
|
-
), /* @__PURE__ */
|
|
1295
|
+
), /* @__PURE__ */ React5.createElement(
|
|
1183
1296
|
Stat,
|
|
1184
1297
|
{
|
|
1185
1298
|
label: "Budget",
|
|
1186
1299
|
value: budget > 0 ? `$${budget.toFixed(2)}` : "Unlimited"
|
|
1187
1300
|
}
|
|
1188
|
-
), /* @__PURE__ */
|
|
1301
|
+
), /* @__PURE__ */ React5.createElement(
|
|
1189
1302
|
Stat,
|
|
1190
1303
|
{
|
|
1191
1304
|
label: "Spend",
|
|
1192
1305
|
value: `$${spend.toFixed(2)}`
|
|
1193
1306
|
}
|
|
1194
|
-
), /* @__PURE__ */
|
|
1307
|
+
), /* @__PURE__ */ React5.createElement(
|
|
1195
1308
|
Stat,
|
|
1196
1309
|
{
|
|
1197
1310
|
label: "TPM",
|
|
1198
1311
|
value: team.tpm_limit != null && team.tpm_limit > 0 ? String(team.tpm_limit) : "\u2014"
|
|
1199
1312
|
}
|
|
1200
|
-
), /* @__PURE__ */
|
|
1313
|
+
), /* @__PURE__ */ React5.createElement(
|
|
1201
1314
|
Stat,
|
|
1202
1315
|
{
|
|
1203
1316
|
label: "RPM",
|
|
1204
1317
|
value: team.rpm_limit != null && team.rpm_limit > 0 ? String(team.rpm_limit) : "\u2014"
|
|
1205
1318
|
}
|
|
1206
|
-
)), budget > 0 && /* @__PURE__ */
|
|
1319
|
+
)), budget > 0 && /* @__PURE__ */ React5.createElement(Box5, { mt: 1.5 }, /* @__PURE__ */ React5.createElement(Box5, { display: "flex", justifyContent: "space-between", mb: 0.5 }, /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2" }, "$", spend.toFixed(2), " / $", budget.toFixed(2)), isOver && /* @__PURE__ */ React5.createElement(Chip4, { label: "Over Budget", size: "small", color: "error" }), isNear && /* @__PURE__ */ React5.createElement(Chip4, { label: "Near Limit", size: "small", color: "warning" })), /* @__PURE__ */ React5.createElement(
|
|
1207
1320
|
LinearProgress4,
|
|
1208
1321
|
{
|
|
1209
1322
|
variant: "determinate",
|
|
@@ -1211,22 +1324,22 @@ var init_TeamUsage = __esm({
|
|
|
1211
1324
|
color: isOver ? "error" : isNear ? "warning" : "primary",
|
|
1212
1325
|
sx: { height: 6, borderRadius: 1 }
|
|
1213
1326
|
}
|
|
1214
|
-
)), /* @__PURE__ */
|
|
1215
|
-
|
|
1327
|
+
)), /* @__PURE__ */ React5.createElement(Collapse, { in: expanded }, /* @__PURE__ */ React5.createElement(Box5, { mt: 2 }, /* @__PURE__ */ React5.createElement(Box5, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React5.createElement(Speed, { fontSize: "small", color: "action" }), /* @__PURE__ */ React5.createElement(Typography5, { variant: "subtitle2", color: "text.secondary" }, "Rate limits")), /* @__PURE__ */ React5.createElement(Stack2, { direction: "row", spacing: 3, flexWrap: "wrap", useFlexGap: true, gap: 1.5, mb: 2 }, /* @__PURE__ */ React5.createElement(Stat, { label: "TPM limit", value: team.tpm_limit != null && team.tpm_limit > 0 ? String(team.tpm_limit) : "Unlimited" }), /* @__PURE__ */ React5.createElement(Stat, { label: "RPM limit", value: team.rpm_limit != null && team.rpm_limit > 0 ? String(team.rpm_limit) : "Unlimited" }), /* @__PURE__ */ React5.createElement(Stat, { label: "Max budget", value: budget > 0 ? `$${budget.toFixed(2)}` : "Unlimited" })), /* @__PURE__ */ React5.createElement(Divider, { sx: { mb: 2 } }), /* @__PURE__ */ React5.createElement(Typography5, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Models"), team.models?.length ? /* @__PURE__ */ React5.createElement(Box5, { display: "flex", gap: 0.5, flexWrap: "wrap", mb: 2 }, team.models.map((m) => /* @__PURE__ */ React5.createElement(Chip4, { key: m, label: m, size: "small", variant: "outlined" }))) : /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2", color: "text.secondary", mb: 2 }, "All models allowed"), /* @__PURE__ */ React5.createElement(Divider, { sx: { mb: 2 } }), usageLoading ? /* @__PURE__ */ React5.createElement(Box5, { display: "flex", justifyContent: "center", p: 2 }, /* @__PURE__ */ React5.createElement(CircularProgress3, { size: 24 })) : dailyData.length > 0 ? /* @__PURE__ */ React5.createElement(React5.Fragment, null, /* @__PURE__ */ React5.createElement(Typography5, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend"), /* @__PURE__ */ React5.createElement(Box5, { height: 160, mb: 2 }, /* @__PURE__ */ React5.createElement(ResponsiveContainer2, { width: "100%", height: "100%" }, /* @__PURE__ */ React5.createElement(AreaChart2, { data: dailyData }, /* @__PURE__ */ React5.createElement(CartesianGrid2, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React5.createElement(XAxis2, { dataKey: "date", tick: { fontSize: 11, fill: tickFill } }), /* @__PURE__ */ React5.createElement(YAxis2, { tick: { fontSize: 11, fill: tickFill }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React5.createElement(Tooltip2, { formatter: (v) => [`$${v.toFixed(4)}`, "Spend"] }), /* @__PURE__ */ React5.createElement(Area2, { type: "monotone", dataKey: "spend", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.3 })))), /* @__PURE__ */ React5.createElement(Divider, { sx: { mb: 2 } })) : null, /* @__PURE__ */ React5.createElement(Box5, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React5.createElement(Memory, { fontSize: "small", color: "action" }), /* @__PURE__ */ React5.createElement(Typography5, { variant: "subtitle2", color: "text.secondary" }, "Members")), team.members_with_roles?.length ? /* @__PURE__ */ React5.createElement(TableContainer3, null, /* @__PURE__ */ React5.createElement(Table3, { size: "small" }, /* @__PURE__ */ React5.createElement(TableHead3, null, /* @__PURE__ */ React5.createElement(TableRow3, null, /* @__PURE__ */ React5.createElement(TableCell3, null, "User"), /* @__PURE__ */ React5.createElement(TableCell3, null, "Role"))), /* @__PURE__ */ React5.createElement(TableBody3, null, team.members_with_roles.map((m) => /* @__PURE__ */ React5.createElement(TableRow3, { key: m.user_id }, /* @__PURE__ */ React5.createElement(TableCell3, null, m.user_email ? /* @__PURE__ */ React5.createElement(React5.Fragment, null, /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2" }, m.user_email), /* @__PURE__ */ React5.createElement(
|
|
1328
|
+
Typography5,
|
|
1216
1329
|
{
|
|
1217
1330
|
variant: "caption",
|
|
1218
1331
|
color: "text.secondary",
|
|
1219
1332
|
sx: { fontFamily: "monospace" }
|
|
1220
1333
|
},
|
|
1221
1334
|
m.user_id
|
|
1222
|
-
)) : /* @__PURE__ */
|
|
1335
|
+
)) : /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2", sx: { fontFamily: "monospace" } }, m.user_id)), /* @__PURE__ */ React5.createElement(TableCell3, null, /* @__PURE__ */ React5.createElement(
|
|
1223
1336
|
Chip4,
|
|
1224
1337
|
{
|
|
1225
1338
|
label: m.role,
|
|
1226
1339
|
size: "small",
|
|
1227
1340
|
color: m.role === "admin" ? "primary" : "default"
|
|
1228
1341
|
}
|
|
1229
|
-
))))))) : /* @__PURE__ */
|
|
1342
|
+
))))))) : /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2", color: "text.secondary" }, "No members assigned."))));
|
|
1230
1343
|
};
|
|
1231
1344
|
TeamUsage = ({
|
|
1232
1345
|
teams,
|
|
@@ -1235,12 +1348,12 @@ var init_TeamUsage = __esm({
|
|
|
1235
1348
|
getTeamUsageLoading
|
|
1236
1349
|
}) => {
|
|
1237
1350
|
if (loading) {
|
|
1238
|
-
return /* @__PURE__ */
|
|
1351
|
+
return /* @__PURE__ */ React5.createElement(Paper4, { sx: { p: 2 } }, /* @__PURE__ */ React5.createElement(LinearProgress4, null));
|
|
1239
1352
|
}
|
|
1240
1353
|
if (!teams.length) {
|
|
1241
|
-
return /* @__PURE__ */
|
|
1354
|
+
return /* @__PURE__ */ React5.createElement(Paper4, { sx: { p: 2 } }, /* @__PURE__ */ React5.createElement(Box5, { display: "flex", alignItems: "flex-start", gap: 1.5 }, /* @__PURE__ */ React5.createElement(Group, { color: "disabled", sx: { mt: 0.5 } }), /* @__PURE__ */ React5.createElement(Box5, null, /* @__PURE__ */ React5.createElement(Typography5, { color: "text.secondary", variant: "body2" }, "You're not a member of any LiteLLM team yet."), /* @__PURE__ */ React5.createElement(Typography5, { color: "text.secondary", variant: "body2", mt: 0.5 }, "That's fine \u2014 your account is provisioned, so you can still generate personal keys and use models. If you need a shared budget with colleagues, ask an admin to add you to a team."))));
|
|
1242
1355
|
}
|
|
1243
|
-
return /* @__PURE__ */
|
|
1356
|
+
return /* @__PURE__ */ React5.createElement(Box5, null, /* @__PURE__ */ React5.createElement(Typography5, { variant: "h6", mb: 1 }, "Teams"), teams.map((team) => /* @__PURE__ */ React5.createElement(
|
|
1244
1357
|
TeamCard,
|
|
1245
1358
|
{
|
|
1246
1359
|
key: team.team_id,
|
|
@@ -1254,11 +1367,11 @@ var init_TeamUsage = __esm({
|
|
|
1254
1367
|
});
|
|
1255
1368
|
|
|
1256
1369
|
// src/components/AuditLog.tsx
|
|
1257
|
-
import
|
|
1370
|
+
import React6, { useState as useState5, useCallback } from "react";
|
|
1258
1371
|
import {
|
|
1259
1372
|
Paper as Paper5,
|
|
1260
|
-
Box as
|
|
1261
|
-
Typography as
|
|
1373
|
+
Box as Box6,
|
|
1374
|
+
Typography as Typography6,
|
|
1262
1375
|
Table as Table4,
|
|
1263
1376
|
TableBody as TableBody4,
|
|
1264
1377
|
TableCell as TableCell4,
|
|
@@ -1266,13 +1379,13 @@ import {
|
|
|
1266
1379
|
TableHead as TableHead4,
|
|
1267
1380
|
TableRow as TableRow4,
|
|
1268
1381
|
TablePagination,
|
|
1269
|
-
TextField as
|
|
1382
|
+
TextField as TextField3,
|
|
1270
1383
|
MenuItem as MenuItem3,
|
|
1271
1384
|
Chip as Chip5,
|
|
1272
|
-
CircularProgress as
|
|
1385
|
+
CircularProgress as CircularProgress4,
|
|
1273
1386
|
Collapse as Collapse2,
|
|
1274
|
-
IconButton as
|
|
1275
|
-
Alert
|
|
1387
|
+
IconButton as IconButton4,
|
|
1388
|
+
Alert as Alert2
|
|
1276
1389
|
} from "@mui/material";
|
|
1277
1390
|
import { KeyboardArrowDown, KeyboardArrowUp } from "@mui/icons-material";
|
|
1278
1391
|
import { useAsync } from "react-use";
|
|
@@ -1302,14 +1415,14 @@ var init_AuditLog = __esm({
|
|
|
1302
1415
|
unblocked: "success"
|
|
1303
1416
|
};
|
|
1304
1417
|
DetailRow = ({ entry }) => {
|
|
1305
|
-
const [open, setOpen] =
|
|
1418
|
+
const [open, setOpen] = useState5(false);
|
|
1306
1419
|
const hasDetail = entry.before_value || entry.updated_values;
|
|
1307
|
-
return /* @__PURE__ */
|
|
1420
|
+
return /* @__PURE__ */ React6.createElement(React6.Fragment, null, /* @__PURE__ */ React6.createElement(TableRow4, { hover: true }, /* @__PURE__ */ React6.createElement(TableCell4, { sx: { width: 40, pr: 0 } }, hasDetail && /* @__PURE__ */ React6.createElement(IconButton4, { size: "small", onClick: () => setOpen((o) => !o) }, open ? /* @__PURE__ */ React6.createElement(KeyboardArrowUp, { fontSize: "small" }) : /* @__PURE__ */ React6.createElement(KeyboardArrowDown, { fontSize: "small" }))), /* @__PURE__ */ React6.createElement(TableCell4, { sx: { whiteSpace: "nowrap" } }, formatDateTime(entry.updated_at)), /* @__PURE__ */ React6.createElement(TableCell4, null, entry.action && /* @__PURE__ */ React6.createElement(Chip5, { label: entry.action, color: actionColor(entry.action), size: "small" })), /* @__PURE__ */ React6.createElement(TableCell4, null, entry.table_name ?? "-"), /* @__PURE__ */ React6.createElement(TableCell4, null, /* @__PURE__ */ React6.createElement(Typography6, { 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__ */ React6.createElement(TableCell4, null, entry.changed_by ?? "-")), hasDetail && /* @__PURE__ */ React6.createElement(TableRow4, null, /* @__PURE__ */ React6.createElement(TableCell4, { colSpan: 6, sx: { py: 0 } }, /* @__PURE__ */ React6.createElement(Collapse2, { in: open, unmountOnExit: true }, /* @__PURE__ */ React6.createElement(Box6, { p: 2, display: "flex", gap: 2, flexWrap: "wrap" }, entry.before_value && /* @__PURE__ */ React6.createElement(Box6, { flex: 1, minWidth: 200 }, /* @__PURE__ */ React6.createElement(Typography6, { variant: "caption", color: "text.secondary", display: "block", mb: 0.5 }, "Before"), /* @__PURE__ */ React6.createElement(Typography6, { component: "pre", variant: "caption", sx: { fontFamily: "monospace", whiteSpace: "pre-wrap", wordBreak: "break-all" } }, JSON.stringify(entry.before_value, null, 2))), entry.updated_values && /* @__PURE__ */ React6.createElement(Box6, { flex: 1, minWidth: 200 }, /* @__PURE__ */ React6.createElement(Typography6, { variant: "caption", color: "text.secondary", display: "block", mb: 0.5 }, "After"), /* @__PURE__ */ React6.createElement(Typography6, { component: "pre", variant: "caption", sx: { fontFamily: "monospace", whiteSpace: "pre-wrap", wordBreak: "break-all" } }, JSON.stringify(entry.updated_values, null, 2))))))));
|
|
1308
1421
|
};
|
|
1309
1422
|
AuditLog = ({ api }) => {
|
|
1310
|
-
const [page, setPage] =
|
|
1311
|
-
const [pageSize, setPageSize] =
|
|
1312
|
-
const [filters, setFilters] =
|
|
1423
|
+
const [page, setPage] = useState5(0);
|
|
1424
|
+
const [pageSize, setPageSize] = useState5(25);
|
|
1425
|
+
const [filters, setFilters] = useState5({});
|
|
1313
1426
|
const fetchParams = useCallback(
|
|
1314
1427
|
() => ({ page: page + 1, page_size: pageSize, ...filters }),
|
|
1315
1428
|
[page, pageSize, filters]
|
|
@@ -1320,8 +1433,8 @@ var init_AuditLog = __esm({
|
|
|
1320
1433
|
);
|
|
1321
1434
|
const entries = value?.audit_logs ?? [];
|
|
1322
1435
|
const total = value?.total ?? 0;
|
|
1323
|
-
return /* @__PURE__ */
|
|
1324
|
-
|
|
1436
|
+
return /* @__PURE__ */ React6.createElement(Paper5, null, /* @__PURE__ */ React6.createElement(Box6, { p: 2, display: "flex", gap: 2, flexWrap: "wrap", alignItems: "center" }, /* @__PURE__ */ React6.createElement(Typography6, { variant: "h6", sx: { flex: "0 0 auto", mr: 1 } }, "Audit Log"), /* @__PURE__ */ React6.createElement(
|
|
1437
|
+
TextField3,
|
|
1325
1438
|
{
|
|
1326
1439
|
size: "small",
|
|
1327
1440
|
label: "Action",
|
|
@@ -1333,13 +1446,13 @@ var init_AuditLog = __esm({
|
|
|
1333
1446
|
},
|
|
1334
1447
|
sx: { minWidth: 160 }
|
|
1335
1448
|
},
|
|
1336
|
-
/* @__PURE__ */
|
|
1337
|
-
/* @__PURE__ */
|
|
1338
|
-
/* @__PURE__ */
|
|
1339
|
-
/* @__PURE__ */
|
|
1340
|
-
/* @__PURE__ */
|
|
1341
|
-
), /* @__PURE__ */
|
|
1342
|
-
|
|
1449
|
+
/* @__PURE__ */ React6.createElement(MenuItem3, { value: "" }, "All actions"),
|
|
1450
|
+
/* @__PURE__ */ React6.createElement(MenuItem3, { value: "created" }, "Created"),
|
|
1451
|
+
/* @__PURE__ */ React6.createElement(MenuItem3, { value: "updated" }, "Updated"),
|
|
1452
|
+
/* @__PURE__ */ React6.createElement(MenuItem3, { value: "deleted" }, "Deleted"),
|
|
1453
|
+
/* @__PURE__ */ React6.createElement(MenuItem3, { value: "blocked" }, "Blocked")
|
|
1454
|
+
), /* @__PURE__ */ React6.createElement(
|
|
1455
|
+
TextField3,
|
|
1343
1456
|
{
|
|
1344
1457
|
size: "small",
|
|
1345
1458
|
label: "Table",
|
|
@@ -1351,12 +1464,12 @@ var init_AuditLog = __esm({
|
|
|
1351
1464
|
},
|
|
1352
1465
|
sx: { minWidth: 160 }
|
|
1353
1466
|
},
|
|
1354
|
-
/* @__PURE__ */
|
|
1355
|
-
/* @__PURE__ */
|
|
1356
|
-
/* @__PURE__ */
|
|
1357
|
-
/* @__PURE__ */
|
|
1358
|
-
), /* @__PURE__ */
|
|
1359
|
-
|
|
1467
|
+
/* @__PURE__ */ React6.createElement(MenuItem3, { value: "" }, "All tables"),
|
|
1468
|
+
/* @__PURE__ */ React6.createElement(MenuItem3, { value: "LiteLLM_VerificationToken" }, "Key"),
|
|
1469
|
+
/* @__PURE__ */ React6.createElement(MenuItem3, { value: "LiteLLM_TeamTable" }, "Team"),
|
|
1470
|
+
/* @__PURE__ */ React6.createElement(MenuItem3, { value: "LiteLLM_UserTable" }, "User")
|
|
1471
|
+
), /* @__PURE__ */ React6.createElement(
|
|
1472
|
+
TextField3,
|
|
1360
1473
|
{
|
|
1361
1474
|
size: "small",
|
|
1362
1475
|
label: "Changed by",
|
|
@@ -1367,7 +1480,7 @@ var init_AuditLog = __esm({
|
|
|
1367
1480
|
},
|
|
1368
1481
|
sx: { minWidth: 200 }
|
|
1369
1482
|
}
|
|
1370
|
-
)), error && /* @__PURE__ */
|
|
1483
|
+
)), error && /* @__PURE__ */ React6.createElement(Box6, { px: 2, pb: 2 }, /* @__PURE__ */ React6.createElement(Alert2, { severity: "error" }, error.message)), /* @__PURE__ */ React6.createElement(TableContainer4, null, /* @__PURE__ */ React6.createElement(Table4, { size: "small" }, /* @__PURE__ */ React6.createElement(TableHead4, null, /* @__PURE__ */ React6.createElement(TableRow4, null, /* @__PURE__ */ React6.createElement(TableCell4, { sx: { width: 40 } }), /* @__PURE__ */ React6.createElement(TableCell4, null, "Time"), /* @__PURE__ */ React6.createElement(TableCell4, null, "Action"), /* @__PURE__ */ React6.createElement(TableCell4, null, "Table"), /* @__PURE__ */ React6.createElement(TableCell4, null, "Object ID"), /* @__PURE__ */ React6.createElement(TableCell4, null, "Changed By"))), /* @__PURE__ */ React6.createElement(TableBody4, null, loading ? /* @__PURE__ */ React6.createElement(TableRow4, null, /* @__PURE__ */ React6.createElement(TableCell4, { colSpan: 6, align: "center", sx: { py: 4 } }, /* @__PURE__ */ React6.createElement(CircularProgress4, { size: 24 }))) : entries.length === 0 ? /* @__PURE__ */ React6.createElement(TableRow4, null, /* @__PURE__ */ React6.createElement(TableCell4, { colSpan: 6, align: "center", sx: { py: 4 } }, /* @__PURE__ */ React6.createElement(Typography6, { color: "text.secondary" }, "No audit events found"))) : entries.map((entry) => /* @__PURE__ */ React6.createElement(DetailRow, { key: entry.id, entry }))))), /* @__PURE__ */ React6.createElement(
|
|
1371
1484
|
TablePagination,
|
|
1372
1485
|
{
|
|
1373
1486
|
component: "div",
|
|
@@ -1391,8 +1504,8 @@ var LiteLLMPage_exports = {};
|
|
|
1391
1504
|
__export(LiteLLMPage_exports, {
|
|
1392
1505
|
LiteLLMPage: () => LiteLLMPage
|
|
1393
1506
|
});
|
|
1394
|
-
import
|
|
1395
|
-
import { Box as
|
|
1507
|
+
import React7, { useState as useState6, useCallback as useCallback2, useMemo as useMemo5 } from "react";
|
|
1508
|
+
import { Box as Box7, Snackbar, Alert as Alert3, CircularProgress as CircularProgress5, Typography as Typography7, Paper as Paper6, Tabs as Tabs3, Tab as Tab3 } from "@mui/material";
|
|
1396
1509
|
import { useAsync as useAsync2, useAsyncRetry } from "react-use";
|
|
1397
1510
|
import { useApi } from "@backstage/core-plugin-api";
|
|
1398
1511
|
function initDateRange() {
|
|
@@ -1415,6 +1528,7 @@ var init_LiteLLMPage = __esm({
|
|
|
1415
1528
|
"use strict";
|
|
1416
1529
|
init_DashboardHeader();
|
|
1417
1530
|
init_KeysTable();
|
|
1531
|
+
init_GenerateKeyDialog();
|
|
1418
1532
|
init_UsageStats();
|
|
1419
1533
|
init_TeamUsage();
|
|
1420
1534
|
init_AuditLog();
|
|
@@ -1422,11 +1536,12 @@ var init_LiteLLMPage = __esm({
|
|
|
1422
1536
|
PERIOD_LS_KEY2 = "litellm_usage_period";
|
|
1423
1537
|
LiteLLMPage = () => {
|
|
1424
1538
|
const api = useApi(liteLlmApiRef);
|
|
1425
|
-
const [dateRange, setDateRange] =
|
|
1426
|
-
const [activeTab, setActiveTab] =
|
|
1427
|
-
const [snackbar, setSnackbar] =
|
|
1428
|
-
const [
|
|
1429
|
-
const [
|
|
1539
|
+
const [dateRange, setDateRange] = useState6(initDateRange);
|
|
1540
|
+
const [activeTab, setActiveTab] = useState6("overview");
|
|
1541
|
+
const [snackbar, setSnackbar] = useState6(null);
|
|
1542
|
+
const [generateDialogOpen, setGenerateDialogOpen] = useState6(false);
|
|
1543
|
+
const [teamUsageCache, setTeamUsageCache] = useState6({});
|
|
1544
|
+
const [teamUsageLoading, setTeamUsageLoading] = useState6({});
|
|
1430
1545
|
const { value: userInfo, loading: userLoading, error: userError } = useAsync2(
|
|
1431
1546
|
() => api.getUserInfo(),
|
|
1432
1547
|
[api]
|
|
@@ -1451,7 +1566,7 @@ var init_LiteLLMPage = __esm({
|
|
|
1451
1566
|
[api]
|
|
1452
1567
|
);
|
|
1453
1568
|
const { value: liteLlmConfig } = useAsync2(() => api.getConfig(), [api]);
|
|
1454
|
-
const teams =
|
|
1569
|
+
const teams = useMemo5(() => {
|
|
1455
1570
|
if (!allTeams?.length) return [];
|
|
1456
1571
|
if (!userInfo) return allTeams;
|
|
1457
1572
|
const userId = userInfo.user_id;
|
|
@@ -1487,7 +1602,7 @@ var init_LiteLLMPage = __esm({
|
|
|
1487
1602
|
setTeamUsageLoading((prev) => ({ ...prev, [teamId]: false }));
|
|
1488
1603
|
}
|
|
1489
1604
|
}, [api, dateRange, teamUsageCache, teamUsageLoading]);
|
|
1490
|
-
const allowedModels =
|
|
1605
|
+
const allowedModels = useMemo5(() => {
|
|
1491
1606
|
if (!allModels?.length) return [];
|
|
1492
1607
|
const userModels = userInfo?.models;
|
|
1493
1608
|
const teamModels = teams?.flatMap((t) => t.models ?? []);
|
|
@@ -1595,25 +1710,34 @@ var init_LiteLLMPage = __esm({
|
|
|
1595
1710
|
);
|
|
1596
1711
|
const isInitialLoading = userLoading && !userInfo;
|
|
1597
1712
|
if (isInitialLoading) {
|
|
1598
|
-
return /* @__PURE__ */
|
|
1713
|
+
return /* @__PURE__ */ React7.createElement(Box7, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "50vh" }, /* @__PURE__ */ React7.createElement(CircularProgress5, null));
|
|
1599
1714
|
}
|
|
1600
1715
|
if (userError || !userInfo) {
|
|
1601
1716
|
const isProvisioningEnabled = userError?.body?.provisioning === true;
|
|
1602
1717
|
const hint = userError?.body?.hint;
|
|
1603
|
-
return /* @__PURE__ */
|
|
1718
|
+
return /* @__PURE__ */ React7.createElement(Box7, { p: 3 }, /* @__PURE__ */ React7.createElement(Paper6, { sx: { p: 3 } }, /* @__PURE__ */ React7.createElement(Typography7, { variant: "h6", gutterBottom: true }, "Account not provisioned"), /* @__PURE__ */ React7.createElement(Typography7, { color: "text.secondary", paragraph: true }, "Your Backstage account is not linked to a LiteLLM user."), hint ? /* @__PURE__ */ React7.createElement(Typography7, { variant: "body2", color: "text.secondary" }, hint) : /* @__PURE__ */ React7.createElement(Typography7, { 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.")));
|
|
1604
1719
|
}
|
|
1605
|
-
return /* @__PURE__ */
|
|
1720
|
+
return /* @__PURE__ */ React7.createElement(Box7, { p: 3 }, /* @__PURE__ */ React7.createElement(Box7, { mb: 2 }, /* @__PURE__ */ React7.createElement(
|
|
1721
|
+
DashboardHeader,
|
|
1722
|
+
{
|
|
1723
|
+
userInfo,
|
|
1724
|
+
teams: teams ?? [],
|
|
1725
|
+
keys: keys ?? [],
|
|
1726
|
+
loading: userLoading || teamsLoading,
|
|
1727
|
+
onGenerateKeyClick: () => setGenerateDialogOpen(true)
|
|
1728
|
+
}
|
|
1729
|
+
)), /* @__PURE__ */ React7.createElement(
|
|
1606
1730
|
Tabs3,
|
|
1607
1731
|
{
|
|
1608
1732
|
value: activeTab,
|
|
1609
1733
|
onChange: (_, v) => setActiveTab(v),
|
|
1610
1734
|
sx: { mb: 2, borderBottom: 1, borderColor: "divider" }
|
|
1611
1735
|
},
|
|
1612
|
-
/* @__PURE__ */
|
|
1613
|
-
/* @__PURE__ */
|
|
1614
|
-
/* @__PURE__ */
|
|
1615
|
-
userInfo.can_view_audit && /* @__PURE__ */
|
|
1616
|
-
), activeTab === "overview" && /* @__PURE__ */
|
|
1736
|
+
/* @__PURE__ */ React7.createElement(Tab3, { label: "Overview", value: "overview" }),
|
|
1737
|
+
/* @__PURE__ */ React7.createElement(Tab3, { label: "Keys", value: "keys" }),
|
|
1738
|
+
/* @__PURE__ */ React7.createElement(Tab3, { label: "Teams", value: "teams" }),
|
|
1739
|
+
userInfo.can_view_audit && /* @__PURE__ */ React7.createElement(Tab3, { label: "Audit Log", value: "audit" })
|
|
1740
|
+
), activeTab === "overview" && /* @__PURE__ */ React7.createElement(
|
|
1617
1741
|
UsageStats,
|
|
1618
1742
|
{
|
|
1619
1743
|
usage: usage ?? null,
|
|
@@ -1623,25 +1747,21 @@ var init_LiteLLMPage = __esm({
|
|
|
1623
1747
|
loading: usageLoading,
|
|
1624
1748
|
userInfo
|
|
1625
1749
|
}
|
|
1626
|
-
), activeTab === "keys" && /* @__PURE__ */
|
|
1750
|
+
), activeTab === "keys" && /* @__PURE__ */ React7.createElement(
|
|
1627
1751
|
KeysTable,
|
|
1628
1752
|
{
|
|
1629
1753
|
keys: keys ?? [],
|
|
1630
1754
|
models: allowedModels,
|
|
1631
|
-
teams: teams ?? [],
|
|
1632
|
-
username: userInfo.user_id,
|
|
1633
|
-
keyGenerationSettings: liteLlmConfig?.keyGeneration,
|
|
1634
1755
|
loading: keysLoading || modelsLoading,
|
|
1635
|
-
|
|
1756
|
+
onGenerateKeyClick: () => setGenerateDialogOpen(true),
|
|
1636
1757
|
onUpdateKey: handleUpdateKey,
|
|
1637
1758
|
onBlockKey: handleBlockKey,
|
|
1638
1759
|
onUnblockKey: handleUnblockKey,
|
|
1639
1760
|
onResetKeySpend: handleResetKeySpend,
|
|
1640
1761
|
onDeleteKey: handleDeleteKey,
|
|
1641
|
-
onPruneExpiredKeys: handlePruneExpiredKeys
|
|
1642
|
-
onGetConfig: () => api.getConfig()
|
|
1762
|
+
onPruneExpiredKeys: handlePruneExpiredKeys
|
|
1643
1763
|
}
|
|
1644
|
-
), activeTab === "teams" && /* @__PURE__ */
|
|
1764
|
+
), activeTab === "teams" && /* @__PURE__ */ React7.createElement(
|
|
1645
1765
|
TeamUsage,
|
|
1646
1766
|
{
|
|
1647
1767
|
teams: teams ?? [],
|
|
@@ -1652,7 +1772,20 @@ var init_LiteLLMPage = __esm({
|
|
|
1652
1772
|
},
|
|
1653
1773
|
getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false
|
|
1654
1774
|
}
|
|
1655
|
-
), activeTab === "audit" && userInfo.can_view_audit && /* @__PURE__ */
|
|
1775
|
+
), activeTab === "audit" && userInfo.can_view_audit && /* @__PURE__ */ React7.createElement(AuditLog, { api }), /* @__PURE__ */ React7.createElement(
|
|
1776
|
+
GenerateKeyDialog,
|
|
1777
|
+
{
|
|
1778
|
+
open: generateDialogOpen,
|
|
1779
|
+
onClose: () => setGenerateDialogOpen(false),
|
|
1780
|
+
keys: keys ?? [],
|
|
1781
|
+
models: allowedModels,
|
|
1782
|
+
teams: teams ?? [],
|
|
1783
|
+
username: userInfo.user_id,
|
|
1784
|
+
keyGenerationSettings: liteLlmConfig?.keyGeneration,
|
|
1785
|
+
onGenerateKey: handleGenerateKey,
|
|
1786
|
+
onGetConfig: () => api.getConfig()
|
|
1787
|
+
}
|
|
1788
|
+
), /* @__PURE__ */ React7.createElement(
|
|
1656
1789
|
Snackbar,
|
|
1657
1790
|
{
|
|
1658
1791
|
open: !!snackbar,
|
|
@@ -1660,7 +1793,7 @@ var init_LiteLLMPage = __esm({
|
|
|
1660
1793
|
onClose: () => setSnackbar(null),
|
|
1661
1794
|
anchorOrigin: { vertical: "bottom", horizontal: "right" }
|
|
1662
1795
|
},
|
|
1663
|
-
snackbar ? /* @__PURE__ */
|
|
1796
|
+
snackbar ? /* @__PURE__ */ React7.createElement(Alert3, { severity: snackbar.severity, onClose: () => setSnackbar(null) }, snackbar.message) : void 0
|
|
1664
1797
|
));
|
|
1665
1798
|
};
|
|
1666
1799
|
}
|
|
@@ -1668,7 +1801,7 @@ var init_LiteLLMPage = __esm({
|
|
|
1668
1801
|
|
|
1669
1802
|
// src/plugin.tsx
|
|
1670
1803
|
init_api();
|
|
1671
|
-
import
|
|
1804
|
+
import React8 from "react";
|
|
1672
1805
|
import { TrendingUp as TrendingUpIcon } from "@mui/icons-material";
|
|
1673
1806
|
import {
|
|
1674
1807
|
createFrontendPlugin,
|
|
@@ -1687,10 +1820,10 @@ var liteLlmPage = PageBlueprint.make({
|
|
|
1687
1820
|
params: {
|
|
1688
1821
|
path: "/litellm",
|
|
1689
1822
|
title: "LiteLLM",
|
|
1690
|
-
icon: /* @__PURE__ */
|
|
1823
|
+
icon: /* @__PURE__ */ React8.createElement(TrendingUpIcon, null),
|
|
1691
1824
|
loader: async () => {
|
|
1692
1825
|
const { LiteLLMPage: LiteLLMPage2 } = await Promise.resolve().then(() => (init_LiteLLMPage(), LiteLLMPage_exports));
|
|
1693
|
-
return /* @__PURE__ */
|
|
1826
|
+
return /* @__PURE__ */ React8.createElement(LiteLLMPage2, null);
|
|
1694
1827
|
}
|
|
1695
1828
|
}
|
|
1696
1829
|
});
|
|
@@ -1709,17 +1842,17 @@ init_TeamUsage();
|
|
|
1709
1842
|
// src/components/LiteLLMHomeWidget.tsx
|
|
1710
1843
|
init_api();
|
|
1711
1844
|
init_format();
|
|
1712
|
-
import
|
|
1845
|
+
import React9, { useState as useState7, useEffect as useEffect2 } from "react";
|
|
1713
1846
|
import {
|
|
1714
1847
|
Paper as Paper7,
|
|
1715
|
-
Box as
|
|
1716
|
-
Typography as
|
|
1848
|
+
Box as Box8,
|
|
1849
|
+
Typography as Typography8,
|
|
1717
1850
|
FormControl as FormControl2,
|
|
1718
1851
|
Select as Select2,
|
|
1719
1852
|
MenuItem as MenuItem4,
|
|
1720
1853
|
Grid as Grid2,
|
|
1721
|
-
CircularProgress as
|
|
1722
|
-
Alert as
|
|
1854
|
+
CircularProgress as CircularProgress6,
|
|
1855
|
+
Alert as Alert4
|
|
1723
1856
|
} from "@mui/material";
|
|
1724
1857
|
import { AreaChart as AreaChart3, Area as Area3, ResponsiveContainer as ResponsiveContainer3 } from "recharts";
|
|
1725
1858
|
import { useApi as useApi2 } from "@backstage/core-plugin-api";
|
|
@@ -1735,19 +1868,19 @@ function presetToDateRange(preset) {
|
|
|
1735
1868
|
}
|
|
1736
1869
|
return { start, end };
|
|
1737
1870
|
}
|
|
1738
|
-
var Kpi = ({ label, value }) => /* @__PURE__ */
|
|
1871
|
+
var Kpi = ({ label, value }) => /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(Typography8, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ React9.createElement(Typography8, { variant: "subtitle1", fontWeight: 600 }, value));
|
|
1739
1872
|
var LiteLLMHomeWidget = ({
|
|
1740
1873
|
defaultPeriod = "7d",
|
|
1741
1874
|
title = "LiteLLM Usage"
|
|
1742
1875
|
}) => {
|
|
1743
1876
|
const api = useApi2(liteLlmApiRef);
|
|
1744
|
-
const [period, setPeriod] =
|
|
1745
|
-
const [loading, setLoading] =
|
|
1746
|
-
const [usageError, setUsageError] =
|
|
1747
|
-
const [keysError, setKeysError] =
|
|
1748
|
-
const [usage, setUsage] =
|
|
1749
|
-
const [keys, setKeys] =
|
|
1750
|
-
|
|
1877
|
+
const [period, setPeriod] = useState7(defaultPeriod);
|
|
1878
|
+
const [loading, setLoading] = useState7(true);
|
|
1879
|
+
const [usageError, setUsageError] = useState7(null);
|
|
1880
|
+
const [keysError, setKeysError] = useState7(null);
|
|
1881
|
+
const [usage, setUsage] = useState7(null);
|
|
1882
|
+
const [keys, setKeys] = useState7([]);
|
|
1883
|
+
useEffect2(() => {
|
|
1751
1884
|
let cancelled = false;
|
|
1752
1885
|
setLoading(true);
|
|
1753
1886
|
setUsageError(null);
|
|
@@ -1784,17 +1917,17 @@ var LiteLLMHomeWidget = ({
|
|
|
1784
1917
|
spend: d.spend
|
|
1785
1918
|
}));
|
|
1786
1919
|
const hasSparkline = dailyData.length > 0;
|
|
1787
|
-
return /* @__PURE__ */
|
|
1920
|
+
return /* @__PURE__ */ React9.createElement(Paper7, { sx: { p: 2 } }, /* @__PURE__ */ React9.createElement(Box8, { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1.5 }, /* @__PURE__ */ React9.createElement(Typography8, { variant: "h6" }, title), /* @__PURE__ */ React9.createElement(FormControl2, { size: "small", sx: { minWidth: 90 } }, /* @__PURE__ */ React9.createElement(
|
|
1788
1921
|
Select2,
|
|
1789
1922
|
{
|
|
1790
1923
|
value: period,
|
|
1791
1924
|
onChange: (e) => setPeriod(e.target.value),
|
|
1792
1925
|
displayEmpty: true
|
|
1793
1926
|
},
|
|
1794
|
-
/* @__PURE__ */
|
|
1795
|
-
/* @__PURE__ */
|
|
1796
|
-
/* @__PURE__ */
|
|
1797
|
-
))), loading && /* @__PURE__ */
|
|
1927
|
+
/* @__PURE__ */ React9.createElement(MenuItem4, { value: "today" }, "Today"),
|
|
1928
|
+
/* @__PURE__ */ React9.createElement(MenuItem4, { value: "7d" }, "7d"),
|
|
1929
|
+
/* @__PURE__ */ React9.createElement(MenuItem4, { value: "30d" }, "30d")
|
|
1930
|
+
))), loading && /* @__PURE__ */ React9.createElement(Box8, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ React9.createElement(CircularProgress6, { size: 32 })), !loading && totalFailure && /* @__PURE__ */ React9.createElement(Alert4, { severity: "error", sx: { mt: 1 } }, usageError ?? "Failed to load usage data"), !loading && !totalFailure && /* @__PURE__ */ React9.createElement(React9.Fragment, null, partialFailure && /* @__PURE__ */ React9.createElement(Alert4, { severity: "warning", sx: { mt: 1, mb: 1 } }, usageError ? `Usage data unavailable (${usageError}).` : "", keysError ? ` Key list unavailable (${keysError}).` : "", " Showing what loaded."), /* @__PURE__ */ React9.createElement(Grid2, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ React9.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React9.createElement(Kpi, { label: "USD Spent", value: fmtUsd(usage?.total_spend ?? 0) })), /* @__PURE__ */ React9.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React9.createElement(Kpi, { label: "Tokens In", value: fmtInt(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ React9.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React9.createElement(Kpi, { label: "Tokens Out", value: fmtInt(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ React9.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React9.createElement(Kpi, { label: "Keys", value: fmtInt(keys.length) }))), hasSparkline && /* @__PURE__ */ React9.createElement(Box8, { height: 120 }, /* @__PURE__ */ React9.createElement(ResponsiveContainer3, { width: "100%", height: "100%" }, /* @__PURE__ */ React9.createElement(AreaChart3, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 } }, /* @__PURE__ */ React9.createElement(
|
|
1798
1931
|
Area3,
|
|
1799
1932
|
{
|
|
1800
1933
|
type: "monotone",
|