@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 CHANGED
@@ -1,5 +1,5 @@
1
1
  import { FetchApi } from '@backstage/core-plugin-api';
2
- import { UserInfo, VirtualKey, ModelInfo, UsageMetrics, TeamInfo, GenerateKeyRequest, GenerateKeyResponse, UpdateKeyRequest, AuditLogsParams, PaginatedAuditLogs } from './types';
2
+ import { UserInfo, VirtualKey, ModelInfo, UsageMetrics, TeamInfo, GenerateKeyRequest, GenerateKeyResponse, UpdateKeyRequest, AuditLogsParams, PaginatedAuditLogs, LiteLlmConfig } from './types';
3
3
  export interface LiteLlmApiInterface {
4
4
  getUserInfo(): Promise<UserInfo>;
5
5
  listKeys(): Promise<VirtualKey[]>;
@@ -20,6 +20,7 @@ export interface LiteLlmApiInterface {
20
20
  getUsage(startDate: string, endDate: string): Promise<UsageMetrics>;
21
21
  getTeamUsage(teamId: string, startDate: string, endDate: string): Promise<UsageMetrics>;
22
22
  getAuditLogs(params: AuditLogsParams): Promise<PaginatedAuditLogs>;
23
+ getConfig(): Promise<LiteLlmConfig>;
23
24
  }
24
25
  export declare const liteLlmApiRef: import("@backstage/core-plugin-api").ApiRef<LiteLlmApiInterface>;
25
26
  export type ExpiryStatus = 'expired' | 'soon' | 'ok';
@@ -51,4 +52,5 @@ export declare class LiteLlmApi implements LiteLlmApiInterface {
51
52
  getTeams(): Promise<TeamInfo[]>;
52
53
  getUsage(startDate: string, endDate: string): Promise<UsageMetrics>;
53
54
  getTeamUsage(teamId: string, startDate: string, endDate: string): Promise<UsageMetrics>;
55
+ getConfig(): Promise<LiteLlmConfig>;
54
56
  }
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { VirtualKey, ModelInfo, TeamInfo, GenerateKeyRequest, GenerateKeyResponse, UpdateKeyRequest } from '../types';
2
+ import { VirtualKey, ModelInfo, TeamInfo, GenerateKeyRequest, GenerateKeyResponse, UpdateKeyRequest, LiteLlmConfig } from '../types';
3
3
  interface KeysTableProps {
4
4
  keys: VirtualKey[];
5
5
  models: ModelInfo[];
@@ -15,6 +15,8 @@ interface KeysTableProps {
15
15
  onPruneExpiredKeys: () => Promise<{
16
16
  pruned: number;
17
17
  }>;
18
+ /** Resolve the public LiteLLM proxy URL, used for ready-to-paste snippets. */
19
+ onGetConfig: () => Promise<LiteLlmConfig>;
18
20
  }
19
21
  export declare const KeysTable: React.FC<KeysTableProps>;
20
22
  export {};
package/dist/format.d.ts CHANGED
@@ -1,2 +1,7 @@
1
1
  export declare const fmtUsd: (n: number) => string;
2
2
  export declare const fmtInt: (n: number) => string;
3
+ /**
4
+ * Estimate how many tokens a USD budget buys at a given per-token price.
5
+ * Returns null when the price is missing or non-positive.
6
+ */
7
+ export declare const estimateTokensFromBudget: (budget: number, pricePerToken?: number) => number | null;
package/dist/index.cjs.js CHANGED
@@ -169,6 +169,9 @@ var init_api = __esm({
169
169
  end_date: endDate
170
170
  });
171
171
  }
172
+ async getConfig() {
173
+ return this.get("/config");
174
+ }
172
175
  };
173
176
  }
174
177
  });
@@ -199,6 +202,20 @@ var init_DashboardHeader = __esm({
199
202
  }
200
203
  });
201
204
 
