@acarmisc/backstage-plugin-litellm-backend 0.22.0 → 0.23.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/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { LiteLLMConfig, UserInfo, VirtualKey, ModelInfo, UsageMetrics, TeamInfo, GenerateKeyRequest, GenerateKeyResponse, UpdateKeyRequest, DeleteKeyRequest, CreateUserRequest, CreateUserResponse, CreateTeamRequest, CreateTeamResponse, UpdateTeamRequest, AuditLogsParams, PaginatedAuditLogs } from './types';
1
+ import { LiteLLMConfig, UserInfo, VirtualKey, ModelInfo, UsageMetrics, TeamInfo, GenerateKeyRequest, GenerateKeyResponse, UpdateKeyRequest, DeleteKeyRequest, CreateUserRequest, CreateUserResponse, CreateTeamRequest, CreateTeamResponse, UpdateTeamRequest, AuditLogsParams, PaginatedAuditLogs, SpendLogsParams, PaginatedSpendLogs } from './types';
2
2
  /**
3
3
  * Typed error for failed upstream LiteLLM responses. Preserves the HTTP
4
4
  * status and the structured `param` (e.g. `key_alias`) from the upstream
@@ -185,4 +185,15 @@ export declare class LiteLLMClient {
185
185
  private transformDailyActivity;
186
186
  getUsage(startDate: string, endDate: string, userId: string): Promise<UsageMetrics>;
187
187
  getTeamUsage(teamId: string, startDate: string, endDate: string): Promise<UsageMetrics>;
188
+ /**
189
+ * Returns per-request spend rows from `/spend/logs` (retried as
190
+ * `/spend/logs/v2` on 404 — the path differs across LiteLLM versions).
191
+ * The response shape is version-dependent, so rows are parsed tolerantly:
192
+ * a bare array or a `{ data | results | logs }` envelope, with per-field
193
+ * fallbacks in normalizeSpendLogs.
194
+ */
195
+ getSpendLogs(params: SpendLogsParams & {
196
+ apiKeyHash?: string;
197
+ }): Promise<PaginatedSpendLogs>;
198
+ private normalizeSpendLogs;
188
199
  }
package/dist/client.js CHANGED
@@ -683,5 +683,96 @@ class LiteLLMClient {
683
683
  throw err;
684
684
  }
685
685
  }
