@acarmisc/backstage-plugin-litellm-backend 0.7.0 → 0.8.1

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,16 @@
1
1
  import { LiteLLMConfig, UserInfo, VirtualKey, ModelInfo, UsageMetrics, TeamInfo, GenerateKeyRequest, GenerateKeyResponse, UpdateKeyRequest, DeleteKeyRequest, CreateUserRequest, CreateUserResponse, AuditLogsParams, PaginatedAuditLogs } from './types';
2
+ /**
3
+ * Typed error for failed upstream LiteLLM responses. Preserves the HTTP
4
+ * status and the structured `param` (e.g. `key_alias`) from the upstream
5
+ * body so the router can surface a 400 instead of collapsing everything
6
+ * into a 500. The message is the upstream `error.message` when present,
7
+ * otherwise the raw body text.
8
+ */
9
+ export declare class LiteLLMUpstreamError extends Error {
10
+ status: number;
11
+ param?: string;
12
+ constructor(status: number, statusText: string, body: string);
13
+ }
2
14
  export declare class LiteLLMClient {
3
15
  private baseUrl;
4
16
  private masterKey;
package/dist/client.js CHANGED
@@ -1,7 +1,43 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.LiteLLMClient = void 0;
3
+ exports.LiteLLMClient = exports.LiteLLMUpstreamError = void 0;
4
4
  const DEFAULT_TIMEOUT = 30000;
5
+ /**
6
+ * Typed error for failed upstream LiteLLM responses. Preserves the HTTP
7
+ * status and the structured `param` (e.g. `key_alias`) from the upstream
8
+ * body so the router can surface a 400 instead of collapsing everything
9
+ * into a 500. The message is the upstream `error.message` when present,
10
+ * otherwise the raw body text.
11
+ */
12
+ class LiteLLMUpstreamError extends Error {
13
+ constructor(status, statusText, body) {
14
+ let message = `LiteLLM API error: ${status} ${statusText} - ${body}`;
15
+ let param;
16
+ try {
17
+ const parsed = JSON.parse(body);
18
+ const inner = parsed?.error ?? parsed;
19
+ if (typeof inner?.message === 'string')
20
+ message = inner.message;
21
+ if (typeof inner?.param === 'string' && inner.param !== 'None') {
22
+ param = inner.param;
23
+ }
24
+ }
25
+ catch {
26
+ // not JSON — keep the raw body in the message
27
+ }
28
+ super(message);
29
+ this.status = status;
30
+ this.param = param;
31
+ }
32
+ }
33
+ exports.LiteLLMUpstreamError = LiteLLMUpstreamError;
34
+ function extractModelList(fallback) {
35
+ if (Array.isArray(fallback))
36
+ return fallback;
37
+ if (Array.isArray(fallback?.data))
38
+ return fallback.data;
39
+ return [];
40
+ }
5
41
  class LiteLLMClient {
6
42
  constructor(config, timeout = DEFAULT_TIMEOUT) {
7
43
  this.baseUrl = config.baseUrl.replace(/\/$/, '');
@@ -28,9 +64,7 @@ class LiteLLMClient {
28
64
  });
29
65
  if (!response.ok) {
30
66
  const errorBody = await response.text();
31
- const err = new Error(`LiteLLM API error: ${response.status} ${response.statusText} - ${errorBody}`);
32
- err.status = response.status;
33
- throw err;
67
+ throw new LiteLLMUpstreamError(response.status, response.statusText, errorBody);
34
68
  }
35
69
  return response.json();
36
70
  }
@@ -245,11 +279,7 @@ class LiteLLMClient {
245
279
  // fall through to /models
246
280
  }
247
281
  const fallback = await this.request('/models');
248
- const data = Array.isArray(fallback)
249
- ? fallback
250
- : Array.isArray(fallback?.data)
251
- ? fallback.data
252
- : [];
282
+ const data = extractModelList(fallback);
253
283
  return data
254
284
  .map((m) => ({
255
285
  model_name: m.model_name ?? m.id ?? '',
@@ -373,7 +403,7 @@ class LiteLLMClient {
373
403
  };
374
404
  if (!kb.key_alias && kmeta.key_alias)
375
405
  kb.key_alias = kmeta.key_alias;
376
- if (kb.team_id == null && kmeta.team_id)
406
+ if (kb.team_id === null && kmeta.team_id)
377
407
  kb.team_id = kmeta.team_id;
378
408
  if (!kb.models.includes(name))
379
409
  kb.models.push(name);
package/dist/index.cjs.js CHANGED
@@ -63,6 +63,29 @@ var import_catalog_client = require("@backstage/catalog-client");
63
63
 
64
64
  // src/client.ts
65
65
  var DEFAULT_TIMEOUT = 3e4;
66
+ var LiteLLMUpstreamError = class extends Error {
67
+ constructor(status, statusText, body) {
68
+ let message2 = `LiteLLM API error: ${status} ${statusText} - ${body}`;
69
+ let param;
70
+ try {
71
+ const parsed = JSON.parse(body);
72
+ const inner = parsed?.error ?? parsed;
73
+ if (typeof inner?.message === "string") message2 = inner.message;
74
+ if (typeof inner?.param === "string" && inner.param !== "None") {
75
+ param = inner.param;
76
+ }
77
+ } catch {
78
+ }
79
+ super(message2);
80
+ this.status = status;
81
+ this.param = param;
82
+ }
83
+ };
84
+ function extractModelList(fallback) {
85
+ if (Array.isArray(fallback)) return fallback;
86
+ if (Array.isArray(fallback?.data)) return fallback.data;
87
+ return [];
88
+ }
66
89
  var LiteLLMClient = class {
67
90
  constructor(config, timeout = DEFAULT_TIMEOUT) {
68
91
  this.baseUrl = config.baseUrl.replace(/\/$/, "");
@@ -84,11 +107,11 @@ var LiteLLMClient = class {
84
107
  });
85
108
  if (!response.ok) {
86
109
  const errorBody = await response.text();
87
- const err = new Error(
88
- `LiteLLM API error: ${response.status} ${response.statusText} - ${errorBody}`
110
+ throw new LiteLLMUpstreamError(
111
+ response.status,
112
+ response.statusText,
113
+ errorBody
89
114
  );
90
- err.status = response.status;
91
- throw err;
92
115
  }
93
116
  return response.json();
94
117
  } finally {
@@ -284,7 +307,7 @@ var LiteLLMClient = class {
284
307
  } catch {
285
308
  }
286
309
  const fallback = await this.request("/models");
287
- const data = Array.isArray(fallback) ? fallback : Array.isArray(fallback?.data) ? fallback.data : [];
310
+ const data = extractModelList(fallback);
288
311
  return data.map((m) => ({
289
312
  model_name: m.model_name ?? m.id ?? "",
290
313
  mode: m.mode ?? "chat",
@@ -403,7 +426,7 @@ var LiteLLMClient = class {
403
426
  ...emptyModelBucket()
404
427
  };
405
428
  if (!kb.key_alias && kmeta.key_alias) kb.key_alias = kmeta.key_alias;
406
- if (kb.team_id == null && kmeta.team_id) kb.team_id = kmeta.team_id;
429
+ if (kb.team_id === null && kmeta.team_id) kb.team_id = kmeta.team_id;
407
430
  if (!kb.models.includes(name)) kb.models.push(name);
408
431
  kb.total_spend += km.spend ?? 0;
409
432
  kb.total_tokens += km.total_tokens ?? 0;
@@ -836,7 +859,7 @@ async function provisionUser(client, userId, defaults, profile, backstageEntity,
836
859
  }
837
860
  };
838
861
  logger.info(
839
- `Provisioning new LiteLLM user for Backstage identity: ${userId}` + (profile.email ? ` (email=${profile.email})` : "")
862
+ `Provisioning new LiteLLM user for Backstage identity: ${userId}${profile.email ? ` (email=${profile.email})` : ""}`
840
863
  );
841
864
  try {
842
865
  await client.createUser(payload);
@@ -2601,12 +2624,18 @@ async function createRouter(options) {
2601
2624
  const tokenEntityRef = await resolveUserId(req, auth);
2602
2625
  const userId = tokenEntityRef ? toLiteLLMUserId(tokenEntityRef, userIdDomain) : req.query.user_id;
2603
2626
  if (!userId) {
2604
- throw { status: 403, body: { error: "Cannot verify key ownership without an authenticated user" } };
2627
+ throw Object.assign(new Error("Cannot verify key ownership without an authenticated user"), {
2628
+ status: 403,
2629
+ body: { error: "Cannot verify key ownership without an authenticated user" }
2630
+ });
2605
2631
  }
2606
2632
  const ownKeys = await client.listKeys(userId);
2607
2633
  const owns = ownKeys.some((k) => (k.token ?? k.key) === keyId);
2608
2634
  if (!owns) {
2609
- throw { status: 403, body: { error: "Access denied: key does not belong to the caller" } };
2635
+ throw Object.assign(new Error("Access denied: key does not belong to the caller"), {
2636
+ status: 403,
2637
+ body: { error: "Access denied: key does not belong to the caller" }
2638
+ });
2610
2639
  }
2611
2640
  return { tokenEntityRef, userId };
2612
2641
  }
@@ -2678,6 +2707,13 @@ async function createRouter(options) {
2678
2707
  });
2679
2708
  return;
2680
2709
  }
2710
+ if (error instanceof LiteLLMUpstreamError) {
2711
+ res.status(error.status).json({
2712
+ error: error.message,
2713
+ ...error.param ? { param: error.param } : {}
2714
+ });
2715
+ return;
2716
+ }
2681
2717
  logger.error("Failed to generate key", error);
2682
2718
  res.status(500).json({ error: error.message });
2683
2719
  }