2026-08-25 02:06:23 +00:00

115 lines
4.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Shared helpers for the three fingerprint benchmarks (LLMmap / LLM Verify /
llm-fingerprint-detector).
These benchmarks do not go through EvalScope's dataset pipeline. Each runner
script probes the target OpenAI-compatible endpoint with its own tool logic and
writes a report JSON shaped like EvalScope reports:
output/<folder>/<benchmark>/seed_<seed>/reports/<benchmark>.json
with at least ``score`` (float 0~1) and ``num`` so that
``bash/collect_results.py`` can aggregate them like any other benchmark.
"""
import argparse
import json
import urllib.error
import urllib.request
# 各工具报告文件名与其 benchmark 名一致
BENCHMARK_LLMMAP = 'llmmap'
BENCHMARK_LLM_VERIFY = 'llm_verify'
BENCHMARK_DETECTOR = 'llm_fingerprint_detector'
ALL_FINGERPRINT_BENCHMARKS = [BENCHMARK_LLMMAP, BENCHMARK_LLM_VERIFY, BENCHMARK_DETECTOR]
# 默认对被测端点关闭 thinking指纹探测需要稳定的可见回答
# 思考链会烧掉 max_tokens 且改变输出分布。sglang/vLLM 均支持该字段。
DEFAULT_EXTRA_BODY = {'chat_template_kwargs': {'thinking': False}}
def add_common_args(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
"""CLI arguments shared by all three fingerprint runners."""
parser.add_argument('--api-url', required=True,
help='Target OpenAI-compatible API base URL, e.g. http://localhost:30000/v1')
parser.add_argument('--model', required=True, help='Served model name to probe')
parser.add_argument('--report-path', required=True,
help='Where to write the EvalScope-style report JSON')
parser.add_argument('--timeout', type=int, default=120,
help='Per-request timeout in seconds (default: %(default)s)')
parser.add_argument('--thinking', action='store_true', default=False,
help='Do NOT disable thinking on the target (default: disabled)')
return parser
def chat_completion(api_url: str, model: str, user_prompt: str,
system_prompt: str = '', temperature: float = 1.0,
max_tokens: int = 512, timeout: int = 120,
extra_body: dict = None):
"""Minimal OpenAI chat-completions call (stdlib only).
Returns:
(content, error) — exactly one of them is None.
"""
messages = []
if system_prompt:
messages.append({'role': 'system', 'content': system_prompt})
messages.append({'role': 'user', 'content': user_prompt})
payload = {
'model': model,
'messages': messages,
'temperature': temperature,
'max_tokens': max_tokens,
'stream': False,
}
payload.update(extra_body or {})
req = urllib.request.Request(
f"{api_url.rstrip('/')}/chat/completions",
data=json.dumps(payload).encode('utf-8'),
headers={'Content-Type': 'application/json'},
method='POST',
)
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
data = json.loads(resp.read().decode('utf-8'))
except urllib.error.HTTPError as e:
detail = ''
try:
detail = e.read().decode('utf-8')[:200]
except Exception:
pass
return None, f'HTTP {e.code}: {detail}'
except Exception as e:
return None, f'request failed: {e}'
choices = data.get('choices') or []
if not choices:
return None, 'empty choices in response'
message = choices[0].get('message') or {}
content = message.get('content')
# 部分推理模型把可见内容放在 reasoning_content仅当 content 为空时兜底。
if not content:
content = message.get('reasoning_content') or ''
return str(content), None
def write_report(report_path: str, benchmark: str, score: float, num: int,
**details) -> None:
"""Write an EvalScope-style report JSON consumable by collect_results.py."""
from pathlib import Path
report_path = Path(report_path)
report_path.parent.mkdir(parents=True, exist_ok=True)
payload = {
'benchmark': benchmark,
'score': float(score),
'num': int(num),
}
payload.update(details)
report_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2),
encoding='utf-8')
print(f'[fingerprint] report written: {report_path}')