@acarmisc/backstage-plugin-litellm-backend 0.2.0 → 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/router.js CHANGED
@@ -1,8 +1,41 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.ProvisioningError = void 0;
4
37
  exports.createRouter = createRouter;
5
- const express_1 = require("express");
38
+ const express_1 = __importStar(require("express"));
6
39
  const catalog_client_1 = require("@backstage/catalog-client");
7
40
  const client_1 = require("./client");
8
41
  const provisioning_1 = require("./provisioning");
@@ -17,9 +50,15 @@ async function createRouter(options) {
17
50
  const roleConfigs = (0, provisioning_1.readRoleConfigs)(config);
18
51
  const catalogClient = new catalog_client_1.CatalogClient({ discoveryApi: discovery });
19
52
  if (provisioningEnabled) {
20
- logger.info(`LiteLLM auto-provisioning enabled — defaults: budget=$${provisioningDefaults.maxBudget}/${provisioningDefaults.budgetDuration}, models=${provisioningDefaults.models.length ? provisioningDefaults.models.join(',') : 'all'}, teams=[${provisioningDefaults.teams.join(',')}]`);
53
+ logger.info(`LiteLLM auto-provisioning enabled — defaults: budget=$${provisioningDefaults.maxBudget}/${provisioningDefaults.budgetDuration}, models=${provisioningDefaults.models.length
54
+ ? provisioningDefaults.models.join(',')
55
+ : 'all'}, teams=[${provisioningDefaults.teams.join(',')}]`);
21
56
  }
22
57
  const router = (0, express_1.Router)();
58
+ // JSON body parser. Without this, every POST/PUT endpoint sees an empty
59
+ // req.body. Backstage's httpRouter does not apply a body parser at the
60
+ // plugin-router level, so each plugin must attach its own.
61
+ router.use(express_1.default.json());
23
62
  router.get('/health', (_req, res) => {
24
63
  res.json({ status: 'ok', provisioning: provisioningEnabled });
25
64
  });
@@ -62,13 +101,52 @@ async function createRouter(options) {
62
101
  });
