26 Commits

Author SHA1 Message Date
DatTT127
7d5c583475 update the modal deployment logic
All checks were successful
Triton Modal Trigger / deploy-to-modal (push) Successful in 11s
2026-07-17 13:51:58 +07:00
DatTT127
196d243e03 update the modal triton workflow
Some checks failed
Triton Modal Trigger / deploy-to-modal (push) Failing after 7s
2026-07-17 13:34:34 +07:00
DatTT127
77c32e93cd update the modal triton workflow
All checks were successful
Triton Modal Trigger / deploy-to-modal (push) Successful in 4s
2026-07-17 13:33:40 +07:00
DatTT127
f011435ab0 update the modal triton workflow
All checks were successful
Triton Modal Trigger / deploy-to-modal (push) Successful in 5s
2026-07-17 13:33:10 +07:00
DatTT127
f55d628e8d update the modal triton workflow
All checks were successful
Triton Modal Trigger / deploy-to-modal (push) Successful in 6s
2026-07-17 13:32:12 +07:00
DatTT127
f35329d6c0 update the modal triton workflow
All checks were successful
Triton Modal Trigger / deploy-to-modal (push) Successful in 8s
2026-07-17 13:30:58 +07:00
DatTT127
53980e2afc update the workflow
Some checks failed
Triton Modal Trigger / deploy-to-modal (push) Failing after 8s
2026-07-17 13:28:28 +07:00
DatTT127
86b135fdf0 update the modal-triton workflows
Some checks failed
Triton Modal Trigger / deploy-to-modal (push) Failing after 7s
2026-07-17 13:27:02 +07:00
DatTT127
9dce7ff426 update workflow
Some checks failed
Triton Modal Trigger / deploy-to-modal (push) Failing after 8s
2026-07-17 13:22:17 +07:00
DatTT127
900e2bb68b update the triton modal trigger
Some checks failed
Test Gitea Secrets / deploy-to-modal (push) Successful in 22s
Triton Modal Trigger / deploy-to-modal (push) Failing after 8s
2026-07-17 13:17:02 +07:00
DatTT127
06197447f7 update_again
All checks were successful
Test Gitea Secrets / deploy-to-modal (push) Successful in 19s
2026-07-17 00:29:00 +07:00
DatTT127
d38ca6bf23 update_again
Some checks failed
Test Gitea Secrets / deploy-to-modal (push) Failing after 16s
2026-07-17 00:26:13 +07:00
DatTT127
0f7f0dddd8 update_again
Some checks failed
Test Gitea Secrets / deploy-to-modal (push) Failing after 14s
2026-07-17 00:24:06 +07:00
DatTT127
bbdc205be2 update_again
Some checks failed
Test Gitea Secrets / deploy-to-modal (push) Failing after 18s
2026-07-17 00:22:57 +07:00
DatTT127
4906005cb9 update_again
Some checks failed
Test Gitea Secrets / deploy-to-modal (push) Failing after 15s
2026-07-17 00:22:00 +07:00
DatTT127
ee1ae91a7a update
Some checks failed
Test Gitea Secrets / deploy-to-modal (push) Failing after 13s
2026-07-17 00:17:12 +07:00
DatTT127
7913119351 update the test_secret.yaml
Some checks failed
Test Gitea Secrets / deploy-to-modal (push) Failing after 16s
2026-07-17 00:02:14 +07:00
DatTT127
2635ddfe8f update the test
Some checks failed
Test Gitea Secrets / deploy-to-modal (push) Failing after 32s
2026-07-16 23:59:43 +07:00
DatTT127
7b9db01f2a update the workflow test
Some checks failed
Test Gitea Secrets / deploy-to-modal (push) Failing after 16s
2026-07-16 23:41:57 +07:00
DatTT127
1e6953d5a1 update the workflows test
Some checks failed
Test Gitea Secrets / deploy-to-modal (push) Failing after 25s
2026-07-16 23:28:23 +07:00
DatTT127
f67fb7a135 update test_secret.yaml
Some checks failed
Test Gitea Secrets / print-secret (push) Failing after 3s
2026-07-16 22:54:32 +07:00
DatTT127
f896e5c932 update test_secret.yaml
Some checks failed
Test Gitea Secrets / print-secret (push) Failing after 10s
2026-07-16 22:50:40 +07:00
DatTT127
94f029eb19 update test_secret.yaml
Some checks failed
Test Gitea Secrets / print-secret (push) Failing after 12s
2026-07-16 22:48:09 +07:00
DatTT127
45972daf29 update test_secret.yaml
Some checks failed
Test Gitea Secrets / print-secret (push) Failing after 11s
2026-07-16 22:46:28 +07:00
DatTT127
8a8f91d99d update the test_gitea
All checks were successful
Test Gitea Secrets / print-secret (push) Successful in 50s
2026-07-16 22:34:16 +07:00
DatTT127
2e439d2787 include the test gitea secrets
Some checks failed
Test Gitea Secrets / print-secret (push) Failing after 2m41s
2026-07-16 16:41:32 +07:00
10 changed files with 654 additions and 168 deletions

