#!/usr/bin/env python3 """ End-to-end LAN inference test for meshnet distributed inference. Sends 3 chat-completion requests to a meshnet node, validates OpenAI-format responses, and prints token counts + latency per request. Usage: python scripts/test_lan_inference.py \\ --tracker http://192.168.1.10:8080 \\ --gateway http://192.168.1.10:8001 Exit 0 on success, 1 on any failure. """ from __future__ import annotations import argparse import json import sys import time import urllib.error import urllib.parse import urllib.request PROMPTS = [ {"role": "user", "content": "What is 7 × 8? Answer in one word."}, {"role": "user", "content": "Name the capital of France in one word."}, {"role": "user", "content": "Complete the sequence: 1, 1, 2, 3, 5, ___. Answer in one word."}, ] MODEL = "microsoft/Phi-3-medium-128k-instruct" def _get(url: str, timeout: float = 10.0) -> dict: with urllib.request.urlopen(url, timeout=timeout) as r: return json.loads(r.read()) def _post(url: str, payload: dict, timeout: float = 60.0) -> dict: data = json.dumps(payload).encode() req = urllib.request.Request( url, data=data, headers={"Content-Type": "application/json"}, method="POST" ) with urllib.request.urlopen(req, timeout=timeout) as r: return json.loads(r.read()) def discover_gateway(tracker_url: str) -> str: """Return the first tracker-mode node endpoint for MODEL.""" nodes = _get(f"{tracker_url}/v1/nodes", timeout=5.0) if isinstance(nodes, dict): nodes = list(nodes.values()) tracker_nodes = [ n for n in nodes if n.get("tracker_mode") and ( n.get("hf_repo") == MODEL or n.get("model") == MODEL.split("/")[-1] ) ] if not tracker_nodes: raise RuntimeError( f"No tracker-mode nodes found for {MODEL!r}. " "Is the first-shard node running and registered?" ) endpoint: str = tracker_nodes[0]["endpoint"] return endpoint.rstrip("/") def check_route(tracker_url: str, gateway_url: str) -> list[str]: """Return the full inference route for MODEL.""" url = f"{tracker_url}/v1/route?model={urllib.parse.quote(MODEL)}" try: resp = _get(url, timeout=5.0) return resp.get("route", []) except Exception as exc: print(f" Warning: could not fetch route: {exc}", file=sys.stderr) return [gateway_url] def run_inference(gateway_url: str, messages: list[dict]) -> tuple[str, int, float]: """Send one chat-completion request. Returns (content, tokens, elapsed_s).""" t0 = time.monotonic() resp = _post( f"{gateway_url}/v1/chat/completions", {"model": MODEL, "messages": messages, "stream": False}, timeout=120.0, ) elapsed = time.monotonic() - t0 choices = resp.get("choices") if not choices: raise ValueError(f"No choices in response: {resp}") content: str = choices[0].get("message", {}).get("content", "") if not isinstance(content, str): raise TypeError(f"Expected string content, got {type(content)}: {content}") usage = resp.get("usage", {}) tokens: int = usage.get("completion_tokens", len(content.split())) return content, tokens, elapsed def main(argv: list[str] | None = None) -> int: p = argparse.ArgumentParser(description=__doc__) p.add_argument("--tracker", required=True, help="Tracker URL, e.g. http://192.168.1.10:8080") p.add_argument( "--gateway", default=None, help="Inference entry point URL. Auto-discovered from tracker if omitted.", ) args = p.parse_args(argv) tracker_url = args.tracker.rstrip("/") print(f"Tracker: {tracker_url}") # Resolve gateway gateway_url = args.gateway.rstrip("/") if args.gateway else None if gateway_url is None: try: gateway_url = discover_gateway(tracker_url) print(f"Gateway (auto-discovered): {gateway_url}") except Exception as exc: print(f"ERROR: {exc}", file=sys.stderr) return 1 else: print(f"Gateway: {gateway_url}") # Show route route = check_route(tracker_url, gateway_url) print(f"Route: {route}") if len(route) < 2: print(" Warning: only one node in route — is the second-shard node registered?") print() failures = 0 for i, msg in enumerate(PROMPTS, start=1): print(f"[{i}] Q: {msg['content']}") try: content, tokens, elapsed = run_inference(gateway_url, [msg]) tps = tokens / elapsed if elapsed > 0 else 0.0 print(f" A: {content.strip()}") print(f" {tokens} tokens {elapsed:.2f}s {tps:.1f} t/s") except urllib.error.HTTPError as exc: body = exc.read().decode(errors="replace") print(f" ERROR {exc.code}: {body}", file=sys.stderr) failures += 1 except Exception as exc: print(f" ERROR: {exc}", file=sys.stderr) failures += 1 print() if failures == 0: print(f"All {len(PROMPTS)} requests completed successfully.") print("Exit code: 0") return 0 else: print(f"{failures}/{len(PROMPTS)} requests failed.", file=sys.stderr) return 1 if __name__ == "__main__": sys.exit(main())