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/
|
.pytest_cache/
|
||||||
.coverage
|
.coverage
|
||||||
htmlcov/
|
htmlcov/
|
||||||
|
build/
|
||||||
|
*.egg-info/
|
||||||
|
|
||||||
# Node / Frontend
|
# Node / Frontend
|
||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
.dist/
|
.dist/
|
||||||
|
backend/app/static/
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.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
|
# 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.
|
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
|
- Node.js 18+ and npm
|
||||||
- (Optional) [Ollama](https://ollama.com/) for local model inference
|
- (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
|
### 1. Clone the Repository
|
||||||
```bash
|
```bash
|
||||||
@@ -152,23 +179,29 @@ npm run dev
|
|||||||
|
|
||||||
## CLI Usage
|
## 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
|
```bash
|
||||||
|
# Launch the AI Traffic Control Room Web UI and API Gateway
|
||||||
|
modelrouter ui --open-browser
|
||||||
|
|
||||||
# Run system diagnostics & provider health checks
|
# 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)
|
# 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
|
# 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
|
# 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
|
# 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**.
|
Maintained and architected by **PicadoLabs**.
|
||||||
|
|
||||||
|
- **Maintainer**: [@Kaap10](https://github.com/Kaap10)
|
||||||
- **Organization**: [PicadoLabs](https://github.com/PicadoLabs)
|
- **Organization**: [PicadoLabs](https://github.com/PicadoLabs)
|
||||||
- **Website**: [https://picadolabs.me](https://picadolabs.me)
|
- **Website**: [https://picadolabs.me](https://picadolabs.me)
|
||||||
- **Contact**: [picadolabs@gmail.com](mailto:picadolabs@gmail.com)
|
- **Contact**: [picadolabs@gmail.com](mailto:picadolabs@gmail.com)
|
||||||
|
|||||||
+19
-1
@@ -1,4 +1,4 @@
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import asyncio
|
import asyncio
|
||||||
import httpx
|
import httpx
|
||||||
@@ -265,5 +265,23 @@ def analytics():
|
|||||||
asyncio.run(_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__":
|
if __name__ == "__main__":
|
||||||
app()
|
app()
|
||||||
|
|
||||||
|
|||||||
+32
-8
@@ -1,4 +1,4 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
@@ -80,15 +80,39 @@ async def stream_live_traffic():
|
|||||||
return EventSourceResponse(event_generator())
|
return EventSourceResponse(event_generator())
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
# Serve Bundled Frontend (SPA) if static directory exists
|
||||||
async def root():
|
import os
|
||||||
return {
|
from fastapi.staticfiles import StaticFiles
|
||||||
"message": "Model Router AI Control Room Backend is active.",
|
from fastapi.responses import FileResponse
|
||||||
"docs": "/docs",
|
from fastapi import HTTPException
|
||||||
"health": "/api/health",
|
|
||||||
}
|
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__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run("main:app", host=settings.HOST, port=settings.PORT, reload=settings.DEBUG)
|
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