# LLM 速度测试台 · API 文档 > 系统所有页面功能均通过 REST API 提供,前端(网页)只是这些 API 的一个可视化客户端。 > 任何页面可见/可操作的数据都可以通过下面的接口调用或访问。 - **服务地址:** `http://:16097` - **数据格式:** 请求/响应均为 `application/json`(导出类接口除外) - **鉴权:** 内部工具,当前无鉴权;如需对外暴露,建议在网关/Nginx 层加访问控制 - **测试启动为异步**:`POST /api/tests` 返回后,任务在后台线程执行,用 `GET /api/tests/` 或日志轮询接口跟踪进度 --- ## 目录 1. [健康检查](#1-健康检查) 2. [提供商配置](#2-提供商配置-configs) 3. [连接测试](#3-连接测试) 4. [速度测试](#4-速度测试-tests) 5. [测试详情与日志](#5-测试详情与日志) 6. [折线图(data-chart-tool)](#6-折线图data-chart-tool) 7. [导出(Excel / JSON)](#7-导出excel--json) 8. [数据模型](#8-数据模型) 9. [curl 使用示例](#9-curl-使用示例) --- ## 1. 健康检查 ### `GET /api/health` 返回服务状态与正在运行的测试。 **响应:** ```json { "ok": true, "port": 16097, "running_tests": [] } ``` --- ## 2. 提供商配置(Configs) ### `GET /api/configs` 列出所有已保存配置(不返回 API Key 明文,仅 `has_key` 标记)。 **响应:** ```json [ { "id": 2, "name": "epyc-test", "provider": "openai", "base_url": "http://121.40.164.32:18003/v1", "model": "unsloth/Qwen3.8-27B-Q4_K_M", "temperature": 0.7, "has_key": 1 } ] ``` ### `POST /api/configs` 新增配置。请求体字段:`name`(必填), `provider`, `base_url`, `api_key`, `model`, `temperature`。 **请求:** ```json { "name": "DeepSeek", "provider": "openai", "base_url": "https://api.deepseek.com/v1", "api_key": "sk-xxx", "model": "deepseek-chat", "temperature": 0.7 } ``` **响应:** `{ "ok": true, "id": 3 }` ### `GET /api/configs/` 获取单个配置(**含 API Key 明文**,用于前端回填;注意保管)。 ### `PUT /api/configs/` 更新配置,**局部更新**(只改请求里出现的字段)。 **请求:** `{ "model": "deepseek-v4-flash" }` → 响应 `{ "ok": true, "id": 3 }` ### `DELETE /api/configs/` 删除配置。响应 `{ "ok": true }` --- ## 3. 连接测试 ### `POST /api/configs/test` 验证 API Key / Base URL / 模型名连通性。只要流式请求成功返回(HTTP 200 + 收到响应流)即视为连通;支持推理型模型(Qwen3/DeepSeek 思维链)。 **请求:** 同配置对象(`provider`, `base_url`, `api_key`, `model`, `temperature`) **成功响应:** ```json { "ok": true, "total_ms": 1308.2, "note": "", "metrics": { "prompt_tokens": 63, "output_tokens": 32, "output_chars": 110, "ttft_ms": 715.9, "prefill_speed": 86.7, "decode_speed": 52.7, "total_ms": 1308.2 } } ``` > `note` 非空表示连接正常但未返回正文(可能为只输出思维链的模型)。 **失败响应:** `{ "ok": false, "error": "HTTP 401: ..." }` --- ## 4. 速度测试(Tests) ### `POST /api/tests` 启动一次速度测试(异步,立即返回测试 id)。 **请求体:** ```json { "config": { "provider": "openai", "name": "Qwen3 对比", "base_url": "http://121.40.164.32:18003/v1", "api_key": "sk-xxx", "model": "unsloth/Qwen3.8-27B-Q4_K_M", "temperature": 0.7 }, "gen": { "name": "Qwen3 不同上下文长度速度对比", "context_lengths": [512, 2048, 4096, 8192, 16384, 32768, 65536, 131072], "max_tokens": 128, "samples": 2, "warmup": true, "avoid_cache": true } } ``` **gen 字段说明:** | 字段 | 类型 | 默认 | 说明 | |------|------|------|------| | `name` | string | `""` | 测试名称/主题(会存入测试记录并展示在历史与详情) | | `context_lengths` | number[] | `[512,2048,4096,8192,16384,32768,65536,131072]` | 要测试的上下文长度列表,每个长度独立校准+预热+采样 | | `max_tokens` | number | `128` | 解码输出 token 长度 | | `samples` | number | `2` | 每个(长度×并发)组合的采样次数 | | `concurrency_levels` | number[] | `[1]` | 并发数列表(默认单流)。>1 时每采样同时发起 N 个并行流,聚合为整批吞吐指标;多档自动并排对比 | | `warmup` | bool | `true` | 测试前空转预热(不计速度,按并发数预热) | | `avoid_cache` | bool | `true` | 随机前缀避免缓存命中(每个并发流独立前缀) | **响应:** `{ "ok": true, "id": 9 }` ### `GET /api/tests?limit=` 测试历史列表(按 id 倒序)。`limit` 默认 100,最大 1000。 **响应:** ```json [ { "id": 9, "created_at": "2026-08-23 18:52:00", "status": "done", "provider": "openai", "model": "unsloth/Qwen3.8-27B-Q4_K_M", "name": "Qwen3 不同上下文长度速度对比", "error": "", "summary": { "samples_ok": 2, "samples_total": 2, "avg_ttft_ms": 1808.7, ... } } ] ``` ### `POST /api/tests//cancel` 停止正在运行的测试。响应 `{ "ok": true, "msg": "正在停止..." }` ### `DELETE /api/tests/` 删除测试及其全部采样与日志。响应 `{ "ok": true }` --- ## 5. 测试详情与日志 ### `GET /api/tests/` 完整测试详情:基本信息 + 配置(API Key 已打码)+ 生成参数 + 汇总 + 每次采样 + 完整日志。 **响应结构:** ```json { "id": 9, "created_at": "...", "status": "done", "provider": "openai", "model": "...", "name": "...", "error": "", "config": { "base_url": "...", "api_key": "sk-x****", ... }, "gen": { "name": "...", "context_lengths": [512, 2048], "max_tokens": 128, "samples": 1, "concurrency_levels": [1, 2, 4], "warmup": true, "avoid_cache": true }, "summary": { "samples_total": 6, "samples_ok": 6, "concurrency_levels": [1, 2, 4], "calibration_chars_per_token": 1.82, "avg_ttft_ms": 1808.7, "min_ttft_ms": 1122.8, "max_ttft_ms": 2494.6, "avg_prefill_speed": 694.2, "min_prefill_speed": 515.7, "max_prefill_speed": 872.7, "avg_decode_speed": 54.7, "min_decode_speed": 54.4, "max_decode_speed": 55.0, "avg_prompt_tokens": 1378.0, "avg_output_tokens": 128.0, "avg_total_ms": 4148.4, "min_total_ms": 3449.9, "max_total_ms": 4846.9, "by_length": { "512": { "samples_total": 3, "samples_ok": 3, "avg_ttft_ms": 1122.8, "avg_prefill_speed": 515.7, "avg_decode_speed": 55.0, "avg_prompt_tokens": 579, "avg_output_tokens": 128, "avg_total_ms": 3449.9 }, "2048": { "samples_total": 3, "samples_ok": 3, "avg_ttft_ms": 2494.6, "avg_prefill_speed": 872.7, "avg_decode_speed": 54.4, "avg_prompt_tokens": 2177, "avg_output_tokens": 128, "avg_total_ms": 4846.9 } }, "by_concurrency": { "1": { "samples_total": 2, "samples_ok": 2, "avg_ttft_ms": 1122.8, "avg_prefill_speed": 515.7, "avg_decode_speed": 55.0, "avg_stream_decode": 55.0, "avg_prompt_tokens": 579, "avg_output_tokens": 128, "avg_total_ms": 3449.9 }, "2": { "samples_total": 2, "samples_ok": 2, "avg_ttft_ms": 2494.6, "avg_prefill_speed": 872.7, "avg_decode_speed": 108.0, "avg_stream_decode": 54.0, "avg_prompt_tokens": 1158, "avg_output_tokens": 256, "avg_total_ms": 4846.9 } }, "by_length_concurrency": { "512": { "1": {...}, "2": {...} }, "2048": {...} } }, "runs": [ { "run_index": 1, "context_length": 512, "metrics": { "prompt_tokens": 579, "output_tokens": 128, "cached_tokens": 0, "ttft_ms": 1122.8, "prefill_speed": 515.7, "decode_speed": 55.0, "total_ms": 3449.9 }, "error": "" } ], "logs": [ { "id": 1, "level": "INFO", "msg": "═══ 开始速度测试 ═══", "rel": 0.0, "ts": "..." } ] } ``` ### `GET /api/tests//logs?after=` 增量日志(前端轮询用)。`after` 为上次取到的最大日志 id,返回其后新增日志 + 最新状态/汇总/最后采样。 **响应:** ```json { "status": "running", "error": "", "summary": {}, "last_run": { "...": "..." }, "logs": [ { "id": 68, "level": "METRIC", "msg": "...", "rel": 3.21 } ], "after": 73 } ``` --- ## 6. 折线图(data-chart-tool) > 调用 [data-chart-tool](http://192.168.2.8:12007/hz4th_coder/data-chart-tool.git) 的 `/api/chart` 接口生成**双Y轴折线图**:左轴=预填充速度(虚线)、右轴=解码速度(实线)、X 轴=上下文长度。本系统在服务端代理转发,前端只需请求本服务的两个接口。 ### `GET /api/tests//chart` 生成折线图 PNG 图片(内部调用 data-chart-tool `/api/chart`,地址可在 `config.py` 的 `CHART_API_BASE` 修改)。 **成功响应:** `Content-Type: image/png`(可直接用于 `` / 下载) **失败响应:** ```json { "ok": false, "error": "无成功采样数据,无法画图" } // 400 { "ok": false, "error": "测试不存在" } // 404 { "ok": false, "error": "图表服务不可用: ..." } // 502 ``` ### `GET /api/tests//chart-data` 返回画图所用的 CSV 数据与图表请求配置,方便手动快速复制。 **响应:** ```json { "ok": true, "csv": "上下文长度, 预填充速度(tok/s), 解码速度(tok/s)\n4096, 126.90, 22.80\n8192, 132.20, 22.40", "rows": [[4096, 126.9, 22.8], [8192, 132.2, 22.4]], "payload": { "data": "...", "chartType": "line", "title": "...", "dualYAxis": true, "leftAxisName": "预填充速度(tok/s)", "rightAxisName": "解码速度(tok/s)", "seriesTypes": ["line", "line"], "seriesAxis": [0, 1], "seriesStyles": ["dashed", "solid"], "width": 1000, "height": 560, "pixelRatio": 2 } } ``` > `csv` 即画图数据(第一列=上下文长度,第二列=预填充速度,第三列=解码速度),前端「复制画图数据」按钮复制的就是它。 ### `GET /api/tests//concurrency-chart` 并发对比折线图 PNG(X 轴=**并发数**,左轴=预填充速度虚线、右轴=解码速度实线),用于直观展示吞吐随并发的变化。仅当本次测试包含**多个并发档**时才有数据。 **失败响应:** `{ "ok": false, "error": "无并发分组采样数据(本次测试可能只测了单流),无法画图" }`(400) ### `GET /api/tests//concurrency-chart-data` 并发对比画图数据(CSV + 图表请求配置)。 ```json { "ok": true, "csv": "并发数, 预填充速度(tok/s), 解码速度(tok/s)\n1, 767.30, 43.40\n2, 1388.20, 70.90\n4, 2085.00, 78.70", "rows": [[1, 767.3, 43.4], [2, 1388.2, 70.9], [4, 2085.0, 78.7]], "payload": { "data": "...", "chartType": "line", "title": "...", "dualYAxis": true, ... } } ``` ### `POST /api/chart` 通用图表代理:把任意 data-chart-tool `/api/chart` 请求体转发过去并返回 PNG(多测试对比面板用)。请求体即 data-chart-tool 的参数(`data`/`chartType`/`seriesTypes`/`seriesStyles`/`dualYAxis`…)。 **成功响应:** `Content-Type: image/png` ### `POST /api/compare` 把多个测试结果放在一起对比。 **请求:** `{ "ids": [9, 10, 11] }`(最多 20 个) **响应:** ```json { "ok": true, "rows": [ { "id": 9, "label": "#9 xxx", "created_at": "...", "name": "...", "model": "...", "concurrency_levels": [1, 2, 4], "by_concurrency": {...}, "samples_ok": 6, "samples_total": 6, "avg_ttft_ms": 1122.8, "avg_prefill_speed": 515.7, "avg_decode_speed": 55.0, "avg_stream_decode": 55.0, "avg_output_tokens": 128, "avg_total_ms": 3449.9 } ], "bar_csv": "测试, 预填充速度(tok/s), 解码速度(tok/s)\n#9 xxx, 515.70, 55.00", "bar_payload": { "data": "...", "chartType": "bar", "seriesTypes": ["bar", "bar"], "seriesStyles": ["hollow", "solid"], ... }, "line": { // 仅当所选测试存在 >=2 个共同并发档时返回,否则为 null "csv": "并发数, #9 xxx, #10 yyy\n1, 55.00, 52.10\n2, 108.00, 99.30\n4, 190.20, 175.60", "payload": { "data": "...", "chartType": "line", ... }, "levels": [1, 2, 4] } } ``` --- ## 7. 导出(Excel / JSON) ### `GET /api/tests//export.xlsx` 导出 Excel 报告(**3 个 Sheet**:汇总 / 采样明细 / 日志),`Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`。 - **汇总**:测试信息(含并发数列表)+ 整体统计指标(平均/最大/最小)+ 按上下文长度分组 + **按并发数分组**(整批吞吐,含单流均解码) - **采样明细**:每次采样的上下文长度、**并发**、流成功/总数与全部指标 - **日志**:完整测试日志 ### `GET /api/tests//export.json` 导出完整测试数据为 JSON(与 `GET /api/tests/` 一致,API Key 打码),`Content-Type: application/json`。 --- ## 8. 数据模型 | 表 | 说明 | 关键字段 | |----|------|----------| | `configs` | 保存的接口配置 | id, name, provider, base_url, api_key, model, temperature | | `tests` | 测试记录 | id, status(running/done/error/canceled), provider, model, **name**, config_json, gen_cfg_json, summary_json, error | | `test_runs` | 每次采样 | id, test_id, run_index, **context_length**, metrics_json, error(metrics 含 `concurrency`/`streams_total`/`streams_ok`/`avg_stream_decode`/`streams`(每流明细)) | | `logs` | 测试日志 | id, test_id, level, msg, rel | **summary 整体指标字段:** `avg_/min_/max_` 前缀 × `ttft_ms` / `prefill_speed` / `decode_speed` / `total_ms`,以及 `avg_prompt_tokens` / `avg_output_tokens` / `avg_cached_tokens` / `best_ttft_ms`(= min_ttft_ms)。 **分组字段:** `by_length`(按上下文长度)、`by_concurrency`(按并发数,含 `avg_stream_decode` 单流均解码)、`by_length_concurrency`(长度×并发全网格)、`concurrency_levels`(本次测试的并发档列表)。 --- ## 9. curl 使用示例 ```bash BASE=http://:16097 # 健康检查 curl $BASE/api/health # 新增配置 curl -X POST $BASE/api/configs -H 'Content-Type: application/json' \ -d '{"name":"Qwen3","provider":"openai","base_url":"http://121.40.164.32:18003/v1","api_key":"sk-xxx","model":"unsloth/Qwen3.8-27B-Q4_K_M","temperature":0.7}' # 测试连接 curl -X POST $BASE/api/configs/test -H 'Content-Type: application/json' \ -d '{"provider":"openai","base_url":"http://121.40.164.32:18003/v1","api_key":"sk-xxx","model":"unsloth/Qwen3.8-27B-Q4_K_M"}' # 启动速度测试(异步,含多并发档) curl -X POST $BASE/api/tests -H 'Content-Type: application/json' -d '{ "config": {"provider":"openai","base_url":"http://121.40.164.32:18003/v1","api_key":"sk-xxx","model":"unsloth/Qwen3.8-27B-Q4_K_M"}, "gen": {"name":"并发对比","context_lengths":[2048],"max_tokens":128,"samples":2,"concurrency_levels":[1,2,4],"warmup":true,"avoid_cache":true} }' # 查询测试列表 / 详情 curl "$BASE/api/tests?limit=10" curl $BASE/api/tests/9 # 画图数据(CSV) curl $BASE/api/tests/9/chart-data # 折线图 PNG(预填充左轴虚线 / 解码右轴实线,X=上下文长度) curl -o chart.png $BASE/api/tests/9/chart # 并发对比折线图(X=并发数) curl -o cc.png $BASE/api/tests/9/concurrency-chart # 多测试对比 curl -X POST $BASE/api/compare -H 'Content-Type: application/json' -d '{"ids":[9,10,11]}' # 导出 curl -OJ $BASE/api/tests/9/export.xlsx curl $BASE/api/tests/9/export.json ```