@acarmisc/backstage-plugin-litellm 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +3 -1
- package/dist/components/KeysTable.d.ts +3 -1
- package/dist/format.d.ts +5 -0
- package/dist/index.cjs.js +179 -32
- package/dist/index.cjs.js.map +3 -3
- package/dist/index.esm.js +194 -43
- package/dist/index.esm.js.map +4 -4
- package/dist/types.d.ts +6 -1
- package/package.json +6 -1
package/dist/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
|
-
|
|
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
|
|
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" },
|
|
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:
|
|
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
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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: "
|
|
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 [
|
|
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
|
-
|
|
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.
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
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 &&
|
|
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",
|