@acarmisc/backstage-plugin-litellm-backend 0.15.0 → 0.16.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.
Files changed (53) hide show
  1. package/config.d.ts +115 -22
  2. package/dist/bridge.d.ts +23 -16
  3. package/dist/bridge.js +51 -11
  4. package/dist/client.d.ts +7 -2
  5. package/dist/client.js +35 -9
  6. package/dist/errors.d.ts +64 -0
  7. package/dist/errors.js +205 -0
  8. package/dist/http/respondTeam.d.ts +15 -0
  9. package/dist/http/respondTeam.js +27 -0
  10. package/dist/http/teamFetch.d.ts +14 -0
  11. package/dist/http/teamFetch.js +44 -0
  12. package/dist/index.cjs.js +4248 -3375
  13. package/dist/index.cjs.js.map +4 -4
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.js +15 -13
  16. package/dist/openapi.js +17 -0
  17. package/dist/plugin.js +2 -2
  18. package/dist/provisioning.d.ts +17 -11
  19. package/dist/provisioning.js +36 -22
  20. package/dist/router.d.ts +3 -4
  21. package/dist/router.js +188 -1349
  22. package/dist/routes/bridge.d.ts +8 -0
  23. package/dist/routes/bridge.js +130 -0
  24. package/dist/routes/config.d.ts +7 -0
  25. package/dist/routes/config.js +89 -0
  26. package/dist/routes/context.d.ts +55 -0
  27. package/dist/routes/context.js +2 -0
  28. package/dist/routes/keys.d.ts +8 -0
  29. package/dist/routes/keys.js +471 -0
  30. package/dist/routes/middleware/withUser.d.ts +18 -0
  31. package/dist/routes/middleware/withUser.js +41 -0
  32. package/dist/routes/objectPermissions.d.ts +3 -0
  33. package/dist/routes/objectPermissions.js +196 -0
  34. package/dist/routes/opencode.d.ts +3 -0
  35. package/dist/routes/opencode.js +239 -0
  36. package/dist/routes/teamUsage.d.ts +3 -0
  37. package/dist/routes/teamUsage.js +73 -0
  38. package/dist/routes/teams.d.ts +3 -0
  39. package/dist/routes/teams.js +367 -0
  40. package/dist/services/keyService.d.ts +61 -0
  41. package/dist/services/keyService.js +179 -0
  42. package/dist/services/modelAccess.d.ts +9 -0
  43. package/dist/services/modelAccess.js +30 -0
  44. package/dist/services/userInfoCache.d.ts +65 -0
  45. package/dist/services/userInfoCache.js +147 -0
  46. package/dist/teamAdmin.d.ts +2 -2
  47. package/dist/teamBudgetVisibility.d.ts +2 -1
  48. package/dist/teamBudgetVisibility.js +5 -5
  49. package/dist/types.cjs.js.map +1 -1
  50. package/dist/types.d.ts +8 -197
  51. package/package.json +6 -3
  52. package/dist/permissions.d.ts +0 -11
  53. package/dist/permissions.js +0 -57
