@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/client.d.ts +46 -0
- package/dist/client.js +136 -9
- package/dist/index.cjs.js +173 -14
- package/dist/index.cjs.js.map +3 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/provisioning.d.ts +17 -2
- package/dist/provisioning.js +168 -25
- package/dist/router.js +82 -4
- package/dist/types.cjs.js.map +1 -1
- package/dist/types.d.ts +33 -0
- package/package.json +3 -3
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
|
|
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
|
|
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
|
-
...
|
|
148
|
+
...body,
|
|
149
|
+
metadata: enrichedMetadata,
|
|
72
150
|
...(resolvedUserId && { user_id: resolvedUserId }),
|
|
73
151
|
};
|
|
74
152
|
const result = await client.generateKey(request);
|
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}\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.
|
|
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": "
|
|
27
|
-
"prepack": "
|
|
26
|
+
"build": "node build.js && tsc -p tsconfig.json",
|
|
27
|
+
"prepack": "npm run build",
|
|
28
28
|
"postpack": ""
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|