@acarmisc/backstage-plugin-litellm-backend 0.10.0 → 0.11.1

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/router.js CHANGED
@@ -46,6 +46,44 @@ const bridge_1 = require("./bridge");
46
46
  const permissions_1 = require("./permissions");
47
47
  const teamAdmin_1 = require("./teamAdmin");
48
48
  const teamCreateInFlight = new Map();
49
+ /** Backoff schedule for `withTeamFetchRetry` — 3 retries over ~1.3s total. */
50
+ const TEAM_FETCH_RETRY_DELAYS_MS = [100, 300, 900];
51
+ /**
52
+ * Only 5xx (and network/timeout) failures are treated as transient — a 4xx
53
+ * like "team not found" or "forbidden" is a deterministic answer from the
54
+ * upstream and retrying it would just add latency for the same result.
55
+ */
56
+ function isRetryableTeamFetchError(err) {
57
+ if (err instanceof client_1.LiteLLMUpstreamError) {
58
+ return err.status >= 500;
59
+ }
60
+ return true;
61
+ }
62
+ /**
63
+ * Retries a LiteLLM team-info fetch with the given backoff schedule.
64
+ *
65
+ * LiteLLM proxies are commonly run behind multiple replicas; a request can
66
+ * land on a replica that hasn't yet caught up with a very recent write
67
+ * (team create/update, membership change), or on one that is transiently
68
+ * unhealthy. Both surface as a failed `getTeamInfo` call that would
69
+ * otherwise be swallowed (e.g. GET /teams silently drops the team from the
70
+ * response) or bubble up as a spurious 500/404 right after a write this
71
+ * same request just made. A short retry smooths over that window without
72
+ * masking a genuine, persistent failure.
73
+ */
74
+ async function withTeamFetchRetry(fn, delaysMs = TEAM_FETCH_RETRY_DELAYS_MS) {
75
+ for (let attempt = 0;; attempt++) {
76
+ try {
77
+ return await fn();
78
+ }
79
+ catch (err) {
80
+ if (attempt >= delaysMs.length || !isRetryableTeamFetchError(err)) {
81
+ throw err;
82
+ }
83
+ await new Promise(resolve => setTimeout(resolve, delaysMs[attempt]));
84
+ }
85
+ }
86
+ }
49
87
  async function createRouter(options) {
50
88
  const { config, logger, auth, discovery, permissions } = options;
51
89
  const baseUrl = config.getString('litellm.baseUrl');
@@ -89,7 +127,7 @@ async function createRouter(options) {
89
127
  keyGeneration: { allowUnlimitedBudget, teamRequired },
90
128
  teamManagement: {
91
129
  enabled: teamMgmtEnabled,
92
- maxBudgetCeiling: teamAdminCfg.maxBudgetCeiling ?? null,
130
+ maxBudgetCeiling: teamAdminCfg.maxBudgetCeiling,
93
131
  allowUnlimitedBudget: teamAdminCfg.allowUnlimitedBudget,
94
132
  objectPermissionsEnabled: objectPermsEnabled,
95
133
  },
@@ -510,8 +548,8 @@ async function createRouter(options) {
510
548
  res.json([]);
511
549
  return;
512
550
  }
513
- const teams = await Promise.all(userInfo.teams.map(teamId => client.getTeamInfo(teamId).catch(err => {
514
- logger.warn(`Failed to fetch team ${teamId}: ${err.message}`);
551
+ const teams = await Promise.all(userInfo.teams.map(teamId => withTeamFetchRetry(() => client.getTeamInfo(teamId)).catch(err => {
552
+ logger.warn(`Failed to fetch team ${teamId} after retries: ${err.message}`);
515
553
  return null;
516
554
  })));
517
555
  res.json(teams.filter(Boolean));
@@ -773,7 +811,7 @@ async function createRouter(options) {
773
811
  }
774
812
  let existing;
775
813
  try {
776
- existing = await client.getTeamInfo(teamId);
814
+ existing = await withTeamFetchRetry(() => client.getTeamInfo(teamId));
777
815
  }
778
816
  catch (err) {
779
817
  if (err instanceof client_1.LiteLLMUpstreamError && err.status === 404) {
@@ -885,7 +923,7 @@ async function createRouter(options) {
885
923
  member: litellmUserId,
886
924
  owningGroup,
887
925
  });
888
- const updated = await client.getTeamInfo(teamId);
926
+ const updated = await withTeamFetchRetry(() => client.getTeamInfo(teamId));
889
927
  res.json(updated);
890
928
  }
891
929
  catch (err) {
@@ -920,7 +958,7 @@ async function createRouter(options) {
920
958
  member: litellmUserId,
921
959
  owningGroup,
922
960
  });
923
- const updated = await client.getTeamInfo(teamId);
961
+ const updated = await withTeamFetchRetry(() => client.getTeamInfo(teamId));
924
962
  res.json(updated);
925
963
  }
926
964
  catch (err) {
@@ -3,6 +3,13 @@ import { Config } from '@backstage/config';
3
3
  import { AuthService, PermissionsService } from '@backstage/backend-plugin-api';
4
4
  import { BasicPermission } from '@backstage/plugin-permission-common';
5
5
  import { CatalogClient } from '@backstage/catalog-client';
6
+ /**
7
+ * Default hard ceiling (USD) for an admin-set team budget when
8
+ * `litellm.teamAdmin.maxBudgetCeiling` is not configured. Chosen so the
9
+ * feature is usable out of the box without letting an admin set an unbounded
10
+ * team budget by omission.
11
+ */
12
+ export declare const DEFAULT_TEAM_BUDGET_CEILING = 1000;
6
13
  /**
7
14
  * Governance limits for the team-administration feature. Every field is
8
15
  * fail-closed: an unset array means "nothing allowed", an unset boolean means
@@ -27,10 +34,10 @@ export interface TeamAdminConfig {
27
34
  */
28
35
  allowedModelAccessGroups: string[];
29
36
  /**
30
- * Hard USD ceiling for a team's max_budget an admin may set.
31
- * Undefined => no admin-settable budget allowed unless allowUnlimitedBudget.
37
+ * Hard USD ceiling for a team's max_budget an admin may set. Always a
38
+ * number defaults to DEFAULT_TEAM_BUDGET_CEILING when not configured.
32
39
  */
33
- maxBudgetCeiling?: number;
40
+ maxBudgetCeiling: number;
34
41
  /**
35
42
  * Allow an admin to create a team with no budget cap.
36
43
  */
package/dist/teamAdmin.js CHANGED
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_TEAM_BUDGET_CEILING = void 0;
3
4
  exports.isTeamManagementEnabled = isTeamManagementEnabled;
4
5
  exports.isObjectPermissionsEnabled = isObjectPermissionsEnabled;
5
6
  exports.assertTeamAdmin = assertTeamAdmin;
@@ -8,6 +9,13 @@ exports.validateTeamWriteInput = validateTeamWriteInput;
8
9
  exports.validateTeamPatchInput = validateTeamPatchInput;
9
10
  const plugin_permission_common_1 = require("@backstage/plugin-permission-common");
10
11
  const provisioning_1 = require("./provisioning");
12
+ /**
13
+ * Default hard ceiling (USD) for an admin-set team budget when
14
+ * `litellm.teamAdmin.maxBudgetCeiling` is not configured. Chosen so the
15
+ * feature is usable out of the box without letting an admin set an unbounded
16
+ * team budget by omission.
17
+ */
18
+ exports.DEFAULT_TEAM_BUDGET_CEILING = 1000;
11
19
  /**
12
20
  * Whether team-management routes should be mounted.
13
21
  * Requires permission.enabled AND a designated admin group.
@@ -95,7 +103,8 @@ function readTeamAdminConfig(config) {
95
103
  group: config.getOptionalString('litellm.teamAdmin.group'),
96
104
  allowedModels: config.getOptionalStringArray('litellm.teamAdmin.allowedModels') ?? [],
97
105
  allowedModelAccessGroups: config.getOptionalStringArray('litellm.teamAdmin.allowedModelAccessGroups') ?? [],
98
- maxBudgetCeiling: config.getOptionalNumber('litellm.teamAdmin.maxBudgetCeiling'),
106
+ maxBudgetCeiling: config.getOptionalNumber('litellm.teamAdmin.maxBudgetCeiling') ??
107
+ exports.DEFAULT_TEAM_BUDGET_CEILING,
99
108
  allowUnlimitedBudget: config.getOptionalBoolean('litellm.teamAdmin.allowUnlimitedBudget') ??
100
109
  false,
101
110
  allowedVectorStores: config.getOptionalStringArray('litellm.teamAdmin.allowedVectorStores') ??
@@ -134,14 +143,11 @@ function validateTeamWriteInput(input, cfg) {
134
143
  if (maxBudget === undefined) {
135
144
  return { ok: false, error: 'max_budget is required' };
136
145
  }
137
- if (cfg.maxBudgetCeiling === undefined) {
138
- return { ok: false, error: 'team budget ceiling is not configured (litellm.teamAdmin.maxBudgetCeiling)' };
139
- }
140
146
  if (maxBudget > cfg.maxBudgetCeiling) {
141
147
  return { ok: false, error: `max_budget $${maxBudget} exceeds the ceiling $${cfg.maxBudgetCeiling}` };
142
148
  }
143
149
  }
144
- else if (maxBudget !== undefined && cfg.maxBudgetCeiling !== undefined && maxBudget > cfg.maxBudgetCeiling) {
150
+ else if (maxBudget !== undefined && maxBudget > cfg.maxBudgetCeiling) {
145
151
  return { ok: false, error: `max_budget $${maxBudget} exceeds the ceiling $${cfg.maxBudgetCeiling}` };
146
152
  }
147
153
  let budgetDuration = input.budget_duration;
@@ -203,7 +209,7 @@ function validateTeamPatchInput(input, cfg) {
203
209
  if (!Number.isFinite(input.max_budget) || input.max_budget <= 0) {
204
210
  return { ok: false, error: 'max_budget must be a positive number' };
205
211
  }
206
- if (cfg.maxBudgetCeiling !== undefined && input.max_budget > cfg.maxBudgetCeiling) {
212
+ if (input.max_budget > cfg.maxBudgetCeiling) {
207
213
  return { ok: false, error: `max_budget $${input.max_budget} exceeds the ceiling $${cfg.maxBudgetCeiling}` };
208
214
  }
209
215
  result.max_budget = input.max_budget;
@@ -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 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"],
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-reset period for max_budget, e.g. \"30d\". */\n budget_duration?: string;\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
@@ -29,6 +29,8 @@ export interface TeamInfo {
29
29
  team_id: string;
30
30
  team_alias?: string;
31
31
  max_budget?: number;
32
+ /** Spend-reset period for max_budget, e.g. "30d". */
33
+ budget_duration?: string;
32
34
  spend: number;
33
35
  members_with_roles?: TeamMember[];
34
36
  models?: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acarmisc/backstage-plugin-litellm-backend",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "description": "The Backstage backend plugin for LiteLLM governance",
5
5
  "backstage": {
6
6
  "role": "backend-plugin",