@acarmisc/backstage-plugin-litellm-backend 0.22.0 → 0.24.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.
@@ -0,0 +1,32 @@
1
+ import { Config } from '@backstage/config';
2
+ /**
3
+ * Generic Langfuse Metrics API v2 integration (litellm.langfuse.*).
4
+ *
5
+ * Powers the "Quality scores" card on the Insights tab with independent
6
+ * answer-quality data. Nothing here is bound to a specific deployment:
7
+ * the server version and the trace↔user join key vary, so a misconfigured
8
+ * or v3 server degrades to `{configured:false}` instead of erroring.
9
+ */
10
+ export interface LangfuseConfig {
11
+ /** Mount GET /insights/quality and query Langfuse. Default false. */
12
+ enabled: boolean;
13
+ /** Langfuse host, e.g. https://langfuse.example.com. */
14
+ baseUrl: string | null;
15
+ /** Langfuse public key (Basic-auth username). */
16
+ publicKey: string | null;
17
+ /** Langfuse secret key (Basic-auth password). */
18
+ secretKey: string | null;
19
+ /**
20
+ * The Langfuse observation field holding the LiteLLM user id, e.g.
21
+ * 'userId'. Deployments whose agents tag traces differently change this
22
+ * one key. Default 'userId'.
23
+ */
24
+ userIdField: string;
25
+ }
26
+ export declare function readLangfuseConfig(config: Config): LangfuseConfig;
27
+ /** True only when the integration is enabled AND fully credentialed. */
28
+ export declare function isLangfuseConfigured(cfg: LangfuseConfig): cfg is LangfuseConfig & {
29
+ baseUrl: string;
30
+ publicKey: string;
31
+ secretKey: string;
32
+ };
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readLangfuseConfig = readLangfuseConfig;
4
+ exports.isLangfuseConfigured = isLangfuseConfigured;
5
+ function readLangfuseConfig(config) {
6
+ const enabled = config.getOptionalBoolean('litellm.langfuse.enabled') ?? false;
7
+ return {
8
+ enabled,
9
+ baseUrl: config.getOptionalString('litellm.langfuse.baseUrl') ?? null,
10
+ publicKey: config.getOptionalString('litellm.langfuse.publicKey') ?? null,
11
+ secretKey: config.getOptionalString('litellm.langfuse.secretKey') ?? null,
12
+ userIdField: config.getOptionalString('litellm.langfuse.userIdField') ?? 'userId',
13
+ };
14
+ }
15
+ /** True only when the integration is enabled AND fully credentialed. */
16
+ function isLangfuseConfigured(cfg) {
17
+ return (cfg.enabled && !!cfg.baseUrl && !!cfg.publicKey && !!cfg.secretKey);
18
+ }
@@ -0,0 +1,23 @@
1
+ import type { QualitySummary } from './types';
2
+ import { type LangfuseConfig } from './langfuse';
3
+ /** Typed error for failed Langfuse Metrics API responses. */
4
+ export declare class LangfuseUpstreamError extends Error {
5
+ status: number;
6
+ constructor(status: number, statusText: string);
7
+ }
8
+ /**
9
+ * Runs one Langfuse Metrics API v2 query:
10
+ * GET {baseUrl}/api/public/v2/metrics with Basic auth (publicKey:secretKey)
11
+ * and `query` = the JSON query object.
12
+ */
13
+ export declare function queryMetrics(baseUrl: string, publicKey: string, secretKey: string, queryObj: unknown, timeout?: number): Promise<any>;
14
+ /**
15
+ * Returns the caller's quality summary, or null when Langfuse is disabled,
16
+ * unconfigured, unreachable, or a v3 server without the v2 metrics endpoint
17
+ * (non-2xx degrades to null — never throws).
18
+ */
19
+ export declare function getQualitySummary(cfg: LangfuseConfig, opts: {
20
+ from: string;
21
+ to: string;
22
+ userId: string;
23
+ }): Promise<QualitySummary | null>;
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LangfuseUpstreamError = void 0;
4
+ exports.queryMetrics = queryMetrics;
5
+ exports.getQualitySummary = getQualitySummary;
6
+ const langfuse_1 = require("./langfuse");
7
+ const DEFAULT_TIMEOUT = 15000;
8
+ /** Typed error for failed Langfuse Metrics API responses. */
9
+ class LangfuseUpstreamError extends Error {
10
+ constructor(status, statusText) {
11
+ super(`Langfuse API error: ${status} ${statusText}`);
12
+ this.status = status;
13
+ }
14
+ }
15
+ exports.LangfuseUpstreamError = LangfuseUpstreamError;
16
+ /**
17
+ * Runs one Langfuse Metrics API v2 query:
18
+ * GET {baseUrl}/api/public/v2/metrics with Basic auth (publicKey:secretKey)
19
+ * and `query` = the JSON query object.
20
+ */
21
+ async function queryMetrics(baseUrl, publicKey, secretKey, queryObj, timeout = DEFAULT_TIMEOUT) {
22
+ const controller = new AbortController();
23
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
24
+ try {
25
+ const url = `${baseUrl.replace(/\/$/, '')}/api/public/v2/metrics?query=${encodeURIComponent(JSON.stringify(queryObj))}`;
26
+ const response = await fetch(url, {
27
+ signal: controller.signal,
28
+ headers: {
29
+ Authorization: `Basic ${Buffer.from(`${publicKey}:${secretKey}`).toString('base64')}`,
30
+ },
31
+ });
32
+ if (!response.ok) {
33
+ throw new LangfuseUpstreamError(response.status, response.statusText);
34
+ }
35
+ return response.json();
36
+ }
37
+ finally {
38
+ clearTimeout(timeoutId);
39
+ }
40
+ }
41
+ function toNum(v) {
42
+ if (typeof v === 'number')
43
+ return Number.isFinite(v) ? v : null;
44
+ if (typeof v === 'string' && v.trim() !== '') {
45
+ const n = Number(v);
46
+ return Number.isFinite(n) ? n : null;
47
+ }
48
+ return null;
49
+ }
50
+ /** First finite number found across the row, its metrics nest, and its dimensions nest. */
51
+ function pick(row, names) {
52
+ const scopes = [row, row?.metrics, row?.dimensions];
53
+ for (const scope of scopes) {
54
+ if (!scope || typeof scope !== 'object')
55
+ continue;
56
+ for (const name of names) {
57
+ const n = toNum(scope[name]);
58
+ if (n !== null)
59
+ return n;
60
+ }
61
+ }
62
+ return null;
63
+ }
64
+ function pickText(row, names) {
65
+ const scopes = [row, row?.metrics, row?.dimensions];
66
+ for (const scope of scopes) {
67
+ if (!scope || typeof scope !== 'object')
68
+ continue;
69
+ for (const name of names) {
70
+ const v = scope[name];
71
+ if (typeof v === 'string' && v)
72
+ return v;
73
+ }
74
+ }
75
+ return null;
76
+ }
77
+ function metricRows(body) {
78
+ if (Array.isArray(body))
79
+ return body;
80
+ if (Array.isArray(body?.data))
81
+ return body.data;
82
+ if (Array.isArray(body?.results))
83
+ return body.results;
84
+ if (Array.isArray(body?.rows))
85
+ return body.rows;
86
+ return [];
87
+ }
88
+ function parseObservations(body) {
89
+ const byModel = metricRows(body)
90
+ .map(row => ({
91
+ model: pickText(row, ['providedModelName', 'model', 'modelName']) ?? '',
92
+ cost: pick(row, ['sum_totalCost', 'totalCost', 'sum_total_cost', 'cost', 'sum_cost']) ?? 0,
93
+ tokens: pick(row, ['sum_totalTokens', 'totalTokens', 'sum_total_tokens', 'tokens']) ?? 0,
94
+ avgLatencyMs: pick(row, ['avg_latency', 'latency', 'avg_latency_ms', 'latencyMs']),
95
+ requests: pick(row, ['count_id', 'count', 'id_count', 'requests', 'observations']) ?? 0,
96
+ }))
97
+ .filter((r) => r.model !== '');
98
+ byModel.sort((a, b) => b.cost - a.cost);
99
+ return byModel;
100
+ }
101
+ function parseScores(body) {
102
+ const scores = metricRows(body)
103
+ .map(row => ({
104
+ name: pickText(row, ['name', 'scoreName']) ?? '',
105
+ avg: pick(row, ['avg_value', 'value', 'avg']) ?? NaN,
106
+ count: pick(row, ['count_id', 'count', 'id_count', 'scores']) ?? 0,
107
+ }))
108
+ .filter(s => s.name !== '' && Number.isFinite(s.avg));
109
+ scores.sort((a, b) => a.name.localeCompare(b.name));
110
+ return scores;
111
+ }
112
+ function toTimestamp(day, endOfDay) {
113
+ if (/^\d{4}-\d{2}-\d{2}$/.test(day)) {
114
+ return endOfDay ? `${day}T23:59:59.999Z` : `${day}T00:00:00.000Z`;
115
+ }
116
+ return day;
117
+ }
118
+ /**
119
+ * Returns the caller's quality summary, or null when Langfuse is disabled,
120
+ * unconfigured, unreachable, or a v3 server without the v2 metrics endpoint
121
+ * (non-2xx degrades to null — never throws).
122
+ */
123
+ async function getQualitySummary(cfg, opts) {
124
+ if (!(0, langfuse_1.isLangfuseConfigured)(cfg))
125
+ return null;
126
+ const { baseUrl, publicKey, secretKey, userIdField } = cfg;
127
+ const fromTimestamp = toTimestamp(opts.from, false);
128
+ const toTimestampValue = toTimestamp(opts.to, true);
129
+ try {
130
+ const observations = await queryMetrics(baseUrl, publicKey, secretKey, {
131
+ view: 'observations',
132
+ metrics: [
133
+ { measure: 'totalCost', aggregation: 'SUM' },
134
+ { measure: 'totalTokens', aggregation: 'SUM' },
135
+ { measure: 'latency', aggregation: 'AVG' },
136
+ { measure: 'id', aggregation: 'COUNT' },
137
+ ],
138
+ dimensions: [{ field: 'providedModelName' }],
139
+ filters: [
140
+ { column: userIdField, operator: '=', value: opts.userId, type: 'string' },
141
+ ],
142
+ fromTimestamp,
143
+ toTimestamp: toTimestampValue,
144
+ config: { row_limit: 50 },
145
+ });
146
+ // Scores are deployment-wide: the scores view does not reliably carry the
147
+ // trace user join key, so filtering it would silently drop every row.
148
+ let scores = [];
149
+ try {
150
+ const scoresBody = await queryMetrics(baseUrl, publicKey, secretKey, {
151
+ view: 'scores-numeric',
152
+ metrics: [
153
+ { measure: 'value', aggregation: 'AVG' },
154
+ { measure: 'id', aggregation: 'COUNT' },
155
+ ],
156
+ dimensions: [{ field: 'name' }],
157
+ fromTimestamp,
158
+ toTimestamp: toTimestampValue,
159
+ config: { row_limit: 50 },
160
+ });
161
+ scores = parseScores(scoresBody);
162
+ }
163
+ catch {
164
+ scores = [];
165
+ }
166
+ return { configured: true, byModel: parseObservations(observations), scores };
167
+ }
168
+ catch {
169
+ return null;
170
+ }
171
+ }
package/dist/router.js CHANGED
@@ -47,6 +47,7 @@ const teamAdmin_1 = require("./teamAdmin");
47
47
  const teamBudgetVisibility_1 = require("./teamBudgetVisibility");
