@acarmisc/backstage-plugin-litellm 0.8.1 → 0.10.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 +241 -38
- package/dist/index.cjs.js.map +3 -3
- package/dist/index.esm.js +259 -50
- package/dist/index.esm.js.map +4 -4
- package/dist/types.d.ts +12 -1
- package/package.json +1 -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,54 @@ 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 formatContextWindow(maxInput, maxOutput) {
|
|
248
|
+
if (!maxInput && !maxOutput) return null;
|
|
249
|
+
const fmt = (n) => {
|
|
250
|
+
if (!n) return null;
|
|
251
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
252
|
+
if (n >= 1e3) return `${Math.round(n / 1e3)}K`;
|
|
253
|
+
return String(n);
|
|
254
|
+
};
|
|
255
|
+
const inPart = fmt(maxInput);
|
|
256
|
+
const outPart = fmt(maxOutput);
|
|
257
|
+
if (inPart && outPart) return `ctx ${inPart} in / ${outPart} out`;
|
|
258
|
+
if (inPart) return `ctx ${inPart}`;
|
|
259
|
+
if (outPart) return `ctx ${outPart} out`;
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
function trimSlash(url) {
|
|
263
|
+
return url.replace(/\/+$/, "");
|
|
264
|
+
}
|
|
265
|
+
function buildSnippets(baseUrl, key, model) {
|
|
266
|
+
const base = trimSlash(baseUrl);
|
|
267
|
+
return {
|
|
268
|
+
curl: `curl ${base}/v1/chat/completions \\
|
|
269
|
+
-H "Authorization: Bearer ${key}" \\
|
|
270
|
+
-H "Content-Type: application/json" \\
|
|
271
|
+
-d '{
|
|
272
|
+
"model": "${model}",
|
|
273
|
+
"messages": [{ "role": "user", "content": "Hello!" }]
|
|
274
|
+
}'`,
|
|
275
|
+
openai: `from openai import OpenAI
|
|
276
|
+
|
|
277
|
+
client = OpenAI(
|
|
278
|
+
api_key="${key}",
|
|
279
|
+
base_url="${base}/v1",
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
response = client.chat.completions.create(
|
|
283
|
+
model="${model}",
|
|
284
|
+
messages=[{ "role": "user", "content": "Hello!" }],
|
|
285
|
+
)
|
|
286
|
+
print(response.choices[0].message.content)`
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
var maskKey, shortKeyId, formatDate, emptyForm, keyToEditForm, KeysTable, SnippetTabs;
|
|
227
290
|
var init_KeysTable = __esm({
|
|
228
291
|
"src/components/KeysTable.tsx"() {
|
|
229
292
|
"use strict";
|
|
230
293
|
init_api();
|
|
294
|
+
init_format();
|
|
231
295
|
maskKey = (key) => {
|
|
232
296
|
if (key.length <= 8) return "***";
|
|
233
297
|
return `${key.slice(0, 4)}...${key.slice(-4)}`;
|
|
@@ -252,7 +316,9 @@ var init_KeysTable = __esm({
|
|
|
252
316
|
tpm_limit: void 0,
|
|
253
317
|
rpm_limit: void 0,
|
|
254
318
|
team_id: void 0,
|
|
255
|
-
key_type: "llm_api"
|
|
319
|
+
key_type: "llm_api",
|
|
320
|
+
auto_rotate: false,
|
|
321
|
+
rotation_interval_days: void 0
|
|
256
322
|
});
|
|
257
323
|
keyToEditForm = (k) => ({
|
|
258
324
|
key_alias: k.key_alias ?? "",
|
|
@@ -273,11 +339,15 @@ var init_KeysTable = __esm({
|
|
|
273
339
|
onUnblockKey,
|
|
274
340
|
onResetKeySpend,
|
|
275
341
|
onDeleteKey,
|
|
276
|
-
onPruneExpiredKeys
|
|
342
|
+
onPruneExpiredKeys,
|
|
343
|
+
onGetConfig
|
|
277
344
|
}) => {
|
|
278
345
|
const [generateModalOpen, setGenerateModalOpen] = useState(false);
|
|
279
346
|
const [newKeyValue, setNewKeyValue] = useState(null);
|
|
347
|
+
const [newKeySnippets, setNewKeySnippets] = useState(null);
|
|
348
|
+
const [newKeyModel, setNewKeyModel] = useState("");
|
|
280
349
|
const [formData, setFormData] = useState(emptyForm());
|
|
350
|
+
const [unlimitedBudget, setUnlimitedBudget] = useState(false);
|
|
281
351
|
const [submitting, setSubmitting] = useState(false);
|
|
282
352
|
const [editingKey, setEditingKey] = useState(null);
|
|
283
353
|
const [editForm, setEditForm] = useState({});
|
|
@@ -300,12 +370,35 @@ var init_KeysTable = __esm({
|
|
|
300
370
|
const selectedModels = models.filter((m) => (formData.models || []).includes(m.model_name));
|
|
301
371
|
const selectedTeam = teams.find((t) => t.team_id === formData.team_id) ?? null;
|
|
302
372
|
const editSelectedModels = models.filter((m) => (editForm.models || []).includes(m.model_name));
|
|
373
|
+
const aliasError = !(formData.alias || "").trim();
|
|
374
|
+
const budgetInvalid = !unlimitedBudget && (formData.max_budget === void 0 || formData.max_budget === null || formData.max_budget <= 0);
|
|
375
|
+
const canGenerate = !aliasError && !budgetInvalid && !submitting;
|
|
376
|
+
const budgetEstimate = useMemo(() => {
|
|
377
|
+
if (unlimitedBudget || budgetInvalid) return null;
|
|
378
|
+
const inputCosts = selectedModels.map((m) => m.input_cost_per_token).filter((c) => typeof c === "number" && c > 0);
|
|
379
|
+
if (inputCosts.length === 0) return null;
|
|
380
|
+
const pricePerToken = Math.max(...inputCosts);
|
|
381
|
+
return estimateTokensFromBudget(formData.max_budget ?? 0, pricePerToken);
|
|
382
|
+
}, [formData.max_budget, selectedModels, unlimitedBudget, budgetInvalid]);
|
|
303
383
|
const handleGenerate = async () => {
|
|
304
384
|
setSubmitting(true);
|
|
305
385
|
try {
|
|
306
|
-
const
|
|
386
|
+
const request = {
|
|
387
|
+
...formData,
|
|
388
|
+
max_budget: unlimitedBudget ? null : formData.max_budget
|
|
389
|
+
};
|
|
390
|
+
const response = await onGenerateKey(request);
|
|
307
391
|
setNewKeyValue(response.key);
|
|
392
|
+
setNewKeyModel(formData.models?.[0] ?? "");
|
|
393
|
+
setNewKeySnippets(null);
|
|
394
|
+
try {
|
|
395
|
+
const config = await onGetConfig();
|
|
396
|
+
const model = formData.models?.[0] ?? "";
|
|
397
|
+
setNewKeySnippets(buildSnippets(config.baseUrl, response.key, model));
|
|
398
|
+
} catch {
|
|
399
|
+
}
|
|
308
400
|
setFormData(emptyForm());
|
|
401
|
+
setUnlimitedBudget(false);
|
|
309
402
|
} catch (error) {
|
|
310
403
|
console.error("Failed to generate key:", error);
|
|
311
404
|
} finally {
|
|
@@ -315,7 +408,9 @@ var init_KeysTable = __esm({
|
|
|
315
408
|
const handleCloseModal = () => {
|
|
316
409
|
setGenerateModalOpen(false);
|
|
317
410
|
setNewKeyValue(null);
|
|
411
|
+
setNewKeySnippets(null);
|
|
318
412
|
setFormData(emptyForm());
|
|
413
|
+
setUnlimitedBudget(false);
|
|
319
414
|
};
|
|
320
415
|
const handleOpenEdit = (k) => {
|
|
321
416
|
setEditingKey(k);
|
|
@@ -406,7 +501,8 @@ var init_KeysTable = __esm({
|
|
|
406
501
|
const modelOption = (m) => {
|
|
407
502
|
const inCost = fmtCost(m.input_cost_per_token);
|
|
408
503
|
const outCost = fmtCost(m.output_cost_per_token);
|
|
409
|
-
|
|
504
|
+
const ctx = formatContextWindow(m.max_input_tokens, m.max_output_tokens);
|
|
505
|
+
return /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement("span", null, m.model_name), m.supports_function_calling && " \u{1F527}", m.supports_vision && " \u{1F441}\uFE0F", ctx && /* @__PURE__ */ React2.createElement(Typography2, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, ctx), (inCost || outCost) && /* @__PURE__ */ React2.createElement(Typography2, { variant: "caption", color: "text.secondary", sx: { ml: 1 } }, inCost, " in \xB7 ", outCost, " out"));
|
|
410
506
|
};
|
|
411
507
|
return /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement(Paper2, { sx: { mb: 2 } }, /* @__PURE__ */ React2.createElement(Box2, { display: "flex", justifyContent: "space-between", alignItems: "center", p: 2, gap: 2 }, /* @__PURE__ */ React2.createElement(Typography2, { variant: "h6" }, "Virtual Keys"), /* @__PURE__ */ React2.createElement(Box2, { display: "flex", gap: 1, alignItems: "center", flex: 1, justifyContent: "flex-end" }, /* @__PURE__ */ React2.createElement(
|
|
412
508
|
TextField,
|
|
@@ -440,7 +536,17 @@ var init_KeysTable = __esm({
|
|
|
440
536
|
onClick: () => setGenerateModalOpen(true)
|
|
441
537
|
},
|
|
442
538
|
"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" },
|
|
539
|
+
))), /* @__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(
|
|
540
|
+
Button,
|
|
541
|
+
{
|
|
542
|
+
variant: "contained",
|
|
543
|
+
color: "primary",
|
|
544
|
+
size: "small",
|
|
545
|
+
startIcon: /* @__PURE__ */ React2.createElement(Add, null),
|
|
546
|
+
onClick: () => setGenerateModalOpen(true)
|
|
547
|
+
},
|
|
548
|
+
"Generate Your First Key"
|
|
549
|
+
)))) : filteredKeys.map((key) => {
|
|
444
550
|
const keyId = key.token ?? key.key;
|
|
445
551
|
const isRotating = rotatingKeyId === keyId;
|
|
446
552
|
const isBlocking = blockingKeyId === keyId;
|
|
@@ -511,12 +617,14 @@ var init_KeysTable = __esm({
|
|
|
511
617
|
newKeyValue
|
|
512
618
|
),
|
|
513
619
|
/* @__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(
|
|
620
|
+
), 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
621
|
TextField,
|
|
516
622
|
{
|
|
517
623
|
label: "Alias",
|
|
518
624
|
value: formData.alias || "",
|
|
519
625
|
onChange: (e) => setFormData({ ...formData, alias: e.target.value }),
|
|
626
|
+
error: aliasError,
|
|
627
|
+
helperText: aliasError ? "Alias is required" : void 0,
|
|
520
628
|
required: true,
|
|
521
629
|
fullWidth: true
|
|
522
630
|
}
|
|
@@ -563,13 +671,35 @@ var init_KeysTable = __esm({
|
|
|
563
671
|
renderOption: (props, m) => /* @__PURE__ */ React2.createElement("li", { ...props }, modelOption(m)),
|
|
564
672
|
renderInput: (params) => /* @__PURE__ */ React2.createElement(TextField, { ...params, label: "Models", helperText: "Leave empty to allow all models", fullWidth: true })
|
|
565
673
|
}
|
|
674
|
+
), /* @__PURE__ */ React2.createElement(
|
|
675
|
+
FormControlLabel,
|
|
676
|
+
{
|
|
677
|
+
control: /* @__PURE__ */ React2.createElement(
|
|
678
|
+
Checkbox,
|
|
679
|
+
{
|
|
680
|
+
checked: unlimitedBudget,
|
|
681
|
+
onChange: (e) => {
|
|
682
|
+
setUnlimitedBudget(e.target.checked);
|
|
683
|
+
if (e.target.checked) {
|
|
684
|
+
setFormData({ ...formData, max_budget: null });
|
|
685
|
+
} else {
|
|
686
|
+
setFormData({ ...formData, max_budget: 100 });
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
),
|
|
691
|
+
label: "Unlimited budget"
|
|
692
|
+
}
|
|
566
693
|
), /* @__PURE__ */ React2.createElement(
|
|
567
694
|
TextField,
|
|
568
695
|
{
|
|
569
696
|
label: "Max Budget (USD)",
|
|
570
697
|
type: "number",
|
|
571
|
-
value: formData.max_budget ?? "",
|
|
698
|
+
value: unlimitedBudget ? "" : formData.max_budget ?? "",
|
|
572
699
|
onChange: (e) => setFormData({ ...formData, max_budget: e.target.value ? Number(e.target.value) : void 0 }),
|
|
700
|
+
error: budgetInvalid,
|
|
701
|
+
helperText: budgetInvalid ? 'Enter a positive budget or tick "Unlimited"' : budgetEstimate !== null ? `\u2248 ${fmtInt(budgetEstimate)} tokens at the selected model's rate` : void 0,
|
|
702
|
+
disabled: unlimitedBudget,
|
|
573
703
|
required: true,
|
|
574
704
|
fullWidth: true
|
|
575
705
|
}
|
|
@@ -591,13 +721,42 @@ var init_KeysTable = __esm({
|
|
|
591
721
|
onChange: (e) => setFormData({ ...formData, rpm_limit: e.target.value ? Number(e.target.value) : void 0 }),
|
|
592
722
|
fullWidth: true
|
|
593
723
|
}
|
|
724
|
+
), /* @__PURE__ */ React2.createElement(
|
|
725
|
+
FormControlLabel,
|
|
726
|
+
{
|
|
727
|
+
control: /* @__PURE__ */ React2.createElement(
|
|
728
|
+
Checkbox,
|
|
729
|
+
{
|
|
730
|
+
checked: !!formData.auto_rotate,
|
|
731
|
+
onChange: (e) => setFormData({
|
|
732
|
+
...formData,
|
|
733
|
+
auto_rotate: e.target.checked,
|
|
734
|
+
rotation_interval_days: e.target.checked ? formData.rotation_interval_days ?? 90 : void 0
|
|
735
|
+
})
|
|
736
|
+
}
|
|
737
|
+
),
|
|
738
|
+
label: "Auto-rotate (LiteLLM rotates the secret on a schedule)"
|
|
739
|
+
}
|
|
740
|
+
), formData.auto_rotate && /* @__PURE__ */ React2.createElement(
|
|
741
|
+
TextField,
|
|
742
|
+
{
|
|
743
|
+
label: "Rotate every (days)",
|
|
744
|
+
type: "number",
|
|
745
|
+
value: formData.rotation_interval_days ?? "",
|
|
746
|
+
onChange: (e) => setFormData({
|
|
747
|
+
...formData,
|
|
748
|
+
rotation_interval_days: e.target.value ? Number(e.target.value) : void 0
|
|
749
|
+
}),
|
|
750
|
+
helperText: "LiteLLM enforces this interval server-side; the old secret stops working the moment it rotates.",
|
|
751
|
+
fullWidth: true
|
|
752
|
+
}
|
|
594
753
|
))), /* @__PURE__ */ React2.createElement(DialogActions, null, /* @__PURE__ */ React2.createElement(Button, { onClick: handleCloseModal }, newKeyValue ? "Done" : "Cancel"), !newKeyValue && /* @__PURE__ */ React2.createElement(
|
|
595
754
|
Button,
|
|
596
755
|
{
|
|
597
756
|
onClick: handleGenerate,
|
|
598
757
|
variant: "contained",
|
|
599
758
|
color: "primary",
|
|
600
|
-
disabled:
|
|
759
|
+
disabled: !canGenerate
|
|
601
760
|
},
|
|
602
761
|
submitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 24 }) : "Generate"
|
|
603
762
|
), 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 +864,44 @@ var init_KeysTable = __esm({
|
|
|
705
864
|
pruneSubmitting ? /* @__PURE__ */ React2.createElement(CircularProgress, { size: 20 }) : "Prune"
|
|
706
865
|
))));
|
|
707
866
|
};
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
867
|
+
SnippetTabs = ({ snippets, model, onCopy }) => {
|
|
868
|
+
const [tab, setTab] = useState("curl");
|
|
869
|
+
const code = tab === "curl" ? snippets.curl : snippets.openai;
|
|
870
|
+
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(
|
|
871
|
+
Box2,
|
|
872
|
+
{
|
|
873
|
+
position: "relative",
|
|
874
|
+
p: 1.5,
|
|
875
|
+
sx: { backgroundColor: "action.hover", border: "1px solid", borderColor: "divider", borderRadius: 1 }
|
|
876
|
+
},
|
|
877
|
+
/* @__PURE__ */ React2.createElement(
|
|
878
|
+
IconButton,
|
|
879
|
+
{
|
|
880
|
+
size: "small",
|
|
881
|
+
onClick: () => onCopy(code),
|
|
882
|
+
title: "Copy snippet",
|
|
883
|
+
sx: { position: "absolute", top: 4, right: 4 }
|
|
884
|
+
},
|
|
885
|
+
/* @__PURE__ */ React2.createElement(ContentCopy, { fontSize: "small" })
|
|
886
|
+
),
|
|
887
|
+
/* @__PURE__ */ React2.createElement(
|
|
888
|
+
Typography2,
|
|
889
|
+
{
|
|
890
|
+
component: "pre",
|
|
891
|
+
sx: {
|
|
892
|
+
fontFamily: "monospace",
|
|
893
|
+
fontSize: 12,
|
|
894
|
+
whiteSpace: "pre-wrap",
|
|
895
|
+
wordBreak: "break-all",
|
|
896
|
+
mb: 0,
|
|
897
|
+
pr: 4
|
|
898
|
+
}
|
|
899
|
+
},
|
|
900
|
+
code
|
|
901
|
+
),
|
|
902
|
+
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.")
|
|
903
|
+
));
|
|
904
|
+
};
|
|
718
905
|
}
|
|
719
906
|
});
|
|
720
907
|
|
|
@@ -729,8 +916,8 @@ import {
|
|
|
729
916
|
Select,
|
|
730
917
|
MenuItem as MenuItem2,
|
|
731
918
|
Grid,
|
|
732
|
-
Tabs,
|
|
733
|
-
Tab,
|
|
919
|
+
Tabs as Tabs2,
|
|
920
|
+
Tab as Tab2,
|
|
734
921
|
Table as Table2,
|
|
735
922
|
TableBody as TableBody2,
|
|
736
923
|
TableCell as TableCell2,
|
|
@@ -739,7 +926,8 @@ import {
|
|
|
739
926
|
TableRow as TableRow2,
|
|
740
927
|
Chip as Chip3,
|
|
741
928
|
LinearProgress as LinearProgress3,
|
|
742
|
-
Skeleton
|
|
929
|
+
Skeleton,
|
|
930
|
+
useTheme
|
|
743
931
|
} from "@mui/material";
|
|
744
932
|
import {
|
|
745
933
|
AreaChart,
|
|
@@ -795,8 +983,14 @@ var init_UsageStats = __esm({
|
|
|
795
983
|
loading,
|
|
796
984
|
userInfo
|
|
797
985
|
}) => {
|
|
986
|
+
const theme = useTheme();
|
|
798
987
|
const [selectedModel, setSelectedModel] = useState2("all");
|
|
799
988
|
const [tab, setTab] = useState2("costs");
|
|
989
|
+
const gridStroke = theme.palette.divider;
|
|
990
|
+
const tickFill = theme.palette.text.secondary;
|
|
991
|
+
const tickStyle = { fontSize: 12, fill: tickFill };
|
|
992
|
+
const tickStyleSmall = { fontSize: 11, fill: tickFill };
|
|
993
|
+
const tickStyleTiny = { fontSize: 10, fill: tickFill };
|
|
800
994
|
const selectedPreset = useMemo2(() => {
|
|
801
995
|
if (dateRange.start.toDateString() === dateRange.end.toDateString()) return "today";
|
|
802
996
|
const diffMs = dateRange.end.getTime() - dateRange.start.getTime();
|
|
@@ -931,7 +1125,7 @@ var init_UsageStats = __esm({
|
|
|
931
1125
|
value: fmtInt(usage?.total_tokens ?? 0),
|
|
932
1126
|
hint: `${fmtInt(usage?.prompt_tokens ?? 0)} in \xB7 ${fmtInt(usage?.completion_tokens ?? 0)} out`
|
|
933
1127
|
}
|
|
934
|
-
))), /* @__PURE__ */ React3.createElement(
|
|
1128
|
+
))), /* @__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", 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)}`, void 0] }), /* @__PURE__ */ React3.createElement(Legend, null), topSpendModels.map((m) => /* @__PURE__ */ React3.createElement(
|
|
935
1129
|
Area,
|
|
936
1130
|
{
|
|
937
1131
|
key: m,
|
|
@@ -942,14 +1136,14 @@ var init_UsageStats = __esm({
|
|
|
942
1136
|
fill: modelColor(m),
|
|
943
1137
|
fillOpacity: 0.6
|
|
944
1138
|
}
|
|
945
|
-
)))))), /* @__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" }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "date", tick: { fontSize: 12 } }), /* @__PURE__ */ React3.createElement(YAxis, { tick: { fontSize: 12 }, 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" }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "date", tick: { fontSize: 12 } }), /* @__PURE__ */ React3.createElement(YAxis, { tick: { fontSize: 12 } }), /* @__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" }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "date", tick: { fontSize: 12 } }), /* @__PURE__ */ React3.createElement(YAxis, { domain: [0, 100], tick: { fontSize: 12 }, 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" }), /* @__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)}`, "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" }), /* @__PURE__ */ React3.createElement(XAxis, { type: "number", tick: { fontSize: 12 }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React3.createElement(YAxis, { type: "category", dataKey: "model", tick: { fontSize: 11 }, 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" }), /* @__PURE__ */ React3.createElement(XAxis, { dataKey: "model", tick: { fontSize: 10 }, interval: 0, angle: -15, textAnchor: "end", height: 60 }), /* @__PURE__ */ React3.createElement(YAxis, { tick: { fontSize: 12 } }), /* @__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(
|
|
1139
|
+
)))))), /* @__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(
|
|
946
1140
|
LinearProgress3,
|
|
947
1141
|
{
|
|
948
1142
|
variant: "determinate",
|
|
949
1143
|
value: r.successRate * 100,
|
|
950
1144
|
sx: { flex: 1, height: 6, borderRadius: 3 }
|
|
951
1145
|
}
|
|
952
|
-
), /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption" }, fmtPct(r.successRate))) : "\u2014")))))))), tab === "keys" && /* @__PURE__ */ React3.createElement(Grid, { container: true, spacing: 3 }, (loading || topKeySpendBars.length > 0) && /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Spend by Key"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : /* @__PURE__ */ React3.createElement(Box3, { height: Math.max(160, topKeySpendBars.length * 36) }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(BarChart, { data: topKeySpendBars, layout: "vertical", margin: { left: 20 } }, /* @__PURE__ */ React3.createElement(CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ React3.createElement(XAxis, { type: "number", tick:
|
|
1146
|
+
), /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption" }, fmtPct(r.successRate))) : "\u2014")))))))), tab === "keys" && /* @__PURE__ */ React3.createElement(Grid, { container: true, spacing: 3 }, (loading || topKeySpendBars.length > 0) && /* @__PURE__ */ React3.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React3.createElement(Typography3, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Spend by Key"), loading ? /* @__PURE__ */ React3.createElement(ChartSkeleton, null) : /* @__PURE__ */ React3.createElement(Box3, { height: Math.max(160, topKeySpendBars.length * 36) }, /* @__PURE__ */ React3.createElement(ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ React3.createElement(BarChart, { data: topKeySpendBars, 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: "keyAlias", tick: tickStyleSmall, width: 160 }), /* @__PURE__ */ React3.createElement(Tooltip, { formatter: (v) => [fmtUsd(v), "Spend"] }), /* @__PURE__ */ React3.createElement(Bar, { dataKey: "spend", name: "Spend", fill: "#8884d8", radius: [0, 4, 4, 0] }))))), /* @__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, "Key"), /* @__PURE__ */ React3.createElement(TableCell2, null, "Models"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Spend"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Requests"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Tokens"), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, "Failed"), /* @__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: 7 }, /* @__PURE__ */ React3.createElement(LinearProgress3, null))) : keyRows.length === 0 ? /* @__PURE__ */ React3.createElement(TableRow2, null, /* @__PURE__ */ React3.createElement(TableCell2, { colSpan: 7, align: "center" }, "No key activity")) : [...keyRows].sort((a, b) => b.spend - a.spend || b.apiRequests - a.apiRequests).map((r) => /* @__PURE__ */ React3.createElement(TableRow2, { key: r.keyHash }, /* @__PURE__ */ React3.createElement(TableCell2, null, /* @__PURE__ */ React3.createElement(Typography3, { variant: "body2" }, r.keyAlias), r.teamId ? /* @__PURE__ */ React3.createElement(Typography3, { variant: "caption", color: "text.secondary" }, "team: ", r.teamId) : null), /* @__PURE__ */ React3.createElement(TableCell2, null, /* @__PURE__ */ React3.createElement(Box3, { display: "flex", gap: 0.5, flexWrap: "wrap" }, r.models.map((m) => /* @__PURE__ */ React3.createElement(Chip3, { key: m, label: m, size: "small", variant: "outlined" })))), /* @__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.totalTokens)), /* @__PURE__ */ React3.createElement(TableCell2, { align: "right" }, fmtInt(r.failedRequests)), /* @__PURE__ */ React3.createElement(TableCell2, null, r.apiRequests > 0 ? /* @__PURE__ */ React3.createElement(Box3, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React3.createElement(
|
|
953
1147
|
LinearProgress3,
|
|
954
1148
|
{
|
|
955
1149
|
variant: "determinate",
|
|
@@ -979,7 +1173,8 @@ import {
|
|
|
979
1173
|
TableContainer as TableContainer3,
|
|
980
1174
|
Collapse,
|
|
981
1175
|
IconButton as IconButton2,
|
|
982
|
-
CircularProgress as CircularProgress2
|
|
1176
|
+
CircularProgress as CircularProgress2,
|
|
1177
|
+
useTheme as useTheme2
|
|
983
1178
|
} from "@mui/material";
|
|
984
1179
|
import { ExpandMore, ExpandLess, Group, Speed, Memory } from "@mui/icons-material";
|
|
985
1180
|
import {
|
|
@@ -998,6 +1193,9 @@ var init_TeamUsage = __esm({
|
|
|
998
1193
|
Stat = ({ label, value }) => /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption", color: "text.secondary", display: "block" }, label), /* @__PURE__ */ React4.createElement(Typography4, { variant: "body2", fontWeight: 600, sx: { fontFamily: "monospace" } }, value));
|
|
999
1194
|
TeamCard = ({ team, usage, usageLoading }) => {
|
|
1000
1195
|
const [expanded, setExpanded] = useState3(false);
|
|
1196
|
+
const theme = useTheme2();
|
|
1197
|
+
const gridStroke = theme.palette.divider;
|
|
1198
|
+
const tickFill = theme.palette.text.secondary;
|
|
1001
1199
|
const budget = team.max_budget ?? 0;
|
|
1002
1200
|
const spend = team.spend ?? 0;
|
|
1003
1201
|
const budgetPct = budget > 0 ? Math.min(spend / budget * 100, 100) : 0;
|
|
@@ -1048,7 +1246,7 @@ var init_TeamUsage = __esm({
|
|
|
1048
1246
|
color: isOver ? "error" : isNear ? "warning" : "primary",
|
|
1049
1247
|
sx: { height: 6, borderRadius: 1 }
|
|
1050
1248
|
}
|
|
1051
|
-
)), /* @__PURE__ */ React4.createElement(Collapse, { in: expanded }, /* @__PURE__ */ React4.createElement(Box4, { mt: 2 }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React4.createElement(Speed, { fontSize: "small", color: "action" }), /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary" }, "Rate limits")), /* @__PURE__ */ React4.createElement(Stack, { direction: "row", spacing: 3, flexWrap: "wrap", useFlexGap: true, gap: 1.5, mb: 2 }, /* @__PURE__ */ React4.createElement(Stat, { label: "TPM limit", value: team.tpm_limit != null && team.tpm_limit > 0 ? String(team.tpm_limit) : "Unlimited" }), /* @__PURE__ */ React4.createElement(Stat, { label: "RPM limit", value: team.rpm_limit != null && team.rpm_limit > 0 ? String(team.rpm_limit) : "Unlimited" }), /* @__PURE__ */ React4.createElement(Stat, { label: "Max budget", value: budget > 0 ? `$${budget.toFixed(2)}` : "Unlimited" })), /* @__PURE__ */ React4.createElement(Divider, { sx: { mb: 2 } }), /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Models"), team.models?.length ? /* @__PURE__ */ React4.createElement(Box4, { display: "flex", gap: 0.5, flexWrap: "wrap", mb: 2 }, team.models.map((m) => /* @__PURE__ */ React4.createElement(Chip4, { key: m, label: m, size: "small", variant: "outlined" }))) : /* @__PURE__ */ React4.createElement(Typography4, { variant: "body2", color: "text.secondary", mb: 2 }, "All models allowed"), /* @__PURE__ */ React4.createElement(Divider, { sx: { mb: 2 } }), usageLoading ? /* @__PURE__ */ React4.createElement(Box4, { display: "flex", justifyContent: "center", p: 2 }, /* @__PURE__ */ React4.createElement(CircularProgress2, { size: 24 })) : dailyData.length > 0 ? /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend"), /* @__PURE__ */ React4.createElement(Box4, { height: 160, mb: 2 }, /* @__PURE__ */ React4.createElement(ResponsiveContainer2, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(AreaChart2, { data: dailyData }, /* @__PURE__ */ React4.createElement(CartesianGrid2, { strokeDasharray: "3 3" }), /* @__PURE__ */ React4.createElement(XAxis2, { dataKey: "date", tick: { fontSize: 11 } }), /* @__PURE__ */ React4.createElement(YAxis2, { tick: { fontSize: 11 }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React4.createElement(Tooltip2, { formatter: (v) => [`$${v.toFixed(4)}`, "Spend"] }), /* @__PURE__ */ React4.createElement(Area2, { type: "monotone", dataKey: "spend", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.3 })))), /* @__PURE__ */ React4.createElement(Divider, { sx: { mb: 2 } })) : null, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React4.createElement(Memory, { fontSize: "small", color: "action" }), /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary" }, "Members")), team.members_with_roles?.length ? /* @__PURE__ */ React4.createElement(TableContainer3, null, /* @__PURE__ */ React4.createElement(Table3, { size: "small" }, /* @__PURE__ */ React4.createElement(TableHead3, null, /* @__PURE__ */ React4.createElement(TableRow3, null, /* @__PURE__ */ React4.createElement(TableCell3, null, "User"), /* @__PURE__ */ React4.createElement(TableCell3, null, "Role"))), /* @__PURE__ */ React4.createElement(TableBody3, null, team.members_with_roles.map((m) => /* @__PURE__ */ React4.createElement(TableRow3, { key: m.user_id }, /* @__PURE__ */ React4.createElement(TableCell3, { sx: { fontFamily: "monospace", fontSize: 12 } }, m.user_id), /* @__PURE__ */ React4.createElement(TableCell3, null, /* @__PURE__ */ React4.createElement(
|
|
1249
|
+
)), /* @__PURE__ */ React4.createElement(Collapse, { in: expanded }, /* @__PURE__ */ React4.createElement(Box4, { mt: 2 }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React4.createElement(Speed, { fontSize: "small", color: "action" }), /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary" }, "Rate limits")), /* @__PURE__ */ React4.createElement(Stack, { direction: "row", spacing: 3, flexWrap: "wrap", useFlexGap: true, gap: 1.5, mb: 2 }, /* @__PURE__ */ React4.createElement(Stat, { label: "TPM limit", value: team.tpm_limit != null && team.tpm_limit > 0 ? String(team.tpm_limit) : "Unlimited" }), /* @__PURE__ */ React4.createElement(Stat, { label: "RPM limit", value: team.rpm_limit != null && team.rpm_limit > 0 ? String(team.rpm_limit) : "Unlimited" }), /* @__PURE__ */ React4.createElement(Stat, { label: "Max budget", value: budget > 0 ? `$${budget.toFixed(2)}` : "Unlimited" })), /* @__PURE__ */ React4.createElement(Divider, { sx: { mb: 2 } }), /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Models"), team.models?.length ? /* @__PURE__ */ React4.createElement(Box4, { display: "flex", gap: 0.5, flexWrap: "wrap", mb: 2 }, team.models.map((m) => /* @__PURE__ */ React4.createElement(Chip4, { key: m, label: m, size: "small", variant: "outlined" }))) : /* @__PURE__ */ React4.createElement(Typography4, { variant: "body2", color: "text.secondary", mb: 2 }, "All models allowed"), /* @__PURE__ */ React4.createElement(Divider, { sx: { mb: 2 } }), usageLoading ? /* @__PURE__ */ React4.createElement(Box4, { display: "flex", justifyContent: "center", p: 2 }, /* @__PURE__ */ React4.createElement(CircularProgress2, { size: 24 })) : dailyData.length > 0 ? /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary", gutterBottom: true }, "Daily Spend"), /* @__PURE__ */ React4.createElement(Box4, { height: 160, mb: 2 }, /* @__PURE__ */ React4.createElement(ResponsiveContainer2, { width: "100%", height: "100%" }, /* @__PURE__ */ React4.createElement(AreaChart2, { data: dailyData }, /* @__PURE__ */ React4.createElement(CartesianGrid2, { strokeDasharray: "3 3", stroke: gridStroke }), /* @__PURE__ */ React4.createElement(XAxis2, { dataKey: "date", tick: { fontSize: 11, fill: tickFill } }), /* @__PURE__ */ React4.createElement(YAxis2, { tick: { fontSize: 11, fill: tickFill }, tickFormatter: (v) => `$${v.toFixed(2)}` }), /* @__PURE__ */ React4.createElement(Tooltip2, { formatter: (v) => [`$${v.toFixed(4)}`, "Spend"] }), /* @__PURE__ */ React4.createElement(Area2, { type: "monotone", dataKey: "spend", stroke: "#8884d8", fill: "#8884d8", fillOpacity: 0.3 })))), /* @__PURE__ */ React4.createElement(Divider, { sx: { mb: 2 } })) : null, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React4.createElement(Memory, { fontSize: "small", color: "action" }), /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2", color: "text.secondary" }, "Members")), team.members_with_roles?.length ? /* @__PURE__ */ React4.createElement(TableContainer3, null, /* @__PURE__ */ React4.createElement(Table3, { size: "small" }, /* @__PURE__ */ React4.createElement(TableHead3, null, /* @__PURE__ */ React4.createElement(TableRow3, null, /* @__PURE__ */ React4.createElement(TableCell3, null, "User"), /* @__PURE__ */ React4.createElement(TableCell3, null, "Role"))), /* @__PURE__ */ React4.createElement(TableBody3, null, team.members_with_roles.map((m) => /* @__PURE__ */ React4.createElement(TableRow3, { key: m.user_id }, /* @__PURE__ */ React4.createElement(TableCell3, { sx: { fontFamily: "monospace", fontSize: 12 } }, m.user_id), /* @__PURE__ */ React4.createElement(TableCell3, null, /* @__PURE__ */ React4.createElement(
|
|
1052
1250
|
Chip4,
|
|
1053
1251
|
{
|
|
1054
1252
|
label: m.role,
|
|
@@ -1067,7 +1265,7 @@ var init_TeamUsage = __esm({
|
|
|
1067
1265
|
return /* @__PURE__ */ React4.createElement(Paper4, { sx: { p: 2 } }, /* @__PURE__ */ React4.createElement(LinearProgress4, null));
|
|
1068
1266
|
}
|
|
1069
1267
|
if (!teams.length) {
|
|
1070
|
-
return /* @__PURE__ */ React4.createElement(Paper4, { sx: { p: 2 } }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "
|
|
1268
|
+
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
1269
|
}
|
|
1072
1270
|
return /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Typography4, { variant: "h6", mb: 1 }, "Teams"), teams.map((team) => /* @__PURE__ */ React4.createElement(
|
|
1073
1271
|
TeamCard,
|
|
@@ -1221,7 +1419,7 @@ __export(LiteLLMPage_exports, {
|
|
|
1221
1419
|
LiteLLMPage: () => LiteLLMPage
|
|
1222
1420
|
});
|
|
1223
1421
|
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
|
|
1422
|
+
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
1423
|
import { useAsync as useAsync2, useAsyncRetry } from "react-use";
|
|
1226
1424
|
import { useApi } from "@backstage/core-plugin-api";
|
|
1227
1425
|
function initDateRange() {
|
|
@@ -1435,16 +1633,16 @@ var init_LiteLLMPage = __esm({
|
|
|
1435
1633
|
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
1634
|
}
|
|
1437
1635
|
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
|
-
|
|
1636
|
+
Tabs3,
|
|
1439
1637
|
{
|
|
1440
1638
|
value: activeTab,
|
|
1441
1639
|
onChange: (_, v) => setActiveTab(v),
|
|
1442
1640
|
sx: { mb: 2, borderBottom: 1, borderColor: "divider" }
|
|
1443
1641
|
},
|
|
1444
|
-
/* @__PURE__ */ React6.createElement(
|
|
1445
|
-
/* @__PURE__ */ React6.createElement(
|
|
1446
|
-
/* @__PURE__ */ React6.createElement(
|
|
1447
|
-
userInfo.can_view_audit && /* @__PURE__ */ React6.createElement(
|
|
1642
|
+
/* @__PURE__ */ React6.createElement(Tab3, { label: "Overview", value: "overview" }),
|
|
1643
|
+
/* @__PURE__ */ React6.createElement(Tab3, { label: "Keys", value: "keys" }),
|
|
1644
|
+
/* @__PURE__ */ React6.createElement(Tab3, { label: "Teams", value: "teams" }),
|
|
1645
|
+
userInfo.can_view_audit && /* @__PURE__ */ React6.createElement(Tab3, { label: "Audit Log", value: "audit" })
|
|
1448
1646
|
), activeTab === "overview" && /* @__PURE__ */ React6.createElement(
|
|
1449
1647
|
UsageStats,
|
|
1450
1648
|
{
|
|
@@ -1469,7 +1667,8 @@ var init_LiteLLMPage = __esm({
|
|
|
1469
1667
|
onUnblockKey: handleUnblockKey,
|
|
1470
1668
|
onResetKeySpend: handleResetKeySpend,
|
|
1471
1669
|
onDeleteKey: handleDeleteKey,
|
|
1472
|
-
onPruneExpiredKeys: handlePruneExpiredKeys
|
|
1670
|
+
onPruneExpiredKeys: handlePruneExpiredKeys,
|
|
1671
|
+
onGetConfig: () => api.getConfig()
|
|
1473
1672
|
}
|
|
1474
1673
|
), activeTab === "teams" && /* @__PURE__ */ React6.createElement(
|
|
1475
1674
|
TeamUsage,
|
|
@@ -1573,32 +1772,42 @@ var LiteLLMHomeWidget = ({
|
|
|
1573
1772
|
const api = useApi2(liteLlmApiRef);
|
|
1574
1773
|
const [period, setPeriod] = useState6(defaultPeriod);
|
|
1575
1774
|
const [loading, setLoading] = useState6(true);
|
|
1576
|
-
const [
|
|
1775
|
+
const [usageError, setUsageError] = useState6(null);
|
|
1776
|
+
const [keysError, setKeysError] = useState6(null);
|
|
1577
1777
|
const [usage, setUsage] = useState6(null);
|
|
1578
1778
|
const [keys, setKeys] = useState6([]);
|
|
1579
1779
|
useEffect(() => {
|
|
1580
1780
|
let cancelled = false;
|
|
1581
1781
|
setLoading(true);
|
|
1582
|
-
|
|
1782
|
+
setUsageError(null);
|
|
1783
|
+
setKeysError(null);
|
|
1583
1784
|
const { start, end } = presetToDateRange(period);
|
|
1584
1785
|
const startDate = start.toISOString().split("T")[0];
|
|
1585
1786
|
const endDate = end.toISOString().split("T")[0];
|
|
1586
|
-
Promise.
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1787
|
+
Promise.allSettled([api.getUsage(startDate, endDate), api.listKeys()]).then(
|
|
1788
|
+
([usageResult, keysResult]) => {
|
|
1789
|
+
if (cancelled) return;
|
|
1790
|
+
if (usageResult.status === "fulfilled") {
|
|
1791
|
+
setUsage(usageResult.value);
|
|
1792
|
+
} else {
|
|
1793
|
+
setUsage(null);
|
|
1794
|
+
setUsageError(usageResult.reason?.message ?? "Failed to load usage data");
|
|
1795
|
+
}
|
|
1796
|
+
if (keysResult.status === "fulfilled") {
|
|
1797
|
+
setKeys(keysResult.value);
|
|
1798
|
+
} else {
|
|
1799
|
+
setKeys([]);
|
|
1800
|
+
setKeysError(keysResult.reason?.message ?? "Failed to load keys");
|
|
1801
|
+
}
|
|
1595
1802
|
setLoading(false);
|
|
1596
1803
|
}
|
|
1597
|
-
|
|
1804
|
+
);
|
|
1598
1805
|
return () => {
|
|
1599
1806
|
cancelled = true;
|
|
1600
1807
|
};
|
|
1601
1808
|
}, [api, period]);
|
|
1809
|
+
const partialFailure = !loading && (usageError || keysError) && (usage || keys.length > 0);
|
|
1810
|
+
const totalFailure = !loading && usageError && keysError;
|
|
1602
1811
|
const dailyData = (usage?.daily_usage ?? []).map((d) => ({
|
|
1603
1812
|
date: d.date,
|
|
1604
1813
|
spend: d.spend
|
|
@@ -1614,7 +1823,7 @@ var LiteLLMHomeWidget = ({
|
|
|
1614
1823
|
/* @__PURE__ */ React8.createElement(MenuItem4, { value: "today" }, "Today"),
|
|
1615
1824
|
/* @__PURE__ */ React8.createElement(MenuItem4, { value: "7d" }, "7d"),
|
|
1616
1825
|
/* @__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 &&
|
|
1826
|
+
))), 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
1827
|
Area3,
|
|
1619
1828
|
{
|
|
1620
1829
|
type: "monotone",
|