@acarmisc/backstage-plugin-litellm-backend 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export { litellmPlugin } from './plugin';
2
2
  export { litellmPlugin as default } from './plugin';
3
3
  export { createRouter } from './router';
4
4
  export * from './types';
5
- export { LiteLLMClient } from './client';
5
+ export { LiteLLMClient, normalizeRequestTags } from './client';
6
6
  export { resolveUserId, resolveUserProfile, toLiteLLMUserId, getOrProvisionUser, provisionUser, readProvisioningDefaults, readRoleConfigs, applyRoleOverrides, resolveUserRole, ProvisioningError, } from './provisioning';
7
7
  export type { BackstageUserProfile } from './provisioning';
8
8
  export { readTeamAdminConfig, isTeamManagementEnabled, assertTeamAdmin, validateTeamWriteInput, validateTeamPatchInput, } from './teamAdmin';
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.litellmTeamDeletePermission = exports.litellmTeamMcpManagePermission = exports.litellmTeamKnowledgebaseManagePermission = exports.litellmTeamMembersManagePermission = exports.litellmTeamManagePermission = exports.litellmTeamCreatePermission = exports.litellmAuditReadPermission = exports.litellmKeyManagePermission = exports.litellmKeyRevokePermission = exports.litellmKeyCreatePermission = exports.litellmPermissions = exports.bridgeGenerateKey = exports.bridgeListKeys = exports.getOrProvisionUserFromClaims = exports.resolveBridgeUserId = exports.readBridgeConfig = exports.newDefaultVerifier = exports.KeycloakJWTVerifier = exports.teamBudgetStatusFor = exports.redactTeamUsage = exports.redactTeamBudget = exports.readTeamBudgetVisibility = exports.validateTeamPatchInput = exports.validateTeamWriteInput = exports.assertTeamAdmin = exports.isTeamManagementEnabled = exports.readTeamAdminConfig = exports.ProvisioningError = exports.resolveUserRole = exports.applyRoleOverrides = exports.readRoleConfigs = exports.readProvisioningDefaults = exports.provisionUser = exports.getOrProvisionUser = exports.toLiteLLMUserId = exports.resolveUserProfile = exports.resolveUserId = exports.LiteLLMClient = exports.createRouter = exports.default = exports.litellmPlugin = void 0;
17
+ exports.litellmTeamDeletePermission = exports.litellmTeamMcpManagePermission = exports.litellmTeamKnowledgebaseManagePermission = exports.litellmTeamMembersManagePermission = exports.litellmTeamManagePermission = exports.litellmTeamCreatePermission = exports.litellmAuditReadPermission = exports.litellmKeyManagePermission = exports.litellmKeyRevokePermission = exports.litellmKeyCreatePermission = exports.litellmPermissions = exports.bridgeGenerateKey = exports.bridgeListKeys = exports.getOrProvisionUserFromClaims = exports.resolveBridgeUserId = exports.readBridgeConfig = exports.newDefaultVerifier = exports.KeycloakJWTVerifier = exports.teamBudgetStatusFor = exports.redactTeamUsage = exports.redactTeamBudget = exports.readTeamBudgetVisibility = exports.validateTeamPatchInput = exports.validateTeamWriteInput = exports.assertTeamAdmin = exports.isTeamManagementEnabled = exports.readTeamAdminConfig = exports.ProvisioningError = exports.resolveUserRole = exports.applyRoleOverrides = exports.readRoleConfigs = exports.readProvisioningDefaults = exports.provisionUser = exports.getOrProvisionUser = exports.toLiteLLMUserId = exports.resolveUserProfile = exports.resolveUserId = exports.normalizeRequestTags = exports.LiteLLMClient = exports.createRouter = exports.default = exports.litellmPlugin = void 0;
18
18
  var plugin_1 = require("./plugin");
19
19
  Object.defineProperty(exports, "litellmPlugin", { enumerable: true, get: function () { return plugin_1.litellmPlugin; } });
20
20
  var plugin_2 = require("./plugin");
