@acarmisc/backstage-plugin-litellm-backend 0.9.0 → 0.11.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/config.d.ts CHANGED
@@ -148,6 +148,106 @@ export interface Config {
148
148
  teamRequired?: boolean;
149
149
  };
150
150
 
151
+ /**
152
+ * Team administration governance. Allows a designated Backstage group to
153
+ * create and manage LiteLLM teams. The feature is disabled (fail-closed)
154
+ * when unset — all fields default to empty/false to prevent accidental
155
+ * delegation of capabilities. Set this block only when you have reviewed
156
+ * the governance policy and are ready to enable the feature.
157
+ */
158
+ teamAdmin?: {
159
+ /**
160
+ * Backstage group entity ref whose members may manage teams,
161
+ * e.g. "group:default/litellm-team-admins".
162
+ * The plugin ships a group template at catalog/litellm-team-admins.yaml.
163
+ * When omitted the feature is disabled entirely.
164
+ * @visibility backend
165
+ */
166
+ group?: string;
167
+
168
+ /**
169
+ * LiteLLM model names an admin may assign to a team.
170
+ * Empty array => none assignable (fail-closed).
171
+ * @default []
172
+ * @visibility backend
173
+ */
174
+ allowedModels?: string[];
175
+
176
+ /**
177
+ * LiteLLM model access-group names an admin may assign to a team.
178
+ * References access_groups defined in litellm.model_info configuration.
179
+ * Empty array => none allowed.
180
+ * @default []
181
+ * @visibility backend
182
+ */
183
+ allowedModelAccessGroups?: string[];
184
+
185
+ /**
186
+ * Hard USD ceiling for max_budget an admin may set on a team.
187
+ * @default 1000
188
+ * @visibility backend
189
+ */
190
+ maxBudgetCeiling?: number;
191
+
192
+ /**
193
+ * Allow an admin to create a team with no budget cap.
194
+ * @default false
195
+ * @visibility backend
196
+ */
197
+ allowUnlimitedBudget?: boolean;
198
+
199
+ /**
200
+ * Vector-store ids/names an admin may attach as team knowledge bases.
201
+ * Empty array => none allowed.
202
+ * @default []
203
+ * @visibility backend
204
+ */
205
+ allowedVectorStores?: string[];
206
+
207
+ /**
208
+ * MCP server ids/names an admin may attach to a team.
209
+ * Empty array => none allowed.
210
+ * @default []
211
+ * @visibility backend
212
+ */
213
+ allowedMcpServers?: string[];
214
+
215
+ /**
216
+ * MCP access-group names an admin may attach to a team.
217
+ * References access_groups defined in litellm.mcp_info configuration.
218
+ * Empty array => none allowed.
219
+ * @default []
220
+ * @visibility backend
221
+ */
222
+ allowedMcpAccessGroups?: string[];
223
+
224
+ /**
225
+ * Allow an admin to delete a team (vs. only block/deactivate).
226
+ * @default false
227
+ * @visibility backend
228
+ */
229
+ allowTeamDelete?: boolean;
230
+
231
+ /**
232
+ * Object-permission management (attaching knowledge bases / MCP servers
233
+ * to a team). This is the highest-risk surface — attaching a vector store
234
+ * exposes its documents to every team key, and attaching an MCP server
235
+ * grants tool execution. Keep disabled unless a real permission policy
236
+ * (@backstage-community/plugin-rbac or a custom PermissionPolicy) is
237
+ * installed and the allowedVectorStores / allowedMcpServers allowlists
238
+ * are set.
239
+ */
240
+ objectPermissions?: {
241
+ /**
242
+ * When true, mount the knowledge-base and MCP management routes.
243
+ * Still gated by the permission framework and the allowlists.
244
+ * @default false
245
+ * @visibility backend
246
+ */
247
+ enabled?: boolean;
248
+ };
249
+ };
250
+
151
251
  bridge?: {
152
252
  /**
153
253
  * When true, mount the /bridge/keys, /bridge/keys (POST), /bridge/models
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { LiteLLMConfig, UserInfo, VirtualKey, ModelInfo, UsageMetrics, TeamInfo, GenerateKeyRequest, GenerateKeyResponse, UpdateKeyRequest, DeleteKeyRequest, CreateUserRequest, CreateUserResponse, AuditLogsParams, PaginatedAuditLogs } from './types';
1
+ import { LiteLLMConfig, UserInfo, VirtualKey, ModelInfo, UsageMetrics, TeamInfo, GenerateKeyRequest, GenerateKeyResponse, UpdateKeyRequest, DeleteKeyRequest, CreateUserRequest, CreateUserResponse, CreateTeamRequest, CreateTeamResponse, UpdateTeamRequest, AuditLogsParams, PaginatedAuditLogs } from './types';
2
2
  /**
3
3
  * Typed error for failed upstream LiteLLM responses. Preserves the HTTP
4
4
  * status and the structured `param` (e.g. `key_alias`) from the upstream
@@ -89,6 +89,66 @@ export declare class LiteLLMClient {
89
89
  * team".
90
90
  */
91
91
  getTeamInfo(teamId: string): Promise<TeamInfo>;
92
+ /**
93
+ * Normalises a single team row into the TeamInfo contract. Tolerates both
94
+ * the `/team/info` shape (row wrapped in a `team_info` envelope alongside
95
+ * sibling arrays) and a bare row as returned per-item by `/team/list`.
96
+ * `metadata` is always surfaced so callers can read `owning_group`.
97
+ */
98
+ private toTeamInfo;
99
+ /**
100
+ * Lists every team known to the LiteLLM proxy. Tolerates the observed
101
+ * response shapes — a bare array, `{ teams: [...] }`, or `{ data: [...] }` —
102
+ * and normalises each row through the same unwrap as `getTeamInfo`.
103
+ *
104
+ * This returns the GLOBAL team list and must never be exposed directly to
105
+ * end users; callers are responsible for scoping the result (e.g. to teams
106
+ * whose `metadata.owning_group` matches the caller's admin group).
107
+ */
108
+ listTeams(): Promise<TeamInfo[]>;
109
+ createTeam(payload: CreateTeamRequest): Promise<CreateTeamResponse>;
110
+ updateTeam(payload: UpdateTeamRequest): Promise<unknown>;
111
+ deleteTeam(teamId: string): Promise<unknown>;
112
+ blockTeam(teamId: string): Promise<unknown>;
113
+ unblockTeam(teamId: string): Promise<unknown>;
114
+ /**
115
+ * Adds a member to a team. LiteLLM's `/team/member_add` nests the member
116
+ * under a `member` object; `role` is 'user' or 'admin' (we only ever send
117
+ * 'user' from Backstage). `max_budget_in_team` optionally caps that member's
118
+ * spend within the team.
119
+ */
120
+ teamMemberAdd(payload: {
121
+ team_id: string;
122
+ user_id: string;
123
+ role?: 'user';
124
+ max_budget_in_team?: number;
125
+ }): Promise<unknown>;
126
+ /** Removes a member from a team via LiteLLM's `/team/member_delete`. */
127
+ teamMemberDelete(payload: {
128
+ team_id: string;
129
+ user_id: string;
130
+ }): Promise<unknown>;
131
+ /**
132
+ * Lists the vector stores (knowledge bases) registered on the LiteLLM proxy.
133
+ * Tolerates a bare array, `{ data: [...] }`, or `{ vector_stores: [...] }`.
134
+ * Each entry is normalised to `{ id, name? }` — `id` prefers
135
+ * `vector_store_id` then `id` then `name`.
136
+ */
137
+ listVectorStores(): Promise<Array<{
138
+ id: string;
139
+ name?: string;
140
+ }>>;
141
+ /**
142
+ * Lists the MCP servers registered on the LiteLLM proxy. Path is
143
+ * `/mcp/server/list`; tolerates a bare array, `{ data: [...] }` or
144
+ * `{ servers: [...] }`. Normalised to `{ id, name?, url? }` — `id` prefers
145
+ * `server_id` then `id` then `alias`/`name`.
146
+ */
147
+ listMcpServers(): Promise<Array<{
148
+ id: string;
149
+ name?: string;
150
+ url?: string;
151
+ }>>;
92
152
  private emptyUsage;
93
153
  /**
94
154
  * Transforms LiteLLM's SpendAnalyticsPaginatedResponse into the flatter
package/dist/client.js CHANGED
@@ -300,18 +300,155 @@ class LiteLLMClient {
300
300
  */
301
301
  async getTeamInfo(teamId) {
302
302
  const raw = await this.request(`/team/info?team_id=${encodeURIComponent(teamId)}`);
303
+ return this.toTeamInfo(raw, teamId);
304
+ }
305
+ /**
306
+ * Normalises a single team row into the TeamInfo contract. Tolerates both
307
+ * the `/team/info` shape (row wrapped in a `team_info` envelope alongside
308
+ * sibling arrays) and a bare row as returned per-item by `/team/list`.
309
+ * `metadata` is always surfaced so callers can read `owning_group`.
310
+ */
311
+ toTeamInfo(raw, fallbackId) {
303
312
  const inner = raw?.team_info ?? {};
304
313
  return {
305
- team_id: raw?.team_id ?? inner.team_id ?? teamId,
314
+ team_id: raw?.team_id ?? inner.team_id ?? fallbackId ?? '',
306
315
  team_alias: inner.team_alias ?? raw?.team_alias,
307
316
  max_budget: inner.max_budget ?? raw?.max_budget,
317
+ budget_duration: inner.budget_duration ?? raw?.budget_duration,
308
318
  spend: inner.spend ?? raw?.spend ?? 0,
309
319
  members_with_roles: inner.members_with_roles ?? raw?.members_with_roles,
310
320
  models: inner.models ?? raw?.models,
311
321
  tpm_limit: inner.tpm_limit ?? raw?.tpm_limit,
312
322
  rpm_limit: inner.rpm_limit ?? raw?.rpm_limit,
323
+ metadata: inner.metadata ?? raw?.metadata,
324
+ object_permission: inner.object_permission ?? raw?.object_permission,
325
+ blocked: inner.blocked ?? raw?.blocked,
326
+ team_member_budget: inner.team_member_budget ?? raw?.team_member_budget,
313
327
  };
314
328
  }
329
+ /**
330
+ * Lists every team known to the LiteLLM proxy. Tolerates the observed
331
+ * response shapes — a bare array, `{ teams: [...] }`, or `{ data: [...] }` —
332
+ * and normalises each row through the same unwrap as `getTeamInfo`.
333
+ *
334
+ * This returns the GLOBAL team list and must never be exposed directly to
335
+ * end users; callers are responsible for scoping the result (e.g. to teams
336
+ * whose `metadata.owning_group` matches the caller's admin group).
337
+ */
338
+ async listTeams() {
339
+ const raw = await this.request('/team/list');
340
+ let rows = [];
341
+ if (Array.isArray(raw))
342
+ rows = raw;
343
+ else if (Array.isArray(raw?.teams))
344
+ rows = raw.teams;
345
+ else if (Array.isArray(raw?.data))
346
+ rows = raw.data;
347
+ return rows.map(row => this.toTeamInfo(row));
348
+ }
349
+ async createTeam(payload) {
350
+ return this.request('/team/new', {
351
+ method: 'POST',
352
+ body: JSON.stringify(payload),
353
+ });
354
+ }
355
+ async updateTeam(payload) {
356
+ return this.request('/team/update', {
357
+ method: 'POST',
358
+ body: JSON.stringify(payload),
359
+ });
360
+ }
361
+ async deleteTeam(teamId) {
362
+ return this.request('/team/delete', {
363
+ method: 'POST',
364
+ body: JSON.stringify({ team_ids: [teamId] }),
365
+ });
366
+ }
367
+ async blockTeam(teamId) {
368
+ return this.request('/team/block', {
369
+ method: 'POST',
370
+ body: JSON.stringify({ team_id: teamId }),
371
+ });
372
+ }
373
+ async unblockTeam(teamId) {
374
+ return this.request('/team/unblock', {
375
+ method: 'POST',
376
+ body: JSON.stringify({ team_id: teamId }),
377
+ });
378
+ }
379
+ /**
380
+ * Adds a member to a team. LiteLLM's `/team/member_add` nests the member
381
+ * under a `member` object; `role` is 'user' or 'admin' (we only ever send
382
+ * 'user' from Backstage). `max_budget_in_team` optionally caps that member's
383
+ * spend within the team.
384
+ */
385
+ async teamMemberAdd(payload) {
386
+ return this.request('/team/member_add', {
387
+ method: 'POST',
388
+ body: JSON.stringify({
389
+ team_id: payload.team_id,
390
+ member: { user_id: payload.user_id, role: payload.role ?? 'user' },
391
+ ...(payload.max_budget_in_team !== undefined && {
392
+ max_budget_in_team: payload.max_budget_in_team,
393
+ }),
394
+ }),
395
+ });
396
+ }
397
+ /** Removes a member from a team via LiteLLM's `/team/member_delete`. */
398
+ async teamMemberDelete(payload) {
399
+ return this.request('/team/member_delete', {
400
+ method: 'POST',
401
+ body: JSON.stringify({
402
+ team_id: payload.team_id,
403
+ user_id: payload.user_id,
404
+ }),
405
+ });
406
+ }
407
+ /**
408
+ * Lists the vector stores (knowledge bases) registered on the LiteLLM proxy.
409
+ * Tolerates a bare array, `{ data: [...] }`, or `{ vector_stores: [...] }`.
410
+ * Each entry is normalised to `{ id, name? }` — `id` prefers
411
+ * `vector_store_id` then `id` then `name`.
412
+ */
413
+ async listVectorStores() {
414
+ const raw = await this.request('/vector_store/list');
415
+ let rows = [];
416
+ if (Array.isArray(raw))
417
+ rows = raw;
418
+ else if (Array.isArray(raw?.data))
419
+ rows = raw.data;
420
+ else if (Array.isArray(raw?.vector_stores))
421
+ rows = raw.vector_stores;
422
+ return rows
423
+ .map(r => ({
424
+ id: r?.vector_store_id ?? r?.id ?? r?.name ?? '',
425
+ name: r?.vector_store_name ?? r?.name ?? undefined,
426
+ }))
427
+ .filter(r => r.id);
428
+ }
429
+ /**
430
+ * Lists the MCP servers registered on the LiteLLM proxy. Path is
431
+ * `/mcp/server/list`; tolerates a bare array, `{ data: [...] }` or
432
+ * `{ servers: [...] }`. Normalised to `{ id, name?, url? }` — `id` prefers
433
+ * `server_id` then `id` then `alias`/`name`.
434
+ */
435
+ async listMcpServers() {
436
+ const raw = await this.request('/mcp/server/list');
437
+ let rows = [];
438
+ if (Array.isArray(raw))
439
+ rows = raw;
440
+ else if (Array.isArray(raw?.data))
441
+ rows = raw.data;
442
+ else if (Array.isArray(raw?.servers))
443
+ rows = raw.servers;
444
+ return rows
445
+ .map(r => ({
446
+ id: r?.server_id ?? r?.id ?? r?.alias ?? r?.name ?? '',
447
+ name: r?.alias ?? r?.name ?? undefined,
448
+ url: r?.url ?? undefined,
449
+ }))
450
+ .filter(r => r.id);
451
+ }
315
452
  emptyUsage() {
316
453
  return {
317
454
  total_spend: 0,