686
+ /**
687
+ * Returns per-request spend rows from `/spend/logs` (retried as
688
+ * `/spend/logs/v2` on 404 — the path differs across LiteLLM versions).
689
+ * The response shape is version-dependent, so rows are parsed tolerantly:
690
+ * a bare array or a `{ data | results | logs }` envelope, with per-field
691
+ * fallbacks in normalizeSpendLogs.
692
+ */
693
+ async getSpendLogs(params) {
694
+ const page = params.page ?? 1;
695
+ const pageSize = Math.min(params.pageSize ?? 25, 100);
696
+ const buildQuery = () => {
697
+ const q = new URLSearchParams({
698
+ summarize: 'false',
699
+ start_date: params.startDate,
700
+ end_date: params.endDate,
701
+ page: String(page),
702
+ page_size: String(pageSize),
703
+ });
704
+ if (params.userId)
705
+ q.set('user_id', params.userId);
706
+ if (params.teamId)
707
+ q.set('team_id', params.teamId);
708
+ if (params.apiKeyHash)
709
+ q.set('api_key', params.apiKeyHash);
710
+ return q.toString();
711
+ };
712
+ let raw;
713
+ try {
714
+ raw = await this.request(`/spend/logs?${buildQuery()}`);
715
+ }
716
+ catch (err) {
717
+ if (err instanceof LiteLLMUpstreamError && err.status === 404) {
718
+ raw = await this.request(`/spend/logs/v2?${buildQuery()}`);
719
+ }
720
+ else {
721
+ throw err;
722
+ }
723
+ }
724
+ return this.normalizeSpendLogs(raw, page, pageSize);
725
+ }
726
+ normalizeSpendLogs(raw, page, pageSize) {
727
+ const rows = Array.isArray(raw)
728
+ ? raw
729
+ : Array.isArray(raw?.data)
730
+ ? raw.data
731
+ : Array.isArray(raw?.results)
732
+ ? raw.results
733
+ : Array.isArray(raw?.logs)
734
+ ? raw.logs
735
+ : [];
736
+ const toNum = (v, fallback = 0) => {
737
+ const n = typeof v === 'number'
738
+ ? v
739
+ : typeof v === 'string' && v.trim() !== ''
740
+ ? Number(v)
741
+ : NaN;
742
+ return Number.isFinite(n) ? n : fallback;
743
+ };
744
+ const data = rows.map((r) => {
745
+ const promptTokens = toNum(r?.prompt_tokens ?? r?.promptTokens ?? r?.input_tokens);
746
+ const completionTokens = toNum(r?.completion_tokens ?? r?.completionTokens ?? r?.output_tokens);
747
+ return {
748
+ requestId: String(r?.request_id ?? r?.requestId ?? r?.id ?? ''),
749
+ model: String(r?.model ?? r?.model_name ?? ''),
750
+ spend: toNum(r?.spend ?? r?.cost),
751
+ promptTokens,
752
+ completionTokens,
753
+ totalTokens: toNum(r?.total_tokens ?? r?.totalTokens, promptTokens + completionTokens),
754
+ status: String(r?.status ?? r?.request_status ?? 'unknown'),
755
+ cacheHit: Boolean(r?.cache_hit ?? r?.cacheHit ?? r?.cached === true),
756
+ ...(r?.user_id ?? r?.userId
757
+ ? { userId: String(r.user_id ?? r.userId) }
758
+ : {}),
759
+ ...(r?.team_id ?? r?.teamId
760
+ ? { teamId: String(r.team_id ?? r.teamId) }
761
+ : {}),
762
+ ...(r?.api_key ?? r?.keyHash
763
+ ? { keyHash: String(r.api_key ?? r.keyHash) }
764
+ : {}),
765
+ timestamp: String(r?.timestamp ?? r?.startTime ?? r?.created_at ?? r?.date ?? ''),
766
+ };
767
+ });
768
+ // Upstream truncation is not signalled in-band, so truncated = full page.
769
+ return {
770
+ data,
771
+ total: toNum(raw?.total ?? raw?.total_count, data.length),
772
+ page,
773
+ pageSize,
774
+ truncated: data.length >= pageSize,
775
+ };
776
+ }
686
777
  }
687
778
  exports.LiteLLMClient = LiteLLMClient;
package/dist/index.cjs.js CHANGED
@@ -8022,7 +8022,7 @@ var import_backend_plugin_api = require("@backstage/backend-plugin-api");
8022
8022
  // src/router.ts
8023
8023
  var import_express = __toESM(require("express"));
8024
8024
  var import_plugin_permission_common2 = __toESM(require_index_cjs2());
8025
- var import_errors21 = __toESM(require_index_cjs());
8025
+ var import_errors22 = __toESM(require_index_cjs());
8026
8026
  var import_catalog_client = require("@backstage/catalog-client");
8027
8027
 
8028
8028
  // src/client.ts
@@ -8655,6 +8655,82 @@ var LiteLLMClient = class {
8655
8655
  throw err;
8656
8656
  }
8657
8657
  }
