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

@@ -251,12 +251,13 @@
"Commit only this story's code changes with a focused conventional commit message"
],
"priority": 10,
"passes": false,
"passes": true,
"notes": "Source issue: .scratch/distributed-inference-network/issues/10-meshnet-sdk.md",
"dependsOn": [
"US-005",
"US-006"
]
],
"completionNotes": "Completed by fresh Ralph/Codex session ef4eea0e; verified by pytest, compileall, editable SDK install, and diff check."
}
],
"metadata": {

View File

@@ -1,7 +1,9 @@
"""Gateway HTTP server — accepts OpenAI-format requests and routes to inference nodes."""
import http.server
import hashlib
import json
from collections import Counter
import threading
import time
import urllib.error
@@ -21,6 +23,7 @@ class _GatewayHTTPServer(http.server.HTTPServer):
model_presets: list[str] | None,
contracts: Any | None,
minimum_stake: int,
cost_per_layer_token_lamport: int,
):
super().__init__(addr, handler)
self.inference_route = inference_route
@@ -28,6 +31,7 @@ class _GatewayHTTPServer(http.server.HTTPServer):
self.model_presets = model_presets
self.contracts = contracts
self.minimum_stake = minimum_stake
self.cost_per_layer_token_lamport = cost_per_layer_token_lamport
class _GatewayHandler(http.server.BaseHTTPRequestHandler):
@@ -35,17 +39,29 @@ class _GatewayHandler(http.server.BaseHTTPRequestHandler):
pass
def do_GET(self):
if self.path == "/v1/health":
parsed = urllib.parse.urlparse(self.path)
if parsed.path == "/v1/health":
self._handle_health()
elif self.path == "/v1/models":
elif parsed.path == "/v1/models":
self._handle_models()
elif parsed.path == "/v1/models/available":
self._handle_available_models()
elif parsed.path == "/v1/wallet/balance":
self._handle_wallet_balance()
elif parsed.path == "/v1/estimate_cost":
self._handle_estimate_cost(parsed)
else:
self.send_response(404)
self.end_headers()
def do_POST(self):
if self.path == "/v1/chat/completions":
parsed = urllib.parse.urlparse(self.path)
if parsed.path == "/v1/chat/completions":
self._handle_chat_completions()
elif parsed.path == "/v1/wallet/top_up":
self._handle_wallet_top_up()
elif parsed.path == "/v1/request":
self._handle_meshnet_request()
else:
self.send_response(404)
self.end_headers()
@@ -71,6 +87,18 @@ class _GatewayHandler(http.server.BaseHTTPRequestHandler):
}
})
def _read_json_body(self) -> dict | None:
length = int(self.headers.get("Content-Length", 0))
try:
body = json.loads(self.rfile.read(length) or b"{}")
except (json.JSONDecodeError, ValueError):
self._send_json_error(400, "invalid JSON body")
return None
if not isinstance(body, dict):
self._send_json_error(400, "JSON body must be an object")
return None
return body
def _handle_health(self) -> None:
self._send_json(200, {"status": "ok"})
@@ -92,76 +120,236 @@ class _GatewayHandler(http.server.BaseHTTPRequestHandler):
]
self._send_json(200, {"object": "list", "data": data})
def _handle_available_models(self) -> None:
server: _GatewayHTTPServer = self.server # type: ignore[assignment]
if server.tracker_url is not None:
try:
models_resp = _get_json(f"{server.tracker_url}/v1/models")
except Exception:
self._send_json_error(503, "tracker unavailable")
return
data = [
{
"id": model["id"],
"shard_coverage_percentage": float(model.get("shard_coverage_percentage", 0.0)),
}
for model in models_resp.get("data", [])
if isinstance(model, dict) and isinstance(model.get("id"), str)
]
self._send_json(200, {"data": data})
return
presets = server.model_presets or ["stub-model"]
self._send_json(200, {
"data": [
{"id": model, "shard_coverage_percentage": 100.0}
for model in presets
]
})
def _handle_wallet_balance(self) -> None:
server: _GatewayHTTPServer = self.server # type: ignore[assignment]
api_key = _api_key_from_authorization(self.headers.get("Authorization"))
if not api_key:
self._send_json_error(401, "missing API key")
return
if server.contracts is None:
self._send_json(200, {"sol": 0.0, "usdc": 0.0})
return
balance = server.contracts.payment.get_balance(api_key)
self._send_json(200, {
"sol": _lamports_to_sol(balance.lamports),
"usdc": balance.usdc_micro / 1_000_000,
})
def _handle_wallet_top_up(self) -> None:
api_key = _api_key_from_authorization(self.headers.get("Authorization"))
if not api_key:
self._send_json_error(401, "missing API key")
return
body = self._read_json_body()
if body is None:
return
try:
amount_sol = float(body.get("amount_sol", 0.0))
amount_usdc = float(body.get("amount_usdc", 0.0))
except (TypeError, ValueError):
self._send_json_error(400, "top-up amount must be numeric")
return
if amount_sol <= 0 and amount_usdc <= 0:
self._send_json_error(400, "top-up amount must be positive")
return
payment_address = _payment_address_for_api_key(api_key)
query = urllib.parse.urlencode({
"amount": amount_sol,
"spl-token": "USDC" if amount_usdc > 0 else "SOL",
})
self._send_json(200, {
"payment_address": payment_address,
"qr": f"solana:{payment_address}?{query}",
"amount_sol": amount_sol,
"amount_usdc": amount_usdc,
})
def _handle_estimate_cost(self, parsed: urllib.parse.ParseResult) -> None:
server: _GatewayHTTPServer = self.server # type: ignore[assignment]
params = urllib.parse.parse_qs(parsed.query)
model = params.get("model", [""])[0]
try:
tokens = int(params.get("tokens", ["0"])[0])
except ValueError:
self._send_json_error(400, "tokens must be an integer")
return
if not model:
self._send_json_error(400, "model is required")
return
if tokens <= 0:
self._send_json_error(400, "tokens must be positive")
return
layer_count = 32
if server.tracker_url is None and server.model_presets is not None and model not in server.model_presets:
self._send_json_error(404, "model not available")
return
lamports = layer_count * tokens * server.cost_per_layer_token_lamport
self._send_json(200, {
"model": model,
"tokens": tokens,
"lamports": lamports,
"sol": _lamports_to_sol(lamports),
})
def _handle_chat_completions(self):
server: _GatewayHTTPServer = self.server # type: ignore[assignment]
length = int(self.headers.get("Content-Length", 0))
try:
body = json.loads(self.rfile.read(length))
except (json.JSONDecodeError, ValueError):
self._send_openai_error(400, "invalid JSON body", "invalid_request_error")
body = self._read_json_body()
if body is None:
return
streaming = bool(body.get("stream", False))
if server.tracker_url is not None:
model = body.get("model", "stub-model")
tracker_path = f"{server.tracker_url}/v1/route?model={urllib.parse.quote(model)}"
try:
tracker_resp = _get_json(tracker_path)
route = tracker_resp["route"]
if (
not isinstance(route, list)
or not route
or not all(isinstance(node_url, str) and node_url for node_url in route)
):
raise ValueError("invalid tracker route")
route_nodes = tracker_resp.get("nodes", [])
if route_nodes and not _valid_route_nodes(route_nodes):
raise ValueError("invalid tracker route metadata")
banned_wallet = _banned_route_wallet(server.contracts, route_nodes)
if banned_wallet is not None:
self._send_openai_error(
503,
f"model not available: route includes banned wallet {banned_wallet}",
"model_not_available",
)
completion = self._build_completion(body)
except _ModelUnavailable as exc:
self._send_openai_error(503, exc.message, "model_not_available")
return
except urllib.error.HTTPError as exc:
error_body = _safe_error_body(exc)
self._send_openai_error(
503,
error_body.get("error", "model not available"),
"model_not_available",
)
return
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, KeyError, TypeError, ValueError):
except _TrackerUnavailable:
self._send_json_error(503, "tracker unavailable")
return
else:
route = server.inference_route # type: ignore[assignment]
route_nodes = []
messages = body.get("messages", [])
try:
# Send messages to the first node in the inference route.
node_resp = _post_json(f"{route[0]}/v1/infer", {"messages": messages})
# Forward activations through remaining nodes in the route.
for node_url in route[1:]:
node_resp = _post_json(f"{node_url}/v1/infer", {"activations": node_resp["activations"]})
except (urllib.error.URLError, TimeoutError, KeyError, json.JSONDecodeError) as exc:
self._send_openai_error(503, f"inference pipeline error: {exc}", "pipeline_error")
return
if streaming:
text = completion["choices"][0]["message"]["content"]
self._send_sse_response(text, completion["model"])
else:
self._send_json(200, completion)
def _handle_meshnet_request(self) -> None:
body = self._read_json_body()
if body is None:
return
try:
redundancy = int(body.get("redundancy", 1))
except (TypeError, ValueError):
self._send_json_error(400, "redundancy must be an integer")
return
if redundancy < 1:
self._send_json_error(400, "redundancy must be at least 1")
return
try:
routes = self._resolve_routes(str(body.get("model", "stub-model")), redundancy)
responses = [self._build_completion(body, route_payload=route_payload) for route_payload in routes]
except _ModelUnavailable as exc:
self._send_openai_error(503, exc.message, "model_not_available")
return
except _TrackerUnavailable:
self._send_json_error(503, "tracker unavailable")
return
except (urllib.error.URLError, TimeoutError, KeyError, json.JSONDecodeError) as exc:
self._send_openai_error(503, f"inference pipeline error: {exc}", "pipeline_error")
return
majority_response = _majority_response(responses)
self._send_json(200, {
"redundancy": redundancy,
"majority_response": majority_response,
"responses": responses,
})
def _build_completion(self, body: dict, route_payload: dict | None = None) -> dict:
server: _GatewayHTTPServer = self.server # type: ignore[assignment]
if route_payload is None:
route_payload = self._resolve_route(str(body.get("model", "stub-model")))
route = route_payload["route"]
route_nodes = route_payload.get("nodes", [])
messages = body.get("messages", [])
node_resp = _post_json(f"{route[0]}/v1/infer", {"messages": messages})
for node_url in route[1:]:
node_resp = _post_json(f"{node_url}/v1/infer", {"activations": node_resp["activations"]})
text = node_resp["text"]
model = body.get("model", "stub-model")
self._record_inference(model, messages, text, route_nodes)
return _completion_response(str(model), text)
def _resolve_route(self, model: str) -> dict:
server: _GatewayHTTPServer = self.server # type: ignore[assignment]
if server.tracker_url is not None:
tracker_path = f"{server.tracker_url}/v1/route?model={urllib.parse.quote(model)}"
try:
tracker_resp = _get_json(tracker_path)
_validate_route_payload(tracker_resp)
banned_wallet = _banned_route_wallet(server.contracts, tracker_resp.get("nodes", []))
if banned_wallet is not None:
raise _ModelUnavailable(f"model not available: route includes banned wallet {banned_wallet}")
return tracker_resp
except urllib.error.HTTPError as exc:
error_body = _safe_error_body(exc)
raise _ModelUnavailable(error_body.get("error", "model not available")) from exc
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, KeyError, TypeError, ValueError) as exc:
raise _TrackerUnavailable() from exc
return {"route": server.inference_route, "nodes": []}
def _resolve_routes(self, model: str, redundancy: int) -> list[dict]:
server: _GatewayHTTPServer = self.server # type: ignore[assignment]
if server.tracker_url is None:
return [self._resolve_route(model) for _ in range(redundancy)]
tracker_path = (
f"{server.tracker_url}/v1/routes?"
f"model={urllib.parse.quote(model)}&redundancy={redundancy}"
)
try:
tracker_resp = _get_json(tracker_path)
routes = tracker_resp["routes"]
if not isinstance(routes, list) or len(routes) != redundancy:
raise ValueError("invalid tracker routes")
for route_payload in routes:
_validate_route_payload(route_payload)
banned_wallet = _banned_route_wallet(server.contracts, route_payload.get("nodes", []))
if banned_wallet is not None:
raise _ModelUnavailable(f"model not available: route includes banned wallet {banned_wallet}")
return routes
except urllib.error.HTTPError as exc:
error_body = _safe_error_body(exc)
raise _ModelUnavailable(error_body.get("error", "model not available")) from exc
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, KeyError, TypeError, ValueError) as exc:
raise _TrackerUnavailable() from exc
def _record_inference(
self,
model: object,
messages: object,
text: str,
route_nodes: object,
) -> None:
server: _GatewayHTTPServer = self.server # type: ignore[assignment]
session_id = str(uuid.uuid4())
if server.contracts is not None and route_nodes:
if server.contracts is not None and isinstance(route_nodes, list):
api_key = _api_key_from_authorization(self.headers.get("Authorization"))
if api_key:
tokens = max(1, _estimate_token_count(messages, text))
for node in route_nodes:
wallet_address = node.get("wallet_address")
wallet_address = node.get("wallet_address") if isinstance(node, dict) else None
if not wallet_address:
continue
server.contracts.payment.record_attribution(
@@ -176,35 +364,12 @@ class _GatewayHandler(http.server.BaseHTTPRequestHandler):
if server.contracts is not None:
server.contracts.validation.record_completed_inference(
session_id=session_id,
model=model,
model=str(model),
messages=messages if isinstance(messages, list) else [],
observed_output=text,
route_nodes=route_nodes,
route_nodes=route_nodes if isinstance(route_nodes, list) else [],
)
if streaming:
self._send_sse_response(text, model)
else:
self._send_json(200, {
"id": "chatcmpl-stub",
"object": "chat.completion",
"created": int(time.time()),
"model": model,
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": text,
},
"finish_reason": "stop",
}],
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0,
},
})
def _send_sse_response(self, text: str, model: str) -> None:
chunk_id = "chatcmpl-stub"
created = int(time.time())
@@ -282,6 +447,19 @@ def _valid_route_nodes(route_nodes: object) -> bool:
return True
def _validate_route_payload(route_payload: dict) -> None:
route = route_payload["route"]
if (
not isinstance(route, list)
or not route
or not all(isinstance(node_url, str) and node_url for node_url in route)
):
raise ValueError("invalid tracker route")
route_nodes = route_payload.get("nodes", [])
if route_nodes and not _valid_route_nodes(route_nodes):
raise ValueError("invalid tracker route metadata")
def _banned_route_wallet(contracts: Any | None, route_nodes: object) -> str | None:
if contracts is None or not isinstance(route_nodes, list):
return None
@@ -308,6 +486,66 @@ def _estimate_token_count(messages: object, completion_text: str) -> int:
return 1
def _lamports_to_sol(lamports: int) -> float:
return lamports / 1_000_000_000
def _payment_address_for_api_key(api_key: str) -> str:
alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
value = int.from_bytes(hashlib.sha256(f"meshnet:{api_key}".encode()).digest(), "big")
chars = []
while value:
value, remainder = divmod(value, 58)
chars.append(alphabet[remainder])
return "".join(reversed(chars))[:44].rjust(32, "1")
def _completion_response(model: str, text: str) -> dict:
return {
"id": "chatcmpl-stub",
"object": "chat.completion",
"created": int(time.time()),
"model": model,
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": text,
},
"finish_reason": "stop",
}],
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0,
},
}
def _majority_response(responses: list[dict]) -> dict:
if not responses:
raise ValueError("responses must not be empty")
contents = [
response["choices"][0]["message"]["content"]
for response in responses
]
majority_content, _ = Counter(contents).most_common(1)[0]
for response in responses:
if response["choices"][0]["message"]["content"] == majority_content:
return response
return responses[0]
class _ModelUnavailable(Exception):
def __init__(self, message: str) -> None:
super().__init__(message)
self.message = message
class _TrackerUnavailable(Exception):
pass
def _post_json(url: str, payload: dict, timeout: float = 5.0) -> dict:
data = json.dumps(payload).encode()
req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"}, method="POST")
@@ -341,6 +579,7 @@ class GatewayServer:
model_presets: list[str] | None = None,
contracts: Any | None = None,
minimum_stake: int = 0,
cost_per_layer_token_lamport: int = 1,
host: str = "127.0.0.1",
port: int = 0,
):
@@ -360,6 +599,7 @@ class GatewayServer:
self._model_presets: list[str] | None = model_presets
self._contracts = contracts
self._minimum_stake = minimum_stake
self._cost_per_layer_token_lamport = cost_per_layer_token_lamport
self._host = host
self._requested_port = port
self._server: _GatewayHTTPServer | None = None
@@ -378,6 +618,7 @@ class GatewayServer:
self._model_presets,
self._contracts,
self._minimum_stake,
self._cost_per_layer_token_lamport,
)
self.port = self._server.server_address[1]
self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)

