@acarmisc/backstage-plugin-litellm-backend 0.20.0 → 0.22.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.
@@ -19,6 +19,11 @@ export interface OpencodeConfig {
19
19
  keyDuration: string;
20
20
  /** Default max budget (USD) for keys created via the connect flow. Default 50. */
21
21
  maxBudget: number;
22
+ /**
23
+ * Reset period of maxBudget (e.g. "30d"). Unset: maxBudget covers the key's whole life,
24
+ * which runs out early on long-lived keys.
25
+ */
26
+ budgetDuration?: string;
22
27
  /**
23
28
  * When true, a team query parameter must reference a team the user is a
24
29
  * member of (or manage); otherwise the key is personal-only.
package/dist/opencode.js CHANGED
@@ -7,6 +7,7 @@ function readOpencodeConfig(config) {
7
7
  enabled,
8
8
  keyDuration: config.getOptionalString('litellm.opencode.keyDuration') ?? '30d',
9
9
  maxBudget: config.getOptionalNumber('litellm.opencode.maxBudget') ?? 50,
10
+ budgetDuration: config.getOptionalString('litellm.opencode.budgetDuration'),
10
11
  requireTeam: config.getOptionalBoolean('litellm.opencode.requireTeam') ?? false,
11
12
  metadata: config.getOptional('litellm.opencode.metadata') ?? {},
12
13
  };
@@ -149,6 +149,13 @@ function registerOpencodeRoutes(router, ctx) {
149
149
  .catch(() => []);
150
150
  // Find a healthy existing key (not blocked).
151
151
  const reusable = existing.find(k => k.key_alias === alias && !k.blocked && k.user_id === userId);
152
+ // The gateway requires MCP access on the key itself (require_key_mcp_access_defined), and a
153
+ // key may only list what its team has: copy the team's MCP servers so local clients get
154
+ // the team's tools. A team lookup failure only costs the tools, never the connection.
155
+ const teamPerm = teamId ? (await client.getTeamInfo(teamId).catch(() => undefined))?.object_permission : undefined;
156
+ const mcp = teamPerm?.mcp_servers?.length || teamPerm?.mcp_access_groups?.length
157
+ ? { mcp_servers: teamPerm.mcp_servers ?? [], mcp_access_groups: teamPerm.mcp_access_groups ?? [] }
158
+ : undefined;
152
159
  // Mint a brand-new key bound to this user (and team) under the slot alias.
153
160
  const mintNewKey = async () => {
154
161
  const profile = await (0, provisioning_1.resolveUserProfile)(tokenEntityRef, catalogClient, auth, logger);
@@ -156,6 +163,7 @@ function registerOpencodeRoutes(router, ctx) {
156
163
  alias,
157
164
  duration: opencodeCfg.keyDuration,
158
165
  max_budget: opencodeCfg.maxBudget,
166
+ ...(opencodeCfg.budgetDuration && { budget_duration: opencodeCfg.budgetDuration }),
159
167
  team_id: teamId,
160
168
  metadata: {
161
169
  ...opencodeCfg.metadata,
@@ -165,6 +173,7 @@ function registerOpencodeRoutes(router, ctx) {
165
173
  ...(teamId && { opencode_team: teamId }),
166
174
  },
167
175
  user_id: userId,
176
+ ...(mcp && { object_permission: mcp }),
168
177
  });
169
178
  return result.key;
170
179
  };
@@ -176,6 +185,10 @@ function registerOpencodeRoutes(router, ctx) {
176
185
  // Rotate the existing key in place via LiteLLM's key regeneration.
177
186
  key = (await client.regenerateKey(keyHashOrId)).key;
178
187
  rotated = true;
188
+ // Rotation keeps the old permissions: refresh them so team MCP changes arrive too.
189
+ // object_permission is not in the shared UpdateKeyRequest type yet (common 0.2.0).
190
+ if (key && mcp)
191
+ await client.updateKey({ key, object_permission: mcp });
179
192
  }
180
193
  catch (err) {
181
194
  // Key regeneration isn't available on every LiteLLM edition. Only
@@ -202,15 +215,18 @@ function registerOpencodeRoutes(router, ctx) {
202
215
  res.status(502).json({ error: 'LiteLLM returned no key material' });
203
216
  return;
204
217
  }
218
+ logger.info('opencode.connect', { userId, team: teamId ?? null, rotated });
219
+ // The Backstage connect page calls this with fetchApi (JSON + Bearer) and does the
220
+ // loopback redirect itself, so it can collect one key per team in a single trip.
221
+ if (req.is('application/json')) {
222
+ res.set('Cache-Control', 'no-store');
223
+ res.json({ key, team: teamId ?? null });
224
+ return;
225
+ }
205
226
  const url = new URL(redirectUri);
206
227
  url.searchParams.set('key', key);
207
228
  if (teamId)
208
229
  url.searchParams.set('team', teamId);
209
- logger.info('opencode.connect', {
210
- userId,
211
- team: teamId ?? null,
212
- rotated,
213
- });
214
230
  res.redirect(302, url.href);
215
231
  }
216
232
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acarmisc/backstage-plugin-litellm-backend",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "description": "The Backstage backend plugin for LiteLLM governance",
5
5
  "backstage": {
6
6
  "role": "backend-plugin",