feat: package distribution with PyPI support, embedded UI, Dockerfile, and CLI enhancements

This commit is contained in:
Vardhman Gupta
2026-09-11 13:14:33 +05:30
parent 02e0f1803c
commit 2e013188b1
7 changed files with 237 additions and 16 deletions
+3
View File
@@ -21,11 +21,14 @@ model_router.db-journal
.pytest_cache/
.coverage
htmlcov/
build/
*.egg-info/
# Node / Frontend
node_modules/
dist/
.dist/
backend/app/static/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+33
View File
@@ -0,0 +1,33 @@
# Stage 1: Build React Frontend UI
FROM node:20-alpine AS frontend-builder
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build
# Stage 2: Python Backend Runtime
FROM python:3.11-slim
WORKDIR /app
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PORT=8000
ENV HOST=0.0.0.0
# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy backend application code
COPY backend/ ./backend/
# Copy compiled static frontend into backend/app/static
COPY --from=frontend-builder /app/frontend/dist ./backend/app/static
EXPOSE 8000
CMD ["python", "backend/main.py"]
+41 -7
View File
@@ -1,5 +1,9 @@
# Model Router
[![PyPI - Version](https://img.shields.io/pypi/v/model-router-cli?logo=pypi&logoColor=white&color=blue)](https://pypi.org/project/model-router-cli/)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/model-router-cli?logo=python&logoColor=white)](https://pypi.org/project/model-router-cli/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
Intelligent, explainable, cost- and latency-aware LLM request routing platform and AI Traffic Control Room.
---
@@ -66,9 +70,32 @@ The platform is designed local-first, allowing full local development and testin
- Node.js 18+ and npm
- (Optional) [Ollama](https://ollama.com/) for local model inference
## 📦 Quick Installation (via PyPI)
Install Model Router and its embedded **AI Traffic Control Room** dashboard directly from PyPI:
```bash
pip install model-router-cli
```
### Quick Commands:
```bash
# Run system diagnostics & environment checks
modelrouter doctor
# Launch the Traffic Control Room Dashboard & API Gateway in your browser
modelrouter ui --open-browser
# Route a prompt and inspect the explainability decision (Dry Run)
modelrouter route "Write an optimized async task worker in Python"
# Route and execute inference through the optimal provider
modelrouter run "Explain the difference between zero-shot and few-shot prompting"
```
---
## Installation & Setup
## Installation & Setup (from Source)
### 1. Clone the Repository
```bash
@@ -152,23 +179,29 @@ npm run dev
## CLI Usage
The built-in Typer CLI provides terminal commands for inspection, diagnostics, and testing:
The built-in Typer CLI provides terminal commands for inspection, diagnostics, and testing (use `modelrouter` if installed via pip, or `python backend/app/cli/main.py` from source):
```bash
# Launch the AI Traffic Control Room Web UI and API Gateway
modelrouter ui --open-browser
# Run system diagnostics & provider health checks
python backend/app/cli/main.py doctor
modelrouter doctor
# Inspect routing decision for a prompt without executing (Dry Run)
python backend/app/cli/main.py route "Write a Python function to parse JSON"
modelrouter route "Write a Python function to parse JSON"
# Route and execute a query through the selected model
python backend/app/cli/main.py run "Debug this distributed async deadlock in worker pool"
modelrouter run "Debug this distributed async deadlock in worker pool"
# List all registered models in the catalog
python backend/app/cli/main.py models
modelrouter models
# View recent routed traffic logs
modelrouter traffic
# View system-wide routing performance and cost savings analytics
python backend/app/cli/main.py analytics
modelrouter analytics
```
---
@@ -301,6 +334,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
Maintained and architected by **PicadoLabs**.
- **Maintainer**: [@Kaap10](https://github.com/Kaap10)
- **Organization**: [PicadoLabs](https://github.com/PicadoLabs)
- **Website**: [https://picadolabs.me](https://picadolabs.me)
- **Contact**: [picadolabs@gmail.com](mailto:picadolabs@gmail.com)
+19 -1
View File
@@ -1,4 +1,4 @@
import sys
import sys
import os
import asyncio
import httpx
@@ -265,5 +265,23 @@ def analytics():
asyncio.run(_analytics())
@app.command()
def ui(
host: str = typer.Option("127.0.0.1", "--host", "-h", help="Host address to bind the web server"),
port: int = typer.Option(8000, "--port", "-p", help="Port to run the Model Router Control Room on"),
):
"""Launch the Model Router API Gateway & AI Traffic Control Room UI."""
import uvicorn
console.print(Panel.fit(
f"[bold cyan]Starting Model Router AI Traffic Control Room[/bold cyan]\n"
f"Web Dashboard: [bold green]http://{host}:{port}[/bold green]\n"
f"API Documentation: [bold green]http://{host}:{port}/docs[/bold green]",
title="MODEL ROUTER UI LAUNCHER",
border_style="cyan"
))
uvicorn.run("main:app", host=host, port=port, reload=False, app_dir=os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
if __name__ == "__main__":
app()
+25 -1
View File
@@ -1,4 +1,4 @@
import asyncio
import asyncio
import json
from contextlib import asynccontextmanager
from fastapi import FastAPI
@@ -80,6 +80,29 @@ async def stream_live_traffic():
return EventSourceResponse(event_generator())
# Serve Bundled Frontend (SPA) if static directory exists
import os
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from fastapi import HTTPException
STATIC_DIR = os.path.join(os.path.dirname(__file__), "app", "static")
if os.path.exists(STATIC_DIR):
assets_dir = os.path.join(STATIC_DIR, "assets")
if os.path.exists(assets_dir):
app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
@app.get("/{full_path:path}")
async def serve_spa(full_path: str):
if full_path.startswith("api/") or full_path.startswith("docs") or full_path.startswith("openapi.json"):
raise HTTPException(status_code=404, detail="Not Found")
target_file = os.path.join(STATIC_DIR, full_path)
if os.path.exists(target_file) and os.path.isfile(target_file):
return FileResponse(target_file)
return FileResponse(os.path.join(STATIC_DIR, "index.html"))
else:
@app.get("/")
async def root():
return {
@@ -92,3 +115,4 @@ async def root():
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host=settings.HOST, port=settings.PORT, reload=settings.DEBUG)
+58
View File
@@ -0,0 +1,58 @@
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "model-router-cli"
version = "1.0.0"
description = "Intelligent, explainable LLM request routing platform & AI Traffic Control Room"
readme = "README.md"
requires-python = ">=3.10"
authors = [
{ name = "PicadoLabs", email = "picadolabs@gmail.com" }
]
maintainers = [
{ name = "Kaap10", email = "picadolabs@gmail.com" }
]
license = { text = "MIT" }
keywords = ["llm", "ai", "router", "fastapi", "ollama", "openai", "cost-optimization"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
"Topic :: Software Development :: Libraries :: Application Frameworks",
]
dependencies = [
"fastapi>=0.110.0",
"uvicorn[standard]>=0.28.0",
"pydantic>=2.6.0",
"pydantic-settings>=2.2.0",
"sqlalchemy>=2.0.28",
"aiosqlite>=0.20.0",
"httpx>=0.27.0",
"typer>=0.12.0",
"rich>=13.7.0",
"sse-starlette>=2.0.0",
"python-multipart>=0.0.9",
"python-dotenv>=1.0.1",
"tiktoken>=0.6.0"
]
[project.urls]
Homepage = "https://picadolabs.me"
Repository = "https://github.com/PicadoLabs/ai-model-router"
Issues = "https://github.com/PicadoLabs/ai-model-router/issues"
[project.scripts]
modelrouter = "app.cli.main:app"
[tool.setuptools.packages.find]
where = ["backend"]
[tool.setuptools.package-data]
"app" = ["static/**/*", "static/*"]
+51
View File
@@ -0,0 +1,51 @@
import os
import sys
import shutil
import subprocess
def build():
print("==================================================")
print("STEP 1: Compiling React Frontend (Traffic Control Room)")
print("==================================================")
frontend_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "frontend"))
result = subprocess.run(["npm", "run", "build"], cwd=frontend_dir, shell=True)
if result.returncode != 0:
print("[ERROR] Frontend build failed!")
sys.exit(1)
print("\n==================================================")
print("STEP 2: Bundling UI Assets into backend/app/static")
print("==================================================")
dist_dir = os.path.join(frontend_dir, "dist")
static_dest = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "backend", "app", "static"))
if os.path.exists(static_dest):
shutil.rmtree(static_dest)
shutil.copytree(dist_dir, static_dest)
print(f"[OK] Successfully copied static UI files from {dist_dir} to {static_dest}")
print("\n==================================================")
print("STEP 3: Building Python Distribution (.tar.gz and .whl)")
print("==================================================")
root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
# Ensure build is installed
subprocess.run([sys.executable, "-m", "pip", "install", "--upgrade", "build", "twine", "wheel"], cwd=root_dir, check=True)
# Build package
build_result = subprocess.run([sys.executable, "-m", "build"], cwd=root_dir)
if build_result.returncode != 0:
print("[ERROR] Python build failed!")
sys.exit(1)
print("\n==================================================")
print("STEP 4: Validating Artifacts with Twine Check")
print("==================================================")
dist_artifacts = os.path.join(root_dir, "dist", "*")
subprocess.run([sys.executable, "-m", "twine", "check", dist_artifacts], cwd=root_dir, shell=True)
print("\n[SUCCESS] ai-model-router package successfully built!")
print(f"Artifacts ready in: {os.path.join(root_dir, 'dist')}")
if __name__ == "__main__":
build()