@acarmisc/backstage-plugin-litellm-backend 0.2.0 → 0.2.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/router.js CHANGED
@@ -1,8 +1,41 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.ProvisioningError = void 0;
4
37
  exports.createRouter = createRouter;
5
- const express_1 = require("express");
38
+ const express_1 = __importStar(require("express"));
6
39
  const catalog_client_1 = require("@backstage/catalog-client");
7
40
  const client_1 = require("./client");
8
41
  const provisioning_1 = require("./provisioning");
@@ -17,9 +50,15 @@ async function createRouter(options) {
17
50
  const roleConfigs = (0, provisioning_1.readRoleConfigs)(config);
18
51
  const catalogClient = new catalog_client_1.CatalogClient({ discoveryApi: discovery });
19
52
  if (provisioningEnabled) {
20
- logger.info(`LiteLLM auto-provisioning enabled — defaults: budget=$${provisioningDefaults.maxBudget}/${provisioningDefaults.budgetDuration}, models=${provisioningDefaults.models.length ? provisioningDefaults.models.join(',') : 'all'}, teams=[${provisioningDefaults.teams.join(',')}]`);
53
+ logger.info(`LiteLLM auto-provisioning enabled — defaults: budget=$${provisioningDefaults.maxBudget}/${provisioningDefaults.budgetDuration}, models=${provisioningDefaults.models.length
54
+ ? provisioningDefaults.models.join(',')
55
+ : 'all'}, teams=[${provisioningDefaults.teams.join(',')}]`);
21
56
  }
22
57
  const router = (0, express_1.Router)();
58
+ // JSON body parser. Without this, every POST/PUT endpoint sees an empty
59
+ // req.body. Backstage's httpRouter does not apply a body parser at the
60
+ // plugin-router level, so each plugin must attach its own.
61
+ router.use(express_1.default.json());
23
62
  router.get('/health', (_req, res) => {
24
63
  res.json({ status: 'ok', provisioning: provisioningEnabled });
25
64
  });
@@ -62,13 +101,52 @@ async function createRouter(options) {
62
101
  });