48
48
  const opencode_1 = require("./opencode");
49
49
  const teamMemberUsage_1 = require("./teamMemberUsage");
50
+ const langfuse_1 = require("./langfuse");
50
51
  const errors_2 = require("./errors");
51
52
  const withUser_1 = require("./routes/middleware/withUser");
52
53
  const userInfoCache_1 = require("./services/userInfoCache");
@@ -56,6 +57,7 @@ const opencode_2 = require("./routes/opencode");
56
57
  const teams_1 = require("./routes/teams");
57
58
  const objectPermissions_1 = require("./routes/objectPermissions");
58
59
  const teamUsage_1 = require("./routes/teamUsage");
60
+ const insights_1 = require("./routes/insights");
59
61
  const bridge_1 = require("./routes/bridge");
60
62
  const teamFetch_1 = require("./http/teamFetch");
61
63
  async function createRouter(options) {
@@ -100,6 +102,7 @@ async function createRouter(options) {
100
102
  const teamBudgetVisibility = (0, teamBudgetVisibility_1.readTeamBudgetVisibility)(config);
101
103
  const opencodeCfg = (0, opencode_1.readOpencodeConfig)(config);
102
104
  const memberBreakdownCfg = (0, teamMemberUsage_1.readMemberBreakdownConfig)(config);
105
+ const langfuseCfg = (0, langfuse_1.readLangfuseConfig)(config);
103
106
  const catalogClient = options.catalogClient ?? new catalog_client_1.CatalogClient({ discoveryApi: discovery });
104
107
  if (provisioningEnabled) {
105
108
  logger.info(`LiteLLM auto-provisioning enabled — defaults: budget=$${provisioningDefaults.maxBudget}/${provisioningDefaults.budgetDuration}, models=${provisioningDefaults.models.length
@@ -146,6 +149,7 @@ async function createRouter(options) {
146
149
  teamBudgetVisibility,
147
150
  opencodeCfg,
148
151
  memberBreakdownCfg,
152
+ langfuseCfg,
149
153
  // Helper functions - defined inline below this object
150
154
  authorizeKeyAction: async (req, keyId) => {
151
155
  // Express middleware should have populated req.res.locals with user info
@@ -311,7 +315,7 @@ async function createRouter(options) {
311
315
  // string or body — so service/external principals (and anonymous callers)
312
316
  // are rejected instead of being allowed to name an arbitrary user_id.
313
317
  const requireUser = (0, withUser_1.createRequireUser)(ctx);
314
- router.use(['/user/info', '/keys', '/usage'], requireUser);
318
+ router.use(['/user/info', '/keys', '/usage', '/insights'], requireUser);
315
319
  // Register config routes (/health, /config, /openapi.json, /provisioning/preview)
316
320
  (0, config_1.registerConfigRoutes)(router, ctx);
317
321
  (0, keys_1.registerKeysRoutes)(router, ctx);
@@ -319,6 +323,7 @@ async function createRouter(options) {
319
323
  (0, teams_1.registerTeamsRoutes)(router, ctx);
320
324
  (0, objectPermissions_1.registerObjectPermissionsRoutes)(router, ctx);
321
325
  (0, teamUsage_1.registerTeamUsageRoutes)(router, ctx);
326
+ (0, insights_1.registerInsightsRoutes)(router, ctx);
322
327
  (0, bridge_1.registerBridgeRoutes)(router, ctx, { config, tokenVerifier: options.tokenVerifier });
323
328
  return router;
324
329
  }
@@ -9,6 +9,7 @@ import type { KeyValidationConfig } from '@acarmisc/backstage-plugin-litellm-com
9
9
  import { TeamBudgetVisibility } from '../teamBudgetVisibility';
10
10
  import { OpencodeConfig } from '../opencode';
11
11
  import { MemberBreakdownConfig } from '../teamMemberUsage';
12
+ import { LangfuseConfig } from '../langfuse';
12
13
  /**
13
14
  * Shared runtime context for all route handlers. Holds config-derived
14
15
  * constants, service clients, and helper functions needed by routes.
@@ -37,6 +38,7 @@ export interface RouterContext {
37
38
  teamBudgetVisibility: TeamBudgetVisibility;
38
39
  opencodeCfg: OpencodeConfig;
39
40
  memberBreakdownCfg: MemberBreakdownConfig;
41
+ langfuseCfg: LangfuseConfig;
40
42
  /**
41
43
  * LiteLLM user id of a Backstage user: the computed id, or the id of an
42
44
  * existing LiteLLM user with the same catalog email.
@@ -0,0 +1,3 @@
1
+ import { Router } from 'express';
2
+ import type { RouterContext } from './context';
3
+ export declare function registerInsightsRoutes(router: Router, ctx: RouterContext): void;
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerInsightsRoutes = registerInsightsRoutes;
4
+ const provisioning_1 = require("../provisioning");
5
+ const errors_1 = require("../errors");
6
+ const withUser_1 = require("./middleware/withUser");
7
+ const langfuse_1 = require("../langfuse");
8
+ const langfuseClient_1 = require("../langfuseClient");
9
+ function registerInsightsRoutes(router, ctx) {
10
+ const { client, catalogClient, auth, logger, auditGroup } = ctx;
11
+ const requireUser = (0, withUser_1.createRequireUser)(ctx);
12
+ router.get('/insights/spend-logs', requireUser, async (req, res) => {
13
+ try {
14
+ const { start_date, end_date, userId, teamId, page, page_size } = req.query;
15
+ if (!start_date || !end_date) {
16
+ res.status(400).json({ error: 'start_date and end_date are required' });
17
+ return;
18
+ }
19
+ const tokenEntityRef = res.locals.tokenEntityRef;
20
+ await (0, withUser_1.getProvisionedUser)(ctx, res);
21
+ const callerId = res.locals.userId;
22
+ const isAudit = auditGroup
23
+ ? await (0, provisioning_1.isUserMemberOfGroup)(tokenEntityRef, auditGroup, catalogClient, auth, logger)
24
+ : false;
25
+ // Server-side scoping: only audit-group members may query other
26
+ // users/teams — everyone else is forced to their own user_id.
27
+ const result = await client.getSpendLogs({
28
+ startDate: start_date,
29
+ endDate: end_date,
30
+ userId: isAudit ? userId || callerId : callerId,
31
+ teamId: isAudit ? teamId : undefined,
32
+ page: page ? Number(page) : undefined,
33
+ pageSize: page_size ? Number(page_size) : undefined,
34
+ });
35
+ res.json(result);
36
+ }
37
+ catch (error) {
38
+ if (error instanceof provisioning_1.ProvisioningError) {
39
+ res.status(error.status).json(error.body);
40
+ return;
41
+ }
42
+ (0, errors_1.sendError)(res, error, logger, 'fetch spend logs');
43
+ }
44
+ });
45
+ router.get('/insights/quality', requireUser, async (req, res) => {
46
+ try {
47
+ const { start_date, end_date, userId } = req.query;
48
+ if (!start_date || !end_date) {
49
+ res.status(400).json({ error: 'start_date and end_date are required' });
50
+ return;
51
+ }
52
+ const tokenEntityRef = res.locals.tokenEntityRef;
53
+ await (0, withUser_1.getProvisionedUser)(ctx, res);
54
+ const callerId = res.locals.userId;
55
+ const isAudit = auditGroup
56
+ ? await (0, provisioning_1.isUserMemberOfGroup)(tokenEntityRef, auditGroup, catalogClient, auth, logger)
57
+ : false;
58
+ if (!(0, langfuse_1.isLangfuseConfigured)(ctx.langfuseCfg)) {
59
+ res.json({ configured: false });
60
+ return;
61
+ }
62
+ // Server-side scoping, same as spend-logs: only audit-group members
63
+ // may query other users — everyone else is forced to their own user_id.
64
+ // Unreachable / v3 servers degrade to {configured:false}, never a 500.
65
+ const summary = await (0, langfuseClient_1.getQualitySummary)(ctx.langfuseCfg, {
66
+ from: start_date,
67
+ to: end_date,
68
+ userId: isAudit ? userId || callerId : callerId,
69
+ });
70
+ res.json(summary ?? { configured: false });
71
+ }
72
+ catch (error) {
73
+ if (error instanceof provisioning_1.ProvisioningError) {
74
+ res.status(error.status).json(error.body);
75
+ return;
76
+ }
77
+ (0, errors_1.sendError)(res, error, logger, 'fetch quality summary');
78
+ }
79
+ });
80
+ }
@@ -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 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"],
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 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"],
5
5
  "mappings": ";;;;;;;;;;;;;;;;AAAA;AAAA;",
6
6
  "names": []
7
7
  }
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { type UserInfo, type TeamMember, type TeamObjectPermission, type TeamInfo, type CreateTeamResponse, type VirtualKey, type ModelInfo, type UsageModelBreakdown, type UsageKeyBreakdown, type UsageDailyPoint, type UsageDailyModelPoint, type UsageMetrics, type TeamMemberUsage, type TeamMemberUsageRow, type GenerateKeyRequest, type UpdateKeyRequest, type GenerateKeyResponse, type AuditLogEntry, type PaginatedAuditLogs, } from '@acarmisc/backstage-plugin-litellm-common';
1
+ export { type UserInfo, type TeamMember, type TeamObjectPermission, type TeamInfo, type CreateTeamResponse, type VirtualKey, type ModelInfo, type UsageModelBreakdown, type UsageKeyBreakdown, type UsageDailyPoint, type UsageDailyModelPoint, type UsageMetrics, type TeamMemberUsage, type TeamMemberUsageRow, type GenerateKeyRequest, type UpdateKeyRequest, type GenerateKeyResponse, type AuditLogEntry, type PaginatedAuditLogs, type SpendLogEntry, type SpendLogsParams, type PaginatedSpendLogs, type QualityByModel, type QualityScore, type QualitySummary, type QualityResponse, } from '@acarmisc/backstage-plugin-litellm-common';
2
2
  export interface CreateTeamRequest {
3
3
  team_alias: string;
4
4
  models?: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acarmisc/backstage-plugin-litellm-backend",
3
- "version": "0.22.0",
3
+ "version": "0.24.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.2.0",
38
+ "@acarmisc/backstage-plugin-litellm-common": "0.4.0",
39
39
  "@backstage/backend-plugin-api": "^1.9.1",
40
40
  "@backstage/catalog-client": "^1.9.0",
41
41
  "@backstage/config": "^1.3.0",