@acarmisc/backstage-plugin-litellm-backend 0.31.0 → 0.32.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/openapi.js CHANGED
@@ -63,6 +63,7 @@ exports.openApiSpec = {
63
63
  content: { 'application/json': { schema: { type: 'object', properties: {
64
64
  baseUrl: { type: 'string' },
65
65
  keyGeneration: { type: 'object', properties: {
66
+ allowedDurations: { type: 'array', items: { type: 'string' }, description: 'Durations a new key may have (litellm.keys.allowedDurations); "never" means no expiry' },
66
67
  allowUnlimitedBudget: { type: 'boolean' },
67
68
  teamRequired: { type: 'boolean' },
68
69
  } },
@@ -40,6 +40,11 @@ export declare function applyRoleOverrides(defaults: ProvisioningDefaults, role:
40
40
  * tpmLimit: none — LiteLLM global / team limits still apply
41
41
  * rpmLimit: none — same
42
42
  * metadata: backstage source tag only
43
+ *
44
+ * With `useGatewayBudgetDefaults: true` the maxBudget / budgetDuration
45
+ * fallbacks are dropped: an unset field stays undefined, provisionUser leaves
46
+ * it out of /user/new, and LiteLLM's max_internal_user_budget /
47
+ * internal_user_budget_duration apply instead (internal_user role only).
43
48
  */
44
49
  export declare function readProvisioningDefaults(config: Config): {
45
50
  enabled: boolean;
@@ -88,12 +88,20 @@ function applyRoleOverrides(defaults, role) {
88
88
  * tpmLimit: none — LiteLLM global / team limits still apply
89
89
  * rpmLimit: none — same
90
90
  * metadata: backstage source tag only
91
+ *
92
+ * With `useGatewayBudgetDefaults: true` the maxBudget / budgetDuration
93
+ * fallbacks are dropped: an unset field stays undefined, provisionUser leaves
94
+ * it out of /user/new, and LiteLLM's max_internal_user_budget /
95
+ * internal_user_budget_duration apply instead (internal_user role only).
91
96
  */
92
97
  function readProvisioningDefaults(config) {
93
98
  const enabled = config.getOptionalBoolean('litellm.provisioning.enabled') ?? false;
99
+ const useGatewayBudgetDefaults = config.getOptionalBoolean('litellm.provisioning.defaults.useGatewayBudgetDefaults') ?? false;
100
+ const maxBudget = config.getOptionalNumber('litellm.provisioning.defaults.maxBudget');
101
+ const budgetDuration = config.getOptionalString('litellm.provisioning.defaults.budgetDuration');
94
102
  const defaults = {
95
- maxBudget: config.getOptionalNumber('litellm.provisioning.defaults.maxBudget') ?? 10,
96
- budgetDuration: config.getOptionalString('litellm.provisioning.defaults.budgetDuration') ?? '30d',
103
+ maxBudget: maxBudget ?? (useGatewayBudgetDefaults ? undefined : 10),
104
+ budgetDuration: budgetDuration ?? (useGatewayBudgetDefaults ? undefined : '30d'),
97
105
  models: config.getOptionalStringArray('litellm.provisioning.defaults.models') ??
98
106
  [],
99
107
  teams: config.getOptionalStringArray('litellm.provisioning.defaults.teams') ??
@@ -180,8 +188,11 @@ async function provisionUser(client, userId, defaults, profile, backstageEntity,
180
188
  user_id: userId,
181
189
  ...(profile.email && { user_email: profile.email }),
182
190
  ...(profile.displayName && { user_alias: profile.displayName }),
183
- max_budget: defaults.maxBudget,
184
- budget_duration: defaults.budgetDuration,
191
+ // Undefined = left out, so LiteLLM's internal-user defaults apply.
192
+ ...(defaults.maxBudget !== undefined && { max_budget: defaults.maxBudget }),
193
+ ...(defaults.budgetDuration !== undefined && {
194
+ budget_duration: defaults.budgetDuration,
195
+ }),
185
196
  models: defaults.models,
186
197
  teams: defaults.teams,
187
198
  ...(defaults.tpmLimit !== undefined && { tpm_limit: defaults.tpmLimit }),
package/dist/router.js CHANGED
@@ -112,7 +112,9 @@ async function createRouter(options) {
112
112
  const langfuseCfg = (0, langfuse_1.readLangfuseConfig)(config);
113
113
  const catalogClient = options.catalogClient ?? new catalog_client_1.CatalogClient({ discoveryApi: discovery });
114
114
  if (provisioningEnabled) {
115
- logger.info(`LiteLLM auto-provisioning enabled — defaults: budget=$${provisioningDefaults.maxBudget}/${provisioningDefaults.budgetDuration}, models=${provisioningDefaults.models.length
115
+ logger.info(`LiteLLM auto-provisioning enabled — defaults: budget=${provisioningDefaults.maxBudget === undefined
116
+ ? 'gateway default'
117
+ : `$${provisioningDefaults.maxBudget}`}/${provisioningDefaults.budgetDuration ?? 'gateway default'}, models=${provisioningDefaults.models.length
116
118
  ? provisioningDefaults.models.join(',')
117
119
  : 'all'}, teams=[${provisioningDefaults.teams.join(',')}]`);
118
120
  }
@@ -18,7 +18,11 @@ function registerConfigRoutes(router, ctx) {
18
18
  res.json({
19
19
  baseUrl: ctx.publicBaseUrl,
20
20
  supportContact: ctx.supportContact,
21
- keyGeneration: { allowUnlimitedBudget: ctx.allowUnlimitedBudget, teamRequired: ctx.teamRequired },
21
+ keyGeneration: {
22
+ allowUnlimitedBudget: ctx.allowUnlimitedBudget,
23
+ teamRequired: ctx.teamRequired,
24
+ allowedDurations: ctx.keyValidationConfig.allowedDurations,
25
+ },
22
26
  keyActions: { allowOwnerResetSpend: ctx.allowOwnerResetSpend },
23
27
  opencode: { enabled: ctx.opencodeCfg.enabled },
24
28
  teamManagement: {
@@ -10,6 +10,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.KeyServiceError = void 0;
11
11
  exports.createKeyForUser = createKeyForUser;
12
12
  const provisioning_1 = require("../provisioning");
13
+ const backstage_plugin_litellm_common_1 = require("@acarmisc/backstage-plugin-litellm-common");
13
14
  const modelAccess_1 = require("./modelAccess");
14
15
  const mcpAccess_1 = require("./mcpAccess");
15
16
  /** Error thrown during key creation — maps to HTTP responses. */
@@ -145,10 +146,19 @@ async function createKeyForUser(user, input, ctx) {
145
146
  upstreamRequest.models = input.models;
146
147
  }
147
148
  // Never mint a non-expiring key by omission: default to 30d, or the first
148
- // allowed duration when 30d isn't offered.
149
+ // allowed duration other than "never" when 30d isn't offered.
149
150
  const durations = ctx.keyValidationConfig.allowedDurations;
150
- const defaultDuration = durations.length === 0 || durations.includes('30d') ? '30d' : durations[0];
151
- upstreamRequest.duration = input.duration ?? defaultDuration;
151
+ const defaultDuration = durations.length === 0 || durations.includes('30d')
152
+ ? '30d'
153
+ : durations.find(d => d !== backstage_plugin_litellm_common_1.NEVER_KEY_DURATION);
154
+ const duration = input.duration ?? defaultDuration;
155
+ if (duration === undefined) {
156
+ throw new KeyServiceError(400, { error: 'duration is required' });
157
+ }
158
+ // LiteLLM treats a missing duration as "never expires".
159
+ if (duration !== backstage_plugin_litellm_common_1.NEVER_KEY_DURATION) {
160
+ upstreamRequest.duration = duration;
161
+ }
152
162
  if (input.max_budget !== undefined) {
153
163
  // max_budget from input can be null (unlimited) or a number
154
164
  upstreamRequest.max_budget = input.max_budget ?? undefined;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/types.ts"],
4
- "sourcesContent": ["// Re-export common types for backward compatibility\nexport {\n type UserInfo,\n type TeamMember,\n type TeamObjectPermission,\n type TeamInfo,\n type CreateTeamResponse,\n type VirtualKey,\n type ModelInfo,\n type UsageModelBreakdown,\n type UsageKeyBreakdown,\n type UsageDailyPoint,\n type UsageDailyModelPoint,\n type UsageMetrics,\n type TeamMemberUsage,\n type TeamMemberUsageRow,\n type TeamKeyUsage,\n type TeamKeyUsageRow,\n type GenerateKeyRequest,\n type UpdateKeyRequest,\n type GenerateKeyResponse,\n type AuditLogEntry,\n type PaginatedAuditLogs,\n type SpendLogEntry,\n type SpendLogsParams,\n type PaginatedSpendLogs,\n type QualityByModel,\n type QualityScore,\n type QualitySummary,\n type QualityResponse,\n} from '@acarmisc/backstage-plugin-litellm-common';\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?: {\n vector_stores?: string[];\n mcp_servers?: string[];\n mcp_access_groups?: string[];\n };\n}\n\nexport interface UpdateTeamRequest extends Partial<CreateTeamRequest> {\n team_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 budget_duration?: string | null;\n user_id?: string | null;\n team_id?: string | null;\n created_at: string;\n blocked?: boolean | null;\n /** Arbitrary metadata stored on the key record. */\n metadata?: Record<string, unknown> | null;\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\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": ["// Re-export common types for backward compatibility\nexport {\n type UserInfo,\n type TeamMember,\n type TeamObjectPermission,\n type TeamInfo,\n type CreateTeamResponse,\n type VirtualKey,\n type ModelInfo,\n type UsageModelBreakdown,\n type UsageKeyBreakdown,\n type UsageDailyPoint,\n type UsageDailyModelPoint,\n type UsageMetrics,\n type TeamMemberUsage,\n type TeamMemberUsageRow,\n type TeamKeyUsage,\n type TeamKeyUsageRow,\n type GenerateKeyRequest,\n type UpdateKeyRequest,\n type GenerateKeyResponse,\n type AuditLogEntry,\n type PaginatedAuditLogs,\n type SpendLogEntry,\n type SpendLogsParams,\n type PaginatedSpendLogs,\n type QualityByModel,\n type QualityScore,\n type QualitySummary,\n type QualityResponse,\n} from '@acarmisc/backstage-plugin-litellm-common';\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?: {\n vector_stores?: string[];\n mcp_servers?: string[];\n mcp_access_groups?: string[];\n };\n}\n\nexport interface UpdateTeamRequest extends Partial<CreateTeamRequest> {\n team_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 budget_duration?: string | null;\n user_id?: string | null;\n team_id?: string | null;\n created_at: string;\n blocked?: boolean | null;\n /** Arbitrary metadata stored on the key record. */\n metadata?: Record<string, unknown> | null;\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 /**\n * max_budget sent on /user/new. Undefined (only possible with\n * litellm.provisioning.defaults.useGatewayBudgetDefaults) leaves it out, so\n * LiteLLM's max_internal_user_budget applies.\n */\n maxBudget?: number;\n /**\n * budget_duration sent on /user/new. Undefined (only possible with\n * litellm.provisioning.defaults.useGatewayBudgetDefaults) leaves it out, so\n * LiteLLM's internal_user_budget_duration applies.\n */\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\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
@@ -49,8 +49,18 @@ export interface LiteLLMConfig {
49
49
  masterKey: string;
50
50
  }
51
51
  export interface ProvisioningDefaults {
52
- maxBudget: number;
53
- budgetDuration: string;
52
+ /**
53
+ * max_budget sent on /user/new. Undefined (only possible with
54
+ * litellm.provisioning.defaults.useGatewayBudgetDefaults) leaves it out, so
55
+ * LiteLLM's max_internal_user_budget applies.
56
+ */
57
+ maxBudget?: number;
58
+ /**
59
+ * budget_duration sent on /user/new. Undefined (only possible with
60
+ * litellm.provisioning.defaults.useGatewayBudgetDefaults) leaves it out, so
61
+ * LiteLLM's internal_user_budget_duration applies.
62
+ */
63
+ budgetDuration?: string;
54
64
  models: string[];
55
65
  teams: string[];
56
66
  tpmLimit?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acarmisc/backstage-plugin-litellm-backend",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "description": "The Backstage backend plugin for LiteLLM governance",
5
5
  "backstage": {
6
6
  "role": "backend-plugin",
@@ -35,7 +35,7 @@
35
35
  "prepack": "npm run build"
36
36
  },
37
37
  "dependencies": {
38
- "@acarmisc/backstage-plugin-litellm-common": "0.6.0",
38
+ "@acarmisc/backstage-plugin-litellm-common": "0.7.0",
39
39
  "@backstage/backend-plugin-api": "^1.11.0",
40
40
  "@backstage/catalog-client": "^1.9.0",
41
41
  "@backstage/config": "^1.3.0",