@acarmisc/backstage-plugin-litellm-backend 0.6.0 → 0.6.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/openapi.js CHANGED
@@ -105,8 +105,6 @@ exports.openApiSpec = {
105
105
  max_budget: { type: 'number', nullable: true, description: 'Positive number caps spend; null = unlimited' },
106
106
  tpm_limit: { type: 'number' },
107
107
  rpm_limit: { type: 'number' },
108
- auto_rotate: { type: 'boolean' },
109
- rotation_interval_days: { type: 'number' },
110
108
  } } } },
111
109
  },
112
110
  responses: {
@@ -123,14 +121,6 @@ exports.openApiSpec = {
123
121
  responses: { '200': { description: 'Deleted' }, '403': { description: 'Not the key owner' } },
124
122
  },
125
123
  },
126
- '/keys/{keyId}/regenerate': {
127
- post: {
128
- tags: ['Keys'],
129
- summary: 'Rotate a key in place (caller must own it)',
130
- parameters: [{ name: 'keyId', in: 'path', required: true, schema: { type: 'string' } }],
131
- responses: { '200': { description: 'New secret' }, '403': { description: 'Not the key owner' } },
132
- },
133
- },
134
124
  '/keys/{keyId}/update': {
135
125
  post: {
136
126
  tags: ['Keys'],
@@ -251,14 +241,6 @@ exports.openApiSpec = {
251
241
  responses: { '200': { description: 'Generated key' }, '401': { description: 'Invalid token' } },
252
242
  },
253
243
  },
254
- '/bridge/keys/regenerate': {
255
- post: {
256
- tags: ['Bridge'],
257
- summary: 'Rotate the caller key by alias (Keycloak token auth)',
258
- requestBody: { content: { 'application/json': { schema: { type: 'object', properties: { alias: { type: 'string' } } } } } },
259
- responses: { '200': { description: 'New secret' }, '401': { description: 'Invalid token' }, '404': { description: 'No key with that alias' } },
260
- },
261
- },
262
244
  '/bridge/models': {
263
245
  get: {
264
246
  tags: ['Bridge'],
package/dist/router.js CHANGED
@@ -173,9 +173,8 @@ async function createRouter(options) {
173
173
  // Every key-mutation route below runs under the LiteLLM master key, so
174
174
  // without an explicit check any authenticated Backstage user could act on
175
175
  // any key whose token they learn (audit logs expose truncated tokens, and
176
- // anyone who has held the raw key knows it). We port the same pattern the
177
- // bridge already uses (bridge.ts:bridgeRegenerateKey): fetch the caller's
178
- // own key list and 403 if the target token isn't in it.
176
+ // anyone who has held the raw key knows it). We fetch the caller's own key
177
+ // list and 403 if the target token isn't in it.
179
178
  //
180
179
  // Returns the caller's LiteLLM user_id (when resolvable) so handlers can
181
180
  // stamp it in logs without re-deriving it.
@@ -202,25 +201,6 @@ async function createRouter(options) {
202
201
  }
203
202
  return false;
204
203
  }
205
- router.post('/keys/:keyId/regenerate', async (req, res) => {
206
- try {
207
- const { keyId } = req.params;
208
- if (!keyId) {
209
- res.status(400).json({ error: 'keyId is required' });
210
- return;
211
- }
212
- const { tokenEntityRef } = await authorizeKeyAction(req, keyId);
213
- const result = await client.regenerateKey(keyId);
214
- logger.info({ action: 'key.rotate', userId: tokenEntityRef ?? 'unknown', keyId });
215
- res.json(result);
216
- }
217
- catch (error) {
218
- if (sendOwnershipError(error, res))
219
- return;
220
- logger.error('Failed to rotate key', error);
221
- res.status(500).json({ error: error.message });
222
- }
223
- });
224
204
  router.post('/keys/generate', async (req, res) => {
225
205
  try {
226
206
  // Only alias is hard-required. max_budget is optional: a positive
@@ -284,6 +264,24 @@ async function createRouter(options) {
284
264
  res.status(error.status).json(error.body);
285
265
  return;
286
266
  }
267
+ // LiteLLM's enterprise team-key hook silently overrides the requested
268
+ // `duration` with the team's `metadata.team_member_key_duration` when a
269
+ // team_id is set, before parsing it. A malformed value there 500s with
270
+ // this exact message regardless of what we sent — surface that instead
271
+ // of the opaque passthrough so it's actionable from the LiteLLM side.
272
+ if (typeof error.message === 'string' &&
273
+ error.message.includes('Invalid duration format') &&
274
+ req.body?.team_id) {
275
+ res.status(502).json({
276
+ error: 'LiteLLM rejected the key duration for this team. The team has a ' +
277
+ '"Team Member Key Duration" set in LiteLLM that is not in a valid ' +
278
+ '<number><unit> format (e.g. "30d") and overrides whatever duration ' +
279
+ 'is requested. Fix or clear it in LiteLLM under Teams → this team → ' +
280
+ 'Team Settings, then retry.',
281
+ teamId: req.body.team_id,
282
+ });
283
+ return;
284
+ }
287
285
  logger.error('Failed to generate key', error);
288
286
  res.status(500).json({ error: error.message });
289
287
  }
@@ -542,21 +540,6 @@ async function createRouter(options) {
542
540
  handleBridgeError(error, res);
543
541
  }
544
542
  });
545
- router.post('/bridge/keys/regenerate', async (req, res) => {
546
- try {
547
- const claims = await requireClaims(req);
548
- const alias = (req.body ?? {}).alias?.trim();
549
- if (!alias) {
550
- res.status(400).json({ error: 'alias is required' });
551
- return;
552
- }
553
- const result = await (0, bridge_1.bridgeRegenerateKey)(client, claims, provisioningEnabled, provisioningDefaults, logger, alias, userIdDomain);
554
- res.json(result);
555
- }
556
- catch (error) {
557
- handleBridgeError(error, res);
558
- }
559
- });
560
543
  router.get('/bridge/models', async (req, res) => {
561
544
  try {
562
545
  await requireClaims(req); // authenticate only
@@ -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 /** Backstage-computed: true when the user is a member of litellm.audit.group */\n can_view_audit?: boolean;\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 blocked?: boolean;\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 blocked?: boolean | null;\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 max_input_tokens?: number;\n max_output_tokens?: 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 /** When true, LiteLLM rotates the key on a schedule. */\n auto_rotate?: boolean;\n /** Rotation interval in days (LiteLLM-enforced when auto_rotate is true). */\n rotation_interval_days?: number;\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\nexport interface AuditLogEntry {\n id: string;\n updated_at: string;\n changed_by?: string;\n changed_by_api_key?: string;\n action?: string;\n table_name?: string;\n object_id?: string;\n before_value?: Record<string, unknown> | null;\n updated_values?: Record<string, unknown> | null;\n}\n\nexport interface PaginatedAuditLogs {\n audit_logs: AuditLogEntry[];\n total: number;\n page: number;\n page_size: number;\n total_pages: number;\n}\n\nexport interface AuditLogsParams {\n page?: number;\n page_size?: number;\n start_date?: string;\n end_date?: string;\n action?: string;\n table_name?: string;\n changed_by?: string;\n sort_by?: string;\n sort_order?: 'asc' | 'desc';\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 /** Backstage-computed: true when the user is a member of litellm.audit.group */\n can_view_audit?: boolean;\n}\n\nexport interface TeamMember {\n user_id: string;\n user_email?: 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 blocked?: boolean;\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 blocked?: boolean | null;\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 max_input_tokens?: number;\n max_output_tokens?: 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\nexport interface AuditLogEntry {\n id: string;\n updated_at: string;\n changed_by?: string;\n changed_by_api_key?: string;\n action?: string;\n table_name?: string;\n object_id?: string;\n before_value?: Record<string, unknown> | null;\n updated_values?: Record<string, unknown> | null;\n}\n\nexport interface PaginatedAuditLogs {\n audit_logs: AuditLogEntry[];\n total: number;\n page: number;\n page_size: number;\n total_pages: number;\n}\n\nexport interface AuditLogsParams {\n page?: number;\n page_size?: number;\n start_date?: string;\n end_date?: string;\n action?: string;\n table_name?: string;\n changed_by?: string;\n sort_by?: string;\n sort_order?: 'asc' | 'desc';\n}\n"],
5
5
  "mappings": ";;;;;;;;;;;;;;;;AAAA;AAAA;",
6
6
  "names": []
7
7
  }
package/dist/types.d.ts CHANGED
@@ -14,6 +14,7 @@ export interface UserInfo {
14
14
  }
15
15
  export interface TeamMember {
16
16
  user_id: string;
17
+ user_email?: string;
17
18
  role: 'admin' | 'user';
18
19
  }
19
20
  export interface TeamInfo {
@@ -137,10 +138,6 @@ export interface GenerateKeyRequest {
137
138
  team_id?: string;
138
139
  key_type?: string;
139
140
  metadata?: Record<string, string>;
140
- /** When true, LiteLLM rotates the key on a schedule. */
141
- auto_rotate?: boolean;
142
- /** Rotation interval in days (LiteLLM-enforced when auto_rotate is true). */
143
- rotation_interval_days?: number;
144
141
  }
145
142
  export interface UpdateKeyRequest {
146
143
  key: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acarmisc/backstage-plugin-litellm-backend",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "The Backstage backend plugin for LiteLLM governance",
5
5
  "backstage": {
6
6
  "role": "backend-plugin",