@acarmisc/backstage-plugin-litellm-backend 0.7.0 → 0.8.1
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 -0
- package/dist/client.js +40 -10
- package/dist/index.cjs.js +45 -9
- package/dist/index.cjs.js.map +3 -3
- package/dist/provisioning.js +1 -2
- package/dist/router.js +18 -2
- package/package.json +3 -1
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import { LiteLLMConfig, UserInfo, VirtualKey, ModelInfo, UsageMetrics, TeamInfo, GenerateKeyRequest, GenerateKeyResponse, UpdateKeyRequest, DeleteKeyRequest, CreateUserRequest, CreateUserResponse, AuditLogsParams, PaginatedAuditLogs } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Typed error for failed upstream LiteLLM responses. Preserves the HTTP
|
|
4
|
+
* status and the structured `param` (e.g. `key_alias`) from the upstream
|
|
5
|
+
* body so the router can surface a 400 instead of collapsing everything
|
|
6
|
+
* into a 500. The message is the upstream `error.message` when present,
|
|
7
|
+
* otherwise the raw body text.
|
|
8
|
+
*/
|
|
9
|
+
export declare class LiteLLMUpstreamError extends Error {
|
|
10
|
+
status: number;
|
|
11
|
+
param?: string;
|
|
12
|
+
constructor(status: number, statusText: string, body: string);
|
|
13
|
+
}
|
|
2
14
|
export declare class LiteLLMClient {
|
|
3
15
|
private baseUrl;
|
|
4
16
|
private masterKey;
|
package/dist/client.js
CHANGED
|
@@ -1,7 +1,43 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.LiteLLMClient = void 0;
|
|
3
|
+
exports.LiteLLMClient = exports.LiteLLMUpstreamError = void 0;
|
|
4
4
|
const DEFAULT_TIMEOUT = 30000;
|
|
5
|
+
/**
|
|
6
|
+
* Typed error for failed upstream LiteLLM responses. Preserves the HTTP
|
|
7
|
+
* status and the structured `param` (e.g. `key_alias`) from the upstream
|
|
8
|
+
* body so the router can surface a 400 instead of collapsing everything
|
|
9
|
+
* into a 500. The message is the upstream `error.message` when present,
|
|
10
|
+
* otherwise the raw body text.
|
|
11
|
+
*/
|
|
12
|
+
class LiteLLMUpstreamError extends Error {
|
|
13
|
+
constructor(status, statusText, body) {
|
|
14
|
+
let message = `LiteLLM API error: ${status} ${statusText} - ${body}`;
|
|
15
|
+
let param;
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(body);
|
|
18
|
+
const inner = parsed?.error ?? parsed;
|
|
19
|
+
if (typeof inner?.message === 'string')
|
|
20
|
+
message = inner.message;
|
|
21
|
+
if (typeof inner?.param === 'string' && inner.param !== 'None') {
|
|
22
|
+
param = inner.param;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// not JSON — keep the raw body in the message
|
|
27
|
+
}
|
|
28
|
+
super(message);
|
|
29
|
+
this.status = status;
|
|
30
|
+
this.param = param;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.LiteLLMUpstreamError = LiteLLMUpstreamError;
|
|
34
|
+
function extractModelList(fallback) {
|
|
35
|
+
if (Array.isArray(fallback))
|
|
36
|
+
return fallback;
|
|
37
|
+
if (Array.isArray(fallback?.data))
|
|
38
|
+
return fallback.data;
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
5
41
|
class LiteLLMClient {
|
|
6
42
|
constructor(config, timeout = DEFAULT_TIMEOUT) {
|
|
7
43
|
this.baseUrl = config.baseUrl.replace(/\/$/, '');
|
|
@@ -28,9 +64,7 @@ class LiteLLMClient {
|
|
|
28
64
|
});
|
|
29
65
|
if (!response.ok) {
|
|
30
66
|
const errorBody = await response.text();
|
|
31
|
-
|
|
32
|
-
err.status = response.status;
|
|
33
|
-
throw err;
|
|
67
|
+
throw new LiteLLMUpstreamError(response.status, response.statusText, errorBody);
|
|
34
68
|
}
|
|
35
69
|
return response.json();
|
|
36
70
|
}
|
|
@@ -245,11 +279,7 @@ class LiteLLMClient {
|
|
|
245
279
|
// fall through to /models
|
|
246
280
|
}
|
|
247
281
|
const fallback = await this.request('/models');
|
|
248
|
-
const data =
|
|
249
|
-
? fallback
|
|
250
|
-
: Array.isArray(fallback?.data)
|
|
251
|
-
? fallback.data
|
|
252
|
-
: [];
|
|
282
|
+
const data = extractModelList(fallback);
|
|
253
283
|
return data
|
|
254
284
|
.map((m) => ({
|
|
255
285
|
model_name: m.model_name ?? m.id ?? '',
|
|
@@ -373,7 +403,7 @@ class LiteLLMClient {
|
|
|
373
403
|
};
|
|
374
404
|
if (!kb.key_alias && kmeta.key_alias)
|
|
375
405
|
kb.key_alias = kmeta.key_alias;
|
|
376
|
-
if (kb.team_id
|
|
406
|
+
if (kb.team_id === null && kmeta.team_id)
|
|
377
407
|
kb.team_id = kmeta.team_id;
|
|
378
408
|
if (!kb.models.includes(name))
|
|
379
409
|
kb.models.push(name);
|
package/dist/index.cjs.js
CHANGED
|
@@ -63,6 +63,29 @@ var import_catalog_client = require("@backstage/catalog-client");
|
|
|
63
63
|
|
|
64
64
|
// src/client.ts
|
|
65
65
|
var DEFAULT_TIMEOUT = 3e4;
|
|
66
|
+
var LiteLLMUpstreamError = class extends Error {
|
|
67
|
+
constructor(status, statusText, body) {
|
|
68
|
+
let message2 = `LiteLLM API error: ${status} ${statusText} - ${body}`;
|
|
69
|
+
let param;
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(body);
|
|
72
|
+
const inner = parsed?.error ?? parsed;
|
|
73
|
+
if (typeof inner?.message === "string") message2 = inner.message;
|
|
74
|
+
if (typeof inner?.param === "string" && inner.param !== "None") {
|
|
75
|
+
param = inner.param;
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
}
|
|
79
|
+
super(message2);
|
|
80
|
+
this.status = status;
|
|
81
|
+
this.param = param;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
function extractModelList(fallback) {
|
|
85
|
+
if (Array.isArray(fallback)) return fallback;
|
|
86
|
+
if (Array.isArray(fallback?.data)) return fallback.data;
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
66
89
|
var LiteLLMClient = class {
|
|
67
90
|
constructor(config, timeout = DEFAULT_TIMEOUT) {
|
|
68
91
|
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
@@ -84,11 +107,11 @@ var LiteLLMClient = class {
|
|
|
84
107
|
});
|
|
85
108
|
if (!response.ok) {
|
|
86
109
|
const errorBody = await response.text();
|
|
87
|
-
|
|
88
|
-
|
|
110
|
+
throw new LiteLLMUpstreamError(
|
|
111
|
+
response.status,
|
|
112
|
+
response.statusText,
|
|
113
|
+
errorBody
|
|
89
114
|
);
|
|
90
|
-
err.status = response.status;
|
|
91
|
-
throw err;
|
|
92
115
|
}
|
|
93
116
|
return response.json();
|
|
94
117
|
} finally {
|
|
@@ -284,7 +307,7 @@ var LiteLLMClient = class {
|
|
|
284
307
|
} catch {
|
|
285
308
|
}
|
|
286
309
|
const fallback = await this.request("/models");
|
|
287
|
-
const data =
|
|
310
|
+
const data = extractModelList(fallback);
|
|
288
311
|
return data.map((m) => ({
|
|
289
312
|
model_name: m.model_name ?? m.id ?? "",
|
|
290
313
|
mode: m.mode ?? "chat",
|
|
@@ -403,7 +426,7 @@ var LiteLLMClient = class {
|
|
|
403
426
|
...emptyModelBucket()
|
|
404
427
|
};
|
|
405
428
|
if (!kb.key_alias && kmeta.key_alias) kb.key_alias = kmeta.key_alias;
|
|
406
|
-
if (kb.team_id
|
|
429
|
+
if (kb.team_id === null && kmeta.team_id) kb.team_id = kmeta.team_id;
|
|
407
430
|
if (!kb.models.includes(name)) kb.models.push(name);
|
|
408
431
|
kb.total_spend += km.spend ?? 0;
|
|
409
432
|
kb.total_tokens += km.total_tokens ?? 0;
|
|
@@ -836,7 +859,7 @@ async function provisionUser(client, userId, defaults, profile, backstageEntity,
|
|
|
836
859
|
}
|
|
837
860
|
};
|
|
838
861
|
logger.info(
|
|
839
|
-
`Provisioning new LiteLLM user for Backstage identity: ${userId}
|
|
862
|
+
`Provisioning new LiteLLM user for Backstage identity: ${userId}${profile.email ? ` (email=${profile.email})` : ""}`
|
|
840
863
|
);
|
|
841
864
|
try {
|
|
842
865
|
await client.createUser(payload);
|
|
@@ -2601,12 +2624,18 @@ async function createRouter(options) {
|
|
|
2601
2624
|
const tokenEntityRef = await resolveUserId(req, auth);
|
|
2602
2625
|
const userId = tokenEntityRef ? toLiteLLMUserId(tokenEntityRef, userIdDomain) : req.query.user_id;
|
|
2603
2626
|
if (!userId) {
|
|
2604
|
-
throw
|
|
2627
|
+
throw Object.assign(new Error("Cannot verify key ownership without an authenticated user"), {
|
|
2628
|
+
status: 403,
|
|
2629
|
+
body: { error: "Cannot verify key ownership without an authenticated user" }
|
|
2630
|
+
});
|
|
2605
2631
|
}
|
|
2606
2632
|
const ownKeys = await client.listKeys(userId);
|
|
2607
2633
|
const owns = ownKeys.some((k) => (k.token ?? k.key) === keyId);
|
|
2608
2634
|
if (!owns) {
|
|
2609
|
-
throw
|
|
2635
|
+
throw Object.assign(new Error("Access denied: key does not belong to the caller"), {
|
|
2636
|
+
status: 403,
|
|
2637
|
+
body: { error: "Access denied: key does not belong to the caller" }
|
|
2638
|
+
});
|
|
2610
2639
|
}
|
|
2611
2640
|
return { tokenEntityRef, userId };
|
|
2612
2641
|
}
|
|
@@ -2678,6 +2707,13 @@ async function createRouter(options) {
|
|
|
2678
2707
|
});
|
|
2679
2708
|
return;
|
|
2680
2709
|
}
|
|
2710
|
+
if (error instanceof LiteLLMUpstreamError) {
|
|
2711
|
+
res.status(error.status).json({
|
|
2712
|
+
error: error.message,
|
|
2713
|
+
...error.param ? { param: error.param } : {}
|
|
2714
|
+
});
|
|
2715
|
+
return;
|
|
2716
|
+
}
|
|
2681
2717
|
logger.error("Failed to generate key", error);
|
|
2682
2718
|
res.status(500).json({ error: error.message });
|
|
2683
2719
|
}
|