@acarmisc/backstage-plugin-litellm-backend 0.2.1 → 0.3.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 +29 -0
- package/dist/bridge.d.ts +69 -0
- package/dist/bridge.js +144 -0
- package/dist/client.d.ts +15 -0
- package/dist/client.js +70 -3
- package/dist/index.cjs.js +1623 -14
- package/dist/index.cjs.js.map +4 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.js +9 -1
- package/dist/router.d.ts +6 -0
- package/dist/router.js +75 -1
- package/package.json +6 -4
package/dist/router.d.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import { Config } from '@backstage/config';
|
|
3
3
|
import { AuthService, DiscoveryService } from '@backstage/backend-plugin-api';
|
|
4
|
+
import { LiteLLMClient } from './client';
|
|
4
5
|
import { ProvisioningError } from './provisioning';
|
|
6
|
+
import { TokenVerifier } from './bridge';
|
|
5
7
|
export { ProvisioningError };
|
|
6
8
|
export interface RouterOptions {
|
|
7
9
|
config: Config;
|
|
8
10
|
logger: any;
|
|
9
11
|
auth: AuthService;
|
|
10
12
|
discovery: DiscoveryService;
|
|
13
|
+
/** Override the LiteLLM client (tests). Defaults to one built from config. */
|
|
14
|
+
client?: LiteLLMClient;
|
|
15
|
+
/** Override the bridge token verifier (tests). Defaults to a Keycloak JWKS verifier. */
|
|
16
|
+
tokenVerifier?: TokenVerifier;
|
|
11
17
|
}
|
|
12
18
|
export declare function createRouter(options: RouterOptions): Promise<Router>;
|
package/dist/router.js
CHANGED
|
@@ -40,12 +40,13 @@ const catalog_client_1 = require("@backstage/catalog-client");
|
|
|
40
40
|
const client_1 = require("./client");
|
|
41
41
|
const provisioning_1 = require("./provisioning");
|
|
42
42
|
Object.defineProperty(exports, "ProvisioningError", { enumerable: true, get: function () { return provisioning_1.ProvisioningError; } });
|
|
43
|
+
const bridge_1 = require("./bridge");
|
|
43
44
|
async function createRouter(options) {
|
|
44
45
|
const { config, logger, auth, discovery } = options;
|
|
45
46
|
const baseUrl = config.getString('litellm.baseUrl');
|
|
46
47
|
const masterKey = config.getString('litellm.masterKey');
|
|
47
48
|
const userIdDomain = config.getOptionalString('litellm.userIdDomain');
|
|
48
|
-
const client = new client_1.LiteLLMClient({ baseUrl, masterKey });
|
|
49
|
+
const client = options.client ?? new client_1.LiteLLMClient({ baseUrl, masterKey });
|
|
49
50
|
const { enabled: provisioningEnabled, defaults: provisioningDefaults } = (0, provisioning_1.readProvisioningDefaults)(config);
|
|
50
51
|
const roleConfigs = (0, provisioning_1.readRoleConfigs)(config);
|
|
51
52
|
const catalogClient = new catalog_client_1.CatalogClient({ discoveryApi: discovery });
|
|
@@ -270,5 +271,78 @@ async function createRouter(options) {
|
|
|
270
271
|
res.status(500).json({ error: error.message });
|
|
271
272
|
}
|
|
272
273
|
});
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
// Bridge endpoints (CLI / Abby)
|
|
276
|
+
//
|
|
277
|
+
// Authenticated by a Keycloak access token (JWKS-verified), NOT by Backstage's
|
|
278
|
+
// own auth. Lets CLI clients list/mint virtual keys without holding the master
|
|
279
|
+
// key. Gated by litellm.bridge.enabled; the verifier needs litellm.bridge.issuer.
|
|
280
|
+
const bridgeCfg = (0, bridge_1.readBridgeConfig)(config);
|
|
281
|
+
let tokenVerifier = options.tokenVerifier;
|
|
282
|
+
if (bridgeCfg.enabled && !tokenVerifier) {
|
|
283
|
+
try {
|
|
284
|
+
tokenVerifier = (0, bridge_1.newDefaultVerifier)(bridgeCfg);
|
|
285
|
+
}
|
|
286
|
+
catch (e) {
|
|
287
|
+
logger.error(`LiteLLM bridge enabled but misconfigured: ${e.message}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (bridgeCfg.enabled && tokenVerifier) {
|
|
291
|
+
const requireClaims = async (req) => {
|
|
292
|
+
const header = req.headers.authorization ?? '';
|
|
293
|
+
const token = header.startsWith('Bearer ') ? header.slice(7) : '';
|
|
294
|
+
if (!token)
|
|
295
|
+
throw new bridge_1.BridgeAuthError('missing Bearer token');
|
|
296
|
+
return tokenVerifier.verify(token);
|
|
297
|
+
};
|
|
298
|
+
const handleBridgeError = (error, res) => {
|
|
299
|
+
if (error instanceof bridge_1.BridgeAuthError) {
|
|
300
|
+
res.status(401).json({ error: 'unauthorized', hint: error.message });
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (error instanceof provisioning_1.ProvisioningError) {
|
|
304
|
+
res.status(error.status).json(error.body);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
logger.error('Bridge request failed', error);
|
|
308
|
+
res.status(500).json({ error: error.message });
|
|
309
|
+
};
|
|
310
|
+
router.get('/bridge/health', (_req, res) => {
|
|
311
|
+
res.json({ status: 'ok', bridge: true, clientId: bridgeCfg.clientId });
|
|
312
|
+
});
|
|
313
|
+
router.get('/bridge/keys', async (req, res) => {
|
|
314
|
+
try {
|
|
315
|
+
const claims = await requireClaims(req);
|
|
316
|
+
const keys = await (0, bridge_1.bridgeListKeys)(client, claims, provisioningEnabled, provisioningDefaults, logger);
|
|
317
|
+
res.json(keys);
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
handleBridgeError(error, res);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
router.post('/bridge/keys', async (req, res) => {
|
|
324
|
+
try {
|
|
325
|
+
const claims = await requireClaims(req);
|
|
326
|
+
const result = await (0, bridge_1.bridgeGenerateKey)(client, claims, provisioningEnabled, provisioningDefaults, logger, (req.body ?? {}));
|
|
327
|
+
res.json(result);
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
handleBridgeError(error, res);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
router.get('/bridge/models', async (req, res) => {
|
|
334
|
+
try {
|
|
335
|
+
await requireClaims(req); // authenticate only
|
|
336
|
+
const models = await client.listModels();
|
|
337
|
+
res.json(models);
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
handleBridgeError(error, res);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
else if (bridgeCfg.enabled) {
|
|
345
|
+
logger.warn('litellm.bridge.enabled is true but no verifier could be built — bridge endpoints not mounted');
|
|
346
|
+
}
|
|
273
347
|
return router;
|
|
274
348
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acarmisc/backstage-plugin-litellm-backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "The Backstage backend plugin for LiteLLM governance",
|
|
5
5
|
"backstage": {
|
|
6
6
|
"role": "backend-plugin",
|
|
@@ -33,13 +33,15 @@
|
|
|
33
33
|
"@backstage/catalog-model": "^1.7.0",
|
|
34
34
|
"@backstage/config": "^1.3.0",
|
|
35
35
|
"@backstage/types": "^1.2.0",
|
|
36
|
-
"express": "^4.18.2"
|
|
36
|
+
"express": "^4.18.2",
|
|
37
|
+
"jose": "^5.9.6"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"@backstage/cli": "^0.36.2",
|
|
40
41
|
"@types/express": "^4.17.25",
|
|
41
42
|
"esbuild": "^0.28.0",
|
|
42
|
-
"typescript": "^5.9.3"
|
|
43
|
+
"typescript": "^5.9.3",
|
|
44
|
+
"@types/node": "^20.0.0"
|
|
43
45
|
},
|
|
44
46
|
"configSchema": "config.d.ts"
|
|
45
|
-
}
|
|
47
|
+
}
|