@acarmisc/backstage-plugin-litellm-backend 0.12.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/config.d.ts +28 -0
- package/dist/client.d.ts +18 -1
- package/dist/client.js +54 -0
- package/dist/index.cjs.js +298 -9
- package/dist/index.cjs.js.map +3 -3
- package/dist/index.d.ts +3 -1
- package/dist/index.js +7 -1
- package/dist/openapi.js +9 -0
- package/dist/opencode.d.ts +30 -0
- package/dist/opencode.js +13 -0
- package/dist/router.js +196 -9
- package/dist/teamBudgetVisibility.d.ts +48 -0
- package/dist/teamBudgetVisibility.js +76 -0
- package/dist/types.cjs.js.map +1 -1
- package/dist/types.d.ts +46 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -2,11 +2,13 @@ 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';
|
|
9
9
|
export type { TeamAdminConfig, TeamAdminCheck, TeamWriteInput, TeamWriteValidation, TeamPatchValidation } from './teamAdmin';
|
|
10
|
+
export { readTeamBudgetVisibility, redactTeamBudget, redactTeamUsage, teamBudgetStatusFor, } from './teamBudgetVisibility';
|
|
11
|
+
export type { TeamBudgetVisibility, TeamBudgetStatus } from './teamBudgetVisibility';
|
|
10
12
|
export { KeycloakJWTVerifier, newDefaultVerifier, readBridgeConfig, resolveBridgeUserId, getOrProvisionUserFromClaims, bridgeListKeys, bridgeGenerateKey, } from './bridge';
|
|
11
13
|
export type { BridgeClaims, TokenVerifier, BridgeConfig, KeycloakJWTVerifierOptions, } from './bridge';
|
|
12
14
|
export { litellmPermissions, litellmKeyCreatePermission, litellmKeyRevokePermission, litellmKeyManagePermission, litellmAuditReadPermission, litellmTeamCreatePermission, litellmTeamManagePermission, litellmTeamMembersManagePermission, litellmTeamKnowledgebaseManagePermission, litellmTeamMcpManagePermission, litellmTeamDeletePermission, } from './permissions';
|
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.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; } });
|
|
@@ -41,6 +42,11 @@ Object.defineProperty(exports, "isTeamManagementEnabled", { enumerable: true, ge
|
|
|
41
42
|
Object.defineProperty(exports, "assertTeamAdmin", { enumerable: true, get: function () { return teamAdmin_1.assertTeamAdmin; } });
|
|
42
43
|
Object.defineProperty(exports, "validateTeamWriteInput", { enumerable: true, get: function () { return teamAdmin_1.validateTeamWriteInput; } });
|
|
43
44
|
Object.defineProperty(exports, "validateTeamPatchInput", { enumerable: true, get: function () { return teamAdmin_1.validateTeamPatchInput; } });
|
|
45
|
+
var teamBudgetVisibility_1 = require("./teamBudgetVisibility");
|
|
46
|
+
Object.defineProperty(exports, "readTeamBudgetVisibility", { enumerable: true, get: function () { return teamBudgetVisibility_1.readTeamBudgetVisibility; } });
|
|
47
|
+
Object.defineProperty(exports, "redactTeamBudget", { enumerable: true, get: function () { return teamBudgetVisibility_1.redactTeamBudget; } });
|
|
48
|
+
Object.defineProperty(exports, "redactTeamUsage", { enumerable: true, get: function () { return teamBudgetVisibility_1.redactTeamUsage; } });
|
|
49
|
+
Object.defineProperty(exports, "teamBudgetStatusFor", { enumerable: true, get: function () { return teamBudgetVisibility_1.teamBudgetStatusFor; } });
|
|
44
50
|
var bridge_1 = require("./bridge");
|
|
45
51
|
Object.defineProperty(exports, "KeycloakJWTVerifier", { enumerable: true, get: function () { return bridge_1.KeycloakJWTVerifier; } });
|
|
46
52
|
Object.defineProperty(exports, "newDefaultVerifier", { enumerable: true, get: function () { return bridge_1.newDefaultVerifier; } });
|
package/dist/openapi.js
CHANGED
|
@@ -66,6 +66,10 @@ exports.openApiSpec = {
|
|
|
66
66
|
allowUnlimitedBudget: { type: 'boolean' },
|
|
67
67
|
teamRequired: { type: 'boolean' },
|
|
68
68
|
} },
|
|
69
|
+
display: { type: 'object', properties: {
|
|
70
|
+
hideTeamBudgetForMembers: { type: 'boolean' },
|
|
71
|
+
hideTeamBudgetForManagers: { type: 'boolean' },
|
|
72
|
+
} },
|
|
69
73
|
} } } },
|
|
70
74
|
},
|
|
71
75
|
},
|
|
@@ -169,6 +173,9 @@ exports.openApiSpec = {
|
|
|
169
173
|
get: {
|
|
170
174
|
tags: ['Teams'],
|
|
171
175
|
summary: 'List teams the current user belongs to',
|
|
176
|
+
description: 'When litellm.display.hideTeamBudgetForMembers is set, max_budget/spend ' +
|
|
177
|
+
'are stripped and budget_pct/budget_status/budget_hidden carry the ' +
|
|
178
|
+
'consumption level instead.',
|
|
172
179
|
responses: { '200': { description: 'Array of teams' } },
|
|
173
180
|
},
|
|
174
181
|
},
|
|
@@ -176,6 +183,8 @@ exports.openApiSpec = {
|
|
|
176
183
|
get: {
|
|
177
184
|
tags: ['Teams'],
|
|
178
185
|
summary: 'Usage metrics for a team',
|
|
186
|
+
description: 'Spend fields are zeroed when the caller is subject to team budget ' +
|
|
187
|
+
'hiding (member or manager flag, whichever applies to them).',
|
|
179
188
|
parameters: [
|
|
180
189
|
{ name: 'teamId', in: 'path', required: true, schema: { type: 'string' } },
|
|
181
190
|
{ name: 'start_date', in: 'query', required: true, schema: { type: 'string' } },
|
|
@@ -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;
|
package/dist/opencode.js
ADDED
|
@@ -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
|
@@ -45,6 +45,8 @@ Object.defineProperty(exports, "ProvisioningError", { enumerable: true, get: fun
|
|
|
45
45
|
const bridge_1 = require("./bridge");
|
|
46
46
|
const permissions_1 = require("./permissions");
|
|
47
47
|
const teamAdmin_1 = require("./teamAdmin");
|
|
48
|
+
const teamBudgetVisibility_1 = require("./teamBudgetVisibility");
|
|
49
|
+
const opencode_1 = require("./opencode");
|
|
48
50
|
const teamCreateInFlight = new Map();
|
|
49
51
|
/** Backoff schedule for `withTeamFetchRetry` — 3 retries over ~1.3s total. */
|
|
50
52
|
const TEAM_FETCH_RETRY_DELAYS_MS = [100, 300, 900];
|
|
@@ -84,6 +86,24 @@ async function withTeamFetchRetry(fn, delaysMs = TEAM_FETCH_RETRY_DELAYS_MS) {
|
|
|
84
86
|
}
|
|
85
87
|
}
|
|
86
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
|
+
}
|
|
87
107
|
async function createRouter(options) {
|
|
88
108
|
const { config, logger, auth, discovery, permissions } = options;
|
|
89
109
|
const baseUrl = config.getString('litellm.baseUrl');
|
|
@@ -102,6 +122,8 @@ async function createRouter(options) {
|
|
|
102
122
|
const teamMgmtEnabled = (0, teamAdmin_1.isTeamManagementEnabled)(config);
|
|
103
123
|
const objectPermsEnabled = (0, teamAdmin_1.isObjectPermissionsEnabled)(config);
|
|
104
124
|
const teamAdminCfg = (0, teamAdmin_1.readTeamAdminConfig)(config);
|
|
125
|
+
const teamBudgetVisibility = (0, teamBudgetVisibility_1.readTeamBudgetVisibility)(config);
|
|
126
|
+
const opencodeCfg = (0, opencode_1.readOpencodeConfig)(config);
|
|
105
127
|
const catalogClient = options.catalogClient ?? new catalog_client_1.CatalogClient({ discoveryApi: discovery });
|
|
106
128
|
if (provisioningEnabled) {
|
|
107
129
|
logger.info(`LiteLLM auto-provisioning enabled — defaults: budget=$${provisioningDefaults.maxBudget}/${provisioningDefaults.budgetDuration}, models=${provisioningDefaults.models.length
|
|
@@ -111,6 +133,10 @@ async function createRouter(options) {
|
|
|
111
133
|
if (config.getOptional('litellm.teamAdmin') && !teamMgmtEnabled) {
|
|
112
134
|
logger.warn('litellm.teamAdmin is configured but team management is disabled — set permission.enabled: true and litellm.teamAdmin.group to enable it.');
|
|
113
135
|
}
|
|
136
|
+
if (teamBudgetVisibility.hideTeamBudgetForMembers ||
|
|
137
|
+
teamBudgetVisibility.hideTeamBudgetForManagers) {
|
|
138
|
+
logger.info(`LiteLLM team budget hiding enabled — members: ${teamBudgetVisibility.hideTeamBudgetForMembers}, managers: ${teamBudgetVisibility.hideTeamBudgetForManagers}`);
|
|
139
|
+
}
|
|
114
140
|
const router = (0, express_1.Router)();
|
|
115
141
|
// JSON body parser. Without this, every POST/PUT endpoint sees an empty
|
|
116
142
|
// req.body. Backstage's httpRouter does not apply a body parser at the
|
|
@@ -125,12 +151,17 @@ async function createRouter(options) {
|
|
|
125
151
|
res.json({
|
|
126
152
|
baseUrl: publicBaseUrl,
|
|
127
153
|
keyGeneration: { allowUnlimitedBudget, teamRequired },
|
|
154
|
+
opencode: { enabled: opencodeCfg.enabled },
|
|
128
155
|
teamManagement: {
|
|
129
156
|
enabled: teamMgmtEnabled,
|
|
130
157
|
maxBudgetCeiling: teamAdminCfg.maxBudgetCeiling,
|
|
131
158
|
allowUnlimitedBudget: teamAdminCfg.allowUnlimitedBudget,
|
|
132
159
|
objectPermissionsEnabled: objectPermsEnabled,
|
|
133
160
|
},
|
|
161
|
+
display: {
|
|
162
|
+
hideTeamBudgetForMembers: teamBudgetVisibility.hideTeamBudgetForMembers,
|
|
163
|
+
hideTeamBudgetForManagers: teamBudgetVisibility.hideTeamBudgetForManagers,
|
|
164
|
+
},
|
|
134
165
|
});
|
|
135
166
|
});
|
|
136
167
|
// Self-hosted OpenAPI 3.1 contract — lets integrators read a spec instead
|
|
@@ -386,6 +417,122 @@ async function createRouter(options) {
|
|
|
386
417
|
res.status(500).json({ error: error.message });
|
|
387
418
|
}
|
|
388
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
|
+
}
|
|
389
536
|
router.post('/keys/:keyId/update', async (req, res) => {
|
|
390
537
|
try {
|
|
391
538
|
const { keyId } = req.params;
|
|
@@ -552,7 +699,12 @@ async function createRouter(options) {
|
|
|
552
699
|
logger.warn(`Failed to fetch team ${teamId} after retries: ${err.message}`);
|
|
553
700
|
return null;
|
|
554
701
|
})));
|
|
555
|
-
|
|
702
|
+
const list = teams.filter(Boolean);
|
|
703
|
+
// Member surface: strip dollar amounts when the operator hides them
|
|
704
|
+
// from members. Managers who need the numbers use /teams/managed.
|
|
705
|
+
res.json(teamBudgetVisibility.hideTeamBudgetForMembers
|
|
706
|
+
? list.map(teamBudgetVisibility_1.redactTeamBudget)
|
|
707
|
+
: list);
|
|
556
708
|
}
|
|
557
709
|
catch (error) {
|
|
558
710
|
if (error instanceof provisioning_1.ProvisioningError) {
|
|
@@ -656,7 +808,10 @@ async function createRouter(options) {
|
|
|
656
808
|
teamCreateInFlight.set(key, createPromise);
|
|
657
809
|
try {
|
|
658
810
|
const result = await createPromise;
|
|
659
|
-
|
|
811
|
+
// Manager surface: redact dollars when hidden even from managers.
|
|
812
|
+
res.json(teamBudgetVisibility.hideTeamBudgetForManagers
|
|
813
|
+
? (0, teamBudgetVisibility_1.redactTeamBudget)(result)
|
|
814
|
+
: result);
|
|
660
815
|
}
|
|
661
816
|
catch (err) {
|
|
662
817
|
sendTeamError(err, res);
|
|
@@ -704,7 +859,9 @@ async function createRouter(options) {
|
|
|
704
859
|
teamId,
|
|
705
860
|
owningGroup,
|
|
706
861
|
});
|
|
707
|
-
res.json(
|
|
862
|
+
res.json(teamBudgetVisibility.hideTeamBudgetForManagers
|
|
863
|
+
? (0, teamBudgetVisibility_1.redactTeamBudget)(r)
|
|
864
|
+
: r);
|
|
708
865
|
}
|
|
709
866
|
catch (err) {
|
|
710
867
|
sendTeamError(err, res);
|
|
@@ -772,7 +929,10 @@ async function createRouter(options) {
|
|
|
772
929
|
// the caller's group set and filter with includes().
|
|
773
930
|
const owned = all.filter(t => typeof t.metadata?.owning_group === 'string' &&
|
|
774
931
|
t.metadata.owning_group === teamAdminCfg.group);
|
|
775
|
-
|
|
932
|
+
// Manager surface: strip dollar amounts when hidden even from managers.
|
|
933
|
+
res.json(teamBudgetVisibility.hideTeamBudgetForManagers
|
|
934
|
+
? owned.map(teamBudgetVisibility_1.redactTeamBudget)
|
|
935
|
+
: owned);
|
|
776
936
|
}
|
|
777
937
|
catch (err) {
|
|
778
938
|
sendTeamError(err, res);
|
|
@@ -924,7 +1084,9 @@ async function createRouter(options) {
|
|
|
924
1084
|
owningGroup,
|
|
925
1085
|
});
|
|
926
1086
|
const updated = await withTeamFetchRetry(() => client.getTeamInfo(teamId));
|
|
927
|
-
res.json(
|
|
1087
|
+
res.json(teamBudgetVisibility.hideTeamBudgetForManagers
|
|
1088
|
+
? (0, teamBudgetVisibility_1.redactTeamBudget)(updated)
|
|
1089
|
+
: updated);
|
|
928
1090
|
}
|
|
929
1091
|
catch (err) {
|
|
930
1092
|
sendTeamError(err, res);
|
|
@@ -959,7 +1121,9 @@ async function createRouter(options) {
|
|
|
959
1121
|
owningGroup,
|
|
960
1122
|
});
|
|
961
1123
|
const updated = await withTeamFetchRetry(() => client.getTeamInfo(teamId));
|
|
962
|
-
res.json(
|
|
1124
|
+
res.json(teamBudgetVisibility.hideTeamBudgetForManagers
|
|
1125
|
+
? (0, teamBudgetVisibility_1.redactTeamBudget)(updated)
|
|
1126
|
+
: updated);
|
|
963
1127
|
}
|
|
964
1128
|
catch (err) {
|
|
965
1129
|
sendTeamError(err, res);
|
|
@@ -1047,7 +1211,9 @@ async function createRouter(options) {
|
|
|
1047
1211
|
owningGroup,
|
|
1048
1212
|
vector_stores: requested,
|
|
1049
1213
|
});
|
|
1050
|
-
res.json(
|
|
1214
|
+
res.json(teamBudgetVisibility.hideTeamBudgetForManagers
|
|
1215
|
+
? (0, teamBudgetVisibility_1.redactTeamBudget)(updated)
|
|
1216
|
+
: updated);
|
|
1051
1217
|
}
|
|
1052
1218
|
catch (err) {
|
|
1053
1219
|
sendTeamError(err, res);
|
|
@@ -1136,7 +1302,9 @@ async function createRouter(options) {
|
|
|
1136
1302
|
before,
|
|
1137
1303
|
after: requested,
|
|
1138
1304
|
});
|
|
1139
|
-
res.json(
|
|
1305
|
+
res.json(teamBudgetVisibility.hideTeamBudgetForManagers
|
|
1306
|
+
? (0, teamBudgetVisibility_1.redactTeamBudget)(updated)
|
|
1307
|
+
: updated);
|
|
1140
1308
|
}
|
|
1141
1309
|
catch (err) {
|
|
1142
1310
|
sendTeamError(err, res);
|
|
@@ -1151,7 +1319,26 @@ async function createRouter(options) {
|
|
|
1151
1319
|
return;
|
|
1152
1320
|
}
|
|
1153
1321
|
const usage = await client.getTeamUsage(teamId, start_date, end_date);
|
|
1154
|
-
|
|
1322
|
+
// Spend figures would let a client derive a hidden budget
|
|
1323
|
+
// (budget = spend / pct), so they follow the same hiding flags.
|
|
1324
|
+
// Which flag applies depends on whether the caller is a team manager;
|
|
1325
|
+
// catalog failures fail closed to the member rule.
|
|
1326
|
+
let hide = teamBudgetVisibility.hideTeamBudgetForMembers;
|
|
1327
|
+
if (teamAdminCfg.group) {
|
|
1328
|
+
try {
|
|
1329
|
+
const tokenEntityRef = await (0, provisioning_1.resolveUserId)(req, auth);
|
|
1330
|
+
const isManager = !!tokenEntityRef &&
|
|
1331
|
+
(await (0, provisioning_1.isUserMemberOfGroup)(tokenEntityRef, teamAdminCfg.group, catalogClient, auth, logger));
|
|
1332
|
+
hide = isManager
|
|
1333
|
+
? teamBudgetVisibility.hideTeamBudgetForManagers
|
|
1334
|
+
: teamBudgetVisibility.hideTeamBudgetForMembers;
|
|
1335
|
+
}
|
|
1336
|
+
catch (err) {
|
|
1337
|
+
logger.warn(`Team-manager check failed, hiding team spend: ${err.message}`);
|
|
1338
|
+
hide = teamBudgetVisibility.hideTeamBudgetForMembers;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
res.json(hide ? (0, teamBudgetVisibility_1.redactTeamUsage)(usage) : usage);
|
|
1155
1342
|
}
|
|
1156
1343
|
catch (error) {
|
|
1157
1344
|
logger.error('Failed to fetch team usage', error);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Config } from '@backstage/config';
|
|
2
|
+
import { TeamInfo, UsageMetrics } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Independent "hide team budget" switches for the two team audiences.
|
|
5
|
+
*
|
|
6
|
+
* - `hideTeamBudgetForMembers` redacts dollar amounts on the member surface:
|
|
7
|
+
* `GET /teams`, the Teams cards, and the `TEAM` section of the budget widget.
|
|
8
|
+
* Members still see the consumption level (percent of cap + ok/near/over
|
|
9
|
+
* status + reset window).
|
|
10
|
+
* - `hideTeamBudgetForManagers` does the same on the manager surface:
|
|
11
|
+
* `GET /teams/managed`, team write responses, and the ManageTeamDialog
|
|
12
|
+
* budget field (which becomes write-only).
|
|
13
|
+
*
|
|
14
|
+
* Both default to false (current behavior: dollars visible). Splitting them
|
|
15
|
+
* lets an operator hide real budgets from regular members while team managers
|
|
16
|
+
* keep the numbers they need to govern — or hide from both.
|
|
17
|
+
*/
|
|
18
|
+
export interface TeamBudgetVisibility {
|
|
19
|
+
hideTeamBudgetForMembers: boolean;
|
|
20
|
+
hideTeamBudgetForManagers: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Consumption status derived from spend vs. cap. Mirrors the frontend
|
|
24
|
+
* `budgetTone` thresholds (80% near, 100% over) so both sides agree.
|
|
25
|
+
*/
|
|
26
|
+
export type TeamBudgetStatus = 'ok' | 'near' | 'over';
|
|
27
|
+
export declare function teamBudgetStatusFor(spend: number, budget: number): TeamBudgetStatus;
|
|
28
|
+
export declare function readTeamBudgetVisibility(config: Config): TeamBudgetVisibility;
|
|
29
|
+
/**
|
|
30
|
+
* Strips dollar amounts from a team record while preserving the consumption
|
|
31
|
+
* signal. `max_budget` is dropped, `spend` is zeroed (the field is required
|
|
32
|
+
* downstream, so it cannot be omitted — `budget_hidden: true` marks the
|
|
33
|
+
* record as redacted), and `budget_pct` + `budget_status` carry the level of
|
|
34
|
+
* consumption. `budget_duration`, models, members, and rate limits are kept:
|
|
35
|
+
* none of them reveal dollars.
|
|
36
|
+
*
|
|
37
|
+
* Teams without a positive budget (unlimited) carry no dollars to hide and
|
|
38
|
+
* are returned untouched.
|
|
39
|
+
*/
|
|
40
|
+
export declare function redactTeamBudget(team: TeamInfo): TeamInfo;
|
|
41
|
+
/**
|
|
42
|
+
* Strips dollar amounts from team usage metrics while keeping the
|
|
43
|
+
* non-monetary signal (tokens, requests). Returning zeroed spend (rather
|
|
44
|
+
* than omitting the fields) keeps the response shape stable for existing
|
|
45
|
+
* clients; the frontend hides spend charts whenever the owning team record
|
|
46
|
+
* is redacted.
|
|
47
|
+
*/
|
|
48
|
+
export declare function redactTeamUsage(usage: UsageMetrics): UsageMetrics;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.teamBudgetStatusFor = teamBudgetStatusFor;
|
|
4
|
+
exports.readTeamBudgetVisibility = readTeamBudgetVisibility;
|
|
5
|
+
exports.redactTeamBudget = redactTeamBudget;
|
|
6
|
+
exports.redactTeamUsage = redactTeamUsage;
|
|
7
|
+
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';
|
|
13
|
+
}
|
|
14
|
+
function readTeamBudgetVisibility(config) {
|
|
15
|
+
return {
|
|
16
|
+
hideTeamBudgetForMembers: config.getOptionalBoolean('litellm.display.hideTeamBudgetForMembers') ?? false,
|
|
17
|
+
hideTeamBudgetForManagers: config.getOptionalBoolean('litellm.display.hideTeamBudgetForManagers') ?? false,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Strips dollar amounts from a team record while preserving the consumption
|
|
22
|
+
* signal. `max_budget` is dropped, `spend` is zeroed (the field is required
|
|
23
|
+
* downstream, so it cannot be omitted — `budget_hidden: true` marks the
|
|
24
|
+
* record as redacted), and `budget_pct` + `budget_status` carry the level of
|
|
25
|
+
* consumption. `budget_duration`, models, members, and rate limits are kept:
|
|
26
|
+
* none of them reveal dollars.
|
|
27
|
+
*
|
|
28
|
+
* Teams without a positive budget (unlimited) carry no dollars to hide and
|
|
29
|
+
* are returned untouched.
|
|
30
|
+
*/
|
|
31
|
+
function redactTeamBudget(team) {
|
|
32
|
+
const budget = team.max_budget ?? 0;
|
|
33
|
+
if (budget <= 0)
|
|
34
|
+
return team;
|
|
35
|
+
const spend = team.spend ?? 0;
|
|
36
|
+
const pct = Math.min(100, Math.max(0, (spend / budget) * 100));
|
|
37
|
+
const { max_budget: _dropped, ...rest } = team;
|
|
38
|
+
return {
|
|
39
|
+
...rest,
|
|
40
|
+
spend: 0,
|
|
41
|
+
budget_pct: pct,
|
|
42
|
+
budget_status: teamBudgetStatusFor(spend, budget),
|
|
43
|
+
budget_hidden: true,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Strips dollar amounts from team usage metrics while keeping the
|
|
48
|
+
* non-monetary signal (tokens, requests). Returning zeroed spend (rather
|
|
49
|
+
* than omitting the fields) keeps the response shape stable for existing
|
|
50
|
+
* clients; the frontend hides spend charts whenever the owning team record
|
|
51
|
+
* is redacted.
|
|
52
|
+
*/
|
|
53
|
+
function redactTeamUsage(usage) {
|
|
54
|
+
const zeroSpend = (p) => ({
|
|
55
|
+
...p,
|
|
56
|
+
spend: 0,
|
|
57
|
+
});
|
|
58
|
+
const zeroTotalSpend = (p) => ({
|
|
59
|
+
...p,
|
|
60
|
+
total_spend: 0,
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
...usage,
|
|
64
|
+
total_spend: 0,
|
|
65
|
+
usage_by_model: Object.fromEntries(Object.entries(usage.usage_by_model ?? {}).map(([k, v]) => [
|
|
66
|
+
k,
|
|
67
|
+
zeroTotalSpend(v),
|
|
68
|
+
])),
|
|
69
|
+
usage_by_key: Object.fromEntries(Object.entries(usage.usage_by_key ?? {}).map(([k, v]) => [
|
|
70
|
+
k,
|
|
71
|
+
zeroTotalSpend(v),
|
|
72
|
+
])),
|
|
73
|
+
daily_usage: (usage.daily_usage ?? []).map(zeroSpend),
|
|
74
|
+
daily_by_model: (usage.daily_by_model ?? []).map(zeroSpend),
|
|
75
|
+
};
|
|
76
|
+
}
|
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": ["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 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
|
}
|