@@ -24,6 +24,7 @@ Object.defineProperty(exports, "createRouter", { enumerable: true, get: function
24
24
  __exportStar(require("./types"), exports);
25
25
  var client_1 = require("./client");
26
26
  Object.defineProperty(exports, "LiteLLMClient", { enumerable: true, get: function () { return client_1.LiteLLMClient; } });
27
+ Object.defineProperty(exports, "normalizeRequestTags", { enumerable: true, get: function () { return client_1.normalizeRequestTags; } });
27
28
  var provisioning_1 = require("./provisioning");
28
29
  Object.defineProperty(exports, "resolveUserId", { enumerable: true, get: function () { return provisioning_1.resolveUserId; } });
29
30
  Object.defineProperty(exports, "resolveUserProfile", { enumerable: true, get: function () { return provisioning_1.resolveUserProfile; } });
@@ -0,0 +1,30 @@
1
+ import { Config } from '@backstage/config';
2
+ /**
3
+ * Configuration for the OpenCode SSO-connect integration (litellm.opencode.*).
4
+ *
5
+ * The connect flow is served by the opencode-portal-auth plugin client-side:
6
+ * the user runs /connect in OpenCode, a browser opens
7
+ * GET {baseUrl}/api/litellm/opencode/connect?team=...&redirect_uri=...
8
+ * and this backend generates (or reuses) a LiteLLM virtual key bound to the
9
+ * signed-in Backstage user and, optionally, one of their teams, then 302s
10
+ * back to the local OpenCode callback listener.
11
+ */
12
+ export interface OpencodeConfig {
13
+ /** Route is mounted only when true. Default false. */
14
+ enabled: boolean;
15
+ /**
16
+ * Default duration for freshly generated keys, in LiteLLM format
17
+ * (e.g. "30d"). Users can re-connect to rotate. Default '30d'.
18
+ */
19
+ keyDuration: string;
20
+ /** Default max budget (USD) for keys created via the connect flow. Default 50. */
21
+ maxBudget: number;
22
+ /**
23
+ * When true, a team query parameter must reference a team the user is a
24
+ * member of (or manage); otherwise the key is personal-only.
25
+ */
26
+ requireTeam: boolean;
27
+ /** Extra metadata stamped on keys created via this flow. */
28
+ metadata: Record<string, string>;
29
+ }
30
+ export declare function readOpencodeConfig(config: Config): OpencodeConfig;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readOpencodeConfig = readOpencodeConfig;
4
+ function readOpencodeConfig(config) {
5
+ const enabled = config.getOptionalBoolean('litellm.opencode.enabled') ?? false;
6
+ return {
7
+ enabled,
8
+ keyDuration: config.getOptionalString('litellm.opencode.keyDuration') ?? '30d',
9
+ maxBudget: config.getOptionalNumber('litellm.opencode.maxBudget') ?? 50,
10
+ requireTeam: config.getOptionalBoolean('litellm.opencode.requireTeam') ?? false,
11
+ metadata: config.getOptional('litellm.opencode.metadata') ?? {},
12
+ };
13
+ }
package/dist/router.js CHANGED
@@ -46,6 +46,7 @@ const bridge_1 = require("./bridge");
46
46
  const permissions_1 = require("./permissions");
47
47
  const teamAdmin_1 = require("./teamAdmin");
48
48
  const teamBudgetVisibility_1 = require("./teamBudgetVisibility");
49
+ const opencode_1 = require("./opencode");
49
50
  const teamCreateInFlight = new Map();
50
51
  /** Backoff schedule for `withTeamFetchRetry` — 3 retries over ~1.3s total. */
51
52
  const TEAM_FETCH_RETRY_DELAYS_MS = [100, 300, 900];
@@ -85,6 +86,24 @@ async function withTeamFetchRetry(fn, delaysMs = TEAM_FETCH_RETRY_DELAYS_MS) {
85
86
  }
86
87
  }
87
88
  }