@@ -0,0 +1,65 @@
1
+ import { UserInfo } from '../types';
2
+ import { LiteLLMClient } from '../client';
3
+ /**
4
+ * Clock abstraction for testing. Allows tests to fake time without real delays.
5
+ */
6
+ export interface Clock {
7
+ now(): number;
8
+ }
9
+ /**
10
+ * Simple LRU cache with per-entry TTL.
11
+ * - Max ~500 entries; LRU eviction when full.
12
+ * - Each entry has its own expiry time.
13
+ * - Tracks pending promises for single-flight deduplication.
14
+ */
15
+ export declare class UserInfoCache {
16
+ private static readonly MAX_ENTRIES;
17
+ private cache;
18
+ private pending;
19
+ private clock;
20
+ private ttlMs;
21
+ constructor(ttlMs?: number, clock?: Clock);
22
+ /**
23
+ * Get a cached value, or null if not found or expired.
24
+ */
25
+ get(userId: string): UserInfo | null;
26
+ /**
27
+ * Set a cache entry (does not expire).
28
+ */
29
+ set(userId: string, value: UserInfo): void;
30
+ /**
31
+ * Invalidate a single user's cache entry.
32
+ */
33
+ invalidate(userId: string): void;
34
+ /**
35
+ * Clear all cache entries and pending promises.
36
+ */
37
+ clear(): void;
38
+ /**
39
+ * Internal: track a pending promise for single-flight deduplication.
40
+ * Returns the promise, which is stored and shared across concurrent requests.
41
+ */
42
+ private getPending;
43
+ /**
44
+ * Load a value through the cache with single-flight deduplication.
45
+ * If the value is already cached and not expired, return it.
46
+ * If another request is loading the same userId, wait for that promise.
47
+ * Otherwise, call the loader and cache the result (unless null or error).
48
+ *
49
+ * Errors and null results are NOT cached.
50
+ */
51
+ load(userId: string, loader: () => Promise<UserInfo | null>): Promise<UserInfo | null>;
52
+ }
53
+ /**
54
+ * Wraps a LiteLLMClient to inject getUserInfo caching and automatic cache
55
+ * invalidation on mutations.
56
+ *
57
+ * Returns a Proxy that:
58
+ * - Routes getUserInfo(userId) through the cache
59
+ * - Invalidates the cache (clear all) after any method matching the mutation pattern
60
+ * - Delegates all other methods to the wrapped client unchanged
61
+ */
62
+ export declare function withUserInfoCache(client: LiteLLMClient, options?: {
63
+ ttlMs?: number;
64
+ clock?: Clock;
65
+ }): LiteLLMClient;
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UserInfoCache = void 0;
4
+ exports.withUserInfoCache = withUserInfoCache;
5
+ /**
6
+ * Default system clock using Date.now().
7
+ */
8
+ const SystemClock = {
9
+ now: () => Date.now(),
10
+ };
11
+ /**
12
+ * Simple LRU cache with per-entry TTL.
13
+ * - Max ~500 entries; LRU eviction when full.
14
+ * - Each entry has its own expiry time.
15
+ * - Tracks pending promises for single-flight deduplication.
16
+ */
17
+ class UserInfoCache {
18
+ constructor(ttlMs = 10000, clock = SystemClock) {
19
+ this.cache = new Map();
20
+ this.pending = new Map();
21
+ this.ttlMs = ttlMs;
22
+ this.clock = clock;
23
+ }
24
+ /**
25
+ * Get a cached value, or null if not found or expired.
26
+ */
27
+ get(userId) {
28
+ const entry = this.cache.get(userId);
29
+ if (!entry)
30
+ return null;
31
+ if (this.clock.now() >= entry.expiresAt) {
32
+ // Expired; remove and return null
33
+ this.cache.delete(userId);
34
+ return null;
35
+ }
36
+ return entry.value;
37
+ }
38
+ /**
39
+ * Set a cache entry (does not expire).
40
+ */
41
+ set(userId, value) {
42
+ // Evict LRU entry if at capacity
43
+ if (this.cache.size >= UserInfoCache.MAX_ENTRIES && !this.cache.has(userId)) {
44
+ const firstKey = this.cache.keys().next().value;
45
+ if (firstKey)
46
+ this.cache.delete(firstKey);
47
+ }
48
+ const expiresAt = this.clock.now() + this.ttlMs;
49
+ this.cache.set(userId, { value, expiresAt });
50
+ }
51
+ /**
52
+ * Invalidate a single user's cache entry.
53
+ */
54
+ invalidate(userId) {
55
+ this.cache.delete(userId);
56
+ }
57
+ /**
58
+ * Clear all cache entries and pending promises.
59
+ */
60
+ clear() {
61
+ this.cache.clear();
62
+ this.pending.clear();
63
+ }
64
+ /**
65
+ * Internal: track a pending promise for single-flight deduplication.
66
+ * Returns the promise, which is stored and shared across concurrent requests.
67
+ */
68
+ getPending(userId, loader) {
69
+ const existing = this.pending.get(userId);
70
+ if (existing)
71
+ return existing;
72
+ const promise = loader().finally(() => {
73
+ // Clean up pending once resolved or rejected
74
+ this.pending.delete(userId);
75
+ });
76
+ this.pending.set(userId, promise);
77
+ return promise;
78
+ }
79
+ /**
80
+ * Load a value through the cache with single-flight deduplication.
81
+ * If the value is already cached and not expired, return it.
82
+ * If another request is loading the same userId, wait for that promise.
83
+ * Otherwise, call the loader and cache the result (unless null or error).
84
+ *
85
+ * Errors and null results are NOT cached.
86
+ */
87
+ async load(userId, loader) {
88
+ // Check cache first
89
+ const cached = this.get(userId);
90
+ if (cached)
91
+ return cached;
92
+ // Use single-flight for concurrent requests
93
+ const result = await this.getPending(userId, loader);
94
+ // Cache only non-null success results
95
+ if (result) {
96
+ this.set(userId, result);
97
+ }
98
+ return result;
99
+ }
100
+ }
101
+ exports.UserInfoCache = UserInfoCache;
102
+ UserInfoCache.MAX_ENTRIES = 500;
103
+ /**
104
+ * Wraps a LiteLLMClient to inject getUserInfo caching and automatic cache
105
+ * invalidation on mutations.
106
+ *
107
+ * Returns a Proxy that:
108
+ * - Routes getUserInfo(userId) through the cache
109
+ * - Invalidates the cache (clear all) after any method matching the mutation pattern
110
+ * - Delegates all other methods to the wrapped client unchanged
111
+ */
112
+ function withUserInfoCache(client, options) {
113
+ const cache = new UserInfoCache(options?.ttlMs ?? 10000, options?.clock);
114
+ // Methods whose names match this pattern indicate a mutation that should
115
+ // invalidate the cache to ensure consistency.
116
+ const isMutatingMethod = /^(generate|update|delete|block|unblock|reset|create|add|remove|set|regenerate|provision|teamMember)/i;
117
+ return new Proxy(client, {
118
+ get(target, prop) {
119
+ // Wrap getUserInfo through the cache
120
+ if (prop === 'getUserInfo') {
121
+ return async (userId) => {
122
+ if (!userId) {
123
+ // No userId => call upstream directly (shouldn't happen in normal flow)
124
+ return target.getUserInfo(userId);
125
+ }
126
+ return cache.load(userId, () => target.getUserInfo(userId));
127
+ };
128
+ }
129
+ // Get the method from the target
130
+ const method = target[prop];
131
+ if (typeof method !== 'function') {
132
+ return method;
133
+ }
134
+ // If the method name matches the mutation pattern, wrap it to invalidate cache
135
+ if (isMutatingMethod.test(String(prop))) {
136
+ return async (...args) => {
137
+ const result = await method.apply(target, args);
138
+ // Invalidate entire cache after any mutation (simple and safe)
139
+ cache.clear();
140
+ return result;
141
+ };
142
+ }
143
+ // For other methods, bind and return as-is
144
+ return method.bind(target);
145
+ },
146
+ });
147
+ }
@@ -1,6 +1,6 @@
1
1
  import { Request } from 'express';