8658
+ /**
8659
+ * Returns per-request spend rows from `/spend/logs` (retried as
8660
+ * `/spend/logs/v2` on 404 — the path differs across LiteLLM versions).
8661
+ * The response shape is version-dependent, so rows are parsed tolerantly:
8662
+ * a bare array or a `{ data | results | logs }` envelope, with per-field
8663
+ * fallbacks in normalizeSpendLogs.
8664
+ */
8665
+ async getSpendLogs(params) {
8666
+ const page = params.page ?? 1;
8667
+ const pageSize = Math.min(params.pageSize ?? 25, 100);
8668
+ const buildQuery = () => {
8669
+ const q = new URLSearchParams({
8670
+ summarize: "false",
8671
+ start_date: params.startDate,
8672
+ end_date: params.endDate,
8673
+ page: String(page),
8674
+ page_size: String(pageSize)
8675
+ });
8676
+ if (params.userId) q.set("user_id", params.userId);
8677
+ if (params.teamId) q.set("team_id", params.teamId);
8678
+ if (params.apiKeyHash) q.set("api_key", params.apiKeyHash);
8679
+ return q.toString();
8680
+ };
8681
+ let raw;
8682
+ try {
8683
+ raw = await this.request(`/spend/logs?${buildQuery()}`);
8684
+ } catch (err) {
8685
+ if (err instanceof LiteLLMUpstreamError && err.status === 404) {
8686
+ raw = await this.request(`/spend/logs/v2?${buildQuery()}`);
8687
+ } else {
8688
+ throw err;
8689
+ }
8690
+ }
8691
+ return this.normalizeSpendLogs(raw, page, pageSize);
8692
+ }
8693
+ normalizeSpendLogs(raw, page, pageSize) {
8694
+ const rows = Array.isArray(raw) ? raw : Array.isArray(raw?.data) ? raw.data : Array.isArray(raw?.results) ? raw.results : Array.isArray(raw?.logs) ? raw.logs : [];
8695
+ const toNum = (v, fallback = 0) => {
8696
+ const n = typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v) : NaN;
8697
+ return Number.isFinite(n) ? n : fallback;
8698
+ };
8699
+ const data = rows.map((r) => {
8700
+ const promptTokens = toNum(
8701
+ r?.prompt_tokens ?? r?.promptTokens ?? r?.input_tokens
8702
+ );
8703
+ const completionTokens = toNum(
8704
+ r?.completion_tokens ?? r?.completionTokens ?? r?.output_tokens
8705
+ );
8706
+ return {
8707
+ requestId: String(r?.request_id ?? r?.requestId ?? r?.id ?? ""),
8708
+ model: String(r?.model ?? r?.model_name ?? ""),
8709
+ spend: toNum(r?.spend ?? r?.cost),
8710
+ promptTokens,
8711
+ completionTokens,
8712
+ totalTokens: toNum(
8713
+ r?.total_tokens ?? r?.totalTokens,
8714
+ promptTokens + completionTokens
8715
+ ),
8716
+ status: String(r?.status ?? r?.request_status ?? "unknown"),
8717
+ cacheHit: Boolean(r?.cache_hit ?? r?.cacheHit ?? r?.cached === true),
8718
+ ...r?.user_id ?? r?.userId ? { userId: String(r.user_id ?? r.userId) } : {},
8719
+ ...r?.team_id ?? r?.teamId ? { teamId: String(r.team_id ?? r.teamId) } : {},
8720
+ ...r?.api_key ?? r?.keyHash ? { keyHash: String(r.api_key ?? r.keyHash) } : {},
8721
+ timestamp: String(
8722
+ r?.timestamp ?? r?.startTime ?? r?.created_at ?? r?.date ?? ""
8723
+ )
8724
+ };
8725
+ });
8726
+ return {
8727
+ data,
8728
+ total: toNum(raw?.total ?? raw?.total_count, data.length),
8729
+ page,
8730
+ pageSize,
8731
+ truncated: data.length >= pageSize
8732
+ };
8733
+ }
8658
8734
  };
8659
8735
 
8660
8736
  // src/errors.ts
@@ -11482,6 +11558,46 @@ function registerTeamUsageRoutes(router, ctx) {
11482
11558
  }
11483
11559
  }
11484
11560
 
