@acarmisc/backstage-plugin-litellm-backend 0.9.0 → 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.
package/config.d.ts CHANGED
@@ -148,6 +148,107 @@ 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
+ * When omitted, no admin-settable budget is allowed unless
188
+ * allowUnlimitedBudget is true.
189
+ * @visibility backend
190
+ */
191
+ maxBudgetCeiling?: number;
192
+
193
+ /**
194
+ * Allow an admin to create a team with no budget cap.
195
+ * @default false
196
+ * @visibility backend
197
+ */
198
+ allowUnlimitedBudget?: boolean;
199
+
200
+ /**
201
+ * Vector-store ids/names an admin may attach as team knowledge bases.
202
+ * Empty array => none allowed.
203
+ * @default []
204
+ * @visibility backend
205
+ */
206
+ allowedVectorStores?: string[];
207
+
208
+ /**
209
+ * MCP server ids/names an admin may attach to a team.
210
+ * Empty array => none allowed.
211
+ * @default []
212
+ * @visibility backend
213
+ */
214
+ allowedMcpServers?: string[];
215
+
216
+ /**
217
+ * MCP access-group names an admin may attach to a team.
218
+ * References access_groups defined in litellm.mcp_info configuration.
219
+ * Empty array => none allowed.
220
+ * @default []
221
+ * @visibility backend
222
+ */
223
+ allowedMcpAccessGroups?: string[];
224
+
225
+ /**
226
+ * Allow an admin to delete a team (vs. only block/deactivate).
227
+ * @default false
228
+ * @visibility backend
229
+ */
230
+ allowTeamDelete?: boolean;
231
+
232
+ /**
233
+ * Object-permission management (attaching knowledge bases / MCP servers
234
+ * to a team). This is the highest-risk surface — attaching a vector store
235
+ * exposes its documents to every team key, and attaching an MCP server
236
+ * grants tool execution. Keep disabled unless a real permission policy
237
+ * (@backstage-community/plugin-rbac or a custom PermissionPolicy) is
238
+ * installed and the allowedVectorStores / allowedMcpServers allowlists
239
+ * are set.
240
+ */
241
+ objectPermissions?: {
242
+ /**
243
+ * When true, mount the knowledge-base and MCP management routes.
244
+ * Still gated by the permission framework and the allowlists.
245
+ * @default false
246
+ * @visibility backend
247
+ */
248
+ enabled?: boolean;
249
+ };
250
+ };
251
+
151
252
  bridge?: {
152
253
  /**
153
254
  * 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,9 +300,18 @@ 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,
308
317
  spend: inner.spend ?? raw?.spend ?? 0,
@@ -310,8 +319,135 @@ class LiteLLMClient {
310
319
  models: inner.models ?? raw?.models,
311
320
  tpm_limit: inner.tpm_limit ?? raw?.tpm_limit,
312
321
  rpm_limit: inner.rpm_limit ?? raw?.rpm_limit,
322
+ metadata: inner.metadata ?? raw?.metadata,
323
+ object_permission: inner.object_permission ?? raw?.object_permission,
324
+ blocked: inner.blocked ?? raw?.blocked,
325
+ team_member_budget: inner.team_member_budget ?? raw?.team_member_budget,
313
326
  };
314
327
  }
328
+ /**
329
+ * Lists every team known to the LiteLLM proxy. Tolerates the observed
330
+ * response shapes — a bare array, `{ teams: [...] }`, or `{ data: [...] }` —
331
+ * and normalises each row through the same unwrap as `getTeamInfo`.
332
+ *
333
+ * This returns the GLOBAL team list and must never be exposed directly to
334
+ * end users; callers are responsible for scoping the result (e.g. to teams
335
+ * whose `metadata.owning_group` matches the caller's admin group).
336
+ */
337
+ async listTeams() {
338
+ const raw = await this.request('/team/list');
339
+ let rows = [];
340
+ if (Array.isArray(raw))
341
+ rows = raw;
342
+ else if (Array.isArray(raw?.teams))
343
+ rows = raw.teams;
344
+ else if (Array.isArray(raw?.data))
345
+ rows = raw.data;
346
+ return rows.map(row => this.toTeamInfo(row));
347
+ }
348
+ async createTeam(payload) {
349
+ return this.request('/team/new', {
350
+ method: 'POST',
351
+ body: JSON.stringify(payload),
352
+ });
353
+ }
354
+ async updateTeam(payload) {
355
+ return this.request('/team/update', {
356
+ method: 'POST',
357
+ body: JSON.stringify(payload),
358
+ });
359
+ }
360
+ async deleteTeam(teamId) {
361
+ return this.request('/team/delete', {
362
+ method: 'POST',
363
+ body: JSON.stringify({ team_ids: [teamId] }),
364
+ });
365
+ }
366
+ async blockTeam(teamId) {
367
+ return this.request('/team/block', {
368
+ method: 'POST',
369
+ body: JSON.stringify({ team_id: teamId }),
370
+ });
371
+ }
372
+ async unblockTeam(teamId) {
373
+ return this.request('/team/unblock', {
374
+ method: 'POST',
375
+ body: JSON.stringify({ team_id: teamId }),
376
+ });
377
+ }
378
+ /**
379
+ * Adds a member to a team. LiteLLM's `/team/member_add` nests the member
380
+ * under a `member` object; `role` is 'user' or 'admin' (we only ever send
381
+ * 'user' from Backstage). `max_budget_in_team` optionally caps that member's
382
+ * spend within the team.
383
+ */
384
+ async teamMemberAdd(payload) {
385
+ return this.request('/team/member_add', {
386
+ method: 'POST',
387
+ body: JSON.stringify({
388
+ team_id: payload.team_id,
389
+ member: { user_id: payload.user_id, role: payload.role ?? 'user' },
390
+ ...(payload.max_budget_in_team !== undefined && {
391
+ max_budget_in_team: payload.max_budget_in_team,
392
+ }),
393
+ }),
394
+ });
395
+ }
396
+ /** Removes a member from a team via LiteLLM's `/team/member_delete`. */
397
+ async teamMemberDelete(payload) {
398
+ return this.request('/team/member_delete', {
399
+ method: 'POST',
400
+ body: JSON.stringify({
401
+ team_id: payload.team_id,
402
+ user_id: payload.user_id,
403
+ }),
404
+ });
405
+ }
406
+ /**
407
+ * Lists the vector stores (knowledge bases) registered on the LiteLLM proxy.
408
+ * Tolerates a bare array, `{ data: [...] }`, or `{ vector_stores: [...] }`.
409
+ * Each entry is normalised to `{ id, name? }` — `id` prefers
410
+ * `vector_store_id` then `id` then `name`.
411
+ */
412
+ async listVectorStores() {
413
+ const raw = await this.request('/vector_store/list');
414
+ let rows = [];
415
+ if (Array.isArray(raw))
416
+ rows = raw;
417
+ else if (Array.isArray(raw?.data))
418
+ rows = raw.data;
419
+ else if (Array.isArray(raw?.vector_stores))
420
+ rows = raw.vector_stores;
421
+ return rows
422
+ .map(r => ({
423
+ id: r?.vector_store_id ?? r?.id ?? r?.name ?? '',
424
+ name: r?.vector_store_name ?? r?.name ?? undefined,
425
+ }))
426
+ .filter(r => r.id);
427
+ }
428
+ /**
429
+ * Lists the MCP servers registered on the LiteLLM proxy. Path is
430
+ * `/mcp/server/list`; tolerates a bare array, `{ data: [...] }` or
431
+ * `{ servers: [...] }`. Normalised to `{ id, name?, url? }` — `id` prefers
432
+ * `server_id` then `id` then `alias`/`name`.
433
+ */
434
+ async listMcpServers() {
435
+ const raw = await this.request('/mcp/server/list');
436
+ let rows = [];
437
+ if (Array.isArray(raw))
438
+ rows = raw;
439
+ else if (Array.isArray(raw?.data))
440
+ rows = raw.data;
441
+ else if (Array.isArray(raw?.servers))
442
+ rows = raw.servers;
443
+ return rows
444
+ .map(r => ({
445
+ id: r?.server_id ?? r?.id ?? r?.alias ?? r?.name ?? '',
446
+ name: r?.alias ?? r?.name ?? undefined,
447
+ url: r?.url ?? undefined,
448
+ }))
449
+ .filter(r => r.id);
450
+ }
315
451
  emptyUsage() {
316
452
  return {
317
453
  total_spend: 0,