free-coding-models 0.5.28 → 0.5.29
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/bin/free-coding-models.js +10 -2
- package/changelog/v0.5.29.md +19 -0
- package/package.json +1 -1
- package/sources.js +3 -2
- package/src/core/config.js +1 -14
- package/src/core/endpoint-installer.js +5 -9
- package/src/core/legacy-proxy-cleanup.js +2 -0
- package/src/core/model-merger.js +4 -13
- package/src/core/router-daemon.js +76 -41
- package/src/core/router-dashboard.js +63 -25
- package/src/core/shared-helpers.js +117 -0
- package/src/core/sync-set.js +6 -21
- package/src/core/tool-launchers.js +24 -92
- package/src/core/utils.js +26 -100
- package/src/tui/app.js +2 -2
- package/src/tui/command-palette.js +1 -0
- package/src/tui/key-handler.js +77 -20
- package/src/tui/render-table.js +3 -3
- package/web/dist/assets/index-BMU58Jju.js +41 -0
- package/web/dist/assets/{index-BRFowJv5.css → index-Dd1jOGEn.css} +1 -1
- package/web/dist/index.html +2 -2
- package/web/src/components/router/RouterView.jsx +124 -56
- package/web/src/components/router/RouterView.module.css +135 -1
- package/src/core/product-flags.js +0 -9
- package/web/dist/assets/index-Pr3waI0-.js +0 -41
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file shared-helpers.js
|
|
3
|
+
* @description Shared utility functions used across multiple modules.
|
|
4
|
+
*
|
|
5
|
+
* @details
|
|
6
|
+
* 📖 DRY helpers extracted from router-daemon.js, tool-launchers.js,
|
|
7
|
+
* 📖 endpoint-installer.js, and legacy-proxy-cleanup.js to eliminate
|
|
8
|
+
* duplicate implementations of the same patterns.
|
|
9
|
+
*
|
|
10
|
+
* @functions
|
|
11
|
+
* → sleep — Promise-based setTimeout
|
|
12
|
+
* → ensureDir — Create parent directory if missing
|
|
13
|
+
* → readJson — Read and parse JSON file with fallback
|
|
14
|
+
* → writeJson — Write JSON file with directory creation
|
|
15
|
+
* → atomicWriteJson — Atomic write via temp file + rename
|
|
16
|
+
* → safeJsonParse — JSON.parse with fallback
|
|
17
|
+
* → maskApiKey — Mask API key for display (show last 4 chars)
|
|
18
|
+
*
|
|
19
|
+
* @exports sleep, ensureDir, readJson, writeJson, atomicWriteJson, safeJsonParse, maskApiKey
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
|
|
23
|
+
import { dirname } from 'node:path'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 📖 Promise-based sleep. Used by daemon probe staggering, TUI animations, etc.
|
|
27
|
+
* @param {number} ms
|
|
28
|
+
* @returns {Promise<void>}
|
|
29
|
+
*/
|
|
30
|
+
export function sleep(ms) {
|
|
31
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 📖 Create parent directory of `filePath` if it doesn't exist.
|
|
36
|
+
* @param {string} filePath
|
|
37
|
+
*/
|
|
38
|
+
export function ensureDir(filePath) {
|
|
39
|
+
const dir = dirname(filePath)
|
|
40
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 📖 Read and parse a JSON file. Returns `fallback` on any error.
|
|
45
|
+
* @param {string} filePath
|
|
46
|
+
* @param {*} [fallback=null]
|
|
47
|
+
* @returns {*}
|
|
48
|
+
*/
|
|
49
|
+
export function readJson(filePath, fallback = null) {
|
|
50
|
+
if (!existsSync(filePath)) return fallback
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(readFileSync(filePath, 'utf8'))
|
|
53
|
+
} catch {
|
|
54
|
+
return fallback
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 📖 Write JSON to file, creating parent directories as needed.
|
|
60
|
+
* @param {string} filePath
|
|
61
|
+
* @param {*} value
|
|
62
|
+
* @param {object} [options]
|
|
63
|
+
* @param {boolean} [options.backup=false] — Not implemented here; callers handle it
|
|
64
|
+
*/
|
|
65
|
+
export function writeJson(filePath, value) {
|
|
66
|
+
ensureDir(filePath)
|
|
67
|
+
writeFileSync(filePath, JSON.stringify(value, null, 2))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 📖 Atomic JSON write: writes to a temp file, then renames over the target.
|
|
72
|
+
* Prevents partial writes from corrupting the file on crash.
|
|
73
|
+
* @param {string} path
|
|
74
|
+
* @param {*} data
|
|
75
|
+
* @param {number} [mode=0o600]
|
|
76
|
+
*/
|
|
77
|
+
export function atomicWriteJson(path, data, mode = 0o600) {
|
|
78
|
+
const tempPath = `${path}.tmp-${process.pid}-${Date.now()}`
|
|
79
|
+
writeFileSync(tempPath, JSON.stringify(data, null, 2), { mode })
|
|
80
|
+
renameSync(tempPath, path)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 📖 JSON.parse with fallback. Returns `fallback` on parse failure.
|
|
85
|
+
* @param {string} raw
|
|
86
|
+
* @param {*} [fallback=null]
|
|
87
|
+
* @returns {*}
|
|
88
|
+
*/
|
|
89
|
+
export function safeJsonParse(raw, fallback = null) {
|
|
90
|
+
try {
|
|
91
|
+
return JSON.parse(raw)
|
|
92
|
+
} catch {
|
|
93
|
+
return fallback
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 📖 Mask an API key for display. Shows last 4 chars, rest as bullets.
|
|
99
|
+
* @param {string} key
|
|
100
|
+
* @returns {string}
|
|
101
|
+
*/
|
|
102
|
+
/**
|
|
103
|
+
* 📖 Check if a provider supports routing (has chat/completions URL, not CLI-only).
|
|
104
|
+
* @param {string} providerKey
|
|
105
|
+
* @param {Record<string, {url?: string, cliOnly?: boolean}>} sources — provider catalog
|
|
106
|
+
* @returns {boolean}
|
|
107
|
+
*/
|
|
108
|
+
export function isRouteableProvider(providerKey, sources) {
|
|
109
|
+
const source = sources[providerKey]
|
|
110
|
+
return Boolean(source?.url && !source.cliOnly && source.url.includes('/chat/completions'))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function maskApiKey(key) {
|
|
114
|
+
if (!key || typeof key !== 'string') return ''
|
|
115
|
+
if (key.length <= 8) return '••••••••'
|
|
116
|
+
return '••••••••' + key.slice(-4)
|
|
117
|
+
}
|
package/src/core/sync-set.js
CHANGED
|
@@ -39,9 +39,8 @@ import {
|
|
|
39
39
|
import { resolveCloudflareUrl } from './ping.js'
|
|
40
40
|
import { ROUTER_PID_PATH } from './router-daemon.js'
|
|
41
41
|
import { existsSync, readFileSync } from 'node:fs'
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const TIER_ORDER = ['S+', 'S', 'A+', 'A', 'A-', 'B+', 'B', 'C']
|
|
42
|
+
import { TIER_ORDER, parseSweToNum } from './utils.js'
|
|
43
|
+
import { isRouteableProvider } from './shared-helpers.js'
|
|
45
44
|
|
|
46
45
|
// 📖 Numeric value per tier for composite scoring.
|
|
47
46
|
const TIER_SCORES = {
|
|
@@ -69,14 +68,7 @@ const OPENROUTER_FREE_MODEL_IDS = new Set([
|
|
|
69
68
|
'openrouter/owl-alpha',
|
|
70
69
|
])
|
|
71
70
|
|
|
72
|
-
|
|
73
|
-
* Check whether a provider's catalog entry supports routing (has a
|
|
74
|
-
* chat/completions URL and is not CLI-only).
|
|
75
|
-
*/
|
|
76
|
-
function isRouteableProvider(providerKey) {
|
|
77
|
-
const source = sources[providerKey]
|
|
78
|
-
return Boolean(source?.url && !source.cliOnly && source.url.includes('/chat/completions'))
|
|
79
|
-
}
|
|
71
|
+
// 📖 isRouteableProvider imported from shared-helpers.js (needs `sources` param)
|
|
80
72
|
|
|
81
73
|
/**
|
|
82
74
|
* Resolve the upstream URL for a provider, handling Cloudflare template substitution.
|
|
@@ -98,14 +90,7 @@ function isOpenRouterFreeModelId(modelId) {
|
|
|
98
90
|
return String(modelId).endsWith(':free') || OPENROUTER_FREE_MODEL_IDS.has(String(modelId))
|
|
99
91
|
}
|
|
100
92
|
|
|
101
|
-
|
|
102
|
-
* Parse a SWE-bench percentage string like "49.2%" to a number.
|
|
103
|
-
*/
|
|
104
|
-
function parseSwePercent(value) {
|
|
105
|
-
if (typeof value !== 'string') return 0
|
|
106
|
-
const numeric = parseFloat(value.replace('%', '').trim())
|
|
107
|
-
return Number.isFinite(numeric) ? numeric : 0
|
|
108
|
-
}
|
|
93
|
+
// 📖 parseSwePercent replaced by shared parseSweToNum (same logic)
|
|
109
94
|
|
|
110
95
|
/**
|
|
111
96
|
* Score a candidate model for ranking. Higher is better.
|
|
@@ -156,12 +141,12 @@ export function buildSyncCandidates(apiKeys, options = {}) {
|
|
|
156
141
|
|
|
157
142
|
for (const [providerKey, sourceData] of Object.entries(sources)) {
|
|
158
143
|
if (!apiKeys[providerKey]) continue
|
|
159
|
-
if (!isRouteableProvider(providerKey)) continue
|
|
144
|
+
if (!isRouteableProvider(providerKey, sources)) continue
|
|
160
145
|
|
|
161
146
|
for (const tuple of sourceData.models || []) {
|
|
162
147
|
const [modelId, label = '', tier = '', swe = '0%'] = tuple
|
|
163
148
|
if (typeof modelId !== 'string' || !modelId.trim()) continue
|
|
164
|
-
const swePercent =
|
|
149
|
+
const swePercent = parseSweToNum(swe)
|
|
165
150
|
if (shouldSkipModel(providerKey, modelId, tier, swePercent, options)) continue
|
|
166
151
|
const score = scoreCandidate(providerKey, modelId, label, tier, swePercent)
|
|
167
152
|
candidates.push({
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
import chalk from 'chalk'
|
|
40
40
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from 'fs'
|
|
41
41
|
import { homedir } from 'os'
|
|
42
|
-
import {
|
|
42
|
+
import { join } from 'path'
|
|
43
43
|
import { spawn, spawnSync } from 'child_process'
|
|
44
44
|
import { sources } from '../../sources.js'
|
|
45
45
|
import { PROVIDER_COLOR } from '../tui/render-table.js'
|
|
@@ -48,6 +48,7 @@ import { ENV_VAR_NAMES, isWindows } from './provider-metadata.js'
|
|
|
48
48
|
import { getToolMeta, TOOL_METADATA } from './tool-metadata.js'
|
|
49
49
|
import { PROVIDER_METADATA } from './provider-metadata.js'
|
|
50
50
|
import { resolveToolBinaryPath } from './tool-bootstrap.js'
|
|
51
|
+
import { ensureDir, readJson, writeJson } from './shared-helpers.js'
|
|
51
52
|
|
|
52
53
|
const OPENAI_COMPAT_ENV_KEYS = [
|
|
53
54
|
'OPENAI_API_KEY',
|
|
@@ -60,11 +61,6 @@ const OPENAI_COMPAT_ENV_KEYS = [
|
|
|
60
61
|
]
|
|
61
62
|
const SANITIZED_TOOL_ENV_KEYS = [...OPENAI_COMPAT_ENV_KEYS]
|
|
62
63
|
|
|
63
|
-
function ensureDir(filePath) {
|
|
64
|
-
const dir = dirname(filePath)
|
|
65
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
66
|
-
}
|
|
67
|
-
|
|
68
64
|
// 📖 Parse a context window string (e.g. "128k", "1M", "32k") to token count number.
|
|
69
65
|
function parseCtxToTokens(ctx) {
|
|
70
66
|
if (!ctx || typeof ctx !== 'string') return null
|
|
@@ -104,19 +100,7 @@ function backupIfExists(filePath) {
|
|
|
104
100
|
return backupPath
|
|
105
101
|
}
|
|
106
102
|
|
|
107
|
-
|
|
108
|
-
if (!existsSync(filePath)) return fallback
|
|
109
|
-
try {
|
|
110
|
-
return JSON.parse(readFileSync(filePath, 'utf8'))
|
|
111
|
-
} catch {
|
|
112
|
-
return fallback
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function writeJson(filePath, value) {
|
|
117
|
-
ensureDir(filePath)
|
|
118
|
-
writeFileSync(filePath, JSON.stringify(value, null, 2))
|
|
119
|
-
}
|
|
103
|
+
// 📖 readJson/writeJson imported from shared-helpers.js
|
|
120
104
|
|
|
121
105
|
function getProviderBaseUrl(providerKey) {
|
|
122
106
|
const url = sources[providerKey]?.url
|
|
@@ -966,55 +950,8 @@ export async function startExternalTool(mode, model, config) {
|
|
|
966
950
|
console.log(chalk.cyan(` ▶ Launching ${meta.label} with ${chalk.bold(model.label)}...`))
|
|
967
951
|
printConfigArtifacts(meta.label, launchPlan.configArtifacts)
|
|
968
952
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
if (mode === 'crush') {
|
|
974
|
-
console.log(chalk.dim(' 📖 Crush will use the provider directly for this launch.'))
|
|
975
|
-
return spawnCommand(resolveLaunchCommand(mode, launchPlan.command), launchPlan.args, launchPlan.env)
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
if (mode === 'goose') {
|
|
979
|
-
return spawnCommand(resolveLaunchCommand(mode, launchPlan.command), launchPlan.args, launchPlan.env)
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
if (mode === 'qwen') {
|
|
983
|
-
return spawnCommand(resolveLaunchCommand(mode, launchPlan.command), launchPlan.args, launchPlan.env)
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
if (mode === 'openhands') {
|
|
987
|
-
console.log(chalk.dim(` 📖 OpenHands launched with model: ${model.modelId}`))
|
|
988
|
-
return spawnCommand(resolveLaunchCommand(mode, launchPlan.command), launchPlan.args, launchPlan.env)
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
if (mode === 'amp') {
|
|
992
|
-
console.log(chalk.dim(` 📖 Amp config updated with model: ${model.modelId}`))
|
|
993
|
-
return spawnCommand(resolveLaunchCommand(mode, launchPlan.command), launchPlan.args, launchPlan.env)
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
if (mode === 'pi') {
|
|
997
|
-
// 📖 Pi supports --provider and --model flags for guaranteed auto-selection
|
|
998
|
-
return spawnCommand(resolveLaunchCommand(mode, launchPlan.command), launchPlan.args, launchPlan.env)
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
if (mode === 'hermes') {
|
|
1002
|
-
// 📖 Restart the Hermes gateway so the new model config takes effect immediately
|
|
1003
|
-
restartHermesGateway()
|
|
1004
|
-
console.log(chalk.dim(` 📖 Hermes Agent configured with model: ${model.modelId}`))
|
|
1005
|
-
return spawnCommand(resolveLaunchCommand(mode, launchPlan.command), launchPlan.args, launchPlan.env)
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
if (mode === 'continue') {
|
|
1009
|
-
console.log(chalk.dim(` 📖 Continue CLI configured with model: ${model.modelId}`))
|
|
1010
|
-
return spawnCommand(resolveLaunchCommand(mode, launchPlan.command), launchPlan.args, launchPlan.env)
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
if (mode === 'cline') {
|
|
1014
|
-
console.log(chalk.dim(` 📖 Cline configured with model: ${model.modelId}`))
|
|
1015
|
-
return spawnCommand(resolveLaunchCommand(mode, launchPlan.command), launchPlan.args, launchPlan.env)
|
|
1016
|
-
}
|
|
1017
|
-
|
|
953
|
+
// 📖 Pre-launch hooks for tools that need special treatment
|
|
954
|
+
if (mode === 'hermes') restartHermesGateway()
|
|
1018
955
|
if (mode === 'xcode') {
|
|
1019
956
|
const xcodeUrl = launchPlan.baseUrl ? launchPlan.baseUrl.replace(/\/v1$/, '').replace(/\/v1\/chat\/completions$/, '') : ''
|
|
1020
957
|
console.log(chalk.bold.cyan('\n 🛠️ Xcode Intelligence Setup Instructions:'))
|
|
@@ -1027,29 +964,24 @@ export async function startExternalTool(mode, model, config) {
|
|
|
1027
964
|
console.log(chalk.dim(' Description: ') + chalk.green(`FCM - ${sources[model.providerKey]?.name || model.providerKey}`))
|
|
1028
965
|
console.log(chalk.white(` 4. Click Add, then select `) + chalk.bold(model.modelId) + chalk.white(` from the list.\n`))
|
|
1029
966
|
console.log(chalk.dim(` 📖 Attempting to launch Xcode...`))
|
|
1030
|
-
return spawnCommand(launchPlan.command, launchPlan.args, launchPlan.env)
|
|
1031
967
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
console.log(chalk.red(` X Unsupported external tool mode: ${mode}`))
|
|
1054
|
-
return 1
|
|
968
|
+
if (mode === 'crush') console.log(chalk.dim(' 📖 Crush will use the provider directly for this launch.'))
|
|
969
|
+
|
|
970
|
+
// 📖 Tool-specific info messages (only for modes that have no prepare-step message)
|
|
971
|
+
const infoMessages = {
|
|
972
|
+
openhands: ` 📖 OpenHands launched with model: ${model.modelId}`,
|
|
973
|
+
amp: ` 📖 Amp config updated with model: ${model.modelId}`,
|
|
974
|
+
hermes: ` 📖 Hermes Agent configured with model: ${model.modelId}`,
|
|
975
|
+
continue: ` 📖 Continue CLI configured with model: ${model.modelId}`,
|
|
976
|
+
cline: ` 📖 Cline configured with model: ${model.modelId}`,
|
|
977
|
+
caveman: ' 📖 Launching Caveman Code...',
|
|
978
|
+
jcode: ' 📖 Launching jcode...',
|
|
979
|
+
copilot: ` 📖 Copilot CLI configured with model: ${model.modelId}`,
|
|
980
|
+
forgecode: ` 📖 ForgeCode configured with model: ${model.modelId}`,
|
|
981
|
+
}
|
|
982
|
+
if (infoMessages[mode]) console.log(chalk.dim(infoMessages[mode]))
|
|
983
|
+
|
|
984
|
+
// 📖 xcode uses raw command ("open"), everything else resolves via tool-bootstrap
|
|
985
|
+
const command = mode === 'xcode' ? launchPlan.command : resolveLaunchCommand(mode, launchPlan.command)
|
|
986
|
+
return spawnCommand(command, launchPlan.args, launchPlan.env)
|
|
1055
987
|
}
|
package/src/core/utils.js
CHANGED
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
* @exports sortResults, filterByTier, findBestModel, parseArgs
|
|
42
42
|
* @exports scoreModelForTask, getTopRecommendations
|
|
43
43
|
* @exports TIER_ORDER, VERDICT_ORDER, TIER_LETTER_MAP, TASK_TYPES, PRIORITY_TYPES, CONTEXT_BUDGETS
|
|
44
|
+
* @exports parseCtxToK, parseSweToNum, formatCtxWindow, labelFromId, NEW_MODELS, getVersionStatusInfo, formatResultsAsJSON
|
|
44
45
|
*
|
|
45
46
|
* @see bin/free-coding-models.js — main CLI that imports these utils
|
|
46
47
|
* @see sources.js — model definitions consumed by these functions
|
|
@@ -115,80 +116,27 @@ export const getAvg = (r) => {
|
|
|
115
116
|
//
|
|
116
117
|
// 📖 The "wasUpBefore" check is key — it distinguishes between a model that's
|
|
117
118
|
// temporarily flaky vs one that was never reachable in the first place.
|
|
119
|
+
// 📖 NEW_MODEL_DURATION_MS — how long a model shows the 🆕 badge after being added.
|
|
120
|
+
// 📖 5 days in milliseconds.
|
|
121
|
+
export const NEW_MODEL_DURATION_MS = 5 * 24 * 60 * 60 * 1000
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 📖 Check if a model should display the 🆕 badge.
|
|
125
|
+
* 📖 A model is "new" if its addedDate is within the last 5 days.
|
|
126
|
+
* 📖 `addedDate` comes from sources.js as the optional 6th element of model tuples.
|
|
127
|
+
* @param {string|null|undefined} addedDate — ISO date string (e.g. '2026-06-10')
|
|
128
|
+
* @returns {boolean}
|
|
129
|
+
*/
|
|
130
|
+
export function isNewModel(addedDate) {
|
|
131
|
+
if (!addedDate || typeof addedDate !== 'string') return false
|
|
132
|
+
const added = Date.parse(addedDate)
|
|
133
|
+
if (!Number.isFinite(added)) return false
|
|
134
|
+
return (Date.now() - added) < NEW_MODEL_DURATION_MS
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 📖 NEW_MODELS kept for backward compat with web/website surfaces that
|
|
138
|
+
// 📖 don't have addedDate. Will be removed once all surfaces migrate.
|
|
118
139
|
export const NEW_MODELS = new Set([
|
|
119
|
-
'nvidia/nemotron-3-ultra-550b-a55b',
|
|
120
|
-
'@cf/meta/llama-3.2-90b-instruct',
|
|
121
|
-
'@cf/mistralai/mistral-7b-instruct-v0.2',
|
|
122
|
-
'@cf/google/gemma-2-9b-it',
|
|
123
|
-
'@cf/anthropic/claude-3-5-sonnet',
|
|
124
|
-
'@cf/openai/gpt-4o-mini',
|
|
125
|
-
'@cf/qwen/qwen3-30b-a3b-fp8',
|
|
126
|
-
'@cf/qwen/qwen2.5-coder-32b-instruct',
|
|
127
|
-
'@cf/meta/llama-3.3-70b-instruct-fp8-fast',
|
|
128
|
-
'@cf/google/gemma-4-26b-a4b-it',
|
|
129
|
-
'@cf/mistralai/mistral-small-3.1-24b-instruct',
|
|
130
|
-
'@cf/ibm/granite-4.0-h-micro',
|
|
131
|
-
'minimaxai/minimax-m2.7',
|
|
132
|
-
'z-ai/glm-5.1',
|
|
133
|
-
'moonshotai/kimi-k2.6',
|
|
134
|
-
'stepfun-ai/step-3.5-flash',
|
|
135
|
-
'stepfun-ai/step-3.7-flash',
|
|
136
|
-
'qwen/qwen3-coder-480b-a35b-instruct',
|
|
137
|
-
'qwen/qwen3.5-397b-a17b',
|
|
138
|
-
'meta/llama-4-maverick-17b-128e-instruct',
|
|
139
|
-
'mistralai/mistral-medium-3.5-128b',
|
|
140
|
-
'mistralai/mistral-small-4-119b-2603',
|
|
141
|
-
'qwen/qwen3.5-122b-a10b',
|
|
142
|
-
'mistralai/mistral-large-3-675b-instruct-2512',
|
|
143
|
-
'nvidia/nemotron-3-super-120b-a12b',
|
|
144
|
-
'nvidia/nemotron-3-nano-omni-30b-a3b-reasoning',
|
|
145
|
-
'google/gemma-4-31b-it',
|
|
146
|
-
'bytedance/seed-oss-36b-instruct',
|
|
147
|
-
'stockmark/stockmark-2-100b-instruct',
|
|
148
|
-
'mistralai/ministral-14b-instruct-2512',
|
|
149
|
-
'meta/llama-3.2-11b-vision-instruct',
|
|
150
|
-
'microsoft/phi-4-mini-instruct',
|
|
151
|
-
'gemma-3-12b-it',
|
|
152
|
-
'nvidia/nemotron-nano-9b-v2',
|
|
153
|
-
'openrouter/owl-alpha',
|
|
154
|
-
'nousresearch/hermes-3-llama-3.1-405b:free',
|
|
155
|
-
'nvidia/nemotron-nano-30b-a3b:free',
|
|
156
|
-
'cognitivecomputations/dolphin-mistral-24b-venice-edition:free',
|
|
157
|
-
'meta-llama/llama-3.3-70b-instruct:free',
|
|
158
|
-
'meta-llama/llama-3.2-3b-instruct:free',
|
|
159
|
-
'liquid/lfm-2.5-1.2b-instruct:free',
|
|
160
|
-
'liquid/lfm-2.5-1.2b-thinking:free',
|
|
161
|
-
'qwen3.7-max',
|
|
162
|
-
'qwen3-max',
|
|
163
|
-
'qwen3.6-plus',
|
|
164
|
-
'qwen3-235b-a22b',
|
|
165
|
-
'qwen3.5-plus',
|
|
166
|
-
'qwen3-coder-plus',
|
|
167
|
-
'qwen3-coder-next',
|
|
168
|
-
'qwen3.6-flash',
|
|
169
|
-
'qwen3.5-flash',
|
|
170
|
-
'qwen3-coder-flash',
|
|
171
|
-
'qwen3-32b',
|
|
172
|
-
'qwen3-coder-30b-a3b-instruct',
|
|
173
|
-
'holo2-30b-a3b',
|
|
174
|
-
'llama-3.3-70b-instruct',
|
|
175
|
-
'mistral-small-3.2-24b-instruct-2506',
|
|
176
|
-
'gemma-3-27b-it',
|
|
177
|
-
'qwen3.5-397b-a17b',
|
|
178
|
-
'qwen3-coder-30b-a3b-instruct',
|
|
179
|
-
'gpt-oss-120b',
|
|
180
|
-
'gpt-oss-20b',
|
|
181
|
-
'Meta-Llama-3_3-70B-Instruct',
|
|
182
|
-
'Qwen3-32B',
|
|
183
|
-
'Mistral-Small-3.2-24B-Instruct-2506',
|
|
184
|
-
'Mistral-7B-Instruct-v0.3',
|
|
185
|
-
'Mistral-Nemo-Instruct-2407',
|
|
186
|
-
'Qwen3.5-9B',
|
|
187
|
-
'big-pickle',
|
|
188
|
-
'deepseek-v4-flash-free',
|
|
189
|
-
'mimo-v2.5-free',
|
|
190
|
-
'nemotron-3-super-free',
|
|
191
|
-
'minimax-m3-free'
|
|
192
140
|
]);
|
|
193
141
|
|
|
194
142
|
export const getVerdict = (r) => {
|
|
@@ -376,34 +324,12 @@ export const sortResults = (results, sortColumn, sortDirection, { benchmarkResul
|
|
|
376
324
|
break
|
|
377
325
|
case 'swe': {
|
|
378
326
|
// 📖 Sort by SWE-bench score — higher is better
|
|
379
|
-
|
|
380
|
-
const parseSwe = (score) => {
|
|
381
|
-
if (!score || score === '—') return 0
|
|
382
|
-
const num = parseFloat(score.replace('%', ''))
|
|
383
|
-
return isNaN(num) ? 0 : num
|
|
384
|
-
}
|
|
385
|
-
cmp = parseSwe(a.sweScore) - parseSwe(b.sweScore)
|
|
327
|
+
cmp = parseSweToNum(a.sweScore) - parseSweToNum(b.sweScore)
|
|
386
328
|
break
|
|
387
329
|
}
|
|
388
330
|
case 'ctx': {
|
|
389
|
-
// 📖 Sort by context window size — larger is better
|
|
390
|
-
|
|
391
|
-
const parseCtx = (ctx) => {
|
|
392
|
-
if (!ctx || ctx === '—') return 0
|
|
393
|
-
const str = ctx.toLowerCase()
|
|
394
|
-
// 📖 Handle millions (1m = 1000k)
|
|
395
|
-
if (str.includes('m')) {
|
|
396
|
-
const num = parseFloat(str.replace('m', ''))
|
|
397
|
-
return num * 1000
|
|
398
|
-
}
|
|
399
|
-
// 📖 Handle thousands (128k)
|
|
400
|
-
if (str.includes('k')) {
|
|
401
|
-
const num = parseFloat(str.replace('k', ''))
|
|
402
|
-
return num
|
|
403
|
-
}
|
|
404
|
-
return 0
|
|
405
|
-
}
|
|
406
|
-
cmp = parseCtx(a.ctx) - parseCtx(b.ctx)
|
|
331
|
+
// 📖 Sort by context window size — larger is better (uses parseCtxToK)
|
|
332
|
+
cmp = parseCtxToK(a.ctx) - parseCtxToK(b.ctx)
|
|
407
333
|
break
|
|
408
334
|
}
|
|
409
335
|
case 'condition':
|
|
@@ -727,7 +653,7 @@ export const CONTEXT_BUDGETS = {
|
|
|
727
653
|
|
|
728
654
|
// 📖 parseCtxToK: Convert context window string ("128k", "1m", "200k") into numeric K tokens.
|
|
729
655
|
// 📖 Used by the scoring engine to compare against CONTEXT_BUDGETS thresholds.
|
|
730
|
-
function parseCtxToK(ctx) {
|
|
656
|
+
export function parseCtxToK(ctx) {
|
|
731
657
|
if (!ctx || ctx === '—') return 0
|
|
732
658
|
const str = ctx.toLowerCase()
|
|
733
659
|
if (str.includes('m')) return parseFloat(str.replace('m', '')) * 1000
|
|
@@ -756,7 +682,7 @@ export function labelFromId(id) {
|
|
|
756
682
|
|
|
757
683
|
// 📖 parseSweToNum: Convert SWE-bench score string ("49.2%", "73.1%") into a 0–100 number.
|
|
758
684
|
// 📖 Returns 0 for missing or invalid scores.
|
|
759
|
-
function parseSweToNum(sweScore) {
|
|
685
|
+
export function parseSweToNum(sweScore) {
|
|
760
686
|
if (!sweScore || sweScore === '—') return 0
|
|
761
687
|
const num = parseFloat(sweScore.replace('%', ''))
|
|
762
688
|
return isNaN(num) ? 0 : num
|
package/src/tui/app.js
CHANGED
|
@@ -307,8 +307,8 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
|
|
|
307
307
|
|
|
308
308
|
let results = MODELS
|
|
309
309
|
.filter(([,,,,,providerKey]) => isProviderEnabled(config, providerKey))
|
|
310
|
-
.map(([modelId, label, tier, sweScore, ctx, providerKey], i) => ({
|
|
311
|
-
idx: i + 1, modelId, label, tier, sweScore, ctx, providerKey,
|
|
310
|
+
.map(([modelId, label, tier, sweScore, ctx, providerKey, addedDate], i) => ({
|
|
311
|
+
idx: i + 1, modelId, label, tier, sweScore, ctx, providerKey, addedDate: addedDate || null,
|
|
312
312
|
status: 'pending',
|
|
313
313
|
pings: [], // 📖 All ping results (ms or 'TIMEOUT')
|
|
314
314
|
httpCode: null,
|
|
@@ -202,6 +202,7 @@ const BASE_COMMAND_TREE = [
|
|
|
202
202
|
{ id: 'action-cycle-theme', label: 'Cycle theme', shortcut: 'G', icon: '🌗', description: 'Switch dark/light/auto', keywords: ['theme', 'dark', 'light', 'auto'] },
|
|
203
203
|
{ id: 'action-reset-view', label: 'Reset view', shortcut: 'N', icon: '\u{1F504}', description: 'Reset filters and sort', keywords: ['reset', 'view', 'sort', 'filters'] },
|
|
204
204
|
{ id: 'action-probe-404', label: 'Probe 404 Models', shortcut: 'Ctrl+Shift+P', icon: '\u{1F50D}', description: 'Test all configured models for 404/410 errors. Auto-hides broken models.', keywords: ['probe', '404', 'broken', 'dead', 'health', 'check', 'test', 'verify'] },
|
|
205
|
+
{ id: 'action-toggle-auto-hide-broken', label: 'Toggle Auto-hide Broken Models', icon: '\u{1F6A9}', description: 'Enable/disable auto-hiding of 404/410 models. Unhides all when disabled.', keywords: ['auto', 'hide', 'broken', '404', 'models', 'toggle', 'probe'] },
|
|
205
206
|
],
|
|
206
207
|
},
|
|
207
208
|
// 📖 Pages - directly at root level, not in submenu
|