@acarmisc/backstage-plugin-litellm-backend 0.18.0 → 0.19.0
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/README.md +58 -0
- package/dist/bridge.d.ts +4 -6
- package/dist/bridge.js +10 -28
- package/dist/client.d.ts +7 -20
- package/dist/client.js +29 -107
- package/dist/errors.d.ts +2 -1
- package/dist/errors.js +4 -1
- package/dist/index.cjs.js +204 -405
- package/dist/index.cjs.js.map +4 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -3
- package/dist/provisioning.d.ts +2 -0
- package/dist/provisioning.js +11 -20
- package/dist/router.js +0 -16
- package/dist/routes/bridge.js +10 -19
- package/dist/routes/context.d.ts +0 -1
- package/dist/routes/keys.js +8 -21
- package/dist/routes/middleware/withUser.d.ts +3 -2
- package/dist/routes/middleware/withUser.js +1 -1
- package/dist/routes/objectPermissions.js +4 -8
- package/dist/routes/teamUsage.js +2 -3
- package/dist/routes/teams.js +7 -20
- package/dist/services/keyService.d.ts +10 -13
- package/dist/services/keyService.js +12 -47
- package/dist/types.cjs.js.map +1 -1
- package/dist/types.d.ts +0 -36
- package/package.json +3 -7
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Key generation service
|
|
3
|
+
* Key generation service.
|
|
4
4
|
*
|
|
5
5
|
* Extracts the common logic for creating API keys from both the UI (POST /keys/generate)
|
|
6
|
-
* and the bridge (POST /bridge/keys). Handles validation,
|
|
7
|
-
* and explicit upstream request construction.
|
|
6
|
+
* and the bridge (POST /bridge/keys). Handles validation, team/model checks,
|
|
7
|
+
* and explicit upstream request construction. Callers resolve the user beforehand.
|
|
8
8
|
*/
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
exports.KeyServiceError = void 0;
|
|
11
11
|
exports.createKeyForUser = createKeyForUser;
|
|
12
12
|
const provisioning_1 = require("../provisioning");
|
|
13
|
-
/** Unified identity for key creation — resolved from either Backstage or JWT claims. */
|
|
14
13
|
const modelAccess_1 = require("./modelAccess");
|
|
15
14
|
/** Error thrown during key creation — maps to HTTP responses. */
|
|
16
15
|
class KeyServiceError extends Error {
|
|
@@ -27,57 +26,21 @@ exports.KeyServiceError = KeyServiceError;
|
|
|
27
26
|
* Performs:
|
|
28
27
|
* - Schema validation of the input
|
|
29
28
|
* - Config flag enforcement (allowUnlimitedBudget, teamRequired)
|
|
30
|
-
* - User provisioning (single-flight, role configs)
|
|
31
29
|
* - Team membership validation
|
|
32
30
|
* - Model subset validation
|
|
33
31
|
* - Metadata stamping
|
|
34
32
|
* - Explicit upstream request construction and call
|
|
35
33
|
*
|
|
36
|
-
* @param user
|
|
34
|
+
* @param user Resolved identity: tokenEntityRef (optional) + userInfo (required, pre-provisioned)
|
|
37
35
|
* @param input Parsed GenerateKeyInput from the schema
|
|
38
36
|
* @param ctx Configuration and service clients
|
|
39
37
|
* @returns The new key response from LiteLLM
|
|
40
38
|
* @throws KeyServiceError with status and body for HTTP response
|
|
41
|
-
* @throws ProvisioningError for provisioning failures
|
|
42
39
|
* @throws LiteLLMUpstreamError for upstream LiteLLM errors
|
|
43
40
|
*/
|
|
44
41
|
async function createKeyForUser(user, input, ctx) {
|
|
45
|
-
const { client, config, catalogClient, auth, logger,
|
|
46
|
-
|
|
47
|
-
// For bridge (no catalogClient/auth), use direct provisioning from JWT;
|
|
48
|
-
// for UI (with catalogClient/auth), use full provisioning with group roles.
|
|
49
|
-
let userInfo;
|
|
50
|
-
if (catalogClient !== undefined && catalogClient !== null && auth !== undefined && auth !== null) {
|
|
51
|
-
userInfo = await (0, provisioning_1.getOrProvisionUser)(client, user.tokenEntityRef || '', user.userId, provisioningEnabled, provisioningDefaults, roleConfigs, catalogClient, auth, logger);
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
// Bridge path: just check if user exists, or provision with minimal info
|
|
55
|
-
userInfo = await client.getUserInfo(user.userId);
|
|
56
|
-
if (!userInfo) {
|
|
57
|
-
if (!provisioningEnabled) {
|
|
58
|
-
throw new KeyServiceError(404, {
|
|
59
|
-
error: 'User not found in LiteLLM',
|
|
60
|
-
detail: 'No LiteLLM user for this identity. Log in to Backstage once to be provisioned, or enable litellm.provisioning.enabled.',
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
// Provision with minimal info (just user_id, will use defaults)
|
|
64
|
-
const created = await client.createUser({
|
|
65
|
-
user_id: user.userId,
|
|
66
|
-
user_email: user.userId,
|
|
67
|
-
max_budget: provisioningDefaults.maxBudget,
|
|
68
|
-
budget_duration: provisioningDefaults.budgetDuration,
|
|
69
|
-
models: provisioningDefaults.models,
|
|
70
|
-
teams: provisioningDefaults.teams?.length ? [provisioningDefaults.teams[0]] : [],
|
|
71
|
-
});
|
|
72
|
-
if (!created) {
|
|
73
|
-
throw new KeyServiceError(500, {
|
|
74
|
-
error: 'User provisioning failed',
|
|
75
|
-
detail: 'Failed to create user in LiteLLM',
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
userInfo = await client.getUserInfo(user.userId);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
42
|
+
const { client, config, catalogClient, auth, logger, } = ctx;
|
|
43
|
+
const userInfo = user.userInfo;
|
|
81
44
|
// ── Enforce config flags ────────────────────────────────────────────
|
|
82
45
|
if (!config.allowUnlimitedBudget && (input.max_budget === null || input.max_budget === undefined)) {
|
|
83
46
|
throw new KeyServiceError(400, { error: 'max_budget is required when unlimited budgets are not allowed' });
|
|
@@ -127,13 +90,15 @@ async function createKeyForUser(user, input, ctx) {
|
|
|
127
90
|
? await (0, provisioning_1.resolveUserProfile)(user.tokenEntityRef, catalogClient, auth, logger)
|
|
128
91
|
: {};
|
|
129
92
|
const clientMetadata = input.metadata ?? {};
|
|
93
|
+
const createdVia = user.createdVia ?? 'backstage';
|
|
130
94
|
const serverMetadata = {
|
|
131
|
-
created_by_backstage_user: user.tokenEntityRef
|
|
95
|
+
...(user.tokenEntityRef && { created_by_backstage_user: user.tokenEntityRef }),
|
|
96
|
+
...(!user.tokenEntityRef && { created_by: userInfo.user_id }),
|
|
132
97
|
...(profile.email && { created_by_email: profile.email }),
|
|
133
98
|
...(profile.displayName && {
|
|
134
99
|
created_by_display_name: profile.displayName,
|
|
135
100
|
}),
|
|
136
|
-
created_via:
|
|
101
|
+
created_via: createdVia,
|
|
137
102
|
created_at_iso: new Date().toISOString(),
|
|
138
103
|
};
|
|
139
104
|
// Server-owned keys override client values
|
|
@@ -145,7 +110,7 @@ async function createKeyForUser(user, input, ctx) {
|
|
|
145
110
|
// Never spread the raw body; only include fields we've explicitly validated.
|
|
146
111
|
const upstreamRequest = {
|
|
147
112
|
alias: input.alias,
|
|
148
|
-
user_id:
|
|
113
|
+
user_id: userInfo.user_id,
|
|
149
114
|
};
|
|
150
115
|
if (input.models !== undefined) {
|
|
151
116
|
upstreamRequest.models = input.models;
|
|
@@ -174,6 +139,6 @@ async function createKeyForUser(user, input, ctx) {
|
|
|
174
139
|
upstreamRequest.metadata = enrichedMetadata;
|
|
175
140
|
// ── Call upstream LiteLLM ───────────────────────────────────────────
|
|
176
141
|
const result = await client.generateKey(upstreamRequest);
|
|
177
|
-
logger.info('key.generate', { userId:
|
|
142
|
+
logger.info('key.generate', { userId: userInfo.user_id, keyAlias: input.alias });
|
|
178
143
|
return result;
|
|
179
144
|
}
|
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": ["// Re-export common types for backward compatibility\nexport {\n type UserInfo,\n type TeamMember,\n type TeamObjectPermission,\n type TeamInfo,\n type CreateTeamResponse,\n type VirtualKey,\n type ModelInfo,\n type UsageModelBreakdown,\n type UsageKeyBreakdown,\n type UsageDailyPoint,\n type UsageDailyModelPoint,\n type UsageMetrics,\n type GenerateKeyRequest,\n type UpdateKeyRequest,\n type GenerateKeyResponse,\n type AuditLogEntry,\n type PaginatedAuditLogs,\n} from '@acarmisc/backstage-plugin-litellm-common';\n\nexport interface CreateTeamRequest {\n team_alias: string;\n models?: string[];\n max_budget?: number;\n budget_duration?: string;\n tpm_limit?: number;\n rpm_limit?: number;\n metadata?: Record<string, unknown>;\n object_permission?: {\n vector_stores?: string[];\n mcp_servers?: string[];\n mcp_access_groups?: string[];\n };\n}\n\nexport interface UpdateTeamRequest extends Partial<CreateTeamRequest> {\n team_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 budget_duration?: string | null;\n user_id?: string | null;\n team_id?: string | null;\n created_at: string;\n blocked?: boolean | null;\n /** Arbitrary metadata stored on the key record. */\n metadata?: Record<string, unknown> | null;\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\n
|
|
4
|
+
"sourcesContent": ["// Re-export common types for backward compatibility\nexport {\n type UserInfo,\n type TeamMember,\n type TeamObjectPermission,\n type TeamInfo,\n type CreateTeamResponse,\n type VirtualKey,\n type ModelInfo,\n type UsageModelBreakdown,\n type UsageKeyBreakdown,\n type UsageDailyPoint,\n type UsageDailyModelPoint,\n type UsageMetrics,\n type GenerateKeyRequest,\n type UpdateKeyRequest,\n type GenerateKeyResponse,\n type AuditLogEntry,\n type PaginatedAuditLogs,\n} from '@acarmisc/backstage-plugin-litellm-common';\n\nexport interface CreateTeamRequest {\n team_alias: string;\n models?: string[];\n max_budget?: number;\n budget_duration?: string;\n tpm_limit?: number;\n rpm_limit?: number;\n metadata?: Record<string, unknown>;\n object_permission?: {\n vector_stores?: string[];\n mcp_servers?: string[];\n mcp_access_groups?: string[];\n };\n}\n\nexport interface UpdateTeamRequest extends Partial<CreateTeamRequest> {\n team_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 budget_duration?: string | null;\n user_id?: string | null;\n team_id?: string | null;\n created_at: string;\n blocked?: boolean | null;\n /** Arbitrary metadata stored on the key record. */\n metadata?: Record<string, unknown> | null;\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\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
|
@@ -96,42 +96,6 @@ export interface CreateUserResponse {
|
|
|
96
96
|
models?: string[];
|
|
97
97
|
teams?: string[];
|
|
98
98
|
}
|
|
99
|
-
/** One row from LiteLLM's `/spend/logs` (per-request spend log entry). */
|
|
100
|
-
export interface SpendLogEntry {
|
|
101
|
-
request_id?: string;
|
|
102
|
-
/** ISO timestamp of the call. */
|
|
103
|
-
startTime?: string;
|
|
104
|
-
endTime?: string;
|
|
105
|
-
spend?: number;
|
|
106
|
-
total_tokens?: number;
|
|
107
|
-
prompt_tokens?: number;
|
|
108
|
-
completion_tokens?: number;
|
|
109
|
-
model?: string;
|
|
110
|
-
/** Virtual key alias or hash the request was billed to. */
|
|
111
|
-
api_key?: string;
|
|
112
|
-
user?: string;
|
|
113
|
-
team_id?: string;
|
|
114
|
-
/**
|
|
115
|
-
* Tags attached to the request (e.g. `channel:backstage`,
|
|
116
|
-
* `session:<thread>`, `invoked-by:<user>`). Shape varies across LiteLLM
|
|
117
|
-
* versions (array or object) and is normalised to a string[].
|
|
118
|
-
*/
|
|
119
|
-
request_tags?: string[] | Record<string, string>;
|
|
120
|
-
/** LiteLLM `metadata` blob; carries trace ids and session grouping. */
|
|
121
|
-
metadata?: Record<string, unknown> | string | null;
|
|
122
|
-
}
|
|
123
|
-
export interface SpendLogsParams {
|
|
124
|
-
/** ISO date `YYYY-MM-DD` (inclusive). */
|
|
125
|
-
start_date: string;
|
|
126
|
-
/** ISO date `YYYY-MM-DD` (inclusive). */
|
|
127
|
-
end_date: string;
|
|
128
|
-
/** Restrict to the given virtual key. */
|
|
129
|
-
api_key?: string;
|
|
130
|
-
user_id?: string;
|
|
131
|
-
team_id?: string;
|
|
132
|
-
/** Max rows to return (LiteLLM caps at 1000 per page). */
|
|
133
|
-
page_size?: number;
|
|
134
|
-
}
|
|
135
99
|
export interface AuditLogsParams {
|
|
136
100
|
page?: number;
|
|
137
101
|
page_size?: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acarmisc/backstage-plugin-litellm-backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "The Backstage backend plugin for LiteLLM governance",
|
|
5
5
|
"backstage": {
|
|
6
6
|
"role": "backend-plugin",
|
|
@@ -32,21 +32,17 @@
|
|
|
32
32
|
"lint": "eslint src --ext .ts",
|
|
33
33
|
"test": "tsc -p tsconfig.test.json && node --test \"dist-test/**/*.test.js\"",
|
|
34
34
|
"api-report": "npm run build && api-extractor run --local --verbose",
|
|
35
|
-
"prepack": "npm run build"
|
|
36
|
-
"postpack": ""
|
|
35
|
+
"prepack": "npm run build"
|
|
37
36
|
},
|
|
38
37
|
"dependencies": {
|
|
39
38
|
"@acarmisc/backstage-plugin-litellm-common": "0.1.0",
|
|
40
39
|
"@backstage/backend-plugin-api": "^1.9.1",
|
|
41
40
|
"@backstage/catalog-client": "^1.9.0",
|
|
42
|
-
"@backstage/catalog-model": "^1.7.0",
|
|
43
41
|
"@backstage/config": "^1.3.0",
|
|
44
42
|
"@backstage/errors": "^1.3.0",
|
|
45
43
|
"@backstage/plugin-permission-common": "^0.9.10",
|
|
46
|
-
"@backstage/types": "^1.2.0",
|
|
47
44
|
"express": "^4.18.2",
|
|
48
|
-
"jose": "^5.9.6"
|
|
49
|
-
"zod": "^3.22.0"
|
|
45
|
+
"jose": "^5.9.6"
|
|
50
46
|
},
|
|
51
47
|
"devDependencies": {
|
|
52
48
|
"@backstage/cli": "^0.36.2",
|