dsh-best-ip 1.1.1 → 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.
Files changed (7) hide show
  1. package/README.md +37 -7
  2. package/bench.js +177 -57
  3. package/client.js +1510 -1398
  4. package/icon.svg +16 -16
  5. package/index.js +2677 -2459
  6. package/package.json +8 -2
  7. package/providers.js +271 -271
package/README.md CHANGED
@@ -12,9 +12,28 @@ Benchmark the current DSH default model's API IPs. Testing recommends an address
12
12
 
13
13
  需要带有插件管理器、默认模型、Settings、凭据和 Web 服务的 DSH。当前兼容声明为 `>=0.2.1-rc.1`,包含 rc.1、rc.2、rc.3 及后续版本,不设版本上限;这只是安装版本范围,不代表所有后续版本都已实测兼容。Node.js 要求 `>=22`,无需单独安装运行时依赖。仅支持使用 API Key 的默认供应商,暂不支持 OAuth 或其他模型适配器。
14
14
 
15
+ ### 从 npm 安装(推荐)
16
+
17
+ 插件已发布到 npm,可直接按包名安装,无需克隆仓库:
18
+
19
+ 1. 在 DSH 侧栏打开 **插件 → 添加插件 / 安装第三方插件**。
20
+ 2. 在安装来源中填写包名:
21
+
22
+ ```text
23
+ dsh-best-ip
24
+ ```
25
+
26
+ 需要固定版本时填写实际已发布的版本号,例如 `dsh-best-ip@1.2.0`。
27
+ 3. 确认信任该插件,点击 **安装**,随后选择 **立即启用**。
28
+ 4. 打开 **设置 → 模型节点优选**。
29
+
30
+ 包页面:https://www.npmjs.com/package/dsh-best-ip
31
+
32
+ 若你的 DSH 使用国内 npm 镜像且暂时提示找不到包,等待镜像同步,或手动触发 https://npmmirror.com/sync/dsh-best-ip 后再试。
33
+
15
34
  ### 从 GitHub 安装
16
35
 
17
- 仓库推送完成且允许其他人读取后:
36
+ 适用于想跟随源码最新提交,或在插件尚未发布到 npm 时:
18
37
 
19
38
  1. 在 DSH 侧栏打开 **插件 → 添加插件 / 安装第三方插件**。
20
39
  2. 在安装来源中填写:
@@ -32,18 +51,19 @@ Benchmark the current DSH default model's API IPs. Testing recommends an address
32
51
 
33
52
  维护者运行 `npm pack` 生成 `.tgz` 后,可以将它作为 GitHub Release 附件分发。在同一个安装输入框中填写该包在 **DSH Host 上的绝对路径**;这不是浏览器文件上传。远程 Web 用户应使用服务器能读取的路径。
34
53
 
35
- 发布到 npm 后,也可在安装输入框中填写 `dsh-best-ip@版本号`。GitHub/tgz 安装不要求先发布 npm;仅创建仓库或发布 npm 不会自动进入 DSH 的 Discover 列表,Discover 需要配置第三方插件索引。
54
+ 这三种方式都可以安装同一个插件,按名字或 tgz 安装不要求仓库可匿名读取。仅创建仓库或发布 npm 不会自动进入 DSH 的 Discover 列表,Discover 需要配置第三方插件索引。
36
55
 
37
56
  ## 使用
38
57
 
39
58
  - **自动测速**:发现有效 IP、测速并保存推荐,不改变当前 IP。
40
59
  - **自动测速并应用**:成功后应用本轮推荐;失败或停止时保留原 IP。
41
- - **最大测速 IP 数**:`10 / 任意(自定义) / 全部`,默认全部。按 ping 从低到高选取,失败的 IP 也占用名额,不用更慢地址补位。当前选择立即用于两个按钮,点击保存可长期保留。
60
+ - **当前 IP 对照**:已保存应用的 IPv4 会在下次测试中重新纳入同一供应商/域名的候选,首轮优先实测;即使 DNS 或随机扫描没返回它,也不会漏测。只保存了推荐、尚未应用的地址不作对照;重复地址只测一份。
61
+ - **最大测速 IP 数**:`10 / 任意(自定义) / 全部`,默认全部。正数上限内先给已选 IP 保留一个名额,其余按 ping 从低到高选取,例如 10 个是“当前地址 + 9 个新候选”,不会额外扩大付费预算;上限为 1 时仅测当前地址。失败也占名额,不用更慢地址补位。未限数量时,已选 IP 至多额外增加一个候选。显式 `only` 子集仍是权威范围,不会强行插入被排除的地址。当前选择立即用于两个按钮,点击保存可长期保留。
42
62
  - **测一下当前地址**:仅测试当前生效地址;未应用 IP 时使用系统 DNS 的 IPv4 地址,不改变选择。
43
63
  - **停止测试**:取消在途和排队请求,保留已完成样本,不应用部分结果。已发出的请求可能已经计费。
44
64
  - **应用此地址 / 清除并恢复系统 DNS**:显式应用或清除选择。
45
65
 
46
- 页面默认显示 **全部最多约12w token**,旁边的 **!** 图标悬停可查看详细预算:160 个有效 IP × 3 次 × 最多 256 输出 token = 122,880 输出 token,另计输入 token。160 是额外扫描预算,DNS 返回地址可能额外增加;实际费用取决于有效 IP、输出长度及供应商计费。
66
+ 页面默认显示 **全部最多约12w token**,旁边的 **!** 图标悬停可查看详细预算:160 个有效 IP × 3 次 × 最多 256 输出 token = 122,880 输出 token,另计输入 token。160 是额外扫描预算,DNS 返回地址和上次已选 IP 可能额外增加;实际费用取决于有效 IP、输出长度及供应商计费。
47
67
 