2
2
  import { Config } from '@backstage/config';
3
- import { AuthService, PermissionsService } from '@backstage/backend-plugin-api';
3
+ import { AuthService, PermissionsService, LoggerService } from '@backstage/backend-plugin-api';
4
4
  import { BasicPermission } from '@backstage/plugin-permission-common';
5
5
  import { CatalogClient } from '@backstage/catalog-client';
6
6
  /**
@@ -108,7 +108,7 @@ export declare function assertTeamAdmin(opts: {
108
108
  catalogClient: CatalogClient;
109
109
  teamAdminGroup: string;
110
110
  permission: BasicPermission;
111
- logger: any;
111
+ logger: LoggerService;
112
112
  }): Promise<TeamAdminCheck>;
113
113
  /**
114
114
  * Reads the team-admin governance block from config, applying fail-closed
@@ -1,5 +1,6 @@
1
1
  import { Config } from '@backstage/config';
2
2
  import { TeamInfo, UsageMetrics } from './types';
3
+ import { BudgetStatus } from '@acarmisc/backstage-plugin-litellm-common';
3
4
  /**
4
5
  * Independent "hide team budget" switches for the two team audiences.
5
6
  *
@@ -23,7 +24,7 @@ export interface TeamBudgetVisibility {
23
24
  * Consumption status derived from spend vs. cap. Mirrors the frontend
24
25
  * `budgetTone` thresholds (80% near, 100% over) so both sides agree.
25
26
  */
