@acarmisc/backstage-plugin-litellm 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +3 -1
- package/dist/components/KeysTable.d.ts +3 -1
- package/dist/format.d.ts +5 -0
- package/dist/index.cjs.js +179 -32
- package/dist/index.cjs.js.map +3 -3
- package/dist/index.esm.js +194 -43
- package/dist/index.esm.js.map +4 -4
- package/dist/types.d.ts +6 -1
- package/package.json +6 -1
package/dist/index.esm.js
CHANGED
|
@@ -147,6 +147,9 @@ var init_api = __esm({
|
|
|
147
147
|
end_date: endDate
|
|
148
148
|
});
|
|
149
149
|
}
|
|
150
|
+
async getConfig() {
|
|
151
|
+
return this.get("/config");
|
|
152
|
+
}
|
|
150
153
|
};
|
|
151
154
|
}
|
|
152
155
|
});
|
|
@@ -177,6 +180,20 @@ var init_DashboardHeader = __esm({
|
|
|
177
180
|
}
|
|
178
181
|
});
|
|
179
182
|
|
|
183
|
+
// src/format.ts
|
|
184
|
+
var fmtUsd, fmtInt, estimateTokensFromBudget;
|
|
185
|
+
var init_format = __esm({
|
|
186
|
+
"src/format.ts"() {
|
|
187
|
+
"use strict";
|
|
188
|
+
fmtUsd = (n) => `$${(n ?? 0).toFixed(n < 1 ? 4 : 2)}`;
|
|
189
|
+
fmtInt = (n) => (n ?? 0).toLocaleString();
|
|
190
|
+
estimateTokensFromBudget = (budget, pricePerToken) => {
|
|
191
|
+
if (!pricePerToken || pricePerToken <= 0 || !budget || budget <= 0) return null;
|
|
192
|
+
return Math.floor(budget / pricePerToken);
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
180
197
|
// src/components/KeysTable.tsx
|
|
181
198
|
import React2, { useState, useMemo } from "react";
|
|
182
199
|
import {
|
|
@@ -201,9 +218,13 @@ import {
|
|
|
201
218
|
CircularProgress,
|
|
202
219
|
Autocomplete,
|
|
203
220
|
LinearProgress as LinearProgress2,
|
|
204
|
-
InputAdornment
|
|
221
|
+
InputAdornment,
|
|
222
|
+
Checkbox,
|
|
223
|
+
FormControlLabel,
|
|
224
|
+
Tabs,
|
|
225
|
+
Tab
|
|
205
226
|
} from "@mui/material";
|
|
206
|
-
import { ContentCopy, Delete, Add, Edit, Autorenew, Search, Warning, Lock, LockOpen } from "@mui/icons-material";
|
|
227
|
+
import { ContentCopy, Delete, Add, Edit, Autorenew, Search, Warning, Lock, LockOpen, Code } from "@mui/icons-material";
|
|
207
228
|
function ExpiryChip({ expiresAt }) {
|
|
208
229
|
const status = expiryStatus(expiresAt);
|
|
209
230
|
if (!status) return /* @__PURE__ */ React2.createElement(Typography2, { variant: "body2", color: "text.secondary" }, "-");
|
|
@@ -223,11 +244,39 @@ function fmtCost(perToken) {
|
|
|
223
244
|
const per1k = perToken * 1e3;
|
|
224
245
|
return per1k < 0.01 ? `$${(perToken * 1e6).toFixed(2)}/M` : `$${per1k.toFixed(3)}/1K`;
|
|
225
246
|
}
|
|
226
|
-
|
|
247
|
+
function trimSlash(url) {
|
|
248
|
+
return url.replace(/\/+$/, "");
|
|
249
|
+
}
|
|
250
|
+
function buildSnippets(baseUrl, key, model) {
|
|
251
|
+
const base = trimSlash(baseUrl);
|
|
252
|
+
return {
|
|
253
|
+
curl: `curl ${base}/v1/chat/completions \\
|
|
254
|
+
-H "Authorization: Bearer ${key}" \\
|
|
255
|
+
-H "Content-Type: application/json" \\
|
|
256
|
+
-d '{
|
|
257
|
+
"model": "${model}",
|
|
258
|
+
"messages": [{ "role": "user", "content": "Hello!" }]
|
|
259
|
+
}'`,
|
|
260
|
+
openai: `from openai import OpenAI
|
|
261
|
+
|
|
262
|
+
client = OpenAI(
|
|
263
|
+
api_key="${key}",
|
|
264
|
+
base_url="${base}/v1",
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
response = client.chat.completions.create(
|
|
268
|
+
model="${model}",
|
|
269
|
+
messages=[{ "role": "user", "content": "Hello!" }],
|
|
270
|
+
)
|
|
271
|
+
print(response.choices[0].message.content)`
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
var maskKey, shortKeyId, formatDate, emptyForm, keyToEditForm, KeysTable, SnippetTabs;
|
|
227
275
|
var init_KeysTable = __esm({
|
|
228
276
|
"src/components/KeysTable.tsx"() {
|
|
229
277
|
"use strict";
|
|
230
278
|
init_api();
|
|
279
|
+
init_format();
|
|
231
280
|
maskKey = (key) => {
|
|
232
281
|
if (key.length <= 8) return "***";
|
|
233
282
|
return `${key.slice(0, 4)}...${key.slice(-4)}`;
|
|
@@ -273,11 +322,15 @@ var init_KeysTable = __esm({
|
|
|
273
322
|
onUnblockKey,
|
|
274
323
|
onResetKeySpend,
|
|
275
324
|
onDeleteKey,
|
|
276
|
-
onPruneExpiredKeys
|
|
325
|
+
onPruneExpiredKeys,
|
|
326
|
+
onGetConfig
|
|
277
327
|
}) => {
|
|
278
328
|
const [generateModalOpen, setGenerateModalOpen] = useState(false);
|
|
279
329
|
const [newKeyValue, setNewKeyValue] = useState(null);
|
|
330
|
+
const [newKeySnippets, setNewKeySnippets] = useState(null);
|
|
331
|
+
const [newKeyModel, setNewKeyModel] = useState("");
|
|
280
332
|
const [formData, setFormData] = useState(emptyForm());
|
|
333
|
+
const [unlimitedBudget, setUnlimitedBudget] = useState(false);
|
|
281
334
|
const [submitting, setSubmitting] = useState(false);
|
|
282
335
|
const [editingKey, setEditingKey] = useState(null);
|
|
283
336
|
const [editForm, setEditForm] = useState({});
|
|
@@ -300,12 +353,35 @@ var init_KeysTable = __esm({
|
|
|
300
353
|
const selectedModels = models.filter((m) => (formData.models || []).includes(m.model_name));
|
|
301
354
|
const selectedTeam = teams.find((t) => t.team_id === formData.team_id) ?? null;
|
|
302
355
|
const editSelectedModels = models.filter((m) => (editForm.models || []).includes(m.model_name));
|
|
356
|
+
const aliasError = !(formData.alias || "").trim();
|
|
357
|
+
const budgetInvalid = !unlimitedBudget && (formData.max_budget === void 0 || formData.max_budget === null || formData.max_budget <= 0);
|
|
358
|
+
const canGenerate = !aliasError && !budgetInvalid && !submitting;
|
|
359
|
+
const budgetEstimate = useMemo(() => {
|
|
360
|
+
if (unlimitedBudget || budgetInvalid) return null;
|
|
361
|
+
const inputCosts = selectedModels.map((m) => m.input_cost_per_token).filter((c) => typeof c === "number" && c > 0);
|
|
362
|
+
if (inputCosts.length === 0) return null;
|
|
363
|
+
const pricePerToken = Math.max(...inputCosts);
|
|
364
|
+
return estimateTokensFromBudget(formData.max_budget ?? 0, pricePerToken);
|
|
365
|
+
}, [formData.max_budget, selectedModels, unlimitedBudget, budgetInvalid]);
|
|
303
366
|
const handleGenerate = async () => {
|
|
304
367
|
setSubmitting(true);
|
|
305
368
|
try {
|
|
306
|
-
const
|
|
369
|
+
const request = {
|
|
370
|
+
...formData,
|
|
371
|
+
max_budget: unlimitedBudget ? null : formData.max_budget
|
|
372
|
+
};
|
|
373
|
+
const response = await onGenerateKey(request);
|
|
307
374
|
setNewKeyValue(response.key);
|
|
375
|
+
setNewKeyModel(formData.models?.[0] ?? "");
|
|
376
|
+
setNewKeySnippets(null);
|
|
377
|
+
try {
|
|
378
|
+
const config = await onGetConfig();
|
|
379
|
+
const model = formData.models?.[0] ?? "";
|
|
380
|
+
setNewKeySnippets(buildSnippets(config.baseUrl, response.key, model));
|
|
381
|
+
} catch {
|
|
382
|
+
}
|
|
308
383
|
setFormData(emptyForm());
|
|
384
|
+
setUnlimitedBudget(false);
|
|
309
385
|
} catch (error) {
|
|
310
386
|
console.error("Failed to generate key:", error);
|
|
311
387
|
} finally {
|
|
@@ -315,7 +391,9 @@ var init_KeysTable = __esm({
|
|
|
315
391
|
const handleCloseModal = () => {
|
|
316
392
|
setGenerateModalOpen(false);
|
|
317
393
|
setNewKeyValue(null);
|
|
394
|
+
setNewKeySnippets(null);
|
|
318
395
|
setFormData(emptyForm());
|
|
396
|
+
setUnlimitedBudget(false);
|
|
319
397
|
};
|
|
320
398
|
const handleOpenEdit = (k) => {
|
|
321
399
|
setEditingKey(k);
|
|
@@ -440,7 +518,17 @@ var init_KeysTable = __esm({
|
|
|
440
518
|
onClick: () => setGenerateModalOpen(true)
|
|
441
519
|
},
|
|
442
520
|
"Generate New Key"
|
|
443
|
-
))), /* @__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" }, /* @__PURE__ */ React2.createElement(Typography2, { color: "text.secondary" },
|
|
521
|
+
))), /* @__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
|
+
Button,
|
|
523
|
+
{
|
|
524
|
+
variant: "contained",
|
|
525
|
+
color: "primary",
|
|
526
|
+
size: "small",
|
|
527
|
+
startIcon: /* @__PURE__ */ React2.createElement(Add, null),
|
|
528
|
+
onClick: () => setGenerateModalOpen(true)
|
|
529
|
+
},
|
|
530
|
+
"Generate Your First Key"
|
|
531
|
+
)))) : filteredKeys.map((key) => {
|
|
444
532
|
const keyId = key.token ?? key.key;
|
|
445
533
|
const isRotating = rotatingKeyId === keyId;
|
|
446
534
|
const isBlocking = blockingKeyId === keyId;
|
|
@@ -511,12 +599,14 @@ var init_KeysTable = __esm({
|
|
|
511
599
|
newKeyValue
|
|
512
600
|
),
|
|
513
601
|
/* @__PURE__ */ React2.createElement(IconButton, { onClick: () => copyToClipboard(newKeyValue) }, /* @__PURE__ */ React2.createElement(ContentCopy, null))
|
|
514
|
-
)) : /* @__PURE__ */ React2.createElement(Box2, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, /* @__PURE__ */ React2.createElement(
|
|
602
|
+
), newKeySnippets && /* @__PURE__ */ React2.createElement(Box2, { mt: 3 }, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React2.createElement(Code, { fontSize: "small", color: "action" }), /* @__PURE__ */ React2.createElement(Typography2, { variant: "subtitle2" }, "Start calling the proxy \u2014 paste and run")), /* @__PURE__ */ React2.createElement(SnippetTabs, { snippets: newKeySnippets, model: newKeyModel, onCopy: copyToClipboard }))) : /* @__PURE__ */ React2.createElement(Box2, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, /* @__PURE__ */ React2.createElement(
|
|
515
603
|
TextField,
|
|
516
604
|
{
|
|
517
605
|
label: "Alias",
|
|
518
606
|
value: formData.alias || "",
|
|
519
607
|
onChange: (e) => setFormData({ ...formData, alias: e.target.value }),
|
|
608
|
+
error: aliasError,
|
|
609
|
+
helperText: aliasError ? "Alias is required" : void 0,
|
|
520
610
|
required: true,
|
|
521
611
|
fullWidth: true
|
|
522
612
|
}
|
|
@@ -563,13 +653,35 @@ var init_KeysTable = __esm({
|
|
|
563
653
|
renderOption: (props, m) => /* @__PURE__ */ React2.createElement("li", { ...props }, modelOption(m)),
|
|
564
654
|
renderInput: (params) => /* @__PURE__ */ React2.createElement(TextField, { ...params, label: "Models", helperText: "Leave empty to allow all models", fullWidth: true })
|
|
565
655
|
}
|
|
656
|
+
), /* @__PURE__ */ React2.createElement(
|
|
657
|
+
FormControlLabel,
|
|
658
|
+
{
|
|
659
|
+
control: /* @__PURE__ */ React2.createElement(
|
|
660
|
+
Checkbox,
|
|
661
|
+
{
|
|
662
|
+
checked: unlimitedBudget,
|
|
663
|
+
onChange: (e) => {
|
|
664
|
+
setUnlimitedBudget(e.target.checked);
|
|
665
|
+
if (e.target.checked) {
|
|
666
|
+
setFormData({ ...formData, max_budget: null });
|
|
667
|
+
} else {
|
|
668
|
+
setFormData({ ...formData, max_budget: 100 });
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
),
|
|
673
|
+
label: "Unlimited budget"
|
|
674
|
+
}
|
|
566
675
|
), /* @__PURE__ */ React2.createElement(
|
|
567
676
|
TextField,
|
|
568
677
|
{
|
|
569
678
|
label: "Max Budget (USD)",
|
|
570
679
|
type: "number",
|
|
571
|
-
value: formData.max_budget ?? "",
|
|
680
|
+
value: unlimitedBudget ? "" : formData.max_budget ?? "",
|
|
572
681
|
onChange: (e) => setFormData({ ...formData, max_budget: e.target.value ? Number(e.target.value) : void 0 }),
|
|
682
|
+
error: budgetInvalid,
|
|
683
|
+
helperText: budgetInvalid ? 'Enter a positive budget or tick "Unlimited"' : budgetEstimate !== null ? `\u2248 ${fmtInt(budgetEstimate)} tokens at the selected model's rate` : void 0,
|
|
684
|
+
disabled: unlimitedBudget,
|
|
573
685
|
required: true,
|
|
574
686
|
fullWidth: true
|
|
575
687
|
}
|
|
@@ -597,7 +709,7 @@ var init_KeysTable = __esm({
|
|
|
597
709
|
onClick: handleGenerate,
|
|
598
710
|
variant: "contained",
|
|
599
711
|
color: "primary",
|
|
600
|
-
disabled:
|
|
712
|
+
disabled: !canGenerate
|
|
601
713
|
},
|
|
602
714
|
submitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 24 }) : "Generate"
|
|
603
715
|
), newKeyValue && /* @__PURE__ */ React2.createElement(Button, { onClick: handleCloseModal, variant: "contained", color: "success" }, "Done"))), /* @__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(
|
|
@@ -705,16 +817,44 @@ var init_KeysTable = __esm({
|
|
|
705
817
|
pruneSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 20 }) : "Prune"
|
|
706
818
|
))));
|
|
707
819
|
};
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
820
|
+
SnippetTabs = ({ snippets, model, onCopy }) => {
|
|
821
|
+
const [tab, setTab] = useState("curl");
|
|
822
|
+
const code = tab === "curl" ? snippets.curl : snippets.openai;
|
|
823
|
+
return /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Tabs, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 1 } }, /* @__PURE__ */ React2.createElement(Tab, { label: "curl", value: "curl" }), /* @__PURE__ */ React2.createElement(Tab, { label: "OpenAI SDK", value: "openai" })), /* @__PURE__ */ React2.createElement(
|
|
824
|
+
Box2,
|
|
825
|
+
{
|
|
826
|
+
position: "relative",
|
|
827
|
+
p: 1.5,
|
|
828
|
+
sx: { backgroundColor: "action.hover", border: "1px solid", borderColor: "divider", borderRadius: 1 }
|
|
829
|
+
},
|
|
830
|
+
/* @__PURE__ */ React2.createElement(
|
|
831
|
+
IconButton,
|
|
832
|
+
{
|
|
833
|
+
size: "small",
|
|
834
|
+
onClick: () => onCopy(code),
|
|
835
|
+
title: "Copy snippet",
|
|
836
|
+
sx: { position: "absolute", top: 4, right: 4 }
|
|
837
|
+
},
|
|
838
|
+
/* @__PURE__ */ React2.createElement(ContentCopy, { fontSize: "small" })
|
|
839
|
+
),
|
|
840
|
+
/* @__PURE__ */ React2.createElement(
|
|
841
|
+
Typography2,
|
|
842
|
+
{
|
|
843
|
+
component: "pre",
|
|
844
|
+
sx: {
|
|
845
|
+
fontFamily: "monospace",
|
|
846
|
+
fontSize: 12,
|
|
847
|
+
whiteSpace: "pre-wrap",
|
|
848
|
+
wordBreak: "break-all",
|
|
849
|
+
mb: 0,
|
|
850
|
+
pr: 4
|
|
851
|
+
}
|
|
852
|
+
},
|
|
853
|
+
code
|
|
854
|
+
),
|
|
855
|
+
model && /* @__PURE__ */ React2.createElement(Typography2, { variant: "caption", color: "text.secondary", display: "block", mt: 1 }, "Using model \u201C", model, "\u201D \u2014 swap it for any model you have access to.")
|
|
856
|
+
));
|
|
857
|
+
};
|
|
718
858
|
}
|
|
719
859
|
});
|
|
720
860
|
|
|
@@ -729,8 +869,8 @@ import {
|
|
|
729
869
|
Select,
|
|
730
870
|
MenuItem as MenuItem2,
|
|
731
871
|
Grid,
|
|
732
|
-
Tabs,
|
|
733
|
-
Tab,
|
|
872
|
+
Tabs as Tabs2,
|
|
873
|
+
Tab as Tab2,
|
|
734
874
|
Table as Table2,
|
|
735
875
|
TableBody as TableBody2,
|
|
736
876
|
TableCell as TableCell2,
|
|
@@ -931,7 +1071,7 @@ var init_UsageStats = __esm({
|
|
|
931
1071
|
value: fmtInt(usage?.total_tokens ?? 0),
|
|
932
1072
|
hint: `${fmtInt(usage?.prompt_tokens ?? 0)} in \xB7 ${fmtInt(usage?.completion_tokens ?? 0)} out`
|
|
933
1073
|
}
|
|
934
|
-
))), /* @__PURE__ */ React3.createElement(
|
|
1074
|
+
))), /* @__PURE__ */ React3.createElement(Tabs2, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 2 } }, /* @__PURE__ */ React3.createElement(Tab2, { value: "costs", label: "Costs" }), /* @__PURE__ */ React3.createElement(Tab2, { value: "models", label: "Model Activity" }), /* @__PURE__ */ React3.createElement(Tab2, { value: "keys", label: "Key Activity" })), tab === "costs" && /* @__PURE__ */ React3.createElement(Grid, { container: true, spacing: 3 }, /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12, md: 6 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend by Model"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : modelSpendByDate.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: modelSpendByDate }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "date", tick: { fontSize: 12 } }), /* @__PURE__ */ React3.createElement(YAxis, { tick: { fontSize: 12 }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [`$${v.toFixed(4)}`, void 0] }), /* @__PURE__ */ React3.createElement(Legend, null), topSpendModels.map((m) => /* @__PURE__ */ React3.createElement(
|
|
935
1075
|
Area,
|
|
936
1076
|
{
|
|
937
1077
|
key: m,
|
|
@@ -1067,7 +1207,7 @@ var init_TeamUsage = __esm({
|
|
|
1067
1207
|
return /* @__PURE__ */ React4.createElement(Paper4, { sx: { p: 2 } }, /* @__PURE__ */ React4.createElement(LinearProgress4, null));
|
|
1068
1208
|
}
|
|
1069
1209
|
if (!teams.length) {
|
|
1070
|
-
return /* @__PURE__ */ React4.createElement(Paper4, { sx: { p: 2 } }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "
|
|
1210
|
+
return /* @__PURE__ */ React4.createElement(Paper4, { sx: { p: 2 } }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "flex-start", gap: 1.5 }, /* @__PURE__ */ React4.createElement(Group, { color: "disabled", sx: { mt: 0.5 } }), /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Typography4, { color: "text.secondary", variant: "body2" }, "You're not a member of any LiteLLM team yet."), /* @__PURE__ */ React4.createElement(Typography4, { 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."))));
|
|
1071
1211
|
}
|
|
1072
1212
|
return /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Typography4, { variant: "h6", mb: 1 }, "Teams"), teams.map((team) => /* @__PURE__ */ React4.createElement(
|
|
1073
1213
|
TeamCard,
|
|
@@ -1221,7 +1361,7 @@ __export(LiteLLMPage_exports, {
|
|
|
1221
1361
|
LiteLLMPage: () => LiteLLMPage
|
|
1222
1362
|
});
|
|
1223
1363
|
import React6, { useState as useState5, useCallback as useCallback2, useMemo as useMemo3 } from "react";
|
|
1224
|
-
import { Box as Box6, Snackbar, Alert as Alert2, CircularProgress as CircularProgress4, Typography as Typography6, Paper as Paper6, Tabs as
|
|
1364
|
+
import { Box as Box6, Snackbar, Alert as Alert2, CircularProgress as CircularProgress4, Typography as Typography6, Paper as Paper6, Tabs as Tabs3, Tab as Tab3 } from "@mui/material";
|
|
1225
1365
|
import { useAsync as useAsync2, useAsyncRetry } from "react-use";
|
|
1226
1366
|
import { useApi } from "@backstage/core-plugin-api";
|
|
1227
1367
|
function initDateRange() {
|
|
@@ -1435,16 +1575,16 @@ var init_LiteLLMPage = __esm({
|
|
|
1435
1575
|
return /* @__PURE__ */ React6.createElement(Box6, { p: 3 }, /* @__PURE__ */ React6.createElement(Paper6, { sx: { p: 3 } }, /* @__PURE__ */ React6.createElement(Typography6, { variant: "h6", gutterBottom: true }, "Account not provisioned"), /* @__PURE__ */ React6.createElement(Typography6, { color: "text.secondary", paragraph: true }, "Your Backstage account is not linked to a LiteLLM user."), hint ? /* @__PURE__ */ React6.createElement(Typography6, { variant: "body2", color: "text.secondary" }, hint) : /* @__PURE__ */ React6.createElement(Typography6, { 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.")));
|
|
1436
1576
|
}
|
|
1437
1577
|
return /* @__PURE__ */ React6.createElement(Box6, { p: 3 }, /* @__PURE__ */ React6.createElement(Box6, { mb: 2 }, /* @__PURE__ */ React6.createElement(DashboardHeader, { userInfo, teams: teams ?? [], loading: userLoading || teamsLoading })), /* @__PURE__ */ React6.createElement(
|
|
1438
|
-
|
|
1578
|
+
Tabs3,
|
|
1439
1579
|
{
|
|
1440
1580
|
value: activeTab,
|
|
1441
1581
|
onChange: (_, v) => setActiveTab(v),
|
|
1442
1582
|
sx: { mb: 2, borderBottom: 1, borderColor: "divider" }
|
|
1443
1583
|
},
|
|
1444
|
-
/* @__PURE__ */ React6.createElement(
|
|
1445
|
-
/* @__PURE__ */ React6.createElement(
|
|
1446
|
-
/* @__PURE__ */ React6.createElement(
|
|
1447
|
-
userInfo.can_view_audit && /* @__PURE__ */ React6.createElement(
|
|
1584
|
+
/* @__PURE__ */ React6.createElement(Tab3, { label: "Overview", value: "overview" }),
|
|
1585
|
+
/* @__PURE__ */ React6.createElement(Tab3, { label: "Keys", value: "keys" }),
|
|
1586
|
+
/* @__PURE__ */ React6.createElement(Tab3, { label: "Teams", value: "teams" }),
|
|
1587
|
+
userInfo.can_view_audit && /* @__PURE__ */ React6.createElement(Tab3, { label: "Audit Log", value: "audit" })
|
|
1448
1588
|
), activeTab === "overview" && /* @__PURE__ */ React6.createElement(
|
|
1449
1589
|
UsageStats,
|
|
1450
1590
|
{
|
|
@@ -1469,7 +1609,8 @@ var init_LiteLLMPage = __esm({
|
|
|
1469
1609
|
onUnblockKey: handleUnblockKey,
|
|
1470
1610
|
onResetKeySpend: handleResetKeySpend,
|
|
1471
1611
|
onDeleteKey: handleDeleteKey,
|
|
1472
|
-
onPruneExpiredKeys: handlePruneExpiredKeys
|
|
1612
|
+
onPruneExpiredKeys: handlePruneExpiredKeys,
|
|
1613
|
+
onGetConfig: () => api.getConfig()
|
|
1473
1614
|
}
|
|
1474
1615
|
), activeTab === "teams" && /* @__PURE__ */ React6.createElement(
|
|
1475
1616
|
TeamUsage,
|
|
@@ -1573,32 +1714,42 @@ var LiteLLMHomeWidget = ({
|
|
|
1573
1714
|
const api = useApi2(liteLlmApiRef);
|
|
1574
1715
|
const [period, setPeriod] = useState6(defaultPeriod);
|
|
1575
1716
|
const [loading, setLoading] = useState6(true);
|
|
1576
|
-
const [
|
|
1717
|
+
const [usageError, setUsageError] = useState6(null);
|
|
1718
|
+
const [keysError, setKeysError] = useState6(null);
|
|
1577
1719
|
const [usage, setUsage] = useState6(null);
|
|
1578
1720
|
const [keys, setKeys] = useState6([]);
|
|
1579
1721
|
useEffect(() => {
|
|
1580
1722
|
let cancelled = false;
|
|
1581
1723
|
setLoading(true);
|
|
1582
|
-
|
|
1724
|
+
setUsageError(null);
|
|
1725
|
+
setKeysError(null);
|
|
1583
1726
|
const { start, end } = presetToDateRange(period);
|
|
1584
1727
|
const startDate = start.toISOString().split("T")[0];
|
|
1585
1728
|
const endDate = end.toISOString().split("T")[0];
|
|
1586
|
-
Promise.
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1729
|
+
Promise.allSettled([api.getUsage(startDate, endDate), api.listKeys()]).then(
|
|
1730
|
+
([usageResult, keysResult]) => {
|
|
1731
|
+
if (cancelled) return;
|
|
1732
|
+
if (usageResult.status === "fulfilled") {
|
|
1733
|
+
setUsage(usageResult.value);
|
|
1734
|
+
} else {
|
|
1735
|
+
setUsage(null);
|
|
1736
|
+
setUsageError(usageResult.reason?.message ?? "Failed to load usage data");
|
|
1737
|
+
}
|
|
1738
|
+
if (keysResult.status === "fulfilled") {
|
|
1739
|
+
setKeys(keysResult.value);
|
|
1740
|
+
} else {
|
|
1741
|
+
setKeys([]);
|
|
1742
|
+
setKeysError(keysResult.reason?.message ?? "Failed to load keys");
|
|
1743
|
+
}
|
|
1595
1744
|
setLoading(false);
|
|
1596
1745
|
}
|
|
1597
|
-
|
|
1746
|
+
);
|
|
1598
1747
|
return () => {
|
|
1599
1748
|
cancelled = true;
|
|
1600
1749
|
};
|
|
1601
1750
|
}, [api, period]);
|
|
1751
|
+
const partialFailure = !loading && (usageError || keysError) && (usage || keys.length > 0);
|
|
1752
|
+
const totalFailure = !loading && usageError && keysError;
|
|
1602
1753
|
const dailyData = (usage?.daily_usage ?? []).map((d) => ({
|
|
1603
1754
|
date: d.date,
|
|
1604
1755
|
spend: d.spend
|
|
@@ -1614,7 +1765,7 @@ var LiteLLMHomeWidget = ({
|
|
|
1614
1765
|
/* @__PURE__ */ React8.createElement(MenuItem4, { value: "today" }, "Today"),
|
|
1615
1766
|
/* @__PURE__ */ React8.createElement(MenuItem4, { value: "7d" }, "7d"),
|
|
1616
1767
|
/* @__PURE__ */ React8.createElement(MenuItem4, { value: "30d" }, "30d")
|
|
1617
|
-
))), loading && /* @__PURE__ */ React8.createElement(Box7, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ React8.createElement(CircularProgress5, { size: 32 })), !loading &&
|
|
1768
|
+
))), loading && /* @__PURE__ */ React8.createElement(Box7, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ React8.createElement(CircularProgress5, { size: 32 })), !loading && totalFailure && /* @__PURE__ */ React8.createElement(Alert3, { severity: "error", sx: { mt: 1 } }, usageError ?? "Failed to load usage data"), !loading && !totalFailure && /* @__PURE__ */ React8.createElement(React8.Fragment, null, partialFailure && /* @__PURE__ */ React8.createElement(Alert3, { severity: "warning", sx: { mt: 1, mb: 1 } }, usageError ? `Usage data unavailable (${usageError}).` : "", keysError ? ` Key list unavailable (${keysError}).` : "", " Showing what loaded."), /* @__PURE__ */ React8.createElement(Grid2, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "USD Spent", value: fmtUsd(usage?.total_spend ?? 0) })), /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "Tokens In", value: fmtInt(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "Tokens Out", value: fmtInt(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ React8.createElement(Grid2, { item: true, xs: 6 }, /* @__PURE__ */ React8.createElement(Kpi, { label: "Keys", value: fmtInt(keys.length) }))), hasSparkline && /* @__PURE__ */ React8.createElement(Box7, { height: 120 }, /* @__PURE__ */ React8.createElement(ResponsiveContainer3, { width: "100%", height: "100%" }, /* @__PURE__ */ React8.createElement(AreaChart3, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 } }, /* @__PURE__ */ React8.createElement(
|
|
1618
1769
|
Area3,
|
|
1619
1770
|
{
|
|
1620
1771
|
type: "monotone",
|