63
102
  router.post('/keys/generate', async (req, res) => {
64
103
  try {
104
+ // Only alias + max_budget are required. An empty models array is
105
+ // intentional — in LiteLLM `models: []` means "all models the user
106
+ // can access" which is the desired default. Forcing a selection
107
+ // up front is too restrictive for the common case.
108
+ const body = (req.body ?? {});
109
+ const missing = [];
110
+ if (!body.alias?.trim())
111
+ missing.push('alias');
112
+ if (typeof body.max_budget !== 'number' || body.max_budget <= 0) {
113
+ missing.push('max_budget (positive number)');
114
+ }
115
+ if (missing.length) {
116
+ res.status(400).json({
117
+ error: 'Missing required fields',
118
+ hint: `Required: ${missing.join(', ')}`,
119
+ });
120
+ return;
121
+ }
65
122
  const tokenEntityRef = await (0, provisioning_1.resolveUserId)(req, auth);
66
- const resolvedUserId = tokenEntityRef ? (0, provisioning_1.toLiteLLMUserId)(tokenEntityRef, userIdDomain) : undefined;
123
+ const resolvedUserId = tokenEntityRef
124
+ ? (0, provisioning_1.toLiteLLMUserId)(tokenEntityRef, userIdDomain)
125
+ : undefined;
67
126
  if (resolvedUserId) {
68
127
  await (0, provisioning_1.getOrProvisionUser)(client, tokenEntityRef, resolvedUserId, provisioningEnabled, provisioningDefaults, roleConfigs, catalogClient, auth, logger);
69
128
  }
129
+ // Stamp ownership into LiteLLM key metadata. LiteLLM's native
130
+ // `created_by` column is only populated when the caller authenticates
131
+ // via JWT/SSO; we always call with the master key, so that column
132
+ // stays null. Enriching `metadata` makes the owner identity visible
133
+ // in LiteLLM's UI and queryable via API.
134
+ const profile = tokenEntityRef
135
+ ? await (0, provisioning_1.resolveUserProfile)(tokenEntityRef, catalogClient, auth, logger)
136
+ : {};
137
+ const enrichedMetadata = {
138
+ ...(body.metadata ?? {}),
139
+ created_by_backstage_user: tokenEntityRef ?? 'unknown',
140
+ ...(profile.email && { created_by_email: profile.email }),
141
+ ...(profile.displayName && {
142
+ created_by_display_name: profile.displayName,
143
+ }),
144
+ created_via: 'backstage',
145
+ created_at_iso: new Date().toISOString(),
146
+ };
70
147
  const request = {
71
- ...req.body,
148
+ ...body,
149
+ metadata: enrichedMetadata,
72
150
  ...(resolvedUserId && { user_id: resolvedUserId }),
73
151
  };
74
152
  const result = await client.generateKey(request);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/types.ts"],
4
- "sourcesContent": ["export interface UserInfo {\n user_id: string;\n user_email?: string;\n email?: string;\n teams?: string[];\n models?: string[];\n max_budget?: number;\n spend?: number;\n current_spend?: number;\n soft_limit?: number;\n hard_limit?: number;\n}\n\nexport interface TeamMember {\n user_id: string;\n role: 'admin' | 'user';\n}\n\nexport interface TeamInfo {\n team_id: string;\n team_alias?: string;\n max_budget?: number;\n spend: number;\n members_with_roles?: TeamMember[];\n models?: string[];\n tpm_limit?: number;\n rpm_limit?: number;\n}\n\nexport interface VirtualKey {\n key: string;\n key_alias?: string;\n created_at: string;\n expires_at?: string;\n spend: number;\n max_budget?: number;\n tpm_limit?: number;\n rpm_limit?: number;\n models?: string[];\n user_id?: string;\n}\n\nexport interface ModelInfo {\n model_name: string;\n mode: string;\n supports_function_calling?: boolean;\n supports_vision?: boolean;\n input_cost_per_token?: number;\n output_cost_per_token?: number;\n}\n\nexport interface UsageModelBreakdown {\n total_spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n}\n\nexport interface UsageKeyBreakdown {\n key_alias?: string;\n team_id?: string | null;\n models: string[];\n total_spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n}\n\nexport interface UsageDailyPoint {\n date: string;\n spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n}\n\nexport interface UsageDailyModelPoint {\n date: string;\n model: string;\n spend: number;\n prompt_tokens: number;\n completion_tokens: number;\n total_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n}\n\nexport interface UsageMetrics {\n total_spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n usage_by_model: Record<string, UsageModelBreakdown>;\n usage_by_key: Record<string, UsageKeyBreakdown>;\n daily_usage: UsageDailyPoint[];\n daily_by_model: UsageDailyModelPoint[];\n}\n\nexport interface GenerateKeyRequest {\n alias?: string;\n models?: string[];\n duration?: string;\n max_budget?: number;\n tpm_limit?: number;\n rpm_limit?: number;\n user_id?: string;\n team_id?: string;\n key_type?: string;\n}\n\nexport interface UpdateKeyRequest {\n key: string;\n key_alias?: string;\n models?: string[];\n max_budget?: number;\n tpm_limit?: number;\n rpm_limit?: number;\n team_id?: string;\n duration?: string;\n}\n\nexport interface GenerateKeyResponse {\n key: string;\n key_alias?: string;\n expires_at?: string;\n max_budget?: number;\n tpm_limit?: number;\n rpm_limit?: number;\n models?: string[];\n}\n\nexport interface DeleteKeyRequest {\n keys: string[];\n}\n\nexport interface LiteLLMConfig {\n baseUrl: string;\n masterKey: string;\n}\n\nexport interface ProvisioningDefaults {\n maxBudget: number;\n budgetDuration: string;\n models: string[];\n teams: string[];\n tpmLimit?: number;\n rpmLimit?: number;\n metadata: Record<string, string>;\n}\n\nexport interface RoleConfig {\n group: string;\n maxBudget?: number;\n budgetDuration?: string;\n models?: string[];\n teams?: string[];\n tpmLimit?: number;\n rpmLimit?: number;\n metadata?: Record<string, string>;\n}\n\nexport interface CreateUserRequest {\n user_id: string;\n user_email?: string;\n max_budget?: number;\n budget_duration?: string;\n models?: string[];\n teams?: string[];\n tpm_limit?: number;\n rpm_limit?: number;\n metadata?: Record<string, string>;\n}\n\nexport interface CreateUserResponse {\n user_id: string;\n user_email?: string;\n max_budget?: number;\n models?: string[];\n teams?: string[];\n}\n"],
4
+ "sourcesContent": ["export interface UserInfo {\n user_id: string;\n user_email?: string;\n email?: string;\n teams?: string[];\n models?: string[];\n max_budget?: number;\n spend?: number;\n current_spend?: number;\n soft_limit?: number;\n hard_limit?: number;\n}\n\nexport interface TeamMember {\n user_id: string;\n role: 'admin' | 'user';\n}\n\nexport interface TeamInfo {\n team_id: string;\n team_alias?: string;\n max_budget?: number;\n spend: number;\n members_with_roles?: TeamMember[];\n models?: string[];\n tpm_limit?: number;\n rpm_limit?: number;\n}\n\nexport interface VirtualKey {\n key: string;\n token: string;\n key_alias?: string;\n created_at: string;\n expires_at?: string;\n spend: number;\n max_budget?: number;\n tpm_limit?: number;\n rpm_limit?: number;\n models?: string[];\n user_id?: string;\n}\n\n/**\n * Shape of a single entry inside LiteLLM's `/user/info` `keys` array.\n * Differs from VirtualKey: uses `expires` (not `expires_at`), exposes\n * both a hashed `token` and a masked `key_name`, and fields are nullable\n * rather than optional.\n */\nexport interface LiteLLMUserKey {\n token: string;\n key_name?: string;\n key_alias?: string | null;\n spend?: number;\n expires?: string | null;\n models?: string[];\n tpm_limit?: number | null;\n rpm_limit?: number | null;\n max_budget?: number | null;\n user_id?: string | null;\n team_id?: string | null;\n created_at: string;\n}\n\nexport interface ModelInfo {\n model_name: string;\n mode: string;\n supports_function_calling?: boolean;\n supports_vision?: boolean;\n input_cost_per_token?: number;\n output_cost_per_token?: number;\n}\n\nexport interface UsageModelBreakdown {\n total_spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n}\n\nexport interface UsageKeyBreakdown {\n key_alias?: string;\n team_id?: string | null;\n models: string[];\n total_spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n}\n\nexport interface UsageDailyPoint {\n date: string;\n spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n}\n\nexport interface UsageDailyModelPoint {\n date: string;\n model: string;\n spend: number;\n prompt_tokens: number;\n completion_tokens: number;\n total_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n}\n\nexport interface UsageMetrics {\n total_spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n usage_by_model: Record<string, UsageModelBreakdown>;\n usage_by_key: Record<string, UsageKeyBreakdown>;\n daily_usage: UsageDailyPoint[];\n daily_by_model: UsageDailyModelPoint[];\n}\n\nexport interface GenerateKeyRequest {\n alias?: string;\n models?: string[];\n duration?: string;\n max_budget?: number;\n tpm_limit?: number;\n rpm_limit?: number;\n user_id?: string;\n team_id?: string;\n key_type?: string;\n metadata?: Record<string, string>;\n}\n\nexport interface UpdateKeyRequest {\n key: string;\n key_alias?: string;\n models?: string[];\n max_budget?: number;\n tpm_limit?: number;\n rpm_limit?: number;\n team_id?: string;\n duration?: string;\n}\n\nexport interface GenerateKeyResponse {\n key: string;\n key_alias?: string;\n expires_at?: string;\n max_budget?: number;\n tpm_limit?: number;\n rpm_limit?: number;\n models?: string[];\n}\n\nexport interface DeleteKeyRequest {\n keys: string[];\n}\n\nexport interface LiteLLMConfig {\n baseUrl: string;\n masterKey: string;\n}\n\nexport interface ProvisioningDefaults {\n maxBudget: number;\n budgetDuration: string;\n models: string[];\n teams: string[];\n tpmLimit?: number;\n rpmLimit?: number;\n /**\n * LiteLLM user role applied on /user/new. Defaults to \"internal_user\"\n * which grants self-service Create/Delete/View on the user's own keys.\n * Valid values: proxy_admin, proxy_admin_viewer, internal_user,\n * internal_user_viewer, team.\n */\n userRole?: string;\n metadata: Record<string, string>;\n}\n\nexport interface RoleConfig {\n group: string;\n maxBudget?: number;\n budgetDuration?: string;\n models?: string[];\n teams?: string[];\n tpmLimit?: number;\n rpmLimit?: number;\n userRole?: string;\n metadata?: Record<string, string>;\n}\n\nexport interface CreateUserRequest {\n user_id: string;\n user_email?: string;\n user_alias?: string;\n user_role?: string;\n max_budget?: number;\n budget_duration?: string;\n models?: string[];\n teams?: string[];\n tpm_limit?: number;\n rpm_limit?: number;\n metadata?: Record<string, string>;\n auto_create_key?: boolean;\n}\n\nexport interface CreateUserResponse {\n user_id: string;\n user_email?: string;\n max_budget?: number;\n models?: string[];\n teams?: string[];\n}\n"],
5
5
  "mappings": ";;;;;;;;;;;;;;;;AAAA;AAAA;",
6
6
  "names": []
7
7
  }
package/dist/types.d.ts CHANGED
@@ -26,6 +26,7 @@ export interface TeamInfo {
26
26
  }
27
27
  export interface VirtualKey {
28
28
  key: string;
29
+ token: string;
29
30
  key_alias?: string;
30
31
  created_at: string;
31
32
  expires_at?: string;
@@ -36,6 +37,26 @@ export interface VirtualKey {
36
37
  models?: string[];
37
38
  user_id?: string;
38
39
  }
40
+ /**
41
+ * Shape of a single entry inside LiteLLM's `/user/info` `keys` array.
42
+ * Differs from VirtualKey: uses `expires` (not `expires_at`), exposes
43
+ * both a hashed `token` and a masked `key_name`, and fields are nullable
44
+ * rather than optional.
45
+ */
46
+ export interface LiteLLMUserKey {
47
+ token: string;
48
+ key_name?: string;
49
+ key_alias?: string | null;
50
+ spend?: number;
51
+ expires?: string | null;
52
+ models?: string[];
53
+ tpm_limit?: number | null;
54
+ rpm_limit?: number | null;
55
+ max_budget?: number | null;
56
+ user_id?: string | null;
57
+ team_id?: string | null;
58
+ created_at: string;
59
+ }
39
60
  export interface ModelInfo {
40
61
  model_name: string;
41
62
  mode: string;
@@ -109,6 +130,7 @@ export interface GenerateKeyRequest {
109
130
  user_id?: string;
110
131
  team_id?: string;
111
132
  key_type?: string;
133
+ metadata?: Record<string, string>;
112
134
  }
113
135
  export interface UpdateKeyRequest {
114
136
  key: string;
@@ -143,6 +165,13 @@ export interface ProvisioningDefaults {
143
165
  teams: string[];
144
166
  tpmLimit?: number;
145
167
  rpmLimit?: number;
168
+ /**
169
+ * LiteLLM user role applied on /user/new. Defaults to "internal_user"
170
+ * which grants self-service Create/Delete/View on the user's own keys.
171
+ * Valid values: proxy_admin, proxy_admin_viewer, internal_user,
172
+ * internal_user_viewer, team.
173
+ */
174
+ userRole?: string;
146
175
  metadata: Record<string, string>;
147
176
  }
148
177
  export interface RoleConfig {
@@ -153,11 +182,14 @@ export interface RoleConfig {
153
182
  teams?: string[];
154
183
  tpmLimit?: number;
155
184
  rpmLimit?: number;
185
+ userRole?: string;
156
186
  metadata?: Record<string, string>;
157
187
  }
158
188
  export interface CreateUserRequest {
159
189
  user_id: string;
160
190
  user_email?: string;
191
+ user_alias?: string;
192
+ user_role?: string;
161
193
  max_budget?: number;
162
194
  budget_duration?: string;
163
195
  models?: string[];
@@ -165,6 +197,7 @@ export interface CreateUserRequest {
165
197
  tpm_limit?: number;
166
198
  rpm_limit?: number;
167
199
  metadata?: Record<string, string>;
200
+ auto_create_key?: boolean;
168
201
  }
169
202
  export interface CreateUserResponse {
170
203
  user_id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acarmisc/backstage-plugin-litellm-backend",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "The Backstage backend plugin for LiteLLM governance",
5
5
  "backstage": {
6
6
  "role": "backend-plugin",
@@ -23,8 +23,8 @@
23
23
  "config.d.ts"
24
24
  ],
25
25
  "scripts": {
26
- "build": "echo 'Build dist/ from Backstage monorepo: backstage-cli package build'",
27
- "prepack": "echo 'Dist files must be built in Backstage monorepo and copied here'",
26
+ "build": "node build.js && tsc -p tsconfig.json",
27
+ "prepack": "npm run build",
28
28
  "postpack": ""
29
29
  },
30
30
  "dependencies": {
package/dist/index.esm.js DELETED
@@ -1,323 +0,0 @@
1
- // src/plugin.ts
2
- import { coreServices, createBackendPlugin } from "@backstage/backend-plugin-api";
3
-
4
- // src/router.ts
5
- import { Router } from "express";
6
-
7
- // src/client.ts
8
- var DEFAULT_TIMEOUT = 3e4;
9
- var LiteLLMClient = class {
10
- constructor(config, timeout = DEFAULT_TIMEOUT) {
11
- this.baseUrl = config.baseUrl.replace(/\/$/, "");
12
- this.masterKey = config.masterKey;
13
- this.timeout = timeout;
14
- }
15
- async request(path, options = {}) {
16
- const controller = new AbortController();
17
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
18
- try {
19
- const response = await fetch(`${this.baseUrl}${path}`, {
20
- ...options,
21
- signal: controller.signal,
22
- headers: {
23
- "Content-Type": "application/json",
24
- "Authorization": `Bearer ${this.masterKey}`,
25
- ...options.headers
26
- }
27
- });
28
- if (!response.ok) {
29
- const errorBody = await response.text();
30
- const err = new Error(`LiteLLM API error: ${response.status} ${response.statusText} - ${errorBody}`);
31
- err.status = response.status;
32
- throw err;
33
- }
34
- return response.json();
35
- } finally {
36
- clearTimeout(timeoutId);
37
- }
38
- }
39
- /**
40
- * Returns null when the user is not found in LiteLLM (404).
41
- * Throws on all other errors so callers know something went wrong.
42
- */
43
- async getUserInfo(userId) {
44
- const query = userId ? `?user_id=${encodeURIComponent(userId)}` : "";
45
- try {
46
- return await this.request(`/user/info${query}`);
47
- } catch (err) {
48
- if (err.status === 404) return null;
49
- throw err;
50
- }
51
- }
52
- async createUser(payload) {
53
- return this.request("/user/new", {
54
- method: "POST",
55
- body: JSON.stringify(payload)
56
- });
57
- }
58
- async listKeys(userId) {
59
- const query = userId ? `?user_id=${encodeURIComponent(userId)}` : "";
60
- const response = await this.request(`/key/info${query}`);
61
- return Array.isArray(response) ? response : response.info ?? [];
62
- }
63
- async generateKey(request) {
64
- return this.request("/key/generate", {
65
- method: "POST",
66
- body: JSON.stringify(request)
67
- });
68
- }
69
- async deleteKeys(request) {
70
- return this.request("/key/delete", {
71
- method: "POST",
72
- body: JSON.stringify(request)
73
- });
74
- }
75
- async listModels() {
76
- const response = await this.request("/models");
77
- return Array.isArray(response) ? response : response.data ?? [];
78
- }
79
- async getTeamInfo(teamId) {
80
- return this.request(`/team/info?team_id=${encodeURIComponent(teamId)}`);
81
- }
82
- async getUsage(startDate, endDate, userId, groupBy) {
83
- const params = new URLSearchParams({ start_date: startDate, end_date: endDate });
84
- if (userId) params.append("user_id", userId);
85
- if (groupBy) params.append("group_by", groupBy);
86
- return this.request(`/usage/keys?${params.toString()}`);
87
- }
88
- async getTeamUsage(teamId, startDate, endDate) {
89
- const params = new URLSearchParams({ start_date: startDate, end_date: endDate, team_id: teamId });
90
- return this.request(`/usage/keys?${params.toString()}`);
91
- }
92
- };
93
-
94
- // src/router.ts
95
- function readProvisioningDefaults(config) {
96
- const enabled = config.getOptionalBoolean("litellm.provisioning.enabled") ?? false;
97
- const defaults = {
98
- maxBudget: config.getOptionalNumber("litellm.provisioning.defaults.maxBudget") ?? 10,
99
- budgetDuration: config.getOptionalString("litellm.provisioning.defaults.budgetDuration") ?? "30d",
100
- models: config.getOptionalStringArray("litellm.provisioning.defaults.models") ?? [],
101
- teams: config.getOptionalStringArray("litellm.provisioning.defaults.teams") ?? [],
102
- tpmLimit: config.getOptionalNumber("litellm.provisioning.defaults.tpmLimit"),
103
- rpmLimit: config.getOptionalNumber("litellm.provisioning.defaults.rpmLimit"),
104
- metadata: config.getOptional("litellm.provisioning.defaults.metadata") ?? {}
105
- };
106
- return { enabled, defaults };
107
- }
108
- async function resolveUserId(req, auth) {
109
- const rawToken = req.headers.authorization?.slice(7);
110
- if (!rawToken) return void 0;
111
- try {
112
- const credentials = await auth.authenticate(rawToken);
113
- const principal = credentials.principal;
114
- if (principal?.type === "user") {
115
- return principal.userEntityRef;
116
- }
117
- } catch {
118
- }
119
- return void 0;
120
- }
121
- async function provisionUser(client, userId, defaults, logger) {
122
- const payload = {
123
- user_id: userId,
124
- max_budget: defaults.maxBudget,
125
- budget_duration: defaults.budgetDuration,
126
- models: defaults.models,
127
- teams: defaults.teams,
128
- ...defaults.tpmLimit !== void 0 && { tpm_limit: defaults.tpmLimit },
129
- ...defaults.rpmLimit !== void 0 && { rpm_limit: defaults.rpmLimit },
130
- metadata: {
131
- ...defaults.metadata,
132
- provisioned_by: "backstage",
133
- provisioned_at: (/* @__PURE__ */ new Date()).toISOString(),
134
- backstage_entity: userId
135
- }
136
- };
137
- logger.info(`Provisioning new LiteLLM user for Backstage identity: ${userId}`);
138
- try {
139
- await client.createUser(payload);
140
- return await client.getUserInfo(userId);
141
- } catch (err) {
142
- logger.error(`Failed to provision LiteLLM user ${userId}: ${err.message}`);
143
- return null;
144
- }
145
- }
146
- async function createRouter(options) {
147
- const { config, logger, auth } = options;
148
- const baseUrl = config.getString("litellm.baseUrl");
149
- const masterKey = config.getString("litellm.masterKey");
150
- const client = new LiteLLMClient({ baseUrl, masterKey });
151
- const { enabled: provisioningEnabled, defaults: provisioningDefaults } = readProvisioningDefaults(config);
152
- if (provisioningEnabled) {
153
- logger.info(
154
- `LiteLLM auto-provisioning enabled \u2014 defaults: budget=$${provisioningDefaults.maxBudget}/${provisioningDefaults.budgetDuration}, models=${provisioningDefaults.models.length ? provisioningDefaults.models.join(",") : "all"}, teams=[${provisioningDefaults.teams.join(",")}]`
155
- );
156
- }
157
- const router = Router();
158
- router.get("/health", (_req, res) => {
159
- res.json({ status: "ok", provisioning: provisioningEnabled });
160
- });
161
- router.get("/user/info", async (req, res) => {
162
- try {
163
- const tokenUserId = await resolveUserId(req, auth);
164
- const userId = tokenUserId ?? req.query.user_id;
165
- let userInfo = await client.getUserInfo(userId);
166
- if (!userInfo) {
167
- if (provisioningEnabled && userId) {
168
- userInfo = await provisionUser(client, userId, provisioningDefaults, logger);
169
- }
170
- if (!userInfo) {
171
- res.status(404).json({
172
- error: "User not found in LiteLLM",
173
- provisioning: provisioningEnabled,
174
- hint: provisioningEnabled ? "Provisioning attempted but failed \u2014 check LiteLLM logs" : "Enable litellm.provisioning.enabled in app-config.yaml or create the user manually"
175
- });
176
- return;
177
- }
178
- }
179
- res.json(userInfo);
180
- } catch (error) {
181
- logger.error("Failed to fetch user info", error);
182
- res.status(500).json({ error: error.message });
183
- }
184
- });
185
- router.get("/keys", async (req, res) => {
186
- try {
187
- const tokenUserId = await resolveUserId(req, auth);
188
- const userId = tokenUserId ?? req.query.user_id;
189
- const keys = await client.listKeys(userId);
190
- res.json(keys);
191
- } catch (error) {
192
- logger.error("Failed to list keys", error);
193
- res.status(500).json({ error: error.message });
194
- }
195
- });
196
- router.post("/keys/generate", async (req, res) => {
197
- try {
198
- const tokenUserId = await resolveUserId(req, auth);
199
- const request = {
200
- ...req.body,
201
- ...tokenUserId && { user_id: tokenUserId }
202
- };
203
- const result = await client.generateKey(request);
204
- res.json(result);
205
- } catch (error) {
206
- logger.error("Failed to generate key", error);
207
- res.status(500).json({ error: error.message });
208
- }
209
- });
210
- router.delete("/keys/:keyId", async (req, res) => {
211
- try {
212
- const { keyId } = req.params;
213
- if (!keyId) {
214
- res.status(400).json({ error: "keyId is required" });
215
- return;
216
- }
217
- await client.deleteKeys({ keys: [keyId] });
218
- res.json({ success: true });
219
- } catch (error) {
220
- logger.error("Failed to delete key", error);
221
- res.status(500).json({ error: error.message });
222
- }
223
- });
224
- router.get("/models", async (_req, res) => {
225
- try {
226
- const models = await client.listModels();
227
- res.json(models);
228
- } catch (error) {
229
- logger.error("Failed to list models", error);
230
- res.status(500).json({ error: error.message });
231
- }
232
- });
233
- router.get("/teams", async (req, res) => {
234
- try {
235
- const tokenUserId = await resolveUserId(req, auth);
236
- const userId = tokenUserId ?? req.query.user_id;
237
- const userInfo = await client.getUserInfo(userId);
238
- if (!userInfo?.teams?.length) {
239
- res.json([]);
240
- return;
241
- }
242
- const teams = await Promise.all(
243
- userInfo.teams.map(
244
- (teamId) => client.getTeamInfo(teamId).catch((err) => {
245
- logger.warn(`Failed to fetch team ${teamId}: ${err.message}`);
246
- return null;
247
- })
248
- )
249
- );
250
- res.json(teams.filter(Boolean));
251
- } catch (error) {
252
- logger.error("Failed to fetch teams", error);
253
- res.status(500).json({ error: error.message });
254
- }
255
- });
256
- router.get("/teams/:teamId/usage", async (req, res) => {
257
- try {
258
- const { teamId } = req.params;
259
- const { start_date, end_date } = req.query;
260
- if (!start_date || !end_date) {
261
- res.status(400).json({ error: "start_date and end_date are required" });
262
- return;
263
- }
264
- const usage = await client.getTeamUsage(
265
- teamId,
266
- start_date,
267
- end_date
268
- );
269
- res.json(usage);
270
- } catch (error) {
271
- logger.error("Failed to fetch team usage", error);
272
- res.status(500).json({ error: error.message });
273
- }
274
- });
275
- router.get("/usage", async (req, res) => {
276
- try {
277
- const { start_date, end_date, group_by } = req.query;
278
- if (!start_date || !end_date) {
279
- res.status(400).json({ error: "start_date and end_date are required" });
280
- return;
281
- }
282
- const tokenUserId = await resolveUserId(req, auth);
283
- const userId = tokenUserId ?? req.query.user_id;
284
- const usage = await client.getUsage(
285
- start_date,
286
- end_date,
287
- userId,
288
- group_by
289
- );
290
- res.json(usage);
291
- } catch (error) {
292
- logger.error("Failed to fetch usage", error);
293
- res.status(500).json({ error: error.message });
294
- }
295
- });
296
- return router;
297
- }
298
-
299
- // src/plugin.ts
300
- var litellmPlugin = createBackendPlugin({
301
- pluginId: "litellm",
302
- register(reg) {
303
- reg.registerInit({
304
- deps: {
305
- httpRouter: coreServices.httpRouter,
306
- config: coreServices.rootConfig,
307
- logger: coreServices.logger,
308
- auth: coreServices.auth,
309
- discovery: coreServices.discovery
310
- },
311
- async init({ httpRouter, config, logger, auth }) {
312
- const router = await createRouter({ config, logger, auth });
313
- httpRouter.use(router);
314
- }
315
- });
316
- }
317
- });
318
- export {
319
- LiteLLMClient,
320
- createRouter,
321
- litellmPlugin
322
- };
323
- //# sourceMappingURL=index.esm.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/plugin.ts", "../src/router.ts", "../src/client.ts"],
4
- "sourcesContent": ["import { coreServices, createBackendPlugin } from '@backstage/backend-plugin-api';\nimport { createRouter } from './router';\n\nexport const litellmPlugin = createBackendPlugin({\n pluginId: 'litellm',\n register(reg) {\n reg.registerInit({\n deps: {\n httpRouter: coreServices.httpRouter,\n config: coreServices.rootConfig,\n logger: coreServices.logger,\n auth: coreServices.auth,\n discovery: coreServices.discovery,\n },\n async init({ httpRouter, config, logger, auth }) {\n const router = await createRouter({ config, logger, auth });\n httpRouter.use(router);\n },\n });\n },\n});\n", "import { Router, Request, Response } from 'express';\nimport { Config } from '@backstage/config';\nimport { AuthService } from '@backstage/backend-plugin-api';\nimport { LiteLLMClient } from './client';\nimport {\n UserInfo,\n VirtualKey,\n ModelInfo,\n UsageMetrics,\n TeamInfo,\n GenerateKeyRequest,\n GenerateKeyResponse,\n ProvisioningDefaults,\n} from './types';\n\nexport interface RouterOptions {\n config: Config;\n logger: any;\n auth: AuthService;\n}\n\n/**\n * Reads the provisioning block from config, applying safe defaults for every\n * field so the feature works out-of-the-box without any YAML required.\n *\n * Safe defaults rationale:\n * maxBudget: $10 \u2014 prevents runaway spend on a forgotten test account\n * budgetDuration: 30d \u2014 monthly reset, aligns with typical billing cycles\n * models: [] \u2014 empty means all proxy models are allowed;\n * restrict here or at team level for tighter control\n * teams: [] \u2014 no automatic team assignment; add IDs to enrol users\n * tpmLimit: none \u2014 LiteLLM global / team limits still apply\n * rpmLimit: none \u2014 same\n * metadata: backstage source tag only\n */\nfunction readProvisioningDefaults(config: Config): { enabled: boolean; defaults: ProvisioningDefaults } {\n const enabled = config.getOptionalBoolean('litellm.provisioning.enabled') ?? false;\n const defaults: ProvisioningDefaults = {\n maxBudget: config.getOptionalNumber('litellm.provisioning.defaults.maxBudget') ?? 10,\n budgetDuration: config.getOptionalString('litellm.provisioning.defaults.budgetDuration') ?? '30d',\n models: config.getOptionalStringArray('litellm.provisioning.defaults.models') ?? [],\n teams: config.getOptionalStringArray('litellm.provisioning.defaults.teams') ?? [],\n tpmLimit: config.getOptionalNumber('litellm.provisioning.defaults.tpmLimit'),\n rpmLimit: config.getOptionalNumber('litellm.provisioning.defaults.rpmLimit'),\n metadata: (config.getOptional<Record<string, string>>('litellm.provisioning.defaults.metadata') ?? {}),\n };\n return { enabled, defaults };\n}\n\n/**\n * Extracts the authenticated Backstage user identity from the request token.\n * Returns the userEntityRef (e.g. \"user:default/john.doe\") or undefined when\n * the request carries no user credential (service-to-service calls).\n */\nasync function resolveUserId(req: Request, auth: AuthService): Promise<string | undefined> {\n const rawToken = req.headers.authorization?.slice(7);\n if (!rawToken) return undefined;\n try {\n const credentials = await auth.authenticate(rawToken);\n const principal = credentials.principal as any;\n if (principal?.type === 'user') {\n return principal.userEntityRef as string;\n }\n } catch {\n // invalid or service token \u2014 caller gets query-param fallback\n }\n return undefined;\n}\n\n/**\n * Creates a LiteLLM user for the given Backstage identity using the configured\n * defaults. Returns the UserInfo of the newly created account.\n */\nasync function provisionUser(\n client: LiteLLMClient,\n userId: string,\n defaults: ProvisioningDefaults,\n logger: any,\n): Promise<UserInfo | null> {\n const payload = {\n user_id: userId,\n max_budget: defaults.maxBudget,\n budget_duration: defaults.budgetDuration,\n models: defaults.models,\n teams: defaults.teams,\n ...(defaults.tpmLimit !== undefined && { tpm_limit: defaults.tpmLimit }),\n ...(defaults.rpmLimit !== undefined && { rpm_limit: defaults.rpmLimit }),\n metadata: {\n ...defaults.metadata,\n provisioned_by: 'backstage',\n provisioned_at: new Date().toISOString(),\n backstage_entity: userId,\n },\n };\n\n logger.info(`Provisioning new LiteLLM user for Backstage identity: ${userId}`);\n try {\n await client.createUser(payload);\n // Fetch the freshly-created user record to return consistent UserInfo shape\n return await client.getUserInfo(userId);\n } catch (err: any) {\n logger.error(`Failed to provision LiteLLM user ${userId}: ${err.message}`);\n return null;\n }\n}\n\nexport async function createRouter(options: RouterOptions): Promise<Router> {\n const { config, logger, auth } = options;\n\n const baseUrl = config.getString('litellm.baseUrl');\n const masterKey = config.getString('litellm.masterKey');\n const client = new LiteLLMClient({ baseUrl, masterKey });\n const { enabled: provisioningEnabled, defaults: provisioningDefaults } = readProvisioningDefaults(config);\n\n if (provisioningEnabled) {\n logger.info(\n `LiteLLM auto-provisioning enabled \u2014 defaults: budget=$${provisioningDefaults.maxBudget}/${provisioningDefaults.budgetDuration}, models=${provisioningDefaults.models.length ? provisioningDefaults.models.join(',') : 'all'}, teams=[${provisioningDefaults.teams.join(',')}]`,\n );\n }\n\n const router = Router();\n\n router.get('/health', (_req: Request, res: Response) => {\n res.json({ status: 'ok', provisioning: provisioningEnabled });\n });\n\n router.get('/user/info', async (req: Request, res: Response) => {\n try {\n const tokenUserId = await resolveUserId(req, auth);\n const userId = tokenUserId ?? (req.query.user_id as string | undefined);\n\n let userInfo: UserInfo | null = await client.getUserInfo(userId);\n\n if (!userInfo) {\n if (provisioningEnabled && userId) {\n userInfo = await provisionUser(client, userId, provisioningDefaults, logger);\n }\n\n if (!userInfo) {\n res.status(404).json({\n error: 'User not found in LiteLLM',\n provisioning: provisioningEnabled,\n hint: provisioningEnabled\n ? 'Provisioning attempted but failed \u2014 check LiteLLM logs'\n : 'Enable litellm.provisioning.enabled in app-config.yaml or create the user manually',\n });\n return;\n }\n }\n\n res.json(userInfo);\n } catch (error: any) {\n logger.error('Failed to fetch user info', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.get('/keys', async (req: Request, res: Response) => {\n try {\n const tokenUserId = await resolveUserId(req, auth);\n const userId = tokenUserId ?? (req.query.user_id as string | undefined);\n const keys: VirtualKey[] = await client.listKeys(userId);\n res.json(keys);\n } catch (error: any) {\n logger.error('Failed to list keys', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.post('/keys/generate', async (req: Request, res: Response) => {\n try {\n const tokenUserId = await resolveUserId(req, auth);\n const request: GenerateKeyRequest = {\n ...req.body,\n ...(tokenUserId && { user_id: tokenUserId }),\n };\n const result: GenerateKeyResponse = await client.generateKey(request);\n res.json(result);\n } catch (error: any) {\n logger.error('Failed to generate key', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.delete('/keys/:keyId', async (req: Request, res: Response) => {\n try {\n const { keyId } = req.params;\n if (!keyId) {\n res.status(400).json({ error: 'keyId is required' });\n return;\n }\n await client.deleteKeys({ keys: [keyId] });\n res.json({ success: true });\n } catch (error: any) {\n logger.error('Failed to delete key', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.get('/models', async (_req: Request, res: Response) => {\n try {\n const models: ModelInfo[] = await client.listModels();\n res.json(models);\n } catch (error: any) {\n logger.error('Failed to list models', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.get('/teams', async (req: Request, res: Response) => {\n try {\n const tokenUserId = await resolveUserId(req, auth);\n const userId = tokenUserId ?? (req.query.user_id as string | undefined);\n const userInfo: UserInfo | null = await client.getUserInfo(userId);\n\n if (!userInfo?.teams?.length) {\n res.json([]);\n return;\n }\n\n const teams = await Promise.all(\n userInfo.teams.map(teamId =>\n client.getTeamInfo(teamId).catch(err => {\n logger.warn(`Failed to fetch team ${teamId}: ${err.message}`);\n return null;\n }),\n ),\n );\n res.json(teams.filter(Boolean) as TeamInfo[]);\n } catch (error: any) {\n logger.error('Failed to fetch teams', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.get('/teams/:teamId/usage', async (req: Request, res: Response) => {\n try {\n const { teamId } = req.params;\n const { start_date, end_date } = req.query;\n if (!start_date || !end_date) {\n res.status(400).json({ error: 'start_date and end_date are required' });\n return;\n }\n const usage: UsageMetrics = await client.getTeamUsage(\n teamId,\n start_date as string,\n end_date as string,\n );\n res.json(usage);\n } catch (error: any) {\n logger.error('Failed to fetch team usage', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.get('/usage', async (req: Request, res: Response) => {\n try {\n const { start_date, end_date, group_by } = req.query;\n if (!start_date || !end_date) {\n res.status(400).json({ error: 'start_date and end_date are required' });\n return;\n }\n const tokenUserId = await resolveUserId(req, auth);\n const userId = tokenUserId ?? (req.query.user_id as string | undefined);\n const usage: UsageMetrics = await client.getUsage(\n start_date as string,\n end_date as string,\n userId,\n group_by as string | undefined,\n );\n res.json(usage);\n } catch (error: any) {\n logger.error('Failed to fetch usage', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n return router;\n}\n", "import {\n LiteLLMConfig,\n UserInfo,\n VirtualKey,\n ModelInfo,\n UsageMetrics,\n TeamInfo,\n GenerateKeyRequest,\n GenerateKeyResponse,\n DeleteKeyRequest,\n CreateUserRequest,\n CreateUserResponse,\n} from './types';\n\nconst DEFAULT_TIMEOUT = 30000;\n\nexport class LiteLLMClient {\n private baseUrl: string;\n private masterKey: string;\n private timeout: number;\n\n constructor(config: LiteLLMConfig, timeout = DEFAULT_TIMEOUT) {\n this.baseUrl = config.baseUrl.replace(/\\/$/, '');\n this.masterKey = config.masterKey;\n this.timeout = timeout;\n }\n\n private async request<T>(path: string, options: RequestInit = {}): Promise<T> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const response = await fetch(`${this.baseUrl}${path}`, {\n ...options,\n signal: controller.signal,\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${this.masterKey}`,\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const errorBody = await response.text();\n const err = new Error(`LiteLLM API error: ${response.status} ${response.statusText} - ${errorBody}`);\n (err as any).status = response.status;\n throw err;\n }\n\n return response.json();\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n /**\n * Returns null when the user is not found in LiteLLM (404).\n * Throws on all other errors so callers know something went wrong.\n */\n async getUserInfo(userId?: string): Promise<UserInfo | null> {\n const query = userId ? `?user_id=${encodeURIComponent(userId)}` : '';\n try {\n return await this.request<UserInfo>(`/user/info${query}`);\n } catch (err: any) {\n if (err.status === 404) return null;\n throw err;\n }\n }\n\n async createUser(payload: CreateUserRequest): Promise<CreateUserResponse> {\n return this.request<CreateUserResponse>('/user/new', {\n method: 'POST',\n body: JSON.stringify(payload),\n });\n }\n\n async listKeys(userId?: string): Promise<VirtualKey[]> {\n const query = userId ? `?user_id=${encodeURIComponent(userId)}` : '';\n const response = await this.request<{ info: VirtualKey[] } | VirtualKey[]>(`/key/info${query}`);\n return Array.isArray(response) ? response : (response.info ?? []);\n }\n\n async generateKey(request: GenerateKeyRequest): Promise<GenerateKeyResponse> {\n return this.request<GenerateKeyResponse>('/key/generate', {\n method: 'POST',\n body: JSON.stringify(request),\n });\n }\n\n async deleteKeys(request: DeleteKeyRequest): Promise<{ success: boolean }> {\n return this.request<{ success: boolean }>('/key/delete', {\n method: 'POST',\n body: JSON.stringify(request),\n });\n }\n\n async listModels(): Promise<ModelInfo[]> {\n const response = await this.request<{ data: ModelInfo[] } | ModelInfo[]>('/models');\n return Array.isArray(response) ? response : (response.data ?? []);\n }\n\n async getTeamInfo(teamId: string): Promise<TeamInfo> {\n return this.request<TeamInfo>(`/team/info?team_id=${encodeURIComponent(teamId)}`);\n }\n\n async getUsage(startDate: string, endDate: string, userId?: string, groupBy?: string): Promise<UsageMetrics> {\n const params = new URLSearchParams({ start_date: startDate, end_date: endDate });\n if (userId) params.append('user_id', userId);\n if (groupBy) params.append('group_by', groupBy);\n return this.request<UsageMetrics>(`/usage/keys?${params.toString()}`);\n }\n\n async getTeamUsage(teamId: string, startDate: string, endDate: string): Promise<UsageMetrics> {\n const params = new URLSearchParams({ start_date: startDate, end_date: endDate, team_id: teamId });\n return this.request<UsageMetrics>(`/usage/keys?${params.toString()}`);\n }\n}\n"],
5
- "mappings": "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",
6
- "names": []
7
- }