@acarmisc/backstage-plugin-litellm-backend 0.10.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/dist/router.js CHANGED
@@ -89,7 +89,7 @@ async function createRouter(options) {
89
89
  keyGeneration: { allowUnlimitedBudget, teamRequired },
90
90
  teamManagement: {
91
91
  enabled: teamMgmtEnabled,
92
- maxBudgetCeiling: teamAdminCfg.maxBudgetCeiling ?? null,
92
+ maxBudgetCeiling: teamAdminCfg.maxBudgetCeiling,
93
93
  allowUnlimitedBudget: teamAdminCfg.allowUnlimitedBudget,
94
94
  objectPermissionsEnabled: objectPermsEnabled,
95
95
  },
@@ -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.0",
4
4
  "description": "The Backstage backend plugin for LiteLLM governance",
5
5
  "backstage": {
6
6
  "role": "backend-plugin",