205
+ // src/format.ts
206
+ var fmtUsd, fmtInt, estimateTokensFromBudget;
207
+ var init_format = __esm({
208
+ "src/format.ts"() {
209
+ "use strict";
210
+ fmtUsd = (n) => `$${(n ?? 0).toFixed(n < 1 ? 4 : 2)}`;
211
+ fmtInt = (n) => (n ?? 0).toLocaleString();
212
+ estimateTokensFromBudget = (budget, pricePerToken) => {
213
+ if (!pricePerToken || pricePerToken <= 0 || !budget || budget <= 0) return null;
214
+ return Math.floor(budget / pricePerToken);
215
+ };
216
+ }
217
+ });
218
+
202
219
  // src/components/KeysTable.tsx
203
220
  function ExpiryChip({ expiresAt }) {
204
221
  const status = expiryStatus(expiresAt);
@@ -219,7 +236,34 @@ function fmtCost(perToken) {
219
236
  const per1k = perToken * 1e3;
220
237
  return per1k < 0.01 ? `$${(perToken * 1e6).toFixed(2)}/M` : `$${per1k.toFixed(3)}/1K`;
221
238
  }
222
- var import_react2, import_material2, import_icons_material, maskKey, shortKeyId, formatDate, emptyForm, keyToEditForm, KeysTable;
239
+ function trimSlash(url) {
240
+ return url.replace(/\/+$/, "");
241
+ }
242
+ function buildSnippets(baseUrl, key, model) {
243
+ const base = trimSlash(baseUrl);
244
+ return {
245
+ curl: `curl ${base}/v1/chat/completions \\
246
+ -H "Authorization: Bearer ${key}" \\
247
+ -H "Content-Type: application/json" \\
248
+ -d '{
249
+ "model": "${model}",
250
+ "messages": [{ "role": "user", "content": "Hello!" }]
251
+ }'`,
252
+ openai: `from openai import OpenAI
253
+
254
+ client = OpenAI(
255
+ api_key="${key}",
256
+ base_url="${base}/v1",
257
+ )
258
+
259
+ response = client.chat.completions.create(
260
+ model="${model}",
261
+ messages=[{ "role": "user", "content": "Hello!" }],
262
+ )
263
+ print(response.choices[0].message.content)`
264
+ };
265
+ }
266
+ var import_react2, import_material2, import_icons_material, maskKey, shortKeyId, formatDate, emptyForm, keyToEditForm, KeysTable, SnippetTabs;
223
267
  var init_KeysTable = __esm({
224
268
  "src/components/KeysTable.tsx"() {
225
269
  "use strict";
@@ -227,6 +271,7 @@ var init_KeysTable = __esm({
227
271
  import_material2 = require("@mui/material");
228
272
  import_icons_material = require("@mui/icons-material");
229
273
  init_api();
274
+ init_format();
230
275
  maskKey = (key) => {
231
276
  if (key.length <= 8) return "***";
232
277
  return `${key.slice(0, 4)}...${key.slice(-4)}`;
@@ -272,11 +317,15 @@ var init_KeysTable = __esm({
272
317
  onUnblockKey,
273
318
  onResetKeySpend,
274
319
  onDeleteKey,
275
- onPruneExpiredKeys
320
+ onPruneExpiredKeys,
321
+ onGetConfig
276
322
  }) => {
277
323
  const [generateModalOpen, setGenerateModalOpen] = (0, import_react2.useState)(false);
278
324
  const [newKeyValue, setNewKeyValue] = (0, import_react2.useState)(null);
325
+ const [newKeySnippets, setNewKeySnippets] = (0, import_react2.useState)(null);
326
+ const [newKeyModel, setNewKeyModel] = (0, import_react2.useState)("");
279
327
  const [formData, setFormData] = (0, import_react2.useState)(emptyForm());
328
+ const [unlimitedBudget, setUnlimitedBudget] = (0, import_react2.useState)(false);
280
329
  const [submitting, setSubmitting] = (0, import_react2.useState)(false);
281
330
  const [editingKey, setEditingKey] = (0, import_react2.useState)(null);
282
331
  const [editForm, setEditForm] = (0, import_react2.useState)({});
@@ -299,12 +348,35 @@ var init_KeysTable = __esm({
299
348
  const selectedModels = models.filter((m) => (formData.models || []).includes(m.model_name));
300
349
  const selectedTeam = teams.find((t) => t.team_id === formData.team_id) ?? null;
301
350
  const editSelectedModels = models.filter((m) => (editForm.models || []).includes(m.model_name));
351
+ const aliasError = !(formData.alias || "").trim();
352
+ const budgetInvalid = !unlimitedBudget && (formData.max_budget === void 0 || formData.max_budget === null || formData.max_budget <= 0);
353
+ const canGenerate = !aliasError && !budgetInvalid && !submitting;
354
+ const budgetEstimate = (0, import_react2.useMemo)(() => {
355
+ if (unlimitedBudget || budgetInvalid) return null;
356
+ const inputCosts = selectedModels.map((m) => m.input_cost_per_token).filter((c) => typeof c === "number" && c > 0);
357
+ if (inputCosts.length === 0) return null;
358
+ const pricePerToken = Math.max(...inputCosts);
359
+ return estimateTokensFromBudget(formData.max_budget ?? 0, pricePerToken);
360
+ }, [formData.max_budget, selectedModels, unlimitedBudget, budgetInvalid]);
302
361
  const handleGenerate = async () => {
303
362
  setSubmitting(true);
304
363
  try {
305
- const response = await onGenerateKey(formData);
364
+ const request = {
365
+ ...formData,
366
+ max_budget: unlimitedBudget ? null : formData.max_budget
367
+ };
368
+ const response = await onGenerateKey(request);
306
369
  setNewKeyValue(response.key);
370
+ setNewKeyModel(formData.models?.[0] ?? "");
371
+ setNewKeySnippets(null);
372
+ try {
373
+ const config = await onGetConfig();
374
+ const model = formData.models?.[0] ?? "";
375
+ setNewKeySnippets(buildSnippets(config.baseUrl, response.key, model));
376
+ } catch {
377
+ }
307
378
  setFormData(emptyForm());
379
+ setUnlimitedBudget(false);
308
380
  } catch (error) {
309
381
  console.error("Failed to generate key:", error);
310
382
  } finally {
@@ -314,7 +386,9 @@ var init_KeysTable = __esm({
314
386
  const handleCloseModal = () => {
315
387
  setGenerateModalOpen(false);
316
388
  setNewKeyValue(null);
389
+ setNewKeySnippets(null);
317
390
  setFormData(emptyForm());
391
+ setUnlimitedBudget(false);
318
392
  };
319
393
  const handleOpenEdit = (k) => {
320
394
  setEditingKey(k);
@@ -439,7 +513,17 @@ var init_KeysTable = __esm({
439
513
  onClick: () => setGenerateModalOpen(true)
440
514
  },
441
515
  "Generate New Key"
442
- ))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableContainer, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Table, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableHead, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Alias"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Key ID"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Created"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Expires"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Budget"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "TPM / RPM"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Models"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { align: "right" }, "Actions"))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableBody, null, loading ? /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 24 }))) : filteredKeys.length === 0 ? /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { color: "text.secondary" }, filterText ? "No keys match filter" : "No keys found"))) : filteredKeys.map((key) => {
516
+ ))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableContainer, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Table, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableHead, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Alias"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Key ID"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Created"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Expires"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Budget"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "TPM / RPM"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, null, "Models"), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { align: "right" }, "Actions"))), /* @__PURE__ */ import_react2.default.createElement(import_material2.TableBody, null, loading ? /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { colSpan: 8, align: "center" }, /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 24 }))) : filteredKeys.length === 0 ? /* @__PURE__ */ import_react2.default.createElement(import_material2.TableRow, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.TableCell, { colSpan: 8, align: "center", sx: { py: 4 } }, filterText ? /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { color: "text.secondary" }, "No keys match filter") : /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { color: "text.secondary", gutterBottom: true }, "No keys yet \u2014 generate your first key to start calling models."), /* @__PURE__ */ import_react2.default.createElement(
517
+ import_material2.Button,
518
+ {
519
+ variant: "contained",
520
+ color: "primary",
521
+ size: "small",
522
+ startIcon: /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Add, null),
523
+ onClick: () => setGenerateModalOpen(true)
524
+ },
525
+ "Generate Your First Key"
526
+ )))) : filteredKeys.map((key) => {
443
527
  const keyId = key.token ?? key.key;
444
528
  const isRotating = rotatingKeyId === keyId;
445
529
  const isBlocking = blockingKeyId === keyId;
@@ -510,12 +594,14 @@ var init_KeysTable = __esm({
510
594
  newKeyValue
511
595
  ),
512
596
  /* @__PURE__ */ import_react2.default.createElement(import_material2.IconButton, { onClick: () => copyToClipboard(newKeyValue) }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material.ContentCopy, null))
513
- )) : /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, /* @__PURE__ */ import_react2.default.createElement(
597
+ ), newKeySnippets && /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { mt: 3 }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ import_react2.default.createElement(import_icons_material.Code, { fontSize: "small", color: "action" }), /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "subtitle2" }, "Start calling the proxy \u2014 paste and run")), /* @__PURE__ */ import_react2.default.createElement(SnippetTabs, { snippets: newKeySnippets, model: newKeyModel, onCopy: copyToClipboard }))) : /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, /* @__PURE__ */ import_react2.default.createElement(
514
598
  import_material2.TextField,
515
599
  {
516
600
  label: "Alias",
517
601
  value: formData.alias || "",
518
602
  onChange: (e) => setFormData({ ...formData, alias: e.target.value }),
603
+ error: aliasError,
604
+ helperText: aliasError ? "Alias is required" : void 0,
519
605
  required: true,
520
606
  fullWidth: true
521
607
  }
@@ -562,13 +648,35 @@ var init_KeysTable = __esm({
562
648
  renderOption: (props, m) => /* @__PURE__ */ import_react2.default.createElement("li", { ...props }, modelOption(m)),
563
649
  renderInput: (params) => /* @__PURE__ */ import_react2.default.createElement(import_material2.TextField, { ...params, label: "Models", helperText: "Leave empty to allow all models", fullWidth: true })
564
650
  }
651
+ ), /* @__PURE__ */ import_react2.default.createElement(
652
+ import_material2.FormControlLabel,
653
+ {
654
+ control: /* @__PURE__ */ import_react2.default.createElement(
655
+ import_material2.Checkbox,
656
+ {
657
+ checked: unlimitedBudget,
658
+ onChange: (e) => {
659
+ setUnlimitedBudget(e.target.checked);
660
+ if (e.target.checked) {
661
+ setFormData({ ...formData, max_budget: null });
662
+ } else {
663
+ setFormData({ ...formData, max_budget: 100 });
664
+ }
665
+ }
666
+ }
667
+ ),
668
+ label: "Unlimited budget"
669
+ }
565
670
  ), /* @__PURE__ */ import_react2.default.createElement(
566
671
  import_material2.TextField,
567
672
  {
568
673
  label: "Max Budget (USD)",
569
674
  type: "number",
570
- value: formData.max_budget ?? "",
675
+ value: unlimitedBudget ? "" : formData.max_budget ?? "",
571
676
  onChange: (e) => setFormData({ ...formData, max_budget: e.target.value ? Number(e.target.value) : void 0 }),
677
+ error: budgetInvalid,
678
+ helperText: budgetInvalid ? 'Enter a positive budget or tick "Unlimited"' : budgetEstimate !== null ? `\u2248 ${fmtInt(budgetEstimate)} tokens at the selected model's rate` : void 0,
679
+ disabled: unlimitedBudget,
572
680
  required: true,
573
681
  fullWidth: true
574
682
  }
@@ -596,7 +704,7 @@ var init_KeysTable = __esm({
596
704
  onClick: handleGenerate,
597
705
  variant: "contained",
598
706
  color: "primary",
599
- disabled: submitting
707
+ disabled: !canGenerate
600
708
  },
601
709
  submitting ? /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 24 }) : "Generate"
602
710
  ), newKeyValue && /* @__PURE__ */ import_react2.default.createElement(import_material2.Button, { onClick: handleCloseModal, variant: "contained", color: "success" }, "Done"))), /* @__PURE__ */ import_react2.default.createElement(import_material2.Dialog, { open: !!editingKey, onClose: handleCloseEdit, maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogTitle, null, "Edit Key"), /* @__PURE__ */ import_react2.default.createElement(import_material2.DialogContent, null, editingKey && /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2", color: "text.secondary" }, /* @__PURE__ */ import_react2.default.createElement("code", { style: { fontFamily: "monospace", color: "inherit" } }, maskKey(editingKey.key))), /* @__PURE__ */ import_react2.default.createElement(
@@ -704,16 +812,44 @@ var init_KeysTable = __esm({
704
812
  pruneSubmitting ? /* @__PURE__ */ import_react2.default.createElement(import_material2.CircularProgress, { size: 20 }) : "Prune"
705
813
  ))));
