@acarmisc/backstage-plugin-litellm-backend 0.19.0 → 0.21.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/config.d.ts +47 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.js +21 -0
- package/dist/index.cjs.js +332 -54
- package/dist/index.cjs.js.map +4 -4
- package/dist/openapi.js +22 -0
- package/dist/opencode.d.ts +5 -0
- package/dist/opencode.js +1 -0
- package/dist/provisioning.d.ts +14 -1
- package/dist/provisioning.js +83 -17
- package/dist/router.js +17 -2
- package/dist/routes/config.js +3 -0
- package/dist/routes/context.d.ts +7 -0
- package/dist/routes/keys.js +2 -2
- package/dist/routes/middleware/withUser.d.ts +1 -0
- package/dist/routes/middleware/withUser.js +5 -1
- package/dist/routes/opencode.js +12 -8
- package/dist/routes/teamUsage.js +109 -0
- package/dist/routes/teams.js +6 -4
- package/dist/services/keyService.js +3 -0
- package/dist/teamMemberUsage.d.ts +34 -0
- package/dist/teamMemberUsage.js +114 -0
- package/dist/types.cjs.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readMemberBreakdownConfig = readMemberBreakdownConfig;
|
|
4
|
+
exports.memberEmails = memberEmails;
|
|
5
|
+
exports.buildTeamMemberUsage = buildTeamMemberUsage;
|
|
6
|
+
exports.redactTeamMemberUsage = redactTeamMemberUsage;
|
|
7
|
+
function readMemberBreakdownConfig(config) {
|
|
8
|
+
return {
|
|
9
|
+
enabled: config.getOptionalBoolean('litellm.teamUsage.memberBreakdown.enabled') ?? false,
|
|
10
|
+
viewerRoles: config.getOptionalStringArray('litellm.teamUsage.memberBreakdown.viewerRoles') ?? [],
|
|
11
|
+
permissionEnabled: config.getOptionalBoolean('permission.enabled') ?? false,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Emails worth looking up in the catalog for the members of a breakdown:
|
|
16
|
+
* the team roster's emails and email-shaped user ids.
|
|
17
|
+
*/
|
|
18
|
+
function memberEmails(team, keys) {
|
|
19
|
+
const emails = new Set();
|
|
20
|
+
for (const m of team.members_with_roles ?? []) {
|
|
21
|
+
if (m.user_email)
|
|
22
|
+
emails.add(m.user_email.toLowerCase());
|
|
23
|
+
else if (m.user_id?.includes('@'))
|
|
24
|
+
emails.add(m.user_id.toLowerCase());
|
|
25
|
+
}
|
|
26
|
+
for (const k of keys) {
|
|
27
|
+
if (k.user_id?.includes('@'))
|
|
28
|
+
emails.add(k.user_id.toLowerCase());
|
|
29
|
+
}
|
|
30
|
+
return [...emails];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Attributes a team's usage to its members. Each key in the team's daily
|
|
34
|
+
* activity is credited to the key's owner (from `/key/list?team_id=`), so the
|
|
35
|
+
* rows add up to the team total. Activity of keys with no known owner
|
|
36
|
+
* (deleted keys, keys without a user) lands in one `user_id: null` row.
|
|
37
|
+
* Team members without activity get zero rows.
|
|
38
|
+
*/
|
|
39
|
+
function buildTeamMemberUsage(teamId, usage, keys, team, profiles = new Map()) {
|
|
40
|
+
const rows = new Map();
|
|
41
|
+
const rowFor = (userId) => {
|
|
42
|
+
let row = rows.get(userId);
|
|
43
|
+
if (!row) {
|
|
44
|
+
row = {
|
|
45
|
+
user_id: userId,
|
|
46
|
+
spend: 0,
|
|
47
|
+
spend_share_pct: 0,
|
|
48
|
+
prompt_tokens: 0,
|
|
49
|
+
completion_tokens: 0,
|
|
50
|
+
total_tokens: 0,
|
|
51
|
+
api_requests: 0,
|
|
52
|
+
successful_requests: 0,
|
|
53
|
+
failed_requests: 0,
|
|
54
|
+
success_rate: null,
|
|
55
|
+
key_count: 0,
|
|
56
|
+
};
|
|
57
|
+
rows.set(userId, row);
|
|
58
|
+
}
|
|
59
|
+
return row;
|
|
60
|
+
};
|
|
61
|
+
for (const m of team.members_with_roles ?? []) {
|
|
62
|
+
if (!m.user_id)
|
|
63
|
+
continue;
|
|
64
|
+
const row = rowFor(m.user_id);
|
|
65
|
+
row.role = m.role;
|
|
66
|
+
if (m.user_email)
|
|
67
|
+
row.user_email = m.user_email;
|
|
68
|
+
}
|
|
69
|
+
const ownerByKey = new Map();
|
|
70
|
+
for (const k of keys) {
|
|
71
|
+
if (!k.user_id || !k.token)
|
|
72
|
+
continue;
|
|
73
|
+
ownerByKey.set(k.token, k.user_id);
|
|
74
|
+
rowFor(k.user_id).key_count += 1;
|
|
75
|
+
}
|
|
76
|
+
for (const [keyHash, k] of Object.entries(usage.usage_by_key ?? {})) {
|
|
77
|
+
const row = rowFor(ownerByKey.get(keyHash) ?? null);
|
|
78
|
+
row.spend += k.total_spend ?? 0;
|
|
79
|
+
row.prompt_tokens += k.prompt_tokens ?? 0;
|
|
80
|
+
row.completion_tokens += k.completion_tokens ?? 0;
|
|
81
|
+
row.total_tokens += k.total_tokens ?? 0;
|
|
82
|
+
row.api_requests += k.api_requests ?? 0;
|
|
83
|
+
row.successful_requests += k.successful_requests ?? 0;
|
|
84
|
+
row.failed_requests += k.failed_requests ?? 0;
|
|
85
|
+
}
|
|
86
|
+
const members = [...rows.values()];
|
|
87
|
+
const totalSpend = members.reduce((sum, r) => sum + r.spend, 0);
|
|
88
|
+
for (const row of members) {
|
|
89
|
+
row.spend_share_pct = totalSpend > 0 ? (row.spend / totalSpend) * 100 : 0;
|
|
90
|
+
const finished = row.successful_requests + row.failed_requests;
|
|
91
|
+
row.success_rate = finished > 0 ? (row.successful_requests / finished) * 100 : null;
|
|
92
|
+
if (!row.user_email && row.user_id?.includes('@'))
|
|
93
|
+
row.user_email = row.user_id;
|
|
94
|
+
const profile = row.user_email && profiles.get(row.user_email.toLowerCase());
|
|
95
|
+
if (profile && profile.displayName)
|
|
96
|
+
row.display_name = profile.displayName;
|
|
97
|
+
}
|
|
98
|
+
members.sort((a, b) => b.spend - a.spend ||
|
|
99
|
+
b.total_tokens - a.total_tokens ||
|
|
100
|
+
String(a.user_id ?? '').localeCompare(String(b.user_id ?? '')));
|
|
101
|
+
return { team_id: teamId, total_spend: totalSpend, members };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Hides dollar amounts: spend becomes 0 and `spend_share_pct` remains the
|
|
105
|
+
* way to compare members. Tokens and requests are kept.
|
|
106
|
+
*/
|
|
107
|
+
function redactTeamMemberUsage(usage) {
|
|
108
|
+
return {
|
|
109
|
+
...usage,
|
|
110
|
+
total_spend: 0,
|
|
111
|
+
budget_hidden: true,
|
|
112
|
+
members: usage.members.map(m => ({ ...m, spend: 0 })),
|
|
113
|
+
};
|
|
114
|
+
}
|
package/dist/types.cjs.js.map
CHANGED
|
@@ -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\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} 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 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, } 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.
|
|
3
|
+
"version": "0.21.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.
|
|
38
|
+
"@acarmisc/backstage-plugin-litellm-common": "0.2.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",
|