26
- export type TeamBudgetStatus = 'ok' | 'near' | 'over';
27
+ export type TeamBudgetStatus = BudgetStatus;
27
28
  export declare function teamBudgetStatusFor(spend: number, budget: number): TeamBudgetStatus;
28
29
  export declare function readTeamBudgetVisibility(config: Config): TeamBudgetVisibility;
29
30
  /**
@@ -4,12 +4,12 @@ exports.teamBudgetStatusFor = teamBudgetStatusFor;
4
4
  exports.readTeamBudgetVisibility = readTeamBudgetVisibility;
5
5
  exports.redactTeamBudget = redactTeamBudget;
6
6
  exports.redactTeamUsage = redactTeamUsage;
7
+ const backstage_plugin_litellm_common_1 = require("@acarmisc/backstage-plugin-litellm-common");
7
8
  function teamBudgetStatusFor(spend, budget) {
8
- if (budget > 0 && spend >= budget)
9
- return 'over';
10
- if (budget > 0 && spend >= budget * 0.8)
11
- return 'near';
12
- return 'ok';
9
+ if (budget <= 0)
10
+ return 'ok';
11
+ const pct = (spend / budget) * 100;
12
+ return (0, backstage_plugin_litellm_common_1.budgetStatusForPct)(pct);
13
13
  }
14
14
  function readTeamBudgetVisibility(config) {
15
15
  return {
@@ -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 /** Spend-reset period for max_budget, e.g. \"30d\". */\n budget_duration?: string;\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 /**\n * Share of max_budget consumed (0-100), present only on redacted records.\n * Set by the backend when litellm.display.hideTeamBudgetFor* is enabled so\n * clients can render the consumption level without dollar amounts.\n */\n budget_pct?: number;\n /** ok | near (>=80%) | over (>=100%), present only on redacted records. */\n budget_status?: 'ok' | 'near' | 'over';\n /** True when max_budget/spend were stripped by budget hiding. */\n budget_hidden?: boolean;\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 budget_duration?: string;\n tpm_limit?: number;\n rpm_limit?: number;\n models?: string[];\n user_id?: string;\n /** Team the key is bound to, when any (mirrors LiteLLM /user/info team_id). */\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}\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\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/** 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"],
5
5
  "mappings": ";;;;;;;;;;;;;;;;AAAA;AAAA;",
6
6
  "names": []
7
7
  }
package/dist/types.d.ts CHANGED
@@ -1,62 +1,4 @@
1
- export interface UserInfo {
2
- user_id: string;
3
- user_email?: string;
4
- email?: string;
5
- teams?: string[];
6
- models?: string[];
7
- max_budget?: number;
8
- spend?: number;
9
- current_spend?: number;
10
- soft_limit?: number;
11
- hard_limit?: number;
12
- /** Spend-reset period for max_budget, e.g. "30d". */
13
- budget_duration?: string;
14
- /** Backstage-computed: true when the user is a member of litellm.audit.group */
15
- can_view_audit?: boolean;
16
- }
17
- export interface TeamMember {
18
- user_id: string;
19
- user_email?: string;
20
- role: 'admin' | 'user';
21
- }
22
- export interface TeamObjectPermission {
23
- /** Vector store ids/names attached to the team. */
24
- vector_stores?: string[];
25
- /** MCP server ids/names attached to the team. */
26
- mcp_servers?: string[];
27
- /** MCP access group names attached to the team. */
28
- mcp_access_groups?: string[];
29
- }
30
- export interface TeamInfo {
31
- team_id: string;
32
- team_alias?: string;
33
- max_budget?: number;
34
- /** Spend-reset period for max_budget, e.g. "30d". */
35
- budget_duration?: string;
36
- spend: number;
37
- /**
38
- * Share of max_budget consumed (0-100), present only on redacted records.
39
- * Set by the backend when litellm.display.hideTeamBudgetFor* is enabled so
40
- * clients can render the consumption level without dollar amounts.
41
- */
42
- budget_pct?: number;
43
- /** ok | near (>=80%) | over (>=100%), present only on redacted records. */
44
- budget_status?: 'ok' | 'near' | 'over';
45
- /** True when max_budget/spend were stripped by budget hiding. */
46
- budget_hidden?: boolean;
47
- members_with_roles?: TeamMember[];
48
- models?: string[];
49
- tpm_limit?: number;
50
- rpm_limit?: number;
51
- /** Arbitrary metadata stored on the team record. */
52
- metadata?: Record<string, unknown>;
53
- /** Knowledge bases (vector stores) and MCP servers attached to the team. */
54
- object_permission?: TeamObjectPermission;
55
- /** Whether the team is blocked/deactivated. */
56
- blocked?: boolean;
57
- /** Max budget available to individual team members (if set per-member). */
58
- team_member_budget?: number;
59
- }
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';
60
2
  export interface CreateTeamRequest {
61
3
  team_alias: string;
62
4
  models?: string[];
@@ -65,33 +7,16 @@ export interface CreateTeamRequest {
65
7
  tpm_limit?: number;
66
8
  rpm_limit?: number;
67
9
  metadata?: Record<string, unknown>;
68
- object_permission?: TeamObjectPermission;
10
+ object_permission?: {
11
+ vector_stores?: string[];
12
+ mcp_servers?: string[];
13
+ mcp_access_groups?: string[];
14
+ };
69
15
  }
70
16
  export interface UpdateTeamRequest extends Partial<CreateTeamRequest> {
71
17
  team_id: string;
72
18
  blocked?: boolean;
73
19
  }
74
- export interface CreateTeamResponse {
75
- team_id: string;
76
- team_alias?: string;
77
- }
78
- export interface VirtualKey {
79
- key: string;
80
- token: string;
81
- key_alias?: string;
82
- created_at: string;
83
- expires_at?: string;
84
- spend: number;
85
- max_budget?: number;
86
- budget_duration?: string;
87
- tpm_limit?: number;
88
- rpm_limit?: number;
89
- models?: string[];
90
- user_id?: string;
91
- /** Team the key is bound to, when any (mirrors LiteLLM /user/info team_id). */
92
- team_id?: string;
93
- blocked?: boolean;
94
- }
95
20
  /**
96
21
  * Shape of a single entry inside LiteLLM's `/user/info` `keys` array.
97
22
  * Differs from VirtualKey: uses `expires` (not `expires_at`), exposes
@@ -113,104 +38,8 @@ export interface LiteLLMUserKey {
113
38
  team_id?: string | null;
114
39
  created_at: string;
115
40
  blocked?: boolean | null;
116
- }
117
- export interface ModelInfo {
118
- model_name: string;
119
- mode: string;
120
- supports_function_calling?: boolean;
121
- supports_vision?: boolean;
122
- input_cost_per_token?: number;
123
- output_cost_per_token?: number;
124
- max_input_tokens?: number;
125
- max_output_tokens?: number;
126
- /** 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. */
127
- access_groups?: string[];
128
- }
129
- export interface UsageModelBreakdown {
130
- total_spend: number;
131
- total_tokens: number;
132
- prompt_tokens: number;
133
- completion_tokens: number;
134
- api_requests: number;
135
- successful_requests: number;
136
- failed_requests: number;
137
- }
138
- export interface UsageKeyBreakdown {
139
- key_alias?: string;
140
- team_id?: string | null;
141
- models: string[];
142
- total_spend: number;
143
- total_tokens: number;
144
- prompt_tokens: number;
145
- completion_tokens: number;
146
- api_requests: number;
147
- successful_requests: number;
148
- failed_requests: number;
149
- }
150
- export interface UsageDailyPoint {
151
- date: string;
152
- spend: number;
153
- total_tokens: number;
154
- prompt_tokens: number;
155
- completion_tokens: number;
156
- api_requests: number;
157
- successful_requests: number;
158
- failed_requests: number;
159
- }
160
- export interface UsageDailyModelPoint {
161
- date: string;
162
- model: string;
163
- spend: number;
164
- prompt_tokens: number;
165
- completion_tokens: number;
166
- total_tokens: number;
167
- api_requests: number;
168
- successful_requests: number;
169
- failed_requests: number;
170
- }
171
- export interface UsageMetrics {
172
- total_spend: number;
173
- total_tokens: number;
174
- prompt_tokens: number;
175
- completion_tokens: number;
176
- api_requests: number;
177
- successful_requests: number;
178
- failed_requests: number;
179
- usage_by_model: Record<string, UsageModelBreakdown>;
180
- usage_by_key: Record<string, UsageKeyBreakdown>;
181
- daily_usage: UsageDailyPoint[];
182
- daily_by_model: UsageDailyModelPoint[];
183
- }
184
- export interface GenerateKeyRequest {
185
- alias?: string;
186
- models?: string[];
187
- duration?: string;
188
- max_budget?: number;
189
- tpm_limit?: number;
190
- rpm_limit?: number;
191
- user_id?: string;
192
- team_id?: string;
193
- key_type?: string;
194
- metadata?: Record<string, string>;
195
- }
196
- export interface UpdateKeyRequest {
197
- key: string;
198
- key_alias?: string;
199
- models?: string[];
200
- max_budget?: number;
201
- tpm_limit?: number;
202
- rpm_limit?: number;
203
- team_id?: string;
204
- duration?: string;
205
- }
206
- export interface GenerateKeyResponse {
207
- key: string;
208
- key_alias?: string;
209
- expires_at?: string;
210
- max_budget?: number;
211
- tpm_limit?: number;
212
- rpm_limit?: number;
213
- models?: string[];
41
+ /** Arbitrary metadata stored on the key record. */
42
+ metadata?: Record<string, unknown> | null;
214
43
  }
