@acarmisc/backstage-plugin-litellm-backend 0.29.0 → 0.30.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/config.d.ts +8 -0
- package/dist/index.cjs.js +37 -5
- package/dist/index.cjs.js.map +3 -3
- package/dist/keySnippets.d.ts +8 -0
- package/dist/keySnippets.js +26 -0
- package/dist/openapi.js +1 -0
- package/dist/router.js +4 -0
- package/dist/routes/config.js +1 -0
- package/dist/routes/context.d.ts +3 -0
- package/dist/services/keyService.js +9 -1
- package/dist/services/mcpAccess.d.ts +10 -2
- package/dist/services/mcpAccess.js +10 -3
- package/package.json +1 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Starter-snippet tabs the key dialog can show, in default order. */
|
|
2
|
+
export declare const KEY_SNIPPET_TABS: readonly ["office365", "opencode", "claude-code", "pi", "openai", "curl"];
|
|
3
|
+
export type KeySnippetTab = (typeof KEY_SNIPPET_TABS)[number];
|
|
4
|
+
/**
|
|
5
|
+
* Reads litellm.display.keySnippets. Omitted means every tab in default order.
|
|
6
|
+
* Throws at startup on an empty, unknown or duplicated list so a typo fails loudly.
|
|
7
|
+
*/
|
|
8
|
+
export declare function parseKeySnippetTabs(raw: string[] | undefined): KeySnippetTab[];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KEY_SNIPPET_TABS = void 0;
|
|
4
|
+
exports.parseKeySnippetTabs = parseKeySnippetTabs;
|
|
5
|
+
/** Starter-snippet tabs the key dialog can show, in default order. */
|
|
6
|
+
exports.KEY_SNIPPET_TABS = ['office365', 'opencode', 'claude-code', 'pi', 'openai', 'curl'];
|
|
7
|
+
/**
|
|
8
|
+
* Reads litellm.display.keySnippets. Omitted means every tab in default order.
|
|
9
|
+
* Throws at startup on an empty, unknown or duplicated list so a typo fails loudly.
|
|
10
|
+
*/
|
|
11
|
+
function parseKeySnippetTabs(raw) {
|
|
12
|
+
if (raw === undefined)
|
|
13
|
+
return [...exports.KEY_SNIPPET_TABS];
|
|
14
|
+
if (raw.length === 0) {
|
|
15
|
+
throw new Error('litellm.display.keySnippets must list at least one tab');
|
|
16
|
+
}
|
|
17
|
+
const allowed = exports.KEY_SNIPPET_TABS;
|
|
18
|
+
const unknown = raw.filter(tab => !allowed.includes(tab));
|
|
19
|
+
if (unknown.length > 0) {
|
|
20
|
+
throw new Error(`litellm.display.keySnippets: unknown tab(s) ${unknown.join(', ')}. Allowed: ${exports.KEY_SNIPPET_TABS.join(', ')}`);
|
|
21
|
+
}
|
|
22
|
+
if (new Set(raw).size !== raw.length) {
|
|
23
|
+
throw new Error('litellm.display.keySnippets lists a tab more than once');
|
|
24
|
+
}
|
|
25
|
+
return raw;
|
|
26
|
+
}
|
package/dist/openapi.js
CHANGED
|
@@ -70,6 +70,7 @@ exports.openApiSpec = {
|
|
|
70
70
|
hideTeamBudgetForMembers: { type: 'boolean' },
|
|
71
71
|
hideTeamBudgetForManagers: { type: 'boolean' },
|
|
72
72
|
serviceKeyPrefixes: { type: 'array', items: { type: 'string' } },
|
|
73
|
+
keySnippets: { type: 'array', items: { type: 'string', enum: ['office365', 'opencode', 'claude-code', 'pi', 'openai', 'curl'] } },
|
|
73
74
|
} },
|
|
74
75
|
} } } },
|
|
75
76
|
},
|
package/dist/router.js
CHANGED
|
@@ -42,6 +42,7 @@ const catalog_client_1 = require("@backstage/catalog-client");
|
|
|
42
42
|
const client_1 = require("./client");
|
|
43
43
|
const provisioning_1 = require("./provisioning");
|
|
44
44
|
Object.defineProperty(exports, "ProvisioningError", { enumerable: true, get: function () { return provisioning_1.ProvisioningError; } });
|
|
45
|
+
const keySnippets_1 = require("./keySnippets");
|
|
45
46
|
const backstage_plugin_litellm_common_1 = require("@acarmisc/backstage-plugin-litellm-common");
|
|
46
47
|
const teamAdmin_1 = require("./teamAdmin");
|
|
47
48
|
const teamBudgetVisibility_1 = require("./teamBudgetVisibility");
|
|
@@ -75,6 +76,8 @@ async function createRouter(options) {
|
|
|
75
76
|
const serviceKeyPrefixes = (config.getOptionalStringArray('litellm.display.serviceKeyPrefixes') ?? [])
|
|
76
77
|
.map(p => p.trim())
|
|
77
78
|
.filter(p => p.length > 0);
|
|
79
|
+
// Starter-snippet tabs shown after key generation, in order (display-only).
|
|
80
|
+
const keySnippets = (0, keySnippets_1.parseKeySnippetTabs)(config.getOptionalStringArray('litellm.display.keySnippets'));
|
|
78
81
|
const baseClient = options.client ?? new client_1.LiteLLMClient({ baseUrl, masterKey });
|
|
79
82
|
// Wrap client with userInfoCache. TTL is configurable (default 10s, 0 disables).
|
|
80
83
|
const userInfoCacheTtlSeconds = config.getOptionalNumber('litellm.cache.userInfoTtlSeconds') ?? 10;
|
|
@@ -139,6 +142,7 @@ async function createRouter(options) {
|
|
|
139
142
|
publicBaseUrl,
|
|
140
143
|
supportContact,
|
|
141
144
|
serviceKeyPrefixes,
|
|
145
|
+
keySnippets,
|
|
142
146
|
userIdDomain,
|
|
143
147
|
provisioningEnabled,
|
|
144
148
|
provisioningDefaults,
|
package/dist/routes/config.js
CHANGED
|
@@ -36,6 +36,7 @@ function registerConfigRoutes(router, ctx) {
|
|
|
36
36
|
hideTeamBudgetForMembers: ctx.teamBudgetVisibility.hideTeamBudgetForMembers,
|
|
37
37
|
hideTeamBudgetForManagers: ctx.teamBudgetVisibility.hideTeamBudgetForManagers,
|
|
38
38
|
serviceKeyPrefixes: ctx.serviceKeyPrefixes,
|
|
39
|
+
keySnippets: ctx.keySnippets,
|
|
39
40
|
},
|
|
40
41
|
});
|
|
41
42
|
});
|
package/dist/routes/context.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { CatalogClient } from '@backstage/catalog-client';
|
|
|
5
5
|
import { LiteLLMClient } from '../client';
|
|
6
6
|
import { ProvisioningDefaults, RoleConfig, VirtualKey, TeamInfo } from '../types';
|
|
7
7
|
import { TeamAdminConfig } from '../teamAdmin';
|
|
8
|
+
import type { KeySnippetTab } from '../keySnippets';
|
|
8
9
|
import type { KeyValidationConfig } from '@acarmisc/backstage-plugin-litellm-common';
|
|
9
10
|
import { TeamBudgetVisibility } from '../teamBudgetVisibility';
|
|
10
11
|
import { OpencodeConfig } from '../opencode';
|
|
@@ -25,6 +26,8 @@ export interface RouterContext {
|
|
|
25
26
|
supportContact: string | undefined;
|
|
26
27
|
/** Key-alias prefixes treated as service keys by the frontend (display-only). */
|
|
27
28
|
serviceKeyPrefixes: string[];
|
|
29
|
+
/** Starter-snippet tabs the key dialog shows, in order (display-only). */
|
|
30
|
+
keySnippets: KeySnippetTab[];
|
|
28
31
|
userIdDomain: string | undefined;
|
|
29
32
|
provisioningEnabled: boolean;
|
|
30
33
|
provisioningDefaults: ProvisioningDefaults;
|
|
@@ -119,7 +119,15 @@ async function createKeyForUser(user, input, ctx) {
|
|
|
119
119
|
if (!input.team_id) {
|
|
120
120
|
throw new KeyServiceError(400, { error: 'team_id is required when mcp_servers is set' });
|
|
121
121
|
}
|
|
122
|
-
const
|
|
122
|
+
const allowed = (0, mcpAccess_1.teamMcpServers)(await getTeam(input.team_id));
|
|
123
|
+
let disallowed = (0, mcpAccess_1.findDisallowedMcpServers)(mcpServers, allowed);
|
|
124
|
+
if (disallowed.length > 0) {
|
|
125
|
+
// LiteLLM stores a team's grant by server id while callers name servers, so
|
|
126
|
+
// resolve the gateway's listing before refusing. A failed listing keeps the
|
|
127
|
+
// plain comparison.
|
|
128
|
+
const servers = await client.listMcpServers().catch(() => []);
|
|
129
|
+
disallowed = (0, mcpAccess_1.findDisallowedMcpServers)(mcpServers, allowed, servers);
|
|
130
|
+
}
|
|
123
131
|
if (disallowed.length > 0) {
|
|
124
132
|
throw new KeyServiceError(403, {
|
|
125
133
|
error: 'One or more requested MCP servers are not allowed',
|
|
@@ -16,5 +16,13 @@ export declare function teamMcpServers(team: {
|
|
|
16
16
|
mcp_servers?: string[];
|
|
17
17
|
};
|
|
18
18
|
} | undefined): string[];
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Requested names that are not granted, de-duplicated. A team's list may hold a
|
|
21
|
+
* server's id while a caller names it (or the reverse), so a request also counts
|
|
22
|
+
* as granted when the gateway's listing links its name and id to an allowed entry.
|
|
23
|
+
* Without `servers` only exact matches count.
|
|
24
|
+
*/
|
|
25
|
+
export declare function findDisallowedMcpServers(requested: string[], allowed: string[], servers?: Array<{
|
|
26
|
+
id: string;
|
|
27
|
+
name?: string;
|
|
28
|
+
}>): string[];
|
|
@@ -21,8 +21,15 @@ function teamMcpServers(team) {
|
|
|
21
21
|
const listed = team?.object_permission?.mcp_servers ?? [];
|
|
22
22
|
return [...new Set(listed)].filter(n => backstage_plugin_litellm_common_1.MCP_SERVER_NAME_RE.test(n));
|
|
23
23
|
}
|
|
24
|
-
/**
|
|
25
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Requested names that are not granted, de-duplicated. A team's list may hold a
|
|
26
|
+
* server's id while a caller names it (or the reverse), so a request also counts
|
|
27
|
+
* as granted when the gateway's listing links its name and id to an allowed entry.
|
|
28
|
+
* Without `servers` only exact matches count.
|
|
29
|
+
*/
|
|
30
|
+
function findDisallowedMcpServers(requested, allowed, servers = []) {
|
|
26
31
|
const ok = new Set(allowed);
|
|
27
|
-
|
|
32
|
+
const granted = (n) => ok.has(n) ||
|
|
33
|
+
servers.some(s => (s.name === n || s.id === n) && (ok.has(s.id) || (s.name !== undefined && ok.has(s.name))));
|
|
34
|
+
return [...new Set(requested)].filter(n => !granted(n));
|
|
28
35
|
}
|