feat: add meshnet python sdk

This commit is contained in:
Dobromir Popov
2026-06-29 10:29:19 +03:00
parent e9c9bf63bc
commit 3286f77e27
10 changed files with 850 additions and 87 deletions

View File

@@ -16,6 +16,9 @@ HTTP API contract:
- GET /v1/route?model=<preset>
Response 200: {"route": ["http://node-a", "http://node-b"]}
Response 400/404/503: {"error": str}
- GET /v1/routes?model=<preset>&redundancy=<n>
Response 200: {"routes": [{"route": [...], "nodes": [...]}]}
Response 400/404/503: {"error": str}
"""
import http.server
@@ -89,6 +92,36 @@ def _select_route(
return route, ""
def _coverage_percentage(
nodes: list[_NodeEntry],
required_start: int,
required_end: int,
) -> float:
required_layers = required_end - required_start + 1
if required_layers <= 0:
return 0.0
intervals = sorted(
(
(max(required_start, node.shard_start), min(required_end, node.shard_end))
for node in nodes
if node.shard_end >= required_start and node.shard_start <= required_end
),
key=lambda interval: interval[0],
)
covered = 0
covered_until = required_start - 1
for start, end in intervals:
if end <= covered_until:
continue
next_start = max(start, covered_until + 1)
if next_start > end:
continue
covered += end - next_start + 1
covered_until = end
return round((covered / required_layers) * 100, 2)
def _registration_ban_error(contracts: Any | None, wallet_address: str | None) -> str | None:
if contracts is None or not wallet_address:
return None
@@ -168,6 +201,8 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
parsed = urllib.parse.urlparse(self.path)
if parsed.path == "/v1/route":
self._handle_route(parsed)
elif parsed.path == "/v1/routes":
self._handle_routes(parsed)
elif parsed.path == "/v1/nodes/assign":
self._handle_assign(parsed)
elif parsed.path == "/v1/models":
@@ -179,10 +214,29 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
def _handle_models(self):
server: _TrackerHTTPServer = self.server # type: ignore[assignment]
created = int(time.time())
data = [
{"id": name, "object": "model", "created": created, "owned_by": "meshnet"}
for name in server.model_presets
]
with server.lock:
self._purge_expired_nodes()
alive = list(server.registry.values())
if server.contracts is not None:
alive = [
node for node in alive
if not node.wallet_address or not server.contracts.registry.get_wallet(node.wallet_address).banned
]
data = []
for name, preset in server.model_presets.items():
model_nodes = [node for node in alive if node.model == name]
coverage = _coverage_percentage(
model_nodes,
preset["layers_start"],
preset["layers_end"],
)
data.append({
"id": name,
"object": "model",
"created": created,
"owned_by": "meshnet",
"shard_coverage_percentage": coverage,
})
self._send_json(200, {"object": "list", "data": data})
def _handle_register(self):
@@ -400,6 +454,68 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
],
})
def _handle_routes(self, parsed: urllib.parse.ParseResult):
server: _TrackerHTTPServer = self.server # type: ignore[assignment]
params = urllib.parse.parse_qs(parsed.query)
model_list = params.get("model")
if not model_list:
self._send_json(400, {"error": "missing 'model' query parameter"})
return
try:
redundancy = int(params.get("redundancy", ["1"])[0])
except ValueError:
self._send_json(400, {"error": "redundancy must be an integer"})
return
if redundancy < 1:
self._send_json(400, {"error": "redundancy must be at least 1"})
return
model = model_list[0]
preset = server.model_presets.get(model)
if preset is None:
self._send_json(404, {"error": f"unknown model preset: {model!r}"})
return
required_start: int = preset["layers_start"]
required_end: int = preset["layers_end"]
with server.lock:
self._purge_expired_nodes()
candidates = [node for node in server.registry.values() if node.model == model]
if server.contracts is not None:
candidates = [
node for node in candidates
if not node.wallet_address or not server.contracts.registry.get_wallet(node.wallet_address).banned
]
routes = []
remaining = list(candidates)
for _ in range(redundancy):
route, error = _select_route(remaining, required_start, required_end)
if error:
self._send_json(503, {"error": error})
return
route_endpoints = {node.endpoint for node in route}
remaining = [node for node in remaining if node.endpoint not in route_endpoints]
routes.append({
"route": [e.endpoint for e in route],
"nodes": [
{
"endpoint": e.endpoint,
"wallet_address": e.wallet_address,
"shard_start": e.shard_start,
"shard_end": e.shard_end,
"model": e.model,
"shard_checksum": e.shard_checksum,
"score": e.score,
}
for e in route
],
})
self._send_json(200, {"routes": routes})
class TrackerServer:
"""HTTP tracker that manages node registration and resolves inference routes.