89
+ /**
90
+ * Only loopback http://localhost:<port>/callback redirect URIs are accepted —
91
+ * the OpenCode plugin runs its callback listener on 127.0.0.1. This
92
+ * keeps the flow from being usable as an open redirect.
93
+ */
94
+ function isValidRedirectUri(uri) {
95
+ if (!uri)
96
+ return false;
97
+ try {
98
+ const url = new URL(uri);
99
+ return (url.protocol === 'http:' &&
100
+ (url.hostname === 'localhost' || url.hostname === '127.0.0.1') &&
101
+ url.pathname === '/callback');
102
+ }
103
+ catch {
104
+ return false;
105
+ }
106
+ }
88
107
  async function createRouter(options) {
89
108
  const { config, logger, auth, discovery, permissions } = options;
90
109
  const baseUrl = config.getString('litellm.baseUrl');
@@ -104,6 +123,7 @@ async function createRouter(options) {
104
123
  const objectPermsEnabled = (0, teamAdmin_1.isObjectPermissionsEnabled)(config);
105
124
  const teamAdminCfg = (0, teamAdmin_1.readTeamAdminConfig)(config);
106
125
  const teamBudgetVisibility = (0, teamBudgetVisibility_1.readTeamBudgetVisibility)(config);
126
+ const opencodeCfg = (0, opencode_1.readOpencodeConfig)(config);
107
127
  const catalogClient = options.catalogClient ?? new catalog_client_1.CatalogClient({ discoveryApi: discovery });
108
128
  if (provisioningEnabled) {
109
129
  logger.info(`LiteLLM auto-provisioning enabled — defaults: budget=$${provisioningDefaults.maxBudget}/${provisioningDefaults.budgetDuration}, models=${provisioningDefaults.models.length
@@ -131,6 +151,7 @@ async function createRouter(options) {
131
151
  res.json({
132
152
  baseUrl: publicBaseUrl,
133
153
  keyGeneration: { allowUnlimitedBudget, teamRequired },
154
+ opencode: { enabled: opencodeCfg.enabled },
134
155
  teamManagement: {
135
156
  enabled: teamMgmtEnabled,
136
157
  maxBudgetCeiling: teamAdminCfg.maxBudgetCeiling,
@@ -396,6 +417,122 @@ async function createRouter(options) {
396
417
  res.status(500).json({ error: error.message });
397
418
  }
398
419
  });
420
+ // ── OpenCode SSO-connect ─────────────────────────────────────────────────
421
+ //
422
+ // Client side: the opencode-portal-auth plugin. Handshake contract (see
423
+ // that package's README):
424
+ //
425
+ // GET /opencode/connect?team=<id>&redirect_uri=http://localhost:<port>/callback
426
+ // → (SSO-authenticated) generate/reuse a virtual key bound to the
427
+ // user and optional team → 302 {redirect_uri}?key=<api-key>
428
+ //
429
+ // Mounted only when litellm.opencode.enabled is true. Key creation is
430
+ // gated by the same litellmKeyCreatePermission as POST /keys/generate.
431
+ if (opencodeCfg.enabled) {
432
+ logger.info(`OpenCode connect endpoint enabled — duration=${opencodeCfg.keyDuration}, maxBudget=$${opencodeCfg.maxBudget}`);
433
+ router.get('/opencode/connect', async (req, res) => {
434
+ try {
435
+ // The request comes from the browser (user session), so SSO
436
+ // auth applies exactly like for any other UI route.
437
+ const tokenEntityRef = await (0, provisioning_1.resolveUserId)(req, auth);
438
+ if (!tokenEntityRef) {
439
+ res.status(401).json({ error: 'Authentication required' });
440
+ return;
441
+ }
442
+ const redirectUri = req.query.redirect_uri;
443
+ if (!isValidRedirectUri(redirectUri)) {
444
+ res.status(400).json({
445
+ error: 'Invalid redirect_uri — expected http://localhost:<port>/callback',
446
+ });
447
+ return;
448
+ }
449
+ if (!(await assertPermission(req, permissions_1.litellmKeyCreatePermission))) {
450
+ sendPermissionDenied(res, permissions_1.litellmKeyCreatePermission);
451
+ return;
452
+ }
453
+ const userId = (0, provisioning_1.toLiteLLMUserId)(tokenEntityRef, userIdDomain);
454
+ const userInfo = await (0, provisioning_1.getOrProvisionUser)(client, tokenEntityRef, userId, provisioningEnabled, provisioningDefaults, roleConfigs, catalogClient, auth, logger);
455
+ // Optional team binding: must be one of the user's own teams so
456
+ // the connect flow can never mint a key billed to someone else.
457
+ const teamId = req.query.team;
458
+ if (teamId) {
459
+ const userTeams = userInfo?.teams ?? [];
460
+ if (!userTeams.includes(teamId)) {
461
+ res.status(403).json({
462
+ error: 'Access denied: team is not one of your teams',
463
+ team: teamId,
464
+ });
465
+ return;
466
+ }
467
+ }
468
+ else if (opencodeCfg.requireTeam) {
469
+ res.status(400).json({ error: 'A team is required for this connection' });
470
+ return;
471
+ }
472
+ // Alias is stable per user+team so re-connecting rotates the same
473
+ // logical key slot instead of accumulating duplicates. LiteLLM
474
+ // rejects duplicate aliases, so on conflict we look up the existing
475
+ // key and reuse it (the user gets a working connection either way).
476
+ const alias = teamId
477
+ ? `opencode-${userId}-${teamId}`
478
+ : `opencode-${userId}`;
479
+ const existing = await client
480
+ .listKeys(userId)
481
+ .catch(() => []);
482
+ // Reuse a healthy existing key for this slot rather than failing
483
+ // on the alias collision LiteLLM would raise. Blocked keys are
484
+ // skipped so a blocked slot can be recovered by re-connecting.
485
+ const reusable = existing.find(k => k.key_alias === alias &&
486
+ !k.blocked &&
487
+ k.user_id === userId);
488
+ let key;
489
+ if (reusable?.token ?? reusable?.key) {
490
+ key = reusable.token ?? reusable.key;
491
+ }
492
+ else {
493
+ const profile = await (0, provisioning_1.resolveUserProfile)(tokenEntityRef, catalogClient, auth, logger);
494
+ const result = await client.generateKey({
495
+ alias,
496
+ duration: opencodeCfg.keyDuration,
497
+ max_budget: opencodeCfg.maxBudget,
498
+ team_id: teamId,
499
+ metadata: {
500
+ ...opencodeCfg.metadata,
501
+ created_via: 'opencode-connect',
502
+ created_by_backstage_user: tokenEntityRef,
503
+ ...(profile.email && { created_by_email: profile.email }),
504
+ ...(teamId && { opencode_team: teamId }),
505
+ },
506
+ user_id: userId,
507
+ });
508
+ if (!result.key) {
509
+ res.status(502).json({ error: 'LiteLLM returned no key material' });
510
+ return;
511
+ }
512
+ key = result.key;
513
+ }
514
+ const url = new URL(redirectUri);
515
+ url.searchParams.set('key', key);
516
+ if (teamId)
517
+ url.searchParams.set('team', teamId);
518
+ logger.info({
519
+ action: 'opencode.connect',
520
+ userId,
521
+ team: teamId ?? null,
522
+ reused: Boolean(reusable?.token ?? reusable?.key),
523
+ });
524
+ res.redirect(302, url.href);
525
+ }
526
+ catch (error) {
527
+ if (error instanceof provisioning_1.ProvisioningError) {
528
+ res.status(error.status).json(error.body);
529
+ return;
530
+ }
531
+ logger.error('OpenCode connect failed', error);
532
+ res.status(500).json({ error: error.message });
533
+ }
534
+ });
535
+ }
399
536
  router.post('/keys/:keyId/update', async (req, res) => {
400
537
  try {
401
538
  const { keyId } = req.params;
@@ -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 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\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": ["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 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"],
5
5
  "mappings": ";;;;;;;;;;;;;;;;AAAA;AAAA;",
6
6
  "names": []
7
7
  }
package/dist/types.d.ts CHANGED
@@ -283,6 +283,42 @@ export interface PaginatedAuditLogs {
283
283
  page_size: number;
284
284
  total_pages: number;
285
285
  }
286
+ /** One row from LiteLLM's `/spend/logs` (per-request spend log entry). */
287
+ export interface SpendLogEntry {
288
+ request_id?: string;
289
+ /** ISO timestamp of the call. */
290
+ startTime?: string;
291
+ endTime?: string;
292
+ spend?: number;
293
+ total_tokens?: number;
294
+ prompt_tokens?: number;
295
+ completion_tokens?: number;
296
+ model?: string;
297
+ /** Virtual key alias or hash the request was billed to. */
298
+ api_key?: string;
299
+ user?: string;
300
+ team_id?: string;
301
+ /**
302
+ * Tags attached to the request (e.g. `channel:backstage`,
303
+ * `session:<thread>`, `invoked-by:<user>`). Shape varies across LiteLLM
304
+ * versions (array or object) and is normalised to a string[].
305
+ */
306
+ request_tags?: string[] | Record<string, string>;
307
+ /** LiteLLM `metadata` blob; carries trace ids and session grouping. */
308
+ metadata?: Record<string, unknown> | string | null;
309
+ }
310
+ export interface SpendLogsParams {
311
+ /** ISO date `YYYY-MM-DD` (inclusive). */
312
+ start_date: string;
313
+ /** ISO date `YYYY-MM-DD` (inclusive). */
314
+ end_date: string;
315
+ /** Restrict to the given virtual key. */
316
+ api_key?: string;
317
+ user_id?: string;
318
+ team_id?: string;
319
+ /** Max rows to return (LiteLLM caps at 1000 per page). */
320
+ page_size?: number;
321
+ }
286
322
  export interface AuditLogsParams {
287
323
  page?: number;
288
324
  page_size?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acarmisc/backstage-plugin-litellm-backend",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "The Backstage backend plugin for LiteLLM governance",
5
5
  "backstage": {
6
6
  "role": "backend-plugin",