@acarmisc/backstage-plugin-litellm-backend 0.6.2 → 0.8.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/config.d.ts CHANGED
@@ -127,6 +127,27 @@ export interface Config {
127
127
  group?: string;
128
128
  };
129
129
 
130
+ /**
131
+ * Controls for the "Generate New Key" form in the frontend.
132
+ */
133
+ keyGeneration?: {
134
+ /**
135
+ * When true, users can tick an "Unlimited budget" checkbox that skips
136
+ * the max-budget cap entirely. The checkbox is hidden from the form
137
+ * when this is false, so a budget is always required.
138
+ * @default false
139
+ */
140
+ allowUnlimitedBudget?: boolean;
141
+
142
+ /**
143
+ * When true (the default), a team must be selected before a key can
144
+ * be generated. Set to false to let users generate personal,
145
+ * team-less keys.
146
+ * @default true
147
+ */
148
+ teamRequired?: boolean;
149
+ };
150
+
130
151
  bridge?: {
131
152
  /**
132
153
  * When true, mount the /bridge/keys, /bridge/keys (POST), /bridge/models
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,36 @@
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;
5
34
  class LiteLLMClient {
6
35
  constructor(config, timeout = DEFAULT_TIMEOUT) {
7
36
  this.baseUrl = config.baseUrl.replace(/\/$/, '');
@@ -28,9 +57,7 @@ class LiteLLMClient {
28
57
  });
29
58
  if (!response.ok) {
30
59
  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;
60
+ throw new LiteLLMUpstreamError(response.status, response.statusText, errorBody);
34
61
  }
35
62
  return response.json();
36
63
  }
package/dist/index.cjs.js CHANGED
@@ -63,6 +63,24 @@ 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
+ };
66
84
  var LiteLLMClient = class {
67
85
  constructor(config, timeout = DEFAULT_TIMEOUT) {
68
86
  this.baseUrl = config.baseUrl.replace(/\/$/, "");
@@ -84,11 +102,11 @@ var LiteLLMClient = class {
84
102
  });
85
103
  if (!response.ok) {
86
104
  const errorBody = await response.text();
87
- const err = new Error(
88
- `LiteLLM API error: ${response.status} ${response.statusText} - ${errorBody}`
105
+ throw new LiteLLMUpstreamError(
106
+ response.status,
107
+ response.statusText,
108
+ errorBody
89
109
  );
90
- err.status = response.status;
91
- throw err;
92
110
  }
93
111
  return response.json();
94
112
  } finally {
@@ -511,12 +529,16 @@ var openApiSpec = {
511
529
  "/config": {
512
530
  get: {
513
531
  tags: ["System"],
514
- summary: "Public LiteLLM proxy base URL (for snippet generation)",
532
+ summary: "Public LiteLLM proxy base URL and key-generation form settings",
515
533
  responses: {
516
534
  "200": {
517
- description: "Proxy URL",
535
+ description: "Frontend config",
518
536
  content: { "application/json": { schema: { type: "object", properties: {
519
- baseUrl: { type: "string" }
537
+ baseUrl: { type: "string" },
538
+ keyGeneration: { type: "object", properties: {
539
+ allowUnlimitedBudget: { type: "boolean" },
540
+ teamRequired: { type: "boolean" }
541
+ } }
520
542
  } } } }
521
543
  }
522
544
  }
@@ -2465,6 +2487,8 @@ async function createRouter(options) {
2465
2487
  const { enabled: provisioningEnabled, defaults: provisioningDefaults } = readProvisioningDefaults(config);
2466
2488
  const roleConfigs = readRoleConfigs(config);
2467
2489
  const auditGroup = config.getOptionalString("litellm.audit.group");
2490
+ const allowUnlimitedBudget = config.getOptionalBoolean("litellm.keyGeneration.allowUnlimitedBudget") ?? false;
2491
+ const teamRequired = config.getOptionalBoolean("litellm.keyGeneration.teamRequired") ?? true;
2468
2492
  const catalogClient = new import_catalog_client.CatalogClient({ discoveryApi: discovery });
2469
2493
  if (provisioningEnabled) {
2470
2494
  logger.info(
@@ -2477,7 +2501,10 @@ async function createRouter(options) {
2477
2501
  res.json({ status: "ok", provisioning: provisioningEnabled });
2478
2502
  });
2479
2503
  router.get("/config", (_req, res) => {
2480
- res.json({ baseUrl: publicBaseUrl });
2504
+ res.json({
2505
+ baseUrl: publicBaseUrl,
2506
+ keyGeneration: { allowUnlimitedBudget, teamRequired }
2507
+ });
2481
2508
  });
2482
2509
  router.get("/openapi.json", (_req, res) => {
2483
2510
  res.json(openApiSpec);
@@ -2669,6 +2696,13 @@ async function createRouter(options) {
2669
2696
  });
2670
2697
  return;
2671
2698
  }
2699
+ if (error instanceof LiteLLMUpstreamError) {
2700
+ res.status(error.status).json({
2701
+ error: error.message,
2702
+ ...error.param ? { param: error.param } : {}
2703
+ });
2704
+ return;
2705
+ }
2672
2706
  logger.error("Failed to generate key", error);
2673
2707
  res.status(500).json({ error: error.message });
2674
2708
  }