From eb6c917a2d99870d37f89b3f80c6efa219fc9353 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 28 Feb 2026 23:43:21 +0000 Subject: [PATCH 1/3] Add AGENTS.md with Cursor Cloud specific instructions Co-authored-by: Ishaan Jaff --- AGENTS.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..75603ef --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,31 @@ +# AGENTS.md + +## Cursor Cloud specific instructions + +This is a **Svelte 4 + Vite 5 + TypeScript** client-side SPA (no backend). All data is fetched at runtime from public GitHub APIs. + +### Running the dev server + +```bash +npm run dev +``` + +The app runs on `http://localhost:5173` by default. + +### Available scripts (see `package.json`) + +| Command | Purpose | +|---|---| +| `npm run dev` | Start Vite dev server | +| `npm run build` | Production build to `dist/` | +| `npm run check` | Run `svelte-check` (TypeScript + Svelte diagnostics) | +| `npm run format` | Run Prettier with `--write` | + +### Known issues + +- `npm run check` reports 9 pre-existing TypeScript errors (type-casting issues in `App.svelte`, `Providers.svelte`, `Main.svelte`). These do **not** block the build or dev server. +- `npx prettier --check .` reports 10 files with formatting issues (pre-existing). + +### Environment variables (optional) + +- `VITE_MIXPANEL_TOKEN` — enables Mixpanel analytics. The app works fully without it. From 58abe5bdf47cd6d158a8f79d174fd1ab3d976ae3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 1 Mar 2026 00:10:28 +0000 Subject: [PATCH 2/3] Redesign UI for enterprise-grade polish Major visual overhaul to make the LiteLLM model catalog feel like a premium dev tool (Stripe docs / Linear quality). Typography & Foundation: - Replace PicoCSS with Inter font for body text - Add JetBrains Mono for code/model names - Clean CSS reset with proper box-sizing - Refined color palette with Slate-based neutrals Navigation: - Add gradient accent bar at page top - Sticky frosted-glass nav with backdrop blur - Active tab underline indicator - Purple 'Request' CTA button - GitHub icon in nav Models Page (biggest impact): - New hero: 'The Most Comprehensive AI Model Catalog' - GitHub stars badge - Trust logos section (NASA, Adobe, Netflix, Stripe, NVIDIA, Cruise) - Sortable table columns (click headers) - Mode badges (Chat, Image Gen, etc.) inline with model names - Structured expanded row replacing raw JSON dump: - Pricing cards (Input, Output, Cache Read, Cache Write) - Model info panel (provider, mode, token limits) - Feature checklist (Function Calling, Vision, JSON, etc.) - Python quick-start code snippet with syntax highlighting - Report incorrect data link - Search clear button, result count, clear-all-filters link - Skeleton loading states Stats Section: - Bolder typography (800 weight, larger size) - Hover with primary color border ring - Skeleton shimmer during load Providers Page: - Tighter card spacing, refined typography - JetBrains Mono for endpoint names - External link icons on provider cards - Better active endpoint highlighting Cookbook Page: - Guide count badge - Relative timestamps (1mo ago, 2w ago) - Refined card hover states - Streamlined submit modal Guardrails Page: - Community-contributed badge - Chevron expand indicators - Category labels in footer - Tighter spacing and refined cards Global: - Consistent 10px border-radius system - Refined shadow scale (sm/md/lg) - Dark mode improvements with deeper backgrounds - Smoother transitions (0.15s) Co-authored-by: Ishaan Jaff --- index.html | 19 +- src/App.svelte | 982 +++++++++++++++++++++++++++--------- src/Cookbook.svelte | 685 ++++++------------------- src/Guardrails.svelte | 432 +++++----------- src/Main.svelte | 402 +++++++-------- src/ProviderDropdown.svelte | 14 +- src/Providers.svelte | 453 +++++------------ 7 files changed, 1371 insertions(+), 1616 deletions(-) diff --git a/index.html b/index.html index f3a2f55..5cba4c5 100644 --- a/index.html +++ b/index.html @@ -5,10 +5,21 @@ LiteLLM Providers & Models - + + + + +
diff --git a/src/App.svelte b/src/App.svelte index 2d8ab84..5bfb206 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -28,6 +28,13 @@ let maxInputTokens: number | null = null; let maxOutputTokens: number | null = null; + // Sorting state + let sortColumn: string = ""; + let sortDirection: "asc" | "desc" = "asc"; + + // Copy toast + let copiedModel = ""; + onMount(() => { const urlParams = new URLSearchParams(window.location.search); query = urlParams.get("q") ?? ""; @@ -56,8 +63,6 @@ ); providers = [...new Set(items.map((i) => i.litellm_provider))]; - - // sort providers alphabetically providers.sort(); index = new Fuse(items, { @@ -72,7 +77,6 @@ ], }); - // Initialize results with all items results = items.map((item, refIndex) => ({ item, refIndex })); loading = false; }); @@ -87,7 +91,6 @@ }; }; - // Debounced search tracking const trackSearchDebounced = debounce((query: string, provider: string, resultsCount: number) => { if (query) { trackSearch(query, provider, resultsCount); @@ -145,27 +148,18 @@ We also need to update [${RESOURCE_BACKUP_NAME}](https://github.com/${REPO_FULL_ function copyToClipboard(text: string) { navigator.clipboard.writeText(text).then(() => { - // You could add a toast notification here if desired + copiedModel = text; + setTimeout(() => { copiedModel = ""; }, 1500); }); } - /** - * Get the display name for a model, adding provider prefix when needed. - * This helps users understand which auth method is required. - * e.g., "gemini-2.0-flash" with provider "vertex_ai-language-models" -> "vertex_ai/gemini-2.0-flash" - */ function getDisplayModelName(name: string, litellm_provider: string): string { - // If the model name already has a provider prefix, return as-is if (name.includes('/')) { return name; } - - // Add prefix for vertex_ai models so users know they need GCP credentials - // Provider can be "vertex_ai", "vertex_ai-language-models", etc. if (litellm_provider && litellm_provider.startsWith('vertex_ai')) { return `vertex_ai/${name}`; } - return name; } @@ -175,7 +169,78 @@ We also need to update [${RESOURCE_BACKUP_NAME}](https://github.com/${REPO_FULL_ } else { expandedRows.add(name); } - expandedRows = expandedRows; // Trigger reactivity + expandedRows = expandedRows; + } + + function handleSort(column: string) { + if (sortColumn === column) { + sortDirection = sortDirection === "asc" ? "desc" : "asc"; + } else { + sortColumn = column; + sortDirection = "asc"; + } + applySorting(); + } + + function getSortValue(item: any, column: string): number { + switch (column) { + case "context": return item.max_input_tokens || 0; + case "input": return item.input_cost_per_token || 0; + case "output": return item.output_cost_per_token || 0; + case "cache_read": return item.cache_read_input_token_cost || 0; + case "cache_write": return item.cache_creation_input_token_cost || 0; + default: return 0; + } + } + + function applySorting() { + if (!sortColumn) return; + results = [...results].sort((a, b) => { + const aVal = getSortValue(a.item, sortColumn); + const bVal = getSortValue(b.item, sortColumn); + return sortDirection === "asc" ? aVal - bVal : bVal - aVal; + }); + } + + function formatCost(costPerToken: number | undefined): string { + if (!costPerToken) return "—"; + const perMillion = costPerToken * 1000000; + if (perMillion < 0.01) return "<$0.01"; + return "$" + perMillion.toFixed(2); + } + + function formatContext(tokens: number | undefined): string { + if (!tokens || tokens <= 0) return "—"; + if (tokens >= 1000000) return (tokens / 1000000).toFixed(0) + "M"; + if (tokens >= 1000) return (tokens / 1000).toFixed(0) + "K"; + return tokens.toString(); + } + + function getFeatureBadges(item: any): string[] { + const badges: string[] = []; + if (item.supports_function_calling) badges.push("Functions"); + if (item.supports_vision) badges.push("Vision"); + if (item.supports_response_schema) badges.push("JSON"); + if (item.supports_tool_choice) badges.push("Tools"); + if (item.supports_parallel_function_calling) badges.push("Parallel"); + if (item.supports_audio_input) badges.push("Audio"); + if (item.supports_prompt_caching) badges.push("Caching"); + return badges; + } + + function getModeLabel(mode: string | undefined): string { + if (!mode) return ""; + const labels: Record = { + "chat": "Chat", + "completion": "Completion", + "embedding": "Embedding", + "image_generation": "Image Gen", + "audio_transcription": "Transcription", + "audio_speech": "TTS", + "moderation": "Moderation", + "rerank": "Rerank", + }; + return labels[mode] || mode; } function filterResults( @@ -187,10 +252,8 @@ We also need to update [${RESOURCE_BACKUP_NAME}](https://github.com/${REPO_FULL_ if (index) { let filteredResults: Item[]; - // Get all items from the index const allItems = index["_docs"] as Item[]; - // Filter by provider and max_input_tokens and max_output_tokens filteredResults = allItems.filter( (item) => (!selectedProvider || item.litellm_provider === selectedProvider) && @@ -202,9 +265,7 @@ We also need to update [${RESOURCE_BACKUP_NAME}](https://github.com/${REPO_FULL_ item.max_output_tokens >= maxOutputTokens)), ); - // Then, apply search query if it's not empty if (query) { - // Create a new Fuse instance with the filtered results const filteredIndex = new Fuse(filteredResults, { threshold: 0.3, keys: [ @@ -221,11 +282,11 @@ We also need to update [${RESOURCE_BACKUP_NAME}](https://github.com/${REPO_FULL_ filteredResults = searchResults.map((result) => result.item); } - // Map the filtered results to the ResultItem format results = filteredResults.map((item, refIndex) => ({ item, refIndex })); loading = false; - // Track search event (debounced) + if (sortColumn) applySorting(); + trackSearchDebounced(query, selectedProvider, results.length); } } @@ -234,9 +295,13 @@ We also need to update [${RESOURCE_BACKUP_NAME}](https://github.com/${REPO_FULL_
-