706
814
  };
707
- }
708
- });
709
-
710
- // src/format.ts
711
- var fmtUsd, fmtInt;
712
- var init_format = __esm({
713
- "src/format.ts"() {
714
- "use strict";
715
- fmtUsd = (n) => `$${(n ?? 0).toFixed(n < 1 ? 4 : 2)}`;
716
- fmtInt = (n) => (n ?? 0).toLocaleString();
815
+ SnippetTabs = ({ snippets, model, onCopy }) => {
816
+ const [tab, setTab] = (0, import_react2.useState)("curl");
817
+ const code = tab === "curl" ? snippets.curl : snippets.openai;
818
+ return /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, null, /* @__PURE__ */ import_react2.default.createElement(import_material2.Tabs, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 1 } }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Tab, { label: "curl", value: "curl" }), /* @__PURE__ */ import_react2.default.createElement(import_material2.Tab, { label: "OpenAI SDK", value: "openai" })), /* @__PURE__ */ import_react2.default.createElement(
819
+ import_material2.Box,
820
+ {
821
+ position: "relative",
822
+ p: 1.5,
823
+ sx: { backgroundColor: "action.hover", border: "1px solid", borderColor: "divider", borderRadius: 1 }
824
+ },
825
+ /* @__PURE__ */ import_react2.default.createElement(
826
+ import_material2.IconButton,
827
+ {
828
+ size: "small",
829
+ onClick: () => onCopy(code),
830
+ title: "Copy snippet",
831
+ sx: { position: "absolute", top: 4, right: 4 }
832
+ },
833
+ /* @__PURE__ */ import_react2.default.createElement(import_icons_material.ContentCopy, { fontSize: "small" })
834
+ ),
835
+ /* @__PURE__ */ import_react2.default.createElement(
836
+ import_material2.Typography,
837
+ {
838
+ component: "pre",
839
+ sx: {
840
+ fontFamily: "monospace",
841
+ fontSize: 12,
842
+ whiteSpace: "pre-wrap",
843
+ wordBreak: "break-all",
844
+ mb: 0,
845
+ pr: 4
846
+ }
847
+ },
848
+ code
849
+ ),
850
+ model && /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "caption", color: "text.secondary", display: "block", mt: 1 }, "Using model \u201C", model, "\u201D \u2014 swap it for any model you have access to.")
851
+ ));
852
+ };
717
853
  }