View File

@@ -1,3 +1,19 @@
"""meshnet — Distributed Inference Network Python SDK."""
from .client import (
Client,
CostEstimate,
RedundantRequestResult,
TopUpQuote,
WalletBalance,
)
__version__ = "0.1.0"
__all__ = [
"Client",
"CostEstimate",
"RedundantRequestResult",
"TopUpQuote",
"WalletBalance",
]

View File

@@ -0,0 +1,10 @@
from .client import (
Client as Client,
CostEstimate as CostEstimate,
RedundantRequestResult as RedundantRequestResult,
TopUpQuote as TopUpQuote,
WalletBalance as WalletBalance,
)
__version__: str
__all__: list[str]

View File

@@ -0,0 +1,167 @@
"""Typed Python SDK for the meshnet OpenAI-compatible gateway."""
from __future__ import annotations
from dataclasses import dataclass
import json
import urllib.parse
import urllib.request
from typing import Any
import openai
from openai.types.chat import ChatCompletion
@dataclass(frozen=True)
class WalletBalance:
"""API-key balance in client-facing currencies."""
sol: float
usdc: float
@dataclass(frozen=True)
class TopUpQuote:
"""Payment details for funding an API key account."""
payment_address: str
qr: str
amount_sol: float
amount_usdc: float
@dataclass(frozen=True)
class CostEstimate:
"""Preflight cost estimate for a request."""
model: str
tokens: int
lamports: int
sol: float
@dataclass(frozen=True)
class RedundantRequestResult:
"""Result of sending a prompt through multiple independent routes."""
redundancy: int
majority_response: ChatCompletion
responses: list[ChatCompletion]
class Client:
"""meshnet SDK client.
``chat`` is the official OpenAI SDK chat resource, configured against the
meshnet gateway. Network-specific controls live on ``wallet``, ``models``,
``estimate_cost``, and ``request``.
"""
def __init__(self, *, api_key: str, base_url: str = "https://api.meshnet.ai") -> None:
self.api_key = api_key
self.base_url = base_url.rstrip("/")
self._openai = openai.OpenAI(
api_key=api_key,
base_url=_openai_base_url(self.base_url),
)
self.chat = self._openai.chat
self.wallet = _WalletClient(self)
self.models = _ModelsClient(self)
def estimate_cost(self, *, model: str, tokens: int) -> CostEstimate:
query = urllib.parse.urlencode({"model": model, "tokens": tokens})
data = self._get_json(f"/v1/estimate_cost?{query}")
return CostEstimate(
model=str(data["model"]),
tokens=int(data["tokens"]),
lamports=int(data["lamports"]),
sol=float(data["sol"]),
)
def request(
self,
*,
model: str,
messages: list[dict[str, Any]],
redundancy: int = 1,
**params: Any,
) -> RedundantRequestResult:
payload: dict[str, Any] = {
"model": model,
"messages": messages,
"redundancy": redundancy,
**params,
}
data = self._post_json("/v1/request", payload)
return RedundantRequestResult(
redundancy=int(data["redundancy"]),
majority_response=_chat_completion(data["majority_response"]),
responses=[_chat_completion(item) for item in data["responses"]],
)
def _get_json(self, path: str) -> dict[str, Any]:
req = urllib.request.Request(
f"{self.base_url}{path}",
headers=self._headers(),
method="GET",
)
with urllib.request.urlopen(req, timeout=10) as resp:
return json.loads(resp.read())
def _post_json(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
data = json.dumps(payload).encode()
req = urllib.request.Request(
f"{self.base_url}{path}",
data=data,
headers={**self._headers(), "Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read())
def _headers(self) -> dict[str, str]:
return {"Authorization": f"Bearer {self.api_key}"}
class _WalletClient:
def __init__(self, client: Client) -> None:
self._client = client
def balance(self) -> WalletBalance:
data = self._client._get_json("/v1/wallet/balance")
return WalletBalance(sol=float(data["sol"]), usdc=float(data["usdc"]))
def top_up(self, *, amount_sol: float = 0.0, amount_usdc: float = 0.0) -> TopUpQuote:
data = self._client._post_json(
"/v1/wallet/top_up",
{"amount_sol": amount_sol, "amount_usdc": amount_usdc},
)
return TopUpQuote(
payment_address=str(data["payment_address"]),
qr=str(data["qr"]),
amount_sol=float(data["amount_sol"]),
amount_usdc=float(data["amount_usdc"]),
)
class _ModelsClient:
def __init__(self, client: Client) -> None:
self._client = client
def available(self) -> list[dict[str, float | str]]:
data = self._client._get_json("/v1/models/available")
return [
{
"id": str(model["id"]),
"shard_coverage_percentage": float(model["shard_coverage_percentage"]),
}
for model in data["data"]
]
def _openai_base_url(base_url: str) -> str:
return base_url if base_url.endswith("/v1") else f"{base_url}/v1"
def _chat_completion(data: dict[str, Any]) -> ChatCompletion:
return ChatCompletion.model_validate(data)

View File

@@ -0,0 +1,64 @@
from typing import Any
from openai.resources.chat.chat import Chat
from openai.types.chat import ChatCompletion
class WalletBalance:
sol: float
usdc: float
def __init__(self, sol: float, usdc: float) -> None: ...
class TopUpQuote:
payment_address: str
qr: str
amount_sol: float
amount_usdc: float
def __init__(
self,
payment_address: str,
qr: str,
amount_sol: float,
amount_usdc: float,
) -> None: ...
class CostEstimate:
model: str
tokens: int
lamports: int
sol: float
def __init__(self, model: str, tokens: int, lamports: int, sol: float) -> None: ...
class RedundantRequestResult:
redundancy: int
majority_response: ChatCompletion
responses: list[ChatCompletion]
def __init__(
self,
redundancy: int,
majority_response: ChatCompletion,
responses: list[ChatCompletion],
) -> None: ...
class _WalletClient:
def balance(self) -> WalletBalance: ...
def top_up(self, *, amount_sol: float = 0.0, amount_usdc: float = 0.0) -> TopUpQuote: ...
class _ModelsClient:
def available(self) -> list[dict[str, float | str]]: ...
class Client:
api_key: str
base_url: str
chat: Chat
wallet: _WalletClient
models: _ModelsClient
def __init__(self, *, api_key: str, base_url: str = "https://api.meshnet.ai") -> None: ...
def estimate_cost(self, *, model: str, tokens: int) -> CostEstimate: ...
def request(
self,
*,
model: str,
messages: list[dict[str, Any]],
redundancy: int = 1,
**params: Any,
) -> RedundantRequestResult: ...

View File

@@ -0,0 +1 @@

View File

@@ -7,7 +7,11 @@ name = "meshnet"
version = "0.1.0"
description = "Distributed Inference Network Python SDK"
requires-python = ">=3.10"
dependencies = ["openai>=1"]
[tool.setuptools.packages.find]
where = ["."]
include = ["meshnet*"]
[tool.setuptools.package-data]
meshnet = ["py.typed", "*.pyi"]

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.

143
tests/test_meshnet_sdk.py Normal file
View File

@@ -0,0 +1,143 @@
"""US-010 integration tests: typed meshnet SDK over local gateway/tracker/nodes."""
import json
import urllib.request
from meshnet import Client
from meshnet_contracts import LocalSolanaContracts
from meshnet_gateway.server import GatewayServer, _payment_address_for_api_key
from meshnet_node.server import StubNodeServer
from meshnet_tracker.server import TrackerServer, _NodeEntry, _coverage_percentage
def test_sdk_support_helpers_are_deterministic_without_network():
"""Non-socket coverage for SDK-facing gateway/tracker helper behavior."""
address = _payment_address_for_api_key("sdk-key")
nodes = [
_NodeEntry("a", "http://node-a", 0, 15, "stub-model", None, {}, None, 1.0),
_NodeEntry("b", "http://node-b", 16, 31, "stub-model", None, {}, None, 1.0),
]
assert 32 <= len(address) <= 44
assert _coverage_percentage(nodes, 0, 31) == 100.0
def _post_json(url: str, payload: dict) -> 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) as r:
return json.loads(r.read())
def _register_node(
tracker_url: str,
endpoint: str,
shard_start: int,
shard_end: int,
*,
wallet_address: str,
) -> None:
_post_json(
f"{tracker_url}/v1/nodes/register",
{
"endpoint": endpoint,
"shard_start": shard_start,
"shard_end": shard_end,
"hardware_profile": {},
"wallet_address": wallet_address,
"score": 1.0,
},
)
def test_meshnet_sdk_methods_against_local_network():
"""SDK covers OpenAI chat passthrough plus meshnet wallet/model/cost/request APIs."""
contracts = LocalSolanaContracts(starting_credit_lamports=0)
contracts.payment.fund_api_key("sdk-key", lamports=2_000_000_000, usdc_micro=123_000_000)
tracker = TrackerServer(contracts=contracts)
tracker_port = tracker.start()
tracker_url = f"http://127.0.0.1:{tracker_port}"
nodes = [
StubNodeServer(shard_start=0, shard_end=15, is_last_shard=False),
StubNodeServer(shard_start=16, shard_end=31, is_last_shard=True),
StubNodeServer(shard_start=0, shard_end=15, is_last_shard=False),
StubNodeServer(shard_start=16, shard_end=31, is_last_shard=True),
]
node_ports = [node.start() for node in nodes]
for index, (port, shard_start, shard_end) in enumerate(
[
(node_ports[0], 0, 15),
(node_ports[1], 16, 31),
(node_ports[2], 0, 15),
(node_ports[3], 16, 31),
],
start=1,
):
_register_node(
tracker_url,
f"http://127.0.0.1:{port}",
shard_start,
shard_end,
wallet_address=f"wallet-{index}",
)
gateway = GatewayServer(tracker_url=tracker_url, contracts=contracts)
gateway_port = gateway.start()
gateway_url = f"http://127.0.0.1:{gateway_port}"
try:
client = Client(api_key="sdk-key", base_url=gateway_url)
chat_response = client.chat.completions.create(
model="stub-model",
messages=[{"role": "user", "content": "hello sdk"}],
)
assert chat_response.choices[0].message.content == "stub response to: hello sdk"
balance = client.wallet.balance()
assert balance.sol == 2.0
assert balance.usdc == 123.0
top_up = client.wallet.top_up(amount_sol=1.0)
assert top_up.amount_sol == 1.0
assert 32 <= len(top_up.payment_address) <= 44
assert top_up.qr.startswith("solana:")
available_models = client.models.available()
assert available_models == [
{
"id": "stub-model",
"shard_coverage_percentage": 100.0,
}
]
estimate = client.estimate_cost(model="stub-model", tokens=1_000)
assert estimate.model == "stub-model"
assert estimate.tokens == 1_000
assert estimate.sol == 0.000032
redundant = client.request(
model="stub-model",
messages=[{"role": "user", "content": "redundant"}],
redundancy=2,
)
assert redundant.redundancy == 2
assert redundant.majority_response.choices[0].message.content == (
"stub response to: redundant"
)
assert len(redundant.responses) == 2
assert nodes[1].received_activations
assert nodes[3].received_activations
finally:
gateway.stop()
for node in nodes:
node.stop()
tracker.stop()