80 lines
3.7 KiB
Python
80 lines
3.7 KiB
Python
"""Summarize model request latency and context size without reading prompt bodies."""
|
|
import argparse
|
|
from collections import Counter
|
|
import json
|
|
import math
|
|
from pathlib import Path
|
|
import statistics
|
|
|
|
|
|
def summarize(path: Path) -> dict:
|
|
requests = {}
|
|
for line in path.read_text(encoding="utf-8").splitlines():
|
|
try:
|
|
event = json.loads(line)
|
|
except ValueError:
|
|
continue
|
|
number = event.get("request")
|
|
if not isinstance(number, int):
|
|
continue
|
|
row = requests.setdefault(number, {"request": number})
|
|
kind = event.get("event")
|
|
if kind == "request":
|
|
row.update(start=event["seconds"], requestBytes=event["bytes"])
|
|
elif kind == "context":
|
|
row.update({key: event.get(key) for key in ("inputItems", "tools", "toolOutputBytes")})
|
|
elif kind == "usage":
|
|
row["inputTokens"] = event["inputTokens"]
|
|
elif kind == "retry":
|
|
row["retries"] = row.get("retries", 0) + 1
|
|
elif kind in {"error", "http_error", "cancelled", "context_rejected"}:
|
|
row["outcome"] = event.get("errorType") or kind
|
|
elif kind == "end":
|
|
row["outcome"] = event.get("terminal") or "incomplete_stream"
|
|
if "start" in row:
|
|
elapsed = round(event["seconds"] - row["start"], 3)
|
|
if kind == "headers":
|
|
row["headerSeconds"] = elapsed
|
|
elif kind == "first_body":
|
|
row["firstBodySeconds"] = elapsed
|
|
elif kind in {"end", "error", "http_error", "cancelled"}:
|
|
row["totalSeconds"] = elapsed
|
|
rows = [{key: value for key, value in row.items() if key != "start"} for row in requests.values()]
|
|
complete = [row for row in rows if row.get("outcome") == "response.completed"]
|
|
def distribution(key):
|
|
values = sorted(row[key] for row in complete if isinstance(row.get(key), (int, float)))
|
|
if not values:
|
|
return {"measured": 0}
|
|
return {"measured": len(values), "min": values[0], "median": statistics.median(values),
|
|
"p95": values[math.ceil(len(values) * 0.95) - 1], "max": values[-1]}
|
|
overlapping = []
|
|
ordered = sorted((row for row in requests.values() if "start" in row), key=lambda row: row["start"])
|
|
for index, row in enumerate(ordered):
|
|
end = row["start"] + row.get("totalSeconds", 0)
|
|
for later in ordered[index + 1:]:
|
|
if later["start"] >= end:
|
|
break
|
|
overlapping.append([row["request"], later["request"]])
|
|
return {"schema": "freecad-web-model-transport-analysis/v1", "source": str(path), "requests": len(rows),
|
|
"outcomes": dict(Counter(row.get("outcome", "pending") for row in rows)),
|
|
"successfulRequests": len(complete), "successfulLatency": {key: distribution(key) for key in
|
|
("headerSeconds", "firstBodySeconds", "totalSeconds", "requestBytes", "inputTokens")},
|
|
"overlappingRequestPairs": overlapping, "rows": rows,
|
|
"scope": "Log observations only. Bytes are not tokens; completed model responses are not completed business features. Overlap and BrokenPipe alone do not establish the cancellation cause."}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("log", type=Path)
|
|
parser.add_argument("--output", type=Path)
|
|
args = parser.parse_args()
|
|
report = summarize(args.log)
|
|
if args.output:
|
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
args.output.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
print(json.dumps({key: value for key, value in report.items() if key != "rows"}, ensure_ascii=False))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|