11561
+ // src/routes/insights.ts
11562
+ function registerInsightsRoutes(router, ctx) {
11563
+ const { client, catalogClient, auth, logger, auditGroup } = ctx;
11564
+ const requireUser = createRequireUser(ctx);
11565
+ router.get("/insights/spend-logs", requireUser, async (req, res) => {
11566
+ try {
11567
+ const { start_date, end_date, userId, teamId, page, page_size } = req.query;
11568
+ if (!start_date || !end_date) {
11569
+ res.status(400).json({ error: "start_date and end_date are required" });
11570
+ return;
11571
+ }
11572
+ const tokenEntityRef = res.locals.tokenEntityRef;
11573
+ await getProvisionedUser(ctx, res);
11574
+ const callerId = res.locals.userId;
11575
+ const isAudit = auditGroup ? await isUserMemberOfGroup(
11576
+ tokenEntityRef,
11577
+ auditGroup,
11578
+ catalogClient,
11579
+ auth,
11580
+ logger
11581
+ ) : false;
11582
+ const result = await client.getSpendLogs({
11583
+ startDate: start_date,
11584
+ endDate: end_date,
11585
+ userId: isAudit ? userId || callerId : callerId,
11586
+ teamId: isAudit ? teamId : void 0,
11587
+ page: page ? Number(page) : void 0,
11588
+ pageSize: page_size ? Number(page_size) : void 0
11589
+ });
11590
+ res.json(result);
11591
+ } catch (error) {
11592
+ if (error instanceof ProvisioningError) {
11593
+ res.status(error.status).json(error.body);
11594
+ return;
11595
+ }
11596
+ sendError(res, error, logger, "fetch spend logs");
11597
+ }
11598
+ });
11599
+ }
11600
+
11485
11601
  // node_modules/jose/dist/node/esm/runtime/base64url.js
11486
11602
  var import_node_buffer = require("node:buffer");
11487
11603
 
@@ -13269,14 +13385,14 @@ async function createRouter(options) {
13269
13385
  // Helper functions - defined inline below this object
13270
13386
  authorizeKeyAction: async (req, keyId) => {
13271
13387
  if (!req.res || !req.res.locals.tokenEntityRef || !req.res.locals.userId) {
13272
- throw new import_errors21.NotAllowedError("User identity not found in request");
13388
+ throw new import_errors22.NotAllowedError("User identity not found in request");
13273
13389
  }
13274
13390
  const tokenEntityRef = req.res.locals.tokenEntityRef;
13275
13391
  const userId = await resolveMemberUserId(tokenEntityRef);
13276
13392
  const ownKeys = await client.listKeys(userId);
13277
13393
  const key = ownKeys.find((k) => (k.token ?? k.key) === keyId);
13278
13394
  if (!key) {
13279
- throw new import_errors21.NotAllowedError("Access denied: key does not belong to the caller");
13395
+ throw new import_errors22.NotAllowedError("Access denied: key does not belong to the caller");
13280
13396
  }
13281
13397
  return { tokenEntityRef, userId, key };
13282
13398
  },
@@ -13410,13 +13526,14 @@ async function createRouter(options) {
13410
13526
  router.use(import_express.default.json());
13411
13527
  router.use(import_express.default.urlencoded({ extended: false }));
13412
13528
  const requireUser = createRequireUser(ctx);
13413
- router.use(["/user/info", "/keys", "/usage"], requireUser);
13529
+ router.use(["/user/info", "/keys", "/usage", "/insights"], requireUser);
13414
13530
  registerConfigRoutes(router, ctx);
13415
13531
  registerKeysRoutes(router, ctx);
13416
13532
  registerOpencodeRoutes(router, ctx);
13417
13533
  registerTeamsRoutes(router, ctx);
13418
13534
  registerObjectPermissionsRoutes(router, ctx);
13419
13535
  registerTeamUsageRoutes(router, ctx);
13536
+ registerInsightsRoutes(router, ctx);
13420
13537
  registerBridgeRoutes(router, ctx, { config, tokenVerifier: options.tokenVerifier });
13421
13538
  return router;
13422
13539
  }