View File

@@ -0,0 +1,38 @@
name: Test Gitea Secrets
on: workflow_dispatch
jobs:
deploy-to-modal:
runs-on: ubuntu-latest
container:
image: gitea-modal-runner:latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Debug Modal Credentials
run: |
echo "DEBUG: Token ID is: '${MODAL_TOKEN_ID}'"
echo "DEBUG: Token ID length is: ${#MODAL_TOKEN_ID}"
# Use 'cut' to get the first 5 characters safely
FIRST_5=$(echo "$MODAL_TOKEN_ID" | cut -c1-5)
echo "DEBUG: First 5 chars are: '$FIRST_5'"
env:
# This maps the Gitea Repository Secret to an Environment Variable
MODAL_TOKEN_ID: ${{ secrets.MODAL_KEY }}
MODAL_TOKEN_SECRET: ${{ secrets.MODAL_SECRET }}
- name: Deploy worker pipeline to Modal
run: |
echo "DEBUG: Token ID is: '${MODAL_TOKEN_ID}'"
echo "DEBUG: Token ID length is: ${#MODAL_TOKEN_ID}"
# Use 'cut' to get the first 5 characters safely
FIRST_5=$(echo "$MODAL_TOKEN_ID" | cut -c1-5)
echo "DEBUG: First 5 chars are: '$FIRST_5'"
cd workspace/sprint_1_2/CODEBASE/deps/implementation
modal deploy test_worker.py
env:
# This maps the Gitea Repository Secret to an Environment Variable
MODAL_TOKEN_ID: ${{ secrets.MODAL_KEY }}
MODAL_TOKEN_SECRET: ${{ secrets.MODAL_SECRET }}

View File