Browse LiteLLM Models

+
+ + Open-source AI Gateway — 27K+ GitHub Stars +
+

The Most Comprehensive
AI Model Catalog

- A catalog of AI models with pricing and context window information, powered by LiteLLM's comprehensive model database. + Compare pricing, context windows, and features for 2,600+ models across 140+ providers. Powered by LiteLLM's open-source model database.

@@ -246,7 +311,7 @@ We also need to update [${RESOURCE_BACKUP_NAME}](https://github.com/${REPO_FULL_ rel="noopener noreferrer" class="btn btn-primary" > - + View on GitHub @@ -262,11 +327,24 @@ We also need to update [${RESOURCE_BACKUP_NAME}](https://github.com/${REPO_FULL_
+ +
+

Powering AI infrastructure at

+
+ + + + + + +
+
+
- + @@ -277,9 +355,14 @@ We also need to update [${RESOURCE_BACKUP_NAME}](https://github.com/${REPO_FULL_ autocomplete="off" name="query" aria-label="Search models" - placeholder="Search model..." + placeholder="Search models... (e.g., gpt-4o, claude-3.5, gemini)" class="search-input" /> + {#if query} + + {/if}
@@ -309,14 +392,37 @@ We also need to update [${RESOURCE_BACKUP_NAME}](https://github.com/${REPO_FULL_ />
+ + {#if !loading} +
+ {results.length.toLocaleString()} models + {#if query || selectedProvider || maxInputTokens || maxOutputTokens} + + {/if} +
+ {/if} {#if loading} - +
+
+ {#each [1,2,3,4,5,6,7,8] as _} +
+
+
+
+
+
+ {/each} +
+
{:else} {#if query != "" && results.length < 12} {/if} @@ -324,20 +430,35 @@ We also need to update [${RESOURCE_BACKUP_NAME}](https://github.com/${REPO_FULL_ - - - - - - + + + + + + - {#each results as { item: { name, mode, litellm_provider, max_input_tokens, max_output_tokens, input_cost_per_token, output_cost_per_token, cache_creation_input_token_cost, cache_read_input_token_cost, ...data } } (name)} + {#each results as { item: { name, mode, litellm_provider, max_input_tokens, max_output_tokens, input_cost_per_token, output_cost_per_token, cache_creation_input_token_cost, cache_read_input_token_cost, supports_function_calling, supports_vision, supports_response_schema, supports_tool_choice, supports_parallel_function_calling, supports_audio_input, supports_prompt_caching, ...data } } (name)} toggleRow(name)}> - - - - - + + + + + {#if expandedRows.has(name)} @@ -397,12 +622,6 @@ We also need to update [${RESOURCE_BACKUP_NAME}](https://github.com/${REPO_FULL_ diff --git a/src/Cookbook.svelte b/src/Cookbook.svelte index 9c53fa1..42a56ad 100644 --- a/src/Cookbook.svelte +++ b/src/Cookbook.svelte @@ -19,9 +19,8 @@ let submitError: string | null = null; const INDEX_URL = "https://raw.githubusercontent.com/BerriAI/litellm/refs/heads/main/cookbook/ai_coding_tool_guides/index.json"; - const WEBHOOK_URL = "https://hooks.zapier.com/hooks/catch/22699356/ugq2dbo/"; // Replace with actual webhook URL + const WEBHOOK_URL = "https://hooks.zapier.com/hooks/catch/22699356/ugq2dbo/"; - // Form state let formData = { title: "", description: "", @@ -43,15 +42,12 @@ const data = await response.json(); - // The API returns an array of guide objects if (Array.isArray(data)) { guides = data; } else { - // Fallback: wrap single object in array for backwards compatibility guides = [data]; } - // Sort by date (newest first) guides = guides.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); loading = false; @@ -63,7 +59,6 @@ } function openGuide(guide: Guide) { - // Open the guide URL in a new tab window.open(guide.url, '_blank', 'noopener,noreferrer'); } @@ -71,11 +66,24 @@ const date = new Date(dateString); return date.toLocaleDateString("en-US", { year: "numeric", - month: "long", + month: "short", day: "numeric", }); } + function getRelativeDate(dateString: string): string { + const date = new Date(dateString); + const now = new Date(); + const diff = now.getTime() - date.getTime(); + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + if (days === 0) return "Today"; + if (days === 1) return "Yesterday"; + if (days < 7) return `${days}d ago`; + if (days < 30) return `${Math.floor(days / 7)}w ago`; + if (days < 365) return `${Math.floor(days / 30)}mo ago`; + return `${Math.floor(days / 365)}y ago`; + } + function openSubmitModal() { showSubmitModal = true; submitSuccess = false; @@ -106,17 +114,14 @@ submitLoading = true; submitError = null; - // Validate form if (!formData.title || !formData.description || !formData.url) { submitError = "Please fill in all required fields"; submitLoading = false; return; } - // Parse tags (comma-separated) const tagsArray = formData.tags.split(",").map(tag => tag.trim()).filter(tag => tag); - // Create guide object with timestamp const guide = { title: formData.title, description: formData.description, @@ -129,7 +134,6 @@ console.log("Submitting guide to webhook:", guide); - // Submit to webhook const response = await fetch(WEBHOOK_URL, { method: "POST", mode: "no-cors", @@ -139,13 +143,11 @@ body: JSON.stringify(guide), }); - // With no-cors mode, we can't read the response, so we assume success console.log("Webhook response:", response); submitSuccess = true; submitLoading = false; - // Close modal after 2 seconds setTimeout(() => { closeSubmitModal(); }, 2000); @@ -165,7 +167,7 @@ {#if loading}
-

Loading cookbook guides...

+

Loading guides...

{:else if error}
@@ -174,13 +176,17 @@
{:else} -
+
+ + {guides.length} guides available +

AI Tool Guides

- Learn how to integrate LiteLLM with popular AI tools + Learn how to integrate LiteLLM with popular AI tools and coding assistants

@@ -191,18 +197,20 @@ {:else}
- {#each guides as guide} + {#each guides as guide, i}
openGuide(guide)} on:keydown={(e) => e.key === 'Enter' && openGuide(guide)} role="button" tabindex="0"> -

{guide.title}

-

{guide.description}

+
+

{guide.title}

+

{guide.description}

+
@@ -212,7 +220,6 @@ {/if} {/if} - {#if showSubmitModal} @@ -221,127 +228,56 @@ {#if submitSuccess}
- - - +

Guide Submitted!

Thank you for your contribution. We'll review it shortly.

{:else}
- - + +
-
- - - Keep it concise and informative + +
-
- - + +
-
- - - Separate multiple tags with commas + +
- -
- - -
- -
- - +
+
+ + +
+
+ + +
{#if submitError} -
- {submitError} -
+
{submitError}
{/if}
- +
@@ -353,516 +289,219 @@ - diff --git a/src/Guardrails.svelte b/src/Guardrails.svelte index 367f7e1..9bd96f9 100644 --- a/src/Guardrails.svelte +++ b/src/Guardrails.svelte @@ -51,7 +51,6 @@ initializeData(items); }) .catch(() => { - // Fallback to local file fetch("/guardrails.json") .then((res) => res.json()) .then((data) => { @@ -162,47 +161,35 @@
-
+
+ + Community-contributed +

Custom Code Guardrails

- Community-contributed guardrails for LiteLLM. Copy, customize, and deploy to protect your LLM applications. + Copy, customize, and deploy guardrails to protect your LLM applications.

-
- + - +
ModelContextInput TokensOutput TokensCache Read TokensCache Write TokensModel handleSort("context")}> + Context + + handleSort("input")}> + Input $/M + + handleSort("output")}> + Output $/M + + handleSort("cache_read")}> + Cache Read + + handleSort("cache_write")}> + Cache Write + +
- +
@@ -360,31 +481,135 @@ We also need to update [${RESOURCE_BACKUP_NAME}](https://github.com/${REPO_FULL_
{/if}
- {getDisplayModelName(name, litellm_provider)} +
+ {getDisplayModelName(name, litellm_provider)} + {#if mode} + {getModeLabel(mode)} + {/if} +
{max_input_tokens && max_input_tokens > 0 && (max_input_tokens / 1000).toFixed(0) !== '0' ? (max_input_tokens >= 1000000 ? (max_input_tokens / 1000000).toFixed(0) + 'M' : (max_input_tokens / 1000).toFixed(0) + 'K') : '—'}{input_cost_per_token ? '$' + (input_cost_per_token * 1000000).toFixed(2) + '/M' : '—'}{output_cost_per_token ? '$' + (output_cost_per_token * 1000000).toFixed(2) + '/M' : '—'}{cache_read_input_token_cost ? '$' + (cache_read_input_token_cost * 1000000).toFixed(2) + '/M' : '—'}{cache_creation_input_token_cost ? '$' + (cache_creation_input_token_cost * 1000000).toFixed(2) + '/M' : '—'}{formatContext(max_input_tokens)}{formatCost(input_cost_per_token)}{formatCost(output_cost_per_token)}{formatCost(cache_read_input_token_cost)}{formatCost(cache_creation_input_token_cost)}
-
-
{JSON.stringify({ name, mode, litellm_provider, max_input_tokens, max_output_tokens, input_cost_per_token, output_cost_per_token, cache_creation_input_token_cost, cache_read_input_token_cost, ...data }, null, 2)}
+
+
+ +
+

Pricing per 1M tokens

+
+
+ Input + {formatCost(input_cost_per_token)} +
+
+ Output + {formatCost(output_cost_per_token)} +
+
+ Cache Read + {formatCost(cache_read_input_token_cost)} +
+
+ Cache Write + {formatCost(cache_creation_input_token_cost)} +
+
+
+ + +
+

Model Info

+
+
+ Provider + {litellm_provider || "—"} +
+
+ Mode + {mode ? getModeLabel(mode) : "—"} +
+
+ Max Input + {max_input_tokens ? max_input_tokens.toLocaleString() + " tokens" : "—"} +
+
+ Max Output + {max_output_tokens ? max_output_tokens.toLocaleString() + " tokens" : "—"} +
+
+
+ + +
+

Features

+
+ {#each [ + { key: supports_function_calling, label: "Function Calling" }, + { key: supports_vision, label: "Vision" }, + { key: supports_response_schema, label: "JSON Mode" }, + { key: supports_tool_choice, label: "Tool Choice" }, + { key: supports_parallel_function_calling, label: "Parallel Calls" }, + { key: supports_audio_input, label: "Audio Input" }, + { key: supports_prompt_caching, label: "Prompt Caching" }, + ] as feature} +
+ {#if feature.key} + + {:else} + + {/if} + {feature.label} +
+ {/each} +
+
+
+ + +
+
+

Quick Start

+ +
+
from litellm import completion
+
+response = completion(
+    model="{getDisplayModelName(name, litellm_provider)}",
+    messages=[{`{`}"role": "user", "content": "Hello!"{`}`}]
+)
+
+ + +