diff --git a/deploy_common/__init__.py b/deploy_common/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/deploy_common/reject_drain.py b/deploy_common/reject_drain.py
new file mode 100644
index 0000000..321dbf4
--- /dev/null
+++ b/deploy_common/reject_drain.py
@@ -0,0 +1,105 @@
+"""Bounded, body-blind draining after an early gateway rejection."""
+from contextlib import suppress
+import queue
+import select
+import socket
+import threading
+import time
+
+
+# Keep a full forwarding wave without exceeding Windows select()'s 512-fd set.
+MAX_REJECTIONS = 480
+MAX_DRAIN_BYTES = 128 * 1024 * 1024
+DRAIN_TIMEOUT = 30
+
+
+class RejectionDrainer:
+ """Keep rejected sockets readable without holding a request admission slot."""
+
+ def __init__(self):
+ self._slots = threading.BoundedSemaphore(MAX_REJECTIONS)
+ self._pending = queue.SimpleQueue()
+ self._stop = threading.Event()
+ self._lock = threading.Lock()
+ self.full = 0
+ self.duplicate_failures = 0
+ self.thread = threading.Thread(target=self._run, daemon=True)
+ self.thread.start()
+
+ def submit(self, connection):
+ with self._lock:
+ if self._stop.is_set():
+ return False
+ if not self._slots.acquire(blocking=False):
+ self.full += 1
+ return False
+ duplicate = None
+ try:
+ duplicate = connection.dup()
+ duplicate.setblocking(False)
+ self._pending.put(duplicate)
+ return True
+ except OSError:
+ self.duplicate_failures += 1
+ if duplicate is not None:
+ duplicate.close()
+ self._slots.release()
+ return False
+
+ def close(self):
+ with self._lock:
+ self._stop.set()
+ self.thread.join(1)
+
+ def _run(self):
+ active = {}
+
+ def release(conn):
+ active.pop(conn, None)
+ conn.close()
+ self._slots.release()
+
+ while not self._stop.is_set():
+ while True:
+ try:
+ conn = self._pending.get_nowait()
+ except queue.Empty:
+ break
+ active[conn] = [time.monotonic() + DRAIN_TIMEOUT, MAX_DRAIN_BYTES]
+ if not active:
+ self._stop.wait(.02)
+ continue
+ try:
+ readable, _, failed = select.select(list(active), [], list(active), .02)
+ except (OSError, ValueError):
+ readable, failed = [], list(active)
+ now = time.monotonic()
+ for conn in set(readable + failed + [conn for conn, (deadline, _) in active.items()
+ if deadline <= now]):
+ if conn not in active:
+ continue
+ deadline, remaining = active[conn]
+ if conn in failed or deadline <= now:
+ release(conn)
+ continue
+ try:
+ data = conn.recv(min(65536, remaining))
+ except BlockingIOError:
+ continue
+ except OSError:
+ data = b''
+ remaining -= len(data)
+ if not data or remaining <= 0:
+ release(conn)
+ else:
+ active[conn][1] = remaining
+ for conn in list(active):
+ release(conn)
+ while True:
+ try:
+ conn = self._pending.get_nowait()
+ except queue.Empty:
+ break
+ with suppress(OSError):
+ conn.close()
+ self._slots.release()
diff --git a/deploy_gateway/server.py b/deploy_gateway/server.py
index d0dffed..2773cd5 100644
--- a/deploy_gateway/server.py
+++ b/deploy_gateway/server.py
@@ -4,29 +4,98 @@
Client Authorization is consumed here and never forwarded to the model.
"""
import argparse
+from concurrent.futures import ThreadPoolExecutor
import hashlib
import hmac
from http.client import HTTPConnection, HTTPException, IncompleteRead
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+import ipaddress
import json
import math
from pathlib import Path
import re
import select
import socket
+import sys
import threading
import time
from urllib.parse import urlsplit
from .routing import Busy, SessionRouter, Unavailable, affinity_key, prefix_rank
+from deploy_common.reject_drain import RejectionDrainer
HOP_HEADERS = {'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization',
'te', 'trailer', 'transfer-encoding', 'upgrade'}
+def _check_statistics_paths(database, key_store, bindings_file, state_path, control_file):
+ """Reject conflicts before opening SQLite or creating its sidecars."""
+ database = Path(database)
+ sqlite_paths = [database, *(Path(str(database) + suffix) for suffix in ('-wal', '-shm', '-journal'))]
+ protected = [Path(key_store), Path(bindings_file), Path(str(bindings_file) + '.new')]
+ if state_path:
+ protected += [Path(state_path), Path(state_path).with_suffix('.new')]
+ if control_file:
+ protected += [Path(control_file), Path(str(control_file) + '.new')]
+ for left in sqlite_paths:
+ for right in protected:
+ if left.resolve() == right.resolve():
+ raise ValueError('STATISTICS_PATH_OVERLAP')
+ try:
+ if left.samefile(right):
+ raise ValueError('STATISTICS_PATH_OVERLAP')
+ except FileNotFoundError:
+ pass
+
+
+def pd_instance_healthy(row, cache):
+ """Probe every reported rank; cache observations within one gateway cycle."""
+ endpoints = row.get('endpoints')
+ if not isinstance(endpoints, list) or not 0 < len(endpoints) <= 1024:
+ return False
+ targets = []
+ for endpoint in endpoints:
+ if not isinstance(endpoint, dict) or endpoint.get('headless') is True:
+ return False
+ try:
+ host_value = endpoint.get('ip')
+ if not isinstance(host_value, str):
+ return False
+ host = str(ipaddress.ip_address(host_value))
+ port = endpoint.get('business_port')
+ if isinstance(port, str) and port.isdecimal():
+ port = int(port)
+ if type(port) is not int or not 1 <= port <= 65535:
+ return False
+ except (ValueError, TypeError):
+ return False
+ targets.append((host, port))
+ if len(set(targets)) != len(targets):
+ return False
+
+ def probe(target):
+ connection = HTTPConnection(target[0], target[1], timeout=2)
+ try:
+ connection.request('GET', '/health', headers={'Connection': 'close'})
+ return connection.getresponse().status == 200
+ except (OSError, HTTPException, ValueError):
+ return False
+ finally:
+ connection.close()
+
+ missing = [target for target in targets if target not in cache]
+ if missing:
+ with ThreadPoolExecutor(max_workers=min(16, len(missing))) as pool:
+ cache.update(zip(missing, pool.map(probe, missing)))
+ return all(cache[target] for target in targets)
+
+
class Backends(SessionRouter):
- def __init__(self, ports, state_path, bindings_file, max_active=32, max_waiting=32, queue_timeout=5, control_file=None):
+ def __init__(self, ports, state_path, bindings_file, max_active=32, max_waiting=32, queue_timeout=5, control_file=None, readiness_ports=None, pd_model=None, pd_probe_endpoints=False):
super().__init__(ports, bindings_file, max_active=max_active, max_waiting=max_waiting, queue_timeout=queue_timeout)
+ self.readiness_ports = dict(readiness_ports or {})
+ self.pd_model = pd_model
+ self.pd_probe_endpoints = pd_probe_endpoints
self.control_file = Path(control_file) if control_file else None
self.stop = threading.Event()
self.state_path = Path(state_path) if state_path else None
@@ -56,14 +125,40 @@ def probe(self, bootstrap=False):
with self.lock:
self.enabled_ports = enabled
versions = dict(self.versions)
- observed = {}
+ observed, endpoint_cache = {}, {}
for port in self.ports:
- connection = HTTPConnection('127.0.0.1', port, timeout=1)
+ readiness_port = self.readiness_ports.get(port)
+ connection = HTTPConnection('127.0.0.1', readiness_port or port, timeout=1)
try:
- connection.request('GET', '/health', headers={'Connection': 'close'})
+ connection.request('GET', '/readiness' if readiness_port else '/health', headers={'Connection': 'close'})
response = connection.getresponse()
- observed[port] = response.status == 200
- except (OSError, HTTPException):
+ if readiness_port and response.status == 200:
+ body = response.read(65537)
+ data = json.loads(body) if len(body) <= 65536 else None
+ observed[port] = isinstance(data, dict) and data.get('ready') is True
+ if observed[port] and self.pd_model is not None:
+ connection.close()
+ connection.request('GET', '/instances', headers={'Connection': 'close'})
+ response = connection.getresponse()
+ body = response.read(1048577)
+ data = json.loads(body) if response.status == 200 and len(body) <= 1048576 else None
+ instances = data.get('instances') if isinstance(data, dict) else None
+ candidates = [row for row in instances if isinstance(row, dict)
+ and row.get('role') in ('prefill', 'decode')
+ and row.get('model_name') == self.pd_model
+ and row.get('healthy') is True and row.get('pool') == 'available'
+ and row.get('status') == 'active'] if isinstance(instances, list) else []
+ roles = set()
+ for row in candidates:
+ if row['role'] not in roles and (not self.pd_probe_endpoints
+ or pd_instance_healthy(row, endpoint_cache)):
+ roles.add(row['role'])
+ if roles == {'prefill', 'decode'}:
+ break
+ observed[port] = {'prefill', 'decode'} <= roles
+ else:
+ observed[port] = response.status == 200 and not readiness_port
+ except (OSError, HTTPException, ValueError, RuntimeError, RecursionError):
observed[port] = False
finally:
connection.close()
@@ -92,7 +187,13 @@ class Gateway(ThreadingHTTPServer):
allow_reuse_address = True
request_queue_size = 256
- def __init__(self, address, key_store, upstream_port=19001, backup_port=19002, state_path=None, bindings_file=None, upstream_timeout=10800, model_context_limit=None, backend_context_limits=None, *, backend_ports=None, max_active=32, max_waiting=32, queue_timeout=5, control_file=None, backend_managed_affinity=False):
+ def server_bind(self):
+ if hasattr(socket, 'SO_EXCLUSIVEADDRUSE'):
+ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1)
+ self.allow_reuse_address = False
+ super().server_bind()
+
+ def __init__(self, address, key_store, upstream_port=19001, backup_port=19002, state_path=None, bindings_file=None, upstream_timeout=10800, model_context_limit=None, backend_context_limits=None, *, backend_ports=None, max_active=32, max_waiting=32, queue_timeout=5, control_file=None, backend_managed_affinity=False, backend_readiness_ports=None, pd_model=None, pd_probe_endpoints=False):
if bindings_file is None or Path(bindings_file).resolve() == Path(key_store).resolve():
raise ValueError('A separate private bindings file is required')
if state_path and Path(state_path).resolve() in (Path(key_store).resolve(), Path(bindings_file).resolve()):
@@ -113,6 +214,15 @@ def __init__(self, address, key_store, upstream_port=19001, backup_port=19002, s
ports = list(backend_ports) if backend_ports is not None else [upstream_port, backup_port]
if not ports or len(set(ports)) != len(ports) or any(type(p) is not int or not 1 <= p <= 65535 for p in ports):
raise ValueError('Invalid backend ports')
+ readiness_ports = dict(backend_readiness_ports or {})
+ if any(type(p) is not int or p not in ports or type(target) is not int or not 1 <= target <= 65535
+ for p, target in readiness_ports.items()):
+ raise ValueError('Invalid backend readiness mapping')
+ if pd_model is not None and (not isinstance(pd_model, str) or not pd_model.strip()
+ or set(readiness_ports) != set(ports)):
+ raise ValueError('PD model requires a management readiness port for each backend')
+ if type(pd_probe_endpoints) is not bool or (pd_probe_endpoints and pd_model is None):
+ raise ValueError('PD endpoint probing requires a PD model')
self.backend_context_limits = dict(backend_context_limits or {})
if self.backend_context_limits and (set(self.backend_context_limits) != set(ports)
or any(type(p) is not int or type(limit) is not int or not 0 < limit <= 10**9 for p, limit in self.backend_context_limits.items())):
@@ -131,12 +241,21 @@ def __init__(self, address, key_store, upstream_port=19001, backup_port=19002, s
raise ValueError('No active gateway keys')
super().__init__(address, Handler)
try:
- self.backends = Backends(ports, state_path, bindings_file, max_active, max_waiting, queue_timeout, control_file)
+ self.backends = Backends(ports, state_path, bindings_file, max_active, max_waiting, queue_timeout, control_file, readiness_ports, pd_model, pd_probe_endpoints)
+ try:
+ self.rejection_drainer = RejectionDrainer()
+ except Exception:
+ self.backends.stop.set()
+ raise
except Exception:
super().server_close()
raise
def server_close(self):
+ if not hasattr(self, 'backends'):
+ return super().server_close()
+ if hasattr(self, 'rejection_drainer'):
+ self.rejection_drainer.close()
self.backends.stop.set()
super().server_close()
@@ -181,7 +300,26 @@ class Handler(BaseHTTPRequestHandler):
def log_message(self, format, *args):
pass
+ def _observe(self, event, *args):
+ observer = getattr(self, '_stats_observer', None)
+ if observer is not None:
+ try:
+ getattr(observer, event)(*args)
+ except Exception:
+ try:
+ observer.fail()
+ except Exception:
+ pass
+
def error(self, status, message):
+ lengths = self.headers.get_all('Content-Length', [])
+ has_unread_body = bool(self.headers.get('Transfer-Encoding'))
+ if lengths:
+ try:
+ has_unread_body |= len(lengths) != 1 or int(lengths[0]) != 0
+ except ValueError:
+ has_unread_body = True
+ self._drain_rejection = has_unread_body and not getattr(self, '_request_body_read', False)
body = json.dumps({'error': {'message': message, 'type': 'gateway_error'}}).encode()
self.send_response(status)
self.send_header('Content-Type', 'application/json')
@@ -194,6 +332,13 @@ def error(self, status, message):
if self.command != 'HEAD':
self.wfile.write(body)
+ def finish(self):
+ try:
+ super().finish()
+ finally:
+ if getattr(self, '_drain_rejection', False):
+ self.server.rejection_drainer.submit(self.connection)
+
def do_GET(self):
self.proxy()
@@ -253,6 +398,7 @@ def proxy_request(self):
if len(body) != length:
self.error(400, 'Incomplete request body')
return
+ self._request_body_read = True
headers = {'Accept-Encoding': 'identity', 'Connection': 'close'}
for name in ['Content-Type', 'Accept', 'User-Agent']:
value = self.headers.get(name)
@@ -262,6 +408,7 @@ def proxy_request(self):
try:
eligible, context_limit = self.server.context_route(self.headers)
key = affinity_key(self.headers, body, hashlib.sha256(fields[1].encode()).hexdigest()) if balanced else None
+ self._observe('session', key)
except ValueError:
self.error(400, 'Invalid session identifier or context window')
return
@@ -346,6 +493,8 @@ def watch_disconnect():
self.close_connection = True
return
sent_headers = True
+ self._observe('response', response.status, response.getheader('Content-Type'),
+ response.getheader('Content-Encoding'))
self.close_connection = True
if model_body is not None:
try:
@@ -359,10 +508,12 @@ def watch_disconnect():
if not chunk:
if response.length:
raise IncompleteRead(b'', response.length)
+ self._observe('complete', not disconnected.is_set())
break
try:
self.wfile.write(chunk)
self.wfile.flush()
+ self._observe('chunk', chunk)
except OSError:
self.close_connection = True
return
@@ -407,6 +558,10 @@ def main():
parser.add_argument('--upstream-timeout', type=float, default=10800)
parser.add_argument('--model-context-limit', type=int)
parser.add_argument('--backend-context-limit', action='append', default=[], metavar='PORT=TOKENS')
+ parser.add_argument('--backend-readiness-port', action='append', default=[], metavar='PORT=MANAGEMENT_PORT')
+ parser.add_argument('--statistics-database', type=Path)
+ parser.add_argument('--backend-pd-model', help='Require healthy Motor prefill and decode instances for this model')
+ parser.add_argument('--backend-pd-probe-endpoints', action='store_true', help='Probe every rank of each candidate PD instance')
args = parser.parse_args()
limits = {}
for item in args.backend_context_limit:
@@ -416,11 +571,45 @@ def main():
if port in limits:
parser.error('Duplicate backend context limit')
limits[port] = limit
- server = Gateway((args.listen, args.port), args.key_store, args.upstream_port, args.backup_port, args.backend_state, args.bindings_file, args.upstream_timeout, args.model_context_limit, limits,
- backend_ports=args.backend_port, max_active=args.max_active_per_backend, max_waiting=args.max_waiting,
- queue_timeout=args.queue_timeout, control_file=args.control_file, backend_managed_affinity=args.backend_managed_affinity)
- print(json.dumps({'state': 'LISTENING', 'listen': args.listen, 'port': args.port}), flush=True)
- server.serve_forever()
+ readiness_ports = {}
+ for item in args.backend_readiness_port:
+ if not re.fullmatch(r'[0-9]{1,5}=[0-9]{1,5}', item):
+ parser.error('Backend readiness mappings must use PORT=MANAGEMENT_PORT')
+ port, management_port = map(int, item.split('='))
+ if port in readiness_ports:
+ parser.error('Duplicate backend readiness mapping')
+ readiness_ports[port] = management_port
+ if args.statistics_database is not None:
+ try:
+ _check_statistics_paths(args.statistics_database, args.key_store, args.bindings_file,
+ args.backend_state, args.control_file)
+ except (OSError, RuntimeError, ValueError):
+ parser.error('Statistics database and gateway runtime paths must be distinct')
+ module = sys.modules[__name__]
+ original_handler = module.Handler
+ recorder = None
+ if args.statistics_database is not None:
+ try:
+ from deploy_stats.inprocess import install
+ recorder = install(module, args.statistics_database, source='gateway-inprocess-v1')
+ except Exception:
+ recorder = None
+ server = None
+ try:
+ server = Gateway((args.listen, args.port), args.key_store, args.upstream_port, args.backup_port, args.backend_state, args.bindings_file, args.upstream_timeout, args.model_context_limit, limits,
+ backend_ports=args.backend_port, max_active=args.max_active_per_backend, max_waiting=args.max_waiting,
+ queue_timeout=args.queue_timeout, control_file=args.control_file, backend_managed_affinity=args.backend_managed_affinity,
+ backend_readiness_ports=readiness_ports, pd_model=args.backend_pd_model,
+ pd_probe_endpoints=args.backend_pd_probe_endpoints)
+ print(json.dumps({'state': 'LISTENING', 'listen': args.listen, 'port': args.port,
+ 'statistics': 'enabled' if recorder else ('unavailable' if args.statistics_database else 'disabled')}), flush=True)
+ server.serve_forever()
+ finally:
+ if server is not None:
+ server.server_close()
+ if recorder is not None:
+ module.Handler = original_handler
+ recorder.close()
if __name__ == '__main__':
diff --git a/deploy_stats/__init__.py b/deploy_stats/__init__.py
new file mode 100644
index 0000000..afc7d33
--- /dev/null
+++ b/deploy_stats/__init__.py
@@ -0,0 +1 @@
+"""Local, metadata-only accounting for an authenticated model gateway."""
diff --git a/deploy_stats/__main__.py b/deploy_stats/__main__.py
new file mode 100644
index 0000000..0de0f39
--- /dev/null
+++ b/deploy_stats/__main__.py
@@ -0,0 +1,38 @@
+"""Run the accounting hop or the local read-only dashboard."""
+import argparse
+from pathlib import Path
+
+from .proxy import Proxy
+from .web import Dashboard
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ commands = parser.add_subparsers(dest='command', required=True)
+ proxy = commands.add_parser('proxy')
+ proxy.add_argument('--port', type=int, default=19882)
+ proxy.add_argument('--upstream-port', type=int, required=True)
+ proxy.add_argument('--database', type=Path, required=True)
+ dashboard = commands.add_parser('dashboard')
+ dashboard.add_argument('--port', type=int, default=19881)
+ dashboard.add_argument('--database', type=Path, required=True)
+ dashboard.add_argument('--backend-port', type=int, action='append', default=[])
+ dashboard.add_argument('--telemetry-port', type=int)
+ dashboard.add_argument('--motor-management-port', type=int, action='append', default=[])
+ dashboard.add_argument('--motor-history-database', type=Path)
+ args = parser.parse_args()
+ server = (Proxy(args.port, args.upstream_port, args.database) if args.command == 'proxy'
+ else Dashboard(args.port, args.database, args.backend_port, args.telemetry_port,
+ motor_management_ports=args.motor_management_port,
+ motor_history_path=args.motor_history_database))
+ print(f'Listening on http://127.0.0.1:{server.server_port}', flush=True)
+ try:
+ server.serve_forever()
+ except KeyboardInterrupt:
+ pass
+ finally:
+ server.server_close()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/deploy_stats/backends.py b/deploy_stats/backends.py
new file mode 100644
index 0000000..806f9b5
--- /dev/null
+++ b/deploy_stats/backends.py
@@ -0,0 +1,77 @@
+"""Read the bounded, public numeric subset of local backend metrics."""
+from http.client import HTTPConnection, HTTPException
+import json
+import math
+import time
+
+METRICS = {
+ 'deepinfer:requests_running': 'running',
+ 'deepinfer:requests_waiting': 'waiting',
+ 'deepinfer:workers_alive': 'workers_alive',
+ 'deepinfer:admission_ready': 'admission_ready',
+ 'deepinfer:prompt_tokens_total': 'input_total',
+ 'deepinfer:prompt_cache_hit_tokens_total': 'cached_total',
+ 'deepinfer:prompt_cache_hit_requests_total': 'cache_hit_requests_total',
+}
+
+
+def read_backend(port):
+ result = {'port': port, 'sampled_at': time.time(), 'available': False}
+ connection = HTTPConnection('127.0.0.1', port, timeout=1)
+ try:
+ connection.request('GET', '/metrics')
+ response = connection.getresponse()
+ if response.status != 200:
+ return result
+ payload = response.read(1024 * 1024 + 1)
+ if len(payload) > 1024 * 1024:
+ return result
+ values = {}
+ for line in payload.decode('utf-8').splitlines():
+ parts = line.split()
+ if len(parts) != 2 or parts[0] not in METRICS:
+ continue
+ name = METRICS[parts[0]]
+ value = float(parts[1])
+ if name in values or not math.isfinite(value) or value < 0:
+ return result
+ values[name] = value
+ if set(values) != set(METRICS.values()) or values['admission_ready'] not in (0, 1):
+ return result
+ result.update(values, available=True)
+ result['hit_rate'] = (values['cached_total'] / values['input_total']
+ if values['input_total'] and values['cached_total'] <= values['input_total'] else None)
+ return result
+ except (OSError, HTTPException, UnicodeError, ValueError, OverflowError):
+ return result
+ finally:
+ connection.close()
+
+
+def read_telemetry(port):
+ unavailable = {'available': False, 'nodes': []}
+ if port is None:
+ return unavailable
+ connection = HTTPConnection('127.0.0.1', port, timeout=2)
+ try:
+ connection.request('GET', '/snapshot')
+ response = connection.getresponse()
+ if response.status != 200:
+ return unavailable
+ payload = response.read(1024 * 1024 + 1)
+ if len(payload) > 1024 * 1024:
+ return unavailable
+ value = json.loads(payload)
+ now = time.time()
+ stamp = value.get('sampled_at') if isinstance(value, dict) else None
+ if (not isinstance(value, dict) or value.get('schema') != 1 or value.get('kind') != 'deepinfer-telemetry' or
+ type(stamp) not in (int, float) or not math.isfinite(stamp) or not 0 <= now - stamp <= 620 or
+ not isinstance(value.get('nodes'), list) or len(value['nodes']) > 8):
+ return unavailable
+ value['available'] = True
+ json.dumps(value, allow_nan=False)
+ return value
+ except (OSError, HTTPException, UnicodeError, ValueError, OverflowError):
+ return unavailable
+ finally:
+ connection.close()
diff --git a/deploy_stats/capture.py b/deploy_stats/capture.py
new file mode 100644
index 0000000..54a63c3
--- /dev/null
+++ b/deploy_stats/capture.py
@@ -0,0 +1,380 @@
+"""Extract final usage without changing a JSON or SSE response."""
+import json
+
+MAX_FRAME = 4 * 1024 * 1024
+MAX_COUNT = 2**53 - 1
+MAX_DATA_LINES = 4096
+MAX_JSON_ESCAPES = 65536
+
+
+def _too_complex(payload, limit):
+ """Bound JSON structure and quote scanning without counting string contents."""
+ if limit is None:
+ return False
+ if payload.count(92) > MAX_JSON_ESCAPES:
+ return True
+ if sum(payload.count(mark) for mark in b'{}[],:') <= limit:
+ return False
+ size, position, structural, strings = len(payload), 0, 0, 0
+ while position < size:
+ opening = payload.find(b'"', position)
+ end = size if opening < 0 else opening
+ structural += sum(payload.count(mark, position, end) for mark in b'{}[],:')
+ if structural > limit:
+ return True
+ if opening < 0:
+ break
+ strings += 1
+ if strings > limit:
+ return True
+ position = opening + 1
+ while position < size:
+ closing = payload.find(b'"', position)
+ if closing < 0:
+ return False # The JSON parser will reject the unterminated string.
+ index = closing - 1
+ while index >= position and payload[index] == 92:
+ index -= 1
+ position = closing + 1
+ if (closing - index - 1) % 2 == 0:
+ break
+ return False
+
+
+def _count(value):
+ return value if type(value) is int and 0 <= value <= MAX_COUNT else None
+
+
+def _may_have_non_null(payload, field):
+ name = b'"' + field + b'"'
+ offset = 0
+ for _ in range(1024):
+ marker = payload.find(name, offset)
+ if marker < 0:
+ return False
+ position = marker + len(name)
+ while position < len(payload) and payload[position] in b' \t\r\n':
+ position += 1
+ if position - marker > 128:
+ return True
+ if position >= len(payload) or payload[position] != 58:
+ return True
+ position += 1
+ while position < len(payload) and payload[position] in b' \t\r\n':
+ position += 1
+ if position - marker > 128:
+ return True
+ if payload[position:position + 4] != b'null':
+ return True
+ offset = position + 4
+ if offset < len(payload) and payload[offset] not in b' \t\r\n,}]':
+ return True
+ return True
+
+
+def _may_have_final_usage(protocol, payload, chat_finished):
+ if protocol == 'chat' and not chat_finished and not _may_have_non_null(payload, b'finish_reason'):
+ return False
+ if protocol == 'responses' and b'response.completed' not in payload and b'response.incomplete' not in payload:
+ return False
+ if protocol == 'messages' and b'message_delta' not in payload:
+ return False
+ return _may_have_non_null(payload, b'usage')
+
+
+def _unique_pairs(pairs):
+ result = {}
+ for key, value in pairs:
+ if key in result:
+ raise ValueError('Duplicate JSON key')
+ result[key] = value
+ return result
+
+
+def extract_usage(protocol, value, *, allow_partial=False):
+ """Return inclusive input, output and cached counts, or None if unknown."""
+ if not isinstance(value, dict):
+ return None
+ if protocol == 'responses' and isinstance(value.get('response'), dict):
+ value = value['response']
+ usage = value.get('usage')
+ if not isinstance(usage, dict):
+ return None
+ if protocol == 'messages':
+ uncached = _count(usage.get('input_tokens'))
+ cached = _count(usage.get('cache_read_input_tokens'))
+ created = _count(usage.get('cache_creation_input_tokens', 0))
+ output = _count(usage.get('output_tokens'))
+ if None in (uncached, cached, created, output):
+ return None
+ prompt = uncached + cached + created
+ elif protocol == 'responses':
+ prompt = _count(usage.get('input_tokens'))
+ output = _count(usage.get('output_tokens'))
+ details = usage.get('input_tokens_details')
+ if details is not None and not isinstance(details, dict):
+ return None
+ cached = _count(details.get('cached_tokens')) if isinstance(details, dict) else None
+ if isinstance(details, dict) and details.get('cached_tokens') is not None and cached is None:
+ return None
+ else:
+ prompt = _count(usage.get('prompt_tokens'))
+ output = _count(usage.get('completion_tokens'))
+ details = usage.get('prompt_tokens_details')
+ if details is not None and not isinstance(details, dict):
+ return None
+ cached = _count(details.get('cached_tokens')) if isinstance(details, dict) else None
+ alternate = _count(usage.get('prompt_cache_hit_tokens'))
+ if ((isinstance(details, dict) and details.get('cached_tokens') is not None and cached is None) or
+ (usage.get('prompt_cache_hit_tokens') is not None and alternate is None)):
+ return None
+ if cached is None:
+ cached = alternate
+ elif alternate is not None and alternate != cached:
+ return None
+ miss = usage.get('prompt_cache_miss_tokens')
+ if miss is not None and (_count(miss) is None or prompt is None or cached is None or miss != prompt - cached):
+ return None
+ if prompt is None or output is None or prompt > MAX_COUNT:
+ return None
+ if cached is None and (not allow_partial or protocol not in ('chat', 'responses')):
+ return None
+ if cached is not None and cached > prompt:
+ return None
+ total = usage.get('total_tokens')
+ if total is not None and _count(total) != prompt + output:
+ return None
+ return prompt, output, cached
+
+
+def has_visible_delta(protocol, value):
+ """Recognize a nonempty output delta, not a role or lifecycle event."""
+ if not isinstance(value, dict):
+ return False
+ if protocol == 'chat':
+ choices = value.get('choices')
+ for choice in choices if isinstance(choices, list) else ():
+ delta = choice.get('delta') if isinstance(choice, dict) else None
+ if not isinstance(delta, dict):
+ continue
+ if any(isinstance(delta.get(k), str) and delta[k] for k in ('content', 'reasoning', 'reasoning_content')):
+ return True
+ for call in delta.get('tool_calls', ()) if isinstance(delta.get('tool_calls'), list) else ():
+ fn = call.get('function') if isinstance(call, dict) else None
+ if isinstance(fn, dict) and any(isinstance(fn.get(k), str) and fn[k] for k in ('name', 'arguments')):
+ return True
+ return False
+ event_type = value.get('type')
+ if protocol == 'responses':
+ return (isinstance(event_type, str) and event_type.startswith('response.') and
+ event_type.endswith('.delta') and isinstance(value.get('delta'), str) and bool(value['delta']))
+ if protocol == 'messages' and event_type == 'content_block_delta':
+ delta = value.get('delta')
+ return isinstance(delta, dict) and any(isinstance(delta.get(k), str) and delta[k]
+ for k in ('text', 'thinking', 'partial_json'))
+ return False
+
+
+class UsageCapture:
+ """Bound memory and retain only numerical usage after each response frame."""
+
+ def __init__(self, protocol, content_type, content_encoding='', on_first_output=None, *,
+ max_frame=MAX_FRAME, max_structure=None, max_delta=None, allow_partial=False):
+ self.protocol = protocol
+ self.allow_partial = allow_partial
+ self.max_frame = max_frame
+ self.max_structure = max_structure
+ self.max_delta = max_delta
+ media_type = (content_type or '').split(';', 1)[0].strip().lower()
+ self.kind = ('sse' if media_type == 'text/event-stream' else
+ 'json' if media_type == 'application/json' else 'other')
+ if (content_encoding or '').strip().lower() not in ('', 'identity'):
+ self.kind = 'other'
+ self.buffer = bytearray()
+ self.line_scan_start = 0
+ self.event_data = bytearray()
+ self.event_bytes = 0
+ self.event_lines = 0
+ self.discard_line = False
+ self.discard_event = False
+ self.too_large = False
+ self.incomplete = False
+ self.usage = None
+ self.terminal = False
+ self.on_first_output = on_first_output
+ self.saw_output = False
+ self.first_unavailable = False
+ self.skipped = False
+ self.chat_finished = False
+
+ def feed(self, data):
+ if self.kind == 'other':
+ return
+ if self.kind == 'json':
+ if not self.too_large and len(self.buffer) + len(data) <= self.max_frame:
+ self.buffer.extend(data)
+ else:
+ self.buffer.clear()
+ self.too_large = True
+ return
+ if self.discard_line:
+ end = data.find(b'\n')
+ if end < 0:
+ return
+ data = data[end + 1:]
+ self.discard_line = False
+ self.buffer.extend(data)
+ while True:
+ end = self.buffer.find(b'\n', self.line_scan_start)
+ if end < 0:
+ if len(self.buffer) > self.max_frame:
+ self.buffer.clear()
+ self.line_scan_start = 0
+ self.event_data.clear()
+ self.event_bytes = 0
+ self.event_lines = 0
+ self.discard_line = True
+ self.discard_event = True
+ self.incomplete = True
+ else:
+ self.line_scan_start = len(self.buffer)
+ break
+ line = bytes(self.buffer[:end]).removesuffix(b'\r')
+ del self.buffer[:end + 1]
+ self.line_scan_start = 0
+ if self.discard_line:
+ self.discard_line = False
+ continue
+ if not line:
+ self._event()
+ elif line.startswith(b'data:') and not self.discard_event:
+ item = line[5:]
+ if item.startswith(b' '):
+ item = item[1:]
+ self.event_lines += 1
+ self.event_bytes += len(item) + (self.event_lines > 1)
+ if self.event_bytes > self.max_frame or self.event_lines > MAX_DATA_LINES:
+ self.discard_event = True
+ self.incomplete = True
+ self.event_data.clear()
+ self.event_bytes = 0
+ self.event_lines = 0
+ else:
+ if self.event_lines > 1:
+ self.event_data.append(10)
+ self.event_data.extend(item)
+
+ def _event(self):
+ if not self.discard_event and self.event_lines:
+ payload = self.event_data
+ if payload == b'[DONE]':
+ if self.protocol == 'chat':
+ self.terminal = True
+ elif not self.terminal:
+ self.incomplete = True
+ elif self.terminal:
+ self.incomplete = True
+ elif (self.max_delta is not None and len(payload) > self.max_delta and
+ not _may_have_final_usage(self.protocol, payload, self.chat_finished)):
+ # A later valid final usage can still account for the request,
+ # but skipped output cannot establish the first output time.
+ self.usage = None
+ self.skipped = True
+ self.chat_finished = False
+ if not self.saw_output:
+ self.first_unavailable = True
+ else:
+ if (self.protocol == 'chat' and self.max_delta is not None and len(payload) > self.max_delta
+ and not self.chat_finished):
+ self.usage = None
+ self.skipped = True
+ value = self._decode(payload)
+ if not isinstance(value, dict):
+ self.incomplete = True
+ else:
+ event_type = value.get('type')
+ # A prior usage snapshot is no longer final when generation
+ # continues. A later valid usage event can restore it.
+ if self.protocol == 'chat':
+ choices = value.get('choices')
+ self.usage = None
+ elif self.protocol == 'messages' and event_type != 'message_stop':
+ self.usage = None
+ elif self.protocol == 'messages' and value.get('usage') is not None:
+ self.incomplete = True
+ if self.protocol == 'chat':
+ if isinstance(choices, list) and any(
+ isinstance(choice, dict) and isinstance(choice.get('finish_reason'), str)
+ and choice['finish_reason'] for choice in choices):
+ self.chat_finished = True
+ if (not self.saw_output and not self.first_unavailable and not self.incomplete and
+ has_visible_delta(self.protocol, value)):
+ self.saw_output = True
+ if self.on_first_output:
+ self.on_first_output()
+ final = (self.protocol == 'chat' or
+ self.protocol == 'messages' and event_type == 'message_delta' or
+ self.protocol == 'responses' and event_type in
+ ('response.completed', 'response.incomplete'))
+ if final:
+ usage = extract_usage(self.protocol, value, allow_partial=self.allow_partial)
+ if usage is not None and (not self.skipped or self.protocol != 'chat' or self.chat_finished):
+ self.usage = usage
+ self.skipped = False
+ elif usage is None:
+ source = value.get('response') if self.protocol == 'responses' else value
+ if isinstance(source, dict) and source.get('usage') is not None:
+ self.incomplete = True
+ if (self.protocol == 'messages' and event_type == 'message_stop' or
+ self.protocol == 'responses' and event_type in
+ ('response.completed', 'response.incomplete')):
+ self.terminal = True
+ self.event_data.clear()
+ self.event_bytes = 0
+ self.event_lines = 0
+ self.discard_event = False
+
+ def _decode(self, payload):
+ if _too_complex(payload, self.max_structure):
+ self.incomplete = True
+ return None
+ try:
+ return json.loads(payload, object_pairs_hook=_unique_pairs)
+ except (ValueError, UnicodeDecodeError):
+ self.incomplete = True
+ return None
+
+ def finish(self):
+ if self.kind == 'json' and not self.too_large:
+ return extract_usage(self.protocol, self._decode(self.buffer), allow_partial=self.allow_partial)
+ if (self.kind == 'sse' and self.terminal and not self.incomplete and not self.buffer and
+ not self.event_lines and not self.discard_line and not self.discard_event):
+ return self.usage
+ return None
+
+
+class SafeCapture:
+ """A statistics parser error must never interrupt the user's response."""
+
+ def __init__(self, protocol, content_type, content_encoding='', on_first_output=None, *,
+ max_frame=MAX_FRAME, max_structure=None, max_delta=None, allow_partial=False):
+ self.reader = UsageCapture(protocol, content_type, content_encoding, on_first_output,
+ max_frame=max_frame, max_structure=max_structure, max_delta=max_delta,
+ allow_partial=allow_partial)
+ self.failed = False
+
+ def feed(self, data):
+ if not self.failed:
+ try:
+ self.reader.feed(data)
+ except Exception:
+ self.failed = True
+
+ def finish(self):
+ if self.failed:
+ return None
+ try:
+ return self.reader.finish()
+ except Exception:
+ self.failed = True
+ return None
diff --git a/deploy_stats/inprocess.py b/deploy_stats/inprocess.py
new file mode 100644
index 0000000..662e1a2
--- /dev/null
+++ b/deploy_stats/inprocess.py
@@ -0,0 +1,209 @@
+"""Observe gateway responses behind Caddy's verified default forwarding policy."""
+import time
+import threading
+import uuid
+from urllib.parse import urlsplit
+
+from .capture import SafeCapture
+from .proxy import PROTOCOLS, client_ip
+from .session import BodyBudget
+from .store import Recorder
+
+
+CAPTURE_BUDGET = 32 * 1024 * 1024
+MAX_CAPTURE_FRAME = 4 * 1024 * 1024
+MAX_CAPTURE_STRUCTURE = 16384
+MAX_CAPTURE_DELTA = 64 * 1024
+
+
+class _Observer:
+ def __init__(self, recorder, budget, headers, protocol):
+ self.recorder = recorder
+ self.budget = budget
+ self.protocol = protocol
+ self.started = time.perf_counter()
+ self.request_id = uuid.uuid4().hex
+ # Caddy must be verified to overwrite untrusted X-Forwarded-For.
+ # Direct loopback callers do not have that provenance guarantee.
+ self.source = client_ip(headers, header='X-Forwarded-For')
+ self.session_id = None
+ self.first = None
+ self.status = None
+ self.completed = False
+ self.capture = None
+ self.reserved = 0
+ self.failed = False
+ self.recorder.begin(self.request_id, time.time(), self.source, None)
+
+ def fail(self):
+ if not self.failed:
+ self.failed = True
+ if self.reserved:
+ self.budget.release(self.reserved)
+ self.reserved = 0
+ self.capture = None
+ try:
+ self.recorder.note_drop()
+ except Exception:
+ pass
+
+ def session(self, value):
+ if not self.failed and value is not None:
+ self.session_id = value
+ self.recorder.set_session(self.request_id, value)
+
+ def error(self, status):
+ self.status = status
+
+ def response(self, status, content_type, content_encoding):
+ self.status = status
+ if not self.failed and status < 400 and self.protocol:
+ def first_output():
+ if self.first is None:
+ self.first = max(0, time.perf_counter() - self.started)
+ self.recorder.first_output(self.request_id, self.first)
+ self.capture = SafeCapture(self.protocol, content_type, content_encoding, first_output,
+ max_frame=MAX_CAPTURE_FRAME, max_structure=MAX_CAPTURE_STRUCTURE,
+ max_delta=MAX_CAPTURE_DELTA,
+ allow_partial=getattr(self.recorder, 'source', None) == 'gateway-inprocess-v1')
+
+ def chunk(self, data):
+ if self.failed or self.capture is None:
+ return
+ if not self.budget.reserve(len(data)):
+ self.fail()
+ return
+ self.reserved += len(data)
+ self.capture.feed(data)
+ if self.capture.failed:
+ self.fail()
+ return
+ retained = len(self.capture.reader.buffer) + self.capture.reader.event_bytes
+ released = self.reserved - retained
+ if released > 0:
+ self.budget.release(released)
+ self.reserved -= released
+
+ def complete(self, clean):
+ self.completed = clean
+
+ def finish(self):
+ state, values = 'interrupted', None
+ try:
+ if self.status is not None and self.status >= 400:
+ state = 'failed'
+ elif self.completed:
+ if not self.failed and self.capture is not None:
+ values = self.capture.finish()
+ state = ('partial' if values is not None and values[2] is None else
+ 'known' if values is not None else 'missing')
+ except Exception:
+ self.fail()
+ state, values = 'missing', None
+ finally:
+ if self.reserved:
+ self.budget.release(self.reserved)
+ self.reserved = 0
+ self.capture = None
+ prompt, output, cached = values if state in ('known', 'partial') else (None, None, None)
+ try:
+ try:
+ self.recorder.submit((time.time(), self.source, max(0, time.perf_counter() - self.started),
+ self.status, state, prompt, output, cached, self.session_id, self.first),
+ self.request_id)
+ finally:
+ self.recorder.end(self.request_id)
+ except Exception:
+ try:
+ self.recorder.note_drop()
+ except Exception:
+ pass
+
+
+def make_handler(base_handler, recorder, *, budget=None):
+ """Wrap one gateway handler; recorder failure never replaces its response."""
+ budget = budget or BodyBudget(CAPTURE_BUDGET)
+
+ class InstrumentedHandler(base_handler):
+ _llm_stats_wrapped = True
+
+ def proxy(self):
+ try:
+ path = urlsplit(self.path).path
+ except ValueError:
+ return super().proxy()
+ if self.command != 'POST' or not path.startswith('/v1/'):
+ return super().proxy()
+ observer = None
+ try:
+ observer = _Observer(recorder, budget, self.headers, PROTOCOLS.get(path))
+ except Exception:
+ try:
+ recorder.note_drop()
+ except Exception:
+ pass
+ self._stats_observer = observer
+ try:
+ return super().proxy()
+ finally:
+ if observer is not None:
+ try:
+ observer.finish()
+ except Exception:
+ observer.fail()
+ self._stats_observer = None
+
+ def error(self, status, message):
+ self._observe('error', status)
+ return super().error(status, message)
+
+ return InstrumentedHandler
+
+
+def install(module, database, *, source=None):
+ """Enable capture on this gateway module; unavailable storage fails open."""
+ if (getattr(module.Handler, '_llm_stats_wrapped', False) or
+ not callable(getattr(module.Handler, '_observe', None))):
+ return None
+ try:
+ recorder = Recorder(database, source=source)
+ except Exception:
+ return None
+ try:
+ module.Handler = make_handler(module.Handler, recorder)
+ except Exception:
+ recorder.close()
+ return None
+ return recorder
+
+
+def serve(module, database, make_servers, *args, **kwargs):
+ """Run an existing two-server bridge with fail-open, in-process capture."""
+ original_handler = module.Handler
+ recorder = install(module, database)
+ installed_handler = module.Handler if recorder is not None else None
+ inner = outer = None
+ inner_started = False
+ try:
+ inner, outer = make_servers(*args, **kwargs)
+ worker = threading.Thread(target=inner.serve_forever, daemon=True)
+ worker.start()
+ inner_started = True
+ outer.serve_forever()
+ finally:
+ try:
+ if outer is not None:
+ outer.server_close()
+ finally:
+ try:
+ if inner is not None:
+ try:
+ if inner_started:
+ inner.shutdown()
+ finally:
+ inner.server_close()
+ finally:
+ if recorder is not None:
+ if module.Handler is installed_handler:
+ module.Handler = original_handler
+ recorder.close()
diff --git a/deploy_stats/motor_metrics.py b/deploy_stats/motor_metrics.py
new file mode 100644
index 0000000..3f650ea
--- /dev/null
+++ b/deploy_stats/motor_metrics.py
@@ -0,0 +1,669 @@
+"""Read Motor's registered vLLM counters without changing the inference path."""
+from collections import Counter
+from concurrent.futures import ThreadPoolExecutor
+from contextlib import closing
+from copy import deepcopy
+import hashlib
+from http.client import HTTPConnection, HTTPException
+import ipaddress
+import json
+import math
+import os
+from pathlib import Path
+import re
+import sqlite3
+import threading
+import time
+
+
+MAX_PAYLOAD = 1024 * 1024
+MAX_INSTANCES = 32
+MAX_ENDPOINTS = 256
+BUCKET_SECONDS = 600
+BUCKET_COUNT = 144
+HISTORY_METHOD = 'vllm-counter-rate-v1'
+APPLICATION_ID = 0x4C444D48
+SCHEMA_VERSION = 1
+MTP_COUNTERS = ('vllm:spec_decode_num_drafts_total', 'vllm:spec_decode_num_draft_tokens_total',
+ 'vllm:spec_decode_num_accepted_tokens_total')
+METRICS = frozenset({
+ 'process_start_time_seconds', 'vllm:prompt_tokens_by_source_total',
+ 'vllm:generation_tokens_total', 'vllm:request_prefill_time_seconds_sum',
+ 'vllm:request_prefill_time_seconds_count', 'vllm:request_decode_time_seconds_sum',
+ 'vllm:request_decode_time_seconds_count', 'vllm:num_requests_running',
+ 'vllm:num_requests_waiting', 'vllm:prompt_tokens_total',
+ 'vllm:prefix_cache_hits_total', 'vllm:prefix_cache_queries_total',
+}) | frozenset(MTP_COUNTERS)
+SAMPLE = re.compile(r'^([a-zA-Z_:][a-zA-Z0-9_:]*)(?:\{(.*)\})?\s+(\S+)(?:\s+\d+)?\s*$')
+LABEL = re.compile(r'\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*("(?:[^"\\]|\\["\\n])*")\s*(,|$)')
+
+
+def parse_metrics(payload):
+ """Parse only the bounded counter subset; duplicate series are ambiguous."""
+ if not isinstance(payload, str) or len(payload.encode('utf-8')) > MAX_PAYLOAD:
+ raise ValueError('INVALID_METRICS')
+ result = {}
+ invalid_mtp = False
+ for line in payload.splitlines():
+ line = line.strip()
+ if not line or line.startswith('#'):
+ continue
+ name = line.split('{', 1)[0].split(None, 1)[0]
+ if name not in METRICS:
+ continue
+ try:
+ sample = SAMPLE.fullmatch(line)
+ if not sample:
+ raise ValueError('INVALID_SAMPLE')
+ labels = {}
+ raw = sample.group(2)
+ if raw is not None:
+ offset = 0
+ while offset < len(raw):
+ match = LABEL.match(raw, offset)
+ if not match or match.group(1) in labels:
+ raise ValueError('INVALID_LABELS')
+ labels[match.group(1)] = json.loads(match.group(2))
+ offset = match.end()
+ if match.group(3) == '' and offset != len(raw):
+ raise ValueError('INVALID_LABELS')
+ if match.group(3) == ',' and offset == len(raw):
+ raise ValueError('INVALID_LABELS')
+ value = float(sample.group(3))
+ if not math.isfinite(value) or value < 0:
+ raise ValueError('INVALID_VALUE')
+ if name.endswith('_count') and not value.is_integer():
+ raise ValueError('INVALID_COUNT')
+ if name in MTP_COUNTERS and (not value.is_integer() or value > 2**53 - 1):
+ raise ValueError('INVALID_MTP_COUNTER')
+ key = tuple(sorted(labels.items()))
+ samples = result.setdefault(name, {})
+ if key in samples:
+ raise ValueError('DUPLICATE_SAMPLE')
+ samples[key] = value
+ except ValueError:
+ if name not in MTP_COUNTERS:
+ raise
+ invalid_mtp = True
+ if invalid_mtp:
+ for name in MTP_COUNTERS:
+ result.pop(name, None)
+ starts = result.get('process_start_time_seconds', {})
+ if set(starts) != {()} or starts[()] <= 0:
+ raise ValueError('MISSING_PROCESS_IDENTITY')
+ return result
+
+
+def _get(host, port, path):
+ connection = HTTPConnection(host, port, timeout=2)
+ try:
+ connection.request('GET', path, headers={'Accept-Encoding': 'identity', 'Connection': 'close'})
+ response = connection.getresponse()
+ if response.status != 200:
+ raise ValueError('SOURCE_UNAVAILABLE')
+ body = response.read(MAX_PAYLOAD + 1)
+ if len(body) > MAX_PAYLOAD:
+ raise ValueError('OVERSIZED_SOURCE')
+ return body.decode('utf-8')
+ finally:
+ connection.close()
+
+
+def _fingerprint(value):
+ return hashlib.sha256(value.encode('utf-8')).hexdigest()
+
+
+def _topology(payload):
+ value = json.loads(payload)
+ instances = value.get('instances') if isinstance(value, dict) else None
+ if not isinstance(instances, list) or len(instances) > MAX_INSTANCES:
+ raise ValueError('INVALID_TOPOLOGY')
+ result = {}
+ count = 0
+ for entry in instances:
+ if not isinstance(entry, dict):
+ raise ValueError('INVALID_INSTANCE')
+ identity, role = entry.get('id'), entry.get('role')
+ if type(identity) is int and 0 <= identity < 2**63:
+ identity = str(identity)
+ if not isinstance(identity, str) or not identity or len(identity) > 512 or identity in result:
+ raise ValueError('INVALID_IDENTITY')
+ if role not in ('prefill', 'decode'):
+ continue
+ endpoints = entry.get('endpoints')
+ if not isinstance(endpoints, list):
+ raise ValueError('INVALID_ENDPOINTS')
+ targets = []
+ for endpoint in endpoints:
+ if not isinstance(endpoint, dict):
+ raise ValueError('INVALID_ENDPOINT')
+ address = str(ipaddress.ip_address(endpoint.get('ip')))
+ port = endpoint.get('business_port')
+ if type(port) is int:
+ parsed_port = port
+ elif isinstance(port, str) and port.isascii() and port.isdigit() and len(port) <= 5:
+ parsed_port = int(port)
+ else:
+ raise ValueError('INVALID_PORT')
+ if not 1 <= parsed_port <= 65535:
+ raise ValueError('INVALID_PORT')
+ targets.append((address, parsed_port))
+ count += len(targets)
+ if count > MAX_ENDPOINTS:
+ raise ValueError('TOO_MANY_ENDPOINTS')
+ model = entry.get('model_name')
+ if model is not None and (not isinstance(model, str) or not model or len(model) > 512):
+ raise ValueError('INVALID_MODEL')
+ result[identity] = {'role': role, 'targets': tuple(sorted(targets)), 'model_name': model,
+ 'ready': entry.get('status') == 'active' and entry.get('healthy') is True}
+ return result
+
+
+def _selected(values, name, source=None):
+ series = values.get(name, {})
+ if source is not None:
+ series = {labels: value for labels, value in series.items() if dict(labels).get('source') == source}
+ return series
+
+
+def _delta(current, previous, name, source=None):
+ after, before = _selected(current, name, source), _selected(previous, name, source)
+ if not after or set(after) != set(before):
+ return None
+ diffs = [after[key] - before[key] for key in after]
+ if any(value < 0 or not math.isfinite(value) for value in diffs):
+ return None
+ value = sum(diffs)
+ return value if math.isfinite(value) else None
+
+
+def _sum_metric(values, name):
+ series = values.get(name, {})
+ if not series:
+ return None
+ result = sum(series.values())
+ return result if math.isfinite(result) else None
+
+
+def _sum_values(values):
+ if not values:
+ return None
+ result = sum(values)
+ return result if math.isfinite(result) else None
+
+
+def _mtp_counts(values, model):
+ """Use only consistent engine/model series; return counts in counter order."""
+ if not isinstance(model, str) or not model:
+ return None
+ series = [values.get(name, {}) for name in MTP_COUNTERS]
+ if not series[0] or any(set(item) != set(series[0]) for item in series[1:]):
+ return None
+ if any(dict(labels).get('model_name') != model for labels in series[0]):
+ return None
+ if any(series[2][labels] > series[1][labels] for labels in series[0]):
+ return None
+ result = tuple(sum(int(value) for value in item.values()) for item in series)
+ return result if all(value <= 2**53 - 1 for value in result) else None
+
+
+def _mtp_change(current, previous, model):
+ if _mtp_counts(current, model) is None or _mtp_counts(previous, model) is None:
+ return None
+ series = [current[name] for name in MTP_COUNTERS]
+ old = [previous[name] for name in MTP_COUNTERS]
+ if any(set(new) != set(before) for new, before in zip(series, old)):
+ return None
+ changes = [{key: new[key] - before[key] for key in new} for new, before in zip(series, old)]
+ if any(value < 0 or not math.isfinite(value) for item in changes for value in item.values()):
+ return None
+ if any(changes[2][labels] > changes[1][labels] for labels in changes[0]):
+ return None
+ result = tuple(sum(int(value) for value in item.values()) for item in changes)
+ return result if all(value <= 2**53 - 1 for value in result) else None
+
+
+def _mtp_summary(items, expected, *, partial=False):
+ if not items:
+ return None
+ steps, drafted, accepted = (sum(row[index] for row in items) for index in range(3))
+ if any(value > 2**53 - 1 for value in (steps, drafted, accepted)) or accepted > drafted:
+ return None
+ return {'steps': steps, 'drafted': drafted, 'accepted': accepted,
+ 'acceptance_rate': accepted / drafted if drafted else None,
+ 'mean_acceptance_length': 1 + accepted / steps if steps else None,
+ 'valid_endpoints': len(items), 'expected_endpoints': expected,
+ 'partial': partial or len(items) < expected}
+
+
+def _agreed(topologies):
+ available = [top for top in topologies if top is not None]
+ groups, ambiguous = {}, False
+ if available:
+ identities = set().union(*(set(top) for top in available))
+ for identity in identities:
+ entries = [top.get(identity) for top in available]
+ if any(entry is None for entry in entries):
+ ambiguous = True
+ continue
+ shape = {(entry['role'], entry['targets'], entry['model_name']) for entry in entries}
+ if len(shape) != 1:
+ ambiguous = True
+ continue
+ groups[identity] = {**entries[0], 'ready': all(entry['ready'] for entry in entries)}
+ return groups, ambiguous
+
+
+class MotorHistory:
+ """Persist sampled rates in ten-minute buckets in a dedicated identified file."""
+ def __init__(self, path):
+ self.path = Path(path).resolve()
+ try:
+ fd = os.open(self.path, os.O_CREAT | os.O_EXCL | os.O_RDWR, 0o600)
+ except FileExistsError:
+ created = False
+ else:
+ os.close(fd)
+ created = True
+ with closing(self._connect()) as connection, connection:
+ if created:
+ connection.execute(f'PRAGMA application_id = {APPLICATION_ID}')
+ connection.execute(f'PRAGMA user_version = {SCHEMA_VERSION}')
+ connection.execute('''CREATE TABLE rate_buckets (
+ group_key TEXT NOT NULL, phase TEXT NOT NULL, bucket INTEGER NOT NULL,
+ rate_sum REAL NOT NULL, samples INTEGER NOT NULL, partial_samples INTEGER NOT NULL,
+ PRIMARY KEY (group_key, phase, bucket)) WITHOUT ROWID''')
+ elif (connection.execute('PRAGMA application_id').fetchone()[0] != APPLICATION_ID or
+ connection.execute('PRAGMA user_version').fetchone()[0] != SCHEMA_VERSION):
+ raise ValueError('INVALID_MOTOR_HISTORY')
+ # Additive table: older readers continue to use rate_buckets unchanged.
+ connection.execute('''CREATE TABLE IF NOT EXISTS mtp_buckets (
+ group_key TEXT NOT NULL, bucket INTEGER NOT NULL,
+ steps INTEGER NOT NULL, drafted INTEGER NOT NULL, accepted INTEGER NOT NULL,
+ samples INTEGER NOT NULL, partial_samples INTEGER NOT NULL,
+ PRIMARY KEY (group_key, bucket)) WITHOUT ROWID''')
+ columns = [(row[1], row[2].upper(), row[5]) for row in connection.execute('PRAGMA table_info(mtp_buckets)')]
+ if columns != [('group_key', 'TEXT', 1), ('bucket', 'INTEGER', 2), ('steps', 'INTEGER', 0),
+ ('drafted', 'INTEGER', 0), ('accepted', 'INTEGER', 0), ('samples', 'INTEGER', 0),
+ ('partial_samples', 'INTEGER', 0)]:
+ raise ValueError('INVALID_MTP_HISTORY')
+ connection.execute('PRAGMA journal_mode = WAL')
+
+ def _connect(self):
+ return sqlite3.connect(self.path.as_uri() + '?mode=rw', uri=True, timeout=1)
+
+ def record(self, group_key, phase, at, rate, *, partial=False):
+ if (not isinstance(group_key, str) or not group_key or phase not in ('prefill', 'decode') or
+ not isinstance(at, (int, float)) or not math.isfinite(at) or at < 0 or
+ not isinstance(rate, (int, float)) or not math.isfinite(rate) or rate < 0 or
+ type(partial) is not bool):
+ raise ValueError('INVALID_RATE_SAMPLE')
+ bucket = int(at // BUCKET_SECONDS) * BUCKET_SECONDS
+ with closing(self._connect()) as connection, connection:
+ connection.execute('''INSERT INTO rate_buckets VALUES (?, ?, ?, ?, 1, ?)
+ ON CONFLICT(group_key, phase, bucket) DO UPDATE SET
+ rate_sum = rate_sum + excluded.rate_sum, samples = samples + 1,
+ partial_samples = partial_samples + excluded.partial_samples''',
+ (group_key, phase, bucket, rate, int(partial)))
+
+ def series(self, group_key, now):
+ end = int(now // BUCKET_SECONDS) * BUCKET_SECONDS
+ start = end - BUCKET_COUNT * BUCKET_SECONDS
+ if start < 0:
+ return None
+ with closing(self._connect()) as connection:
+ rows = connection.execute('''SELECT bucket, phase, rate_sum, samples, partial_samples FROM rate_buckets
+ WHERE group_key = ? AND bucket >= ? AND bucket < ?''', (group_key, start, end)).fetchall()
+ grouped = {(bucket, phase): (total, samples, partial) for bucket, phase, total, samples, partial in rows}
+ points = []
+ for bucket in range(start, end, BUCKET_SECONDS):
+ point = {'start': bucket, 'end': bucket + BUCKET_SECONDS}
+ for phase in ('prefill', 'decode'):
+ total, samples, partial = grouped.get((bucket, phase), (0, 0, 0))
+ point[phase + '_tps'] = total / samples if samples else None
+ point[phase + '_samples'] = samples
+ point[phase + '_partial'] = partial > 0
+ points.append(point)
+ return {'method': HISTORY_METHOD, 'bucket_seconds': BUCKET_SECONDS, 'points': points}
+
+ def record_mtp(self, group_key, at, steps, drafted, accepted, *, partial=False):
+ if (not isinstance(group_key, str) or not group_key or type(at) not in (int, float) or
+ not math.isfinite(at) or at < 0 or any(type(v) is not int or not 0 <= v <= 2**53 - 1
+ for v in (steps, drafted, accepted)) or
+ accepted > drafted or type(partial) is not bool):
+ raise ValueError('INVALID_MTP_SAMPLE')
+ bucket = int(at // BUCKET_SECONDS) * BUCKET_SECONDS
+ with closing(self._connect()) as connection, connection:
+ connection.execute('''INSERT INTO mtp_buckets VALUES (?, ?, ?, ?, ?, 1, ?)
+ ON CONFLICT(group_key, bucket) DO UPDATE SET
+ steps=steps+excluded.steps, drafted=drafted+excluded.drafted,
+ accepted=accepted+excluded.accepted, samples=samples+1,
+ partial_samples=partial_samples+excluded.partial_samples''',
+ (group_key, bucket, steps, drafted, accepted, int(partial)))
+
+ def mtp_series(self, group_key, now):
+ end = int(now // BUCKET_SECONDS) * BUCKET_SECONDS
+ start = end - BUCKET_COUNT * BUCKET_SECONDS
+ if start < 0:
+ return None
+ with closing(self._connect()) as connection:
+ rows = connection.execute('''SELECT bucket, steps, drafted, accepted, samples, partial_samples
+ FROM mtp_buckets WHERE group_key=? AND bucket>=? AND bucket''',
+ (group_key, start, end)).fetchall()
+ grouped = {bucket: (steps, drafted, accepted, samples, partial)
+ for bucket, steps, drafted, accepted, samples, partial in rows}
+ points = []
+ for bucket in range(start, end, BUCKET_SECONDS):
+ steps, drafted, accepted, samples, partial = grouped.get(bucket, (0, 0, 0, 0, 0))
+ points.append({'start': bucket, 'end': bucket + BUCKET_SECONDS, 'steps': steps if samples else None,
+ 'drafted': drafted if samples else None, 'accepted': accepted if samples else None,
+ 'acceptance_rate': accepted / drafted if samples and drafted > 0 else None,
+ # A sampled delta cannot establish coverage of the whole bucket.
+ 'samples': samples, 'partial': samples > 0,
+ 'time_coverage': 'unverified' if samples else None,
+ 'partial_workers': partial > 0})
+ return {'method': 'vllm-mtp-counter-delta-v1', 'bucket_seconds': BUCKET_SECONDS, 'points': points}
+
+
+class MotorCollector:
+ """Bounded passive scrapes; only same-process counter deltas produce rates."""
+ def __init__(self, management_ports, *, fetch_topology=None, fetch_endpoint=None,
+ wall_time=time.time, monotonic=time.monotonic, interval=30, history=None):
+ ports = tuple(dict.fromkeys(management_ports))
+ if (not ports or len(ports) > 8 or any(type(port) is not int or not 1 <= port <= 65535 for port in ports)
+ or not 5 <= interval <= 300):
+ raise ValueError('INVALID_MOTOR_CONFIG')
+ self.ports = ports
+ self.fetch_topology = fetch_topology or (lambda port: _get('127.0.0.1', port, '/instances'))
+ self.fetch_endpoint = fetch_endpoint or (lambda host, port: _get(host, port, '/metrics'))
+ self.wall_time, self.monotonic, self.interval = wall_time, monotonic, interval
+ self.history = history
+ self.previous = {}
+ self.sequence = 0
+ self.lock = threading.Lock()
+ self.collect_lock = threading.Lock()
+ self.stop_event = threading.Event()
+ self.value = {'schema': 1, 'kind': 'vllm-motor-telemetry', 'sampled_at': 0,
+ 'nodes': [], 'history_available': False}
+ self.worker = None
+
+ def _read_topology(self, port):
+ try:
+ return _topology(self.fetch_topology(port))
+ except Exception:
+ return None
+
+ def _read_endpoint(self, target):
+ started = self.monotonic()
+ try:
+ values = parse_metrics(self.fetch_endpoint(*target))
+ ended = self.monotonic()
+ return values, ended, self.wall_time(), max(0, ended - started)
+ except Exception:
+ return None
+
+ def collect(self):
+ with self.collect_lock:
+ return self._collect()
+
+ def _collect(self):
+ with ThreadPoolExecutor(max_workers=min(4, len(self.ports))) as executor:
+ before = list(executor.map(self._read_topology, self.ports))
+ before_groups, ambiguous = _agreed(before)
+ before_addresses = Counter(target for group in before_groups.values() for target in group['targets'])
+ targets = [target for target, count in before_addresses.items() if count == 1]
+ with ThreadPoolExecutor(max_workers=4) as executor:
+ readings = dict(zip(targets, executor.map(self._read_endpoint, targets)))
+ with ThreadPoolExecutor(max_workers=min(4, len(self.ports))) as executor:
+ after = list(executor.map(self._read_topology, self.ports))
+ after_groups, after_ambiguous = _agreed(after)
+ common_sources = sum(left is not None and right is not None for left, right in zip(before, after))
+ ambiguous = ambiguous or after_ambiguous or not common_sources
+ changed = {identity for identity in set(before_groups) | set(after_groups)
+ if before_groups.get(identity) != after_groups.get(identity)}
+ if changed:
+ ambiguous = True
+ groups = {identity: value for identity, value in before_groups.items()
+ if common_sources and identity not in changed}
+ after_addresses = Counter(target for group in after_groups.values() for target in group['targets'])
+ ambiguous_targets = {target for target in set(before_addresses) | set(after_addresses)
+ if before_addresses[target] > 1 or after_addresses[target] > 1}
+ for topology in before + after:
+ if topology is not None:
+ counts = Counter(target for group in topology.values() for target in group['targets'])
+ ambiguous_targets.update(target for target, count in counts.items() if count > 1)
+ for identity in changed:
+ for group in (before_groups.get(identity), after_groups.get(identity)):
+ if group is not None:
+ ambiguous_targets.update(group['targets'])
+ if ambiguous_targets:
+ ambiguous = True
+ self.sequence += 1
+ now = self.wall_time()
+ now_monotonic = self.monotonic()
+ new_previous, nodes = {}, []
+ history_ok = self.history is not None
+ for identity, group in sorted(groups.items()):
+ role = group['role']
+ # Previous histories did not identify a model and cannot safely be attributed.
+ group_key = _fingerprint(json.dumps(['motor-v2', identity, role, group['targets'], group['model_name']],
+ separators=(',', ':')))
+ # 48 bits stay exactly representable as an integer in the browser.
+ public_id = int(group_key[:12], 16)
+ node = {'label': ('Prefill ' if role == 'prefill' else 'Decode ') + group_key[:8],
+ 'backend_port': public_id, 'role': role, 'received_at': now,
+ 'source_ok': False, 'backend_ready': group['ready'], 'clock_ok': True,
+ 'clock_uncertainty_seconds': 0, 'driver_ok': False,
+ 'expected_endpoints': len(group['targets']),
+ 'prefill': None, 'decode': None, 'hbm': None, 'phase_timers': None, 'history': None,
+ 'mtp': None, 'mtp_history': None}
+ rates, window_starts, window_ends, deadlines, observed_times, count_delta, seconds_delta = [], [], [], [], [], 0, 0.0
+ mtp_lifetime, mtp_interval, mtp_times, mtp_window = [], [], [], []
+ mtp_key = _fingerprint(json.dumps(['mtp-v1', identity, role, group['targets'], group['model_name']],
+ separators=(',', ':')))
+ metrics = {'running': [], 'waiting': [], 'input_total': [], 'hit_pairs': []}
+ observed_endpoints = 0
+ valid_timer_workers = 0
+ for target in group['targets']:
+ if target in ambiguous_targets or not group['ready']:
+ continue
+ reading = readings.get(target)
+ if reading is None:
+ continue
+ values, stamp, sampled_at, uncertainty = reading
+ # Motor's registered model must match every series used in the display.
+ model = group['model_name']
+ values = {name: ({labels: value for labels, value in series.items()
+ if model is not None and dict(labels).get('model_name') == model}
+ if name.startswith('vllm:') else series)
+ for name, series in values.items()}
+ age = now_monotonic - stamp
+ earliest = sampled_at - uncertainty
+ if (not 0 <= age + uncertainty < self.interval * 2 + 5 or
+ not 0 <= now - earliest < self.interval * 2 + 5 or uncertainty > 20):
+ continue
+ observed_endpoints += 1
+ observed_times.append(earliest)
+ node['source_ok'] = True
+ node['clock_uncertainty_seconds'] = max(node['clock_uncertainty_seconds'], uncertainty)
+ mtp = _mtp_counts(values, group['model_name']) if role == 'decode' else None
+ if mtp is not None:
+ mtp_lifetime.append(mtp)
+ mtp_times.append(earliest)
+ for key, name in (('running', 'vllm:num_requests_running'),
+ ('waiting', 'vllm:num_requests_waiting')):
+ value = _sum_metric(values, name)
+ if value is not None:
+ metrics[key].append(value)
+ if role == 'prefill':
+ total = _sum_metric(values, 'vllm:prompt_tokens_total')
+ hits = _sum_metric(values, 'vllm:prefix_cache_hits_total')
+ queries = _sum_metric(values, 'vllm:prefix_cache_queries_total')
+ if total is not None:
+ metrics['input_total'].append(total)
+ if hits is not None and queries is not None and hits <= queries:
+ metrics['hit_pairs'].append((hits, queries))
+ endpoint_key = (group_key, target)
+ previous = self.previous.get(endpoint_key)
+ new_previous[endpoint_key] = (values, stamp, sampled_at, uncertainty)
+ if previous is None:
+ continue
+ old, old_stamp, old_sampled_at, old_uncertainty = previous
+ duration = stamp - old_stamp
+ if (not 0 < duration <= self.interval * 1.5 + 5 or
+ sampled_at < old_sampled_at or
+ abs((sampled_at - old_sampled_at) - duration) > 5 + uncertainty + old_uncertainty or
+ values['process_start_time_seconds'] != old['process_start_time_seconds']):
+ continue
+ mtp_change = _mtp_change(values, old, group['model_name']) if role == 'decode' else None
+ if mtp_change is not None:
+ mtp_interval.append(mtp_change)
+ mtp_window.append((old_sampled_at - old_uncertainty, sampled_at, earliest))
+ name = 'vllm:prompt_tokens_by_source_total' if role == 'prefill' else 'vllm:generation_tokens_total'
+ change = _delta(values, old, name, 'local_compute' if role == 'prefill' else None)
+ if change is None:
+ continue
+ rates.append(change / duration)
+ window_starts.append(old_sampled_at - old_uncertainty)
+ window_ends.append(sampled_at)
+ deadlines.append(earliest + self.interval * 2 + 5)
+ prefix = 'vllm:request_' + role + '_time_seconds_'
+ count = _delta(values, old, prefix + 'count')
+ seconds = _delta(values, old, prefix + 'sum')
+ same_series = (set(_selected(values, prefix + 'count')) == set(_selected(values, prefix + 'sum'))
+ and set(_selected(old, prefix + 'count')) == set(_selected(old, prefix + 'sum')))
+ if count is not None and seconds is not None and count > 0 and same_series:
+ count_delta += count
+ seconds_delta += seconds
+ valid_timer_workers += 1
+ if rates:
+ rate = sum(rates)
+ span = max(window_ends) - min(window_starts)
+ valid_until = min(deadlines)
+ if math.isfinite(rate) and 0 <= span <= 60 and valid_until > now:
+ part = {'tokens_per_second': rate, 'log_at': max(window_ends), 'log_skew_seconds': span,
+ 'valid_until': valid_until, 'step': self.sequence,
+ 'valid_endpoints': len(rates), 'expected_endpoints': len(group['targets']),
+ 'partial': len(rates) < len(group['targets']) or common_sources < len(self.ports) or ambiguous,
+ 'mfu_estimate': None}
+ node[role] = part
+ if valid_timer_workers and count_delta and math.isfinite(count_delta) and math.isfinite(seconds_delta):
+ node['phase_timers'] = {'step': self.sequence, role: {
+ 'mean_ms': seconds_delta * 1000 / count_delta,
+ 'samples': int(count_delta), 'ranks': valid_timer_workers,
+ 'expected_ranks': len(group['targets'])}}
+ if self.history is not None:
+ try:
+ self.history.record(group_key, role, part['log_at'], rate, partial=part['partial'])
+ except (OSError, sqlite3.Error, ValueError, OverflowError):
+ history_ok = False
+ mtp_partial = common_sources < len(self.ports) or ambiguous
+ lifetime = _mtp_summary(mtp_lifetime, len(group['targets']), partial=mtp_partial)
+ interval = _mtp_summary(mtp_interval, len(group['targets']), partial=mtp_partial)
+ if lifetime is not None:
+ lifetime.update(sampled_at=min(mtp_times), valid_until=min(mtp_times) + self.interval * 2 + 5)
+ if interval is not None:
+ span = max(end for _, end, _ in mtp_window) - min(start for start, _, _ in mtp_window)
+ if span > 60:
+ interval = None
+ else:
+ interval.update(sampled_at=max(end for _, end, _ in mtp_window),
+ valid_until=min(earliest for _, _, earliest in mtp_window) + self.interval * 2 + 5,
+ span_seconds=span)
+ if self.history is not None:
+ try:
+ self.history.record_mtp(mtp_key, interval['sampled_at'], interval['steps'],
+ interval['drafted'], interval['accepted'],
+ partial=interval['partial'])
+ except (OSError, sqlite3.Error, ValueError, OverflowError):
+ history_ok = False
+ if lifetime is not None or interval is not None:
+ node['mtp'] = {'lifetime': lifetime, 'interval': interval}
+ if self.history is not None and role == 'decode' and group['model_name']:
+ try:
+ node['mtp_history'] = self.history.mtp_series(mtp_key, now)
+ except (OSError, sqlite3.Error, ValueError, OverflowError):
+ history_ok = False
+ pairs = metrics['hit_pairs']
+ hit_total = _sum_values([hit for hit, _ in pairs])
+ query_total = _sum_values([query for _, query in pairs])
+ node['received_at'] = min(observed_times) if observed_times else None
+ node['engine_metrics'] = {
+ 'running': _sum_values(metrics['running']),
+ 'waiting': _sum_values(metrics['waiting']),
+ 'input_total': _sum_values(metrics['input_total']),
+ 'hit_rate': hit_total / query_total if hit_total is not None and query_total else None,
+ 'observed_endpoints': observed_endpoints, 'expected_endpoints': len(group['targets']),
+ 'sampled_at': min(observed_times), 'valid_until': min(observed_times) + self.interval * 2 + 5,
+ 'partial': (observed_endpoints < len(group['targets']) or common_sources < len(self.ports)
+ or ambiguous or len(metrics['running']) < observed_endpoints
+ or len(metrics['waiting']) < observed_endpoints or
+ (role == 'prefill' and (len(metrics['input_total']) < observed_endpoints or
+ len(metrics['hit_pairs']) < observed_endpoints))),
+ } if observed_times else None
+ if self.history is not None:
+ try:
+ node['history'] = self.history.series(group_key, now)
+ except (OSError, sqlite3.Error, ValueError, OverflowError):
+ history_ok = False
+ nodes.append(node)
+ self.previous = new_previous
+ if len({node['backend_port'] for node in nodes}) != len(nodes):
+ nodes, ambiguous = [], True
+ result = {'schema': 1, 'kind': 'vllm-motor-telemetry', 'sampled_at': now, 'nodes': nodes,
+ 'topology_sources': {'available': common_sources, 'total': len(self.ports)},
+ 'topology_ambiguous': ambiguous, 'prefill_tps': None, 'decode_tps': None,
+ 'prefill_mfu': None, 'hbm_percent': None, 'history_available': history_ok,
+ 'history_dropped': 0}
+ with self.lock:
+ self.value = result
+ return deepcopy(result)
+
+ def snapshot(self):
+ with self.lock:
+ value = deepcopy(self.value)
+ now = self.wall_time()
+ age = now - value['sampled_at']
+ if not 0 <= age <= self.interval * 2 + 20:
+ return {'schema': 1, 'kind': 'vllm-motor-telemetry', 'sampled_at': 0,
+ 'nodes': [], 'history_available': False}
+ for node in value['nodes']:
+ for phase in ('prefill', 'decode'):
+ part = node.get(phase)
+ if part is not None and (not 0 <= now - part['log_at'] or now >= part['valid_until']):
+ node[phase] = None
+ timers = node.get('phase_timers')
+ if isinstance(timers, dict) and not any(node.get(phase) and node[phase]['step'] == timers.get('step')
+ for phase in ('prefill', 'decode')):
+ node['phase_timers'] = None
+ metrics = node.get('engine_metrics')
+ if metrics is not None and (not 0 <= now - metrics['sampled_at'] or now >= metrics['valid_until']):
+ node['engine_metrics'] = None
+ mtp = node.get('mtp')
+ if mtp is not None:
+ for key in ('lifetime', 'interval'):
+ part = mtp.get(key)
+ if part is not None and (not 0 <= now - part['sampled_at'] or now >= part['valid_until']):
+ mtp[key] = None
+ if not any(mtp.values()):
+ node['mtp'] = None
+ return value
+
+ def start(self):
+ if self.worker is not None:
+ raise RuntimeError('COLLECTOR_ALREADY_STARTED')
+ def run():
+ while not self.stop_event.is_set():
+ try:
+ self.collect()
+ except Exception:
+ pass
+ if self.stop_event.wait(self.interval):
+ break
+ self.worker = threading.Thread(target=run, name='motor-metrics', daemon=True)
+ self.worker.start()
+
+ def close(self):
+ self.stop_event.set()
+ if self.worker is not None:
+ self.worker.join(timeout=5)
diff --git a/deploy_stats/proxy.py b/deploy_stats/proxy.py
new file mode 100644
index 0000000..8042892
--- /dev/null
+++ b/deploy_stats/proxy.py
@@ -0,0 +1,551 @@
+"""A loopback-only, byte-transparent accounting hop before the existing gateway."""
+import asyncio
+from contextlib import suppress
+from email.parser import Parser
+from http.client import HTTPMessage
+import ipaddress
+import os
+import socket
+import threading
+import time
+import uuid
+from urllib.parse import urlsplit
+
+from .capture import SafeCapture
+from .session import MAX_BODY, SessionBodyCapture, explicit_session, key_digest
+from .store import Recorder
+
+CLIENT_HEADER = 'x-llmdeploy-client-ip'
+PROTOCOLS = {'/v1/chat/completions': 'chat', '/v1/responses': 'responses',
+ '/v1/messages': 'messages'}
+# Allow a large incoming header plus additional fields from the front proxy.
+MAX_HEADER = 1024 * 1024 + 65536
+MAX_RESPONSE_HEADER = 10 * 1024 * 1024
+WINDOWS_WORKERS = 3
+WINDOWS_CONNECTIONS_PER_WORKER = 160
+MAX_CONNECTIONS = WINDOWS_WORKERS * WINDOWS_CONNECTIONS_PER_WORKER
+
+
+def client_ip(headers, *, header=CLIENT_HEADER):
+ values = headers.get_all(header, [])
+ if len(values) != 1 or not isinstance(values[0], str):
+ return None
+ try:
+ address = ipaddress.ip_address(values[0].strip())
+ if isinstance(address, ipaddress.IPv6Address) and address.ipv4_mapped:
+ address = address.ipv4_mapped
+ return str(address)
+ except ValueError:
+ return None
+
+
+def session_fingerprint(headers):
+ return explicit_session(headers)[1]
+
+
+def _headers(raw):
+ # Match http.client.parse_headers, used by the existing gateway: header
+ # octets are decoded as ISO-8859-1 before the email parser sees them.
+ return Parser(_class=HTTPMessage).parsestr((raw + b'\r\n').decode('iso-8859-1'))
+
+
+async def _read(reader, size, timeout):
+ return await asyncio.wait_for(reader.read(size), timeout)
+
+
+async def _readexactly(reader, size, timeout):
+ return await asyncio.wait_for(reader.readexactly(size), timeout)
+
+
+async def _line(reader, timeout):
+ return await asyncio.wait_for(reader.readuntil(b'\r\n'), timeout)
+
+
+async def _response_body(reader, writer, headers, method, status, capture, timeout):
+ async def send(raw, observe=False):
+ if observe and capture:
+ capture.feed(raw)
+ writer.write(raw)
+ await writer.drain()
+
+ if method == 'HEAD' or status in (204, 304) or 100 <= status < 200:
+ return
+ encoding = (headers.get('Transfer-Encoding') or '').lower()
+ lengths = headers.get_all('Content-Length', [])
+ if encoding:
+ if encoding.strip() != 'chunked' or lengths:
+ raise ValueError('INVALID_RESPONSE_FRAMING')
+ while True:
+ line = await _line(reader, timeout)
+ size = int(line[:-2].split(b';', 1)[0], 16)
+ if size < 0:
+ raise ValueError('INVALID_CHUNK_SIZE')
+ await send(line)
+ if size == 0:
+ while True:
+ trailer = await _line(reader, timeout)
+ await send(trailer)
+ if trailer == b'\r\n':
+ return
+ left = size
+ while left:
+ data = await _read(reader, min(left, 65536), timeout)
+ if not data:
+ raise asyncio.IncompleteReadError(b'', left)
+ await send(data, True)
+ left -= len(data)
+ suffix = await _readexactly(reader, 2, timeout)
+ if suffix != b'\r\n':
+ raise ValueError('INVALID_CHUNK_END')
+ await send(suffix)
+ elif lengths:
+ if len(lengths) != 1:
+ raise ValueError('INVALID_RESPONSE_LENGTH')
+ left = int(lengths[0])
+ if left < 0:
+ raise ValueError('INVALID_RESPONSE_LENGTH')
+ while left:
+ data = await _read(reader, min(left, 65536), timeout)
+ if not data:
+ raise asyncio.IncompleteReadError(b'', left)
+ await send(data, True)
+ left -= len(data)
+ else:
+ while True:
+ data = await _read(reader, 65536, timeout)
+ if not data:
+ return
+ await send(data, True)
+
+
+class Proxy:
+ """Async transfer keeps cancellation independent from slow upstream reads."""
+
+ def __init__(self, port, upstream_port, database, *, timeout=10800):
+ if type(port) is not int or type(upstream_port) is not int or not 0 <= port <= 65535 or not 1 <= upstream_port <= 65535:
+ raise ValueError('INVALID_PORT')
+ if not 0 < timeout <= 86400:
+ raise ValueError('INVALID_TIMEOUT')
+ self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ try:
+ if hasattr(socket, 'SO_EXCLUSIVEADDRUSE'):
+ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1)
+ self.socket.bind(('127.0.0.1', port))
+ self.socket.listen(MAX_CONNECTIONS)
+ self.socket.setblocking(False)
+ self.server_port = self.socket.getsockname()[1]
+ if self.server_port == upstream_port:
+ raise ValueError('UPSTREAM_EQUALS_PROXY')
+ self.recorder = Recorder(database)
+ except Exception:
+ self.socket.close()
+ raise
+ self.upstream_port = upstream_port
+ self.timeout = timeout
+ self.active_lock = threading.Lock()
+ self.active = 0
+ self.loop = None
+ self.stop = None
+ self.finished = threading.Event()
+ self.ready = threading.Event()
+ self.tasks = set()
+ self.tasks_lock = threading.Lock()
+ self.connections_lock = threading.Lock()
+ self.connections = 0
+ self.shutdown_requested = threading.Event()
+
+ def serve_forever(self, poll_interval=None):
+ try:
+ if os.name == 'nt':
+ self._serve_windows()
+ else:
+ asyncio.run(self._serve())
+ finally:
+ self.finished.set()
+
+ async def _serve(self):
+ self.loop = asyncio.get_running_loop()
+ self.stop = asyncio.Event()
+ server = await asyncio.start_server(self._handle, sock=self.socket, limit=MAX_HEADER)
+ self.ready.set()
+ async with server:
+ await self.stop.wait()
+ with self.tasks_lock:
+ tasks = list(self.tasks)
+ for task in tasks:
+ task.cancel()
+ if tasks:
+ await asyncio.gather(*tasks, return_exceptions=True)
+
+ def shutdown(self):
+ if self.ready.wait(3):
+ self.shutdown_requested.set()
+ if self.loop and self.stop:
+ self.loop.call_soon_threadsafe(self.stop.set)
+ self.finished.wait(5)
+
+ def _serve_windows(self):
+ # One Windows selector can watch only 512 sockets. Dispatch accepted
+ # connections across independent loops, each with room for both the
+ # client and upstream sockets, instead of using the reset-prone proactor.
+ workers = []
+ worker_lock = threading.Lock()
+ started = [threading.Event() for _ in range(WINDOWS_WORKERS)]
+ loops = [None] * WINDOWS_WORKERS
+ stops = [None] * WINDOWS_WORKERS
+ assigned = [0] * WINDOWS_WORKERS
+ errors = []
+
+ async def worker(index):
+ loop = asyncio.get_running_loop()
+ loops[index], stops[index] = loop, asyncio.Event()
+ started[index].set()
+ await stops[index].wait()
+ with self.tasks_lock:
+ tasks = [task for task in self.tasks if task.get_loop() is loop]
+ for task in tasks:
+ task.cancel()
+ if tasks:
+ await asyncio.gather(*tasks, return_exceptions=True)
+
+ def run_worker(index):
+ try:
+ with asyncio.Runner(loop_factory=asyncio.SelectorEventLoop) as runner:
+ runner.run(worker(index))
+ except Exception as exc:
+ errors.append(exc)
+ started[index].set()
+ self.shutdown_requested.set()
+
+ async def handle_socket(conn, index):
+ try:
+ loop = asyncio.get_running_loop()
+ reader = asyncio.StreamReader(limit=MAX_HEADER)
+ protocol = asyncio.StreamReaderProtocol(reader)
+ transport, _ = await loop.connect_accepted_socket(lambda: protocol, conn)
+ writer = asyncio.StreamWriter(transport, protocol, reader, loop)
+ await self._handle(reader, writer)
+ finally:
+ conn.close()
+ with worker_lock:
+ assigned[index] -= 1
+
+ def dispatch(conn, index):
+ task = asyncio.create_task(handle_socket(conn, index))
+ # Track handoff as well as the handler so shutdown cannot leave an
+ # accepted but not-yet-attached socket behind.
+ with self.tasks_lock:
+ self.tasks.add(task)
+ task.add_done_callback(lambda done: self._discard_task(done))
+
+ try:
+ for index in range(WINDOWS_WORKERS):
+ thread = threading.Thread(target=run_worker, args=(index,), daemon=True)
+ workers.append(thread)
+ thread.start()
+ if not all(event.wait(3) for event in started) or errors:
+ raise RuntimeError('PROXY_WORKER_START_FAILED')
+ self.socket.settimeout(.2)
+ self.ready.set()
+ while not self.shutdown_requested.is_set():
+ try:
+ conn, _ = self.socket.accept()
+ except socket.timeout:
+ continue
+ conn.setblocking(False)
+ with worker_lock:
+ index = min(range(WINDOWS_WORKERS), key=assigned.__getitem__)
+ if assigned[index] < WINDOWS_CONNECTIONS_PER_WORKER:
+ assigned[index] += 1
+ else:
+ index = None
+ if index is None:
+ self.recorder.note_drop()
+ with suppress(OSError):
+ # Give an already-arriving head a short chance to drain;
+ # closing with unread bytes otherwise resets Windows peers.
+ conn.settimeout(.05)
+ conn.recv(MAX_HEADER)
+ with suppress(OSError):
+ conn.sendall(b'HTTP/1.1 429 Too Many Requests\r\nContent-Length: 0\r\nConnection: close\r\n\r\n')
+ conn.close()
+ else:
+ loops[index].call_soon_threadsafe(dispatch, conn, index)
+ finally:
+ for loop, stop in zip(loops, stops):
+ if loop and stop:
+ with suppress(RuntimeError):
+ loop.call_soon_threadsafe(stop.set)
+ for thread in workers:
+ thread.join(5)
+
+ def _discard_task(self, task):
+ with self.tasks_lock:
+ self.tasks.discard(task)
+
+ def server_close(self):
+ if self.ready.is_set() and not self.finished.is_set():
+ self.shutdown()
+ if not self.finished.is_set():
+ raise RuntimeError('PROXY_SHUTDOWN_TIMEOUT')
+ self.socket.close()
+ with self.active_lock:
+ with self.recorder.lock:
+ self.recorder.dropped += self.active
+ self.recorder.close()
+
+ async def _handle(self, client_reader, client_writer):
+ task = asyncio.current_task()
+ with self.tasks_lock:
+ self.tasks.add(task)
+ with self.connections_lock:
+ accepted = self.connections < MAX_CONNECTIONS
+ if accepted:
+ self.connections += 1
+ if not accepted:
+ self.recorder.note_drop()
+ # Read the request head before closing so Windows does not reset a
+ # small request while the caller is trying to receive the 429.
+ with suppress(OSError, ConnectionError, asyncio.TimeoutError, asyncio.LimitOverrunError,
+ asyncio.IncompleteReadError):
+ await asyncio.wait_for(client_reader.readuntil(b'\r\n\r\n'), 1)
+ with suppress(OSError, ConnectionError):
+ client_writer.write(b'HTTP/1.1 429 Too Many Requests\r\nContent-Length: 0\r\nConnection: close\r\n\r\n')
+ await client_writer.drain()
+ client_writer.close()
+ with suppress(OSError, ConnectionError, asyncio.TimeoutError):
+ await asyncio.wait_for(client_writer.wait_closed(), 1)
+ self._discard_task(task)
+ return
+ started = time.perf_counter()
+ started_wall = time.time()
+ counted = False
+ source = None
+ session = None
+ has_session_header = False
+ request_id = None
+ first_visible = None
+ status, state, values = None, 'interrupted', None
+ sent_headers = False
+ upstream_writer = None
+ body_task = watch_task = copy_task = head_task = None
+ body_done = asyncio.Event()
+ abort_send = asyncio.Event()
+ disconnected = asyncio.Event()
+ try:
+ raw = await asyncio.wait_for(client_reader.readuntil(b'\r\n\r\n'), 30)
+ request_line, _, header_bytes = raw[:-4].partition(b'\r\n')
+ method_bytes, target_bytes, version = request_line.split(b' ', 2)
+ method = method_bytes.decode('ascii')
+ target = target_bytes.decode('ascii')
+ if version not in (b'HTTP/1.0', b'HTTP/1.1'):
+ raise ValueError('INVALID_HTTP_VERSION')
+ path = urlsplit(target).path
+ counted = method == 'POST' and path.startswith('/v1/')
+ protocol = PROTOCOLS.get(path)
+ headers = _headers(header_bytes)
+ source = client_ip(headers)
+ if counted:
+ has_session_header, session = explicit_session(headers)
+ request_id = uuid.uuid4().hex
+ with self.active_lock:
+ self.active += 1
+ try:
+ self.recorder.begin(request_id, started_wall, source, session)
+ except Exception:
+ self.recorder.note_drop()
+ lines = header_bytes.split(b'\r\n')
+ if any(line.startswith((b' ', b'\t')) for line in lines):
+ raise ValueError('FOLDED_HEADER_UNSUPPORTED')
+ forward = [line for line in lines if line.split(b':', 1)[0].strip().lower()
+ not in (CLIENT_HEADER.encode(), b'connection')]
+ forward.append(b'Connection: close')
+ upstream_reader, upstream_writer = await asyncio.wait_for(
+ asyncio.open_connection('127.0.0.1', self.upstream_port, limit=MAX_RESPONSE_HEADER), 5)
+ upstream_writer.write(request_line + b'\r\n' + b'\r\n'.join(forward) + b'\r\n\r\n')
+ await upstream_writer.drain()
+ lengths = headers.get_all('Content-Length', [])
+ try:
+ length = int(lengths[0]) if len(lengths) == 1 else 0
+ except ValueError:
+ length = 0
+ if headers.get('Transfer-Encoding') or len(lengths) > 1 or not 0 <= length <= 64 * 1024 * 1024:
+ length = 0 # Existing gateway rejects the unchanged headers.
+ body_capture = None
+ if counted and not has_session_header and 0 < length <= MAX_BODY:
+ try:
+ body_capture = SessionBodyCapture(key_digest(headers))
+ except Exception:
+ self.recorder.note_drop()
+
+ async def send_body():
+ nonlocal session, body_capture
+ left = length
+ try:
+ while left:
+ chunk = await _read(client_reader, min(left, 65536), 30)
+ if not chunk:
+ disconnected.set()
+ upstream_writer.close()
+ return
+ left -= len(chunk)
+ if not abort_send.is_set():
+ if body_capture:
+ try:
+ body_capture.feed(chunk)
+ except Exception:
+ with suppress(Exception):
+ body_capture.close()
+ body_capture = None
+ self.recorder.note_drop()
+ try:
+ upstream_writer.write(chunk)
+ await upstream_writer.drain()
+ await asyncio.sleep(0)
+ except (OSError, ConnectionError):
+ abort_send.set()
+ elif body_capture:
+ with suppress(Exception):
+ body_capture.close()
+ body_capture = None
+ except Exception:
+ disconnected.set()
+ upstream_writer.close()
+ finally:
+ try:
+ if body_capture:
+ if left == 0 and not disconnected.is_set():
+ session = body_capture.finish()
+ if session is not None:
+ self.recorder.set_session(request_id, session)
+ else:
+ body_capture.close()
+ except Exception:
+ if body_capture:
+ with suppress(Exception):
+ body_capture.close()
+ self.recorder.note_drop()
+ finally:
+ body_done.set()
+
+ async def watch_client():
+ await body_done.wait()
+ try:
+ while await client_reader.read(65536):
+ pass
+ except Exception:
+ pass
+ disconnected.set()
+ upstream_writer.close()
+
+ head_task = asyncio.create_task(asyncio.wait_for(
+ upstream_reader.readuntil(b'\r\n\r\n'), self.timeout))
+ if length:
+ # Let an immediate auth/admission rejection arrive before a
+ # large upload can reset an upstream that does not read it.
+ deadline = asyncio.get_running_loop().time() + (.1 if length >= 4096 else .01)
+ while True:
+ remaining = max(0, deadline - asyncio.get_running_loop().time())
+ done, _ = await asyncio.wait((head_task,), timeout=remaining)
+ if not done:
+ break
+ preliminary = head_task.result()
+ parts = preliminary.split(b'\r\n', 1)[0].split(b' ', 2)
+ if len(parts) < 2 or not parts[1].isdigit():
+ break
+ code = int(parts[1])
+ if code < 100 or code > 599:
+ raise ValueError('INVALID_RESPONSE_STATUS')
+ if code >= 200:
+ abort_send.set()
+ break
+ client_writer.write(preliminary)
+ await client_writer.drain()
+ head_task = asyncio.create_task(asyncio.wait_for(
+ upstream_reader.readuntil(b'\r\n\r\n'), self.timeout))
+ body_task = asyncio.create_task(send_body())
+ watch_task = asyncio.create_task(watch_client())
+ while True:
+ response_head = await head_task
+ status_line, _, response_fields = response_head[:-4].partition(b'\r\n')
+ status_parts = status_line.split(b' ', 2)
+ status = int(status_parts[1])
+ if status < 100 or status > 599:
+ raise ValueError('INVALID_RESPONSE_STATUS')
+ if status >= 200:
+ fields = [line for line in response_fields.split(b'\r\n') if line and
+ line.split(b':', 1)[0].strip().lower() != b'connection']
+ response_head = status_line + b'\r\n' + b'\r\n'.join(fields + [b'Connection: close']) + b'\r\n\r\n'
+ client_writer.write(response_head)
+ await client_writer.drain()
+ if status >= 200:
+ sent_headers = True
+ break
+ head_task = asyncio.create_task(asyncio.wait_for(
+ upstream_reader.readuntil(b'\r\n\r\n'), self.timeout))
+ if not body_done.is_set():
+ abort_send.set()
+ response_headers = _headers(response_fields)
+ def first_output():
+ nonlocal first_visible
+ if first_visible is None:
+ first_visible = max(0, time.perf_counter() - started)
+ if request_id is not None:
+ self.recorder.first_output(request_id, first_visible)
+
+ capture = SafeCapture(protocol, response_headers.get('Content-Type'),
+ response_headers.get('Content-Encoding'), first_output) if protocol else None
+ copy_task = asyncio.create_task(_response_body(upstream_reader, client_writer, response_headers,
+ method, status, capture, self.timeout))
+ done, _ = await asyncio.wait((copy_task, watch_task), return_when=asyncio.FIRST_COMPLETED)
+ error_response_completed = (status >= 400 and copy_task in done and
+ not copy_task.cancelled() and copy_task.exception() is None)
+ if disconnected.is_set() and not error_response_completed:
+ copy_task.cancel()
+ await asyncio.gather(copy_task, return_exceptions=True)
+ else:
+ await copy_task
+ if status >= 400:
+ state = 'failed'
+ elif capture and (values := capture.finish()) is not None:
+ state = 'known'
+ else:
+ state = 'missing'
+ except Exception:
+ if not sent_headers and not disconnected.is_set():
+ with suppress(OSError, ConnectionError):
+ status = 502
+ client_writer.write(b'HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\nConnection: close\r\n\r\n')
+ await client_writer.drain()
+ finally:
+ abort_send.set()
+ if body_task and not body_task.done():
+ with suppress(asyncio.TimeoutError, OSError, ConnectionError):
+ await asyncio.wait_for(asyncio.shield(body_task), 2)
+ pending_tasks = [pending for pending in (body_task, watch_task, copy_task, head_task) if pending]
+ for pending in pending_tasks:
+ if pending and not pending.done():
+ pending.cancel()
+ if pending_tasks:
+ await asyncio.gather(*pending_tasks, return_exceptions=True)
+ if upstream_writer:
+ upstream_writer.close()
+ with suppress(OSError, ConnectionError, asyncio.TimeoutError):
+ await asyncio.wait_for(upstream_writer.wait_closed(), 1)
+ client_writer.close()
+ with suppress(OSError, ConnectionError, asyncio.TimeoutError):
+ await asyncio.wait_for(client_writer.wait_closed(), 1)
+ if counted:
+ prompt, output, cached = values if state == 'known' else (None, None, None)
+ with self.active_lock:
+ try:
+ self.recorder.end(request_id)
+ self.recorder.submit((time.time(), source, max(0, time.perf_counter() - started),
+ status, state, prompt, output, cached, session, first_visible), request_id)
+ except Exception:
+ self.recorder.note_drop()
+ finally:
+ self.active -= 1
+ with self.connections_lock:
+ self.connections -= 1
+ self._discard_task(task)
diff --git a/deploy_stats/remote_observe.py b/deploy_stats/remote_observe.py
new file mode 100644
index 0000000..d4432f8
--- /dev/null
+++ b/deploy_stats/remote_observe.py
@@ -0,0 +1,248 @@
+"""Read bounded DeepInfer log summaries and Ascend device metrics on a host.
+
+This file is streamed to the remote Python interpreter; raw container logs and
+container configuration are not returned or written by the probe.
+"""
+import ctypes
+from datetime import datetime, timezone
+import hashlib
+import json
+import math
+import os
+import re
+import selectors
+import subprocess
+import sys
+import time
+
+
+SOURCES = {'stats': '/opt/deepinfer-build/source/deepinfer/engine/stats_logger.py',
+ 'backend': '/opt/deepinfer-build/source/examples/recipe_api/backend.py',
+ 'parallel': '/opt/deepinfer-build/source/deepinfer/config.py',
+ 'engine': '/opt/deepinfer-build/source/deepinfer/engine/llm_engine.py',
+ 'sampler': '/opt/deepinfer-build/source/deepinfer/model_executor/layers/sampler.py',
+ 'native': '/opt/deepinfer-build/source/deepinfer/_so/ascend-aarch64/_C.cpython-312-aarch64-linux-gnu.so'}
+LOG_LIMIT = 8 * 1024 * 1024
+DRIVER = '/usr/local/Ascend/driver/lib64/driver/libdcmi.so'
+CONTAINER = re.compile(r'[A-Za-z0-9][A-Za-z0-9_.-]{0,127}\Z')
+HEADER = re.compile(r'RANK\s+(\d+),\s*GPU\s+(\d+),\s*step\s+(\d+),\s*\[(stats|cpu)\]')
+STAMP = re.compile(r'^(\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?Z)\s')
+NUMBER = r'(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?'
+TIMER = re.compile(r'engine_step:(prefill|decode)\s+cur(' + NUMBER + r')ms/mean(' + NUMBER +
+ r')ms/max(' + NUMBER + r')ms/(\d+)calls/')
+
+
+def _number(line, name):
+ match = re.search(r'(?:^|\|\s*)' + re.escape(name) + r'\s+(' + NUMBER + r')(?=\s|\||\))', line)
+ if not match:
+ return None
+ value = float(match.group(1))
+ return value if math.isfinite(value) and value >= 0 else None
+
+
+def parse_log(data):
+ """Extract numerical log fields only; invalid fields do not erase good ones."""
+ rows = []
+ for line in data.decode('utf-8', errors='replace').splitlines():
+ stamp, head = STAMP.match(line), HEADER.search(line)
+ if not stamp or not head:
+ continue
+ try:
+ at = datetime.fromisoformat(stamp.group(1).replace('Z', '+00:00')).timestamp()
+ rank, device, step = (int(head.group(i)) for i in (1, 2, 3))
+ except (ValueError, OverflowError):
+ continue
+ if rank > 255 or device > 255 or step > 2**63 - 1:
+ continue
+ row = {'at': at, 'rank': rank, 'device': device, 'step': step, 'kind': head.group(4)}
+ if row['kind'] == 'stats':
+ phase = re.search(r'\bis_prompt\s+(True|False)(?=\s|\||\))', line)
+ row.update(is_prompt=(phase.group(1) == 'True') if phase else None,
+ current_tps=_number(line, 'cur_TPS'), current_sps=_number(line, 'cur_SPS'),
+ prompt_tps=_number(line, 'prompt_TPS'), decode_tps=_number(line, 'completion_TPS'),
+ mixed_mfu=_number(line, 'MFU'))
+ share = re.search(r'\bprefill/decode\s+(' + NUMBER + r')%/(' + NUMBER + r')%', line)
+ if share:
+ p, d = (float(share.group(i)) for i in (1, 2))
+ row['phase_share'] = [p, d] if math.isfinite(p) and math.isfinite(d) and p <= 100 and d <= 100 else None
+ else:
+ row['phase_share'] = None
+ else:
+ timers = {}
+ for match in TIMER.finditer(line):
+ phase = match.group(1)
+ if phase in timers:
+ timers[phase] = None
+ continue
+ cur, mean, maximum = (float(match.group(i)) for i in (2, 3, 4))
+ calls = int(match.group(5))
+ timers[phase] = ({'mean_ms': mean, 'calls': calls} if calls > 0 and calls < 10**9 and
+ all(math.isfinite(x) and x >= 0 for x in (cur, mean, maximum)) else None)
+ row['timers'] = timers
+ rows.append(row)
+ return rows
+
+
+def bounded_command(command, timeout, limit):
+ """Collect finite output without leaving a blocked or unbounded log reader."""
+ if command and command[0] == 'docker' and not docker_ready():
+ raise ValueError('docker is not safely available')
+ proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
+ output = bytearray()
+ selector = selectors.DefaultSelector()
+ deadline = time.monotonic() + timeout
+ try:
+ os.set_blocking(proc.stdout.fileno(), False)
+ selector.register(proc.stdout, selectors.EVENT_READ)
+ while selector.get_map():
+ left = deadline - time.monotonic()
+ if left <= 0:
+ raise TimeoutError('probe timeout')
+ for key, _ in selector.select(min(left, .25)):
+ chunk = os.read(key.fd, min(65536, limit + 1 - len(output)))
+ if not chunk:
+ selector.unregister(key.fileobj)
+ break
+ output.extend(chunk)
+ if len(output) > limit:
+ raise ValueError('probe output too large')
+ if proc.wait(timeout=max(.01, deadline - time.monotonic())) != 0:
+ raise ValueError('probe command failed')
+ return bytes(output)
+ finally:
+ selector.close()
+ if proc.poll() is None:
+ proc.kill()
+ proc.wait(timeout=2)
+ proc.stdout.close()
+
+
+def docker_ready():
+ """Do not trigger Docker socket activation while observing an idle host."""
+ try:
+ service = subprocess.run(['systemctl', 'is-active', 'docker.service'], stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL, timeout=3, check=False)
+ socket = subprocess.run(['systemctl', 'is-active', 'docker.socket'], stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL, timeout=3, check=False)
+ return service.returncode == 0 and service.stdout.strip() == b'active' and socket.stdout.strip() == b'inactive'
+ except (OSError, subprocess.SubprocessError):
+ return False
+
+
+class HbmInfo(ctypes.Structure):
+ _fields_ = [('memory_size', ctypes.c_ulonglong), ('freq', ctypes.c_uint),
+ ('memory_usage', ctypes.c_ulonglong), ('temp', ctypes.c_int),
+ ('bandwidth', ctypes.c_uint)]
+
+
+def sample_hbm():
+ """Read device-wide driver percentages; failures remain unknown, never zero."""
+ lib = ctypes.CDLL(DRIVER)
+ lib.dcmiv2_init.argtypes = []
+ lib.dcmiv2_init.restype = ctypes.c_int
+ lib.dcmiv2_get_device_list.argtypes = [ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_int), ctypes.c_int]
+ lib.dcmiv2_get_device_list.restype = ctypes.c_int
+ lib.dcmiv2_get_device_hbm_info.argtypes = [ctypes.c_int, ctypes.POINTER(HbmInfo)]
+ lib.dcmiv2_get_device_hbm_info.restype = ctypes.c_int
+ if lib.dcmiv2_init() != 0:
+ return None
+ ids, count = (ctypes.c_int * 64)(), ctypes.c_int()
+ if lib.dcmiv2_get_device_list(ids, ctypes.byref(count), 64) != 0 or not 0 < count.value <= 64:
+ return None
+ results = []
+ for dev in list(ids)[:count.value]:
+ started = time.time()
+ value = HbmInfo()
+ status = lib.dcmiv2_get_device_hbm_info(dev, ctypes.byref(value))
+ ended = time.time()
+ valid = status == 0 and 0 < value.memory_size and value.bandwidth <= 100
+ results.append({'id': dev, 'started_at': started, 'finished_at': ended,
+ 'hbm_bandwidth_percent': value.bandwidth if valid else None})
+ return results
+
+
+def source_predates(mtimes, started_at):
+ return (len(mtimes) == len(SOURCES) and math.isfinite(started_at) and all(
+ part.isascii() and part.isdigit() and int(part) < math.floor(started_at) for part in mtimes))
+
+
+def driver_digest():
+ if not 0 < os.stat(DRIVER).st_size <= 16 * 1024 * 1024:
+ raise ValueError('driver size invalid')
+ digest = hashlib.sha256()
+ with open(DRIVER, 'rb') as source:
+ for chunk in iter(lambda: source.read(65536), b''):
+ digest.update(chunk)
+ return digest.hexdigest()
+
+
+def probe(container):
+ if not CONTAINER.fullmatch(container):
+ raise ValueError('invalid container')
+ cpu_start = time.process_time()
+ try:
+ import resource
+ child_start = resource.getrusage(resource.RUSAGE_CHILDREN)
+ except ImportError:
+ resource, child_start = None, None
+ result = {'schema': 1, 'started_at': time.time(), 'source_id': None, 'host_fingerprint': None,
+ 'image_fingerprint': None, 'driver_sha256': None, 'source_started_at': None,
+ 'source_before_start': False, 'source_sha256': {},
+ 'engine_rows': [], 'hbm': None}
+ identity = None
+ identity_cmd = ['docker', 'inspect', '--format',
+ '{{.Id}} {{.Image}} {{.State.StartedAt}} {{.State.Running}}', container]
+ try:
+ identity = bounded_command(identity_cmd, 5, 1024).decode().strip()
+ fields = identity.split()
+ if len(fields) != 4 or fields[-1] != 'true':
+ raise ValueError('container unavailable')
+ machine_id = open('/etc/machine-id', encoding='ascii').read().strip()
+ if not re.fullmatch(r'[0-9a-f]{32}', machine_id) or machine_id == '0' * 32:
+ raise ValueError('host identity unavailable')
+ result['host_fingerprint'] = hashlib.sha256(machine_id.encode()).hexdigest()
+ result['image_fingerprint'] = hashlib.sha256(fields[1].encode()).hexdigest()
+ result['source_id'] = hashlib.sha256(identity.encode()).hexdigest()
+ result['source_started_at'] = datetime.fromisoformat(fields[2].replace('Z', '+00:00')).timestamp()
+ for key, path in SOURCES.items():
+ digest = bounded_command(['docker', 'exec', container, 'sha256sum', path], 5, 1024).decode().split()[0]
+ if not re.fullmatch(r'[0-9a-f]{64}', digest):
+ raise ValueError('source unavailable')
+ result['source_sha256'][key] = digest
+ mtimes = bounded_command(['docker', 'exec', container, 'stat', '-L', '-c', '%Y', *SOURCES.values()],
+ 5, 1024).decode().split()
+ result['source_before_start'] = source_predates(mtimes, result['source_started_at'])
+ if not result['source_before_start']:
+ raise ValueError('source changed after container start')
+ raw = bounded_command(['docker', 'logs', '--timestamps', '--since', '120s', '--tail', '3000', container],
+ 8, LOG_LIMIT)
+ result['engine_rows'] = parse_log(raw)[-1024:]
+ except (OSError, ValueError, TimeoutError, UnicodeError, IndexError, subprocess.SubprocessError):
+ pass
+ try:
+ result['driver_sha256'] = driver_digest()
+ result['hbm'] = sample_hbm()
+ if driver_digest() != result['driver_sha256']:
+ result['driver_sha256'], result['hbm'] = None, None
+ except (OSError, ValueError, AttributeError):
+ result['driver_sha256'], result['hbm'] = None, None
+ if identity is not None:
+ try:
+ if bounded_command(identity_cmd, 5, 1024).decode().strip() != identity:
+ raise ValueError('container changed')
+ except (OSError, ValueError, TimeoutError, UnicodeError, subprocess.SubprocessError):
+ result.update(source_id=None, host_fingerprint=None, image_fingerprint=None, driver_sha256=None,
+ source_before_start=False, source_sha256={}, engine_rows=[], hbm=None)
+ result['finished_at'] = time.time()
+ result['probe_cpu_seconds'] = time.process_time() - cpu_start
+ if resource is not None:
+ child_end = resource.getrusage(resource.RUSAGE_CHILDREN)
+ result['probe_cpu_seconds'] += (child_end.ru_utime + child_end.ru_stime -
+ child_start.ru_utime - child_start.ru_stime)
+ return result
+
+
+if __name__ == '__main__':
+ if len(sys.argv) != 2 or not CONTAINER.fullmatch(sys.argv[1]):
+ raise SystemExit(2)
+ print(json.dumps(probe(sys.argv[1]), allow_nan=False, separators=(',', ':')))
diff --git a/deploy_stats/report.py b/deploy_stats/report.py
new file mode 100644
index 0000000..040bd12
--- /dev/null
+++ b/deploy_stats/report.py
@@ -0,0 +1,388 @@
+"""Read-only windows, per-IP aggregates, and completion-time buckets."""
+from contextlib import closing
+from datetime import datetime, timedelta, timezone
+import ipaddress
+import math
+from pathlib import Path
+import sqlite3
+import time
+
+from .store import IDENTITY, SCHEMA, partial_schema_available
+
+TZ = timezone(timedelta(hours=8))
+BUCKET_SECONDS = 600
+COVERAGE_EPSILON = 1e-6
+REQUEST_SORTS = frozenset(('finished', 'ip', 'session', 'state', 'input', 'output', 'cached', 'uncached_input', 'hit_rate',
+ 'first_visible', 'after_first', 'input_per_wait', 'output_per_after_first', 'first_share'))
+SESSION_SORTS = frozenset(('last_finished', 'session', 'ips', 'requests', 'input', 'output', 'cached', 'uncached_input', 'hit_rate',
+ 'first_visible', 'after_first', 'input_per_wait', 'output_per_after_first', 'first_share'))
+
+
+def _coverage(runs, start, end, now):
+ intervals = []
+ dropped = 0
+ unclean = 0
+ for run in runs:
+ left, seen, ended, lost = run
+ live = ended is None and 0 <= now - seen < 15
+ right = ended if ended is not None else (now if live else seen)
+ if left < end and right > start:
+ intervals.append((max(start, left), min(end, right)))
+ dropped += lost
+ if ended is None and not live:
+ # Events queued before the last heartbeat may never have been
+ # committed when a process exited without a clean drain.
+ unclean += 1
+ intervals.sort()
+ covered, edge = 0.0, start
+ for left, right in intervals:
+ covered += max(0, right - max(left, edge))
+ edge = max(edge, right)
+ return min(max(0.0, end - start), covered), dropped, unclean
+
+
+def _counts(rows):
+ out = {'requests': 0, 'known': 0, 'partial': 0, 'missing': 0, 'failed': 0, 'interrupted': 0,
+ 'input': 0, 'output': 0, 'cached': 0, 'uncached_input': 0, 'elapsed': 0.0}
+ hit_input = 0
+ for row in rows:
+ state, prompt, output, cached, elapsed = row
+ out['requests'] += 1
+ if state in ('known', 'partial'):
+ out[state] += 1
+ out['input'] += prompt
+ out['output'] += output
+ if state == 'known':
+ out['cached'] += cached
+ out['uncached_input'] += prompt - cached
+ hit_input += prompt
+ out['elapsed'] += elapsed
+ else:
+ out[state if state in ('missing', 'failed', 'interrupted') else 'missing'] += 1
+ out['uncached_input'] = out['uncached_input'] if out['known'] else None
+ out['hit_rate'] = out['cached'] / hit_input if hit_input else None
+ out['request_tps'] = out['output'] / out['elapsed'] if out['elapsed'] else None
+ return out
+
+
+def _percentile(values, fraction):
+ if not values:
+ return None
+ values = sorted(values)
+ position = (len(values) - 1) * fraction
+ left = int(position)
+ right = min(left + 1, len(values) - 1)
+ return values[left] + (values[right] - values[left]) * (position - left)
+
+
+def _request_view(row):
+ finished, ip, state, prompt, output, cached, elapsed, session, first, status, record_id = row
+ observed = (state in ('known', 'partial') and first is not None and math.isfinite(first) and
+ 0 <= first <= elapsed and elapsed > 0)
+ tail = elapsed - first if observed else None
+ known = state == 'known'
+ measured = state in ('known', 'partial')
+ return {'id': record_id, 'finished': finished, 'ip': ip, 'session': session, 'state': state,
+ 'status': status, 'input': prompt, 'output': output, 'cached': cached,
+ 'uncached_input': prompt - cached if known else None,
+ 'hit_rate': cached / prompt if known and prompt else None, 'elapsed': elapsed,
+ 'first_visible': first if observed else None, 'after_first': tail,
+ 'first_share': first / elapsed if observed else None,
+ 'after_first_share': tail / elapsed if observed else None,
+ # Estimates use gateway-observed intervals, not engine phase times.
+ 'input_per_wait': (prompt - cached) / first if known and observed and first > 0 else None,
+ 'output_per_after_first': (output - 1) / tail if measured and observed and output > 1 and tail > 0 else None}
+
+
+def _session_views(details):
+ groups = {}
+ for item in details:
+ if item['session']:
+ groups.setdefault(item['session'], []).append(item)
+ result = []
+ for session, items in groups.items():
+ known = [item for item in items if item['state'] == 'known']
+ partial = [item for item in items if item['state'] == 'partial']
+ measured = known + partial
+ timed = [item for item in measured if item['first_visible'] is not None]
+ rate_items = [item for item in known if item['first_visible'] is not None and item['first_visible'] > 0]
+ wait = sum(item['first_visible'] for item in timed)
+ after = sum(item['after_first'] for item in timed)
+ total_input = sum(item['input'] for item in measured)
+ total_output = sum(item['output'] for item in measured)
+ hit_input = sum(item['input'] for item in known)
+ total_cached = sum(item['cached'] for item in known)
+ in_wait = sum(item['first_visible'] for item in rate_items)
+ out_items = [item for item in timed if item['output'] > 1 and item['after_first'] > 0]
+ out_time = sum(item['after_first'] for item in out_items)
+ result.append({'session': session, 'ips': sorted({item['ip'] or '未知来源' for item in items}),
+ 'requests': len(items), 'known': len(known), 'partial': len(partial), 'timed': len(timed),
+ 'input': total_input if measured else None, 'output': total_output if measured else None,
+ 'cached': total_cached if known else None,
+ 'uncached_input': sum(item['uncached_input'] for item in known) if known else None,
+ 'hit_rate': total_cached / hit_input if hit_input else None,
+ 'first_visible': wait if timed else None, 'after_first': after if timed else None,
+ 'first_share': wait / (wait + after) if timed and wait + after > 0 else None,
+ 'input_per_wait': sum(item['input'] - item['cached'] for item in rate_items) / in_wait if in_wait else None,
+ 'output_per_after_first': sum(item['output'] - 1 for item in out_items) / out_time if out_time else None,
+ 'last_finished': max(item['finished'] for item in items)})
+ return sorted(result, key=lambda item: item['last_finished'], reverse=True)
+
+
+def _ordered_page(items, field, direction, offset, limit, identity):
+ def value(item):
+ result = item.get(field)
+ if field == 'ips':
+ known = [address for address in result if address != '未知来源'] if result else []
+ result = ', '.join(known) if known else None
+ elif field == 'ip' and result is not None:
+ try:
+ address = ipaddress.ip_address(result)
+ return address.version, int(address)
+ except ValueError:
+ return None
+ elif field == 'state':
+ labels = {'known': '已计量', 'partial': '部分用量', 'missing': '未计量', 'failed': '失败', 'interrupted': '中断'}
+ return labels.get(result, '未知'), item.get('status') or 0
+ if isinstance(result, str):
+ result = result.casefold()
+ return result
+
+ present = [item for item in items if value(item) is not None]
+ missing = [item for item in items if value(item) is None]
+ present.sort(key=lambda item: (value(item), item[identity]), reverse=direction == 'desc')
+ missing.sort(key=lambda item: item[identity])
+ ordered = present + missing
+ offset = min(offset, ((len(ordered) - 1) // limit) * limit) if ordered else 0
+ return ordered[offset:offset + limit], offset
+
+
+def _check_database(db):
+ if db.execute('PRAGMA user_version').fetchone()[0] != SCHEMA:
+ raise ValueError('STATS_DATABASE_SCHEMA_MISMATCH')
+ try:
+ marker = db.execute("SELECT value FROM stats_meta WHERE key='application'").fetchone()
+ except sqlite3.Error as error:
+ raise ValueError('STATS_DATABASE_SCHEMA_MISMATCH') from error
+ if marker != (IDENTITY,):
+ raise ValueError('STATS_DATABASE_SCHEMA_MISMATCH')
+
+
+def _active_snapshot(db, now, source=None):
+ source_filter = (" AND EXISTS (SELECT 1 FROM stats_meta m WHERE m.key='source:' || r.id AND m.value=?)"
+ if source is not None else '')
+ params = (now - 15, now, source) if source is not None else (now - 15, now)
+ samples = list(db.execute('''SELECT r.last_seen,s.seen FROM runs r
+ LEFT JOIN active_samples s ON s.run_id=r.id WHERE r.ended IS NULL AND r.last_seen>? AND r.last_seen<=?''' + source_filter,
+ params))
+ ready = bool(samples) and all(seen is not None and 0 <= now - seen < 3 for _, seen in samples)
+ rows = list(db.execute('''SELECT a.client_ip,a.session_id,a.started,a.first_visible
+ FROM active_requests a JOIN runs r ON a.run_id=r.id
+ WHERE r.ended IS NULL AND r.last_seen>? AND r.last_seen<=?''' + source_filter + ' ORDER BY a.started', params)) if ready else []
+ current = [{'ip': ip, 'session': session, 'started': started, 'elapsed': max(0, now - started),
+ 'first_visible': first, 'input': None, 'output': None, 'cached': None}
+ for ip, session, started, first in rows]
+ return {'as_of': now, 'collector_live': bool(samples), 'active': current, 'active_ready': ready,
+ 'active_sampled_at': min((seen for _, seen in samples), default=None) if ready else None}
+
+
+def active_report(path, *, now=None, source=None):
+ now = time.time() if now is None else float(now)
+ uri = Path(path).resolve().as_uri() + '?mode=ro'
+ with closing(sqlite3.connect(uri, uri=True, timeout=1)) as db:
+ db.execute('PRAGMA query_only=ON')
+ db.execute('BEGIN')
+ _check_database(db)
+ return _active_snapshot(db, now, source)
+
+
+def report(path, window='24h', *, now=None, request_offset=0, request_limit=100, session_offset=0,
+ session_limit=None, request_sort='finished', request_dir='desc',
+ session_sort='last_finished', session_dir='desc', snapshot_at=None, snapshot_id=None, source=None):
+ if window not in ('today', '24h', '7d', 'total'):
+ raise ValueError('INVALID_STATS_WINDOW')
+ if session_limit is None:
+ session_limit = request_limit
+ if (type(request_offset) is not int or request_offset < 0 or type(session_offset) is not int or session_offset < 0 or
+ type(request_limit) is not int or not 1 <= request_limit <= 200 or
+ type(session_limit) is not int or not 1 <= session_limit <= 200 or
+ not isinstance(request_sort, str) or request_sort not in REQUEST_SORTS or
+ not isinstance(session_sort, str) or session_sort not in SESSION_SORTS or
+ request_dir not in ('asc', 'desc') or session_dir not in ('asc', 'desc')):
+ raise ValueError('INVALID_STATS_PAGE')
+ now = time.time() if now is None else float(now)
+ if ((snapshot_at is None) != (snapshot_id is None) or
+ snapshot_id is not None and (type(snapshot_id) is not int or snapshot_id < 0) or
+ snapshot_at is not None and (type(snapshot_at) not in (int, float) or
+ not math.isfinite(snapshot_at) or not 0 < snapshot_at <= now)):
+ raise ValueError('INVALID_STATS_SNAPSHOT')
+ uri = Path(path).resolve().as_uri() + '?mode=ro'
+ with closing(sqlite3.connect(uri, uri=True, timeout=1)) as db:
+ db.execute('PRAGMA query_only=ON')
+ db.execute('BEGIN')
+ _check_database(db)
+ all_runs = list(db.execute('SELECT started,last_seen,ended,dropped FROM runs'))
+ runs = (all_runs if source is None else list(db.execute('''
+ SELECT started,last_seen,ended,dropped FROM runs r
+ WHERE EXISTS (SELECT 1 FROM stats_meta m WHERE m.key='source:' || r.id AND m.value=?)''', (source,))))
+ overload_runs = list(db.execute('''SELECT runs.started,runs.last_seen,runs.ended,stats_meta.value
+ FROM runs LEFT JOIN stats_meta ON stats_meta.key='overload:' || runs.id'''))
+ since = min((r[0] for r in all_runs), default=now)
+ if snapshot_at is None:
+ snapshot_at = now
+ snapshot_id = db.execute('SELECT COALESCE(MAX(id),0) FROM requests').fetchone()[0]
+ midnight = datetime.fromtimestamp(now, TZ).replace(hour=0, minute=0, second=0, microsecond=0).timestamp()
+ start = {'today': midnight, '24h': now - 86400, '7d': now - 7 * 86400,
+ 'total': since}[window]
+ start = min(start, now)
+ snapshot_midnight = datetime.fromtimestamp(snapshot_at, TZ).replace(hour=0, minute=0, second=0, microsecond=0).timestamp()
+ snapshot_start = min(snapshot_at, {'today': snapshot_midnight, '24h': snapshot_at - 86400,
+ '7d': snapshot_at - 7 * 86400, 'total': since}[window])
+ if partial_schema_available(db):
+ query = '''SELECT r.finished,r.client_ip,CASE WHEN p.request_id IS NULL THEN r.state ELSE 'partial' END,
+ COALESCE(p.input_tokens,r.input_tokens),COALESCE(p.output_tokens,r.output_tokens),r.cached_tokens,r.elapsed,
+ r.session_id,r.first_visible,r.http_status,r.id FROM requests r
+ LEFT JOIN stats_partial_usage p ON p.request_id=r.id WHERE r.finished>=? AND r.finished ORDER BY r.finished'''
+ else:
+ query = '''SELECT finished,client_ip,state,input_tokens,output_tokens,cached_tokens,elapsed,
+ session_id,first_visible,http_status,id FROM requests WHERE finished>=? AND finished ORDER BY finished'''
+ rows = list(db.execute(query, (min(start, snapshot_start, now - 14 * 86400), now)))
+ live_active = _active_snapshot(db, now, source)
+ selected, grouped, series_rows, day_rows = [], {}, {}, {}
+ details, snapshot_details = [], []
+ for row in rows:
+ stamp = row[0]
+ view = None
+ if stamp >= start:
+ selected.append(row[2:7])
+ grouped.setdefault(row[1], []).append(row[2:7])
+ view = _request_view(row)
+ details.append(view)
+ if snapshot_start <= stamp < snapshot_at and row[-1] <= snapshot_id:
+ snapshot_details.append(view if view is not None else _request_view(row))
+ if stamp >= now - 86400:
+ bucket = int(stamp // BUCKET_SECONDS) * BUCKET_SECONDS
+ series_rows.setdefault(bucket, []).append(row)
+ if stamp >= midnight - 13 * 86400:
+ day = int((stamp + 8 * 3600) // 86400) * 86400 - 8 * 3600
+ day_rows.setdefault(day, []).append(row[2:7])
+ totals = _counts(selected)
+ seconds = max(0.0, now - start)
+ covered, dropped, unclean = _coverage(runs, start, now, now)
+ totals['window_tps'] = totals['output'] / seconds if seconds else None
+ totals['unknown_source'] = len(grouped.get(None, []))
+ totals['covered_seconds'] = covered
+ totals['seconds'] = seconds
+ totals['dropped'] = dropped
+ totals['unclean_runs'] = unclean
+ overload_count, overload_tracked, overload_untracked = 0, 0, 0
+ for left, seen, ended, value in overload_runs:
+ live = ended is None and 0 <= now - seen < 15
+ right = ended if ended is not None else (now if live else seen)
+ if left < now and right > start:
+ try:
+ if value is None or not value.isascii() or not value.isdigit():
+ raise ValueError()
+ overload_count += int(value)
+ overload_tracked += 1
+ except (ValueError, TypeError, OverflowError):
+ overload_untracked += 1
+ totals['overload_rejected'] = overload_count
+ totals['overload_tracked_runs'] = overload_tracked
+ totals['overload_untracked_runs'] = overload_untracked
+ classified = [item for item in details if item['state'] in ('known', 'partial', 'failed', 'interrupted')]
+ successes = [item for item in classified if item['state'] in ('known', 'partial') and item['status'] is not None
+ and 200 <= item['status'] < 300]
+ timed = [item for item in successes if item['first_visible'] is not None]
+ first_values = [item['first_visible'] for item in timed]
+ prefill_items = [item for item in timed if item['state'] == 'known' and item['first_visible'] > 0]
+ decode_items = [item for item in timed if item['output'] > 1 and item['after_first'] > 0]
+ prefill_time = sum(item['first_visible'] for item in prefill_items)
+ decode_time = sum(item['after_first'] for item in decode_items)
+ duration_values = [item['elapsed'] for item in successes]
+ totals.update(successes=len(successes), classified=len(classified),
+ success_rate=len(successes) / len(classified) if classified else None,
+ requests_per_second=len(details) / seconds if seconds else None,
+ first_p50=_percentile(first_values, .50), first_p95=_percentile(first_values, .95),
+ first_mean=sum(first_values) / len(first_values) if first_values else None,
+ first_samples=len(first_values),
+ prefill_estimate_tps=sum(item['input'] - item['cached'] for item in prefill_items) / prefill_time if prefill_time else None,
+ prefill_samples=len(prefill_items),
+ decode_estimate_tps=sum(item['output'] - 1 for item in decode_items) / decode_time if decode_time else None,
+ decode_samples=len(decode_items), elapsed_p50=_percentile(duration_values, .50),
+ elapsed_p95=_percentile(duration_values, .95), elapsed_samples=len(duration_values),
+ identified_sessions=len({item['session'] for item in details if item['session']}),
+ unknown_session=sum(1 for item in details if not item['session']))
+ totals['complete'] = (bool(runs) and seconds > 0 and covered >= seconds - COVERAGE_EPSILON and dropped == 0 and unclean == 0 and
+ totals['partial'] == 0 and totals['missing'] == 0 and totals['interrupted'] == 0 and totals['failed'] == 0)
+ if source is not None:
+ # A recorder heartbeat does not establish that every public entry routes through it.
+ totals['complete'] = False
+ clients = []
+ for ip, items in grouped.items():
+ item = _counts(items)
+ item['ip'] = ip
+ item['window_tps'] = item['output'] / seconds if seconds else None
+ clients.append(item)
+ clients.sort(key=lambda x: (-x['output'], x['ip'] or ''))
+ first_bucket = int((now - 86400) // BUCKET_SECONDS) * BUCKET_SECONDS
+ last_bucket = int(now // BUCKET_SECONDS) * BUCKET_SECONDS
+ series = []
+ for bucket in range(first_bucket, last_bucket + 1, BUCKET_SECONDS):
+ a, b = max(bucket, now - 86400), min(bucket + BUCKET_SECONDS, now)
+ bucket_rows = series_rows.get(bucket, [])
+ counts = _counts([row[2:7] for row in bucket_rows])
+ bucket_requests = [_request_view(row) for row in bucket_rows]
+ hits = [item['hit_rate'] for item in bucket_requests if item['hit_rate'] is not None]
+ timed = [item for item in bucket_requests if item['state'] in ('known', 'partial') and
+ item['status'] is not None and 200 <= item['status'] < 300 and
+ item['first_visible'] is not None]
+ prefill = [item for item in timed if item['state'] == 'known' and item['first_visible'] > 0]
+ decode = [item for item in timed if item['output'] > 1 and item['after_first'] > 0]
+ prefill_tokens = sum(item['input'] - item['cached'] for item in prefill)
+ prefill_seconds = sum(item['first_visible'] for item in prefill)
+ decode_tokens = sum(item['output'] - 1 for item in decode)
+ decode_seconds = sum(item['after_first'] for item in decode)
+ coverage, lost, unclean = _coverage(runs, a, b, now)
+ counts.update(start=a, end=b, covered_seconds=coverage, dropped=lost, unclean_runs=unclean,
+ mean_request_hit_rate=sum(hits) / len(hits) if hits else None,
+ request_hit_sum=sum(hits), request_hit_samples=len(hits),
+ prefill_estimate_tps=prefill_tokens / prefill_seconds if prefill_seconds else None,
+ prefill_tokens=prefill_tokens, prefill_seconds=prefill_seconds, prefill_samples=len(prefill),
+ decode_estimate_tps=decode_tokens / decode_seconds if decode_seconds else None,
+ decode_tokens=decode_tokens, decode_seconds=decode_seconds, decode_samples=len(decode),
+ complete=bool(runs) and b > a and coverage >= b - a - COVERAGE_EPSILON and lost == 0 and unclean == 0 and
+ counts['partial'] == 0 and counts['missing'] == 0 and counts['interrupted'] == 0 and counts['failed'] == 0)
+ if source is not None:
+ counts['complete'] = False
+ series.append(counts)
+ days = []
+ for day in range(13, -1, -1):
+ a, b = midnight - day * 86400, min(midnight - (day - 1) * 86400, now)
+ counts = _counts(day_rows.get(a, []))
+ coverage, lost, unclean = _coverage(runs, a, b, now)
+ counts.update(date=datetime.fromtimestamp(a, TZ).strftime('%Y-%m-%d'), start=a, end=b,
+ covered_seconds=coverage, dropped=lost, unclean_runs=unclean,
+ complete=bool(runs) and b > a and coverage >= b - a - COVERAGE_EPSILON and lost == 0 and unclean == 0 and
+ counts['partial'] == 0 and counts['missing'] == 0 and counts['interrupted'] == 0 and counts['failed'] == 0)
+ if source is not None:
+ counts['complete'] = False
+ days.append(counts)
+ snapshot_covered, snapshot_dropped, snapshot_unclean = _coverage(runs, snapshot_start, snapshot_at, now)
+ snapshot_complete = (bool(runs) and snapshot_at > snapshot_start and
+ snapshot_covered >= snapshot_at - snapshot_start - COVERAGE_EPSILON and
+ snapshot_dropped == 0 and snapshot_unclean == 0 and
+ all(item['state'] == 'known' for item in snapshot_details))
+ if source is not None:
+ snapshot_complete = False
+ sessions = _session_views(snapshot_details)
+ recent, request_offset = _ordered_page(snapshot_details, request_sort, request_dir, request_offset, request_limit, 'id')
+ session_page, session_offset = _ordered_page(sessions, session_sort, session_dir, session_offset, session_limit, 'session')
+ return {'schema': SCHEMA, 'as_of': now, 'since': since if all_runs else None,
+ 'window': window, 'collector_live': live_active['collector_live'], 'totals': totals,
+ 'clients': clients, 'series_24h': series, 'days': days, 'active': live_active['active'],
+ 'active_ready': live_active['active_ready'], 'active_sampled_at': live_active['active_sampled_at'],
+ 'snapshot_at': snapshot_at, 'snapshot_id': snapshot_id, 'snapshot_complete': snapshot_complete,
+ 'requests': recent, 'request_count': len(snapshot_details), 'request_offset': request_offset, 'request_limit': request_limit,
+ 'sessions': session_page, 'session_count': len(sessions), 'session_offset': session_offset, 'session_limit': session_limit,
+ 'session_unknown_count': sum(1 for item in snapshot_details if not item['session'])}
diff --git a/deploy_stats/session.py b/deploy_stats/session.py
new file mode 100644
index 0000000..aa45b4a
--- /dev/null
+++ b/deploy_stats/session.py
@@ -0,0 +1,116 @@
+"""Derive the gateway's session fingerprint without persisting request bodies."""
+import hashlib
+import json
+import threading
+
+MAX_BODY = 1024 * 1024
+MAX_PENDING = 32 * 1024 * 1024
+
+
+class BodyBudget:
+ def __init__(self, limit=MAX_PENDING):
+ self.limit = limit
+ self.used = 0
+ self.lock = threading.Lock()
+
+ def reserve(self, size):
+ with self.lock:
+ if size < 0 or self.used + size > self.limit:
+ return False
+ self.used += size
+ return True
+
+ def release(self, size):
+ with self.lock:
+ self.used -= size
+
+
+BUDGET = BodyBudget()
+
+
+def key_digest(headers):
+ auth = headers.get_all('Authorization', [])
+ if len(auth) != 1 or not isinstance(auth[0], str):
+ return None
+ fields = auth[0].split()
+ if len(fields) != 2 or fields[0].lower() != 'bearer' or len(fields[1]) > 256:
+ return None
+ try:
+ return hashlib.sha256(fields[1].encode()).hexdigest()
+ except UnicodeError:
+ return None
+
+
+def fingerprint(digest, seed):
+ try:
+ return hashlib.sha256((digest + '\0' + seed).encode()).hexdigest()
+ except UnicodeError:
+ return None
+
+
+def explicit_session(headers):
+ """Return whether an explicit header exists and its fingerprint if valid."""
+ digest = key_digest(headers)
+ for name in ('x-session-affinity', 'x-deepseek-harness-session-id', 'x-session-id'):
+ values = headers.get_all(name, [])
+ if values:
+ if not digest or len(values) != 1 or not isinstance(values[0], str) or not 0 < len(values[0]) <= 256:
+ return True, None
+ return True, fingerprint(digest, 'session:' + values[0])
+ return False, None
+
+
+class SessionBodyCapture:
+ """Bounded temporary buffering for the gateway's JSON fallback identity."""
+
+ def __init__(self, digest, *, budget=BUDGET, max_body=MAX_BODY):
+ self.digest = digest
+ self.budget = budget
+ self.max_body = max_body
+ self.buffer = bytearray()
+ self.reserved = 0
+ self.disabled = not bool(digest)
+
+ def feed(self, data):
+ if self.disabled:
+ return
+ if len(self.buffer) + len(data) > self.max_body or not self.budget.reserve(len(data)):
+ self.close()
+ return
+ self.reserved += len(data)
+ try:
+ self.buffer.extend(data)
+ except Exception:
+ self.close()
+
+ def finish(self):
+ if self.disabled:
+ return None
+ try:
+ value = json.loads(self.buffer)
+ if not isinstance(value, dict):
+ return None
+ cache_key = value.get('prompt_cache_key')
+ if isinstance(cache_key, str) and cache_key:
+ seed = 'session:' + cache_key
+ elif isinstance(value.get('messages'), list):
+ first = next((item for item in value['messages']
+ if isinstance(item, dict) and item.get('role') == 'user'), None)
+ if first is None or not isinstance(first.get('content'), (str, list)):
+ return None
+ seed = 'first-user:' + json.dumps(first['content'], sort_keys=True, ensure_ascii=False,
+ separators=(',', ':'))
+ else:
+ return None
+ return fingerprint(self.digest, seed)
+ except (ValueError, TypeError, UnicodeError, RecursionError, MemoryError):
+ return None
+ finally:
+ self.close()
+
+ def close(self):
+ if self.reserved:
+ self.budget.release(self.reserved)
+ self.reserved = 0
+ self.buffer.clear()
+ self.disabled = True
diff --git a/deploy_stats/static/app.css b/deploy_stats/static/app.css
new file mode 100644
index 0000000..48eacad
--- /dev/null
+++ b/deploy_stats/static/app.css
@@ -0,0 +1,16 @@
+:root{font-family:Inter,"Segoe UI","Microsoft YaHei",sans-serif;color:#1a2841;background:#f5f7fb;font-synthesis:none}*{box-sizing:border-box}body{margin:0}.app-shell{min-height:100vh;display:grid;grid-template-columns:216px minmax(0,1fr)}.sidebar{background:#17243c;color:#c4cde0;display:flex;flex-direction:column;padding:26px 14px 20px}.brand{display:flex;align-items:center;gap:10px;padding:0 8px 40px}.brand-mark{width:36px;height:36px;display:grid;place-items:center;color:#e7fbf8;background:#2d8f89;border-radius:10px;font-size:23px}.brand strong{display:block;color:#fff;font-size:16px;letter-spacing:.2px}.brand small,.sidebar-label{display:block;font-size:9px;letter-spacing:1.45px;color:#8a9bb6;margin-top:4px}.sidebar-label{padding:0 11px;margin-bottom:12px}.sidebar nav{display:grid;gap:5px}.sidebar nav a{display:flex;gap:12px;align-items:center;color:#b5c2d8;font-size:13px;font-weight:600;text-decoration:none;padding:11px 12px;border-radius:7px}.sidebar nav a span{font-size:17px;width:18px;text-align:center}.sidebar nav a:hover,.sidebar nav .nav-active{color:#fff;background:#293a55}.sidebar-bottom{display:flex;align-items:center;gap:10px;margin-top:auto;padding:14px 8px;border-top:1px solid #33435a}.sidebar-bottom .shield{font-size:22px;color:#73c8b8}.sidebar-bottom strong,.sidebar-bottom small{display:block;font-size:12px}.sidebar-bottom small{font-size:10px;color:#8fa0b9;margin-top:4px}.topbar{height:62px;background:#fff;border-bottom:1px solid #e7ebf2;display:flex;align-items:center;justify-content:space-between;padding:0 42px}.breadcrumb{font-size:12px;color:#8b97a9}.breadcrumb span{padding:0 10px;color:#ccd3df}.top-right{display:flex;align-items:center;gap:16px}.local-pill,.outline-pill{font-size:10px;font-weight:700;letter-spacing:.7px;border:1px solid #e1e7ef;border-radius:6px;padding:7px 9px;color:#8290a2;white-space:nowrap}.local-pill i{display:inline-block;width:6px;height:6px;border-radius:50%;background:#2aab87;margin-right:5px}.refresh{border:1px solid #e1e7ef;background:#fff;color:#586b85;width:30px;height:30px;border-radius:7px;font-size:19px;cursor:pointer}.refresh:hover{background:#f5f8fb}.content{max-width:1680px;margin:0 auto;padding:32px 42px 18px}.page-title{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-bottom:34px}.eyebrow,.section-kicker{font-size:9px;font-weight:800;letter-spacing:1.55px;color:#8190a4}.page-title h1{font-size:26px;letter-spacing:-.4px;margin:8px 0 7px;color:#17253d}.page-title p,.panel-head p{font-size:12px;color:#8a97aa;margin:0}.status{display:flex;gap:10px;align-items:center;border:1px solid #e0e8ee;background:white;border-radius:9px;padding:10px 13px;min-width:160px}.status strong,.status small{display:block}.status strong{font-size:11px;color:#516378}.status small{font-size:10px;color:#9aa6b7;margin-top:3px}.status-dot{width:8px;height:8px;border-radius:50%;background:#e2ad50;box-shadow:0 0 0 4px #fbf2df}.status-dot.live{background:#36ad8e;box-shadow:0 0 0 4px #e5f6f0}.toolbar{display:flex;justify-content:space-between;align-items:end;gap:15px;margin-bottom:15px}.toolbar h2,.panel-head h2{font-size:16px;margin:5px 0 0;letter-spacing:-.1px}.segmented{background:#e9eef5;padding:3px;border-radius:8px;display:flex;gap:2px}.segmented button{border:0;background:transparent;padding:7px 12px;font-size:11px;font-weight:600;color:#8090a5;border-radius:6px;cursor:pointer;white-space:nowrap}.segmented button.is-active{background:white;color:#293c57;box-shadow:0 1px 4px #233a5a19}.notice{border:1px solid #d8e8ef;background:#eff7f9;color:#4a6e7a;border-radius:7px;padding:10px 13px;font-size:11px;line-height:1.6;margin-bottom:14px}.notice.warning{background:#fff9ed;border-color:#f5e6c4;color:#8a6a2e}.cards{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;margin-bottom:17px}.metric,.panel{background:white;border:1px solid #e6ebf1;border-radius:10px;box-shadow:0 3px 12px #2c476407}.metric{min-height:120px;padding:16px 18px}.metric-top{display:flex;align-items:center;justify-content:space-between;color:#708099;font-size:12px;font-weight:600}.metric-icon{width:28px;height:28px;display:grid;place-items:center;border-radius:8px;font-size:16px}.blue{color:#477bdb;background:#edf3ff}.violet{color:#8562cd;background:#f2edfb}.green{color:#299c89;background:#e6f6f2}.orange{color:#d79243;background:#fff3e3}.metric strong{display:block;font-size:24px;letter-spacing:-.7px;color:#1a2c45;margin:9px 0 5px}.metric small{color:#96a2b2;font-size:10px}.panel{margin-bottom:17px;min-width:0}.panel-head{display:flex;justify-content:space-between;align-items:flex-start;gap:15px;padding:20px 21px 15px}.panel-head p{margin-top:7px;line-height:1.5}.chart-lanes{padding:0 21px}.lane{display:grid;grid-template-columns:100px minmax(0,1fr);align-items:center;border-top:1px solid #f0f3f7;min-height:72px}.lane-label{display:grid;grid-template-columns:8px 1fr;align-items:center;gap:5px 7px;color:#53647d;font-size:11px;font-weight:700}.lane-label small{grid-column:2;font-size:10px;color:#96a3b3;font-weight:500}.dot{width:7px;height:7px;border-radius:50%;display:inline-block}.dot.input{background:#5388e7}.dot.output{background:#9c77df}.dot.cached{background:#31aa99}.lane svg{width:100%;height:68px;overflow:visible}.lane svg .grid{stroke:#eff2f7;stroke-width:1}.lane svg .input{stroke:#5388e7;fill:none;stroke-width:2.2;vector-effect:non-scaling-stroke}.lane svg .output{stroke:#9c77df;fill:none;stroke-width:2.2;vector-effect:non-scaling-stroke}.lane svg .cached{stroke:#31aa99;fill:none;stroke-width:2.2;vector-effect:non-scaling-stroke}.lane svg circle.input{fill:#5388e7}.lane svg circle.output{fill:#9c77df}.lane svg circle.cached{fill:#31aa99}.chart-axis{display:flex;justify-content:space-between;color:#a7b1bf;font-size:10px;padding:5px 21px 13px 121px}.panel-foot{border-top:1px solid #f0f3f7;padding:11px 21px;color:#9aa6b6;font-size:10px;line-height:1.5}.lower-grid{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(370px,1fr);gap:17px}.table-wrap{overflow-x:auto}.table-panel table{width:100%;border-collapse:collapse;text-align:right;white-space:nowrap}.table-panel th{padding:10px 12px;color:#9ba8b9;background:#f8fafc;font-size:10px;font-weight:600}.table-panel td{padding:12px;color:#53657e;font-size:11px;border-bottom:1px solid #f1f4f7}.table-panel th:first-child,.table-panel td:first-child{text-align:left;padding-left:21px}.table-panel td:first-child{font-weight:650;color:#293c57}.table-panel th:last-child,.table-panel td:last-child{padding-right:21px}.table-panel .empty{text-align:center!important;color:#a0acba!important;font-weight:400!important;padding:28px!important}.daily .table-wrap{max-height:340px;overflow:auto}footer{text-align:center;color:#aab4c2;font-size:10px;padding:7px 0 15px}button:focus-visible,a:focus-visible{outline:2px solid #3a8cca;outline-offset:2px}@media(max-width:1100px){.lower-grid{grid-template-columns:1fr}.daily .table-wrap{max-height:300px}}@media(max-width:850px){.app-shell{grid-template-columns:1fr}.sidebar{display:none}.topbar{padding:0 20px}.content{padding:26px 20px}.cards{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:520px){.topbar{height:54px}.page-title{align-items:flex-start;flex-direction:column;margin-bottom:24px}.status{min-width:0}.toolbar{align-items:flex-start;flex-direction:column}.segmented{width:100%}.segmented button{flex:1;padding:8px 4px}.metric{padding:14px;min-height:110px}.metric strong{font-size:20px}.panel-head{padding:18px 15px 13px}.chart-lanes{padding:0 15px}.lane{grid-template-columns:72px minmax(0,1fr)}.chart-axis{padding-left:87px;padding-right:15px}.outline-pill{font-size:9px}}
+.cards{grid-template-columns:repeat(5,minmax(0,1fr))}.slate{color:#56738f;background:#edf2f7}@media(max-width:1100px){.cards{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(max-width:850px){.cards{grid-template-columns:repeat(2,minmax(0,1fr))}}
+.pager{display:flex;align-items:center;justify-content:flex-end;gap:10px;margin-top:12px}.pager button{border:1px solid #dfe7ef;background:#fff;border-radius:6px;padding:5px 9px;color:#51667d;cursor:pointer}.pager button:disabled{opacity:.45;cursor:default}.pager span{white-space:nowrap}.service-cards .metric{min-height:110px}.table-panel td{font-variant-numeric:tabular-nums}
+.app-shell>main{min-width:0}@media(max-width:850px){.app-shell{grid-template-columns:minmax(0,1fr)}}
+.table-panel th[aria-sort]{cursor:pointer}.sort-control{border:0;background:transparent;color:inherit;font:inherit;padding:0;white-space:nowrap;cursor:pointer}.sort-control:after{content:' ↕';color:#aab7c8}.table-panel th[aria-sort=ascending] .sort-control:after{content:' ↑';color:#4978b9}.table-panel th[aria-sort=descending] .sort-control:after{content:' ↓';color:#4978b9}.pager label{display:flex;align-items:center;gap:5px;white-space:nowrap}.pager select{border:1px solid #dfe7ef;border-radius:6px;background:#fff;color:#51667d;padding:4px 6px;font:inherit}.pager select:focus-visible{outline:2px solid #3a8cca;outline-offset:2px}@media(max-width:520px){.pager{flex-wrap:wrap;gap:8px}}
+[data-view][hidden],.segmented[hidden]{display:none!important}.lower-grid{display:block}.sidebar{position:sticky;top:0;align-self:start;height:100vh;overflow-y:auto}@media(max-width:850px){.sidebar{display:block;position:sticky;top:0;z-index:10;height:auto;overflow-y:visible;padding:6px 10px}.sidebar .brand,.sidebar-label,.sidebar-bottom{display:none}.sidebar nav{display:flex;gap:4px;overflow-x:auto}.sidebar nav a{flex:0 0 auto;white-space:nowrap;padding:8px 10px}}
+.dot.prefill{background:#d99036}.dot.decode{background:#c26589}.lane svg .prefill{stroke:#d99036;fill:none;stroke-width:2.2;vector-effect:non-scaling-stroke}.lane svg .decode{stroke:#c26589;fill:none;stroke-width:2.2;vector-effect:non-scaling-stroke}.lane svg circle.prefill{fill:#d99036}.lane svg circle.decode{fill:#c26589}.lane-label small{overflow-wrap:anywhere}
+.lane{grid-template-columns:155px minmax(0,1fr)}.chart-axis{padding-left:176px}@media(max-width:520px){.lane{grid-template-columns:125px minmax(0,1fr)}.chart-axis{padding-left:140px}}
+.lane svg .partial{opacity:.58;stroke-dasharray:4 3}.lane svg circle.partial{fill:#fff}
+.pager span{white-space:normal}.snapshot-hint{display:block;margin-top:6px}
+.cards.phase-cards{grid-template-columns:repeat(4,minmax(0,1fr))}.phase-cards .metric{min-width:0;padding:14px}.phase-cards .metric-top{font-size:11px}.phase-cards .metric strong{font-size:18px;overflow-wrap:anywhere}.phase-note{font-size:11px;line-height:1.5;color:#8796a9;margin:-7px 0 18px}@media(max-width:520px){.cards.phase-cards{grid-template-columns:repeat(2,minmax(0,1fr))}}
+.phase-heading{margin:2px 0 8px;color:#8190a4;font-size:10px;font-weight:800;letter-spacing:1.25px}.cards.phase-cards.decode-cards{grid-template-columns:repeat(3,minmax(0,1fr))}@media(max-width:850px){.cards.phase-cards.prefill-cards,.cards.phase-cards.decode-cards{grid-template-columns:repeat(2,minmax(0,1fr))}}
+.telemetry-scope{display:flex;align-items:center;gap:10px;margin:0 0 14px;color:#8190a4;font-size:12px;flex-wrap:wrap}.telemetry-scope label{font-weight:700;color:#475569}.telemetry-scope select{border:1px solid #dce3ec;border-radius:7px;background:#fff;padding:6px 9px;color:#253449;font:inherit}
+.phase-cards .metric small{color:#657891;line-height:1.4}
+.trend-tooltip{position:fixed;z-index:50;max-width:min(320px,calc(100vw - 16px));padding:8px 10px;background:#17243c;color:#fff;border-radius:6px;box-shadow:0 5px 18px #17243c35;font-size:12px;line-height:1.5;white-space:pre-line;pointer-events:none}.trend-tooltip[hidden]{display:none}.trend-hover-overlay{cursor:crosshair}
+.cards.phase-cards.decode-cards{grid-template-columns:repeat(4,minmax(0,1fr))}.dot.mtp{background:#4f7de3}.lane svg .mtp{stroke:#4f7de3;fill:none;stroke-width:2.2;vector-effect:non-scaling-stroke}.lane svg circle.mtp{fill:#4f7de3}.lane svg circle.mtp.partial{fill:#fff}@media(max-width:850px){.cards.phase-cards.decode-cards{grid-template-columns:repeat(2,minmax(0,1fr))}}
diff --git a/deploy_stats/static/app.js b/deploy_stats/static/app.js
new file mode 100644
index 0000000..d8a55a5
--- /dev/null
+++ b/deploy_stats/static/app.js
@@ -0,0 +1,1118 @@
+'use strict';
+
+const $ = (id) => document.getElementById(id);
+const number = new Intl.NumberFormat('zh-CN', {maximumFractionDigits: 2});
+const integer = new Intl.NumberFormat('zh-CN');
+const clock = new Intl.DateTimeFormat('zh-CN', {timeZone: 'Asia/Shanghai', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false});
+const dateTime = new Intl.DateTimeFormat('zh-CN', {timeZone: 'Asia/Shanghai', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false});
+const snapshotTime = new Intl.DateTimeFormat('zh-CN', {timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false});
+let selected = '24h';
+let revision = 0;
+let lastSnapshot = null;
+let pendingRefresh = null;
+let activeExpiry = null;
+let pendingActive = null;
+let pendingTelemetry = null;
+let telemetrySnapshot = null;
+let telemetryExpiry = null;
+let backendExpiry = null;
+let telemetryNodePort = 'all';
+let telemetryOptionsKey = null;
+let latestActiveAsOf = 0;
+let currentView = 'overview';
+const tables = {
+ active: {selector: '#active', sort: 'elapsed', direction: 'desc', columns: ['ip', 'session', 'elapsed', 'phase', 'input', 'output', 'hit_rate', 'tps']},
+ backends: {selector: '#backends', sort: 'port', direction: 'asc', columns: ['port', 'running', 'waiting', 'workers_alive', 'admission_ready', 'input_total', 'hit_rate'], noPager: true},
+ sessions: {selector: '#sessions', sort: 'last_finished', direction: 'desc', columns: ['session', 'ips', 'requests', 'input', 'output', 'cached', 'uncached_input', 'hit_rate', 'first_visible', 'after_first', 'input_per_wait', 'output_per_after_first', 'first_share']},
+ requests: {selector: '[aria-labelledby="sessions-heading"]', sort: 'finished', direction: 'desc', columns: ['finished', 'ip', 'session', 'state', 'input', 'output', 'cached', 'uncached_input', 'hit_rate', 'first_visible', 'after_first', 'input_per_wait', 'output_per_after_first', 'first_share']},
+ clients: {selector: '#clients', sort: 'output', direction: 'desc', columns: ['ip', 'requests', 'total', 'input', 'output', 'cached', 'request_tps', 'window_tps']},
+ days: {selector: '#daily', sort: 'date', direction: 'desc', columns: ['date', 'input', 'output', 'cached']},
+};
+const textSorts = new Set(['ip', 'ips', 'session', 'state', 'date']);
+Object.values(tables).forEach((table) => { table.offset = 0; table.size = 20; });
+const views = {
+ overview: ['总览', 'OVERVIEW'], active: ['当前连接', 'LIVE REQUESTS'],
+ sessions: ['会话记录', 'SESSIONS'], clients: ['请求来源', 'CLIENTS'],
+ daily: ['每日用量', 'DAILY'], backends: ['后端状态', 'BACKENDS'],
+};
+const seconds = (value) => value === null || value === undefined ? '—' : `${number.format(value)} s`;
+const rate = (value) => value === null || value === undefined ? '—' : number.format(value);
+const speed = (value) => value === null || value === undefined ? '—' : `${rate(value)} tok/s`;
+const percent = (value) => value === null || value === undefined ? '—' : `${number.format(value * 100)}%`;
+const count = (value) => value === null || value === undefined ? '—' : integer.format(value);
+const fingerprint = (value) => value ? value.slice(0, 12) : '未知';
+const setTextIfPresent = (id, value) => { const element = $(id); if (element) element.textContent = value; };
+
+function freshTelemetry(part, now) {
+ return part && typeof part.valid_until === 'number' && Number.isFinite(part.valid_until) && now < part.valid_until ? part : null;
+}
+
+function validMtp(part, now) {
+ if (!freshTelemetry(part, now) || typeof part.sampled_at !== 'number' || !Number.isFinite(part.sampled_at) ||
+ part.sampled_at > now || !['steps', 'drafted', 'accepted', 'valid_endpoints', 'expected_endpoints'].every(
+ (key) => Number.isSafeInteger(part[key]) && part[key] >= 0) ||
+ part.accepted > part.drafted || part.valid_endpoints < 1 || part.valid_endpoints > part.expected_endpoints) return null;
+ return part;
+}
+
+function mtpSummary(data, nodes, port, now, kind) {
+ if (!data || data.available !== true || data.kind !== 'vllm-motor-telemetry') return null;
+ const groups = nodes.filter((node) => node && node.role === 'decode' && (port === 'all' || node.backend_port === port));
+ if (!groups.length || new Set(groups.map((node) => node.backend_port)).size !== groups.length) return null;
+ const parts = groups.flatMap((node) => {
+ const part = node.source_ok === true && node.backend_ready === true && node.mtp && validMtp(node.mtp[kind], now);
+ return part ? [part] : [];
+ });
+ if (!parts.length) return null;
+ const steps = parts.reduce((sum, part) => sum + part.steps, 0);
+ const drafted = parts.reduce((sum, part) => sum + part.drafted, 0);
+ const accepted = parts.reduce((sum, part) => sum + part.accepted, 0);
+ if (![steps, drafted, accepted].every(Number.isSafeInteger) || accepted > drafted) return null;
+ return {steps, drafted, accepted, rate: drafted ? accepted / drafted : null,
+ groups: parts.length, group_total: groups.length,
+ workers: parts.reduce((sum, part) => sum + part.valid_endpoints, 0),
+ workers_total: groups.reduce((sum, node) => sum + (node.expected_endpoints || 0), 0),
+ partial: parts.length < groups.length || parts.some((part) => part.partial === true)};
+}
+
+function renderMtp(data, nodes, port, now) {
+ const lifetime = mtpSummary(data, nodes, port, now, 'lifetime');
+ const interval = mtpSummary(data, nodes, port, now, 'interval');
+ setTextIfPresent('mtp-acceptance', lifetime ? percent(lifetime.rate) : '—');
+ const coverage = lifetime ? `${lifetime.groups}/${lifetime.group_total} 注册组 · ${lifetime.workers}/${lifetime.workers_total} 端点${lifetime.partial ? ' · 部分数据' : ''}` : '所选范围暂无推测解码指标';
+ const recent = interval && interval.drafted ? ` · 最近采样 ${percent(interval.rate)}(${interval.groups}/${interval.group_total} 组,${interval.workers}/${interval.workers_total} 端点${interval.partial ? ',部分数据' : ''})` : ' · 最近采样无有效草稿';
+ setTextIfPresent('mtp-detail', `${coverage}${lifetime ? recent : ''}`);
+ renderMtpHistory(data, nodes, port);
+}
+
+function telemetryValues(data, port, now) {
+ const nodes = data && data.available === true && Array.isArray(data.nodes) ? data.nodes : [];
+ const read = (value, max = Infinity) => typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= max ? value : null;
+ const motor = data && data.kind === 'vllm-motor-telemetry';
+ if (port === 'all') {
+ const coverage = Object.fromEntries(['prefill', 'decode', 'mfu', 'hbm'].map((key) =>
+ [key, {valid: 0, total: nodes.filter((node) => node && (!motor || key === 'hbm' ||
+ node.role === (key === 'mfu' ? 'prefill' : key))).length, span_seconds: null,
+ workers_valid: 0, workers_total: nodes.filter((node) => node && (!motor || key === 'hbm' ||
+ node.role === (key === 'mfu' ? 'prefill' : key))).reduce((sum, node) => sum + (node.expected_endpoints || 0), 0), partial: false}]));
+ const empty = {prefill: null, decode: null, mfu: null, hbm: null, coverage};
+ if (!nodes.length || new Set(nodes.map((node) => node && node.backend_port)).size !== nodes.length) return empty;
+ const parts = (name, timestamp, duration, requireReady = true) => {
+ return nodes.flatMap((node) => {
+ if (motor && name !== 'hbm' && node && node.role !== name) return [];
+ if (!node || node.source_ok !== true || (requireReady && node.backend_ready !== true) || node.clock_ok !== true ||
+ read(node.clock_uncertainty_seconds, 20) === null) return [];
+ const part = freshTelemetry(node[name], now);
+ if (!part || read(part[timestamp]) === null || read(part[duration], 60) === null) return [];
+ return [{part, first: part[timestamp] - part[duration] - node.clock_uncertainty_seconds,
+ last: part[timestamp] + node.clock_uncertainty_seconds}];
+ });
+ };
+ const prefill = parts('prefill', 'log_at', 'log_skew_seconds');
+ const decode = parts('decode', 'log_at', 'log_skew_seconds');
+ const hbm = parts('hbm', 'sample_at', 'span_seconds', false);
+ const recordCoverage = (key, values) => {
+ coverage[key].valid = values.length;
+ coverage[key].workers_valid = values.reduce((sum, {part}) => sum + (part.valid_endpoints || 0), 0);
+ coverage[key].partial = values.some(({part}) => part.partial === true);
+ if (values.length) coverage[key].span_seconds = Math.max(...values.map((x) => x.last)) - Math.min(...values.map((x) => x.first));
+ };
+ const sumRate = (key, values) => {
+ const valid = values.filter(({part}) => read(part.tokens_per_second) !== null);
+ recordCoverage(key, valid);
+ const total = valid.reduce((sum, {part}) => sum + part.tokens_per_second, 0);
+ return valid.length && Number.isFinite(total) ? total : null;
+ };
+ const weighted = (key, values, value, weight, limit = Infinity) => {
+ const valid = values.filter(({part}) => read(part[value], limit) !== null && read(part[weight]) > 0 &&
+ (weight !== 'device_count' || Number.isSafeInteger(part[weight])));
+ recordCoverage(key, valid);
+ const denominator = valid.reduce((sum, {part}) => sum + part[weight], 0);
+ const numerator = valid.reduce((sum, {part}) => sum + part[value] * part[weight], 0);
+ return denominator > 0 && Number.isFinite(denominator) && Number.isFinite(numerator) ? numerator / denominator : null;
+ };
+ return {prefill: sumRate('prefill', prefill), decode: sumRate('decode', decode),
+ mfu: weighted('mfu', prefill, 'mfu_estimate', 'peak_flops'),
+ hbm: weighted('hbm', hbm, 'mean_percent', 'device_count', 100), coverage};
+ }
+ const item = nodes.find((node) => node && node.backend_port === port && node.source_ok === true);
+ const usable = item && item.clock_ok === true && read(item.clock_uncertainty_seconds, 20) !== null;
+ const engineReady = usable && item.backend_ready === true;
+ const prefill = engineReady && freshTelemetry(item.prefill, now);
+ const decode = engineReady && freshTelemetry(item.decode, now);
+ const hbm = usable && freshTelemetry(item.hbm, now);
+ return {prefill: read(prefill && prefill.tokens_per_second), decode: read(decode && decode.tokens_per_second),
+ mfu: read(prefill && prefill.mfu_estimate), hbm: read(hbm && hbm.mean_percent, 100)};
+}
+
+function latestHistoricalSample(history, phase, now, bucketStart = null) {
+ if (!history || !['prefill', 'decode'].includes(phase) || !Number.isFinite(now) ||
+ !['deepinfer-log-step-approx-v1', 'vllm-counter-rate-v1'].includes(history.method) || history.bucket_seconds !== 600 ||
+ !Array.isArray(history.points) || history.points.length !== 144) return null;
+ const points = history.points;
+ if (points.some((point, i) => !point || !Number.isSafeInteger(point.start) || point.start < 0 ||
+ point.end !== point.start + 600 || point.end > now || (i && point.start !== points[i - 1].start + 600))) return null;
+ if (history.aggregate && (!Number.isSafeInteger(history.group_count) || history.group_count <= 0)) return null;
+ for (let i = points.length - 1; i >= 0; i--) {
+ const point = points[i], value = point[`${phase}_tps`], samples = point[`${phase}_samples`];
+ if (bucketStart !== null && point.start !== bucketStart) continue;
+ const groups = history.aggregate ? point[`${phase}_groups`] : 1;
+ const total = history.aggregate ? (history[`${phase}_group_count`] ?? history.group_count) : 1;
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0 &&
+ Number.isSafeInteger(samples) && samples > 0 && Number.isSafeInteger(groups) && groups > 0 && groups <= total) {
+ return {value, samples, start: point.start, end: point.end, groups, total, partial: point[`${phase}_partial`] === true};
+ }
+ }
+ return null;
+}
+
+function latestGroupHistory(data, nodes, port, phase, now) {
+ if (!data || data.available !== true || data.history_available !== true || !Array.isArray(nodes) || !nodes.length ||
+ new Set(nodes.map((node) => node && node.backend_port)).size !== nodes.length) return null;
+ const selected = (port === 'all' ? nodes : nodes.filter((node) => node && node.backend_port === port))
+ .filter((node) => data.kind !== 'vllm-motor-telemetry' || node.role === phase);
+ if (!selected.length || (port !== 'all' && selected.length !== 1)) return null;
+ const candidates = selected.flatMap((node) => {
+ if (!node || node.source_ok !== true || !node.history || node.history.aggregate) return [];
+ const value = latestHistoricalSample(node.history, phase, now);
+ return value ? [value] : [];
+ });
+ if (!candidates.length) return null;
+ const newest = Math.max(...candidates.map((value) => value.start));
+ const matching = candidates.filter((value) => value.start === newest);
+ const value = matching.reduce((sum, point) => sum + point.value, 0);
+ if (!Number.isFinite(value)) return null;
+ return {value, start: newest, end: newest + 600, groups: matching.length,
+ total: selected.length, samples: port === 'all' ? 1 : matching[0].samples,
+ partial: matching.some((point) => point.partial)};
+}
+
+function historicalPrefillMfu(nodes, port, sample, now) {
+ if (!sample || !Array.isArray(nodes)) return null;
+ let work = 0, peak = 0, groups = 0;
+ nodes.forEach((node) => {
+ if (!node || (port !== 'all' && node.backend_port !== port) || node.source_ok !== true ||
+ node.clock_ok !== true || typeof node.clock_uncertainty_seconds !== 'number' ||
+ !Number.isFinite(node.clock_uncertainty_seconds) || node.clock_uncertainty_seconds < 0 ||
+ node.clock_uncertainty_seconds > 20) return;
+ const point = latestHistoricalSample(node.history, 'prefill', now, sample.start);
+ const hbm = freshTelemetry(node.hbm, now), assumptions = node.assumptions;
+ if (!point || point.end !== sample.end || !hbm || !Number.isSafeInteger(hbm.device_count) ||
+ hbm.device_count < 1 || hbm.device_count > 64 || !assumptions ||
+ typeof assumptions.prefill_flops_per_token !== 'number' || !Number.isFinite(assumptions.prefill_flops_per_token) ||
+ assumptions.prefill_flops_per_token <= 0 || typeof assumptions.peak_flops_per_device !== 'number' ||
+ !Number.isFinite(assumptions.peak_flops_per_device) || assumptions.peak_flops_per_device <= 0) return;
+ const numerator = point.value * assumptions.prefill_flops_per_token;
+ const denominator = hbm.device_count * assumptions.peak_flops_per_device;
+ if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator <= 0) return;
+ work += numerator; peak += denominator; groups++;
+ });
+ const value = work / peak;
+ return groups && Number.isFinite(value) && value >= 0 ? {...sample, value, groups} : null;
+}
+
+function renderTelemetry(data) {
+ telemetrySnapshot = data;
+ if (telemetryExpiry) clearTimeout(telemetryExpiry);
+ telemetryExpiry = null;
+ const now = Date.now() / 1000;
+ const motor = data && data.kind === 'vllm-motor-telemetry';
+ const nodes = data && data.available === true && Array.isArray(data.nodes) ? data.nodes.filter((item) => item && typeof item === 'object') : [];
+ if (telemetryNodePort !== 'all' && !nodes.some((item) => item.backend_port === telemetryNodePort)) telemetryNodePort = 'all';
+ const select = $('telemetry-node');
+ if (select) {
+ const key = JSON.stringify(nodes.map((item) => [item.backend_port, item.label]));
+ if (key !== telemetryOptionsKey) {
+ select.replaceChildren();
+ const all = document.createElement('option'); all.value = 'all'; all.textContent = '跨设备组(全部)'; select.appendChild(all);
+ nodes.forEach((item) => {
+ const option = document.createElement('option'); option.value = String(item.backend_port);
+ option.textContent = item.label || '未知实例'; select.appendChild(option);
+ });
+ telemetryOptionsKey = key;
+ }
+ select.value = String(telemetryNodePort);
+ }
+ const values = telemetryValues(data, telemetryNodePort, now);
+ renderMtp(data, nodes, telemetryNodePort, now);
+ const selectedNode = telemetryNodePort === 'all' ? combinedDeviceHistory(data, nodes) : nodes.find((item) => item.backend_port === telemetryNodePort);
+ renderDeviceHistory(data, selectedNode);
+ const prefillSample = selectedNode && freshTelemetry(selectedNode.prefill, now);
+ const decodeSample = selectedNode && freshTelemetry(selectedNode.decode, now);
+ const measured = (value) => value === null || value === undefined ? '—' : speed(value);
+ const showMfu = (value) => value !== null && value !== undefined && value > 0 && value < .0001 ? '<0.01%' : percent(value);
+ const previousPrefill = latestGroupHistory(data, nodes, telemetryNodePort, 'prefill', now);
+ const previousDecode = latestGroupHistory(data, nodes, telemetryNodePort, 'decode', now);
+ const previousMfu = historicalPrefillMfu(nodes, telemetryNodePort, previousPrefill, now);
+ // A browser can receive old HTML and new JS across a local restart. Never
+ // place a historical value in the primary position unless both its title
+ // and its non-real-time detail can be shown by that HTML version.
+ const usePrefillHistory = values.prefill === null && previousPrefill && $('live-prefill-title') && $('live-prefill-samples');
+ const useDecodeHistory = values.decode === null && previousDecode && $('live-decode-title') && $('live-decode-samples');
+ const useMfuHistory = values.mfu === null && previousMfu && $('live-mfu-title') && $('live-mfu-detail');
+ setTextIfPresent('live-prefill-title', usePrefillHistory ? 'Prefill 并发吞吐(历史均值)' : 'Prefill 并发吞吐(采样)');
+ setTextIfPresent('live-decode-title', useDecodeHistory ? 'Decode 并发吞吐(历史均值)' : 'Decode 并发吞吐(采样)');
+ setTextIfPresent('live-mfu-title', useMfuHistory ? 'Prefill-MFU(历史估算)' : 'Prefill-MFU(单步估算)');
+ setTextIfPresent('live-prefill-tps', measured(usePrefillHistory ? previousPrefill.value : values.prefill));
+ setTextIfPresent('live-decode-tps', measured(useDecodeHistory ? previousDecode.value : values.decode));
+ setTextIfPresent('live-mfu', showMfu(useMfuHistory ? previousMfu.value : values.mfu));
+ setTextIfPresent('live-mbu', '—');
+ setTextIfPresent('live-hbm', values.hbm === null ? '—' : `${number.format(values.hbm)}%`);
+ const historicalDetail = (sample, method) => {
+ if (!sample) return '';
+ const period = `${dateTime.format(new Date(sample.start * 1000))}–${clock.format(new Date(sample.end * 1000))}`;
+ const scope = `${sample.groups}/${sample.total} 组` +
+ (telemetryNodePort === 'all' ? '' : ` · ${count(sample.samples)} 个采样`);
+ return `${method} · ${period} · 非实时 · ${scope}${sample.partial ? ' · 部分数据' : ''}`;
+ };
+ const groupDetail = (key, method) => {
+ const item = values.coverage[key];
+ if (!item.total) return '采集未连接 · 组数未知';
+ if (!item.valid) return `0/${item.total} 组有样本 · 暂无有效数据`;
+ const partial = item.valid < item.total || item.partial ? ' · 部分数据' : '';
+ const span = item.span_seconds > 30 ? ` · 时间跨度约 ${number.format(item.span_seconds)} s` : '';
+ const workers = motor && (key === 'prefill' || key === 'decode') ? ` · ${item.workers_valid}/${item.workers_total} worker` : '';
+ return `${item.valid}/${item.total} 组有样本${partial}${workers} · ${method}${span}`;
+ };
+ const detail = (key, value, sample) => telemetryNodePort === 'all' ? groupDetail(key, motor ? '计数差 / 间隔' : '采样和') :
+ value === null ? '所选设备组暂无有效样本' : motor ?
+ `${sample.valid_endpoints}/${sample.expected_endpoints} worker · 计数差 / 间隔${sample.partial ? ' · 部分数据' : ''}` :
+ `日志 ${clock.format(new Date(sample.log_at * 1000))} 写出 · rank 近似和`;
+ setTextIfPresent('live-prefill-samples', usePrefillHistory ? historicalDetail(previousPrefill, '历史采样均值') :
+ detail('prefill', values.prefill, prefillSample));
+ setTextIfPresent('live-decode-samples', useDecodeHistory ? historicalDetail(previousDecode, '历史采样均值') :
+ detail('decode', values.decode, decodeSample));
+ setTextIfPresent('live-mfu-detail', useMfuHistory ? historicalDetail(previousMfu, '按当前假设估算') :
+ telemetryNodePort === 'all' ? groupDetail('mfu', '按峰值算力加权') :
+ values.mfu === null ? '所选设备组暂无有效样本' : '引擎吞吐与模型假设换算');
+ setTextIfPresent('live-hbm-detail', telemetryNodePort === 'all' ? groupDetail('hbm', '按卡数加权') :
+ values.hbm === null ? '所选设备组暂无有效样本' : '所选设备组驱动读数的平均');
+ const topology = motor && data.topology_sources ? ` · 管理源 ${data.topology_sources.available}/${data.topology_sources.total}${data.topology_ambiguous ? ' · 拓扑待确认' : ''}` : '';
+ setTextIfPresent('telemetry-status', nodes.length ? `最近读取于 ${clock.format(new Date(data.sampled_at * 1000))}${topology}` : '采样未连接');
+ const tbody = $('telemetry-body');
+ if (!tbody) return;
+ tbody.replaceChildren();
+ nodes.forEach((item) => {
+ const row = document.createElement('tr');
+ const usable = item.source_ok === true && item.clock_ok === true &&
+ typeof item.clock_uncertainty_seconds === 'number' && Number.isFinite(item.clock_uncertainty_seconds) &&
+ item.clock_uncertainty_seconds >= 0 && item.clock_uncertainty_seconds <= 20;
+ const prefill = usable && item.backend_ready === true ? freshTelemetry(item.prefill, now) : null;
+ const decode = usable && item.backend_ready === true ? freshTelemetry(item.decode, now) : null;
+ const hbm = usable ? freshTelemetry(item.hbm, now) : null;
+ const timerStep = item.phase_timers && item.phase_timers.step;
+ const timersFresh = prefill && prefill.step === timerStep || decode && decode.step === timerStep;
+ const timer = (phase) => {
+ const value = timersFresh && item.phase_timers && item.phase_timers[phase];
+ return value && value.mean_ms !== null ? motor ? `${number.format(value.mean_ms)} ms · ${count(value.samples)} 请求 · ${value.ranks}/${value.expected_ranks} worker` :
+ `${number.format(value.mean_ms)} ms · ${value.ranks}/${value.expected_ranks} rank` : '—';
+ };
+ cell(row, item.label || '未知实例');
+ cell(row, prefill ? `${measured(prefill.tokens_per_second)} · ${motor ? '采样' : '日志'} ${clock.format(new Date(prefill.log_at * 1000))}` : '—');
+ cell(row, decode ? `${measured(decode.tokens_per_second)} · ${motor ? '采样' : '日志'} ${clock.format(new Date(decode.log_at * 1000))}` : '—');
+ cell(row, prefill ? showMfu(prefill.mfu_estimate) : '—');
+ cell(row, hbm ? `${number.format(hbm.mean_percent)}% · ${hbm.device_count} 卡` : '—');
+ cell(row, timer('prefill'));
+ cell(row, timer('decode'));
+ const mtp = usable && item.backend_ready === true && item.mtp && validMtp(item.mtp.lifetime, now);
+ cell(row, mtp ? `${percent(mtp.drafted ? mtp.accepted / mtp.drafted : null)} · ${mtp.valid_endpoints}/${mtp.expected_endpoints} 端点${mtp.partial ? ' · 部分数据' : ''}` : '—');
+ const engine = Boolean(prefill || decode);
+ const device = Boolean(hbm);
+ cell(row, item.duplicate_source ? '设备组重复,待核验' : !item.clock_ok ? '时钟不可用' :
+ !item.source_ok ? (motor ? '指标不可达或实例未就绪' : '引擎版本待核验') : item.backend_ready === false ? '后端未就绪' :
+ item.driver_ok === false && !motor ? '驱动版本待核验' :
+ engine && device ? '引擎与设备有样本' : engine ? '仅引擎有样本' : device ? '仅设备有样本' : '来源已核验,暂无样本');
+ tbody.appendChild(row);
+ });
+ if (!nodes.length) {
+ const row = document.createElement('tr');
+ const value = document.createElement('td'); value.colSpan = 9; value.className = 'empty';
+ value.textContent = '真实采样尚未连接'; row.appendChild(value); tbody.appendChild(row);
+ }
+ const profile = selectedNode && selectedNode.assumptions;
+ const assumption = profile ? `当前假设 ${number.format(profile.prefill_flops_per_token / 1e9)} GFLOP/输入 Token、${number.format(profile.peak_flops_per_device / 1e12)} TFLOP/s/卡;` : '';
+ setTextIfPresent('live-efficiency-note', motor ? '当前 vLLM 指标未提供可用的真实阶段算力或 HBM 带宽归因;Prefill-MFU、Decode-MBU 和 HBM 利用率暂显示未知。' :
+ `${assumption}Prefill-MFU 按引擎吞吐和模型假设估算,未计上下文 Attention 等,并非硬件算力实测。HBM 为设备级混合阶段采样,无法直接归因 Decode。`);
+ setTextIfPresent('phase-source-note', motor ?
+ '吞吐为各 worker 在连续两次采样间的计数增量除以墙钟间隔并相加;Prefill 只计本地计算的输入,Decode 只计输出。采样前后核对拓扑,但无法保证两次读取之间没有变化;通过进程标识和计数回退检测重启,仍可能漏掉内部重启。缺失或冲突的样本不作为零。' :
+ '并发吞吐是相同日志 step 的各 DP rank 单步速率近似和。跨组视图对有效采样求和,缺失组不参与并标注覆盖数;利用率只对有效组加权。各组并非同时测量,两个阶段也可能来自不同时刻。');
+ setTextIfPresent('telemetry-source-note', motor ?
+ '阶段耗时为引擎请求阶段直方图的增量均值,不能还原单个会话;Prefill 和 Decode 分别取对应角色。HBM 阶段带宽尚未接入。' :
+ '步耗时来自引擎 CPU 计时日志,包含调度等开销;括号内为有有效计时的 rank 数。HBM 为驱动设备级百分比,不能可靠分离纯 Decode 阶段。');
+ setTextIfPresent('device-history-note', motor ?
+ '速度为每个 10 分钟区间内的 vLLM 计数差速率采样均值;跨组对当前设备组的有效采样求和。空心点表示部分采集;采集前或不可用期间留空。' :
+ '速度是各组日志步骤速率的区间采样均值之和;空心点表示部分设备组有样本。采集前或不可用期间留空。');
+ const deadlines = nodes.flatMap((item) => [item.prefill, item.decode, item.hbm, item.mtp && item.mtp.lifetime, item.mtp && item.mtp.interval])
+ .map((part) => part && part.valid_until).filter((value) => typeof value === 'number' && Number.isFinite(value) && value > now);
+ if (deadlines.length) telemetryExpiry = setTimeout(() => renderTelemetry(telemetrySnapshot),
+ Math.min(2147483647, Math.max(1, (Math.min(...deadlines) - now) * 1000 + 20)));
+}
+
+async function refreshTelemetry() {
+ if (pendingTelemetry) return pendingTelemetry;
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), 4000);
+ pendingTelemetry = (async () => {
+ try {
+ const response = await fetch('/api/v5/telemetry', {cache: 'no-store', signal: controller.signal});
+ if (!response.ok) throw new Error('telemetry unavailable');
+ renderTelemetry(await response.json());
+ } catch (_) {
+ renderTelemetry({available: false, nodes: []});
+ } finally {
+ clearTimeout(timeout);
+ pendingTelemetry = null;
+ }
+ })();
+ return pendingTelemetry;
+}
+
+function activateView(name) {
+ const view = Object.prototype.hasOwnProperty.call(views, name) ? name : 'overview';
+ currentView = view;
+ document.querySelectorAll('[data-view]').forEach((element) => { element.hidden = element.dataset.view !== view; });
+ document.querySelectorAll('.sidebar nav a').forEach((link) => {
+ const active = link.getAttribute('href') === `#${view}`;
+ link.classList.toggle('nav-active', active);
+ if (active) link.setAttribute('aria-current', 'page'); else link.removeAttribute('aria-current');
+ });
+ $('view-title').textContent = views[view][0];
+ $('view-kicker').textContent = views[view][1];
+ document.querySelector('.segmented').hidden = view === 'active' || view === 'backends';
+}
+
+function setupNavigation() {
+ const navigate = () => {
+ activateView(window.location.hash.slice(1));
+ if (currentView === 'overview' || currentView === 'active') refreshActive();
+ };
+ document.querySelectorAll('.sidebar nav a').forEach((link) => link.addEventListener('click', (event) => {
+ event.preventDefault();
+ const view = link.getAttribute('href').slice(1);
+ if (window.location.hash !== `#${view}`) history.pushState(null, '', `#${view}`);
+ activateView(view);
+ if (view === 'overview' || view === 'active') refreshActive();
+ window.scrollTo(0, 0);
+ }));
+ window.addEventListener('popstate', navigate);
+ window.addEventListener('hashchange', navigate);
+ navigate();
+}
+
+function display(value, stats, digits = false, includePartial = true) {
+ if (!stats.known && !(includePartial && stats.partial) && !stats.complete) return '—';
+ const prefix = stats.complete ? '' : '≥';
+ return prefix + (digits ? number.format(value) : integer.format(value));
+}
+
+function cell(row, text) {
+ const node = document.createElement('td');
+ node.textContent = text;
+ row.appendChild(node);
+}
+
+function empty(body, columns, text) {
+ const row = document.createElement('tr');
+ const item = document.createElement('td');
+ item.colSpan = columns;
+ item.className = 'empty';
+ item.textContent = text;
+ row.appendChild(item);
+ body.appendChild(row);
+}
+
+function setupTables() {
+ Object.values(tables).forEach((table) => {
+ const section = document.querySelector(table.selector);
+ const title = section.querySelector('h2').textContent;
+ table.headers = [...section.querySelectorAll('thead th')];
+ table.headers.forEach((header, index) => {
+ const key = table.columns[index];
+ const label = header.textContent;
+ const button = document.createElement('button');
+ button.type = 'button'; button.className = 'sort-control'; button.textContent = label;
+ button.setAttribute('aria-label', `${label},点击排序`);
+ header.replaceChildren(button);
+ header.addEventListener('click', () => {
+ if (table.sort === key) table.direction = table.direction === 'asc' ? 'desc' : 'asc';
+ else { table.sort = key; table.direction = textSorts.has(key) ? 'asc' : 'desc'; }
+ table.offset = 0;
+ refresh();
+ });
+ });
+ if (table.noPager) return;
+ let foot = section.querySelector('.panel-foot');
+ if (!foot) { foot = document.createElement('div'); foot.className = 'panel-foot'; section.appendChild(foot); }
+ let pager = foot.querySelector('.pager');
+ if (!pager) { pager = document.createElement('div'); pager.className = 'pager'; foot.appendChild(pager); }
+ pager.replaceChildren();
+ if (table === tables.sessions || table === tables.requests) {
+ const hint = document.createElement('span'); hint.className = 'snapshot-hint';
+ hint.textContent = '翻页快照只包含建立时已入账的记录;将会话和请求都返回第一页,或点击右上角刷新,可查看后续入账。';
+ foot.insertBefore(hint, pager);
+ }
+ if (table === tables.active) {
+ const hint = document.createElement('span'); hint.className = 'snapshot-hint';
+ hint.textContent = '当前连接随采样更新,翻页结果可能变化。';
+ foot.insertBefore(hint, pager);
+ }
+ const label = document.createElement('label');
+ label.textContent = '每页 ';
+ const select = document.createElement('select');
+ select.setAttribute('aria-label', `${title}每页显示条数`);
+ [10, 20, 50].forEach((size) => {
+ const option = document.createElement('option'); option.value = String(size); option.textContent = String(size);
+ select.appendChild(option);
+ });
+ select.value = String(table.size);
+ select.addEventListener('change', () => { table.size = Number(select.value); table.offset = 0; refresh(); });
+ label.appendChild(select);
+ const previous = document.createElement('button'); previous.type = 'button'; previous.textContent = '上一页'; previous.disabled = true;
+ const page = document.createElement('span');
+ const next = document.createElement('button'); next.type = 'button'; next.textContent = '下一页'; next.disabled = true;
+ previous.setAttribute('aria-label', `${title}上一页`); next.setAttribute('aria-label', `${title}下一页`);
+ previous.addEventListener('click', () => { table.offset = Math.max(0, table.offset - table.size); refresh(); });
+ next.addEventListener('click', () => { table.offset += table.size; refresh(); });
+ pager.append(label, previous, page, next);
+ table.pager = {previous, page, next};
+ });
+}
+
+function updateHeaders(table) {
+ table.headers.forEach((header, index) => {
+ const direction = table.columns[index] === table.sort ? (table.direction === 'asc' ? 'ascending' : 'descending') : 'none';
+ header.setAttribute('aria-sort', direction);
+ });
+}
+
+function updatePager(table, total, available = true) {
+ if (!table.pager) return;
+ const at = table === tables.requests || table === tables.sessions ? lastSnapshot?.at :
+ table.offset > 0 ? table.localData?.as_of : null;
+ const snapshot = at ? ` · 快照 ${snapshotTime.format(new Date(at * 1000))}` : '';
+ table.pager.page.textContent = available ? `共 ${count(total)} 条 · ${total ? table.offset + 1 : 0}–${Math.min(table.offset + table.size, total)}${snapshot}` : '数据不可用';
+ table.pager.previous.disabled = !available || table.offset === 0;
+ table.pager.next.disabled = !available || table.offset + table.size >= total;
+}
+
+function localSource(name, data) {
+ const table = tables[name];
+ if (table.offset > 0 && table.localData) return table.localData;
+ table.localData = data;
+ return data;
+}
+
+function localValue(name, item, key, data) {
+ if (name === 'active' && key === 'phase') return item.first_visible === null ? 0 : 1;
+ if (name === 'active' && (key === 'hit_rate' || key === 'tps')) return null;
+ if (name === 'backends' && key !== 'port' && !item.available) return null;
+ if (name === 'clients' && key === 'total') return item.known || item.partial || data.totals.complete ? item.input + item.output : null;
+ if (name === 'clients' && key === 'cached' && !item.known && !data.totals.complete) return null;
+ if (name === 'clients' && ['input', 'output', 'window_tps'].includes(key) && !item.known && !item.partial && !data.totals.complete) return null;
+ if (name === 'days' && key === 'cached' && !item.known && !item.complete) return null;
+ if (name === 'days' && key !== 'date' && !item.known && !item.partial && !item.complete) return null;
+ return item[key] ?? null;
+}
+
+function localPage(name, rows, data, available = true) {
+ const table = tables[name];
+ const ordered = rows.map((item, index) => ({item, index})).sort((a, b) => {
+ const left = localValue(name, a.item, table.sort, data), right = localValue(name, b.item, table.sort, data);
+ if (left === null || right === null) return left === right ? a.index - b.index : left === null ? 1 : -1;
+ const delta = typeof left === 'number' && typeof right === 'number' ? left - right :
+ String(left).localeCompare(String(right), 'zh-CN', {numeric: true, sensitivity: 'base'});
+ return (table.direction === 'asc' ? delta : -delta) || a.index - b.index;
+ }).map((entry) => entry.item);
+ if (table.noPager) return ordered;
+ table.offset = ordered.length ? Math.min(table.offset, Math.floor((ordered.length - 1) / table.size) * table.size) : 0;
+ updatePager(table, ordered.length, available);
+ return ordered.slice(table.offset, table.offset + table.size);
+}
+
+function renderClients(data) {
+ data = localSource('clients', data);
+ const body = $('clients-body');
+ body.replaceChildren();
+ $('client-count').textContent = `${data.clients.length} 个来源`;
+ const clients = localPage('clients', data.clients, data);
+ if (!data.clients.length) {
+ empty(body, 8, '当前范围内暂无请求记录');
+ return;
+ }
+ clients.forEach((client) => {
+ const row = document.createElement('tr');
+ const complete = data.totals.complete && !client.partial && !client.missing && !client.failed && !client.interrupted;
+ const view = {...client, complete, covered_seconds: data.totals.covered_seconds};
+ const unknown = client.missing + client.failed + client.interrupted;
+ cell(row, client.ip || '未知来源');
+ const details = [client.partial ? `${integer.format(client.partial)} 部分` : '', unknown ? `${integer.format(unknown)} 未计量` : ''].filter(Boolean).join(',');
+ cell(row, details ? `${integer.format(client.requests)}(${details})` : integer.format(client.requests));
+ cell(row, display(client.input + client.output, view));
+ cell(row, display(client.input, view));
+ cell(row, display(client.output, view));
+ cell(row, display(client.cached, view, false, false));
+ cell(row, rate(client.request_tps));
+ cell(row, client.window_tps === null ? '—' : display(client.window_tps, view, true));
+ body.appendChild(row);
+ });
+}
+
+function renderActive(data) {
+ if (data.as_of < latestActiveAsOf) return;
+ latestActiveAsOf = data.as_of;
+ const body = $('active-body');
+ body.replaceChildren();
+ updateHeaders(tables.active);
+ if (activeExpiry) clearTimeout(activeExpiry);
+ const age = (Date.now() / 1000) - data.active_sampled_at;
+ const ready = data.collector_live && data.active_ready && data.active_sampled_at !== null && age >= 0 && age < 3;
+ const unavailable = () => {
+ tables.active.offset = 0;
+ $('active-count').textContent = '—'; $('active-badge').textContent = '快照不可用';
+ body.replaceChildren(); empty(body, 8, '采集快照过期或不可用');
+ updatePager(tables.active, 0, false);
+ };
+ if (!ready) {
+ unavailable();
+ return;
+ }
+ $('active-count').textContent = count(data.active.length);
+ $('active-badge').textContent = `快照 ${clock.format(new Date(data.active_sampled_at * 1000))}`;
+ const active = localPage('active', data.active, data);
+ activeExpiry = setTimeout(unavailable, Math.max(0, (3 - age) * 1000));
+ if (!data.active.length) { empty(body, 8, '当前采集快照中暂无在途推理请求'); return; }
+ active.forEach((item) => {
+ const row = document.createElement('tr');
+ cell(row, item.ip || '未知来源');
+ cell(row, fingerprint(item.session));
+ cell(row, seconds(item.elapsed));
+ cell(row, item.first_visible === null ? '等待可见增量' : '已有可见增量');
+ ['input', 'output'].forEach((key) => cell(row, count(item[key])));
+ cell(row, '—');
+ cell(row, '—');
+ body.appendChild(row);
+ });
+}
+
+async function refreshActive() {
+ if (pendingActive || (currentView !== 'overview' && currentView !== 'active')) return;
+ const controller = new AbortController();
+ pendingActive = controller;
+ const timeout = setTimeout(() => controller.abort(), 3000);
+ try {
+ const response = await fetch('/api/v5/active', {cache: 'no-store', signal: controller.signal});
+ if (!response.ok) throw new Error('unavailable');
+ const data = await response.json();
+ if (currentView === 'overview' || currentView === 'active') renderActive(data);
+ } catch (_) {
+ // The existing sample expires independently if the lightweight poll fails.
+ } finally {
+ clearTimeout(timeout);
+ if (pendingActive === controller) pendingActive = null;
+ }
+}
+
+function renderBackends(data) {
+ if (backendExpiry) clearTimeout(backendExpiry);
+ backendExpiry = null;
+ const now = Date.now() / 1000;
+ const body = $('backends-body');
+ body.replaceChildren();
+ if (!data.backends.length) { empty(body, 7, '未配置后端原生指标'); return; }
+ localPage('backends', data.backends, data).forEach((backend) => {
+ const row = document.createElement('tr');
+ cell(row, backend.label || String(backend.port));
+ const hasDeadline = Object.prototype.hasOwnProperty.call(backend, 'valid_until');
+ const current = backend.available && (!hasDeadline || typeof backend.valid_until === 'number' &&
+ Number.isFinite(backend.valid_until) && now < backend.valid_until);
+ if (!current) {
+ cell(row, '不可用');
+ for (let i = 0; i < 5; i++) cell(row, '—');
+ } else {
+ const prefix = backend.partial ? '≥' : '';
+ cell(row, backend.running === null || backend.running === undefined ? '—' : prefix + count(backend.running));
+ cell(row, backend.waiting === null || backend.waiting === undefined ? '—' : prefix + count(backend.waiting));
+ cell(row, backend.workers_expected === undefined ? count(backend.workers_alive) : `${count(backend.workers_alive)}/${count(backend.workers_expected)}`);
+ cell(row, backend.admission_ready === 1 ? '就绪' : '未就绪');
+ cell(row, backend.input_total === null || backend.input_total === undefined ? '—' : prefix + count(backend.input_total));
+ cell(row, `${percent(backend.hit_rate)}${backend.partial && backend.hit_rate !== null && backend.hit_rate !== undefined ? ' · 部分' : ''}`);
+ }
+ body.appendChild(row);
+ });
+ const deadlines = data.backends.filter((backend) => backend.available && typeof backend.valid_until === 'number' &&
+ Number.isFinite(backend.valid_until) && backend.valid_until > now).map((backend) => backend.valid_until);
+ if (deadlines.length) backendExpiry = setTimeout(() => renderBackends(data),
+ Math.min(2147483647, Math.max(1, (Math.min(...deadlines) - now) * 1000 + 20)));
+}
+
+function renderSessions(data) {
+ const summary = $('session-summary-body');
+ summary.replaceChildren();
+ tables.sessions.offset = data.session_offset;
+ tables.requests.offset = data.request_offset;
+ updatePager(tables.sessions, data.session_count);
+ updatePager(tables.requests, data.request_count);
+ $('session-count').textContent = `${count(data.session_count)} 个可识别会话 · ${count(data.session_unknown_count)} 个请求未知`;
+ if (!data.sessions.length) empty(summary, 13, '当前页暂无可识别会话');
+ data.sessions.forEach((item) => {
+ const row = document.createElement('tr');
+ const view = {known: item.known, partial: item.partial, complete: data.snapshot_complete && item.known === item.requests};
+ cell(row, fingerprint(item.session));
+ cell(row, item.ips.length ? item.ips.join(', ') : '未知来源');
+ cell(row, `${count(item.requests)} / ${count(item.known)} 完整 / ${count(item.partial || 0)} 部分`);
+ cell(row, item.input === null ? '—' : display(item.input, view));
+ cell(row, item.output === null ? '—' : display(item.output, view));
+ cell(row, item.cached === null || item.cached === undefined ? '—' : display(item.cached, view, false, false));
+ cell(row, item.uncached_input === null || item.uncached_input === undefined ? '—' : display(item.uncached_input, view, false, false));
+ cell(row, percent(item.hit_rate));
+ cell(row, seconds(item.first_visible)); cell(row, seconds(item.after_first));
+ cell(row, rate(item.input_per_wait)); cell(row, rate(item.output_per_after_first));
+ cell(row, item.first_share === null ? '—' : `${percent(item.first_share)} / ${percent(1 - item.first_share)}`);
+ summary.appendChild(row);
+ });
+ const body = $('sessions-body');
+ body.replaceChildren();
+ if (!data.requests.length) empty(body, 14, '当前页暂无请求记录');
+ const states = {known: '已计量', partial: '部分用量', missing: '未计量', failed: '失败', interrupted: '中断'};
+ data.requests.forEach((item) => {
+ const row = document.createElement('tr');
+ cell(row, dateTime.format(new Date(item.finished * 1000)));
+ cell(row, item.ip || '未知来源');
+ cell(row, fingerprint(item.session));
+ cell(row, `${states[item.state] || '未知'}${item.status ? ` ${item.status}` : ''}`);
+ cell(row, count(item.input)); cell(row, count(item.output)); cell(row, count(item.cached));
+ cell(row, count(item.uncached_input)); cell(row, percent(item.hit_rate));
+ cell(row, seconds(item.first_visible)); cell(row, seconds(item.after_first));
+ cell(row, rate(item.input_per_wait)); cell(row, rate(item.output_per_after_first));
+ cell(row, item.first_share === null ? '—' : `${percent(item.first_share)} / ${percent(item.after_first_share)}`);
+ body.appendChild(row);
+ });
+}
+
+function renderDays(data) {
+ data = localSource('days', data);
+ const body = $('days-body');
+ body.replaceChildren();
+ localPage('days', data.days, data).forEach((day) => {
+ const row = document.createElement('tr');
+ cell(row, day.date);
+ cell(row, display(day.input, day));
+ cell(row, display(day.output, day));
+ cell(row, display(day.cached, day, false, false));
+ body.appendChild(row);
+ });
+}
+
+function svgElement(name, attributes) {
+ const el = document.createElementNS('http://www.w3.org/2000/svg', name);
+ Object.entries(attributes).forEach(([key, value]) => el.setAttribute(key, String(value)));
+ return el;
+}
+
+function attachTrendTooltip(svg, descriptions) {
+ const tooltip = $('trend-tooltip');
+ const previousPointer = tooltip && tooltip._trendActive === svg ? svg._trendPointer : null;
+ const previousKeyboard = tooltip && tooltip._trendActive === svg ? svg._trendKeyboardIndex : null;
+ const hide = () => {
+ if (tooltip && tooltip._trendOwner === svg) { tooltip.hidden = true; tooltip._trendOwner = null; }
+ };
+ hide();
+ svg.onpointermove = svg.onpointerleave = svg.onfocus = svg.onblur = svg.onkeydown = null;
+ svg.removeAttribute('tabindex');
+ svg.removeAttribute('aria-describedby');
+ if (!tooltip || !Array.isArray(descriptions) || !descriptions.some((text) => typeof text === 'string' && text)) {
+ svg._trendPointer = null; svg._trendKeyboardIndex = null;
+ if (tooltip && tooltip._trendActive === svg) tooltip._trendActive = null;
+ return;
+ }
+ svg.appendChild(svgElement('rect', {x: 0, y: 0, width: 800, height: 68, fill: 'transparent', class: 'trend-hover-overlay'}));
+ svg.setAttribute('tabindex', '0');
+ svg.setAttribute('aria-describedby', 'trend-tooltip');
+ let selected = Number.isSafeInteger(svg._trendKeyboardIndex) ? svg._trendKeyboardIndex : -1;
+ const activate = () => {
+ if (tooltip._trendOwner && tooltip._trendOwner !== svg) { tooltip.hidden = true; tooltip._trendOwner = null; }
+ tooltip._trendActive = svg;
+ };
+ const show = (index, x, y) => {
+ const description = descriptions[index];
+ selected = index;
+ if (typeof description !== 'string' || !description) { hide(); return; }
+ tooltip._trendOwner = svg;
+ tooltip.textContent = description;
+ tooltip.hidden = false;
+ const box = tooltip.getBoundingClientRect();
+ const width = document.documentElement.clientWidth || window.innerWidth;
+ const height = document.documentElement.clientHeight || window.innerHeight;
+ tooltip.style.left = `${Math.max(8, Math.min(x + 12, width - box.width - 8))}px`;
+ tooltip.style.top = `${Math.max(8, Math.min(y + 12, height - box.height - 8))}px`;
+ };
+ svg.onpointermove = (event) => {
+ const bounds = svg.getBoundingClientRect();
+ if (!Number.isFinite(event.clientX) || !Number.isFinite(event.clientY) || bounds.width <= 0 ||
+ event.clientX < bounds.left || event.clientX > bounds.left + bounds.width) { svg._trendPointer = null; hide(); return; }
+ activate();
+ svg._trendPointer = {clientX: event.clientX, clientY: event.clientY};
+ svg._trendKeyboardIndex = null;
+ const index = descriptions.length === 1 ? 0 : Math.round((event.clientX - bounds.left) / bounds.width * (descriptions.length - 1));
+ show(index, event.clientX, event.clientY);
+ };
+ const showSelected = (index) => {
+ const bounds = svg.getBoundingClientRect();
+ const fraction = descriptions.length === 1 ? 0 : index / (descriptions.length - 1);
+ svg._trendKeyboardIndex = index;
+ svg._trendPointer = null;
+ show(index, bounds.left + fraction * bounds.width, bounds.top);
+ };
+ svg.onpointerleave = () => { svg._trendPointer = null; hide(); if (tooltip._trendActive === svg) tooltip._trendActive = null; };
+ svg.onfocus = () => { activate(); const first = descriptions.findIndex((value) => typeof value === 'string' && value); if (first >= 0) showSelected(first); };
+ svg.onblur = () => { svg._trendKeyboardIndex = null; hide(); if (tooltip._trendActive === svg) tooltip._trendActive = null; };
+ svg.onkeydown = (event) => {
+ if (!['ArrowLeft', 'ArrowRight'].includes(event.key)) return;
+ activate();
+ const direction = event.key === 'ArrowRight' ? 1 : -1;
+ for (let index = selected + direction; index >= 0 && index < descriptions.length; index += direction) {
+ if (typeof descriptions[index] === 'string' && descriptions[index]) { event.preventDefault(); showSelected(index); break; }
+ }
+ };
+ if (previousPointer) svg.onpointermove(previousPointer);
+ else if (Number.isSafeInteger(previousKeyboard) && previousKeyboard >= 0 && previousKeyboard < descriptions.length) showSelected(previousKeyboard);
+}
+
+const charts = [
+ {id: 'input', field: 'input', kind: 'tokens', label: '输入'},
+ {id: 'output', field: 'output', kind: 'tokens', label: '输出'},
+ {id: 'cache-rate', style: 'cached', field: 'mean_request_hit_rate', kind: 'hit', label: '平均请求命中率', samples: 'request_hit_samples'},
+];
+
+function combinedDeviceHistory(data, nodes) {
+ if (!data || data.available !== true || data.history_available !== true || !nodes.length ||
+ new Set(nodes.map((node) => node && node.backend_port)).size !== nodes.length) return null;
+ const method = data.kind === 'vllm-motor-telemetry' ? 'vllm-counter-rate-v1' : 'deepinfer-log-step-approx-v1';
+ const usable = nodes.flatMap((node) => {
+ if (!node || node.source_ok !== true || !node.history || node.history.method !== method ||
+ node.history.bucket_seconds !== 600 || !Array.isArray(node.history.points) || node.history.points.length !== 144) return [];
+ const buckets = new Map();
+ for (const point of node.history.points) {
+ if (!point || !Number.isSafeInteger(point.start) || point.end !== point.start + 600 || buckets.has(point.start)) return [];
+ buckets.set(point.start, point);
+ }
+ return [{points: node.history.points, buckets}];
+ });
+ if (!usable.length) return null;
+ const points = usable[0].points.map((point, index) => {
+ if (point.start !== usable[0].points[0].start + index * 600) return null;
+ const all = usable.map((node) => node.buckets.get(point.start)).filter((item) =>
+ item && item.start === point.start && item.end === point.end);
+ const combined = {start: point.start, end: point.end};
+ ['prefill', 'decode'].forEach((phase) => {
+ const valid = all.filter((item) => typeof item[`${phase}_tps`] === 'number' &&
+ Number.isFinite(item[`${phase}_tps`]) && item[`${phase}_tps`] >= 0 &&
+ Number.isSafeInteger(item[`${phase}_samples`]) && item[`${phase}_samples`] > 0);
+ const total = valid.length ? valid.reduce((sum, item) => sum + item[`${phase}_tps`], 0) : null;
+ combined[`${phase}_tps`] = total !== null && Number.isFinite(total) ? total : null;
+ combined[`${phase}_samples`] = combined[`${phase}_tps`] === null ? 0 : 1;
+ combined[`${phase}_groups`] = combined[`${phase}_tps`] === null ? 0 : valid.length;
+ combined[`${phase}_partial`] = valid.some((item) => item[`${phase}_partial`] === true);
+ });
+ return combined;
+ });
+ if (points.some((point) => point === null)) return null;
+ return {history: {method, bucket_seconds: 600, aggregate: true, group_count: nodes.length,
+ prefill_group_count: nodes.filter((node) => data.kind !== 'vllm-motor-telemetry' || node.role === 'prefill').length,
+ decode_group_count: nodes.filter((node) => data.kind !== 'vllm-motor-telemetry' || node.role === 'decode').length, points}};
+}
+
+function renderMtpHistory(data, nodes, port) {
+ const svg = $('chart-mtp-svg'), label = $('chart-mtp');
+ if (!svg || !label) return;
+ svg.replaceChildren();
+ [16, 34, 52].forEach((y) => svg.appendChild(svgElement('line', {x1: 0, y1: y, x2: 800, y2: y, class: 'grid'})));
+ const groups = data && data.available === true && data.history_available === true && data.kind === 'vllm-motor-telemetry' ?
+ nodes.filter((node) => node && node.role === 'decode' && (port === 'all' || node.backend_port === port)) : [];
+ const histories = groups.flatMap((node) => {
+ const history = node.mtp_history;
+ if (!history || history.method !== 'vllm-mtp-counter-delta-v1' || history.bucket_seconds !== 600 ||
+ !Array.isArray(history.points) || history.points.length !== 144 ||
+ history.points.some((point, i) => !point || !Number.isSafeInteger(point.start) || point.start < 0 ||
+ point.end !== point.start + 600 || (i && point.start !== history.points[i - 1].start + 600))) return [];
+ return [new Map(history.points.map((point) => [point.start, point]))];
+ });
+ if (!groups.length || new Set(groups.map((node) => node.backend_port)).size !== groups.length || !histories.length) {
+ attachTrendTooltip(svg, []); label.textContent = '等待 MTP 历史采样'; return;
+ }
+ const starts = Array.from(histories[0].keys());
+ const points = starts.map((start) => {
+ const valid = histories.map((history) => history.get(start)).filter((point) => point &&
+ Number.isSafeInteger(point.samples) && point.samples > 0 && Number.isSafeInteger(point.drafted) && point.drafted >= 0 &&
+ Number.isSafeInteger(point.accepted) && point.accepted >= 0 && point.accepted <= point.drafted);
+ const drafted = valid.reduce((sum, point) => sum + point.drafted, 0);
+ const accepted = valid.reduce((sum, point) => sum + point.accepted, 0);
+ if (!Number.isSafeInteger(drafted) || !Number.isSafeInteger(accepted) || !drafted) return null;
+ return {start, drafted, accepted, rate: accepted / drafted, groups: valid.length,
+ timeUnverified: valid.some((point) => point.time_coverage !== 'complete'),
+ partial: valid.length < groups.length || valid.some((point) => point.partial === true || point.time_coverage !== 'complete')};
+ });
+ const descriptions = Array(points.length).fill(null);
+ let segment = [], segmentPartial = false, drafted = 0, accepted = 0, buckets = 0, partialBuckets = 0;
+ const draw = () => {
+ if (segment.length > 1) svg.appendChild(svgElement('polyline', {points: segment.join(' '), class: `mtp${segmentPartial ? ' partial' : ''}`}));
+ segment = [];
+ };
+ points.forEach((point, index) => {
+ if (!point) { draw(); return; }
+ if (segment.length && point.partial !== segmentPartial) draw();
+ segmentPartial = point.partial;
+ const x = index * 800 / (points.length - 1), y = 59 - point.rate * 50;
+ segment.push(`${x.toFixed(2)},${y.toFixed(2)}`);
+ const marker = svgElement('circle', {cx: x, cy: y, r: 2.5, class: `mtp${point.partial ? ' partial' : ''}`});
+ const period = `${dateTime.format(new Date(point.start * 1000))}–${dateTime.format(new Date((point.start + 600) * 1000))}`;
+ const description = `${period}\n推测解码草稿接受率:${percent(point.rate)}\n接受 ${count(point.accepted)} / 草稿 ${count(point.drafted)} · ${point.groups}/${groups.length} 注册组${point.partial ? ' · 部分数据' : ''}${point.timeUnverified ? ' · 时间覆盖未核验' : ''}`;
+ const title = svgElement('title', {}); title.textContent = description;
+ marker.appendChild(title); svg.appendChild(marker); descriptions[index] = description;
+ drafted += point.drafted; accepted += point.accepted; buckets++; if (point.partial) partialBuckets++;
+ });
+ draw(); attachTrendTooltip(svg, descriptions);
+ label.textContent = drafted && Number.isSafeInteger(drafted) && Number.isSafeInteger(accepted) ?
+ `${percent(accepted / drafted)} · ${count(buckets)} 个有草稿区间${partialBuckets ? ` · ${count(partialBuckets)} 个部分区间` : ''}` : '暂无有草稿的区间';
+}
+
+function renderDeviceHistory(data, selectedNode) {
+ const dropped = data && Number.isSafeInteger(data.history_dropped) && data.history_dropped > 0 ? data.history_dropped : 0;
+ setTextIfPresent('device-history-health', dropped ? ` 历史队列已跳过 ${count(dropped)} 次采集。` : '');
+ const history = data && data.available === true && data.history_available === true && selectedNode && selectedNode.history;
+ const points = history && ['deepinfer-log-step-approx-v1', 'vllm-counter-rate-v1'].includes(history.method) && history.bucket_seconds === 600 &&
+ Array.isArray(history.points) && history.points.length === 144 ? history.points : null;
+ ['prefill', 'decode'].forEach((phase) => {
+ const svg = $(`chart-device-${phase}-svg`);
+ const label = $(`chart-device-${phase}`);
+ if (!svg || !label) return;
+ svg.replaceChildren();
+ [16, 34, 52].forEach((y) => svg.appendChild(svgElement('line', {x1: 0, y1: y, x2: 800, y2: y, class: 'grid'})));
+ if (!points) { attachTrendTooltip(svg, []); label.textContent = '设备历史采样未连接'; return; }
+ const valid = points.map((point) => {
+ const value = point && point[`${phase}_tps`];
+ const samples = point && point[`${phase}_samples`];
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0 && Number.isSafeInteger(samples) && samples > 0 ? value : null;
+ });
+ const peak = Math.max(1, ...valid.filter((value) => value !== null));
+ const descriptions = Array(points.length).fill(null);
+ let segment = [], total = 0, samples = 0, segmentPartial = false, partialBuckets = 0;
+ const draw = () => {
+ if (segment.length > 1) svg.appendChild(svgElement('polyline', {points: segment.join(' '), class: `${phase}${segmentPartial ? ' partial' : ''}`}));
+ segment = [];
+ };
+ valid.forEach((value, i) => {
+ if (value === null) { draw(); return; }
+ const groups = history[`${phase}_group_count`] ?? history.group_count;
+ const partial = points[i][`${phase}_partial`] === true || history.aggregate && points[i][`${phase}_groups`] < groups;
+ if (segment.length && partial !== segmentPartial) draw();
+ segmentPartial = partial;
+ const x = i * 800 / (points.length - 1), y = 59 - value / peak * 50;
+ segment.push(`${x.toFixed(2)},${y.toFixed(2)}`);
+ const marker = svgElement('circle', {cx: x, cy: y, r: 2.5, class: `${phase}${partial ? ' partial' : ''}`});
+ const title = svgElement('title', {});
+ const scope = history.aggregate ? `${points[i][`${phase}_groups`]}/${groups} 组采样均值之和${partial ? ' · 部分数据' : ''}` : `${count(points[i][`${phase}_samples`])} 个采样${partial ? ' · 部分数据' : ''}`;
+ const period = `${dateTime.format(new Date(points[i].start * 1000))}–${dateTime.format(new Date(points[i].end * 1000))}`;
+ title.textContent = `${period}:${speed(value)} · ${scope}`;
+ descriptions[i] = `${period}\n${phase === 'prefill' ? 'Prefill' : 'Decode'} 并发吞吐:${speed(value)}\n${scope}`;
+ marker.appendChild(title); svg.appendChild(marker);
+ if (partial) partialBuckets++;
+ total += value * points[i][`${phase}_samples`]; samples += points[i][`${phase}_samples`];
+ });
+ draw();
+ attachTrendTooltip(svg, descriptions);
+ label.textContent = samples ? `${speed(total / samples)} · ${count(samples)} ${history.aggregate ? `个有采样区间${partialBuckets ? ` · ${count(partialBuckets)} 个仅部分组` : ''}` : '个采样'}` : '暂无设备采样';
+ });
+}
+
+function renderPlot(chart, points) {
+ const {id, field, kind} = chart;
+ const svg = $(`chart-${id}-svg`);
+ const label = $(`chart-${id}`);
+ if (!svg || !label) return;
+ svg.replaceChildren();
+ [16, 34, 52].forEach((y) => svg.appendChild(svgElement('line', {x1: 0, y1: y, x2: 800, y2: y, class: 'grid'})));
+ const required = kind === 'hit' ? [field, 'request_hit_sum', 'request_hit_samples'] :
+ kind === 'speed' ? [field, `${id}_tokens`, `${id}_seconds`, chart.samples] : [];
+ if (points.some((point) => required.some((key) => !Object.prototype.hasOwnProperty.call(point, key)))) {
+ attachTrendTooltip(svg, []);
+ label.textContent = '当前数据版本不支持';
+ return;
+ }
+ const values = points.map((point) => point.end - point.start >= 599.99 && (point.known || point.partial || point.complete) &&
+ typeof point[field] === 'number' && Number.isFinite(point[field]) ? point[field] : null);
+ const peak = Math.max(1, ...values.filter((value) => value !== null));
+ const descriptions = Array(points.length).fill(null);
+ let segment = [];
+ const draw = () => {
+ if (segment.length > 1) svg.appendChild(svgElement('polyline', {points: segment.join(' '), class: chart.style || id}));
+ segment = [];
+ };
+ values.forEach((value, i) => {
+ if (value === null) { draw(); return; }
+ const x = points.length > 1 ? i * 800 / (points.length - 1) : 0;
+ const y = 59 - value / peak * 50;
+ segment.push(`${x.toFixed(2)},${y.toFixed(2)}`);
+ const marker = svgElement('circle', {cx: x, cy: y, r: 2.5, class: chart.style || id});
+ const title = svgElement('title', {});
+ const formatted = kind === 'tokens' ? `${integer.format(value)} Token` : kind === 'hit' ? percent(value) : speed(value);
+ const boundary = points[i].complete ? '' : kind === 'tokens' ? '至少 ' : '已观测样本 ';
+ const samples = chart.samples ? ` · ${count(points[i][chart.samples])} 个样本` : '';
+ const period = `${dateTime.format(new Date(points[i].start * 1000))}–${dateTime.format(new Date(points[i].end * 1000))}`;
+ title.textContent = `${period}:${boundary}${formatted}${samples}`;
+ descriptions[i] = `${period}\n${chart.label || id}:${boundary}${formatted}${samples}`;
+ marker.appendChild(title);
+ svg.appendChild(marker);
+ });
+ draw();
+ attachTrendTooltip(svg, descriptions);
+ const sum = (key) => points.reduce((total, point) => total + (point[key] || 0), 0);
+ if (kind === 'tokens') {
+ const any = points.some((point) => point.known || point.partial || point.complete);
+ const complete = points.filter((point) => point.end > point.start).every((point) => point.complete);
+ label.textContent = any ? `${complete ? '' : '≥'}${integer.format(sum(field))}` : '暂无数据';
+ } else if (kind === 'hit') {
+ const samples = sum(chart.samples);
+ label.textContent = samples ? `${percent(sum('request_hit_sum') / samples)} · ${count(samples)} 请求` : '暂无样本';
+ } else {
+ const samples = sum(chart.samples);
+ const duration = sum(`${id}_seconds`);
+ label.textContent = samples && duration ? `${speed(sum(`${id}_tokens`) / duration)} · ${count(samples)} 样本` : '暂无样本';
+ }
+}
+
+function render(data) {
+ const stats = data.totals;
+ $('status-dot').classList.toggle('live', data.collector_live);
+ $('status-label').textContent = data.collector_live ? '采集器心跳正常' : '入口采集未连接';
+ $('updated').textContent = `数据查询于 ${clock.format(new Date(data.as_of * 1000))}`;
+ $('total-usage').textContent = display(stats.input + stats.output, stats);
+ $('total-input').textContent = display(stats.input, stats);
+ $('total-output').textContent = display(stats.output, stats);
+ $('total-cached').textContent = display(stats.cached, stats, false, false);
+ setTextIfPresent('total-uncached', stats.uncached_input === null || stats.uncached_input === undefined ? '—' :
+ display(stats.uncached_input, stats, false, false));
+ $('total-tps').textContent = stats.window_tps === null ? '—' : display(stats.window_tps, stats, true);
+ $('cache-detail').textContent = stats.hit_rate === null ? '有缓存明细的请求命中率 —' : `有缓存明细的请求按输入 Token 加权 ${number.format(stats.hit_rate * 100)}%`;
+ setTextIfPresent('overview-prefill-wait', seconds(stats.first_mean));
+ setTextIfPresent('overview-hit-rate', percent(stats.hit_rate));
+ setTextIfPresent('overview-wait-samples', `${count(stats.first_samples)} 个可见增量样本`);
+ setTextIfPresent('overview-hit-samples', `按输入 Token 加权 · ${count(stats.known)} 个有缓存明细的请求`);
+ $('success-rate').textContent = percent(stats.success_rate);
+ $('success-detail').textContent = `${count(stats.successes)} / ${count(stats.classified)} 个可判定请求`;
+ $('requests-rate').textContent = !stats.covered_seconds || stats.requests_per_second === null ? '—' :
+ `${stats.complete ? '' : '≥'}${rate(stats.requests_per_second)}`;
+ $('first-p95').textContent = seconds(stats.first_p95);
+ $('first-detail').textContent = `${count(stats.first_samples)} 个已计量成功样本`;
+ $('elapsed-p95').textContent = seconds(stats.elapsed_p95);
+ $('elapsed-detail').textContent = `${count(stats.elapsed_samples)} 个已计量成功样本`;
+ const unknown = stats.missing + stats.interrupted + stats.failed;
+ const coverage = stats.seconds ? Math.min(100, stats.covered_seconds / stats.seconds * 100) : 0;
+ const notice = $('notice');
+ notice.classList.toggle('warning', !stats.complete || !data.collector_live || stats.unknown_source > 0);
+ const collectorNotice = data.collector_live ? '' : '入口采集未连接不代表模型服务不可用;无法确认新请求已记录,后端状态仍可单独查看。';
+ let overload = '';
+ if (stats.overload_untracked_runs || stats.unclean_runs) {
+ const reasons = [stats.overload_untracked_runs ? '部分周期未细分' : '',
+ stats.unclean_runs ? '存在未正常结束周期' : ''].filter(Boolean).join('、');
+ overload = stats.overload_tracked_runs ?
+ ` · 相关采集周期内代理超载降级已记录至少 ${integer.format(stats.overload_rejected)} 次(${reasons})` :
+ ` · 相关采集周期内代理超载降级次数未知(${reasons})`;
+ } else if (stats.overload_tracked_runs) {
+ overload = ` · 相关采集周期内代理超载降级已记录 ${integer.format(stats.overload_rejected)} 次`;
+ }
+ notice.textContent = `${collectorNotice}完整用量 ${integer.format(stats.known)} 个请求 · 仅输入/输出 ${integer.format(stats.partial || 0)} 个 · 未计量 ${integer.format(unknown)} 个 · 未知来源 ${integer.format(stats.unknown_source)} 个 · 未正常结束或失联 ${integer.format(stats.unclean_runs)} 次 · 相关采集周期未完整计入 ${integer.format(stats.dropped)} 个${overload} · 采集进程运行覆盖 ${number.format(coverage)}%。仅统计采集器已写盘的请求记录;缓存命中率仅使用有缓存明细的请求。入口回退、网关内部重试及取消时的消耗可能无法观测,因此不是后端物理总消耗。${stats.complete ? '' : ' ≥ 表示已观测下限;无覆盖不视为零。'}`;
+ charts.forEach((chart) => renderPlot(chart, data.series_24h));
+ renderClients(data);
+ renderDays(data);
+ renderActive(data);
+ renderBackends(data);
+ renderSessions(data);
+ Object.values(tables).forEach(updateHeaders);
+}
+
+async function refresh() {
+ const current = ++revision;
+ if (pendingRefresh) pendingRefresh.abort();
+ const controller = new AbortController();
+ pendingRefresh = controller;
+ const timeout = setTimeout(() => controller.abort(), 15000);
+ try {
+ const query = new URLSearchParams({window: selected,
+ offset: String(tables.requests.offset), limit: String(tables.requests.size),
+ sort: tables.requests.sort, direction: tables.requests.direction,
+ session_offset: String(tables.sessions.offset), session_limit: String(tables.sessions.size),
+ session_sort: tables.sessions.sort, session_direction: tables.sessions.direction});
+ if (lastSnapshot && (tables.requests.offset > 0 || tables.sessions.offset > 0)) {
+ query.set('snapshot_at', String(lastSnapshot.at));
+ query.set('snapshot_id', String(lastSnapshot.id));
+ }
+ const response = await fetch(`/api/v5/stats?${query}`, {cache: 'no-store', signal: controller.signal});
+ if (!response.ok) throw new Error('unavailable');
+ const data = await response.json();
+ if (current === revision) {
+ lastSnapshot = {at: data.snapshot_at, id: data.snapshot_id};
+ render(data);
+ }
+ } catch (_) {
+ if (current !== revision) return;
+ if (activeExpiry) clearTimeout(activeExpiry);
+ activeExpiry = null;
+ if (backendExpiry) clearTimeout(backendExpiry);
+ backendExpiry = null;
+ $('status-dot').classList.remove('live');
+ $('status-label').textContent = '统计不可用';
+ $('updated').textContent = '请检查本机采集和账本';
+ ['total-usage', 'total-input', 'total-output', 'total-cached', 'total-uncached', 'total-tps',
+ 'overview-prefill-wait', 'overview-hit-rate'].forEach((id) => setTextIfPresent(id, '—'));
+ ['overview-wait-samples', 'overview-hit-samples'].forEach((id) => setTextIfPresent(id, '统计不可用'));
+ charts.forEach(({id}) => {
+ const svg = $(`chart-${id}-svg`), label = $(`chart-${id}`);
+ if (svg) { svg.replaceChildren(); attachTrendTooltip(svg, []); }
+ if (label) label.textContent = '暂无数据';
+ });
+ $('clients-body').replaceChildren();
+ $('days-body').replaceChildren();
+ empty($('clients-body'), 8, '统计不可用');
+ empty($('days-body'), 4, '统计不可用');
+ ['active-count', 'success-rate', 'requests-rate', 'first-p95', 'elapsed-p95'].forEach((id) => { $(id).textContent = '—'; });
+ [['active-body', 8], ['backends-body', 7], ['session-summary-body', 13], ['sessions-body', 14]].forEach(([id, columns]) => {
+ $(id).replaceChildren(); empty($(id), columns, '统计不可用');
+ });
+ $('notice').classList.add('warning');
+ $('notice').textContent = '查询超时或无法读取统计。历史缺失与采集故障不会显示为零。';
+ Object.values(tables).forEach((table) => updatePager(table, 0, false));
+ } finally {
+ clearTimeout(timeout);
+ if (pendingRefresh === controller) pendingRefresh = null;
+ }
+}
+
+document.querySelectorAll('[data-window]').forEach((button) => button.addEventListener('click', () => {
+ selected = button.dataset.window;
+ Object.values(tables).forEach((table) => { table.offset = 0; });
+ lastSnapshot = null;
+ document.querySelectorAll('[data-window]').forEach((item) => item.classList.toggle('is-active', item === button));
+ refresh();
+}));
+$('refresh').addEventListener('click', () => {
+ Object.values(tables).forEach((table) => { table.offset = 0; table.localData = null; });
+ lastSnapshot = null; refresh();
+});
+setupTables();
+setupNavigation();
+$('telemetry-node')?.addEventListener('change', (event) => {
+ telemetryNodePort = event.target.value === 'all' ? 'all' : Number(event.target.value);
+ if (telemetrySnapshot) renderTelemetry(telemetrySnapshot);
+});
+document.addEventListener('visibilitychange', () => {
+ if (!document.hidden) {
+ if (telemetrySnapshot) renderTelemetry(telemetrySnapshot);
+ refreshTelemetry();
+ }
+});
+refresh();
+refreshTelemetry();
+setInterval(() => { if (!pendingRefresh) refresh(); }, 10000);
+setInterval(refreshActive, 2000);
+setInterval(refreshTelemetry, 15000);
diff --git a/deploy_stats/static/index.html b/deploy_stats/static/index.html
new file mode 100644
index 0000000..646515a
--- /dev/null
+++ b/deploy_stats/static/index.html
@@ -0,0 +1,78 @@
+
+
+
+
+
+ 模型用量监控
+
+
+
+
+
+
+
+
+
+
MODEL OBSERVABILITY
模型用量监控
按最终响应统计请求来源、Token 用量与缓存复用情况。
+
+
正在读取采集状态…
+
默认汇总全部设备组,也可选择单个后端;入口请求用量不受此选择影响
+
PREFILL
+
+
Prefill 并发吞吐(采样)↘
—等待引擎采样
+
Prefill 平均等待(估算)◷
—等待可测样本
+
Prefill-MFU(单步估算)%
—等待引擎采样
+
缓存命中率◈
—等待已计量样本
+
+
DECODE
+
+
Decode 并发吞吐(采样)↗
—等待引擎采样
+
Decode-MBU(纯阶段)%
—尚未接入阶段带宽归因
+
HBM 带宽利用率(设备采样)%
—等待设备采样
+
推测解码接受率(MTP 等)%
—等待 Decode 指标
+
+
并发吞吐是相同日志 step 的各 DP rank 单步速率近似和。跨组视图对有效采样求和,缺失组不参与并标注覆盖数;利用率只对有效组加权。各组并非同时测量,两个阶段也可能来自不同时刻。
+
Prefill-MFU 按引擎吞吐和模型假设估算,并非硬件算力实测;HBM 为设备级混合阶段采样,无法直接归因 Decode。
+
ENGINE & NPU引擎与设备采样
按实例分别展示,避免把不同设备组或未对齐的采样合并。
等待采样| 实例 | Prefill 吞吐 | Decode 吞吐 | Prefill-MFU | HBM 带宽 | Prefill 阶段耗时 | Decode 阶段耗时 | 推测解码接受率 | 状态 |
|---|
| 等待采样 |
+
+
总 TokenΣ
—输入 + 输出,缓存不重复计算
+
输入 Token↘
—包含缓存命中的输入
+
输出 Token↗
—已完成并取得用量的请求
+
缓存命中◈
—命中率 —
+
未命中输入(估算)↘
—已知缓存的输入 − 命中,非引擎实际计算量
+
窗口输出速度⌁
—输出 Token / 窗口秒数
+
+
+
当前在途请求◉
—仅采集已识别请求
+
可判定成功率✓
—已排除结果未知的请求
+
已观测请求速率↗
—可见结束请求 / 窗口秒数
+
首可见增量 p95◷
—入口观测,非纯 prefill
+
入口内耗时 p95◷
—已计量成功请求
+
+
LIVE REQUESTS当前在途连接
仅显示采集器已识别的推理请求;切换前或绕过采集入口的连接不可见。
—| 请求 IP | 会话指纹 | 已用时 | 阶段 | 输入 | 输出 | 命中率 | 输出 tok/s |
|---|
| 等待采集 |
+
BACKENDS后端运行状态
后端原生指标的范围可能包含其他入口,与入口观察范围不同。
LIVE METRICS| 实例 | 运行请求 | 等待请求 | 可采集 worker | 接入状态 | 累计输入 | 累计缓存命中率 |
|---|
| 等待采集 |
+
SESSIONS会话汇总
按网关会话口径和认证主体生成哈希指纹;无法识别的请求显示未知。
—| 会话指纹 | 来源 IP | 请求 / 完整 / 部分 | 输入 | 输出 | 缓存命中 | 未命中输入* | 命中率 | 首增量等待之和 | 后段时间之和 | Prefill 估算 (tok/s)* | Decode 估算 (tok/s)* | 等待/后段占比 |
|---|
| 暂无会话 |
+
SESSION REQUESTS会话请求记录
每行一个请求;无法识别会话时显示未知。
| 完成时间 | 请求 IP | 会话指纹 | 状态 | 输入 | 输出 | 缓存命中 | 未命中输入* | 命中率 | 首增量等待 | 首增量后 | Prefill 估算 (tok/s)* | Decode 估算 (tok/s)* | 等待/后段占比 |
|---|
| 暂无记录 |
+
TREND最近 24 小时
用量按请求完成时间分桶;速度随设备组选择切换。
10 分钟分桶
+ 24 小时前12 小时前现在
+
+
CLIENTS请求来源
按当前所选时间范围统计。经 Caddy 的请求显示其直接对端;本机直连网关的请求可伪造来源头。
0 个来源| 请求 IP | 请求数 | 总用量 | 输入 | 输出 | 缓存命中 | 请求均速 | 窗口 tok/s |
|---|
| 暂无统计 |
+
+
+
+
+
+
+
diff --git a/deploy_stats/store.py b/deploy_stats/store.py
new file mode 100644
index 0000000..52dff28
--- /dev/null
+++ b/deploy_stats/store.py
@@ -0,0 +1,244 @@
+"""Asynchronous, local-only storage of request accounting metadata."""
+from pathlib import Path
+import queue
+import sqlite3
+import threading
+import time
+import uuid
+
+
+SCHEMA = 2
+IDENTITY = 'llmdeploy-gateway-stats'
+PARTIAL_MARKER = 'partial_usage_schema'
+
+
+def partial_schema_available(db):
+ marker = db.execute('SELECT value FROM stats_meta WHERE key=?', (PARTIAL_MARKER,)).fetchone()
+ exists = db.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name='stats_partial_usage'").fetchone()
+ if marker is None and exists is None:
+ return False
+ if marker != ('1',) or exists is None:
+ raise ValueError('PARTIAL_USAGE_SCHEMA_MISMATCH')
+ columns = [(row[1], row[2].upper(), row[5]) for row in db.execute('PRAGMA table_info(stats_partial_usage)')]
+ if columns != [('request_id', 'INTEGER', 1), ('input_tokens', 'INTEGER', 0), ('output_tokens', 'INTEGER', 0)]:
+ raise ValueError('PARTIAL_USAGE_SCHEMA_MISMATCH')
+ return True
+
+
+def _enable_partial_schema(db):
+ if partial_schema_available(db):
+ return
+ db.execute('''CREATE TABLE stats_partial_usage (
+ request_id INTEGER PRIMARY KEY REFERENCES requests(id),
+ input_tokens INTEGER NOT NULL CHECK(input_tokens >= 0),
+ output_tokens INTEGER NOT NULL CHECK(output_tokens >= 0))''')
+ db.execute('INSERT INTO stats_meta(key,value) VALUES (?,?)', (PARTIAL_MARKER, '1'))
+
+
+def _open(path):
+ path = Path(path)
+ if not path.parent.is_dir():
+ raise ValueError('STATS_DIRECTORY_MISSING')
+ db = sqlite3.connect(path, timeout=1)
+ try:
+ version = db.execute('PRAGMA user_version').fetchone()[0]
+ tables = db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").fetchall()
+ if version not in (0, 1, SCHEMA) or version == 0 and tables:
+ raise ValueError('STATS_DATABASE_SCHEMA_MISMATCH')
+ if version in (1, SCHEMA):
+ try:
+ marker = db.execute("SELECT value FROM stats_meta WHERE key='application'").fetchone()
+ except sqlite3.Error as error:
+ raise ValueError('STATS_DATABASE_SCHEMA_MISMATCH') from error
+ if marker != (IDENTITY,):
+ raise ValueError('STATS_DATABASE_SCHEMA_MISMATCH')
+ db.execute('PRAGMA journal_mode=WAL')
+ db.execute('PRAGMA synchronous=NORMAL')
+ if version == 0:
+ db.executescript('''
+ CREATE TABLE stats_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
+ INSERT INTO stats_meta VALUES ('application','llmdeploy-gateway-stats');
+ CREATE TABLE runs (
+ id TEXT PRIMARY KEY, started REAL NOT NULL, last_seen REAL NOT NULL,
+ ended REAL, dropped INTEGER NOT NULL DEFAULT 0
+ );
+ CREATE TABLE requests (
+ id INTEGER PRIMARY KEY, run_id TEXT NOT NULL, finished REAL NOT NULL,
+ client_ip TEXT, elapsed REAL NOT NULL, http_status INTEGER,
+ state TEXT NOT NULL CHECK(state IN ('known','missing','failed','interrupted')),
+ input_tokens INTEGER, output_tokens INTEGER, cached_tokens INTEGER,
+ session_id TEXT, first_visible REAL,
+ CHECK(state != 'known' OR (input_tokens IS NOT NULL AND output_tokens IS NOT NULL
+ AND cached_tokens IS NOT NULL AND input_tokens >= 0 AND output_tokens >= 0
+ AND cached_tokens >= 0 AND cached_tokens <= input_tokens)),
+ CHECK(state = 'known' OR (input_tokens IS NULL AND output_tokens IS NULL AND cached_tokens IS NULL))
+ );
+ CREATE INDEX requests_finished ON requests(finished);
+ CREATE TABLE active_requests (
+ id TEXT PRIMARY KEY, run_id TEXT NOT NULL, started REAL NOT NULL,
+ client_ip TEXT, session_id TEXT, first_visible REAL
+ );
+ CREATE TABLE active_samples (run_id TEXT PRIMARY KEY, seen REAL NOT NULL);
+ PRAGMA user_version=2;
+ ''')
+ db.commit()
+ elif version == 1:
+ db.execute('BEGIN IMMEDIATE')
+ db.execute('ALTER TABLE requests ADD COLUMN session_id TEXT')
+ db.execute('ALTER TABLE requests ADD COLUMN first_visible REAL')
+ db.execute('''CREATE TABLE active_requests (
+ id TEXT PRIMARY KEY, run_id TEXT NOT NULL, started REAL NOT NULL,
+ client_ip TEXT, session_id TEXT, first_visible REAL
+ )''')
+ db.execute('CREATE TABLE active_samples (run_id TEXT PRIMARY KEY, seen REAL NOT NULL)')
+ db.execute('PRAGMA user_version=2')
+ db.commit()
+ return db
+ except Exception:
+ db.close()
+ raise
+
+
+class Recorder:
+ def __init__(self, path, *, queue_size=4096, heartbeat=5, source=None):
+ if source is not None and source != 'gateway-inprocess-v1':
+ raise ValueError('INVALID_STATS_SOURCE')
+ self.path = Path(path)
+ self.source = source
+ self.run = uuid.uuid4().hex
+ self.started = time.time()
+ self.heartbeat = heartbeat
+ self.pending = queue.Queue(maxsize=queue_size)
+ self.stop = threading.Event()
+ self.ready = threading.Event()
+ self.lock = threading.Lock()
+ self.live_lock = threading.Lock()
+ self.live = {}
+ self.dropped = 0
+ self.failed = False
+ self.thread = threading.Thread(target=self._write, name='stats-writer', daemon=True)
+ self.thread.start()
+ if not self.ready.wait(3) or self.failed:
+ self.stop.set()
+ raise ValueError('STATS_DATABASE_UNAVAILABLE')
+
+ def _enqueue(self, item):
+ if self.failed or self.stop.is_set():
+ return False
+ try:
+ self.pending.put_nowait(item)
+ return True
+ except queue.Full:
+ self.note_drop()
+ return False
+
+ def begin(self, request_id, started, client_ip, session_id):
+ with self.live_lock:
+ self.live[request_id] = (started, client_ip, session_id, None)
+
+ def first_output(self, request_id, first_visible):
+ with self.live_lock:
+ item = self.live.get(request_id)
+ if item is not None:
+ self.live[request_id] = (*item[:3], first_visible)
+
+ def set_session(self, request_id, session_id):
+ with self.live_lock:
+ item = self.live.get(request_id)
+ if item is not None:
+ self.live[request_id] = (item[0], item[1], session_id, item[3])
+
+ def end(self, request_id):
+ with self.live_lock:
+ self.live.pop(request_id, None)
+
+ def submit(self, event, request_id=None):
+ return self._enqueue(('event', (request_id, event)))
+
+ def note_drop(self, count=1):
+ with self.lock:
+ self.dropped += count
+
+ def flush(self, timeout=3):
+ done = threading.Event()
+ try:
+ self.pending.put(('barrier', done), timeout=timeout)
+ except queue.Full:
+ return False
+ return done.wait(timeout) and not self.failed
+
+ def close(self, timeout=3):
+ self.stop.set()
+ self.thread.join(timeout)
+ return not self.thread.is_alive() and not self.failed
+
+ def _write(self):
+ db = None
+ try:
+ db = _open(self.path)
+ db.execute('INSERT INTO runs(id,started,last_seen) VALUES (?,?,?)',
+ (self.run, self.started, self.started))
+ db.execute('INSERT INTO active_samples(run_id,seen) VALUES (?,?)', (self.run, self.started))
+ if self.source is not None:
+ _enable_partial_schema(db)
+ db.execute('INSERT INTO stats_meta(key,value) VALUES (?,?)', ('source:' + self.run, self.source))
+ db.commit()
+ self.ready.set()
+ last_tick = time.monotonic()
+ last_snapshot = last_tick
+ while not self.stop.is_set() or not self.pending.empty():
+ try:
+ kind, item = self.pending.get(timeout=min(self.heartbeat, 0.2))
+ except queue.Empty:
+ kind, item = None, None
+ if kind == 'event':
+ request_id, event = item
+ event = tuple(event) + (None,) * (10 - len(event))
+ partial = event[4] == 'partial'
+ if partial:
+ if (self.source != 'gateway-inprocess-v1' or type(event[5]) is not int or
+ type(event[6]) is not int or event[5] < 0 or event[6] < 0 or event[7] is not None):
+ raise ValueError('INVALID_PARTIAL_USAGE')
+ input_tokens, output_tokens = event[5:7]
+ event = (*event[:4], 'missing', None, None, None, *event[8:])
+ cursor = db.execute('''INSERT INTO requests
+ (run_id,finished,client_ip,elapsed,http_status,state,
+ input_tokens,output_tokens,cached_tokens,session_id,first_visible)
+ VALUES (?,?,?,?,?,?,?,?,?,?,?)''', (self.run, *event))
+ if partial:
+ db.execute('INSERT INTO stats_partial_usage VALUES (?,?,?)',
+ (cursor.lastrowid, input_tokens, output_tokens))
+ db.commit()
+ elif kind == 'barrier':
+ item.set()
+ if time.monotonic() - last_tick >= self.heartbeat:
+ with self.lock:
+ dropped = self.dropped
+ db.execute('UPDATE runs SET last_seen=?,dropped=? WHERE id=?',
+ (time.time(), dropped, self.run))
+ db.commit()
+ last_tick = time.monotonic()
+ if time.monotonic() - last_snapshot >= 1:
+ with self.live_lock:
+ snapshot = list(self.live.items())
+ sampled_at = time.time()
+ db.execute('DELETE FROM active_requests WHERE run_id=?', (self.run,))
+ db.executemany('''INSERT INTO active_requests
+ (id,run_id,started,client_ip,session_id,first_visible) VALUES (?,?,?,?,?,?)''',
+ [(request_id, self.run, *item) for request_id, item in snapshot])
+ db.execute('UPDATE active_samples SET seen=? WHERE run_id=?', (sampled_at, self.run))
+ db.commit()
+ last_snapshot = time.monotonic()
+ with self.lock:
+ dropped = self.dropped
+ now = time.time()
+ db.execute('DELETE FROM active_requests WHERE run_id=?', (self.run,))
+ db.execute('UPDATE runs SET last_seen=?,ended=?,dropped=? WHERE id=?',
+ (now, now, dropped, self.run))
+ db.commit()
+ except Exception:
+ self.failed = True
+ self.ready.set()
+ finally:
+ if db is not None:
+ db.close()
diff --git a/deploy_stats/telemetry.py b/deploy_stats/telemetry.py
new file mode 100644
index 0000000..97e551f
--- /dev/null
+++ b/deploy_stats/telemetry.py
@@ -0,0 +1,502 @@
+"""Collect sanitized engine-step and device samples via read-only SSH probes."""
+import argparse
+from copy import deepcopy
+from concurrent.futures import ThreadPoolExecutor
+import hashlib
+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+import json
+import math
+from pathlib import Path
+import queue
+import re
+import sqlite3
+import subprocess
+import threading
+import time
+
+from .backends import read_backend
+from .telemetry_history import History
+
+
+SOURCE_KEYS = frozenset(('stats', 'backend', 'parallel', 'engine', 'sampler', 'native'))
+HOST = re.compile(r'[A-Za-z0-9_][A-Za-z0-9_.-]{0,63}@[A-Za-z0-9][A-Za-z0-9.-]{0,252}\Z')
+CONTAINER = re.compile(r'[A-Za-z0-9][A-Za-z0-9_.-]{0,127}\Z')
+SHA256 = re.compile(r'[0-9a-f]{64}\Z')
+MAX_AGE = 60.0
+MAX_LOG_SKEW = 5.0
+MAX_STEP_SPREAD = .10
+HBM_MAX_AGE = 90.0
+
+
+def finite(value, *, minimum=None, maximum=None):
+ try:
+ return (type(value) in (int, float) and math.isfinite(value) and
+ (minimum is None or value >= minimum) and (maximum is None or value <= maximum))
+ except OverflowError:
+ return False
+
+
+def validate_config(value):
+ if not isinstance(value, dict) or set(value) != {'interval_seconds', 'nodes'}:
+ raise ValueError('INVALID_TELEMETRY_CONFIG')
+ interval, nodes = value['interval_seconds'], value['nodes']
+ if not finite(interval, minimum=15, maximum=60) or not isinstance(nodes, list) or not 1 <= len(nodes) <= 8:
+ raise ValueError('INVALID_TELEMETRY_CONFIG')
+ ports, labels, targets = set(), set(), set()
+ fields = {'label', 'backend_port', 'ssh_target', 'container', 'source_sha256', 'rank_devices',
+ 'image_fingerprint', 'driver_sha256', 'prefill_flops_per_token', 'peak_flops_per_device'}
+ for node in nodes:
+ if not isinstance(node, dict) or set(node) != fields:
+ raise ValueError('INVALID_TELEMETRY_NODE')
+ label, port = node['label'], node['backend_port']
+ target, container, sources, devices = (node['ssh_target'], node['container'],
+ node['source_sha256'], node['rank_devices'])
+ if (not isinstance(label, str) or not 1 <= len(label) <= 32 or any(ord(c) < 32 for c in label) or
+ type(port) is not int or not 1 <= port <= 65535 or port in ports or label in labels or
+ not isinstance(target, str) or not HOST.fullmatch(target) or target in targets or
+ not isinstance(container, str) or not CONTAINER.fullmatch(container) or
+ not isinstance(node['image_fingerprint'], str) or not SHA256.fullmatch(node['image_fingerprint']) or
+ not isinstance(node['driver_sha256'], str) or not SHA256.fullmatch(node['driver_sha256']) or
+ not isinstance(sources, dict) or set(sources) != SOURCE_KEYS or
+ any(not isinstance(v, str) or not SHA256.fullmatch(v) for v in sources.values()) or
+ not isinstance(devices, list) or not 1 <= len(devices) <= 64 or
+ any(type(d) is not int or not 0 <= d <= 255 for d in devices) or len(set(devices)) != len(devices) or
+ not finite(node['prefill_flops_per_token'], minimum=1, maximum=1e30) or
+ not finite(node['peak_flops_per_device'], minimum=1, maximum=1e25)):
+ raise ValueError('INVALID_TELEMETRY_NODE')
+ ports.add(port)
+ labels.add(label)
+ targets.add(target)
+ return value
+
+
+def _phase_groups(rows, devices, started, observed):
+ """Align unique DP ranks by logger step; the result remains a sampled approximation."""
+ if not isinstance(rows, list) or len(rows) > 1024:
+ return {}
+ groups, duplicates = {}, set()
+ for row in rows:
+ if not isinstance(row, dict) or row.get('kind') != 'stats':
+ continue
+ rank, device, step, at = (row.get(k) for k in ('rank', 'device', 'step', 'at'))
+ if (type(rank) is not int or not 0 <= rank < len(devices) or type(device) is not int or device != devices[rank] or
+ type(step) is not int or step < 0 or not finite(at, minimum=started, maximum=observed + 2)):
+ continue
+ group = groups.setdefault(step, {})
+ if rank in group:
+ duplicates.add(step)
+ group[rank] = row
+ latest = {}
+ for step, group in groups.items():
+ if step in duplicates or set(group) != set(range(len(devices))):
+ continue
+ data = list(group.values())
+ at = [r['at'] for r in data]
+ if max(at) - min(at) > MAX_LOG_SKEW or observed - min(at) > MAX_AGE:
+ continue
+ phases = {r.get('is_prompt') for r in data}
+ if len(phases) != 1 or next(iter(phases)) not in (True, False) or any(type(r.get('is_prompt')) is not bool for r in data):
+ continue
+ if any(not finite(r.get('current_tps'), minimum=0, maximum=1e15) or
+ not finite(r.get('current_sps'), minimum=.001, maximum=1e9) for r in data):
+ continue
+ rates = [r['current_sps'] for r in data]
+ # cur_SPS is printed to three decimals; include the rounding interval.
+ spread = (max(rates) + .0005) / (min(rates) - .0005) - 1
+ if not finite(spread, maximum=MAX_STEP_SPREAD):
+ continue
+ phase = 'prefill' if data[0]['is_prompt'] else 'decode'
+ item = {'step': step, 'log_at': max(at), 'rank_count': len(data),
+ 'log_skew_seconds': max(at) - min(at), 'step_rate_spread': spread,
+ 'tokens_per_second': sum(r['current_tps'] for r in data),
+ '_rank_at': {rank: row['at'] for rank, row in group.items()}}
+ identity = [step, phase, sorted(item['_rank_at'].items())]
+ item['_observation_id'] = hashlib.sha256(json.dumps(identity, separators=(',', ':')).encode()).hexdigest()
+ if phase not in latest or item['log_at'] > latest[phase]['log_at']:
+ latest[phase] = item
+ return latest
+
+
+def _phase_timers(rows, step, devices, source_started, rank_times):
+ groups, duplicates = {}, set()
+ for row in rows:
+ if not isinstance(row, dict) or row.get('kind') != 'cpu' or row.get('step') != step:
+ continue
+ rank, at = row.get('rank'), row.get('at')
+ if (type(rank) is not int or not 0 <= rank < len(devices) or row.get('device') != devices[rank] or
+ rank not in rank_times or not finite(at, minimum=max(source_started, rank_times[rank]),
+ maximum=rank_times[rank] + MAX_LOG_SKEW)):
+ continue
+ if rank in groups:
+ duplicates.add(rank)
+ groups[rank] = row.get('timers')
+ result = {}
+ for phase in ('prefill', 'decode'):
+ pairs = []
+ for rank, timers in groups.items():
+ if rank in duplicates or not isinstance(timers, dict):
+ continue
+ timer = timers.get(phase)
+ if (isinstance(timer, dict) and finite(timer.get('mean_ms'), minimum=0, maximum=1e9) and
+ type(timer.get('calls')) is int and 0 < timer['calls'] < 10**9):
+ pairs.append((timer['mean_ms'], timer['calls']))
+ calls = sum(c for _, c in pairs)
+ result[phase] = {'mean_ms': sum(m*c for m, c in pairs) / calls if calls else None,
+ 'ranks': len(pairs), 'expected_ranks': len(devices)}
+ return result
+
+
+def summarize_node(raw, node, local_start, local_end):
+ result = {'label': node['label'], 'backend_port': node['backend_port'], 'received_at': local_end,
+ 'clock_ok': False, 'clock_uncertainty_seconds': None, 'source_ok': False, 'driver_ok': False,
+ 'prefill': None, 'decode': None,
+ 'hbm': None, 'phase_timers': None}
+ if not isinstance(raw, dict) or raw.get('schema') != 1:
+ return result
+ start, end, born = (raw.get(k) for k in ('started_at', 'finished_at', 'source_started_at'))
+ if (not finite(start) or not finite(end, minimum=start) or not finite(local_start) or
+ not finite(local_end, minimum=local_start) or end - start > 20 or
+ end - start > local_end - local_start + .1):
+ return result
+ # Remote start follows local send, and remote finish precedes local receipt.
+ # These two inequalities bound the unknown host clock offset without changing clocks.
+ low, high = end - local_end, start - local_start
+ if high < low:
+ return result
+ offset, uncertainty = (low + high) / 2, (high - low) / 2
+ result['clock_ok'] = True
+ result['clock_uncertainty_seconds'] = uncertainty
+ result['probe_wall_seconds'] = end - start
+ cost = raw.get('probe_cpu_seconds')
+ result['probe_cpu_seconds'] = cost if finite(cost, minimum=0, maximum=20) else None
+ identity, host, sources = raw.get('source_id'), raw.get('host_fingerprint'), raw.get('source_sha256')
+ if (not isinstance(identity, str) or not SHA256.fullmatch(identity) or
+ not isinstance(host, str) or not SHA256.fullmatch(host) or
+ raw.get('image_fingerprint') != node['image_fingerprint'] or
+ sources != node['source_sha256'] or raw.get('source_before_start') is not True or
+ not finite(born, maximum=start)):
+ return result
+ result['source_ok'] = True
+ result['host_fingerprint'] = host
+ result['source_id'] = identity
+ result['driver_ok'] = raw.get('driver_sha256') == node['driver_sha256']
+ devices = node['rank_devices']
+ result['assumptions'] = {'prefill_flops_per_token': node['prefill_flops_per_token'],
+ 'peak_flops_per_device': node['peak_flops_per_device']}
+ hbm = raw.get('hbm')
+ if result['driver_ok'] and isinstance(hbm, list) and len(hbm) == len(devices):
+ values = {}
+ for item in hbm:
+ if not isinstance(item, dict):
+ continue
+ device, left, right, value = (item.get('id'), item.get('started_at'), item.get('finished_at'),
+ item.get('hbm_bandwidth_percent'))
+ if (type(device) is not int or device in values or device not in devices or
+ not finite(left, minimum=start, maximum=end) or not finite(right, minimum=left, maximum=end) or
+ not finite(value, minimum=0, maximum=100)):
+ continue
+ values[device] = (left, right, value)
+ if set(values) == set(devices):
+ first, last = min(v[0] for v in values.values()), max(v[1] for v in values.values())
+ if last - first <= MAX_LOG_SKEW:
+ result['hbm'] = {'mean_percent': sum(v[2] for v in values.values()) / len(values),
+ 'device_count': len(values), 'sample_at': last - offset,
+ 'span_seconds': last - first,
+ 'valid_until': first - offset - uncertainty + HBM_MAX_AGE}
+ rows = raw.get('engine_rows')
+ groups = _phase_groups(rows, devices, born, end)
+ for phase in ('prefill', 'decode'):
+ result[phase] = groups.get(phase)
+ if result[phase] is not None:
+ result[phase]['log_at'] -= offset
+ result[phase]['valid_until'] = (result[phase]['log_at'] - result[phase]['log_skew_seconds'] -
+ uncertainty + MAX_AGE)
+ if result['prefill'] is not None:
+ rate = result['prefill']['tokens_per_second']
+ efficiency = rate * node['prefill_flops_per_token'] / (len(devices) * node['peak_flops_per_device'])
+ result['prefill']['mfu_estimate'] = efficiency if finite(efficiency, minimum=0) else None
+ result['prefill']['peak_flops'] = len(devices) * node['peak_flops_per_device']
+ steps = [v['step'] for v in groups.values()]
+ if steps and isinstance(rows, list):
+ latest = max(groups.values(), key=lambda value: value['log_at'])
+ result['phase_timers'] = _phase_timers(rows, latest['step'], devices, born, latest['_rank_at'])
+ result['phase_timers']['step'] = latest['step']
+ for value in groups.values():
+ value.pop('_rank_at', None)
+ return result
+
+
+def summarize(nodes, sampled_at):
+ """Keep independent device groups separate and reject duplicate host identities."""
+ nodes = deepcopy(nodes)
+ for node in nodes:
+ host, identity = node.get('host_fingerprint'), node.get('source_id')
+ duplicate = any(other is not node and ((host and other.get('host_fingerprint') == host) or
+ (identity and other.get('source_id') == identity)) for other in nodes)
+ if duplicate:
+ node.update(source_ok=False, duplicate_source=True, prefill=None, decode=None, hbm=None, phase_timers=None)
+ out = {'schema': 1, 'kind': 'deepinfer-telemetry', 'sampled_at': sampled_at, 'nodes': nodes,
+ 'prefill_tps': None, 'decode_tps': None,
+ 'prefill_mfu': None, 'hbm_percent': None}
+ if len(nodes) != 1 or not nodes[0].get('source_ok'):
+ return out
+ current = nodes[0]
+ for phase in ('prefill', 'decode'):
+ part = current.get(phase)
+ if isinstance(part, dict):
+ out[phase + '_tps'] = part['tokens_per_second']
+ if phase == 'prefill':
+ out['prefill_mfu'] = part['mfu_estimate']
+ if isinstance(current.get('hbm'), dict):
+ out['hbm_percent'] = current['hbm']['mean_percent']
+ return out
+
+
+def public_snapshot(value):
+ value = deepcopy(value)
+ for node in value.get('nodes', []):
+ node.pop('host_fingerprint', None)
+ node.pop('source_id', None)
+ for phase in ('prefill', 'decode'):
+ if isinstance(node.get(phase), dict):
+ node[phase].pop('_observation_id', None)
+ return value
+
+
+def ssh_sample(node, script):
+ start = time.time()
+ try:
+ before = read_backend(node['backend_port'])
+ args = ['ssh', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=5', '-o', 'ConnectionAttempts=1',
+ '-o', 'StrictHostKeyChecking=yes', node['ssh_target'], 'python3 - ' + node['container']]
+ result = subprocess.run(args, input=script, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
+ timeout=20, check=False, creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0))
+ end = time.time()
+ if result.returncode or len(result.stdout) > 2 * 1024 * 1024:
+ return summarize_node(None, node, start, end)
+ sampled = summarize_node(json.loads(result.stdout), node, start, end)
+ after = read_backend(node['backend_port'])
+ ready = all(value.get('available') is True and value.get('admission_ready') == 1 and
+ value.get('workers_alive') == len(node['rank_devices']) for value in (before, after))
+ sampled['backend_ready'] = ready
+ if not ready:
+ sampled.update(prefill=None, decode=None, phase_timers=None)
+ return sampled
+ except (OSError, UnicodeError, ValueError, subprocess.SubprocessError):
+ return summarize_node(None, node, start, time.time())
+
+
+class HistoryWorker:
+ """Slow storage can never block the live sampling or HTTP threads."""
+
+ def __init__(self, config, history=None, path=None):
+ self.configs = {node['backend_port']: node for node in config['nodes']}
+ self.history, self.path = history, path
+ self.queue = queue.Queue(maxsize=1)
+ self.lock = threading.Lock()
+ self.cache = None
+ self.dropped = 0
+ self.changed = threading.Event()
+ self.stop_event = threading.Event()
+ self.thread = threading.Thread(target=self.run, daemon=True, name='telemetry-history')
+ self.thread.start()
+
+ def submit(self, value):
+ try:
+ self.queue.put_nowait(deepcopy(value))
+ except queue.Full:
+ with self.lock:
+ self.dropped += 1
+
+ def current(self):
+ with self.lock:
+ return deepcopy(self.cache), self.dropped
+
+ def run(self):
+ while not self.stop_event.is_set():
+ try:
+ value = self.queue.get(timeout=.2)
+ except queue.Empty:
+ continue
+ try:
+ if self.history is None:
+ self.history = open_history(self.path)
+ if self.history is None:
+ raise ValueError('TELEMETRY_HISTORY_UNAVAILABLE')
+ result = {}
+ for node in value['nodes']:
+ config = self.configs[node['backend_port']]
+ self.history.record(node, config, value['sampled_at'])
+ host = node.get('host_fingerprint') if node.get('source_ok') else None
+ result[node['backend_port']] = {'host': host,
+ 'history': self.history.series(config, host, value['sampled_at']) if host else None}
+ with self.lock:
+ self.cache = {'sampled_at': value['sampled_at'], 'nodes': result}
+ except (OSError, sqlite3.Error, ValueError, OverflowError):
+ with self.lock:
+ self.cache = None
+ finally:
+ self.changed.set()
+ self.queue.task_done()
+
+ def close(self):
+ self.stop_event.set()
+ self.thread.join(timeout=1)
+
+
+class Collector:
+ def __init__(self, config, sample=ssh_sample, history=None, history_path=None):
+ self.config = validate_config(config)
+ self.sample = sample
+ self.script = Path(__file__).with_name('remote_observe.py').read_bytes()
+ self.lock = threading.Lock()
+ self.value = summarize([], 0)
+ self.history_worker = (HistoryWorker(self.config, history=history, path=history_path)
+ if history is not None or history_path is not None else None)
+ self.stop_event = threading.Event()
+
+ def collect(self):
+ with ThreadPoolExecutor(max_workers=len(self.config['nodes'])) as executor:
+ nodes = list(executor.map(lambda node: self.sample(node, self.script), self.config['nodes']))
+ value = summarize(nodes, time.time())
+ with self.lock:
+ self.value = value
+ if self.history_worker is not None:
+ self.history_worker.submit(value)
+ return value
+
+ def snapshot(self):
+ with self.lock:
+ value = deepcopy(self.value)
+ now = time.time()
+ age = now - value['sampled_at']
+ if not 0 <= age <= 2 * self.config['interval_seconds'] + 20:
+ return summarize([], 0)
+ for node in value['nodes']:
+ uncertainty = node.get('clock_uncertainty_seconds')
+ for phase in ('prefill', 'decode'):
+ part = node.get(phase)
+ if (part is not None and (not finite(uncertainty, minimum=0) or
+ not finite(part.get('valid_until')) or now >= part['valid_until'])):
+ node[phase] = None
+ part = node.get('hbm')
+ if (part is not None and (not finite(uncertainty, minimum=0) or
+ not finite(part.get('valid_until')) or now >= part['valid_until'])):
+ node['hbm'] = None
+ timers = node.get('phase_timers')
+ valid_steps = {node[p]['step'] for p in ('prefill', 'decode') if node.get(p) is not None}
+ if isinstance(timers, dict) and timers.get('step') not in valid_steps:
+ node['phase_timers'] = None
+ value = summarize(value['nodes'], value['sampled_at'])
+ cached, dropped = self.history_worker.current() if self.history_worker is not None else (None, 0)
+ history_ok = (isinstance(cached, dict) and finite(cached.get('sampled_at')) and
+ 0 <= now - cached['sampled_at'] <= 2 * self.config['interval_seconds'] + 20)
+ value['history_available'] = bool(history_ok)
+ value['history_dropped'] = dropped
+ if history_ok:
+ for node in value['nodes']:
+ entry = cached['nodes'].get(node['backend_port'])
+ node['history'] = (entry['history'] if entry and node.get('source_ok') and
+ entry['host'] == node.get('host_fingerprint') else None)
+ return value
+
+ def close(self):
+ self.stop_event.set()
+ if self.history_worker is not None:
+ self.history_worker.close()
+
+ def run(self):
+ while not self.stop_event.is_set():
+ try:
+ self.collect()
+ except Exception:
+ with self.lock:
+ self.value = summarize([], 0)
+ self.stop_event.wait(self.config['interval_seconds'])
+
+
+class Server(ThreadingHTTPServer):
+ daemon_threads = True
+ allow_reuse_address = False
+
+ def __init__(self, port, collector):
+ if type(port) is not int or not 0 <= port <= 65535:
+ raise ValueError('INVALID_PORT')
+ self.collector = collector
+ super().__init__(('127.0.0.1', port), Handler)
+
+ def server_bind(self):
+ import socket
+ if hasattr(socket, 'SO_EXCLUSIVEADDRUSE'):
+ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1)
+ super().server_bind()
+
+ def handle_error(self, request, client_address):
+ pass
+
+
+class Handler(BaseHTTPRequestHandler):
+ def log_message(self, format, *args):
+ pass
+
+ def do_GET(self):
+ hosts = self.headers.get_all('Host', [])
+ origins = self.headers.get_all('Origin', [])
+ allowed = {f'127.0.0.1:{self.server.server_port}', f'localhost:{self.server.server_port}'}
+ trusted = (len(hosts) == 1 and hosts[0].lower() in allowed and len(origins) <= 1 and
+ (not origins or origins[0].lower() == 'http://' + hosts[0].lower()) and
+ self.headers.get('Sec-Fetch-Site', '').lower() not in ('cross-site', 'same-site'))
+ if not trusted:
+ status, body = 403, b'Forbidden'
+ elif self.path != '/snapshot':
+ status, body = 404, b'Not found'
+ else:
+ status = 200
+ body = json.dumps(public_snapshot(self.server.collector.snapshot()), allow_nan=False, separators=(',', ':')).encode()
+ self.send_response(status)
+ self.send_header('Content-Type', 'application/json' if status == 200 else 'text/plain')
+ self.send_header('Content-Length', str(len(body)))
+ self.send_header('Cache-Control', 'no-store')
+ self.end_headers()
+ self.wfile.write(body)
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--config', required=True, type=Path)
+ parser.add_argument('--port', type=int, default=19883)
+ parser.add_argument('--history-database', type=Path)
+ parser.add_argument('--once', action='store_true')
+ args = parser.parse_args()
+ config = json.loads(args.config.read_text(encoding='utf-8'))
+ collector = Collector(config, history_path=args.history_database)
+ if args.once:
+ collector.collect()
+ if collector.history_worker is not None:
+ collector.history_worker.changed.wait(timeout=3)
+ print(json.dumps(public_snapshot(collector.snapshot()), allow_nan=False, separators=(',', ':')))
+ collector.close()
+ return
+ server = Server(args.port, collector)
+ thread = threading.Thread(target=collector.run, daemon=True, name='telemetry-collector')
+ thread.start()
+ try:
+ server.serve_forever()
+ except KeyboardInterrupt:
+ pass
+ finally:
+ collector.close()
+ server.server_close()
+
+
+def open_history(path):
+ if path is None:
+ return None
+ try:
+ return History(path)
+ except (OSError, sqlite3.Error, ValueError, OverflowError):
+ return None
+
+
+if __name__ == '__main__':
+ main()
diff --git a/deploy_stats/telemetry_history.py b/deploy_stats/telemetry_history.py
new file mode 100644
index 0000000..e0c25c6
--- /dev/null
+++ b/deploy_stats/telemetry_history.py
@@ -0,0 +1,127 @@
+"""Persist bounded, device-group step-rate samples without inventing missing data."""
+import hashlib
+import json
+import math
+import os
+from contextlib import closing
+from pathlib import Path
+import re
+import sqlite3
+
+
+APPLICATION_ID = 0x4C445448
+SCHEMA_VERSION = 1
+BUCKET_SECONDS = 600
+BUCKET_COUNT = 144
+SOURCE = 'deepinfer-log-step-approx-v1'
+HASH = re.compile(r'[0-9a-f]{64}\Z')
+
+
+def _finite(value, minimum=None, maximum=None):
+ try:
+ return (type(value) in (int, float) and math.isfinite(value) and
+ (minimum is None or value >= minimum) and (maximum is None or value <= maximum))
+ except OverflowError:
+ return False
+
+
+def _config_key(config):
+ keys = ('backend_port', 'ssh_target', 'container', 'rank_devices', 'source_sha256',
+ 'image_fingerprint', 'driver_sha256')
+ value = {key: config[key] for key in keys}
+ return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':'), allow_nan=False).encode()).hexdigest()
+
+
+class History:
+ """One file, one writer; readers open their own SQLite connections."""
+
+ def __init__(self, path):
+ self.path = Path(path).resolve()
+ try:
+ fd = os.open(self.path, os.O_CREAT | os.O_EXCL | os.O_RDWR, 0o600)
+ except FileExistsError:
+ created = False
+ else:
+ os.close(fd)
+ created = True
+ with closing(self._connect()) as connection, connection:
+ if created:
+ connection.execute(f'PRAGMA application_id = {APPLICATION_ID}')
+ connection.execute(f'PRAGMA user_version = {SCHEMA_VERSION}')
+ connection.execute('''CREATE TABLE step_samples (
+ config_key TEXT NOT NULL,
+ host_key TEXT NOT NULL,
+ source_id TEXT NOT NULL,
+ phase TEXT NOT NULL CHECK (phase IN ('prefill', 'decode')),
+ observation_id TEXT NOT NULL,
+ step INTEGER NOT NULL,
+ bucket INTEGER NOT NULL,
+ rate REAL NOT NULL,
+ PRIMARY KEY (config_key, host_key, source_id, phase, observation_id)
+ ) WITHOUT ROWID''')
+ connection.execute('CREATE INDEX step_samples_by_bucket ON step_samples (config_key, host_key, bucket)')
+ elif (connection.execute('PRAGMA application_id').fetchone()[0] != APPLICATION_ID or
+ connection.execute('PRAGMA user_version').fetchone()[0] != SCHEMA_VERSION):
+ raise ValueError('INVALID_TELEMETRY_HISTORY')
+ # Only an identified history file may be modified.
+ connection.execute('PRAGMA journal_mode = WAL')
+
+ def _connect(self):
+ # mode=rw does not silently create a replacement if an existing database disappears.
+ return sqlite3.connect(self.path.as_uri() + '?mode=rw', uri=True, timeout=1)
+
+ def record(self, node, config, now):
+ if (not isinstance(node, dict) or node.get('source_ok') is not True or
+ node.get('backend_ready') is not True or node.get('clock_ok') is not True or
+ node.get('backend_port') != config.get('backend_port') or
+ not isinstance(node.get('host_fingerprint'), str) or not HASH.fullmatch(node['host_fingerprint']) or
+ not isinstance(node.get('source_id'), str) or not HASH.fullmatch(node['source_id']) or
+ not _finite(now, minimum=0) or
+ not _finite(node.get('clock_uncertainty_seconds'), minimum=0, maximum=20)):
+ return 0
+ uncertainty = node['clock_uncertainty_seconds']
+ rows = []
+ for phase in ('prefill', 'decode'):
+ part = node.get(phase)
+ if not isinstance(part, dict):
+ continue
+ at, skew, step, rate, until, identity = (part.get(k) for k in
+ ('log_at', 'log_skew_seconds', 'step', 'tokens_per_second', 'valid_until', '_observation_id'))
+ if (not _finite(at, minimum=0, maximum=now + uncertainty) or
+ not _finite(skew, minimum=0, maximum=5) or type(step) is not int or not 0 <= step < 2**63 or
+ not isinstance(identity, str) or not HASH.fullmatch(identity) or
+ not _finite(rate, minimum=0, maximum=1e15) or not _finite(until) or until <= now):
+ continue
+ first, last = at - skew - uncertainty, at + uncertainty
+ if first < 0 or int(first // BUCKET_SECONDS) != int(last // BUCKET_SECONDS):
+ continue
+ rows.append((_config_key(config), node['host_fingerprint'], node['source_id'], phase,
+ identity, step, int(first // BUCKET_SECONDS) * BUCKET_SECONDS, rate))
+ if not rows:
+ return 0
+ with closing(self._connect()) as connection, connection:
+ before = connection.total_changes
+ connection.executemany('INSERT OR IGNORE INTO step_samples VALUES (?, ?, ?, ?, ?, ?, ?, ?)', rows)
+ return connection.total_changes - before
+
+ def series(self, config, host_key, now):
+ if not isinstance(host_key, str) or not HASH.fullmatch(host_key) or not _finite(now, minimum=0):
+ return None
+ end = int(now // BUCKET_SECONDS) * BUCKET_SECONDS
+ start = end - BUCKET_COUNT * BUCKET_SECONDS
+ if start < 0:
+ return None
+ with closing(self._connect()) as connection, connection:
+ rows = connection.execute('''SELECT bucket, phase, SUM(rate), COUNT(*) FROM step_samples
+ WHERE config_key = ? AND host_key = ? AND bucket >= ? AND bucket < ?
+ GROUP BY bucket, phase''', (_config_key(config), host_key, start, end)).fetchall()
+ grouped = {(bucket, phase): (total, count) for bucket, phase, total, count in rows}
+ values = []
+ for bucket in range(start, end, BUCKET_SECONDS):
+ point = {'start': bucket, 'end': bucket + BUCKET_SECONDS}
+ for phase in ('prefill', 'decode'):
+ total, count = grouped.get((bucket, phase), (0, 0))
+ point[phase + '_tps'] = total / count if count and _finite(total, minimum=0) else None
+ point[phase + '_samples'] = count
+ values.append(point)
+ return {'method': SOURCE, 'bucket_seconds': BUCKET_SECONDS, 'points': values}
diff --git a/deploy_stats/web.py b/deploy_stats/web.py
new file mode 100644
index 0000000..b766740
--- /dev/null
+++ b/deploy_stats/web.py
@@ -0,0 +1,182 @@
+"""Serve the read-only dashboard exclusively on IPv4 loopback."""
+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+import json
+from pathlib import Path
+import socket
+import sqlite3
+from urllib.parse import parse_qs, urlsplit
+
+from .backends import read_backend, read_telemetry
+from .motor_metrics import MotorCollector, MotorHistory
+from .report import active_report, report
+
+STATIC = Path(__file__).resolve().parent / 'static'
+ASSETS = {'/': ('index.html', 'text/html; charset=utf-8'),
+ '/app.css': ('app.css', 'text/css; charset=utf-8'),
+ '/app-telemetry-v7.js': ('app.js', 'text/javascript; charset=utf-8')}
+
+
+class Dashboard(ThreadingHTTPServer):
+ daemon_threads = True
+ allow_reuse_address = False
+
+ def __init__(self, port, database, backend_ports=(), telemetry_port=None, *,
+ motor_management_ports=(), motor_history_path=None):
+ if type(port) is not int or not 0 <= port <= 65535:
+ raise ValueError('INVALID_PORT')
+ self.database = Path(database)
+ if len(backend_ports) > 8 or any(type(p) is not int or not 1 <= p <= 65535 for p in backend_ports):
+ raise ValueError('INVALID_BACKEND_PORTS')
+ self.backend_ports = tuple(dict.fromkeys(backend_ports))
+ if telemetry_port is not None and (type(telemetry_port) is not int or not 1 <= telemetry_port <= 65535):
+ raise ValueError('INVALID_TELEMETRY_PORT')
+ self.telemetry_port = telemetry_port
+ if motor_management_ports and (backend_ports or telemetry_port is not None):
+ raise ValueError('MIXED_BACKEND_SOURCES')
+ if motor_history_path is not None and not motor_management_ports:
+ raise ValueError('MOTOR_SOURCE_REQUIRED')
+ self.motor_collector = None
+ super().__init__(('127.0.0.1', port), Handler)
+ try:
+ history = MotorHistory(motor_history_path) if motor_history_path is not None else None
+ self.motor_collector = (MotorCollector(motor_management_ports, history=history)
+ if motor_management_ports else None)
+ if self.motor_collector is not None:
+ self.motor_collector.start()
+ except Exception:
+ super().server_close()
+ raise
+
+ def server_bind(self):
+ if hasattr(socket, 'SO_EXCLUSIVEADDRUSE'):
+ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1)
+ super().server_bind()
+
+ def handle_error(self, request, client_address):
+ pass
+
+ def server_close(self):
+ if self.motor_collector is not None:
+ self.motor_collector.close()
+ super().server_close()
+
+ def motor_snapshot(self):
+ value = self.motor_collector.snapshot()
+ value['available'] = value.get('sampled_at', 0) > 0
+ return value
+
+
+class Handler(BaseHTTPRequestHandler):
+ def log_message(self, format, *args):
+ pass
+
+ def trusted(self):
+ hosts = self.headers.get_all('Host', [])
+ allowed = {f'127.0.0.1:{self.server.server_port}', f'localhost:{self.server.server_port}'}
+ if len(hosts) != 1 or hosts[0].lower() not in allowed:
+ return False
+ origins = self.headers.get_all('Origin', [])
+ if len(origins) > 1 or origins and origins[0].lower() != 'http://' + hosts[0].lower():
+ return False
+ return self.headers.get('Sec-Fetch-Site', '').lower() not in ('cross-site', 'same-site')
+
+ def send(self, status, body, content_type):
+ self.send_response(status)
+ self.send_header('Content-Type', content_type)
+ self.send_header('Content-Length', str(len(body)))
+ self.send_header('Cache-Control', 'no-store')
+ self.send_header('X-Content-Type-Options', 'nosniff')
+ self.send_header('Content-Security-Policy', "default-src 'self'; base-uri 'none'; frame-ancestors 'none'")
+ self.end_headers()
+ self.wfile.write(body)
+
+ def do_GET(self):
+ if not self.trusted():
+ self.send(403, b'Forbidden', 'text/plain; charset=utf-8')
+ return
+ url = urlsplit(self.path)
+ if (self.server.motor_collector is not None and
+ url.path in ('/api/telemetry', '/api/active', '/api/stats')):
+ self.send(409, b'{"error":"DASHBOARD_RELOAD_REQUIRED"}', 'application/json; charset=utf-8')
+ return
+ if url.path in ASSETS:
+ name, media = ASSETS[url.path]
+ try:
+ self.send(200, (STATIC / name).read_bytes(), media)
+ except OSError:
+ self.send(503, b'Unavailable', 'text/plain; charset=utf-8')
+ return
+ if url.path in ('/api/telemetry', '/api/v5/telemetry'):
+ if url.query:
+ self.send(400, b'{"error":"INVALID_TELEMETRY_QUERY"}', 'application/json; charset=utf-8')
+ return
+ value = (self.server.motor_snapshot() if self.server.motor_collector is not None
+ else read_telemetry(self.server.telemetry_port))
+ body = json.dumps(value, allow_nan=False, separators=(',', ':')).encode()
+ self.send(200, body, 'application/json; charset=utf-8')
+ return
+ if url.path in ('/api/active', '/api/v5/active'):
+ if url.query:
+ self.send(400, b'{"error":"INVALID_ACTIVE_QUERY"}', 'application/json; charset=utf-8')
+ return
+ try:
+ value = active_report(self.server.database, source='gateway-inprocess-v1'
+ if self.server.motor_collector is not None else None)
+ body = json.dumps(value, ensure_ascii=False, allow_nan=False, separators=(',', ':')).encode()
+ self.send(200, body, 'application/json; charset=utf-8')
+ except (ValueError, sqlite3.Error, OSError, OverflowError):
+ self.send(503, b'{"error":"STATS_UNAVAILABLE"}', 'application/json; charset=utf-8')
+ return
+ if url.path in ('/api/stats', '/api/v5/stats'):
+ values = parse_qs(url.query, keep_blank_values=True)
+ defaults = {'window': '24h', 'offset': '0', 'limit': '100', 'sort': 'finished', 'direction': 'desc',
+ 'session_offset': '0', 'session_limit': None, 'session_sort': 'last_finished',
+ 'session_direction': 'desc', 'snapshot_at': None, 'snapshot_id': None}
+ if set(values) - set(defaults) or any(len(value) != 1 for value in values.values()):
+ self.send(400, b'{"error":"INVALID_WINDOW"}', 'application/json; charset=utf-8')
+ return
+ try:
+ params = {key: values[key][0] if key in values else default for key, default in defaults.items()}
+ if params['session_limit'] is None:
+ params['session_limit'] = params['limit']
+ if any(not params[key].isascii() or not params[key].isdigit() or len(params[key]) > 10
+ for key in ('offset', 'limit', 'session_offset', 'session_limit')):
+ raise ValueError('INVALID_STATS_PAGE')
+ if params['snapshot_id'] is not None and (not params['snapshot_id'].isascii() or
+ not params['snapshot_id'].isdigit() or len(params['snapshot_id']) > 19):
+ raise ValueError('INVALID_STATS_SNAPSHOT')
+ if params['snapshot_at'] is not None and len(params['snapshot_at']) > 32:
+ raise ValueError('INVALID_STATS_SNAPSHOT')
+ value = report(self.server.database, params['window'],
+ request_offset=int(params['offset']), request_limit=int(params['limit']),
+ session_offset=int(params['session_offset']), session_limit=int(params['session_limit']),
+ request_sort=params['sort'], request_dir=params['direction'],
+ session_sort=params['session_sort'], session_dir=params['session_direction'],
+ snapshot_at=float(params['snapshot_at']) if params['snapshot_at'] is not None else None,
+ snapshot_id=int(params['snapshot_id']) if params['snapshot_id'] is not None else None,
+ source='gateway-inprocess-v1' if self.server.motor_collector is not None else None)
+ if self.server.motor_collector is not None:
+ value['collector_source'] = 'gateway-inprocess-v1'
+ if self.server.motor_collector is None:
+ value['backends'] = [read_backend(port) for port in self.server.backend_ports]
+ else:
+ value['backends'] = []
+ for node in self.server.motor_snapshot().get('nodes', []):
+ metrics = node.get('engine_metrics') or {}
+ value['backends'].append({
+ 'port': node['backend_port'], 'label': node['label'],
+ 'available': node['source_ok'] and bool(metrics), 'running': metrics.get('running'),
+ 'waiting': metrics.get('waiting'), 'workers_alive': metrics.get('observed_endpoints'),
+ 'workers_expected': metrics.get('expected_endpoints'),
+ 'admission_ready': int(node['backend_ready']), 'input_total': metrics.get('input_total'),
+ 'hit_rate': metrics.get('hit_rate'), 'partial': metrics.get('partial', True),
+ 'valid_until': metrics.get('valid_until'),
+ })
+ body = json.dumps(value, ensure_ascii=False, allow_nan=False, separators=(',', ':')).encode()
+ self.send(200, body, 'application/json; charset=utf-8')
+ except ValueError:
+ self.send(400, b'{"error":"INVALID_OR_UNAVAILABLE_STATS"}', 'application/json; charset=utf-8')
+ except (sqlite3.Error, OSError, OverflowError):
+ self.send(503, b'{"error":"STATS_UNAVAILABLE"}', 'application/json; charset=utf-8')
+ return
+ self.send(404, b'Not found', 'text/plain; charset=utf-8')
diff --git a/docs/gateway.md b/docs/gateway.md
index efba0fe..5304efc 100644
--- a/docs/gateway.md
+++ b/docs/gateway.md
@@ -20,6 +20,10 @@ python -m deploy_gateway --key-store local/gateway-keys.json --bindings-file loc
DeepInfer 等自行调度 DP rank 的后端使用 `--backend-managed-affinity`:网关保留上述会话头和 `x-deepinfer-cache-namespace`,不注入 vLLM 的 `X-data-parallel-rank`。机器层绑定与原生 token 前缀缓存调度各自保留;机器故障或满载迁移后需要在新机器重新建立缓存。
+后端如在独立管理端口提供就绪检查,可重复设置 `--backend-readiness-port 19001=19101`,将推理端口映射到 loopback 管理端口。映射后网关请求管理端口的 `/readiness`,仅在 HTTP 200 且 JSON 的 `ready` 为 `true` 时认为该后端可用;其他后端继续使用其推理端口的 `/health`。MindIE Motor Coordinator 的推理和管理接口分离时应配置此映射,并使用 `--backend-managed-affinity`。
+
+严格 P/D 分离部署还应设置 `--backend-pd-model `,并为每个后端配置管理端口。网关会同时读取 `/instances`,要求该模型至少有一个 `active`、`available` 且 `healthy` 的 P 和 D;请求失败、响应无效或任一角色缺失时,将该协调器视为不可用。Motor 的 `ready` 单独为真不保证 P、D 同时存在。再设置 `--backend-pd-probe-endpoints` 后,网关还会对候选实例报告的每个 rank 请求 `/health`;该模式要求网关主机能直接访问这些端点。Motor 报告的健康状态可能滞后,端点探测也不能证明实际推理或集体通信正常;同一协调器内的故障实例摘除和恢复仍需另行验证。管理端口须限制为本机访问;该检查不提供 Coordinator 自身的注册状态持久化或入口高可用。
+
重复 `--backend-port` 可配置任意数量的 loopback 后端,替代默认的两个端口。例如三个后端使用 `--backend-port 19001 --backend-port 19002 --backend-port 19008`。`--max-active-per-backend` 默认 32;全池同时等待上限由 `--max-waiting` 控制,默认 32,最长等待由 `--queue-timeout` 控制,默认 5 秒。选机和占用配额在同一把锁内完成;全池满载、队列满额或等待超时返回 429 和 `Retry-After: 1`,没有健康且准入的后端返回 503。该等待时限不包含模型执行时间。请求体最多 64 MiB,读取请求体也计入服务请求槽位;客户端断开时关闭对应上游连接并释放在途配额。
多个公开端口必须由同一个反向代理转发至同一网关进程,才能共享流控和会话绑定;不要为每个入口各启动一套网关。反向代理需保留 SSE 实时传输、客户端取消和认证头,关闭包含请求或认证材料的访问日志。可先启动一个新入口,验收后排空旧入口,再通过代理的平滑重载将旧端口作为别名加入。
diff --git a/docs/usage.md b/docs/usage.md
index 4020466..c66f2de 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -40,3 +40,87 @@ SQLite 使用 WAL / NORMAL;已提交记录可跨普通进程退出保留,正
通知进程每约 30 秒在后台通过 SSH 只读查询,两台分别超时,查询不会阻塞健康观察。仅允许固定报告命令、已验证的路径与目标;远端结果仅提取有界数字,原始文本和异常不进入消息。以通知机本地接收时间判断缓存是否超过 90 秒,避免跨机时钟偏差造成误判。事件与窗口仍按部署机时间;与通知机的估计偏差超过 5 秒时显示提示,不自动修改系统时钟。
`@小鲁班 状态` 保留部署数量和健康状态,并按实例返回五个窗口的输入、输出、缓存、命中率、均速和生成速度。IP 仍只显示最后一段;查询游标、投递去重及失败不自动重发的合同不变。统计故障不等同于 vLLM 故障。
+
+## 统一入口的 IP 用量与本机页面
+
+**进程内采集候选仍在隔离验证,未经现场切换审查不要启用。** 候选在既有认证网关内观察已写出的响应,不另加 HTTP 转发跳;不改变原请求认证、后端选择或响应内容。采集记录器启动失败时网关按原路径运行,页面应显示采集失联;网关启动失败时记录器必须结束,避免空心跳。网关的拒绝体排空单独限制为最多 480 个在途连接、每连接最多 128 MiB 和 30 秒,超限或排空器无法接收时回到原关闭行为,不保证所有过载响应完整。切换前必须验证两个网关和监督进程的实际源码、端口及状态文件归属,由调用方暂停新请求,等待包括慢上传和排队在内的已接受请求全部结束后再关闭准入;否则关闭准入也可能拒绝已接受的请求。切换过程中仍可能短暂返回 502 或 503。Windows 独占绑定仅防止同地址监听冲突,不替代对全部监听地址、PID 和已接受连接的检查。原有入口如另有独立网关,需要分别验证和切换。
+
+进程内候选从 Caddy 默认生成的单值 `X-Forwarded-For` 获取直接对端 IP,忽略客户端可自行提供的 `X-LLMDeploy-Client-IP`。仅在已核实 Caddy 未配置可信代理、未改写此头并确由该 Caddy 转发的入口上使用;配置变化须重新验证。头缺失、多值或非法时显示未知。Caddy 前还有其他代理时,显示的是其直接对端而非自动追溯最终客户端;本机进程直接连接网关时可以伪造该头,不能把来源 IP 当作安全身份。
+
+候选的 32 MiB 限制是**保留原始采集缓冲**的合计,不是进程 RSS 上限;JSON 解码与 Python 对象会额外占用内存。单个事件或 JSON 响应超过 4 MiB、事件 data 行超过 4096、JSON 结构或转义工作超过保护上限,以及捕获资源不足时,相关统计保守显示未知,不影响原响应。超过 64 KiB 且未发现符合协议的最终用量候选的 SSE 事件可跳过解析:它会清除此前用量;只有之后重新观察到有效的协议结束用量及结束标记,才计量该请求。跳过之前若尚未确认首增量,首增量等待和速度也显示未知。此口径验证最终用量和结束顺序,不保证跳过的正文事件可解析;后端或协议版本变化时须重新核验。速度与原有账本一样是入口估算,不是引擎阶段实测。
+
+`deploy_stats` 是独立的入口统计组件,适用于固定 DeepInfer recipe API 的 Chat Completions、Responses 和 Messages JSON/SSE。设计上它放在 Caddy 与**现有认证网关之间**,转发请求和响应,从正常结束的原生 `usage` 提取输入、输出及缓存命中。页面和采集代理都只监听 `127.0.0.1`。这是与上面的 vLLM Stat Logger 分开的账本,不能把两者直接相加,也不自动补齐启用前的历史。
+
+**当前入口代理尚未通过部署审查,不要接入真实流量。** Windows 上,在请求体较大、网关延迟拒绝且未读取完整请求体时,连接复位可能导致错误响应正文截断。固定等待或预读不能保证保留未收到的字节。必须先修复并验证拒绝端关闭行为,或改用不增加此 HTTP 跳转的采集方式,再重新审查真实入口切换。
+
+Chat Completions 的最终流式用量语义来自[固定版本的 recipe 源码](https://github.com/deepseek-ai/deepseek-recipe/blob/935817c66f5d6e64838f419692545f06787e485f/deepseek-recipe/src/protocol/openai/chat_completion/response/chunk_generator.rs);更换后端或协议版本时须重新核对,不按旧版本假设填零。
+
+页面展示今天、滚动 24 小时、7 天和累计的总 Token、输入、输出、缓存命中、窗口输出 tok/s 与各请求 IP;另有最近 24 小时和近 14 日的统计。**总 Token = 输入 + 输出**,缓存命中是输入子集。窗口输出 tok/s 为已计量输出 / 窗口墙钟秒数,包含空闲时间,不是单请求解码速度。所有 Token 按请求完成时间入桶;24 小时曲线显示完整的 10 分钟区间,左侧 24 小时汇总包含未满区间。这不是逐 token 生成时刻的曲线,长请求在结束前没有最终用量。
+
+24 小时曲线展示输入和输出 Token、平均请求缓存命中率以及设备 Prefill 和 Decode 采样吞吐。每个区间的平均请求命中率是各已计量请求的 `缓存输入 / 输入` 的算术平均,输入为零的请求不计入;这与概览中 `缓存输入之和 / 输入之和` 的加权命中率不同。命中率汇总按请求数加权;设备吞吐曲线的计算和覆盖边界见下文。无可测样本显示未知;覆盖有缺口时均值仅代表已观测样本,不是全部请求的下限。
+
+左侧导航分别打开用量概览、当前连接、会话记录、请求来源、每日用量和后端状态;窄屏时导航显示在页面顶部。当前连接、会话汇总、请求记录、请求来源和每日用量分别支持每页 10、20 或 50 条,默认每页 20 条;点击表头可切换升序和降序。会话及请求在完整结果上排序后分页,未知值排在已知值之后。翻页时保留查询快照,避免新请求导致重复或漏行;会话和请求任一不在首页时共用同一快照。请求来源和每日用量在非首页时分别保留自身快照;概览和当前连接页每约 2 秒通过只读轻量接口查询当前连接,连接随采样变化,翻页结果可能变动;失联或快照超过 3 秒时显示未知并回到第一页。顶部刷新会回到首页并获取新快照;后端状态表支持排序。
+
+页面同时展示当前在途推理请求、可判定请求成功率和可见请求速率、入口观测的首个可见增量与代理内耗时 p95,以及按网关身份口径分组的会话汇总和逐请求记录。成功率只以已计量成功、明确失败和中断的请求为分母;结果未知的请求被排除,因此不是全部请求的成功率。指标选择参考 [vLLM 生产指标](https://docs.vllm.ai/en/latest/usage/metrics/)与 [NVIDIA 推理指标定义](https://docs.nvidia.com/nim/benchmarking/llm/metrics)。采集进程每秒把内存中的当前在途集合写成快照;快照超过 3 秒或心跳失联时显示未知。列表只覆盖经过本代理且请求头已识别的推理连接;客户端绕行、入口回退和切换前的连接不在其中。后端通常只在响应结束时提供用量,进行中的输入、输出、命中率和速度为未知而非零。
+
+新采集的会话依次使用 `x-session-affinity`、`x-deepseek-harness-session-id`、`x-session-id`,以及 JSON 的 `prompt_cache_key` 或首条用户内容,按网关的认证主体和身份规则计算哈希指纹。指纹不是加密:知道同一 API key 的人仍可能通过候选内容比对低熵输入,账本和页面必须限制访问。请求体只在内存中短暂处理,不写入账本或日志;原始字节缓冲单请求最多 1 MiB、全进程合计最多 32 MiB,JSON 解码和规范化还会产生额外临时内存。超限、请求体完整识别之前中断或无有效身份时显示未知;请求体已识别后取消,仍可能保留会话指纹。由首条用户内容推导的身份与网关一样只是近似:不同会话的首条内容相同或历史被压缩时可能合并或变化。历史上未识别的会话由于未保存请求体,无法回填。会话汇总只包含可识别会话,未计量用量及覆盖缺口显示为下限;命中率只针对已计量请求。页面可翻页查看各会话和请求,不保存原始会话标识或请求正文。
+
+当前后端没有导出逐请求引擎 prefill 或 decode 时间。对有非空文本、推理或工具增量的 SSE,代理可观测从入口接收请求到首个可见增量的时间,以及从首个增量到响应结束的时间;前者包含上传、排队、网络和首输出等,后者包含流式输出和响应收尾。页面提供(输入−缓存命中)/首增量等待、(输出−1)/首增量后时长及两段时间占比作为**入口估算**;首个增量可能包含多个 Token,后者不等于精确 decode tok/s。会话汇总的时间是可观测请求时间之和,并发时不能当作会话墙钟时间。非流式、没有可见增量或终止用量缺失时相关值显示未知。p50/p95 为窗口内可用样本的线性插值,不代表未覆盖请求。
+
+会话汇总的 Prefill、Decode 入口估算分别是可测成功请求的未命中输入或输出减一之和,除以相应入口时间之和,按请求耗时加权而非并发总吞吐;总览首增量等待为可测成功请求的算术平均。Prefill 速度排除等待时间为零的样本,Decode 速度排除输出不超过 1 或后段时间为零的样本。缓存命中率为已计量请求的缓存输入之和 / 输入之和。无可测样本时显示未知。
+
+总览可选接入独立的只读引擎和设备采集器。每个实例只有同一日志 step 收齐全部配置的 DP rank、阶段一致、设备映射正确且步速差和日志时间差在界限内时,才展示各 rank 当前步速率之和;这是日志采样近似,不是严格共同墙钟窗口的精确总吞吐。默认跨设备组视图仅相加有有效样本的实例,忽略缺失组并展示有效组数和配置组数,全部缺失时显示“—”;也可选择单个实例。各组并非同时测量,采样时间范围含误差超过 30 秒时显示跨度。跨组 Prefill-MFU 以有效组的设备峰值算力加权,驱动 HBM 百分比以有效组的设备数加权,不将百分比直接相加。日志时间是写出时间而非执行时间,异步队列可能延迟,且日志不包含 worker 启动标识;因此不能证明所展示 step 与当前工作进程的实时执行严格对应。推测解码和未暴露的回退路径均可能造成偏差,Decode 计数不保证等于客户端收到的 Token。
+
+Prefill-MFU 采用上述 Prefill 采样吞吐乘以操作者配置的模型 FLOP/token,除以对应设备理论峰值之和,仍为模型假设下的阶段估算,不是硬件 FLOP 实测;未经明确计入的 Attention 等计算不包含在内。设备 HBM 带宽百分比来自驱动的设备级采样,可能包含其他工作负载或混合阶段;目前没有可验证的 Decode 阶段带宽归因,因此 Decode-MBU 显示未知,而不把设备整体带宽冒充纯 Decode。表格中的阶段步耗时来自引擎 CPU 计时日志及其有效 rank,包含调度等开销,不是每请求 GPU/NPU kernel 耗时。
+
+采集器通过 SSH 只读获取严格数值化的引擎日志摘要和 DCMI 设备读数,不保存或向本机返回原始日志、请求内容或认证信息。需在私有配置中绑定后端端口、容器、rank 与设备映射、已核验的引擎源码与 native 哈希、镜像指纹和 DCMI 库哈希,以及模型 FLOP/token 和设备理论峰值;模型、版本或映射变化后应重新核对。SSH 使用现有密钥或代理,配置不放密码、密钥或其他凭据;机器标识和实际配置只放被忽略的私有目录。
+
+[采集配置示例](../hosts/telemetry.example.json)中的模型参数和哈希均为占位值,不能用于真实估算。复制并核验私有配置后,可用 `python -m deploy_stats.telemetry --config <私有配置> --once` 做单次只读预检;持续采集使用 `--port` 指定 loopback 端口,看板通过 `dashboard --telemetry-port <端口>` 读取该采集器。可另用 `--history-database <私有 SQLite 路径>` 保存设备采样历史。每个完整的 10 分钟桶只取已收到的各步速率采样均值,不是该桶 Token 数除以墙钟时长;跨组显示有样本组的同桶采样均值之和,并标出每桶有效组数,采样时刻不一定重合。空心点和虚线表示部分组缺失。跨组曲线的 24 小时数字是有采样的桶均值,覆盖组数可能随桶变化;单组则按采样数加权。全部缺失的桶留空,启用前的设备历史无法补齐。配置或主机身份变化后只展示当前身份的历史;文件不会自动删除旧记录,需自行监控磁盘空间。存储缓慢或不可用时实时采样继续运行,但历史采样可能缺失。入口用量曲线仍来自入口账本。
+
+这些数字仅来自客户端最终响应中可解析的用量。认证网关可能在响应头发出前重试另一后端;前一次失败尝试即使已经消耗 Token,也不会出现在最终响应的用量中,入口采集器无法感知或补计。取消或中断的请求也可能已在后端消耗 Token。因此,即使采集覆盖和可见响应完整,这些数值也不是后端物理总消耗或计费账本。
+
+请求中断、上游错误、未知协议、缺失或互相矛盾的用量字段记为未计量,不当成零。Chat 流的用量后若再出现其他数据事件,或 Messages 流的用量后再出现非 `message_stop` 事件,先前快照不再视为最终用量;只有随后再次收到有效用量并正常结束,才计量该请求。无采集覆盖显示空缺;`≥` 表示已观测下限,命中率仅针对已计量请求。统计通过异步队列写盘,页面只展示已提交数据,可能滞后于请求完成。队列满时记录丢失数并将相关采集运行周期视为不完整;运行周期与心跳用于标识中断,队列丢失数可能包含所选窗口以外的时间。采集进程失联或异常退出而未正常排空时,其运行周期内的统计也保守标为不完整,因为上次心跳前仍可能存在未写盘事件。SQLite 使用 WAL / NORMAL,异常退出或断电可能丢失尾部;这不是故障下逐 token 零丢失的计费账本。
+
+页面的运行覆盖只反映采集进程心跳,不证明 Caddy 已经切换或所有入口流量均经过该代理。启动代理但尚未切换、入口配置后来变化或客户端绕过入口时,仍可能有未记录流量;必须单独核验真实入口路由,不能将心跳 100% 当作入口完整性证明。示例配置将原认证网关作为回退入口:采集代理的连接失败或其他往返网络错误可能触发被动健康标记,使随后新请求暂时绕过采集器。回退期间的请求不会记入账本,即使采集进程随后恢复也无法补计。
+
+采集代理最多同时处理 480 个连接;Windows 下分派到三个独立事件循环,超限时尝试返回 429 并标记统计缺口。对于带请求体的请求,代理在转发请求体前会等待上游提前返回的认证或准入拒绝:4 KiB 以下最多约 10ms,4 KiB 及以上最多约 100ms。Windows 定时器精度可能使等待提前结束,固定等待不能消除所有拒绝与上传之间的 TCP 竞态。上线前必须核对连接上限不低于实际网关需要的并发与等待配额,并实测对首输出延迟的影响;扩大服务池后需重新评估,不能把连接数当作模型吞吐能力。
+
+账本只保留 IP、不可逆会话指纹、请求时间、HTTP 状态及数值用量,不保存认证信息、原始会话 ID、提示词、模型回答或原始 Token。IP、指纹和账本属于私有运行数据,目录需限制本机访问,只放忽略的 `local/` 或操作者自己的私有位置。旧版账本首次由采集器打开时会原地迁移并保留历史;旧记录没有阶段时间和会话指纹。页面拒绝跨站 Origin 和非 loopback Host,且不提供修改接口;同一台机器上有权限读取账本的用户仍可读取这些 IP。
+
+可选 `--backend-port` 指定本机回环地址上的 DeepInfer 原生 `/metrics` 端口,每个端口单独读取运行数、排队数、存活 worker、接入状态、累计输入和累计缓存命中。后端不可达、计数缺项或累计值不一致时显示未知;累计数按各后端进程生命周期统计,重启会重置。当前接口没有 KV 占用、设备利用率或逐请求引擎阶段时延,不以估算代替。
+
+## Motor 与 vLLM 指标
+
+Motor 部署可在现有网关命令中增加 `--statistics-database <私有账本>`,将响应观察器装入网关进程;不修改客户端请求、流内容或上游认证。账本打开失败时网关仍提供服务,采集不可用。只在 Caddy 已验证覆盖客户端转发地址且网关确为入口目标时,来源 IP 才有入口侧的可信来源;直接访问网关的客户端不享有该保证。实际流量是否经过已装配的网关仍需在上线后独立核验,采集器心跳不能证明入口接线。
+
+看板以 `dashboard --database <同一账本> --motor-management-port <管理端口> [--motor-management-port <管理端口>] --motor-history-database <新的私有历史文件>` 运行;Motor 模式不能同时使用旧的 `--backend-port` 或 `--telemetry-port`。管理端口必须通过本机回环隧道连接可信的 Motor 管理服务。看板每约 30 秒只读 `/instances` 和已注册 worker 的 `/metrics`,最多四个并发 worker 请求;采样前后核对实例角色、端点和就绪状态,管理端不一致、采样期间拓扑变化、worker 缺失或计数重置时保守标记部分或未知。两次拓扑读取一致仍不能证明整个间隔内没有发生过变化。速率按相同进程和相同指标序列的相邻计数差 / 墙钟间隔计算:Prefill 取本地实际计算输入,Decode 取生成 Token;两阶段不相加作为请求总量。后端计数的范围可能包含其他入口。
+
+通过导出进程启动时间和计数回退识别重启,但不能保证发现导出进程未退出的所有内部引擎重启。真实阶段耗时仅为 vLLM 相邻采样间请求阶段直方图的聚合均值,不是单会话时间。Prefix Cache 命中率按 Prefill 侧 Token 加权,不能替代入口每请求命中率的算术平均,也不把 Decode 的跨实例 KV 传输当作用户前缀复用。历史文件按十分钟保存速率采样均值,仅显示当前实例身份的历史;历史开始之前、采集故障和 worker 变化期间不补造数据。当前未采集到可归因的 NPU 阶段算力和 HBM 流量,Prefill-MFU、Decode-MBU 和 HBM 利用率保持未知。
+
+Decode 采集 vLLM 的草稿步数、草稿 Token 和接受的草稿 Token。推测解码草稿接受率 = 接受的草稿 Token / 草稿 Token;跨指标端点或设备组先分别相加分子和分母,再相除,不平均各组百分率。这些通用指标没有方法标签,可能是 MTP 或其他推测解码,不能仅凭指标断言方法。总览的累计值混合各进程各自启动以来的计数,进程重启会重置;最近 24 小时曲线则在现有私有历史库中按 10 分钟记录两次采样间的计数增量,归入采样结束时所在区间。历史增量不能证明整段时间连续覆盖,所以有数据的点也标记为时间覆盖未核验;计数重置、序列或模型不一致、无草稿、采集前的区间不补造数据。覆盖分母是当前 Motor 已注册的指标端点,不能证明包含所有 engine 或已离线设备组。后端计数可能包含其他入口,无法在并发下按请求、IP 或会话分摊;单请求 MTP 指标须由后端明确输出,不能用组计数差代替。
+
+Motor 设备组的 vLLM 数据只读取与注册模型名称一致的指标标签;模型变化时重新建立采样基线,并使用独立历史身份。旧历史未绑定模型,保留在数据库中但不自动归属新模型。某些引擎不提供注册模型名或对应标签时,设备指标显示未知。
+
+请求记录直接展示已保存的输入、输出和缓存命中。未命中输入仅在缓存明细已知时按输入减缓存命中计算;未知缓存不当作零。此值是入口响应口径的估算,不能保证等于引擎实际执行的 Prefill Token;后端 `local_compute` 计数是设备级数据,不可直接归因单个请求。老账本中已完整记录输入及缓存的请求可直接计算,无需改写历史行;部分用量或中断的请求不补造结果。
+
+Motor 模式只用新网关进程记录的来源标记判断当前采集心跳,旧采集器的心跳不会冒充现网心跳。心跳覆盖不证明入口路由覆盖,因此该模式下总量均标为已观测下限;历史账本继续保留,未能验证的时段不宣称完整。如果最终响应提供输入和输出但没有缓存明细,仅记录这两项并标为部分用量;缓存命中率的分母只包含有缓存明细的请求。流式响应未提供最终用量时请求标记为未计量,不能从引擎聚合计数反推每 IP 或会话的用量。
+
+Motor 模式使用 v7 页面脚本和 v5 接口;旧页面脚本的资源路径不可用,避免新表头与旧渲染逻辑混用。旧版页面继续轮询旧接口时会显示统计不可用,而不会把新计数器按旧日志口径展示。切换后用户需要刷新看板标签页。
+
+下面是**待单独授权部署**时的示例;目录、端口和原网关地址须先核实,命令不会由开发或测试自动执行:
+
+```powershell
+New-Item -ItemType Directory -Path local/stats -Force
+python -m deploy_stats proxy --port 19882 --upstream-port 19010 --database local/stats/usage.sqlite3
+python -m deploy_stats dashboard --port 19881 --database local/stats/usage.sqlite3 --backend-port 19001 --backend-port 19002
+```
+
+示例中的 `19010` 仅代表既有网关的 loopback 端口。[Caddy 示例](../services/deepinfer/Caddyfile.stats.example)仅供审阅。先在隔离环境验证代理,再按维护流程核实入口及现有连接,将 Caddy 的 `reverse_proxy` 首选目标改为 `127.0.0.1:19882`,连接失败时回退到同一个既有认证网关,并在该反代块内设置:
+
+```caddyfile
+header_up X-LLMDeploy-Client-IP {remote_host}
+```
+
+Caddy 必须覆盖客户端传入的同名头;采集器不信任客户端的 `X-Forwarded-For`,缺失、重复或非法来源显示为“未知来源”。`{remote_host}` 是 Caddy 的直接对端:若 Caddy 前还有其他代理,显示的是该代理 IP,需先明确可信代理链,不能自行相信转发头。回退只在尚未连接采集代理时重试 POST;不要配置会在已连接后重放 POST 的 `lb_retry_match`,不要对认证入口配置无凭据的主动健康探测。参见 Caddy 的[负载均衡与重试说明](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy#load-balancing)、[反向代理头部说明](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy#headers)和[请求占位符](https://caddyserver.com/docs/caddyfile/concepts#placeholders)。所有入口别名须采用相同配置,直接回退网关的请求不在统计范围。
+
+切换前保留原 Caddy 配置,验证认证失败控制、JSON/SSE 字节内容、并发、取消、大请求、来源 IP 和页面;核对源版本与真实用量字段,检查记录器心跳及缺失数。验证通过才恢复通知。回退时把 Caddy 的目标恢复到原网关,保留账本并停止本次采集/页面进程;回退期间形成统计空缺,不得视为零。上述均需另行授权,离线测试和合成预览不构成实际部署或真机验收。
diff --git a/hosts/telemetry.example.json b/hosts/telemetry.example.json
new file mode 100644
index 0000000..57725a6
--- /dev/null
+++ b/hosts/telemetry.example.json
@@ -0,0 +1,24 @@
+{
+ "interval_seconds": 30,
+ "nodes": [
+ {
+ "label": "示例实例",
+ "backend_port": 19001,
+ "ssh_target": "operator@example.invalid",
+ "container": "example-container",
+ "image_fingerprint": "0000000000000000000000000000000000000000000000000000000000000000",
+ "driver_sha256": "0000000000000000000000000000000000000000000000000000000000000000",
+ "source_sha256": {
+ "stats": "0000000000000000000000000000000000000000000000000000000000000000",
+ "backend": "0000000000000000000000000000000000000000000000000000000000000000",
+ "parallel": "0000000000000000000000000000000000000000000000000000000000000000",
+ "engine": "0000000000000000000000000000000000000000000000000000000000000000",
+ "sampler": "0000000000000000000000000000000000000000000000000000000000000000",
+ "native": "0000000000000000000000000000000000000000000000000000000000000000"
+ },
+ "rank_devices": [0],
+ "prefill_flops_per_token": 1,
+ "peak_flops_per_device": 1
+ }
+ ]
+}
diff --git a/services/deepinfer/Caddyfile.stats.example b/services/deepinfer/Caddyfile.stats.example
new file mode 100644
index 0000000..2b4e582
--- /dev/null
+++ b/services/deepinfer/Caddyfile.stats.example
@@ -0,0 +1,19 @@
+# Example only: both ingress aliases pass through one local statistics proxy.
+# Configure exposure/TLS separately and preserve the existing authenticated gateway.
+:19000, :19003 {
+ bind 127.0.0.1
+ request_body {
+ max_size 64MB
+ }
+ reverse_proxy 127.0.0.1:19882 127.0.0.1:19010 {
+ # Keep the original authenticated gateway available if the collector is
+ # marked unhealthy; requests on the fallback are not accounted.
+ lb_policy first
+ fail_duration 2s
+ lb_try_duration 6s
+ lb_try_interval 20ms
+ # Override any client-supplied value with Caddy's direct peer address.
+ header_up X-LLMDeploy-Client-IP {remote_host}
+ flush_interval 10ms
+ }
+}
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..8966260
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1 @@
+"""Local test package and shared integration helpers."""
diff --git a/tests/caddy_helpers.py b/tests/caddy_helpers.py
new file mode 100644
index 0000000..fd94db1
--- /dev/null
+++ b/tests/caddy_helpers.py
@@ -0,0 +1,50 @@
+import os
+from pathlib import Path
+import subprocess
+import unittest
+
+
+def synthetic_caddy_owns_listener(process, port):
+ """Check ownership before sending requests to a released test port."""
+ if process.poll() is not None:
+ return False
+ if os.name == 'nt':
+ result = subprocess.run(['netstat', '-ano', '-p', 'tcp'], capture_output=True,
+ timeout=10, creationflags=subprocess.CREATE_NO_WINDOW)
+ if result.returncode:
+ return False
+ listeners = []
+ for line in result.stdout.decode('ascii', errors='replace').splitlines():
+ fields = line.split()
+ if len(fields) != 5 or fields[0] != 'TCP' or fields[3] != 'LISTENING':
+ continue
+ address, separator, candidate_port = fields[1].rpartition(':')
+ if separator and candidate_port.isdigit() and int(candidate_port) == port:
+ listeners.append((address, fields[4]))
+ return listeners == [('127.0.0.1', str(process.pid))] and process.poll() is None
+ proc = Path('/proc')
+ if not (proc/'net/tcp').is_file():
+ raise unittest.SkipTest('synthetic Caddy listener ownership cannot be verified')
+ listeners = []
+ for name in ('tcp', 'tcp6'):
+ path = proc/'net'/name
+ if not path.is_file():
+ continue
+ for line in path.read_text().splitlines()[1:]:
+ fields = line.split()
+ address, candidate_port = fields[1].split(':')
+ if int(candidate_port, 16) == port and fields[3] == '0A':
+ listeners.append((name, address, fields[9]))
+ if len(listeners) != 1 or listeners[0][:2] != ('tcp', '0100007F'):
+ return False
+ try:
+ descriptors = list((proc/str(process.pid)/'fd').iterdir())
+ except OSError:
+ return False
+ for descriptor in descriptors:
+ try:
+ if os.readlink(descriptor) == f'socket:[{listeners[0][2]}]':
+ return process.poll() is None
+ except OSError:
+ continue
+ return False
diff --git a/tests/test_dashboard_efficiency.py b/tests/test_dashboard_efficiency.py
new file mode 100644
index 0000000..835dc0c
--- /dev/null
+++ b/tests/test_dashboard_efficiency.py
@@ -0,0 +1,538 @@
+import shutil
+import subprocess
+import unittest
+from pathlib import Path
+
+
+class DashboardEfficiencyTests(unittest.TestCase):
+ def test_motor_backend_row_expires_without_waiting_for_next_poll(self):
+ node = shutil.which('node')
+ if node is None:
+ self.skipTest('Node.js is required to validate dashboard calculations')
+ script = r'''
+const assert = require('node:assert/strict'), fs = require('node:fs'), vm = require('node:vm');
+const source = fs.readFileSync(process.argv[1], 'utf8');
+const start = source.indexOf('function renderBackends('), end = source.indexOf('\n}\n', start);
+assert(start >= 0 && end > start);
+let now = 100, timer;
+const body = {children: [], replaceChildren() {this.children=[];}, appendChild(row) {this.children.push(row);}};
+const context = {backendExpiry: null, Date: class extends Date {static now() {return now * 1000;}},
+ $: () => body, localPage: (_name, rows) => rows, document: {createElement: () => ({cells: []})},
+ cell: (row, value) => row.cells.push(value), count: String, percent: String, empty: () => {},
+ clearTimeout: () => {}, setTimeout: (fn) => {timer=fn; return 1;}};
+const render = vm.runInNewContext('(' + source.slice(start, end + 2) + ')', context);
+const data = {backends: [{port: 1, label: 'fixture', available: true, valid_until: 105,
+ running: 4, waiting: 0, workers_alive: 1, workers_expected: 1, admission_ready: 1, input_total: 10, hit_rate: .5}]};
+render(data); assert.equal(body.children[0].cells[1], '4');
+now = 106; timer(); assert.equal(body.children[0].cells[1], '不可用');
+'''
+ source = Path(__file__).resolve().parents[1]/'deploy_stats/static/app.js'
+ result = subprocess.run([node, '-e', script, str(source)], capture_output=True, text=True, encoding='utf-8', timeout=15)
+ self.assertEqual(result.returncode, 0, result.stderr)
+
+ def test_partial_usage_displays_input_output_but_not_unknown_cache(self):
+ node = shutil.which('node')
+ if node is None:
+ self.skipTest('Node.js is required to validate dashboard calculations')
+ script = r'''
+const assert = require('node:assert/strict'), fs = require('node:fs'), vm = require('node:vm');
+const source = fs.readFileSync(process.argv[1], 'utf8');
+const start = source.indexOf('function display('), end = source.indexOf('\n}\n', start);
+assert(start >= 0 && end > start);
+const display = vm.runInNewContext('(' + source.slice(start, end + 2) + ')',
+ {number: {format: String}, integer: {format: String}});
+const partial = {known: 0, partial: 1, complete: false};
+assert.equal(display(120, partial), '≥120');
+assert.equal(display(20, partial), '≥20');
+assert.equal(display(0, partial, false, false), '—');
+'''
+ source = Path(__file__).resolve().parents[1]/'deploy_stats/static/app.js'
+ result = subprocess.run([node, '-e', script, str(source)], capture_output=True, text=True, timeout=15)
+ self.assertEqual(result.returncode, 0, result.stderr)
+
+ def test_motor_rates_count_only_relevant_phase_groups_and_label_partial_workers(self):
+ node = shutil.which('node')
+ if node is None:
+ self.skipTest('Node.js is required to validate dashboard calculations')
+ script = r'''
+const assert = require('node:assert/strict'), fs = require('node:fs'), vm = require('node:vm');
+const source = fs.readFileSync(process.argv[1], 'utf8');
+function extract(name, context) {
+ const start = source.indexOf(`function ${name}(`), end = source.indexOf('\n}\n', start);
+ assert(start >= 0 && end > start); return vm.runInNewContext('(' + source.slice(start, end + 2) + ')', context);
+}
+const context = {};
+context.freshTelemetry = extract('freshTelemetry', context);
+const values = extract('telemetryValues', context);
+const base = {source_ok: true, backend_ready: true, clock_ok: true, clock_uncertainty_seconds: 0, expected_endpoints: 8};
+const part = (rate, valid, partial=false) => ({tokens_per_second: rate, valid_endpoints: valid,
+ expected_endpoints: 8, partial, log_at: 100, log_skew_seconds: 30, valid_until: 150});
+const data = {available: true, kind: 'vllm-motor-telemetry', nodes: [
+ {...base, backend_port: 1, role: 'prefill', prefill: part(20, 7, true)},
+ {...base, backend_port: 2, role: 'decode', decode: part(70, 8)}]};
+const result = values(data, 'all', 110);
+assert.equal(result.prefill, 20); assert.equal(result.decode, 70);
+assert.equal(result.coverage.prefill.total, 1); assert.equal(result.coverage.decode.total, 1);
+assert.equal(result.coverage.prefill.workers_valid, 7); assert.equal(result.coverage.prefill.workers_total, 8);
+assert.equal(result.coverage.prefill.partial, true); assert.equal(result.coverage.decode.partial, false);
+assert.equal(result.mfu, null); assert.equal(result.hbm, null);
+'''
+ source = Path(__file__).resolve().parents[1]/'deploy_stats/static/app.js'
+ result = subprocess.run([node, '-e', script, str(source)], capture_output=True, text=True, timeout=15)
+ self.assertEqual(result.returncode, 0, result.stderr)
+
+ def test_old_and_new_phase_cards_cannot_cross_write(self):
+ from deploy_stats.web import ASSETS
+ root = Path(__file__).resolve().parents[1]/'deploy_stats/static'
+ html = (root/'index.html').read_text(encoding='utf-8')
+ script = (root/'app.js').read_text(encoding='utf-8')
+ legacy_ids = ('overview-prefill-tps', 'overview-prefill-samples', 'overview-decode-tps',
+ 'overview-decode-samples', 'overview-mfu', 'overview-mfu-detail',
+ 'overview-mbu', 'overview-mbu-detail', 'overview-efficiency-note')
+ for legacy in legacy_ids:
+ self.assertNotIn(f'id="{legacy}"', html)
+ self.assertNotIn(f"'{legacy}'", script)
+ self.assertIn('/app-telemetry-v7.js', html)
+ self.assertIn('/app-telemetry-v7.js', ASSETS)
+ self.assertNotIn('/app-telemetry-v6.js', ASSETS)
+ self.assertNotIn('/app-telemetry-v5.js', ASSETS)
+ self.assertNotIn('/app-telemetry-v4.js', ASSETS)
+ self.assertNotIn('/app-telemetry-v3.js', ASSETS)
+ self.assertNotIn('/app-telemetry-v2.js', ASSETS)
+ self.assertIn('chart-device-prefill', html)
+ self.assertIn('chart-device-decode', html)
+ self.assertIn('value="all">跨设备组(全部)', html)
+ self.assertIn("let telemetryNodePort = 'all'", script)
+ self.assertNotIn('id="chart-prefill"', html)
+ self.assertNotIn('id="chart-decode"', html)
+ self.assertNotIn('/app-trends-v1.js', ASSETS)
+ self.assertNotIn('/app.js', ASSETS)
+
+ def test_telemetry_values_preserve_unknown_and_real_zero(self):
+ node = shutil.which('node')
+ if node is None:
+ self.skipTest('Node.js is required to validate dashboard calculations')
+ script = r'''
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const vm = require('node:vm');
+const source = fs.readFileSync(process.argv[1], 'utf8');
+const freshStart = source.indexOf('function freshTelemetry(');
+const freshEnd = source.indexOf('\n}\n', freshStart);
+assert(freshStart >= 0 && freshEnd > freshStart);
+const freshTelemetry = vm.runInNewContext('(' + source.slice(freshStart, freshEnd + 2) + ')');
+const start = source.indexOf('function telemetryValues(');
+const end = source.indexOf('\n}\n', start);
+assert(start >= 0 && end > start);
+const read = vm.runInNewContext('(' + source.slice(start, end + 2) + ')', {freshTelemetry});
+let data = {available: true, nodes: [{backend_port: 1, source_ok: true, backend_ready: true,
+ clock_ok: true, clock_uncertainty_seconds: .1,
+ prefill: {tokens_per_second: 0, mfu_estimate: .25, valid_until: 101},
+ decode: {tokens_per_second: 100, valid_until: 101}, hbm: {mean_percent: 35, valid_until: 101}}]};
+let value = read(data, 1, 100);
+assert.equal(value.prefill, 0);
+assert.equal(value.decode, 100);
+assert.equal(value.mfu, .25);
+assert.equal(value.hbm, 35);
+value = read({available: true, nodes: [{backend_port: 1, source_ok: true, backend_ready: true,
+ clock_ok: true, clock_uncertainty_seconds: .1,
+ prefill: {tokens_per_second: null, mfu_estimate: -1, valid_until: 101},
+ decode: {tokens_per_second: Infinity, valid_until: 101}, hbm: {mean_percent: 101, valid_until: 101}}]}, 1, 100);
+for (const key of ['prefill', 'decode', 'mfu', 'hbm']) assert.equal(value[key], null);
+assert.equal(read(data, 1, 101).prefill, null);
+assert.equal(read(data, 2, 100).prefill, null);
+assert.equal(read({available: false, nodes: data.nodes}, 1, 100).prefill, null);
+data.nodes[0].backend_ready = false;
+value = read(data, 1, 100);
+assert.equal(value.prefill, null); assert.equal(value.decode, null); assert.equal(value.mfu, null);
+assert.equal(value.hbm, 35); // Device reading remains separately valid.
+data.nodes[0].backend_ready = true; data.nodes[0].clock_ok = false;
+value = read(data, 1, 100);
+for (const key of ['prefill', 'decode', 'mfu', 'hbm']) assert.equal(value[key], null);
+data.nodes[0].clock_ok = true; data.nodes[0].clock_uncertainty_seconds = null;
+assert.equal(read(data, 1, 100).decode, null);
+'''
+ source = Path(__file__).resolve().parents[1]/'deploy_stats/static/app.js'
+ result = subprocess.run([node, '-e', script, str(source)], capture_output=True, text=True, timeout=15)
+ self.assertEqual(result.returncode, 0, result.stderr)
+
+ def test_backend_table_does_not_show_unready_or_clock_invalid_rates(self):
+ node = shutil.which('node')
+ if node is None:
+ self.skipTest('Node.js is required to validate dashboard calculations')
+ script = r'''
+const assert = require('node:assert/strict'), fs = require('node:fs'), vm = require('node:vm');
+const source = fs.readFileSync(process.argv[1], 'utf8');
+function extract(name, ctx) {
+ const start = source.indexOf(`function ${name}(`), end = source.indexOf('\n}\n', start);
+ assert(start >= 0 && end > start); return vm.runInNewContext('(' + source.slice(start, end + 2) + ')', ctx);
+}
+const body = {children: [], replaceChildren() {this.children = [];}, appendChild(row) {this.children.push(row);}};
+const context = {telemetrySnapshot: null, telemetryExpiry: null, telemetryNodePort: 'all', telemetryOptionsKey: null,
+ Date: class extends Date {static now() {return 100000;}}, clearTimeout: () => {}, setTimeout: () => 1,
+ $: (id) => id === 'telemetry-body' ? body : null, document: {createElement: () => ({cells: []})},
+ cell: (row, value) => row.cells.push(value), setTextIfPresent: () => {},
+ number: {format: String}, speed: (v) => `${v} tok/s`, percent: (v) => `${v * 100}%`,
+ clock: {format: () => 'time'}, combinedDeviceHistory: () => null, renderDeviceHistory: () => {},
+ latestGroupHistory: () => null, historicalPrefillMfu: () => null, renderMtp: () => {}, validMtp: () => null};
+context.freshTelemetry = extract('freshTelemetry', context); context.telemetryValues = extract('telemetryValues', context);
+const render = extract('renderTelemetry', context);
+const n = {backend_port: 1, source_ok: true, backend_ready: true, clock_ok: true, clock_uncertainty_seconds: .1,
+ prefill: {tokens_per_second: 20, mfu_estimate: .2, log_at: 95, log_skew_seconds: 0, valid_until: 110},
+ decode: {tokens_per_second: 100, log_at: 95, log_skew_seconds: 0, valid_until: 110},
+ hbm: {mean_percent: 30, device_count: 1, sample_at: 95, span_seconds: 0, valid_until: 110}};
+const data = {available: true, sampled_at: 100, nodes: [n]};
+render(data); assert.match(body.children[0].cells[2], /^100 tok\/s/);
+n.backend_ready = false; render(data);
+assert.equal(body.children[0].cells[1], '—'); assert.equal(body.children[0].cells[2], '—');
+assert.equal(body.children[0].cells[4], '30% · 1 卡');
+n.backend_ready = true; n.clock_ok = false; render(data);
+assert.equal(body.children[0].cells[2], '—'); assert.equal(body.children[0].cells[4], '—');
+'''
+ source = Path(__file__).resolve().parents[1]/'deploy_stats/static/app.js'
+ result = subprocess.run([node, '-e', script, str(source)], capture_output=True, text=True, timeout=15)
+ self.assertEqual(result.returncode, 0, result.stderr)
+
+ def test_device_history_uses_only_device_samples_and_preserves_gaps(self):
+ node = shutil.which('node')
+ if node is None:
+ self.skipTest('Node.js is required to validate dashboard calculations')
+ script = r'''
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const vm = require('node:vm');
+const source = fs.readFileSync(process.argv[1], 'utf8');
+const start = source.indexOf('function renderDeviceHistory(');
+const end = source.indexOf('\n}\n', start);
+assert(start >= 0 && end > start);
+const elements = {};
+function makeElement() { return {textContent: '', children: [], replaceChildren() {this.children=[];}, appendChild(v) {this.children.push(v);}}; }
+for (const phase of ['prefill', 'decode']) {
+ elements[`chart-device-${phase}`] = makeElement();
+ elements[`chart-device-${phase}-svg`] = makeElement();
+}
+const context = {$: (id) => elements[id], svgElement: () => makeElement(), setTextIfPresent: () => {}, attachTrendTooltip: () => {},
+ clock: {format: () => 'time'}, dateTime: {format: () => 'date'}, speed: (v) => `${v.toFixed(2)} tok/s`, count: String};
+const render = vm.runInNewContext('(' + source.slice(start, end + 2) + ')', context);
+const points = Array.from({length: 144}, (_, i) => ({start: i*600, end: (i+1)*600,
+ prefill_tps: null, decode_tps: null, prefill_samples: 0, decode_samples: 0, prefill_estimate_tps: 99999}));
+points[1].prefill_tps = 0; points[1].prefill_samples = 1;
+points[3].prefill_tps = 3000; points[3].prefill_samples = 2;
+const current = {history: {method: 'deepinfer-log-step-approx-v1', bucket_seconds: 600, points}};
+render({available: true, history_available: true}, current);
+assert.equal(elements['chart-device-prefill'].textContent, '2000.00 tok/s · 3 个采样');
+assert.equal(elements['chart-device-decode'].textContent, '暂无设备采样');
+const lines = elements['chart-device-prefill-svg'].children.filter((x) => x.children.length > 0);
+assert.equal(lines.length, 2); // Two isolated markers, no line across the missing bucket.
+render({available: true, history_available: false}, current);
+assert.equal(elements['chart-device-prefill'].textContent, '设备历史采样未连接');
+'''
+ source = Path(__file__).resolve().parents[1]/'deploy_stats/static/app.js'
+ result = subprocess.run([node, '-e', script, str(source)], capture_output=True, text=True, timeout=15)
+ self.assertEqual(result.returncode, 0, result.stderr)
+
+ def test_cross_group_values_ignore_missing_and_weight_only_valid_groups(self):
+ node = shutil.which('node')
+ if node is None:
+ self.skipTest('Node.js is required to validate dashboard calculations')
+ script = r'''
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const vm = require('node:vm');
+const source = fs.readFileSync(process.argv[1], 'utf8');
+function extract(name, context) {
+ const start = source.indexOf(`function ${name}(`), end = source.indexOf('\n}\n', start);
+ assert(start >= 0 && end > start);
+ return vm.runInNewContext('(' + source.slice(start, end + 2) + ')', context);
+}
+const freshTelemetry = extract('freshTelemetry', {});
+const read = extract('telemetryValues', {freshTelemetry});
+function group(port, rate, mfu, peak, devices, hbm, at = 95) {
+ return {backend_port: port, source_ok: true, backend_ready: true, clock_ok: true, clock_uncertainty_seconds: .1,
+ prefill: {tokens_per_second: rate, mfu_estimate: mfu, peak_flops: peak, log_at: at, log_skew_seconds: 0, valid_until: 110},
+ decode: {tokens_per_second: rate * 2, log_at: at, log_skew_seconds: 0, valid_until: 110},
+ hbm: {mean_percent: hbm, device_count: devices, sample_at: at, span_seconds: 0, valid_until: 110}};
+}
+const a = group(1, 100, .2, 100, 2, 20), b = group(2, 300, .6, 300, 6, 60);
+let data = {available: true, nodes: [a, b]};
+let value = read(data, 'all', 100);
+assert.equal(value.prefill, 400); assert.equal(value.decode, 800);
+assert(Math.abs(value.mfu - .5) < 1e-12); assert.equal(value.hbm, 50);
+assert.equal(value.coverage.prefill.valid, 2); assert.equal(value.coverage.prefill.total, 2);
+assert.equal(read(data, 1, 100).prefill, 100);
+b.prefill.tokens_per_second = 0; assert.equal(read(data, 'all', 100).prefill, 100);
+b.prefill.log_at = 60; value = read(data, 'all', 100);
+assert.equal(value.prefill, 100); assert(value.coverage.prefill.span_seconds > 30);
+b.prefill.log_at = 95; b.decode = null; value = read(data, 'all', 100);
+assert.equal(value.decode, 200); assert.equal(value.coverage.decode.valid, 1);
+b.source_ok = false; value = read(data, 'all', 100);
+assert.equal(value.prefill, 100); assert.equal(value.coverage.prefill.valid, 1);
+assert.equal(value.coverage.prefill.total, 2); assert.equal(value.mfu, .2); assert.equal(value.hbm, 20);
+b.source_ok = true; b.prefill = null; value = read(data, 'all', 100);
+assert.equal(value.prefill, 100); assert.equal(value.mfu, .2);
+assert.equal(value.coverage.mfu.valid, 1); assert.equal(value.hbm, 50);
+b.backend_ready = false; value = read(data, 'all', 100);
+assert.equal(value.hbm, 50); assert.equal(value.coverage.hbm.valid, 2);
+b.backend_ready = true;
+a.prefill.tokens_per_second = 0; value = read(data, 'all', 100);
+assert.equal(value.prefill, 0); assert.equal(value.coverage.prefill.valid, 1);
+a.prefill.valid_until = 100; value = read(data, 'all', 100);
+assert.equal(value.prefill, null); assert.equal(value.coverage.prefill.valid, 0);
+b.source_ok = true; b.backend_port = 1; assert.equal(read(data, 'all', 100).prefill, null);
+assert.equal(read({available: false, nodes: [a]}, 'all', 100).prefill, null);
+'''
+ source = Path(__file__).resolve().parents[1]/'deploy_stats/static/app.js'
+ result = subprocess.run([node, '-e', script, str(source)], capture_output=True, text=True, timeout=15)
+ self.assertEqual(result.returncode, 0, result.stderr)
+
+ def test_overview_shows_partial_coverage_and_no_fake_decode_mbu(self):
+ node = shutil.which('node')
+ if node is None:
+ self.skipTest('Node.js is required to validate dashboard calculations')
+ script = r'''
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const vm = require('node:vm');
+const source = fs.readFileSync(process.argv[1], 'utf8');
+function extract(name, context) {
+ const start = source.indexOf(`function ${name}(`), end = source.indexOf('\n}\n', start);
+ assert(start >= 0 && end > start);
+ return vm.runInNewContext('(' + source.slice(start, end + 2) + ')', context);
+}
+const elements = {};
+for (const id of ['live-prefill-tps', 'live-decode-tps', 'live-mfu', 'live-mbu', 'live-hbm',
+ 'live-prefill-samples', 'live-decode-samples', 'live-mfu-detail', 'live-hbm-detail']) elements[id] = {textContent: ''};
+const context = {telemetrySnapshot: null, telemetryExpiry: null, telemetryNodePort: 'all', telemetryOptionsKey: null,
+ Date: class extends Date {static now() {return 100000;}}, clearTimeout: () => {},
+ $: (id) => elements[id] || null, setTextIfPresent: (id, value) => {if (elements[id]) elements[id].textContent = value;},
+ number: {format: String}, speed: (v) => `${v} tok/s`, percent: (v) => `${v * 100}%`,
+ clock: {format: () => 'time'}, combinedDeviceHistory: () => null, renderDeviceHistory: () => {},
+ latestGroupHistory: () => null, historicalPrefillMfu: () => null, renderMtp: () => {}, validMtp: () => null};
+context.freshTelemetry = extract('freshTelemetry', context);
+context.telemetryValues = extract('telemetryValues', context);
+const render = extract('renderTelemetry', context);
+const base = {source_ok: true, backend_ready: true, clock_ok: true, clock_uncertainty_seconds: .1};
+const a = {...base, backend_port: 1, prefill: {tokens_per_second: 100, mfu_estimate: .2, peak_flops: 100,
+ log_at: 95, log_skew_seconds: 0, valid_until: 110}, decode: null,
+ hbm: {mean_percent: 20, device_count: 2, sample_at: 95, span_seconds: 0, valid_until: 110}};
+const b = {...base, backend_port: 2, prefill: null, decode: null, hbm: null};
+render({available: true, sampled_at: 100, nodes: [a, b]});
+assert.equal(elements['live-prefill-tps'].textContent, '100 tok/s');
+assert.match(elements['live-prefill-samples'].textContent, /1\/2 组有样本 · 部分数据/);
+assert.match(elements['live-mfu-detail'].textContent, /1\/2 组有样本 · 部分数据/);
+assert.equal(elements['live-decode-tps'].textContent, '—');
+assert.match(elements['live-decode-samples'].textContent, /0\/2 组有样本/);
+assert.equal(elements['live-mbu'].textContent, '—');
+assert.equal(elements['live-hbm'].textContent, '20%');
+assert.match(elements['live-hbm-detail'].textContent, /1\/2 组有样本 · 部分数据/);
+render({available:false,nodes:[]});
+assert.equal(elements['live-prefill-tps'].textContent, '—');
+assert.equal(elements['live-prefill-samples'].textContent, '采集未连接 · 组数未知');
+'''
+ source = Path(__file__).resolve().parents[1]/'deploy_stats/static/app.js'
+ result = subprocess.run([node, '-e', script, str(source)], capture_output=True, text=True, timeout=15)
+ self.assertEqual(result.returncode, 0, result.stderr)
+
+ def test_overview_adds_clearly_labeled_history_only_when_live_samples_are_missing(self):
+ node = shutil.which('node')
+ if node is None:
+ self.skipTest('Node.js is required to validate dashboard calculations')
+ script = r'''
+const assert = require('node:assert/strict'), fs = require('node:fs'), vm = require('node:vm');
+const source = fs.readFileSync(process.argv[1], 'utf8');
+function extract(name, ctx) {
+ const start = source.indexOf(`function ${name}(`), end = source.indexOf('\n}\n', start);
+ assert(start >= 0 && end > start, `missing ${name}`); return vm.runInNewContext('(' + source.slice(start, end + 2) + ')', ctx);
+}
+const elements = {};
+for (const id of ['live-prefill-tps','live-decode-tps','live-mfu','live-mbu','live-hbm',
+ 'live-prefill-samples','live-decode-samples','live-mfu-detail','live-hbm-detail',
+ 'live-prefill-title','live-decode-title','live-mfu-title']) elements[id] = {textContent:''};
+const context = {telemetrySnapshot:null,telemetryExpiry:null,telemetryNodePort:'all',telemetryOptionsKey:null,
+ Date:class extends Date {static now() {return 86400000;}},clearTimeout:()=>{},setTimeout:()=>1,
+ $:(id)=>elements[id]||null,setTextIfPresent:(id,value)=>{if(elements[id])elements[id].textContent=value;},
+ number:{format:String},speed:(v)=>`${v} tok/s`,percent:(v)=>v===null||v===undefined?'—':`${v*100}%`,
+ clock:{format:(v)=>`clock-${v.getTime()/1000}`},dateTime:{format:(v)=>`date-${v.getTime()/1000}`},
+ renderDeviceHistory:()=>{},renderMtp:()=>{},validMtp:()=>null};
+for (const name of ['freshTelemetry','telemetryValues','combinedDeviceHistory','latestHistoricalSample','latestGroupHistory','historicalPrefillMfu'])
+ context[name]=extract(name,context);
+const render=extract('renderTelemetry',context);
+const points=Array.from({length:144},(_,i)=>({start:i*600,end:(i+1)*600,
+ prefill_tps:null,prefill_samples:0,decode_tps:null,decode_samples:0}));
+points[142].prefill_tps=100;points[142].prefill_samples=2;
+points[143].decode_tps=40;points[143].decode_samples=1;
+const a={backend_port:1,source_ok:true,backend_ready:true,clock_ok:true,clock_uncertainty_seconds:.1,
+ prefill:null,decode:null,hbm:{mean_percent:0,device_count:2,sample_at:86399,span_seconds:0,valid_until:86401},
+ assumptions:{prefill_flops_per_token:4,peak_flops_per_device:1000},
+ history:{method:'deepinfer-log-step-approx-v1',bucket_seconds:600,points}};
+const b={backend_port:2,source_ok:false,backend_ready:false,clock_ok:true,clock_uncertainty_seconds:.1};
+const data={available:true,history_available:true,sampled_at:86400,nodes:[a,b]};
+render(data);
+assert.equal(elements['live-prefill-tps'].textContent,'100 tok/s');
+assert.equal(elements['live-decode-tps'].textContent,'40 tok/s');
+assert.equal(elements['live-mfu'].textContent,'20%');
+assert.match(elements['live-prefill-title'].textContent,/历史/);
+assert.match(elements['live-decode-title'].textContent,/历史/);
+assert.match(elements['live-mfu-title'].textContent,/历史/);
+assert.match(elements['live-prefill-samples'].textContent,/历史.*非实时.*1\/2/);
+assert.match(elements['live-decode-samples'].textContent,/历史.*非实时.*1\/2/);
+assert.match(elements['live-mfu-detail'].textContent,/当前假设.*非实时.*1\/2/);
+assert.equal(elements['live-mbu'].textContent,'—');
+// Old HTML loaded across a dashboard switch lacks the new title nodes.
+const savedTitles={};
+for(const id of ['live-prefill-title','live-decode-title','live-mfu-title']){savedTitles[id]=elements[id];delete elements[id];}
+render(data);
+for(const id of ['live-prefill-tps','live-decode-tps','live-mfu'])assert.equal(elements[id].textContent,'—');
+Object.assign(elements,savedTitles);
+const savedDetail=elements['live-decode-samples'];delete elements['live-decode-samples'];
+render(data);assert.equal(elements['live-decode-tps'].textContent,'—');elements['live-decode-samples']=savedDetail;
+a.prefill={tokens_per_second:0,mfu_estimate:0,peak_flops:2000,log_at:86399,log_skew_seconds:0,valid_until:86401};
+a.decode={tokens_per_second:3,log_at:86399,log_skew_seconds:0,valid_until:86401};
+render(data);
+assert.equal(elements['live-prefill-tps'].textContent,'0 tok/s');
+assert.doesNotMatch(elements['live-prefill-title'].textContent,/历史/);
+assert.doesNotMatch(elements['live-decode-title'].textContent,/历史/);
+assert.doesNotMatch(elements['live-mfu-title'].textContent,/历史/);
+a.prefill=null;a.decode=null;data.history_available=false;render(data);
+for(const id of ['live-prefill-tps','live-decode-tps','live-mfu'])assert.equal(elements[id].textContent,'—');
+'''
+ source = Path(__file__).resolve().parents[1]/'deploy_stats/static/app.js'
+ result = subprocess.run([node, '-e', script, str(source)], capture_output=True, text=True, encoding='utf-8', timeout=15)
+ self.assertEqual(result.returncode, 0, result.stderr)
+
+ def test_cross_group_history_sums_available_groups_and_labels_coverage(self):
+ node = shutil.which('node')
+ if node is None:
+ self.skipTest('Node.js is required to validate dashboard calculations')
+ script = r'''
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const vm = require('node:vm');
+const source = fs.readFileSync(process.argv[1], 'utf8');
+const start = source.indexOf('function combinedDeviceHistory('), end = source.indexOf('\n}\n', start);
+assert(start >= 0 && end > start);
+const combine = vm.runInNewContext('(' + source.slice(start, end + 2) + ')');
+function group(port, rate, samples) {
+ const points = Array.from({length: 144}, (_, i) => ({start: i*600, end: (i+1)*600,
+ prefill_tps: rate, prefill_samples: samples, decode_tps: null, decode_samples: 0}));
+ return {backend_port: port, source_ok: true, history: {method: 'deepinfer-log-step-approx-v1', bucket_seconds: 600, points}};
+}
+const a = group(1, 100, 100), b = group(2, 300, 1), data = {available: true, history_available: true};
+let result = combine(data, [a, b]);
+assert.equal(result.history.points[0].prefill_tps, 400); // Sum group means, never pool samples.
+assert.equal(result.history.points[0].prefill_samples, 1); // Each complete bucket has equal weight.
+assert.equal(result.history.points[0].decode_tps, null);
+b.history.points[2].prefill_samples = 0;
+result = combine(data, [a, b]); assert.equal(result.history.points[2].prefill_tps, 100);
+assert.equal(result.history.points[2].prefill_groups, 1); assert.equal(result.history.group_count, 2);
+b.history = null; result = combine(data, [a, b]);
+assert.equal(result.history.points[0].prefill_tps, 100); assert.equal(result.history.points[0].prefill_groups, 1);
+const shifted = group(2, 300, 1); shifted.history.points.forEach((point) => {point.start += 600; point.end += 600;});
+result = combine(data, [a, shifted]); assert.equal(result.history.points[0].prefill_tps, 100);
+assert.equal(result.history.points[0].prefill_groups, 1);
+assert.equal(result.history.points[1].prefill_tps, 400); assert.equal(result.history.points[1].prefill_groups, 2);
+a.history.points[1].start = a.history.points[0].start; a.history.points[1].end = a.history.points[0].end;
+assert.equal(combine(data, [a, b]), null); // Duplicate buckets cannot be trusted.
+a.history.points[1].start = 600; a.history.points[1].end = 1200;
+a.source_ok = false; assert.equal(combine(data, [a, b]), null);
+assert.equal(combine({available: true, history_available: false}, [a]), null);
+'''
+ source = Path(__file__).resolve().parents[1]/'deploy_stats/static/app.js'
+ result = subprocess.run([node, '-e', script, str(source)], capture_output=True, text=True, timeout=15)
+ self.assertEqual(result.returncode, 0, result.stderr)
+
+ def test_history_fallback_uses_same_bucket_and_weights_only_verified_groups(self):
+ node = shutil.which('node')
+ if node is None:
+ self.skipTest('Node.js is required to validate dashboard calculations')
+ script = r'''
+const assert=require('node:assert/strict'),fs=require('node:fs'),vm=require('node:vm');
+const src=fs.readFileSync(process.argv[1],'utf8'),ctx={};
+for(const name of ['freshTelemetry','combinedDeviceHistory','latestHistoricalSample','latestGroupHistory','historicalPrefillMfu']){
+ const start=src.indexOf(`function ${name}(`),end=src.indexOf('\n}\n',start);
+ assert(start>=0&&end>start);ctx[name]=vm.runInNewContext('('+src.slice(start,end+2)+')',ctx);
+}
+function group(port,rate,count,peak,flops) {
+ const points=Array.from({length:144},(_,i)=>({start:i*600,end:(i+1)*600,
+ prefill_tps:null,prefill_samples:0,decode_tps:null,decode_samples:0}));
+ points[143].prefill_tps=rate; points[143].prefill_samples=3;
+ return {backend_port:port,source_ok:true,clock_ok:true,clock_uncertainty_seconds:.1,
+ hbm:{device_count:count,valid_until:90000},assumptions:{prefill_flops_per_token:flops,peak_flops_per_device:peak},
+ history:{method:'deepinfer-log-step-approx-v1',bucket_seconds:600,points}};
+}
+const a=group(1,100,2,1000,4),b=group(2,300,3,2000,10),nodes=[a,b];
+const data={available:true,history_available:true}, now=86400;
+let combined=ctx.combinedDeviceHistory(data,nodes);
+let sample=ctx.latestHistoricalSample(combined.history,'prefill',now);
+assert.equal(sample.value,400);assert.equal(sample.groups,2);
+let mfu=ctx.historicalPrefillMfu(nodes,'all',sample,now);
+assert.equal(mfu.value,.425);assert.equal(mfu.groups,2);assert.equal(mfu.total,2);
+b.hbm.valid_until=now; mfu=ctx.historicalPrefillMfu(nodes,'all',sample,now);
+assert.equal(mfu.value,.2);assert.equal(mfu.groups,1);assert.equal(mfu.total,2);
+b.history.points[143].prefill_samples=0;b.history.points[142].prefill_tps=300;b.history.points[142].prefill_samples=2;
+combined=ctx.combinedDeviceHistory(data,nodes);sample=ctx.latestHistoricalSample(combined.history,'prefill',now);
+assert.equal(sample.value,100);assert.equal(sample.groups,1); // Never add different time buckets.
+a.history.points[143].prefill_tps=0;combined=ctx.combinedDeviceHistory(data,nodes);
+sample=ctx.latestHistoricalSample(combined.history,'prefill',now);assert.equal(sample.value,0);
+assert.equal(ctx.latestHistoricalSample(a.history,'prefill',86399),null); // Future end cannot be historical.
+a.history.points[1].start=a.history.points[0].start;
+assert.equal(ctx.latestHistoricalSample(a.history,'prefill',now),null);
+assert.equal(ctx.latestHistoricalSample({method:'other',bucket_seconds:600,points:[]},'prefill',now),null);
+// Misaligned grids: prefer the actual newest complete bucket across groups.
+const c=group(3,100,2,1000,4),d=group(4,500,3,2000,10);
+d.history.points.forEach((p)=>{p.start+=600;p.end+=600;});
+d.history.points[142].prefill_tps=300;d.history.points[142].prefill_samples=1;
+let latest=ctx.latestGroupHistory(data,[c,d],'all','prefill',87000);
+assert.equal(latest.start,86400);assert.equal(latest.value,500);assert.equal(latest.groups,1);
+// Restricting the available timeline to the prior bucket must include both groups.
+d.history.points[143].prefill_samples=0;
+latest=ctx.latestGroupHistory(data,[c,d],'all','prefill',87000);
+assert.equal(latest.start,85800);assert.equal(latest.value,400);assert.equal(latest.groups,2);
+const weighted=ctx.historicalPrefillMfu([c,d],'all',latest,87000);
+assert.equal(weighted.value,.425);assert.equal(weighted.groups,2);
+'''
+ source = Path(__file__).resolve().parents[1]/'deploy_stats/static/app.js'
+ result = subprocess.run([node, '-e', script, str(source)], capture_output=True, text=True, timeout=15)
+ self.assertEqual(result.returncode, 0, result.stderr)
+
+ def test_history_marks_partially_observed_buckets(self):
+ node = shutil.which('node')
+ if node is None:
+ self.skipTest('Node.js is required to validate dashboard calculations')
+ script = r'''
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const vm = require('node:vm');
+const source = fs.readFileSync(process.argv[1], 'utf8');
+const start = source.indexOf('function renderDeviceHistory('), end = source.indexOf('\n}\n', start);
+assert(start >= 0 && end > start);
+function element(name, attrs = {}) {return {name, attrs, textContent: '', children: [],
+ replaceChildren() {this.children=[];}, appendChild(value) {this.children.push(value);}};}
+const elements = {};
+for (const phase of ['prefill', 'decode']) {
+ elements[`chart-device-${phase}`] = element('label');
+ elements[`chart-device-${phase}-svg`] = element('svg');
+}
+const context = {$: (id) => elements[id], svgElement: element, setTextIfPresent: () => {}, attachTrendTooltip: () => {},
+ clock: {format: () => 'time'}, dateTime: {format: () => 'date'}, speed: (value) => `${value} tok/s`, count: String};
+const render = vm.runInNewContext('(' + source.slice(start, end + 2) + ')', context);
+const points = Array.from({length: 144}, (_, i) => ({start:i*600,end:(i+1)*600,
+ prefill_tps:null,prefill_samples:0,prefill_groups:0,decode_tps:null,decode_samples:0,decode_groups:0}));
+points[1].prefill_tps = 100; points[1].prefill_samples = 1; points[1].prefill_groups = 1;
+points[2].prefill_tps = 120; points[2].prefill_samples = 1; points[2].prefill_groups = 1;
+points[3].prefill_tps = 200; points[3].prefill_samples = 1; points[3].prefill_groups = 2;
+render({available:true,history_available:true},{history:{method:'deepinfer-log-step-approx-v1',
+ bucket_seconds:600,aggregate:true,group_count:2,points}});
+assert.match(elements['chart-device-prefill'].textContent, /3 个有采样区间 · 2 个仅部分组/);
+const children = elements['chart-device-prefill-svg'].children;
+assert(children.some((item) => item.name === 'polyline' && item.attrs.class === 'prefill partial'));
+const partial = children.find((item) => item.name === 'circle' && item.attrs.class === 'prefill partial');
+assert.match(partial.children[0].textContent, /1\/2 组采样均值之和 · 部分数据/);
+'''
+ source = Path(__file__).resolve().parents[1]/'deploy_stats/static/app.js'
+ result = subprocess.run([node, '-e', script, str(source)], capture_output=True, text=True, timeout=15)
+ self.assertEqual(result.returncode, 0, result.stderr)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_gateway.py b/tests/test_gateway.py
index 84ed705..bce64f2 100644
--- a/tests/test_gateway.py
+++ b/tests/test_gateway.py
@@ -5,6 +5,7 @@
from pathlib import Path
import socket
import socketserver
+import sys
import tempfile
import threading
import unittest
@@ -12,7 +13,7 @@
from unittest.mock import patch
from deploy_gateway.routing import SessionRouter, Unavailable
-from deploy_gateway.server import Gateway
+from deploy_gateway.server import Gateway, main
class Model(BaseHTTPRequestHandler):
@@ -56,6 +57,105 @@ def do_POST(self):
self.wfile.write(value)
+class Readiness(BaseHTTPRequestHandler):
+ def log_message(self, *args):
+ pass
+
+ def do_GET(self):
+ self.server.paths.append(self.path)
+ self.send_response(self.server.status)
+ body = self.server.instance_body if self.path == '/instances' and hasattr(self.server, 'instance_body') else self.server.body
+ self.send_header('Content-Length', str(len(body)))
+ self.end_headers()
+ try:
+ self.wfile.write(body)
+ except (BrokenPipeError, ConnectionResetError):
+ pass
+
+
+class GatewayCliTests(unittest.TestCase):
+ def test_statistics_paths_cannot_overlap_gateway_state_or_sidecars(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ base = ['gateway', '--key-store', str(root/'keys'), '--bindings-file', str(root/'bindings'),
+ '--backend-state', str(root/'state.json'), '--control-file', str(root/'control')]
+ for database in (root/'state.json', root/'state.new', root/'bindings.new', root/'control.new', root/'keys'):
+ with self.subTest(database=database), patch.object(sys, 'argv', base + ['--statistics-database', str(database)]), \
+ patch('deploy_stats.inprocess.install') as install, patch('sys.stderr'), self.assertRaises(SystemExit):
+ main()
+ install.assert_not_called()
+ self.assertFalse(database.exists())
+ database = root/'metrics'
+ sidecar_state = root/'metrics-wal'
+ base = ['gateway', '--key-store', str(root/'keys'), '--bindings-file', str(root/'bindings'),
+ '--backend-state', str(sidecar_state), '--statistics-database', str(database)]
+ with patch.object(sys, 'argv', base), patch('deploy_stats.inprocess.install') as install, \
+ patch('sys.stderr'), self.assertRaises(SystemExit):
+ main()
+ install.assert_not_called()
+ self.assertFalse(database.exists())
+
+ def test_statistics_database_cannot_be_hardlink_to_runtime_file(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ state, database = root/'state.json', root/'usage.sqlite3'
+ state.write_text('synthetic state', encoding='utf-8')
+ try:
+ database.hardlink_to(state)
+ except OSError:
+ self.skipTest('Hard links are not supported by the test filesystem')
+ argv = ['gateway', '--key-store', str(root/'keys'), '--bindings-file', str(root/'bindings'),
+ '--backend-state', str(state), '--statistics-database', str(database)]
+ with patch.object(sys, 'argv', argv), patch('deploy_stats.inprocess.install') as install, \
+ patch('sys.stderr'), self.assertRaises(SystemExit):
+ main()
+ install.assert_not_called()
+ self.assertEqual(state.read_text(encoding='utf-8'), 'synthetic state')
+
+ def test_statistics_recorder_is_tagged_and_closed(self):
+ argv = ['gateway', '--key-store', 'keys.json', '--bindings-file', 'bindings.json',
+ '--backend-state', 'state.json', '--statistics-database', 'usage.sqlite3']
+ with patch.object(sys, 'argv', argv), patch('deploy_gateway.server.Gateway') as gateway, \
+ patch('deploy_stats.inprocess.install') as install, patch('builtins.print'):
+ main()
+ self.assertEqual(install.call_args.kwargs['source'], 'gateway-inprocess-v1')
+ install.return_value.close.assert_called_once()
+ gateway.return_value.server_close.assert_called_once()
+
+ def test_statistics_setup_failure_does_not_stop_gateway(self):
+ argv = ['gateway', '--key-store', 'keys.json', '--bindings-file', 'bindings.json',
+ '--backend-state', 'state.json', '--statistics-database', 'usage.sqlite3']
+ with patch.object(sys, 'argv', argv), patch('deploy_gateway.server.Gateway') as gateway, \
+ patch('deploy_stats.inprocess.install', side_effect=OSError), patch('builtins.print'):
+ main()
+ gateway.return_value.serve_forever.assert_called_once()
+
+ def test_repeated_management_mappings_reach_gateway(self):
+ argv = ['gateway', '--key-store', 'keys.json', '--bindings-file', 'bindings.json',
+ '--backend-state', 'state.json', '--backend-port', '19001', '--backend-port', '19002',
+ '--backend-readiness-port', '19001=19101', '--backend-readiness-port', '19002=19102',
+ '--backend-pd-model', 'example', '--backend-pd-probe-endpoints',
+ '--statistics-database', 'usage.sqlite3']
+ with patch.object(sys, 'argv', argv), patch('deploy_gateway.server.Gateway') as gateway, \
+ patch('deploy_stats.inprocess.install') as install, patch('builtins.print'):
+ main()
+ self.assertEqual(gateway.call_args.kwargs['backend_readiness_ports'], {19001: 19101, 19002: 19102})
+ self.assertEqual(gateway.call_args.kwargs['pd_model'], 'example')
+ self.assertTrue(gateway.call_args.kwargs['pd_probe_endpoints'])
+ self.assertEqual(install.call_args.kwargs['source'], 'gateway-inprocess-v1')
+ install.return_value.close.assert_called_once()
+ gateway.return_value.serve_forever.assert_called_once()
+
+ def test_duplicate_or_malformed_management_mapping_is_rejected(self):
+ base = ['gateway', '--key-store', 'keys.json', '--bindings-file', 'bindings.json',
+ '--backend-state', 'state.json']
+ for suffix in [['--backend-readiness-port', '19001'],
+ ['--backend-readiness-port', '19001=19101', '--backend-readiness-port', '19001=19102']]:
+ with self.subTest(suffix=suffix), patch.object(sys, 'argv', base + suffix), \
+ patch('sys.stderr'), self.assertRaises(SystemExit):
+ main()
+
+
class GatewayTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
@@ -132,6 +232,138 @@ def test_balance_sticky_history_auth_and_private_state(self):
self.assertEqual(self.request('bad', token='incorrect-placeholder')[0], 401)
self.assertEqual(sum(len(m.calls) for m in self.models), 3)
+ def test_management_readiness_controls_routing_and_recovery(self):
+ management = ThreadingHTTPServer(('127.0.0.1', 0), Readiness)
+ management.daemon_threads = True
+ management.status, management.body, management.paths = 200, b'{"ready":false}', []
+ self.start(management)
+ gateway = Gateway(('127.0.0.1', 0), self.store, *self.ports,
+ self.root / 'motor-state.json', self.root / 'motor-bindings.json', 3,
+ backend_readiness_ports={self.ports[0]: management.server_port})
+ gateway.backends.stop.set()
+ gateway.backends.thread.join(3)
+ self.start(gateway)
+
+ self.assertFalse(gateway.backends.healthy[self.ports[0]])
+ self.assertTrue(gateway.backends.healthy[self.ports[1]])
+ self.assertEqual(self.request('management-unready', gateway=gateway)[1], str(self.ports[1]))
+ self.assertEqual(management.paths, ['/readiness'])
+
+ management.body = b'{"ready":true}'
+ gateway.backends.probe()
+ self.assertFalse(gateway.backends.healthy[self.ports[0]])
+ gateway.backends.probe()
+ self.assertTrue(gateway.backends.healthy[self.ports[0]])
+
+ for status, body in [(200, b'{"ready":false}'), (200, b'{"ready":1}'),
+ (200, b'{"ready":"true"}'), (200, b'[]'),
+ (200, b'invalid'), (200, b' ' * 65537),
+ (503, b'{"ready":true}')]:
+ with self.subTest(status=status, body=body[:30]):
+ management.status, management.body = status, body
+ gateway.backends.probe()
+ self.assertFalse(gateway.backends.healthy[self.ports[0]])
+ self.assertTrue(gateway.backends.healthy[self.ports[1]])
+ management.status, management.body = 200, b'{"ready":true}'
+ gateway.backends.probe()
+ gateway.backends.probe()
+ self.assertTrue(gateway.backends.healthy[self.ports[0]])
+
+ def test_invalid_management_readiness_mapping_is_rejected(self):
+ bad_mappings = [{self.ports[0]: 0}, {self.ports[0]: 65536},
+ {self.ports[0]: True}, {self.ports[0]: '1234'},
+ {0: 1234}, {True: 1234}]
+ for mapping in bad_mappings:
+ with self.subTest(mapping=mapping), self.assertRaises(ValueError):
+ Gateway(('127.0.0.1', 0), self.store, *self.ports,
+ self.root / 'invalid-state.json', self.root / 'invalid-bindings.json',
+ backend_readiness_ports=mapping)
+
+ def test_motor_pd_requires_healthy_prefill_and_decode_despite_ready(self):
+ management = ThreadingHTTPServer(('127.0.0.1', 0), Readiness)
+ management.daemon_threads = True
+ management.status, management.body, management.paths = 200, b'{"ready":true}', []
+ prefill = {'role': 'prefill', 'pool': 'available', 'status': 'active', 'healthy': True, 'model_name': 'example'}
+ decode = {'role': 'decode', 'pool': 'available', 'status': 'active', 'healthy': True, 'model_name': 'example'}
+ management.instance_body = json.dumps({'instances': [prefill]}).encode()
+ self.start(management)
+ backup_management = ThreadingHTTPServer(('127.0.0.1', 0), Readiness)
+ backup_management.daemon_threads = True
+ backup_management.status, backup_management.body, backup_management.paths = 200, b'{"ready":true}', []
+ backup_management.instance_body = json.dumps({'instances': [prefill, decode]}).encode()
+ self.start(backup_management)
+ gateway = Gateway(('127.0.0.1', 0), self.store, *self.ports,
+ self.root / 'pd-state.json', self.root / 'pd-bindings.json', 3,
+ backend_readiness_ports={self.ports[0]: management.server_port,
+ self.ports[1]: backup_management.server_port},
+ pd_model='example')
+ gateway.backends.stop.set()
+ gateway.backends.thread.join(3)
+ self.start(gateway)
+ self.assertFalse(gateway.backends.healthy[self.ports[0]])
+ self.assertEqual(self.request('only-prefill', gateway=gateway)[1], str(self.ports[1]))
+
+ for instances in ([decode], [prefill, {**decode, 'healthy': False}],
+ [prefill, {**decode, 'pool': 'unavailable'}],
+ [prefill, {**decode, 'healthy': 1}],
+ [prefill, {**decode, 'model_name': 'other'}],
+ [prefill, {**decode, 'role': []}], []):
+ with self.subTest(instances=instances):
+ management.instance_body = json.dumps({'instances': instances}).encode()
+ gateway.backends.probe()
+ self.assertFalse(gateway.backends.healthy[self.ports[0]])
+ for body in (b'invalid', b'[]', b'{"instances":{}}', b' ' * 1048577):
+ management.instance_body = body
+ gateway.backends.probe()
+ self.assertFalse(gateway.backends.healthy[self.ports[0]])
+
+ management.instance_body = json.dumps({'instances': [prefill, decode]}).encode()
+ gateway.backends.probe()
+ self.assertFalse(gateway.backends.healthy[self.ports[0]])
+ gateway.backends.probe()
+ self.assertTrue(gateway.backends.healthy[self.ports[0]])
+
+ def test_pd_model_requires_all_management_mappings(self):
+ with self.assertRaises(ValueError):
+ Gateway(('127.0.0.1', 0), self.store, *self.ports,
+ self.root / 'pd-invalid-state.json', self.root / 'pd-invalid-bindings.json',
+ backend_readiness_ports={self.ports[0]: 12345}, pd_model='example')
+ with self.assertRaises(ValueError):
+ Gateway(('127.0.0.1', 0), self.store, *self.ports,
+ self.root / 'pd-invalid-state.json', self.root / 'pd-invalid-bindings.json',
+ pd_probe_endpoints=True)
+
+ def test_pd_endpoint_probe_rejects_stale_motor_health_and_accepts_a_live_alternative(self):
+ management = ThreadingHTTPServer(('127.0.0.1', 0), Readiness)
+ management.daemon_threads = True
+ management.status, management.body, management.paths = 200, b'{"ready":true}', []
+ self.start(management)
+ endpoint = lambda port: {'ip': '127.0.0.1', 'business_port': str(port), 'headless': False}
+ row = lambda role, port: {'role': role, 'pool': 'available', 'status': 'active',
+ 'healthy': True, 'model_name': 'example', 'endpoints': [endpoint(port)]}
+ prefill, decode = row('prefill', self.ports[0]), row('decode', self.ports[1])
+ management.instance_body = json.dumps({'instances': [prefill, decode]}).encode()
+ self.models[1].up = False
+ gateway = Gateway(('127.0.0.1', 0), self.store, self.ports[0], self.ports[0] + 1,
+ self.root / 'endpoint-state.json', self.root / 'endpoint-bindings.json', 3,
+ backend_ports=[self.ports[0]],
+ backend_readiness_ports={self.ports[0]: management.server_port},
+ pd_model='example', pd_probe_endpoints=True)
+ gateway.backends.stop.set()
+ gateway.backends.thread.join(3)
+ self.start(gateway)
+ self.assertFalse(gateway.backends.healthy[self.ports[0]])
+
+ management.instance_body = json.dumps({'instances': [prefill, decode, row('decode', self.ports[0])]}).encode()
+ gateway.backends.probe()
+ gateway.backends.probe()
+ self.assertTrue(gateway.backends.healthy[self.ports[0]])
+ for invalid in ([], [{'ip': [], 'business_port': '123'}], [endpoint(0)]):
+ management.instance_body = json.dumps({'instances': [prefill, {**decode, 'endpoints': invalid}]}).encode()
+ gateway.backends.probe()
+ gateway.backends.probe()
+ self.assertFalse(gateway.backends.healthy[self.ports[0]])
+
def test_transport_failover_persists_after_recovery_and_restart(self):
first = self.request('sticky')
old = self.models[self.ports.index(int(first[1]))]
diff --git a/tests/test_inprocess.py b/tests/test_inprocess.py
new file mode 100644
index 0000000..fde20eb
--- /dev/null
+++ b/tests/test_inprocess.py
@@ -0,0 +1,806 @@
+import hashlib
+from http.client import HTTPConnection, HTTPResponse
+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+import json
+import os
+from pathlib import Path
+import socket
+import subprocess
+import tempfile
+import threading
+import time
+import unittest
+from email.message import Message
+from unittest.mock import patch
+
+import deploy_gateway.server as gateway_module
+from deploy_common.reject_drain import MAX_REJECTIONS
+from deploy_gateway.routing import affinity_key
+from deploy_stats import inprocess
+from deploy_stats.capture import MAX_FRAME, UsageCapture, extract_usage
+from deploy_stats.inprocess import _Observer, install
+from deploy_stats.report import report
+from deploy_stats.session import BodyBudget
+from deploy_stats.web import Dashboard
+from tests.caddy_helpers import synthetic_caddy_owns_listener
+
+
+STREAM = (b'data: {"choices":[{"delta":{"content":"synthetic"}}]}\n\n'
+ b'data: {"usage":{"prompt_tokens":100,"completion_tokens":7,"total_tokens":107,'
+ b'"prompt_tokens_details":{"cached_tokens":60}}}\n\n' b'data: [DONE]\n\n')
+
+
+class Model(BaseHTTPRequestHandler):
+ protocol_version = 'HTTP/1.1'
+ def log_message(self, *args):
+ pass
+ def do_GET(self):
+ self.send_response(200)
+ self.send_header('Content-Length', '0')
+ self.end_headers()
+ def do_POST(self):
+ body = self.rfile.read(int(self.headers.get('Content-Length', '0')))
+ self.server.calls.append(body)
+ mode = self.server.mode
+ self.send_response(200)
+ self.send_header('Content-Type', 'text/event-stream')
+ self.send_header('Content-Length', str(len(STREAM) + (20 if mode == 'truncate' else 0)))
+ self.end_headers()
+ if mode == 'slow':
+ self.wfile.write(STREAM[:40])
+ self.wfile.flush()
+ self.server.started.set()
+ self.server.release.wait(5)
+ try:
+ self.wfile.write(STREAM[40:])
+ self.wfile.flush()
+ except OSError:
+ self.server.cancelled.set()
+ return
+ self.wfile.write(STREAM)
+ self.wfile.flush()
+ if mode == 'truncate':
+ self.connection.shutdown(socket.SHUT_RDWR)
+ self.connection.close()
+ self.close_connection = True
+
+
+class InProcessCaptureTests(unittest.TestCase):
+ def setUp(self):
+ self.temp = tempfile.TemporaryDirectory()
+ self.addCleanup(self.temp.cleanup)
+ self.root = Path(self.temp.name)
+ self.key = 'synthetic-gateway-key'
+ self.keys = self.root/'keys.json'
+ self.keys.write_text(json.dumps({'version': 1, 'records': {'fixture': {
+ 'sha256': hashlib.sha256(self.key.encode()).hexdigest(), 'active': True}}}))
+ self.model = ThreadingHTTPServer(('127.0.0.1', 0), Model)
+ self.model.daemon_threads = True
+ self.model.calls = []
+ self.model.mode = 'normal'
+ self.model.started, self.model.release, self.model.cancelled = (threading.Event() for _ in range(3))
+ self.start(self.model)
+ self.original_handler = gateway_module.Handler
+ self.baseline = self.gateway('baseline')
+ self.recorder = install(gateway_module, self.root/'usage.sqlite3')
+ self.assertIsNotNone(self.recorder)
+ self.addCleanup(self.recorder.close)
+ self.addCleanup(setattr, gateway_module, 'Handler', self.original_handler)
+ self.gateway_under_test = self.gateway('observed')
+ gateway_module.Handler = self.original_handler
+
+ def start(self, server):
+ thread = threading.Thread(target=server.serve_forever, kwargs={'poll_interval': .02}, daemon=True)
+ thread.start()
+ def close():
+ server.shutdown()
+ server.server_close()
+ thread.join(3)
+ self.addCleanup(close)
+
+ def gateway(self, name):
+ server = gateway_module.Gateway(('127.0.0.1', 0), self.keys,
+ state_path=self.root/(name+'-state.json'), bindings_file=self.root/(name+'-bindings.json'),
+ backend_ports=[self.model.server_port], backend_managed_affinity=True)
+ self.start(server)
+ return server
+
+ def request(self, server, payload, headers=None):
+ connection = HTTPConnection('127.0.0.1', server.server_port, timeout=5)
+ try:
+ connection.request('POST', '/v1/chat/completions', payload, headers or {})
+ response = connection.getresponse()
+ return response.status, response.read()
+ finally:
+ connection.close()
+
+ def wait_recorded(self):
+ deadline = time.monotonic() + 4
+ while time.monotonic() < deadline:
+ with self.recorder.live_lock:
+ if not self.recorder.live:
+ self.assertTrue(self.recorder.flush())
+ return
+ time.sleep(.01)
+ self.fail('observer did not finish')
+
+ def test_response_and_session_match_existing_gateway_without_storing_content(self):
+ payload = b'{"messages":[{"role":"user","content":"synthetic-private-prompt"}]}'
+ headers = {'Authorization': 'Bearer '+self.key, 'Content-Type': 'application/json',
+ 'X-Forwarded-For': '192.0.2.4'}
+ self.assertEqual(self.request(self.baseline, payload, headers), (200, STREAM))
+ self.assertEqual(self.request(self.gateway_under_test, payload, headers), (200, STREAM))
+ self.wait_recorded()
+ result = report(self.recorder.path, 'total', now=time.time() + .01)
+ self.assertEqual((result['totals']['known'], result['totals']['input'],
+ result['totals']['output'], result['totals']['cached']), (1, 100, 7, 60))
+ self.assertEqual(result['requests'][0]['ip'], '192.0.2.4')
+ message = Message()
+ message['Authorization'] = 'Bearer ' + self.key
+ digest = hashlib.sha256(self.key.encode()).hexdigest()
+ self.assertEqual(result['requests'][0]['session'], affinity_key(message, payload, digest))
+ self.assertIsNotNone(result['requests'][0]['first_visible'])
+ for file in self.root.glob('usage.sqlite3*'):
+ data = file.read_bytes()
+ for private in (self.key.encode(), b'synthetic-private-prompt'):
+ self.assertNotIn(private, data)
+
+ def test_inprocess_source_uses_only_single_caddy_forwarded_address(self):
+ headers = {'Authorization': 'Bearer '+self.key, 'Content-Type': 'application/json',
+ 'X-Forwarded-For': '192.0.2.4', 'X-LLMDeploy-Client-IP': '198.51.100.9'}
+ self.assertEqual(self.request(self.gateway_under_test, b'{}', headers), (200, STREAM))
+ self.wait_recorded()
+ result = report(self.recorder.path, 'total', now=time.time() + .01)
+ self.assertEqual(result['requests'][0]['ip'], '192.0.2.4')
+ headers.pop('X-Forwarded-For')
+ self.assertEqual(self.request(self.gateway_under_test, b'{}', headers), (200, STREAM))
+ self.wait_recorded()
+ result = report(self.recorder.path, 'total', now=time.time() + .01)
+ self.assertEqual(result['totals']['unknown_source'], 1)
+ for forwarded in ('192.0.2.4, 198.51.100.2', 'not-an-ip'):
+ headers['X-Forwarded-For'] = forwarded
+ self.assertEqual(self.request(self.gateway_under_test, b'{}', headers), (200, STREAM))
+ self.wait_recorded()
+ result = report(self.recorder.path, 'total', now=time.time() + .01)
+ self.assertEqual(result['totals']['unknown_source'], 3)
+ message = Message()
+ message['X-Forwarded-For'] = '192.0.2.4'
+ message['X-Forwarded-For'] = '198.51.100.2'
+ observer = _Observer(self.recorder, BodyBudget(), message, 'chat')
+ self.assertIsNone(observer.source)
+ observer.finish()
+
+ @unittest.skipUnless(os.environ.get('LLMDEPLOY_CADDY'), 'Set LLMDEPLOY_CADDY for Caddy integration')
+ def test_caddy_default_forwarding_overwrites_spoofed_client_addresses(self):
+ with socket.socket() as probe:
+ probe.bind(('127.0.0.1', 0))
+ front = probe.getsockname()[1]
+ config = self.root/'Caddyfile-default-xff'
+ config.write_text('{\n admin off\n auto_https off\n}\n'
+ f'http://127.0.0.1:{front} {{\n bind 127.0.0.1\n'
+ f' reverse_proxy 127.0.0.1:{self.gateway_under_test.server_port}\n}}\n', encoding='utf-8')
+ env = dict(os.environ, XDG_DATA_HOME=str(self.root), XDG_CONFIG_HOME=str(self.root),
+ APPDATA=str(self.root), LOCALAPPDATA=str(self.root))
+ flags = subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
+ caddy = subprocess.Popen([os.environ['LLMDEPLOY_CADDY'], 'run', '--config', str(config),
+ '--adapter', 'caddyfile'], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL, env=env, creationflags=flags)
+ try:
+ deadline = time.monotonic() + 5
+ while not synthetic_caddy_owns_listener(caddy, front):
+ if caddy.poll() is not None or time.monotonic() >= deadline:
+ self.fail('synthetic Caddy listener ownership could not be verified')
+ time.sleep(.02)
+ cases = ([], [('X-Forwarded-For', '192.0.2.55')],
+ [('X-Forwarded-For', '192.0.2.55, 198.51.100.8')],
+ [('X-Forwarded-For', '192.0.2.55'), ('x-forwarded-for', '198.51.100.8')],
+ [('X-LLMDeploy-Client-IP', '192.0.2.55')])
+ for extra in cases:
+ self.assertTrue(synthetic_caddy_owns_listener(caddy, front))
+ conn = HTTPConnection('127.0.0.1', front, timeout=5)
+ try:
+ conn.putrequest('POST', '/v1/chat/completions')
+ conn.putheader('Authorization', 'Bearer '+self.key)
+ conn.putheader('Content-Length', '2')
+ for name, value in extra:
+ conn.putheader(name, value)
+ conn.endheaders(b'{}')
+ response = conn.getresponse()
+ self.assertEqual((response.status, response.read()), (200, STREAM))
+ finally:
+ conn.close()
+ self.wait_recorded()
+ result = report(self.recorder.path, 'total', now=time.time() + .01)
+ self.assertEqual(result['totals']['known'], len(cases))
+ self.assertEqual(result['totals']['unknown_source'], 0)
+ self.assertEqual({item['ip'] for item in result['clients']}, {'127.0.0.1'})
+ finally:
+ if caddy.poll() is None:
+ caddy.terminate()
+ caddy.wait(timeout=5)
+
+ def test_dashboard_api_updates_after_successive_gateway_requests(self):
+ dashboard = Dashboard(0, self.recorder.path)
+ self.start(dashboard)
+ headers = {'Authorization': 'Bearer '+self.key, 'Content-Type': 'application/json',
+ 'X-Forwarded-For': '192.0.2.4'}
+ def stats():
+ conn = HTTPConnection('127.0.0.1', dashboard.server_port, timeout=5)
+ try:
+ conn.request('GET', '/api/stats?window=total')
+ response = conn.getresponse()
+ self.assertEqual(response.status, 200)
+ return json.loads(response.read())
+ finally:
+ conn.close()
+ self.assertEqual(stats()['totals']['known'], 0)
+ for expected in (1, 2):
+ self.assertEqual(self.request(self.gateway_under_test, b'{}', headers), (200, STREAM))
+ self.wait_recorded()
+ deadline = time.monotonic() + 4
+ value = stats()
+ while value['totals']['known'] < expected and time.monotonic() < deadline:
+ time.sleep(.02)
+ value = stats()
+ self.assertEqual((value['totals']['known'], value['totals']['input'], value['totals']['output'],
+ value['totals']['cached']), (expected, 100*expected, 7*expected, 60*expected))
+
+ def test_motor_readiness_tagged_capture_and_dashboard_end_to_end(self):
+ class Readiness(BaseHTTPRequestHandler):
+ def log_message(self, *args):
+ pass
+ def do_GET(self):
+ body = self.server.instance_body if self.path == '/instances' else b'{"ready":true}'
+ self.send_response(200)
+ self.send_header('Content-Length', str(len(body)))
+ self.end_headers()
+ self.wfile.write(body)
+
+ readiness = ThreadingHTTPServer(('127.0.0.1', 0), Readiness)
+ endpoint = {'ip': '127.0.0.1', 'business_port': str(self.model.server_port), 'headless': False}
+ readiness.instance_body = json.dumps({'instances': [
+ {'role': role, 'pool': 'available', 'status': 'active', 'healthy': True,
+ 'model_name': 'example', 'endpoints': [endpoint]}
+ for role in ('prefill', 'decode')]}).encode()
+ self.start(readiness)
+ database = self.root/'motor-usage.sqlite3'
+ old_handler = gateway_module.Handler
+ recorder = install(gateway_module, database, source='gateway-inprocess-v1')
+ self.assertIsNotNone(recorder)
+ self.addCleanup(recorder.close)
+ try:
+ gateway = gateway_module.Gateway(('127.0.0.1', 0), self.keys,
+ state_path=self.root/'motor-state.json', bindings_file=self.root/'motor-bindings.json',
+ backend_ports=[self.model.server_port], backend_managed_affinity=True,
+ backend_readiness_ports={self.model.server_port: readiness.server_port},
+ pd_model='example', pd_probe_endpoints=True)
+ finally:
+ gateway_module.Handler = old_handler
+ self.start(gateway)
+ self.assertTrue(gateway.backends.healthy[self.model.server_port])
+ with patch('deploy_stats.web.MotorCollector') as collector:
+ collector.return_value.snapshot.return_value = {'sampled_at': time.time(), 'nodes': []}
+ dashboard = Dashboard(0, database, motor_management_ports=[readiness.server_port])
+ self.start(dashboard)
+ headers = {'Authorization': 'Bearer '+self.key, 'Content-Type': 'application/json',
+ 'X-Forwarded-For': '192.0.2.4'}
+ self.assertEqual(self.request(gateway, b'{}', headers), (200, STREAM))
+ self.assertTrue(recorder.flush())
+ deadline = time.monotonic() + 4
+ value = report(database, 'total', source='gateway-inprocess-v1')
+ while value['totals']['known'] < 1 and time.monotonic() < deadline:
+ time.sleep(.02)
+ value = report(database, 'total', source='gateway-inprocess-v1')
+ self.assertTrue(value['collector_live'])
+ self.assertEqual((value['totals']['input'], value['totals']['output'], value['totals']['cached']), (100, 7, 60))
+ conn = HTTPConnection('127.0.0.1', dashboard.server_port, timeout=5)
+ try:
+ conn.request('GET', '/api/v5/stats?window=total')
+ response = conn.getresponse()
+ payload = json.loads(response.read())
+ self.assertEqual(response.status, 200)
+ self.assertTrue(payload['collector_live'])
+ self.assertEqual(payload['requests'][0]['ip'], '192.0.2.4')
+ self.assertEqual(payload['totals']['known'], 1)
+ finally:
+ conn.close()
+
+ def test_vllm_stream_without_cache_details_records_partial_usage(self):
+ old_handler = gateway_module.Handler
+ recorder = install(gateway_module, self.root/'partial-usage.sqlite3', source='gateway-inprocess-v1')
+ self.assertIsNotNone(recorder)
+ self.addCleanup(recorder.close)
+ try:
+ gateway = gateway_module.Gateway(('127.0.0.1', 0), self.keys,
+ state_path=self.root/'partial-state.json', bindings_file=self.root/'partial-bindings.json',
+ backend_ports=[self.model.server_port], backend_managed_affinity=True)
+ finally:
+ gateway_module.Handler = old_handler
+ self.start(gateway)
+ partial_stream = (b'data: {"choices":[{"delta":{"content":"synthetic"}}]}\n\n'
+ b'data: {"usage":{"prompt_tokens":120,"completion_tokens":20,"total_tokens":140}}\n\n'
+ b'data: [DONE]\n\n')
+ headers = {'Authorization': 'Bearer '+self.key, 'Content-Type': 'application/json',
+ 'X-Forwarded-For': '192.0.2.4'}
+ with patch(f'{__name__}.STREAM', partial_stream):
+ self.assertEqual(self.request(gateway, b'{}', headers), (200, partial_stream))
+ deadline = time.monotonic() + 4
+ value = report(recorder.path, 'total', source='gateway-inprocess-v1')
+ while value['totals']['partial'] < 1 and time.monotonic() < deadline:
+ time.sleep(.02)
+ value = report(recorder.path, 'total', source='gateway-inprocess-v1')
+ self.assertEqual((value['totals']['partial'], value['totals']['input'], value['totals']['output']), (1, 120, 20))
+ self.assertIsNone(value['totals']['hit_rate'])
+ self.assertIsNone(value['requests'][0]['cached'])
+
+ @unittest.skipUnless(os.environ.get('LLMDEPLOY_CADDY'), 'Set LLMDEPLOY_CADDY for Caddy integration')
+ def test_caddy_reload_keeps_one_active_stream(self):
+ # This checks an active stream, not idle keep-alive connections.
+ def port():
+ with socket.socket() as probe:
+ probe.bind(('127.0.0.1', 0))
+ return probe.getsockname()[1]
+ front, admin = port(), port()
+ before, maintenance, after = (self.root/'before.Caddyfile', self.root/'maintenance.Caddyfile',
+ self.root/'after.Caddyfile')
+ template = ('{{\n admin 127.0.0.1:{admin}\n auto_https off\n}}\n'
+ 'http://127.0.0.1:{front} {{\n bind 127.0.0.1\n reverse_proxy 127.0.0.1:{gateway} {{\n'
+ '{header} }}\n}}\n')
+ values = {'admin': admin, 'front': front, 'gateway': self.gateway_under_test.server_port}
+ before.write_text(template.format(**values, header=''), encoding='utf-8')
+ maintenance.write_text(('{{\n admin 127.0.0.1:{admin}\n auto_https off\n}}\n'
+ 'http://127.0.0.1:{front} {{\n bind 127.0.0.1\n respond 503\n}}\n').format(**values),
+ encoding='utf-8')
+ after.write_text(template.format(**values,
+ header=' header_up X-LLMDeploy-Client-IP {remote_host}\n'), encoding='utf-8')
+ env = dict(os.environ, XDG_DATA_HOME=str(self.root), XDG_CONFIG_HOME=str(self.root),
+ APPDATA=str(self.root), LOCALAPPDATA=str(self.root))
+ flags = subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
+ caddy = subprocess.Popen([os.environ['LLMDEPLOY_CADDY'], 'run', '--config', str(before),
+ '--adapter', 'caddyfile'], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL, env=env, creationflags=flags)
+ client = None
+ try:
+ deadline = time.monotonic() + 5
+ while not (synthetic_caddy_owns_listener(caddy, front) and
+ synthetic_caddy_owns_listener(caddy, admin)):
+ if caddy.poll() is not None or time.monotonic() >= deadline:
+ self.fail('synthetic Caddy listener ownership could not be verified')
+ time.sleep(.02)
+ self.model.mode = 'slow'
+ client = socket.create_connection(('127.0.0.1', front), timeout=5)
+ client.sendall((f'POST /v1/chat/completions HTTP/1.1\r\nHost: 127.0.0.1:{front}\r\n'
+ f'Authorization: Bearer {self.key}\r\nContent-Length: 2\r\n\r\n{{}}').encode())
+ self.assertTrue(self.model.started.wait(3))
+ received = bytearray(client.recv(4096))
+ self.assertIn(b'200', received)
+ self.model.mode = 'normal'
+ def reload(config):
+ self.assertTrue(synthetic_caddy_owns_listener(caddy, admin))
+ self.assertTrue(synthetic_caddy_owns_listener(caddy, front))
+ result = subprocess.run([os.environ['LLMDEPLOY_CADDY'], 'reload', '--address',
+ f'127.0.0.1:{admin}', '--config', str(config), '--adapter', 'caddyfile'],
+ stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
+ env=env, creationflags=flags, timeout=5)
+ self.assertEqual(result.returncode, 0)
+ reload(maintenance)
+ self.assertTrue(synthetic_caddy_owns_listener(caddy, front))
+ conn = HTTPConnection('127.0.0.1', front, timeout=5)
+ conn.request('POST', '/v1/chat/completions', b'{}')
+ maintenance_response = conn.getresponse()
+ self.assertEqual(maintenance_response.status, 503)
+ maintenance_response.read()
+ conn.close()
+ reload(after)
+ self.assertTrue(synthetic_caddy_owns_listener(caddy, front))
+ conn = HTTPConnection('127.0.0.1', front, timeout=5)
+ conn.request('POST', '/v1/chat/completions', b'{}',
+ {'Authorization': 'Bearer '+self.key, 'X-LLMDeploy-Client-IP': '192.0.2.55'})
+ response = conn.getresponse()
+ self.assertEqual((response.status, response.read()), (200, STREAM))
+ conn.close()
+ self.model.release.set()
+ while b'data: [DONE]\n\n' not in received:
+ chunk = client.recv(4096)
+ self.assertTrue(chunk)
+ received.extend(chunk)
+ self.wait_recorded()
+ result = report(self.recorder.path, 'total', now=time.time() + .01)
+ sources = {item['ip'] for item in result['clients']}
+ self.assertIn('127.0.0.1', sources)
+ self.assertNotIn('192.0.2.55', sources)
+ finally:
+ self.model.release.set()
+ if client:
+ client.close()
+ if caddy.poll() is None:
+ caddy.terminate()
+ caddy.wait(timeout=5)
+
+ def test_terminal_marker_does_not_make_a_truncated_response_complete(self):
+ self.model.mode = 'truncate'
+ headers = {'Authorization': 'Bearer '+self.key, 'Content-Type': 'application/json'}
+ self.assertEqual(self.request(self.gateway_under_test, b'{}', headers), (200, STREAM))
+ self.wait_recorded()
+ result = report(self.recorder.path, 'total', now=time.time() + .01)
+ self.assertEqual((result['totals']['known'], result['totals']['interrupted']), (0, 1))
+
+ def test_recording_and_parser_faults_never_change_response(self):
+ headers = {'Authorization': 'Bearer '+self.key, 'Content-Type': 'application/json'}
+ for target in ('deploy_stats.inprocess.SafeCapture.feed', 'deploy_stats.inprocess._Observer.session'):
+ with self.subTest(target=target), patch(target, side_effect=RuntimeError('synthetic failure')):
+ self.assertEqual(self.request(self.gateway_under_test, b'{}', headers), (200, STREAM))
+ self.wait_recorded()
+ self.assertGreaterEqual(self.recorder.dropped, 2)
+
+ def test_unavailable_recorder_does_not_change_handler(self):
+ module = type('SyntheticModule', (), {'Handler': self.original_handler})
+ with patch('deploy_stats.inprocess.Recorder', side_effect=OSError('synthetic failure')):
+ self.assertIsNone(install(module, self.root/'unavailable.sqlite3'))
+ self.assertIs(module.Handler, self.original_handler)
+
+ def test_bridge_start_failure_closes_recorder_and_restores_handler(self):
+ module = type('SyntheticModule', (), {'Handler': self.original_handler})
+ with patch('deploy_stats.inprocess.Recorder') as recorder:
+ def unavailable():
+ raise OSError('synthetic bind failure')
+ with self.assertRaises(OSError):
+ inprocess.serve(module, self.root/'failed-start.sqlite3', unavailable)
+ recorder.return_value.close.assert_called_once()
+ self.assertIs(module.Handler, self.original_handler)
+
+ def test_recorder_failure_does_not_prevent_bridge_start(self):
+ module = type('SyntheticModule', (), {'Handler': self.original_handler})
+ called = []
+ def unavailable_server():
+ called.append(True)
+ raise OSError('synthetic bind failure')
+ with patch('deploy_stats.inprocess.Recorder', side_effect=OSError('synthetic storage failure')):
+ with self.assertRaises(OSError):
+ inprocess.serve(module, self.root/'failed-recorder.sqlite3', unavailable_server)
+ self.assertEqual(called, [True])
+ self.assertIs(module.Handler, self.original_handler)
+
+ @unittest.skipUnless(hasattr(socket, 'SO_EXCLUSIVEADDRUSE'), 'Windows exclusive socket option')
+ def test_gateway_cannot_bind_over_same_address_listener(self):
+ duplicate = None
+ try:
+ with self.assertRaises(OSError):
+ duplicate = gateway_module.Gateway(('127.0.0.1', self.gateway_under_test.server_port), self.keys,
+ state_path=self.root/'duplicate-state.json', bindings_file=self.root/'duplicate-bindings.json',
+ backend_ports=[self.model.server_port], backend_managed_affinity=True)
+ finally:
+ if duplicate is not None:
+ duplicate.server_close()
+
+ def test_early_auth_rejection_keeps_the_response_complete_during_upload(self):
+ connection = socket.create_connection(('127.0.0.1', self.gateway_under_test.server_port), timeout=5)
+ try:
+ connection.sendall(b'POST /v1/chat/completions HTTP/1.1\r\nHost: localhost\r\n'
+ b'Authorization: Bearer synthetic-invalid\r\nContent-Length: 1048576\r\n\r\n'
+ + b'x' * 1024)
+ response = HTTPResponse(connection)
+ response.begin()
+ self.assertEqual(response.status, 401)
+ self.assertEqual(json.loads(response.read())['error']['message'], 'Invalid bearer API key')
+ for _ in range(15):
+ connection.sendall(b'x' * 65536)
+ finally:
+ connection.close()
+ deadline = time.monotonic() + 3
+ while self.gateway_under_test.rejection_drainer._slots._value != MAX_REJECTIONS and time.monotonic() < deadline:
+ time.sleep(.01)
+ self.assertEqual(self.gateway_under_test.rejection_drainer._slots._value, MAX_REJECTIONS)
+
+ def test_install_is_idempotent_and_does_not_start_second_recorder(self):
+ module = type('SyntheticModule', (), {'Handler': self.original_handler})
+ first = install(module, self.root/'idempotent.sqlite3')
+ self.assertIsNotNone(first)
+ self.addCleanup(first.close)
+ installed_handler = module.Handler
+ with patch('deploy_stats.inprocess.Recorder') as second_recorder:
+ self.assertIsNone(install(module, self.root/'idempotent.sqlite3'))
+ second_recorder.assert_not_called()
+ self.assertIs(module.Handler, installed_handler)
+
+ def test_discarded_oversize_sse_releases_budget_for_other_requests(self):
+ for long_line in (False, True):
+ with self.subTest(long_line=long_line):
+ budget = BodyBudget(MAX_FRAME + 256 * 1024)
+ observer = _Observer(self.recorder, budget, Message(), 'chat')
+ observer.response(200, 'text/event-stream', '')
+ if long_line:
+ observer.chunk(b'data: prior\n')
+ while not observer.capture.reader.discard_line:
+ observer.chunk(b'x' * 65536)
+ else:
+ while not observer.capture.reader.discard_event:
+ observer.chunk(b'data: ' + b'x' * 65000 + b'\n')
+ self.assertEqual(budget.used, 0)
+ other = _Observer(self.recorder, budget, Message(), 'chat')
+ other.response(200, 'text/event-stream', '')
+ other.chunk(STREAM)
+ other.complete(True)
+ other.finish()
+ observer.finish()
+ self.assertEqual(budget.used, 0)
+ self.assertTrue(self.recorder.flush())
+ result = report(self.recorder.path, 'total', now=time.time() + .01)
+ self.assertEqual(result['totals']['known'], 1 if not long_line else 2)
+
+ def test_discarded_sse_frame_never_reports_stale_usage_or_late_first_output(self):
+ budget = BodyBudget(MAX_FRAME + 256 * 1024)
+ observer = _Observer(self.recorder, budget, Message(), 'chat')
+ observer.response(200, 'text/event-stream', '')
+ observer.chunk(b'data: {"usage":{"prompt_tokens":2,"completion_tokens":1,'
+ b'"prompt_tokens_details":{"cached_tokens":1}}}\n\n')
+ while not observer.capture.reader.discard_event:
+ observer.chunk(b'data: ' + b'x' * 65000 + b'\n')
+ observer.chunk(b'\n' + STREAM)
+ observer.complete(True)
+ observer.finish()
+ self.assertTrue(self.recorder.flush())
+ result = report(self.recorder.path, 'total', now=time.time() + .01)
+ self.assertEqual((result['totals']['known'], result['totals']['missing']), (0, 1))
+ self.assertIsNone(result['requests'][0]['first_visible'])
+
+ def test_large_inprocess_frames_are_skipped_without_stopping_other_requests(self):
+ budget = BodyBudget(MAX_FRAME + 256 * 1024)
+ for content_type in ('text/event-stream', 'application/json'):
+ with self.subTest(content_type=content_type):
+ observer = _Observer(self.recorder, budget, Message(), 'chat')
+ observer.response(200, content_type, '')
+ if content_type == 'text/event-stream':
+ observer.chunk(b'data: ')
+ for _ in range(65):
+ observer.chunk(b'x' * 65536)
+ reader = observer.capture.reader
+ self.assertTrue(reader.discard_event if reader.kind == 'sse' else reader.too_large)
+ self.assertEqual(budget.used, 0)
+ observer.complete(True)
+ observer.finish()
+
+ def test_invalid_later_sse_never_reuses_previous_usage(self):
+ bad_events = (
+ b'{"usage":{"prompt_tokens":1,"completion_tokens":1,'
+ b'"prompt_tokens_details":{"cached_tokens":2}}}',
+ b'{"usage": BROKEN}',
+ b'{"unclosed":',
+ )
+ for bad in bad_events:
+ for first_before in (False, True):
+ with self.subTest(bad=bad, first_before=first_before):
+ first = []
+ parser = UsageCapture('chat', 'text/event-stream', on_first_output=lambda: first.append(True))
+ if first_before:
+ parser.feed(b'data: {"choices":[{"delta":{"content":"visible"}}]}\n\n')
+ parser.feed(b'data: {"usage":{"prompt_tokens":2,"completion_tokens":1,'
+ b'"prompt_tokens_details":{"cached_tokens":1}}}\n\n')
+ parser.feed(b'data: ' + bad + b'\n\n')
+ parser.feed(STREAM)
+ self.assertIsNone(parser.finish())
+ self.assertEqual(first, [True] if first_before else [])
+
+ def test_many_empty_or_mixed_data_lines_are_bounded_and_do_not_starve_other_requests(self):
+ for line in (b'data:\n', b'data: x\ndata:\n'):
+ with self.subTest(line=line):
+ budget = BodyBudget(MAX_FRAME)
+ observer = _Observer(self.recorder, budget, Message(), 'chat')
+ observer.response(200, 'text/event-stream', '')
+ for _ in range(12):
+ observer.chunk(line * 500)
+ self.assertTrue(observer.capture.reader.discard_event)
+ self.assertEqual(budget.used, 0)
+ other = _Observer(self.recorder, budget, Message(), 'chat')
+ other.response(200, 'text/event-stream', '')
+ other.chunk(STREAM)
+ other.complete(True)
+ other.finish()
+ observer.finish()
+ self.assertEqual(budget.used, 0)
+ self.assertTrue(self.recorder.flush())
+ result = report(self.recorder.path, 'total', now=time.time() + .01)
+ self.assertEqual(result['totals']['known'], 1 if line == b'data:\n' else 2)
+
+ def test_large_simple_responses_completion_retains_usage(self):
+ budget = BodyBudget(32 * 1024 * 1024)
+ observer = _Observer(self.recorder, budget, Message(), 'responses')
+ observer.response(200, 'text/event-stream', '')
+ observer.chunk(b'data: {"type":"response.output_text.delta","delta":"x"}\n\n')
+ text = 'x' * (1024 * 1024) + ('[{"值":"中文🙂","s":"\\\\","q":"\\\""}]' * 1000)
+ completed = {'type': 'response.completed', 'response': {'output': text,
+ 'usage': {'input_tokens': 100, 'output_tokens': 7, 'input_tokens_details': {'cached_tokens': 60}}}}
+ frame = b'data: ' + json.dumps(completed, ensure_ascii=False).encode() + b'\n\n'
+ for start in range(0, len(frame), 65536):
+ observer.chunk(frame[start:start + 65536])
+ observer.complete(True)
+ observer.finish()
+ self.assertTrue(self.recorder.flush())
+ result = report(self.recorder.path, 'total', now=time.time() + .01)
+ self.assertEqual((result['totals']['known'], result['totals']['input'], result['totals']['output'],
+ result['totals']['cached']), (1, 100, 7, 60))
+
+ def test_structurally_dense_json_is_missing_but_other_requests_still_record(self):
+ budget = BodyBudget(MAX_FRAME)
+ for content_type in ('text/event-stream', 'application/json'):
+ with self.subTest(content_type=content_type):
+ observer = _Observer(self.recorder, budget, Message(), 'chat')
+ observer.response(200, content_type, '')
+ body = json.dumps({'choices': [], 'extra': [0] * 20000,
+ 'usage': {'prompt_tokens': 100, 'completion_tokens': 7,
+ 'prompt_tokens_details': {'cached_tokens': 60}}}).encode()
+ frame = b'data: ' + body + b'\n\ndata: [DONE]\n\n' if content_type.startswith('text/') else body
+ for start in range(0, len(frame), 8192):
+ observer.chunk(frame[start:start + 8192])
+ observer.complete(True)
+ observer.finish()
+ self.assertEqual(budget.used, 0)
+ self.assertTrue(self.recorder.flush())
+ result = report(self.recorder.path, 'total', now=time.time() + .01)
+ self.assertEqual((result['totals']['known'], result['totals']['missing']), (0, 2))
+
+ def test_large_sse_delta_requires_later_usage_and_preserves_first_output_certainty(self):
+ large = b'data: ' + json.dumps({'choices': [{'delta': {'content': 'x' * 70000}}],
+ 'usage': None}).encode() + b'\n\n'
+ usage = (b'data: {"choices":[{"finish_reason":"stop"}],"usage":{"prompt_tokens":100,"completion_tokens":7,'
+ b'"prompt_tokens_details":{"cached_tokens":60}}}\n\n')
+ first_delta = b'data: {"choices":[{"delta":{"content":"x"}}]}\n\n'
+ for first_before, final_usage in ((False, True), (True, True), (True, False)):
+ with self.subTest(first_before=first_before, final_usage=final_usage):
+ first = []
+ parser = UsageCapture('chat', 'text/event-stream', on_first_output=lambda: first.append(True),
+ max_structure=16384, max_delta=65536)
+ if first_before:
+ parser.feed(first_delta)
+ parser.feed(usage)
+ parser.feed(large)
+ if final_usage:
+ parser.feed(usage)
+ parser.feed(b'data: [DONE]\n\n')
+ self.assertEqual(parser.finish(), (100, 7, 60) if final_usage else None)
+ self.assertEqual(first, [True] if first_before else [])
+
+ def test_chat_usage_after_skipped_event_requires_verified_finish(self):
+ large = b'data: ' + json.dumps({'choices': [{'delta': {'content': 'x' * 70000}}]}).encode() + b'\n\n'
+ usage = (b'data: {"usage":{"prompt_tokens":100,"completion_tokens":7,'
+ b'"prompt_tokens_details":{"cached_tokens":60}}}\n\n')
+ finish = b'data: {"choices":[{"finish_reason":"stop"}]}\n\n'
+ for finish_first in (False, True):
+ with self.subTest(finish_first=finish_first):
+ parser = UsageCapture('chat', 'text/event-stream', max_delta=65536)
+ parser.feed(large)
+ if finish_first:
+ parser.feed(finish)
+ parser.feed(usage + b'data: [DONE]\n\n')
+ self.assertEqual(parser.finish(), (100, 7, 60) if finish_first else None)
+
+ def test_data_after_done_never_restores_usage(self):
+ large = b'data: ' + json.dumps({'choices': [{'delta': {'content': 'x' * 70000}}]}).encode() + b'\n\n'
+ final = (b'data: {"choices":[{"finish_reason":"stop"}],"usage":{"prompt_tokens":100,'
+ b'"completion_tokens":7,"prompt_tokens_details":{"cached_tokens":60}}}\n\n')
+ parser = UsageCapture('chat', 'text/event-stream', max_delta=65536)
+ parser.feed(large + b'data: [DONE]\n\n' + final)
+ self.assertIsNone(parser.finish())
+
+ def test_fragmented_long_sse_line_does_not_rescan_previous_bytes(self):
+ class CountedBuffer(bytearray):
+ scanned = 0
+ def find(self, value, start=0, end=None):
+ self.scanned += (len(self) if end is None else min(end, len(self))) - start
+ return super().find(value, start) if end is None else super().find(value, start, end)
+ parser = UsageCapture('chat', 'text/event-stream')
+ parser.buffer = CountedBuffer()
+ for _ in range(32):
+ parser.feed(b'x' * 65536)
+ self.assertLessEqual(parser.buffer.scanned, 2 * 32 * 65536)
+
+ def test_duplicate_usage_or_count_keys_do_not_produce_known_usage(self):
+ for body in (
+ b'{"usage":{"prompt_tokens":2,"completion_tokens":1,"prompt_tokens_details":{"cached_tokens":1}},'
+ b'"usage":{"prompt_tokens":20,"completion_tokens":1,"prompt_tokens_details":{"cached_tokens":1}}}',
+ b'{"usage":{"prompt_tokens":2,"prompt_tokens":20,"completion_tokens":1,'
+ b'"prompt_tokens_details":{"cached_tokens":1}}}',
+ ):
+ with self.subTest(body=body):
+ parser = UsageCapture('chat', 'text/event-stream')
+ parser.feed(b'data: ' + body + b'\n\ndata: [DONE]\n\n')
+ self.assertIsNone(parser.finish())
+
+ def test_present_cache_hit_and_miss_counts_must_be_valid_and_consistent(self):
+ valid = {'prompt_tokens': 100, 'completion_tokens': 7, 'prompt_cache_hit_tokens': 60,
+ 'prompt_cache_miss_tokens': 40, 'prompt_tokens_details': {'cached_tokens': 60}}
+ self.assertEqual(extract_usage('chat', {'usage': valid}), (100, 7, 60))
+ invalid = [dict(valid, prompt_cache_miss_tokens=99), dict(valid, prompt_cache_hit_tokens='bad'),
+ dict(valid, prompt_cache_miss_tokens='bad'),
+ dict(valid, prompt_tokens_details={'cached_tokens': 'bad'}),
+ dict(valid, prompt_tokens_details='bad')]
+ for usage in invalid:
+ with self.subTest(usage=usage):
+ self.assertIsNone(extract_usage('chat', {'usage': usage}))
+
+ def test_large_event_with_nested_null_and_final_usage_is_not_skipped(self):
+ payload = {'extra': {'usage': None, 'text': 'x' * 70000}, 'choices': [{'finish_reason': 'stop'}],
+ 'usage': {'prompt_tokens': 100, 'completion_tokens': 7,
+ 'prompt_tokens_details': {'cached_tokens': 60}}}
+ parser = UsageCapture('chat', 'text/event-stream', max_delta=65536, max_structure=16384)
+ parser.feed(b'data: ' + json.dumps(payload).encode() + b'\n\ndata: [DONE]\n\n')
+ self.assertEqual(parser.finish(), (100, 7, 60))
+
+ def test_large_nonterminal_event_with_usage_waits_for_verified_terminal(self):
+ first = []
+ parser = UsageCapture('chat', 'text/event-stream', on_first_output=lambda: first.append(True),
+ max_delta=65536, max_structure=16384)
+ frame = {'choices': [{'delta': {'content': 'x' * 70000}}],
+ 'usage': {'prompt_tokens': 100, 'completion_tokens': 7,
+ 'prompt_tokens_details': {'cached_tokens': 60}}}
+ parser.feed(b'data: ' + json.dumps(frame).encode() + b'\n\n')
+ self.assertTrue(parser.skipped)
+ self.assertEqual(first, [])
+ parser.feed(b'data: {"choices":[{"finish_reason":"stop"}],"usage":{"prompt_tokens":100,'
+ b'"completion_tokens":7,"prompt_tokens_details":{"cached_tokens":60}}}\n\n'
+ b'data: [DONE]\n\n')
+ self.assertEqual(parser.finish(), (100, 7, 60))
+
+ def test_nested_finish_marker_cannot_validate_large_chat_usage(self):
+ payload = {'meta': {'finish_reason': 'stop'}, 'content': 'x' * 70000,
+ 'usage': {'prompt_tokens': 100, 'completion_tokens': 7,
+ 'prompt_tokens_details': {'cached_tokens': 60}}}
+ parser = UsageCapture('chat', 'text/event-stream', max_delta=65536)
+ parser.feed(b'data: ' + json.dumps(payload).encode() + b'\n\ndata: [DONE]\n\n')
+ self.assertIsNone(parser.finish())
+
+ def test_capture_budget_exhaustion_does_not_interrupt_response(self):
+ headers = {'Authorization': 'Bearer '+self.key, 'Content-Type': 'application/json'}
+ with patch('deploy_stats.inprocess.CAPTURE_BUDGET', 10):
+ recorder = install(gateway_module, self.root/'limited-usage.sqlite3')
+ self.assertIsNotNone(recorder)
+ self.addCleanup(recorder.close)
+ gateway = self.gateway('limited')
+ gateway_module.Handler = self.original_handler
+ self.assertEqual(self.request(gateway, b'{}', headers), (200, STREAM))
+ deadline = time.monotonic() + 4
+ while recorder.live and time.monotonic() < deadline:
+ time.sleep(.01)
+ self.assertTrue(recorder.flush())
+ result = report(recorder.path, 'total', now=time.time() + .01)
+ self.assertEqual((result['totals']['known'], result['totals']['missing']), (0, 1))
+ self.assertGreaterEqual(recorder.dropped, 1)
+
+ def test_large_request_session_uses_gateway_fingerprint_without_storing_body(self):
+ payload = json.dumps({'messages': [{'role': 'user', 'content': 'x' * (1024 * 1024 + 123)}]}).encode()
+ headers = {'Authorization': 'Bearer '+self.key, 'Content-Type': 'application/json'}
+ self.assertEqual(self.request(self.gateway_under_test, payload, headers), (200, STREAM))
+ self.wait_recorded()
+ result = report(self.recorder.path, 'total', now=time.time() + .01)
+ message = Message()
+ message['Authorization'] = 'Bearer ' + self.key
+ digest = hashlib.sha256(self.key.encode()).hexdigest()
+ self.assertEqual(result['requests'][0]['session'], affinity_key(message, payload, digest))
+ self.assertEqual(result['totals']['unknown_source'], 1)
+ for file in self.root.glob('usage.sqlite3*'):
+ self.assertNotIn(b'x' * 100, file.read_bytes())
+
+ def test_client_cancellation_is_not_counted_as_complete(self):
+ self.model.mode = 'slow'
+ conn = socket.create_connection(('127.0.0.1', self.gateway_under_test.server_port), timeout=5)
+ try:
+ conn.sendall((f'POST /v1/chat/completions HTTP/1.1\r\nHost: localhost\r\n'
+ f'Authorization: Bearer {self.key}\r\nContent-Length: 2\r\n\r\n{{}}').encode())
+ self.assertTrue(self.model.started.wait(3))
+ self.assertTrue(conn.recv(4096))
+ conn.shutdown(socket.SHUT_RDWR)
+ conn.close()
+ # The existing gateway may not unblock a silent upstream read on
+ # Windows until the upstream sends again; keep that baseline.
+ time.sleep(.3)
+ self.model.release.set()
+ self.wait_recorded()
+ result = report(self.recorder.path, 'total', now=time.time() + .01)
+ self.assertEqual((result['totals']['known'], result['totals']['interrupted']), (0, 1))
+ finally:
+ self.model.release.set()
+ conn.close()
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_motor_metrics.py b/tests/test_motor_metrics.py
new file mode 100644
index 0000000..c6f3384
--- /dev/null
+++ b/tests/test_motor_metrics.py
@@ -0,0 +1,386 @@
+import json
+from contextlib import closing
+from http.client import HTTPConnection
+import sqlite3
+import tempfile
+import threading
+import time
+import unittest
+from pathlib import Path
+from unittest.mock import patch
+
+from deploy_stats.motor_metrics import MotorCollector, MotorHistory, parse_metrics
+from deploy_stats.capture import SafeCapture, extract_usage
+from deploy_stats.report import active_report, report
+from deploy_stats.store import Recorder
+from deploy_stats.web import Dashboard
+
+
+def instance(name, role, ports):
+ return {'id': name, 'role': role, 'status': 'active', 'healthy': True,
+ 'model_name': 'synthetic',
+ 'endpoints': [{'id': str(port), 'ip': '127.0.0.1', 'business_port': str(port)}
+ for port in ports]}
+
+
+def metrics(start, prefill, decode, count=0, seconds=0):
+ labels = 'engine="0",model_name="synthetic"'
+ return '\n'.join([
+ f'process_start_time_seconds {start}',
+ f'vllm:prompt_tokens_by_source_total{{{labels},source="local_compute"}} {prefill}',
+ f'vllm:generation_tokens_total{{{labels}}} {decode}',
+ f'vllm:request_prefill_time_seconds_sum{{{labels}}} {seconds}',
+ f'vllm:request_prefill_time_seconds_count{{{labels}}} {count}',
+ f'vllm:request_decode_time_seconds_sum{{{labels}}} {seconds}',
+ f'vllm:request_decode_time_seconds_count{{{labels}}} {count}',
+ ])
+
+
+class SequentialExecutor:
+ def __init__(self, **_kwargs):
+ pass
+ def __enter__(self):
+ return self
+ def __exit__(self, *_args):
+ return False
+ def map(self, function, values):
+ return map(function, values)
+
+
+class MotorMetricsTests(unittest.TestCase):
+ def setUp(self):
+ self.now = 1000.0
+ self.topology = [instance('p', 'prefill', [12001, 12002]),
+ instance('d', 'decode', [12003])]
+ self.data = {port: metrics(50, 100, 20, 2, 4) for port in (12001, 12002, 12003)}
+ self.failed = set()
+
+ def topology(port):
+ return json.dumps({'instances': self.topology})
+
+ def endpoint(ip, port):
+ if port in self.failed:
+ raise OSError('fixture failure')
+ return self.data[port]
+
+ self.collector = MotorCollector([13001, 13002], fetch_topology=topology,
+ fetch_endpoint=endpoint, wall_time=lambda: self.now,
+ monotonic=lambda: self.now, interval=30)
+
+ def sample(self, role):
+ value = self.collector.collect()
+ return next(node for node in value['nodes'] if node['role'] == role), value
+
+ def test_duplicate_coordinators_deduplicated_and_concurrent_rate_uses_counter_delta(self):
+ first, result = self.sample('prefill')
+ self.assertIsNone(first['prefill'])
+ self.assertEqual(len(result['nodes']), 2)
+ self.now += 30
+ self.data[12001] = metrics(50, 400, 999, 5, 10)
+ self.data[12002] = metrics(50, 700, 999, 5, 13)
+ self.data[12003] = metrics(50, 9000, 170, 5, 10)
+ result = self.collector.collect()
+ prefill = next(node for node in result['nodes'] if node['role'] == 'prefill')
+ decode = next(node for node in result['nodes'] if node['role'] == 'decode')
+ self.assertEqual(prefill['prefill']['tokens_per_second'], 30)
+ self.assertEqual(decode['decode']['tokens_per_second'], 5)
+ self.assertEqual(prefill['prefill']['valid_endpoints'], 2)
+ self.assertEqual(prefill['prefill']['expected_endpoints'], 2)
+ self.assertEqual(prefill['phase_timers']['prefill']['mean_ms'], 2500)
+ self.assertIsNone(prefill['decode'])
+ self.assertIsNone(decode['prefill'])
+ self.assertIsNone(result['prefill_mfu'])
+ self.assertIsNone(result['hbm_percent'])
+
+ def test_missing_worker_is_partial_and_a_gap_does_not_become_zero(self):
+ self.collector.collect()
+ self.now += 30
+ self.failed.add(12002)
+ self.data[12001] = metrics(50, 400, 20)
+ node, _ = self.sample('prefill')
+ self.assertEqual(node['prefill']['tokens_per_second'], 10)
+ self.assertEqual((node['prefill']['valid_endpoints'], node['prefill']['expected_endpoints']), (1, 2))
+ self.assertTrue(node['prefill']['partial'])
+ self.now += 30
+ self.failed.remove(12002)
+ self.data[12001] = metrics(50, 700, 20)
+ self.data[12002] = metrics(50, 900, 20)
+ node, _ = self.sample('prefill')
+ self.assertEqual(node['prefill']['tokens_per_second'], 10)
+ self.assertEqual(node['prefill']['valid_endpoints'], 1)
+
+ def test_restart_and_counter_reset_do_not_produce_rates(self):
+ self.collector.collect()
+ self.now += 30
+ self.data[12001] = metrics(51, 110, 25)
+ self.data[12002] = metrics(50, 2, 1)
+ node, _ = self.sample('prefill')
+ self.assertIsNone(node['prefill'])
+
+ def test_motor_numeric_instance_ids_are_accepted(self):
+ self.topology[0]['id'] = 123
+ self.collector.collect()
+ self.now += 30
+ self.data[12001] = metrics(50, 400, 20)
+ self.data[12002] = metrics(50, 700, 20)
+ node, _ = self.sample('prefill')
+ self.assertEqual(node['prefill']['tokens_per_second'], 30)
+
+ def test_disagreeing_coordinators_do_not_admit_unconfirmed_groups(self):
+ self.collector.fetch_topology = lambda port: json.dumps({
+ 'instances': self.topology if port == 13001 else self.topology[1:]})
+ result = self.collector.collect()
+ self.assertTrue(result['topology_ambiguous'])
+ self.assertEqual([node['role'] for node in result['nodes']], ['decode'])
+
+ def test_role_change_during_worker_scrape_does_not_create_prefill_rate(self):
+ self.collector.collect()
+ self.now += 30
+ self.data[12001] = metrics(50, 400, 20)
+ self.data[12002] = metrics(50, 700, 20)
+ calls = {13001: 0, 13002: 0}
+ changed = [instance('p', 'decode', [12001, 12002]), self.topology[1]]
+ def topology(port):
+ calls[port] += 1
+ return json.dumps({'instances': self.topology if calls[port] == 1 else changed})
+ self.collector.fetch_topology = topology
+ result = self.collector.collect()
+ self.assertTrue(result['topology_ambiguous'])
+ self.assertFalse(any(node['prefill'] is not None for node in result['nodes']))
+
+ def test_oldest_worker_sample_controls_group_expiry(self):
+ with patch('deploy_stats.motor_metrics.ThreadPoolExecutor', SequentialExecutor):
+ self.collector.collect()
+ self.now += 30
+ self.data[12001] = metrics(50, 400, 20)
+ self.data[12002] = metrics(50, 700, 20)
+ def slow_second(_ip, port):
+ if port == 12002:
+ self.now += 20
+ return self.data[port]
+ self.collector.fetch_endpoint = slow_second
+ node, _ = self.sample('prefill')
+ self.assertIsNotNone(node['prefill'])
+ self.assertLessEqual(node['prefill']['valid_until'], 1095)
+
+ def test_very_old_worker_sample_cannot_be_relabelled_as_fresh(self):
+ with patch('deploy_stats.motor_metrics.ThreadPoolExecutor', SequentialExecutor):
+ self.collector.collect()
+ self.now += 30
+ self.data[12001] = metrics(50, 400, 20)
+ self.data[12002] = metrics(50, 700, 20)
+ def stalled_second(_ip, port):
+ if port == 12002:
+ self.now += 70
+ return self.data[port]
+ self.collector.fetch_endpoint = stalled_second
+ node, _ = self.sample('prefill')
+ self.assertIsNone(node['prefill'])
+
+ def test_backend_gauges_expire_at_the_oldest_worker_deadline(self):
+ self.collector.collect()
+ initial = self.collector.snapshot()
+ self.assertTrue(all(node['engine_metrics'] is not None for node in initial['nodes']))
+ self.now += 66
+ expired = self.collector.snapshot()
+ self.assertTrue(expired['nodes']) # The overall collector snapshot has not expired yet.
+ self.assertTrue(all(node['engine_metrics'] is None for node in expired['nodes']))
+
+ def test_slow_worker_response_uses_earliest_possible_sample_time(self):
+ self.topology = [instance('p', 'prefill', [12001])]
+ with patch('deploy_stats.motor_metrics.ThreadPoolExecutor', SequentialExecutor):
+ self.collector.collect()
+ self.now += 30
+ self.data[12001] = metrics(50, 400, 20)
+ def slow_worker(_ip, port):
+ self.now += 20
+ return self.data[port]
+ self.collector.fetch_endpoint = slow_worker
+ node, _ = self.sample('prefill')
+ self.assertEqual(node['prefill']['valid_until'], 1095)
+ self.assertEqual(node['engine_metrics']['sampled_at'], 1030)
+ self.assertEqual(node['engine_metrics']['valid_until'], 1095)
+
+ def test_duplicate_worker_address_is_not_counted_twice(self):
+ self.topology.append(instance('duplicate', 'prefill', [12001]))
+ self.collector.collect()
+ self.now += 30
+ self.data[12001] = metrics(50, 400, 20)
+ self.data[12002] = metrics(50, 700, 20)
+ node, result = self.sample('prefill')
+ usable = [n for n in result['nodes'] if n['role'] == 'prefill' and n['prefill']]
+ self.assertEqual(sum(n['prefill']['tokens_per_second'] for n in usable), 20)
+ self.assertTrue(result['topology_ambiguous'])
+
+ def test_counter_series_must_remain_identical(self):
+ self.collector.collect()
+ self.now += 30
+ self.data[12001] = metrics(50, 400, 20).replace('engine="0"', 'engine="1"')
+ self.data[12002] = metrics(50, 700, 20)
+ node, _ = self.sample('prefill')
+ self.assertEqual(node['prefill']['tokens_per_second'], 20)
+ self.assertEqual(node['prefill']['valid_endpoints'], 1)
+
+ def test_reassigned_instance_cannot_reuse_previous_topology_history(self):
+ first, _ = self.sample('prefill')
+ self.now += 30
+ self.topology[0] = instance('p', 'prefill', [12001, 12003])
+ self.topology[1] = instance('d', 'decode', [12002])
+ second, _ = self.sample('prefill')
+ self.assertNotEqual(first['backend_port'], second['backend_port'])
+ self.assertIsNone(second['prefill'])
+
+ def test_parser_rejects_duplicate_or_nonfinite_counter(self):
+ raw = metrics(50, 100, 20)
+ with self.assertRaises(ValueError):
+ parse_metrics(raw + '\n' + raw.splitlines()[1])
+ with self.assertRaises(ValueError):
+ parse_metrics(raw.replace(' 100', ' NaN'))
+
+ def test_whitespace_and_one_endpoint_failure_do_not_break_other_workers(self):
+ self.assertIn('process_start_time_seconds', parse_metrics('process_start_time_seconds 1\n \n'))
+ self.collector.collect()
+ self.now += 30
+ self.data[12001] = metrics(50, 400, 20)
+ def broken_endpoint(_ip, port):
+ if port == 12002:
+ raise RuntimeError('fixture failure')
+ return self.data[port]
+ self.collector.fetch_endpoint = broken_endpoint
+ node, _ = self.sample('prefill')
+ self.assertEqual(node['prefill']['tokens_per_second'], 10)
+ self.assertTrue(node['prefill']['partial'])
+
+ def test_history_persists_samples_without_filling_unknown_buckets(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ history = MotorHistory(Path(tmp) / 'history.sqlite3')
+ history.record('group', 'prefill', 100801, 10)
+ history.record('group', 'prefill', 100820, 20, partial=True)
+ points = history.series('group', 102000)['points']
+ point = next(p for p in points if p['start'] == 100800)
+ self.assertEqual(point['prefill_tps'], 15)
+ self.assertEqual(point['prefill_samples'], 2)
+ self.assertTrue(point['prefill_partial'])
+ self.assertIsNone(point['decode_tps'])
+ self.assertIsNone(next(p for p in points if p['start'] == 101400)['prefill_tps'])
+
+ def test_legacy_heartbeat_does_not_claim_motor_capture_is_live(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ database = Path(tmp) / 'usage.sqlite3'
+ legacy = Recorder(database)
+ try:
+ self.assertTrue(active_report(database)['collector_live'])
+ self.assertFalse(active_report(database, source='gateway-inprocess-v1')['collector_live'])
+ current = Recorder(database, source='gateway-inprocess-v1')
+ try:
+ self.assertTrue(active_report(database, source='gateway-inprocess-v1')['collector_live'])
+ with closing(sqlite3.connect(database)) as connection, connection:
+ connection.execute('UPDATE runs SET started=? WHERE id=?', (time.time() - 172800, current.run))
+ # Even a full-window heartbeat cannot prove Caddy actually routed traffic through this run.
+ self.assertFalse(report(database, 'today', source='gateway-inprocess-v1')['totals']['complete'])
+ finally:
+ current.close()
+ self.assertFalse(active_report(database, source='gateway-inprocess-v1')['collector_live'])
+ finally:
+ legacy.close()
+
+ def test_dashboard_serves_motor_snapshot_without_trusting_legacy_heartbeat(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ database = Path(tmp) / 'usage.sqlite3'
+ legacy = Recorder(database)
+ try:
+ node, snapshot = self.sample('prefill')
+ with patch('deploy_stats.web.MotorCollector') as collector:
+ collector.return_value.snapshot.return_value = snapshot
+ dashboard = Dashboard(0, database, motor_management_ports=[13001, 13002])
+ worker = threading.Thread(target=dashboard.serve_forever, kwargs={'poll_interval': .02}, daemon=True)
+ worker.start()
+ try:
+ conn = HTTPConnection('127.0.0.1', dashboard.server_port, timeout=3)
+ conn.request('GET', '/api/v5/telemetry')
+ response = conn.getresponse()
+ value = json.loads(response.read())
+ conn.close()
+ self.assertEqual(response.status, 200)
+ self.assertTrue(value['available'])
+ self.assertEqual(value['kind'], 'vllm-motor-telemetry')
+ conn = HTTPConnection('127.0.0.1', dashboard.server_port, timeout=3)
+ conn.request('GET', '/api/v5/stats')
+ response = conn.getresponse()
+ value = json.loads(response.read())
+ conn.close()
+ self.assertEqual(response.status, 200)
+ self.assertFalse(value['collector_live'])
+ self.assertEqual(len(value['backends']), 2)
+ for path in ('/api/v5/active', '/app-telemetry-v7.js'):
+ conn = HTTPConnection('127.0.0.1', dashboard.server_port, timeout=3)
+ conn.request('GET', path)
+ response = conn.getresponse()
+ response.read()
+ conn.close()
+ self.assertEqual(response.status, 200, path)
+ for path in ('/api/stats', '/api/active', '/api/telemetry'):
+ conn = HTTPConnection('127.0.0.1', dashboard.server_port, timeout=3)
+ conn.request('GET', path)
+ response = conn.getresponse()
+ response.read()
+ conn.close()
+ self.assertEqual(response.status, 409, path)
+ conn = HTTPConnection('127.0.0.1', dashboard.server_port, timeout=3)
+ conn.request('GET', '/app-telemetry-v6.js')
+ response = conn.getresponse()
+ response.read()
+ conn.close()
+ self.assertEqual(response.status, 404)
+ finally:
+ dashboard.shutdown()
+ dashboard.server_close()
+ worker.join(3)
+ collector.return_value.close.assert_called_once()
+ finally:
+ legacy.close()
+
+ def test_partial_vllm_usage_preserves_input_and_output_without_inventing_cache(self):
+ usage = {'usage': {'prompt_tokens': 120, 'completion_tokens': 20, 'total_tokens': 140}}
+ self.assertIsNone(extract_usage('chat', usage))
+ self.assertEqual(extract_usage('chat', usage, allow_partial=True), (120, 20, None))
+ reader = SafeCapture('chat', 'text/event-stream', allow_partial=True)
+ reader.feed(b'data: {"choices":[{"delta":{"content":"fixture"}}]}\n\n')
+ reader.feed(b'data: {"usage":{"prompt_tokens":120,"completion_tokens":20,"total_tokens":140}}\n\n')
+ reader.feed(b'data: [DONE]\n\n')
+ self.assertEqual(reader.finish(), (120, 20, None))
+ with tempfile.TemporaryDirectory() as tmp:
+ database = Path(tmp) / 'usage.sqlite3'
+ recorder = Recorder(database, source='gateway-inprocess-v1')
+ try:
+ now = time.time()
+ recorder.submit((now, '127.0.0.1', 2, 200, 'partial', 120, 20, None, 'a'*64, .5))
+ recorder.submit((now, '127.0.0.1', 2, 200, 'known', 100, 7, 60, 'a'*64, .5))
+ self.assertTrue(recorder.flush())
+ result = report(database, 'total', now=now + 1, source='gateway-inprocess-v1')
+ self.assertEqual((result['totals']['known'], result['totals']['partial']), (1, 1))
+ self.assertEqual((result['totals']['input'], result['totals']['output'], result['totals']['cached']), (220, 27, 60))
+ self.assertEqual(result['totals']['hit_rate'], .6)
+ partial = next(row for row in result['requests'] if row['state'] == 'partial')
+ self.assertEqual((partial['input'], partial['output'], partial['cached'], partial['hit_rate']), (120, 20, None, None))
+ self.assertIsNone(partial['input_per_wait'])
+ self.assertIsNotNone(partial['output_per_after_first'])
+ self.assertEqual(result['sessions'][0]['input'], 220)
+ self.assertEqual(result['sessions'][0]['hit_rate'], .6)
+ with closing(sqlite3.connect(database)) as connection:
+ legacy = connection.execute('SELECT state,input_tokens,output_tokens,cached_tokens FROM requests ORDER BY id LIMIT 1').fetchone()
+ self.assertEqual(legacy, ('missing', None, None, None))
+ finally:
+ recorder.close()
+
+ def test_partial_does_not_accept_invalid_or_ambiguous_usage(self):
+ base = {'prompt_tokens': 120, 'completion_tokens': 20, 'total_tokens': 140}
+ for invalid in ({**base, 'total_tokens': 999}, {**base, 'prompt_tokens_details': []},
+ {**base, 'prompt_tokens_details': {'cached_tokens': -1}},
+ {**base, 'prompt_cache_miss_tokens': 80}):
+ self.assertIsNone(extract_usage('chat', {'usage': invalid}, allow_partial=True))
+ self.assertIsNone(extract_usage('messages', {'usage': {'input_tokens': 120, 'output_tokens': 20}}, allow_partial=True))
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_mtp_usage.py b/tests/test_mtp_usage.py
new file mode 100644
index 0000000..aabcc20
--- /dev/null
+++ b/tests/test_mtp_usage.py
@@ -0,0 +1,308 @@
+"""Contract tests for request token accounting and speculative metrics."""
+from contextlib import closing
+import hashlib
+import json
+from pathlib import Path
+import shutil
+import subprocess
+import tempfile
+import time
+import unittest
+
+from deploy_stats.report import report
+from deploy_stats.store import _enable_partial_schema, _open
+from deploy_stats.motor_metrics import MotorCollector, MotorHistory, _mtp_change, _mtp_counts, parse_metrics
+
+
+class RequestAccountingTests(unittest.TestCase):
+ def test_session_prefill_rate_excludes_zero_wait_from_numerator(self):
+ from deploy_stats.report import _request_view, _session_views
+ rows = [(100, None, 'known', 100, 2, 0, 1, 's', 0, 200, 1),
+ (101, None, 'known', 10, 2, 0, 2, 's', 1, 200, 2)]
+ views = [_request_view(row) for row in rows]
+ self.assertIsNone(views[0]['input_per_wait'])
+ self.assertEqual(_session_views(views)[0]['input_per_wait'], 10)
+
+ def test_cached_and_uncached_requests_use_only_known_usage(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ path = Path(tmp) / 'usage.sqlite3'
+ now = time.time()
+ with closing(_open(path)) as db:
+ _enable_partial_schema(db)
+ db.execute('INSERT INTO runs VALUES (?,?,?,?,?)', ('run', now - 100, now, now, 0))
+ statement = '''INSERT INTO requests(run_id,finished,client_ip,elapsed,http_status,state,
+ input_tokens,output_tokens,cached_tokens,session_id,first_visible)
+ VALUES(?,?,?,?,?,?,?,?,?,?,?)'''
+ db.execute(statement, ('run', now - 3, '192.0.2.1', 1, 200, 'known', 100, 8, 60, 's', .5))
+ db.execute(statement, ('run', now - 2, '192.0.2.1', 1, 200, 'known', 20, 2, 0, 's', .5))
+ partial = db.execute(statement, ('run', now - 1, '192.0.2.1', 1, 200, 'missing',
+ None, None, None, 's', .5)).lastrowid
+ db.execute('INSERT INTO stats_partial_usage VALUES (?,?,?)', (partial, 30, 5))
+ db.commit()
+ result = report(path, 'total', now=now + 1, request_sort='uncached_input', request_dir='desc')
+ self.assertEqual([item['uncached_input'] for item in result['requests']], [40, 20, None])
+ self.assertEqual([item['cached'] for item in result['requests']], [60, 0, None])
+ self.assertEqual(result['totals']['uncached_input'], 60)
+ session = result['sessions'][0]
+ self.assertEqual((session['input'], session['output'], session['cached'], session['uncached_input']),
+ (150, 15, 60, 60))
+ self.assertEqual((session['known'], session['partial']), (2, 1))
+
+ def test_no_cache_data_does_not_become_zero_uncached(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ path = Path(tmp) / 'usage.sqlite3'
+ now = time.time()
+ with closing(_open(path)) as db:
+ db.execute('INSERT INTO runs VALUES (?,?,?,?,?)', ('run', now - 10, now, now, 0))
+ db.execute('''INSERT INTO requests(run_id,finished,client_ip,elapsed,http_status,state,
+ input_tokens,output_tokens,cached_tokens,session_id,first_visible)
+ VALUES(?,?,?,?,?,?,?,?,?,?,?)''', ('run', now - 1, None, 1, 200, 'missing',
+ None, None, None, 's', None))
+ db.commit()
+ result = report(path, 'total', now=now + 1)
+ self.assertIsNone(result['requests'][0]['uncached_input'])
+ self.assertIsNone(result['totals']['uncached_input'])
+ self.assertIsNone(result['sessions'][0]['uncached_input'])
+
+
+def mtp_metrics(start, steps, drafted, accepted, *, model='synthetic', engine='0'):
+ labels = f'engine="{engine}",model_name="{model}"'
+ return '\n'.join((f'process_start_time_seconds {start}',
+ f'vllm:spec_decode_num_drafts_total{{{labels}}} {steps}',
+ f'vllm:spec_decode_num_draft_tokens_total{{{labels}}} {drafted}',
+ f'vllm:spec_decode_num_accepted_tokens_total{{{labels}}} {accepted}'))
+
+
+class MtpTests(unittest.TestCase):
+ def test_matching_labels_and_counter_reset_are_required(self):
+ before = parse_metrics(mtp_metrics(10, 10, 50, 20))
+ after = parse_metrics(mtp_metrics(10, 15, 75, 35))
+ self.assertEqual(_mtp_counts(after, 'synthetic'), (15, 75, 35))
+ self.assertEqual(_mtp_change(after, before, 'synthetic'), (5, 25, 15))
+ self.assertIsNone(_mtp_counts(after, 'different-model'))
+ self.assertIsNone(_mtp_change(before, after, 'synthetic'))
+ mismatched = mtp_metrics(10, 15, 75, 35).replace(
+ 'vllm:spec_decode_num_accepted_tokens_total{engine="0"',
+ 'vllm:spec_decode_num_accepted_tokens_total{engine="1"')
+ self.assertIsNone(_mtp_counts(parse_metrics(mismatched), 'synthetic'))
+ self.assertIsNone(_mtp_counts(parse_metrics(mtp_metrics(10, 10, 20, 21)), 'synthetic'))
+ self.assertIsNone(_mtp_counts(parse_metrics(mtp_metrics(10, 10, '10.5', 2)), 'synthetic'))
+
+ def test_counts_are_weighted_and_partial_not_fabricated_as_zero(self):
+ now = [102000.0]
+ topology = [{'id': 'd', 'role': 'decode', 'status': 'active', 'healthy': True,
+ 'model_name': 'synthetic',
+ 'endpoints': [{'ip': '127.0.0.1', 'business_port': 12001},
+ {'ip': '127.0.0.1', 'business_port': 12002}]}]
+ data = {12001: mtp_metrics(10, 10, 100, 90), 12002: mtp_metrics(10, 10, 900, 90)}
+ with tempfile.TemporaryDirectory() as tmp:
+ history = MotorHistory(Path(tmp) / 'history.sqlite3')
+ collector = MotorCollector([13001], fetch_topology=lambda _: json.dumps({'instances': topology}),
+ fetch_endpoint=lambda _ip, port: data[port], history=history,
+ wall_time=lambda: now[0], monotonic=lambda: now[0])
+ first = collector.collect()['nodes'][0]
+ self.assertAlmostEqual(first['mtp']['lifetime']['acceptance_rate'], .18)
+ self.assertIsNone(first['mtp']['interval'])
+ now[0] += 30
+ data[12001] = mtp_metrics(10, 11, 110, 99)
+ data[12002] = mtp_metrics(10, 11, 990, 99)
+ second = collector.collect()['nodes'][0]
+ interval = second['mtp']['interval']
+ self.assertEqual((interval['drafted'], interval['accepted']), (100, 18))
+ self.assertAlmostEqual(interval['acceptance_rate'], .18)
+ self.assertFalse(interval['partial'])
+ now[0] += 30
+ data[12001] = mtp_metrics(11, 1, 5, 1) # restarted; no delta for this worker
+ data[12002] = mtp_metrics(10, 12, 1080, 108)
+ third = collector.collect()['nodes'][0]
+ self.assertTrue(third['mtp']['interval']['partial'])
+ self.assertEqual(third['mtp']['interval']['valid_endpoints'], 1)
+ self.assertEqual(third['mtp']['interval']['drafted'], 90)
+ now[0] += 600
+ point = next(p for p in history.mtp_series(
+ hashlib.sha256(json.dumps(
+ ['mtp-v1', 'd', 'decode', (('127.0.0.1', 12001), ('127.0.0.1', 12002)), 'synthetic'],
+ separators=(',', ':')).encode()).hexdigest(), now[0])['points']
+ if p['drafted'] is not None)
+ self.assertEqual((point['drafted'], point['accepted']), (190, 27))
+ self.assertTrue(point['partial'])
+
+ def test_zero_drafted_is_unknown_rate(self):
+ from deploy_stats.motor_metrics import _mtp_summary
+ data = _mtp_summary([(0, 0, 0)], 1)
+ self.assertIsNone(data['acceptance_rate'])
+ self.assertIsNone(data['mean_acceptance_length'])
+
+ def test_invalid_optional_mtp_does_not_discard_other_metrics(self):
+ baseline = 'process_start_time_seconds 10\nvllm:generation_tokens_total{model_name="synthetic"} 123\n'
+ for bad in ('NaN', '1.5'):
+ with self.subTest(bad=bad):
+ values = parse_metrics(baseline + mtp_metrics(10, 3, bad, 1).split('\n', 1)[1])
+ self.assertEqual(next(iter(values['vllm:generation_tokens_total'].values())), 123)
+ self.assertIsNone(_mtp_counts(values, 'synthetic'))
+ duplicate = mtp_metrics(10, 3, 10, 1).split('\n', 1)[1]
+ values = parse_metrics(baseline + duplicate + '\n' + duplicate.splitlines()[0])
+ self.assertIn('vllm:generation_tokens_total', values)
+ self.assertIsNone(_mtp_counts(values, 'synthetic'))
+
+ def test_sparse_history_does_not_claim_full_time_coverage(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ history = MotorHistory(Path(tmp) / 'history.sqlite3')
+ history.record_mtp('g', 90030, 1, 10, 5)
+ point = next(p for p in history.mtp_series('g', 91200)['points'] if p['start'] == 90000)
+ self.assertTrue(point['partial'])
+ self.assertEqual(point['time_coverage'], 'unverified')
+
+ def test_model_change_cannot_reuse_rates_or_record_mismatched_metrics(self):
+ now = [102000.0]
+ topology = [{'id': 'd', 'role': 'decode', 'status': 'active', 'healthy': True,
+ 'model_name': 'a', 'endpoints': [{'ip': '127.0.0.1', 'business_port': 12001}]}]
+ data = [None]
+ def metrics_for(model, amount):
+ return (f'process_start_time_seconds 10\n'
+ f'vllm:generation_tokens_total{{model_name="{model}"}} {amount}\n')
+ with tempfile.TemporaryDirectory() as tmp:
+ collector = MotorCollector([13001], fetch_topology=lambda _: json.dumps({'instances': topology}),
+ fetch_endpoint=lambda *_: data[0], history=MotorHistory(Path(tmp) / 'history.sqlite3'),
+ wall_time=lambda: now[0], monotonic=lambda: now[0])
+ data[0] = metrics_for('a', 100)
+ first = collector.collect()['nodes'][0]
+ now[0] += 30
+ data[0] = metrics_for('a', 130)
+ self.assertEqual(collector.collect()['nodes'][0]['decode']['tokens_per_second'], 1)
+ now[0] += 600
+ topology[0]['model_name'] = 'b'
+ data[0] = metrics_for('b', 200)
+ second = collector.collect()['nodes'][0]
+ self.assertNotEqual(first['backend_port'], second['backend_port'])
+ self.assertIsNone(second['decode'])
+ self.assertFalse(any(point['decode_samples'] for point in second['history']['points']))
+ now[0] += 30
+ data[0] = metrics_for('c', 300)
+ collector.collect()
+ now[0] += 30
+ data[0] = metrics_for('c', 330)
+ self.assertIsNone(collector.collect()['nodes'][0]['decode'])
+
+ def test_older_history_file_is_upgraded_without_losing_rate_buckets(self):
+ import sqlite3
+ from deploy_stats.motor_metrics import APPLICATION_ID, SCHEMA_VERSION
+ with tempfile.TemporaryDirectory() as tmp:
+ path = Path(tmp) / 'history.sqlite3'
+ with closing(sqlite3.connect(path)) as db, db:
+ db.execute(f'PRAGMA application_id = {APPLICATION_ID}')
+ db.execute(f'PRAGMA user_version = {SCHEMA_VERSION}')
+ db.execute('''CREATE TABLE rate_buckets (group_key TEXT NOT NULL, phase TEXT NOT NULL,
+ bucket INTEGER NOT NULL, rate_sum REAL NOT NULL, samples INTEGER NOT NULL,
+ partial_samples INTEGER NOT NULL, PRIMARY KEY (group_key, phase, bucket)) WITHOUT ROWID''')
+ db.execute('INSERT INTO rate_buckets VALUES (?,?,?,?,?,?)', ('g', 'decode', 90000, 60, 2, 0))
+ history = MotorHistory(path)
+ self.assertEqual(next(p for p in history.series('g', 91200)['points']
+ if p['start'] == 90000)['decode_tps'], 30)
+ history.record_mtp('g', 90001, 10, 100, 25)
+ with closing(sqlite3.connect(path)) as db:
+ self.assertEqual(db.execute('PRAGMA user_version').fetchone()[0], SCHEMA_VERSION)
+ self.assertEqual(db.execute('SELECT rate_sum, samples FROM rate_buckets').fetchone(), (60, 2))
+ self.assertAlmostEqual(next(p for p in history.mtp_series('g', 91200)['points']
+ if p['start'] == 90000)['acceptance_rate'], .25)
+
+
+class MtpDashboardTests(unittest.TestCase):
+ def run_javascript(self, script):
+ node = shutil.which('node')
+ if node is None:
+ self.skipTest('Node.js is required to validate dashboard calculations')
+ source = Path(__file__).resolve().parents[1] / 'deploy_stats/static/app.js'
+ result = subprocess.run([node, '-e', script, str(source)], capture_output=True, text=True, timeout=15)
+ self.assertEqual(result.returncode, 0, result.stderr)
+
+ def test_summary_weights_drafts_and_preserves_missing_and_stale_data(self):
+ self.run_javascript(r'''
+const assert = require('node:assert/strict'), fs = require('node:fs'), vm = require('node:vm');
+const source = fs.readFileSync(process.argv[1], 'utf8'), context = {};
+function extract(name) {
+ const start = source.indexOf(`function ${name}(`), end = source.indexOf('\n}\n', start);
+ assert(start >= 0 && end > start); return vm.runInNewContext('(' + source.slice(start, end + 2) + ')', context);
+}
+context.freshTelemetry = extract('freshTelemetry'); context.validMtp = extract('validMtp');
+const summary = extract('mtpSummary');
+const data = {available: true, kind: 'vllm-motor-telemetry'};
+const part = (drafted, accepted) => ({sampled_at: 90, valid_until: 110, steps: 10, drafted, accepted,
+ valid_endpoints: 1, expected_endpoints: 1, partial: false});
+const group = (backend_port, sample) => ({backend_port, role: 'decode', source_ok: true, backend_ready: true,
+ expected_endpoints: 1, mtp: {lifetime: sample}});
+const a = group(1, part(100, 90)), b = group(2, part(900, 90));
+let value = summary(data, [a, b], 'all', 100, 'lifetime');
+assert.equal(value.rate, .18); assert.equal(value.partial, false);
+value = summary(data, [a, b], 1, 100, 'lifetime'); assert.equal(value.rate, .9);
+b.source_ok = false; value = summary(data, [a, b], 'all', 100, 'lifetime');
+assert.equal(value.rate, .9); assert.equal(value.partial, true); assert.equal(value.groups, 1);
+b.source_ok = true; b.mtp.lifetime.valid_until = 99;
+assert.equal(summary(data, [b], 'all', 100, 'lifetime'), null);
+assert.equal(summary(data, [group(3, part(0, 0))], 'all', 100, 'lifetime').rate, null);
+assert.equal(summary(data, [group(3, part(1, 2))], 'all', 100, 'lifetime'), null);
+assert.equal(summary(data, [a, {...a}], 'all', 100, 'lifetime'), null);
+''')
+
+ def test_history_weights_counts_marks_partial_and_keeps_gaps(self):
+ self.run_javascript(r'''
+const assert = require('node:assert/strict'), fs = require('node:fs'), vm = require('node:vm');
+const source = fs.readFileSync(process.argv[1], 'utf8');
+const start = source.indexOf('function renderMtpHistory('), end = source.indexOf('\n}\n', start);
+assert(start >= 0 && end > start);
+const element = () => ({children: [], textContent: '', replaceChildren() {this.children = [];},
+ appendChild(value) {this.children.push(value);}});
+const svg = element(), label = element(); let descriptions;
+const context = {$: (id) => id === 'chart-mtp-svg' ? svg : label,
+ svgElement: (name, attrs) => Object.assign(element(), {name, attrs}),
+ attachTrendTooltip: (_svg, values) => {descriptions = values;},
+ dateTime: {format: () => 'time'}, percent: (v) => `${v * 100}%`, count: String};
+const render = vm.runInNewContext('(' + source.slice(start, end + 2) + ')', context);
+const points = () => Array.from({length: 144}, (_, i) => ({start: i * 600, end: (i + 1) * 600,
+ drafted: null, accepted: null, samples: 0, partial: false}));
+const first = points(), second = points();
+Object.assign(first[1], {drafted: 100, accepted: 90, samples: 1});
+Object.assign(second[1], {drafted: 900, accepted: 90, samples: 1});
+Object.assign(first[3], {drafted: 10, accepted: 5, samples: 1});
+const group = (backend_port, values) => ({backend_port, role: 'decode', mtp_history:
+ {method: 'vllm-mtp-counter-delta-v1', bucket_seconds: 600, points: values}});
+const a = group(1, first), b = group(2, second), data = {available: true, history_available: true,
+ kind: 'vllm-motor-telemetry'};
+render(data, [a, b], 'all');
+assert.match(descriptions[1], /18%/); assert.match(descriptions[1], /2\/2/);
+assert.equal(descriptions[2], null); assert.match(descriptions[3], /50%/);
+assert.match(descriptions[3], /1\/2.*部分数据/);
+assert.match(descriptions[1], /时间覆盖未核验/);
+assert.match(label.textContent, /2 个部分区间/);
+assert.equal(svg.children.filter((v) => v.name === 'circle').length, 2);
+render(data, [a, {...a}], 'all'); assert.equal(label.textContent, '等待 MTP 历史采样');
+''')
+
+ def test_recent_coverage_is_not_borrowed_from_lifetime(self):
+ self.run_javascript(r'''
+const assert = require('node:assert/strict'), fs = require('node:fs'), vm = require('node:vm');
+const source = fs.readFileSync(process.argv[1], 'utf8'), texts = {};
+const context = {setTextIfPresent: (id, value) => {texts[id] = value;}, percent: (v) => `${v * 100}%`,
+ renderMtpHistory: () => {}};
+function extract(name) {
+ const start = source.indexOf(`function ${name}(`), end = source.indexOf('\n}\n', start);
+ assert(start >= 0 && end > start); return vm.runInNewContext('(' + source.slice(start, end + 2) + ')', context);
+}
+for (const name of ['freshTelemetry', 'validMtp', 'mtpSummary']) context[name] = extract(name);
+const render = extract('renderMtp');
+const part = (valid, drafted, accepted) => ({sampled_at: 90, valid_until: 110, steps: 10, drafted, accepted,
+ valid_endpoints: valid, expected_endpoints: 8, partial: valid < 8});
+const node = {backend_port: 1, role: 'decode', source_ok: true, backend_ready: true, expected_endpoints: 8,
+ mtp: {lifetime: part(8, 100, 90), interval: part(1, 100, 20)}};
+render({available: true, kind: 'vllm-motor-telemetry'}, [node], 'all', 100);
+assert.match(texts['mtp-detail'], /8\/8 端点/);
+assert.match(texts['mtp-detail'], /最近采样 20%(1\/1 组,1\/8 端点,部分数据)/);
+''')
+
+ def test_partial_mtp_markers_remain_hollow(self):
+ css = (Path(__file__).resolve().parents[1] / 'deploy_stats/static/app.css').read_text(encoding='utf-8')
+ self.assertIn('.lane svg circle.mtp.partial{fill:#fff}', css)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_stats.py b/tests/test_stats.py
new file mode 100644
index 0000000..81bc113
--- /dev/null
+++ b/tests/test_stats.py
@@ -0,0 +1,1652 @@
+from concurrent.futures import ThreadPoolExecutor
+from contextlib import closing
+from datetime import datetime, timezone, timedelta
+from email.message import Message
+import hashlib
+from http.client import HTTPConnection, HTTPResponse, IncompleteRead, parse_headers
+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+from io import BytesIO
+import json
+import os
+from pathlib import Path
+import queue
+import sqlite3
+import socket
+import subprocess
+import tempfile
+import threading
+import time
+import unittest
+from unittest.mock import patch
+
+from deploy_stats.capture import UsageCapture, extract_usage
+from deploy_stats.proxy import Proxy, _headers, client_ip, session_fingerprint
+from deploy_stats.backends import read_backend
+from deploy_stats.report import active_report, report
+from deploy_stats.session import BodyBudget, SessionBodyCapture
+from deploy_stats.store import _open, Recorder
+from deploy_stats.web import Dashboard
+from deploy_gateway.server import Gateway
+from deploy_gateway.routing import affinity_key
+from tests.caddy_helpers import synthetic_caddy_owns_listener
+
+
+def usage(input_tokens=100, output_tokens=7, cached=60):
+ return {'prompt_tokens': input_tokens, 'completion_tokens': output_tokens,
+ 'total_tokens': input_tokens + output_tokens,
+ 'prompt_tokens_details': {'cached_tokens': cached}, 'prompt_cache_hit_tokens': cached}
+
+
+def sse(value):
+ return b'data: ' + json.dumps(value, ensure_ascii=False).encode() + b'\n\n'
+
+
+class CaptureTests(unittest.TestCase):
+ def test_pinned_protocol_usage_and_invalid_counts(self):
+ self.assertEqual(extract_usage('chat', {'usage': usage()}), (100, 7, 60))
+ self.assertEqual(extract_usage('responses', {'response': {'usage': {
+ 'input_tokens': 100, 'output_tokens': 7, 'input_tokens_details': {'cached_tokens': 60},
+ 'total_tokens': 107}}}), (100, 7, 60))
+ self.assertEqual(extract_usage('messages', {'usage': {'input_tokens': 35, 'output_tokens': 7,
+ 'cache_read_input_tokens': 60, 'cache_creation_input_tokens': 5}}), (100, 7, 60))
+ for invalid in ({**usage(), 'completion_tokens': True}, {**usage(), 'total_tokens': 999},
+ {**usage(), 'prompt_cache_hit_tokens': 50}, {**usage(), 'prompt_tokens': -1}):
+ self.assertIsNone(extract_usage('chat', {'usage': invalid}))
+
+ def test_fragmented_chat_sse_requires_final_marker(self):
+ content = sse({'choices': [{'delta': {'content': 'synthetic'}}]})
+ content += sse({'choices': [{'finish_reason': 'stop'}], 'usage': usage()}) + b'data: [DONE]\n\n'
+ for size in (1, 2, 7, 257):
+ parser = UsageCapture('chat', 'text/event-stream; charset=utf-8')
+ for at in range(0, len(content), size):
+ parser.feed(content[at:at + size])
+ self.assertEqual(parser.finish(), (100, 7, 60))
+ parser = UsageCapture('chat', 'text/event-stream')
+ parser.feed(content.removesuffix(b'data: [DONE]\n\n'))
+ self.assertIsNone(parser.finish())
+
+ def test_responses_and_messages_stream_terminal_events(self):
+ response = {'type': 'response.completed', 'response': {'usage': {'input_tokens': 100,
+ 'output_tokens': 7, 'input_tokens_details': {'cached_tokens': 60}, 'total_tokens': 107}}}
+ parser = UsageCapture('responses', 'text/event-stream')
+ parser.feed(sse(response))
+ self.assertEqual(parser.finish(), (100, 7, 60))
+ parser = UsageCapture('messages', 'text/event-stream')
+ parser.feed(sse({'type': 'message_delta', 'usage': {'input_tokens': 40,
+ 'cache_read_input_tokens': 60, 'cache_creation_input_tokens': 0, 'output_tokens': 7}}))
+ self.assertIsNone(parser.finish())
+ parser.feed(sse({'type': 'message_stop'}))
+ self.assertEqual(parser.finish(), (100, 7, 60))
+
+ def test_earlier_usage_is_not_final_if_later_chat_events_continue(self):
+ early = sse({'usage': usage(output_tokens=1)})
+ content = sse({'choices': [{'delta': {'content': 'later'}}]})
+ finish = sse({'choices': [{'finish_reason': 'stop'}]})
+ final = sse({'choices': [], 'usage': usage(output_tokens=7)})
+ for later in (content + finish, finish, sse({'unrecognized': 'later event'})):
+ with self.subTest(later=later):
+ parser = UsageCapture('chat', 'text/event-stream')
+ parser.feed(early + later + b'data: [DONE]\n\n')
+ self.assertIsNone(parser.finish())
+ parser = UsageCapture('chat', 'text/event-stream')
+ parser.feed(early + later + final + b'data: [DONE]\n\n')
+ self.assertEqual(parser.finish(), (100, 7, 60))
+
+ def test_messages_usage_before_later_content_is_not_final(self):
+ delta = sse({'type': 'message_delta', 'usage': {'input_tokens': 40,
+ 'cache_read_input_tokens': 60, 'output_tokens': 7}})
+ content = sse({'type': 'content_block_delta', 'delta': {'text': 'later'}})
+ stop = sse({'type': 'message_stop'})
+ parser = UsageCapture('messages', 'text/event-stream')
+ parser.feed(delta + content + stop)
+ self.assertIsNone(parser.finish())
+ parser = UsageCapture('messages', 'text/event-stream')
+ parser.feed(delta + content + delta + stop)
+ self.assertEqual(parser.finish(), (100, 7, 60))
+
+ def test_messages_stop_with_conflicting_usage_is_unknown(self):
+ delta = sse({'type': 'message_delta', 'usage': {'input_tokens': 40,
+ 'cache_read_input_tokens': 60, 'output_tokens': 1}})
+ stop = sse({'type': 'message_stop', 'usage': {'input_tokens': 40,
+ 'cache_read_input_tokens': 60, 'output_tokens': 2}})
+ parser = UsageCapture('messages', 'text/event-stream')
+ parser.feed(delta + stop)
+ self.assertIsNone(parser.finish())
+
+ def test_done_cannot_replace_protocol_specific_terminal_event(self):
+ delta = sse({'type': 'message_delta', 'usage': {'input_tokens': 40,
+ 'cache_read_input_tokens': 60, 'output_tokens': 7}})
+ parser = UsageCapture('messages', 'text/event-stream')
+ parser.feed(delta + b'data: [DONE]\n\n')
+ self.assertIsNone(parser.finish())
+ parser = UsageCapture('messages', 'text/event-stream')
+ parser.feed(delta + sse({'type': 'message_stop'}) + b'data: [DONE]\n\n')
+ self.assertEqual(parser.finish(), (100, 7, 60))
+
+ def test_unfinished_data_after_terminal_invalidates_previous_usage(self):
+ body = sse({'usage': usage()}) + b'data: [DONE]\n\n'
+ for tail in (b'data: {"choices":[', b'data: {"choices":[]}\n'):
+ with self.subTest(tail=tail):
+ parser = UsageCapture('chat', 'text/event-stream')
+ parser.feed(body + tail)
+ self.assertIsNone(parser.finish())
+
+ def test_origin_header_requires_one_valid_ip(self):
+ headers = Message()
+ self.assertIsNone(client_ip(headers))
+ headers['X-LLMDeploy-Client-IP'] = '::ffff:192.0.2.4'
+ self.assertEqual(client_ip(headers), '192.0.2.4')
+ headers['X-LLMDeploy-Client-IP'] = '192.0.2.5'
+ self.assertIsNone(client_ip(headers))
+
+ def test_session_fingerprint_matches_gateway_identity_without_storing_key(self):
+ headers = Message()
+ headers['Authorization'] = 'Bearer synthetic-gateway-key'
+ headers['X-Session-Id'] = 'synthetic-private-session'
+ digest = hashlib.sha256(b'synthetic-gateway-key').hexdigest()
+ self.assertEqual(session_fingerprint(headers), affinity_key(headers, b'{}', digest))
+ other = Message()
+ other['Authorization'] = 'Bearer other-synthetic-key'
+ other['X-Session-Id'] = 'synthetic-private-session'
+ self.assertNotEqual(session_fingerprint(headers), session_fingerprint(other))
+ headers['X-Session-Id'] = 'duplicate'
+ self.assertIsNone(session_fingerprint(headers))
+
+ def test_non_ascii_header_bytes_match_gateway_header_parser(self):
+ raw = (b'Authorization: Bearer synthetic-gateway-key\r\n'
+ b'X-Session-Id: synthetic-\xe4\xb8\xad')
+ actual = _headers(raw)
+ gateway = parse_headers(BytesIO(raw + b'\r\n\r\n'))
+ self.assertEqual(actual.get('X-Session-Id'), gateway.get('X-Session-Id'))
+ digest = hashlib.sha256(b'synthetic-gateway-key').hexdigest()
+ self.assertEqual(session_fingerprint(actual), affinity_key(gateway, b'{}', digest))
+ self.assertIsNone(client_ip(_headers(b'X-LLMDeploy-Client-IP: \xff')))
+
+ def test_first_visible_delta_ignores_roles_and_empty_events(self):
+ observed = []
+ parser = UsageCapture('chat', 'text/event-stream', on_first_output=lambda: observed.append(1))
+ parser.feed(sse({'choices': [{'delta': {'role': 'assistant', 'content': ''}}]}))
+ self.assertEqual(observed, [])
+ parser.feed(sse({'choices': [{'delta': {'content': 'synthetic'}}]}))
+ parser.feed(sse({'choices': [{'delta': {'content': 'more'}}]}))
+ self.assertEqual(observed, [1])
+ response = UsageCapture('responses', 'text/event-stream', on_first_output=lambda: observed.append(2))
+ response.feed(sse({'type': 'response.created'}))
+ response.feed(sse({'type': 'response.output_text.delta', 'delta': 'synthetic'}))
+ messages = UsageCapture('messages', 'text/event-stream', on_first_output=lambda: observed.append(3))
+ messages.feed(sse({'type': 'message_start'}))
+ messages.feed(sse({'type': 'content_block_delta', 'delta': {'thinking': 'synthetic'}}))
+ self.assertEqual(observed, [1, 2, 3])
+
+ def test_body_session_fallback_matches_gateway_for_fragmented_json(self):
+ headers = Message()
+ headers['Authorization'] = 'Bearer synthetic-gateway-key'
+ digest = hashlib.sha256(b'synthetic-gateway-key').hexdigest()
+ cases = [
+ {'prompt_cache_key': 'synthetic-private-cache-key',
+ 'messages': [{'role': 'user', 'content': 'synthetic-private-prompt'}]},
+ {'messages': [{'role': 'system', 'content': 'ignored'},
+ {'role': 'user', 'content': 'synthetic-private-prompt'},
+ {'role': 'user', 'content': 'later'}]},
+ {'messages': [{'role': 'user', 'content': [{'type': 'text', 'text': '测试'},
+ {'z': 2, 'a': 'synthetic'}]}]},
+ {'messages': [{'role': 'assistant', 'content': 'no user'}]},
+ ]
+ for value in cases:
+ body = json.dumps(value, ensure_ascii=False).encode()
+ for size in (1, 7, 67):
+ budget = BodyBudget(2 * len(body))
+ capture = SessionBodyCapture(digest, budget=budget)
+ for start in range(0, len(body), size):
+ capture.feed(body[start:start + size])
+ self.assertEqual(capture.finish(), affinity_key(headers, body, digest))
+ self.assertEqual(budget.used, 0)
+
+ def test_session_body_budget_and_size_fail_closed(self):
+ budget = BodyBudget(5)
+ first = SessionBodyCapture('synthetic-digest', budget=budget, max_body=10)
+ second = SessionBodyCapture('synthetic-digest', budget=budget, max_body=10)
+ first.feed(b'1234')
+ second.feed(b'12')
+ self.assertIsNone(second.finish())
+ self.assertEqual(budget.used, 4)
+ first.close()
+ self.assertEqual(budget.used, 0)
+ oversized = SessionBodyCapture('synthetic-digest', budget=budget, max_body=3)
+ oversized.feed(b'1234')
+ self.assertIsNone(oversized.finish())
+ self.assertEqual(budget.used, 0)
+
+
+class SyntheticGateway(BaseHTTPRequestHandler):
+ protocol_version = 'HTTP/1.1'
+
+ def log_message(self, format, *args):
+ pass
+
+ def do_GET(self):
+ self.send_response(200 if self.path == '/health' else 404)
+ self.send_header('Content-Length', '0')
+ self.end_headers()
+
+ def do_POST(self):
+ if self.server.mode == 'reject':
+ time.sleep(getattr(self.server, 'reject_delay', 0))
+ self.send_response(429)
+ self.send_header('Content-Length', '0')
+ self.send_header('Connection', 'close')
+ self.end_headers()
+ self.close_connection = True
+ return
+ payload = self.rfile.read(int(self.headers.get('Content-Length', 0)))
+ self.server.calls.append((payload, self.headers.get('Authorization'),
+ self.headers.get('X-LLMDeploy-Client-IP')))
+ if self.server.barrier:
+ self.server.barrier.wait(timeout=5)
+ mode = self.server.mode
+ if mode == 'slow_headers':
+ self.server.stream_started.set()
+ self.server.stream_release.wait(3)
+ self.close_connection = True
+ return
+ if mode == 'slow':
+ self.send_response(200)
+ self.send_header('Content-Type', 'text/event-stream')
+ self.send_header('Connection', 'close')
+ self.end_headers()
+ self.wfile.write(sse({'choices': [{'delta': {'content': 'synthetic'}}]}))
+ self.wfile.flush()
+ self.server.stream_started.set()
+ self.server.stream_release.wait(3)
+ try:
+ self.wfile.write(sse({'usage': usage()}) + b'data: [DONE]\n\n')
+ self.wfile.flush()
+ except OSError:
+ pass
+ self.close_connection = True
+ return
+ if mode == 'json':
+ body = json.dumps({'content': 'synthetic-output', 'usage': usage()}).encode()
+ media = 'application/json'
+ elif mode == 'missing':
+ body = sse({'choices': [{'finish_reason': 'stop'}]}) + b'data: [DONE]\n\n'
+ media = 'text/event-stream'
+ else:
+ body = sse({'choices': [{'delta': {'content': 'synthetic-output'}}]})
+ body += sse({'choices': [{'finish_reason': 'stop'}], 'usage': usage()}) + b'data: [DONE]\n\n'
+ media = 'text/event-stream'
+ self.server.last_body = body
+ self.send_response(200)
+ self.send_header('Content-Type', media)
+ if mode in ('slow_length', 'slow_chunked'):
+ if mode == 'slow_chunked':
+ self.send_header('Transfer-Encoding', 'chunked')
+ else:
+ self.send_header('Content-Length', str(len(body)))
+ self.end_headers()
+ if mode == 'slow_chunked':
+ self.wfile.write(f'{len(body):x}\r\n'.encode())
+ self.wfile.write(body[:24])
+ self.wfile.flush()
+ self.server.stream_started.set()
+ self.server.stream_release.wait(3)
+ try:
+ self.wfile.write(body[24:] + (b'\r\n0\r\n\r\n' if mode == 'slow_chunked' else b''))
+ self.wfile.flush()
+ except OSError:
+ pass
+ return
+ if mode == 'chunked':
+ self.send_header('Transfer-Encoding', 'chunked')
+ self.end_headers()
+ for at in range(0, len(body), 11):
+ part = body[at:at + 11]
+ self.wfile.write(f'{len(part):x}\r\n'.encode() + part + b'\r\n')
+ self.wfile.write(b'0\r\n\r\n')
+ self.wfile.flush()
+ return
+ self.send_header('Content-Length', str(len(body) + (10 if mode == 'truncate' else 0)))
+ self.end_headers()
+ for at in range(0, len(body), 13):
+ self.wfile.write(body[at:at + 13])
+ self.wfile.flush()
+ if mode == 'truncate':
+ self.close_connection = True
+
+
+class ProxyTests(unittest.TestCase):
+ def setUp(self):
+ self.temp = tempfile.TemporaryDirectory()
+ self.addCleanup(self.temp.cleanup)
+ self.path = Path(self.temp.name) / 'usage.sqlite3'
+ class SyntheticServer(ThreadingHTTPServer):
+ request_queue_size = 128
+ def handle_error(self, request, client_address):
+ pass
+ self.upstream = SyntheticServer(('127.0.0.1', 0), SyntheticGateway)
+ self.upstream.daemon_threads = True
+ self.upstream.mode = 'sse'
+ self.upstream.calls = []
+ self.upstream.barrier = None
+ self.upstream.stream_started = threading.Event()
+ self.upstream.stream_release = threading.Event()
+ self.start(self.upstream)
+ self.proxy = Proxy(0, self.upstream.server_port, self.path)
+ self.start(self.proxy)
+
+ def start(self, server):
+ thread = threading.Thread(target=server.serve_forever, kwargs={'poll_interval': .02}, daemon=True)
+ thread.start()
+ def cleanup():
+ server.shutdown()
+ server.server_close()
+ thread.join(3)
+ self.addCleanup(cleanup)
+
+ def request(self, ip='192.0.2.4'):
+ connection = HTTPConnection('127.0.0.1', self.proxy.server_port, timeout=5)
+ payload = b'{"prompt":"synthetic-prompt"}'
+ headers = {'Content-Type': 'application/json', 'Authorization': 'Bearer synthetic-placeholder',
+ 'X-LLMDeploy-Client-IP': ip}
+ try:
+ connection.request('POST', '/v1/chat/completions', payload, headers)
+ response = connection.getresponse()
+ return response.status, response.read()
+ finally:
+ connection.close()
+
+ def wait_recorded(self):
+ deadline = time.monotonic() + 3
+ while time.monotonic() < deadline:
+ with self.proxy.active_lock:
+ if self.proxy.active == 0:
+ self.assertTrue(self.proxy.recorder.flush())
+ return
+ time.sleep(.01)
+ self.fail('proxy requests did not drain')
+
+ def test_stream_is_unchanged_and_only_metadata_is_stored(self):
+ status, body = self.request()
+ self.assertEqual(status, 200)
+ self.assertEqual(body, self.upstream.last_body)
+ self.assertEqual(self.upstream.calls[-1], (b'{"prompt":"synthetic-prompt"}',
+ 'Bearer synthetic-placeholder', None))
+ self.wait_recorded()
+ data = report(self.path, 'total', now=time.time() + .01)
+ self.assertEqual((data['totals']['input'], data['totals']['output'], data['totals']['cached']), (100, 7, 60))
+ self.assertEqual(data['clients'][0]['ip'], '192.0.2.4')
+ for dbfile in Path(self.temp.name).glob('usage.sqlite3*'):
+ raw = dbfile.read_bytes()
+ for secret in (b'synthetic-prompt', b'synthetic-output', b'synthetic-placeholder'):
+ self.assertNotIn(secret, raw)
+
+ def test_json_and_unknown_usage_are_separate(self):
+ self.upstream.mode = 'json'
+ self.assertEqual(self.request()[0], 200)
+ self.upstream.mode = 'missing'
+ self.assertEqual(self.request()[0], 200)
+ self.wait_recorded()
+ data = report(self.path, 'total', now=time.time() + .01)
+ self.assertEqual((data['totals']['requests'], data['totals']['known'], data['totals']['missing']), (2, 1, 1))
+ self.assertFalse(data['totals']['complete'])
+
+ def test_invalid_client_ip_is_shown_as_unknown(self):
+ self.assertEqual(self.request('invalid-source')[0], 200)
+ self.wait_recorded()
+ data = report(self.path, 'total', now=time.time() + .01)
+ self.assertEqual(data['totals']['unknown_source'], 1)
+ self.assertIsNone(data['clients'][0]['ip'])
+
+ def test_parser_failure_does_not_interrupt_the_response(self):
+ with patch('deploy_stats.capture.UsageCapture.feed', side_effect=ValueError('synthetic failure')):
+ status, body = self.request()
+ self.assertEqual((status, body), (200, self.upstream.last_body))
+ self.wait_recorded()
+ data = report(self.path, 'total', now=time.time() + .01)
+ self.assertEqual((data['totals']['known'], data['totals']['missing']), (0, 1))
+
+ def test_recorder_failure_does_not_interrupt_the_response(self):
+ self.proxy.recorder.heartbeat = .02
+ with patch.object(self.proxy.recorder, 'submit', side_effect=OSError('synthetic failure')):
+ status, body = self.request()
+ deadline = time.monotonic() + 2
+ while self.proxy.active and time.monotonic() < deadline:
+ time.sleep(.01)
+ self.assertEqual((status, body), (200, self.upstream.last_body))
+ self.wait_recorded()
+ deadline = time.monotonic() + 2
+ while time.monotonic() < deadline:
+ data = report(self.path, 'total', now=time.time() + .01)
+ if data['totals']['dropped']:
+ break
+ time.sleep(.02)
+ self.assertGreater(data['totals']['dropped'], 0)
+ self.assertFalse(data['totals']['complete'])
+
+ def test_chunked_sse_is_forwarded_and_accounted(self):
+ self.upstream.mode = 'chunked'
+ status, body = self.request()
+ self.assertEqual((status, body), (200, self.upstream.last_body))
+ self.wait_recorded()
+ self.assertEqual(report(self.path, 'total', now=time.time() + .01)['totals']['output'], 7)
+
+ def test_sse_first_bytes_do_not_wait_for_remaining_body(self):
+ for mode in ('slow_length', 'slow_chunked'):
+ with self.subTest(mode=mode):
+ self.upstream.mode = mode
+ self.upstream.stream_started.clear()
+ self.upstream.stream_release.clear()
+ connection = socket.create_connection(('127.0.0.1', self.proxy.server_port), timeout=1)
+ connection.sendall(b'POST /v1/chat/completions HTTP/1.1\r\nHost: localhost\r\n'
+ b'Content-Length: 2\r\nX-LLMDeploy-Client-IP: 192.0.2.4\r\n\r\n{}')
+ try:
+ self.assertTrue(self.upstream.stream_started.wait(1))
+ received = b''
+ while b'data:' not in received:
+ received += connection.recv(4096)
+ self.assertIn(b'data:', received)
+ finally:
+ self.upstream.stream_release.set()
+ connection.close()
+
+ def test_truncated_stream_is_not_scored_as_zero(self):
+ self.upstream.mode = 'truncate'
+ with self.assertRaises(IncompleteRead):
+ self.request()
+ self.wait_recorded()
+ data = report(self.path, 'total', now=time.time() + .01)
+ self.assertEqual((data['totals']['known'], data['totals']['interrupted']), (0, 1))
+ self.assertFalse(data['totals']['complete'])
+
+ def test_concurrent_clients_are_attributed_once(self):
+ with ThreadPoolExecutor(max_workers=8) as pool:
+ results = list(pool.map(lambda i: self.request(f'192.0.2.{i + 1}')[0], range(12)))
+ self.assertEqual(results, [200] * 12)
+ self.wait_recorded()
+ data = report(self.path, 'total', now=time.time() + .01)
+ self.assertEqual((data['totals']['known'], data['totals']['output'], len(data['clients'])), (12, 84, 12))
+
+ def test_session_summary_tracks_requests_without_storing_secrets(self):
+ for token, session in (('synthetic-key-a', 'synthetic-private-session'),
+ ('synthetic-key-a', 'synthetic-private-session'),
+ ('synthetic-key-b', 'synthetic-private-session')):
+ connection = HTTPConnection('127.0.0.1', self.proxy.server_port, timeout=5)
+ connection.request('POST', '/v1/chat/completions', b'{}', {
+ 'Authorization': 'Bearer ' + token, 'X-Session-Id': session,
+ 'X-LLMDeploy-Client-IP': '192.0.2.4'})
+ response = connection.getresponse()
+ self.assertEqual(response.status, 200)
+ response.read()
+ connection.close()
+ self.wait_recorded()
+ data = report(self.path, 'total', now=time.time() + .01)
+ self.assertEqual(data['session_count'], 2)
+ self.assertEqual(sorted(item['requests'] for item in data['sessions']), [1, 2])
+ self.assertEqual(sorted(item['input'] for item in data['sessions']), [100, 200])
+ self.assertTrue(all(item['first_visible'] is not None for item in data['requests']))
+ for dbfile in Path(self.temp.name).glob('usage.sqlite3*'):
+ raw = dbfile.read_bytes()
+ for secret in (b'synthetic-key-a', b'synthetic-key-b', b'synthetic-private-session'):
+ self.assertNotIn(secret, raw)
+
+ def test_body_fallback_session_groups_without_persisting_content(self):
+ bodies = [
+ {'messages': [{'role': 'user', 'content': 'synthetic-private-first'}]},
+ {'messages': [{'role': 'user', 'content': 'synthetic-private-first'},
+ {'role': 'assistant', 'content': 'synthetic-response'},
+ {'role': 'user', 'content': 'synthetic-second'}]},
+ {'prompt_cache_key': 'synthetic-private-cache',
+ 'messages': [{'role': 'user', 'content': 'synthetic-private-first'}]},
+ ]
+ for value in bodies:
+ connection = HTTPConnection('127.0.0.1', self.proxy.server_port, timeout=5)
+ connection.request('POST', '/v1/chat/completions', json.dumps(value).encode(), {
+ 'Authorization': 'Bearer synthetic-gateway-key', 'Content-Type': 'application/json',
+ 'X-LLMDeploy-Client-IP': '192.0.2.4'})
+ response = connection.getresponse()
+ self.assertEqual(response.status, 200)
+ response.read()
+ connection.close()
+ self.wait_recorded()
+ value = report(self.path, 'total', now=time.time() + .01)
+ self.assertEqual(value['totals']['unknown_session'], 0)
+ self.assertEqual(sorted(item['requests'] for item in value['sessions']), [1, 2])
+ for dbfile in Path(self.temp.name).glob('usage.sqlite3*'):
+ raw = dbfile.read_bytes()
+ for secret in (b'synthetic-gateway-key', b'synthetic-private-first', b'synthetic-private-cache'):
+ self.assertNotIn(secret, raw)
+
+ def test_body_session_parser_failure_does_not_interrupt_response(self):
+ with patch('deploy_stats.proxy.SessionBodyCapture.finish', side_effect=RuntimeError):
+ connection = HTTPConnection('127.0.0.1', self.proxy.server_port, timeout=5)
+ connection.request('POST', '/v1/chat/completions', b'{"messages":[]}', {
+ 'Authorization': 'Bearer synthetic-gateway-key', 'Content-Type': 'application/json'})
+ response = connection.getresponse()
+ self.assertEqual(response.status, 200)
+ response.read()
+ connection.close()
+ self.wait_recorded()
+ self.assertGreater(self.proxy.recorder.dropped, 0)
+
+ def test_body_session_feed_or_budget_failure_does_not_interrupt_response(self):
+ for target in ('deploy_stats.proxy.SessionBodyCapture.feed', 'deploy_stats.session.BUDGET.reserve'):
+ with self.subTest(target=target), patch(target, side_effect=RuntimeError):
+ self.assertEqual(self.request()[0], 200)
+ self.wait_recorded()
+ self.assertGreaterEqual(self.proxy.recorder.dropped, 2)
+
+ def test_active_snapshot_clears_when_completion_event_is_dropped(self):
+ self.upstream.mode = 'slow'
+ connection = socket.create_connection(('127.0.0.1', self.proxy.server_port), timeout=5)
+ connection.sendall(b'POST /v1/chat/completions HTTP/1.1\r\nHost: localhost\r\n'
+ b'Content-Length: 2\r\nX-LLMDeploy-Client-IP: 192.0.2.4\r\n\r\n{}')
+ self.assertTrue(connection.recv(1024))
+ self.assertTrue(self.upstream.stream_started.wait(2))
+ deadline = time.monotonic() + 4
+ while time.monotonic() < deadline:
+ data = report(self.path, 'total', now=time.time() + .01)
+ if data['active_ready'] and len(data['active']) == 1:
+ break
+ time.sleep(.05)
+ self.assertEqual(len(data['active']), 1)
+ try:
+ with patch.object(self.proxy.recorder.pending, 'put_nowait', side_effect=queue.Full):
+ connection.shutdown(socket.SHUT_RDWR)
+ connection.close()
+ deadline = time.monotonic() + 3
+ while self.proxy.active and time.monotonic() < deadline:
+ time.sleep(.01)
+ self.assertEqual(self.proxy.active, 0)
+ deadline = time.monotonic() + 4
+ while time.monotonic() < deadline:
+ data = report(self.path, 'total', now=time.time() + .01)
+ if data['active_ready'] and not data['active']:
+ break
+ time.sleep(.05)
+ self.assertTrue(data['active_ready'])
+ self.assertEqual(data['active'], [])
+ self.assertGreater(self.proxy.recorder.dropped, 0)
+ finally:
+ self.upstream.stream_release.set()
+ connection.close()
+
+ def test_sixty_four_simultaneous_streams_are_independently_accounted(self):
+ self.upstream.barrier = threading.Barrier(64)
+ with ThreadPoolExecutor(max_workers=64) as pool:
+ statuses = list(pool.map(lambda i: self.request(f'192.0.2.{i + 1}')[0], range(64)))
+ self.assertEqual(statuses, [200] * 64)
+ self.wait_recorded()
+ data = report(self.path, 'total', now=time.time() + .01)
+ self.assertEqual((data['totals']['known'], data['totals']['output'], len(data['clients'])), (64, 448, 64))
+
+ def test_client_cancellation_is_recorded_as_incomplete(self):
+ self.upstream.mode = 'slow'
+ connection = socket.create_connection(('127.0.0.1', self.proxy.server_port), timeout=5)
+ connection.sendall(b'POST /v1/chat/completions HTTP/1.1\r\nHost: localhost\r\n'
+ b'Content-Length: 2\r\nX-LLMDeploy-Client-IP: 192.0.2.4\r\n\r\n{}')
+ self.assertTrue(connection.recv(1024))
+ self.assertTrue(self.upstream.stream_started.wait(2))
+ connection.shutdown(socket.SHUT_RDWR)
+ connection.close()
+ try:
+ self.wait_recorded()
+ finally:
+ self.upstream.stream_release.set()
+ data = report(self.path, 'total', now=time.time() + .01)
+ self.assertEqual((data['totals']['known'], data['totals']['interrupted']), (0, 1))
+
+ def test_client_cancellation_before_response_headers(self):
+ self.upstream.mode = 'slow_headers'
+ connection = socket.create_connection(('127.0.0.1', self.proxy.server_port), timeout=5)
+ connection.sendall(b'POST /v1/chat/completions HTTP/1.1\r\nHost: localhost\r\n'
+ b'Content-Length: 2\r\nX-LLMDeploy-Client-IP: 192.0.2.4\r\n\r\n{}')
+ self.assertTrue(self.upstream.stream_started.wait(2))
+ connection.shutdown(socket.SHUT_RDWR)
+ connection.close()
+ try:
+ self.wait_recorded()
+ finally:
+ self.upstream.stream_release.set()
+ data = report(self.path, 'total', now=time.time() + .01)
+ self.assertEqual(data['totals']['interrupted'], 1)
+
+ def test_large_body_is_forwarded_intact(self):
+ payload = b'x' * (1024 * 1024 + 123)
+ connection = HTTPConnection('127.0.0.1', self.proxy.server_port, timeout=5)
+ connection.request('POST', '/v1/chat/completions', payload, {'X-LLMDeploy-Client-IP': '192.0.2.4'})
+ response = connection.getresponse()
+ self.assertEqual(response.status, 200)
+ response.read()
+ connection.close()
+ self.wait_recorded()
+ self.assertEqual(self.upstream.calls[-1][0], payload)
+
+ def test_large_combined_headers_reach_the_existing_gateway(self):
+ connection = HTTPConnection('127.0.0.1', self.proxy.server_port, timeout=5)
+ headers = {f'X-Synthetic-{i}': 'x' * 45000 for i in range(3)}
+ connection.request('POST', '/v1/chat/completions', b'{}', headers)
+ response = connection.getresponse()
+ self.assertEqual(response.status, 200)
+ response.read()
+ connection.close()
+ self.wait_recorded()
+ self.assertEqual(len(self.upstream.calls), 1)
+
+ @unittest.skipUnless(os.name == 'nt', 'Windows selector capacity')
+ def test_windows_keeps_480_streams_in_flight(self):
+ listener = socket.socket()
+ listener.bind(('127.0.0.1', 0))
+ listener.listen(480)
+ listener.settimeout(.1)
+ accepted, clients = [], []
+ stop = threading.Event()
+
+ def accept_upstreams():
+ while not stop.is_set():
+ try:
+ conn, _ = listener.accept()
+ accepted.append(conn)
+ except socket.timeout:
+ continue
+ except OSError:
+ return
+
+ thread = threading.Thread(target=accept_upstreams, daemon=True)
+ thread.start()
+ proxy = Proxy(0, listener.getsockname()[1], Path(self.temp.name) / 'capacity.sqlite3')
+ self.start(proxy)
+ try:
+ for _ in range(480):
+ conn = socket.create_connection(('127.0.0.1', proxy.server_port), timeout=15)
+ clients.append(conn)
+ conn.sendall(b'POST /v1/chat/completions HTTP/1.1\r\nHost: localhost\r\n'
+ b'Content-Length: 2\r\nX-LLMDeploy-Client-IP: 192.0.2.4\r\n\r\n{}')
+ deadline = time.monotonic() + 20
+ while (len(accepted) < 480 or proxy.connections < 480) and time.monotonic() < deadline:
+ time.sleep(.02)
+ self.assertEqual(len(accepted), 480)
+ self.assertEqual(proxy.connections, 480)
+ with socket.create_connection(('127.0.0.1', proxy.server_port), timeout=5) as extra:
+ extra.sendall(b'GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n')
+ self.assertIn(b'429 Too Many Requests', extra.recv(1024))
+ extra = HTTPConnection('127.0.0.1', proxy.server_port, timeout=5)
+ try:
+ extra.request('POST', '/v1/chat/completions', b'x' * (1024 * 1024))
+ rejected = extra.getresponse()
+ self.assertEqual(rejected.status, 429)
+ rejected.read()
+ except (OSError, IncompleteRead):
+ # A peer still uploading at the limit may observe a reset.
+ pass
+ finally:
+ extra.close()
+ body = sse({'usage': usage()}) + b'data: [DONE]\n\n'
+ response = (b'HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: '
+ + str(len(body)).encode() + b'\r\n\r\n' + body)
+ for conn in accepted:
+ conn.sendall(response)
+ for conn in clients:
+ received = bytearray()
+ while b'data: [DONE]\n\n' not in received:
+ part = conn.recv(4096)
+ self.assertTrue(part)
+ received.extend(part)
+ self.assertIn(b'HTTP/1.1 200 OK', received)
+ deadline = time.monotonic() + 10
+ while proxy.connections and time.monotonic() < deadline:
+ time.sleep(.02)
+ self.assertEqual(proxy.connections, 0)
+ self.assertTrue(proxy.recorder.flush())
+ data = report(proxy.recorder.path, 'total', now=time.time() + .01)
+ self.assertEqual(data['totals']['known'], 480)
+ finally:
+ for conn in clients + accepted:
+ conn.close()
+ stop.set()
+ listener.close()
+ thread.join(3)
+
+ def test_gateway_can_reject_a_large_body_before_reading_it(self):
+ self.upstream.mode = 'reject'
+ for delay, expect in ((0, False), (.05, True), (.15, False)):
+ self.upstream.reject_delay = delay
+ for size in (2, 4095, 4096, 65535, 1024 * 1024):
+ connection = HTTPConnection('127.0.0.1', self.proxy.server_port, timeout=5)
+ headers = {'X-LLMDeploy-Client-IP': '192.0.2.4'}
+ if expect:
+ headers['Expect'] = '100-continue'
+ connection.request('POST', '/v1/chat/completions', b'x' * size, headers)
+ response = connection.getresponse()
+ self.assertEqual(response.status, 429)
+ response.read()
+ connection.close()
+ self.wait_recorded()
+ self.assertEqual(report(self.path, 'total', now=time.time() + .01)['totals']['failed'], 15)
+
+ def test_large_invalid_auth_preserves_real_gateway_response(self):
+ keyfile = Path(self.temp.name) / 'auth-keys.json'
+ keyfile.write_text(json.dumps({'version': 1, 'records': {'fixture': {
+ 'sha256': hashlib.sha256(b'synthetic-gateway-key').hexdigest(), 'active': True}}}))
+ gateway = Gateway(('127.0.0.1', 0), keyfile,
+ state_path=Path(self.temp.name) / 'auth-state.json',
+ bindings_file=Path(self.temp.name) / 'auth-bindings.json',
+ backend_ports=[self.upstream.server_port], backend_managed_affinity=True)
+ self.start(gateway)
+ proxy = Proxy(0, gateway.server_port, Path(self.temp.name) / 'auth-stats.sqlite3')
+ self.start(proxy)
+ direct = HTTPConnection('127.0.0.1', gateway.server_port, timeout=5)
+ direct.request('POST', '/v1/chat/completions', b'{}', {'Authorization': 'Bearer incorrect-placeholder'})
+ response = direct.getresponse()
+ expected = response.status, response.read()
+ direct.close()
+ self.assertEqual(expected[0], 401)
+ for size in (2, 4095, 4096, 65535, 65536, 1024 * 1024):
+ for expect in (False, True):
+ for _ in range(2):
+ connection = HTTPConnection('127.0.0.1', proxy.server_port, timeout=5)
+ headers = {'Authorization': 'Bearer incorrect-placeholder'}
+ if expect:
+ headers['Expect'] = '100-continue'
+ connection.request('POST', '/v1/chat/completions', b'x' * size, headers)
+ response = connection.getresponse()
+ self.assertEqual((response.status, response.read()), expected)
+ connection.close()
+ for size in (4095, 4096, 1024 * 1024):
+ sock = socket.create_connection(('127.0.0.1', proxy.server_port), timeout=5)
+ stop = threading.Event()
+ uploader = None
+ try:
+ sock.sendall((f'POST /v1/chat/completions HTTP/1.1\r\nHost: localhost\r\n'
+ f'Authorization: Bearer incorrect-placeholder\r\nContent-Length: {size}\r\n'
+ 'Expect: 100-continue\r\n\r\n').encode())
+ def upload():
+ remaining = size
+ while remaining and not stop.is_set():
+ amount = min(remaining, 512)
+ try:
+ sock.sendall(b'x' * amount)
+ except OSError:
+ return
+ remaining -= amount
+ time.sleep(.002)
+ uploader = threading.Thread(target=upload, daemon=True)
+ uploader.start()
+ response = HTTPResponse(sock)
+ response.begin()
+ self.assertEqual((response.status, response.read()), expected)
+ finally:
+ stop.set()
+ if uploader:
+ uploader.join(1)
+ sock.close()
+
+ def test_dashboard_is_loopback_only_and_rejects_untrusted_hosts(self):
+ self.request()
+ self.wait_recorded()
+ dashboard = Dashboard(0, self.path)
+ self.start(dashboard)
+ connection = HTTPConnection('127.0.0.1', dashboard.server_port, timeout=5)
+ connection.request('GET', '/api/stats?window=total')
+ result = connection.getresponse()
+ self.assertEqual(result.status, 200)
+ self.assertEqual(json.loads(result.read())['totals']['output'], 7)
+ connection.close()
+ for headers in ({'Host': 'untrusted.invalid'}, {'Origin': 'https://untrusted.invalid'},
+ {'Sec-Fetch-Site': 'cross-site'}):
+ for path in ('/', '/api/active'):
+ connection = HTTPConnection('127.0.0.1', dashboard.server_port, timeout=5)
+ connection.request('GET', path, headers=headers)
+ self.assertEqual(connection.getresponse().status, 403)
+ connection.close()
+
+ def test_dashboard_validates_pagination_and_sort_parameters(self):
+ self.request()
+ self.wait_recorded()
+ dashboard = Dashboard(0, self.path)
+ self.start(dashboard)
+ queries = [
+ ('?limit=10&sort=input&direction=asc&session_limit=50&session_sort=output&session_direction=desc', 200),
+ ('?limit=0', 400), ('?limit=201', 400), ('?session_limit=', 400),
+ ('?sort=not_a_column', 400), ('?session_sort=not_a_column', 400),
+ ('?direction=wrong', 400), ('?offset=1&offset=2', 400),
+ ('?snapshot_id=1', 400), ('?snapshot_at=1', 400), ('?snapshot_at=nan&snapshot_id=1', 400),
+ ]
+ for query, expected in queries:
+ connection = HTTPConnection('127.0.0.1', dashboard.server_port, timeout=5)
+ try:
+ connection.request('GET', '/api/stats' + query)
+ response = connection.getresponse()
+ payload = json.loads(response.read())
+ self.assertEqual(response.status, expected, query)
+ if expected == 200:
+ self.assertEqual((payload['request_limit'], payload['session_limit']), (10, 50))
+ finally:
+ connection.close()
+ for path, expected in (('/api/active', 200), ('/api/active?bad=1', 400)):
+ connection = HTTPConnection('127.0.0.1', dashboard.server_port, timeout=5)
+ try:
+ connection.request('GET', path)
+ response = connection.getresponse()
+ payload = json.loads(response.read())
+ self.assertEqual(response.status, expected)
+ if expected == 200:
+ self.assertIn('active_ready', payload)
+ self.assertIn('active_sampled_at', payload)
+ finally:
+ connection.close()
+
+ def test_existing_listener_is_never_reused(self):
+ with socket.socket() as occupied:
+ occupied.bind(('127.0.0.1', 0))
+ occupied.listen(1)
+ port = occupied.getsockname()[1]
+ with self.assertRaises(OSError):
+ Dashboard(port, self.path)
+ with self.assertRaises(OSError):
+ Proxy(port, self.upstream.server_port, Path(self.temp.name) / 'must-not-exist.sqlite3')
+ self.assertFalse((Path(self.temp.name) / 'must-not-exist.sqlite3').exists())
+
+ def test_server_close_stops_a_running_proxy(self):
+ proxy = Proxy(0, self.upstream.server_port, Path(self.temp.name) / 'close.sqlite3')
+ thread = threading.Thread(target=proxy.serve_forever, daemon=True)
+ thread.start()
+ self.assertTrue(proxy.ready.wait(3))
+ proxy.server_close()
+ thread.join(3)
+ self.assertTrue(proxy.finished.is_set())
+ self.assertFalse(thread.is_alive())
+ self.assertFalse(proxy.recorder.thread.is_alive())
+
+ def test_connection_limit_rejects_and_marks_a_gap(self):
+ self.upstream.mode = 'slow_headers'
+ self.proxy.recorder.heartbeat = .02
+ with patch('deploy_stats.proxy.MAX_CONNECTIONS', 1):
+ first = socket.create_connection(('127.0.0.1', self.proxy.server_port), timeout=2)
+ first.sendall(b'POST /v1/chat/completions HTTP/1.1\r\nHost: localhost\r\nContent-Length: 2\r\n\r\n{}')
+ self.assertTrue(self.upstream.stream_started.wait(2))
+ second = socket.create_connection(('127.0.0.1', self.proxy.server_port), timeout=2)
+ try:
+ second.sendall(b'GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n')
+ self.assertIn(b'429 Too Many Requests', second.recv(1024))
+ finally:
+ second.close()
+ first.close()
+ self.upstream.stream_release.set()
+ self.wait_recorded()
+ deadline = time.monotonic() + 2
+ while time.monotonic() < deadline:
+ data = report(self.path, 'total', now=time.time() + .01)
+ if data['totals']['dropped']:
+ break
+ time.sleep(.02)
+ self.assertGreater(data['totals']['dropped'], 0)
+ self.assertFalse(data['totals']['complete'])
+
+ def test_real_gateway_authentication_is_preserved_through_stats_proxy(self):
+ keyfile = Path(self.temp.name) / 'synthetic-keys.json'
+ keyfile.write_text(json.dumps({'version': 1, 'records': {'fixture': {
+ 'sha256': hashlib.sha256(b'synthetic-gateway-key').hexdigest(), 'active': True}}}))
+ gateway = Gateway(('127.0.0.1', 0), keyfile,
+ state_path=Path(self.temp.name) / 'gateway-state.json',
+ bindings_file=Path(self.temp.name) / 'gateway-bindings.json',
+ backend_ports=[self.upstream.server_port], backend_managed_affinity=True)
+ self.start(gateway)
+ proxy = Proxy(0, gateway.server_port, Path(self.temp.name) / 'gateway-stats.sqlite3')
+ self.start(proxy)
+ for token, expected in ((None, 401), ('incorrect-placeholder', 401), ('synthetic-gateway-key', 200)):
+ headers = {'Content-Type': 'application/json', 'X-LLMDeploy-Client-IP': '192.0.2.8'}
+ if token is not None:
+ headers['Authorization'] = 'Bearer ' + token
+ connection = HTTPConnection('127.0.0.1', proxy.server_port, timeout=5)
+ connection.request('POST', '/v1/chat/completions',
+ json.dumps({'messages': [{'role': 'user', 'content': 'synthetic'}]}).encode(), headers)
+ response = connection.getresponse()
+ self.assertEqual(response.status, expected)
+ response.read()
+ connection.close()
+ deadline = time.monotonic() + 3
+ while time.monotonic() < deadline:
+ with proxy.active_lock:
+ if proxy.active == 0:
+ break
+ time.sleep(.01)
+ self.assertTrue(proxy.recorder.flush())
+ data = report(proxy.recorder.path, 'total', now=time.time() + .01)
+ self.assertEqual((data['totals']['known'], data['totals']['failed']), (1, 2))
+ self.assertIsNone(self.upstream.calls[-1][1])
+ for port in (gateway.server_port, proxy.server_port):
+ connection = HTTPConnection('127.0.0.1', port, timeout=5)
+ connection.request('POST', '/v1/chat/completions', b'{}', {
+ 'Authorization': b'Bearer invalid-\xff', 'X-Session-Id': b'synthetic-\xe4\xb8\xad'})
+ response = connection.getresponse()
+ self.assertEqual(response.status, 401)
+ response.read()
+ connection.close()
+ connection = HTTPConnection('127.0.0.1', port, timeout=5)
+ connection.request('POST', '/v1/chat/completions', b'{}', {
+ 'Authorization': 'Bearer synthetic-gateway-key', 'X-Session-Id': b'synthetic-\xe4\xb8\xad'})
+ response = connection.getresponse()
+ self.assertEqual(response.status, 200)
+ response.read()
+ connection.close()
+ deadline = time.monotonic() + 3
+ while proxy.active and time.monotonic() < deadline:
+ time.sleep(.01)
+ self.assertTrue(proxy.recorder.flush())
+ raw = (b'Authorization: Bearer synthetic-gateway-key\r\n'
+ b'X-Session-Id: synthetic-\xe4\xb8\xad\r\n\r\n')
+ expected = affinity_key(parse_headers(BytesIO(raw)), b'{}', hashlib.sha256(b'synthetic-gateway-key').hexdigest())
+ data = report(proxy.recorder.path, 'total', now=time.time() + .01)
+ self.assertIn(expected, [row['session'] for row in data['requests']])
+
+ @unittest.skipUnless(os.environ.get('LLMDEPLOY_CADDY'), 'Set LLMDEPLOY_CADDY for local Caddy integration')
+ def test_caddy_reload_preserves_an_existing_stream(self):
+ with socket.socket() as probe:
+ probe.bind(('127.0.0.1', 0))
+ port = probe.getsockname()[1]
+ with socket.socket() as probe:
+ probe.bind(('127.0.0.1', 0))
+ admin_port = probe.getsockname()[1]
+ old = Path(self.temp.name) / 'Caddyfile-before'
+ new = Path(self.temp.name) / 'Caddyfile-after'
+ template = '''{{
+ admin 127.0.0.1:{admin}
+ auto_https off
+}}
+http://127.0.0.1:{port} {{
+ bind 127.0.0.1
+ log {{
+ output discard
+ }}
+ reverse_proxy 127.0.0.1:{upstream} {{
+ flush_interval 10ms
+ }}
+}}
+'''
+ old.write_text(template.format(admin=admin_port, port=port, upstream=self.upstream.server_port), encoding='utf-8')
+ new.write_text(template.format(admin=admin_port, port=port, upstream=self.proxy.server_port), encoding='utf-8')
+ environment = dict(os.environ)
+ environment['XDG_DATA_HOME'] = self.temp.name
+ environment['XDG_CONFIG_HOME'] = self.temp.name
+ environment['APPDATA'] = self.temp.name
+ environment['LOCALAPPDATA'] = self.temp.name
+ flags = subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
+ process = subprocess.Popen([os.environ['LLMDEPLOY_CADDY'], 'run', '--config', str(old),
+ '--adapter', 'caddyfile'], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL, env=environment, creationflags=flags)
+ client = None
+ try:
+ deadline = time.monotonic() + 5
+ while not (synthetic_caddy_owns_listener(process, port) and
+ synthetic_caddy_owns_listener(process, admin_port)):
+ if process.poll() is not None or time.monotonic() >= deadline:
+ self.fail('synthetic Caddy listener ownership could not be verified')
+ time.sleep(.05)
+ self.upstream.mode = 'slow_length'
+ client = socket.create_connection(('127.0.0.1', port), timeout=5)
+ client.sendall((f'POST /v1/chat/completions HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\n'
+ 'Content-Length: 2\r\n\r\n{}').encode())
+ self.assertTrue(self.upstream.stream_started.wait(2))
+ received = bytearray()
+ while b'data:' not in received:
+ received.extend(client.recv(4096))
+ self.upstream.mode = 'json'
+ self.assertTrue(synthetic_caddy_owns_listener(process, admin_port))
+ self.assertTrue(synthetic_caddy_owns_listener(process, port))
+ reload = subprocess.run([os.environ['LLMDEPLOY_CADDY'], 'reload', '--address',
+ f'127.0.0.1:{admin_port}', '--config', str(new), '--adapter', 'caddyfile'],
+ stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
+ env=environment, creationflags=flags, timeout=5)
+ self.assertEqual(reload.returncode, 0)
+ self.assertTrue(synthetic_caddy_owns_listener(process, port))
+ connection = HTTPConnection('127.0.0.1', port, timeout=5)
+ connection.request('POST', '/v1/chat/completions', b'{}')
+ response = connection.getresponse()
+ self.assertEqual(response.status, 200)
+ self.assertIn('application/json', response.getheader('Content-Type'))
+ response.read()
+ connection.close()
+ self.wait_recorded()
+ self.assertEqual(report(self.path, 'total', now=time.time() + .01)['totals']['known'], 1)
+ self.upstream.stream_release.set()
+ while b'data: [DONE]\n\n' not in received:
+ part = client.recv(4096)
+ self.assertTrue(part)
+ received.extend(part)
+ finally:
+ self.upstream.stream_release.set()
+ if client:
+ client.close()
+ if process.poll() is None:
+ process.terminate()
+ process.wait(timeout=5)
+
+ @unittest.skipUnless(os.environ.get('LLMDEPLOY_CADDY'), 'Set LLMDEPLOY_CADDY for local Caddy integration')
+ def test_caddy_fallback_preserves_auth_and_never_replays_post(self):
+ keyfile = Path(self.temp.name) / 'fallback-keys.json'
+ keyfile.write_text(json.dumps({'version': 1, 'records': {'fixture': {
+ 'sha256': hashlib.sha256(b'synthetic-gateway-key').hexdigest(), 'active': True}}}))
+ gateway = Gateway(('127.0.0.1', 0), keyfile,
+ state_path=Path(self.temp.name) / 'fallback-state.json',
+ bindings_file=Path(self.temp.name) / 'fallback-bindings.json',
+ backend_ports=[self.upstream.server_port], backend_managed_affinity=True)
+ self.start(gateway)
+ proxy = Proxy(0, gateway.server_port, Path(self.temp.name) / 'fallback-stats.sqlite3')
+ self.start(proxy)
+ reservation = socket.socket()
+ if os.name == 'nt':
+ reservation.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1)
+ reservation.bind(('127.0.0.1', 0))
+ self.addCleanup(reservation.close)
+ primary_port = reservation.getsockname()[1]
+ with socket.socket() as probe:
+ probe.bind(('127.0.0.1', 0))
+ port = probe.getsockname()[1]
+ config = Path(self.temp.name) / 'Caddyfile-fallback'
+ config.write_text('''{
+ admin off
+ auto_https off
+}
+http://127.0.0.1:%d {
+ bind 127.0.0.1
+ log {
+ output discard
+ }
+ reverse_proxy 127.0.0.1:%d 127.0.0.1:%d {
+ lb_policy first
+ fail_duration 2s
+ lb_try_duration 6s
+ lb_try_interval 20ms
+ header_up X-LLMDeploy-Client-IP {remote_host}
+ flush_interval 10ms
+ transport http {
+ dial_timeout 5s
+ }
+ }
+}
+''' % (port, primary_port, gateway.server_port), encoding='utf-8')
+ environment = dict(os.environ)
+ environment['XDG_DATA_HOME'] = self.temp.name
+ environment['XDG_CONFIG_HOME'] = self.temp.name
+ environment['APPDATA'] = self.temp.name
+ environment['LOCALAPPDATA'] = self.temp.name
+ flags = subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
+ process = subprocess.Popen([os.environ['LLMDEPLOY_CADDY'], 'run', '--config', str(config),
+ '--adapter', 'caddyfile'], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL, env=environment, creationflags=flags)
+ primary = None
+ try:
+ deadline = time.monotonic() + 5
+ while not synthetic_caddy_owns_listener(process, port):
+ if process.poll() is not None or time.monotonic() >= deadline:
+ self.fail('synthetic Caddy listener ownership could not be verified')
+ time.sleep(.05)
+
+ def request(key):
+ self.assertTrue(synthetic_caddy_owns_listener(process, port))
+ connection = HTTPConnection('127.0.0.1', port, timeout=5)
+ connection.request('POST', '/v1/chat/completions',
+ b'{"messages":[{"role":"user","content":"synthetic"}]}',
+ {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + key})
+ return connection, connection.getresponse()
+
+ connection, response = request('incorrect-placeholder')
+ self.assertEqual(response.status, 401)
+ response.read()
+ connection.close()
+ self.assertEqual(len(self.upstream.calls), 0)
+ connection, response = request('synthetic-gateway-key')
+ self.assertEqual(response.status, 200)
+ response.read()
+ connection.close()
+ self.assertEqual(len(self.upstream.calls), 1)
+
+ class Primary(BaseHTTPRequestHandler):
+ protocol_version = 'HTTP/1.1'
+ def log_message(self, format, *args):
+ pass
+ def do_POST(self):
+ payload = self.rfile.read(int(self.headers['Content-Length']))
+ self.server.calls += 1
+ if self.server.mode == 'drop':
+ self.connection.shutdown(socket.SHUT_RDWR)
+ self.connection.close()
+ self.close_connection = True
+ return
+ if self.server.mode == 'sse-drop':
+ self.send_response(200)
+ self.send_header('Content-Type', 'text/event-stream')
+ self.send_header('Content-Length', '100')
+ self.end_headers()
+ self.wfile.write(b'data: partial\n\n')
+ self.wfile.flush()
+ self.connection.shutdown(socket.SHUT_RDWR)
+ self.connection.close()
+ self.close_connection = True
+ return
+ upstream = HTTPConnection('127.0.0.1', proxy.server_port, timeout=5)
+ upstream.request('POST', self.path, payload, {
+ 'Content-Type': self.headers.get('Content-Type'),
+ 'Authorization': self.headers.get('Authorization'),
+ 'X-LLMDeploy-Client-IP': self.headers.get('X-LLMDeploy-Client-IP')})
+ result = upstream.getresponse()
+ body = result.read()
+ self.send_response(result.status)
+ self.send_header('Content-Type', result.getheader('Content-Type'))
+ self.send_header('Content-Length', str(len(body)))
+ self.end_headers()
+ self.wfile.write(body)
+ upstream.close()
+
+ class PrimaryServer(ThreadingHTTPServer):
+ def handle_error(self, request, client_address):
+ pass # Deliberate reset scenarios in this test.
+
+ reservation.close()
+ primary = PrimaryServer(('127.0.0.1', primary_port), Primary)
+ primary.mode, primary.calls = 'normal', 0
+ primary.daemon_threads = True
+ self.start(primary)
+ time.sleep(2.2)
+ connection, response = request('synthetic-gateway-key')
+ self.assertEqual(response.status, 200)
+ response.read()
+ connection.close()
+ self.assertEqual((primary.calls, len(self.upstream.calls)), (1, 2))
+ self.assertTrue(proxy.recorder.flush())
+ self.assertEqual(report(proxy.recorder.path, 'total', now=time.time() + .01)['totals']['known'], 1)
+
+ primary.mode = 'drop'
+ try:
+ connection, response = request('synthetic-gateway-key')
+ self.assertNotEqual(response.status, 200)
+ response.read()
+ connection.close()
+ except (OSError, IncompleteRead):
+ pass
+ self.assertEqual((primary.calls, len(self.upstream.calls)), (2, 2))
+ connection, response = request('synthetic-gateway-key')
+ self.assertEqual(response.status, 200)
+ response.read()
+ connection.close()
+ self.assertEqual((primary.calls, len(self.upstream.calls)), (2, 3))
+
+ time.sleep(2.2)
+ primary.mode = 'sse-drop'
+ connection, response = request('synthetic-gateway-key')
+ self.assertEqual(response.status, 200)
+ self.assertEqual(response.read(5), b'data:')
+ with self.assertRaises(IncompleteRead):
+ response.read()
+ connection.close()
+ self.assertEqual((primary.calls, len(self.upstream.calls)), (3, 3))
+
+ time.sleep(2.2)
+ primary.mode = 'normal'
+ connection, response = request('synthetic-gateway-key')
+ self.assertEqual(response.status, 200)
+ response.read()
+ connection.close()
+ self.assertEqual((primary.calls, len(self.upstream.calls)), (4, 4))
+ finally:
+ process.terminate()
+ process.wait(timeout=5)
+
+ @unittest.skipUnless(os.environ.get('LLMDEPLOY_CADDY'), 'Set LLMDEPLOY_CADDY for local Caddy integration')
+ def test_caddy_overwrites_spoofed_origin_and_preserves_response(self):
+ with socket.socket() as probe:
+ probe.bind(('127.0.0.1', 0))
+ port = probe.getsockname()[1]
+ config = Path(self.temp.name) / 'Caddyfile'
+ config.write_text('''{
+ admin off
+ auto_https off
+}
+http://127.0.0.1:%d {
+ bind 127.0.0.1
+ log {
+ output discard
+ }
+ reverse_proxy 127.0.0.1:%d {
+ header_up X-LLMDeploy-Client-IP {remote_host}
+ flush_interval 10ms
+ }
+}
+''' % (port, self.proxy.server_port), encoding='utf-8')
+ environment = dict(os.environ)
+ environment['XDG_DATA_HOME'] = self.temp.name
+ environment['XDG_CONFIG_HOME'] = self.temp.name
+ environment['APPDATA'] = self.temp.name
+ environment['LOCALAPPDATA'] = self.temp.name
+ flags = subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
+ process = subprocess.Popen([os.environ['LLMDEPLOY_CADDY'], 'run', '--config', str(config),
+ '--adapter', 'caddyfile'], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL, env=environment, creationflags=flags)
+ try:
+ deadline = time.monotonic() + 5
+ while not synthetic_caddy_owns_listener(process, port):
+ if process.poll() is not None or time.monotonic() >= deadline:
+ self.fail('synthetic Caddy listener ownership could not be verified')
+ time.sleep(.05)
+ connection = HTTPConnection('127.0.0.1', port, timeout=5)
+ connection.request('POST', '/v1/chat/completions', b'{}', {
+ 'Authorization': 'Bearer synthetic-placeholder',
+ 'X-LLMDeploy-Client-IP': '192.0.2.99'})
+ response = connection.getresponse()
+ self.assertEqual((response.status, response.read()), (200, self.upstream.last_body))
+ connection.close()
+ self.wait_recorded()
+ data = report(self.path, 'total', now=time.time() + .01)
+ self.assertEqual(data['clients'][0]['ip'], '127.0.0.1')
+ self.assertEqual(self.upstream.calls[-1][1:], ('Bearer synthetic-placeholder', None))
+ connection = HTTPConnection('127.0.0.1', port, timeout=5)
+ connection.request('POST', '/v1/chat/completions', b'{}', {
+ 'Authorization': 'Bearer synthetic-placeholder', 'X-Session-Id': b'synthetic-\xe4\xb8\xad'})
+ response = connection.getresponse()
+ self.assertEqual(response.status, 200)
+ response.read()
+ connection.close()
+ self.wait_recorded()
+ gateway_headers = parse_headers(BytesIO(b'Authorization: Bearer synthetic-placeholder\r\n'
+ b'X-Session-Id: synthetic-\xe4\xb8\xad\r\n\r\n'))
+ expected = affinity_key(gateway_headers, b'{}', hashlib.sha256(b'synthetic-placeholder').hexdigest())
+ self.assertIn(expected, [row['session'] for row in report(self.path, 'total', now=time.time() + .01)['requests']])
+ large_headers = {f'X-Synthetic-{i}': 'x' * 5000 for i in range(3)}
+ connection = HTTPConnection('127.0.0.1', port, timeout=5)
+ connection.request('POST', '/v1/chat/completions', b'{}', large_headers)
+ response = connection.getresponse()
+ self.assertEqual(response.status, 200)
+ response.read()
+ connection.close()
+ self.upstream.mode = 'slow_length'
+ self.upstream.stream_started.clear()
+ self.upstream.stream_release.clear()
+ client = socket.create_connection(('127.0.0.1', port), timeout=1)
+ client.sendall((f'POST /v1/chat/completions HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\n'
+ 'Content-Length: 2\r\n\r\n{}').encode())
+ try:
+ self.assertTrue(self.upstream.stream_started.wait(1))
+ received = b''
+ while b'data:' not in received:
+ received += client.recv(4096)
+ self.assertIn(b'data:', received)
+ client.shutdown(socket.SHUT_RDWR)
+ client.close()
+ self.wait_recorded()
+ data = report(self.path, 'total', now=time.time() + .01)
+ self.assertEqual(data['totals']['interrupted'], 1)
+ finally:
+ self.upstream.stream_release.set()
+ client.close()
+ finally:
+ if process.poll() is None:
+ process.terminate()
+ process.wait(timeout=5)
+
+
+class ReportTests(unittest.TestCase):
+ def test_overview_phase_estimates_use_measurable_success_samples(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ path = Path(tmp) / 'usage.sqlite3'
+ now = time.time()
+ db = _open(path)
+ db.execute('INSERT INTO runs VALUES (?,?,?,?,?)', ('fixture', now - 1000, now, now, 0))
+ statement = '''INSERT INTO requests(run_id,finished,client_ip,elapsed,http_status,state,
+ input_tokens,output_tokens,cached_tokens,session_id,first_visible) VALUES(?,?,?,?,?,?,?,?,?,?,?)'''
+ db.execute(statement, ('fixture', now - 10, '192.0.2.4', 2, 200, 'known', 100, 7, 50, None, None))
+ db.commit()
+ empty = report(path, 'total', now=now + .01)['totals']
+ self.assertIsNone(empty['first_mean'])
+ self.assertIsNone(empty['prefill_estimate_tps'])
+ self.assertIsNone(empty['decode_estimate_tps'])
+ for elapsed, prompt, output, cached, first in ((2.5, 100, 11, 40, .5),
+ (4.5, 50, 6, 0, 1.5),
+ (2, 50, 1, 10, 1)):
+ db.execute(statement, ('fixture', now - 5, '192.0.2.4', elapsed, 200, 'known',
+ prompt, output, cached, None, first))
+ db.execute(statement, ('fixture', now - 5, '192.0.2.4', 2, 500, 'failed',
+ None, None, None, None, None))
+ db.commit()
+ db.close()
+ totals = report(path, 'total', now=now + .01)['totals']
+ self.assertEqual((totals['known'], totals['first_samples'], totals['prefill_samples'], totals['decode_samples']),
+ (4, 3, 3, 2))
+ self.assertAlmostEqual(totals['first_mean'], 1)
+ self.assertAlmostEqual(totals['prefill_estimate_tps'], 50)
+ self.assertAlmostEqual(totals['decode_estimate_tps'], 3)
+ self.assertAlmostEqual(totals['hit_rate'], 1 / 3)
+
+ def test_subsecond_coverage_gap_is_incomplete_everywhere(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ path = Path(tmp) / 'usage.sqlite3'
+ now = datetime(2026, 1, 3, 12, tzinfo=timezone(timedelta(hours=8))).timestamp()
+ start = now - 12 * 3600
+ db = _open(path)
+ db.execute('INSERT INTO runs VALUES (?,?,?,?,?)', ('first', start, now - 300.4, now - 300.4, 0))
+ db.execute('INSERT INTO runs VALUES (?,?,?,?,?)', ('second', now - 299.6, now, now, 0))
+ db.commit()
+ db.close()
+ value = report(path, 'today', now=now)
+ self.assertFalse(value['totals']['complete'])
+ self.assertFalse(value['days'][-1]['complete'])
+ bucket = next(point for point in value['series_24h'] if point['start'] == now - 600)
+ self.assertFalse(bucket['complete'])
+ self.assertAlmostEqual(bucket['covered_seconds'], 599.2, places=4)
+
+ def test_stale_active_snapshot_is_unknown_even_with_live_heartbeat(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ path = Path(tmp) / 'usage.sqlite3'
+ now = time.time()
+ db = _open(path)
+ db.execute('INSERT INTO runs VALUES (?,?,?,?,?)', ('live', now - 60, now - 1, None, 0))
+ db.execute('INSERT INTO active_samples VALUES (?,?)', ('live', now - 4))
+ db.execute('INSERT INTO active_requests VALUES (?,?,?,?,?,?)',
+ ('fixture', 'live', now - 30, '192.0.2.4', None, None))
+ db.commit()
+ db.close()
+ value = report(path, 'total', now=now)
+ self.assertTrue(value['collector_live'])
+ self.assertFalse(value['active_ready'])
+ self.assertEqual(value['active'], [])
+ lightweight = active_report(path, now=now)
+ self.assertTrue(lightweight['collector_live'])
+ self.assertFalse(lightweight['active_ready'])
+ self.assertEqual(lightweight['active'], [])
+
+ def test_lightweight_active_report_matches_full_report(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ path = Path(tmp) / 'usage.sqlite3'
+ now = time.time()
+ db = _open(path)
+ db.execute('INSERT INTO runs VALUES (?,?,?,?,?)', ('live', now - 60, now - 1, None, 0))
+ db.execute('INSERT INTO active_samples VALUES (?,?)', ('live', now - 1))
+ db.execute('INSERT INTO active_requests VALUES (?,?,?,?,?,?)',
+ ('fixture', 'live', now - 30, '192.0.2.4', 'synthetic-session', None))
+ db.commit()
+ db.close()
+ full = report(path, 'total', now=now)
+ lightweight = active_report(path, now=now)
+ for key in ('collector_live', 'active_ready', 'active_sampled_at', 'active'):
+ self.assertEqual(lightweight[key], full[key])
+ self.assertTrue(lightweight['active_ready'])
+ self.assertEqual(len(lightweight['active']), 1)
+ self.assertFalse(active_report(path, now=now + 3)['active_ready'])
+
+ def test_session_and_request_pagination_do_not_drop_history(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ path = Path(tmp) / 'usage.sqlite3'
+ now = time.time()
+ db = _open(path)
+ db.execute('INSERT INTO runs VALUES (?,?,?,?,?)', ('fixture', now - 1000, now, now, 0))
+ for i in range(205):
+ db.execute('''INSERT INTO requests(run_id,finished,client_ip,elapsed,http_status,state,
+ input_tokens,output_tokens,cached_tokens,session_id,first_visible)
+ VALUES(?,?,?,?,?,?,?,?,?,?,?)''',
+ ('fixture', now - 205 + i, '192.0.2.4', 2, 200, 'known', 100, 7, 60,
+ f'session-{i % 105}', .5))
+ db.commit()
+ db.close()
+ value = report(path, 'total', now=now + .01, request_offset=100, session_offset=100)
+ self.assertEqual((value['request_count'], len(value['requests'])), (205, 100))
+ self.assertEqual((value['session_count'], len(value['sessions'])), (105, 5))
+ self.assertEqual(value['totals']['input'], 20500)
+
+ def test_sorting_is_global_before_independent_pagination(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ path = Path(tmp) / 'usage.sqlite3'
+ now = time.time()
+ db = _open(path)
+ db.execute('INSERT INTO runs VALUES (?,?,?,?,?)', ('fixture', now - 1000, now, now, 0))
+ for i in range(73):
+ known = i % 11 != 0
+ db.execute('''INSERT INTO requests(run_id,finished,client_ip,elapsed,http_status,state,
+ input_tokens,output_tokens,cached_tokens,session_id,first_visible)
+ VALUES(?,?,?,?,?,?,?,?,?,?,?)''',
+ ('fixture', now - 100 + i, None if i == 5 else f'192.0.2.{i % 15 + 1}', 2, 200, 'known' if known else 'missing',
+ 1000 - i if known else None, i + 1 if known else None, 0 if known else None,
+ f'session-{i % 17:02}', .5 if known else None))
+ db.commit()
+ db.close()
+ for size in (10, 20, 50):
+ for direction in ('asc', 'desc'):
+ pages = [report(path, 'total', now=now + .01, request_offset=offset, request_limit=size,
+ request_sort='input', request_dir=direction, session_limit=10)
+ for offset in range(0, 73, size)]
+ records = [item for page in pages for item in page['requests']]
+ self.assertEqual(len({item['id'] for item in records}), 73)
+ inputs = [item['input'] for item in records if item['input'] is not None]
+ self.assertEqual(inputs, sorted(inputs, reverse=direction == 'desc'))
+ self.assertTrue(all(item['input'] is None for item in records[len(inputs):]))
+ self.assertEqual(pages[0]['request_limit'], size)
+ self.assertEqual(pages[0]['session_limit'], 10)
+ for direction in ('asc', 'desc'):
+ ip_page = report(path, 'total', now=now + .01, request_limit=200,
+ request_sort='ip', request_dir=direction)['requests']
+ self.assertIsNone(ip_page[-1]['ip'])
+ octets = [int(item['ip'].split('.')[-1]) for item in ip_page if item['ip'] is not None]
+ self.assertEqual(octets, sorted(octets, reverse=direction == 'desc'))
+ session_pages = [report(path, 'total', now=now + .01, request_limit=20, session_offset=offset,
+ session_limit=10, session_sort='output', session_dir='asc')
+ for offset in (0, 10)]
+ sessions = [item for page in session_pages for item in page['sessions']]
+ self.assertEqual(len({item['session'] for item in sessions}), 17)
+ self.assertEqual([item['output'] for item in sessions], sorted(item['output'] for item in sessions))
+ clamped = report(path, 'total', now=now + .01, request_offset=10000, request_limit=20,
+ session_offset=10000, session_limit=10)
+ self.assertEqual((clamped['request_offset'], clamped['session_offset']), (60, 10))
+ for bad in ({'request_sort': 'bad'}, {'session_sort': 'bad'}, {'request_dir': 'bad'},
+ {'session_dir': 'bad'}, {'request_limit': 0}, {'session_limit': 201}):
+ with self.assertRaises(ValueError):
+ report(path, 'total', now=now + .01, **bad)
+
+ def test_session_sort_puts_unknown_source_last_in_both_directions(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ path = Path(tmp) / 'usage.sqlite3'
+ now = time.time()
+ db = _open(path)
+ db.execute('INSERT INTO runs VALUES (?,?,?,?,?)', ('fixture', now - 100, now, now, 0))
+ for i, ip in enumerate((None, '192.0.2.1', '192.0.2.20')):
+ db.execute('''INSERT INTO requests(run_id,finished,client_ip,elapsed,http_status,state,
+ input_tokens,output_tokens,cached_tokens,session_id,first_visible)
+ VALUES(?,?,?,?,?,?,?,?,?,?,?)''',
+ ('fixture', now - 10 + i, ip, 2, 200, 'known', 100, 7, 0, f'session-{i}', .5))
+ db.commit()
+ db.close()
+ for direction in ('asc', 'desc'):
+ sessions = report(path, 'total', now=now + .01, session_sort='ips', session_dir=direction)['sessions']
+ self.assertEqual(sessions[-1]['ips'], ['未知来源'])
+
+ def test_page_snapshot_excludes_new_and_late_commits_but_live_totals_advance(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ path = Path(tmp) / 'usage.sqlite3'
+ now = time.time()
+ db = _open(path)
+ db.execute('INSERT INTO runs VALUES (?,?,?,?,?)', ('fixture', now - 200000, now, now, 0))
+ statement = '''INSERT INTO requests(run_id,finished,client_ip,elapsed,http_status,state,
+ input_tokens,output_tokens,cached_tokens,session_id,first_visible) VALUES(?,?,?,?,?,?,?,?,?,?,?)'''
+ for i in range(40):
+ db.execute(statement, ('fixture', now - 100 + i, '192.0.2.4', 2, 200, 'known',
+ 100, 7, 0, f'session-{i:02}', .5))
+ db.commit()
+ db.close()
+ first = report(path, 'total', now=now, request_limit=20, session_limit=20)
+ db = _open(path)
+ for i, finished in enumerate((now + 1, now - 1)):
+ db.execute(statement, ('fixture', finished, '192.0.2.4', 2, 200, 'known',
+ 100, 7, 0, f'new-session-{i}', .5))
+ db.execute('UPDATE runs SET last_seen=?,ended=? WHERE id=?', (now + 5, now + 5, 'fixture'))
+ db.commit()
+ db.close()
+ second = report(path, 'total', now=now + 5, request_offset=20, request_limit=20,
+ session_offset=20, session_limit=20,
+ snapshot_at=first['snapshot_at'], snapshot_id=first['snapshot_id'])
+ self.assertEqual((second['request_count'], second['session_count'], second['totals']['requests']), (40, 40, 42))
+ self.assertEqual(len({item['id'] for item in first['requests'] + second['requests']}), 40)
+ self.assertEqual(len({item['session'] for item in first['sessions'] + second['sessions']}), 40)
+ latest = report(path, 'total', now=now + 5)
+ self.assertEqual((latest['request_count'], latest['session_count']), (42, 42))
+
+ def test_snapshot_completeness_uses_snapshot_window(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ path = Path(tmp) / 'usage.sqlite3'
+ now = time.time()
+ db = _open(path)
+ db.execute('INSERT INTO runs VALUES (?,?,?,?,?)', ('fixture', now - 86400, now, now, 0))
+ db.execute('''INSERT INTO requests(run_id,finished,client_ip,elapsed,http_status,state,
+ input_tokens,output_tokens,cached_tokens,session_id,first_visible) VALUES(?,?,?,?,?,?,?,?,?,?,?)''',
+ ('fixture', now - 121, '192.0.2.4', 2, 200, 'known', 100, 7, 0, 'session', .5))
+ db.commit()
+ db.close()
+ value = report(path, '24h', now=now, snapshot_at=now - 120, snapshot_id=1)
+ self.assertTrue(value['totals']['complete'])
+ self.assertFalse(value['snapshot_complete'])
+ self.assertEqual(value['sessions'][0]['input'], 100)
+
+ def test_schema_one_migrates_without_losing_history(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ path = Path(tmp) / 'usage.sqlite3'
+ db = sqlite3.connect(path)
+ db.executescript('''CREATE TABLE stats_meta (key TEXT PRIMARY KEY,value TEXT NOT NULL);
+ INSERT INTO stats_meta VALUES ('application','llmdeploy-gateway-stats');
+ CREATE TABLE runs (id TEXT PRIMARY KEY,started REAL NOT NULL,last_seen REAL NOT NULL,
+ ended REAL,dropped INTEGER NOT NULL DEFAULT 0);
+ CREATE TABLE requests (id INTEGER PRIMARY KEY,run_id TEXT NOT NULL,finished REAL NOT NULL,
+ client_ip TEXT,elapsed REAL NOT NULL,http_status INTEGER,state TEXT NOT NULL,
+ input_tokens INTEGER,output_tokens INTEGER,cached_tokens INTEGER);
+ INSERT INTO runs VALUES ('old',100,200,200,0);
+ INSERT INTO requests VALUES (1,'old',150,'192.0.2.4',2,200,'known',100,7,60);
+ PRAGMA user_version=1;''')
+ db.commit()
+ db.close()
+ migrated = _open(path)
+ self.assertEqual(migrated.execute('PRAGMA user_version').fetchone()[0], 2)
+ self.assertEqual(migrated.execute('SELECT input_tokens,session_id,first_visible FROM requests').fetchone(),
+ (100, None, None))
+ migrated.execute("UPDATE runs SET started=?,last_seen=?,ended=? WHERE id='old'",
+ (1800000100, 1800000200, 1800000200))
+ migrated.execute("UPDATE requests SET finished=1800000150 WHERE run_id='old'")
+ migrated.commit()
+ migrated.close()
+ value = report(path, 'total', now=1800000201)
+ self.assertEqual((value['totals']['input'], value['totals']['unknown_session']), (100, 1))
+
+ def test_backend_metric_subset_and_unavailable(self):
+ class Metrics(BaseHTTPRequestHandler):
+ def log_message(self, format, *args):
+ pass
+ def do_GET(self):
+ body = (b'deepinfer:requests_running 2\n' b'deepinfer:requests_waiting 1\n'
+ b'deepinfer:workers_alive 8\n' b'deepinfer:admission_ready 1\n'
+ b'deepinfer:prompt_tokens_total 100\n' b'deepinfer:prompt_cache_hit_tokens_total 30\n'
+ b'deepinfer:prompt_cache_hit_requests_total 4\n')
+ if getattr(self.server, 'invalid_ratio', False):
+ body = body.replace(b'prompt_cache_hit_tokens_total 30', b'prompt_cache_hit_tokens_total 130')
+ self.send_response(200)
+ self.send_header('Content-Length', str(len(body)))
+ self.end_headers()
+ self.wfile.write(body)
+ server = ThreadingHTTPServer(('127.0.0.1', 0), Metrics)
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
+ thread.start()
+ try:
+ value = read_backend(server.server_port)
+ self.assertTrue(value['available'])
+ self.assertEqual((value['running'], value['waiting'], value['cached_total']), (2, 1, 30))
+ self.assertEqual(value['hit_rate'], .3)
+ server.invalid_ratio = True
+ self.assertIsNone(read_backend(server.server_port)['hit_rate'])
+ finally:
+ server.shutdown()
+ server.server_close()
+ thread.join(3)
+
+ def test_unclean_stale_run_never_claims_a_historical_day_is_complete(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ path = Path(tmp) / 'usage.sqlite3'
+ tz = timezone(timedelta(hours=8))
+ now = datetime(2026, 1, 3, 12, tzinfo=tz).timestamp()
+ start = datetime(2026, 1, 1, 0, tzinfo=tz).timestamp()
+ sample = datetime(2026, 1, 2, 12, tzinfo=tz).timestamp()
+ db = _open(path)
+ db.execute('INSERT INTO runs VALUES (?,?,?,?,?)', ('unclean', start, now - 7200, None, 0))
+ db.execute('''INSERT INTO requests(run_id,finished,client_ip,elapsed,http_status,state,
+ input_tokens,output_tokens,cached_tokens) VALUES(?,?,?,?,?,?,?,?,?)''',
+ ('unclean', sample, '192.0.2.4', 2, 200, 'known', 100, 7, 60))
+ db.commit()
+ db.close()
+ value = report(path, 'total', now=now)
+ day = next(item for item in value['days'] if item['date'] == '2026-01-02')
+ self.assertEqual(day['covered_seconds'], 86400)
+ self.assertEqual(day['known'], 1)
+ self.assertFalse(day['complete'])
+
+ def test_cleanly_drained_run_can_cover_a_complete_historical_day(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ path = Path(tmp) / 'usage.sqlite3'
+ tz = timezone(timedelta(hours=8))
+ now = datetime(2026, 1, 3, 12, tzinfo=tz).timestamp()
+ start = datetime(2026, 1, 1, 0, tzinfo=tz).timestamp()
+ db = _open(path)
+ db.execute('INSERT INTO runs VALUES (?,?,?,?,?)', ('clean', start, now - 7200, now - 7200, 0))
+ db.commit()
+ db.close()
+ value = report(path, 'total', now=now)
+ day = next(item for item in value['days'] if item['date'] == '2026-01-02')
+ self.assertTrue(day['complete'])
+ self.assertEqual(day['unclean_runs'], 0)
+
+ def test_local_days_and_gaps_are_reported_without_zero_filling(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ path = Path(tmp) / 'usage.sqlite3'
+ now = datetime(2026, 1, 2, 1, tzinfo=timezone(timedelta(hours=8))).timestamp()
+ db = _open(path)
+ db.execute('INSERT INTO runs VALUES (?,?,?,?,?)', ('fixture', now - 7200, now - 1, None, 0))
+ for stamp, state, values in ((now - 3700, 'known', (100, 7, 60)),
+ (now - 100, 'missing', (None, None, None))):
+ db.execute('''INSERT INTO requests(run_id,finished,client_ip,elapsed,http_status,state,
+ input_tokens,output_tokens,cached_tokens) VALUES(?,?,?,?,?,?,?,?,?)''',
+ ('fixture', stamp, '192.0.2.4', 2, 200, state, *values))
+ db.commit()
+ db.close()
+ value = report(path, '24h', now=now)
+ self.assertFalse(value['totals']['complete'])
+ self.assertEqual(value['totals']['known'], 1)
+ self.assertEqual(value['totals']['missing'], 1)
+ yesterday = next(d for d in value['days'] if d['date'] == '2026-01-01')
+ self.assertEqual((yesterday['input'], yesterday['output'], yesterday['cached']), (100, 7, 60))
+ self.assertTrue(any(not point['complete'] and not point['known'] for point in value['series_24h']))
+
+ def test_existing_unrelated_database_is_not_repurposed(self):
+ for version in (0, 1):
+ with self.subTest(version=version), tempfile.TemporaryDirectory() as tmp:
+ path = Path(tmp) / 'existing.sqlite3'
+ with closing(sqlite3.connect(path)) as db:
+ db.execute('CREATE TABLE keep_me(value TEXT)')
+ db.execute("INSERT INTO keep_me VALUES ('preserve')")
+ db.execute(f'PRAGMA user_version={version}')
+ db.commit()
+ with self.assertRaises(ValueError):
+ Recorder(path)
+ with closing(sqlite3.connect(path)) as db:
+ self.assertEqual(db.execute('SELECT value FROM keep_me').fetchone()[0], 'preserve')
+ self.assertEqual(db.execute('PRAGMA journal_mode').fetchone()[0], 'delete')
+
+ def test_full_writer_queue_is_reported_as_a_gap(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ path = Path(tmp) / 'usage.sqlite3'
+ recorder = Recorder(path, queue_size=1, heartbeat=.02)
+ with closing(sqlite3.connect(path, timeout=1)) as blocker:
+ blocker.execute('BEGIN IMMEDIATE')
+ event = (time.time(), '192.0.2.4', 1.0, 200, 'known', 10, 2, 5)
+ accepted = [recorder.submit(event) for _ in range(20)]
+ self.assertIn(False, accepted)
+ blocker.rollback()
+ self.assertTrue(recorder.flush())
+ self.assertTrue(recorder.close())
+ data = report(path, 'total', now=time.time() + .01)
+ self.assertGreater(data['totals']['dropped'], 0)
+ self.assertFalse(data['totals']['complete'])
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_telemetry.py b/tests/test_telemetry.py
new file mode 100644
index 0000000..0388371
--- /dev/null
+++ b/tests/test_telemetry.py
@@ -0,0 +1,259 @@
+from copy import deepcopy
+from http.client import HTTPConnection
+import threading
+import time
+import unittest
+from unittest import mock
+
+from deploy_stats import remote_observe, telemetry, web
+
+
+HASHES = {key: 'a' * 64 for key in telemetry.SOURCE_KEYS}
+
+
+def node(label='one', port=19001):
+ return {'label': label, 'backend_port': port, 'ssh_target': 'user@example.invalid',
+ 'container': 'example-container', 'image_fingerprint': 'c' * 64, 'driver_sha256': '9' * 64,
+ 'source_sha256': HASHES.copy(), 'rank_devices': [0, 1],
+ 'prefill_flops_per_token': 10.0, 'peak_flops_per_device': 1000.0}
+
+
+def sample(now, *, phase=True):
+ rows = [{'kind': 'stats', 'at': now - 1 + rank * .01, 'rank': rank, 'device': rank,
+ 'step': 10, 'is_prompt': phase, 'current_tps': 100 + rank * 10,
+ 'current_sps': 10 + rank * .01} for rank in range(2)]
+ rows += [{'kind': 'cpu', 'at': now - .9 + rank * .01, 'rank': rank, 'device': rank, 'step': 10,
+ 'timers': {'prefill': {'mean_ms': 20 + rank * 10, 'calls': rank + 1}}} for rank in range(2)]
+ return {'schema': 1, 'source_id': 'b' * 64, 'host_fingerprint': 'd' * 64, 'image_fingerprint': 'c' * 64,
+ 'driver_sha256': '9' * 64,
+ 'source_sha256': HASHES.copy(), 'source_before_start': True,
+ 'source_started_at': now - 100, 'started_at': now - 2, 'finished_at': now,
+ 'hbm': [{'id': i, 'started_at': now - .55 + i * .01, 'finished_at': now - .45 + i * .01,
+ 'hbm_bandwidth_percent': 20 + i * 10} for i in range(2)],
+ 'engine_rows': rows}
+
+
+class TelemetryTests(unittest.TestCase):
+ def test_log_parsing_keeps_good_fields_when_phase_share_is_nan(self):
+ line = ('2026-11-02T00:00:00.123456789Z INFO RANK 0, GPU 0, step 17, [stats] '
+ '(is_prompt True | cur_SPS 10.000 | cur_TPS 12.5 | prompt_TPS 0.0 | '
+ 'completion_TPS 0.0 | prefill/decode nan%/nan% | MFU 0.123)\n')
+ row = remote_observe.parse_log(line.encode())[0]
+ self.assertTrue(row['is_prompt'])
+ self.assertEqual(row['current_tps'], 12.5)
+ self.assertEqual(row['current_sps'], 10)
+ self.assertIsNone(row['phase_share'])
+
+ def test_same_second_source_mtime_does_not_prove_runtime_identity(self):
+ count = len(remote_observe.SOURCES)
+ self.assertFalse(remote_observe.source_predates(['100'] * count, 100.9))
+ self.assertTrue(remote_observe.source_predates(['99'] * count, 100.1))
+ self.assertFalse(remote_observe.source_predates(['99'] * (count - 1), 100.1))
+
+ def test_docker_observation_does_not_activate_socket(self):
+ active = mock.Mock(returncode=0, stdout=b'active\n')
+ inactive = mock.Mock(returncode=3, stdout=b'inactive\n')
+ with mock.patch.object(remote_observe.subprocess, 'run', side_effect=[active, inactive]):
+ self.assertTrue(remote_observe.docker_ready())
+ with mock.patch.object(remote_observe.subprocess, 'run', side_effect=[inactive, active]):
+ self.assertFalse(remote_observe.docker_ready())
+ with mock.patch.object(remote_observe, 'docker_ready', return_value=False), \
+ mock.patch.object(remote_observe.subprocess, 'Popen') as popen:
+ with self.assertRaises(ValueError):
+ remote_observe.bounded_command(['docker', 'inspect', 'synthetic'], 1, 100)
+ popen.assert_not_called()
+
+ def test_complete_matching_step_and_device_samples(self):
+ now = time.time()
+ result = telemetry.summarize_node(sample(now), node(), now - 3, now + 1)
+ self.assertTrue(result['source_ok'])
+ self.assertEqual(result['prefill']['tokens_per_second'], 210)
+ self.assertAlmostEqual(result['prefill']['mfu_estimate'], 1.05)
+ self.assertEqual(result['hbm']['mean_percent'], 25)
+ self.assertAlmostEqual(result['phase_timers']['prefill']['mean_ms'], 80 / 3)
+ self.assertIsNone(result['decode'])
+
+ def test_missing_duplicate_mixed_stale_and_skewed_steps_are_unknown(self):
+ now = time.time()
+ variants = []
+ missing = sample(now); missing['engine_rows'] = missing['engine_rows'][1:]; variants.append(missing)
+ duplicate = sample(now); duplicate['engine_rows'].append(deepcopy(duplicate['engine_rows'][0])); variants.append(duplicate)
+ mixed = sample(now); mixed['engine_rows'][1]['is_prompt'] = False; variants.append(mixed)
+ stale = sample(now); stale['engine_rows'][0]['at'] = now - 70; variants.append(stale)
+ spread = sample(now); spread['engine_rows'][1]['current_sps'] = 20; variants.append(spread)
+ rounded = sample(now); rounded['engine_rows'][0]['current_sps'] = .001; rounded['engine_rows'][1]['current_sps'] = .001; variants.append(rounded)
+ wrong_device = sample(now); wrong_device['engine_rows'][1]['device'] = 0; variants.append(wrong_device)
+ for raw in variants:
+ with self.subTest(raw=raw):
+ value = telemetry.summarize_node(raw, node(), now - 3, now + 1)
+ self.assertIsNone(value['prefill'])
+ self.assertIsNotNone(value['hbm'])
+
+ def test_unknown_source_invalid_bandwidth_and_clock_are_not_zero(self):
+ now = time.time()
+ raw = sample(now); raw['source_sha256']['stats'] = 'b' * 64
+ self.assertFalse(telemetry.summarize_node(raw, node(), now - 3, now + 1)['source_ok'])
+ raw = sample(now); raw['source_before_start'] = False
+ self.assertFalse(telemetry.summarize_node(raw, node(), now - 3, now + 1)['source_ok'])
+ raw = sample(now); raw['image_fingerprint'] = 'e' * 64
+ self.assertFalse(telemetry.summarize_node(raw, node(), now - 3, now + 1)['source_ok'])
+ raw = sample(now); raw['driver_sha256'] = '8' * 64
+ value = telemetry.summarize_node(raw, node(), now - 3, now + 1)
+ self.assertTrue(value['source_ok'])
+ self.assertFalse(value['driver_ok'])
+ self.assertIsNone(value['hbm'])
+ self.assertIsNotNone(value['prefill'])
+ raw = sample(now); raw['hbm'][1]['hbm_bandwidth_percent'] = None
+ value = telemetry.summarize_node(raw, node(), now - 3, now + 1)
+ self.assertIsNone(value['hbm'])
+ self.assertIsNotNone(value['prefill'])
+ raw = sample(now); raw['finished_at'] = now + 20
+ self.assertFalse(telemetry.summarize_node(raw, node(), now - 3, now + 1)['clock_ok'])
+
+ def test_host_clock_offset_is_bounded_before_cross_node_combination(self):
+ now = time.time()
+ raw = sample(now)
+ for key in ('source_started_at', 'started_at', 'finished_at'):
+ raw[key] += 9
+ for item in raw['engine_rows']:
+ item['at'] += 9
+ for item in raw['hbm']:
+ item['started_at'] += 9
+ item['finished_at'] += 9
+ corrected = telemetry.summarize_node(raw, node(), now - 3, now + 1)
+ self.assertTrue(corrected['clock_ok'])
+ self.assertAlmostEqual(corrected['prefill']['log_at'], now - .99)
+ self.assertAlmostEqual(corrected['clock_uncertainty_seconds'], 1)
+ self.assertIsNotNone(corrected['hbm'])
+ self.assertEqual(telemetry.summarize([corrected], now)['prefill_tps'], 210)
+
+ def test_distinct_device_groups_are_not_combined(self):
+ now = time.time()
+ first = telemetry.summarize_node(sample(now), node(), now - 3, now + 1)
+ other_node = node('two', 19002); other_node['peak_flops_per_device'] = 2000.0
+ raw = sample(now); raw['host_fingerprint'] = 'e' * 64; raw['source_id'] = 'f' * 64
+ second = telemetry.summarize_node(raw, other_node, now - 3, now + 1)
+ total = telemetry.summarize([first, second], now)
+ self.assertIsNone(total['prefill_tps'])
+ self.assertIsNone(total['prefill_mfu'])
+ self.assertIsNone(total['hbm_percent'])
+ self.assertTrue(all(n['source_ok'] for n in total['nodes']))
+
+ def test_duplicate_lifecycle_or_host_invalidates_both_groups(self):
+ now = time.time()
+ first = telemetry.summarize_node(sample(now), node(), now - 3, now + 1)
+ second = telemetry.summarize_node(sample(now), node('two', 19002), now - 3, now + 1)
+ values = telemetry.summarize([first, second], now)['nodes']
+ self.assertTrue(all(n['duplicate_source'] and not n['source_ok'] and n['hbm'] is None for n in values))
+
+ def test_public_snapshot_omits_internal_identity_fingerprints(self):
+ now = time.time()
+ value = telemetry.summarize([telemetry.summarize_node(sample(now), node(), now - 3, now + 1)], now)
+ public = telemetry.public_snapshot(value)
+ self.assertNotIn('source_id', public['nodes'][0])
+ self.assertNotIn('host_fingerprint', public['nodes'][0])
+ self.assertIn('source_id', value['nodes'][0])
+
+ def test_combination_checks_full_rank_and_device_intervals(self):
+ now = time.time()
+ first = telemetry.summarize_node(sample(now), node(), now - 3, now + 1)
+ second = telemetry.summarize_node(sample(now), node('two', 19002), now - 3, now + 1)
+ first['clock_uncertainty_seconds'] = second['clock_uncertainty_seconds'] = 0
+ first['prefill']['log_at'], first['prefill']['log_skew_seconds'] = now - 5, 5
+ second['prefill']['log_at'], second['prefill']['log_skew_seconds'] = now, 5
+ first['hbm']['sample_at'], first['hbm']['span_seconds'] = now - 5, 5
+ second['hbm']['sample_at'], second['hbm']['span_seconds'] = now, 4.8
+ result = telemetry.summarize([first, second], now)
+ self.assertIsNone(result['prefill_tps'])
+ self.assertIsNone(result['hbm_percent'])
+
+ def test_snapshot_expires_by_oldest_rank_not_latest(self):
+ now = time.time()
+ collector = telemetry.Collector({'interval_seconds': 30, 'nodes': [node()]})
+ item = telemetry.summarize_node(sample(now), node(), now - 3, now + 1)
+ collector.value = telemetry.summarize([item], now)
+ with mock.patch.object(telemetry.time, 'time', return_value=now + 59):
+ self.assertIsNone(collector.snapshot()['nodes'][0]['prefill'])
+
+ def test_device_mean_rejects_long_or_slow_sampling_spans(self):
+ now = time.time()
+ raw = sample(now)
+ raw['started_at'] = now - 19
+ raw['hbm'][0]['started_at'] = now - 18
+ raw['hbm'][0]['finished_at'] = now - 17
+ value = telemetry.summarize_node(raw, node(), now - 20, now + 1)
+ self.assertTrue(value['source_ok'])
+ self.assertIsNone(value['hbm'])
+
+ def test_old_lifecycle_cpu_timer_cannot_attach_to_new_step(self):
+ now = time.time()
+ raw = sample(now)
+ raw['source_started_at'] = now - 5
+ for row in raw['engine_rows']:
+ if row['kind'] == 'cpu':
+ row['at'] = now - 60
+ result = telemetry.summarize_node(raw, node(), now - 3, now + 1)
+ self.assertIsNotNone(result['prefill'])
+ self.assertIsNone(result['phase_timers']['prefill']['mean_ms'])
+
+ def test_newer_log_group_wins_even_if_worker_step_counter_restarts(self):
+ now = time.time()
+ raw = sample(now)
+ old = deepcopy(raw['engine_rows'][:2])
+ for row in old:
+ row['step'] = 99999
+ row['at'] -= 20
+ raw['engine_rows'].extend(old)
+ result = telemetry.summarize_node(raw, node(), now - 3, now + 1)
+ self.assertEqual(result['prefill']['step'], 10)
+
+ def test_config_rejects_command_injection_and_duplicate_devices(self):
+ config = {'interval_seconds': 30, 'nodes': [node()]}
+ telemetry.validate_config(config)
+ config['nodes'][0]['container'] = 'valid;evil'
+ with self.assertRaises(ValueError):
+ telemetry.validate_config(config)
+ config = {'interval_seconds': 30, 'nodes': [node()]}
+ config['nodes'][0]['rank_devices'] = [0, 0]
+ with self.assertRaises(ValueError):
+ telemetry.validate_config(config)
+ config = {'interval_seconds': 30, 'nodes': [node()]}
+ config['nodes'][0]['prefill_flops_per_token'] = 10 ** 400
+ with self.assertRaises(ValueError):
+ telemetry.validate_config(config)
+ config = {'interval_seconds': 30, 'nodes': [node(), node('two', 19002)]}
+ with self.assertRaises(ValueError):
+ telemetry.validate_config(config)
+
+ def test_loopback_server_rejects_untrusted_origin(self):
+ collector = telemetry.Collector({'interval_seconds': 30, 'nodes': [node()]})
+ server = telemetry.Server(0, collector)
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
+ thread.start()
+ self.addCleanup(thread.join, 5)
+ self.addCleanup(server.server_close)
+ self.addCleanup(server.shutdown)
+ connection = HTTPConnection('127.0.0.1', server.server_port, timeout=3)
+ connection.request('GET', '/snapshot', headers={'Origin': 'https://example.invalid'})
+ self.assertEqual(connection.getresponse().status, 403)
+ connection.close()
+
+ def test_dashboard_exposes_only_configured_telemetry(self):
+ server = web.Dashboard(0, 'unused.db', telemetry_port=19883)
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
+ thread.start()
+ self.addCleanup(thread.join, 5)
+ self.addCleanup(server.server_close)
+ self.addCleanup(server.shutdown)
+ with mock.patch.object(web, 'read_telemetry', return_value={'available': True, 'nodes': []}) as reader:
+ connection = HTTPConnection('127.0.0.1', server.server_port, timeout=3)
+ connection.request('GET', '/api/telemetry')
+ response = connection.getresponse()
+ self.assertEqual(response.status, 200)
+ self.assertIn(b'"available":true', response.read())
+ reader.assert_called_once_with(19883)
+ connection.close()
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_telemetry_history.py b/tests/test_telemetry_history.py
new file mode 100644
index 0000000..2adb16e
--- /dev/null
+++ b/tests/test_telemetry_history.py
@@ -0,0 +1,221 @@
+import json
+from contextlib import closing
+from http.client import HTTPConnection
+from pathlib import Path
+import sqlite3
+import tempfile
+import threading
+import time
+import unittest
+from unittest import mock
+
+from deploy_stats import telemetry, web
+from deploy_stats.telemetry_history import BUCKET_SECONDS, History, SOURCE
+from tests.test_telemetry import node as engine_config, sample as engine_sample
+
+
+def config(port=19001, target='operator@example.invalid'):
+ return {'backend_port': port, 'ssh_target': target, 'container': 'example-container',
+ 'rank_devices': [0, 1], 'source_sha256': {'engine': 'a' * 64},
+ 'image_fingerprint': 'b' * 64, 'driver_sha256': 'c' * 64}
+
+
+def node(at, port=19001, host='d' * 64, source='e' * 64, step=1):
+ return {'source_ok': True, 'backend_ready': True, 'clock_ok': True, 'backend_port': port,
+ 'host_fingerprint': host, 'source_id': source, 'clock_uncertainty_seconds': .1,
+ 'prefill': {'log_at': at, 'log_skew_seconds': .2, 'step': step,
+ 'tokens_per_second': 100, 'mfu_estimate': .5, 'valid_until': at + 40,
+ '_observation_id': f'{step:064x}'},
+ 'decode': {'log_at': at, 'log_skew_seconds': .2, 'step': step + 1,
+ 'tokens_per_second': 0, 'valid_until': at + 40, '_observation_id': f'{step+1:064x}'}}
+
+
+class TelemetryHistoryTests(unittest.TestCase):
+ def setUp(self):
+ self.tmp = tempfile.TemporaryDirectory(dir=Path.cwd() / 'local')
+ self.addCleanup(self.tmp.cleanup)
+ self.path = Path(self.tmp.name) / 'history.sqlite3'
+ self.history = History(self.path)
+ self.at = 100000 * BUCKET_SECONDS + 100
+
+ def test_samples_are_per_group_deduplicated_and_keep_true_zero(self):
+ item = node(self.at)
+ self.assertEqual(self.history.record(item, config(), self.at + 1), 2)
+ item['prefill']['log_at'] += .05 # changed clock estimate, same observed step
+ self.assertEqual(self.history.record(item, config(), self.at + 1), 0)
+ result = self.history.series(config(), item['host_fingerprint'], self.at + BUCKET_SECONDS)
+ self.assertEqual(result['method'], SOURCE)
+ self.assertEqual(len(result['points']), 144)
+ point = result['points'][-1]
+ self.assertEqual(point['prefill_tps'], 100)
+ self.assertEqual(point['prefill_samples'], 1)
+ self.assertEqual(point['decode_tps'], 0)
+ self.assertEqual(point['decode_samples'], 1)
+ self.assertIsNone(result['points'][-2]['prefill_tps'])
+
+ def test_bucket_boundary_expiry_and_unknown_source_are_not_recorded(self):
+ boundary = 100000 * BUCKET_SECONDS + BUCKET_SECONDS - .05
+ self.assertEqual(self.history.record(node(boundary), config(), boundary + 1), 0)
+ item = node(self.at)
+ item['prefill']['valid_until'] = self.at
+ item['decode'] = None
+ self.assertEqual(self.history.record(item, config(), self.at + 1), 0)
+ item = node(self.at)
+ item['source_ok'] = False
+ self.assertEqual(self.history.record(item, config(), self.at + 1), 0)
+
+ def test_history_is_not_shared_across_host_or_configuration(self):
+ item = node(self.at)
+ self.history.record(item, config(), self.at + 1)
+ for other, host in ((config(19002), item['host_fingerprint']), (config(), 'f' * 64)):
+ result = self.history.series(other, host, self.at + BUCKET_SECONDS)
+ self.assertIsNone(result['points'][-1]['prefill_tps'])
+ item2 = node(self.at, source='1' * 64)
+ self.history.record(item2, config(), self.at + 1)
+ result = self.history.series(config(), item['host_fingerprint'], self.at + BUCKET_SECONDS)
+ self.assertEqual(result['points'][-1]['prefill_samples'], 2)
+
+ def test_worker_restart_can_reuse_step_without_losing_new_observation(self):
+ first = node(self.at, step=7)
+ self.assertEqual(self.history.record(first, config(), self.at + 1), 2)
+ second = node(self.at + BUCKET_SECONDS, step=7)
+ second['prefill']['_observation_id'] = '9' * 64
+ second['decode']['_observation_id'] = 'a' * 64
+ self.assertEqual(self.history.record(second, config(), self.at + BUCKET_SECONDS + 1), 2)
+ result = self.history.series(config(), first['host_fingerprint'], self.at + 2 * BUCKET_SECONDS)
+ self.assertEqual(result['points'][-1]['prefill_samples'], 1)
+ self.assertEqual(result['points'][-2]['prefill_samples'], 1)
+
+ def test_observation_identity_uses_original_rank_logs_not_clock_estimate(self):
+ raw = engine_sample(self.at)
+ first = telemetry.summarize_node(raw, engine_config(), self.at - 3, self.at + 1)
+ same = telemetry.summarize_node(raw, engine_config(), self.at - 2.9, self.at + 1)
+ self.assertNotEqual(first['prefill']['log_at'], same['prefill']['log_at'])
+ self.assertEqual(first['prefill']['_observation_id'], same['prefill']['_observation_id'])
+ later = telemetry.summarize_node(engine_sample(self.at + 10), engine_config(),
+ self.at + 7, self.at + 11)
+ self.assertEqual(first['prefill']['step'], later['prefill']['step'])
+ self.assertNotEqual(first['prefill']['_observation_id'], later['prefill']['_observation_id'])
+
+ def test_unidentified_existing_file_is_not_modified(self):
+ other = Path(self.tmp.name) / 'not-history.sqlite3'
+ with closing(sqlite3.connect(other)) as db, db:
+ db.execute('CREATE TABLE unrelated (value TEXT)')
+ db.execute('INSERT INTO unrelated VALUES (?)', ('keep',))
+ before = other.read_bytes()
+ with self.assertRaisesRegex(ValueError, 'INVALID_TELEMETRY_HISTORY'):
+ History(other)
+ self.assertEqual(other.read_bytes(), before)
+
+ def test_reopen_keeps_previous_samples_and_output_has_no_source_identity(self):
+ item = node(self.at)
+ self.history.record(item, config(), self.at + 1)
+ result = History(self.path).series(config(), item['host_fingerprint'], self.at + BUCKET_SECONDS)
+ self.assertEqual(result['points'][-1]['prefill_tps'], 100)
+ serialized = json.dumps(result)
+ self.assertNotIn(item['host_fingerprint'], serialized)
+ self.assertNotIn(item['source_id'], serialized)
+
+ def test_collector_records_and_exposes_only_sanitized_history(self):
+ conf = engine_config()
+ first = node(self.at)
+ collector = telemetry.Collector({'interval_seconds': 30, 'nodes': [conf]},
+ sample=lambda *_: first, history=self.history)
+ self.addCleanup(collector.close)
+ with mock.patch.object(telemetry.time, 'time', return_value=self.at + 1):
+ collector.collect()
+ self.assertTrue(collector.history_worker.changed.wait(3))
+ collector.history_worker.changed.clear()
+ next_now = (self.at // BUCKET_SECONDS + 1) * BUCKET_SECONDS + 10
+ second = node(next_now - 1, step=5)
+ collector.sample = lambda *_: second
+ with mock.patch.object(telemetry.time, 'time', return_value=next_now):
+ collector.collect()
+ self.assertTrue(collector.history_worker.changed.wait(3))
+ result = telemetry.public_snapshot(collector.snapshot())
+ self.assertTrue(result['history_available'])
+ point = result['nodes'][0]['history']['points'][-1]
+ self.assertEqual(point['prefill_tps'], 100)
+ self.assertNotIn('host_fingerprint', result['nodes'][0])
+ self.assertNotIn('source_id', result['nodes'][0])
+ self.assertNotIn('_observation_id', result['nodes'][0]['prefill'])
+
+ def test_history_failure_does_not_clear_live_observations(self):
+ conf = engine_config()
+ item = node(self.at)
+ broken = mock.Mock()
+ broken.record.side_effect = sqlite3.OperationalError('synthetic')
+ collector = telemetry.Collector({'interval_seconds': 30, 'nodes': [conf]},
+ sample=lambda *_: item, history=broken)
+ self.addCleanup(collector.close)
+ with mock.patch.object(telemetry.time, 'time', return_value=self.at + 1):
+ collector.collect()
+ self.assertTrue(collector.history_worker.changed.wait(3))
+ result = collector.snapshot()
+ self.assertFalse(result['history_available'])
+ self.assertEqual(result['nodes'][0]['prefill']['tokens_per_second'], 100)
+
+ def test_history_flows_through_the_loopback_dashboard(self):
+ now = 100001 * BUCKET_SECONDS + 1
+ conf = engine_config()
+ item = node(now - 3)
+ collector = telemetry.Collector({'interval_seconds': 30, 'nodes': [conf]},
+ sample=lambda *_: item, history=self.history)
+ self.addCleanup(collector.close)
+ sidecar = telemetry.Server(0, collector)
+ dashboard = web.Dashboard(0, Path(self.tmp.name)/'unused.sqlite3', telemetry_port=sidecar.server_port)
+ threads = [threading.Thread(target=server.serve_forever, daemon=True) for server in (sidecar, dashboard)]
+ for thread in threads:
+ thread.start()
+ try:
+ with mock.patch.object(telemetry.time, 'time', return_value=now):
+ collector.collect()
+ self.assertTrue(collector.history_worker.changed.wait(3))
+ connection = HTTPConnection('127.0.0.1', dashboard.server_port, timeout=3)
+ connection.request('GET', '/api/telemetry')
+ response = connection.getresponse()
+ self.assertEqual(response.status, 200)
+ result = json.loads(response.read())
+ connection.close()
+ self.assertTrue(result['available'])
+ self.assertTrue(result['history_available'])
+ self.assertEqual(result['nodes'][0]['history']['points'][-1]['prefill_tps'], 100)
+ self.assertEqual(result['nodes'][0]['history']['points'][-1]['decode_tps'], 0)
+ self.assertNotIn('host_fingerprint', result['nodes'][0])
+ self.assertNotIn('source_id', result['nodes'][0])
+ finally:
+ for server in (dashboard, sidecar):
+ server.shutdown()
+ server.server_close()
+ for thread in threads:
+ thread.join(3)
+
+ def test_slow_history_io_does_not_block_live_sampling_or_snapshot(self):
+ gate = threading.Event()
+ started = threading.Event()
+ slow = mock.Mock()
+ def slow_record(*_):
+ started.set()
+ gate.wait(3)
+ slow.record.side_effect = slow_record
+ slow.series.return_value = None
+ collector = telemetry.Collector({'interval_seconds': 30, 'nodes': [engine_config()]},
+ sample=lambda *_: node(self.at), history=slow)
+ self.addCleanup(collector.close)
+ self.addCleanup(gate.set)
+ with mock.patch.object(telemetry.time, 'time', return_value=self.at + 1):
+ start = time.monotonic()
+ collector.collect()
+ self.assertTrue(started.wait(1))
+ result = collector.snapshot()
+ self.assertLess(time.monotonic() - start, 1.5)
+ self.assertFalse(result['history_available'])
+ self.assertEqual(result['nodes'][0]['prefill']['tokens_per_second'], 100)
+
+ def test_history_initialization_failure_does_not_raise(self):
+ missing = Path(self.tmp.name) / 'absent' / 'history.sqlite3'
+ self.assertIsNone(telemetry.open_history(missing))
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_trend_tooltip.py b/tests/test_trend_tooltip.py
new file mode 100644
index 0000000..488ab00
--- /dev/null
+++ b/tests/test_trend_tooltip.py
@@ -0,0 +1,106 @@
+import shutil
+import subprocess
+import unittest
+from pathlib import Path
+
+
+class TrendTooltipTests(unittest.TestCase):
+ def test_hover_reports_exact_bucket_value_and_preserves_gaps(self):
+ node = shutil.which('node')
+ if node is None:
+ self.skipTest('Node.js is required to validate dashboard interactions')
+ script = r'''
+const assert=require('node:assert/strict'),fs=require('node:fs'),vm=require('node:vm');
+const source=fs.readFileSync(process.argv[1],'utf8');
+function element(tag,attrs={}) {return {tag,attrs,children:[],textContent:'',
+ replaceChildren(){this.children=[];},appendChild(x){this.children.push(x);},
+ classList:{add(){},remove(){}},
+ setAttribute(k,v){this.attrs[k]=v;},removeAttribute(k){delete this.attrs[k];},
+ getBoundingClientRect(){return {left:100,top:100,width:800,height:68};}};}
+const tooltip={hidden:true,textContent:'',style:{},getBoundingClientRect:()=>({width:180,height:60})};
+const elements={'trend-tooltip':tooltip};
+for(const id of ['input','output','cache-rate','device-prefill','device-decode']){
+ elements[`chart-${id}`]=element('label');elements[`chart-${id}-svg`]=element('svg');
+}
+const ctx={$:(id)=>elements[id]||null,svgElement:element,setTextIfPresent:()=>{},
+ document:{documentElement:{clientWidth:1200,clientHeight:800}},window:{innerWidth:1200,innerHeight:800},
+ number:{format:String},integer:{format:String},count:String,speed:(v)=>`${v} tok/s`,percent:(v)=>`${v*100}%`,
+ clock:{format:(v)=>`time${v.getTime()/1000}`},dateTime:{format:(v)=>`date${v.getTime()/1000}`}};
+function extract(name){const a=source.indexOf(`function ${name}(`),b=source.indexOf('\n}\n',a);
+ assert(a>=0&&b>a,`missing ${name}`);return vm.runInNewContext('('+source.slice(a,b+2)+')',ctx);}
+ctx.attachTrendTooltip=extract('attachTrendTooltip');
+const render=extract('renderPlot'),renderDevice=extract('renderDeviceHistory');
+const points=[{start:0,end:600,input:120,known:true,complete:true},
+ {start:600,end:1200,input:43,known:true,complete:false},
+ {start:1200,end:1800,input:null,known:false,complete:false}];
+render({id:'input',field:'input',kind:'tokens',label:'输入'},points);
+let svg=elements['chart-input-svg'];
+svg.onpointermove({clientX:100,clientY:130});
+assert.equal(tooltip.hidden,false);assert.match(tooltip.textContent,/输入.*120 Token/);
+assert.match(tooltip.textContent,/date0.*date600/);
+points[0].input=121;render({id:'input',field:'input',kind:'tokens',label:'输入'},points);
+assert.equal(tooltip.hidden,false);assert.match(tooltip.textContent,/121 Token/); // Refresh while hovered.
+svg.onpointermove({clientX:500,clientY:130});assert.match(tooltip.textContent,/至少 43 Token/);
+svg.onpointermove({clientX:900,clientY:130});assert.equal(tooltip.hidden,true); // Gap is not zero.
+svg.onpointermove({clientX:100,clientY:130});svg.onpointerleave();assert.equal(tooltip.hidden,true);
+svg.onfocus();assert.equal(tooltip.hidden,false);assert.match(tooltip.textContent,/121 Token/);
+svg.onkeydown({key:'ArrowRight',preventDefault(){}});assert.match(tooltip.textContent,/43 Token/);
+points[1].input=null;points[1].known=false;
+render({id:'input',field:'input',kind:'tokens',label:'输入'},points);
+assert.equal(tooltip.hidden,true); // Focused bucket disappeared during refresh.
+render({id:'input',field:'input',kind:'tokens',label:'输入'},points); // Remains missing through another refresh.
+svg.onkeydown({key:'ArrowLeft',preventDefault(){}});assert.match(tooltip.textContent,/121 Token/);
+svg.onblur();assert.equal(tooltip.hidden,true);
+const output=[{start:0,end:600,output:456,known:true,complete:true}];
+render({id:'output',field:'output',kind:'tokens',label:'输出'},output);
+svg=elements['chart-output-svg'];svg.onpointermove({clientX:100,clientY:130});
+assert.match(tooltip.textContent,/输出.*456 Token/);
+const hit=[{start:0,end:600,known:true,complete:true,mean_request_hit_rate:.25,request_hit_sum:.5,request_hit_samples:2}];
+render({id:'cache-rate',field:'mean_request_hit_rate',kind:'hit',label:'平均请求命中率',samples:'request_hit_samples'},hit);
+svg=elements['chart-cache-rate-svg'];svg.onpointermove({clientX:100,clientY:130});
+assert.match(tooltip.textContent,/平均请求命中率.*25%.*2 个样本/);
+const device=Array.from({length:144},(_,i)=>({start:i*600,end:(i+1)*600,
+ prefill_tps:null,prefill_samples:0,prefill_groups:0,decode_tps:null,decode_samples:0,decode_groups:0}));
+device[20].decode_tps=200;device[20].decode_samples=1;device[20].decode_groups=1;
+device[20].prefill_tps=1200;device[20].prefill_samples=2;device[20].prefill_groups=2;
+renderDevice({available:true,history_available:true},{history:{method:'deepinfer-log-step-approx-v1',
+ bucket_seconds:600,aggregate:true,group_count:2,points:device}});
+svg=elements['chart-device-prefill-svg'];svg.onpointermove({clientX:100+20/143*800,clientY:130});
+assert.match(tooltip.textContent,/Prefill.*1200 tok\/s[\s\S]*2\/2 组/);
+elements['chart-input-svg'].onfocus(); // Keyboard focus may remain while another chart is hovered.
+svg=elements['chart-device-decode-svg'];svg.onpointermove({clientX:100+20/143*800,clientY:130});
+assert.match(tooltip.textContent,/Decode.*200 tok\/s[\s\S]*1\/2 组.*部分数据/);
+render({id:'input',field:'input',kind:'tokens',label:'输入'},points);
+assert.match(tooltip.textContent,/Decode.*200 tok\/s/); // Unrelated refresh cannot steal the tooltip.
+svg.onpointermove({clientX:100+21/143*800,clientY:130});assert.equal(tooltip.hidden,true);
+renderDevice({available:true,history_available:false},null);
+assert.equal(tooltip.hidden,true);assert.equal(svg.onpointermove,null);
+// A failed statistics refresh must clear the previous tooltip and handlers.
+render({id:'input',field:'input',kind:'tokens',label:'输入'},points);
+svg=elements['chart-input-svg'];svg.onpointermove({clientX:100,clientY:130});assert.equal(tooltip.hidden,false);
+ctx.$=(id)=>elements[id]||(elements[id]=element('div'));
+Object.assign(ctx,{revision:0,pendingRefresh:null,activeExpiry:null,backendExpiry:null,lastSnapshot:null,selected:'24h',
+ tables:{requests:{offset:0,size:20,sort:'finished',direction:'desc'},
+ sessions:{offset:0,size:20,sort:'last_finished',direction:'desc'}},
+ charts:[{id:'input'},{id:'output'},{id:'cache-rate'}],empty:()=>{},updatePager:()=>{},
+ AbortController,URLSearchParams,setTimeout:()=>0,clearTimeout:()=>{},fetch:async()=>{throw Error('synthetic failure');}});
+const start=source.indexOf('async function refresh('),end=source.indexOf('\n}\n',start);
+assert(start>=0&&end>start);
+const refresh=vm.runInNewContext('('+source.slice(start,end+2)+')',ctx);
+refresh().then(()=>{assert.equal(tooltip.hidden,true);assert.equal(svg.onpointermove,null);
+ assert.equal(elements['chart-input'].textContent,'暂无数据');
+ renderDevice({available:true,history_available:true},{history:{method:'deepinfer-log-step-approx-v1',
+ bucket_seconds:600,aggregate:true,group_count:2,points:device}});
+ svg=elements['chart-device-decode-svg'];svg.onpointermove({clientX:100+20/143*800,clientY:130});
+ return refresh();
+}).then(()=>{assert.equal(tooltip.hidden,false);assert.match(tooltip.textContent,/Decode.*200 tok\/s/);
+}).catch((error)=>{console.error(error);process.exitCode=1;});
+'''
+ source = Path(__file__).resolve().parents[1] / 'deploy_stats/static/app.js'
+ result = subprocess.run([node, '-e', script, str(source)], capture_output=True, text=True,
+ encoding='utf-8', timeout=15)
+ self.assertEqual(result.returncode, 0, result.stderr)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_trends.py b/tests/test_trends.py
new file mode 100644
index 0000000..bb8d0f3
--- /dev/null
+++ b/tests/test_trends.py
@@ -0,0 +1,66 @@
+from datetime import datetime, timedelta, timezone
+from pathlib import Path
+import tempfile
+import time
+import unittest
+
+from deploy_stats.report import report
+from deploy_stats.store import _open
+
+
+class TrendTests(unittest.TestCase):
+ def test_24h_rates_are_computed_from_eligible_requests_and_include_partial_bucket(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ path = Path(tmp) / 'series.sqlite3'
+ now = datetime(2026, 1, 3, 12, 2, 3, tzinfo=timezone(timedelta(hours=8))).timestamp()
+ db = _open(path)
+ db.execute('INSERT INTO runs VALUES (?,?,?,?,?)', ('fixture', now - 2400, now, now, 0))
+ statement = '''INSERT INTO requests(run_id,finished,client_ip,elapsed,http_status,state,
+ input_tokens,output_tokens,cached_tokens,session_id,first_visible) VALUES(?,?,?,?,?,?,?,?,?,?,?)'''
+ for elapsed, prompt, output, cached, first in ((3, 100, 11, 90, 1),
+ (5, 10, 3, 0, 2),
+ (1, 0, 1, 0, 0),
+ (2, 20, 2, 20, None)):
+ db.execute(statement, ('fixture', now - 900, '192.0.2.4', elapsed, 200, 'known',
+ prompt, output, cached, None, first))
+ db.execute(statement, ('fixture', now - 900, '192.0.2.4', 1, 500, 'failed',
+ None, None, None, None, None))
+ db.execute(statement, ('fixture', now - 100, '192.0.2.4', 3, 200, 'known', 20, 5, 0, None, 1))
+ db.commit()
+ db.close()
+ points = report(path, 'today', now=now)['series_24h']
+ bucket_start = int((now - 900) // 600) * 600
+ bucket = next(point for point in points if point['start'] == bucket_start)
+ self.assertAlmostEqual(bucket['mean_request_hit_rate'], 1.9 / 3)
+ self.assertEqual(bucket['request_hit_samples'], 3)
+ self.assertAlmostEqual(bucket['prefill_estimate_tps'], 20 / 3)
+ self.assertEqual((bucket['prefill_samples'], bucket['prefill_tokens'], bucket['prefill_seconds']), (2, 20, 3))
+ self.assertAlmostEqual(bucket['decode_estimate_tps'], 12 / 5)
+ self.assertEqual((bucket['decode_samples'], bucket['decode_tokens'], bucket['decode_seconds']), (2, 12, 5))
+ empty = next(point for point in points if point['start'] == bucket_start - 600)
+ self.assertTrue(empty['complete'])
+ self.assertIsNone(empty['mean_request_hit_rate'])
+ self.assertIsNone(empty['prefill_estimate_tps'])
+ self.assertIsNone(empty['decode_estimate_tps'])
+ partial = next(point for point in points if point['start'] <= now - 100 < point['end'])
+ self.assertLess(partial['end'] - partial['start'], 600)
+ self.assertAlmostEqual(sum(point['request_hit_sum'] for point in points) /
+ sum(point['request_hit_samples'] for point in points), 1.9 / 4)
+
+ def test_historical_overload_detail_is_not_treated_as_zero(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ path = Path(tmp) / 'overload.sqlite3'
+ now = time.time()
+ db = _open(path)
+ db.execute('INSERT INTO runs VALUES (?,?,?,?,?)', ('old', now - 20, now - 10, now - 10, 1))
+ db.execute('INSERT INTO runs VALUES (?,?,?,?,?)', ('new', now - 10, now, now, 3))
+ db.execute('INSERT INTO stats_meta VALUES (?,?)', ('overload:new', '3'))
+ db.commit()
+ db.close()
+ totals = report(path, 'total', now=now + .01)['totals']
+ self.assertEqual((totals['dropped'], totals['overload_rejected'],
+ totals['overload_tracked_runs'], totals['overload_untracked_runs']), (4, 3, 1, 1))
+
+
+if __name__ == '__main__':
+ unittest.main()