mindXtrain / mindxtrain /deploy /ab_test.py
Gregory-L's picture
fork mindXtrain from GitHub (Professor-Codephreak/mindXtrain@661bd41) as the mindX-specific line
dfb775d verified
Raw History Blame Contribute Delete
1.57 kB
"""A/B traffic split — canary vs live.
Pure-Python `Splitter`: deterministic per-request seed → canary or live based
on `cfg.canary_pct`. The actual traffic injection happens in the operator
FastAPI app's chat handler (which calls `Splitter.pick(req)` to decide which
slot's backend to route to).
"""
from __future__ import annotations
import hashlib
import random
from pydantic import BaseModel, ConfigDict, Field
class AbConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
canary_pct: float = Field(default=0.05, ge=0.0, le=1.0)
auto_rollback_threshold: float = Field(default=0.02, ge=0.0)
class Splitter:
"""Decides per-request which slot serves traffic."""
def __init__(self, cfg: AbConfig | None = None) -> None:
self.cfg = cfg or AbConfig()
def pick(self, request_id: str | None = None) -> str:
"""Return `'canary'` or `'live'`.
If `request_id` is provided we hash it (stable across retries); else
we use random.random() (stateless).
"""
if self.cfg.canary_pct <= 0.0:
return "live"
if request_id is None:
r = random.random()
else:
digest = hashlib.blake2s(request_id.encode("utf-8"), digest_size=8).digest()
r = int.from_bytes(digest, "big") / float(2**64)
return "canary" if r < self.cfg.canary_pct else "live"
def serve_split(cfg: AbConfig) -> Splitter:
"""Construct the Splitter for an active A/B run."""
return Splitter(cfg)
__all__ = ["AbConfig", "Splitter", "serve_split"]