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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 { createRouter } from './router';\nexport * from './types';\nexport { LiteLLMClient } from './client';", "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 { 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 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 } = 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=$${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 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 const tokenEntityRef = await resolveUserId(req, auth);\n const resolvedUserId = tokenEntityRef ? toLiteLLMUserId(tokenEntityRef, userIdDomain) : 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 const request: GenerateKeyRequest = {\n ...req.body,\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 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>(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 try {\n const response = await this.request<{ info: VirtualKey[] } | VirtualKey[]>(`/key/info${query}`);\n return Array.isArray(response) ? response : (response.info ?? []);\n } catch (err: any) {\n if (err.status === 404 || err.message.includes('not found')) {\n return [];\n }\n throw err;\n }\n }\n\n async generateKey(request: GenerateKeyRequest): Promise<GenerateKeyResponse> {\n return this.request<GenerateKeyResponse>('/key/generate', {\n method: 'POST',\n body: JSON.stringify({ json: request }),\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[]>('/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 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) ? response.results : [];\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(startDate: string, endDate: string, userId?: string, _groupBy?: string): 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>(`/user/daily/activity?${params.toString()}`);\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(teamId: string, startDate: string, endDate: string): 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>(`/team/daily/activity?${params.toString()}`);\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 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": []
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"]
7
7
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { litellmPlugin } from './plugin';
2
+ export { litellmPlugin as default } from './plugin';
2
3
  export { createRouter } from './router';
3
4
  export * from './types';
4
5
  export { LiteLLMClient } from './client';
package/dist/index.js CHANGED
@@ -14,9 +14,11 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.LiteLLMClient = exports.createRouter = exports.litellmPlugin = void 0;
17
+ exports.LiteLLMClient = exports.createRouter = exports.default = exports.litellmPlugin = void 0;
18
18
  var plugin_1 = require("./plugin");
19
19
  Object.defineProperty(exports, "litellmPlugin", { enumerable: true, get: function () { return plugin_1.litellmPlugin; } });
20
+ var plugin_2 = require("./plugin");
21
+ Object.defineProperty(exports, "default", { enumerable: true, get: function () { return plugin_2.litellmPlugin; } });
20
22
  var router_1 = require("./router");
21
23
  Object.defineProperty(exports, "createRouter", { enumerable: true, get: function () { return router_1.createRouter; } });
22
24
  __exportStar(require("./types"), exports);
@@ -45,11 +45,26 @@ export declare function readProvisioningDefaults(config: Config): {
45
45
  * the request carries no user credential (service-to-service calls).
46
46
  */
47
47
  export declare function resolveUserId(req: Request, auth: AuthService): Promise<string | undefined>;
48
+ /**
49
+ * Profile data extracted from a Backstage Catalog User entity, used to
50
+ * populate user_email / user_alias on the LiteLLM record.
51
+ */
52
+ export interface BackstageUserProfile {
53
+ email?: string;
54
+ displayName?: string;
55
+ }
56
+ /**
57
+ * Looks up the catalog User entity for the authenticated user and returns
58
+ * the profile block. Returns an empty object when the user has no catalog
59
+ * entity (e.g. dangerouslyAllowSignInWithoutUserInCatalog was used) — the
60
+ * caller falls back to deriving identity from userIdDomain.
61
+ */
62
+ export declare function resolveUserProfile(userEntityRef: string, catalogClient: CatalogClient, auth: AuthService, logger: any): Promise<BackstageUserProfile>;
48
63
  /**
49
64
  * Creates a LiteLLM user for the given Backstage identity using the configured
50
65
  * defaults. Returns the UserInfo of the newly created account.
51
66
  */
52
- export declare function provisionUser(client: LiteLLMClient, userId: string, defaults: ProvisioningDefaults, logger: any): Promise<UserInfo | null>;
67
+ export declare function provisionUser(client: LiteLLMClient, userId: string, defaults: ProvisioningDefaults, profile: BackstageUserProfile, backstageEntity: string | undefined, logger: any): Promise<UserInfo | null>;
53
68
  export declare class ProvisioningError extends Error {
54
69
  status: number;
55
70
  body: {
@@ -57,7 +72,7 @@ export declare class ProvisioningError extends Error {
57
72
  hint: string;
58
73
  provisioning: boolean;
59
74
  };
60
- constructor(message: string, hint: string, provisioning: boolean);
75
+ constructor(message: string, hint: string, provisioning: boolean, status?: number);
61
76
  }
62
77
  /**
63
78
  * Ensures the LiteLLM user exists, returning its UserInfo.
@@ -6,6 +6,7 @@ exports.readRoleConfigs = readRoleConfigs;
6
6
  exports.applyRoleOverrides = applyRoleOverrides;
7
7
  exports.readProvisioningDefaults = readProvisioningDefaults;
8
8
  exports.resolveUserId = resolveUserId;
9
+ exports.resolveUserProfile = resolveUserProfile;
9
10
  exports.provisionUser = provisionUser;
10
11
  exports.getOrProvisionUser = getOrProvisionUser;
11
12
  exports.resolveUserRole = resolveUserRole;
@@ -22,6 +23,13 @@ exports.resolveUserRole = resolveUserRole;
22
23
  */
23
24
  function toLiteLLMUserId(userEntityRef, userIdDomain) {
24
25
  const name = userEntityRef.split('/').pop() ?? userEntityRef;
26
+ // Defensive: if the entity name is already email-shaped (e.g. when the
27
+ // Keycloak provider imports usernames as full emails without our
28
+ // name-rewrite transformer running, or when a catalog change leaves
29
+ // an entity ref like "user:default/foo@bar.it"), do NOT append the
30
+ // userIdDomain — that produced "foo@bar.it@bar.it" in production.
31
+ if (name.includes('@'))
32
+ return name;
25
33
  return userIdDomain ? `${name}@${userIdDomain}` : name;
26
34
  }
27
35
  /**
@@ -50,6 +58,7 @@ function readRoleConfigs(config) {
50
58
  teams: r.teams,
51
59
  tpmLimit: r.tpmLimit,
52
60
  rpmLimit: r.rpmLimit,
61
+ userRole: r.userRole,
53
62
  metadata: r.metadata,
54
63
  }));
55
64
  }
@@ -64,6 +73,7 @@ function applyRoleOverrides(defaults, role) {
64
73
  teams: role.teams ?? defaults.teams,
65
74
  tpmLimit: role.tpmLimit ?? defaults.tpmLimit,
66
75
  rpmLimit: role.rpmLimit ?? defaults.rpmLimit,
76
+ userRole: role.userRole ?? defaults.userRole,
67
77
  metadata: { ...defaults.metadata, ...(role.metadata ?? {}) },
68
78
  };
69
79
  }
@@ -72,11 +82,15 @@ function readProvisioningDefaults(config) {
72
82
  const defaults = {
73
83
  maxBudget: config.getOptionalNumber('litellm.provisioning.defaults.maxBudget') ?? 10,
74
84
  budgetDuration: config.getOptionalString('litellm.provisioning.defaults.budgetDuration') ?? '30d',
75
- models: config.getOptionalStringArray('litellm.provisioning.defaults.models') ?? [],
76
- teams: config.getOptionalStringArray('litellm.provisioning.defaults.teams') ?? [],
85
+ models: config.getOptionalStringArray('litellm.provisioning.defaults.models') ??
86
+ [],
87
+ teams: config.getOptionalStringArray('litellm.provisioning.defaults.teams') ??
88
+ [],
77
89
  tpmLimit: config.getOptionalNumber('litellm.provisioning.defaults.tpmLimit'),
78
90
  rpmLimit: config.getOptionalNumber('litellm.provisioning.defaults.rpmLimit'),
79
- metadata: (config.getOptional('litellm.provisioning.defaults.metadata') ?? {}),
91
+ userRole: config.getOptionalString('litellm.provisioning.defaults.userRole') ??
92
+ 'internal_user',
93
+ metadata: config.getOptional('litellm.provisioning.defaults.metadata') ?? {},
80
94
  };
81
95
  return { enabled, defaults };
82
96
  }
@@ -101,41 +115,119 @@ async function resolveUserId(req, auth) {
101
115
  }
102
116
  return undefined;
103
117
  }
118
+ /**
119
+ * Looks up the catalog User entity for the authenticated user and returns
120
+ * the profile block. Returns an empty object when the user has no catalog
121
+ * entity (e.g. dangerouslyAllowSignInWithoutUserInCatalog was used) — the
122
+ * caller falls back to deriving identity from userIdDomain.
123
+ */
124
+ async function resolveUserProfile(userEntityRef, catalogClient, auth, logger) {
125
+ try {
126
+ const { token } = await auth.getPluginRequestToken({
127
+ onBehalfOf: await auth.getOwnServiceCredentials(),
128
+ targetPluginId: 'catalog',
129
+ });
130
+ const entity = await catalogClient.getEntityByRef(userEntityRef, { token });
131
+ const profile = entity?.spec?.profile ?? {};
132
+ return {
133
+ email: profile.email,
134
+ displayName: profile.displayName,
135
+ };
136
+ }
137
+ catch (err) {
138
+ logger.warn(`Could not fetch catalog profile for ${userEntityRef}: ${err.message}`);
139
+ return {};
140
+ }
141
+ }
104
142
  /**
105
143
  * Creates a LiteLLM user for the given Backstage identity using the configured
106
144
  * defaults. Returns the UserInfo of the newly created account.
107
145
  */
108
- async function provisionUser(client, userId, defaults, logger) {
146
+ async function provisionUser(client, userId, defaults, profile, backstageEntity, logger) {
109
147
  const payload = {
110
148
  user_id: userId,
149
+ ...(profile.email && { user_email: profile.email }),
150
+ ...(profile.displayName && { user_alias: profile.displayName }),
111
151
  max_budget: defaults.maxBudget,
112
152
  budget_duration: defaults.budgetDuration,
113
153
  models: defaults.models,
114
154
  teams: defaults.teams,
115
155
  ...(defaults.tpmLimit !== undefined && { tpm_limit: defaults.tpmLimit }),
116
156
  ...(defaults.rpmLimit !== undefined && { rpm_limit: defaults.rpmLimit }),
157
+ ...(defaults.userRole && { user_role: defaults.userRole }),
158
+ auto_create_key: false,
117
159
  metadata: {
118
160
  ...defaults.metadata,
119
161
  provisioned_by: 'backstage',
120
162
  provisioned_at: new Date().toISOString(),
121
- backstage_entity: userId,
163
+ backstage_entity: backstageEntity ?? userId,
164
+ ...(profile.email && { backstage_email: profile.email }),
165
+ ...(profile.displayName && {
166
+ backstage_display_name: profile.displayName,
167
+ }),
122
168
  },
123
169
  };
124
- logger.info(`Provisioning new LiteLLM user for Backstage identity: ${userId}`);
170
+ logger.info(`Provisioning new LiteLLM user for Backstage identity: ${userId}` +
171
+ (profile.email ? ` (email=${profile.email})` : ''));
125
172
  try {
126
173
  await client.createUser(payload);
174
+ // Defensive /user/update: LiteLLM's /user/new upsert path has been
175
+ // observed to drop user_role under concurrent inserts (the first
176
+ // call sets the field, a racing second call upserts and clears it).
177
+ // Re-asserting the role-bearing fields immediately after creation
178
+ // is cheap and makes the role guarantee robust.
179
+ if (defaults.userRole) {
180
+ try {
181
+ await client.updateUser({
182
+ user_id: userId,
183
+ user_role: defaults.userRole,
184
+ ...(profile.email && { user_email: profile.email }),
185
+ ...(profile.displayName && { user_alias: profile.displayName }),
186
+ });
187
+ }
188
+ catch (updateErr) {
189
+ logger.warn(`Defensive /user/update after provisioning ${userId} failed: ${updateErr.message}`);
190
+ }
191
+ }
127
192
  // Fetch the freshly-created user record to return consistent UserInfo shape
128
193
  return await client.getUserInfo(userId);
129
194
  }
130
195
  catch (err) {
131
196
  logger.error(`Failed to provision LiteLLM user ${userId}: ${err.message}`);
132
- return null;
197
+ throw err;
133
198
  }
134
199
  }
200
+ /**
201
+ * Module-scope single-flight cache keyed by LiteLLM user_id. Coalesces
202
+ * concurrent provisioning attempts for the same user so /user/new fires
203
+ * at most once per user across parallel requests. Without this, an
204
+ * authenticated page load that fires /keys, /teams and /usage in
205
+ * parallel triggers three concurrent /user/new calls; LiteLLM's
206
+ * upsert path then creates one default key per call (so the user lands
207
+ * with 3 unexpected keys) and may silently lose user_role.
208
+ *
209
+ * Cache entries are removed once the promise settles, so subsequent
210
+ * requests for a re-deleted user can still trigger fresh provisioning.
211
+ */
212
+ const provisioningInFlight = new Map();
213
+ /**
214
+ * Strips any echoed Authorization bearer token from upstream LiteLLM error
215
+ * messages before they're shipped back to the browser. LiteLLM normally does
216
+ * not echo the master key, but defense in depth: never let a `Bearer …`
217
+ * substring travel out in a response body.
218
+ */
219
+ function sanitizeUpstreamMessage(message) {
220
+ if (!message)
221
+ return 'unknown error';
222
+ return message
223
+ .replace(/Bearer\s+[A-Za-z0-9._\-+/=]+/g, 'Bearer [redacted]')
224
+ .replace(/sk-[A-Za-z0-9_\-]{8,}/g, 'sk-[redacted]')
225
+ .slice(0, 500);
226
+ }
135
227
  class ProvisioningError extends Error {
136
- constructor(message, hint, provisioning) {
228
+ constructor(message, hint, provisioning, status = 404) {
137
229
  super(message);
138
- this.status = 404;
230
+ this.status = status;
139
231
  this.body = { error: message, hint, provisioning };
140
232
  }
141
233
  }
@@ -149,27 +241,78 @@ async function getOrProvisionUser(client, tokenEntityRef, userId, provisioningEn
149
241
  if (!userId) {
150
242
  throw new ProvisioningError('User not found in LiteLLM', 'No user identity could be resolved from the request.', provisioningEnabled);
151
243
  }
152
- let userInfo = await client.getUserInfo(userId);
153
- if (!userInfo) {
154
- if (provisioningEnabled) {
155
- const catalogRef = tokenEntityRef ?? userId;
156
- const matchedRole = await resolveUserRole(catalogRef, roleConfigs, catalogClient, auth, logger);
157
- const effectiveDefaults = matchedRole
158
- ? applyRoleOverrides(provisioningDefaults, matchedRole)
159
- : provisioningDefaults;
160
- if (matchedRole) {
161
- logger.info(`User ${userId} matched role group ${matchedRole.group} using role-specific provisioning`);
244
+ const existing = await client.getUserInfo(userId);
245
+ if (existing) {
246
+ return existing;
247
+ }
248
+ if (!provisioningEnabled) {
249
+ throw new ProvisioningError('User not found in LiteLLM', 'Enable litellm.provisioning.enabled in app-config.yaml or create the user manually', false);
250
+ }
251
+ // Single-flight: if another request for the same userId is already
252
+ // provisioning, await its result instead of starting a new /user/new.
253
+ // This collapses the /keys + /teams + /usage page-load thundering
254
+ // herd into a single LiteLLM round-trip.
255
+ const pending = provisioningInFlight.get(userId);
256
+ if (pending) {
257
+ logger.info(`LiteLLM provisioning already in flight for ${userId} — joining`);
258
+ return pending;
259
+ }
260
+ const provisionPromise = (async () => {
261
+ const catalogRef = tokenEntityRef ?? userId;
262
+ const [matchedRole, profile] = await Promise.all([
263
+ resolveUserRole(catalogRef, roleConfigs, catalogClient, auth, logger),
264
+ tokenEntityRef
265
+ ? resolveUserProfile(tokenEntityRef, catalogClient, auth, logger)
266
+ : Promise.resolve({}),
267
+ ]);
268
+ const effectiveDefaults = matchedRole
269
+ ? applyRoleOverrides(provisioningDefaults, matchedRole)
270
+ : provisioningDefaults;
271
+ if (matchedRole) {
272
+ logger.info(`User ${userId} matched role group ${matchedRole.group} — using role-specific provisioning`);
273
+ }
274
+ try {
275
+ const created = await provisionUser(client, userId, effectiveDefaults, profile, tokenEntityRef, logger);
276
+ if (!created) {
277
+ throw new ProvisioningError('User not found in LiteLLM', 'Provisioning attempted but returned no user — check LiteLLM logs', true);
162
278
  }
163
- userInfo = await provisionUser(client, userId, effectiveDefaults, logger);
279
+ return created;
164
280
  }
165
- if (!userInfo) {
166
- if (provisioningEnabled) {
167
- throw new ProvisioningError('User not found in LiteLLM', 'Provisioning attempted but failed — check LiteLLM logs', true);
281
+ catch (err) {
282
+ // The single-flight cache should prevent the parallel-409 race,
283
+ // but keep the recovery path: if /user/new still 409s (e.g.
284
+ // multi-replica deploys where the lock is per-process), treat
285
+ // it as "user exists" and re-fetch.
286
+ if (err.status === 409 || /already exists/i.test(err.message ?? '')) {
287
+ logger.info(`LiteLLM user ${userId} already exists during provisioning — re-fetching`);
288
+ const refetched = await client.getUserInfo(userId);
289
+ if (refetched) {
290
+ return refetched;
291
+ }
168
292
  }
169
- throw new ProvisioningError('User not found in LiteLLM', 'Enable litellm.provisioning.enabled in app-config.yaml or create the user manually', false);
293
+ if (err instanceof ProvisioningError) {
294
+ throw err;
295
+ }
296
+ // Map upstream LiteLLM status to a Backstage-safe gateway status.
297
+ // 401/403/5xx from LiteLLM mean the gateway (this plugin) cannot
298
+ // talk to LiteLLM — they MUST NOT propagate as 401/403 to the
299
+ // browser, otherwise Backstage's fetch middleware treats the
300
+ // user's Backstage session as expired and forces a re-login.
301
+ // Only safe client-semantic codes pass through.
302
+ const upstreamStatus = err.status;
303
+ const passThrough = [400, 404, 409, 422].includes(upstreamStatus)
304
+ ? upstreamStatus
305
+ : 502;
306
+ throw new ProvisioningError('LiteLLM auto-provisioning failed', `LiteLLM upstream ${upstreamStatus ?? 'error'}: ${sanitizeUpstreamMessage(err.message)}`, true, passThrough);
170
307
  }
308
+ })();
309
+ provisioningInFlight.set(userId, provisionPromise);
310
+ try {
311
+ return await provisionPromise;
312
+ }
313
+ finally {
314
+ provisioningInFlight.delete(userId);
171
315
  }
172
- return userInfo;
173
316
  }
174
317
  /**
175
318
  * Fetches the user's Backstage group memberships and returns the first matching