Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccfd7eb689 | ||
|
|
31e5bbb873 |
@@ -3,6 +3,7 @@
|
|||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@@ -112,13 +113,17 @@ def list_models_api():
|
|||||||
|
|
||||||
@app.route("/api/configs/test", methods=["POST"])
|
@app.route("/api/configs/test", methods=["POST"])
|
||||||
def test_config():
|
def test_config():
|
||||||
cfg = _fill_defaults(request.get_json(force=True) or {})
|
body = request.get_json(force=True) or {}
|
||||||
|
cfg = _fill_defaults(body)
|
||||||
if not cfg.get("api_key"):
|
if not cfg.get("api_key"):
|
||||||
return jsonify({"ok": False, "error": "请填写 API Key"}), 400
|
return jsonify({"ok": False, "error": "请填写 API Key"}), 400
|
||||||
try:
|
try:
|
||||||
# 连接测试:只要流式请求成功返回(哪怕正文为空/只有思维链)都算连通
|
# 连接测试:只要流式请求成功返回(哪怕正文为空/只有思维链)都算连通
|
||||||
|
# 慢接口长预处理可能长时间等不到首字,这里直接用请求里带的超时(默认 30 分钟),避免误判接口不通
|
||||||
m = call_stream(cfg, "你好,请简要回答:1+1=?",
|
m = call_stream(cfg, "你好,请简要回答:1+1=?",
|
||||||
{"max_tokens": 32, "avoid_cache": False})
|
{"max_tokens": 32, "avoid_cache": False,
|
||||||
|
"read_timeout": body.get("read_timeout"),
|
||||||
|
"connect_timeout": body.get("connect_timeout")})
|
||||||
note = ""
|
note = ""
|
||||||
if not (m.get("output_tokens") or m.get("output_chars")):
|
if not (m.get("output_tokens") or m.get("output_chars")):
|
||||||
note = "(连接正常,但本次未返回正文内容,可能为推理型模型)"
|
note = "(连接正常,但本次未返回正文内容,可能为推理型模型)"
|
||||||
@@ -188,7 +193,20 @@ def list_tests():
|
|||||||
|
|
||||||
@app.route("/api/tests", methods=["DELETE"])
|
@app.route("/api/tests", methods=["DELETE"])
|
||||||
def clear_tests():
|
def clear_tests():
|
||||||
"""一键清空全部测试历史(含采样指标与日志)"""
|
"""一键清空全部测试历史(含采样指标与日志)。
|
||||||
|
先停止所有运行中的测试线程,避免清空后它们继续写入孤儿数据;
|
||||||
|
再由 db.clear_tests 做物理删除(重置自增 + VACUUM 瘦身),真正清空、不可恢复。"""
|
||||||
|
# 1) 停止所有运行中的测试,并等待其退出
|
||||||
|
alive = [r for r in RUNNERS.values() if r.is_alive()]
|
||||||
|
for r in alive:
|
||||||
|
r.request_cancel()
|
||||||
|
if alive:
|
||||||
|
deadline = time.time() + 5
|
||||||
|
for r in alive:
|
||||||
|
if r.is_alive():
|
||||||
|
r.join(timeout=max(0.1, deadline - time.time()))
|
||||||
|
RUNNERS.clear()
|
||||||
|
# 2) 物理清空数据库
|
||||||
db.clear_tests()
|
db.clear_tests()
|
||||||
return jsonify({"ok": True})
|
return jsonify({"ok": True})
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ DATA_DIR = os.path.join(BASE_DIR, "data")
|
|||||||
LOG_DIR = os.path.join(BASE_DIR, "logs")
|
LOG_DIR = os.path.join(BASE_DIR, "logs")
|
||||||
DB_PATH = os.path.join(DATA_DIR, "llm_speed_tester.db")
|
DB_PATH = os.path.join(DATA_DIR, "llm_speed_tester.db")
|
||||||
|
|
||||||
# 流式请求超时:连接 60s,两次数据包间隔最长 300s(推理型/长上文模型也够用)
|
# 流式请求超时:连接 60s,两次数据包间隔最长 1800s(30 分钟)
|
||||||
|
# 注意:慢接口长上下文预处理(预填充)期间服务端可能长时间不返回任何字节,
|
||||||
|
# 等待首字受 STREAM_READ_TIMEOUT 限制;如接口更慢可在「速度测试配置」里按次调大请求超时。
|
||||||
CONNECT_TIMEOUT = 60
|
CONNECT_TIMEOUT = 60
|
||||||
STREAM_READ_TIMEOUT = 300
|
STREAM_READ_TIMEOUT = 1800
|
||||||
|
|
||||||
# data-chart-tool 图表服务地址(用它的 /api/chart 画折线图:预填充左轴虚线 / 解码右轴实线)
|
# data-chart-tool 图表服务地址(用它的 /api/chart 画折线图:预填充左轴虚线 / 解码右轴实线)
|
||||||
CHART_API_BASE = "http://127.0.0.1:16016"
|
CHART_API_BASE = "http://127.0.0.1:16016"
|
||||||
+12
-1
@@ -277,14 +277,21 @@ def query_tests(q="", status="", provider="", sort="id", order="desc",
|
|||||||
|
|
||||||
|
|
||||||
def clear_tests():
|
def clear_tests():
|
||||||
"""一键清空全部历史(测试记录 + 采样指标 + 日志)"""
|
"""一键清空全部历史(测试记录 + 采样指标 + 日志)。
|
||||||
|
真正清空:删除全部行 + 重置自增序列 + WAL 检查点 + VACUUM 物理回收文件空间,
|
||||||
|
数据不可恢复(不是只从列表里移除,文件里也不再残留)。"""
|
||||||
with _lock:
|
with _lock:
|
||||||
conn = _connect()
|
conn = _connect()
|
||||||
try:
|
try:
|
||||||
conn.execute("DELETE FROM test_runs")
|
conn.execute("DELETE FROM test_runs")
|
||||||
conn.execute("DELETE FROM logs")
|
conn.execute("DELETE FROM logs")
|
||||||
conn.execute("DELETE FROM tests")
|
conn.execute("DELETE FROM tests")
|
||||||
|
# 重置自增序列:清空后新测试 ID 从 1 重新开始
|
||||||
|
conn.execute("DELETE FROM sqlite_sequence WHERE name IN ('tests','test_runs','logs')")
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
# WAL 检查点截断 + VACUUM:把文件里已删除的数据页物理抹掉、文件瘦身
|
||||||
|
conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
||||||
|
conn.execute("VACUUM")
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
@@ -296,6 +303,10 @@ def delete_test(tid: int):
|
|||||||
conn.execute("DELETE FROM tests WHERE id=?", (tid,))
|
conn.execute("DELETE FROM tests WHERE id=?", (tid,))
|
||||||
conn.execute("DELETE FROM test_runs WHERE test_id=?", (tid,))
|
conn.execute("DELETE FROM test_runs WHERE test_id=?", (tid,))
|
||||||
conn.execute("DELETE FROM logs WHERE test_id=?", (tid,))
|
conn.execute("DELETE FROM logs WHERE test_id=?", (tid,))
|
||||||
|
# 全部删光时顺带重置自增序列
|
||||||
|
n = conn.execute("SELECT COUNT(*) FROM tests").fetchone()[0]
|
||||||
|
if n == 0:
|
||||||
|
conn.execute("DELETE FROM sqlite_sequence WHERE name IN ('tests','test_runs','logs')")
|
||||||
conn.commit()
|
conn.commit()
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|||||||
+39
-3
@@ -116,7 +116,7 @@ def stream_openai(cfg, prompt, gen, log, should_stop=None):
|
|||||||
if should_stop and should_stop():
|
if should_stop and should_stop():
|
||||||
raise StopRequested()
|
raise StopRequested()
|
||||||
resp = requests.post(url, json=build_payload(), headers=headers, stream=True,
|
resp = requests.post(url, json=build_payload(), headers=headers, stream=True,
|
||||||
timeout=(config.CONNECT_TIMEOUT, config.STREAM_READ_TIMEOUT))
|
timeout=_timeouts(gen))
|
||||||
if resp.status_code == 200:
|
if resp.status_code == 200:
|
||||||
break
|
break
|
||||||
err = resp.text[:400]
|
err = resp.text[:400]
|
||||||
@@ -152,6 +152,10 @@ def stream_openai(cfg, prompt, gen, log, should_stop=None):
|
|||||||
cached_tokens = details.get("cached_tokens") or 0
|
cached_tokens = details.get("cached_tokens") or 0
|
||||||
except StopRequested:
|
except StopRequested:
|
||||||
raise
|
raise
|
||||||
|
except requests.exceptions.ReadTimeout:
|
||||||
|
raise ProviderError(_read_timeout_err(gen))
|
||||||
|
except requests.exceptions.ConnectTimeout:
|
||||||
|
raise ProviderError(_connect_timeout_err(gen))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ProviderError("流式请求异常: %s" % e)
|
raise ProviderError("流式请求异常: %s" % e)
|
||||||
finally:
|
finally:
|
||||||
@@ -171,6 +175,30 @@ def _bad_stream_options(err: str):
|
|||||||
or "additional properties" in err)
|
or "additional properties" in err)
|
||||||
|
|
||||||
|
|
||||||
|
def _timeouts(gen):
|
||||||
|
"""从测试参数 gen 里取超时配置(秒),未配置则用全局默认。
|
||||||
|
返回 (connect, read) 元组,供 requests timeout 使用。"""
|
||||||
|
try:
|
||||||
|
connect = float(gen.get("connect_timeout") or config.CONNECT_TIMEOUT)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
connect = config.CONNECT_TIMEOUT
|
||||||
|
try:
|
||||||
|
read = float(gen.get("read_timeout") or config.STREAM_READ_TIMEOUT)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
read = config.STREAM_READ_TIMEOUT
|
||||||
|
return (max(connect, 5), max(read, 10))
|
||||||
|
|
||||||
|
|
||||||
|
def _read_timeout_err(gen):
|
||||||
|
"""读超时的友好报错:提示这是等待数据超时,可调大请求超时"""
|
||||||
|
return ("等待响应超时:超过 %.0f 秒未收到数据(接口预处理/首字耗时过长或网络过慢),"
|
||||||
|
"请在「速度测试配置」中调大“请求超时(秒)”" % _timeouts(gen)[1])
|
||||||
|
|
||||||
|
|
||||||
|
def _connect_timeout_err(gen):
|
||||||
|
return "连接超时:超过 %.0f 秒未建立连接(请检查地址/网络)" % _timeouts(gen)[0]
|
||||||
|
|
||||||
|
|
||||||
def _iter_json(resp):
|
def _iter_json(resp):
|
||||||
"""解析 SSE data: 行,逐个返回 JSON 对象"""
|
"""解析 SSE data: 行,逐个返回 JSON 对象"""
|
||||||
for raw in resp.iter_lines(decode_unicode=True):
|
for raw in resp.iter_lines(decode_unicode=True):
|
||||||
@@ -206,7 +234,7 @@ def stream_anthropic(cfg, prompt, gen, log, should_stop=None):
|
|||||||
if should_stop and should_stop():
|
if should_stop and should_stop():
|
||||||
raise StopRequested()
|
raise StopRequested()
|
||||||
resp = requests.post(url, json=payload, headers=headers, stream=True,
|
resp = requests.post(url, json=payload, headers=headers, stream=True,
|
||||||
timeout=(config.CONNECT_TIMEOUT, config.STREAM_READ_TIMEOUT))
|
timeout=_timeouts(gen))
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
err = resp.text[:400]
|
err = resp.text[:400]
|
||||||
resp.close()
|
resp.close()
|
||||||
@@ -233,6 +261,10 @@ def stream_anthropic(cfg, prompt, gen, log, should_stop=None):
|
|||||||
output_tokens = usage.get("output_tokens") or output_tokens
|
output_tokens = usage.get("output_tokens") or output_tokens
|
||||||
except StopRequested:
|
except StopRequested:
|
||||||
raise
|
raise
|
||||||
|
except requests.exceptions.ReadTimeout:
|
||||||
|
raise ProviderError(_read_timeout_err(gen))
|
||||||
|
except requests.exceptions.ConnectTimeout:
|
||||||
|
raise ProviderError(_connect_timeout_err(gen))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ProviderError("流式请求异常: %s" % e)
|
raise ProviderError("流式请求异常: %s" % e)
|
||||||
finally:
|
finally:
|
||||||
@@ -272,7 +304,7 @@ def stream_google(cfg, prompt, gen, log, should_stop=None):
|
|||||||
if should_stop and should_stop():
|
if should_stop and should_stop():
|
||||||
raise StopRequested()
|
raise StopRequested()
|
||||||
resp = requests.post(url, params=params, json=payload, headers=headers, stream=True,
|
resp = requests.post(url, params=params, json=payload, headers=headers, stream=True,
|
||||||
timeout=(config.CONNECT_TIMEOUT, config.STREAM_READ_TIMEOUT))
|
timeout=_timeouts(gen))
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
err = resp.text[:400]
|
err = resp.text[:400]
|
||||||
resp.close()
|
resp.close()
|
||||||
@@ -299,6 +331,10 @@ def stream_google(cfg, prompt, gen, log, should_stop=None):
|
|||||||
cached_tokens = um.get("cachedContentTokenCount") or 0
|
cached_tokens = um.get("cachedContentTokenCount") or 0
|
||||||
except StopRequested:
|
except StopRequested:
|
||||||
raise
|
raise
|
||||||
|
except requests.exceptions.ReadTimeout:
|
||||||
|
raise ProviderError(_read_timeout_err(gen))
|
||||||
|
except requests.exceptions.ConnectTimeout:
|
||||||
|
raise ProviderError(_connect_timeout_err(gen))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ProviderError("流式请求异常: %s" % e)
|
raise ProviderError("流式请求异常: %s" % e)
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -125,6 +125,11 @@
|
|||||||
<span class="slider"></span>
|
<span class="slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>请求超时(秒)<span class="hint-inline">等待首字/预处理上限,接口慢就调大</span></label>
|
||||||
|
<div class="icon-input"><span class="icon">⏱</span><input id="gen-timeout" type="number" min="30" step="30" value="1800"></div>
|
||||||
|
<div class="hint">连接测试与速度采样共用:超过此时长仍未收到数据才判定超时(默认 1800 秒 = 30 分钟)。测慢接口/超长上下文时建议调大,如 3600。</div>
|
||||||
|
</div>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn primary block" id="btn-start">▶ 开始测试</button>
|
<button class="btn primary block" id="btn-start">▶ 开始测试</button>
|
||||||
<button class="btn danger block" id="btn-cancel" disabled>■ 停止</button>
|
<button class="btn danger block" id="btn-cancel" disabled>■ 停止</button>
|
||||||
|
|||||||
+6
-2
@@ -69,6 +69,7 @@ function currentGen() {
|
|||||||
concurrency_levels: [...concurrencyLevelsActive].sort((a, b) => a - b),
|
concurrency_levels: [...concurrencyLevelsActive].sort((a, b) => a - b),
|
||||||
avoid_cache: $("#gen-avoid-cache").checked,
|
avoid_cache: $("#gen-avoid-cache").checked,
|
||||||
warmup: $("#gen-warmup").checked,
|
warmup: $("#gen-warmup").checked,
|
||||||
|
read_timeout: parseInt($("#gen-timeout").value) || 1800,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,9 +202,11 @@ function loadSelectedConfig() {
|
|||||||
async function testConnection() {
|
async function testConnection() {
|
||||||
const cfg = currentConfig();
|
const cfg = currentConfig();
|
||||||
if (!cfg.api_key) { showConn(false, "请先填写 API Key"); return; }
|
if (!cfg.api_key) { showConn(false, "请先填写 API Key"); return; }
|
||||||
|
// 慢接口长预处理可能等首字很久,连接测试也带上请求超时,避免误判接口不通
|
||||||
|
cfg.read_timeout = parseInt($("#gen-timeout").value) || 1800;
|
||||||
const box = $("#conn-result");
|
const box = $("#conn-result");
|
||||||
box.hidden = false; box.className = "conn-result";
|
box.hidden = false; box.className = "conn-result";
|
||||||
box.textContent = "⏳ 正在测试连接...";
|
box.textContent = "⏳ 正在测试连接(等待首字,最长约 " + cfg.read_timeout + " 秒)...";
|
||||||
try {
|
try {
|
||||||
const r = await api("/api/configs/test", "POST", cfg);
|
const r = await api("/api/configs/test", "POST", cfg);
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
@@ -254,7 +257,8 @@ async function listModels() {
|
|||||||
const box = $("#model-result");
|
const box = $("#model-result");
|
||||||
box.hidden = false;
|
box.hidden = false;
|
||||||
box.className = "conn-result fail";
|
box.className = "conn-result fail";
|
||||||
box.textContent = "❌ 获取模型失败:" + e.message;
|
box.textContent = "❌ 接口不通,请检查 Base URL / API Key 配置后重试";
|
||||||
|
console.warn("[listModels] 获取模型失败(已隐藏原始错误):", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ class TestRunner(threading.Thread):
|
|||||||
% (" / ".join(str(x) for x in lengths), max_tokens,
|
% (" / ".join(str(x) for x in lengths), max_tokens,
|
||||||
" / ".join(str(x) for x in concurrency_levels), n,
|
" / ".join(str(x) for x in concurrency_levels), n,
|
||||||
"开" if warmup else "关", "开" if avoid_cache else "关"))
|
"开" if warmup else "关", "开" if avoid_cache else "关"))
|
||||||
|
to = self.gen.get("read_timeout") or "默认(1800)"
|
||||||
|
self.log("INFO", "请求超时: 连接 %s s | 等待首字/预处理 %s s(接口慢可在左侧调大)"
|
||||||
|
% (self.gen.get("connect_timeout") or 60, to))
|
||||||
|
|
||||||
ratio = self._calibrate()
|
ratio = self._calibrate()
|
||||||
self.ratio = ratio
|
self.ratio = ratio
|
||||||
@@ -157,7 +160,9 @@ class TestRunner(threading.Thread):
|
|||||||
"""
|
"""
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
gen_opt = {"max_tokens": max_tokens, "avoid_cache": avoid_cache}
|
gen_opt = {"max_tokens": max_tokens, "avoid_cache": avoid_cache,
|
||||||
|
"connect_timeout": self.gen.get("connect_timeout"),
|
||||||
|
"read_timeout": self.gen.get("read_timeout")}
|
||||||
|
|
||||||
def worker(idx):
|
def worker(idx):
|
||||||
prompt = self._finalize_prompt(base_prompt) # 每流独立随机前缀,避免共享缓存
|
prompt = self._finalize_prompt(base_prompt) # 每流独立随机前缀,避免共享缓存
|
||||||
@@ -240,7 +245,9 @@ class TestRunner(threading.Thread):
|
|||||||
self.log("INFO", "正在校准 token/字符 比例(发送小探测请求)...")
|
self.log("INFO", "正在校准 token/字符 比例(发送小探测请求)...")
|
||||||
try:
|
try:
|
||||||
m = lp.call_stream(self.cfg, probe,
|
m = lp.call_stream(self.cfg, probe,
|
||||||
{"max_tokens": 8, "avoid_cache": False},
|
{"max_tokens": 8, "avoid_cache": False,
|
||||||
|
"connect_timeout": self.gen.get("connect_timeout"),
|
||||||
|
"read_timeout": self.gen.get("read_timeout")},
|
||||||
log=lambda lv, msg: self.log(lv, msg),
|
log=lambda lv, msg: self.log(lv, msg),
|
||||||
should_stop=self.should_stop)
|
should_stop=self.should_stop)
|
||||||
pt = m.get("prompt_tokens") or 0
|
pt = m.get("prompt_tokens") or 0
|
||||||
|
|||||||
Reference in New Issue
Block a user