new tasks, devnet topup, cli, new model support
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user