63
102
  router.post('/keys/generate', async (req, res) => {
64
103
  try {
104
+ // Only alias + max_budget are required. An empty models array is
105
+ // intentional — in LiteLLM `models: []` means "all models the user
106
+ // can access" which is the desired default. Forcing a selection
107
+ // up front is too restrictive for the common case.
108
+ const body = (req.body ?? {});
109
+ const missing = [];
110
+ if (!body.alias?.trim())
111
+ missing.push('alias');
112
+ if (typeof body.max_budget !== 'number' || body.max_budget <= 0) {
113
+ missing.push('max_budget (positive number)');
114
+ }
115
+ if (missing.length) {
116
+ res.status(400).json({
117
+ error: 'Missing required fields',
118
+ hint: `Required: ${missing.join(', ')}`,
119
+ });
120
+ return;
121
+ }
65
122
  const tokenEntityRef = await (0, provisioning_1.resolveUserId)(req, auth);
66
- const resolvedUserId = tokenEntityRef ? (0, provisioning_1.toLiteLLMUserId)(tokenEntityRef, userIdDomain) : undefined;
123
+ const resolvedUserId = tokenEntityRef
124
+ ? (0, provisioning_1.toLiteLLMUserId)(tokenEntityRef, userIdDomain)
125
+ : undefined;
67
126
  if (resolvedUserId) {
68
127
  await (0, provisioning_1.getOrProvisionUser)(client, tokenEntityRef, resolvedUserId, provisioningEnabled, provisioningDefaults, roleConfigs, catalogClient, auth, logger);
69
128
  }
129
+ // Stamp ownership into LiteLLM key metadata. LiteLLM's native
130
+ // `created_by` column is only populated when the caller authenticates
131
+ // via JWT/SSO; we always call with the master key, so that column
132
+ // stays null. Enriching `metadata` makes the owner identity visible
133
+ // in LiteLLM's UI and queryable via API.
134
+ const profile = tokenEntityRef
135
+ ? await (0, provisioning_1.resolveUserProfile)(tokenEntityRef, catalogClient, auth, logger)
136
+ : {};
137
+ const enrichedMetadata = {
138
+ ...(body.metadata ?? {}),
139
+ created_by_backstage_user: tokenEntityRef ?? 'unknown',
140
+ ...(profile.email && { created_by_email: profile.email }),
141
+ ...(profile.displayName && {
142
+ created_by_display_name: profile.displayName,
143
+ }),
144
+ created_via: 'backstage',
145
+ created_at_iso: new Date().toISOString(),
146
+ };
70
147
  const request = {
71
- ...req.body,
148
+ ...body,
149
+ metadata: enrichedMetadata,
72
150
  ...(resolvedUserId && { user_id: resolvedUserId }),
73
151
  };
74
152
  const result = await client.generateKey(request);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/types.ts"],
4
- "sourcesContent": ["export interface UserInfo {\n user_id: string;\n user_email?: string;\n email?: string;\n teams?: string[];\n models?: string[];\n max_budget?: number;\n spend?: number;\n current_spend?: number;\n soft_limit?: number;\n hard_limit?: number;\n}\n\nexport interface TeamMember {\n user_id: string;\n role: 'admin' | 'user';\n}\n\nexport interface TeamInfo {\n team_id: string;\n team_alias?: string;\n max_budget?: number;\n spend: number;\n members_with_roles?: TeamMember[];\n models?: string[];\n tpm_limit?: number;\n rpm_limit?: number;\n}\n\nexport interface VirtualKey {\n key: string;\n key_alias?: string;\n created_at: string;\n expires_at?: string;\n spend: number;\n max_budget?: number;\n tpm_limit?: number;\n rpm_limit?: number;\n models?: string[];\n user_id?: string;\n}\n\nexport interface ModelInfo {\n model_name: string;\n mode: string;\n supports_function_calling?: boolean;\n supports_vision?: boolean;\n input_cost_per_token?: number;\n output_cost_per_token?: number;\n}\n\nexport interface UsageModelBreakdown {\n total_spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n}\n\nexport interface UsageKeyBreakdown {\n key_alias?: string;\n team_id?: string | null;\n models: string[];\n total_spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n}\n\nexport interface UsageDailyPoint {\n date: string;\n spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n}\n\nexport interface UsageDailyModelPoint {\n date: string;\n model: string;\n spend: number;\n prompt_tokens: number;\n completion_tokens: number;\n total_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n}\n\nexport interface UsageMetrics {\n total_spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n usage_by_model: Record<string, UsageModelBreakdown>;\n usage_by_key: Record<string, UsageKeyBreakdown>;\n daily_usage: UsageDailyPoint[];\n daily_by_model: UsageDailyModelPoint[];\n}\n\nexport interface GenerateKeyRequest {\n alias?: string;\n models?: string[];\n duration?: string;\n max_budget?: number;\n tpm_limit?: number;\n rpm_limit?: number;\n user_id?: string;\n team_id?: string;\n key_type?: string;\n}\n\nexport interface UpdateKeyRequest {\n key: string;\n key_alias?: string;\n models?: string[];\n max_budget?: number;\n tpm_limit?: number;\n rpm_limit?: number;\n team_id?: string;\n duration?: string;\n}\n\nexport interface GenerateKeyResponse {\n key: string;\n key_alias?: string;\n expires_at?: string;\n max_budget?: number;\n tpm_limit?: number;\n rpm_limit?: number;\n models?: string[];\n}\n\nexport interface DeleteKeyRequest {\n keys: string[];\n}\n\nexport interface LiteLLMConfig {\n baseUrl: string;\n masterKey: string;\n}\n\nexport interface ProvisioningDefaults {\n maxBudget: number;\n budgetDuration: string;\n models: string[];\n teams: string[];\n tpmLimit?: number;\n rpmLimit?: number;\n metadata: Record<string, string>;\n}\n\nexport interface RoleConfig {\n group: string;\n maxBudget?: number;\n budgetDuration?: string;\n models?: string[];\n teams?: string[];\n tpmLimit?: number;\n rpmLimit?: number;\n metadata?: Record<string, string>;\n}\n\nexport interface CreateUserRequest {\n user_id: string;\n user_email?: string;\n max_budget?: number;\n budget_duration?: string;\n models?: string[];\n teams?: string[];\n tpm_limit?: number;\n rpm_limit?: number;\n metadata?: Record<string, string>;\n}\n\nexport interface CreateUserResponse {\n user_id: string;\n user_email?: string;\n max_budget?: number;\n models?: string[];\n teams?: string[];\n}\n"],
4
+ "sourcesContent": ["export interface UserInfo {\n user_id: string;\n user_email?: string;\n email?: string;\n teams?: string[];\n models?: string[];\n max_budget?: number;\n spend?: number;\n current_spend?: number;\n soft_limit?: number;\n hard_limit?: number;\n}\n\nexport interface TeamMember {\n user_id: string;\n role: 'admin' | 'user';\n}\n\nexport interface TeamInfo {\n team_id: string;\n team_alias?: string;\n max_budget?: number;\n spend: number;\n members_with_roles?: TeamMember[];\n models?: string[];\n tpm_limit?: number;\n rpm_limit?: number;\n}\n\nexport interface VirtualKey {\n key: string;\n token: string;\n key_alias?: string;\n created_at: string;\n expires_at?: string;\n spend: number;\n max_budget?: number;\n tpm_limit?: number;\n rpm_limit?: number;\n models?: string[];\n user_id?: string;\n}\n\n/**\n * Shape of a single entry inside LiteLLM's `/user/info` `keys` array.\n * Differs from VirtualKey: uses `expires` (not `expires_at`), exposes\n * both a hashed `token` and a masked `key_name`, and fields are nullable\n * rather than optional.\n */\nexport interface LiteLLMUserKey {\n token: string;\n key_name?: string;\n key_alias?: string | null;\n spend?: number;\n expires?: string | null;\n models?: string[];\n tpm_limit?: number | null;\n rpm_limit?: number | null;\n max_budget?: number | null;\n user_id?: string | null;\n team_id?: string | null;\n created_at: string;\n}\n\nexport interface ModelInfo {\n model_name: string;\n mode: string;\n supports_function_calling?: boolean;\n supports_vision?: boolean;\n input_cost_per_token?: number;\n output_cost_per_token?: number;\n}\n\nexport interface UsageModelBreakdown {\n total_spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n}\n\nexport interface UsageKeyBreakdown {\n key_alias?: string;\n team_id?: string | null;\n models: string[];\n total_spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n}\n\nexport interface UsageDailyPoint {\n date: string;\n spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n}\n\nexport interface UsageDailyModelPoint {\n date: string;\n model: string;\n spend: number;\n prompt_tokens: number;\n completion_tokens: number;\n total_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n}\n\nexport interface UsageMetrics {\n total_spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n api_requests: number;\n successful_requests: number;\n failed_requests: number;\n usage_by_model: Record<string, UsageModelBreakdown>;\n usage_by_key: Record<string, UsageKeyBreakdown>;\n daily_usage: UsageDailyPoint[];\n daily_by_model: UsageDailyModelPoint[];\n}\n\nexport interface GenerateKeyRequest {\n alias?: string;\n models?: string[];\n duration?: string;\n max_budget?: number;\n tpm_limit?: number;\n rpm_limit?: number;\n user_id?: string;\n team_id?: string;\n key_type?: string;\n metadata?: Record<string, string>;\n}\n\nexport interface UpdateKeyRequest {\n key: string;\n key_alias?: string;\n models?: string[];\n max_budget?: number;\n tpm_limit?: number;\n rpm_limit?: number;\n team_id?: string;\n duration?: string;\n}\n\nexport interface GenerateKeyResponse {\n key: string;\n key_alias?: string;\n expires_at?: string;\n max_budget?: number;\n tpm_limit?: number;\n rpm_limit?: number;\n models?: string[];\n}\n\nexport interface DeleteKeyRequest {\n keys: string[];\n}\n\nexport interface LiteLLMConfig {\n baseUrl: string;\n masterKey: string;\n}\n\nexport interface ProvisioningDefaults {\n maxBudget: number;\n budgetDuration: string;\n models: string[];\n teams: string[];\n tpmLimit?: number;\n rpmLimit?: number;\n /**\n * LiteLLM user role applied on /user/new. Defaults to \"internal_user\"\n * which grants self-service Create/Delete/View on the user's own keys.\n * Valid values: proxy_admin, proxy_admin_viewer, internal_user,\n * internal_user_viewer, team.\n */\n userRole?: string;\n metadata: Record<string, string>;\n}\n\nexport interface RoleConfig {\n group: string;\n maxBudget?: number;\n budgetDuration?: string;\n models?: string[];\n teams?: string[];\n tpmLimit?: number;\n rpmLimit?: number;\n userRole?: string;\n metadata?: Record<string, string>;\n}\n\nexport interface CreateUserRequest {\n user_id: string;\n user_email?: string;\n user_alias?: string;\n user_role?: string;\n max_budget?: number;\n budget_duration?: string;\n models?: string[];\n teams?: string[];\n tpm_limit?: number;\n rpm_limit?: number;\n metadata?: Record<string, string>;\n auto_create_key?: boolean;\n}\n\nexport interface CreateUserResponse {\n user_id: string;\n user_email?: string;\n max_budget?: number;\n models?: string[];\n teams?: string[];\n}\n"],
5
5
  "mappings": ";;;;;;;;;;;;;;;;AAAA;AAAA;",
6
6
  "names": []
7
7
  }
package/dist/types.d.ts CHANGED
@@ -26,6 +26,7 @@ export interface TeamInfo {
26
26
  }
27
27
  export interface VirtualKey {
28
28
  key: string;
29
+ token: string;
29
30
  key_alias?: string;
30
31
  created_at: string;
31
32
  expires_at?: string;
@@ -36,6 +37,26 @@ export interface VirtualKey {
36
37
  models?: string[];
37
38
  user_id?: string;
38
39
  }
40
+ /**
41
+ * Shape of a single entry inside LiteLLM's `/user/info` `keys` array.
42
+ * Differs from VirtualKey: uses `expires` (not `expires_at`), exposes
43
+ * both a hashed `token` and a masked `key_name`, and fields are nullable
44
+ * rather than optional.
45
+ */
46
+ export interface LiteLLMUserKey {
47
+ token: string;
48
+ key_name?: string;
49
+ key_alias?: string | null;
50
+ spend?: number;
51
+ expires?: string | null;
52
+ models?: string[];
53
+ tpm_limit?: number | null;
54
+ rpm_limit?: number | null;
55
+ max_budget?: number | null;
56
+ user_id?: string | null;
57
+ team_id?: string | null;
58
+ created_at: string;
59
+ }
39
60
  export interface ModelInfo {
40
61
  model_name: string;
41
62
  mode: string;
@@ -109,6 +130,7 @@ export interface GenerateKeyRequest {
109
130
  user_id?: string;
110
131
  team_id?: string;
111
132
  key_type?: string;
133
+ metadata?: Record<string, string>;
112
134
  }
113
135
  export interface UpdateKeyRequest {
114
136
  key: string;
@@ -143,6 +165,13 @@ export interface ProvisioningDefaults {
143
165
  teams: string[];
144
166
  tpmLimit?: number;
145
167
  rpmLimit?: number;
168
+ /**
169
+ * LiteLLM user role applied on /user/new. Defaults to "internal_user"
170
+ * which grants self-service Create/Delete/View on the user's own keys.
171
+ * Valid values: proxy_admin, proxy_admin_viewer, internal_user,
172
+ * internal_user_viewer, team.
173
+ */
174
+ userRole?: string;
146
175
  metadata: Record<string, string>;
147
176
  }
148
177
  export interface RoleConfig {
@@ -153,11 +182,14 @@ export interface RoleConfig {
153
182
  teams?: string[];
154
183
  tpmLimit?: number;
155
184
  rpmLimit?: number;
185
+ userRole?: string;
156
186
  metadata?: Record<string, string>;
157
187
  }
158
188
  export interface CreateUserRequest {
159
189
  user_id: string;
160
190
  user_email?: string;
191
+ user_alias?: string;
192
+ user_role?: string;
161
193
  max_budget?: number;
162
194
  budget_duration?: string;
163
195
  models?: string[];
@@ -165,6 +197,7 @@ export interface CreateUserRequest {
165
197
  tpm_limit?: number;
166
198
  rpm_limit?: number;
167
199
  metadata?: Record<string, string>;
200
+ auto_create_key?: boolean;
168
201
  }
169
202
  export interface CreateUserResponse {
170
203
  user_id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acarmisc/backstage-plugin-litellm-backend",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "The Backstage backend plugin for LiteLLM governance",
5
5
  "backstage": {
6
6
  "role": "backend-plugin",
@@ -23,8 +23,8 @@
23
23
  "config.d.ts"
24
24
  ],
25
25
  "scripts": {
26
- "build": "echo 'Build dist/ from Backstage monorepo: backstage-cli package build'",
27
- "prepack": "echo 'Dist files must be built in Backstage monorepo and copied here'",
26
+ "build": "node build.js && tsc -p tsconfig.json",
27
+ "prepack": "npm run build",
28
28
  "postpack": ""
29
29
  },
30
30
  "dependencies": {