@acarmisc/backstage-plugin-litellm-backend 0.2.1 → 0.2.2

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
@@ -8,6 +8,11 @@ export declare class LiteLLMClient {
8
8
  /**
9
9
  * Returns null when the user is not found in LiteLLM (404).
10
10
  * Throws on all other errors so callers know something went wrong.
11
+ *
12
+ * LiteLLM's `/user/info` wraps the user row inside `user_info` and returns
13
+ * `teams` as an array of full team objects, not team_id strings. We flatten
14
+ * `user_info` onto the top level and reduce `teams` to a string[] of ids so
15
+ * the rest of the code can rely on the UserInfo contract.
11
16
  */
12
17
  getUserInfo(userId?: string): Promise<UserInfo | null>;
13
18
  createUser(payload: CreateUserRequest): Promise<CreateUserResponse>;
@@ -48,6 +53,16 @@ export declare class LiteLLMClient {
48
53
  deleteKeys(request: DeleteKeyRequest): Promise<{
49
54
  success: boolean;
50
55
  }>;
56
+ /**
57
+ * Returns the proxy's model catalogue normalised to the ModelInfo shape.
58
+ *
59
+ * Prefers `/model/info` which exposes `model_name`, `mode`, capability flags
60
+ * and per-token costs. Falls back to OpenAI-compatible `/models` (which only
61
+ * returns `{id}`) so the dropdown still works on installs where `/model/info`
62
+ * isn't reachable. Without this normalisation the UI saw blank labels and
63
+ * a single "other" group because `/models` doesn't populate `model_name`
64
+ * or `mode`.
65
+ */
51
66
  listModels(): Promise<ModelInfo[]>;
52
67
  getTeamInfo(teamId: string): Promise<TeamInfo>;
53
68
  private emptyUsage;
package/dist/client.js CHANGED
@@ -36,11 +36,34 @@ class LiteLLMClient {
36
36
  /**
37
37
  * Returns null when the user is not found in LiteLLM (404).
38
38
  * Throws on all other errors so callers know something went wrong.
39
+ *
40
+ * LiteLLM's `/user/info` wraps the user row inside `user_info` and returns
41
+ * `teams` as an array of full team objects, not team_id strings. We flatten
42
+ * `user_info` onto the top level and reduce `teams` to a string[] of ids so
43
+ * the rest of the code can rely on the UserInfo contract.
39
44
  */
40
45
  async getUserInfo(userId) {
41
46
  const query = userId ? `?user_id=${encodeURIComponent(userId)}` : '';
42
47
  try {
43
- return await this.request(`/user/info${query}`);
48
+ const raw = await this.request(`/user/info${query}`);
49
+ const inner = raw?.user_info ?? {};
50
+ const teamIds = Array.isArray(raw?.teams)
51
+ ? raw.teams
52
+ .map((t) => (typeof t === 'string' ? t : t?.team_id))
53
+ .filter((t) => typeof t === 'string')
54
+ : [];
55
+ return {
56
+ user_id: raw?.user_id ?? inner.user_id ?? userId ?? '',
57
+ user_email: inner.user_email ?? raw?.user_email,
58
+ email: inner.email ?? raw?.email,
59
+ teams: teamIds,
60
+ models: inner.models ?? raw?.models,
61
+ max_budget: inner.max_budget ?? raw?.max_budget,
62
+ spend: inner.spend ?? raw?.spend,
63
+ current_spend: inner.current_spend ?? raw?.current_spend,
64
+ soft_limit: inner.soft_limit ?? raw?.soft_limit,
65
+ hard_limit: inner.hard_limit ?? raw?.hard_limit,
66
+ };
44
67
  }
45
68
  catch (err) {
46
69
  if (err.status === 404)
@@ -143,9 +166,53 @@ class LiteLLMClient {
143
166
  body: JSON.stringify(request),
144
167
  });
145
168
  }
169
+ /**
170
+ * Returns the proxy's model catalogue normalised to the ModelInfo shape.
171
+ *
172
+ * Prefers `/model/info` which exposes `model_name`, `mode`, capability flags
173
+ * and per-token costs. Falls back to OpenAI-compatible `/models` (which only
174
+ * returns `{id}`) so the dropdown still works on installs where `/model/info`
175
+ * isn't reachable. Without this normalisation the UI saw blank labels and
176
+ * a single "other" group because `/models` doesn't populate `model_name`
177
+ * or `mode`.
178
+ */
146
179
  async listModels() {
147
- const response = await this.request('/models');
148
- return Array.isArray(response) ? response : response.data ?? [];
180
+ try {
181
+ const response = await this.request('/model/info');
182
+ const data = Array.isArray(response?.data) ? response.data : [];
183
+ const normalised = data.map((m) => {
184
+ const info = m.model_info ?? {};
185
+ const params = m.litellm_params ?? {};
186
+ return {
187
+ model_name: m.model_name ?? params.model ?? info.id ?? '',
188
+ mode: info.mode ?? m.mode ?? 'chat',
189
+ supports_function_calling: info.supports_function_calling ?? m.supports_function_calling,
190
+ supports_vision: info.supports_vision ?? m.supports_vision,
191
+ input_cost_per_token: info.input_cost_per_token ?? params.input_cost_per_token,
192
+ output_cost_per_token: info.output_cost_per_token ?? params.output_cost_per_token,
193
+ };
194
+ });
195
+ const filtered = normalised.filter(m => m.model_name);
196
+ if (filtered.length)
197
+ return filtered;
198
+ }
199
+ catch {
200
+ // fall through to /models
201
+ }
202
+ const fallback = await this.request('/models');
203
+ const data = Array.isArray(fallback)
204
+ ? fallback
205
+ : Array.isArray(fallback?.data)
206
+ ? fallback.data
207
+ : [];
208
+ return data
209
+ .map((m) => ({
210
+ model_name: m.model_name ?? m.id ?? '',
211
+ mode: m.mode ?? 'chat',
212
+ supports_function_calling: m.supports_function_calling,
213
+ supports_vision: m.supports_vision,
214
+ }))
215
+ .filter((m) => m.model_name);
149
216
  }
150
217
  async getTeamInfo(teamId) {
151
218
  return this.request(`/team/info?team_id=${encodeURIComponent(teamId)}`);
package/dist/index.cjs.js CHANGED
@@ -81,11 +81,30 @@ var LiteLLMClient = class {
81
81
  /**
82
82
  * Returns null when the user is not found in LiteLLM (404).
83
83
  * Throws on all other errors so callers know something went wrong.
84
+ *
85
+ * LiteLLM's `/user/info` wraps the user row inside `user_info` and returns
86
+ * `teams` as an array of full team objects, not team_id strings. We flatten
87
+ * `user_info` onto the top level and reduce `teams` to a string[] of ids so
88
+ * the rest of the code can rely on the UserInfo contract.
84
89
  */
85
90
  async getUserInfo(userId) {
86
91
  const query = userId ? `?user_id=${encodeURIComponent(userId)}` : "";
87
92
  try {
88
- return await this.request(`/user/info${query}`);
93
+ const raw = await this.request(`/user/info${query}`);
94
+ const inner = raw?.user_info ?? {};
95
+ const teamIds = Array.isArray(raw?.teams) ? raw.teams.map((t) => typeof t === "string" ? t : t?.team_id).filter((t) => typeof t === "string") : [];
96
+ return {
97
+ user_id: raw?.user_id ?? inner.user_id ?? userId ?? "",
98
+ user_email: inner.user_email ?? raw?.user_email,
99
+ email: inner.email ?? raw?.email,
100
+ teams: teamIds,
101
+ models: inner.models ?? raw?.models,
102
+ max_budget: inner.max_budget ?? raw?.max_budget,
103
+ spend: inner.spend ?? raw?.spend,
104
+ current_spend: inner.current_spend ?? raw?.current_spend,
105
+ soft_limit: inner.soft_limit ?? raw?.soft_limit,
106
+ hard_limit: inner.hard_limit ?? raw?.hard_limit
107
+ };
89
108
  } catch (err) {
90
109
  if (err.status === 404) return null;
91
110
  throw err;
@@ -186,11 +205,44 @@ var LiteLLMClient = class {
186
205
  body: JSON.stringify(request)
187
206
  });
188
207
  }
208
+ /**
209
+ * Returns the proxy's model catalogue normalised to the ModelInfo shape.
210
+ *
211
+ * Prefers `/model/info` which exposes `model_name`, `mode`, capability flags
212
+ * and per-token costs. Falls back to OpenAI-compatible `/models` (which only
213
+ * returns `{id}`) so the dropdown still works on installs where `/model/info`
214
+ * isn't reachable. Without this normalisation the UI saw blank labels and
215
+ * a single "other" group because `/models` doesn't populate `model_name`
216
+ * or `mode`.
217
+ */
189
218
  async listModels() {
190
- const response = await this.request(
191
- "/models"
192
- );
193
- return Array.isArray(response) ? response : response.data ?? [];
219
+ try {
220
+ const response = await this.request("/model/info");
221
+ const data2 = Array.isArray(response?.data) ? response.data : [];
222
+ const normalised = data2.map((m) => {
223
+ const info = m.model_info ?? {};
224
+ const params = m.litellm_params ?? {};
225
+ return {
226
+ model_name: m.model_name ?? params.model ?? info.id ?? "",
227
+ mode: info.mode ?? m.mode ?? "chat",
228
+ supports_function_calling: info.supports_function_calling ?? m.supports_function_calling,
229
+ supports_vision: info.supports_vision ?? m.supports_vision,
230
+ input_cost_per_token: info.input_cost_per_token ?? params.input_cost_per_token,
231
+ output_cost_per_token: info.output_cost_per_token ?? params.output_cost_per_token
232
+ };
233
+ });
234
+ const filtered = normalised.filter((m) => m.model_name);
235
+ if (filtered.length) return filtered;
236
+ } catch {
237
+ }
238
+ const fallback = await this.request("/models");
239
+ const data = Array.isArray(fallback) ? fallback : Array.isArray(fallback?.data) ? fallback.data : [];
240
+ return data.map((m) => ({
241
+ model_name: m.model_name ?? m.id ?? "",
242
+ mode: m.mode ?? "chat",
243
+ supports_function_calling: m.supports_function_calling,
244
+ supports_vision: m.supports_vision
245
+ })).filter((m) => m.model_name);
194
246
  }
195
247
  async getTeamInfo(teamId) {
196
248
  return this.request(
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts", "../src/plugin.ts", "../src/router.ts", "../src/client.ts", "../src/provisioning.ts"],
4
- "sourcesContent": ["export { litellmPlugin } from './plugin';\nexport { litellmPlugin as default } from './plugin';\nexport { createRouter } from './router';\nexport * from './types';\nexport { LiteLLMClient } from './client';\n", "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, discovery }) {\n const router = await createRouter({ config, logger, auth, discovery });\n httpRouter.use(router);\n },\n });\n },\n});\n", "import express, { Router, Request, Response } from 'express';\nimport { Config } from '@backstage/config';\nimport { AuthService, DiscoveryService } from '@backstage/backend-plugin-api';\nimport { CatalogClient } from '@backstage/catalog-client';\nimport { LiteLLMClient } from './client';\nimport {\n VirtualKey,\n ModelInfo,\n UsageMetrics,\n TeamInfo,\n GenerateKeyRequest,\n GenerateKeyResponse,\n UpdateKeyRequest,\n} from './types';\nimport {\n toLiteLLMUserId,\n resolveUserId,\n resolveUserProfile,\n getOrProvisionUser,\n readProvisioningDefaults,\n readRoleConfigs,\n ProvisioningError,\n} from './provisioning';\n\nexport { ProvisioningError };\n\nexport interface RouterOptions {\n config: Config;\n logger: any;\n auth: AuthService;\n discovery: DiscoveryService;\n}\n\nexport async function createRouter(options: RouterOptions): Promise<Router> {\n const { config, logger, auth, discovery } = options;\n\n const baseUrl = config.getString('litellm.baseUrl');\n const masterKey = config.getString('litellm.masterKey');\n const userIdDomain = config.getOptionalString('litellm.userIdDomain');\n const client = new LiteLLMClient({ baseUrl, masterKey });\n const { enabled: provisioningEnabled, defaults: provisioningDefaults } =\n readProvisioningDefaults(config);\n const roleConfigs = readRoleConfigs(config);\n const catalogClient = new CatalogClient({ discoveryApi: discovery });\n\n if (provisioningEnabled) {\n logger.info(\n `LiteLLM auto-provisioning enabled \u2014 defaults: budget=$${\n provisioningDefaults.maxBudget\n }/${provisioningDefaults.budgetDuration}, models=${\n provisioningDefaults.models.length\n ? provisioningDefaults.models.join(',')\n : 'all'\n }, teams=[${provisioningDefaults.teams.join(',')}]`,\n );\n }\n\n const router = Router();\n // JSON body parser. Without this, every POST/PUT endpoint sees an empty\n // req.body. Backstage's httpRouter does not apply a body parser at the\n // plugin-router level, so each plugin must attach its own.\n router.use(express.json());\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 tokenEntityRef = await resolveUserId(req, auth);\n const userId = tokenEntityRef\n ? toLiteLLMUserId(tokenEntityRef, userIdDomain)\n : (req.query.user_id as string | undefined);\n\n const userInfo = await getOrProvisionUser(\n client,\n tokenEntityRef,\n userId,\n provisioningEnabled,\n provisioningDefaults,\n roleConfigs,\n catalogClient,\n auth,\n logger,\n );\n res.json(userInfo);\n } catch (error: any) {\n if (error instanceof ProvisioningError) {\n res.status(error.status).json(error.body);\n return;\n }\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 tokenEntityRef = await resolveUserId(req, auth);\n const userId = tokenEntityRef\n ? toLiteLLMUserId(tokenEntityRef, userIdDomain)\n : (req.query.user_id as string | undefined);\n\n await getOrProvisionUser(\n client,\n tokenEntityRef,\n userId,\n provisioningEnabled,\n provisioningDefaults,\n roleConfigs,\n catalogClient,\n auth,\n logger,\n );\n\n const keys: VirtualKey[] = await client.listKeys(userId);\n res.json(keys);\n } catch (error: any) {\n if (error instanceof ProvisioningError) {\n res.status(error.status).json(error.body);\n return;\n }\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 // Only alias + max_budget are required. An empty models array is\n // intentional \u2014 in LiteLLM `models: []` means \"all models the user\n // can access\" which is the desired default. Forcing a selection\n // up front is too restrictive for the common case.\n const body = (req.body ?? {}) as GenerateKeyRequest;\n const missing: string[] = [];\n if (!body.alias?.trim()) missing.push('alias');\n if (typeof body.max_budget !== 'number' || body.max_budget <= 0) {\n missing.push('max_budget (positive number)');\n }\n if (missing.length) {\n res.status(400).json({\n error: 'Missing required fields',\n hint: `Required: ${missing.join(', ')}`,\n });\n return;\n }\n\n const tokenEntityRef = await resolveUserId(req, auth);\n const resolvedUserId = tokenEntityRef\n ? toLiteLLMUserId(tokenEntityRef, userIdDomain)\n : undefined;\n\n if (resolvedUserId) {\n await getOrProvisionUser(\n client,\n tokenEntityRef,\n resolvedUserId,\n provisioningEnabled,\n provisioningDefaults,\n roleConfigs,\n catalogClient,\n auth,\n logger,\n );\n }\n\n // Stamp ownership into LiteLLM key metadata. LiteLLM's native\n // `created_by` column is only populated when the caller authenticates\n // via JWT/SSO; we always call with the master key, so that column\n // stays null. Enriching `metadata` makes the owner identity visible\n // in LiteLLM's UI and queryable via API.\n const profile = tokenEntityRef\n ? await resolveUserProfile(tokenEntityRef, catalogClient, auth, logger)\n : {};\n const enrichedMetadata = {\n ...(body.metadata ?? {}),\n created_by_backstage_user: tokenEntityRef ?? 'unknown',\n ...(profile.email && { created_by_email: profile.email }),\n ...(profile.displayName && {\n created_by_display_name: profile.displayName,\n }),\n created_via: 'backstage',\n created_at_iso: new Date().toISOString(),\n };\n\n const request: GenerateKeyRequest = {\n ...body,\n metadata: enrichedMetadata,\n ...(resolvedUserId && { user_id: resolvedUserId }),\n };\n const result: GenerateKeyResponse = await client.generateKey(request);\n res.json(result);\n } catch (error: any) {\n if (error instanceof ProvisioningError) {\n res.status(error.status).json(error.body);\n return;\n }\n logger.error('Failed to generate key', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.post('/keys/:keyId/update', 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 const request: UpdateKeyRequest = { ...req.body, key: keyId };\n const result = await client.updateKey(request);\n res.json(result);\n } catch (error: any) {\n logger.error('Failed to update 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 tokenEntityRef = await resolveUserId(req, auth);\n const userId = tokenEntityRef\n ? toLiteLLMUserId(tokenEntityRef, userIdDomain)\n : (req.query.user_id as string | undefined);\n\n const userInfo = await getOrProvisionUser(\n client,\n tokenEntityRef,\n userId,\n provisioningEnabled,\n provisioningDefaults,\n roleConfigs,\n catalogClient,\n auth,\n logger,\n );\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 if (error instanceof ProvisioningError) {\n res.status(error.status).json(error.body);\n return;\n }\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 tokenEntityRef = await resolveUserId(req, auth);\n const userId = tokenEntityRef\n ? toLiteLLMUserId(tokenEntityRef, userIdDomain)\n : (req.query.user_id as string | undefined);\n\n if (userId) {\n await getOrProvisionUser(\n client,\n tokenEntityRef,\n userId,\n provisioningEnabled,\n provisioningDefaults,\n roleConfigs,\n catalogClient,\n auth,\n logger,\n );\n }\n\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 if (error instanceof ProvisioningError) {\n res.status(error.status).json(error.body);\n return;\n }\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 LiteLLMUserKey,\n ModelInfo,\n UsageMetrics,\n TeamInfo,\n GenerateKeyRequest,\n GenerateKeyResponse,\n UpdateKeyRequest,\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>(\n path: string,\n options: RequestInit = {},\n ): 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(\n `LiteLLM API error: ${response.status} ${response.statusText} - ${errorBody}`,\n );\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 /**\n * Updates an existing LiteLLM user record. Used as a defensive follow-up\n * after /user/new because the upsert path of /user/new has been observed\n * to silently drop fields like user_role under concurrent inserts.\n */\n async updateUser(\n payload: Partial<CreateUserRequest> & { user_id: string },\n ): Promise<unknown> {\n return this.request<unknown>('/user/update', {\n method: 'POST',\n body: JSON.stringify(payload),\n });\n }\n\n /**\n * Returns the keys belonging to a user.\n *\n * Implementation note: LiteLLM's `/key/info` endpoint requires a `key`\n * hash and returns 404 when only `user_id` is passed. The correct way\n * to enumerate a user's keys is `/user/info?user_id=X`, which embeds\n * a `keys` array with per-key metadata. We unwrap that array and\n * normalise field names to match the frontend VirtualKey shape\n * (LiteLLM exposes `key_name` for the masked display value and\n * `expires` instead of `expires_at`).\n */\n async listKeys(userId?: string): Promise<VirtualKey[]> {\n if (!userId) return [];\n try {\n const response = await this.request<{ keys?: LiteLLMUserKey[] }>(\n `/user/info?user_id=${encodeURIComponent(userId)}`,\n );\n const rawKeys = response.keys ?? [];\n return rawKeys.map(this.toVirtualKey);\n } catch (err: any) {\n if (err.status === 404 || err.message.includes('not found')) {\n return [];\n }\n throw err;\n }\n }\n\n private toVirtualKey(k: LiteLLMUserKey): VirtualKey {\n return {\n // The hashed `token` never leaves LiteLLM in a usable form; the\n // masked `key_name` (\"sk-...XXXX\") is what the UI displays. Fall\n // back to `token` only when `key_name` is missing.\n key: k.key_name ?? k.token,\n token: k.token,\n key_alias: k.key_alias ?? undefined,\n created_at: k.created_at,\n expires_at: k.expires ?? undefined,\n spend: k.spend ?? 0,\n max_budget: k.max_budget ?? undefined,\n tpm_limit: k.tpm_limit ?? undefined,\n rpm_limit: k.rpm_limit ?? undefined,\n models: k.models ?? [],\n user_id: k.user_id ?? undefined,\n };\n }\n\n /**\n * Creates a new virtual key on the LiteLLM proxy.\n *\n * Implementation notes \u2014 both required to avoid silently-empty keys:\n * 1. The body must be the plain payload. An earlier version wrapped\n * it as `{ json: request }`; LiteLLM doesn't unwrap that envelope\n * and treats the request as having no fields, returning a key\n * with null alias / models / budget / limits.\n * 2. LiteLLM expects `key_alias`, not `alias`. Without the rename,\n * the alias the user typed is dropped on the floor.\n */\n async generateKey(request: GenerateKeyRequest): Promise<GenerateKeyResponse> {\n const { alias, ...rest } = request;\n const payload = {\n ...rest,\n ...(alias && { key_alias: alias }),\n };\n return this.request<GenerateKeyResponse>('/key/generate', {\n method: 'POST',\n body: JSON.stringify(payload),\n });\n }\n\n async updateKey(request: UpdateKeyRequest): Promise<VirtualKey> {\n return this.request<VirtualKey>('/key/update', {\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[]>(\n '/models',\n );\n return Array.isArray(response) ? response : response.data ?? [];\n }\n\n async getTeamInfo(teamId: string): Promise<TeamInfo> {\n return this.request<TeamInfo>(\n `/team/info?team_id=${encodeURIComponent(teamId)}`,\n );\n }\n\n private emptyUsage(): UsageMetrics {\n return {\n total_spend: 0,\n total_tokens: 0,\n prompt_tokens: 0,\n completion_tokens: 0,\n api_requests: 0,\n successful_requests: 0,\n failed_requests: 0,\n usage_by_model: {},\n usage_by_key: {},\n daily_usage: [],\n daily_by_model: [],\n };\n }\n\n /**\n * Transforms LiteLLM's SpendAnalyticsPaginatedResponse into the flatter\n * UsageMetrics shape consumed by the frontend charts.\n *\n * Source shape (per result row):\n * { date, metrics, breakdown: { models: { [name]: { metrics, api_key_breakdown: { [keyHash]: { metrics, metadata } } } } } }\n *\n * We fan that out into three views the UI consumes:\n * - daily_usage \u2192 spend + request trends over time\n * - usage_by_model \u2192 which models drove cost / traffic\n * - usage_by_key \u2192 which keys drove cost / traffic (with key_alias + team_id from metadata)\n */\n private transformDailyActivity(response: any): UsageMetrics {\n const results: any[] = Array.isArray(response?.results)\n ? response.results\n : [];\n const meta = response?.metadata ?? {};\n\n const daily_usage = results\n .map(r => ({\n date: r.date,\n spend: r.metrics?.spend ?? 0,\n total_tokens: r.metrics?.total_tokens ?? 0,\n prompt_tokens: r.metrics?.prompt_tokens ?? 0,\n completion_tokens: r.metrics?.completion_tokens ?? 0,\n api_requests: r.metrics?.api_requests ?? 0,\n successful_requests: r.metrics?.successful_requests ?? 0,\n failed_requests: r.metrics?.failed_requests ?? 0,\n }))\n .sort((a, b) => a.date.localeCompare(b.date));\n\n const usage_by_model: UsageMetrics['usage_by_model'] = {};\n const usage_by_key: UsageMetrics['usage_by_key'] = {};\n const daily_by_model: UsageMetrics['daily_by_model'] = [];\n\n const emptyModelBucket = () => ({\n total_spend: 0,\n total_tokens: 0,\n prompt_tokens: 0,\n completion_tokens: 0,\n api_requests: 0,\n successful_requests: 0,\n failed_requests: 0,\n });\n\n for (const r of results) {\n const models = r.breakdown?.models ?? {};\n for (const [name, entry] of Object.entries<any>(models)) {\n const m = entry?.metrics ?? {};\n const bucket = usage_by_model[name] ?? emptyModelBucket();\n bucket.total_spend += m.spend ?? 0;\n bucket.total_tokens += m.total_tokens ?? 0;\n bucket.prompt_tokens += m.prompt_tokens ?? 0;\n bucket.completion_tokens += m.completion_tokens ?? 0;\n bucket.api_requests += m.api_requests ?? 0;\n bucket.successful_requests += m.successful_requests ?? 0;\n bucket.failed_requests += m.failed_requests ?? 0;\n usage_by_model[name] = bucket;\n\n daily_by_model.push({\n date: r.date,\n model: name,\n spend: m.spend ?? 0,\n prompt_tokens: m.prompt_tokens ?? 0,\n completion_tokens: m.completion_tokens ?? 0,\n total_tokens: m.total_tokens ?? 0,\n api_requests: m.api_requests ?? 0,\n successful_requests: m.successful_requests ?? 0,\n failed_requests: m.failed_requests ?? 0,\n });\n\n const keyMap = entry?.api_key_breakdown ?? {};\n for (const [keyHash, keyEntry] of Object.entries<any>(keyMap)) {\n const km = keyEntry?.metrics ?? {};\n const kmeta = keyEntry?.metadata ?? {};\n const kb = usage_by_key[keyHash] ?? {\n key_alias: kmeta.key_alias,\n team_id: kmeta.team_id ?? null,\n models: [] as string[],\n ...emptyModelBucket(),\n };\n if (!kb.key_alias && kmeta.key_alias) kb.key_alias = kmeta.key_alias;\n if (kb.team_id == null && kmeta.team_id) kb.team_id = kmeta.team_id;\n if (!kb.models.includes(name)) kb.models.push(name);\n kb.total_spend += km.spend ?? 0;\n kb.total_tokens += km.total_tokens ?? 0;\n kb.prompt_tokens += km.prompt_tokens ?? 0;\n kb.completion_tokens += km.completion_tokens ?? 0;\n kb.api_requests += km.api_requests ?? 0;\n kb.successful_requests += km.successful_requests ?? 0;\n kb.failed_requests += km.failed_requests ?? 0;\n usage_by_key[keyHash] = kb;\n }\n }\n }\n\n return {\n total_spend: meta.total_spend ?? 0,\n total_tokens: meta.total_tokens ?? 0,\n prompt_tokens: meta.total_prompt_tokens ?? 0,\n completion_tokens: meta.total_completion_tokens ?? 0,\n api_requests: meta.total_api_requests ?? 0,\n successful_requests: meta.total_successful_requests ?? 0,\n failed_requests: meta.total_failed_requests ?? 0,\n usage_by_model,\n usage_by_key,\n daily_usage,\n daily_by_model,\n };\n }\n\n async getUsage(\n startDate: string,\n endDate: string,\n userId?: string,\n _groupBy?: string,\n ): Promise<UsageMetrics> {\n const params = new URLSearchParams({\n start_date: startDate,\n end_date: endDate,\n page_size: '100',\n });\n if (userId) params.append('user_id', userId);\n try {\n const response = await this.request<any>(\n `/user/daily/activity?${params.toString()}`,\n );\n return this.transformDailyActivity(response);\n } catch (err: any) {\n if (err.status === 404 || err.message.includes('not found')) {\n return this.emptyUsage();\n }\n throw err;\n }\n }\n\n async getTeamUsage(\n teamId: string,\n startDate: string,\n endDate: string,\n ): Promise<UsageMetrics> {\n const params = new URLSearchParams({\n start_date: startDate,\n end_date: endDate,\n team_ids: teamId,\n page_size: '100',\n });\n try {\n const response = await this.request<any>(\n `/team/daily/activity?${params.toString()}`,\n );\n return this.transformDailyActivity(response);\n } catch (err: any) {\n if (err.status === 404 || err.message.includes('not found')) {\n return this.emptyUsage();\n }\n throw err;\n }\n }\n}\n", "import { Config } from '@backstage/config';\nimport { AuthService } from '@backstage/backend-plugin-api';\nimport { CatalogClient } from '@backstage/catalog-client';\nimport { Request } from 'express';\nimport { LiteLLMClient } from './client';\nimport { UserInfo, ProvisioningDefaults, RoleConfig } from './types';\n\n/**\n * Converts a Backstage user entity ref to a LiteLLM user_id.\n *\n * When userIdDomain is configured, the entity name is suffixed with the domain\n * so that LiteLLM user_ids match the organisation's email addresses:\n * \"user:default/andrea.carmisciano\" + \"abstract.it\"\n * \u2192 \"andrea.carmisciano@abstract.it\"\n *\n * Without a domain the bare entity name is returned unchanged, which works for\n * deployments where LiteLLM users were created with plain usernames.\n */\nexport function toLiteLLMUserId(\n userEntityRef: string,\n userIdDomain?: string,\n): string {\n const name = userEntityRef.split('/').pop() ?? userEntityRef;\n // Defensive: if the entity name is already email-shaped (e.g. when the\n // Keycloak provider imports usernames as full emails without our\n // name-rewrite transformer running, or when a catalog change leaves\n // an entity ref like \"user:default/foo@bar.it\"), do NOT append the\n // userIdDomain \u2014 that produced \"foo@bar.it@bar.it\" in production.\n if (name.includes('@')) return name;\n return userIdDomain ? `${name}@${userIdDomain}` : name;\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 */\nexport function readRoleConfigs(config: Config): RoleConfig[] {\n const raw = config.getOptional<any[]>('litellm.provisioning.roles');\n if (!raw?.length) return [];\n return raw.map((r: any) => ({\n group: r.group as string,\n maxBudget: r.maxBudget,\n budgetDuration: r.budgetDuration,\n models: r.models,\n teams: r.teams,\n tpmLimit: r.tpmLimit,\n rpmLimit: r.rpmLimit,\n userRole: r.userRole,\n metadata: r.metadata,\n }));\n}\n\n/**\n * Merges role config over defaults. Role fields override defaults only when explicitly set.\n */\nexport function applyRoleOverrides(\n defaults: ProvisioningDefaults,\n role: RoleConfig,\n): ProvisioningDefaults {\n return {\n maxBudget: role.maxBudget ?? defaults.maxBudget,\n budgetDuration: role.budgetDuration ?? defaults.budgetDuration,\n models: role.models ?? defaults.models,\n teams: role.teams ?? defaults.teams,\n tpmLimit: role.tpmLimit ?? defaults.tpmLimit,\n rpmLimit: role.rpmLimit ?? defaults.rpmLimit,\n userRole: role.userRole ?? defaults.userRole,\n metadata: { ...defaults.metadata, ...(role.metadata ?? {}) },\n };\n}\n\nexport function readProvisioningDefaults(config: Config): {\n enabled: boolean;\n defaults: ProvisioningDefaults;\n} {\n const enabled =\n config.getOptionalBoolean('litellm.provisioning.enabled') ?? false;\n const defaults: ProvisioningDefaults = {\n maxBudget:\n config.getOptionalNumber('litellm.provisioning.defaults.maxBudget') ?? 10,\n budgetDuration:\n config.getOptionalString(\n 'litellm.provisioning.defaults.budgetDuration',\n ) ?? '30d',\n models:\n config.getOptionalStringArray('litellm.provisioning.defaults.models') ??\n [],\n teams:\n config.getOptionalStringArray('litellm.provisioning.defaults.teams') ??\n [],\n tpmLimit: config.getOptionalNumber(\n 'litellm.provisioning.defaults.tpmLimit',\n ),\n rpmLimit: config.getOptionalNumber(\n 'litellm.provisioning.defaults.rpmLimit',\n ),\n userRole:\n config.getOptionalString('litellm.provisioning.defaults.userRole') ??\n 'internal_user',\n metadata:\n config.getOptional<Record<string, string>>(\n 'litellm.provisioning.defaults.metadata',\n ) ?? {},\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 */\nexport async function resolveUserId(\n req: Request,\n auth: AuthService,\n): 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 * Profile data extracted from a Backstage Catalog User entity, used to\n * populate user_email / user_alias on the LiteLLM record.\n */\nexport interface BackstageUserProfile {\n email?: string;\n displayName?: string;\n}\n\n/**\n * Looks up the catalog User entity for the authenticated user and returns\n * the profile block. Returns an empty object when the user has no catalog\n * entity (e.g. dangerouslyAllowSignInWithoutUserInCatalog was used) \u2014 the\n * caller falls back to deriving identity from userIdDomain.\n */\nexport async function resolveUserProfile(\n userEntityRef: string,\n catalogClient: CatalogClient,\n auth: AuthService,\n logger: any,\n): Promise<BackstageUserProfile> {\n try {\n const { token } = await auth.getPluginRequestToken({\n onBehalfOf: await auth.getOwnServiceCredentials(),\n targetPluginId: 'catalog',\n });\n const entity = await catalogClient.getEntityByRef(userEntityRef, { token });\n const profile = (entity?.spec as any)?.profile ?? {};\n return {\n email: profile.email,\n displayName: profile.displayName,\n };\n } catch (err: any) {\n logger.warn(\n `Could not fetch catalog profile for ${userEntityRef}: ${err.message}`,\n );\n return {};\n }\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 */\nexport async function provisionUser(\n client: LiteLLMClient,\n userId: string,\n defaults: ProvisioningDefaults,\n profile: BackstageUserProfile,\n backstageEntity: string | undefined,\n logger: any,\n): Promise<UserInfo | null> {\n const payload = {\n user_id: userId,\n ...(profile.email && { user_email: profile.email }),\n ...(profile.displayName && { user_alias: profile.displayName }),\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 ...(defaults.userRole && { user_role: defaults.userRole }),\n auto_create_key: false,\n metadata: {\n ...defaults.metadata,\n provisioned_by: 'backstage',\n provisioned_at: new Date().toISOString(),\n backstage_entity: backstageEntity ?? userId,\n ...(profile.email && { backstage_email: profile.email }),\n ...(profile.displayName && {\n backstage_display_name: profile.displayName,\n }),\n },\n };\n\n logger.info(\n `Provisioning new LiteLLM user for Backstage identity: ${userId}` +\n (profile.email ? ` (email=${profile.email})` : ''),\n );\n try {\n await client.createUser(payload);\n // Defensive /user/update: LiteLLM's /user/new upsert path has been\n // observed to drop user_role under concurrent inserts (the first\n // call sets the field, a racing second call upserts and clears it).\n // Re-asserting the role-bearing fields immediately after creation\n // is cheap and makes the role guarantee robust.\n if (defaults.userRole) {\n try {\n await client.updateUser({\n user_id: userId,\n user_role: defaults.userRole,\n ...(profile.email && { user_email: profile.email }),\n ...(profile.displayName && { user_alias: profile.displayName }),\n });\n } catch (updateErr: any) {\n logger.warn(\n `Defensive /user/update after provisioning ${userId} failed: ${updateErr.message}`,\n );\n }\n }\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 throw err;\n }\n}\n\n/**\n * Module-scope single-flight cache keyed by LiteLLM user_id. Coalesces\n * concurrent provisioning attempts for the same user so /user/new fires\n * at most once per user across parallel requests. Without this, an\n * authenticated page load that fires /keys, /teams and /usage in\n * parallel triggers three concurrent /user/new calls; LiteLLM's\n * upsert path then creates one default key per call (so the user lands\n * with 3 unexpected keys) and may silently lose user_role.\n *\n * Cache entries are removed once the promise settles, so subsequent\n * requests for a re-deleted user can still trigger fresh provisioning.\n */\nconst provisioningInFlight = new Map<string, Promise<UserInfo>>();\n\n/**\n * Strips any echoed Authorization bearer token from upstream LiteLLM error\n * messages before they're shipped back to the browser. LiteLLM normally does\n * not echo the master key, but defense in depth: never let a `Bearer \u2026`\n * substring travel out in a response body.\n */\nfunction sanitizeUpstreamMessage(message: string): string {\n if (!message) return 'unknown error';\n return message\n .replace(/Bearer\\s+[A-Za-z0-9._\\-+/=]+/g, 'Bearer [redacted]')\n .replace(/sk-[A-Za-z0-9_\\-]{8,}/g, 'sk-[redacted]')\n .slice(0, 500);\n}\n\nexport class ProvisioningError extends Error {\n status: number;\n body: { error: string; hint: string; provisioning: boolean };\n\n constructor(\n message: string,\n hint: string,\n provisioning: boolean,\n status = 404,\n ) {\n super(message);\n this.status = status;\n this.body = { error: message, hint, provisioning };\n }\n}\n\n/**\n * Ensures the LiteLLM user exists, returning its UserInfo.\n * When the user is missing and provisioning is enabled, attempts to create it.\n * When provisioning is disabled, throws a ProvisioningError with a clear message.\n */\nexport async function getOrProvisionUser(\n client: LiteLLMClient,\n tokenEntityRef: string | undefined,\n userId: string | undefined,\n provisioningEnabled: boolean,\n provisioningDefaults: ProvisioningDefaults,\n roleConfigs: RoleConfig[],\n catalogClient: CatalogClient,\n auth: AuthService,\n logger: any,\n): Promise<UserInfo> {\n if (!userId) {\n throw new ProvisioningError(\n 'User not found in LiteLLM',\n 'No user identity could be resolved from the request.',\n provisioningEnabled,\n );\n }\n\n const existing = await client.getUserInfo(userId);\n if (existing) {\n return existing;\n }\n\n if (!provisioningEnabled) {\n throw new ProvisioningError(\n 'User not found in LiteLLM',\n 'Enable litellm.provisioning.enabled in app-config.yaml or create the user manually',\n false,\n );\n }\n\n // Single-flight: if another request for the same userId is already\n // provisioning, await its result instead of starting a new /user/new.\n // This collapses the /keys + /teams + /usage page-load thundering\n // herd into a single LiteLLM round-trip.\n const pending = provisioningInFlight.get(userId);\n if (pending) {\n logger.info(\n `LiteLLM provisioning already in flight for ${userId} \u2014 joining`,\n );\n return pending;\n }\n\n const provisionPromise = (async () => {\n const catalogRef = tokenEntityRef ?? userId;\n const [matchedRole, profile] = await Promise.all([\n resolveUserRole(catalogRef, roleConfigs, catalogClient, auth, logger),\n tokenEntityRef\n ? resolveUserProfile(tokenEntityRef, catalogClient, auth, logger)\n : Promise.resolve<BackstageUserProfile>({}),\n ]);\n const effectiveDefaults = matchedRole\n ? applyRoleOverrides(provisioningDefaults, matchedRole)\n : provisioningDefaults;\n if (matchedRole) {\n logger.info(\n `User ${userId} matched role group ${matchedRole.group} \u2014 using role-specific provisioning`,\n );\n }\n try {\n const created = await provisionUser(\n client,\n userId,\n effectiveDefaults,\n profile,\n tokenEntityRef,\n logger,\n );\n if (!created) {\n throw new ProvisioningError(\n 'User not found in LiteLLM',\n 'Provisioning attempted but returned no user \u2014 check LiteLLM logs',\n true,\n );\n }\n return created;\n } catch (err: any) {\n // The single-flight cache should prevent the parallel-409 race,\n // but keep the recovery path: if /user/new still 409s (e.g.\n // multi-replica deploys where the lock is per-process), treat\n // it as \"user exists\" and re-fetch.\n if (err.status === 409 || /already exists/i.test(err.message ?? '')) {\n logger.info(\n `LiteLLM user ${userId} already exists during provisioning \u2014 re-fetching`,\n );\n const refetched = await client.getUserInfo(userId);\n if (refetched) {\n return refetched;\n }\n }\n if (err instanceof ProvisioningError) {\n throw err;\n }\n // Map upstream LiteLLM status to a Backstage-safe gateway status.\n // 401/403/5xx from LiteLLM mean the gateway (this plugin) cannot\n // talk to LiteLLM \u2014 they MUST NOT propagate as 401/403 to the\n // browser, otherwise Backstage's fetch middleware treats the\n // user's Backstage session as expired and forces a re-login.\n // Only safe client-semantic codes pass through.\n const upstreamStatus = err.status;\n const passThrough = [400, 404, 409, 422].includes(upstreamStatus)\n ? upstreamStatus\n : 502;\n throw new ProvisioningError(\n 'LiteLLM auto-provisioning failed',\n `LiteLLM upstream ${\n upstreamStatus ?? 'error'\n }: ${sanitizeUpstreamMessage(err.message)}`,\n true,\n passThrough,\n );\n }\n })();\n\n provisioningInFlight.set(userId, provisionPromise);\n try {\n return await provisionPromise;\n } finally {\n provisioningInFlight.delete(userId);\n }\n}\n\n/**\n * Fetches the user's Backstage group memberships and returns the first matching\n * role config (priority order), or undefined when no role matches.\n */\nexport async function resolveUserRole(\n userEntityRef: string,\n roleConfigs: RoleConfig[],\n catalogClient: CatalogClient,\n auth: AuthService,\n logger: any,\n): Promise<RoleConfig | undefined> {\n if (!roleConfigs.length) return undefined;\n try {\n const { token } = await auth.getPluginRequestToken({\n onBehalfOf: await auth.getOwnServiceCredentials(),\n targetPluginId: 'catalog',\n });\n const entity = await catalogClient.getEntityByRef(userEntityRef, { token });\n const groups = (entity?.relations ?? [])\n .filter(r => r.type === 'memberOf')\n .map(r => r.targetRef);\n return roleConfigs.find(rc => groups.includes(rc.group));\n } catch (err: any) {\n logger.warn(\n `Could not resolve Backstage groups for ${userEntityRef}: ${err.message}`,\n );\n return undefined;\n }\n}\n"],
5
- "mappings": "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",
6
- "names": ["express"]
4
+ "sourcesContent": ["export { litellmPlugin } from './plugin';\nexport { litellmPlugin as default } from './plugin';\nexport { createRouter } from './router';\nexport * from './types';\nexport { LiteLLMClient } from './client';\n", "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, discovery }) {\n const router = await createRouter({ config, logger, auth, discovery });\n httpRouter.use(router);\n },\n });\n },\n});\n", "import express, { Router, Request, Response } from 'express';\nimport { Config } from '@backstage/config';\nimport { AuthService, DiscoveryService } from '@backstage/backend-plugin-api';\nimport { CatalogClient } from '@backstage/catalog-client';\nimport { LiteLLMClient } from './client';\nimport {\n VirtualKey,\n ModelInfo,\n UsageMetrics,\n TeamInfo,\n GenerateKeyRequest,\n GenerateKeyResponse,\n UpdateKeyRequest,\n} from './types';\nimport {\n toLiteLLMUserId,\n resolveUserId,\n resolveUserProfile,\n getOrProvisionUser,\n readProvisioningDefaults,\n readRoleConfigs,\n ProvisioningError,\n} from './provisioning';\n\nexport { ProvisioningError };\n\nexport interface RouterOptions {\n config: Config;\n logger: any;\n auth: AuthService;\n discovery: DiscoveryService;\n}\n\nexport async function createRouter(options: RouterOptions): Promise<Router> {\n const { config, logger, auth, discovery } = options;\n\n const baseUrl = config.getString('litellm.baseUrl');\n const masterKey = config.getString('litellm.masterKey');\n const userIdDomain = config.getOptionalString('litellm.userIdDomain');\n const client = new LiteLLMClient({ baseUrl, masterKey });\n const { enabled: provisioningEnabled, defaults: provisioningDefaults } =\n readProvisioningDefaults(config);\n const roleConfigs = readRoleConfigs(config);\n const catalogClient = new CatalogClient({ discoveryApi: discovery });\n\n if (provisioningEnabled) {\n logger.info(\n `LiteLLM auto-provisioning enabled \u2014 defaults: budget=$${\n provisioningDefaults.maxBudget\n }/${provisioningDefaults.budgetDuration}, models=${\n provisioningDefaults.models.length\n ? provisioningDefaults.models.join(',')\n : 'all'\n }, teams=[${provisioningDefaults.teams.join(',')}]`,\n );\n }\n\n const router = Router();\n // JSON body parser. Without this, every POST/PUT endpoint sees an empty\n // req.body. Backstage's httpRouter does not apply a body parser at the\n // plugin-router level, so each plugin must attach its own.\n router.use(express.json());\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 tokenEntityRef = await resolveUserId(req, auth);\n const userId = tokenEntityRef\n ? toLiteLLMUserId(tokenEntityRef, userIdDomain)\n : (req.query.user_id as string | undefined);\n\n const userInfo = await getOrProvisionUser(\n client,\n tokenEntityRef,\n userId,\n provisioningEnabled,\n provisioningDefaults,\n roleConfigs,\n catalogClient,\n auth,\n logger,\n );\n res.json(userInfo);\n } catch (error: any) {\n if (error instanceof ProvisioningError) {\n res.status(error.status).json(error.body);\n return;\n }\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 tokenEntityRef = await resolveUserId(req, auth);\n const userId = tokenEntityRef\n ? toLiteLLMUserId(tokenEntityRef, userIdDomain)\n : (req.query.user_id as string | undefined);\n\n await getOrProvisionUser(\n client,\n tokenEntityRef,\n userId,\n provisioningEnabled,\n provisioningDefaults,\n roleConfigs,\n catalogClient,\n auth,\n logger,\n );\n\n const keys: VirtualKey[] = await client.listKeys(userId);\n res.json(keys);\n } catch (error: any) {\n if (error instanceof ProvisioningError) {\n res.status(error.status).json(error.body);\n return;\n }\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 // Only alias + max_budget are required. An empty models array is\n // intentional \u2014 in LiteLLM `models: []` means \"all models the user\n // can access\" which is the desired default. Forcing a selection\n // up front is too restrictive for the common case.\n const body = (req.body ?? {}) as GenerateKeyRequest;\n const missing: string[] = [];\n if (!body.alias?.trim()) missing.push('alias');\n if (typeof body.max_budget !== 'number' || body.max_budget <= 0) {\n missing.push('max_budget (positive number)');\n }\n if (missing.length) {\n res.status(400).json({\n error: 'Missing required fields',\n hint: `Required: ${missing.join(', ')}`,\n });\n return;\n }\n\n const tokenEntityRef = await resolveUserId(req, auth);\n const resolvedUserId = tokenEntityRef\n ? toLiteLLMUserId(tokenEntityRef, userIdDomain)\n : undefined;\n\n if (resolvedUserId) {\n await getOrProvisionUser(\n client,\n tokenEntityRef,\n resolvedUserId,\n provisioningEnabled,\n provisioningDefaults,\n roleConfigs,\n catalogClient,\n auth,\n logger,\n );\n }\n\n // Stamp ownership into LiteLLM key metadata. LiteLLM's native\n // `created_by` column is only populated when the caller authenticates\n // via JWT/SSO; we always call with the master key, so that column\n // stays null. Enriching `metadata` makes the owner identity visible\n // in LiteLLM's UI and queryable via API.\n const profile = tokenEntityRef\n ? await resolveUserProfile(tokenEntityRef, catalogClient, auth, logger)\n : {};\n const enrichedMetadata = {\n ...(body.metadata ?? {}),\n created_by_backstage_user: tokenEntityRef ?? 'unknown',\n ...(profile.email && { created_by_email: profile.email }),\n ...(profile.displayName && {\n created_by_display_name: profile.displayName,\n }),\n created_via: 'backstage',\n created_at_iso: new Date().toISOString(),\n };\n\n const request: GenerateKeyRequest = {\n ...body,\n metadata: enrichedMetadata,\n ...(resolvedUserId && { user_id: resolvedUserId }),\n };\n const result: GenerateKeyResponse = await client.generateKey(request);\n res.json(result);\n } catch (error: any) {\n if (error instanceof ProvisioningError) {\n res.status(error.status).json(error.body);\n return;\n }\n logger.error('Failed to generate key', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.post('/keys/:keyId/update', 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 const request: UpdateKeyRequest = { ...req.body, key: keyId };\n const result = await client.updateKey(request);\n res.json(result);\n } catch (error: any) {\n logger.error('Failed to update 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 tokenEntityRef = await resolveUserId(req, auth);\n const userId = tokenEntityRef\n ? toLiteLLMUserId(tokenEntityRef, userIdDomain)\n : (req.query.user_id as string | undefined);\n\n const userInfo = await getOrProvisionUser(\n client,\n tokenEntityRef,\n userId,\n provisioningEnabled,\n provisioningDefaults,\n roleConfigs,\n catalogClient,\n auth,\n logger,\n );\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 if (error instanceof ProvisioningError) {\n res.status(error.status).json(error.body);\n return;\n }\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 tokenEntityRef = await resolveUserId(req, auth);\n const userId = tokenEntityRef\n ? toLiteLLMUserId(tokenEntityRef, userIdDomain)\n : (req.query.user_id as string | undefined);\n\n if (userId) {\n await getOrProvisionUser(\n client,\n tokenEntityRef,\n userId,\n provisioningEnabled,\n provisioningDefaults,\n roleConfigs,\n catalogClient,\n auth,\n logger,\n );\n }\n\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 if (error instanceof ProvisioningError) {\n res.status(error.status).json(error.body);\n return;\n }\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 LiteLLMUserKey,\n ModelInfo,\n UsageMetrics,\n TeamInfo,\n GenerateKeyRequest,\n GenerateKeyResponse,\n UpdateKeyRequest,\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>(\n path: string,\n options: RequestInit = {},\n ): 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(\n `LiteLLM API error: ${response.status} ${response.statusText} - ${errorBody}`,\n );\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 * LiteLLM's `/user/info` wraps the user row inside `user_info` and returns\n * `teams` as an array of full team objects, not team_id strings. We flatten\n * `user_info` onto the top level and reduce `teams` to a string[] of ids so\n * the rest of the code can rely on the UserInfo contract.\n */\n async getUserInfo(userId?: string): Promise<UserInfo | null> {\n const query = userId ? `?user_id=${encodeURIComponent(userId)}` : '';\n try {\n const raw = await this.request<any>(`/user/info${query}`);\n const inner = raw?.user_info ?? {};\n const teamIds: string[] = Array.isArray(raw?.teams)\n ? raw.teams\n .map((t: any) => (typeof t === 'string' ? t : t?.team_id))\n .filter((t: unknown): t is string => typeof t === 'string')\n : [];\n return {\n user_id: raw?.user_id ?? inner.user_id ?? userId ?? '',\n user_email: inner.user_email ?? raw?.user_email,\n email: inner.email ?? raw?.email,\n teams: teamIds,\n models: inner.models ?? raw?.models,\n max_budget: inner.max_budget ?? raw?.max_budget,\n spend: inner.spend ?? raw?.spend,\n current_spend: inner.current_spend ?? raw?.current_spend,\n soft_limit: inner.soft_limit ?? raw?.soft_limit,\n hard_limit: inner.hard_limit ?? raw?.hard_limit,\n };\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 /**\n * Updates an existing LiteLLM user record. Used as a defensive follow-up\n * after /user/new because the upsert path of /user/new has been observed\n * to silently drop fields like user_role under concurrent inserts.\n */\n async updateUser(\n payload: Partial<CreateUserRequest> & { user_id: string },\n ): Promise<unknown> {\n return this.request<unknown>('/user/update', {\n method: 'POST',\n body: JSON.stringify(payload),\n });\n }\n\n /**\n * Returns the keys belonging to a user.\n *\n * Implementation note: LiteLLM's `/key/info` endpoint requires a `key`\n * hash and returns 404 when only `user_id` is passed. The correct way\n * to enumerate a user's keys is `/user/info?user_id=X`, which embeds\n * a `keys` array with per-key metadata. We unwrap that array and\n * normalise field names to match the frontend VirtualKey shape\n * (LiteLLM exposes `key_name` for the masked display value and\n * `expires` instead of `expires_at`).\n */\n async listKeys(userId?: string): Promise<VirtualKey[]> {\n if (!userId) return [];\n try {\n const response = await this.request<{ keys?: LiteLLMUserKey[] }>(\n `/user/info?user_id=${encodeURIComponent(userId)}`,\n );\n const rawKeys = response.keys ?? [];\n return rawKeys.map(this.toVirtualKey);\n } catch (err: any) {\n if (err.status === 404 || err.message.includes('not found')) {\n return [];\n }\n throw err;\n }\n }\n\n private toVirtualKey(k: LiteLLMUserKey): VirtualKey {\n return {\n // The hashed `token` never leaves LiteLLM in a usable form; the\n // masked `key_name` (\"sk-...XXXX\") is what the UI displays. Fall\n // back to `token` only when `key_name` is missing.\n key: k.key_name ?? k.token,\n token: k.token,\n key_alias: k.key_alias ?? undefined,\n created_at: k.created_at,\n expires_at: k.expires ?? undefined,\n spend: k.spend ?? 0,\n max_budget: k.max_budget ?? undefined,\n tpm_limit: k.tpm_limit ?? undefined,\n rpm_limit: k.rpm_limit ?? undefined,\n models: k.models ?? [],\n user_id: k.user_id ?? undefined,\n };\n }\n\n /**\n * Creates a new virtual key on the LiteLLM proxy.\n *\n * Implementation notes \u2014 both required to avoid silently-empty keys:\n * 1. The body must be the plain payload. An earlier version wrapped\n * it as `{ json: request }`; LiteLLM doesn't unwrap that envelope\n * and treats the request as having no fields, returning a key\n * with null alias / models / budget / limits.\n * 2. LiteLLM expects `key_alias`, not `alias`. Without the rename,\n * the alias the user typed is dropped on the floor.\n */\n async generateKey(request: GenerateKeyRequest): Promise<GenerateKeyResponse> {\n const { alias, ...rest } = request;\n const payload = {\n ...rest,\n ...(alias && { key_alias: alias }),\n };\n return this.request<GenerateKeyResponse>('/key/generate', {\n method: 'POST',\n body: JSON.stringify(payload),\n });\n }\n\n async updateKey(request: UpdateKeyRequest): Promise<VirtualKey> {\n return this.request<VirtualKey>('/key/update', {\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 /**\n * Returns the proxy's model catalogue normalised to the ModelInfo shape.\n *\n * Prefers `/model/info` which exposes `model_name`, `mode`, capability flags\n * and per-token costs. Falls back to OpenAI-compatible `/models` (which only\n * returns `{id}`) so the dropdown still works on installs where `/model/info`\n * isn't reachable. Without this normalisation the UI saw blank labels and\n * a single \"other\" group because `/models` doesn't populate `model_name`\n * or `mode`.\n */\n async listModels(): Promise<ModelInfo[]> {\n try {\n const response = await this.request<{ data?: any[] }>('/model/info');\n const data = Array.isArray(response?.data) ? response.data : [];\n const normalised = data.map((m: any) => {\n const info = m.model_info ?? {};\n const params = m.litellm_params ?? {};\n return {\n model_name: m.model_name ?? params.model ?? info.id ?? '',\n mode: info.mode ?? m.mode ?? 'chat',\n supports_function_calling:\n info.supports_function_calling ?? m.supports_function_calling,\n supports_vision: info.supports_vision ?? m.supports_vision,\n input_cost_per_token:\n info.input_cost_per_token ?? params.input_cost_per_token,\n output_cost_per_token:\n info.output_cost_per_token ?? params.output_cost_per_token,\n } as ModelInfo;\n });\n const filtered = normalised.filter(m => m.model_name);\n if (filtered.length) return filtered;\n } catch {\n // fall through to /models\n }\n const fallback = await this.request<{ data?: any[] } | any[]>('/models');\n const data = Array.isArray(fallback)\n ? fallback\n : Array.isArray(fallback?.data)\n ? fallback!.data!\n : [];\n return data\n .map((m: any) => ({\n model_name: m.model_name ?? m.id ?? '',\n mode: m.mode ?? 'chat',\n supports_function_calling: m.supports_function_calling,\n supports_vision: m.supports_vision,\n }))\n .filter((m: ModelInfo) => m.model_name);\n }\n\n async getTeamInfo(teamId: string): Promise<TeamInfo> {\n return this.request<TeamInfo>(\n `/team/info?team_id=${encodeURIComponent(teamId)}`,\n );\n }\n\n private emptyUsage(): UsageMetrics {\n return {\n total_spend: 0,\n total_tokens: 0,\n prompt_tokens: 0,\n completion_tokens: 0,\n api_requests: 0,\n successful_requests: 0,\n failed_requests: 0,\n usage_by_model: {},\n usage_by_key: {},\n daily_usage: [],\n daily_by_model: [],\n };\n }\n\n /**\n * Transforms LiteLLM's SpendAnalyticsPaginatedResponse into the flatter\n * UsageMetrics shape consumed by the frontend charts.\n *\n * Source shape (per result row):\n * { date, metrics, breakdown: { models: { [name]: { metrics, api_key_breakdown: { [keyHash]: { metrics, metadata } } } } } }\n *\n * We fan that out into three views the UI consumes:\n * - daily_usage \u2192 spend + request trends over time\n * - usage_by_model \u2192 which models drove cost / traffic\n * - usage_by_key \u2192 which keys drove cost / traffic (with key_alias + team_id from metadata)\n */\n private transformDailyActivity(response: any): UsageMetrics {\n const results: any[] = Array.isArray(response?.results)\n ? response.results\n : [];\n const meta = response?.metadata ?? {};\n\n const daily_usage = results\n .map(r => ({\n date: r.date,\n spend: r.metrics?.spend ?? 0,\n total_tokens: r.metrics?.total_tokens ?? 0,\n prompt_tokens: r.metrics?.prompt_tokens ?? 0,\n completion_tokens: r.metrics?.completion_tokens ?? 0,\n api_requests: r.metrics?.api_requests ?? 0,\n successful_requests: r.metrics?.successful_requests ?? 0,\n failed_requests: r.metrics?.failed_requests ?? 0,\n }))\n .sort((a, b) => a.date.localeCompare(b.date));\n\n const usage_by_model: UsageMetrics['usage_by_model'] = {};\n const usage_by_key: UsageMetrics['usage_by_key'] = {};\n const daily_by_model: UsageMetrics['daily_by_model'] = [];\n\n const emptyModelBucket = () => ({\n total_spend: 0,\n total_tokens: 0,\n prompt_tokens: 0,\n completion_tokens: 0,\n api_requests: 0,\n successful_requests: 0,\n failed_requests: 0,\n });\n\n for (const r of results) {\n const models = r.breakdown?.models ?? {};\n for (const [name, entry] of Object.entries<any>(models)) {\n const m = entry?.metrics ?? {};\n const bucket = usage_by_model[name] ?? emptyModelBucket();\n bucket.total_spend += m.spend ?? 0;\n bucket.total_tokens += m.total_tokens ?? 0;\n bucket.prompt_tokens += m.prompt_tokens ?? 0;\n bucket.completion_tokens += m.completion_tokens ?? 0;\n bucket.api_requests += m.api_requests ?? 0;\n bucket.successful_requests += m.successful_requests ?? 0;\n bucket.failed_requests += m.failed_requests ?? 0;\n usage_by_model[name] = bucket;\n\n daily_by_model.push({\n date: r.date,\n model: name,\n spend: m.spend ?? 0,\n prompt_tokens: m.prompt_tokens ?? 0,\n completion_tokens: m.completion_tokens ?? 0,\n total_tokens: m.total_tokens ?? 0,\n api_requests: m.api_requests ?? 0,\n successful_requests: m.successful_requests ?? 0,\n failed_requests: m.failed_requests ?? 0,\n });\n\n const keyMap = entry?.api_key_breakdown ?? {};\n for (const [keyHash, keyEntry] of Object.entries<any>(keyMap)) {\n const km = keyEntry?.metrics ?? {};\n const kmeta = keyEntry?.metadata ?? {};\n const kb = usage_by_key[keyHash] ?? {\n key_alias: kmeta.key_alias,\n team_id: kmeta.team_id ?? null,\n models: [] as string[],\n ...emptyModelBucket(),\n };\n if (!kb.key_alias && kmeta.key_alias) kb.key_alias = kmeta.key_alias;\n if (kb.team_id == null && kmeta.team_id) kb.team_id = kmeta.team_id;\n if (!kb.models.includes(name)) kb.models.push(name);\n kb.total_spend += km.spend ?? 0;\n kb.total_tokens += km.total_tokens ?? 0;\n kb.prompt_tokens += km.prompt_tokens ?? 0;\n kb.completion_tokens += km.completion_tokens ?? 0;\n kb.api_requests += km.api_requests ?? 0;\n kb.successful_requests += km.successful_requests ?? 0;\n kb.failed_requests += km.failed_requests ?? 0;\n usage_by_key[keyHash] = kb;\n }\n }\n }\n\n return {\n total_spend: meta.total_spend ?? 0,\n total_tokens: meta.total_tokens ?? 0,\n prompt_tokens: meta.total_prompt_tokens ?? 0,\n completion_tokens: meta.total_completion_tokens ?? 0,\n api_requests: meta.total_api_requests ?? 0,\n successful_requests: meta.total_successful_requests ?? 0,\n failed_requests: meta.total_failed_requests ?? 0,\n usage_by_model,\n usage_by_key,\n daily_usage,\n daily_by_model,\n };\n }\n\n async getUsage(\n startDate: string,\n endDate: string,\n userId?: string,\n _groupBy?: string,\n ): Promise<UsageMetrics> {\n const params = new URLSearchParams({\n start_date: startDate,\n end_date: endDate,\n page_size: '100',\n });\n if (userId) params.append('user_id', userId);\n try {\n const response = await this.request<any>(\n `/user/daily/activity?${params.toString()}`,\n );\n return this.transformDailyActivity(response);\n } catch (err: any) {\n if (err.status === 404 || err.message.includes('not found')) {\n return this.emptyUsage();\n }\n throw err;\n }\n }\n\n async getTeamUsage(\n teamId: string,\n startDate: string,\n endDate: string,\n ): Promise<UsageMetrics> {\n const params = new URLSearchParams({\n start_date: startDate,\n end_date: endDate,\n team_ids: teamId,\n page_size: '100',\n });\n try {\n const response = await this.request<any>(\n `/team/daily/activity?${params.toString()}`,\n );\n return this.transformDailyActivity(response);\n } catch (err: any) {\n if (err.status === 404 || err.message.includes('not found')) {\n return this.emptyUsage();\n }\n throw err;\n }\n }\n}\n", "import { Config } from '@backstage/config';\nimport { AuthService } from '@backstage/backend-plugin-api';\nimport { CatalogClient } from '@backstage/catalog-client';\nimport { Request } from 'express';\nimport { LiteLLMClient } from './client';\nimport { UserInfo, ProvisioningDefaults, RoleConfig } from './types';\n\n/**\n * Converts a Backstage user entity ref to a LiteLLM user_id.\n *\n * When userIdDomain is configured, the entity name is suffixed with the domain\n * so that LiteLLM user_ids match the organisation's email addresses:\n * \"user:default/andrea.carmisciano\" + \"abstract.it\"\n * \u2192 \"andrea.carmisciano@abstract.it\"\n *\n * Without a domain the bare entity name is returned unchanged, which works for\n * deployments where LiteLLM users were created with plain usernames.\n */\nexport function toLiteLLMUserId(\n userEntityRef: string,\n userIdDomain?: string,\n): string {\n const name = userEntityRef.split('/').pop() ?? userEntityRef;\n // Defensive: if the entity name is already email-shaped (e.g. when the\n // Keycloak provider imports usernames as full emails without our\n // name-rewrite transformer running, or when a catalog change leaves\n // an entity ref like \"user:default/foo@bar.it\"), do NOT append the\n // userIdDomain \u2014 that produced \"foo@bar.it@bar.it\" in production.\n if (name.includes('@')) return name;\n return userIdDomain ? `${name}@${userIdDomain}` : name;\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 */\nexport function readRoleConfigs(config: Config): RoleConfig[] {\n const raw = config.getOptional<any[]>('litellm.provisioning.roles');\n if (!raw?.length) return [];\n return raw.map((r: any) => ({\n group: r.group as string,\n maxBudget: r.maxBudget,\n budgetDuration: r.budgetDuration,\n models: r.models,\n teams: r.teams,\n tpmLimit: r.tpmLimit,\n rpmLimit: r.rpmLimit,\n userRole: r.userRole,\n metadata: r.metadata,\n }));\n}\n\n/**\n * Merges role config over defaults. Role fields override defaults only when explicitly set.\n */\nexport function applyRoleOverrides(\n defaults: ProvisioningDefaults,\n role: RoleConfig,\n): ProvisioningDefaults {\n return {\n maxBudget: role.maxBudget ?? defaults.maxBudget,\n budgetDuration: role.budgetDuration ?? defaults.budgetDuration,\n models: role.models ?? defaults.models,\n teams: role.teams ?? defaults.teams,\n tpmLimit: role.tpmLimit ?? defaults.tpmLimit,\n rpmLimit: role.rpmLimit ?? defaults.rpmLimit,\n userRole: role.userRole ?? defaults.userRole,\n metadata: { ...defaults.metadata, ...(role.metadata ?? {}) },\n };\n}\n\nexport function readProvisioningDefaults(config: Config): {\n enabled: boolean;\n defaults: ProvisioningDefaults;\n} {\n const enabled =\n config.getOptionalBoolean('litellm.provisioning.enabled') ?? false;\n const defaults: ProvisioningDefaults = {\n maxBudget:\n config.getOptionalNumber('litellm.provisioning.defaults.maxBudget') ?? 10,\n budgetDuration:\n config.getOptionalString(\n 'litellm.provisioning.defaults.budgetDuration',\n ) ?? '30d',\n models:\n config.getOptionalStringArray('litellm.provisioning.defaults.models') ??\n [],\n teams:\n config.getOptionalStringArray('litellm.provisioning.defaults.teams') ??\n [],\n tpmLimit: config.getOptionalNumber(\n 'litellm.provisioning.defaults.tpmLimit',\n ),\n rpmLimit: config.getOptionalNumber(\n 'litellm.provisioning.defaults.rpmLimit',\n ),\n userRole:\n config.getOptionalString('litellm.provisioning.defaults.userRole') ??\n 'internal_user',\n metadata:\n config.getOptional<Record<string, string>>(\n 'litellm.provisioning.defaults.metadata',\n ) ?? {},\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 */\nexport async function resolveUserId(\n req: Request,\n auth: AuthService,\n): 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 * Profile data extracted from a Backstage Catalog User entity, used to\n * populate user_email / user_alias on the LiteLLM record.\n */\nexport interface BackstageUserProfile {\n email?: string;\n displayName?: string;\n}\n\n/**\n * Looks up the catalog User entity for the authenticated user and returns\n * the profile block. Returns an empty object when the user has no catalog\n * entity (e.g. dangerouslyAllowSignInWithoutUserInCatalog was used) \u2014 the\n * caller falls back to deriving identity from userIdDomain.\n */\nexport async function resolveUserProfile(\n userEntityRef: string,\n catalogClient: CatalogClient,\n auth: AuthService,\n logger: any,\n): Promise<BackstageUserProfile> {\n try {\n const { token } = await auth.getPluginRequestToken({\n onBehalfOf: await auth.getOwnServiceCredentials(),\n targetPluginId: 'catalog',\n });\n const entity = await catalogClient.getEntityByRef(userEntityRef, { token });\n const profile = (entity?.spec as any)?.profile ?? {};\n return {\n email: profile.email,\n displayName: profile.displayName,\n };\n } catch (err: any) {\n logger.warn(\n `Could not fetch catalog profile for ${userEntityRef}: ${err.message}`,\n );\n return {};\n }\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 */\nexport async function provisionUser(\n client: LiteLLMClient,\n userId: string,\n defaults: ProvisioningDefaults,\n profile: BackstageUserProfile,\n backstageEntity: string | undefined,\n logger: any,\n): Promise<UserInfo | null> {\n const payload = {\n user_id: userId,\n ...(profile.email && { user_email: profile.email }),\n ...(profile.displayName && { user_alias: profile.displayName }),\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 ...(defaults.userRole && { user_role: defaults.userRole }),\n auto_create_key: false,\n metadata: {\n ...defaults.metadata,\n provisioned_by: 'backstage',\n provisioned_at: new Date().toISOString(),\n backstage_entity: backstageEntity ?? userId,\n ...(profile.email && { backstage_email: profile.email }),\n ...(profile.displayName && {\n backstage_display_name: profile.displayName,\n }),\n },\n };\n\n logger.info(\n `Provisioning new LiteLLM user for Backstage identity: ${userId}` +\n (profile.email ? ` (email=${profile.email})` : ''),\n );\n try {\n await client.createUser(payload);\n // Defensive /user/update: LiteLLM's /user/new upsert path has been\n // observed to drop user_role under concurrent inserts (the first\n // call sets the field, a racing second call upserts and clears it).\n // Re-asserting the role-bearing fields immediately after creation\n // is cheap and makes the role guarantee robust.\n if (defaults.userRole) {\n try {\n await client.updateUser({\n user_id: userId,\n user_role: defaults.userRole,\n ...(profile.email && { user_email: profile.email }),\n ...(profile.displayName && { user_alias: profile.displayName }),\n });\n } catch (updateErr: any) {\n logger.warn(\n `Defensive /user/update after provisioning ${userId} failed: ${updateErr.message}`,\n );\n }\n }\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 throw err;\n }\n}\n\n/**\n * Module-scope single-flight cache keyed by LiteLLM user_id. Coalesces\n * concurrent provisioning attempts for the same user so /user/new fires\n * at most once per user across parallel requests. Without this, an\n * authenticated page load that fires /keys, /teams and /usage in\n * parallel triggers three concurrent /user/new calls; LiteLLM's\n * upsert path then creates one default key per call (so the user lands\n * with 3 unexpected keys) and may silently lose user_role.\n *\n * Cache entries are removed once the promise settles, so subsequent\n * requests for a re-deleted user can still trigger fresh provisioning.\n */\nconst provisioningInFlight = new Map<string, Promise<UserInfo>>();\n\n/**\n * Strips any echoed Authorization bearer token from upstream LiteLLM error\n * messages before they're shipped back to the browser. LiteLLM normally does\n * not echo the master key, but defense in depth: never let a `Bearer \u2026`\n * substring travel out in a response body.\n */\nfunction sanitizeUpstreamMessage(message: string): string {\n if (!message) return 'unknown error';\n return message\n .replace(/Bearer\\s+[A-Za-z0-9._\\-+/=]+/g, 'Bearer [redacted]')\n .replace(/sk-[A-Za-z0-9_\\-]{8,}/g, 'sk-[redacted]')\n .slice(0, 500);\n}\n\nexport class ProvisioningError extends Error {\n status: number;\n body: { error: string; hint: string; provisioning: boolean };\n\n constructor(\n message: string,\n hint: string,\n provisioning: boolean,\n status = 404,\n ) {\n super(message);\n this.status = status;\n this.body = { error: message, hint, provisioning };\n }\n}\n\n/**\n * Ensures the LiteLLM user exists, returning its UserInfo.\n * When the user is missing and provisioning is enabled, attempts to create it.\n * When provisioning is disabled, throws a ProvisioningError with a clear message.\n */\nexport async function getOrProvisionUser(\n client: LiteLLMClient,\n tokenEntityRef: string | undefined,\n userId: string | undefined,\n provisioningEnabled: boolean,\n provisioningDefaults: ProvisioningDefaults,\n roleConfigs: RoleConfig[],\n catalogClient: CatalogClient,\n auth: AuthService,\n logger: any,\n): Promise<UserInfo> {\n if (!userId) {\n throw new ProvisioningError(\n 'User not found in LiteLLM',\n 'No user identity could be resolved from the request.',\n provisioningEnabled,\n );\n }\n\n const existing = await client.getUserInfo(userId);\n if (existing) {\n return existing;\n }\n\n if (!provisioningEnabled) {\n throw new ProvisioningError(\n 'User not found in LiteLLM',\n 'Enable litellm.provisioning.enabled in app-config.yaml or create the user manually',\n false,\n );\n }\n\n // Single-flight: if another request for the same userId is already\n // provisioning, await its result instead of starting a new /user/new.\n // This collapses the /keys + /teams + /usage page-load thundering\n // herd into a single LiteLLM round-trip.\n const pending = provisioningInFlight.get(userId);\n if (pending) {\n logger.info(\n `LiteLLM provisioning already in flight for ${userId} \u2014 joining`,\n );\n return pending;\n }\n\n const provisionPromise = (async () => {\n const catalogRef = tokenEntityRef ?? userId;\n const [matchedRole, profile] = await Promise.all([\n resolveUserRole(catalogRef, roleConfigs, catalogClient, auth, logger),\n tokenEntityRef\n ? resolveUserProfile(tokenEntityRef, catalogClient, auth, logger)\n : Promise.resolve<BackstageUserProfile>({}),\n ]);\n const effectiveDefaults = matchedRole\n ? applyRoleOverrides(provisioningDefaults, matchedRole)\n : provisioningDefaults;\n if (matchedRole) {\n logger.info(\n `User ${userId} matched role group ${matchedRole.group} \u2014 using role-specific provisioning`,\n );\n }\n try {\n const created = await provisionUser(\n client,\n userId,\n effectiveDefaults,\n profile,\n tokenEntityRef,\n logger,\n );\n if (!created) {\n throw new ProvisioningError(\n 'User not found in LiteLLM',\n 'Provisioning attempted but returned no user \u2014 check LiteLLM logs',\n true,\n );\n }\n return created;\n } catch (err: any) {\n // The single-flight cache should prevent the parallel-409 race,\n // but keep the recovery path: if /user/new still 409s (e.g.\n // multi-replica deploys where the lock is per-process), treat\n // it as \"user exists\" and re-fetch.\n if (err.status === 409 || /already exists/i.test(err.message ?? '')) {\n logger.info(\n `LiteLLM user ${userId} already exists during provisioning \u2014 re-fetching`,\n );\n const refetched = await client.getUserInfo(userId);\n if (refetched) {\n return refetched;\n }\n }\n if (err instanceof ProvisioningError) {\n throw err;\n }\n // Map upstream LiteLLM status to a Backstage-safe gateway status.\n // 401/403/5xx from LiteLLM mean the gateway (this plugin) cannot\n // talk to LiteLLM \u2014 they MUST NOT propagate as 401/403 to the\n // browser, otherwise Backstage's fetch middleware treats the\n // user's Backstage session as expired and forces a re-login.\n // Only safe client-semantic codes pass through.\n const upstreamStatus = err.status;\n const passThrough = [400, 404, 409, 422].includes(upstreamStatus)\n ? upstreamStatus\n : 502;\n throw new ProvisioningError(\n 'LiteLLM auto-provisioning failed',\n `LiteLLM upstream ${\n upstreamStatus ?? 'error'\n }: ${sanitizeUpstreamMessage(err.message)}`,\n true,\n passThrough,\n );\n }\n })();\n\n provisioningInFlight.set(userId, provisionPromise);\n try {\n return await provisionPromise;\n } finally {\n provisioningInFlight.delete(userId);\n }\n}\n\n/**\n * Fetches the user's Backstage group memberships and returns the first matching\n * role config (priority order), or undefined when no role matches.\n */\nexport async function resolveUserRole(\n userEntityRef: string,\n roleConfigs: RoleConfig[],\n catalogClient: CatalogClient,\n auth: AuthService,\n logger: any,\n): Promise<RoleConfig | undefined> {\n if (!roleConfigs.length) return undefined;\n try {\n const { token } = await auth.getPluginRequestToken({\n onBehalfOf: await auth.getOwnServiceCredentials(),\n targetPluginId: 'catalog',\n });\n const entity = await catalogClient.getEntityByRef(userEntityRef, { token });\n const groups = (entity?.relations ?? [])\n .filter(r => r.type === 'memberOf')\n .map(r => r.targetRef);\n return roleConfigs.find(rc => groups.includes(rc.group));\n } catch (err: any) {\n logger.warn(\n `Could not resolve Backstage groups for ${userEntityRef}: ${err.message}`,\n );\n return undefined;\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["data", "express"]
7
7
  }
@@ -0,0 +1,323 @@
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
@@ -0,0 +1,7 @@
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": ";AAAA,SAAS,cAAc,2BAA2B;;;ACAlD,SAAS,cAAiC;;;ACc1C,IAAM,kBAAkB;AAEjB,IAAM,gBAAN,MAAoB;AAAA,EAKzB,YAAY,QAAuB,UAAU,iBAAiB;AAC5D,SAAK,UAAU,OAAO,QAAQ,QAAQ,OAAO,EAAE;AAC/C,SAAK,YAAY,OAAO;AACxB,SAAK,UAAU;AAAA,EACjB;AAAA,EAEA,MAAc,QAAW,MAAc,UAAuB,CAAC,GAAe;AAC5E,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,YAAY,WAAW,MAAM,WAAW,MAAM,GAAG,KAAK,OAAO;AAEnE,QAAI;AACF,YAAM,WAAW,MAAM,MAAM,GAAG,KAAK,OAAO,GAAG,IAAI,IAAI;AAAA,QACrD,GAAG;AAAA,QACH,QAAQ,WAAW;AAAA,QACnB,SAAS;AAAA,UACP,gBAAgB;AAAA,UAChB,iBAAiB,UAAU,KAAK,SAAS;AAAA,UACzC,GAAG,QAAQ;AAAA,QACb;AAAA,MACF,CAAC;AAED,UAAI,CAAC,SAAS,IAAI;AAChB,cAAM,YAAY,MAAM,SAAS,KAAK;AACtC,cAAM,MAAM,IAAI,MAAM,sBAAsB,SAAS,MAAM,IAAI,SAAS,UAAU,MAAM,SAAS,EAAE;AACnG,QAAC,IAAY,SAAS,SAAS;AAC/B,cAAM;AAAA,MACR;AAEA,aAAO,SAAS,KAAK;AAAA,IACvB,UAAE;AACA,mBAAa,SAAS;AAAA,IACxB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,YAAY,QAA2C;AAC3D,UAAM,QAAQ,SAAS,YAAY,mBAAmB,MAAM,CAAC,KAAK;AAClE,QAAI;AACF,aAAO,MAAM,KAAK,QAAkB,aAAa,KAAK,EAAE;AAAA,IAC1D,SAAS,KAAU;AACjB,UAAI,IAAI,WAAW,IAAK,QAAO;AAC/B,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAM,WAAW,SAAyD;AACxE,WAAO,KAAK,QAA4B,aAAa;AAAA,MACnD,QAAQ;AAAA,MACR,MAAM,KAAK,UAAU,OAAO;AAAA,IAC9B,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,SAAS,QAAwC;AACrD,UAAM,QAAQ,SAAS,YAAY,mBAAmB,MAAM,CAAC,KAAK;AAClE,UAAM,WAAW,MAAM,KAAK,QAA+C,YAAY,KAAK,EAAE;AAC9F,WAAO,MAAM,QAAQ,QAAQ,IAAI,WAAY,SAAS,QAAQ,CAAC;AAAA,EACjE;AAAA,EAEA,MAAM,YAAY,SAA2D;AAC3E,WAAO,KAAK,QAA6B,iBAAiB;AAAA,MACxD,QAAQ;AAAA,MACR,MAAM,KAAK,UAAU,OAAO;AAAA,IAC9B,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,WAAW,SAA0D;AACzE,WAAO,KAAK,QAA8B,eAAe;AAAA,MACvD,QAAQ;AAAA,MACR,MAAM,KAAK,UAAU,OAAO;AAAA,IAC9B,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,aAAmC;AACvC,UAAM,WAAW,MAAM,KAAK,QAA6C,SAAS;AAClF,WAAO,MAAM,QAAQ,QAAQ,IAAI,WAAY,SAAS,QAAQ,CAAC;AAAA,EACjE;AAAA,EAEA,MAAM,YAAY,QAAmC;AACnD,WAAO,KAAK,QAAkB,sBAAsB,mBAAmB,MAAM,CAAC,EAAE;AAAA,EAClF;AAAA,EAEA,MAAM,SAAS,WAAmB,SAAiB,QAAiB,SAAyC;AAC3G,UAAM,SAAS,IAAI,gBAAgB,EAAE,YAAY,WAAW,UAAU,QAAQ,CAAC;AAC/E,QAAI,OAAQ,QAAO,OAAO,WAAW,MAAM;AAC3C,QAAI,QAAS,QAAO,OAAO,YAAY,OAAO;AAC9C,WAAO,KAAK,QAAsB,eAAe,OAAO,SAAS,CAAC,EAAE;AAAA,EACtE;AAAA,EAEA,MAAM,aAAa,QAAgB,WAAmB,SAAwC;AAC5F,UAAM,SAAS,IAAI,gBAAgB,EAAE,YAAY,WAAW,UAAU,SAAS,SAAS,OAAO,CAAC;AAChG,WAAO,KAAK,QAAsB,eAAe,OAAO,SAAS,CAAC,EAAE;AAAA,EACtE;AACF;;;ADjFA,SAAS,yBAAyB,QAAsE;AACtG,QAAM,UAAU,OAAO,mBAAmB,8BAA8B,KAAK;AAC7E,QAAM,WAAiC;AAAA,IACrC,WAAW,OAAO,kBAAkB,yCAAyC,KAAK;AAAA,IAClF,gBAAgB,OAAO,kBAAkB,8CAA8C,KAAK;AAAA,IAC5F,QAAQ,OAAO,uBAAuB,sCAAsC,KAAK,CAAC;AAAA,IAClF,OAAO,OAAO,uBAAuB,qCAAqC,KAAK,CAAC;AAAA,IAChF,UAAU,OAAO,kBAAkB,wCAAwC;AAAA,IAC3E,UAAU,OAAO,kBAAkB,wCAAwC;AAAA,IAC3E,UAAW,OAAO,YAAoC,wCAAwC,KAAK,CAAC;AAAA,EACtG;AACA,SAAO,EAAE,SAAS,SAAS;AAC7B;AAOA,eAAe,cAAc,KAAc,MAAgD;AACzF,QAAM,WAAW,IAAI,QAAQ,eAAe,MAAM,CAAC;AACnD,MAAI,CAAC,SAAU,QAAO;AACtB,MAAI;AACF,UAAM,cAAc,MAAM,KAAK,aAAa,QAAQ;AACpD,UAAM,YAAY,YAAY;AAC9B,QAAI,WAAW,SAAS,QAAQ;AAC9B,aAAO,UAAU;AAAA,IACnB;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAMA,eAAe,cACb,QACA,QACA,UACA,QAC0B;AAC1B,QAAM,UAAU;AAAA,IACd,SAAS;AAAA,IACT,YAAY,SAAS;AAAA,IACrB,iBAAiB,SAAS;AAAA,IAC1B,QAAQ,SAAS;AAAA,IACjB,OAAO,SAAS;AAAA,IAChB,GAAI,SAAS,aAAa,UAAa,EAAE,WAAW,SAAS,SAAS;AAAA,IACtE,GAAI,SAAS,aAAa,UAAa,EAAE,WAAW,SAAS,SAAS;AAAA,IACtE,UAAU;AAAA,MACR,GAAG,SAAS;AAAA,MACZ,gBAAgB;AAAA,MAChB,iBAAgB,oBAAI,KAAK,GAAE,YAAY;AAAA,MACvC,kBAAkB;AAAA,IACpB;AAAA,EACF;AAEA,SAAO,KAAK,yDAAyD,MAAM,EAAE;AAC7E,MAAI;AACF,UAAM,OAAO,WAAW,OAAO;AAE/B,WAAO,MAAM,OAAO,YAAY,MAAM;AAAA,EACxC,SAAS,KAAU;AACjB,WAAO,MAAM,oCAAoC,MAAM,KAAK,IAAI,OAAO,EAAE;AACzE,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,aAAa,SAAyC;AAC1E,QAAM,EAAE,QAAQ,QAAQ,KAAK,IAAI;AAEjC,QAAM,UAAU,OAAO,UAAU,iBAAiB;AAClD,QAAM,YAAY,OAAO,UAAU,mBAAmB;AACtD,QAAM,SAAS,IAAI,cAAc,EAAE,SAAS,UAAU,CAAC;AACvD,QAAM,EAAE,SAAS,qBAAqB,UAAU,qBAAqB,IAAI,yBAAyB,MAAM;AAExG,MAAI,qBAAqB;AACvB,WAAO;AAAA,MACL,8DAAyD,qBAAqB,SAAS,IAAI,qBAAqB,cAAc,YAAY,qBAAqB,OAAO,SAAS,qBAAqB,OAAO,KAAK,GAAG,IAAI,KAAK,YAAY,qBAAqB,MAAM,KAAK,GAAG,CAAC;AAAA,IAC9Q;AAAA,EACF;AAEA,QAAM,SAAS,OAAO;AAEtB,SAAO,IAAI,WAAW,CAAC,MAAe,QAAkB;AACtD,QAAI,KAAK,EAAE,QAAQ,MAAM,cAAc,oBAAoB,CAAC;AAAA,EAC9D,CAAC;AAED,SAAO,IAAI,cAAc,OAAO,KAAc,QAAkB;AAC9D,QAAI;AACF,YAAM,cAAc,MAAM,cAAc,KAAK,IAAI;AACjD,YAAM,SAAS,eAAgB,IAAI,MAAM;AAEzC,UAAI,WAA4B,MAAM,OAAO,YAAY,MAAM;AAE/D,UAAI,CAAC,UAAU;AACb,YAAI,uBAAuB,QAAQ;AACjC,qBAAW,MAAM,cAAc,QAAQ,QAAQ,sBAAsB,MAAM;AAAA,QAC7E;AAEA,YAAI,CAAC,UAAU;AACb,cAAI,OAAO,GAAG,EAAE,KAAK;AAAA,YACnB,OAAO;AAAA,YACP,cAAc;AAAA,YACd,MAAM,sBACF,gEACA;AAAA,UACN,CAAC;AACD;AAAA,QACF;AAAA,MACF;AAEA,UAAI,KAAK,QAAQ;AAAA,IACnB,SAAS,OAAY;AACnB,aAAO,MAAM,6BAA6B,KAAK;AAC/C,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,MAAM,QAAQ,CAAC;AAAA,IAC/C;AAAA,EACF,CAAC;AAED,SAAO,IAAI,SAAS,OAAO,KAAc,QAAkB;AACzD,QAAI;AACF,YAAM,cAAc,MAAM,cAAc,KAAK,IAAI;AACjD,YAAM,SAAS,eAAgB,IAAI,MAAM;AACzC,YAAM,OAAqB,MAAM,OAAO,SAAS,MAAM;AACvD,UAAI,KAAK,IAAI;AAAA,IACf,SAAS,OAAY;AACnB,aAAO,MAAM,uBAAuB,KAAK;AACzC,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,MAAM,QAAQ,CAAC;AAAA,IAC/C;AAAA,EACF,CAAC;AAED,SAAO,KAAK,kBAAkB,OAAO,KAAc,QAAkB;AACnE,QAAI;AACF,YAAM,cAAc,MAAM,cAAc,KAAK,IAAI;AACjD,YAAM,UAA8B;AAAA,QAClC,GAAG,IAAI;AAAA,QACP,GAAI,eAAe,EAAE,SAAS,YAAY;AAAA,MAC5C;AACA,YAAM,SAA8B,MAAM,OAAO,YAAY,OAAO;AACpE,UAAI,KAAK,MAAM;AAAA,IACjB,SAAS,OAAY;AACnB,aAAO,MAAM,0BAA0B,KAAK;AAC5C,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,MAAM,QAAQ,CAAC;AAAA,IAC/C;AAAA,EACF,CAAC;AAED,SAAO,OAAO,gBAAgB,OAAO,KAAc,QAAkB;AACnE,QAAI;AACF,YAAM,EAAE,MAAM,IAAI,IAAI;AACtB,UAAI,CAAC,OAAO;AACV,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,oBAAoB,CAAC;AACnD;AAAA,MACF;AACA,YAAM,OAAO,WAAW,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC;AACzC,UAAI,KAAK,EAAE,SAAS,KAAK,CAAC;AAAA,IAC5B,SAAS,OAAY;AACnB,aAAO,MAAM,wBAAwB,KAAK;AAC1C,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,MAAM,QAAQ,CAAC;AAAA,IAC/C;AAAA,EACF,CAAC;AAED,SAAO,IAAI,WAAW,OAAO,MAAe,QAAkB;AAC5D,QAAI;AACF,YAAM,SAAsB,MAAM,OAAO,WAAW;AACpD,UAAI,KAAK,MAAM;AAAA,IACjB,SAAS,OAAY;AACnB,aAAO,MAAM,yBAAyB,KAAK;AAC3C,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,MAAM,QAAQ,CAAC;AAAA,IAC/C;AAAA,EACF,CAAC;AAED,SAAO,IAAI,UAAU,OAAO,KAAc,QAAkB;AAC1D,QAAI;AACF,YAAM,cAAc,MAAM,cAAc,KAAK,IAAI;AACjD,YAAM,SAAS,eAAgB,IAAI,MAAM;AACzC,YAAM,WAA4B,MAAM,OAAO,YAAY,MAAM;AAEjE,UAAI,CAAC,UAAU,OAAO,QAAQ;AAC5B,YAAI,KAAK,CAAC,CAAC;AACX;AAAA,MACF;AAEA,YAAM,QAAQ,MAAM,QAAQ;AAAA,QAC1B,SAAS,MAAM;AAAA,UAAI,YACjB,OAAO,YAAY,MAAM,EAAE,MAAM,SAAO;AACtC,mBAAO,KAAK,wBAAwB,MAAM,KAAK,IAAI,OAAO,EAAE;AAC5D,mBAAO;AAAA,UACT,CAAC;AAAA,QACH;AAAA,MACF;AACA,UAAI,KAAK,MAAM,OAAO,OAAO,CAAe;AAAA,IAC9C,SAAS,OAAY;AACnB,aAAO,MAAM,yBAAyB,KAAK;AAC3C,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,MAAM,QAAQ,CAAC;AAAA,IAC/C;AAAA,EACF,CAAC;AAED,SAAO,IAAI,wBAAwB,OAAO,KAAc,QAAkB;AACxE,QAAI;AACF,YAAM,EAAE,OAAO,IAAI,IAAI;AACvB,YAAM,EAAE,YAAY,SAAS,IAAI,IAAI;AACrC,UAAI,CAAC,cAAc,CAAC,UAAU;AAC5B,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,uCAAuC,CAAC;AACtE;AAAA,MACF;AACA,YAAM,QAAsB,MAAM,OAAO;AAAA,QACvC;AAAA,QACA;AAAA,QACA;AAAA,MACF;AACA,UAAI,KAAK,KAAK;AAAA,IAChB,SAAS,OAAY;AACnB,aAAO,MAAM,8BAA8B,KAAK;AAChD,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,MAAM,QAAQ,CAAC;AAAA,IAC/C;AAAA,EACF,CAAC;AAED,SAAO,IAAI,UAAU,OAAO,KAAc,QAAkB;AAC1D,QAAI;AACF,YAAM,EAAE,YAAY,UAAU,SAAS,IAAI,IAAI;AAC/C,UAAI,CAAC,cAAc,CAAC,UAAU;AAC5B,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,uCAAuC,CAAC;AACtE;AAAA,MACF;AACA,YAAM,cAAc,MAAM,cAAc,KAAK,IAAI;AACjD,YAAM,SAAS,eAAgB,IAAI,MAAM;AACzC,YAAM,QAAsB,MAAM,OAAO;AAAA,QACvC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AACA,UAAI,KAAK,KAAK;AAAA,IAChB,SAAS,OAAY;AACnB,aAAO,MAAM,yBAAyB,KAAK;AAC3C,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,MAAM,QAAQ,CAAC;AAAA,IAC/C;AAAA,EACF,CAAC;AAED,SAAO;AACT;;;ADnRO,IAAM,gBAAgB,oBAAoB;AAAA,EAC/C,UAAU;AAAA,EACV,SAAS,KAAK;AACZ,QAAI,aAAa;AAAA,MACf,MAAM;AAAA,QACJ,YAAY,aAAa;AAAA,QACzB,QAAQ,aAAa;AAAA,QACrB,QAAQ,aAAa;AAAA,QACrB,MAAM,aAAa;AAAA,QACnB,WAAW,aAAa;AAAA,MAC1B;AAAA,MACA,MAAM,KAAK,EAAE,YAAY,QAAQ,QAAQ,KAAK,GAAG;AAC/C,cAAM,SAAS,MAAM,aAAa,EAAE,QAAQ,QAAQ,KAAK,CAAC;AAC1D,mBAAW,IAAI,MAAM;AAAA,MACvB;AAAA,IACF,CAAC;AAAA,EACH;AACF,CAAC;",
6
+ "names": []
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acarmisc/backstage-plugin-litellm-backend",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "The Backstage backend plugin for LiteLLM governance",
5
5
  "backstage": {
6
6
  "role": "backend-plugin",