dsh-best-ip 1.1.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -6
- package/bench.js +177 -57
- package/client.js +200 -88
- package/index.js +280 -62
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ Benchmark the current DSH default model's API IPs. Testing recommends an address
|
|
|
23
23
|
dsh-best-ip
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
需要固定版本时填写实际已发布的版本号,例如 `dsh-best-ip@1.2.0`。
|
|
27
27
|
3. 确认信任该插件,点击 **安装**,随后选择 **立即启用**。
|
|
28
28
|
4. 打开 **设置 → 模型节点优选**。
|
|
29
29
|
|
|
@@ -57,12 +57,13 @@ Benchmark the current DSH default model's API IPs. Testing recommends an address
|
|
|
57
57
|
|
|
58
58
|
- **自动测速**:发现有效 IP、测速并保存推荐,不改变当前 IP。
|
|
59
59
|
- **自动测速并应用**:成功后应用本轮推荐;失败或停止时保留原 IP。
|
|
60
|
-
-
|
|
60
|
+
- **当前 IP 对照**:已保存应用的 IPv4 会在下次测试中重新纳入同一供应商/域名的候选,首轮优先实测;即使 DNS 或随机扫描没返回它,也不会漏测。只保存了推荐、尚未应用的地址不作对照;重复地址只测一份。
|
|
61
|
+
- **最大测速 IP 数**:`10 / 任意(自定义) / 全部`,默认全部。正数上限内先给已选 IP 保留一个名额,其余按 ping 从低到高选取,例如 10 个是“当前地址 + 9 个新候选”,不会额外扩大付费预算;上限为 1 时仅测当前地址。失败也占名额,不用更慢地址补位。未限数量时,已选 IP 至多额外增加一个候选。显式 `only` 子集仍是权威范围,不会强行插入被排除的地址。当前选择立即用于两个按钮,点击保存可长期保留。
|
|
61
62
|
- **测一下当前地址**:仅测试当前生效地址;未应用 IP 时使用系统 DNS 的 IPv4 地址,不改变选择。
|
|
62
63
|
- **停止测试**:取消在途和排队请求,保留已完成样本,不应用部分结果。已发出的请求可能已经计费。
|
|
63
64
|
- **应用此地址 / 清除并恢复系统 DNS**:显式应用或清除选择。
|
|
64
65
|
|
|
65
|
-
页面默认显示 **全部最多约12w token**,旁边的 **!** 图标悬停可查看详细预算:160 个有效 IP × 3 次 × 最多 256 输出 token = 122,880 输出 token,另计输入 token。160 是额外扫描预算,DNS
|
|
66
|
+
页面默认显示 **全部最多约12w token**,旁边的 **!** 图标悬停可查看详细预算:160 个有效 IP × 3 次 × 最多 256 输出 token = 122,880 输出 token,另计输入 token。160 是额外扫描预算,DNS 返回地址和上次已选 IP 可能额外增加;实际费用取决于有效 IP、输出长度及供应商计费。
|
|
66
67
|
|
|
67
68
|
插件始终跟随目标用户的 DSH 默认供应商/模型,不携带预设供应商 ID、API 地址、凭据或已选 IP。切换默认模型不会自动测速。
|
|
68
69
|
|
|
@@ -74,15 +75,25 @@ Benchmark the current DSH default model's API IPs. Testing recommends an address
|
|
|
74
75
|
| --- | --- | --- |
|
|
75
76
|
| `maxBenchmarkIps` | `0` | 0 为全部,正整数为付费测速 IP 上限 |
|
|
76
77
|
| `maxCandidates` | `160` | 额外 Cloudflare 地址探测上限,不含 DNS 返回地址 |
|
|
77
|
-
| `benchmarkRuns` | `3` | 每个入选 IP
|
|
78
|
+
| `benchmarkRuns` | `3` | 每个入选 IP 的计划次数,范围 1–5;最后一轮允许跳过已证明无竞争力的失败尾部 |
|
|
79
|
+
| `benchmarkTimeoutMs` | `60000` | 单次付费测速的总超时(连接至流结束),整数毫秒,范围 1000–300000;与 DNS、探测超时独立 |
|
|
80
|
+
| `adaptiveBenchmarkTimeout` | `true` | 有足够健康节点时,缩短上一轮超时节点的下一次等待;设为 `false` 则始终使用基础超时 |
|
|
81
|
+
| `adaptiveBenchmarkSampling` | `true` | 最后一轮优先确认领先者,并按乐观分数上界跳过失败尾部;设为 `false` 则测满计划次数(仍按新评分安排轮间顺序) |
|
|
78
82
|
| `scanEdges` | `true` | 在 DNS 地址之外采样 Cloudflare 边缘网段 |
|
|
79
83
|
| `dnsServers` | 公共 DNS 列表 | 只用于手动发现候选;空列表仅用系统解析器 |
|
|
80
84
|
|
|
81
85
|
- 支持 HTTPS OpenAI Chat Completions、OpenAI Responses、Anthropic Messages;不猜测其他协议或特殊认证。
|
|
82
86
|
- 仅扫描/测速 IPv4。探测验证目标域名的 TLS 证书;测速使用原域名的 Host/SNI,连接候选 IP。
|
|
83
|
-
-
|
|
87
|
+
- 已选 IP 只从相同供应商/域名的保存记录获取,不借用其他供应商或未应用的推荐;会尝试重新探测,但免费探测失败时仍保留它做真实 API 诊断,测速仍验证域名 TLS 证书,实际错误照常记录。其他探测失败的地址不会因此获得付费名额;无 DNS 且无已选地址时仍不盲扫。
|
|
88
|
+
- TCP 延迟(界面的 ping)用于剩余名额及首轮对照之后的顺序;首轮完成后,每轮按最新统一评分从高到低安排下一轮。TPS 原值和每次实测结果始终保留;平分时依次考虑可信样本数、有效速度、慢尾、首字延迟、ping 和 IP。
|
|
89
|
+
- 自适应超时只使用本次入选节点上一轮的数据:至少 3 个且占比达到 60% 的节点产出可信样本,才对上一轮 `timeout` / `ETIMEDOUT` 的节点缩短等待。预算为 `min(基础超时, max(基础超时的一半, 健康样本总时长 P90 × 2))`,使用 nearest-rank P90 并向上取整为毫秒;默认通常为 60 秒 → 30 秒。健康节点本身较慢时保留更长等待;整体变差、样本不足、节点恢复成功或出现其他错误时恢复基础超时,不会逐轮减成 15 秒、7.5 秒。第一轮及“测一下当前地址”始终使用基础超时,每个 IP 的计划次数不变。
|
|
90
|
+
- 两项超时配置可在插件配置行设置;保存接口也接受这两个字段。新样本及进度中的 `timeoutMs` 保留实际预算,便于区分 30 秒和 60 秒超时;不改变正常模型请求的超时,也不重写旧测速记录。
|
|
84
91
|
- 流式速度为输出 token 数 ÷ 首 token 到响应结束的时长。无有效 usage、截断、错误或过短样本不能获得可靠推荐。
|
|
85
|
-
-
|
|
92
|
+
- **评分 v2**:`100 × (0.8 × Hc + 0.2 × Q25c) / R × 成功率² × 测量可信度`。Hc 为封顶后的调和平均 TPS,Q25c 为封顶后的 nearest-rank Q25,R 为本次入选节点各自原始调和 TPS 的中位数。成功率仅统计真实已完成请求;测量可信度为 `可信样本数 / 成功请求数 × min(1, 可信样本数 / 2)`。失败与测量不充分分开,未测、跳过、取消不伪装成失败;IQR 仅作诊断,不再因纯粹提速而扣分。TTFT 仅参与平分决胜。
|
|
93
|
+
- **群体速度上限**:至少 3 个节点且合计 6 个技术可信样本时,以各节点调和 TPS 的 nearest-rank Q90 × 1.5 作为经验上限(不低于 R)。超上限只封顶计分、标记 `high-capped`,不删除原值、不扣成功率。少量样本时不启用封顶/提前淘汰;无群体数据的单地址观测采用参考值 100,不能与另一轮的相对分数直接比较。
|
|
94
|
+
- **最后一轮筛选**:参考量在轮间统一更新,最后一轮冻结同一参照和上限;先完成领先者复测,再对曾失败或没有可信样本的尾部,假设其剩余请求全部成功且达到上限,计算最高可能分。只有该上界仍低于已确认领先者得分的 95% 才跳过;成功但慢的节点、接近的竞争者仍继续测。全场两次都失败或参照不足时保留第三次机会。
|
|
95
|
+
- **推荐与进度**:至少 2 次技术可信样本才有正式推荐资格,正常结束自动应用还须完整复测且无跳过/取消;只有 1 次成功的恢复结果仅显示暂列,不自动应用。进度分别显示已测、跳过、原始预算和轮次;“暂列领先 / 已确认 / 已淘汰 / 旧记录”清楚分开。每轮内分数实时更新、行顺序保持稳定,轮末统一重排。
|
|
96
|
+
- **旧记录兼容**:不将 v1 分数重标成 v2。不同测试轮次用独立标识隔离;未在本轮入选的旧 v2 数据也只作历史展示,不混入当前榜单或冒充新推荐。单独重新发现候选只保留旧样本并统一标为历史,重启前后状态一致;已有手动/生效 IP 保持不变,重新测速后才获得新的可比分数。
|
|
86
97
|
- 网络路径、代理、模型负载和限流都会影响结果。推荐只针对本次测试候选和工作负载,不保证全局最快。
|
|
87
98
|
- 只有显式应用的地址才影响新建 DNS 连接;不修改操作系统 DNS,也不代表已存在的连接池会立即切换。同域名的不同供应商无法通过全局 DNS 钩子独立选择冲突 IP。
|
|
88
99
|
- 设置接口仅接受本机回环客户端的同源 POST。远程 Web 管理目前不在支持范围内。
|
package/bench.js
CHANGED
|
@@ -376,12 +376,12 @@ export function measureStream({
|
|
|
376
376
|
})
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
-
//
|
|
380
|
-
|
|
381
|
-
export const SCORING_VERSION = 1
|
|
379
|
+
// Versioned technical thresholds: throughput alone never makes a sample suspect.
|
|
380
|
+
export const SCORING_VERSION = 2
|
|
382
381
|
export const MIN_RELIABLE_OUTPUT_TOKENS = 32
|
|
383
382
|
export const MIN_RELIABLE_DECODE_MS = 200
|
|
384
383
|
export const MIN_RELIABLE_RUNS = 2
|
|
384
|
+
// Compatibility exports; only the low-tail diagnostic still uses these values.
|
|
385
385
|
export const OUTLIER_MULTIPLIER = 4
|
|
386
386
|
export const MIN_OUTLIER_RUNS = 3
|
|
387
387
|
|
|
@@ -399,14 +399,120 @@ const range = (values) => {
|
|
|
399
399
|
const sorted = sortedNumbers(values)
|
|
400
400
|
return { min: sorted[0] ?? null, max: sorted[sorted.length - 1] ?? null }
|
|
401
401
|
}
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
Number.isSafeInteger(run.outputTokens) && run.outputTokens > 0 &&
|
|
402
|
+
const successful = (run) => run != null && run.error == null && run.cancelled !== true
|
|
403
|
+
const measurable = (run) => successful(run) && run.usageReported === true &&
|
|
404
|
+
Number.isFinite(run.tps) && run.tps > 0 && Number.isSafeInteger(run.outputTokens) && run.outputTokens > 0 &&
|
|
405
405
|
Number.isFinite(run.ttftMs) && run.ttftMs >= 0
|
|
406
|
+
const reliable = (run) => measurable(run) && run.outputTokens >= MIN_RELIABLE_OUTPUT_TOKENS &&
|
|
407
|
+
Number.isFinite(run.decodeMs) && run.decodeMs >= MIN_RELIABLE_DECODE_MS
|
|
408
|
+
const nonnegativeCount = (value) => Number.isSafeInteger(value) && value >= 0
|
|
409
|
+
const quantile = (sorted, fraction) => sorted.length === 0 ? null : sorted[Math.ceil(sorted.length * fraction) - 1]
|
|
410
|
+
|
|
411
|
+
// Scaling by the minimum avoids overflowing 1 / tinyTPS or summing huge TPS.
|
|
412
|
+
// Optional cap copies let optimisticScore handle even safe-integer plans in O(runs).
|
|
413
|
+
function harmonicMean(values, cap = null, copies = 0) {
|
|
414
|
+
const count = values.length + copies
|
|
415
|
+
if (count === 0) return null
|
|
416
|
+
let min = copies > 0 ? cap : Number.MAX_VALUE
|
|
417
|
+
let max = copies > 0 ? cap : 0
|
|
418
|
+
for (const value of values) {
|
|
419
|
+
min = Math.min(min, value)
|
|
420
|
+
max = Math.max(max, value)
|
|
421
|
+
}
|
|
422
|
+
let scaledSum = 0
|
|
423
|
+
for (const value of values) scaledSum += min / value
|
|
424
|
+
const average = scaledSum / count + (copies > 0 ? (min / cap) * (copies / count) : 0)
|
|
425
|
+
return Math.min(max, min / average)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const DEFAULT_SCORING_CONTEXT = Object.freeze({
|
|
429
|
+
scoringVersion: SCORING_VERSION, calibrated: false, peerCount: 0, sampleCount: 0,
|
|
430
|
+
speedReference: 100, speedCap: null,
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
/** Build a fresh, node-balanced context from this cohort's raw samples only. */
|
|
434
|
+
export function buildScoringContext(groups) {
|
|
435
|
+
if (!Array.isArray(groups) || groups.some((group) => !Array.isArray(group))) {
|
|
436
|
+
throw new TypeError('Scoring groups must be arrays of raw runs')
|
|
437
|
+
}
|
|
438
|
+
const means = []
|
|
439
|
+
let sampleCount = 0
|
|
440
|
+
for (const group of new Set(groups)) {
|
|
441
|
+
const values = group.filter(reliable).map((run) => run.tps)
|
|
442
|
+
if (values.length === 0) continue
|
|
443
|
+
means.push(harmonicMean(values))
|
|
444
|
+
sampleCount += values.length
|
|
445
|
+
if (!Number.isSafeInteger(sampleCount)) throw new TypeError('Too many scoring samples')
|
|
446
|
+
}
|
|
447
|
+
means.sort((a, b) => a - b)
|
|
448
|
+
const peerCount = means.length
|
|
449
|
+
const speedReference = median(means) ?? 100
|
|
450
|
+
const calibrated = peerCount >= 3 && sampleCount >= 6
|
|
451
|
+
const upper = calibrated ? quantile(means, 0.9) : null
|
|
452
|
+
const speedCap = calibrated
|
|
453
|
+
? Math.max(speedReference, upper > Number.MAX_VALUE / 1.5 ? Number.MAX_VALUE : upper * 1.5) : null
|
|
454
|
+
return Object.freeze({ scoringVersion: SCORING_VERSION, calibrated, peerCount, sampleCount, speedReference, speedCap })
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Fail closed rather than silently comparing scores in different units. Copy only
|
|
458
|
+
// the public contract so callers cannot persist credentials through context extras.
|
|
459
|
+
function scoringContextOrDefault(context) {
|
|
460
|
+
if (context === undefined) return { ...DEFAULT_SCORING_CONTEXT }
|
|
461
|
+
if (context === null || typeof context !== 'object' || Array.isArray(context)) throw new TypeError('Invalid scoring context')
|
|
462
|
+
const { scoringVersion, calibrated, peerCount, sampleCount, speedReference, speedCap } = context
|
|
463
|
+
const countsValid = nonnegativeCount(peerCount) && nonnegativeCount(sampleCount) && sampleCount >= peerCount &&
|
|
464
|
+
(peerCount > 0 || sampleCount === 0)
|
|
465
|
+
const enoughPeers = countsValid && peerCount >= 3 && sampleCount >= 6
|
|
466
|
+
if (scoringVersion !== SCORING_VERSION || typeof calibrated !== 'boolean' || !countsValid ||
|
|
467
|
+
calibrated !== enoughPeers || !Number.isFinite(speedReference) || speedReference <= 0 ||
|
|
468
|
+
(peerCount === 0 && speedReference !== 100) ||
|
|
469
|
+
(calibrated ? !Number.isFinite(speedCap) || speedCap <= 0 || speedCap < speedReference : speedCap !== null)) {
|
|
470
|
+
throw new TypeError('Invalid scoring context')
|
|
471
|
+
}
|
|
472
|
+
return { scoringVersion, calibrated, peerCount, sampleCount, speedReference, speedCap }
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function normalizedCounts(runs, plannedRuns, skippedRuns = 0) {
|
|
476
|
+
const skipped = nonnegativeCount(skippedRuns) ? Math.min(skippedRuns, Number.MAX_SAFE_INTEGER - runs.length) : 0
|
|
477
|
+
const actual = runs.length + skipped
|
|
478
|
+
return { planned: nonnegativeCount(plannedRuns) ? Math.max(plannedRuns, actual) : actual, skipped }
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function scoreMetrics(values, successfulRuns, completedRuns, context, copies = 0) {
|
|
482
|
+
const scored = sortedNumbers(values.map((value) => context.speedCap === null ? value : Math.min(value, context.speedCap)))
|
|
483
|
+
const count = scored.length + copies
|
|
484
|
+
const successRate = completedRuns > 0 ? successfulRuns / completedRuns : 0
|
|
485
|
+
const measurementConfidence = successfulRuns > 0
|
|
486
|
+
? (count / successfulRuns) * Math.min(1, count / MIN_RELIABLE_RUNS) : 0
|
|
487
|
+
if (count === 0) return { successRate, measurementConfidence, effectiveTps: null, provisionalScore: null }
|
|
488
|
+
const effectiveTps = harmonicMean(scored, context.speedCap, copies)
|
|
489
|
+
const qIndex = Math.ceil(count * 0.25) - 1
|
|
490
|
+
const lower = qIndex < scored.length ? scored[qIndex] : context.speedCap
|
|
491
|
+
// Score with an unrounded dimensionless H/Q25 ratio. The displayed H may be
|
|
492
|
+
// subnormal: rounding it first can make a faster sample lower the score and
|
|
493
|
+
// even invalidate the optimistic pruning bound.
|
|
494
|
+
const scale = lower
|
|
495
|
+
const minimum = Math.min(scored[0] ?? context.speedCap, copies > 0 ? context.speedCap : Number.MAX_VALUE)
|
|
496
|
+
const meanReciprocal = scored.reduce((sum, value) => sum + minimum / value, 0) / count +
|
|
497
|
+
(copies > 0 ? (minimum / context.speedCap) * (copies / count) : 0)
|
|
498
|
+
const harmonicRatio = (minimum / scale) / meanReciprocal
|
|
499
|
+
const blend = 0.8 * harmonicRatio + 0.2
|
|
500
|
+
const penalty = 100 * successRate ** 2 * measurementConfidence
|
|
501
|
+
const ratio = scale / context.speedReference
|
|
502
|
+
let provisionalScore = ratio * (blend * penalty)
|
|
503
|
+
const minNormal = 2 ** -1022
|
|
504
|
+
if (!Number.isFinite(provisionalScore) || ratio < minNormal || provisionalScore < minNormal) {
|
|
505
|
+
// Take only the final rounding when an intermediate ratio is subnormal or
|
|
506
|
+
// overflows. The normal fast path retains exact constant-speed scores.
|
|
507
|
+
const logScore = Math.log(scale) - Math.log(context.speedReference) + Math.log(blend) + Math.log(penalty)
|
|
508
|
+
provisionalScore = logScore >= Math.log(Number.MAX_VALUE) ? Number.MAX_VALUE : Math.exp(logScore)
|
|
509
|
+
}
|
|
510
|
+
return { successRate, measurementConfidence, effectiveTps, provisionalScore }
|
|
511
|
+
}
|
|
406
512
|
|
|
407
513
|
export function sampleError(run) {
|
|
408
514
|
if (run?.cancelled === true) return 'test stopped'
|
|
409
|
-
if (run?.error
|
|
515
|
+
if (run?.error == null) return run == null ? 'missing run' : null
|
|
410
516
|
// Provider bodies and exception messages can echo prompts, URLs or secrets.
|
|
411
517
|
// Persist a bounded error category, never their arbitrary trailing text.
|
|
412
518
|
if (typeof run.error !== 'string') return 'request failed'
|
|
@@ -428,89 +534,103 @@ export function sampleError(run) {
|
|
|
428
534
|
}
|
|
429
535
|
|
|
430
536
|
/**
|
|
431
|
-
* Preserve transport/median display fields independently of ranking.
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
* stabilityScore = Q25/(1+relativeSpread) * (reliableRuns/plannedRuns)^2.
|
|
437
|
-
* rankEligible is authoritative; rejected scores are null, not transport errors.
|
|
537
|
+
* Preserve raw transport/median display fields independently of ranking. Only
|
|
538
|
+
* technical rules select scoring samples; caps never lower reliability. The
|
|
539
|
+
* score is 100 * (0.8 * capped H + 0.2 * capped nearest-rank Q25) / reference
|
|
540
|
+
* times successRate squared and measurementConfidence. Spread is diagnostic.
|
|
541
|
+
* Pending, cancelled and deliberately skipped work are distinct from failures.
|
|
438
542
|
*/
|
|
439
|
-
export function summarize(runs, {
|
|
440
|
-
plannedRuns =
|
|
441
|
-
|
|
442
|
-
const
|
|
543
|
+
export function summarize(runs, {
|
|
544
|
+
plannedRuns = runs.length, scoringContext, skippedRuns = 0, skippedReason,
|
|
545
|
+
} = {}) {
|
|
546
|
+
const context = scoringContextOrDefault(scoringContext)
|
|
547
|
+
const counts = normalizedCounts(runs, plannedRuns, skippedRuns)
|
|
548
|
+
plannedRuns = counts.planned
|
|
549
|
+
skippedRuns = counts.skipped
|
|
550
|
+
const ok = runs.filter(successful)
|
|
443
551
|
const valid = ok.filter(measurable)
|
|
444
552
|
const samples = Array.from(runs, (run) => {
|
|
445
553
|
const flags = []
|
|
446
554
|
if (run?.cancelled === true) flags.push('cancelled')
|
|
447
|
-
if (run?.error
|
|
448
|
-
|
|
449
|
-
if (!valid) flags.push('unmeasurable')
|
|
555
|
+
if (run?.error != null && run?.cancelled !== true) flags.push('transport-error')
|
|
556
|
+
if (!measurable(run)) flags.push('unmeasurable')
|
|
450
557
|
if (Number.isSafeInteger(run?.outputTokens) && run.outputTokens > 0 && run.outputTokens < MIN_RELIABLE_OUTPUT_TOKENS) {
|
|
451
558
|
flags.push('insufficient-tokens')
|
|
452
559
|
}
|
|
453
560
|
if (run?.decodeMs == null) flags.push('missing-decode-window')
|
|
454
561
|
else if (!Number.isFinite(run.decodeMs) || run.decodeMs < 0) flags.push('invalid-decode-window')
|
|
455
562
|
else if (run.decodeMs < MIN_RELIABLE_DECODE_MS) flags.push('insufficient-decode-window')
|
|
563
|
+
const trusted = reliable(run)
|
|
564
|
+
const capped = trusted && context.speedCap !== null && run.tps > context.speedCap
|
|
565
|
+
if (capped) flags.push('high-capped')
|
|
456
566
|
return {
|
|
457
567
|
status: finite(run?.status), tps: finite(run?.tps), ttftMs: finite(run?.ttftMs),
|
|
458
568
|
totalMs: finite(run?.totalMs), decodeMs: finite(run?.decodeMs), outputTokens: finite(run?.outputTokens),
|
|
569
|
+
...(Number.isSafeInteger(run?.timeoutMs) && run.timeoutMs > 0 ? { timeoutMs: run.timeoutMs } : {}),
|
|
459
570
|
usageReported: run?.usageReported === true, error: sampleError(run), cancelled: run?.cancelled === true,
|
|
460
|
-
flags, reliable:
|
|
461
|
-
Number.isFinite(run.decodeMs) && run.decodeMs >= MIN_RELIABLE_DECODE_MS,
|
|
571
|
+
flags, reliable: trusted, scoredTps: trusted ? (capped ? context.speedCap : run.tps) : null,
|
|
462
572
|
}
|
|
463
573
|
})
|
|
464
574
|
const technical = samples.filter((sample) => sample.reliable)
|
|
465
575
|
if (technical.length >= MIN_OUTLIER_RUNS) {
|
|
466
576
|
const center = median(technical.map((sample) => sample.tps))
|
|
467
577
|
for (const sample of technical) {
|
|
468
|
-
if (sample.tps
|
|
469
|
-
sample.flags.push('high-outlier')
|
|
470
|
-
sample.reliable = false
|
|
471
|
-
} else if (sample.tps < center / OUTLIER_MULTIPLIER) {
|
|
472
|
-
// Keep the low tail in v: discarding it would reward unstable routes.
|
|
473
|
-
sample.flags.push('low-tail')
|
|
474
|
-
}
|
|
578
|
+
if (sample.tps < center / OUTLIER_MULTIPLIER) sample.flags.push('low-tail')
|
|
475
579
|
}
|
|
476
580
|
}
|
|
477
|
-
const values = sortedNumbers(
|
|
581
|
+
const values = sortedNumbers(technical.map((sample) => sample.tps))
|
|
478
582
|
const reliableRuns = values.length
|
|
479
|
-
const rankEligible = reliableRuns >= MIN_RELIABLE_RUNS
|
|
480
|
-
const q25 =
|
|
481
|
-
const q75 =
|
|
583
|
+
const rankEligible = reliableRuns >= MIN_RELIABLE_RUNS
|
|
584
|
+
const q25 = quantile(values, 0.25)
|
|
585
|
+
const q75 = quantile(values, 0.75)
|
|
482
586
|
const center = median(values)
|
|
483
587
|
const relativeSpread = center === null ? null : Math.min(Number.MAX_VALUE, (q75 - q25) / center)
|
|
484
|
-
const stabilityScore = rankEligible
|
|
485
|
-
? q25 / (1 + relativeSpread) * (reliableRuns / plannedRuns) ** 2 : null
|
|
486
|
-
const scoreReason = rankEligible
|
|
487
|
-
? `ranked using ${reliableRuns}/${plannedRuns} reliable runs; lower-quartile TPS, spread and completion penalties`
|
|
488
|
-
: reliableRuns < MIN_RELIABLE_RUNS
|
|
489
|
-
? `only ${reliableRuns}/${plannedRuns} reliable runs; at least ${MIN_RELIABLE_RUNS} are required (>=${MIN_RELIABLE_OUTPUT_TOKENS} output tokens and >=${MIN_RELIABLE_DECODE_MS}ms decode window)`
|
|
490
|
-
: `only ${reliableRuns}/${plannedRuns} reliable runs; a strict majority of planned runs is required`
|
|
491
588
|
const cancelled = samples.filter((sample) => sample.cancelled).length
|
|
492
|
-
const
|
|
589
|
+
const completedRuns = runs.length - cancelled
|
|
590
|
+
const successfulRuns = ok.length
|
|
591
|
+
const metrics = scoreMetrics(values, successfulRuns, completedRuns, context)
|
|
592
|
+
const pendingRuns = Math.max(0, plannedRuns - runs.length - skippedRuns)
|
|
593
|
+
const confirmed = rankEligible && pendingRuns === 0 && skippedRuns === 0 && cancelled === 0 && runs.length >= plannedRuns
|
|
594
|
+
const scoreReason = reliableRuns === 0 ? 'No technically reliable samples'
|
|
595
|
+
: !rankEligible ? 'Provisional; at least 2 reliable runs required'
|
|
596
|
+
: confirmed ? 'Confirmed; harmonic speed, success and confidence'
|
|
597
|
+
: 'Provisional; pending, cancelled or skipped runs remain'
|
|
598
|
+
const firstFailure = samples.find((sample) => sample.error !== null)
|
|
493
599
|
const eligible = valid.length > runs.length / 2
|
|
494
600
|
const tps = range(valid.map((run) => run.tps))
|
|
495
601
|
const ttft = range(valid.map((run) => run.ttftMs))
|
|
496
602
|
return {
|
|
497
|
-
runs: runs.length,
|
|
498
|
-
|
|
499
|
-
measurable: valid.length,
|
|
500
|
-
unmeasurable: ok.length - valid.length,
|
|
501
|
-
eligible,
|
|
603
|
+
runs: runs.length, failed: completedRuns - successfulRuns,
|
|
604
|
+
measurable: valid.length, unmeasurable: ok.length - valid.length, eligible,
|
|
502
605
|
...(!eligible ? { error: valid.length === 0
|
|
503
|
-
? (firstFailure === undefined ? 'no run produced measurable TPS' :
|
|
606
|
+
? (firstFailure === undefined ? 'no run produced measurable TPS' : firstFailure.error)
|
|
504
607
|
: `only ${valid.length}/${runs.length} runs produced measurable TPS; a strict majority is required` } : {}),
|
|
505
|
-
ttftMs: median(valid.map((run) => run.ttftMs)),
|
|
506
|
-
tps:
|
|
507
|
-
|
|
508
|
-
ttftMinMs: ttft.min, ttftMaxMs: ttft.max,
|
|
509
|
-
totalMs: median(valid.map((run) => run.totalMs)),
|
|
510
|
-
outputTokens: median(valid.map((run) => run.outputTokens)),
|
|
608
|
+
ttftMs: median(valid.map((run) => run.ttftMs)), tps: median(valid.map((run) => run.tps)),
|
|
609
|
+
tpsMin: tps.min, tpsMax: tps.max, ttftMinMs: ttft.min, ttftMaxMs: ttft.max,
|
|
610
|
+
totalMs: median(valid.map((run) => run.totalMs)), outputTokens: median(valid.map((run) => run.outputTokens)),
|
|
511
611
|
usageReported: ok.length > 0 && ok.every((run) => run.usageReported === true),
|
|
512
|
-
samples, plannedRuns, completedRuns
|
|
513
|
-
|
|
514
|
-
|
|
612
|
+
samples, plannedRuns, completedRuns, successfulRuns, cancelled, skippedRuns, pendingRuns,
|
|
613
|
+
...(skippedRuns > 0 ? { skippedReason: skippedReason === 'optimistic-bound' ? 'optimistic-bound' : 'not scheduled' } : {}),
|
|
614
|
+
reliableRuns, suspectRuns: samples.length - reliableRuns, rankEligible, confirmed, eliminated: skippedRuns > 0,
|
|
615
|
+
harmonicTps: harmonicMean(values), conservativeTps: q25, relativeSpread,
|
|
616
|
+
...metrics, stabilityScore: metrics.provisionalScore,
|
|
617
|
+
scoringVersion: SCORING_VERSION, scoreContext: context, scoreReason,
|
|
515
618
|
}
|
|
516
619
|
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Best possible final score under a frozen calibrated cap. Real failures and
|
|
623
|
+
* cancellations stay untouched. Unmeasured attempts are successful, technically
|
|
624
|
+
* reliable cap-TPS samples with zero TTFT. Use summarize's exact score primitive
|
|
625
|
+
* without allocating a potentially enormous synthetic-sample array.
|
|
626
|
+
*/
|
|
627
|
+
export function optimisticScore(runs, { plannedRuns = runs.length, scoringContext } = {}) {
|
|
628
|
+
const context = scoringContextOrDefault(scoringContext)
|
|
629
|
+
if (!context.calibrated || !Number.isFinite(context.speedCap) || context.speedCap <= 0) return null
|
|
630
|
+
const { planned } = normalizedCounts(runs, plannedRuns)
|
|
631
|
+
const remaining = planned - runs.length
|
|
632
|
+
const values = runs.filter(reliable).map((run) => run.tps)
|
|
633
|
+
const successfulRuns = runs.filter(successful).length + remaining
|
|
634
|
+
const completedRuns = runs.filter((run) => run?.cancelled !== true).length + remaining
|
|
635
|
+
return scoreMetrics(values, successfulRuns, completedRuns, context, remaining).provisionalScore
|
|
636
|
+
}
|