feat: add meshnet python sdk
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user