@acarmisc/backstage-plugin-litellm-backend 0.8.2 → 0.10.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.
@@ -0,0 +1,150 @@
1
+ import { Request } from 'express';
2
+ import { Config } from '@backstage/config';
3
+ import { AuthService, PermissionsService } from '@backstage/backend-plugin-api';
4
+ import { BasicPermission } from '@backstage/plugin-permission-common';
5
+ import { CatalogClient } from '@backstage/catalog-client';
6
+ /**
7
+ * Governance limits for the team-administration feature. Every field is
8
+ * fail-closed: an unset array means "nothing allowed", an unset boolean means
9
+ * "deny", and an unset ceiling means "no admin-settable budget". An absent
10
+ * `litellm.teamAdmin` block therefore leaves the feature completely dark.
11
+ */
12
+ export interface TeamAdminConfig {
13
+ /**
14
+ * Backstage group entity ref whose members may manage teams,
15
+ * e.g. "group:default/litellm-team-admins".
16
+ * Undefined => feature stays disabled.
17
+ */
18
+ group?: string;
19
+ /**
20
+ * LiteLLM model names an admin may assign to a team.
21
+ * Empty => none assignable (fail-closed).
22
+ */
23
+ allowedModels: string[];
24
+ /**
25
+ * LiteLLM model access-group names an admin may assign.
26
+ * Empty => none.
27
+ */
28
+ allowedModelAccessGroups: string[];
29
+ /**
30
+ * Hard USD ceiling for a team's max_budget an admin may set.
31
+ * Undefined => no admin-settable budget allowed unless allowUnlimitedBudget.
32
+ */
33
+ maxBudgetCeiling?: number;
34
+ /**
35
+ * Allow an admin to create a team with no budget cap.
36
+ */
37
+ allowUnlimitedBudget: boolean;
38
+ /**
39
+ * Vector-store ids/names an admin may attach as team knowledge bases.
40
+ * Empty => none.
41
+ */
42
+ allowedVectorStores: string[];
43
+ /**
44
+ * MCP server ids/names an admin may attach to a team.
45
+ * Empty => none.
46
+ */
47
+ allowedMcpServers: string[];
48
+ /**
49
+ * MCP access-group names an admin may attach.
50
+ * Empty => none.
51
+ */
52
+ allowedMcpAccessGroups: string[];
53
+ /**
54
+ * Allow an admin to delete a team (vs. only block/deactivate).
55
+ */
56
+ allowTeamDelete: boolean;
57
+ }
58
+ /**
59
+ * Whether team-management routes should be mounted.
60
+ * Requires permission.enabled AND a designated admin group.
61
+ * Returns false when either is missing (fail-closed).
62
+ */
63
+ export declare function isTeamManagementEnabled(config: Config): boolean;
64
+ /**
65
+ * Whether the object-permission routes (knowledge bases / MCP servers) should
66
+ * be mounted. Requires team management to be enabled AND an explicit
67
+ * `litellm.teamAdmin.objectPermissions.enabled: true` opt-in, because these
68
+ * routes grant data access (vector stores) and tool execution (MCP) to every
69
+ * team key. Fail-closed: any missing piece disables the whole surface.
70
+ */
71
+ export declare function isObjectPermissionsEnabled(config: Config): boolean;
72
+ /**
73
+ * Discriminated result: either successful auth or a failure with HTTP status + error message.
74
+ */
75
+ export type TeamAdminCheck = {
76
+ ok: true;
77
+ userEntityRef: string;
78
+ } | {
79
+ ok: false;
80
+ status: 401 | 403;
81
+ error: string;
82
+ };
83
+ /**
84
+ * Layered fail-closed authz check for team-management operations.
85
+ *
86
+ * This is the security primitive that gates all team-write routes. It checks in order:
87
+ * 1. Authentication (user identity from token)
88
+ * 2. Group membership (user in the designated litellm-team-admins group)
89
+ * 3. Permission framework decision (via assertPermission, defaults to DENY if no policy)
90
+ *
91
+ * ALL three must pass. The group membership check exists because the permission
92
+ * framework alone cannot be trusted — its default depends on the policy engine
93
+ * (permissive if unconfigured), so we layer a guaranteed group check on top.
94
+ *
95
+ * Callers: check `result.ok` and respond with `status` + `error` when false.
96
+ */
97
+ export declare function assertTeamAdmin(opts: {
98
+ req: Request;
99
+ auth: AuthService;
100
+ permissions: PermissionsService;
101
+ catalogClient: CatalogClient;
102
+ teamAdminGroup: string;
103
+ permission: BasicPermission;
104
+ logger: any;
105
+ }): Promise<TeamAdminCheck>;
106
+ /**
107
+ * Reads the team-admin governance block from config, applying fail-closed
108
+ * defaults for every field: unset arrays remain empty (nothing allowed), unset
109
+ * booleans stay false (deny by default), and an undefined ceiling means no
110
+ * admin-settable budget.
111
+ *
112
+ * Fail-closed rationale: a governance feature is only safe when absent config
113
+ * defaults to "deny all". An unconfigured teamAdmin block must leave the feature
114
+ * completely dark, ready to be explicitly enabled only once an operator has
115
+ * reviewed and understood the delegation policy.
116
+ */
117
+ export declare function readTeamAdminConfig(config: Config): TeamAdminConfig;
118
+ export interface TeamWriteInput {
119
+ team_alias?: string;
120
+ models?: string[];
121
+ max_budget?: number | null;
122
+ budget_duration?: string;
123
+ tpm_limit?: number;
124
+ rpm_limit?: number;
125
+ }
126
+ export type TeamWriteValidation = {
127
+ ok: true;
128
+ value: {
129
+ team_alias: string;
130
+ models: string[];
131
+ max_budget?: number;
132
+ budget_duration?: string;
133
+ tpm_limit?: number;
134
+ rpm_limit?: number;
135
+ };
136
+ } | {
137
+ ok: false;
138
+ error: string;
139
+ };
140
+ export declare function validateTeamWriteInput(input: TeamWriteInput, cfg: TeamAdminConfig): TeamWriteValidation;
141
+ export type TeamPatchValidation = {
142
+ ok: true;
143
+ value: Record<string, unknown>;
144
+ } | {
145
+ ok: false;
146
+ error: string;
147
+ };
148
+ export declare function validateTeamPatchInput(input: TeamWriteInput & {
149
+ blocked?: boolean;
150
+ }, cfg: TeamAdminConfig): TeamPatchValidation;
@@ -0,0 +1,235 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isTeamManagementEnabled = isTeamManagementEnabled;
4
+ exports.isObjectPermissionsEnabled = isObjectPermissionsEnabled;
5
+ exports.assertTeamAdmin = assertTeamAdmin;
6
+ exports.readTeamAdminConfig = readTeamAdminConfig;
7
+ exports.validateTeamWriteInput = validateTeamWriteInput;
8
+ exports.validateTeamPatchInput = validateTeamPatchInput;
9
+ const plugin_permission_common_1 = require("@backstage/plugin-permission-common");
10
+ const provisioning_1 = require("./provisioning");
11
+ /**
12
+ * Whether team-management routes should be mounted.
13
+ * Requires permission.enabled AND a designated admin group.
14
+ * Returns false when either is missing (fail-closed).
15
+ */
16
+ function isTeamManagementEnabled(config) {
17
+ const permEnabled = config.getOptionalBoolean('permission.enabled') ?? false;
18
+ const adminConfig = readTeamAdminConfig(config);
19
+ return permEnabled && !!adminConfig.group;
20
+ }
21
+ /**
22
+ * Whether the object-permission routes (knowledge bases / MCP servers) should
23
+ * be mounted. Requires team management to be enabled AND an explicit
24
+ * `litellm.teamAdmin.objectPermissions.enabled: true` opt-in, because these
25
+ * routes grant data access (vector stores) and tool execution (MCP) to every
26
+ * team key. Fail-closed: any missing piece disables the whole surface.
27
+ */
28
+ function isObjectPermissionsEnabled(config) {
29
+ return (isTeamManagementEnabled(config) &&
30
+ (config.getOptionalBoolean('litellm.teamAdmin.objectPermissions.enabled') ??
31
+ false));
32
+ }
33
+ /**
34
+ * Layered fail-closed authz check for team-management operations.
35
+ *
36
+ * This is the security primitive that gates all team-write routes. It checks in order:
37
+ * 1. Authentication (user identity from token)
38
+ * 2. Group membership (user in the designated litellm-team-admins group)
39
+ * 3. Permission framework decision (via assertPermission, defaults to DENY if no policy)
40
+ *
41
+ * ALL three must pass. The group membership check exists because the permission
42
+ * framework alone cannot be trusted — its default depends on the policy engine
43
+ * (permissive if unconfigured), so we layer a guaranteed group check on top.
44
+ *
45
+ * Callers: check `result.ok` and respond with `status` + `error` when false.
46
+ */
47
+ async function assertTeamAdmin(opts) {
48
+ const { req, auth, permissions, catalogClient, teamAdminGroup, permission, logger, } = opts;
49
+ // Step 1: Resolve authenticated user
50
+ const userEntityRef = await (0, provisioning_1.resolveUserId)(req, auth);
51
+ if (!userEntityRef) {
52
+ return { ok: false, status: 401, error: 'Authentication required' };
53
+ }
54
+ // Step 2: Verify credentials can be resolved (needed for permission check)
55
+ const credentials = await (0, provisioning_1.resolveCredentials)(req, auth);
56
+ if (!credentials) {
57
+ return { ok: false, status: 401, error: 'Authentication required' };
58
+ }
59
+ // Step 3: Check group membership
60
+ const isMember = await (0, provisioning_1.isUserMemberOfGroup)(userEntityRef, teamAdminGroup, catalogClient, auth, logger);
61
+ if (!isMember) {
62
+ return {
63
+ ok: false,
64
+ status: 403,
65
+ error: `Access denied: not a member of ${teamAdminGroup}`,
66
+ };
67
+ }
68
+ // Step 4: Check permission framework decision
69
+ const [decision] = await permissions.authorize([{ permission }], {
70
+ credentials,
71
+ });
72
+ if (decision.result !== plugin_permission_common_1.AuthorizeResult.ALLOW) {
73
+ return {
74
+ ok: false,
75
+ status: 403,
76
+ error: `Access denied: missing permission "${permission.name}"`,
77
+ };
78
+ }
79
+ // All checks passed
80
+ return { ok: true, userEntityRef };
81
+ }
82
+ /**
83
+ * Reads the team-admin governance block from config, applying fail-closed
84
+ * defaults for every field: unset arrays remain empty (nothing allowed), unset
85
+ * booleans stay false (deny by default), and an undefined ceiling means no
86
+ * admin-settable budget.
87
+ *
88
+ * Fail-closed rationale: a governance feature is only safe when absent config
89
+ * defaults to "deny all". An unconfigured teamAdmin block must leave the feature
90
+ * completely dark, ready to be explicitly enabled only once an operator has
91
+ * reviewed and understood the delegation policy.
92
+ */
93
+ function readTeamAdminConfig(config) {
94
+ return {
95
+ group: config.getOptionalString('litellm.teamAdmin.group'),
96
+ allowedModels: config.getOptionalStringArray('litellm.teamAdmin.allowedModels') ?? [],
97
+ allowedModelAccessGroups: config.getOptionalStringArray('litellm.teamAdmin.allowedModelAccessGroups') ?? [],
98
+ maxBudgetCeiling: config.getOptionalNumber('litellm.teamAdmin.maxBudgetCeiling'),
99
+ allowUnlimitedBudget: config.getOptionalBoolean('litellm.teamAdmin.allowUnlimitedBudget') ??
100
+ false,
101
+ allowedVectorStores: config.getOptionalStringArray('litellm.teamAdmin.allowedVectorStores') ??
102
+ [],
103
+ allowedMcpServers: config.getOptionalStringArray('litellm.teamAdmin.allowedMcpServers') ?? [],
104
+ allowedMcpAccessGroups: config.getOptionalStringArray('litellm.teamAdmin.allowedMcpAccessGroups') ?? [],
105
+ allowTeamDelete: config.getOptionalBoolean('litellm.teamAdmin.allowTeamDelete') ?? false,
106
+ };
107
+ }
108
+ function validateTeamWriteInput(input, cfg) {
109
+ const trimmedAlias = (input.team_alias ?? '').trim();
110
+ if (!trimmedAlias) {
111
+ return { ok: false, error: 'team_alias is required' };
112
+ }
113
+ const models = input.models ?? [];
114
+ if (!Array.isArray(models) || models.length === 0) {
115
+ return { ok: false, error: 'models must list at least one allowed model (a team with no explicit models grants access to every proxy model)' };
116
+ }
117
+ const allowedSet = new Set([...cfg.allowedModels, ...cfg.allowedModelAccessGroups]);
118
+ if (allowedSet.size === 0) {
119
+ return { ok: false, error: 'models are not allowed (team admin has configured no allowlist)' };
120
+ }
121
+ for (const model of models) {
122
+ if (!allowedSet.has(model)) {
123
+ return { ok: false, error: `model "${model}" is not in the allowed set for team admins` };
124
+ }
125
+ }
126
+ let maxBudget;
127
+ if (input.max_budget !== undefined && input.max_budget !== null) {
128
+ if (!Number.isFinite(input.max_budget) || input.max_budget <= 0) {
129
+ return { ok: false, error: 'max_budget must be a positive number' };
130
+ }
131
+ maxBudget = input.max_budget;
132
+ }
133
+ if (!cfg.allowUnlimitedBudget) {
134
+ if (maxBudget === undefined) {
135
+ return { ok: false, error: 'max_budget is required' };
136
+ }
137
+ if (cfg.maxBudgetCeiling === undefined) {
138
+ return { ok: false, error: 'team budget ceiling is not configured (litellm.teamAdmin.maxBudgetCeiling)' };
139
+ }
140
+ if (maxBudget > cfg.maxBudgetCeiling) {
141
+ return { ok: false, error: `max_budget $${maxBudget} exceeds the ceiling $${cfg.maxBudgetCeiling}` };
142
+ }
143
+ }
144
+ else if (maxBudget !== undefined && cfg.maxBudgetCeiling !== undefined && maxBudget > cfg.maxBudgetCeiling) {
145
+ return { ok: false, error: `max_budget $${maxBudget} exceeds the ceiling $${cfg.maxBudgetCeiling}` };
146
+ }
147
+ let budgetDuration = input.budget_duration;
148
+ if (!budgetDuration || typeof budgetDuration !== 'string' || budgetDuration.trim() === '') {
149
+ if (maxBudget !== undefined) {
150
+ budgetDuration = '30d';
151
+ }
152
+ }
153
+ const tpmLimit = input.tpm_limit !== undefined && Number.isFinite(input.tpm_limit) && input.tpm_limit >= 0 ? input.tpm_limit : undefined;
154
+ const rpmLimit = input.rpm_limit !== undefined && Number.isFinite(input.rpm_limit) && input.rpm_limit >= 0 ? input.rpm_limit : undefined;
155
+ return {
156
+ ok: true,
157
+ value: {
158
+ team_alias: trimmedAlias,
159
+ models,
160
+ ...(maxBudget !== undefined && { max_budget: maxBudget }),
161
+ ...(budgetDuration && { budget_duration: budgetDuration }),
162
+ ...(tpmLimit !== undefined && { tpm_limit: tpmLimit }),
163
+ ...(rpmLimit !== undefined && { rpm_limit: rpmLimit }),
164
+ },
165
+ };
166
+ }
167
+ function validateTeamPatchInput(input, cfg) {
168
+ const result = {};
169
+ let hasRecognizedField = false;
170
+ if (input.team_alias !== undefined) {
171
+ hasRecognizedField = true;
172
+ const trimmedAlias = (input.team_alias ?? '').trim();
173
+ if (!trimmedAlias) {
174
+ return { ok: false, error: 'team_alias must not be empty' };
175
+ }
176
+ result.team_alias = trimmedAlias;
177
+ }
178
+ if (input.models !== undefined) {
179
+ hasRecognizedField = true;
180
+ if (!Array.isArray(input.models) || input.models.length === 0) {
181
+ return { ok: false, error: 'models must list at least one allowed model (a team with no explicit models grants access to every proxy model)' };
182
+ }
183
+ const allowedSet = new Set([...cfg.allowedModels, ...cfg.allowedModelAccessGroups]);
184
+ if (allowedSet.size === 0) {
185
+ return { ok: false, error: 'models are not allowed (team admin has configured no allowlist)' };
186
+ }
187
+ for (const model of input.models) {
188
+ if (!allowedSet.has(model)) {
189
+ return { ok: false, error: `model "${model}" is not in the allowed set for team admins` };
190
+ }
191
+ }
192
+ result.models = input.models;
193
+ }
194
+ if (input.max_budget !== undefined) {
195
+ hasRecognizedField = true;
196
+ if (input.max_budget === null) {
197
+ if (!cfg.allowUnlimitedBudget) {
198
+ return { ok: false, error: 'max_budget cannot be cleared (unlimited budgets are disabled)' };
199
+ }
200
+ result.max_budget = null;
201
+ }
202
+ else {
203
+ if (!Number.isFinite(input.max_budget) || input.max_budget <= 0) {
204
+ return { ok: false, error: 'max_budget must be a positive number' };
205
+ }
206
+ if (cfg.maxBudgetCeiling !== undefined && input.max_budget > cfg.maxBudgetCeiling) {
207
+ return { ok: false, error: `max_budget $${input.max_budget} exceeds the ceiling $${cfg.maxBudgetCeiling}` };
208
+ }
209
+ result.max_budget = input.max_budget;
210
+ }
211
+ }
212
+ if (input.blocked !== undefined) {
213
+ hasRecognizedField = true;
214
+ if (typeof input.blocked !== 'boolean') {
215
+ return { ok: false, error: 'blocked must be a boolean' };
216
+ }
217
+ result.blocked = input.blocked;
218
+ }
219
+ if (input.budget_duration !== undefined && typeof input.budget_duration === 'string' && input.budget_duration.trim() !== '') {
220
+ hasRecognizedField = true;
221
+ result.budget_duration = input.budget_duration;
222
+ }
223
+ if (input.tpm_limit !== undefined && Number.isFinite(input.tpm_limit) && input.tpm_limit >= 0) {
224
+ hasRecognizedField = true;
225
+ result.tpm_limit = input.tpm_limit;
226
+ }
227
+ if (input.rpm_limit !== undefined && Number.isFinite(input.rpm_limit) && input.rpm_limit >= 0) {
228
+ hasRecognizedField = true;
229
+ result.rpm_limit = input.rpm_limit;
230
+ }
231
+ if (!hasRecognizedField) {
232
+ return { ok: false, error: 'no updatable fields provided' };
233
+ }
234
+ return { ok: true, value: result };
235
+ }
@@ -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 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 /** Access group names this model belongs to (litellm.model_info.access_groups). A team's `models` list can reference a group name instead of a literal model_name. */\n access_groups?: string[];\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"],
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 TeamObjectPermission {\n /** Vector store ids/names attached to the team. */\n vector_stores?: string[];\n /** MCP server ids/names attached to the team. */\n mcp_servers?: string[];\n /** MCP access group names attached to the team. */\n mcp_access_groups?: string[];\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 /** Arbitrary metadata stored on the team record. */\n metadata?: Record<string, unknown>;\n /** Knowledge bases (vector stores) and MCP servers attached to the team. */\n object_permission?: TeamObjectPermission;\n /** Whether the team is blocked/deactivated. */\n blocked?: boolean;\n /** Max budget available to individual team members (if set per-member). */\n team_member_budget?: number;\n}\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?: TeamObjectPermission;\n}\n\nexport interface UpdateTeamRequest extends Partial<CreateTeamRequest> {\n team_id: string;\n blocked?: boolean;\n}\n\nexport interface CreateTeamResponse {\n team_id: string;\n team_alias?: string;\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 /** Access group names this model belongs to (litellm.model_info.access_groups). A team's `models` list can reference a group name instead of a literal model_name. */\n access_groups?: string[];\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
@@ -17,6 +17,14 @@ export interface TeamMember {
17
17
  user_email?: string;
18
18
  role: 'admin' | 'user';
19
19
  }
20
+ export interface TeamObjectPermission {
21
+ /** Vector store ids/names attached to the team. */
22
+ vector_stores?: string[];
23
+ /** MCP server ids/names attached to the team. */
24
+ mcp_servers?: string[];
25
+ /** MCP access group names attached to the team. */
26
+ mcp_access_groups?: string[];
27
+ }
20
28
  export interface TeamInfo {
21
29
  team_id: string;
22
30
  team_alias?: string;
@@ -26,6 +34,32 @@ export interface TeamInfo {
26
34
  models?: string[];
27
35
  tpm_limit?: number;
28
36
  rpm_limit?: number;
37
+ /** Arbitrary metadata stored on the team record. */
38
+ metadata?: Record<string, unknown>;
39
+ /** Knowledge bases (vector stores) and MCP servers attached to the team. */
40
+ object_permission?: TeamObjectPermission;
41
+ /** Whether the team is blocked/deactivated. */
42
+ blocked?: boolean;
43
+ /** Max budget available to individual team members (if set per-member). */
44
+ team_member_budget?: number;
45
+ }
46
+ export interface CreateTeamRequest {
47
+ team_alias: string;
48
+ models?: string[];
49
+ max_budget?: number;
50
+ budget_duration?: string;
51
+ tpm_limit?: number;
52
+ rpm_limit?: number;
53
+ metadata?: Record<string, unknown>;
54
+ object_permission?: TeamObjectPermission;
55
+ }
56
+ export interface UpdateTeamRequest extends Partial<CreateTeamRequest> {
57
+ team_id: string;
58
+ blocked?: boolean;
59
+ }
60
+ export interface CreateTeamResponse {
61
+ team_id: string;
62
+ team_alias?: string;
29
63
  }
30
64
  export interface VirtualKey {
31
65
  key: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acarmisc/backstage-plugin-litellm-backend",
3
- "version": "0.8.2",
3
+ "version": "0.10.0",
4
4
  "description": "The Backstage backend plugin for LiteLLM governance",
5
5
  "backstage": {
6
6
  "role": "backend-plugin",
@@ -31,6 +31,7 @@
31
31
  "build": "node build.js && tsc -p tsconfig.json",
32
32
  "lint": "eslint src --ext .ts",
33
33
  "test": "tsc -p tsconfig.test.json && node --test dist-test/**/*.test.js dist-test/*.test.js",
34
+ "api-report": "npm run build && api-extractor run --local --verbose",
34
35
  "prepack": "npm run build",
35
36
  "postpack": ""
36
37
  },
@@ -39,16 +40,18 @@
39
40
  "@backstage/catalog-client": "^1.9.0",
40
41
  "@backstage/catalog-model": "^1.7.0",
41
42
  "@backstage/config": "^1.3.0",
43
+ "@backstage/plugin-permission-common": "^0.9.10",
42
44
  "@backstage/types": "^1.2.0",
43
45
  "express": "^4.18.2",
44
46
  "jose": "^5.9.6"
45
47
  },
46
48
  "devDependencies": {
47
49
  "@backstage/cli": "^0.36.2",
50
+ "@microsoft/api-extractor": "^7.59.0",
48
51
  "@types/express": "^4.17.25",
52
+ "@types/node": "^20.0.0",
49
53
  "esbuild": "^0.28.0",
50
- "typescript": "^5.9.3",
51
- "@types/node": "^20.0.0"
54
+ "typescript": "^5.9.3"
52
55
  },
53
56
  "configSchema": "config.d.ts"
54
57
  }