1# Author: Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca2"""Provider-to-role assignment for the three committees (all_anthropic, all_openai, mixed_random).34``mixed_random`` draws a balanced, seeded permutation per property (seed = global seed + tx_id) so that5the assignment is reproducible and recorded in the transcript.6"""7from __future__ import annotations89import hashlib10import random1112from ..config import SEED, load_config1314COMMITTEES = ("all_anthropic", "all_openai", "mixed_random")151617def pair_for_tier(tier: str | None = None) -> dict[str, str]:18 cfg = load_config("models")19 tier = tier or cfg.get("default_tier", "mid")20 pairs = cfg["cost_matched_pairs"]21 if tier not in pairs:22 raise KeyError(f"tier {tier!r} not in configs/models.yaml cost_matched_pairs")23 return dict(pairs[tier])242526def committee_models(committee: str, tier: str | None = None) -> list[str]:27 pair = pair_for_tier(tier)28 if committee == "all_anthropic":29 return [pair["anthropic"]]30 if committee == "all_openai":31 return [pair["openai"]]32 if committee == "mixed_random":33 return [pair["anthropic"], pair["openai"]]34 raise KeyError(f"unknown committee {committee!r} (expected one of {COMMITTEES})")353637def assign(roles: list[str], committee: str, seed: int = SEED, tx_id: str = "", tier: str | None = None) -> dict[str, str]:38 """role id -> model id. Deterministic given (roles, committee, seed, tx_id, tier)."""39 pair = pair_for_tier(tier)40 if committee == "all_anthropic":41 return {r: pair["anthropic"] for r in roles}42 if committee == "all_openai":43 return {r: pair["openai"] for r in roles}44 if committee != "mixed_random":45 raise KeyError(f"unknown committee {committee!r} (expected one of {COMMITTEES})")46 n = len(roles)47 vendors = ["anthropic", "openai"] * (n // 2 + 1)48 vendors = vendors[:n] # balanced: ceil(n/2) anthropic, floor(n/2) openai before shuffling49 rng = random.Random(int(hashlib.sha256(f"{seed}|{tx_id}".encode()).hexdigest()[:16], 16))50 rng.shuffle(vendors)51 if n % 2 == 1 and rng.random() < 0.5: # the odd slot goes to either vendor with equal probability52 vendors = ["openai" if v == "anthropic" else "anthropic" for v in vendors]53 return {r: pair[v] for r, v in zip(roles, vendors)}54