From e27d7d98c80698192b4454bbbfb0781bb7f30b42 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 15 Oct 2026 21:16:21 +0000 Subject: [PATCH] fix(ui): keep docs index entries without a description Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com> --- .../components/liteadmin/docsSearch.test.ts | 6 ++ .../src/components/liteadmin/docsSearch.ts | 64 ++++++++++--------- 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/ui/litellm-dashboard/src/components/liteadmin/docsSearch.test.ts b/ui/litellm-dashboard/src/components/liteadmin/docsSearch.test.ts index 65a9eb37a91..6ba49010bf6 100644 --- a/ui/litellm-dashboard/src/components/liteadmin/docsSearch.test.ts +++ b/ui/litellm-dashboard/src/components/liteadmin/docsSearch.test.ts @@ -13,6 +13,7 @@ const INDEX_FIXTURE = `# LiteLLM Some prose that is not an entry. - [Setting Team Budgets](https://docs.litellm.ai/docs/proxy/team_budgets.md): Give each team a spend budget +- [How Model Access Works](https://docs.litellm.ai/docs/proxy/model_access_guide.md) - [Vector stores](https://docs.litellm.ai/docs/vector_stores/index.md): Store and search embeddings - [Docs home](https://docs.litellm.ai/docs/index.md): Documentation index - [Blog](https://litellm.ai/blog): Not on the docs site @@ -28,6 +29,11 @@ describe("parseDocsIndex", () => { url: "https://docs.litellm.ai/docs/proxy/team_budgets", description: "Give each team a spend budget", }, + { + title: "How Model Access Works", + url: "https://docs.litellm.ai/docs/proxy/model_access_guide", + description: "", + }, { title: "Vector stores", url: "https://docs.litellm.ai/docs/vector_stores", diff --git a/ui/litellm-dashboard/src/components/liteadmin/docsSearch.ts b/ui/litellm-dashboard/src/components/liteadmin/docsSearch.ts index 2e33fa1cbe2..037276125d3 100644 --- a/ui/litellm-dashboard/src/components/liteadmin/docsSearch.ts +++ b/ui/litellm-dashboard/src/components/liteadmin/docsSearch.ts @@ -11,28 +11,27 @@ export interface DocEntry { readonly description: string; } -const ENTRY_PATTERN = /^- \[([^\]]+)\]\(([^)\s]+)\)\s*:\s*(.*)$/; +const ENTRY_PATTERN = /^- \[([^\]]+)\]\(([^)\s]+)\)\s*(?::\s*(.*))?$/; + +const normalizeDocsUrl = (rawUrl: string): string | null => { + try { + const parsed = new URL(rawUrl); + if (parsed.protocol !== "https:" || parsed.host !== "docs.litellm.ai") return null; + } catch { + return null; + } + const noMarkdown = rawUrl.replace(/\.md$/, ""); + if (!noMarkdown.endsWith("/index")) return noMarkdown; + const parent = noMarkdown.slice(0, -"/index".length); + return parent === "https://docs.litellm.ai/docs" ? `${parent}/` : parent; +}; export function parseDocsIndex(text: string): readonly DocEntry[] { return text.split("\n").flatMap((line) => { const match = ENTRY_PATTERN.exec(line.trim()); - if (!match) return []; - const [, title, rawUrl, description] = match; - let host: string; - try { - const parsed = new URL(rawUrl); - host = parsed.host; - if (parsed.protocol !== "https:") return []; - } catch { - return []; - } - if (host !== "docs.litellm.ai") return []; - let url = rawUrl.replace(/\.md$/, ""); - if (url.endsWith("/index")) { - url = url.slice(0, -"/index".length); - if (url === "https://docs.litellm.ai/docs") url += "/"; - } - return [{ title, url, description }]; + const url = match ? normalizeDocsUrl(match[2]) : null; + if (!match || url === null) return []; + return [{ title: match[1], url, description: match[3] ?? "" }]; }); } @@ -104,7 +103,7 @@ const bundledEntries: readonly DocEntry[] = [ let docsIndexPromise: Promise | null = null; -function loadDocsIndex(signal: AbortSignal, fetchIndex: () => Promise): Promise { +function loadDocsIndex(fetchIndex: () => Promise): Promise { docsIndexPromise ??= fetchIndex() .then(parseDocsIndex) .catch((error: unknown) => { @@ -122,19 +121,24 @@ export interface DocsSearchToolOptions { const truncate = (text: string): string => (text.length > 200 ? `${text.slice(0, 200)}…` : text); +const loadCorpus = async ( + options: DocsSearchToolOptions, + fetchIndex: (signal: AbortSignal) => Promise, +): Promise<{ entries: readonly DocEntry[]; source: "docs" | "bundled" }> => { + try { + const fetched = await loadDocsIndex(() => fetchIndex(options.signal)); + return { + entries: [...fetched, ...bundledEntries.filter((b) => !fetched.some((f) => f.url === b.url))], + source: "docs", + }; + } catch { + return { entries: bundledEntries, source: "bundled" }; + } +}; + export async function runDocsSearch(options: DocsSearchToolOptions, query: string) { options.beforeTool(); - const fetchIndex = options.fetchIndex ?? fetchDocsIndex; - let entries: readonly DocEntry[]; - let source: "docs" | "bundled"; - try { - const fetched = await loadDocsIndex(options.signal, () => fetchIndex(options.signal)); - entries = [...fetched, ...bundledEntries.filter((b) => !fetched.some((f) => f.url === b.url))]; - source = "docs"; - } catch { - entries = bundledEntries; - source = "bundled"; - } + const { entries, source } = await loadCorpus(options, options.fetchIndex ?? fetchDocsIndex); return { operation: "docs_search", success: true,