new tasks, devnet topup, cli, new model support

This commit is contained in:
Dobromir Popov
2026-07-06 14:17:36 +03:00
parent f841dfaeed
commit b547034741
24 changed files with 1298 additions and 63 deletions

View File

@@ -159,6 +159,30 @@ CURATED_MODELS: list[ModelPreset] = [
]
def layers_from_config(cfg) -> int | None:
"""Extract the transformer layer count from a HuggingFace config object.
Composite configs (vision-language, some MoE) nest the decoder settings in
``text_config`` — e.g. Qwen3.5-MoE has no top-level ``num_hidden_layers``.
"""
candidates = [cfg]
get_text_config = getattr(cfg, "get_text_config", None)
if callable(get_text_config):
try:
candidates.append(get_text_config())
except Exception:
pass
nested = getattr(cfg, "text_config", None)
if nested is not None:
candidates.append(nested)
for candidate in candidates:
for attr in ("num_hidden_layers", "num_layers", "n_layer", "n_layers"):
value = getattr(candidate, attr, None)
if value is not None:
return int(value)
return None
def detect_num_layers(hf_repo: str) -> int | None:
"""Return num_hidden_layers from HuggingFace config.json (downloads ~1 KB only)."""
# Check curated list first (no network call)
@@ -168,7 +192,7 @@ def detect_num_layers(hf_repo: str) -> int | None:
try:
from transformers import AutoConfig # type: ignore[import]
cfg = AutoConfig.from_pretrained(hf_repo)
return int(cfg.num_hidden_layers)
return layers_from_config(cfg)
except Exception:
return None
@@ -195,6 +219,8 @@ def model_metadata_for(
hf_repo,
cache_dir=str(cache_dir) if cache_dir is not None else None,
)
# Composite configs (VLM/MoE) nest decoder fields in text_config.
text_cfg = getattr(cfg, "text_config", None) or cfg
for attr, key in (
("model_type", "architecture"),
("num_hidden_layers", "num_layers"),
@@ -204,8 +230,14 @@ def model_metadata_for(
("max_position_embeddings", "context_length"),
):
value = getattr(cfg, attr, None)
if value is None:
value = getattr(text_cfg, attr, None)
if value is not None:
metadata[key] = value
if "num_layers" not in metadata:
layers = layers_from_config(cfg)
if layers is not None:
metadata["num_layers"] = layers
except Exception:
pass
return metadata

View File

@@ -5,14 +5,18 @@ from __future__ import annotations
import base64
import json
import logging
import os
import threading
import time
import urllib.error
import urllib.request
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
log = logging.getLogger(__name__)
DEFAULT_MAX_CONCURRENCY = 8
@dataclass(frozen=True)
class RelayBridgeInfo:
@@ -32,8 +36,23 @@ def _make_envelope(topic: str, payload: dict, peer_id: str) -> dict:
}
def _max_concurrency_from_env() -> int:
try:
value = int(os.environ.get("MESHNET_RELAY_CONCURRENCY", DEFAULT_MAX_CONCURRENCY))
except (TypeError, ValueError):
return DEFAULT_MAX_CONCURRENCY
return max(1, value)
class RelayHttpBridge:
"""Connect outbound to a relay and proxy relay HTTP requests to localhost."""
"""Connect outbound to a relay and proxy relay HTTP requests to localhost.
Requests are dispatched on a bounded worker pool (US-037) so a node that is
the head of one route can still serve per-token ``/forward`` hops of another.
Streaming responses (``text/event-stream``) are forwarded as multiple chunk
frames sharing one ``request_id`` (US-036); everything else stays a single
frame for backward compatibility.
"""
def __init__(
self,
@@ -42,15 +61,20 @@ class RelayHttpBridge:
local_base_url: str,
advertised_addr: str,
reconnect_interval: float = 3.0,
max_concurrency: int | None = None,
) -> None:
self.relay_url = relay_url.rstrip("/")
self.peer_id = peer_id
self.local_base_url = local_base_url.rstrip("/")
self.advertised_addr = advertised_addr
self.reconnect_interval = reconnect_interval
self.max_concurrency = max(1, max_concurrency) if max_concurrency else _max_concurrency_from_env()
self._running = False
self._thread: threading.Thread | None = None
self._connected = threading.Event()
self._executor: ThreadPoolExecutor | None = None
self._send_lock = threading.Lock()
self._ws = None
@property
def relay_addr(self) -> str:
@@ -61,6 +85,9 @@ class RelayHttpBridge:
def start(self) -> RelayBridgeInfo:
self._running = True
self._executor = ThreadPoolExecutor(
max_workers=self.max_concurrency, thread_name_prefix="relay-http-worker"
)
self._thread = threading.Thread(target=self._run, daemon=True, name="relay-http-bridge")
self._thread.start()
return RelayBridgeInfo(peer_id=self.peer_id, relay_addr=self.relay_addr)
@@ -72,6 +99,8 @@ class RelayHttpBridge:
self._running = False
if self._thread:
self._thread.join(timeout=3.0)
if self._executor is not None:
self._executor.shutdown(wait=False)
def _run(self) -> None:
import websockets.sync.client as wsc # type: ignore[import]
@@ -79,6 +108,7 @@ class RelayHttpBridge:
while self._running:
try:
with wsc.connect(self.relay_url, open_timeout=5) as ws:
self._ws = ws
self._connected.set()
ws.send(json.dumps(_make_envelope(
"peer-register",
@@ -99,19 +129,36 @@ class RelayHttpBridge:
payload = envelope.get("payload", {})
if payload.get("target_peer") not in {None, self.peer_id}:
continue
response = self._handle_request(payload)
ws.send(json.dumps(_make_envelope(
"relay-http-response",
response,
self.peer_id,
)))
if self._executor is None:
break
self._executor.submit(self._process_request, payload)
except Exception as exc:
self._connected.clear()
self._ws = None
if self._running:
log.debug("relay bridge disconnected: %s", exc)
time.sleep(self.reconnect_interval)
self._ws = None
def _handle_request(self, payload: dict) -> dict:
def _send_response_frame(self, payload: dict) -> bool:
"""Send one relay-http-response frame; False if the socket is gone.
The lock is held per frame so concurrent workers interleave whole
frames on the shared WebSocket, never torn ones.
"""
ws = self._ws
if ws is None:
return False
message = json.dumps(_make_envelope("relay-http-response", payload, self.peer_id))
try:
with self._send_lock:
ws.send(message)
return True
except Exception as exc:
log.debug("relay bridge send failed (request orphaned): %s", exc)
return False
def _process_request(self, payload: dict) -> None:
request_id = str(payload.get("request_id") or "")
method = str(payload.get("method") or "POST").upper()
path = str(payload.get("path") or "/")
@@ -130,10 +177,14 @@ class RelayHttpBridge:
req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=300.0) as resp:
resp_bytes = resp.read()
resp_headers = dict(resp.headers)
content_type = resp.headers.get("Content-Type", "")
if "text/event-stream" in content_type:
self._stream_response(request_id, resp, resp_headers)
return
resp_bytes = resp.read()
# Forward all X-Meshnet-* headers so the caller can reconstruct the activation.
is_binary = "octet-stream" in resp.headers.get("Content-Type", "")
is_binary = "octet-stream" in content_type
result: dict = {
"request_id": request_id,
"status": resp.status,
@@ -143,21 +194,66 @@ class RelayHttpBridge:
result["body_base64"] = base64.b64encode(resp_bytes).decode()
else:
result["body"] = resp_bytes.decode(errors="replace")
return result
self._send_response_frame(result)
except urllib.error.HTTPError as exc:
return {
self._send_response_frame({
"request_id": request_id,
"status": exc.code,
"headers": {"Content-Type": exc.headers.get("Content-Type", "application/json")},
"body": exc.read().decode(errors="replace"),
}
})
except Exception as exc:
return {
self._send_response_frame({
"request_id": request_id,
"status": 503,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"error": f"relay bridge local request failed: {exc}"}),
}
})
def _stream_response(self, request_id: str, resp, resp_headers: dict) -> None:
"""Forward an SSE response as chunk frames, one per complete SSE event.
Frame order: header frame (status + headers), chunk frames, done frame.
A receiver that sees no ``stream`` key treats the frame as a complete
legacy response, so non-streaming peers are unaffected.
"""
sent = self._send_response_frame({
"request_id": request_id,
"status": resp.status,
"headers": resp_headers,
"stream": True,
"done": False,
})
if not sent:
return
event_lines: list[str] = []
for raw_line in resp:
line = raw_line.decode(errors="replace")
event_lines.append(line)
if line.strip():
continue
# Blank line terminates one SSE event — flush it as a frame.
if not self._send_response_frame({
"request_id": request_id,
"stream": True,
"chunk": "".join(event_lines),
"done": False,
}):
return
event_lines = []
if event_lines:
if not self._send_response_frame({
"request_id": request_id,
"stream": True,
"chunk": "".join(event_lines),
"done": False,
}):
return
self._send_response_frame({
"request_id": request_id,
"stream": True,
"done": True,
})
def peer_id_from_wallet(wallet_address: str) -> str:

View File

@@ -734,11 +734,20 @@ def _detect_num_layers(model_id: str, cache_dir: Path | None = None) -> int | No
"""Fetch num_hidden_layers from HuggingFace model config (downloads ~1 KB config.json only)."""
try:
from transformers import AutoConfig # type: ignore[import]
from .model_catalog import layers_from_config
cfg = AutoConfig.from_pretrained(
model_id,
cache_dir=str(cache_dir) if cache_dir is not None else None,
)
return int(cfg.num_hidden_layers)
layers = layers_from_config(cfg)
if layers is None:
print(
f" Warning: no layer count in {model_id} config "
"(checked top level and text_config)", flush=True,
)
return layers
except Exception as exc:
print(f" Warning: could not read model config from HF: {exc}", flush=True)
return None