free-coding-models 0.1.67 → 0.1.69
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 +99 -18
- package/bin/free-coding-models.js +788 -53
- package/lib/config.js +163 -4
- package/lib/utils.js +172 -5
- package/package.json +1 -1
- package/sources.js +45 -2
package/lib/config.js
CHANGED
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
* "siliconflow":"sk-xxx",
|
|
29
29
|
* "together": "together-xxx",
|
|
30
30
|
* "cloudflare": "cf-xxx",
|
|
31
|
-
* "perplexity": "pplx-xxx"
|
|
31
|
+
* "perplexity": "pplx-xxx",
|
|
32
|
+
* "zai": "zai-xxx"
|
|
32
33
|
* },
|
|
33
34
|
* "providers": {
|
|
34
35
|
* "nvidia": { "enabled": true },
|
|
@@ -47,18 +48,35 @@
|
|
|
47
48
|
* "siliconflow":{ "enabled": true },
|
|
48
49
|
* "together": { "enabled": true },
|
|
49
50
|
* "cloudflare": { "enabled": true },
|
|
50
|
-
* "perplexity": { "enabled": true }
|
|
51
|
+
* "perplexity": { "enabled": true },
|
|
52
|
+
* "zai": { "enabled": true }
|
|
51
53
|
* },
|
|
52
54
|
* "favorites": [
|
|
53
55
|
* "nvidia/deepseek-ai/deepseek-v3.2"
|
|
54
|
-
*
|
|
56
|
+
* ],
|
|
55
57
|
* "telemetry": {
|
|
56
58
|
* "enabled": true,
|
|
57
59
|
* "consentVersion": 1,
|
|
58
60
|
* "anonymousId": "anon_550e8400-e29b-41d4-a716-446655440000"
|
|
61
|
+
* },
|
|
62
|
+
* "activeProfile": "work",
|
|
63
|
+
* "profiles": {
|
|
64
|
+
* "work": { "apiKeys": {...}, "providers": {...}, "favorites": [...], "settings": {...} },
|
|
65
|
+
* "personal": { "apiKeys": {...}, "providers": {...}, "favorites": [...], "settings": {...} },
|
|
66
|
+
* "fast": { "apiKeys": {...}, "providers": {...}, "favorites": [...], "settings": {...} }
|
|
59
67
|
* }
|
|
60
68
|
* }
|
|
61
69
|
*
|
|
70
|
+
* 📖 Profiles store a snapshot of the user's configuration. Each profile contains:
|
|
71
|
+
* - apiKeys: API keys per provider (can differ between work/personal setups)
|
|
72
|
+
* - providers: enabled/disabled state per provider
|
|
73
|
+
* - favorites: list of pinned favorite models
|
|
74
|
+
* - settings: extra TUI preferences (tierFilter, sortColumn, sortAsc, pingInterval)
|
|
75
|
+
*
|
|
76
|
+
* 📖 When a profile is loaded via --profile <name> or Shift+P, the main config's
|
|
77
|
+
* apiKeys/providers/favorites are replaced with the profile's values. The profile
|
|
78
|
+
* data itself stays in the profiles section — it's a named snapshot, not a fork.
|
|
79
|
+
*
|
|
62
80
|
* 📖 Migration: On first run, if the old plain-text ~/.free-coding-models exists
|
|
63
81
|
* and the new JSON file does not, the old key is auto-migrated as the nvidia key.
|
|
64
82
|
* The old file is left in place (not deleted) for safety.
|
|
@@ -68,8 +86,17 @@
|
|
|
68
86
|
* → saveConfig(config) — Write config to ~/.free-coding-models.json with 0o600 permissions
|
|
69
87
|
* → getApiKey(config, providerKey) — Get effective API key (env var override > config > null)
|
|
70
88
|
* → isProviderEnabled(config, providerKey) — Check if provider is enabled (defaults true)
|
|
89
|
+
* → saveAsProfile(config, name) — Snapshot current apiKeys/providers/favorites/settings into a named profile
|
|
90
|
+
* → loadProfile(config, name) — Apply a named profile's values onto the live config
|
|
91
|
+
* → listProfiles(config) — Return array of profile names
|
|
92
|
+
* → deleteProfile(config, name) — Remove a named profile
|
|
93
|
+
* → getActiveProfileName(config) — Get the currently active profile name (or null)
|
|
94
|
+
* → setActiveProfile(config, name) — Set which profile is active (null to clear)
|
|
95
|
+
* → _emptyProfileSettings() — Default TUI settings for a profile
|
|
71
96
|
*
|
|
72
|
-
* @exports loadConfig, saveConfig, getApiKey
|
|
97
|
+
* @exports loadConfig, saveConfig, getApiKey, isProviderEnabled
|
|
98
|
+
* @exports saveAsProfile, loadProfile, listProfiles, deleteProfile
|
|
99
|
+
* @exports getActiveProfileName, setActiveProfile
|
|
73
100
|
* @exports CONFIG_PATH — path to the JSON config file
|
|
74
101
|
*
|
|
75
102
|
* @see bin/free-coding-models.js — main CLI that uses these functions
|
|
@@ -106,6 +133,8 @@ const ENV_VARS = {
|
|
|
106
133
|
together: 'TOGETHER_API_KEY',
|
|
107
134
|
cloudflare: ['CLOUDFLARE_API_TOKEN', 'CLOUDFLARE_API_KEY'],
|
|
108
135
|
perplexity: ['PERPLEXITY_API_KEY', 'PPLX_API_KEY'],
|
|
136
|
+
zai: 'ZAI_API_KEY',
|
|
137
|
+
iflow: 'IFLOW_API_KEY',
|
|
109
138
|
}
|
|
110
139
|
|
|
111
140
|
/**
|
|
@@ -137,6 +166,9 @@ export function loadConfig() {
|
|
|
137
166
|
if (typeof parsed.telemetry.enabled !== 'boolean') parsed.telemetry.enabled = null
|
|
138
167
|
if (typeof parsed.telemetry.consentVersion !== 'number') parsed.telemetry.consentVersion = 0
|
|
139
168
|
if (typeof parsed.telemetry.anonymousId !== 'string' || !parsed.telemetry.anonymousId.trim()) parsed.telemetry.anonymousId = null
|
|
169
|
+
// 📖 Ensure profiles section exists (added in profile system)
|
|
170
|
+
if (!parsed.profiles || typeof parsed.profiles !== 'object') parsed.profiles = {}
|
|
171
|
+
if (parsed.activeProfile && typeof parsed.activeProfile !== 'string') parsed.activeProfile = null
|
|
140
172
|
return parsed
|
|
141
173
|
} catch {
|
|
142
174
|
// 📖 Corrupted JSON — return empty config (user will re-enter keys)
|
|
@@ -222,6 +254,129 @@ export function isProviderEnabled(config, providerKey) {
|
|
|
222
254
|
return providerConfig.enabled !== false
|
|
223
255
|
}
|
|
224
256
|
|
|
257
|
+
// ─── Config Profiles ──────────────────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 📖 _emptyProfileSettings: Default TUI settings stored in a profile.
|
|
261
|
+
*
|
|
262
|
+
* 📖 These settings are saved/restored when switching profiles so each profile
|
|
263
|
+
* can have different sort, filter, and ping preferences.
|
|
264
|
+
*
|
|
265
|
+
* @returns {{ tierFilter: string|null, sortColumn: string, sortAsc: boolean, pingInterval: number }}
|
|
266
|
+
*/
|
|
267
|
+
export function _emptyProfileSettings() {
|
|
268
|
+
return {
|
|
269
|
+
tierFilter: null, // 📖 null = show all tiers, or 'S'|'A'|'B'|'C'|'D'
|
|
270
|
+
sortColumn: 'avg', // 📖 default sort column
|
|
271
|
+
sortAsc: true, // 📖 true = ascending (fastest first for latency)
|
|
272
|
+
pingInterval: 8000, // 📖 default ms between pings
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* 📖 saveAsProfile: Snapshot the current config state into a named profile.
|
|
278
|
+
*
|
|
279
|
+
* 📖 Takes the current apiKeys, providers, favorites, plus explicit TUI settings
|
|
280
|
+
* and stores them under config.profiles[name]. Does NOT change activeProfile —
|
|
281
|
+
* call setActiveProfile() separately if you want to switch to this profile.
|
|
282
|
+
*
|
|
283
|
+
* 📖 If a profile with the same name exists, it's overwritten.
|
|
284
|
+
*
|
|
285
|
+
* @param {object} config — Live config object (will be mutated)
|
|
286
|
+
* @param {string} name — Profile name (e.g. 'work', 'personal', 'fast')
|
|
287
|
+
* @param {object} [settings] — TUI settings to save (tierFilter, sortColumn, etc.)
|
|
288
|
+
* @returns {object} The config object (for chaining)
|
|
289
|
+
*/
|
|
290
|
+
export function saveAsProfile(config, name, settings = null) {
|
|
291
|
+
if (!config.profiles || typeof config.profiles !== 'object') config.profiles = {}
|
|
292
|
+
config.profiles[name] = {
|
|
293
|
+
apiKeys: JSON.parse(JSON.stringify(config.apiKeys || {})),
|
|
294
|
+
providers: JSON.parse(JSON.stringify(config.providers || {})),
|
|
295
|
+
favorites: [...(config.favorites || [])],
|
|
296
|
+
settings: settings ? { ..._emptyProfileSettings(), ...settings } : _emptyProfileSettings(),
|
|
297
|
+
}
|
|
298
|
+
return config
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 📖 loadProfile: Apply a named profile's values onto the live config.
|
|
303
|
+
*
|
|
304
|
+
* 📖 Replaces config.apiKeys, config.providers, config.favorites with the
|
|
305
|
+
* profile's stored values. Also sets config.activeProfile to the loaded name.
|
|
306
|
+
*
|
|
307
|
+
* 📖 Returns the profile's TUI settings so the caller (main CLI) can apply them
|
|
308
|
+
* to the live state object (sortColumn, tierFilter, etc.).
|
|
309
|
+
*
|
|
310
|
+
* 📖 If the profile doesn't exist, returns null (caller should show an error).
|
|
311
|
+
*
|
|
312
|
+
* @param {object} config — Live config object (will be mutated)
|
|
313
|
+
* @param {string} name — Profile name to load
|
|
314
|
+
* @returns {{ tierFilter: string|null, sortColumn: string, sortAsc: boolean, pingInterval: number }|null}
|
|
315
|
+
* The profile's TUI settings, or null if profile not found
|
|
316
|
+
*/
|
|
317
|
+
export function loadProfile(config, name) {
|
|
318
|
+
const profile = config?.profiles?.[name]
|
|
319
|
+
if (!profile) return null
|
|
320
|
+
|
|
321
|
+
// 📖 Deep-copy the profile data into the live config (don't share references)
|
|
322
|
+
config.apiKeys = JSON.parse(JSON.stringify(profile.apiKeys || {}))
|
|
323
|
+
config.providers = JSON.parse(JSON.stringify(profile.providers || {}))
|
|
324
|
+
config.favorites = [...(profile.favorites || [])]
|
|
325
|
+
config.activeProfile = name
|
|
326
|
+
|
|
327
|
+
return profile.settings ? { ..._emptyProfileSettings(), ...profile.settings } : _emptyProfileSettings()
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* 📖 listProfiles: Get all saved profile names.
|
|
332
|
+
*
|
|
333
|
+
* @param {object} config
|
|
334
|
+
* @returns {string[]} Array of profile names, sorted alphabetically
|
|
335
|
+
*/
|
|
336
|
+
export function listProfiles(config) {
|
|
337
|
+
if (!config?.profiles || typeof config.profiles !== 'object') return []
|
|
338
|
+
return Object.keys(config.profiles).sort()
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* 📖 deleteProfile: Remove a named profile from the config.
|
|
343
|
+
*
|
|
344
|
+
* 📖 If the deleted profile is the active one, clears activeProfile.
|
|
345
|
+
*
|
|
346
|
+
* @param {object} config — Live config object (will be mutated)
|
|
347
|
+
* @param {string} name — Profile name to delete
|
|
348
|
+
* @returns {boolean} True if the profile existed and was deleted
|
|
349
|
+
*/
|
|
350
|
+
export function deleteProfile(config, name) {
|
|
351
|
+
if (!config?.profiles?.[name]) return false
|
|
352
|
+
delete config.profiles[name]
|
|
353
|
+
if (config.activeProfile === name) config.activeProfile = null
|
|
354
|
+
return true
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* 📖 getActiveProfileName: Get the currently active profile name.
|
|
359
|
+
*
|
|
360
|
+
* @param {object} config
|
|
361
|
+
* @returns {string|null} Profile name, or null if no profile is active
|
|
362
|
+
*/
|
|
363
|
+
export function getActiveProfileName(config) {
|
|
364
|
+
return config?.activeProfile || null
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* 📖 setActiveProfile: Set which profile is active (or null to clear).
|
|
369
|
+
*
|
|
370
|
+
* 📖 This just stores the name — it does NOT load the profile's data.
|
|
371
|
+
* Call loadProfile() first to actually apply the profile's values.
|
|
372
|
+
*
|
|
373
|
+
* @param {object} config — Live config object (will be mutated)
|
|
374
|
+
* @param {string|null} name — Profile name, or null to clear
|
|
375
|
+
*/
|
|
376
|
+
export function setActiveProfile(config, name) {
|
|
377
|
+
config.activeProfile = name || null
|
|
378
|
+
}
|
|
379
|
+
|
|
225
380
|
// 📖 Internal helper: create a blank config with the right shape
|
|
226
381
|
function _emptyConfig() {
|
|
227
382
|
return {
|
|
@@ -235,5 +390,9 @@ function _emptyConfig() {
|
|
|
235
390
|
consentVersion: 0,
|
|
236
391
|
anonymousId: null,
|
|
237
392
|
},
|
|
393
|
+
// 📖 Active profile name — null means no profile is loaded (using raw config).
|
|
394
|
+
activeProfile: null,
|
|
395
|
+
// 📖 Named profiles: each is a snapshot of apiKeys + providers + favorites + settings.
|
|
396
|
+
profiles: {},
|
|
238
397
|
}
|
|
239
398
|
}
|
package/lib/utils.js
CHANGED
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
*
|
|
40
40
|
* @exports getAvg, getVerdict, getUptime, getP95, getJitter, getStabilityScore
|
|
41
41
|
* @exports sortResults, filterByTier, findBestModel, parseArgs
|
|
42
|
-
* @exports
|
|
42
|
+
* @exports scoreModelForTask, getTopRecommendations
|
|
43
|
+
* @exports TIER_ORDER, VERDICT_ORDER, TIER_LETTER_MAP, TASK_TYPES, PRIORITY_TYPES, CONTEXT_BUDGETS
|
|
43
44
|
*
|
|
44
45
|
* @see bin/free-coding-models.js — main CLI that imports these utils
|
|
45
46
|
* @see sources.js — model definitions consumed by these functions
|
|
@@ -385,17 +386,27 @@ export function parseArgs(argv) {
|
|
|
385
386
|
let apiKey = null
|
|
386
387
|
const flags = []
|
|
387
388
|
|
|
388
|
-
// Determine which arg
|
|
389
|
+
// 📖 Determine which arg indices are consumed by --tier and --profile so we skip them
|
|
389
390
|
const tierIdx = args.findIndex(a => a.toLowerCase() === '--tier')
|
|
390
391
|
const tierValueIdx = (tierIdx !== -1 && args[tierIdx + 1] && !args[tierIdx + 1].startsWith('--'))
|
|
391
392
|
? tierIdx + 1
|
|
392
393
|
: -1
|
|
393
394
|
|
|
395
|
+
const profileIdx = args.findIndex(a => a.toLowerCase() === '--profile')
|
|
396
|
+
const profileValueIdx = (profileIdx !== -1 && args[profileIdx + 1] && !args[profileIdx + 1].startsWith('--'))
|
|
397
|
+
? profileIdx + 1
|
|
398
|
+
: -1
|
|
399
|
+
|
|
400
|
+
// 📖 Set of arg indices that are values for flags (not API keys)
|
|
401
|
+
const skipIndices = new Set()
|
|
402
|
+
if (tierValueIdx !== -1) skipIndices.add(tierValueIdx)
|
|
403
|
+
if (profileValueIdx !== -1) skipIndices.add(profileValueIdx)
|
|
404
|
+
|
|
394
405
|
for (const [i, arg] of args.entries()) {
|
|
395
406
|
if (arg.startsWith('--')) {
|
|
396
407
|
flags.push(arg.toLowerCase())
|
|
397
|
-
} else if (i
|
|
398
|
-
// Skip
|
|
408
|
+
} else if (skipIndices.has(i)) {
|
|
409
|
+
// 📖 Skip — this is a value for --tier or --profile, not an API key
|
|
399
410
|
} else if (!apiKey) {
|
|
400
411
|
apiKey = arg
|
|
401
412
|
}
|
|
@@ -410,5 +421,161 @@ export function parseArgs(argv) {
|
|
|
410
421
|
|
|
411
422
|
let tierFilter = tierValueIdx !== -1 ? args[tierValueIdx].toUpperCase() : null
|
|
412
423
|
|
|
413
|
-
|
|
424
|
+
const profileName = profileValueIdx !== -1 ? args[profileValueIdx] : null
|
|
425
|
+
|
|
426
|
+
// 📖 --recommend — launch directly into Smart Recommend mode (Q key equivalent)
|
|
427
|
+
const recommendMode = flags.includes('--recommend')
|
|
428
|
+
|
|
429
|
+
return { apiKey, bestMode, fiableMode, openCodeMode, openCodeDesktopMode, openClawMode, noTelemetry, tierFilter, profileName, recommendMode }
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ─── Smart Recommend — Scoring Engine ─────────────────────────────────────────
|
|
433
|
+
|
|
434
|
+
// 📖 Task types for the Smart Recommend questionnaire.
|
|
435
|
+
// 📖 Each task type has different weight priorities — quick fixes favor speed,
|
|
436
|
+
// deep refactors favor SWE score and context, code review needs balanced quality,
|
|
437
|
+
// test generation needs high SWE score + medium context.
|
|
438
|
+
export const TASK_TYPES = {
|
|
439
|
+
quickfix: { label: 'Quick Fix', sweWeight: 0.2, speedWeight: 0.5, ctxWeight: 0.1, stabilityWeight: 0.2 },
|
|
440
|
+
refactor: { label: 'Deep Refactor', sweWeight: 0.4, speedWeight: 0.1, ctxWeight: 0.3, stabilityWeight: 0.2 },
|
|
441
|
+
review: { label: 'Code Review', sweWeight: 0.35, speedWeight: 0.2, ctxWeight: 0.25, stabilityWeight: 0.2 },
|
|
442
|
+
testgen: { label: 'Test Generation', sweWeight: 0.35, speedWeight: 0.15, ctxWeight: 0.2, stabilityWeight: 0.3 },
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// 📖 Priority presets — bias the scoring toward speed or quality.
|
|
446
|
+
// 📖 'speed' amplifies latency weighting, 'quality' amplifies SWE score weighting.
|
|
447
|
+
export const PRIORITY_TYPES = {
|
|
448
|
+
speed: { label: 'Speed', speedMultiplier: 1.5, sweMultiplier: 0.7 },
|
|
449
|
+
quality: { label: 'Quality', speedMultiplier: 0.7, sweMultiplier: 1.5 },
|
|
450
|
+
balanced:{ label: 'Balanced', speedMultiplier: 1.0, sweMultiplier: 1.0 },
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// 📖 Context budget categories — match against model's context window size.
|
|
454
|
+
// 📖 'small' (<4K tokens) can use any model. 'large' (>32K) strongly penalizes small-ctx models.
|
|
455
|
+
export const CONTEXT_BUDGETS = {
|
|
456
|
+
small: { label: 'Small file (<4K)', minCtx: 0, idealCtx: 32 },
|
|
457
|
+
medium: { label: 'Medium project (<32K)', minCtx: 32, idealCtx: 128 },
|
|
458
|
+
large: { label: 'Large codebase (>32K)', minCtx: 128, idealCtx: 256 },
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// 📖 parseCtxToK: Convert context window string ("128k", "1m", "200k") into numeric K tokens.
|
|
462
|
+
// 📖 Used by the scoring engine to compare against CONTEXT_BUDGETS thresholds.
|
|
463
|
+
function parseCtxToK(ctx) {
|
|
464
|
+
if (!ctx || ctx === '—') return 0
|
|
465
|
+
const str = ctx.toLowerCase()
|
|
466
|
+
if (str.includes('m')) return parseFloat(str.replace('m', '')) * 1000
|
|
467
|
+
if (str.includes('k')) return parseFloat(str.replace('k', ''))
|
|
468
|
+
return 0
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// 📖 parseSweToNum: Convert SWE-bench score string ("49.2%", "73.1%") into a 0–100 number.
|
|
472
|
+
// 📖 Returns 0 for missing or invalid scores.
|
|
473
|
+
function parseSweToNum(sweScore) {
|
|
474
|
+
if (!sweScore || sweScore === '—') return 0
|
|
475
|
+
const num = parseFloat(sweScore.replace('%', ''))
|
|
476
|
+
return isNaN(num) ? 0 : num
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* 📖 scoreModelForTask: Score a single model result for a specific task/priority/context combination.
|
|
481
|
+
*
|
|
482
|
+
* 📖 The score is a weighted composite of 4 signals:
|
|
483
|
+
* - SWE quality score (0–100): how good the model is at coding (from sources.js benchmarks)
|
|
484
|
+
* - Speed score (0–100): inverse of average latency (faster = higher score)
|
|
485
|
+
* - Context fit score (0–100): how well the model's context window matches the user's budget
|
|
486
|
+
* - Stability score (0–100): composite p95/jitter/uptime from getStabilityScore()
|
|
487
|
+
*
|
|
488
|
+
* 📖 Each signal is weighted by the task type, then further adjusted by the priority multiplier.
|
|
489
|
+
* 📖 Models that are down/timeout get a harsh penalty but aren't completely excluded
|
|
490
|
+
* (they might come back up during the analysis phase).
|
|
491
|
+
*
|
|
492
|
+
* @param {object} result — A model result object (from state.results)
|
|
493
|
+
* @param {string} taskType — Key from TASK_TYPES ('quickfix'|'refactor'|'review'|'testgen')
|
|
494
|
+
* @param {string} priority — Key from PRIORITY_TYPES ('speed'|'quality'|'balanced')
|
|
495
|
+
* @param {string} contextBudget — Key from CONTEXT_BUDGETS ('small'|'medium'|'large')
|
|
496
|
+
* @returns {number} Score between 0 and 100 (higher = better recommendation)
|
|
497
|
+
*/
|
|
498
|
+
export function scoreModelForTask(result, taskType, priority, contextBudget) {
|
|
499
|
+
const task = TASK_TYPES[taskType]
|
|
500
|
+
const prio = PRIORITY_TYPES[priority]
|
|
501
|
+
const budget = CONTEXT_BUDGETS[contextBudget]
|
|
502
|
+
if (!task || !prio || !budget) return 0
|
|
503
|
+
|
|
504
|
+
// 📖 SWE quality signal (0–100) — raw SWE-bench score
|
|
505
|
+
const sweNum = parseSweToNum(result.sweScore)
|
|
506
|
+
const sweScore = Math.min(100, sweNum * (100 / 80)) // 📖 Normalize: 80% SWE → 100 score
|
|
507
|
+
|
|
508
|
+
// 📖 Speed signal (0–100) — inverse latency, capped at 5000ms
|
|
509
|
+
const avg = getAvg(result)
|
|
510
|
+
let speedScore
|
|
511
|
+
if (avg === Infinity) {
|
|
512
|
+
speedScore = 0 // 📖 No data yet — can't judge speed
|
|
513
|
+
} else {
|
|
514
|
+
speedScore = Math.max(0, Math.min(100, 100 * (1 - avg / 5000)))
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// 📖 Context fit signal (0–100):
|
|
518
|
+
// - Full score if model ctx >= idealCtx
|
|
519
|
+
// - Partial score if model ctx >= minCtx but < idealCtx (linear interpolation)
|
|
520
|
+
// - Zero if model ctx < minCtx (too small for the job)
|
|
521
|
+
const modelCtx = parseCtxToK(result.ctx)
|
|
522
|
+
let ctxScore
|
|
523
|
+
if (modelCtx >= budget.idealCtx) {
|
|
524
|
+
ctxScore = 100
|
|
525
|
+
} else if (modelCtx >= budget.minCtx) {
|
|
526
|
+
ctxScore = budget.idealCtx === budget.minCtx
|
|
527
|
+
? 100
|
|
528
|
+
: Math.round(100 * (modelCtx - budget.minCtx) / (budget.idealCtx - budget.minCtx))
|
|
529
|
+
} else {
|
|
530
|
+
ctxScore = 0
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// 📖 Stability signal (0–100) — from getStabilityScore(), or 0 if no data
|
|
534
|
+
const stability = getStabilityScore(result)
|
|
535
|
+
const stabScore = stability === -1 ? 0 : stability
|
|
536
|
+
|
|
537
|
+
// 📖 Weighted combination: task weights × priority multipliers
|
|
538
|
+
const rawScore =
|
|
539
|
+
(sweScore * task.sweWeight * prio.sweMultiplier) +
|
|
540
|
+
(speedScore * task.speedWeight * prio.speedMultiplier) +
|
|
541
|
+
(ctxScore * task.ctxWeight) +
|
|
542
|
+
(stabScore * task.stabilityWeight)
|
|
543
|
+
|
|
544
|
+
// 📖 Normalize by total effective weight to keep result in 0–100 range
|
|
545
|
+
const totalWeight =
|
|
546
|
+
(task.sweWeight * prio.sweMultiplier) +
|
|
547
|
+
(task.speedWeight * prio.speedMultiplier) +
|
|
548
|
+
task.ctxWeight +
|
|
549
|
+
task.stabilityWeight
|
|
550
|
+
|
|
551
|
+
let score = totalWeight > 0 ? rawScore / totalWeight : 0
|
|
552
|
+
|
|
553
|
+
// 📖 Penalty for models that are currently down/timeout — still scoreable but penalized
|
|
554
|
+
if (result.status === 'down' || result.status === 'timeout') {
|
|
555
|
+
score *= 0.2
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return Math.round(Math.min(100, Math.max(0, score)))
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* 📖 getTopRecommendations: Score all models and return the top N recommendations.
|
|
563
|
+
*
|
|
564
|
+
* 📖 Filters out hidden models, scores each one, sorts descending, returns topN.
|
|
565
|
+
* 📖 Each returned item includes the original result + computed score for display.
|
|
566
|
+
*
|
|
567
|
+
* @param {Array} results — Full state.results array
|
|
568
|
+
* @param {string} taskType — Key from TASK_TYPES
|
|
569
|
+
* @param {string} priority — Key from PRIORITY_TYPES
|
|
570
|
+
* @param {string} contextBudget — Key from CONTEXT_BUDGETS
|
|
571
|
+
* @param {number} [topN=3] — How many recommendations to return
|
|
572
|
+
* @returns {Array<{result: object, score: number}>} Top N scored models, descending by score
|
|
573
|
+
*/
|
|
574
|
+
export function getTopRecommendations(results, taskType, priority, contextBudget, topN = 3) {
|
|
575
|
+
const scored = results
|
|
576
|
+
.filter(r => !r.hidden)
|
|
577
|
+
.map(r => ({ result: r, score: scoreModelForTask(r, taskType, priority, contextBudget) }))
|
|
578
|
+
.sort((a, b) => b.score - a.score)
|
|
579
|
+
|
|
580
|
+
return scored.slice(0, topN)
|
|
414
581
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.69",
|
|
4
4
|
"description": "Find the fastest coding LLM models in seconds — ping free models from multiple providers, pick the best one for OpenCode, Cursor, or any AI coding assistant.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nvidia",
|
package/sources.js
CHANGED
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
* 📖 Secondary: https://swe-rebench.com (independent evals, scores are lower)
|
|
28
28
|
* 📖 Leaderboard tracker: https://www.marc0.dev/en/leaderboard
|
|
29
29
|
*
|
|
30
|
-
* @exports nvidiaNim, groq, cerebras, sambanova, openrouter, huggingface, replicate, deepinfra, fireworks, codestral, hyperbolic, scaleway, googleai, siliconflow, together, cloudflare, perplexity — model arrays per provider
|
|
31
|
-
* @exports sources — map of { nvidia, groq, cerebras, sambanova, openrouter, huggingface, replicate, deepinfra, fireworks, codestral, hyperbolic, scaleway, googleai, siliconflow, together, cloudflare, perplexity } each with { name, url, models }
|
|
30
|
+
* @exports nvidiaNim, groq, cerebras, sambanova, openrouter, huggingface, replicate, deepinfra, fireworks, codestral, hyperbolic, scaleway, googleai, siliconflow, together, cloudflare, perplexity, iflow — model arrays per provider
|
|
31
|
+
* @exports sources — map of { nvidia, groq, cerebras, sambanova, openrouter, huggingface, replicate, deepinfra, fireworks, codestral, hyperbolic, scaleway, googleai, siliconflow, together, cloudflare, perplexity, iflow } each with { name, url, models }
|
|
32
32
|
* @exports MODELS — flat array of [modelId, label, tier, sweScore, ctx, providerKey]
|
|
33
33
|
*
|
|
34
34
|
* 📖 MODELS now includes providerKey as 6th element so ping() knows which
|
|
@@ -230,6 +230,18 @@ export const googleai = [
|
|
|
230
230
|
['gemma-3-4b-it', 'Gemma 3 4B', 'C', '10.0%', '128k'],
|
|
231
231
|
]
|
|
232
232
|
|
|
233
|
+
// 📖 ZAI source - https://open.z.ai
|
|
234
|
+
// 📖 Free API keys available at https://open.z.ai — GLM frontier models
|
|
235
|
+
// 📖 OpenAI-compatible endpoint for coding tasks
|
|
236
|
+
export const zai = [
|
|
237
|
+
// ── S+ tier — SWE-bench Verified ≥70% ──
|
|
238
|
+
['zai/glm-5', 'GLM-5', 'S+', '77.8%', '128k'],
|
|
239
|
+
['zai/glm-4.7', 'GLM-4.7', 'S+', '73.8%', '200k'],
|
|
240
|
+
['zai/glm-4.5', 'GLM-4.5', 'S+', '75.0%', '128k'],
|
|
241
|
+
['zai/glm-4.5-air', 'GLM-4.5-Air', 'S+', '72.0%', '128k'],
|
|
242
|
+
['zai/glm-4.6', 'GLM-4.6', 'S+', '70.0%', '128k'],
|
|
243
|
+
]
|
|
244
|
+
|
|
233
245
|
// 📖 SiliconFlow source - https://cloud.siliconflow.cn
|
|
234
246
|
// 📖 OpenAI-compatible endpoint: https://api.siliconflow.com/v1/chat/completions
|
|
235
247
|
// 📖 Free model quotas vary by model and can change over time.
|
|
@@ -278,6 +290,27 @@ export const perplexity = [
|
|
|
278
290
|
['sonar', 'Sonar', 'B', '25.0%', '128k'],
|
|
279
291
|
]
|
|
280
292
|
|
|
293
|
+
// 📖 iFlow source - https://platform.iflow.cn
|
|
294
|
+
// 📖 OpenAI-compatible endpoint: https://apis.iflow.cn/v1/chat/completions
|
|
295
|
+
// 📖 Free for individual users with no request limits (API key expires every 7 days)
|
|
296
|
+
// 📖 Provides high-performance models including DeepSeek, Qwen3, Kimi K2, GLM, and TBStars2
|
|
297
|
+
export const iflow = [
|
|
298
|
+
// ── S+ tier — SWE-bench Verified ≥70% ──
|
|
299
|
+
['TBStars2-200B-A13B', 'TBStars2 200B', 'S+', '77.8%', '128k'],
|
|
300
|
+
['deepseek-v3.2', 'DeepSeek V3.2', 'S+', '73.1%', '128k'],
|
|
301
|
+
['qwen3-coder-plus', 'Qwen3 Coder Plus', 'S+', '72.0%', '256k'],
|
|
302
|
+
['qwen3-235b-a22b-instruct', 'Qwen3 235B', 'S+', '70.0%', '256k'],
|
|
303
|
+
['deepseek-r1', 'DeepSeek R1', 'S+', '70.6%', '128k'],
|
|
304
|
+
// ── S tier — SWE-bench Verified 60–70% ──
|
|
305
|
+
['kimi-k2', 'Kimi K2', 'S', '65.8%', '128k'],
|
|
306
|
+
['kimi-k2-0905', 'Kimi K2 0905', 'S', '68.0%', '256k'],
|
|
307
|
+
['glm-4.6', 'GLM 4.6', 'S', '62.0%', '200k'],
|
|
308
|
+
['deepseek-v3', 'DeepSeek V3', 'S', '62.0%', '128k'],
|
|
309
|
+
// ── A+ tier — SWE-bench Verified 50–60% ──
|
|
310
|
+
['qwen3-32b', 'Qwen3 32B', 'A+', '50.0%', '128k'],
|
|
311
|
+
['qwen3-max', 'Qwen3 Max', 'A+', '55.0%', '256k'],
|
|
312
|
+
]
|
|
313
|
+
|
|
281
314
|
// 📖 All sources combined - used by the main script
|
|
282
315
|
// 📖 Each source has: name (display), url (API endpoint), models (array of model tuples)
|
|
283
316
|
export const sources = {
|
|
@@ -346,6 +379,11 @@ export const sources = {
|
|
|
346
379
|
url: 'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions',
|
|
347
380
|
models: googleai,
|
|
348
381
|
},
|
|
382
|
+
zai: {
|
|
383
|
+
name: 'ZAI',
|
|
384
|
+
url: 'https://api.z.ai/api/coding/paas/v4/chat/completions',
|
|
385
|
+
models: zai,
|
|
386
|
+
},
|
|
349
387
|
siliconflow: {
|
|
350
388
|
name: 'SiliconFlow',
|
|
351
389
|
url: 'https://api.siliconflow.com/v1/chat/completions',
|
|
@@ -366,6 +404,11 @@ export const sources = {
|
|
|
366
404
|
url: 'https://api.perplexity.ai/chat/completions',
|
|
367
405
|
models: perplexity,
|
|
368
406
|
},
|
|
407
|
+
iflow: {
|
|
408
|
+
name: 'iFlow',
|
|
409
|
+
url: 'https://apis.iflow.cn/v1/chat/completions',
|
|
410
|
+
models: iflow,
|
|
411
|
+
},
|
|
369
412
|
}
|
|
370
413
|
|
|
371
414
|
// 📖 Flatten all models from all sources — each entry includes providerKey as 6th element
|