refactor: gitignore generated openapi files, keep only alias types

- Filter schema to management endpoints only (182 paths vs 510),
  reducing api.generated.ts from 50k to 18.5k lines
- Gitignore openapi.json and api.generated.ts (intermediate artifacts)
- Only committed files are the script, alias types, and package.json
This commit is contained in:
Ryan Crabbe 2026-03-23 13:56:46 -07:00
parent d7123aef05
commit e7893c07d7
4 changed files with 133 additions and 22 deletions

3
.gitignore vendored
View file

@ -103,6 +103,5 @@ STABILIZATION_TODO.md
**/*.storageState.json
**/coverage
# Generated OpenAPI types (regenerate with: cd ui/litellm-dashboard && npm run generate:all)
ui/litellm-dashboard/openapi.json
# Generated OpenAPI types (regenerate with: cd ui/litellm-dashboard && npm run generate:types)
ui/litellm-dashboard/src/types/api.generated.ts

View file

@ -1,25 +1,106 @@
"""
Export the LiteLLM proxy's OpenAPI schema to a JSON file.
Generate TypeScript types for LiteLLM management endpoints.
Usage:
python scripts/export_openapi.py [output_path]
python scripts/export_openapi.py
Defaults to ui/litellm-dashboard/openapi.json if no path is given.
Does NOT require a running server imports the FastAPI app and calls app.openapi().
Filters to management endpoints only (no passthrough/LLM API routes).
Produces ui/litellm-dashboard/src/types/api.generated.ts (gitignored).
"""
import json
import re
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Any
# Tags for routes the UI dashboard actually uses.
# Routes without these tags (passthrough, OpenAI-compatible API, etc.) are excluded.
INCLUDED_TAGS = {
# Management endpoints (UI dashboard)
"access group management",
"budget management",
"credential management",
"email management",
"key management",
"model management",
"organization management",
"policy management",
"project management",
"tag management",
"team management",
"tool management",
"vector store management",
"Settings",
"SSO Settings",
"UI Settings",
"UI Theme Settings",
"Router Settings",
"[beta] MCP",
"[beta] Agents",
"[beta] A2A Agents",
"health",
# LLM API endpoints
"chat/completions",
"completions",
"responses",
}
def filter_management_routes(schema: dict[str, Any]) -> dict[str, Any]:
"""
Remove all routes that aren't management endpoints.
Also prunes unused schemas from components.
"""
filtered_paths: dict[str, Any] = {}
for path, path_obj in schema.get("paths", {}).items():
filtered_methods: dict[str, Any] = {}
for method, method_obj in path_obj.items():
if not isinstance(method_obj, dict):
filtered_methods[method] = method_obj
continue
tags = set(method_obj.get("tags", []))
if tags & INCLUDED_TAGS:
filtered_methods[method] = method_obj
if any(isinstance(v, dict) for v in filtered_methods.values()):
filtered_paths[path] = filtered_methods
schema["paths"] = filtered_paths
# Prune schemas not referenced by the remaining paths
paths_json = json.dumps(filtered_paths)
referenced = set(re.findall(r'#/components/schemas/([\w.\-]+)', paths_json))
# Schemas can reference other schemas, so resolve transitively
all_schemas = schema.get("components", {}).get("schemas", {})
resolved: set[str] = set()
to_resolve = list(referenced)
while to_resolve:
name = to_resolve.pop()
if name in resolved:
continue
resolved.add(name)
if name in all_schemas:
nested_json = json.dumps(all_schemas[name])
for ref in re.findall(r'#/components/schemas/([\w.\-]+)', nested_json):
if ref not in resolved:
to_resolve.append(ref)
schema["components"]["schemas"] = {
k: v for k, v in all_schemas.items() if k in resolved
}
return schema
def deduplicate_operation_ids(schema: dict[str, Any]) -> dict[str, Any]:
"""
FastAPI pass-through proxy routes generate duplicate operationIds when
multiple HTTP methods share the same path pattern. This causes
openapi-typescript to emit duplicate TS identifiers.
multiple HTTP methods share the same path pattern.
Fix: append _2, _3, etc. to duplicates.
"""
seen: dict[str, int] = {}
@ -38,24 +119,57 @@ def deduplicate_operation_ids(schema: dict[str, Any]) -> dict[str, Any]:
def main() -> None:
# Determine output path
repo_root = Path(__file__).resolve().parent.parent
default_output = repo_root / "ui" / "litellm-dashboard" / "openapi.json"
output_path = Path(sys.argv[1]) if len(sys.argv) > 1 else default_output
ui_dir = repo_root / "ui" / "litellm-dashboard"
output_ts = ui_dir / "src" / "types" / "api.generated.ts"
# Import the FastAPI app — this triggers module-level setup including
# the assignment of app.openapi = get_openapi_schema
# Step 1: Export OpenAPI schema from FastAPI app
print("Exporting OpenAPI schema from FastAPI app...")
from litellm.proxy.proxy_server import app
schema = app.openapi()
full_paths = len(schema.get("paths", {}))
full_schemas = len(schema.get("components", {}).get("schemas", {}))
# Step 2: Filter to management endpoints only
schema = filter_management_routes(schema)
schema = deduplicate_operation_ids(schema)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps(schema, indent=2) + "\n")
mgmt_paths = len(schema.get("paths", {}))
mgmt_schemas = len(schema.get("components", {}).get("schemas", {}))
print(f"OpenAPI schema written to {output_path}")
print(f" paths: {len(schema.get('paths', {}))}")
print(f" schemas: {len(schema.get('components', {}).get('schemas', {}))}")
print(f" Full schema: {full_paths} paths, {full_schemas} schemas")
print(f" After filtering: {mgmt_paths} paths, {mgmt_schemas} schemas")
# Step 3: Run openapi-typescript to generate TS types
with tempfile.NamedTemporaryFile(
mode="w", suffix=".json", delete=False
) as tmp:
json.dump(schema, tmp, indent=2)
tmp_path = tmp.name
print(f"Generating TypeScript types -> {output_ts.relative_to(repo_root)}")
output_ts.parent.mkdir(parents=True, exist_ok=True)
result = subprocess.run(
["npx", "openapi-typescript", tmp_path, "-o", str(output_ts)],
cwd=str(ui_dir),
capture_output=True,
text=True,
)
Path(tmp_path).unlink(missing_ok=True)
if result.returncode != 0:
print(f"openapi-typescript failed:\n{result.stderr}", file=sys.stderr)
sys.exit(1)
# Report final size
lines = output_ts.read_text().count("\n")
size_kb = output_ts.stat().st_size / 1024
print(f" Output: {lines} lines, {size_kb:.0f} KB")
print("Done.")
if __name__ == "__main__":

View file

@ -18,9 +18,7 @@
"e2e:ui": "playwright test --ui --config e2e_tests/playwright.config.ts",
"knip": "knip",
"knip:fix": "knip --fix",
"generate:schema": "python ../../scripts/export_openapi.py",
"generate:types": "openapi-typescript ./openapi.json -o src/types/api.generated.ts",
"generate:all": "npm run generate:schema && npm run generate:types"
"generate:types": "python ../../scripts/export_openapi.py"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.54.0",

View file

@ -2,7 +2,7 @@
* Key management types derived from the backend OpenAPI schema.
*
* Depends on api.generated.ts which is gitignored.
* Run `npm run generate:all` from ui/litellm-dashboard/ to generate it.
* Run `npm run generate:types` from ui/litellm-dashboard/ to generate it.
*/
import type { components, paths } from "./api.generated";