@acarmisc/backstage-plugin-litellm-backend 0.18.0 → 0.19.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/README.md +58 -0
- package/dist/bridge.d.ts +4 -6
- package/dist/bridge.js +10 -28
- package/dist/client.d.ts +7 -20
- package/dist/client.js +29 -107
- package/dist/errors.d.ts +2 -1
- package/dist/errors.js +4 -1
- package/dist/index.cjs.js +204 -405
- package/dist/index.cjs.js.map +4 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -3
- package/dist/provisioning.d.ts +2 -0
- package/dist/provisioning.js +11 -20
- package/dist/router.js +0 -16
- package/dist/routes/bridge.js +10 -19
- package/dist/routes/context.d.ts +0 -1
- package/dist/routes/keys.js +8 -21
- package/dist/routes/middleware/withUser.d.ts +3 -2
- package/dist/routes/middleware/withUser.js +1 -1
- package/dist/routes/objectPermissions.js +4 -8
- package/dist/routes/teamUsage.js +2 -3
- package/dist/routes/teams.js +7 -20
- package/dist/services/keyService.d.ts +10 -13
- package/dist/services/keyService.js +12 -47
- package/dist/types.cjs.js.map +1 -1
- package/dist/types.d.ts +0 -36
- package/package.json +3 -7
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# @acarmisc/backstage-plugin-litellm-backend
|
|
2
|
+
|
|
3
|
+
Backend for the **Backstage LiteLLM Governance Plugin** (New Backend System).
|
|
4
|
+
It is the only component that talks to your [LiteLLM](https://github.com/BerriAI/litellm)
|
|
5
|
+
proxy: it holds the master key, resolves the caller's Backstage identity, and
|
|
6
|
+
enforces budgets, key limits, team membership, model access and permissions
|
|
7
|
+
before calling LiteLLM.
|
|
8
|
+
|
|
9
|
+
Pair it with the frontend, [`@acarmisc/backstage-plugin-litellm`](https://www.npmjs.com/package/@acarmisc/backstage-plugin-litellm).
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
Full documentation — adoption guide, configuration reference, permissions,
|
|
14
|
+
security model and architecture — starts at the
|
|
15
|
+
[project README](https://github.com/acarmisc/backstage-plugin-litellm-govai#readme).
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
yarn --cwd packages/backend add @acarmisc/backstage-plugin-litellm-backend
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
In `packages/backend/src/index.ts`:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
backend.add(import('@acarmisc/backstage-plugin-litellm-backend'));
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Minimal configuration
|
|
30
|
+
|
|
31
|
+
```yaml
|
|
32
|
+
litellm:
|
|
33
|
+
baseUrl: http://litellm-proxy:4000 # internal URL of the LiteLLM proxy
|
|
34
|
+
publicBaseUrl: https://llm.example.com # optional: shown to users in snippets
|
|
35
|
+
masterKey: ${LITELLM_MASTER_KEY}
|
|
36
|
+
userIdDomain: example.com # optional: user:default/jane → jane@example.com
|
|
37
|
+
provisioning:
|
|
38
|
+
enabled: true # optional: create LiteLLM users on first visit
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The configuration schema ships with the package (`config.d.ts`); see the
|
|
42
|
+
[configuration reference](https://github.com/acarmisc/backstage-plugin-litellm-govai/blob/main/docs/configuration.md)
|
|
43
|
+
for every key (key ceilings, team management, budget hiding, CLI bridge,
|
|
44
|
+
OpenCode connect).
|
|
45
|
+
|
|
46
|
+
## What it exposes
|
|
47
|
+
|
|
48
|
+
Routes are mounted under `/api/litellm`. A machine-readable OpenAPI 3.1
|
|
49
|
+
description is served at `/api/litellm/openapi.json`; the
|
|
50
|
+
[endpoint table](https://github.com/acarmisc/backstage-plugin-litellm-govai/blob/main/docs/api.md)
|
|
51
|
+
summarises them.
|
|
52
|
+
|
|
53
|
+
The plugin registers the `litellm.*` permissions with the permission
|
|
54
|
+
framework (see [Permissions](https://github.com/acarmisc/backstage-plugin-litellm-govai/blob/main/docs/permissions.md)).
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
Apache-2.0
|
package/dist/bridge.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Config } from '@backstage/config';
|
|
2
2
|
import { LoggerService } from '@backstage/backend-plugin-api';
|
|
3
3
|
import { LiteLLMClient } from './client';
|
|
4
|
-
import {
|
|
4
|
+
import { ProvisioningDefaults, UserInfo, VirtualKey } from './types';
|
|
5
5
|
/** Claims extracted from a verified Keycloak access token. */
|
|
6
6
|
export interface BridgeClaims {
|
|
7
7
|
sub: string;
|
|
@@ -94,7 +94,7 @@ export interface BridgeIdentityOptions {
|
|
|
94
94
|
*
|
|
95
95
|
* @throws BridgeIdentityError (403) if identity validation fails
|
|
96
96
|
*/
|
|
97
|
-
export declare function resolveBridgeUserId(claims: BridgeClaims, identity?:
|
|
97
|
+
export declare function resolveBridgeUserId(claims: BridgeClaims, identity?: BridgeIdentityOptions): string;
|
|
98
98
|
/**
|
|
99
99
|
* Ensures a LiteLLM user exists for the verified identity. If the user is
|
|
100
100
|
* missing and provisioning is enabled, creates it from the JWT claims (email +
|
|
@@ -107,8 +107,6 @@ export declare function resolveBridgeUserId(claims: BridgeClaims, identity?: str
|
|
|
107
107
|
* preferred_username), and helps recover from cases where the bridge's userId
|
|
108
108
|
* computation and the UI's differ.
|
|
109
109
|
*/
|
|
110
|
-
export declare function getOrProvisionUserFromClaims(client: LiteLLMClient, claims: BridgeClaims, provisioningEnabled: boolean, provisioningDefaults: ProvisioningDefaults, logger: LoggerService,
|
|
110
|
+
export declare function getOrProvisionUserFromClaims(client: LiteLLMClient, claims: BridgeClaims, provisioningEnabled: boolean, provisioningDefaults: ProvisioningDefaults, logger: LoggerService, identity?: BridgeIdentityOptions): Promise<UserInfo>;
|
|
111
111
|
/** Lists the caller's virtual keys (provisioning the user first if needed). */
|
|
112
|
-
export declare function bridgeListKeys(client: LiteLLMClient, claims: BridgeClaims, provisioningEnabled: boolean, provisioningDefaults: ProvisioningDefaults, logger: LoggerService,
|
|
113
|
-
/** Mints a new virtual key for the caller (provisioning the user first if needed). */
|
|
114
|
-
export declare function bridgeGenerateKey(client: LiteLLMClient, claims: BridgeClaims, provisioningEnabled: boolean, provisioningDefaults: ProvisioningDefaults, logger: LoggerService, request: Partial<GenerateKeyRequest>, userIdDomain?: string | BridgeIdentityOptions): Promise<GenerateKeyResponse>;
|
|
112
|
+
export declare function bridgeListKeys(client: LiteLLMClient, claims: BridgeClaims, provisioningEnabled: boolean, provisioningDefaults: ProvisioningDefaults, logger: LoggerService, identity?: BridgeIdentityOptions): Promise<VirtualKey[]>;
|
package/dist/bridge.js
CHANGED
|
@@ -6,7 +6,6 @@ exports.newDefaultVerifier = newDefaultVerifier;
|
|
|
6
6
|
exports.resolveBridgeUserId = resolveBridgeUserId;
|
|
7
7
|
exports.getOrProvisionUserFromClaims = getOrProvisionUserFromClaims;
|
|
8
8
|
exports.bridgeListKeys = bridgeListKeys;
|
|
9
|
-
exports.bridgeGenerateKey = bridgeGenerateKey;
|
|
10
9
|
/**
|
|
11
10
|
* Bridge — lets CLI clients (Abby) list/mint LiteLLM virtual keys without ever
|
|
12
11
|
* holding the LiteLLM master key.
|
|
@@ -111,18 +110,16 @@ function newDefaultVerifier(cfg) {
|
|
|
111
110
|
}
|
|
112
111
|
return new KeycloakJWTVerifier({ issuer: cfg.issuer, clientId: cfg.clientId });
|
|
113
112
|
}
|
|
114
|
-
/** A bare string is the legacy form: `userIdDomain`, also the only trusted domain. */
|
|
115
113
|
function normalizeIdentityOptions(opts) {
|
|
116
|
-
const o = typeof opts === 'string' ? { userIdDomain: opts } : opts ?? {};
|
|
117
114
|
let domains = [];
|
|
118
|
-
if (
|
|
119
|
-
domains =
|
|
115
|
+
if (opts.trustedEmailDomains?.length) {
|
|
116
|
+
domains = opts.trustedEmailDomains;
|
|
120
117
|
}
|
|
121
|
-
else if (
|
|
122
|
-
domains = [
|
|
118
|
+
else if (opts.userIdDomain) {
|
|
119
|
+
domains = [opts.userIdDomain];
|
|
123
120
|
}
|
|
124
121
|
const trusted = domains.map(d => d.toLowerCase());
|
|
125
|
-
return { userIdDomain:
|
|
122
|
+
return { userIdDomain: opts.userIdDomain, trusted };
|
|
126
123
|
}
|
|
127
124
|
/**
|
|
128
125
|
* Resolves the LiteLLM user_id for a verified bridge token.
|
|
@@ -147,7 +144,7 @@ function normalizeIdentityOptions(opts) {
|
|
|
147
144
|
* @throws BridgeIdentityError (403) if identity validation fails
|
|
148
145
|
*/
|
|
149
146
|
function resolveBridgeUserId(claims, identity) {
|
|
150
|
-
const { userIdDomain, trusted } = normalizeIdentityOptions(identity);
|
|
147
|
+
const { userIdDomain, trusted } = normalizeIdentityOptions(identity ?? {});
|
|
151
148
|
if (trusted.length === 0) {
|
|
152
149
|
throw new BridgeIdentityError('set litellm.bridge.allowedEmailDomains (or litellm.userIdDomain) to use the CLI bridge');
|
|
153
150
|
}
|
|
@@ -193,8 +190,8 @@ function resolveBridgeUserId(claims, identity) {
|
|
|
193
190
|
* preferred_username), and helps recover from cases where the bridge's userId
|
|
194
191
|
* computation and the UI's differ.
|
|
195
192
|
*/
|
|
196
|
-
async function getOrProvisionUserFromClaims(client, claims, provisioningEnabled, provisioningDefaults, logger,
|
|
197
|
-
const userId = resolveBridgeUserId(claims,
|
|
193
|
+
async function getOrProvisionUserFromClaims(client, claims, provisioningEnabled, provisioningDefaults, logger, identity) {
|
|
194
|
+
const userId = resolveBridgeUserId(claims, identity);
|
|
198
195
|
const existing = await client.getUserInfo(userId);
|
|
199
196
|
if (existing)
|
|
200
197
|
return existing;
|
|
@@ -222,22 +219,7 @@ async function getOrProvisionUserFromClaims(client, claims, provisioningEnabled,
|
|
|
222
219
|
return created;
|
|
223
220
|
}
|
|
224
221
|
/** Lists the caller's virtual keys (provisioning the user first if needed). */
|
|
225
|
-
async function bridgeListKeys(client, claims, provisioningEnabled, provisioningDefaults, logger,
|
|
226
|
-
const user = await getOrProvisionUserFromClaims(client, claims, provisioningEnabled, provisioningDefaults, logger,
|
|
222
|
+
async function bridgeListKeys(client, claims, provisioningEnabled, provisioningDefaults, logger, identity) {
|
|
223
|
+
const user = await getOrProvisionUserFromClaims(client, claims, provisioningEnabled, provisioningDefaults, logger, identity);
|
|
227
224
|
return client.listKeys(user.user_id);
|
|
228
225
|
}
|
|
229
|
-
/** Mints a new virtual key for the caller (provisioning the user first if needed). */
|
|
230
|
-
async function bridgeGenerateKey(client, claims, provisioningEnabled, provisioningDefaults, logger, request, userIdDomain) {
|
|
231
|
-
const user = await getOrProvisionUserFromClaims(client, claims, provisioningEnabled, provisioningDefaults, logger, userIdDomain);
|
|
232
|
-
const enriched = {
|
|
233
|
-
...request,
|
|
234
|
-
user_id: user.user_id,
|
|
235
|
-
metadata: {
|
|
236
|
-
...(request.metadata ?? {}),
|
|
237
|
-
created_via: 'abby-cli',
|
|
238
|
-
created_by: user.user_id,
|
|
239
|
-
created_at_iso: new Date().toISOString(),
|
|
240
|
-
},
|
|
241
|
-
};
|
|
242
|
-
return client.generateKey(enriched);
|
|
243
|
-
}
|
package/dist/client.d.ts
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import { LiteLLMConfig, UserInfo, VirtualKey, ModelInfo, UsageMetrics, TeamInfo, GenerateKeyRequest, GenerateKeyResponse, UpdateKeyRequest, DeleteKeyRequest, CreateUserRequest, CreateUserResponse, CreateTeamRequest, CreateTeamResponse, UpdateTeamRequest, AuditLogsParams, PaginatedAuditLogs
|
|
2
|
-
/**
|
|
3
|
-
* LiteLLM versions differ on `request_tags`: an array of `k:v` strings in
|
|
4
|
-
* current builds, but historically an object map. Normalise both to a flat
|
|
5
|
-
* string array so callers can `includes('session:...')` uniformly.
|
|
6
|
-
*/
|
|
7
|
-
export declare function normalizeRequestTags(tags: SpendLogEntry['request_tags']): string[];
|
|
1
|
+
import { LiteLLMConfig, UserInfo, VirtualKey, ModelInfo, UsageMetrics, TeamInfo, GenerateKeyRequest, GenerateKeyResponse, UpdateKeyRequest, DeleteKeyRequest, CreateUserRequest, CreateUserResponse, CreateTeamRequest, CreateTeamResponse, UpdateTeamRequest, AuditLogsParams, PaginatedAuditLogs } from './types';
|
|
8
2
|
/**
|
|
9
3
|
* Typed error for failed upstream LiteLLM responses. Preserves the HTTP
|
|
10
4
|
* status and the structured `param` (e.g. `key_alias`) from the upstream
|
|
@@ -23,6 +17,12 @@ export declare class LiteLLMClient {
|
|
|
23
17
|
private timeout;
|
|
24
18
|
constructor(config: LiteLLMConfig, timeout?: number);
|
|
25
19
|
private request;
|
|
20
|
+
/**
|
|
21
|
+
* Normalizes a raw LiteLLM user response to the UserInfo contract.
|
|
22
|
+
* Handles both `/user/info` (with `user_info` wrapper) and `/user/list`
|
|
23
|
+
* response shapes, and converts team arrays to team_id strings.
|
|
24
|
+
*/
|
|
25
|
+
private normalizeUserInfo;
|
|
26
26
|
/**
|
|
27
27
|
* Returns null when the user is not found in LiteLLM (404).
|
|
28
28
|
* Throws on all other errors so callers know something went wrong.
|
|
@@ -126,8 +126,6 @@ export declare class LiteLLMClient {
|
|
|
126
126
|
createTeam(payload: CreateTeamRequest): Promise<CreateTeamResponse>;
|
|
127
127
|
updateTeam(payload: UpdateTeamRequest): Promise<unknown>;
|
|
128
128
|
deleteTeam(teamId: string): Promise<unknown>;
|
|
129
|
-
blockTeam(teamId: string): Promise<unknown>;
|
|
130
|
-
unblockTeam(teamId: string): Promise<unknown>;
|
|
131
129
|
/**
|
|
132
130
|
* Adds a member to a team. LiteLLM's `/team/member_add` nests the member
|
|
133
131
|
* under a `member` object; `role` is 'user' or 'admin' (we only ever send
|
|
@@ -166,17 +164,6 @@ export declare class LiteLLMClient {
|
|
|
166
164
|
name?: string;
|
|
167
165
|
url?: string;
|
|
168
166
|
}>>;
|
|
169
|
-
/**
|
|
170
|
-
* Reads per-request spend logs from LiteLLM (`GET /spend/logs`), filtered
|
|
171
|
-
* by date range and optionally by key/user/team. Rows are normalised so
|
|
172
|
-
* `request_tags` is always a `string[]` (LiteLLM returns either an array
|
|
173
|
-
* or an object depending on version) — callers group by tag to attribute
|
|
174
|
-
* spend to a conversation or caller.
|
|
175
|
-
*
|
|
176
|
-
* This is the admin/DB-backed endpoint: it can return a large number of
|
|
177
|
-
* rows, so callers should keep the date window tight and cap `page_size`.
|
|
178
|
-
*/
|
|
179
|
-
getSpendLogs(params: SpendLogsParams): Promise<SpendLogEntry[]>;
|
|
180
167
|
private emptyUsage;
|
|
181
168
|
/**
|
|
182
169
|
* Transforms LiteLLM's SpendAnalyticsPaginatedResponse into the flatter
|
package/dist/client.js
CHANGED
|
@@ -1,20 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.LiteLLMClient = exports.LiteLLMUpstreamError = void 0;
|
|
4
|
-
exports.normalizeRequestTags = normalizeRequestTags;
|
|
5
4
|
const DEFAULT_TIMEOUT = 30000;
|
|
6
|
-
/**
|
|
7
|
-
* LiteLLM versions differ on `request_tags`: an array of `k:v` strings in
|
|
8
|
-
* current builds, but historically an object map. Normalise both to a flat
|
|
9
|
-
* string array so callers can `includes('session:...')` uniformly.
|
|
10
|
-
*/
|
|
11
|
-
function normalizeRequestTags(tags) {
|
|
12
|
-
if (!tags)
|
|
13
|
-
return [];
|
|
14
|
-
if (Array.isArray(tags))
|
|
15
|
-
return tags.filter((t) => typeof t === 'string');
|
|
16
|
-
return Object.entries(tags).map(([k, v]) => `${k}:${v}`);
|
|
17
|
-
}
|
|
18
5
|
/**
|
|
19
6
|
* Typed error for failed upstream LiteLLM responses. Preserves the HTTP
|
|
20
7
|
* status and the structured `param` (e.g. `key_alias`) from the upstream
|
|
@@ -87,6 +74,33 @@ class LiteLLMClient {
|
|
|
87
74
|
clearTimeout(timeoutId);
|
|
88
75
|
}
|
|
89
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Normalizes a raw LiteLLM user response to the UserInfo contract.
|
|
79
|
+
* Handles both `/user/info` (with `user_info` wrapper) and `/user/list`
|
|
80
|
+
* response shapes, and converts team arrays to team_id strings.
|
|
81
|
+
*/
|
|
82
|
+
normalizeUserInfo(raw, fallbackUserId) {
|
|
83
|
+
const inner = raw?.user_info ?? {};
|
|
84
|
+
const teamIds = Array.isArray(raw?.teams)
|
|
85
|
+
? raw.teams
|
|
86
|
+
.map((t) => (typeof t === 'string' ? t : t?.team_id))
|
|
87
|
+
.filter((t) => typeof t === 'string')
|
|
88
|
+
: [];
|
|
89
|
+
return {
|
|
90
|
+
user_id: raw?.user_id ?? inner.user_id ?? fallbackUserId ?? '',
|
|
91
|
+
user_email: inner.user_email ?? raw?.user_email,
|
|
92
|
+
email: inner.email ?? raw?.email,
|
|
93
|
+
teams: teamIds,
|
|
94
|
+
models: inner.models ?? raw?.models,
|
|
95
|
+
max_budget: inner.max_budget ?? raw?.max_budget,
|
|
96
|
+
budget_duration: inner.budget_duration ?? raw?.budget_duration,
|
|
97
|
+
budget_reset_at: inner.budget_reset_at ?? raw?.budget_reset_at,
|
|
98
|
+
spend: inner.spend ?? raw?.spend,
|
|
99
|
+
current_spend: inner.current_spend ?? raw?.current_spend,
|
|
100
|
+
soft_limit: inner.soft_limit ?? raw?.soft_limit,
|
|
101
|
+
hard_limit: inner.hard_limit ?? raw?.hard_limit,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
90
104
|
/**
|
|
91
105
|
* Returns null when the user is not found in LiteLLM (404).
|
|
92
106
|
* Throws on all other errors so callers know something went wrong.
|
|
@@ -100,26 +114,7 @@ class LiteLLMClient {
|
|
|
100
114
|
const query = userId ? `?user_id=${encodeURIComponent(userId)}` : '';
|
|
101
115
|
try {
|
|
102
116
|
const raw = await this.request(`/user/info${query}`);
|
|
103
|
-
|
|
104
|
-
const teamIds = Array.isArray(raw?.teams)
|
|
105
|
-
? raw.teams
|
|
106
|
-
.map((t) => (typeof t === 'string' ? t : t?.team_id))
|
|
107
|
-
.filter((t) => typeof t === 'string')
|
|
108
|
-
: [];
|
|
109
|
-
return {
|
|
110
|
-
user_id: raw?.user_id ?? inner.user_id ?? userId ?? '',
|
|
111
|
-
user_email: inner.user_email ?? raw?.user_email,
|
|
112
|
-
email: inner.email ?? raw?.email,
|
|
113
|
-
teams: teamIds,
|
|
114
|
-
models: inner.models ?? raw?.models,
|
|
115
|
-
max_budget: inner.max_budget ?? raw?.max_budget,
|
|
116
|
-
budget_duration: inner.budget_duration ?? raw?.budget_duration,
|
|
117
|
-
budget_reset_at: inner.budget_reset_at ?? raw?.budget_reset_at,
|
|
118
|
-
spend: inner.spend ?? raw?.spend,
|
|
119
|
-
current_spend: inner.current_spend ?? raw?.current_spend,
|
|
120
|
-
soft_limit: inner.soft_limit ?? raw?.soft_limit,
|
|
121
|
-
hard_limit: inner.hard_limit ?? raw?.hard_limit,
|
|
122
|
-
};
|
|
117
|
+
return this.normalizeUserInfo(raw, userId);
|
|
123
118
|
}
|
|
124
119
|
catch (err) {
|
|
125
120
|
if (err instanceof LiteLLMUpstreamError && err.status === 404)
|
|
@@ -149,27 +144,7 @@ class LiteLLMClient {
|
|
|
149
144
|
.toLowerCase() === email.toLowerCase());
|
|
150
145
|
if (!found)
|
|
151
146
|
return null;
|
|
152
|
-
|
|
153
|
-
const inner = found.user_info ?? {};
|
|
154
|
-
const teamIds = Array.isArray(found.teams)
|
|
155
|
-
? found.teams
|
|
156
|
-
.map((t) => (typeof t === 'string' ? t : t?.team_id))
|
|
157
|
-
.filter((t) => typeof t === 'string')
|
|
158
|
-
: [];
|
|
159
|
-
return {
|
|
160
|
-
user_id: found.user_id ?? inner.user_id ?? '',
|
|
161
|
-
user_email: inner.user_email ?? found.user_email,
|
|
162
|
-
email: inner.email ?? found.email,
|
|
163
|
-
teams: teamIds,
|
|
164
|
-
models: inner.models ?? found.models,
|
|
165
|
-
max_budget: inner.max_budget ?? found.max_budget,
|
|
166
|
-
budget_duration: inner.budget_duration ?? found.budget_duration,
|
|
167
|
-
budget_reset_at: inner.budget_reset_at ?? found.budget_reset_at,
|
|
168
|
-
spend: inner.spend ?? found.spend,
|
|
169
|
-
current_spend: inner.current_spend ?? found.current_spend,
|
|
170
|
-
soft_limit: inner.soft_limit ?? found.soft_limit,
|
|
171
|
-
hard_limit: inner.hard_limit ?? found.hard_limit,
|
|
172
|
-
};
|
|
147
|
+
return this.normalizeUserInfo(found);
|
|
173
148
|
}
|
|
174
149
|
catch (err) {
|
|
175
150
|
if (err instanceof LiteLLMUpstreamError && err.status === 404)
|
|
@@ -447,18 +422,6 @@ class LiteLLMClient {
|
|
|
447
422
|
body: JSON.stringify({ team_ids: [teamId] }),
|
|
448
423
|
});
|
|
449
424
|
}
|
|
450
|
-
async blockTeam(teamId) {
|
|
451
|
-
return this.request('/team/block', {
|
|
452
|
-
method: 'POST',
|
|
453
|
-
body: JSON.stringify({ team_id: teamId }),
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
async unblockTeam(teamId) {
|
|
457
|
-
return this.request('/team/unblock', {
|
|
458
|
-
method: 'POST',
|
|
459
|
-
body: JSON.stringify({ team_id: teamId }),
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
425
|
/**
|
|
463
426
|
* Adds a member to a team. LiteLLM's `/team/member_add` nests the member
|
|
464
427
|
* under a `member` object; `role` is 'user' or 'admin' (we only ever send
|
|
@@ -532,47 +495,6 @@ class LiteLLMClient {
|
|
|
532
495
|
}))
|
|
533
496
|
.filter(r => r.id);
|
|
534
497
|
}
|
|
535
|
-
/**
|
|
536
|
-
* Reads per-request spend logs from LiteLLM (`GET /spend/logs`), filtered
|
|
537
|
-
* by date range and optionally by key/user/team. Rows are normalised so
|
|
538
|
-
* `request_tags` is always a `string[]` (LiteLLM returns either an array
|
|
539
|
-
* or an object depending on version) — callers group by tag to attribute
|
|
540
|
-
* spend to a conversation or caller.
|
|
541
|
-
*
|
|
542
|
-
* This is the admin/DB-backed endpoint: it can return a large number of
|
|
543
|
-
* rows, so callers should keep the date window tight and cap `page_size`.
|
|
544
|
-
*/
|
|
545
|
-
async getSpendLogs(params) {
|
|
546
|
-
const query = new URLSearchParams({
|
|
547
|
-
start_date: params.start_date,
|
|
548
|
-
end_date: params.end_date,
|
|
549
|
-
});
|
|
550
|
-
if (params.api_key)
|
|
551
|
-
query.append('api_key', params.api_key);
|
|
552
|
-
if (params.user_id)
|
|
553
|
-
query.append('user_id', params.user_id);
|
|
554
|
-
if (params.team_id)
|
|
555
|
-
query.append('team_id', params.team_id);
|
|
556
|
-
if (params.page_size)
|
|
557
|
-
query.append('page_size', String(params.page_size));
|
|
558
|
-
const raw = await this.request(`/spend/logs?${query.toString()}`);
|
|
559
|
-
const rows = Array.isArray(raw) ? raw : raw?.data ?? [];
|
|
560
|
-
return rows.map(row => ({
|
|
561
|
-
request_id: row?.request_id,
|
|
562
|
-
startTime: row?.startTime ?? row?.start_time,
|
|
563
|
-
endTime: row?.endTime ?? row?.end_time,
|
|
564
|
-
spend: row?.spend ?? 0,
|
|
565
|
-
total_tokens: row?.total_tokens ?? 0,
|
|
566
|
-
prompt_tokens: row?.prompt_tokens ?? 0,
|
|
567
|
-
completion_tokens: row?.completion_tokens ?? 0,
|
|
568
|
-
model: row?.model,
|
|
569
|
-
api_key: row?.api_key,
|
|
570
|
-
user: row?.user,
|
|
571
|
-
team_id: row?.team_id,
|
|
572
|
-
request_tags: normalizeRequestTags(row?.request_tags),
|
|
573
|
-
metadata: row?.metadata,
|
|
574
|
-
}));
|
|
575
|
-
}
|
|
576
498
|
emptyUsage() {
|
|
577
499
|
return {
|
|
578
500
|
total_spend: 0,
|
package/dist/errors.d.ts
CHANGED
|
@@ -25,11 +25,12 @@ export declare function isHttpishError(error: unknown): error is {
|
|
|
25
25
|
export declare function errorMessage(error: unknown): string;
|
|
26
26
|
/**
|
|
27
27
|
* Sanitizes a message by:
|
|
28
|
+
* - Redacting Bearer tokens and sk- keys
|
|
28
29
|
* - Stripping HTML tags
|
|
29
30
|
* - Collapsing consecutive whitespace
|
|
30
31
|
* - Truncating to 500 characters
|
|
31
32
|
*
|
|
32
|
-
* @param msg The raw message (may contain HTML, newlines, etc.)
|
|
33
|
+
* @param msg The raw message (may contain HTML, newlines, secrets, etc.)
|
|
33
34
|
* @returns A safe, printable message
|
|
34
35
|
*/
|
|
35
36
|
export declare function sanitizeUpstreamMessage(msg: string): string;
|
package/dist/errors.js
CHANGED
|
@@ -49,17 +49,20 @@ function errorMessage(error) {
|
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
51
|
* Sanitizes a message by:
|
|
52
|
+
* - Redacting Bearer tokens and sk- keys
|
|
52
53
|
* - Stripping HTML tags
|
|
53
54
|
* - Collapsing consecutive whitespace
|
|
54
55
|
* - Truncating to 500 characters
|
|
55
56
|
*
|
|
56
|
-
* @param msg The raw message (may contain HTML, newlines, etc.)
|
|
57
|
+
* @param msg The raw message (may contain HTML, newlines, secrets, etc.)
|
|
57
58
|
* @returns A safe, printable message
|
|
58
59
|
*/
|
|
59
60
|
function sanitizeUpstreamMessage(msg) {
|
|
60
61
|
if (!msg || typeof msg !== 'string')
|
|
61
62
|
return '';
|
|
62
63
|
return msg
|
|
64
|
+
.replace(/Bearer\s+[A-Za-z0-9._\-+/=]+/g, 'Bearer [redacted]') // Redact Bearer tokens
|
|
65
|
+
.replace(/sk-[A-Za-z0-9_\-]{8,}/g, 'sk-[redacted]') // Redact sk-* keys
|
|
63
66
|
.replace(HTML_TAG_PATTERN, '') // Strip HTML tags and entities
|
|
64
67
|
.replace(/\s+/g, ' ') // Collapse whitespace
|
|
65
68
|
.trim()
|