@@ -0,0 +1,28 @@
# What is the purpose of this workflows
# for a modal GPU instance that hosting the CV model's on Nvidia Triton
name: Triton Modal Trigger
on: [push]
jobs:
deploy-to-modal:
runs-on: ubuntu-latest
container:
image: gitea-modal-runner:latest # where build the code
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
sparse-checkout: |
workspace/sprint_1_2/CODEBASE/infra/implementation/triton_run/*
sparse-checkout-cone-mode: false
- name: Deploy Triton model to Modal
run: |
echo "Deploying Triton..."
cd workspace/sprint_1_2/CODEBASE/infra/implementation/triton_run
modal deploy modal_triton.py
env:
MODAL_TOKEN_ID: ${{ secrets.MODAL_KEY }}
MODAL_TOKEN_SECRET: ${{ secrets.MODAL_SECRET }}

View File

@@ -0,0 +1 @@
# Replace with actual MedGemma Modal endpoint API key (never commit real secrets)

View File

@@ -0,0 +1,3 @@
modal secret create gitea-runner-secrets \
MODAL_KEY=your-modal-key \
MODAL_SECRET=your-modal-secret

View File

@@ -0,0 +1,28 @@
# --- Stage 1: Builder ---
FROM python:3.12-slim AS builder
# Install build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
# Install python dependencies into a local folder
RUN pip install --user modal
# --- Stage 2: Runner ---
FROM python:3.12-slim
# Install only the runtime dependencies needed for Gitea Actions
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
&& apt-get install -y nodejs \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# Copy the installed packages from the builder stage
COPY --from=builder /root/.local /root/.local
# Ensure the local bin is in the PATH
ENV PATH=/root/.local/bin:$PATH

View File

@@ -0,0 +1,33 @@
import modal
import subprocess
# 1. Define the App
app = modal.App("hello-world-build-worker")
# 2. Define a clean container image representing your worker environment
image = modal.Image.debian_slim().pip_install("requests")
# 3. Define the worker function
@app.function(
image=image,
# This keeps the worker container alive for 5 minutes after finishing,
# so subsequent runs start instantly (avoiding cold starts).
scaledown_window=300
)
def run_worker_job():
print("--- WORKER CONTAINER STARTED ---")
# Mimic fetching project files or setting up environment variables
test_env_var = "Lumina-Build-Sandbox"
print(f"Environment initialized. Target deployment: {test_env_var}")
# Run a simple shell command inside the container to mimic building/testing
print("Running build script simulation...")
result = subprocess.run(
["echo", "Hello World! Your Modal container is successfully building your Python app."],
capture_output=True,
text=True
)
print(f"Build output: {result.stdout.strip()}")
print("--- WORKER JOB COMPLETED SUCCESSFULLY ---")

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 KiB

View File

@@ -0,0 +1,142 @@
* The build was for creating the build pipeline in the CI/CD system of the project
that involve: Lightsail instance as broker, Modal-Serverless as the build-worker, and Gitea as the source repository hosted on the same Lightsail VM.
```mermaid
sequenceDiagram
autonumber
actor Developer
participant Git as Codebase (Local Git)
participant Gitea as Gitea (on Lightsail VM)
participant Webhook as Webhook Listener (Lightsail VM)
participant Modal as Modal Serverless (VM Sandbox)
Developer->>Git: git push code
Git->>Gitea: Push commits to remote repo (Lightsail)
Note over Gitea: Gitea registers a new pending job<br/>in the Actions queue
Gitea->>Webhook: HTTP POST Webhook (Push event)
rect rgb(240, 248, 255)
Note over Webhook: Triggered by Webhook (same Lightsail VM)
Webhook->>Modal: Spawn sandbox container via Modal SDK
end
rect rgb(245, 245, 245)
Note over Modal: Boot Sandbox VM (<2 seconds)
Modal->>Gitea: ./act_runner register (using registration token)
Modal->>Gitea: ./act_runner daemon --once (asks for work)
Gitea->>Modal: Delivers the queued build job
Note over Modal: Runner clones codebase,<br/>builds Docker container, & runs test/compile
Modal->>Gitea: Reports build results (Success/Failure)
end
Note over Modal: act_runner exits automatically (--once)<br/>Modal Sandbox self-destructs
Gitea->>Developer: Displays build status in UI
```
---
### Phase 1: Gitea Preparation (Lightsail VM)
This phase configures the Gitea instance running on the Lightsail VM to accept serverless runners.
- [ ] **Task 1.1: Enable Gitea Actions**
- SSH into the Lightsail VM.
- Open `/etc/gitea/app.ini` on the Lightsail instance.
- Add or update the following block to enable the Actions feature:Ini, TOML
```
[actions]
ENABLED = true
```
- Restart Gitea (`sudo systemctl restart gitea`).
- [ ] **Task 1.2: Retrieve Gitea Runner Registration Token**
- Access the Gitea web UI running on the Lightsail VM (e.g., `http://<lightsail-ip>:3000`) as an administrator.
- Navigate to **Site Administration** > **Actions** > **Runners**.
- Click **Registration Token** and copy this token (you will need it for the Modal Sandbox registration).
> **Note:** All Gitea administration tasks in this plan assume you are managing the Gitea instance hosted on the Lightsail VM, either via SSH or its web UI.
### Phase 2: Create the Webhook Listener (Lightsail VM)
The listener runs on the same Lightsail VM as Gitea and catches the git push notification from Gitea to call Modal.
- [ ] **Task 2.1: Write the Express/Node.js Webhook Server**
- Create a simple script on the Lightsail VM that listens on a port (e.g., `3333`) and exposes a `/deploy` route.
- The route must verify Gitea's Webhook Secret header to ensure requests are authentic.
- When a valid POST request arrives, use the `child_process` module to run a local command: `modal run deploy_runner.py`.
- [ ] **Task 2.2: Configure Webhook in Gitea**
- In your Gitea repository settings (on the Lightsail-hosted Gitea), create a Gitea Webhook pointing to `http://127.0.0.1:3333/deploy` (or your Lightsail public domain).
- Set the trigger event to **Push Events** and assign a strong webhook secret token.
### Phase 3: Build the Modal Serverless Runner (Modal Cloud)
This python script defines the sandbox container and coordinates the registration, execution, and termination with the Gitea instance on Lightsail.
- [ ] **Task 3.1: Define the Modal Image (`deploy_runner.py`)**
- Configure a Modal image containing:
- `git`
- `docker` (or `podman`/`kaniko` if you are building Docker images inside the sandbox)
- The Gitea `act_runner` binary (downloaded directly from Gitea's release page).
- **Build dependencies for the project:**
- Node.js 18+ and npm (for frontend build)
- Python 3.10+ and pip (for any Python-based build steps)
- Build essentials (`build-essential`, `python3-dev`, `pkg-config`) for native module compilation
- `ca-certificates` and `curl` (for downloading dependencies and act_runner)
- [ ] **Task 3.2: Implement the Modal Function Logic**
- Set up a Modal function (e.g., `@app.function()`) that executes the following shell sequence inside the container when invoked:
1. **Register the runner:**Bash
```
./act_runner register --no-interactive --instance <YOUR_GITEA_URL> --token <YOUR_REGISTRATION_TOKEN> --name modal-sandbox-runner --labels "ubuntu-latest:docker://node:18-bookworm"
```
2. **Run once and block until job completion:**Bash
```
./act_runner daemon --once
```
> **Note:** `<YOUR_GITEA_URL>` must be the publicly reachable address of the Gitea instance on your Lightsail VM (e.g., `http://<lightsail-ip>:3000`), since the Modal sandbox runs outside of your Lightsail network.
- [ ] **Task 3.3: Deploy and Test the Modal App**
- Authenticate your Lightsail server with Modal using `modal setup` (run this on the Lightsail VM).
- Run `modal run deploy_runner.py` manually to verify the sandbox boots, registers in Gitea, checks for a job, and exits gracefully.
### Phase 4: Configure the Repository CI/CD Workflow
Define what the runner should actually do when it receives the codebase from the Lightsail-hosted Gitea.
- [ ] **Task 4.1: Create Gitea Action Workflow File**
- In your repository (hosted on the Lightsail Gitea instance), create a directory structure: `.gitea/workflows/demo.yaml`.
- Define a basic test pipeline:YAML
#
```
name: Gitea CI/CD
on: [push]
jobs:
test-build:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Build Application
run: |
echo "Compiling and testing..."
# Add your test or build commands here
```
### Phase 5: End-to-End Integration Test
- [ ] **Task 5.1: Push a Commit**
- Run `git push` from your local machine to the Gitea instance on Lightsail.
- Verify the sequence:
1. Gitea (on Lightsail) displays a pending job badge.
2. Webhook triggers on Lightsail (same VM as Gitea).
3. Modal sandbox boots up.
4. The sandbox logs into Gitea (via public Lightsail URL), processes the workflow, and turns off.
5. Gitea UI (on Lightsail) updates to green (Success) or red (Failure).

View File

@@ -0,0 +1,211 @@
# Secrets Management Guideline
This document defines how to handle secrets, environment variables, and sensitive configuration across the PILOT project CI/CD pipeline and infrastructure.
## Principles
1. **Never commit secrets to version control** — Use `.gitignore` and secret stores
2. **Least-privilege access** — Each component gets only the secrets it needs
3. **Environment isolation** — Separate secrets for local, staging, and production
4. **Auditability** — Track secret creation, rotation, and access
## Secret Storage Locations
### 1. Gitea Repository Secrets (CI/CD Workflows)
Store secrets used by GitHub Actions / Gitea Actions workflows in Gitea's built-in secrets manager.
**Access path:** Gitea UI → Repository Settings → Secrets → Actions
```bash
# Repository-level secrets (per project)
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
MODAL_TOKEN_ID
MODAL_TOKEN_SECRET
POSTGRES_PASSWORD
GITEA_WEBHOOK_SECRET
CADDY_API_TOKEN
```
**Access in workflow:**
```yaml
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }}
MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }}
```
### 2. Modal Secrets (Serverless Workers)
Store Modal-specific configuration in Modal's secrets system.
**Create secret:**
```bash
modal secret create gitea-runner-secrets \
MODAL_KEY=your-modal-key \
MODAL_SECRET=your-modal-secret
```
**Use in deploy_runner.py:**
```python
import os
import modal
app = modal.App("gitea-runner")
secret = modal.Secret.from_name("gitea-runner-secrets")
@app.function(secret=secret)
def run_runner():
modal_key = os.environ["MODAL_KEY"]
modal_secret = os.environ["MODAL_SECRET"]
```
### 3. AWS Secrets Manager (Production Infrastructure)
Store AWS credentials and infrastructure secrets in AWS Secrets Manager.
**Create secret:**
```bash
aws secretsmanager create-secret \
--name gitea/prod/aws-credentials \
--secret-string '{"access_key":"...","secret_key":"..."}'
```
**Retrieve secret:**
```bash
aws secretsmanager get-secret-value \
--secret-id gitea/prod/aws-credentials \
--query SecretString --output text
```
**Use in Python (boto3):**
```python
import boto3
import json
client = boto3.client('secretsmanager')
secret = client.get_secret_value(SecretId='gitea/prod/aws-credentials')
credentials = json.loads(secret['SecretString'])
```
### 4. Local Development (.env)
Use `.env` files for local development with strict `.gitignore` rules.
```bash
# .env (NEVER commit this file)
AWS_ACCESS_KEY_ID=local-dev-key
AWS_SECRET_ACCESS_KEY=local-dev-secret
AWS_DEFAULT_REGION=us-east-1
HF_TOKEN=hf-local-token
MODAL_KEY=local-modal-key
MODAL_SECRET=local-modal-secret
FIGMA_ACCESS_TOKEN=local-figma-token
SUPABASE_URL=http://localhost:54321
SUPABASE_PUBLISHABLE_KEY=local-supabase-key
SUPABASE_DB_URL=postgresql://postgres:postgres@localhost:54322/postgres
SUPABASE_SERVICE_ROLE_KEY=local-supabase-service-key
EXA_API_KEY=local-exa-key
GITHUB_PAT=local-github-pat
GITHUB_GITEA_CLIENT_SECRET=local-gitea-client-secret
POSTGRES_PASSWORD=local-password
GITEA_URL=http://localhost:3000
GITEA_WEBHOOK_SECRET=local-webhook-secret
```
```bash
# .gitignore
.env
.env.local
.env.*.local
secrets/
*.pem
*.key
```
## Gitea Workflow Secrets Template
Use this template to reference secrets in `.gitea/workflows/*.yml` files:
```yaml
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }}
HF_TOKEN: ${{ secrets.HF_TOKEN }}
MODAL_KEY: ${{ secrets.MODAL_KEY }}
MODAL_SECRET: ${{ secrets.MODAL_SECRET }}
FIGMA_ACCESS_TOKEN: ${{ secrets.FIGMA_ACCESS_TOKEN }}
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_PUBLISHABLE_KEY: ${{ secrets.SUPABASE_PUBLISHABLE_KEY }}
SUPABASE_DB_URL: ${{ secrets.SUPABASE_DB_URL }}
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
EXA_API_KEY: ${{ secrets.EXA_API_KEY }}
GITHUB_PAT: ${{ secrets.GITHUB_PAT }}
GITHUB_GITEA_CLIENT_SECRET: ${{ secrets.GITHUB_GITEA_CLIENT_SECRET }}
```
All secrets must be registered in Gitea under **Repository Settings → Secrets → Actions** before the workflow runs.
## Secret Reference Matrix
| Secret | Local Dev | Gitea CI/CD | Modal Worker | AWS Infra |
|--------|-----------|-------------|--------------|-----------|
| AWS_ACCESS_KEY_ID | .env | Gitea Secret | — | Secrets Manager |
| AWS_SECRET_ACCESS_KEY | .env | Gitea Secret | — | Secrets Manager |
| AWS_DEFAULT_REGION | .env | Gitea Secret | — | — |
| HF_TOKEN | .env | Gitea Secret | — | — |
| MODAL_KEY | .env | Gitea Secret | Modal Secret | — |
| MODAL_SECRET | .env | Gitea Secret | Modal Secret | — |
| FIGMA_ACCESS_TOKEN | .env | Gitea Secret | — | — |
| SUPABASE_URL | .env | Gitea Secret | — | — |
| SUPABASE_PUBLISHABLE_KEY | .env | Gitea Secret | — | — |
| SUPABASE_DB_URL | .env | Gitea Secret | — | Secrets Manager |
| SUPABASE_SERVICE_ROLE_KEY | .env | Gitea Secret | — | — |
| EXA_API_KEY | .env | Gitea Secret | — | — |
| GITHUB_PAT | .env | Gitea Secret | — | — |
| GITHUB_GITEA_CLIENT_SECRET | .env | Gitea Secret | — | — |
| POSTGRES_PASSWORD | .env | Gitea Secret | — | Secrets Manager |
| GITEA_WEBHOOK_SECRET | .env | Gitea Secret | — | — |
| CADDY_API_TOKEN | .env | Gitea Secret | — | — |
## Security Best Practices
1. **Rotate secrets regularly** — Especially AWS keys and tokens (every 90 days)
2. **Use IAM roles** — Prefer IAM roles over hardcoded AWS keys in production
3. **Encrypt at rest** — Use AWS KMS for Secrets Manager encryption
4. **Audit access** — Enable CloudTrail for AWS Secrets Manager, Gitea audit logs
5. **Pre-commit hooks** — Use `git-secrets` or `pre-commit` to block secret commits
6. **Separate environments** — Use different secret namespaces for dev/staging/prod
7. **Minimal scope** — AWS keys should have only required permissions
## Pre-commit Hook Setup (Optional)
Install `git-secrets` to prevent committing secrets:
```bash
# Install git-secrets
git secrets --install
# Register common patterns
git secrets --register-aws
git secrets --add 'GITEA_TOKEN'
git secrets --add 'MODAL_TOKEN'
```
## Emergency: Secret Exposure
If a secret is accidentally committed:
1. **Rotate immediately** — Generate a new secret value
2. **Revoke the old secret** — Invalidate the exposed credential
3. **Audit access logs** — Check for unauthorized usage
4. **Update all references** — Deploy the new secret to all environments
5. **Force push cleanup** — Use `git filter-branch` or `BFG` to remove from history
## Related Files
- `build_server_plan.md` — CI/CD pipeline architecture
- `CICD_architecture.png` — Architecture diagram

View File

@@ -25,174 +25,6 @@ triton_image = (
) )
app = modal.App("triton-s3-service", image=triton_image) app = modal.App("triton-s3-service", image=triton_image)
from fastapi import FastAPI, Response, Request,HTTPException
from fastapi.responses import StreamingResponse # 👈 ADD THIS IMPORT
import httpx
web_app = FastAPI()
# -------------------------------------------------------------
# FASTAPI PROXY ROUTING (Living inside the container)
# -------------------------------------------------------------
@web_app.get("/v2/health/ready")
async def forward_health():
"""Proxies external HTTP REST calls straight to Triton's internal inference engine"""
async with httpx.AsyncClient() as client:
try:
response = await client.get("http://127.0.0.1:8000/v2/health/ready")
return Response(content=response.content, status_code=response.status_code, media_type=response.headers.get("content-type"))
except Exception as e:
return Response(content=f"Triton booting models from S3... Error: {str(e)}", status_code=503)
@web_app.get("/metrics")
@web_app.get("/")
async def forward_metrics():
"""Proxies external metric calls straight to Triton's internal metrics engine"""
async with httpx.AsyncClient() as client:
try:
response = await client.get("http://127.0.0.1:8002/metrics")
return Response(content=response.content, status_code=response.status_code, media_type=response.headers.get("content-type"))
except Exception as e:
return Response(content=f"Waiting for metrics channel... Error: {str(e)}", status_code=503)
# 👇 ADD THIS CATCH-ALL ROUTE HERE 👇
@web_app.api_route("/v2/{path:path}", methods=["GET", "POST"])
async def proxy_all_triton_request(path: str, request: Request):
import tritonclient.grpc.aio as grpcclient
from tritonclient.grpc import service_pb2, service_pb2_grpc
from tritonclient.grpc import _utils as grpc_utils #InferenceServerClient
import grpc
import numpy as np
# 1. Keep HTTP proxy ONLY for metadata/health checks
if "infer" not in path:
async with httpx.AsyncClient(timeout=60.0) as client:
url = f"http://127.0.0.1:8000/v2/{path}"
headers = dict(request.headers)
headers.pop("host", None)
triton_response = await client.request(
method=request.method, url=url, headers=headers, content=await request.body()
)
return Response(
content=triton_response.content,
status_code=triton_response.status_code,
headers=dict(triton_response.headers)
)
# 2. 🚀 FOR INFERENCE: Convert the incoming HTTP raw body into a gRPC call
if "infer" in path:
try:
# Extract model name from the route path (e.g., v2/models/MODEL_NAME/infer)
parts = path.split("/")
model_name = parts[parts.index("models") + 1]
# Read incoming raw binary HTTP payload
raw_http_body = await request.body()
header_length_str = request.headers.get("Inference-Header-Content-Length")
if not header_length_str:
raise HTTPException(
status_code=400,
detail="Missing 'Inference-Header-Content-Length' header required for binary Triton transcoding."
)
header_length = int(header_length_str)
# --- 💥 KSERVE V2 MULTI-PART BODY PARSING ---
# Extract the front JSON metadata and the trailing raw binary tensors
import json
json_bytes = raw_http_body[:header_length]
binary_data = raw_http_body[header_length:]
request_metadata = json.loads(json_bytes.decode('utf-8'))
# Setup async gRPC connection
triton_url = "127.0.0.1:8001"
# Configure channels to accept large payload returns (100MB limit override)
max_msg_length = 100 * 1024 * 1024
channel_options = [
('grpc.max_receive_message_length', max_msg_length),
('grpc.max_send_message_length', max_msg_length),
]
async with grpc.aio.insecure_channel(triton_url, options=channel_options) as channel:
stub = service_pb2_grpc.GRPCInferenceServiceStub(channel=channel)
# Construct the native ModelInferRequest protobuf
grpc_request = service_pb2.ModelInferRequest()
grpc_request.model_name = model_name
grpc_request.model_version = ""
# Populate inputs dynamically from incoming KServe metadata
binary_offset = 0
for input_tensor in request_metadata.get("inputs", []):
# Correct Protobuf repeated field instantiation via .add()
infer_input = grpc_request.inputs.add()
infer_input.name = input_tensor["name"]
infer_input.datatype = input_tensor["datatype"]
infer_input.shape.extend(input_tensor["shape"]) # Explicit clean integers!
# Extract the binary slice matching this tensor out of the raw payload block
if "parameters" in input_tensor and "binary_data_size" in input_tensor["parameters"]:
data_size = input_tensor["parameters"]["binary_data_size"]
grpc_request.raw_input_contents.append(
binary_data[binary_offset : binary_offset + data_size]
)
binary_offset += data_size
# Request output tensor mappings dynamically based on what the client requested
for output_tensor in request_metadata.get("outputs", []):
infer_output = grpc_request.outputs.add()
infer_output.name = output_tensor["name"]
# Signal Triton to return output via raw binary buffers
infer_output.parameters["binary_data"].bool_param = True
# ✅ Send the transcoding payload straight into Triton over internal gRPC loop
grpc_response = await stub.ModelInfer(request=grpc_request, timeout=None)
# --- 💥 TRANSCODE gRPC RESPONSE BACK TO MULTI-PART KSERVE HTTP ---
response_metadata = {
"model_name": grpc_response.model_name,
"model_version": grpc_response.model_version,
"outputs": []
}
response_binary_body = b""
for i, output in enumerate(grpc_response.outputs):
out_desc = {
"name": output.name,
"datatype": output.datatype,
"shape": list(output.shape),
"parameters": {
"binary_data_size": len(grpc_response.raw_output_contents[i])
}
}
response_metadata["outputs"].append(out_desc)
response_binary_body += grpc_response.raw_output_contents[i]
# Re-bundle into [JSON metadata] + [Raw binary output chunks]
response_json_bytes = json.dumps(response_metadata).encode('utf-8')
output_http_body = response_json_bytes + response_binary_body
return Response(
content=output_http_body,
status_code=200,
headers={
"Content-Type": "application/octet-stream",
"Inference-Header-Content-Length": str(len(response_json_bytes))
}
)
except Exception as e:
import traceback
print(f"CRITICAL TRANSLATION EXCEPTION: {traceback.format_exc()}")
return Response(
content=f"Internal gRPC Pipeline Multiplex Error: {str(e)}",
status_code=502
)
@web_app.get("/v2/models")
async def forward_list_models():
async with httpx.AsyncClient() as client:
r = await client.get("http://127.0.0.1:8000/v2/models")
return Response(content=r.content, status_code=r.status_code, media_type=r.headers.get("content-type"))
# ------------------------------------------------------------- # -------------------------------------------------------------
# THE UNIFIED SERVICE FUNCTION (1 Container, 1 GPU, 1 Triton Process) # THE UNIFIED SERVICE FUNCTION (1 Container, 1 GPU, 1 Triton Process)
@@ -212,6 +44,176 @@ async def forward_list_models():
) )
@modal.asgi_app() @modal.asgi_app()
def unified_triton_server(): def unified_triton_server():
from fastapi import FastAPI, Response, Request,HTTPException
from fastapi.responses import StreamingResponse # 👈 ADD THIS IMPORT
import httpx
web_app = FastAPI()
# -------------------------------------------------------------
# FASTAPI PROXY ROUTING (Living inside the container)
# -------------------------------------------------------------
@web_app.get("/v2/health/ready")
async def forward_health():
"""Proxies external HTTP REST calls straight to Triton's internal inference engine"""
async with httpx.AsyncClient() as client:
try:
response = await client.get("http://127.0.0.1:8000/v2/health/ready")
return Response(content=response.content, status_code=response.status_code, media_type=response.headers.get("content-type"))
except Exception as e:
return Response(content=f"Triton booting models from S3... Error: {str(e)}", status_code=503)
@web_app.get("/metrics")
@web_app.get("/")
async def forward_metrics():
"""Proxies external metric calls straight to Triton's internal metrics engine"""
async with httpx.AsyncClient() as client:
try:
response = await client.get("http://127.0.0.1:8002/metrics")
return Response(content=response.content, status_code=response.status_code, media_type=response.headers.get("content-type"))
except Exception as e:
return Response(content=f"Waiting for metrics channel... Error: {str(e)}", status_code=503)
# 👇 ADD THIS CATCH-ALL ROUTE HERE 👇
@web_app.api_route("/v2/{path:path}", methods=["GET", "POST"])
async def proxy_all_triton_request(path: str, request: Request):
import tritonclient.grpc.aio as grpcclient
from tritonclient.grpc import service_pb2, service_pb2_grpc
from tritonclient.grpc import _utils as grpc_utils #InferenceServerClient
import grpc
import numpy as np
# 1. Keep HTTP proxy ONLY for metadata/health checks
if "infer" not in path:
async with httpx.AsyncClient(timeout=60.0) as client:
url = f"http://127.0.0.1:8000/v2/{path}"
headers = dict(request.headers)
headers.pop("host", None)
triton_response = await client.request(
method=request.method, url=url, headers=headers, content=await request.body()
)
return Response(
content=triton_response.content,
status_code=triton_response.status_code,
headers=dict(triton_response.headers)
)
# 2. 🚀 FOR INFERENCE: Convert the incoming HTTP raw body into a gRPC call
if "infer" in path:
try:
# Extract model name from the route path (e.g., v2/models/MODEL_NAME/infer)
parts = path.split("/")
model_name = parts[parts.index("models") + 1]
# Read incoming raw binary HTTP payload
raw_http_body = await request.body()
header_length_str = request.headers.get("Inference-Header-Content-Length")
if not header_length_str:
raise HTTPException(
status_code=400,
detail="Missing 'Inference-Header-Content-Length' header required for binary Triton transcoding."
)
header_length = int(header_length_str)
# --- 💥 KSERVE V2 MULTI-PART BODY PARSING ---
# Extract the front JSON metadata and the trailing raw binary tensors
import json
json_bytes = raw_http_body[:header_length]
binary_data = raw_http_body[header_length:]
request_metadata = json.loads(json_bytes.decode('utf-8'))
# Setup async gRPC connection
triton_url = "127.0.0.1:8001"
# Configure channels to accept large payload returns (100MB limit override)
max_msg_length = 100 * 1024 * 1024
channel_options = [
('grpc.max_receive_message_length', max_msg_length),
('grpc.max_send_message_length', max_msg_length),
]
async with grpc.aio.insecure_channel(triton_url, options=channel_options) as channel:
stub = service_pb2_grpc.GRPCInferenceServiceStub(channel=channel)
# Construct the native ModelInferRequest protobuf
grpc_request = service_pb2.ModelInferRequest()
grpc_request.model_name = model_name
grpc_request.model_version = ""
# Populate inputs dynamically from incoming KServe metadata
binary_offset = 0
for input_tensor in request_metadata.get("inputs", []):
# Correct Protobuf repeated field instantiation via .add()
infer_input = grpc_request.inputs.add()
infer_input.name = input_tensor["name"]
infer_input.datatype = input_tensor["datatype"]
infer_input.shape.extend(input_tensor["shape"]) # Explicit clean integers!
# Extract the binary slice matching this tensor out of the raw payload block
if "parameters" in input_tensor and "binary_data_size" in input_tensor["parameters"]:
data_size = input_tensor["parameters"]["binary_data_size"]
grpc_request.raw_input_contents.append(
binary_data[binary_offset : binary_offset + data_size]
)
binary_offset += data_size
# Request output tensor mappings dynamically based on what the client requested
for output_tensor in request_metadata.get("outputs", []):
infer_output = grpc_request.outputs.add()
infer_output.name = output_tensor["name"]
# Signal Triton to return output via raw binary buffers
infer_output.parameters["binary_data"].bool_param = True
# ✅ Send the transcoding payload straight into Triton over internal gRPC loop
grpc_response = await stub.ModelInfer(request=grpc_request, timeout=None)
# --- 💥 TRANSCODE gRPC RESPONSE BACK TO MULTI-PART KSERVE HTTP ---
response_metadata = {
"model_name": grpc_response.model_name,
"model_version": grpc_response.model_version,
"outputs": []
}
response_binary_body = b""
for i, output in enumerate(grpc_response.outputs):
out_desc = {
"name": output.name,
"datatype": output.datatype,
"shape": list(output.shape),
"parameters": {
"binary_data_size": len(grpc_response.raw_output_contents[i])
}
}
response_metadata["outputs"].append(out_desc)
response_binary_body += grpc_response.raw_output_contents[i]
# Re-bundle into [JSON metadata] + [Raw binary output chunks]
response_json_bytes = json.dumps(response_metadata).encode('utf-8')
output_http_body = response_json_bytes + response_binary_body
return Response(
content=output_http_body,
status_code=200,
headers={
"Content-Type": "application/octet-stream",
"Inference-Header-Content-Length": str(len(response_json_bytes))
}
)
except Exception as e:
import traceback
print(f"CRITICAL TRANSLATION EXCEPTION: {traceback.format_exc()}")
return Response(
content=f"Internal gRPC Pipeline Multiplex Error: {str(e)}",
status_code=502
)
@web_app.get("/v2/models")
async def forward_list_models():
async with httpx.AsyncClient() as client:
r = await client.get("http://127.0.0.1:8000/v2/models")
return Response(content=r.content, status_code=r.status_code, media_type=r.headers.get("content-type"))
print("🚀 Booting ONE Triton Instance inside ONE A100 Container...") print("🚀 Booting ONE Triton Instance inside ONE A100 Container...")
# Spawns Triton in the background. It will automatically read # Spawns Triton in the background. It will automatically read