@acarmisc/backstage-plugin-litellm-backend 0.21.0 → 0.23.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/client.d.ts +12 -1
- package/dist/client.js +91 -0
- package/dist/index.cjs.js +126 -5
- package/dist/index.cjs.js.map +3 -3
- package/dist/router.js +3 -1
- package/dist/routes/insights.d.ts +3 -0
- package/dist/routes/insights.js +43 -0
- package/dist/routes/opencode.js +12 -0
- package/dist/types.cjs.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/package.json +2 -2
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LiteLLMConfig, UserInfo, VirtualKey, ModelInfo, UsageMetrics, TeamInfo, GenerateKeyRequest, GenerateKeyResponse, UpdateKeyRequest, DeleteKeyRequest, CreateUserRequest, CreateUserResponse, CreateTeamRequest, CreateTeamResponse, UpdateTeamRequest, AuditLogsParams, PaginatedAuditLogs } from './types';
|
|
1
|
+
import { LiteLLMConfig, UserInfo, VirtualKey, ModelInfo, UsageMetrics, TeamInfo, GenerateKeyRequest, GenerateKeyResponse, UpdateKeyRequest, DeleteKeyRequest, CreateUserRequest, CreateUserResponse, CreateTeamRequest, CreateTeamResponse, UpdateTeamRequest, AuditLogsParams, PaginatedAuditLogs, SpendLogsParams, PaginatedSpendLogs } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* Typed error for failed upstream LiteLLM responses. Preserves the HTTP
|
|
4
4
|
* status and the structured `param` (e.g. `key_alias`) from the upstream
|
|
@@ -185,4 +185,15 @@ export declare class LiteLLMClient {
|
|
|
185
185
|
private transformDailyActivity;
|
|
186
186
|
getUsage(startDate: string, endDate: string, userId: string): Promise<UsageMetrics>;
|
|
187
187
|
getTeamUsage(teamId: string, startDate: string, endDate: string): Promise<UsageMetrics>;
|
|
188
|
+
/**
|
|
189
|
+
* Returns per-request spend rows from `/spend/logs` (retried as
|
|
190
|
+
* `/spend/logs/v2` on 404 — the path differs across LiteLLM versions).
|
|
191
|
+
* The response shape is version-dependent, so rows are parsed tolerantly:
|
|
192
|
+
* a bare array or a `{ data | results | logs }` envelope, with per-field
|
|
193
|
+
* fallbacks in normalizeSpendLogs.
|
|
194
|
+
*/
|
|
195
|
+
getSpendLogs(params: SpendLogsParams & {
|
|
196
|
+
apiKeyHash?: string;
|
|
197
|
+
}): Promise<PaginatedSpendLogs>;
|
|
198
|
+
private normalizeSpendLogs;
|
|
188
199
|
}
|
package/dist/client.js
CHANGED
|
@@ -683,5 +683,96 @@ class LiteLLMClient {
|
|
|
683
683
|
throw err;
|
|
684
684
|
}
|
|
685
685
|
}
|
|
686
|
+
/**
|
|
687
|
+
* Returns per-request spend rows from `/spend/logs` (retried as
|
|
688
|
+
* `/spend/logs/v2` on 404 — the path differs across LiteLLM versions).
|
|
689
|
+
* The response shape is version-dependent, so rows are parsed tolerantly:
|
|
690
|
+
* a bare array or a `{ data | results | logs }` envelope, with per-field
|
|
691
|
+
* fallbacks in normalizeSpendLogs.
|
|
692
|
+
*/
|
|
693
|
+
async getSpendLogs(params) {
|
|
694
|
+
const page = params.page ?? 1;
|
|
695
|
+
const pageSize = Math.min(params.pageSize ?? 25, 100);
|
|
696
|
+
const buildQuery = () => {
|
|
697
|
+
const q = new URLSearchParams({
|
|
698
|
+
summarize: 'false',
|
|
699
|
+
start_date: params.startDate,
|
|
700
|
+
end_date: params.endDate,
|
|
701
|
+
page: String(page),
|
|
702
|
+
page_size: String(pageSize),
|
|
703
|
+
});
|
|
704
|
+
if (params.userId)
|
|
705
|
+
q.set('user_id', params.userId);
|
|
706
|
+
if (params.teamId)
|
|
707
|
+
q.set('team_id', params.teamId);
|
|
708
|
+
if (params.apiKeyHash)
|
|
709
|
+
q.set('api_key', params.apiKeyHash);
|
|
710
|
+
return q.toString();
|
|
711
|
+
};
|
|
712
|
+
let raw;
|
|
713
|
+
try {
|
|
714
|
+
raw = await this.request(`/spend/logs?${buildQuery()}`);
|
|
715
|
+
}
|
|
716
|
+
catch (err) {
|
|
717
|
+
if (err instanceof LiteLLMUpstreamError && err.status === 404) {
|
|
718
|
+
raw = await this.request(`/spend/logs/v2?${buildQuery()}`);
|
|
719
|
+
}
|
|
720
|
+
else {
|
|
721
|
+
throw err;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return this.normalizeSpendLogs(raw, page, pageSize);
|
|
725
|
+
}
|
|
726
|
+
normalizeSpendLogs(raw, page, pageSize) {
|
|
727
|
+
const rows = Array.isArray(raw)
|
|
728
|
+
? raw
|
|
729
|
+
: Array.isArray(raw?.data)
|
|
730
|
+
? raw.data
|
|
731
|
+
: Array.isArray(raw?.results)
|
|
732
|
+
? raw.results
|
|
733
|
+
: Array.isArray(raw?.logs)
|
|
734
|
+
? raw.logs
|
|
735
|
+
: [];
|
|
736
|
+
const toNum = (v, fallback = 0) => {
|
|
737
|
+
const n = typeof v === 'number'
|
|
738
|
+
? v
|
|
739
|
+
: typeof v === 'string' && v.trim() !== ''
|
|
740
|
+
? Number(v)
|
|
741
|
+
: NaN;
|
|
742
|
+
return Number.isFinite(n) ? n : fallback;
|
|
743
|
+
};
|
|
744
|
+
const data = rows.map((r) => {
|
|
745
|
+
const promptTokens = toNum(r?.prompt_tokens ?? r?.promptTokens ?? r?.input_tokens);
|
|
746
|
+
const completionTokens = toNum(r?.completion_tokens ?? r?.completionTokens ?? r?.output_tokens);
|
|
747
|
+
return {
|
|
748
|
+
requestId: String(r?.request_id ?? r?.requestId ?? r?.id ?? ''),
|
|
749
|
+
model: String(r?.model ?? r?.model_name ?? ''),
|
|
750
|
+
spend: toNum(r?.spend ?? r?.cost),
|
|
751
|
+
promptTokens,
|
|
752
|
+
completionTokens,
|
|
753
|
+
totalTokens: toNum(r?.total_tokens ?? r?.totalTokens, promptTokens + completionTokens),
|
|
754
|
+
status: String(r?.status ?? r?.request_status ?? 'unknown'),
|
|
755
|
+
cacheHit: Boolean(r?.cache_hit ?? r?.cacheHit ?? r?.cached === true),
|
|
756
|
+
...(r?.user_id ?? r?.userId
|
|
757
|
+
? { userId: String(r.user_id ?? r.userId) }
|
|
758
|
+
: {}),
|
|
759
|
+
...(r?.team_id ?? r?.teamId
|
|
760
|
+
? { teamId: String(r.team_id ?? r.teamId) }
|
|
761
|
+
: {}),
|
|
762
|
+
...(r?.api_key ?? r?.keyHash
|
|
763
|
+
? { keyHash: String(r.api_key ?? r.keyHash) }
|
|
764
|
+
: {}),
|
|
765
|
+
timestamp: String(r?.timestamp ?? r?.startTime ?? r?.created_at ?? r?.date ?? ''),
|
|
766
|
+
};
|
|
767
|
+
});
|
|
768
|
+
// Upstream truncation is not signalled in-band, so truncated = full page.
|
|
769
|
+
return {
|
|
770
|
+
data,
|
|
771
|
+
total: toNum(raw?.total ?? raw?.total_count, data.length),
|
|
772
|
+
page,
|
|
773
|
+
pageSize,
|
|
774
|
+
truncated: data.length >= pageSize,
|
|
775
|
+
};
|
|
776
|
+
}
|
|
686
777
|
}
|
|
687
778
|
exports.LiteLLMClient = LiteLLMClient;
|
package/dist/index.cjs.js
CHANGED
|
@@ -8022,7 +8022,7 @@ var import_backend_plugin_api = require("@backstage/backend-plugin-api");
|
|
|
8022
8022
|
// src/router.ts
|
|
8023
8023
|
var import_express = __toESM(require("express"));
|
|
8024
8024
|
var import_plugin_permission_common2 = __toESM(require_index_cjs2());
|
|
8025
|
-
var
|
|
8025
|
+
var import_errors22 = __toESM(require_index_cjs());
|
|
8026
8026
|
var import_catalog_client = require("@backstage/catalog-client");
|
|
8027
8027
|
|
|
8028
8028
|
// src/client.ts
|
|
@@ -8655,6 +8655,82 @@ var LiteLLMClient = class {
|
|
|
8655
8655
|
throw err;
|
|
8656
8656
|
}
|
|
8657
8657
|
}
|
|
8658
|
+
/**
|
|
8659
|
+
* Returns per-request spend rows from `/spend/logs` (retried as
|
|
8660
|
+
* `/spend/logs/v2` on 404 — the path differs across LiteLLM versions).
|
|
8661
|
+
* The response shape is version-dependent, so rows are parsed tolerantly:
|
|
8662
|
+
* a bare array or a `{ data | results | logs }` envelope, with per-field
|
|
8663
|
+
* fallbacks in normalizeSpendLogs.
|
|
8664
|
+
*/
|
|
8665
|
+
async getSpendLogs(params) {
|
|
8666
|
+
const page = params.page ?? 1;
|
|
8667
|
+
const pageSize = Math.min(params.pageSize ?? 25, 100);
|
|
8668
|
+
const buildQuery = () => {
|
|
8669
|
+
const q = new URLSearchParams({
|
|
8670
|
+
summarize: "false",
|
|
8671
|
+
start_date: params.startDate,
|
|
8672
|
+
end_date: params.endDate,
|
|
8673
|
+
page: String(page),
|
|
8674
|
+
page_size: String(pageSize)
|
|
8675
|
+
});
|
|
8676
|
+
if (params.userId) q.set("user_id", params.userId);
|
|
8677
|
+
if (params.teamId) q.set("team_id", params.teamId);
|
|
8678
|
+
if (params.apiKeyHash) q.set("api_key", params.apiKeyHash);
|
|
8679
|
+
return q.toString();
|
|
8680
|
+
};
|
|
8681
|
+
let raw;
|
|
8682
|
+
try {
|
|
8683
|
+
raw = await this.request(`/spend/logs?${buildQuery()}`);
|
|
8684
|
+
} catch (err) {
|
|
8685
|
+
if (err instanceof LiteLLMUpstreamError && err.status === 404) {
|
|
8686
|
+
raw = await this.request(`/spend/logs/v2?${buildQuery()}`);
|
|
8687
|
+
} else {
|
|
8688
|
+
throw err;
|
|
8689
|
+
}
|
|
8690
|
+
}
|
|
8691
|
+
return this.normalizeSpendLogs(raw, page, pageSize);
|
|
8692
|
+
}
|
|
8693
|
+
normalizeSpendLogs(raw, page, pageSize) {
|
|
8694
|
+
const rows = Array.isArray(raw) ? raw : Array.isArray(raw?.data) ? raw.data : Array.isArray(raw?.results) ? raw.results : Array.isArray(raw?.logs) ? raw.logs : [];
|
|
8695
|
+
const toNum = (v, fallback = 0) => {
|
|
8696
|
+
const n = typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v) : NaN;
|
|
8697
|
+
return Number.isFinite(n) ? n : fallback;
|
|
8698
|
+
};
|
|
8699
|
+
const data = rows.map((r) => {
|
|
8700
|
+
const promptTokens = toNum(
|
|
8701
|
+
r?.prompt_tokens ?? r?.promptTokens ?? r?.input_tokens
|
|
8702
|
+
);
|
|
8703
|
+
const completionTokens = toNum(
|
|
8704
|
+
r?.completion_tokens ?? r?.completionTokens ?? r?.output_tokens
|
|
8705
|
+
);
|
|
8706
|
+
return {
|
|
8707
|
+
requestId: String(r?.request_id ?? r?.requestId ?? r?.id ?? ""),
|
|
8708
|
+
model: String(r?.model ?? r?.model_name ?? ""),
|
|
8709
|
+
spend: toNum(r?.spend ?? r?.cost),
|
|
8710
|
+
promptTokens,
|
|
8711
|
+
completionTokens,
|
|
8712
|
+
totalTokens: toNum(
|
|
8713
|
+
r?.total_tokens ?? r?.totalTokens,
|
|
8714
|
+
promptTokens + completionTokens
|
|
8715
|
+
),
|
|
8716
|
+
status: String(r?.status ?? r?.request_status ?? "unknown"),
|
|
8717
|
+
cacheHit: Boolean(r?.cache_hit ?? r?.cacheHit ?? r?.cached === true),
|
|
8718
|
+
...r?.user_id ?? r?.userId ? { userId: String(r.user_id ?? r.userId) } : {},
|
|
8719
|
+
...r?.team_id ?? r?.teamId ? { teamId: String(r.team_id ?? r.teamId) } : {},
|
|
8720
|
+
...r?.api_key ?? r?.keyHash ? { keyHash: String(r.api_key ?? r.keyHash) } : {},
|
|
8721
|
+
timestamp: String(
|
|
8722
|
+
r?.timestamp ?? r?.startTime ?? r?.created_at ?? r?.date ?? ""
|
|
8723
|
+
)
|
|
8724
|
+
};
|
|
8725
|
+
});
|
|
8726
|
+
return {
|
|
8727
|
+
data,
|
|
8728
|
+
total: toNum(raw?.total ?? raw?.total_count, data.length),
|
|
8729
|
+
page,
|
|
8730
|
+
pageSize,
|
|
8731
|
+
truncated: data.length >= pageSize
|
|
8732
|
+
};
|
|
8733
|
+
}
|
|
8658
8734
|
};
|
|
8659
8735
|
|
|
8660
8736
|
// src/errors.ts
|
|
@@ -10637,6 +10713,8 @@ function registerOpencodeRoutes(router, ctx) {
|
|
|
10637
10713
|
const reusable = existing.find(
|
|
10638
10714
|
(k) => k.key_alias === alias && !k.blocked && k.user_id === userId
|
|
10639
10715
|
);
|
|
10716
|
+
const teamPerm = teamId ? (await client.getTeamInfo(teamId).catch(() => void 0))?.object_permission : void 0;
|
|
10717
|
+
const mcp = teamPerm?.mcp_servers?.length || teamPerm?.mcp_access_groups?.length ? { mcp_servers: teamPerm.mcp_servers ?? [], mcp_access_groups: teamPerm.mcp_access_groups ?? [] } : void 0;
|
|
10640
10718
|
const mintNewKey = async () => {
|
|
10641
10719
|
const profile = await resolveUserProfile(
|
|
10642
10720
|
tokenEntityRef,
|
|
@@ -10657,7 +10735,8 @@ function registerOpencodeRoutes(router, ctx) {
|
|
|
10657
10735
|
...profile.email && { created_by_email: profile.email },
|
|
10658
10736
|
...teamId && { opencode_team: teamId }
|
|
10659
10737
|
},
|
|
10660
|
-
user_id: userId
|
|
10738
|
+
user_id: userId,
|
|
10739
|
+
...mcp && { object_permission: mcp }
|
|
10661
10740
|
});
|
|
10662
10741
|
return result.key;
|
|
10663
10742
|
};
|
|
@@ -10668,6 +10747,7 @@ function registerOpencodeRoutes(router, ctx) {
|
|
|
10668
10747
|
try {
|
|
10669
10748
|
key = (await client.regenerateKey(keyHashOrId)).key;
|
|
10670
10749
|
rotated = true;
|
|
10750
|
+
if (key && mcp) await client.updateKey({ key, object_permission: mcp });
|
|
10671
10751
|
} catch (err) {
|
|
10672
10752
|
const unsupported = err instanceof LiteLLMUpstreamError && ([400, 404, 405, 501].includes(err.status) || /enterprise|premium/i.test(err.message));
|
|
10673
10753
|
if (!unsupported) throw err;
|
|
@@ -11478,6 +11558,46 @@ function registerTeamUsageRoutes(router, ctx) {
|
|
|
11478
11558
|
}
|
|
11479
11559
|
}
|
|
11480
11560
|
|
|
11561
|
+
// src/routes/insights.ts
|
|
11562
|
+
function registerInsightsRoutes(router, ctx) {
|
|
11563
|
+
const { client, catalogClient, auth, logger, auditGroup } = ctx;
|
|
11564
|
+
const requireUser = createRequireUser(ctx);
|
|
11565
|
+
router.get("/insights/spend-logs", requireUser, async (req, res) => {
|
|
11566
|
+
try {
|
|
11567
|
+
const { start_date, end_date, userId, teamId, page, page_size } = req.query;
|
|
11568
|
+
if (!start_date || !end_date) {
|
|
11569
|
+
res.status(400).json({ error: "start_date and end_date are required" });
|
|
11570
|
+
return;
|
|
11571
|
+
}
|
|
11572
|
+
const tokenEntityRef = res.locals.tokenEntityRef;
|
|
11573
|
+
await getProvisionedUser(ctx, res);
|
|
11574
|
+
const callerId = res.locals.userId;
|
|
11575
|
+
const isAudit = auditGroup ? await isUserMemberOfGroup(
|
|
11576
|
+
tokenEntityRef,
|
|
11577
|
+
auditGroup,
|
|
11578
|
+
catalogClient,
|
|
11579
|
+
auth,
|
|
11580
|
+
logger
|
|
11581
|
+
) : false;
|
|
11582
|
+
const result = await client.getSpendLogs({
|
|
11583
|
+
startDate: start_date,
|
|
11584
|
+
endDate: end_date,
|
|
11585
|
+
userId: isAudit ? userId || callerId : callerId,
|
|
11586
|
+
teamId: isAudit ? teamId : void 0,
|
|
11587
|
+
page: page ? Number(page) : void 0,
|
|
11588
|
+
pageSize: page_size ? Number(page_size) : void 0
|
|
11589
|
+
});
|
|
11590
|
+
res.json(result);
|
|
11591
|
+
} catch (error) {
|
|
11592
|
+
if (error instanceof ProvisioningError) {
|
|
11593
|
+
res.status(error.status).json(error.body);
|
|
11594
|
+
return;
|
|
11595
|
+
}
|
|
11596
|
+
sendError(res, error, logger, "fetch spend logs");
|
|
11597
|
+
}
|
|
11598
|
+
});
|
|
11599
|
+
}
|
|
11600
|
+
|
|
11481
11601
|
// node_modules/jose/dist/node/esm/runtime/base64url.js
|
|
11482
11602
|
var import_node_buffer = require("node:buffer");
|
|
11483
11603
|
|
|
@@ -13265,14 +13385,14 @@ async function createRouter(options) {
|
|
|
13265
13385
|
// Helper functions - defined inline below this object
|
|
13266
13386
|
authorizeKeyAction: async (req, keyId) => {
|
|
13267
13387
|
if (!req.res || !req.res.locals.tokenEntityRef || !req.res.locals.userId) {
|
|
13268
|
-
throw new
|
|
13388
|
+
throw new import_errors22.NotAllowedError("User identity not found in request");
|
|
13269
13389
|
}
|
|
13270
13390
|
const tokenEntityRef = req.res.locals.tokenEntityRef;
|
|
13271
13391
|
const userId = await resolveMemberUserId(tokenEntityRef);
|
|
13272
13392
|
const ownKeys = await client.listKeys(userId);
|
|
13273
13393
|
const key = ownKeys.find((k) => (k.token ?? k.key) === keyId);
|
|
13274
13394
|
if (!key) {
|
|
13275
|
-
throw new
|
|
13395
|
+
throw new import_errors22.NotAllowedError("Access denied: key does not belong to the caller");
|
|
13276
13396
|
}
|
|
13277
13397
|
return { tokenEntityRef, userId, key };
|
|
13278
13398
|
},
|
|
@@ -13406,13 +13526,14 @@ async function createRouter(options) {
|
|
|
13406
13526
|
router.use(import_express.default.json());
|
|
13407
13527
|
router.use(import_express.default.urlencoded({ extended: false }));
|
|
13408
13528
|
const requireUser = createRequireUser(ctx);
|
|
13409
|
-
router.use(["/user/info", "/keys", "/usage"], requireUser);
|
|
13529
|
+
router.use(["/user/info", "/keys", "/usage", "/insights"], requireUser);
|
|
13410
13530
|
registerConfigRoutes(router, ctx);
|
|
13411
13531
|
registerKeysRoutes(router, ctx);
|
|
13412
13532
|
registerOpencodeRoutes(router, ctx);
|
|
13413
13533
|
registerTeamsRoutes(router, ctx);
|
|
13414
13534
|
registerObjectPermissionsRoutes(router, ctx);
|
|
13415
13535
|
registerTeamUsageRoutes(router, ctx);
|
|
13536
|
+
registerInsightsRoutes(router, ctx);
|
|
13416
13537
|
registerBridgeRoutes(router, ctx, { config, tokenVerifier: options.tokenVerifier });
|
|
13417
13538
|
return router;
|
|
13418
13539
|
}
|