Files

172 lines
5.8 KiB
Python

#!/usr/bin/env python3
"""Measure DeerFlow model-factory latency without exposing prompts or secrets."""
from __future__ import annotations
import argparse
import json
import sys
import time
from pathlib import Path
from typing import Any
from langchain_core.messages import HumanMessage
from deerflow.config import get_app_config
from deerflow.models import create_chat_model
def _args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Diagnose DeerFlow LLM first-chunk and completion latency")
parser.add_argument("--model", required=True, help="DeerFlow config model name")
parser.add_argument("--prompt", default="请先思考,然后只回答:17乘以23等于多少?")
parser.add_argument("--prompt-file", type=Path, help="UTF-8 prompt file; content is never printed")
parser.add_argument("--repeat", type=int, default=1, help="Repeat the selected prompt N times")
parser.add_argument("--max-tokens", type=int, default=256)
parser.add_argument("--thinking", action=argparse.BooleanOptionalAction, default=True)
return parser.parse_args()
def _reasoning_text(chunk: Any) -> str:
kwargs = getattr(chunk, "additional_kwargs", None) or {}
value = kwargs.get("reasoning_content") or kwargs.get("reasoning")
return value if isinstance(value, str) else ""
def _content_text(chunk: Any) -> str:
content = getattr(chunk, "content", "")
if isinstance(content, str):
return content
if isinstance(content, list):
return "".join(part.get("text", "") for part in content if isinstance(part, dict))
return str(content or "")
def main() -> int:
args = _args()
prompt = args.prompt_file.read_text(encoding="utf-8") if args.prompt_file else args.prompt
prompt *= max(args.repeat, 1)
config = get_app_config()
model_config = config.get_model_config(args.model)
if model_config is None:
print(f"ERROR model_not_found={args.model}", file=sys.stderr)
return 2
print(
"CONFIG "
+ json.dumps(
{
"name": model_config.name,
"use": model_config.use,
"base_url": str(model_config.base_url or ""),
"request_timeout": model_config.request_timeout,
"max_retries": model_config.max_retries,
"supports_thinking": model_config.supports_thinking,
"when_thinking_enabled": model_config.when_thinking_enabled,
"prompt_chars": len(prompt),
},
ensure_ascii=False,
),
flush=True,
)
build_started = time.monotonic()
model = create_chat_model(args.model, thinking_enabled=args.thinking)
build_ms = int((time.monotonic() - build_started) * 1000)
print(
"MODEL "
+ json.dumps(
{
"runtime_class": f"{type(model).__module__}.{type(model).__name__}",
"build_ms": build_ms,
}
),
flush=True,
)
messages = [HumanMessage(content=prompt)]
if hasattr(model, "_get_request_payload"):
payload = model._get_request_payload(messages, max_tokens=args.max_tokens)
safe_payload = dict(payload)
safe_payload["messages"] = [
{
"role": item.get("role"),
"content_chars": len(str(item.get("content") or "")),
"has_tools": bool(item.get("tool_calls")),
}
for item in payload.get("messages", [])
]
print("PAYLOAD " + json.dumps(safe_payload, ensure_ascii=False, default=str), flush=True)
started = time.monotonic()
first_chunk_ms: int | None = None
first_reasoning_ms: int | None = None
first_content_ms: int | None = None
chunks = 0
reasoning_chars = 0
content_chars = 0
usage: dict[str, Any] = {}
try:
for chunk in model.stream(messages, max_tokens=args.max_tokens):
chunks += 1
now_ms = int((time.monotonic() - started) * 1000)
if first_chunk_ms is None:
first_chunk_ms = now_ms
print(f"FIRST_CHUNK elapsed_ms={now_ms}", flush=True)
reasoning = _reasoning_text(chunk)
content = _content_text(chunk)
if reasoning:
reasoning_chars += len(reasoning)
if first_reasoning_ms is None:
first_reasoning_ms = now_ms
print(f"FIRST_REASONING elapsed_ms={now_ms}", flush=True)
if content:
content_chars += len(content)
if first_content_ms is None:
first_content_ms = now_ms
print(f"FIRST_CONTENT elapsed_ms={now_ms}", flush=True)
chunk_usage = getattr(chunk, "usage_metadata", None)
if chunk_usage:
usage = dict(chunk_usage)
except Exception as exc:
total_ms = int((time.monotonic() - started) * 1000)
print(
"FAILED "
+ json.dumps(
{"total_ms": total_ms, "error_type": type(exc).__name__, "error": str(exc)},
ensure_ascii=False,
),
file=sys.stderr,
flush=True,
)
return 1
total_ms = int((time.monotonic() - started) * 1000)
print(
"RESULT "
+ json.dumps(
{
"status": "success",
"first_chunk_ms": first_chunk_ms,
"first_reasoning_ms": first_reasoning_ms,
"first_content_ms": first_content_ms,
"total_ms": total_ms,
"chunks": chunks,
"reasoning_chars": reasoning_chars,
"content_chars": content_chars,
"usage": usage,
},
ensure_ascii=False,
),
flush=True,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())