215
44
  export interface DeleteKeyRequest {
216
45
  keys: string[];
@@ -267,24 +96,6 @@ export interface CreateUserResponse {
267
96
  models?: string[];
268
97
  teams?: string[];
269
98
  }
270
- export interface AuditLogEntry {
271
- id: string;
272
- updated_at: string;
273
- changed_by?: string;
274
- changed_by_api_key?: string;
275
- action?: string;
276
- table_name?: string;
277
- object_id?: string;
278
- before_value?: Record<string, unknown> | null;
279
- updated_values?: Record<string, unknown> | null;
280
- }
281
- export interface PaginatedAuditLogs {
282
- audit_logs: AuditLogEntry[];
283
- total: number;
284
- page: number;
285
- page_size: number;
286
- total_pages: number;
287
- }
288
99
  /** One row from LiteLLM's `/spend/logs` (per-request spend log entry). */
289
100
  export interface SpendLogEntry {
290
101
  request_id?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acarmisc/backstage-plugin-litellm-backend",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "The Backstage backend plugin for LiteLLM governance",
5
5
  "backstage": {
6
6
  "role": "backend-plugin",
@@ -30,20 +30,23 @@
30
30
  "scripts": {
31
31
  "build": "node build.js && tsc -p tsconfig.json",
32
32
  "lint": "eslint src --ext .ts",
33
- "test": "tsc -p tsconfig.test.json && node --test dist-test/**/*.test.js dist-test/*.test.js",
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
35
  "prepack": "npm run build",
36
36
  "postpack": ""
37
37
  },
38
38
  "dependencies": {
39
+ "@acarmisc/backstage-plugin-litellm-common": "0.1.0",
39
40
  "@backstage/backend-plugin-api": "^1.9.1",
40
41
  "@backstage/catalog-client": "^1.9.0",
41
42
  "@backstage/catalog-model": "^1.7.0",
42
43
  "@backstage/config": "^1.3.0",
44
+ "@backstage/errors": "^1.3.0",
43
45
  "@backstage/plugin-permission-common": "^0.9.10",
44
46
  "@backstage/types": "^1.2.0",
45
47
  "express": "^4.18.2",
46
- "jose": "^5.9.6"
48
+ "jose": "^5.9.6",
49
+ "zod": "^3.22.0"
47
50
  },
48
51
  "devDependencies": {
49
52
  "@backstage/cli": "^0.36.2",
@@ -1,11 +0,0 @@
1
- export declare const litellmKeyCreatePermission: import("@backstage/plugin-permission-common").BasicPermission;
2
- export declare const litellmKeyRevokePermission: import("@backstage/plugin-permission-common").BasicPermission;
3
- export declare const litellmKeyManagePermission: import("@backstage/plugin-permission-common").BasicPermission;
4
- export declare const litellmAuditReadPermission: import("@backstage/plugin-permission-common").BasicPermission;
5
- export declare const litellmTeamCreatePermission: import("@backstage/plugin-permission-common").BasicPermission;
6
- export declare const litellmTeamManagePermission: import("@backstage/plugin-permission-common").BasicPermission;
7
- export declare const litellmTeamMembersManagePermission: import("@backstage/plugin-permission-common").BasicPermission;
8
- export declare const litellmTeamKnowledgebaseManagePermission: import("@backstage/plugin-permission-common").BasicPermission;
9
- export declare const litellmTeamMcpManagePermission: import("@backstage/plugin-permission-common").BasicPermission;
10
- export declare const litellmTeamDeletePermission: import("@backstage/plugin-permission-common").BasicPermission;
11
- export declare const litellmPermissions: import("@backstage/plugin-permission-common").BasicPermission[];
@@ -1,57 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.litellmPermissions = exports.litellmTeamDeletePermission = exports.litellmTeamMcpManagePermission = exports.litellmTeamKnowledgebaseManagePermission = exports.litellmTeamMembersManagePermission = exports.litellmTeamManagePermission = exports.litellmTeamCreatePermission = exports.litellmAuditReadPermission = exports.litellmKeyManagePermission = exports.litellmKeyRevokePermission = exports.litellmKeyCreatePermission = void 0;
4
- const plugin_permission_common_1 = require("@backstage/plugin-permission-common");
5
- exports.litellmKeyCreatePermission = (0, plugin_permission_common_1.createPermission)({
6
- name: 'litellm.key.create',
7
- attributes: { action: 'create' },
8
- });
9
- exports.litellmKeyRevokePermission = (0, plugin_permission_common_1.createPermission)({
10
- name: 'litellm.key.revoke',
11
- attributes: { action: 'delete' },
12
- });
13
- exports.litellmKeyManagePermission = (0, plugin_permission_common_1.createPermission)({
14
- name: 'litellm.key.manage',
15
- attributes: { action: 'update' },
16
- });
17
- exports.litellmAuditReadPermission = (0, plugin_permission_common_1.createPermission)({
18
- name: 'litellm.audit.read',
19
- attributes: { action: 'read' },
20
- });
21
- // Team-management permissions — delegated to litellm-team-admins group
22
- exports.litellmTeamCreatePermission = (0, plugin_permission_common_1.createPermission)({
23
- name: 'litellm.team.create',
24
- attributes: { action: 'create' },
25
- });
26
- exports.litellmTeamManagePermission = (0, plugin_permission_common_1.createPermission)({
27
- name: 'litellm.team.manage',
28
- attributes: { action: 'update' },
29
- });
30
- exports.litellmTeamMembersManagePermission = (0, plugin_permission_common_1.createPermission)({
31
- name: 'litellm.team.members.manage',
32
- attributes: { action: 'update' },
33
- });
34
- exports.litellmTeamKnowledgebaseManagePermission = (0, plugin_permission_common_1.createPermission)({
35
- name: 'litellm.team.knowledgebase.manage',
36
- attributes: { action: 'update' },
37
- });
38
- exports.litellmTeamMcpManagePermission = (0, plugin_permission_common_1.createPermission)({
39
- name: 'litellm.team.mcp.manage',
40
- attributes: { action: 'update' },
41
- });
42
- exports.litellmTeamDeletePermission = (0, plugin_permission_common_1.createPermission)({
43
- name: 'litellm.team.delete',
44
- attributes: { action: 'delete' },
45
- });
46
- exports.litellmPermissions = [
47
- exports.litellmKeyCreatePermission,
48
- exports.litellmKeyRevokePermission,
49
- exports.litellmKeyManagePermission,
50
- exports.litellmAuditReadPermission,
51
- exports.litellmTeamCreatePermission,
52
- exports.litellmTeamManagePermission,
53
- exports.litellmTeamMembersManagePermission,
54
- exports.litellmTeamKnowledgebaseManagePermission,
55
- exports.litellmTeamMcpManagePermission,
56
- exports.litellmTeamDeletePermission,
57
- ];