48
68
  插件始终跟随目标用户的 DSH 默认供应商/模型,不携带预设供应商 ID、API 地址、凭据或已选 IP。切换默认模型不会自动测速。
49
69
 
@@ -55,15 +75,25 @@ Benchmark the current DSH default model's API IPs. Testing recommends an address
55
75
  | --- | --- | --- |
56
76
  | `maxBenchmarkIps` | `0` | 0 为全部,正整数为付费测速 IP 上限 |
57
77
  | `maxCandidates` | `160` | 额外 Cloudflare 地址探测上限,不含 DNS 返回地址 |
58
- | `benchmarkRuns` | `3` | 每个入选 IP 的相同复测次数,范围 1–5 |
78
+ | `benchmarkRuns` | `3` | 每个入选 IP 的计划次数,范围 1–5;最后一轮允许跳过已证明无竞争力的失败尾部 |
79
+ | `benchmarkTimeoutMs` | `60000` | 单次付费测速的总超时(连接至流结束),整数毫秒,范围 1000–300000;与 DNS、探测超时独立 |
80
+ | `adaptiveBenchmarkTimeout` | `true` | 有足够健康节点时,缩短上一轮超时节点的下一次等待;设为 `false` 则始终使用基础超时 |
81
+ | `adaptiveBenchmarkSampling` | `true` | 最后一轮优先确认领先者,并按乐观分数上界跳过失败尾部;设为 `false` 则测满计划次数(仍按新评分安排轮间顺序) |
59
82
  | `scanEdges` | `true` | 在 DNS 地址之外采样 Cloudflare 边缘网段 |
60
83
  | `dnsServers` | 公共 DNS 列表 | 只用于手动发现候选;空列表仅用系统解析器 |
61
84
 
62
85
  - 支持 HTTPS OpenAI Chat Completions、OpenAI Responses、Anthropic Messages;不猜测其他协议或特殊认证。
63
86
  - 仅扫描/测速 IPv4。探测验证目标域名的 TLS 证书;测速使用原域名的 Host/SNI,连接候选 IP。
64
- - TCP 延迟(界面的 ping)只用于选择测试顺序,不决定最终推荐。
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 秒超时;不改变正常模型请求的超时,也不重写旧测速记录。
65
91
  - 流式速度为输出 token 数 ÷ 首 token 到响应结束的时长。无有效 usage、截断、错误或过短样本不能获得可靠推荐。
66
- - 稳健得分使用 Q25、相对波动和可靠样本率:`Q25 / (1 + IQR / median) × 可靠样本率²`。至少需要 2 次可靠样本和计划次数的严格多数;原始 TPS 保留显示。
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 保持不变,重新测速后才获得新的可比分数。
67
97
  - 网络路径、代理、模型负载和限流都会影响结果。推荐只针对本次测试候选和工作负载,不保证全局最快。
68
98
  - 只有显式应用的地址才影响新建 DNS 连接;不修改操作系统 DNS,也不代表已存在的连接池会立即切换。同域名的不同供应商无法通过全局 DNS 钩子独立选择冲突 IP。
69
99
  - 设置接口仅接受本机回环客户端的同源 POST。远程 Web 管理目前不在支持范围内。
package/bench.js CHANGED
@@ -376,12 +376,12 @@ export function measureStream({
376
376
  })
377
377
  }
378
378
 
379
- // Fixed, versioned scoring policy: short streams amplify timing/buffering noise.
380
- // High spikes can be artifacts, but slow tails are genuine stability evidence.
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 measurable = (run) => run !== undefined && run !== null && run.error === undefined &&
403
- run.cancelled !== true && run.usageReported === true && Number.isFinite(run.tps) && run.tps > 0 &&
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 === undefined) return run == null ? 'missing run' : null
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. Samples are
432
- * a fixed-size allowlist per attempted run: finite raw numbers are unchanged,
433
- * non-finite/missing values become null, and text/credentials are never copied.
434
- * plannedRuns includes unstarted work, so stopping early cannot inflate a score.
435
- * conservativeTps is nearest-rank Q25(v); relativeSpread is (Q75-Q25)/median(v).
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, { plannedRuns = runs.length } = {}) {
440
- plannedRuns = Number.isSafeInteger(plannedRuns) && plannedRuns >= 0
441
- ? Math.max(plannedRuns, runs.length) : runs.length
442
- const ok = runs.filter((run) => run !== undefined && run !== null && run.error === undefined && run.cancelled !== true)
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 !== undefined && run?.cancelled !== true) flags.push('transport-error')
448
- const valid = measurable(run)
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: valid && run.outputTokens >= MIN_RELIABLE_OUTPUT_TOKENS &&
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 / center > OUTLIER_MULTIPLIER) {
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(samples.filter((sample) => sample.reliable).map((sample) => sample.tps))
581
+ const values = sortedNumbers(technical.map((sample) => sample.tps))
478
582
  const reliableRuns = values.length
479
- const rankEligible = reliableRuns >= MIN_RELIABLE_RUNS && reliableRuns > plannedRuns / 2
480
- const q25 = values.length > 0 ? values[Math.ceil(values.length * 0.25) - 1] : null
481
- const q75 = values.length > 0 ? values[Math.ceil(values.length * 0.75) - 1] : null
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 firstFailure = runs.find((run) => run?.error !== undefined)
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
- failed: runs.length - ok.length,
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' : sampleError(firstFailure))
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: median(valid.map((run) => run.tps)),
507
- tpsMin: tps.min, tpsMax: tps.max,
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: runs.length - cancelled, cancelled,
513
- reliableRuns, suspectRuns: samples.length - reliableRuns, rankEligible,
514
- conservativeTps: q25, relativeSpread, stabilityScore, scoringVersion: SCORING_VERSION, scoreReason,
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
+ }