@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/bridge.d.ts +0 -10
- package/dist/bridge.js +0 -18
- package/dist/client.d.ts +0 -7
- package/dist/client.js +0 -9
- package/dist/index.cjs.js +8 -92
- package/dist/index.cjs.js.map +2 -2
- package/dist/openapi.js +0 -18
- package/dist/router.js +20 -37
- package/dist/types.cjs.js.map +1 -1
- package/dist/types.d.ts +1 -4
- package/package.json +1 -1
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
|
|
177
|
-
//
|
|
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
|
package/dist/types.cjs.js.map
CHANGED
|
@@ -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
|
|
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;
|