1111# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212# See the License for the specific language governing permissions and
1313# limitations under the License.
14+ import io
1415import json
1516import logging
1617import os # Added this import
18+ import random
19+ import tarfile
1720import threading
1821import time
1922from collections import deque
2427import pandas as pd
2528import uvicorn
2629from fastapi import FastAPI , HTTPException , status
27- from fastapi .responses import FileResponse , JSONResponse , Response
30+ from fastapi .responses import FileResponse , JSONResponse , Response , StreamingResponse
2831from pydantic import BaseModel , Field
2932from scipy .stats import norm
3033from sklearn .linear_model import BayesianRidge
@@ -184,6 +187,18 @@ def quantile_violation_rate(y_true, y_pred, quantile):
184187 return violation_rate * 100
185188
186189
190+ def _get_model_paths ():
191+ """Canonical mapping of model names to file paths."""
192+ return {
193+ "ttft" : settings .TTFT_MODEL_PATH ,
194+ "tpot" : settings .TPOT_MODEL_PATH ,
195+ "ttft_scaler" : settings .TTFT_SCALER_PATH ,
196+ "tpot_scaler" : settings .TPOT_SCALER_PATH ,
197+ "ttft_gated" : settings .TTFT_GATED_MODEL_PATH ,
198+ "tpot_gated" : settings .TPOT_GATED_MODEL_PATH ,
199+ }
200+
201+
187202class LatencyPredictor :
188203 """
189204 Manages model training, prediction, and data handling.
@@ -1438,6 +1453,14 @@ def load_models(self):
14381453 self .ttft_model = joblib .load (settings .TTFT_MODEL_PATH )
14391454 if self .model_type == ModelType .BAYESIAN_RIDGE and os .path .exists (settings .TTFT_SCALER_PATH ):
14401455 self .ttft_scaler = joblib .load (settings .TTFT_SCALER_PATH )
1456+ meta_path = os .path .join (os .path .dirname (settings .TTFT_MODEL_PATH ), "metadata.json" )
1457+ if os .path .exists (meta_path ):
1458+ try :
1459+ with open (meta_path ) as f :
1460+ seed_meta = json .load (f )
1461+ logging .info ("Loaded seed model: %s" , seed_meta )
1462+ except Exception :
1463+ logging .warning ("Failed to read seed metadata from %s" , meta_path )
14411464 else :
14421465 result = self ._create_default_model ("ttft" )
14431466 if self .model_type == ModelType .BAYESIAN_RIDGE :
@@ -1846,6 +1869,53 @@ async def readiness_check():
18461869 return {"status" : "ready" }
18471870
18481871
1872+ @app .get ("/model/export" )
1873+ def export_models ():
1874+ """Bundle trained models and metadata for seeding new deployments."""
1875+ if not predictor .is_ready :
1876+ raise HTTPException (status_code = status .HTTP_503_SERVICE_UNAVAILABLE , detail = "Models are not ready." )
1877+
1878+ # Exclude gated ensemble models from seed bundles: load_models() never
1879+ # loads them, but the prediction server syncs them on file existence and
1880+ # would then dispatch all traffic through the source deployment's frozen
1881+ # gate, masking local base-model retraining until ensemble_active flips.
1882+ paths = {name : path for name , path in _get_model_paths ().items () if name not in ("ttft_gated" , "tpot_gated" )}
1883+
1884+ with predictor .lock :
1885+ snapshots = {}
1886+ for name , p in paths .items ():
1887+ if os .path .exists (p ):
1888+ with open (p , "rb" ) as f :
1889+ snapshots [name ] = f .read ()
1890+ meta = {
1891+ "model_type" : predictor .model_type .value ,
1892+ "quantile_alpha" : settings .QUANTILE_ALPHA ,
1893+ "exported_at" : datetime .now (UTC ).isoformat (),
1894+ "ttft_samples" : sum (len (d ) for d in predictor .ttft_data_buckets .values ()),
1895+ "tpot_samples" : sum (len (d ) for d in predictor .tpot_data_buckets .values ()),
1896+ }
1897+
1898+ buf = io .BytesIO ()
1899+ with tarfile .open (fileobj = buf , mode = "w:gz" ) as tar :
1900+ for name , data in snapshots .items ():
1901+ info = tarfile .TarInfo (name = os .path .basename (paths [name ]))
1902+ info .size = len (data )
1903+ info .mtime = int (time .time ())
1904+ tar .addfile (info , io .BytesIO (data ))
1905+ meta_bytes = json .dumps (meta , indent = 2 ).encode ()
1906+ meta_info = tarfile .TarInfo (name = "metadata.json" )
1907+ meta_info .size = len (meta_bytes )
1908+ meta_info .mtime = int (time .time ())
1909+ tar .addfile (meta_info , io .BytesIO (meta_bytes ))
1910+
1911+ buf .seek (0 )
1912+ return StreamingResponse (
1913+ buf ,
1914+ media_type = "application/gzip" ,
1915+ headers = {"Content-Disposition" : "attachment; filename=seed-model.tar.gz" },
1916+ )
1917+
1918+
18491919@app .get ("/metrics" , status_code = status .HTTP_200_OK )
18501920async def metrics ():
18511921 """Prometheus metrics including coefficients/importances, bucket counts, and quantile-specific metrics."""
@@ -2078,14 +2148,7 @@ async def tpot_xgb_json():
20782148@app .get ("/model/{model_name}/info" )
20792149async def model_info (model_name : str ):
20802150 """Get model file information including last modified time."""
2081- model_paths = {
2082- "ttft" : settings .TTFT_MODEL_PATH ,
2083- "tpot" : settings .TPOT_MODEL_PATH ,
2084- "ttft_scaler" : settings .TTFT_SCALER_PATH ,
2085- "tpot_scaler" : settings .TPOT_SCALER_PATH ,
2086- "ttft_gated" : settings .TTFT_GATED_MODEL_PATH ,
2087- "tpot_gated" : settings .TPOT_GATED_MODEL_PATH ,
2088- }
2151+ model_paths = _get_model_paths ()
20892152
20902153 if model_name not in model_paths :
20912154 raise HTTPException (status_code = 404 , detail = f"Unknown model: { model_name } " )
@@ -2113,14 +2176,7 @@ async def model_info(model_name: str):
21132176@app .get ("/model/{model_name}/download" )
21142177async def download_model (model_name : str ):
21152178 """Download a model file."""
2116- model_paths = {
2117- "ttft" : settings .TTFT_MODEL_PATH ,
2118- "tpot" : settings .TPOT_MODEL_PATH ,
2119- "ttft_scaler" : settings .TTFT_SCALER_PATH ,
2120- "tpot_scaler" : settings .TPOT_SCALER_PATH ,
2121- "ttft_gated" : settings .TTFT_GATED_MODEL_PATH ,
2122- "tpot_gated" : settings .TPOT_GATED_MODEL_PATH ,
2123- }
2179+ model_paths = _get_model_paths ()
21242180
21252181 if model_name not in model_paths :
21262182 raise HTTPException (status_code = 404 , detail = f"Unknown model: { model_name } " )
@@ -2139,14 +2195,7 @@ async def download_model(model_name: str):
21392195async def list_models ():
21402196 """List all available models with their status."""
21412197 models = {}
2142- model_paths = {
2143- "ttft" : settings .TTFT_MODEL_PATH ,
2144- "tpot" : settings .TPOT_MODEL_PATH ,
2145- "ttft_scaler" : settings .TTFT_SCALER_PATH ,
2146- "tpot_scaler" : settings .TPOT_SCALER_PATH ,
2147- "ttft_gated" : settings .TTFT_GATED_MODEL_PATH ,
2148- "tpot_gated" : settings .TPOT_GATED_MODEL_PATH ,
2149- }
2198+ model_paths = _get_model_paths ()
21502199
21512200 for model_name , model_path in model_paths .items ():
21522201 if os .path .exists (model_path ):
0 commit comments