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

View File

@@ -57,7 +57,9 @@ class RelayServer:
self._ready = threading.Event()
self._running = False
self._stop_event: asyncio.Event | None = None
self._pending_rpc: dict[str, asyncio.Future] = {}
# request_id → queue of relay-http-response frames (US-036: a streamed
# response is a sequence of frames; a frame without "stream" is terminal).
self._pending_rpc: dict[str, asyncio.Queue] = {}
@property
def registry(self) -> PeerRegistry:
@@ -172,9 +174,9 @@ class RelayServer:
if topic == "relay-http-response":
payload = envelope.get("payload", {})
request_id = payload.get("request_id")
fut = self._pending_rpc.pop(request_id, None)
if fut is not None and not fut.done():
fut.set_result(payload)
queue = self._pending_rpc.get(request_id)
if queue is not None:
queue.put_nowait(payload)
continue
# Fan out to all other registered peers
@@ -240,8 +242,12 @@ class RelayServer:
request_id = str(payload.get("request_id") or uuid.uuid4())
payload["request_id"] = request_id
payload["target_peer"] = target_peer_id
fut = self._loop.create_future() if self._loop is not None else asyncio.get_running_loop().create_future()
self._pending_rpc[request_id] = fut
queue: asyncio.Queue = asyncio.Queue()
self._pending_rpc[request_id] = queue
overall_timeout = 310.0
idle_timeout = 120.0
loop = asyncio.get_running_loop()
deadline = loop.time() + overall_timeout
try:
await target.ws.send(json.dumps({
"topic": "relay-http-request",
@@ -249,8 +255,19 @@ class RelayServer:
"from_peer": "relay",
"payload": payload,
}))
response = await asyncio.wait_for(fut, timeout=310.0)
await ws_requester.send(json.dumps(response))
# Forward frames until a terminal one: streamed responses (US-036)
# end with {"stream": true, "done": true}; a frame without "stream"
# is a complete legacy single response.
while True:
remaining = deadline - loop.time()
if remaining <= 0:
raise asyncio.TimeoutError
frame = await asyncio.wait_for(
queue.get(), timeout=min(idle_timeout, remaining)
)
await ws_requester.send(json.dumps(frame))
if not frame.get("stream") or frame.get("done"):
break
except asyncio.TimeoutError:
await ws_requester.send(json.dumps({
"request_id": request_id,

View File

@@ -176,6 +176,14 @@ class AccountStore:
with self._lock:
return api_key in self._revoked_keys
def is_active_key(self, api_key: str) -> bool:
with self._lock:
return api_key in self._active_keys
def owner_of_key(self, api_key: str) -> str | None:
with self._lock:
return self._active_keys.get(api_key)
# ---- views ----
def _public_view(self, record: dict) -> dict:

View File

@@ -87,6 +87,29 @@ class BillingLedger:
def has_funds(self, api_key: str) -> bool:
return self.ensure_client(api_key) > 0.0
def grant_caller_credit(self, api_key: str, account_id: str, amount: float) -> bool:
"""Grant the one-time Caller Credit for an account (US-039).
The event id is derived from the account, not the key, so the grant is
idempotent per account — across retries, additional keys, and hive
gossip replication alike. Returns True only when credit was applied.
"""
if amount <= 0:
return False
event_id = f"caller-credit-{account_id}"
with self._lock:
if event_id in self._seen_event_ids:
return False
self._apply_locked({
"id": event_id,
"type": "credit",
"api_key": api_key,
"amount": amount,
"ts": time.time(),
"note": "caller-credit",
})
return True
def credit_client(self, api_key: str, amount: float, *, note: str = "deposit") -> float:
if amount <= 0:
raise ValueError("credit amount must be positive")

View File

@@ -7,7 +7,12 @@ import time
from .accounts import DEFAULT_ACCOUNTS_DB_PATH
from .billing import DEFAULT_BILLING_DB_PATH
from .hf_pricing import DEFAULT_HF_PRICING_LOG_DB_PATH
from .server import TrackerServer, derive_relay_url_from_public_tracker_url
from .server import (
DEFAULT_CALLER_CREDIT_USDT,
DEFAULT_DEVNET_TOPUP_USDT,
TrackerServer,
derive_relay_url_from_public_tracker_url,
)
DEFAULT_REGISTRY_DB_PATH = "meshnet_registry.sqlite3"
@@ -66,6 +71,28 @@ def main() -> None:
"token bound would cost more than this many USDT"
),
)
common.add_argument(
"--starting-credit",
type=float,
default=DEFAULT_CALLER_CREDIT_USDT,
metavar="USDT",
help=(
"One-time Caller Credit granted when an account creates its first "
f"API key (default: {DEFAULT_CALLER_CREDIT_USDT}; set 0 to require "
"deposits before inference)"
),
)
common.add_argument(
"--devnet-topup",
type=float,
default=DEFAULT_DEVNET_TOPUP_USDT,
metavar="USDT",
help=(
"Dashboard devnet top-up faucet: each click credits this many USDT "
f"to one of the account's keys (default: {DEFAULT_DEVNET_TOPUP_USDT}; "
"MUST be 0 on mainnet deployments)"
),
)
common.add_argument(
"--registry-db",
default=DEFAULT_REGISTRY_DB_PATH,
@@ -231,6 +258,8 @@ def main() -> None:
enable_billing=not args.no_billing,
billing_db=None if args.no_billing else args.billing_db,
max_charge_per_request=args.max_charge_per_request,
starting_credit=args.starting_credit,
devnet_topup_amount=args.devnet_topup,
contracts=contracts,
accounts_db=None if args.no_accounts else args.accounts_db,
treasury=treasury,

View File

@@ -297,6 +297,12 @@ async function revokeKey(key) {
await renderAccountPanel();
}
async function topupKey(key) {
const r = await apiCall("/v1/account/topup", "POST", { api_key: key });
if (!r.ok) alert(r.data.error || "top-up failed");
await renderAccountPanel();
}
async function renderAccountPanel() {
const r = await apiCall("/v1/account");
if (r.status === 404) { // accounts disabled on this tracker
@@ -305,7 +311,7 @@ async function renderAccountPanel() {
return;
}
if (!r.ok) { setSession(null); renderAuthForms(); return; }
const { account, api_keys, balances, total_balance, usage } = r.data;
const { account, api_keys, balances, total_balance, usage, topup_amount } = r.data;
const who = account.email || account.wallet || account.account_id;
let html =
`<div><b>${esc(who)}</b> <span class="pill">${esc(account.role)}</span> ` +
@@ -319,6 +325,9 @@ async function renderAccountPanel() {
for (const key of api_keys) {
html += `<div class="keybox">${esc(key)}` +
` <span class="dim">(${usdt(balances[key] ?? 0)} USDT)</span>` +
(topup_amount > 0
? ` <button class="small" onclick="topupKey('${esc(key)}')">+${usdt(topup_amount)} (devnet)</button>`
: "") +
` <button class="small" onclick="revokeKey('${esc(key)}')">revoke</button></div>`;
}
} else {

View File

@@ -102,6 +102,11 @@ DEFAULT_VRAM_BYTES = 8 * 1024 * 1024 * 1024
DEFAULT_RAM_BYTES = 16 * 1024 * 1024 * 1024
DEFAULT_QUANTIZATIONS = ["bfloat16"]
DEFAULT_BENCHMARK_TOKENS_PER_SEC = 1.0
# US-039/US-040 — single source of truth for the credit defaults (referenced by
# TrackerServer, _TrackerHTTPServer, and the CLI). Alpha runs devnet-friendly;
# flip both to 0 before any deployment holding a mainnet treasury.
DEFAULT_CALLER_CREDIT_USDT = 1.0
DEFAULT_DEVNET_TOPUP_USDT = 1.0
def _model_aliases(model: str | None) -> set[str]:
@@ -909,18 +914,29 @@ def _coverage_gaps(coverage: list[dict]) -> list[tuple[int, int]]:
]
def _relay_http_request(
def _relay_http_request_frames(
relay_addr: str,
path: str,
body: bytes,
headers: dict[str, str],
timeout: float = 310.0,
) -> dict | None:
"""Send an HTTP-shaped request through a relay RPC WebSocket."""
idle_timeout: float = 120.0,
):
"""Send an HTTP-shaped request through a relay RPC WebSocket, yielding
response frames until a terminal one (US-036).
A frame with ``stream: true`` is part of a chunked SSE response ending with
``done: true``; a frame without ``stream`` is a complete single response.
Yields nothing when the relay is unreachable; stops silently on idle or
overall timeout (the caller bills whatever was observed).
"""
try:
import websockets.sync.client as wsc # type: ignore[import]
request_id = str(uuid.uuid4())
except Exception:
return
request_id = str(uuid.uuid4())
deadline = time.monotonic() + timeout
try:
with wsc.connect(relay_addr, open_timeout=10, close_timeout=5) as ws:
ws.send(json.dumps({
"request_id": request_id,
@@ -929,13 +945,63 @@ def _relay_http_request(
"headers": headers,
"body": body.decode(errors="replace"),
}))
raw = ws.recv(timeout=timeout)
response = json.loads(raw)
if response.get("request_id") not in {None, request_id}:
return None
return response
while True:
remaining = deadline - time.monotonic()
if remaining <= 0:
return
raw = ws.recv(timeout=min(idle_timeout, remaining))
frame = json.loads(raw)
if frame.get("request_id") not in {None, request_id}:
continue
yield frame
if not frame.get("stream") or frame.get("done"):
return
except Exception:
return
def _relay_http_request(
relay_addr: str,
path: str,
body: bytes,
headers: dict[str, str],
timeout: float = 310.0,
) -> dict | None:
"""Send an HTTP-shaped request through a relay and buffer the response.
Streamed frame sequences are collapsed into one response dict whose body is
the concatenated SSE text — used by non-chat callers and kept for
backward compatibility; the chat proxy streams frames directly.
"""
frames = _relay_http_request_frames(relay_addr, path, body, headers, timeout=timeout)
first = next(frames, None)
if first is None:
return None
if not first.get("stream"):
return first
chunks = [first.get("chunk") or ""]
for frame in frames:
chunks.append(frame.get("chunk") or "")
return {
"request_id": first.get("request_id"),
"status": first.get("status", 200),
"headers": first.get("headers") or {},
"body": "".join(chunks),
}
def _stream_line_tokens(line: bytes) -> tuple[int, int | None]:
"""Token accounting for one SSE line: (observed delta, reported total or None)."""
if not line.startswith(b"data:"):
return 0, None
payload = line[5:].strip()
if not payload or payload == b"[DONE]":
return 0, None
try:
chunk_payload = json.loads(payload)
except json.JSONDecodeError:
return 1, None
return _observed_stream_tokens(chunk_payload), _usage_total_tokens(chunk_payload)
def _find_pinned_route(
@@ -1507,6 +1573,8 @@ class _TrackerHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
validator_service_token: str | None = None,
hive_secret: str | None = None,
max_charge_per_request: float | None = None,
starting_credit: float = DEFAULT_CALLER_CREDIT_USDT,
devnet_topup_amount: float = DEFAULT_DEVNET_TOPUP_USDT,
toploc_calibration: "ToplocCalibrationStore | None" = None,
toploc_reference_node_url: str | None = None,
toploc_calibration_gate_min_hardware_profiles: int = 1,
@@ -1533,6 +1601,8 @@ class _TrackerHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
self.validator_service_token = validator_service_token
self.hive_secret = hive_secret
self.max_charge_per_request = max_charge_per_request
self.starting_credit = starting_credit
self.devnet_topup_amount = devnet_topup_amount
self.toploc_calibration: ToplocCalibrationStore | None = toploc_calibration
self.toploc_reference_node_url = (
toploc_reference_node_url.rstrip("/") if toploc_reference_node_url else None
@@ -1672,6 +1742,9 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
if self.path == "/v1/account/keys/revoke":
self._handle_account_key_revoke()
return
if self.path == "/v1/account/topup":
self._handle_account_topup()
return
if self.path == "/v1/accounts/gossip":
self._handle_accounts_gossip()
return
@@ -2008,6 +2081,15 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
"code": "invalid_api_key",
}})
return
# US-039: with accounts enabled, only real account keys may spend —
# arbitrary bearer strings must never become billable clients.
if server.accounts is not None and not server.accounts.is_active_key(api_key):
self._send_json(401, {"error": {
"message": "unknown API key: create one at /dashboard (register, then + new key)",
"type": "invalid_request_error",
"code": "invalid_api_key",
}})
return
if not server.billing.has_funds(api_key):
self._send_json(402, {"error": {
"message": "insufficient balance: deposit USDT to continue",
@@ -2181,17 +2263,26 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
flush=True,
)
started = time.monotonic()
relayed = _relay_http_request(
frames = _relay_http_request_frames(
node.relay_addr,
path="/v1/chat/completions",
body=raw_body,
headers=relay_headers,
)
elapsed = time.monotonic() - started
if relayed is not None:
self._send_relayed_response(relayed)
if int(relayed.get("status", 503)) < 400:
body_text = relayed.get("body") or ""
first = next(frames, None)
if first is not None and first.get("stream"):
# Streamed response (US-036): forward SSE chunks as they arrive
# and run the same token accounting as the direct stream path.
self._stream_relayed_frames(
first, frames, started,
model, route_model, route_nodes, api_key, node_work,
)
return
if first is not None:
elapsed = time.monotonic() - started
self._send_relayed_response(first)
if int(first.get("status", 503)) < 400:
body_text = first.get("body") or ""
try:
tokens = _billable_non_stream_tokens(json.loads(body_text), body)
except (json.JSONDecodeError, TypeError):
@@ -2254,18 +2345,10 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
break
self.wfile.write(line)
self.wfile.flush()
if line.startswith(b"data:"):
payload = line[5:].strip()
if payload and payload != b"[DONE]":
try:
chunk_payload = json.loads(payload)
except json.JSONDecodeError:
observed_stream_tokens += 1
continue
observed_stream_tokens += _observed_stream_tokens(chunk_payload)
found = _usage_total_tokens(chunk_payload)
if found is not None:
reported_stream_tokens = found
observed, reported = _stream_line_tokens(line)
observed_stream_tokens += observed
if reported is not None:
reported_stream_tokens = reported
except BrokenPipeError:
pass
elapsed = time.monotonic() - started
@@ -2410,6 +2493,49 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
except Exception as exc:
print(f"[tracker] billing failed for model={model!r}: {exc}", flush=True)
def _stream_relayed_frames(
self,
first: dict,
frames,
started: float,
model: str,
route_model: str,
route_nodes: list,
api_key: str | None,
node_work: list,
) -> None:
"""Forward a streamed relay response (US-036) to the client as SSE,
billing with the same accounting as the direct stream path."""
headers = first.get("headers") if isinstance(first.get("headers"), dict) else {}
self.send_response(int(first.get("status", 200)))
self.send_header("Content-Type", headers.get("Content-Type", "text/event-stream; charset=utf-8"))
self.send_header("Cache-Control", "no-cache")
self.end_headers()
reported_stream_tokens: int | None = None
observed_stream_tokens = 0
client_gone = False
for frame in itertools.chain([first], frames):
chunk = frame.get("chunk") or ""
if not chunk:
continue
data = chunk.encode()
if not client_gone:
try:
self.wfile.write(data)
self.wfile.flush()
except BrokenPipeError:
# Keep draining frames — the nodes did the work; bill it.
client_gone = True
for line in data.splitlines():
observed, reported = _stream_line_tokens(line)
observed_stream_tokens += observed
if reported is not None:
reported_stream_tokens = reported
elapsed = time.monotonic() - started
observed_tokens = _billable_stream_tokens(observed_stream_tokens, reported_stream_tokens)
self._record_observed_throughput(model, route_model, observed_tokens, elapsed, route_nodes)
self._bill_completed(api_key, model, observed_tokens, node_work)
def _send_relayed_response(self, response: dict) -> None:
status = int(response.get("status", 503))
headers = response.get("headers") if isinstance(response.get("headers"), dict) else {}
@@ -2909,6 +3035,12 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
api_key = accounts.create_api_key(account["account_id"])
server: _TrackerHTTPServer = self.server # type: ignore[assignment]
if server.billing is not None:
# US-039: registration creates the account's first key, so Caller
# Credit lands here; the account-derived event id keeps later
# grants (e.g. via /v1/account/keys) no-ops.
server.billing.grant_caller_credit(
api_key, account["account_id"], server.starting_credit
)
server.billing.ensure_client(api_key)
print(
f"[tracker] account registered: {account.get('email') or account.get('wallet')} "
@@ -2959,6 +3091,7 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
"balances": balances,
"total_balance": sum(balances.values()),
"usage": usage,
"topup_amount": server.devnet_topup_amount if server.billing is not None else 0.0,
})
def _handle_account_key_create(self):
@@ -2971,9 +3104,48 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
return
api_key = accounts.create_api_key(account["account_id"])
server: _TrackerHTTPServer = self.server # type: ignore[assignment]
credited = False
if server.billing is not None:
# US-039: Caller Credit lands with the account's first key; the
# account-derived event id keeps it once-per-account forever.
credited = server.billing.grant_caller_credit(
api_key, account["account_id"], server.starting_credit
)
server.billing.ensure_client(api_key)
self._send_json(200, {"api_key": api_key})
self._send_json(200, {"api_key": api_key, "caller_credit_granted": credited})
def _handle_account_topup(self):
"""Devnet faucet (US-040): credit the configured amount to one of the
logged-in account's keys. 404 unless the operator enabled it."""
server: _TrackerHTTPServer = self.server # type: ignore[assignment]
accounts = self._require_accounts()
if accounts is None:
return
if server.billing is None or server.devnet_topup_amount <= 0:
self._send_json(404, {"error": "top-up is not enabled on this tracker"})
return
account = self._session_account()
if account is None:
self._send_json(401, {"error": "login required"})
return
body = self._read_json_body()
if body is None:
return
api_key = body.get("api_key")
if not api_key or not isinstance(api_key, str):
self._send_json(400, {"error": "api_key is required"})
return
if accounts.owner_of_key(api_key) != account["account_id"]:
self._send_json(403, {"error": "key not found on this account"})
return
balance = server.billing.credit_client(
api_key, server.devnet_topup_amount, note="devnet-topup"
)
self._send_json(200, {
"api_key": api_key,
"credited": server.devnet_topup_amount,
"balance": balance,
})
def _handle_account_key_revoke(self):
accounts = self._require_accounts()
@@ -3851,6 +4023,8 @@ class TrackerServer:
validator_service_token: str | None = None,
hive_secret: str | None = None,
max_charge_per_request: float | None = None,
starting_credit: float = DEFAULT_CALLER_CREDIT_USDT,
devnet_topup_amount: float = DEFAULT_DEVNET_TOPUP_USDT,
toploc_calibration: ToplocCalibrationStore | None = None,
toploc_calibration_db: str | None = None,
toploc_reference_node_url: str | None = None,
@@ -3923,6 +4097,8 @@ class TrackerServer:
if max_charge_per_request is not None and max_charge_per_request <= 0.0:
raise ValueError("max_charge_per_request must be positive")
self._max_charge_per_request = max_charge_per_request
self._starting_credit = max(0.0, starting_credit)
self._devnet_topup_amount = max(0.0, devnet_topup_amount)
self._validator_service_token = (
validator_service_token
if validator_service_token is not None
@@ -3976,6 +4152,8 @@ class TrackerServer:
validator_service_token=self._validator_service_token,
hive_secret=self._hive_secret,
max_charge_per_request=self._max_charge_per_request,
starting_credit=self._starting_credit,
devnet_topup_amount=self._devnet_topup_amount,
toploc_calibration=self._toploc_calibration,
toploc_reference_node_url=self._toploc_reference_node_url,
toploc_calibration_gate_min_hardware_profiles=self._toploc_calibration_gate_min_hardware_profiles,