free-coding-models 0.3.79 β 0.4.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 +4 -0
- package/changelog/v0.3.80.md +9 -0
- package/changelog/v0.3.81.md +14 -0
- package/changelog/v0.4.0.md +23 -0
- package/package.json +1 -1
- package/src/app.js +19 -1
- package/src/benchmark.js +102 -52
- package/src/cli-help.js +1 -1
- package/src/config.js +3 -1
- package/src/key-handler.js +82 -8
- package/src/overlays.js +15 -4
- package/src/ping.js +69 -9
- package/src/render-helpers.js +4 -3
- package/src/render-table.js +222 -101
- package/src/router-daemon.js +6 -7
- package/src/setup.js +16 -6
- package/src/theme.js +1 -1
- package/src/tui-state.js +5 -0
- package/src/utils.js +25 -1
- package/web/dist/assets/{index-DCD5slDY.js β index-Dr33-jga.js} +1 -1
- package/web/dist/index.html +1 -1
package/src/render-table.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* with consistent alignment, colorization, and viewport clipping.
|
|
9
9
|
*
|
|
10
10
|
* π― Key features:
|
|
11
|
-
* - Full table layout with tier, latency, stability, uptime, token totals, and usage columns
|
|
11
|
+
* - Full table layout with micro verdict indicator, tier, latency, stability, uptime, token totals, and usage columns
|
|
12
12
|
* - Hotkey-aware header lettering so highlighted letters always match live sort/filter keys
|
|
13
13
|
* - Emoji-aware padding via padEndDisplay for aligned verdict/status cells
|
|
14
14
|
* - Viewport clipping with above/below indicators
|
|
@@ -45,11 +45,11 @@ import {
|
|
|
45
45
|
TABLE_FOOTER_LINES,
|
|
46
46
|
FRAMES
|
|
47
47
|
} from './constants.js'
|
|
48
|
-
import { themeColors, getProviderRgb, getTierRgb, getReadableTextRgb, getTheme } from './theme.js'
|
|
48
|
+
import { themeColors, currentPalette, getProviderRgb, getTierRgb, getReadableTextRgb, getTheme } from './theme.js'
|
|
49
49
|
import { TIER_COLOR } from './tier-colors.js'
|
|
50
50
|
import { getAvg, getVerdict, getUptime, getStabilityScore, getVersionStatusInfo } from './utils.js'
|
|
51
51
|
import { usagePlaceholderForProvider } from './ping.js'
|
|
52
|
-
import {
|
|
52
|
+
import { formatBenchmarkLatency, formatBenchmarkTps } from './benchmark.js'
|
|
53
53
|
import { calculateViewport, sortResultsWithPinnedFavorites, padEndDisplay, displayWidth, stripAnsi } from './render-helpers.js'
|
|
54
54
|
import { getToolMeta, TOOL_METADATA, TOOL_MODE_ORDER, isModelCompatibleWithTool } from './tool-metadata.js'
|
|
55
55
|
import { getColumnSpacing } from './ui-config.js'
|
|
@@ -79,6 +79,7 @@ export function getLastLayout() { return _lastLayout }
|
|
|
79
79
|
|
|
80
80
|
// π Column name β sort key mapping for mouse click-to-sort on header row
|
|
81
81
|
const COLUMN_SORT_MAP = {
|
|
82
|
+
mood: 'verdict',
|
|
82
83
|
rank: 'rank',
|
|
83
84
|
tier: null, // π Tier column click cycles tier filter rather than sorting
|
|
84
85
|
swe: 'swe',
|
|
@@ -91,6 +92,8 @@ const COLUMN_SORT_MAP = {
|
|
|
91
92
|
verdict: 'verdict',
|
|
92
93
|
stability: 'stability',
|
|
93
94
|
uptime: 'uptime',
|
|
95
|
+
aiLatency: 'aiLatency',
|
|
96
|
+
tps: 'tps',
|
|
94
97
|
}
|
|
95
98
|
export { COLUMN_SORT_MAP }
|
|
96
99
|
|
|
@@ -184,7 +187,8 @@ export function renderTable({
|
|
|
184
187
|
routerFooterRequests = 0,
|
|
185
188
|
benchmarkResults = {},
|
|
186
189
|
benchmarkRunning = new Set(),
|
|
187
|
-
|
|
190
|
+
headerFlashColumn = null,
|
|
191
|
+
} = _) {
|
|
188
192
|
// π Filter out hidden models for display
|
|
189
193
|
const visibleResults = results.filter(r => !r.hidden)
|
|
190
194
|
|
|
@@ -268,16 +272,18 @@ export function renderTable({
|
|
|
268
272
|
const COL_SEP = getColumnSpacing()
|
|
269
273
|
const SEP_W = 3 // ' β ' display width
|
|
270
274
|
const ROW_MARGIN = 2 // left margin ' '
|
|
275
|
+
const W_MOOD = 2
|
|
271
276
|
const W_RANK = 6
|
|
272
277
|
const W_TIER = 5
|
|
273
278
|
const W_CTX = 4
|
|
274
279
|
const W_SOURCE = 14
|
|
275
280
|
const W_MODEL = 26
|
|
276
281
|
const W_SWE = 5
|
|
277
|
-
const W_STATUS =
|
|
278
|
-
const W_VERDICT =
|
|
282
|
+
const W_STATUS = 17
|
|
283
|
+
const W_VERDICT = 13
|
|
279
284
|
const W_UPTIME = 6
|
|
280
|
-
const
|
|
285
|
+
const W_AI_LATENCY = 17
|
|
286
|
+
const W_TPS = 5
|
|
281
287
|
|
|
282
288
|
// const W_TOKENS = 7 // Used column removed
|
|
283
289
|
// const W_USAGE = 7 // Usage column removed
|
|
@@ -285,17 +291,19 @@ export function renderTable({
|
|
|
285
291
|
|
|
286
292
|
// π Responsive column visibility: progressively hide least-useful columns
|
|
287
293
|
// π and shorten header labels when terminal width is insufficient.
|
|
288
|
-
// π Hiding order (least useful first): Rank β
|
|
289
|
-
// π
|
|
290
|
-
// π
|
|
291
|
-
// π
|
|
292
|
-
|
|
293
|
-
let
|
|
294
|
+
// π Hiding order (least useful first): Rank β AI Latency/TPS β Up% β Tier β Stability
|
|
295
|
+
// π Ping columns stay compact because the cell values are tiny numbers without a "ms" suffix.
|
|
296
|
+
// π Both ping columns use the same 9-char width so Last Ping and Avg Ping fit cleanly.
|
|
297
|
+
// π Compact mode also shrinks StabilityβStaB. (8), Providerβ4chars+β¦ (7), Healthβ6chars+β¦ (13).
|
|
298
|
+
// π Breakpoints are computed dynamically from active column widths.
|
|
299
|
+
let wPing = 9
|
|
300
|
+
let wAvg = 9
|
|
294
301
|
let wStab = 11
|
|
295
302
|
let wSource = W_SOURCE
|
|
296
303
|
let wStatus = W_STATUS
|
|
304
|
+
let wAiLatency = W_AI_LATENCY
|
|
297
305
|
let showRank = true
|
|
298
|
-
let
|
|
306
|
+
let showBenchmarkColumns = true
|
|
299
307
|
let showUptime = true
|
|
300
308
|
let showTier = true
|
|
301
309
|
let showStability = true
|
|
@@ -305,27 +313,29 @@ export function renderTable({
|
|
|
305
313
|
// π Dynamically compute needed row width from visible columns
|
|
306
314
|
const calcWidth = () => {
|
|
307
315
|
const cols = []
|
|
316
|
+
cols.push(W_MOOD)
|
|
308
317
|
if (showRank) cols.push(W_RANK)
|
|
309
318
|
if (showTier) cols.push(W_TIER)
|
|
310
319
|
cols.push(W_SWE, W_CTX, W_MODEL, wSource, wPing, wAvg, wStatus, W_VERDICT)
|
|
311
320
|
if (showStability) cols.push(wStab)
|
|
312
321
|
if (showUptime) cols.push(W_UPTIME)
|
|
313
|
-
if (
|
|
322
|
+
if (showBenchmarkColumns) cols.push(wAiLatency, W_TPS)
|
|
314
323
|
return ROW_MARGIN + cols.reduce((a, b) => a + b, 0) + (cols.length - 1) * SEP_W
|
|
315
324
|
}
|
|
316
325
|
|
|
317
326
|
// π Step 1: Compact mode β shorten labels and reduce column widths
|
|
318
327
|
if (calcWidth() > terminalCols) {
|
|
319
328
|
isCompact = true
|
|
320
|
-
wPing = 9 // '
|
|
321
|
-
wAvg =
|
|
329
|
+
wPing = 9 // 'Last Ping' stays compact and matches Avg Ping width
|
|
330
|
+
wAvg = 9 // 'Avg Ping' stays aligned with Last Ping
|
|
322
331
|
wStab = 8 // 'StaB.' instead of 'Stability'
|
|
323
332
|
wSource = 7 // Provider truncated to 4 chars + 'β¦', 7 cols total
|
|
324
333
|
wStatus = 13 // Health truncated after 6 chars + 'β¦'
|
|
334
|
+
wAiLatency = 13 // Mirror compact Health text when health is not good
|
|
325
335
|
}
|
|
326
336
|
// π Steps 2β6: Progressive column hiding (least useful first)
|
|
327
337
|
if (calcWidth() > terminalCols) showRank = false
|
|
328
|
-
if (calcWidth() > terminalCols)
|
|
338
|
+
if (calcWidth() > terminalCols) showBenchmarkColumns = false
|
|
329
339
|
if (calcWidth() > terminalCols) showUptime = false
|
|
330
340
|
if (calcWidth() > terminalCols) showTier = false
|
|
331
341
|
if (calcWidth() > terminalCols) showStability = false
|
|
@@ -336,6 +346,7 @@ export function renderTable({
|
|
|
336
346
|
// π matching exactly what renderTable paints so click-to-sort hits the right column.
|
|
337
347
|
{
|
|
338
348
|
const colDefs = []
|
|
349
|
+
colDefs.push({ name: 'mood', width: W_MOOD })
|
|
339
350
|
if (showRank) colDefs.push({ name: 'rank', width: W_RANK })
|
|
340
351
|
if (showTier) colDefs.push({ name: 'tier', width: W_TIER })
|
|
341
352
|
colDefs.push({ name: 'swe', width: W_SWE })
|
|
@@ -348,7 +359,10 @@ export function renderTable({
|
|
|
348
359
|
colDefs.push({ name: 'verdict', width: W_VERDICT })
|
|
349
360
|
if (showStability) colDefs.push({ name: 'stability', width: wStab })
|
|
350
361
|
if (showUptime) colDefs.push({ name: 'uptime', width: W_UPTIME })
|
|
351
|
-
if (
|
|
362
|
+
if (showBenchmarkColumns) {
|
|
363
|
+
colDefs.push({ name: 'aiLatency', width: wAiLatency })
|
|
364
|
+
colDefs.push({ name: 'tps', width: W_TPS })
|
|
365
|
+
}
|
|
352
366
|
let x = ROW_MARGIN + 1 // π 1-based: first column starts after the 2-char left margin
|
|
353
367
|
const columns = []
|
|
354
368
|
for (let i = 0; i < colDefs.length; i++) {
|
|
@@ -394,6 +408,7 @@ export function renderTable({
|
|
|
394
408
|
// π Sort models using the shared helper
|
|
395
409
|
const sorted = sortResultsWithPinnedFavorites(visibleResults, sortColumn, sortDirection, {
|
|
396
410
|
pinFavorites: favoritesPinnedAndSticky,
|
|
411
|
+
benchmarkResults,
|
|
397
412
|
})
|
|
398
413
|
|
|
399
414
|
const lines = [
|
|
@@ -410,24 +425,20 @@ export function renderTable({
|
|
|
410
425
|
// π Solution: build plain text first, then colorize
|
|
411
426
|
const dir = sortDirection === 'asc' ? 'β' : 'β'
|
|
412
427
|
|
|
413
|
-
|
|
414
|
-
const
|
|
415
|
-
const
|
|
416
|
-
const
|
|
417
|
-
const
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
const
|
|
421
|
-
const
|
|
422
|
-
const
|
|
423
|
-
const
|
|
424
|
-
const
|
|
425
|
-
const
|
|
426
|
-
const
|
|
427
|
-
// π Stability: in non-compact the arrow eats 2 chars ('β '), so truncate to fit wStab.
|
|
428
|
-
// π Compact is fine because 'β StaB.' (7) < wStab (8).
|
|
429
|
-
const stabH = sortColumn === 'stability' ? (dir + (isCompact ? ' ' + stabLabel : 'Stability')) : stabLabel
|
|
430
|
-
const uptimeH = sortColumn === 'uptime' ? (dir + 'Up%') : 'Up%'
|
|
428
|
+
// π Plain header labels β arrows are appended dynamically below.
|
|
429
|
+
const moodLabel = 'β'
|
|
430
|
+
const rankLabel = 'Rank'
|
|
431
|
+
const tierLabel = 'Tier'
|
|
432
|
+
const originLabel = isCompact ? 'PrODβ¦' : 'Provider'
|
|
433
|
+
const modelLabel = 'Model'
|
|
434
|
+
const sweLabel = 'SWE%'
|
|
435
|
+
const ctxLabel = 'CTX'
|
|
436
|
+
const pingLabel = 'Last Ping'
|
|
437
|
+
const avgLabel = 'Avg Ping'
|
|
438
|
+
const healthLabel = 'Health'
|
|
439
|
+
const verdictLabel = 'Verdict'
|
|
440
|
+
const stabLabel = isCompact ? 'StaB.' : 'Stability'
|
|
441
|
+
const uptimeLabel = 'Up%'
|
|
431
442
|
|
|
432
443
|
// π Helper to colorize first letter for keyboard shortcuts
|
|
433
444
|
// π IMPORTANT: Pad PLAIN TEXT first, then apply colors to avoid alignment issues
|
|
@@ -439,59 +450,130 @@ export function renderTable({
|
|
|
439
450
|
return colorFn(first) + themeColors.dim(rest + padding)
|
|
440
451
|
}
|
|
441
452
|
|
|
442
|
-
// π
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
// π
|
|
466
|
-
|
|
453
|
+
// π Flash animation: when a column header is clicked, it briefly renders with
|
|
454
|
+
// π a vivid inverse style (bright accent bg + white bold fg) for ~250ms.
|
|
455
|
+
// π This gives satisfying visual feedback that the click was registered.
|
|
456
|
+
const flashHeader = (plainText, width) => {
|
|
457
|
+
const padded = plainText.length <= width ? plainText.padEnd(width) : plainText.slice(0, width)
|
|
458
|
+
const [r, g, b] = currentPalette().accentStrong
|
|
459
|
+
return chalk.bold.rgb(255, 255, 255).bgRgb(r, g, b)(padded)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// π Sort-active header: renders the column header with a subtle background color
|
|
463
|
+
// π to visually indicate which column is currently sorted.
|
|
464
|
+
// π Includes the β/β arrow and bold white text on a tinted background.
|
|
465
|
+
// π If the arrow prefix doesn't fit, appends it: 'SWEβ' instead of 'β SWE%'.
|
|
466
|
+
const sortActiveHeader = (label, width) => {
|
|
467
|
+
const arrow = dir
|
|
468
|
+
const prefixed = arrow + ' ' + label
|
|
469
|
+
const text = prefixed.length <= width ? prefixed : label + arrow
|
|
470
|
+
const padded = text.padEnd(width).slice(0, width)
|
|
471
|
+
// π Subtle dark accent background β visible but not overwhelming.
|
|
472
|
+
const bg = currentPalette().cursor.defaultBg
|
|
473
|
+
return chalk.bold.rgb(255, 255, 255).bgRgb(...bg)(padded)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// π Now colorize each column header.
|
|
477
|
+
// π Three rendering states per column:
|
|
478
|
+
// π 1. FLASH β headerFlashColumn matches β vivid inverse style (click feedback)
|
|
479
|
+
// π 2. ACTIVE β sortColumn matches β subtle bg + β/β arrow
|
|
480
|
+
// π 3. DEFAULT β normal dim text with highlighted first letter
|
|
481
|
+
|
|
482
|
+
// π Helper: pick the right style for a standard column header.
|
|
483
|
+
// π colKey = sort key (e.g. 'rank', 'swe'), label = plain text, width = column width.
|
|
484
|
+
const headerStyle = (colKey, label, width) => {
|
|
485
|
+
const arrowText = dir + ' ' + label
|
|
486
|
+
const flashText = arrowText.length <= width ? arrowText : label + dir
|
|
487
|
+
if (headerFlashColumn === colKey) return flashHeader(flashText, width)
|
|
488
|
+
if (sortColumn === colKey) return sortActiveHeader(label, width)
|
|
489
|
+
return colorFirst(label, width)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const moodH_c = (() => {
|
|
493
|
+
// π Tiny verdict indicator column: keep it emoji-only, no arrow, so it stays 2 cells wide.
|
|
494
|
+
const padded = padEndDisplay(moodLabel, W_MOOD)
|
|
495
|
+
if (headerFlashColumn === 'verdict') return chalk.bold.rgb(255, 255, 255).bgRgb(...currentPalette().accentStrong)(padded)
|
|
496
|
+
if (sortColumn === 'verdict') return chalk.bold.rgb(255, 255, 255).bgRgb(...currentPalette().cursor.defaultBg)(padded)
|
|
497
|
+
return themeColors.hotkey(padded)
|
|
498
|
+
})()
|
|
499
|
+
const rankH_c = headerStyle('rank', rankLabel, W_RANK)
|
|
500
|
+
const tierH_c = (() => {
|
|
501
|
+
if (headerFlashColumn === 'tier') return flashHeader(tierLabel, W_TIER)
|
|
502
|
+
return colorFirst(tierLabel, W_TIER)
|
|
503
|
+
})()
|
|
504
|
+
const modelH_c = headerStyle('model', modelLabel, W_MODEL)
|
|
505
|
+
const sweH_c = headerStyle('swe', sweLabel, W_SWE)
|
|
506
|
+
const ctxH_c = headerStyle('ctx', ctxLabel, W_CTX)
|
|
507
|
+
const pingH_c = headerStyle('ping', pingLabel, wPing)
|
|
508
|
+
const avgH_c = headerStyle('avg', avgLabel, wAvg)
|
|
509
|
+
const healthH_c = headerStyle('condition', healthLabel, wStatus)
|
|
510
|
+
const verdictH_c = headerStyle('verdict', verdictLabel, W_VERDICT)
|
|
511
|
+
const stabH_c = (() => {
|
|
512
|
+
if (headerFlashColumn === 'stability') {
|
|
513
|
+
const ft = (dir + ' ' + stabLabel).length <= wStab ? dir + ' ' + stabLabel : stabLabel + dir
|
|
514
|
+
return flashHeader(ft, wStab)
|
|
515
|
+
}
|
|
516
|
+
if (sortColumn === 'stability') return sortActiveHeader(stabLabel, wStab)
|
|
467
517
|
const plain = stabLabel
|
|
468
518
|
const padding = ' '.repeat(Math.max(0, wStab - plain.length))
|
|
469
519
|
return themeColors.dim('Sta') + themeColors.hotkey('B') + themeColors.dim((isCompact ? '.' : 'ility') + padding)
|
|
470
520
|
})()
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
521
|
+
const uptimeH_c = (() => {
|
|
522
|
+
if (headerFlashColumn === 'uptime') {
|
|
523
|
+
const ft = (dir + ' ' + uptimeLabel).length <= W_UPTIME ? dir + ' ' + uptimeLabel : uptimeLabel + dir
|
|
524
|
+
return flashHeader(ft, W_UPTIME)
|
|
525
|
+
}
|
|
526
|
+
if (sortColumn === 'uptime') return sortActiveHeader(uptimeLabel, W_UPTIME)
|
|
527
|
+
const padding = ' '.repeat(Math.max(0, W_UPTIME - uptimeLabel.length))
|
|
475
528
|
return themeColors.hotkey('U') + themeColors.dim('p%' + padding)
|
|
476
529
|
})()
|
|
530
|
+
const originH_c = (() => {
|
|
531
|
+
if (headerFlashColumn === 'origin') {
|
|
532
|
+
const ft = (dir + ' ' + originLabel).length <= wSource ? dir + ' ' + originLabel : originLabel + dir
|
|
533
|
+
return flashHeader(ft, wSource)
|
|
534
|
+
}
|
|
535
|
+
if (sortColumn === 'origin') return sortActiveHeader(originLabel, wSource)
|
|
536
|
+
if (originFilterMode > 0) return themeColors.accentBold(originLabel.padEnd(wSource))
|
|
537
|
+
const plain = isCompact ? 'PrODβ¦' : 'PrOviDer'
|
|
538
|
+
const padding = ' '.repeat(Math.max(0, wSource - plain.length))
|
|
539
|
+
if (isCompact) {
|
|
540
|
+
return themeColors.dim('Pr') + themeColors.hotkey('O') + themeColors.hotkey('D') + themeColors.dim('β¦' + padding)
|
|
541
|
+
}
|
|
542
|
+
return themeColors.dim('Pr') + themeColors.hotkey('O') + themeColors.dim('vi') + themeColors.hotkey('D') + themeColors.dim('er' + padding)
|
|
543
|
+
})()
|
|
477
544
|
|
|
478
|
-
// π
|
|
479
|
-
const
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
545
|
+
// π Benchmark headers β split the old combined AI Speed field into latency + throughput.
|
|
546
|
+
const aiLatencyLabel = isCompact ? 'AI Lat.' : 'AI Latency'
|
|
547
|
+
const aiLatencyH_c = (() => {
|
|
548
|
+
if (headerFlashColumn === 'aiLatency') {
|
|
549
|
+
const ft = (dir + ' ' + aiLatencyLabel).length <= wAiLatency ? dir + ' ' + aiLatencyLabel : aiLatencyLabel + dir
|
|
550
|
+
return flashHeader(ft, wAiLatency)
|
|
551
|
+
}
|
|
552
|
+
if (sortColumn === 'aiLatency') return sortActiveHeader(aiLatencyLabel, wAiLatency)
|
|
553
|
+
const plain = aiLatencyLabel
|
|
554
|
+
const padding = ' '.repeat(Math.max(0, wAiLatency - plain.length))
|
|
555
|
+
return themeColors.dim(plain + padding)
|
|
556
|
+
})()
|
|
557
|
+
const tpsH_c = (() => {
|
|
558
|
+
if (headerFlashColumn === 'tps') {
|
|
559
|
+
const ft = (dir + ' ' + 'TPS').length <= W_TPS ? dir + ' ' + 'TPS' : 'TPS' + dir
|
|
560
|
+
return flashHeader(ft, W_TPS)
|
|
561
|
+
}
|
|
562
|
+
if (sortColumn === 'tps') return sortActiveHeader('TPS', W_TPS)
|
|
563
|
+
const plain = 'TPS'
|
|
564
|
+
const padding = ' '.repeat(Math.max(0, W_TPS - plain.length))
|
|
483
565
|
return themeColors.dim(plain + padding)
|
|
484
566
|
})()
|
|
485
567
|
|
|
486
568
|
// π Usage column removed from UI β no header or separator for it.
|
|
487
569
|
// π Header row: conditionally include columns based on responsive visibility
|
|
488
|
-
const headerParts = []
|
|
570
|
+
const headerParts = [moodH_c]
|
|
489
571
|
if (showRank) headerParts.push(rankH_c)
|
|
490
572
|
if (showTier) headerParts.push(tierH_c)
|
|
491
573
|
headerParts.push(sweH_c, ctxH_c, modelH_c, originH_c, pingH_c, avgH_c, healthH_c, verdictH_c)
|
|
492
574
|
if (showStability) headerParts.push(stabH_c)
|
|
493
575
|
if (showUptime) headerParts.push(uptimeH_c)
|
|
494
|
-
if (
|
|
576
|
+
if (showBenchmarkColumns) headerParts.push(aiLatencyH_c, tpsH_c)
|
|
495
577
|
lines.push(' ' + headerParts.join(COL_SEP))
|
|
496
578
|
|
|
497
579
|
// π Mouse support: the column header row is the last line we just pushed.
|
|
@@ -702,52 +784,63 @@ export function renderTable({
|
|
|
702
784
|
|
|
703
785
|
// π Verdict column - use getVerdict() for stability-aware verdicts, then render with emoji
|
|
704
786
|
const verdict = getVerdict(r)
|
|
705
|
-
let verdictText, verdictColor
|
|
787
|
+
let verdictText, verdictIcon, verdictColor
|
|
706
788
|
// π Verdict colors follow the same greenβred gradient as TIER_COLOR / SWE%
|
|
707
789
|
switch (verdict) {
|
|
708
790
|
case 'Perfect':
|
|
709
|
-
|
|
791
|
+
verdictIcon = 'π©'
|
|
792
|
+
verdictText = `${verdictIcon} Perfect`
|
|
710
793
|
verdictColor = themeColors.successBold
|
|
711
794
|
break
|
|
712
795
|
case 'Normal':
|
|
713
|
-
|
|
796
|
+
verdictIcon = 'π’'
|
|
797
|
+
verdictText = `${verdictIcon} Normal`
|
|
714
798
|
verdictColor = themeColors.metricGood
|
|
715
799
|
break
|
|
716
800
|
case 'Spiky':
|
|
717
|
-
|
|
801
|
+
verdictIcon = 'π‘'
|
|
802
|
+
verdictText = `${verdictIcon} Spiky`
|
|
718
803
|
verdictColor = (text) => chalk.bold.rgb(...getTierRgb('A+'))(text)
|
|
719
804
|
break
|
|
720
805
|
case 'Slow':
|
|
721
|
-
|
|
806
|
+
verdictIcon = 'π '
|
|
807
|
+
verdictText = `${verdictIcon} Slow`
|
|
722
808
|
verdictColor = (text) => chalk.bold.rgb(...getTierRgb('A-'))(text)
|
|
723
809
|
break
|
|
724
810
|
case 'Very Slow':
|
|
725
|
-
|
|
811
|
+
verdictIcon = 'π΄'
|
|
812
|
+
verdictText = `${verdictIcon} Very Slow`
|
|
726
813
|
verdictColor = (text) => chalk.bold.rgb(...getTierRgb('B+'))(text)
|
|
727
814
|
break
|
|
728
815
|
case 'Overloaded':
|
|
729
|
-
|
|
816
|
+
verdictIcon = 'π₯'
|
|
817
|
+
verdictText = `${verdictIcon} Overloaded`
|
|
730
818
|
verdictColor = (text) => chalk.bold.rgb(...getTierRgb('B'))(text)
|
|
731
819
|
break
|
|
732
820
|
case 'Unstable':
|
|
733
|
-
|
|
821
|
+
verdictIcon = 'β οΈ'
|
|
822
|
+
verdictText = `${verdictIcon} Unstable`
|
|
734
823
|
verdictColor = themeColors.errorBold
|
|
735
824
|
break
|
|
736
825
|
case 'Not Active':
|
|
737
|
-
|
|
826
|
+
verdictIcon = 'β«'
|
|
827
|
+
verdictText = `${verdictIcon} Not Active`
|
|
738
828
|
verdictColor = themeColors.dim
|
|
739
829
|
break
|
|
740
830
|
case 'Pending':
|
|
741
|
-
|
|
831
|
+
verdictIcon = 'β³'
|
|
832
|
+
verdictText = `${verdictIcon} Pending`
|
|
742
833
|
verdictColor = themeColors.dim
|
|
743
834
|
break
|
|
744
835
|
default:
|
|
745
|
-
|
|
836
|
+
verdictIcon = 'π'
|
|
837
|
+
verdictText = `${verdictIcon} Unusable`
|
|
746
838
|
verdictColor = (text) => chalk.bold.rgb(...getTierRgb('C'))(text)
|
|
747
839
|
break
|
|
748
840
|
}
|
|
749
841
|
// π Use padEndDisplay to account for emoji display width (2 cols each) so all rows align
|
|
750
842
|
const speedCell = verdictColor(padEndDisplay(verdictText, W_VERDICT))
|
|
843
|
+
const moodCell = padEndDisplay(verdictIcon, W_MOOD)
|
|
751
844
|
|
|
752
845
|
// π Stability column - composite score (0β100) from p95 + jitter + spikes + uptime
|
|
753
846
|
// π Left-aligned to sit flush under the column header
|
|
@@ -793,33 +886,61 @@ export function renderTable({
|
|
|
793
886
|
// (We keep the logic but do not render it.)
|
|
794
887
|
const usageCell = ''
|
|
795
888
|
|
|
796
|
-
// π
|
|
889
|
+
// π AI Latency + TPS columns β same benchmark result, split into two readable metrics.
|
|
890
|
+
// π Benchmark results are shown regardless of health status (up/timeout/down/429/noauth).
|
|
891
|
+
// π If benchmark failed β red dash. Error details live in the Health column.
|
|
892
|
+
// π If no benchmark has been run yet, show dim dash.
|
|
893
|
+
// π Retry badge (β»N) is colored blue and spaced from the main value.
|
|
797
894
|
const benchmarkKey = `${r.providerKey}/${r.modelId}`
|
|
798
895
|
const benchmarkResult = benchmarkResults[benchmarkKey]
|
|
799
896
|
const isBenchmarkRunning = benchmarkRunning.has(benchmarkKey)
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
897
|
+
const hasBenchmark = benchmarkResult || isBenchmarkRunning
|
|
898
|
+
const benchmarkOk = benchmarkResult && benchmarkResult.ok
|
|
899
|
+
|
|
900
|
+
// π Build latency cell: value + blue retry badge
|
|
901
|
+
const latParsed = isBenchmarkRunning
|
|
902
|
+
? formatBenchmarkLatency(benchmarkResult, { running: true, frame })
|
|
903
|
+
: benchmarkOk
|
|
904
|
+
? formatBenchmarkLatency(benchmarkResult)
|
|
905
|
+
: { text: 'β', retryBadge: '' }
|
|
906
|
+
const latValue = benchmarkOk
|
|
907
|
+
? themeColors.metricGood(latParsed.text)
|
|
908
|
+
: hasBenchmark
|
|
909
|
+
? themeColors.metricBad(latParsed.text)
|
|
910
|
+
: themeColors.dim(latParsed.text)
|
|
911
|
+
const latBadge = latParsed.retryBadge
|
|
912
|
+
? themeColors.info(' ' + latParsed.retryBadge)
|
|
913
|
+
: ''
|
|
914
|
+
const latBadgeWidth = latParsed.retryBadge ? displayWidth(' ' + latParsed.retryBadge) : 0
|
|
915
|
+
const latPad = wAiLatency - displayWidth(latParsed.text) - latBadgeWidth
|
|
916
|
+
const latencyCell = latValue + latBadge + themeColors.dim(''.padEnd(Math.max(0, latPad)))
|
|
917
|
+
|
|
918
|
+
// π Build TPS cell: value + blue retry badge
|
|
919
|
+
const tpsParsed = isBenchmarkRunning
|
|
920
|
+
? formatBenchmarkTps(benchmarkResult, { running: true, frame })
|
|
921
|
+
: benchmarkOk
|
|
922
|
+
? formatBenchmarkTps(benchmarkResult)
|
|
923
|
+
: { text: 'β', retryBadge: '' }
|
|
924
|
+
const tpsValue = benchmarkOk || isBenchmarkRunning
|
|
925
|
+
? themeColors.metricGood(tpsParsed.text)
|
|
926
|
+
: hasBenchmark
|
|
927
|
+
? themeColors.metricBad(tpsParsed.text)
|
|
928
|
+
: themeColors.dim(tpsParsed.text)
|
|
929
|
+
const tpsBadge = tpsParsed.retryBadge
|
|
930
|
+
? themeColors.info(' ' + tpsParsed.retryBadge)
|
|
931
|
+
: ''
|
|
932
|
+
const tpsBadgeWidth = tpsParsed.retryBadge ? displayWidth(' ' + tpsParsed.retryBadge) : 0
|
|
933
|
+
const tpsPad = W_TPS - displayWidth(tpsParsed.text) - tpsBadgeWidth
|
|
934
|
+
const tpsCell = tpsValue + tpsBadge + themeColors.dim(''.padEnd(Math.max(0, tpsPad)))
|
|
814
935
|
|
|
815
936
|
// π Build row: conditionally include columns based on responsive visibility
|
|
816
|
-
const rowParts = []
|
|
937
|
+
const rowParts = [moodCell]
|
|
817
938
|
if (showRank) rowParts.push(num)
|
|
818
939
|
if (showTier) rowParts.push(tier)
|
|
819
940
|
rowParts.push(sweCell, ctxCell, nameCell, sourceCell, pingCell, avgCell, status, speedCell)
|
|
820
941
|
if (showStability) rowParts.push(stabCell)
|
|
821
942
|
if (showUptime) rowParts.push(uptimeCell)
|
|
822
|
-
if (
|
|
943
|
+
if (showBenchmarkColumns) rowParts.push(latencyCell, tpsCell)
|
|
823
944
|
const row = ' ' + rowParts.join(COL_SEP)
|
|
824
945
|
|
|
825
946
|
if (isCursor) {
|
package/src/router-daemon.js
CHANGED
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
normalizeRouterConfig,
|
|
48
48
|
saveConfig,
|
|
49
49
|
} from './config.js'
|
|
50
|
-
import { resolveCloudflareUrl } from './ping.js'
|
|
50
|
+
import { buildChatCompletionPingBody, resolveCloudflareUrl, shouldUseDisabledThinkingForProvider } from './ping.js'
|
|
51
51
|
import { sendUsageTelemetry } from './telemetry.js'
|
|
52
52
|
|
|
53
53
|
export const ROUTER_DEFAULT_PORT = 19280
|
|
@@ -1200,12 +1200,11 @@ class RouterRuntime {
|
|
|
1200
1200
|
: await fetch(providerUrl, {
|
|
1201
1201
|
method: 'POST',
|
|
1202
1202
|
headers: cloneHeadersForUpstream({}, apiKey, candidate.provider),
|
|
1203
|
-
body: JSON.stringify(
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
}),
|
|
1203
|
+
body: JSON.stringify(buildChatCompletionPingBody(
|
|
1204
|
+
getApiModelId(candidate.provider, candidate.model),
|
|
1205
|
+
{ stream: false },
|
|
1206
|
+
{ disableThinking: shouldUseDisabledThinkingForProvider(candidate.provider) }
|
|
1207
|
+
)),
|
|
1209
1208
|
signal: controller.signal,
|
|
1210
1209
|
})
|
|
1211
1210
|
const latencyMs = Math.round(performance.now() - started)
|
package/src/setup.js
CHANGED
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
* 1. Builds a `providers` list from `Object.keys(sources)` so new providers added to
|
|
17
17
|
* sources.js automatically appear in the wizard without any code changes here.
|
|
18
18
|
* 2. Uses `readline.createInterface` for line-at-a-time input (not raw mode).
|
|
19
|
-
* 3.
|
|
20
|
-
* 4.
|
|
19
|
+
* 3. Asks whether the opt-in startup AI Speed Test should run on every launch.
|
|
20
|
+
* 4. Calls `saveConfig(config)` once after collecting all answers.
|
|
21
|
+
* 5. Returns the nvidia key (or the first entered key) for backward-compatibility with
|
|
21
22
|
* the `main()` caller that originally checked for `nvidiKey !== null` before continuing.
|
|
22
23
|
*
|
|
23
24
|
* @functions
|
|
@@ -86,18 +87,27 @@ export async function promptApiKey(config) {
|
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
// π Check at least one key was entered
|
|
90
|
+
// π Check at least one key was entered before asking optional behavior questions.
|
|
92
91
|
const anyKey = Object.values(config.apiKeys).some(v => v)
|
|
93
92
|
if (!anyKey) {
|
|
93
|
+
rl.close()
|
|
94
94
|
return null
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
console.log(chalk.bold(' β‘ Startup AI Speed Scan'))
|
|
98
|
+
console.log(chalk.dim(' FCM can automatically run the Ctrl+U benchmark after launch to fill AI Latency + TPS.'))
|
|
99
|
+
console.log(chalk.dim(' This uses real provider requests, so it is opt-in and can be changed later in Settings.'))
|
|
100
|
+
const autoBenchmarkAnswer = await ask(chalk.dim(' Run the AI Speed Scan automatically on every launch? (y/N): '))
|
|
101
|
+
if (!config.settings || typeof config.settings !== 'object') config.settings = {}
|
|
102
|
+
config.settings.runAiSpeedTestOnStartup = ['y', 'yes', 'oui', 'o'].includes(autoBenchmarkAnswer.toLowerCase())
|
|
103
|
+
console.log()
|
|
104
|
+
|
|
105
|
+
rl.close()
|
|
106
|
+
|
|
97
107
|
saveConfig(config)
|
|
98
108
|
const savedCount = Object.values(config.apiKeys).filter(v => v).length
|
|
99
109
|
console.log(chalk.green(` β
${savedCount} key(s) saved to ~/.free-coding-models.json`))
|
|
100
|
-
console.log(chalk.dim(' You can add
|
|
110
|
+
console.log(chalk.dim(' You can add/change keys and toggle Startup AI Speed Scan anytime with the ') + chalk.yellow('P') + chalk.dim(' key in the TUI.'))
|
|
101
111
|
console.log()
|
|
102
112
|
|
|
103
113
|
// π Return nvidia key for backward-compat (main() checks it exists before continuing)
|
package/src/theme.js
CHANGED
package/src/tui-state.js
CHANGED
|
@@ -273,5 +273,10 @@ export function createTuiState({
|
|
|
273
273
|
globalBenchmarkRunning: false,
|
|
274
274
|
globalBenchmarkTotal: 0,
|
|
275
275
|
globalBenchmarkCompleted: 0,
|
|
276
|
+
|
|
277
|
+
// π Header click flash animation: briefly highlights the clicked column header
|
|
278
|
+
// π with an inverse/bright style for ~250ms (3 frames at 12 FPS).
|
|
279
|
+
headerFlashColumn: null, // π Column name being flashed (null = no flash active)
|
|
280
|
+
headerFlashUntilFrame: 0, // π Frame number when flash expires
|
|
276
281
|
}
|
|
277
282
|
}
|