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

@@ -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,