@acarmisc/backstage-plugin-litellm 0.13.0 → 0.14.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/index.cjs.js CHANGED
@@ -1353,10 +1353,18 @@ var init_format = __esm({
1353
1353
  function trimSlash(url) {
1354
1354
  return url.replace(/\/+$/, "");
1355
1355
  }
1356
+ function isModelAllowedByTeam(model, teamModels) {
1357
+ if (!teamModels || teamModels.length === 0) return true;
1358
+ if (teamModels.includes(ALL_PROXY_MODELS)) return true;
1359
+ if (teamModels.includes(model.model_name)) return true;
1360
+ return !!model.access_groups?.some((group) => teamModels.includes(group));
1361
+ }
1356
1362
  function buildSnippets(baseUrl, key, model) {
1357
1363
  const base = trimSlash(baseUrl);
1364
+ const apiBase = `${base}/v1`;
1358
1365
  return {
1359
- curl: `curl ${base}/v1/chat/completions \\
1366
+ publicEndpoint: apiBase,
1367
+ curl: `curl ${apiBase}/chat/completions \\
1360
1368
  -H "Authorization: Bearer ${key}" \\
1361
1369
  -H "Content-Type: application/json" \\
1362
1370
  -d '{
@@ -1367,14 +1375,40 @@ function buildSnippets(baseUrl, key, model) {
1367
1375
 
1368
1376
  client = OpenAI(
1369
1377
  api_key="${key}",
1370
- base_url="${base}/v1",
1378
+ base_url="${apiBase}",
1371
1379
  )
1372
1380
 
1373
1381
  response = client.chat.completions.create(
1374
1382
  model="${model}",
1375
1383
  messages=[{ "role": "user", "content": "Hello!" }],
1376
1384
  )
1377
- print(response.choices[0].message.content)`
1385
+ print(response.choices[0].message.content)`,
1386
+ opencode: `{
1387
+ "$schema": "https://opencode.ai/config.json",
1388
+ "provider": {
1389
+ "litellm": {
1390
+ "npm": "@ai-sdk/openai-compatible",
1391
+ "name": "LiteLLM",
1392
+ "options": {
1393
+ "baseURL": "${apiBase}",
1394
+ "apiKey": "${key}"
1395
+ },
1396
+ "models": {
1397
+ "${model}": {}
1398
+ }
1399
+ }
1400
+ }
1401
+ }`,
1402
+ pi: `{
1403
+ "litellm": {
1404
+ "baseUrl": "${apiBase}",
1405
+ "apiKey": "${key}",
1406
+ "api": "openai-completions",
1407
+ "models": [
1408
+ { "id": "${model}", "name": "${model}" }
1409
+ ]
1410
+ }
1411
+ }`
1378
1412
  };
1379
1413
  }
1380
1414
  function aliasHelperText(aliasError, aliasDuplicate) {
@@ -1412,7 +1446,7 @@ function formatContextWindow2(maxInput, maxOutput) {
1412
1446
  if (outPart) return `ctx ${outPart} out`;
1413
1447
  return null;
1414
1448
  }
1415
- var import_react4, import_Box4, import_Typography4, import_Dialog2, import_DialogTitle2, import_DialogContent2, import_DialogActions2, import_TextField2, import_MenuItem, import_Button3, import_IconButton2, import_CircularProgress2, import_Autocomplete2, import_Checkbox, import_FormControlLabel, import_Alert, import_Tabs, import_Tab, import_icons_material3, generateDefaultAlias, emptyForm, SnippetTabs, GenerateKeyDialog;
1449
+ var import_react4, import_Box4, import_Typography4, import_Dialog2, import_DialogTitle2, import_DialogContent2, import_DialogActions2, import_TextField2, import_MenuItem, import_Button3, import_IconButton2, import_CircularProgress2, import_Autocomplete2, import_Checkbox, import_FormControlLabel, import_Alert, import_Tabs, import_Tab, import_icons_material3, generateDefaultAlias, emptyForm, ALL_PROXY_MODELS, SNIPPET_FILE_HINTS, SnippetTabs, GenerateKeyDialog;
1416
1450
  var init_GenerateKeyDialog = __esm({
1417
1451
  "src/components/GenerateKeyDialog.tsx"() {
1418
1452
  "use strict";
@@ -1451,10 +1485,16 @@ var init_GenerateKeyDialog = __esm({
1451
1485
  team_id: void 0,
1452
1486
  key_type: "llm_api"
1453
1487
  });
1488
+ ALL_PROXY_MODELS = "all-proxy-models";
1489
+ SNIPPET_FILE_HINTS = {
1490
+ opencode: "Add to ~/.config/opencode/opencode.json",
1491
+ pi: "Add to ~/.pi/agent/models.json"
1492
+ };
1454
1493
  SnippetTabs = ({ snippets, model, onCopy }) => {
1455
1494
  const [tab, setTab] = (0, import_react4.useState)("curl");
1456
- const code = tab === "curl" ? snippets.curl : snippets.openai;
1457
- return /* @__PURE__ */ import_react4.default.createElement(import_Box4.default, null, /* @__PURE__ */ import_react4.default.createElement(import_Tabs.default, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 1 } }, /* @__PURE__ */ import_react4.default.createElement(import_Tab.default, { label: "curl", value: "curl" }), /* @__PURE__ */ import_react4.default.createElement(import_Tab.default, { label: "OpenAI SDK", value: "openai" })), /* @__PURE__ */ import_react4.default.createElement(
1495
+ const code = snippets[tab];
1496
+ const fileHint = SNIPPET_FILE_HINTS[tab];
1497
+ return /* @__PURE__ */ import_react4.default.createElement(import_Box4.default, null, /* @__PURE__ */ import_react4.default.createElement(import_Tabs.default, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 1 }, variant: "scrollable" }, /* @__PURE__ */ import_react4.default.createElement(import_Tab.default, { label: "curl", value: "curl" }), /* @__PURE__ */ import_react4.default.createElement(import_Tab.default, { label: "OpenAI SDK", value: "openai" }), /* @__PURE__ */ import_react4.default.createElement(import_Tab.default, { label: "opencode", value: "opencode" }), /* @__PURE__ */ import_react4.default.createElement(import_Tab.default, { label: "pi", value: "pi" })), fileHint && /* @__PURE__ */ import_react4.default.createElement(import_Typography4.default, { variant: "caption", color: "text.secondary", display: "block", mb: 0.5 }, fileHint), /* @__PURE__ */ import_react4.default.createElement(
1458
1498
  import_Box4.default,
1459
1499
  {
1460
1500
  position: "relative",
@@ -1521,12 +1561,7 @@ var init_GenerateKeyDialog = __esm({
1521
1561
  const teamRequired = keyGenerationSettings?.teamRequired ?? true;
1522
1562
  const selectedTeam = teams.find((t) => t.team_id === formData.team_id) ?? null;
1523
1563
  const availableModels = (0, import_react4.useMemo)(() => {
1524
- const teamModels = selectedTeam?.models;
1525
- if (teamModels && teamModels.length > 0) {
1526
- const allowed = new Set(teamModels);
1527
- return models.filter((m) => allowed.has(m.model_name));
1528
- }
1529
- return models;
1564
+ return models.filter((m) => isModelAllowedByTeam(m, selectedTeam?.models));
1530
1565
  }, [models, selectedTeam]);
1531
1566
  const selectedModels = availableModels.filter((m) => (formData.models || []).includes(m.model_name));
1532
1567
  const aliasError = !(formData.alias || "").trim();
@@ -1550,12 +1585,12 @@ var init_GenerateKeyDialog = __esm({
1550
1585
  max_budget: unlimitedBudget ? null : formData.max_budget
1551
1586
  };
1552
1587
  const response = await onGenerateKey(request);
1588
+ const model = formData.models?.[0] ?? availableModels[0]?.model_name ?? "";
1553
1589
  setNewKeyValue(response.key);
1554
- setNewKeyModel(formData.models?.[0] ?? "");
1590
+ setNewKeyModel(model);
1555
1591
  setNewKeySnippets(null);
1556
1592
  try {
1557
1593
  const config = await onGetConfig();
1558
- const model = formData.models?.[0] ?? "";
1559
1594
  setNewKeySnippets(buildSnippets(config.baseUrl, response.key, model));
1560
1595
  } catch {
1561
1596
  }
@@ -1593,8 +1628,10 @@ var init_GenerateKeyDialog = __esm({
1593
1628
  getOptionLabel: (t) => t.team_alias || t.team_id,
1594
1629
  value: selectedTeam,
1595
1630
  onChange: (_e, team) => {
1596
- const teamModels = team?.models;
1597
- const restrictedModels = teamModels && teamModels.length > 0 ? (formData.models || []).filter((m) => teamModels.includes(m)) : formData.models;
1631
+ const restrictedModels = (formData.models || []).filter((name) => {
1632
+ const model = models.find((m) => m.model_name === name);
1633
+ return model ? isModelAllowedByTeam(model, team?.models) : false;
1634
+ });
1598
1635
  setFormData({ ...formData, team_id: team?.team_id, models: restrictedModels });
1599
1636
  },
1600
1637
  renderInput: (params) => /* @__PURE__ */ import_react4.default.createElement(
@@ -1641,7 +1678,31 @@ var init_GenerateKeyDialog = __esm({
1641
1678
  newKeyValue
1642
1679
  ),
1643
1680
  /* @__PURE__ */ import_react4.default.createElement(import_IconButton2.default, { onClick: () => copyToClipboard(newKeyValue) }, /* @__PURE__ */ import_react4.default.createElement(import_icons_material3.ContentCopy, null))
1644
- ), newKeySnippets && /* @__PURE__ */ import_react4.default.createElement(import_Box4.default, { mt: 3 }, /* @__PURE__ */ import_react4.default.createElement(import_Box4.default, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_icons_material3.Code, { fontSize: "small", color: "action" }), /* @__PURE__ */ import_react4.default.createElement(import_Typography4.default, { variant: "subtitle2" }, "Start calling the proxy \u2014 paste and run")), /* @__PURE__ */ import_react4.default.createElement(SnippetTabs, { snippets: newKeySnippets, model: newKeyModel, onCopy: copyToClipboard }))) : /* @__PURE__ */ import_react4.default.createElement(import_Box4.default, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, generateError && /* @__PURE__ */ import_react4.default.createElement(import_Alert.default, { severity: "error", onClose: () => setGenerateError(null) }, generateError), /* @__PURE__ */ import_react4.default.createElement(
1681
+ ), newKeySnippets && /* @__PURE__ */ import_react4.default.createElement(import_Box4.default, { mt: 2 }, /* @__PURE__ */ import_react4.default.createElement(import_Typography4.default, { variant: "caption", color: "text.secondary", display: "block", gutterBottom: true }, "Public endpoint \u2014 paste into any tool's base URL / API base field"), /* @__PURE__ */ import_react4.default.createElement(
1682
+ import_Box4.default,
1683
+ {
1684
+ display: "flex",
1685
+ alignItems: "center",
1686
+ gap: 1,
1687
+ p: 1.5,
1688
+ sx: {
1689
+ backgroundColor: "action.hover",
1690
+ border: "1px solid",
1691
+ borderColor: "divider",
1692
+ borderRadius: 1
1693
+ }
1694
+ },
1695
+ /* @__PURE__ */ import_react4.default.createElement(
1696
+ import_Typography4.default,
1697
+ {
1698
+ component: "code",
1699
+ color: "text.primary",
1700
+ sx: { fontFamily: "monospace", fontSize: 13, wordBreak: "break-all", flex: 1 }
1701
+ },
1702
+ newKeySnippets.publicEndpoint
1703
+ ),
1704
+ /* @__PURE__ */ import_react4.default.createElement(import_IconButton2.default, { size: "small", onClick: () => copyToClipboard(newKeySnippets.publicEndpoint) }, /* @__PURE__ */ import_react4.default.createElement(import_icons_material3.ContentCopy, { fontSize: "small" }))
1705
+ )), newKeySnippets && /* @__PURE__ */ import_react4.default.createElement(import_Box4.default, { mt: 3 }, /* @__PURE__ */ import_react4.default.createElement(import_Box4.default, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_icons_material3.Code, { fontSize: "small", color: "action" }), /* @__PURE__ */ import_react4.default.createElement(import_Typography4.default, { variant: "subtitle2" }, "Start calling the proxy \u2014 paste and run")), /* @__PURE__ */ import_react4.default.createElement(SnippetTabs, { snippets: newKeySnippets, model: newKeyModel, onCopy: copyToClipboard }))) : /* @__PURE__ */ import_react4.default.createElement(import_Box4.default, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, generateError && /* @__PURE__ */ import_react4.default.createElement(import_Alert.default, { severity: "error", onClose: () => setGenerateError(null) }, generateError), /* @__PURE__ */ import_react4.default.createElement(
1645
1706
  import_TextField2.default,
1646
1707
  {
1647
1708
  label: "Alias",