718
854
  });
719
855
 
@@ -1007,7 +1143,7 @@ var init_TeamUsage = __esm({
1007
1143
  return /* @__PURE__ */ import_react4.default.createElement(import_material4.Paper, { sx: { p: 2 } }, /* @__PURE__ */ import_react4.default.createElement(import_material4.LinearProgress, null));
1008
1144
  }
1009
1145
  if (!teams.length) {
1010
- return /* @__PURE__ */ import_react4.default.createElement(import_material4.Paper, { sx: { p: 2 } }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_icons_material2.Group, { color: "disabled" }), /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { color: "text.secondary", variant: "body2" }, "No team membership found in LiteLLM for this account.")));
1146
+ return /* @__PURE__ */ import_react4.default.createElement(import_material4.Paper, { sx: { p: 2 } }, /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, { display: "flex", alignItems: "flex-start", gap: 1.5 }, /* @__PURE__ */ import_react4.default.createElement(import_icons_material2.Group, { color: "disabled", sx: { mt: 0.5 } }), /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { color: "text.secondary", variant: "body2" }, "You're not a member of any LiteLLM team yet."), /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { 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."))));
1011
1147
  }
1012
1148
  return /* @__PURE__ */ import_react4.default.createElement(import_material4.Box, null, /* @__PURE__ */ import_react4.default.createElement(import_material4.Typography, { variant: "h6", mb: 1 }, "Teams"), teams.map((team) => /* @__PURE__ */ import_react4.default.createElement(
1013
1149
  TeamCard,
@@ -1391,7 +1527,8 @@ var init_LiteLLMPage = __esm({
1391
1527
  onUnblockKey: handleUnblockKey,
1392
1528
  onResetKeySpend: handleResetKeySpend,
1393
1529
  onDeleteKey: handleDeleteKey,
1394
- onPruneExpiredKeys: handlePruneExpiredKeys
1530
+ onPruneExpiredKeys: handlePruneExpiredKeys,
1531
+ onGetConfig: () => api.getConfig()
1395
1532
  }
1396
1533
  ), activeTab === "teams" && /* @__PURE__ */ import_react6.default.createElement(
1397
1534
  TeamUsage,
@@ -1495,32 +1632,42 @@ var LiteLLMHomeWidget = ({
1495
1632
  const api = (0, import_core_plugin_api3.useApi)(liteLlmApiRef);
1496
1633
  const [period, setPeriod] = (0, import_react8.useState)(defaultPeriod);
1497
1634
  const [loading, setLoading] = (0, import_react8.useState)(true);
1498
- const [error, setError] = (0, import_react8.useState)(null);
1635
+ const [usageError, setUsageError] = (0, import_react8.useState)(null);
1636
+ const [keysError, setKeysError] = (0, import_react8.useState)(null);
1499
1637
  const [usage, setUsage] = (0, import_react8.useState)(null);
1500
1638
  const [keys, setKeys] = (0, import_react8.useState)([]);
1501
1639
  (0, import_react8.useEffect)(() => {
1502
1640
  let cancelled = false;
1503
1641
  setLoading(true);
1504
- setError(null);
1642
+ setUsageError(null);
1643
+ setKeysError(null);
1505
1644
  const { start, end } = presetToDateRange(period);
1506
1645
  const startDate = start.toISOString().split("T")[0];
1507
1646
  const endDate = end.toISOString().split("T")[0];
1508
- Promise.all([api.getUsage(startDate, endDate), api.listKeys()]).then(([usageData, keysData]) => {
1509
- if (!cancelled) {
1510
- setUsage(usageData);
1511
- setKeys(keysData);
1512
- setLoading(false);
1513
- }
1514
- }).catch((err) => {
1515
- if (!cancelled) {
1516
- setError(err.message ?? "Failed to load usage data");
1647
+ Promise.allSettled([api.getUsage(startDate, endDate), api.listKeys()]).then(
1648
+ ([usageResult, keysResult]) => {
1649
+ if (cancelled) return;
1650
+ if (usageResult.status === "fulfilled") {
1651
+ setUsage(usageResult.value);
1652
+ } else {
1653
+ setUsage(null);
1654
+ setUsageError(usageResult.reason?.message ?? "Failed to load usage data");
1655
+ }
1656
+ if (keysResult.status === "fulfilled") {
1657
+ setKeys(keysResult.value);
1658
+ } else {
1659
+ setKeys([]);
1660
+ setKeysError(keysResult.reason?.message ?? "Failed to load keys");
1661
+ }
1517
1662
  setLoading(false);
1518
1663
  }
1519
- });
1664
+ );
1520
1665
  return () => {
1521
1666
  cancelled = true;
1522
1667
  };
1523
1668
  }, [api, period]);
1669
+ const partialFailure = !loading && (usageError || keysError) && (usage || keys.length > 0);
1670
+ const totalFailure = !loading && usageError && keysError;
1524
1671
  const dailyData = (usage?.daily_usage ?? []).map((d) => ({
1525
1672
  date: d.date,
1526
1673
  spend: d.spend
@@ -1536,7 +1683,7 @@ var LiteLLMHomeWidget = ({
1536
1683
  /* @__PURE__ */ import_react8.default.createElement(import_material7.MenuItem, { value: "today" }, "Today"),
1537
1684
  /* @__PURE__ */ import_react8.default.createElement(import_material7.MenuItem, { value: "7d" }, "7d"),
1538
1685
  /* @__PURE__ */ import_react8.default.createElement(import_material7.MenuItem, { value: "30d" }, "30d")
1539
- ))), loading && /* @__PURE__ */ import_react8.default.createElement(import_material7.Box, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ import_react8.default.createElement(import_material7.CircularProgress, { size: 32 })), !loading && error && /* @__PURE__ */ import_react8.default.createElement(import_material7.Alert, { severity: "error", sx: { mt: 1 } }, error), !loading && !error && /* @__PURE__ */ import_react8.default.createElement(import_react8.default.Fragment, null, /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "USD Spent", value: fmtUsd(usage?.total_spend ?? 0) })), /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "Tokens In", value: fmtInt(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "Tokens Out", value: fmtInt(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "Keys", value: fmtInt(keys.length) }))), hasSparkline && /* @__PURE__ */ import_react8.default.createElement(import_material7.Box, { height: 120 }, /* @__PURE__ */ import_react8.default.createElement(import_recharts3.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react8.default.createElement(import_recharts3.AreaChart, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 } }, /* @__PURE__ */ import_react8.default.createElement(
1686
+ ))), loading && /* @__PURE__ */ import_react8.default.createElement(import_material7.Box, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: 120 }, /* @__PURE__ */ import_react8.default.createElement(import_material7.CircularProgress, { size: 32 })), !loading && totalFailure && /* @__PURE__ */ import_react8.default.createElement(import_material7.Alert, { severity: "error", sx: { mt: 1 } }, usageError ?? "Failed to load usage data"), !loading && !totalFailure && /* @__PURE__ */ import_react8.default.createElement(import_react8.default.Fragment, null, partialFailure && /* @__PURE__ */ import_react8.default.createElement(import_material7.Alert, { severity: "warning", sx: { mt: 1, mb: 1 } }, usageError ? `Usage data unavailable (${usageError}).` : "", keysError ? ` Key list unavailable (${keysError}).` : "", " Showing what loaded."), /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { container: true, spacing: 2, sx: { mb: hasSparkline ? 1.5 : 0 } }, /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "USD Spent", value: fmtUsd(usage?.total_spend ?? 0) })), /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "Tokens In", value: fmtInt(usage?.prompt_tokens ?? 0) })), /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "Tokens Out", value: fmtInt(usage?.completion_tokens ?? 0) })), /* @__PURE__ */ import_react8.default.createElement(import_material7.Grid, { item: true, xs: 6 }, /* @__PURE__ */ import_react8.default.createElement(Kpi, { label: "Keys", value: fmtInt(keys.length) }))), hasSparkline && /* @__PURE__ */ import_react8.default.createElement(import_material7.Box, { height: 120 }, /* @__PURE__ */ import_react8.default.createElement(import_recharts3.ResponsiveContainer, { width: "100%", height: "100%" }, /* @__PURE__ */ import_react8.default.createElement(import_recharts3.AreaChart, { data: dailyData, margin: { top: 4, right: 0, bottom: 0, left: 0 } }, /* @__PURE__ */ import_react8.default.createElement(
1540
1687
  import_recharts3.Area,
1541
1688
  {
1542
1689
  type: "monotone",