feat: package distribution with PyPI support, embedded UI, Dockerfile, and CLI enhancements
This commit is contained in:
@@ -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
@@ -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"]
|
||||
@@ -1,5 +1,9 @@
|
||||
# Model Router
|
||||
|
||||
[](https://pypi.org/project/model-router-cli/)
|
||||
[](https://pypi.org/project/model-router-cli/)
|
||||
[](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
@@ -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()
|
||||
|
||||
|
||||
+32
-8
@@ -1,4 +1,4 @@
|
||||
import asyncio
|
||||
import asyncio
|
||||
import json
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI
|
||||
@@ -80,15 +80,39 @@ async def stream_live_traffic():
|
||||
return EventSourceResponse(event_generator())
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {
|
||||
"message": "Model Router AI Control Room Backend is active.",
|
||||
"docs": "/docs",
|
||||
"health": "/api/health",
|
||||
}
|
||||
# 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 {
|
||||
"message": "Model Router AI Control Room Backend is active.",
|
||||
"docs": "/docs",
|
||||
"health": "/api/health",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run("main:app", host=settings.HOST, port=settings.PORT, reload=settings.DEBUG)
|
||||
|
||||
|
||||
@@ -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/*"]
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user