@acarmisc/backstage-plugin-litellm 0.10.2 → 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 -5
- package/dist/index.cjs.js +560 -397
- package/dist/index.cjs.js.map +4 -4
- package/dist/index.esm.js +564 -388
- package/dist/index.esm.js.map +4 -4
- package/dist/types.d.ts +7 -0
- 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,39 +276,11 @@ 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, 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)}`;
|
|
@@ -306,16 +297,6 @@ var init_KeysTable = __esm({
|
|
|
306
297
|
return dateStr;
|
|
307
298
|
}
|
|
308
299
|
};
|
|
309
|
-
emptyForm = () => ({
|
|
310
|
-
alias: "",
|
|
311
|
-
models: [],
|
|
312
|
-
duration: "30d",
|
|
313
|
-
max_budget: 100,
|
|
314
|
-
tpm_limit: void 0,
|
|
315
|
-
rpm_limit: void 0,
|
|
316
|
-
team_id: void 0,
|
|
317
|
-
key_type: "llm_api"
|
|
318
|
-
});
|
|
319
300
|
keyToEditForm = (k) => ({
|
|
320
301
|
key_alias: k.key_alias ?? "",
|
|
321
302
|
models: k.models ?? [],
|
|
@@ -326,24 +307,15 @@ var init_KeysTable = __esm({
|
|
|
326
307
|
KeysTable = ({
|
|
327
308
|
keys,
|
|
328
309
|
models,
|
|
329
|
-
teams,
|
|
330
310
|
loading,
|
|
331
|
-
|
|
311
|
+
onGenerateKeyClick,
|
|
332
312
|
onUpdateKey,
|
|
333
313
|
onBlockKey,
|
|
334
314
|
onUnblockKey,
|
|
335
315
|
onResetKeySpend,
|
|
336
316
|
onDeleteKey,
|
|
337
|
-
onPruneExpiredKeys
|
|
338
|
-
onGetConfig
|
|
317
|
+
onPruneExpiredKeys
|
|
339
318
|
}) => {
|
|
340
|
-
const [generateModalOpen, setGenerateModalOpen] = useState(false);
|
|
341
|
-
const [newKeyValue, setNewKeyValue] = useState(null);
|
|
342
|
-
const [newKeySnippets, setNewKeySnippets] = useState(null);
|
|
343
|
-
const [newKeyModel, setNewKeyModel] = useState("");
|
|
344
|
-
const [formData, setFormData] = useState(emptyForm());
|
|
345
|
-
const [unlimitedBudget, setUnlimitedBudget] = useState(false);
|
|
346
|
-
const [submitting, setSubmitting] = useState(false);
|
|
347
319
|
const [editingKey, setEditingKey] = useState(null);
|
|
348
320
|
const [editForm, setEditForm] = useState({});
|
|
349
321
|
const [editSubmitting, setEditSubmitting] = useState(false);
|
|
@@ -353,58 +325,14 @@ var init_KeysTable = __esm({
|
|
|
353
325
|
const [resetSpendConfirm, setResetSpendConfirm] = useState(false);
|
|
354
326
|
const [resetSpendSubmitting, setResetSpendSubmitting] = useState(false);
|
|
355
327
|
const [filterText, setFilterText] = useState("");
|
|
356
|
-
const filteredKeys =
|
|
328
|
+
const filteredKeys = useMemo2(() => {
|
|
357
329
|
if (!filterText.trim()) return keys;
|
|
358
330
|
const q = filterText.toLowerCase();
|
|
359
331
|
return keys.filter(
|
|
360
332
|
(k) => (k.key_alias ?? "").toLowerCase().includes(q) || k.models?.some((m) => m.toLowerCase().includes(q))
|
|
361
333
|
);
|
|
362
334
|
}, [keys, filterText]);
|
|
363
|
-
const selectedModels = models.filter((m) => (formData.models || []).includes(m.model_name));
|
|
364
|
-
const selectedTeam = teams.find((t) => t.team_id === formData.team_id) ?? null;
|
|
365
335
|
const editSelectedModels = models.filter((m) => (editForm.models || []).includes(m.model_name));
|
|
366
|
-
const aliasError = !(formData.alias || "").trim();
|
|
367
|
-
const budgetInvalid = !unlimitedBudget && (formData.max_budget === void 0 || formData.max_budget === null || formData.max_budget <= 0);
|
|
368
|
-
const canGenerate = !aliasError && !budgetInvalid && !submitting;
|
|
369
|
-
const budgetEstimate = useMemo(() => {
|
|
370
|
-
if (unlimitedBudget || budgetInvalid) return null;
|
|
371
|
-
const inputCosts = selectedModels.map((m) => m.input_cost_per_token).filter((c) => typeof c === "number" && c > 0);
|
|
372
|
-
if (inputCosts.length === 0) return null;
|
|
373
|
-
const pricePerToken = Math.max(...inputCosts);
|
|
374
|
-
return estimateTokensFromBudget(formData.max_budget ?? 0, pricePerToken);
|
|
375
|
-
}, [formData.max_budget, selectedModels, unlimitedBudget, budgetInvalid]);
|
|
376
|
-
const handleGenerate = async () => {
|
|
377
|
-
setSubmitting(true);
|
|
378
|
-
try {
|
|
379
|
-
const request = {
|
|
380
|
-
...formData,
|
|
381
|
-
max_budget: unlimitedBudget ? null : formData.max_budget
|
|
382
|
-
};
|
|
383
|
-
const response = await onGenerateKey(request);
|
|
384
|
-
setNewKeyValue(response.key);
|
|
385
|
-
setNewKeyModel(formData.models?.[0] ?? "");
|
|
386
|
-
setNewKeySnippets(null);
|
|
387
|
-
try {
|
|
388
|
-
const config = await onGetConfig();
|
|
389
|
-
const model = formData.models?.[0] ?? "";
|
|
390
|
-
setNewKeySnippets(buildSnippets(config.baseUrl, response.key, model));
|
|
391
|
-
} catch {
|
|
392
|
-
}
|
|
393
|
-
setFormData(emptyForm());
|
|
394
|
-
setUnlimitedBudget(false);
|
|
395
|
-
} catch (error) {
|
|
396
|
-
console.error("Failed to generate key:", error);
|
|
397
|
-
} finally {
|
|
398
|
-
setSubmitting(false);
|
|
399
|
-
}
|
|
400
|
-
};
|
|
401
|
-
const handleCloseModal = () => {
|
|
402
|
-
setGenerateModalOpen(false);
|
|
403
|
-
setNewKeyValue(null);
|
|
404
|
-
setNewKeySnippets(null);
|
|
405
|
-
setFormData(emptyForm());
|
|
406
|
-
setUnlimitedBudget(false);
|
|
407
|
-
};
|
|
408
336
|
const handleOpenEdit = (k) => {
|
|
409
337
|
setEditingKey(k);
|
|
410
338
|
setEditForm(keyToEditForm(k));
|
|
@@ -465,7 +393,7 @@ var init_KeysTable = __esm({
|
|
|
465
393
|
};
|
|
466
394
|
const [pruneConfirmCount, setPruneConfirmCount] = useState(null);
|
|
467
395
|
const [pruneSubmitting, setPruneSubmitting] = useState(false);
|
|
468
|
-
const expiredKeys =
|
|
396
|
+
const expiredKeys = useMemo2(() => {
|
|
469
397
|
return keys.filter((k) => expiryStatus(k.expires_at) === "expired");
|
|
470
398
|
}, [keys]);
|
|
471
399
|
const handlePruneExpired = async () => {
|
|
@@ -499,7 +427,7 @@ var init_KeysTable = __esm({
|
|
|
499
427
|
}
|
|
500
428
|
}
|
|
501
429
|
), expiredKeys.length > 0 && /* @__PURE__ */ React2.createElement(
|
|
502
|
-
|
|
430
|
+
Button2,
|
|
503
431
|
{
|
|
504
432
|
variant: "outlined",
|
|
505
433
|
color: "error",
|
|
@@ -510,22 +438,22 @@ var init_KeysTable = __esm({
|
|
|
510
438
|
expiredKeys.length,
|
|
511
439
|
")"
|
|
512
440
|
), /* @__PURE__ */ React2.createElement(
|
|
513
|
-
|
|
441
|
+
Button2,
|
|
514
442
|
{
|
|
515
443
|
variant: "contained",
|
|
516
444
|
color: "primary",
|
|
517
|
-
startIcon: /* @__PURE__ */ React2.createElement(
|
|
518
|
-
onClick:
|
|
445
|
+
startIcon: /* @__PURE__ */ React2.createElement(Add2, null),
|
|
446
|
+
onClick: onGenerateKeyClick
|
|
519
447
|
},
|
|
520
448
|
"Generate New Key"
|
|
521
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(
|
|
522
|
-
|
|
450
|
+
Button2,
|
|
523
451
|
{
|
|
524
452
|
variant: "contained",
|
|
525
453
|
color: "primary",
|
|
526
454
|
size: "small",
|
|
527
|
-
startIcon: /* @__PURE__ */ React2.createElement(
|
|
528
|
-
onClick:
|
|
455
|
+
startIcon: /* @__PURE__ */ React2.createElement(Add2, null),
|
|
456
|
+
onClick: onGenerateKeyClick
|
|
529
457
|
},
|
|
530
458
|
"Generate Your First Key"
|
|
531
459
|
)))) : filteredKeys.map((key) => {
|
|
@@ -565,8 +493,297 @@ var init_KeysTable = __esm({
|
|
|
565
493
|
},
|
|
566
494
|
/* @__PURE__ */ React2.createElement(Delete, null)
|
|
567
495
|
)));
|
|
568
|
-
}))))), /* @__PURE__ */ React2.createElement(Dialog, { open:
|
|
569
|
-
|
|
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);
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
const handleClose = () => {
|
|
769
|
+
onClose();
|
|
770
|
+
setGenerateError(null);
|
|
771
|
+
setNewKeyValue(null);
|
|
772
|
+
setNewKeySnippets(null);
|
|
773
|
+
setFormData(emptyForm());
|
|
774
|
+
setUnlimitedBudget(false);
|
|
775
|
+
};
|
|
776
|
+
const copyToClipboard = (text) => {
|
|
777
|
+
navigator.clipboard.writeText(text);
|
|
778
|
+
};
|
|
779
|
+
const modelOption = (m) => {
|
|
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"));
|
|
784
|
+
};
|
|
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,
|
|
570
787
|
{
|
|
571
788
|
display: "flex",
|
|
572
789
|
alignItems: "center",
|
|
@@ -580,8 +797,8 @@ var init_KeysTable = __esm({
|
|
|
580
797
|
borderRadius: 1
|
|
581
798
|
}
|
|
582
799
|
},
|
|
583
|
-
/* @__PURE__ */
|
|
584
|
-
|
|
800
|
+
/* @__PURE__ */ React3.createElement(
|
|
801
|
+
Typography3,
|
|
585
802
|
{
|
|
586
803
|
component: "code",
|
|
587
804
|
color: "text.primary",
|
|
@@ -589,20 +806,24 @@ var init_KeysTable = __esm({
|
|
|
589
806
|
},
|
|
590
807
|
newKeyValue
|
|
591
808
|
),
|
|
592
|
-
/* @__PURE__ */
|
|
593
|
-
), newKeySnippets && /* @__PURE__ */
|
|
594
|
-
|
|
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,
|
|
595
812
|
{
|
|
596
813
|
label: "Alias",
|
|
597
814
|
value: formData.alias || "",
|
|
598
|
-
onChange: (e) =>
|
|
815
|
+
onChange: (e) => {
|
|
816
|
+
setFormData({ ...formData, alias: e.target.value });
|
|
817
|
+
setGenerateError(null);
|
|
818
|
+
},
|
|
599
819
|
error: aliasError,
|
|
600
|
-
|
|
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,
|
|
601
822
|
required: true,
|
|
602
823
|
fullWidth: true
|
|
603
824
|
}
|
|
604
|
-
), /* @__PURE__ */
|
|
605
|
-
|
|
825
|
+
), /* @__PURE__ */ React3.createElement(
|
|
826
|
+
TextField2,
|
|
606
827
|
{
|
|
607
828
|
select: true,
|
|
608
829
|
label: "Duration",
|
|
@@ -610,44 +831,58 @@ var init_KeysTable = __esm({
|
|
|
610
831
|
onChange: (e) => setFormData({ ...formData, duration: e.target.value }),
|
|
611
832
|
fullWidth: true
|
|
612
833
|
},
|
|
613
|
-
/* @__PURE__ */
|
|
614
|
-
/* @__PURE__ */
|
|
615
|
-
/* @__PURE__ */
|
|
616
|
-
/* @__PURE__ */
|
|
617
|
-
/* @__PURE__ */
|
|
618
|
-
), teams.length > 0
|
|
619
|
-
|
|
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,
|
|
620
841
|
{
|
|
621
842
|
options: teams,
|
|
622
843
|
getOptionLabel: (t) => t.team_alias || t.team_id,
|
|
623
844
|
value: selectedTeam,
|
|
624
|
-
onChange: (_e, team) =>
|
|
625
|
-
|
|
626
|
-
|
|
845
|
+
onChange: (_e, team) => {
|
|
846
|
+
const teamModels = team?.models;
|
|
847
|
+
const restrictedModels = teamModels && teamModels.length > 0 ? (formData.models || []).filter((m) => teamModels.includes(m)) : formData.models;
|
|
848
|
+
setFormData({ ...formData, team_id: team?.team_id, models: restrictedModels });
|
|
849
|
+
},
|
|
850
|
+
renderInput: (params) => /* @__PURE__ */ React3.createElement(
|
|
851
|
+
TextField2,
|
|
627
852
|
{
|
|
628
853
|
...params,
|
|
629
854
|
label: "Team",
|
|
630
|
-
|
|
855
|
+
error: teamError,
|
|
856
|
+
helperText: teamError ? "Team is required" : teamRequired ? "Bind this key to a team for scoped access" : "Optional: bind this key to a specific team for scoped access",
|
|
857
|
+
required: teamRequired,
|
|
631
858
|
fullWidth: true
|
|
632
859
|
}
|
|
633
860
|
)
|
|
634
861
|
}
|
|
635
|
-
),
|
|
636
|
-
|
|
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,
|
|
637
864
|
{
|
|
638
865
|
multiple: true,
|
|
639
|
-
options:
|
|
866
|
+
options: availableModels,
|
|
640
867
|
groupBy: (m) => m.mode || "other",
|
|
641
868
|
getOptionLabel: (m) => m.model_name,
|
|
642
869
|
value: selectedModels,
|
|
643
870
|
onChange: (_e, selected) => setFormData({ ...formData, models: selected.map((m) => m.model_name) }),
|
|
644
|
-
renderOption: (props, m) => /* @__PURE__ */
|
|
645
|
-
renderInput: (params) => /* @__PURE__ */
|
|
871
|
+
renderOption: (props, m) => /* @__PURE__ */ React3.createElement("li", { ...props }, modelOption(m)),
|
|
872
|
+
renderInput: (params) => /* @__PURE__ */ React3.createElement(
|
|
873
|
+
TextField2,
|
|
874
|
+
{
|
|
875
|
+
...params,
|
|
876
|
+
label: "Models",
|
|
877
|
+
helperText: selectedTeam ? "Leave empty to allow all models available to this team" : "Leave empty to allow all models",
|
|
878
|
+
fullWidth: true
|
|
879
|
+
}
|
|
880
|
+
)
|
|
646
881
|
}
|
|
647
|
-
), /* @__PURE__ */
|
|
882
|
+
), allowUnlimitedBudget && /* @__PURE__ */ React3.createElement(
|
|
648
883
|
FormControlLabel,
|
|
649
884
|
{
|
|
650
|
-
control: /* @__PURE__ */
|
|
885
|
+
control: /* @__PURE__ */ React3.createElement(
|
|
651
886
|
Checkbox,
|
|
652
887
|
{
|
|
653
888
|
checked: unlimitedBudget,
|
|
@@ -663,8 +898,8 @@ var init_KeysTable = __esm({
|
|
|
663
898
|
),
|
|
664
899
|
label: "Unlimited budget"
|
|
665
900
|
}
|
|
666
|
-
), /* @__PURE__ */
|
|
667
|
-
|
|
901
|
+
), /* @__PURE__ */ React3.createElement(
|
|
902
|
+
TextField2,
|
|
668
903
|
{
|
|
669
904
|
label: "Max Budget (USD)",
|
|
670
905
|
type: "number",
|
|
@@ -676,141 +911,59 @@ var init_KeysTable = __esm({
|
|
|
676
911
|
required: true,
|
|
677
912
|
fullWidth: true
|
|
678
913
|
}
|
|
679
|
-
), /* @__PURE__ */
|
|
680
|
-
|
|
914
|
+
), /* @__PURE__ */ React3.createElement(
|
|
915
|
+
TextField2,
|
|
681
916
|
{
|
|
682
917
|
label: "TPM Limit",
|
|
683
918
|
type: "number",
|
|
684
919
|
value: formData.tpm_limit ?? "",
|
|
685
920
|
onChange: (e) => setFormData({ ...formData, tpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
|
|
921
|
+
helperText: "Max tokens per minute this key can consume across all models. Leave blank for no limit.",
|
|
686
922
|
fullWidth: true
|
|
687
923
|
}
|
|
688
|
-
), /* @__PURE__ */
|
|
689
|
-
|
|
924
|
+
), /* @__PURE__ */ React3.createElement(
|
|
925
|
+
TextField2,
|
|
690
926
|
{
|
|
691
927
|
label: "RPM Limit",
|
|
692
928
|
type: "number",
|
|
693
929
|
value: formData.rpm_limit ?? "",
|
|
694
930
|
onChange: (e) => setFormData({ ...formData, rpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
|
|
931
|
+
helperText: "Max requests per minute this key can make across all models. Leave blank for no limit.",
|
|
695
932
|
fullWidth: true
|
|
696
933
|
}
|
|
697
|
-
))), /* @__PURE__ */
|
|
698
|
-
|
|
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,
|
|
699
936
|
{
|
|
700
937
|
onClick: handleGenerate,
|
|
701
938
|
variant: "contained",
|
|
702
939
|
color: "primary",
|
|
703
940
|
disabled: !canGenerate
|
|
704
941
|
},
|
|
705
|
-
submitting ? /* @__PURE__ */
|
|
706
|
-
)))), /* @__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(
|
|
707
|
-
TextField,
|
|
708
|
-
{
|
|
709
|
-
label: "Alias",
|
|
710
|
-
value: editForm.key_alias || "",
|
|
711
|
-
onChange: (e) => setEditForm({ ...editForm, key_alias: e.target.value }),
|
|
712
|
-
fullWidth: true
|
|
713
|
-
}
|
|
714
|
-
), models.length > 0 && /* @__PURE__ */ React2.createElement(
|
|
715
|
-
Autocomplete,
|
|
716
|
-
{
|
|
717
|
-
multiple: true,
|
|
718
|
-
options: models,
|
|
719
|
-
groupBy: (m) => m.mode || "other",
|
|
720
|
-
getOptionLabel: (m) => m.model_name,
|
|
721
|
-
value: editSelectedModels,
|
|
722
|
-
onChange: (_e, selected) => setEditForm({ ...editForm, models: selected.map((m) => m.model_name) }),
|
|
723
|
-
renderOption: (props, m) => /* @__PURE__ */ React2.createElement("li", { ...props }, modelOption(m)),
|
|
724
|
-
renderInput: (params) => /* @__PURE__ */ React2.createElement(TextField, { ...params, label: "Models", fullWidth: true })
|
|
725
|
-
}
|
|
726
|
-
), /* @__PURE__ */ React2.createElement(
|
|
727
|
-
TextField,
|
|
728
|
-
{
|
|
729
|
-
label: "Max Budget (USD)",
|
|
730
|
-
type: "number",
|
|
731
|
-
value: editForm.max_budget ?? "",
|
|
732
|
-
onChange: (e) => setEditForm({ ...editForm, max_budget: e.target.value ? Number(e.target.value) : void 0 }),
|
|
733
|
-
fullWidth: true
|
|
734
|
-
}
|
|
735
|
-
), /* @__PURE__ */ React2.createElement(
|
|
736
|
-
TextField,
|
|
737
|
-
{
|
|
738
|
-
label: "TPM Limit",
|
|
739
|
-
type: "number",
|
|
740
|
-
value: editForm.tpm_limit ?? "",
|
|
741
|
-
onChange: (e) => setEditForm({ ...editForm, tpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
|
|
742
|
-
fullWidth: true
|
|
743
|
-
}
|
|
744
|
-
), /* @__PURE__ */ React2.createElement(
|
|
745
|
-
TextField,
|
|
746
|
-
{
|
|
747
|
-
label: "RPM Limit",
|
|
748
|
-
type: "number",
|
|
749
|
-
value: editForm.rpm_limit ?? "",
|
|
750
|
-
onChange: (e) => setEditForm({ ...editForm, rpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
|
|
751
|
-
fullWidth: true
|
|
752
|
-
}
|
|
753
|
-
), /* @__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(
|
|
754
|
-
Button,
|
|
755
|
-
{
|
|
756
|
-
size: "small",
|
|
757
|
-
color: "warning",
|
|
758
|
-
variant: "outlined",
|
|
759
|
-
onClick: () => setResetSpendConfirm(true)
|
|
760
|
-
},
|
|
761
|
-
"Reset Spend to $0"
|
|
762
|
-
) : /* @__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(
|
|
763
|
-
Button,
|
|
764
|
-
{
|
|
765
|
-
size: "small",
|
|
766
|
-
color: "warning",
|
|
767
|
-
variant: "contained",
|
|
768
|
-
disabled: resetSpendSubmitting,
|
|
769
|
-
onClick: handleResetSpend
|
|
770
|
-
},
|
|
771
|
-
resetSpendSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 16 }) : "Confirm"
|
|
772
|
-
), /* @__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(
|
|
773
|
-
Button,
|
|
774
|
-
{
|
|
775
|
-
onClick: handleConfirmDelete,
|
|
776
|
-
variant: "contained",
|
|
777
|
-
color: "error",
|
|
778
|
-
disabled: deleteSubmitting
|
|
779
|
-
},
|
|
780
|
-
deleteSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 20 }) : "Revoke"
|
|
781
|
-
))), /* @__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(
|
|
782
|
-
Button,
|
|
783
|
-
{
|
|
784
|
-
onClick: handlePruneExpired,
|
|
785
|
-
variant: "contained",
|
|
786
|
-
color: "error",
|
|
787
|
-
disabled: pruneSubmitting
|
|
788
|
-
},
|
|
789
|
-
pruneSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 20 }) : "Prune"
|
|
942
|
+
submitting ? /* @__PURE__ */ React3.createElement(CircularProgress2, { size: 24 }) : "Generate"
|
|
790
943
|
))));
|
|
791
944
|
};
|
|
792
945
|
SnippetTabs = ({ snippets, model, onCopy }) => {
|
|
793
|
-
const [tab, setTab] =
|
|
946
|
+
const [tab, setTab] = useState2("curl");
|
|
794
947
|
const code = tab === "curl" ? snippets.curl : snippets.openai;
|
|
795
|
-
return /* @__PURE__ */
|
|
796
|
-
|
|
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,
|
|
797
950
|
{
|
|
798
951
|
position: "relative",
|
|
799
952
|
p: 1.5,
|
|
800
953
|
sx: { backgroundColor: "action.hover", border: "1px solid", borderColor: "divider", borderRadius: 1 }
|
|
801
954
|
},
|
|
802
|
-
/* @__PURE__ */
|
|
803
|
-
|
|
955
|
+
/* @__PURE__ */ React3.createElement(
|
|
956
|
+
IconButton2,
|
|
804
957
|
{
|
|
805
958
|
size: "small",
|
|
806
959
|
onClick: () => onCopy(code),
|
|
807
960
|
title: "Copy snippet",
|
|
808
961
|
sx: { position: "absolute", top: 4, right: 4 }
|
|
809
962
|
},
|
|
810
|
-
/* @__PURE__ */
|
|
963
|
+
/* @__PURE__ */ React3.createElement(ContentCopy2, { fontSize: "small" })
|
|
811
964
|
),
|
|
812
|
-
/* @__PURE__ */
|
|
813
|
-
|
|
965
|
+
/* @__PURE__ */ React3.createElement(
|
|
966
|
+
Typography3,
|
|
814
967
|
{
|
|
815
968
|
component: "pre",
|
|
816
969
|
sx: {
|
|
@@ -824,18 +977,18 @@ var init_KeysTable = __esm({
|
|
|
824
977
|
},
|
|
825
978
|
code
|
|
826
979
|
),
|
|
827
|
-
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.")
|
|
828
981
|
));
|
|
829
982
|
};
|
|
830
983
|
}
|
|
831
984
|
});
|
|
832
985
|
|
|
833
986
|
// src/components/UsageStats.tsx
|
|
834
|
-
import
|
|
987
|
+
import React4, { useMemo as useMemo4, useState as useState3 } from "react";
|
|
835
988
|
import {
|
|
836
989
|
Paper as Paper3,
|
|
837
|
-
Box as
|
|
838
|
-
Typography as
|
|
990
|
+
Box as Box4,
|
|
991
|
+
Typography as Typography4,
|
|
839
992
|
FormControl,
|
|
840
993
|
InputLabel,
|
|
841
994
|
Select,
|
|
@@ -894,12 +1047,12 @@ var init_UsageStats = __esm({
|
|
|
894
1047
|
"#d0ed57"
|
|
895
1048
|
];
|
|
896
1049
|
fmtPct = (n) => `${(n * 100).toFixed(1)}%`;
|
|
897
|
-
KpiCard = ({ label, value, hint }) => /* @__PURE__ */
|
|
898
|
-
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 } });
|
|
899
1052
|
EmptyChart = ({
|
|
900
1053
|
height = 260,
|
|
901
1054
|
message = "No data for this period"
|
|
902
|
-
}) => /* @__PURE__ */
|
|
1055
|
+
}) => /* @__PURE__ */ React4.createElement(Box4, { height, display: "flex", alignItems: "center", justifyContent: "center" }, /* @__PURE__ */ React4.createElement(Typography4, { color: "text.secondary", variant: "body2" }, message));
|
|
903
1056
|
UsageStats = ({
|
|
904
1057
|
usage,
|
|
905
1058
|
models,
|
|
@@ -909,14 +1062,14 @@ var init_UsageStats = __esm({
|
|
|
909
1062
|
userInfo
|
|
910
1063
|
}) => {
|
|
911
1064
|
const theme = useTheme();
|
|
912
|
-
const [selectedModel, setSelectedModel] =
|
|
913
|
-
const [tab, setTab] =
|
|
1065
|
+
const [selectedModel, setSelectedModel] = useState3("all");
|
|
1066
|
+
const [tab, setTab] = useState3("costs");
|
|
914
1067
|
const gridStroke = theme.palette.divider;
|
|
915
1068
|
const tickFill = theme.palette.text.secondary;
|
|
916
1069
|
const tickStyle = { fontSize: 12, fill: tickFill };
|
|
917
1070
|
const tickStyleSmall = { fontSize: 11, fill: tickFill };
|
|
918
1071
|
const tickStyleTiny = { fontSize: 10, fill: tickFill };
|
|
919
|
-
const selectedPreset =
|
|
1072
|
+
const selectedPreset = useMemo4(() => {
|
|
920
1073
|
if (dateRange.start.toDateString() === dateRange.end.toDateString()) return "today";
|
|
921
1074
|
const diffMs = dateRange.end.getTime() - dateRange.start.getTime();
|
|
922
1075
|
const diffHours = diffMs / (1e3 * 60 * 60);
|
|
@@ -938,7 +1091,7 @@ var init_UsageStats = __esm({
|
|
|
938
1091
|
}
|
|
939
1092
|
onDateRangeChange({ start, end });
|
|
940
1093
|
};
|
|
941
|
-
const dailyData =
|
|
1094
|
+
const dailyData = useMemo4(
|
|
942
1095
|
() => (usage?.daily_usage ?? []).map((d) => ({
|
|
943
1096
|
date: d.date,
|
|
944
1097
|
spend: d.spend,
|
|
@@ -951,21 +1104,21 @@ var init_UsageStats = __esm({
|
|
|
951
1104
|
})),
|
|
952
1105
|
[usage]
|
|
953
1106
|
);
|
|
954
|
-
const cumulativeData =
|
|
1107
|
+
const cumulativeData = useMemo4(() => {
|
|
955
1108
|
let cum = 0;
|
|
956
1109
|
return dailyData.map((d) => {
|
|
957
1110
|
cum += d.spend;
|
|
958
1111
|
return { date: d.date, cumulative: cum };
|
|
959
1112
|
});
|
|
960
1113
|
}, [dailyData]);
|
|
961
|
-
const successRateData =
|
|
1114
|
+
const successRateData = useMemo4(
|
|
962
1115
|
() => dailyData.filter((d) => d.apiRequests > 0).map((d) => ({
|
|
963
1116
|
date: d.date,
|
|
964
1117
|
successRate: parseFloat((d.successfulRequests / d.apiRequests * 100).toFixed(1))
|
|
965
1118
|
})),
|
|
966
1119
|
[dailyData]
|
|
967
1120
|
);
|
|
968
|
-
const { modelSpendByDate, topModels: topSpendModels } =
|
|
1121
|
+
const { modelSpendByDate, topModels: topSpendModels } = useMemo4(() => {
|
|
969
1122
|
const rows = usage?.daily_by_model ?? [];
|
|
970
1123
|
const modelTotals = {};
|
|
971
1124
|
for (const r of rows) modelTotals[r.model] = (modelTotals[r.model] ?? 0) + r.spend;
|
|
@@ -980,7 +1133,7 @@ var init_UsageStats = __esm({
|
|
|
980
1133
|
const hasOther = sorted.some((r) => r.Other > 0);
|
|
981
1134
|
return { modelSpendByDate: sorted, topModels: hasOther ? [...top, "Other"] : top };
|
|
982
1135
|
}, [usage]);
|
|
983
|
-
const modelRows =
|
|
1136
|
+
const modelRows = useMemo4(() => {
|
|
984
1137
|
const entries = Object.entries(usage?.usage_by_model ?? {}).map(([model, d]) => ({
|
|
985
1138
|
model,
|
|
986
1139
|
spend: d.total_spend,
|
|
@@ -994,11 +1147,11 @@ var init_UsageStats = __esm({
|
|
|
994
1147
|
}));
|
|
995
1148
|
return selectedModel === "all" ? entries : entries.filter((e) => e.model === selectedModel);
|
|
996
1149
|
}, [usage, selectedModel]);
|
|
997
|
-
const topModelSpendBars =
|
|
1150
|
+
const topModelSpendBars = useMemo4(
|
|
998
1151
|
() => [...modelRows].sort((a, b) => b.spend - a.spend).slice(0, 10),
|
|
999
1152
|
[modelRows]
|
|
1000
1153
|
);
|
|
1001
|
-
const keyRows =
|
|
1154
|
+
const keyRows = useMemo4(
|
|
1002
1155
|
() => Object.entries(usage?.usage_by_key ?? {}).map(([keyHash, d]) => ({
|
|
1003
1156
|
keyHash,
|
|
1004
1157
|
keyAlias: d.key_alias ?? keyHash.slice(0, 8),
|
|
@@ -1015,7 +1168,7 @@ var init_UsageStats = __esm({
|
|
|
1015
1168
|
})),
|
|
1016
1169
|
[usage]
|
|
1017
1170
|
);
|
|
1018
|
-
const topKeySpendBars =
|
|
1171
|
+
const topKeySpendBars = useMemo4(
|
|
1019
1172
|
() => [...keyRows].sort((a, b) => b.spend - a.spend).slice(0, 10),
|
|
1020
1173
|
[keyRows]
|
|
1021
1174
|
);
|
|
@@ -1023,34 +1176,34 @@ var init_UsageStats = __esm({
|
|
|
1023
1176
|
const overallSuccessRate = totalRequests > 0 ? (usage?.successful_requests ?? 0) / totalRequests : 0;
|
|
1024
1177
|
const maxBudget = userInfo?.max_budget ?? 0;
|
|
1025
1178
|
const totalCumSpend = cumulativeData[cumulativeData.length - 1]?.cumulative ?? 0;
|
|
1026
|
-
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(
|
|
1027
1180
|
Select,
|
|
1028
1181
|
{
|
|
1029
1182
|
value: selectedPreset,
|
|
1030
1183
|
label: "Period",
|
|
1031
1184
|
onChange: (e) => handlePresetChange(e.target.value)
|
|
1032
1185
|
},
|
|
1033
|
-
/* @__PURE__ */
|
|
1034
|
-
/* @__PURE__ */
|
|
1035
|
-
/* @__PURE__ */
|
|
1036
|
-
/* @__PURE__ */
|
|
1037
|
-
)), 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(
|
|
1038
1191
|
Select,
|
|
1039
1192
|
{
|
|
1040
1193
|
value: selectedModel,
|
|
1041
1194
|
label: "Model",
|
|
1042
1195
|
onChange: (e) => setSelectedModel(e.target.value)
|
|
1043
1196
|
},
|
|
1044
|
-
/* @__PURE__ */
|
|
1045
|
-
models.map((m) => /* @__PURE__ */
|
|
1046
|
-
)))), /* @__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(
|
|
1047
1200
|
KpiCard,
|
|
1048
1201
|
{
|
|
1049
1202
|
label: "Total Tokens",
|
|
1050
1203
|
value: fmtInt(usage?.total_tokens ?? 0),
|
|
1051
1204
|
hint: `${fmtInt(usage?.prompt_tokens ?? 0)} in \xB7 ${fmtInt(usage?.completion_tokens ?? 0)} out`
|
|
1052
1205
|
}
|
|
1053
|
-
))), /* @__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(
|
|
1054
1207
|
Area,
|
|
1055
1208
|
{
|
|
1056
1209
|
key: m,
|
|
@@ -1061,35 +1214,35 @@ var init_UsageStats = __esm({
|
|
|
1061
1214
|
fill: modelColor(m),
|
|
1062
1215
|
fillOpacity: 0.6
|
|
1063
1216
|
}
|
|
1064
|
-
)))))), /* @__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(
|
|
1065
1218
|
LinearProgress3,
|
|
1066
1219
|
{
|
|
1067
1220
|
variant: "determinate",
|
|
1068
1221
|
value: r.successRate * 100,
|
|
1069
1222
|
sx: { flex: 1, height: 6, borderRadius: 3 }
|
|
1070
1223
|
}
|
|
1071
|
-
), /* @__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(
|
|
1072
1225
|
LinearProgress3,
|
|
1073
1226
|
{
|
|
1074
1227
|
variant: "determinate",
|
|
1075
1228
|
value: r.successRate * 100,
|
|
1076
1229
|
sx: { flex: 1, height: 6, borderRadius: 3 }
|
|
1077
1230
|
}
|
|
1078
|
-
), /* @__PURE__ */
|
|
1231
|
+
), /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption" }, fmtPct(r.successRate))) : "\u2014")))))))));
|
|
1079
1232
|
};
|
|
1080
1233
|
}
|
|
1081
1234
|
});
|
|
1082
1235
|
|
|
1083
1236
|
// src/components/TeamUsage.tsx
|
|
1084
|
-
import
|
|
1237
|
+
import React5, { useState as useState4 } from "react";
|
|
1085
1238
|
import {
|
|
1086
1239
|
Paper as Paper4,
|
|
1087
|
-
Box as
|
|
1088
|
-
Typography as
|
|
1240
|
+
Box as Box5,
|
|
1241
|
+
Typography as Typography5,
|
|
1089
1242
|
LinearProgress as LinearProgress4,
|
|
1090
1243
|
Chip as Chip4,
|
|
1091
1244
|
Divider,
|
|
1092
|
-
Stack,
|
|
1245
|
+
Stack as Stack2,
|
|
1093
1246
|
Table as Table3,
|
|
1094
1247
|
TableBody as TableBody3,
|
|
1095
1248
|
TableCell as TableCell3,
|
|
@@ -1097,8 +1250,8 @@ import {
|
|
|
1097
1250
|
TableRow as TableRow3,
|
|
1098
1251
|
TableContainer as TableContainer3,
|
|
1099
1252
|
Collapse,
|
|
1100
|
-
IconButton as
|
|
1101
|
-
CircularProgress as
|
|
1253
|
+
IconButton as IconButton3,
|
|
1254
|
+
CircularProgress as CircularProgress3,
|
|
1102
1255
|
useTheme as useTheme2
|
|
1103
1256
|
} from "@mui/material";
|
|
1104
1257
|
import { ExpandMore, ExpandLess, Group, Speed, Memory } from "@mui/icons-material";
|
|
@@ -1115,9 +1268,9 @@ var Stat, TeamCard, TeamUsage;
|
|
|
1115
1268
|
var init_TeamUsage = __esm({
|
|
1116
1269
|
"src/components/TeamUsage.tsx"() {
|
|
1117
1270
|
"use strict";
|
|
1118
|
-
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));
|
|
1119
1272
|
TeamCard = ({ team, usage, usageLoading }) => {
|
|
1120
|
-
const [expanded, setExpanded] =
|
|
1273
|
+
const [expanded, setExpanded] = useState4(false);
|
|
1121
1274
|
const theme = useTheme2();
|
|
1122
1275
|
const gridStroke = theme.palette.divider;
|
|
1123
1276
|
const tickFill = theme.palette.text.secondary;
|
|
@@ -1127,43 +1280,43 @@ var init_TeamUsage = __esm({
|
|
|
1127
1280
|
const isOver = budget > 0 && spend >= budget;
|
|
1128
1281
|
const isNear = budget > 0 && spend >= budget * 0.8 && !isOver;
|
|
1129
1282
|
const dailyData = usage?.daily_usage?.map((d) => ({ date: d.date, spend: d.spend })) ?? [];
|
|
1130
|
-
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(
|
|
1131
1284
|
Stat,
|
|
1132
1285
|
{
|
|
1133
1286
|
label: "Members",
|
|
1134
1287
|
value: team.members_with_roles?.length ? String(team.members_with_roles.length) : "\u2014"
|
|
1135
1288
|
}
|
|
1136
|
-
), /* @__PURE__ */
|
|
1289
|
+
), /* @__PURE__ */ React5.createElement(
|
|
1137
1290
|
Stat,
|
|
1138
1291
|
{
|
|
1139
1292
|
label: "Models",
|
|
1140
1293
|
value: team.models?.length ? String(team.models.length) : "All"
|
|
1141
1294
|
}
|
|
1142
|
-
), /* @__PURE__ */
|
|
1295
|
+
), /* @__PURE__ */ React5.createElement(
|
|
1143
1296
|
Stat,
|
|
1144
1297
|
{
|
|
1145
1298
|
label: "Budget",
|
|
1146
1299
|
value: budget > 0 ? `$${budget.toFixed(2)}` : "Unlimited"
|
|
1147
1300
|
}
|
|
1148
|
-
), /* @__PURE__ */
|
|
1301
|
+
), /* @__PURE__ */ React5.createElement(
|
|
1149
1302
|
Stat,
|
|
1150
1303
|
{
|
|
1151
1304
|
label: "Spend",
|
|
1152
1305
|
value: `$${spend.toFixed(2)}`
|
|
1153
1306
|
}
|
|
1154
|
-
), /* @__PURE__ */
|
|
1307
|
+
), /* @__PURE__ */ React5.createElement(
|
|
1155
1308
|
Stat,
|
|
1156
1309
|
{
|
|
1157
1310
|
label: "TPM",
|
|
1158
1311
|
value: team.tpm_limit != null && team.tpm_limit > 0 ? String(team.tpm_limit) : "\u2014"
|
|
1159
1312
|
}
|
|
1160
|
-
), /* @__PURE__ */
|
|
1313
|
+
), /* @__PURE__ */ React5.createElement(
|
|
1161
1314
|
Stat,
|
|
1162
1315
|
{
|
|
1163
1316
|
label: "RPM",
|
|
1164
1317
|
value: team.rpm_limit != null && team.rpm_limit > 0 ? String(team.rpm_limit) : "\u2014"
|
|
1165
1318
|
}
|
|
1166
|
-
)), 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(
|
|
1167
1320
|
LinearProgress4,
|
|
1168
1321
|
{
|
|
1169
1322
|
variant: "determinate",
|
|
@@ -1171,22 +1324,22 @@ var init_TeamUsage = __esm({
|
|
|
1171
1324
|
color: isOver ? "error" : isNear ? "warning" : "primary",
|
|
1172
1325
|
sx: { height: 6, borderRadius: 1 }
|
|
1173
1326
|
}
|
|
1174
|
-
)), /* @__PURE__ */
|
|
1175
|
-
|
|
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,
|
|
1176
1329
|
{
|
|
1177
1330
|
variant: "caption",
|
|
1178
1331
|
color: "text.secondary",
|
|
1179
1332
|
sx: { fontFamily: "monospace" }
|
|
1180
1333
|
},
|
|
1181
1334
|
m.user_id
|
|
1182
|
-
)) : /* @__PURE__ */
|
|
1335
|
+
)) : /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2", sx: { fontFamily: "monospace" } }, m.user_id)), /* @__PURE__ */ React5.createElement(TableCell3, null, /* @__PURE__ */ React5.createElement(
|
|
1183
1336
|
Chip4,
|
|
1184
1337
|
{
|
|
1185
1338
|
label: m.role,
|
|
1186
1339
|
size: "small",
|
|
1187
1340
|
color: m.role === "admin" ? "primary" : "default"
|
|
1188
1341
|
}
|
|
1189
|
-
))))))) : /* @__PURE__ */
|
|
1342
|
+
))))))) : /* @__PURE__ */ React5.createElement(Typography5, { variant: "body2", color: "text.secondary" }, "No members assigned."))));
|
|
1190
1343
|
};
|
|
1191
1344
|
TeamUsage = ({
|
|
1192
1345
|
teams,
|
|
@@ -1195,12 +1348,12 @@ var init_TeamUsage = __esm({
|
|
|
1195
1348
|
getTeamUsageLoading
|
|
1196
1349
|
}) => {
|
|
1197
1350
|
if (loading) {
|
|
1198
|
-
return /* @__PURE__ */
|
|
1351
|
+
return /* @__PURE__ */ React5.createElement(Paper4, { sx: { p: 2 } }, /* @__PURE__ */ React5.createElement(LinearProgress4, null));
|
|
1199
1352
|
}
|
|
1200
1353
|
if (!teams.length) {
|
|
1201
|
-
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."))));
|
|
1202
1355
|
}
|
|
1203
|
-
return /* @__PURE__ */
|
|
1356
|
+
return /* @__PURE__ */ React5.createElement(Box5, null, /* @__PURE__ */ React5.createElement(Typography5, { variant: "h6", mb: 1 }, "Teams"), teams.map((team) => /* @__PURE__ */ React5.createElement(
|
|
1204
1357
|
TeamCard,
|
|
1205
1358
|
{
|
|
1206
1359
|
key: team.team_id,
|
|
@@ -1214,11 +1367,11 @@ var init_TeamUsage = __esm({
|
|
|
1214
1367
|
});
|
|
1215
1368
|
|
|
1216
1369
|
// src/components/AuditLog.tsx
|
|
1217
|
-
import
|
|
1370
|
+
import React6, { useState as useState5, useCallback } from "react";
|
|
1218
1371
|
import {
|
|
1219
1372
|
Paper as Paper5,
|
|
1220
|
-
Box as
|
|
1221
|
-
Typography as
|
|
1373
|
+
Box as Box6,
|
|
1374
|
+
Typography as Typography6,
|
|
1222
1375
|
Table as Table4,
|
|
1223
1376
|
TableBody as TableBody4,
|
|
1224
1377
|
TableCell as TableCell4,
|
|
@@ -1226,13 +1379,13 @@ import {
|
|
|
1226
1379
|
TableHead as TableHead4,
|
|
1227
1380
|
TableRow as TableRow4,
|
|
1228
1381
|
TablePagination,
|
|
1229
|
-
TextField as
|
|
1382
|
+
TextField as TextField3,
|
|
1230
1383
|
MenuItem as MenuItem3,
|
|
1231
1384
|
Chip as Chip5,
|
|
1232
|
-
CircularProgress as
|
|
1385
|
+
CircularProgress as CircularProgress4,
|
|
1233
1386
|
Collapse as Collapse2,
|
|
1234
|
-
IconButton as
|
|
1235
|
-
Alert
|
|
1387
|
+
IconButton as IconButton4,
|
|
1388
|
+
Alert as Alert2
|
|
1236
1389
|
} from "@mui/material";
|
|
1237
1390
|
import { KeyboardArrowDown, KeyboardArrowUp } from "@mui/icons-material";
|
|
1238
1391
|
import { useAsync } from "react-use";
|
|
@@ -1262,14 +1415,14 @@ var init_AuditLog = __esm({
|
|
|
1262
1415
|
unblocked: "success"
|
|
1263
1416
|
};
|
|
1264
1417
|
DetailRow = ({ entry }) => {
|
|
1265
|
-
const [open, setOpen] =
|
|
1418
|
+
const [open, setOpen] = useState5(false);
|
|
1266
1419
|
const hasDetail = entry.before_value || entry.updated_values;
|
|
1267
|
-
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))))))));
|
|
1268
1421
|
};
|
|
1269
1422
|
AuditLog = ({ api }) => {
|
|
1270
|
-
const [page, setPage] =
|
|
1271
|
-
const [pageSize, setPageSize] =
|
|
1272
|
-
const [filters, setFilters] =
|
|
1423
|
+
const [page, setPage] = useState5(0);
|
|
1424
|
+
const [pageSize, setPageSize] = useState5(25);
|
|
1425
|
+
const [filters, setFilters] = useState5({});
|
|
1273
1426
|
const fetchParams = useCallback(
|
|
1274
1427
|
() => ({ page: page + 1, page_size: pageSize, ...filters }),
|
|
1275
1428
|
[page, pageSize, filters]
|
|
@@ -1280,8 +1433,8 @@ var init_AuditLog = __esm({
|
|
|
1280
1433
|
);
|
|
1281
1434
|
const entries = value?.audit_logs ?? [];
|
|
1282
1435
|
const total = value?.total ?? 0;
|
|
1283
|
-
return /* @__PURE__ */
|
|
1284
|
-
|
|
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,
|
|
1285
1438
|
{
|
|
1286
1439
|
size: "small",
|
|
1287
1440
|
label: "Action",
|
|
@@ -1293,13 +1446,13 @@ var init_AuditLog = __esm({
|
|
|
1293
1446
|
},
|
|
1294
1447
|
sx: { minWidth: 160 }
|
|
1295
1448
|
},
|
|
1296
|
-
/* @__PURE__ */
|
|
1297
|
-
/* @__PURE__ */
|
|
1298
|
-
/* @__PURE__ */
|
|
1299
|
-
/* @__PURE__ */
|
|
1300
|
-
/* @__PURE__ */
|
|
1301
|
-
), /* @__PURE__ */
|
|
1302
|
-
|
|
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,
|
|
1303
1456
|
{
|
|
1304
1457
|
size: "small",
|
|
1305
1458
|
label: "Table",
|
|
@@ -1311,12 +1464,12 @@ var init_AuditLog = __esm({
|
|
|
1311
1464
|
},
|
|
1312
1465
|
sx: { minWidth: 160 }
|
|
1313
1466
|
},
|
|
1314
|
-
/* @__PURE__ */
|
|
1315
|
-
/* @__PURE__ */
|
|
1316
|
-
/* @__PURE__ */
|
|
1317
|
-
/* @__PURE__ */
|
|
1318
|
-
), /* @__PURE__ */
|
|
1319
|
-
|
|
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,
|
|
1320
1473
|
{
|
|
1321
1474
|
size: "small",
|
|
1322
1475
|
label: "Changed by",
|
|
@@ -1327,7 +1480,7 @@ var init_AuditLog = __esm({
|
|
|
1327
1480
|
},
|
|
1328
1481
|
sx: { minWidth: 200 }
|
|
1329
1482
|
}
|
|
1330
|
-
)), 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(
|
|
1331
1484
|
TablePagination,
|
|
1332
1485
|
{
|
|
1333
1486
|
component: "div",
|
|
@@ -1351,8 +1504,8 @@ var LiteLLMPage_exports = {};
|
|
|
1351
1504
|
__export(LiteLLMPage_exports, {
|
|
1352
1505
|
LiteLLMPage: () => LiteLLMPage
|
|
1353
1506
|
});
|
|
1354
|
-
import
|
|
1355
|
-
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";
|
|
1356
1509
|
import { useAsync as useAsync2, useAsyncRetry } from "react-use";
|
|
1357
1510
|
import { useApi } from "@backstage/core-plugin-api";
|
|
1358
1511
|
function initDateRange() {
|
|
@@ -1375,6 +1528,7 @@ var init_LiteLLMPage = __esm({
|
|
|
1375
1528
|
"use strict";
|
|
1376
1529
|
init_DashboardHeader();
|
|
1377
1530
|
init_KeysTable();
|
|
1531
|
+
init_GenerateKeyDialog();
|
|
1378
1532
|
init_UsageStats();
|
|
1379
1533
|
init_TeamUsage();
|
|
1380
1534
|
init_AuditLog();
|
|
@@ -1382,11 +1536,12 @@ var init_LiteLLMPage = __esm({
|
|
|
1382
1536
|
PERIOD_LS_KEY2 = "litellm_usage_period";
|
|
1383
1537
|
LiteLLMPage = () => {
|
|
1384
1538
|
const api = useApi(liteLlmApiRef);
|
|
1385
|
-
const [dateRange, setDateRange] =
|
|
1386
|
-
const [activeTab, setActiveTab] =
|
|
1387
|
-
const [snackbar, setSnackbar] =
|
|
1388
|
-
const [
|
|
1389
|
-
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({});
|
|
1390
1545
|
const { value: userInfo, loading: userLoading, error: userError } = useAsync2(
|
|
1391
1546
|
() => api.getUserInfo(),
|
|
1392
1547
|
[api]
|
|
@@ -1410,7 +1565,8 @@ var init_LiteLLMPage = __esm({
|
|
|
1410
1565
|
() => api.getTeams().catch(() => []),
|
|
1411
1566
|
[api]
|
|
1412
1567
|
);
|
|
1413
|
-
const
|
|
1568
|
+
const { value: liteLlmConfig } = useAsync2(() => api.getConfig(), [api]);
|
|
1569
|
+
const teams = useMemo5(() => {
|
|
1414
1570
|
if (!allTeams?.length) return [];
|
|
1415
1571
|
if (!userInfo) return allTeams;
|
|
1416
1572
|
const userId = userInfo.user_id;
|
|
@@ -1446,7 +1602,7 @@ var init_LiteLLMPage = __esm({
|
|
|
1446
1602
|
setTeamUsageLoading((prev) => ({ ...prev, [teamId]: false }));
|
|
1447
1603
|
}
|
|
1448
1604
|
}, [api, dateRange, teamUsageCache, teamUsageLoading]);
|
|
1449
|
-
const allowedModels =
|
|
1605
|
+
const allowedModels = useMemo5(() => {
|
|
1450
1606
|
if (!allModels?.length) return [];
|
|
1451
1607
|
const userModels = userInfo?.models;
|
|
1452
1608
|
const teamModels = teams?.flatMap((t) => t.models ?? []);
|
|
@@ -1554,25 +1710,34 @@ var init_LiteLLMPage = __esm({
|
|
|
1554
1710
|
);
|
|
1555
1711
|
const isInitialLoading = userLoading && !userInfo;
|
|
1556
1712
|
if (isInitialLoading) {
|
|
1557
|
-
return /* @__PURE__ */
|
|
1713
|
+
return /* @__PURE__ */ React7.createElement(Box7, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "50vh" }, /* @__PURE__ */ React7.createElement(CircularProgress5, null));
|
|
1558
1714
|
}
|
|
1559
1715
|
if (userError || !userInfo) {
|
|
1560
1716
|
const isProvisioningEnabled = userError?.body?.provisioning === true;
|
|
1561
1717
|
const hint = userError?.body?.hint;
|
|
1562
|
-
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.")));
|
|
1563
1719
|
}
|
|
1564
|
-
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(
|
|
1565
1730
|
Tabs3,
|
|
1566
1731
|
{
|
|
1567
1732
|
value: activeTab,
|
|
1568
1733
|
onChange: (_, v) => setActiveTab(v),
|
|
1569
1734
|
sx: { mb: 2, borderBottom: 1, borderColor: "divider" }
|
|
1570
1735
|
},
|
|
1571
|
-
/* @__PURE__ */
|
|
1572
|
-
/* @__PURE__ */
|
|
1573
|
-
/* @__PURE__ */
|
|
1574
|
-
userInfo.can_view_audit && /* @__PURE__ */
|
|
1575
|
-
), 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(
|
|
1576
1741
|
UsageStats,
|
|
1577
1742
|
{
|
|
1578
1743
|
usage: usage ?? null,
|
|
@@ -1582,23 +1747,21 @@ var init_LiteLLMPage = __esm({
|
|
|
1582
1747
|
loading: usageLoading,
|
|
1583
1748
|
userInfo
|
|
1584
1749
|
}
|
|
1585
|
-
), activeTab === "keys" && /* @__PURE__ */
|
|
1750
|
+
), activeTab === "keys" && /* @__PURE__ */ React7.createElement(
|
|
1586
1751
|
KeysTable,
|
|
1587
1752
|
{
|
|
1588
1753
|
keys: keys ?? [],
|
|
1589
1754
|
models: allowedModels,
|
|
1590
|
-
teams: teams ?? [],
|
|
1591
1755
|
loading: keysLoading || modelsLoading,
|
|
1592
|
-
|
|
1756
|
+
onGenerateKeyClick: () => setGenerateDialogOpen(true),
|
|
1593
1757
|
onUpdateKey: handleUpdateKey,
|
|
1594
1758
|
onBlockKey: handleBlockKey,
|
|
1595
1759
|
onUnblockKey: handleUnblockKey,
|
|
1596
1760
|
onResetKeySpend: handleResetKeySpend,
|
|
1597
1761
|
onDeleteKey: handleDeleteKey,
|
|
1598
|
-
onPruneExpiredKeys: handlePruneExpiredKeys
|
|
1599
|
-
onGetConfig: () => api.getConfig()
|
|
1762
|
+
onPruneExpiredKeys: handlePruneExpiredKeys
|
|
1600
1763
|
}
|
|
1601
|
-
), activeTab === "teams" && /* @__PURE__ */
|
|
1764
|
+
), activeTab === "teams" && /* @__PURE__ */ React7.createElement(
|
|
1602
1765
|
TeamUsage,
|
|
1603
1766
|
{
|
|
1604
1767
|
teams: teams ?? [],
|
|
@@ -1609,7 +1772,20 @@ var init_LiteLLMPage = __esm({
|
|
|
1609
1772
|
},
|
|
1610
1773
|
getTeamUsageLoading: (teamId) => teamUsageLoading[teamId] ?? false
|
|
1611
1774
|
}
|
|
1612
|
-
), 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(
|
|
1613
1789
|
Snackbar,
|
|
1614
1790
|
{
|
|
1615
1791
|
open: !!snackbar,
|
|
@@ -1617,7 +1793,7 @@ var init_LiteLLMPage = __esm({
|
|
|
1617
1793
|
onClose: () => setSnackbar(null),
|
|
1618
1794
|
anchorOrigin: { vertical: "bottom", horizontal: "right" }
|
|
1619
1795
|
},
|
|
1620
|
-
snackbar ? /* @__PURE__ */
|
|
1796
|
+
snackbar ? /* @__PURE__ */ React7.createElement(Alert3, { severity: snackbar.severity, onClose: () => setSnackbar(null) }, snackbar.message) : void 0
|
|
1621
1797
|
));
|
|
1622
1798
|
};
|
|
1623
1799
|
}
|
|
@@ -1625,7 +1801,7 @@ var init_LiteLLMPage = __esm({
|
|
|
1625
1801
|
|
|
1626
1802
|
// src/plugin.tsx
|
|
1627
1803
|
init_api();
|
|
1628
|
-
import
|
|
1804
|
+
import React8 from "react";
|
|
1629
1805
|
import { TrendingUp as TrendingUpIcon } from "@mui/icons-material";
|
|
1630
1806
|
import {
|
|
1631
1807
|
createFrontendPlugin,
|
|
@@ -1644,10 +1820,10 @@ var liteLlmPage = PageBlueprint.make({
|
|
|
1644
1820
|
params: {
|
|
1645
1821
|
path: "/litellm",
|
|
1646
1822
|
title: "LiteLLM",
|
|
1647
|
-
icon: /* @__PURE__ */
|
|
1823
|
+
icon: /* @__PURE__ */ React8.createElement(TrendingUpIcon, null),
|
|
1648
1824
|
loader: async () => {
|
|
1649
1825
|
const { LiteLLMPage: LiteLLMPage2 } = await Promise.resolve().then(() => (init_LiteLLMPage(), LiteLLMPage_exports));
|
|
1650
|
-
return /* @__PURE__ */
|
|
1826
|
+
return /* @__PURE__ */ React8.createElement(LiteLLMPage2, null);
|
|
1651
1827
|
}
|
|
1652
1828
|
}
|
|
1653
1829
|
});
|
|
@@ -1666,17 +1842,17 @@ init_TeamUsage();
|
|
|
1666
1842
|
// src/components/LiteLLMHomeWidget.tsx
|
|
1667
1843
|
init_api();
|
|
1668
1844
|
init_format();
|
|
1669
|
-
import
|
|
1845
|
+
import React9, { useState as useState7, useEffect as useEffect2 } from "react";
|
|
1670
1846
|
import {
|
|
1671
1847
|
Paper as Paper7,
|
|
1672
|
-
Box as
|
|
1673
|
-
Typography as
|
|
1848
|
+
Box as Box8,
|
|
1849
|
+
Typography as Typography8,
|
|
1674
1850
|
FormControl as FormControl2,
|
|
1675
1851
|
Select as Select2,
|
|
1676
1852
|
MenuItem as MenuItem4,
|
|
1677
1853
|
Grid as Grid2,
|
|
1678
|
-
CircularProgress as
|
|
1679
|
-
Alert as
|
|
1854
|
+
CircularProgress as CircularProgress6,
|
|
1855
|
+
Alert as Alert4
|
|
1680
1856
|
} from "@mui/material";
|
|
1681
1857
|
import { AreaChart as AreaChart3, Area as Area3, ResponsiveContainer as ResponsiveContainer3 } from "recharts";
|
|
1682
1858
|
import { useApi as useApi2 } from "@backstage/core-plugin-api";
|
|
@@ -1692,19 +1868,19 @@ function presetToDateRange(preset) {
|
|
|
1692
1868
|
}
|
|
1693
1869
|
return { start, end };
|
|
1694
1870
|
}
|
|
1695
|
-
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));
|
|
1696
1872
|
var LiteLLMHomeWidget = ({
|
|
1697
1873
|
defaultPeriod = "7d",
|
|
1698
1874
|
title = "LiteLLM Usage"
|
|
1699
1875
|
}) => {
|
|
1700
1876
|
const api = useApi2(liteLlmApiRef);
|
|
1701
|
-
const [period, setPeriod] =
|
|
1702
|
-
const [loading, setLoading] =
|
|
1703
|
-
const [usageError, setUsageError] =
|
|
1704
|
-
const [keysError, setKeysError] =
|
|
1705
|
-
const [usage, setUsage] =
|
|
1706
|
-
const [keys, setKeys] =
|
|
1707
|
-
|
|
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(() => {
|
|
1708
1884
|
let cancelled = false;
|
|
1709
1885
|
setLoading(true);
|
|
1710
1886
|
setUsageError(null);
|
|
@@ -1741,17 +1917,17 @@ var LiteLLMHomeWidget = ({
|
|
|
1741
1917
|
spend: d.spend
|
|
1742
1918
|
}));
|
|
1743
1919
|
const hasSparkline = dailyData.length > 0;
|
|
1744
|
-
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(
|
|
1745
1921
|
Select2,
|
|
1746
1922
|
{
|
|
1747
1923
|
value: period,
|
|
1748
1924
|
onChange: (e) => setPeriod(e.target.value),
|
|
1749
1925
|
displayEmpty: true
|
|
1750
1926
|
},
|
|
1751
|
-
/* @__PURE__ */
|
|
1752
|
-
/* @__PURE__ */
|
|
1753
|
-
/* @__PURE__ */
|
|
1754
|
-
))), 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(
|
|
1755
1931
|
Area3,
|
|
1756
1932
|
{
|
|
1757
1933
|
type: "monotone",
|