@acarmisc/backstage-plugin-litellm-backend 0.18.0 → 0.19.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.
@@ -1,16 +1,15 @@
1
1
  "use strict";
2
2
  /**
3
- * Key generation service — unified by PR-6.
3
+ * Key generation service.
4
4
  *
5
5
  * Extracts the common logic for creating API keys from both the UI (POST /keys/generate)
6
- * and the bridge (POST /bridge/keys). Handles validation, provisioning, team/model checks,
7
- * and explicit upstream request construction.
6
+ * and the bridge (POST /bridge/keys). Handles validation, team/model checks,
7
+ * and explicit upstream request construction. Callers resolve the user beforehand.
8
8
  */
9
9
  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
- /** Unified identity for key creation — resolved from either Backstage or JWT claims. */
14
13
  const modelAccess_1 = require("./modelAccess");
15
14
  /** Error thrown during key creation — maps to HTTP responses. */
16
15
  class KeyServiceError extends Error {
@@ -27,57 +26,21 @@ exports.KeyServiceError = KeyServiceError;
27
26
  * Performs:
28
27
  * - Schema validation of the input
29
28
  * - Config flag enforcement (allowUnlimitedBudget, teamRequired)
30
- * - User provisioning (single-flight, role configs)
31
29
  * - Team membership validation
32
30
  * - Model subset validation
33
31
  * - Metadata stamping
34
32
  * - Explicit upstream request construction and call
35
33
  *
36
- * @param user Verified identity: tokenEntityRef (optional) + userId (required)
34
+ * @param user Resolved identity: tokenEntityRef (optional) + userInfo (required, pre-provisioned)
37
35
  * @param input Parsed GenerateKeyInput from the schema
38
36
  * @param ctx Configuration and service clients
39
37
  * @returns The new key response from LiteLLM
40
38
  * @throws KeyServiceError with status and body for HTTP response
41
- * @throws ProvisioningError for provisioning failures
42
39
  * @throws LiteLLMUpstreamError for upstream LiteLLM errors
43
40
  */
44
41
  async function createKeyForUser(user, input, ctx) {
45
- const { client, config, catalogClient, auth, logger, provisioningEnabled, provisioningDefaults, roleConfigs, } = ctx;
46
- // ── Ensure user is provisioned ──────────────────────────────────────
47
- // For bridge (no catalogClient/auth), use direct provisioning from JWT;
48
- // for UI (with catalogClient/auth), use full provisioning with group roles.
49
- let userInfo;
50
- if (catalogClient !== undefined && catalogClient !== null && auth !== undefined && auth !== null) {
51
- userInfo = await (0, provisioning_1.getOrProvisionUser)(client, user.tokenEntityRef || '', user.userId, provisioningEnabled, provisioningDefaults, roleConfigs, catalogClient, auth, logger);
52
- }
53
- else {
54
- // Bridge path: just check if user exists, or provision with minimal info
55
- userInfo = await client.getUserInfo(user.userId);
56
- if (!userInfo) {
57
- if (!provisioningEnabled) {
58
- throw new KeyServiceError(404, {
59
- error: 'User not found in LiteLLM',
60
- detail: 'No LiteLLM user for this identity. Log in to Backstage once to be provisioned, or enable litellm.provisioning.enabled.',
61
- });
62
- }
63
- // Provision with minimal info (just user_id, will use defaults)
64
- const created = await client.createUser({
65
- user_id: user.userId,
66
- user_email: user.userId,
67
- max_budget: provisioningDefaults.maxBudget,
68
- budget_duration: provisioningDefaults.budgetDuration,
69
- models: provisioningDefaults.models,
70
- teams: provisioningDefaults.teams?.length ? [provisioningDefaults.teams[0]] : [],
71
- });
72
- if (!created) {
73
- throw new KeyServiceError(500, {
74
- error: 'User provisioning failed',
75
- detail: 'Failed to create user in LiteLLM',
76
- });
77
- }
78
- userInfo = await client.getUserInfo(user.userId);
79
- }
80
- }
42
+ const { client, config, catalogClient, auth, logger, } = ctx;
43
+ const userInfo = user.userInfo;
81
44
  // ── Enforce config flags ────────────────────────────────────────────
82
45
  if (!config.allowUnlimitedBudget && (input.max_budget === null || input.max_budget === undefined)) {
83
46
  throw new KeyServiceError(400, { error: 'max_budget is required when unlimited budgets are not allowed' });
@@ -127,13 +90,15 @@ async function createKeyForUser(user, input, ctx) {
127
90
  ? await (0, provisioning_1.resolveUserProfile)(user.tokenEntityRef, catalogClient, auth, logger)
128
91
  : {};
129
92
  const clientMetadata = input.metadata ?? {};
93
+ const createdVia = user.createdVia ?? 'backstage';
130
94
  const serverMetadata = {
131
- created_by_backstage_user: user.tokenEntityRef ?? 'unknown',
95
+ ...(user.tokenEntityRef && { created_by_backstage_user: user.tokenEntityRef }),
96
+ ...(!user.tokenEntityRef && { created_by: userInfo.user_id }),
132
97
  ...(profile.email && { created_by_email: profile.email }),
133
98
  ...(profile.displayName && {
134
99
  created_by_display_name: profile.displayName,
135
100
  }),
136
- created_via: 'backstage',
101
+ created_via: createdVia,
137
102
  created_at_iso: new Date().toISOString(),
138
103
  };
139
104
  // Server-owned keys override client values
@@ -145,7 +110,7 @@ async function createKeyForUser(user, input, ctx) {
145
110
  // Never spread the raw body; only include fields we've explicitly validated.
146
111
  const upstreamRequest = {
147
112
  alias: input.alias,
148
- user_id: user.userId,
113
+ user_id: userInfo.user_id,
149
114
  };
150
115
  if (input.models !== undefined) {
151
116
  upstreamRequest.models = input.models;
@@ -174,6 +139,6 @@ async function createKeyForUser(user, input, ctx) {
174
139
  upstreamRequest.metadata = enrichedMetadata;
175
140
  // ── Call upstream LiteLLM ───────────────────────────────────────────
176
141
  const result = await client.generateKey(upstreamRequest);
177
- logger.info('key.generate', { userId: user.userId, keyAlias: input.alias });
142
+ logger.info('key.generate', { userId: userInfo.user_id, keyAlias: input.alias });
178
143
  return result;
179
144
  }
@@ -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 GenerateKeyRequest,\n type UpdateKeyRequest,\n type GenerateKeyResponse,\n type AuditLogEntry,\n type PaginatedAuditLogs,\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/** One row from LiteLLM's `/spend/logs` (per-request spend log entry). */\nexport interface SpendLogEntry {\n request_id?: string;\n /** ISO timestamp of the call. */\n startTime?: string;\n endTime?: string;\n spend?: number;\n total_tokens?: number;\n prompt_tokens?: number;\n completion_tokens?: number;\n model?: string;\n /** Virtual key alias or hash the request was billed to. */\n api_key?: string;\n user?: string;\n team_id?: string;\n /**\n * Tags attached to the request (e.g. `channel:backstage`,\n * `session:<thread>`, `invoked-by:<user>`). Shape varies across LiteLLM\n * versions (array or object) and is normalised to a string[].\n */\n request_tags?: string[] | Record<string, string>;\n /** LiteLLM `metadata` blob; carries trace ids and session grouping. */\n metadata?: Record<string, unknown> | string | null;\n}\n\nexport interface SpendLogsParams {\n /** ISO date `YYYY-MM-DD` (inclusive). */\n start_date: string;\n /** ISO date `YYYY-MM-DD` (inclusive). */\n end_date: string;\n /** Restrict to the given virtual key. */\n api_key?: string;\n user_id?: string;\n team_id?: string;\n /** Max rows to return (LiteLLM caps at 1000 per page). */\n page_size?: 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": ["// 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 GenerateKeyRequest,\n type UpdateKeyRequest,\n type GenerateKeyResponse,\n type AuditLogEntry,\n type PaginatedAuditLogs,\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"],
5
5
  "mappings": ";;;;;;;;;;;;;;;;AAAA;AAAA;",
6
6
  "names": []
7
7
  }
package/dist/types.d.ts CHANGED
@@ -96,42 +96,6 @@ export interface CreateUserResponse {
96
96
  models?: string[];
97
97
  teams?: string[];
98
98
  }
99
- /** One row from LiteLLM's `/spend/logs` (per-request spend log entry). */
100
- export interface SpendLogEntry {
101
- request_id?: string;
102
- /** ISO timestamp of the call. */
103
- startTime?: string;
104
- endTime?: string;
105
- spend?: number;
106
- total_tokens?: number;
107
- prompt_tokens?: number;
108
- completion_tokens?: number;
109
- model?: string;
110
- /** Virtual key alias or hash the request was billed to. */
111
- api_key?: string;
112
- user?: string;
113
- team_id?: string;
114
- /**
115
- * Tags attached to the request (e.g. `channel:backstage`,
116
- * `session:<thread>`, `invoked-by:<user>`). Shape varies across LiteLLM
117
- * versions (array or object) and is normalised to a string[].
118
- */
119
- request_tags?: string[] | Record<string, string>;
120
- /** LiteLLM `metadata` blob; carries trace ids and session grouping. */
121
- metadata?: Record<string, unknown> | string | null;
122
- }
123
- export interface SpendLogsParams {
124
- /** ISO date `YYYY-MM-DD` (inclusive). */
125
- start_date: string;
126
- /** ISO date `YYYY-MM-DD` (inclusive). */
127
- end_date: string;
128
- /** Restrict to the given virtual key. */
129
- api_key?: string;
130
- user_id?: string;
131
- team_id?: string;
132
- /** Max rows to return (LiteLLM caps at 1000 per page). */
133
- page_size?: number;
134
- }
135
99
  export interface AuditLogsParams {
136
100
  page?: number;
137
101
  page_size?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acarmisc/backstage-plugin-litellm-backend",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "The Backstage backend plugin for LiteLLM governance",
5
5
  "backstage": {
6
6
  "role": "backend-plugin",
@@ -32,21 +32,17 @@
32
32
  "lint": "eslint src --ext .ts",
33
33
  "test": "tsc -p tsconfig.test.json && node --test \"dist-test/**/*.test.js\"",
34
34
  "api-report": "npm run build && api-extractor run --local --verbose",
35
- "prepack": "npm run build",
36
- "postpack": ""
35
+ "prepack": "npm run build"
37
36
  },
38
37
  "dependencies": {
39
38
  "@acarmisc/backstage-plugin-litellm-common": "0.1.0",
40
39
  "@backstage/backend-plugin-api": "^1.9.1",
41
40
  "@backstage/catalog-client": "^1.9.0",
42
- "@backstage/catalog-model": "^1.7.0",
43
41
  "@backstage/config": "^1.3.0",
44
42
  "@backstage/errors": "^1.3.0",
45
43
  "@backstage/plugin-permission-common": "^0.9.10",
46
- "@backstage/types": "^1.2.0",
47
44
  "express": "^4.18.2",
48
- "jose": "^5.9.6",
49
- "zod": "^3.22.0"
45
+ "jose": "^5.9.6"
50
46
  },
51
47
  "devDependencies": {
52
48
  "@backstage/cli": "^0.36.2",