@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.esm.js CHANGED
@@ -1350,10 +1350,18 @@ import { ContentCopy as ContentCopy2, Code } from "@mui/icons-material";
1350
1350
  function trimSlash(url) {
1351
1351
  return url.replace(/\/+$/, "");
1352
1352
  }
1353
+ function isModelAllowedByTeam(model, teamModels) {
1354
+ if (!teamModels || teamModels.length === 0) return true;
1355
+ if (teamModels.includes(ALL_PROXY_MODELS)) return true;
1356
+ if (teamModels.includes(model.model_name)) return true;
1357
+ return !!model.access_groups?.some((group) => teamModels.includes(group));
1358
+ }
1353
1359
  function buildSnippets(baseUrl, key, model) {
1354
1360
  const base = trimSlash(baseUrl);
1361
+ const apiBase = `${base}/v1`;
1355
1362
  return {
1356
- curl: `curl ${base}/v1/chat/completions \\
1363
+ publicEndpoint: apiBase,
1364
+ curl: `curl ${apiBase}/chat/completions \\
1357
1365
  -H "Authorization: Bearer ${key}" \\
1358
1366
  -H "Content-Type: application/json" \\
1359
1367
  -d '{
@@ -1364,14 +1372,40 @@ function buildSnippets(baseUrl, key, model) {
1364
1372
 
1365
1373
  client = OpenAI(
1366
1374
  api_key="${key}",
1367
- base_url="${base}/v1",
1375
+ base_url="${apiBase}",
1368
1376
  )
1369
1377
 
1370
1378
  response = client.chat.completions.create(
1371
1379
  model="${model}",
1372
1380
  messages=[{ "role": "user", "content": "Hello!" }],
1373
1381
  )
1374
- print(response.choices[0].message.content)`
1382
+ print(response.choices[0].message.content)`,
1383
+ opencode: `{
1384
+ "$schema": "https://opencode.ai/config.json",
1385
+ "provider": {
1386
+ "litellm": {
1387
+ "npm": "@ai-sdk/openai-compatible",
1388
+ "name": "LiteLLM",
1389
+ "options": {
1390
+ "baseURL": "${apiBase}",
1391
+ "apiKey": "${key}"
1392
+ },
1393
+ "models": {
1394
+ "${model}": {}
1395
+ }
1396
+ }
1397
+ }
1398
+ }`,
1399
+ pi: `{
1400
+ "litellm": {
1401
+ "baseUrl": "${apiBase}",
1402
+ "apiKey": "${key}",
1403
+ "api": "openai-completions",
1404
+ "models": [
1405
+ { "id": "${model}", "name": "${model}" }
1406
+ ]
1407
+ }
1408
+ }`
1375
1409
  };
1376
1410
  }
1377
1411
  function aliasHelperText(aliasError, aliasDuplicate) {
@@ -1409,7 +1443,7 @@ function formatContextWindow2(maxInput, maxOutput) {
1409
1443
  if (outPart) return `ctx ${outPart} out`;
1410
1444
  return null;
1411
1445
  }
1412
- var generateDefaultAlias, emptyForm, SnippetTabs, GenerateKeyDialog;
1446
+ var generateDefaultAlias, emptyForm, ALL_PROXY_MODELS, SNIPPET_FILE_HINTS, SnippetTabs, GenerateKeyDialog;
1413
1447
  var init_GenerateKeyDialog = __esm({
1414
1448
  "src/components/GenerateKeyDialog.tsx"() {
1415
1449
  "use strict";
@@ -1429,10 +1463,16 @@ var init_GenerateKeyDialog = __esm({
1429
1463
  team_id: void 0,
1430
1464
  key_type: "llm_api"
1431
1465
  });
1466
+ ALL_PROXY_MODELS = "all-proxy-models";
1467
+ SNIPPET_FILE_HINTS = {
1468
+ opencode: "Add to ~/.config/opencode/opencode.json",
1469
+ pi: "Add to ~/.pi/agent/models.json"
1470
+ };
1432
1471
  SnippetTabs = ({ snippets, model, onCopy }) => {
1433
1472
  const [tab, setTab] = useState2("curl");
1434
- const code = tab === "curl" ? snippets.curl : snippets.openai;
1435
- return /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Tabs, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 1 } }, /* @__PURE__ */ React4.createElement(Tab, { label: "curl", value: "curl" }), /* @__PURE__ */ React4.createElement(Tab, { label: "OpenAI SDK", value: "openai" })), /* @__PURE__ */ React4.createElement(
1473
+ const code = snippets[tab];
1474
+ const fileHint = SNIPPET_FILE_HINTS[tab];
1475
+ return /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Tabs, { value: tab, onChange: (_, v) => setTab(v), sx: { mb: 1 }, variant: "scrollable" }, /* @__PURE__ */ React4.createElement(Tab, { label: "curl", value: "curl" }), /* @__PURE__ */ React4.createElement(Tab, { label: "OpenAI SDK", value: "openai" }), /* @__PURE__ */ React4.createElement(Tab, { label: "opencode", value: "opencode" }), /* @__PURE__ */ React4.createElement(Tab, { label: "pi", value: "pi" })), fileHint && /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption", color: "text.secondary", display: "block", mb: 0.5 }, fileHint), /* @__PURE__ */ React4.createElement(
1436
1476
  Box4,
1437
1477
  {
1438
1478
  position: "relative",
@@ -1499,12 +1539,7 @@ var init_GenerateKeyDialog = __esm({
1499
1539
  const teamRequired = keyGenerationSettings?.teamRequired ?? true;
1500
1540
  const selectedTeam = teams.find((t) => t.team_id === formData.team_id) ?? null;
1501
1541
  const availableModels = useMemo3(() => {
1502
- const teamModels = selectedTeam?.models;
1503
- if (teamModels && teamModels.length > 0) {
1504
- const allowed = new Set(teamModels);
1505
- return models.filter((m) => allowed.has(m.model_name));
1506
- }
1507
- return models;
1542
+ return models.filter((m) => isModelAllowedByTeam(m, selectedTeam?.models));
1508
1543
  }, [models, selectedTeam]);
1509
1544
  const selectedModels = availableModels.filter((m) => (formData.models || []).includes(m.model_name));
1510
1545
  const aliasError = !(formData.alias || "").trim();
@@ -1528,12 +1563,12 @@ var init_GenerateKeyDialog = __esm({
1528
1563
  max_budget: unlimitedBudget ? null : formData.max_budget
1529
1564
  };
1530
1565
  const response = await onGenerateKey(request);
1566
+ const model = formData.models?.[0] ?? availableModels[0]?.model_name ?? "";
1531
1567
  setNewKeyValue(response.key);
1532
- setNewKeyModel(formData.models?.[0] ?? "");
1568
+ setNewKeyModel(model);
1533
1569
  setNewKeySnippets(null);
1534
1570
  try {
1535
1571
  const config = await onGetConfig();
1536
- const model = formData.models?.[0] ?? "";
1537
1572
  setNewKeySnippets(buildSnippets(config.baseUrl, response.key, model));
1538
1573
  } catch {
1539
1574
  }
@@ -1571,8 +1606,10 @@ var init_GenerateKeyDialog = __esm({
1571
1606
  getOptionLabel: (t) => t.team_alias || t.team_id,
1572
1607
  value: selectedTeam,
1573
1608
  onChange: (_e, team) => {
1574
- const teamModels = team?.models;
1575
- const restrictedModels = teamModels && teamModels.length > 0 ? (formData.models || []).filter((m) => teamModels.includes(m)) : formData.models;
1609
+ const restrictedModels = (formData.models || []).filter((name) => {
1610
+ const model = models.find((m) => m.model_name === name);
1611
+ return model ? isModelAllowedByTeam(model, team?.models) : false;
1612
+ });
1576
1613
  setFormData({ ...formData, team_id: team?.team_id, models: restrictedModels });
1577
1614
  },
1578
1615
  renderInput: (params) => /* @__PURE__ */ React4.createElement(
@@ -1619,7 +1656,31 @@ var init_GenerateKeyDialog = __esm({
1619
1656
  newKeyValue
1620
1657
  ),
1621
1658
  /* @__PURE__ */ React4.createElement(IconButton2, { onClick: () => copyToClipboard(newKeyValue) }, /* @__PURE__ */ React4.createElement(ContentCopy2, null))
1622
- ), newKeySnippets && /* @__PURE__ */ React4.createElement(Box4, { mt: 3 }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React4.createElement(Code, { fontSize: "small", color: "action" }), /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2" }, "Start calling the proxy \u2014 paste and run")), /* @__PURE__ */ React4.createElement(SnippetTabs, { snippets: newKeySnippets, model: newKeyModel, onCopy: copyToClipboard }))) : /* @__PURE__ */ React4.createElement(Box4, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, generateError && /* @__PURE__ */ React4.createElement(Alert, { severity: "error", onClose: () => setGenerateError(null) }, generateError), /* @__PURE__ */ React4.createElement(
1659
+ ), newKeySnippets && /* @__PURE__ */ React4.createElement(Box4, { mt: 2 }, /* @__PURE__ */ React4.createElement(Typography4, { variant: "caption", color: "text.secondary", display: "block", gutterBottom: true }, "Public endpoint \u2014 paste into any tool's base URL / API base field"), /* @__PURE__ */ React4.createElement(
1660
+ Box4,
1661
+ {
1662
+ display: "flex",
1663
+ alignItems: "center",
1664
+ gap: 1,
1665
+ p: 1.5,
1666
+ sx: {
1667
+ backgroundColor: "action.hover",
1668
+ border: "1px solid",
1669
+ borderColor: "divider",
1670
+ borderRadius: 1
1671
+ }
1672
+ },
1673
+ /* @__PURE__ */ React4.createElement(
1674
+ Typography4,
1675
+ {
1676
+ component: "code",
1677
+ color: "text.primary",
1678
+ sx: { fontFamily: "monospace", fontSize: 13, wordBreak: "break-all", flex: 1 }
1679
+ },
1680
+ newKeySnippets.publicEndpoint
1681
+ ),
1682
+ /* @__PURE__ */ React4.createElement(IconButton2, { size: "small", onClick: () => copyToClipboard(newKeySnippets.publicEndpoint) }, /* @__PURE__ */ React4.createElement(ContentCopy2, { fontSize: "small" }))
1683
+ )), newKeySnippets && /* @__PURE__ */ React4.createElement(Box4, { mt: 3 }, /* @__PURE__ */ React4.createElement(Box4, { display: "flex", alignItems: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React4.createElement(Code, { fontSize: "small", color: "action" }), /* @__PURE__ */ React4.createElement(Typography4, { variant: "subtitle2" }, "Start calling the proxy \u2014 paste and run")), /* @__PURE__ */ React4.createElement(SnippetTabs, { snippets: newKeySnippets, model: newKeyModel, onCopy: copyToClipboard }))) : /* @__PURE__ */ React4.createElement(Box4, { display: "flex", flexDirection: "column", gap: 2, mt: 1 }, generateError && /* @__PURE__ */ React4.createElement(Alert, { severity: "error", onClose: () => setGenerateError(null) }, generateError), /* @__PURE__ */ React4.createElement(
1623
1684
  TextField2,
1624
1685
  {
1625
1686
  label: "Alias",