free-coding-models 0.3.55 → 0.3.56
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/CHANGELOG.md +47 -56
- package/README.md +236 -160
- package/bin/free-coding-models.js +46 -0
- package/package.json +2 -2
- package/sources.js +133 -309
- package/src/analysis.js +23 -10
- package/src/app.js +113 -7
- package/src/cache.js +1 -1
- package/src/cli-help.js +9 -0
- package/src/command-palette.js +16 -12
- package/src/config.js +199 -32
- package/src/endpoint-installer.js +45 -1
- package/src/favorites.js +22 -0
- package/src/graphify-out/cache/089db1c1def873cf6d112f1590da4490e61e691aff0db41e006aa2fb15ba0656.json +1 -0
- package/src/graphify-out/cache/0b510b53cf1a1393fb52b1fc3bbbf88b63938e961ec5b82119a2e9715fee8bd7.json +1 -0
- package/src/graphify-out/cache/0ec9a95a326bde58e0316889018b278062d06d494d0f31ba177c9de71e5fed2d.json +1 -0
- package/src/graphify-out/cache/1548663a24a68dce740ebab1bd1d3091048c9604e9d067a1650a42a6d82541d4.json +1 -0
- package/src/graphify-out/cache/1783af63cb6d0dfb4d469009f71ac83a74ba0b33d48186ff2c6e63f9429e900a.json +1 -0
- package/src/graphify-out/cache/1e109f5eb5dc4fd285871c3613e32b6b14a8c225f4080ee34b51c7e1a1764571.json +1 -0
- package/src/graphify-out/cache/1eb24dbeb69b46c8bc1caf925df2f2a964af0f33aea143adf8ddf88e017db6ca.json +1 -0
- package/src/graphify-out/cache/21e1bcfed11685e8347243f9d8516072dda183266a4bfe22c52fb31753a446c8.json +1 -0
- package/src/graphify-out/cache/2327473478b9c4b1940bf7ef66c9ee960b3cba8d5302e56b625df8274246e0b4.json +1 -0
- package/src/graphify-out/cache/25955b81fd25454c8fa90fb71a47db8d1215cf621beb8ff3cbd580aaf011b4f3.json +1 -0
- package/src/graphify-out/cache/2739677f19c702f88f3de0a0bac475066adbda98709907ad3de967aef689f86d.json +1 -0
- package/src/graphify-out/cache/2bba03422f6b3ee7f5b5d29cc90314a064d259e5822a176657bda3e04505cf00.json +1 -0
- package/src/graphify-out/cache/2ddf1d2c6d10147b0402446bc71a7988187b79b6210dd7e7250be8c555b9ff35.json +1 -0
- package/src/graphify-out/cache/2ee07457a5767c95a57f8e9eb95b28f800044f35666e0715e9d88ad1103a092e.json +1 -0
- package/src/graphify-out/cache/2fe9f75dc2951c417f2c8dd22749092cf550dc67599f1c8d1866900dc6e9154e.json +1 -0
- package/src/graphify-out/cache/41c4b7c27e7fc3e2948d3a4bf95a72de2ed9a6f0463994babdce8ed2cc84598c.json +1 -0
- package/src/graphify-out/cache/5028defd54b7fbd3c7e444973e493de036e097e9b1d2a7cae7f19b88d68aacde.json +1 -0
- package/src/graphify-out/cache/5b133aba3fb16410c5b1fdbd1730039fc7fa1ac93abd99d7be08f60da70fc8d4.json +1 -0
- package/src/graphify-out/cache/74252e5b0978d85ab3421a3de1a9384aa282ffd2be2cfe7db2530139089f4275.json +1 -0
- package/src/graphify-out/cache/7695ebeea056095edd14332963cc43354ef3a097caf46f1e28d0f01369642901.json +1 -0
- package/src/graphify-out/cache/777aa7085c395a935c6556bbde182cd871edb61f3a685ed8068ec0c8f6fb0075.json +1 -0
- package/src/graphify-out/cache/82a723881980e82273c113def8315533d7da28827e300413d9ad30f27b7407df.json +1 -0
- package/src/graphify-out/cache/86b87c9603e6cd188f42c7eed3b86c291d48a781c223a707e74f3e7ed0c02a21.json +1 -0
- package/src/graphify-out/cache/890fead9a78cadaed560a2d2453916121fa605c3e43a334910ac4bc951a9ef6d.json +1 -0
- package/src/graphify-out/cache/89d3ea66f52783caa775ef9a30923d7d6225e1d8ae9e962f4741b8c7785dab1e.json +1 -0
- package/src/graphify-out/cache/8cc82cd9edce41f0e1c092f14a94fd52bf847addf3237b616dc5a9e505bd05bd.json +1 -0
- package/src/graphify-out/cache/93ba2e25e3ff7ad525f397902345fbd375df7315de7b402e20cc803c14eccde8.json +1 -0
- package/src/graphify-out/cache/99beed29580b9c7bfecfee794cb3d8e535fcf0eb3b92113108f88bdd0a8e79b3.json +1 -0
- package/src/graphify-out/cache/aeeb931fa477c65ce2e51d8149957350fa54225c613222bbbe8448998d1afd3d.json +1 -0
- package/src/graphify-out/cache/baf91bef5b5ecb2a476433b6cc0c48c563c54ee2d07fc3c192e543685e3e7222.json +1 -0
- package/src/graphify-out/cache/bd98b94ac4e9b92b6336d47b26e0366b51a4eaf0711d722f05f98dfae23ab42b.json +1 -0
- package/src/graphify-out/cache/bfcb51e9328e9cbfbee4f6fee0f56635d7b03488addc9f6c4e4b190b70a73362.json +1 -0
- package/src/graphify-out/cache/c0d3dabeb093aa758c49eadf41b87ecc96a16c1449c2670aaf48cbfc891d8da6.json +1 -0
- package/src/graphify-out/cache/c20d6630236f473c1406068c3ae205853e649b216495c93dfec055dd222c55cf.json +1 -0
- package/src/graphify-out/cache/c22b9122816bebce0a2f79af41a986559d01e00163dbcd579c5755621b4cb483.json +1 -0
- package/src/graphify-out/cache/ca556ec14453ddb8f9e0c5a832dac90d77111b9bad5f8c2d80d272e2e7a06371.json +1 -0
- package/src/graphify-out/cache/d6dbc9135dfa35a756b3b09b06700e4bc229fdccba11bb963f2ba44028e0bbae.json +1 -0
- package/src/graphify-out/cache/e1cf71276f1779d0fa075f79bd7c8a9fd0b8eef6932ac043137451b7c7fa7cbe.json +1 -0
- package/src/graphify-out/cache/e4b3be14494467df2d2ed389bc4f18f099021cb5bc355b901fa88387b2d8b8a2.json +1 -0
- package/src/graphify-out/cache/eaea0dded097f6f9553b654220046c6ec0c9be592a5973d906564ee60af34e0d.json +1 -0
- package/src/graphify-out/cache/ef07d0cd2675d1f79d2a2fdbf3bc3319687638751e9ce89b0d0d97ed1cd9f7e1.json +1 -0
- package/src/graphify-out/cache/f81272d6eb8aaff9e96d5a1d9f06777db70ac3652a646b951ded51f79871d733.json +1 -0
- package/src/graphify-out/cache/f9619dd92186f75a6dbda937e0c606647153918524cdb5763f956e6ec2a9e386.json +1 -0
- package/src/graphify-out/cache/fd88b1b2ff4bfcae08559d9c2aaeeb9a3f1e2f5cd8928762c311196956c170a5.json +1 -0
- package/src/key-handler.js +312 -12
- package/src/kilo.js +20 -1
- package/src/opencode.js +23 -2
- package/src/overlays.js +206 -5
- package/src/provider-metadata.js +26 -17
- package/src/quota-capabilities.js +6 -10
- package/src/render-table.js +37 -4
- package/src/router-daemon.js +1986 -0
- package/src/router-dashboard.js +893 -0
- package/src/sync-set.js +479 -0
- package/src/theme.js +4 -0
- package/src/tool-launchers.js +1 -0
- package/src/tool-metadata.js +6 -2
- package/src/utils.js +30 -6
- package/web/dist/assets/{index-C03JjCgA.js → index-DNRCaWPi.js} +2 -2
- package/web/dist/index.html +1 -1
package/src/key-handler.js
CHANGED
|
@@ -33,7 +33,10 @@
|
|
|
33
33
|
|
|
34
34
|
import { loadChangelog } from './changelog-loader.js'
|
|
35
35
|
import { getToolMeta, isModelCompatibleWithTool, getCompatibleTools, findSimilarCompatibleModels } from './tool-metadata.js'
|
|
36
|
-
import { loadConfig, replaceConfigContents } from './config.js'
|
|
36
|
+
import { loadConfig, saveConfig, replaceConfigContents } from './config.js'
|
|
37
|
+
import { join, dirname } from 'node:path'
|
|
38
|
+
import { fileURLToPath } from 'node:url'
|
|
39
|
+
import { spawn } from 'node:child_process'
|
|
37
40
|
import { cleanupLegacyProxyArtifacts } from './legacy-proxy-cleanup.js'
|
|
38
41
|
import { getLastLayout, COLUMN_SORT_MAP } from './render-table.js'
|
|
39
42
|
import { cycleThemeSetting, detectActiveTheme } from './theme.js'
|
|
@@ -42,12 +45,23 @@ import { buildCommandPaletteTree, flattenCommandTree, filterCommandPaletteEntrie
|
|
|
42
45
|
import { WIDTH_WARNING_MIN_COLS, VERDICT_CYCLE, HEALTH_CYCLE } from './constants.js'
|
|
43
46
|
import { scanAllToolConfigs, softDeleteModel } from './installed-models-manager.js'
|
|
44
47
|
import { startExternalTool } from './tool-launchers.js'
|
|
48
|
+
import {
|
|
49
|
+
clearRouterDashboardRequestLog,
|
|
50
|
+
closeRouterDashboardOverlay,
|
|
51
|
+
cycleRouterDashboardActiveSet,
|
|
52
|
+
cycleRouterDashboardProbeMode,
|
|
53
|
+
openRouterDashboardOverlay,
|
|
54
|
+
restartRouterDashboardDaemon,
|
|
55
|
+
toggleRouterDashboardProbePause,
|
|
56
|
+
} from './router-dashboard.js'
|
|
45
57
|
|
|
46
58
|
// 📖 Some providers need an explicit probe model because the first catalog entry
|
|
47
59
|
// 📖 is not guaranteed to be accepted by their chat endpoint.
|
|
48
60
|
const PROVIDER_TEST_MODEL_OVERRIDES = {
|
|
49
|
-
sambanova: ['DeepSeek-V3-
|
|
50
|
-
nvidia: ['deepseek-ai/deepseek-
|
|
61
|
+
sambanova: ['MiniMax-M2.5', 'DeepSeek-V3.1', 'DeepSeek-V3.2'],
|
|
62
|
+
nvidia: ['deepseek-ai/deepseek-v4-flash', 'openai/gpt-oss-120b'],
|
|
63
|
+
'github-models': ['openai/gpt-4.1-mini'],
|
|
64
|
+
mistral: ['mistral-small-latest', 'devstral-small-latest'],
|
|
51
65
|
}
|
|
52
66
|
|
|
53
67
|
// 📖 Settings key tests retry retryable failures across several models so a
|
|
@@ -67,6 +81,7 @@ const PROVIDER_AUTH_ENDPOINTS = {
|
|
|
67
81
|
cerebras: { url: 'https://api.cerebras.ai/v1/models', method: 'GET' },
|
|
68
82
|
sambanova: { url: 'https://api.sambanova.ai/v1/models', method: 'GET' },
|
|
69
83
|
openrouter: { url: 'https://openrouter.ai/api/v1/models', method: 'GET' },
|
|
84
|
+
mistral: { url: 'https://api.mistral.ai/v1/models', method: 'GET' },
|
|
70
85
|
huggingface: { url: 'https://router.huggingface.co/v1/models', method: 'GET' },
|
|
71
86
|
deepinfra: { url: 'https://api.deepinfra.com/v1/models', method: 'GET' },
|
|
72
87
|
fireworks: { url: 'https://api.fireworks.ai/v1/models', method: 'GET' },
|
|
@@ -79,6 +94,7 @@ const PROVIDER_AUTH_ENDPOINTS = {
|
|
|
79
94
|
ovhcloud: { url: 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1/models', method: 'GET' },
|
|
80
95
|
qwen: { url: 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1/models', method: 'GET' },
|
|
81
96
|
iflow: { url: 'https://apis.iflow.cn/v1/models', method: 'GET' },
|
|
97
|
+
'github-models': null, // 📖 GitHub Models catalog is public; use chat ping to validate the token.
|
|
82
98
|
replicate: null, // 📖 Replicate has no /models endpoint; use chat completions ping
|
|
83
99
|
cloudflare: null, // 📖 Workers AI has no auth-check endpoint; use ping only
|
|
84
100
|
zai: null, // 📖 ZAI undocumented; use ping only
|
|
@@ -363,6 +379,40 @@ export function createKeyHandler(ctx) {
|
|
|
363
379
|
})
|
|
364
380
|
}
|
|
365
381
|
|
|
382
|
+
async function syncFavoritesToRouter(selected) {
|
|
383
|
+
if (state.config?.router?.enabled !== true) return
|
|
384
|
+
const favorites = state.config.favorites || []
|
|
385
|
+
const selKey = toFavoriteKey(selected.providerKey, selected.modelId)
|
|
386
|
+
const chain = [selKey, ...favorites.filter((f) => f !== selKey)]
|
|
387
|
+
const models = chain.map((f, i) => {
|
|
388
|
+
const slashIdx = f.indexOf('/')
|
|
389
|
+
const provider = slashIdx >= 0 ? f.slice(0, slashIdx) : '?'
|
|
390
|
+
const model = slashIdx >= 0 ? f.slice(slashIdx + 1) : f
|
|
391
|
+
return { provider, model, priority: i + 1 }
|
|
392
|
+
})
|
|
393
|
+
try {
|
|
394
|
+
const port = await readDaemonPort()
|
|
395
|
+
if (!port) return
|
|
396
|
+
const baseUrl = `http://127.0.0.1:${port}`
|
|
397
|
+
const setPayload = { name: 'fast-coding', models, created: new Date().toISOString() }
|
|
398
|
+
await globalThis.fetch(`${baseUrl}/sets/fast-coding`, {
|
|
399
|
+
method: 'PUT',
|
|
400
|
+
headers: { 'Content-Type': 'application/json' },
|
|
401
|
+
body: JSON.stringify(setPayload),
|
|
402
|
+
})
|
|
403
|
+
await globalThis.fetch(`${baseUrl}/sets/fast-coding/activate`, { method: 'POST' })
|
|
404
|
+
} catch {}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function readDaemonPort() {
|
|
408
|
+
try {
|
|
409
|
+
const { readFileSync } = await import('node:fs')
|
|
410
|
+
const raw = readFileSync(`${process.env.HOME}/.free-coding-models-daemon.port`, 'utf8').trim()
|
|
411
|
+
if (/^\d+$/.test(raw)) return Number(raw)
|
|
412
|
+
} catch {}
|
|
413
|
+
return null
|
|
414
|
+
}
|
|
415
|
+
|
|
366
416
|
async function launchSelectedModel(selected, options = {}) {
|
|
367
417
|
const { uiAlreadyStopped = false } = options
|
|
368
418
|
userSelected = { modelId: selected.modelId, label: selected.label, tier: selected.tier, providerKey: selected.providerKey }
|
|
@@ -373,7 +423,9 @@ export function createKeyHandler(ctx) {
|
|
|
373
423
|
stopUi()
|
|
374
424
|
}
|
|
375
425
|
|
|
376
|
-
// 📖
|
|
426
|
+
// 📖 If router is enabled, push [selected, ...favorites] to daemon as the active set
|
|
427
|
+
await syncFavoritesToRouter(selected)
|
|
428
|
+
|
|
377
429
|
if (selected.status === 'timeout') {
|
|
378
430
|
console.log(chalk.yellow(` ⚠ Selected: ${selected.label} (currently timing out)`))
|
|
379
431
|
} else if (selected.status === 'down') {
|
|
@@ -928,6 +980,51 @@ export function createKeyHandler(ctx) {
|
|
|
928
980
|
}
|
|
929
981
|
}
|
|
930
982
|
|
|
983
|
+
|
|
984
|
+
// 📖 Token Usage screen — Shift+T from main table. Fetches daily token history
|
|
985
|
+
// 📖 from the daemon and renders a 7-day chart plus today/all-time breakdowns.
|
|
986
|
+
async function openTokenUsageOverlay() {
|
|
987
|
+
state.tokenUsageOpen = true
|
|
988
|
+
state.tokenUsageScrollOffset = 0
|
|
989
|
+
state.tokenUsageError = null
|
|
990
|
+
state.tokenUsageData = null
|
|
991
|
+
// 📖 Discover daemon port
|
|
992
|
+
let port = 19280
|
|
993
|
+
try {
|
|
994
|
+
const { readFileSync: rfs } = await import('node:fs')
|
|
995
|
+
const portPath = `${process.env.HOME}/.free-coding-models-daemon.port`
|
|
996
|
+
const savedPort = rfs(portPath, 'utf8').trim()
|
|
997
|
+
if (/^\d+$/.test(savedPort)) port = Number(savedPort)
|
|
998
|
+
} catch {}
|
|
999
|
+
state.tokenUsageLastFetchAt = Date.now()
|
|
1000
|
+
try {
|
|
1001
|
+
const controller = new AbortController()
|
|
1002
|
+
const timer = setTimeout(() => controller.abort(), 2000)
|
|
1003
|
+
const res = await globalThis.fetch(`http://127.0.0.1:${port}/stats/tokens`, { signal: controller.signal })
|
|
1004
|
+
clearTimeout(timer)
|
|
1005
|
+
if (!res.ok) {
|
|
1006
|
+
state.tokenUsageError = `Daemon returned HTTP ${res.status} — is the router running?`
|
|
1007
|
+
return
|
|
1008
|
+
}
|
|
1009
|
+
// 📖 Guard: res.json() can throw on malformed response body
|
|
1010
|
+
const text = await res.text()
|
|
1011
|
+
try {
|
|
1012
|
+
state.tokenUsageData = JSON.parse(text)
|
|
1013
|
+
} catch {
|
|
1014
|
+
state.tokenUsageError = 'Daemon returned invalid JSON — try restarting the daemon'
|
|
1015
|
+
}
|
|
1016
|
+
} catch (err) {
|
|
1017
|
+
state.tokenUsageError = err?.name === 'AbortError' ? 'Request timed out — is the router daemon running?' : (err?.message || 'Failed to fetch token stats')
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function closeTokenUsageOverlay() {
|
|
1022
|
+
state.tokenUsageOpen = false
|
|
1023
|
+
state.tokenUsageScrollOffset = 0
|
|
1024
|
+
state.tokenUsageError = null
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
|
|
931
1028
|
function cycleToolMode() {
|
|
932
1029
|
const modeOrder = getToolModeOrder()
|
|
933
1030
|
const currentIndex = modeOrder.indexOf(state.mode)
|
|
@@ -1016,6 +1113,7 @@ export function createKeyHandler(ctx) {
|
|
|
1016
1113
|
|| state.installEndpointsOpen
|
|
1017
1114
|
|| state.toolInstallPromptOpen
|
|
1018
1115
|
|| state.installedModelsOpen
|
|
1116
|
+
|| state.routerDashboardOpen
|
|
1019
1117
|
|| state.recommendOpen
|
|
1020
1118
|
|| state.feedbackOpen
|
|
1021
1119
|
|| state.helpVisible
|
|
@@ -1207,6 +1305,8 @@ export function createKeyHandler(ctx) {
|
|
|
1207
1305
|
case 'open-changelog': return openChangelogOverlay()
|
|
1208
1306
|
case 'open-feedback': return openFeedbackOverlay()
|
|
1209
1307
|
case 'open-recommend': return openRecommendOverlay()
|
|
1308
|
+
case 'open-router-dashboard': return openRouterDashboardOverlay(state)
|
|
1309
|
+
case 'open-token-usage': return openTokenUsageOverlay()
|
|
1210
1310
|
case 'open-install-endpoints': return openInstallEndpointsOverlay()
|
|
1211
1311
|
case 'open-installed-models': return openInstalledModelsOverlay()
|
|
1212
1312
|
case 'action-cycle-theme': return cycleGlobalTheme()
|
|
@@ -1340,6 +1440,59 @@ export function createKeyHandler(ctx) {
|
|
|
1340
1440
|
|
|
1341
1441
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
1342
1442
|
|
|
1443
|
+
// 📖 Router Dashboard captures local dashboard controls while open so keys
|
|
1444
|
+
// 📖 like S/I/R/C do not leak through to sort/filter actions in the table.
|
|
1445
|
+
if (state.routerDashboardOpen) {
|
|
1446
|
+
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
1447
|
+
const pageStep = Math.max(1, (state.terminalRows || 1) - 4)
|
|
1448
|
+
|
|
1449
|
+
if (key.name === 'escape') {
|
|
1450
|
+
closeRouterDashboardOverlay(state)
|
|
1451
|
+
return
|
|
1452
|
+
}
|
|
1453
|
+
if (key.name === 'up' || key.name === 'k') {
|
|
1454
|
+
state.routerDashboardScrollOffset = Math.max(0, (state.routerDashboardScrollOffset || 0) - 1)
|
|
1455
|
+
return
|
|
1456
|
+
}
|
|
1457
|
+
if (key.name === 'down' || key.name === 'j') {
|
|
1458
|
+
state.routerDashboardScrollOffset = (state.routerDashboardScrollOffset || 0) + 1
|
|
1459
|
+
return
|
|
1460
|
+
}
|
|
1461
|
+
if (key.name === 'pageup') {
|
|
1462
|
+
state.routerDashboardScrollOffset = Math.max(0, (state.routerDashboardScrollOffset || 0) - pageStep)
|
|
1463
|
+
return
|
|
1464
|
+
}
|
|
1465
|
+
if (key.name === 'pagedown') {
|
|
1466
|
+
state.routerDashboardScrollOffset = (state.routerDashboardScrollOffset || 0) + pageStep
|
|
1467
|
+
return
|
|
1468
|
+
}
|
|
1469
|
+
if (key.name === 'home') {
|
|
1470
|
+
state.routerDashboardScrollOffset = 0
|
|
1471
|
+
return
|
|
1472
|
+
}
|
|
1473
|
+
if (key.name === 's') {
|
|
1474
|
+
try { await cycleRouterDashboardActiveSet(state) } catch {}
|
|
1475
|
+
return
|
|
1476
|
+
}
|
|
1477
|
+
if (key.name === 'i') {
|
|
1478
|
+
try { await cycleRouterDashboardProbeMode(state) } catch {}
|
|
1479
|
+
return
|
|
1480
|
+
}
|
|
1481
|
+
if (key.name === 'r') {
|
|
1482
|
+
restartRouterDashboardDaemon(state)
|
|
1483
|
+
return
|
|
1484
|
+
}
|
|
1485
|
+
if (key.name === 'c') {
|
|
1486
|
+
clearRouterDashboardRequestLog(state)
|
|
1487
|
+
return
|
|
1488
|
+
}
|
|
1489
|
+
if (key.name === 'p') {
|
|
1490
|
+
toggleRouterDashboardProbePause(state)
|
|
1491
|
+
return
|
|
1492
|
+
}
|
|
1493
|
+
return
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1343
1496
|
// 📖 Install Endpoints overlay: provider → tool → connection → scope → optional model subset.
|
|
1344
1497
|
if (state.installEndpointsOpen) {
|
|
1345
1498
|
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
@@ -1820,6 +1973,116 @@ export function createKeyHandler(ctx) {
|
|
|
1820
1973
|
return
|
|
1821
1974
|
}
|
|
1822
1975
|
|
|
1976
|
+
// 📖 Token Usage overlay: Shift+T shows token history chart and today/all-time breakdowns.
|
|
1977
|
+
if (state.tokenUsageOpen) {
|
|
1978
|
+
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
1979
|
+
const pageStep = Math.max(1, (state.terminalRows || 1) - 4)
|
|
1980
|
+
if (key.name === 'escape') {
|
|
1981
|
+
closeTokenUsageOverlay()
|
|
1982
|
+
return
|
|
1983
|
+
}
|
|
1984
|
+
if (key.name === 'up' || key.name === 'k') {
|
|
1985
|
+
state.tokenUsageScrollOffset = Math.max(0, state.tokenUsageScrollOffset - 1)
|
|
1986
|
+
return
|
|
1987
|
+
}
|
|
1988
|
+
if (key.name === 'down' || key.name === 'j') {
|
|
1989
|
+
state.tokenUsageScrollOffset += 1
|
|
1990
|
+
return
|
|
1991
|
+
}
|
|
1992
|
+
if (key.name === 'pageup') {
|
|
1993
|
+
state.tokenUsageScrollOffset = Math.max(0, state.tokenUsageScrollOffset - pageStep)
|
|
1994
|
+
return
|
|
1995
|
+
}
|
|
1996
|
+
if (key.name === 'pagedown') {
|
|
1997
|
+
state.tokenUsageScrollOffset += pageStep
|
|
1998
|
+
return
|
|
1999
|
+
}
|
|
2000
|
+
if (key.name === 'home') {
|
|
2001
|
+
state.tokenUsageScrollOffset = 0
|
|
2002
|
+
return
|
|
2003
|
+
}
|
|
2004
|
+
if (key.name === 'end') {
|
|
2005
|
+
state.tokenUsageScrollOffset = Number.MAX_SAFE_INTEGER
|
|
2006
|
+
return
|
|
2007
|
+
}
|
|
2008
|
+
return
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
// 📖 Router Onboarding overlay: shown on first launch. Y=yes enable, N=not now, Esc=cancel.
|
|
2012
|
+
if (state.routerOnboardingOpen) {
|
|
2013
|
+
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
2014
|
+
if (state.routerOnboardingPhase === 'loading' || state.routerOnboardingPhase === 'success' || state.routerOnboardingPhase === 'error') {
|
|
2015
|
+
if (key.name === 'escape' || key.name === 'return') {
|
|
2016
|
+
state.routerOnboardingOpen = false
|
|
2017
|
+
// 📖 Mark onboarding as seen (don't show again)
|
|
2018
|
+
if (state.config?.router) {
|
|
2019
|
+
state.config.router.onboardingSeen = true
|
|
2020
|
+
}
|
|
2021
|
+
return
|
|
2022
|
+
}
|
|
2023
|
+
return
|
|
2024
|
+
}
|
|
2025
|
+
if (key.name === 'escape' || key.name === 'n') {
|
|
2026
|
+
state.routerOnboardingOpen = false
|
|
2027
|
+
// 📖 Mark as seen and disabled
|
|
2028
|
+
if (state.config?.router) {
|
|
2029
|
+
state.config.router.onboardingSeen = true
|
|
2030
|
+
state.config.router.enabled = false
|
|
2031
|
+
}
|
|
2032
|
+
return
|
|
2033
|
+
}
|
|
2034
|
+
if (key.name === 'up' || key.name === 'k') {
|
|
2035
|
+
state.routerOnboardingCursor = 0
|
|
2036
|
+
return
|
|
2037
|
+
}
|
|
2038
|
+
if (key.name === 'down' || key.name === 'j') {
|
|
2039
|
+
state.routerOnboardingCursor = 1
|
|
2040
|
+
return
|
|
2041
|
+
}
|
|
2042
|
+
if (key.name === 'return' || key.name === 'y') {
|
|
2043
|
+
const shouldEnable = key.name === 'return' ? true : (state.routerOnboardingCursor === 0)
|
|
2044
|
+
if (!shouldEnable) {
|
|
2045
|
+
state.routerOnboardingOpen = false
|
|
2046
|
+
if (state.config?.router) {
|
|
2047
|
+
state.config.router.onboardingSeen = true
|
|
2048
|
+
state.config.router.enabled = false
|
|
2049
|
+
}
|
|
2050
|
+
return
|
|
2051
|
+
}
|
|
2052
|
+
// 📖 Enable router: start daemon in background and mark onboarding seen
|
|
2053
|
+
state.routerOnboardingPhase = 'loading'
|
|
2054
|
+
state.routerOnboardingError = null
|
|
2055
|
+
void (async () => {
|
|
2056
|
+
try {
|
|
2057
|
+
const binPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'free-coding-models.js')
|
|
2058
|
+
const child = spawn('node', [binPath, '--daemon-bg'], {
|
|
2059
|
+
detached: true,
|
|
2060
|
+
stdio: 'ignore',
|
|
2061
|
+
})
|
|
2062
|
+
child.unref()
|
|
2063
|
+
await new Promise((r) => setTimeout(r, 2000))
|
|
2064
|
+
if (state.routerOnboardingPhase === 'loading') {
|
|
2065
|
+
state.routerOnboardingPhase = 'success'
|
|
2066
|
+
if (state.config?.router) {
|
|
2067
|
+
state.config.router.enabled = true
|
|
2068
|
+
state.config.router.onboardingSeen = true
|
|
2069
|
+
saveConfig(state.config)
|
|
2070
|
+
}
|
|
2071
|
+
trackTelemetryEvent('app_router_install', { router_version: '0.4.0' })
|
|
2072
|
+
await new Promise((r) => setTimeout(r, 1500))
|
|
2073
|
+
state.routerOnboardingOpen = false
|
|
2074
|
+
openRouterDashboardOverlay(state)
|
|
2075
|
+
}
|
|
2076
|
+
} catch (err) {
|
|
2077
|
+
state.routerOnboardingPhase = 'error'
|
|
2078
|
+
state.routerOnboardingError = err?.message || 'Failed to start router'
|
|
2079
|
+
}
|
|
2080
|
+
})()
|
|
2081
|
+
return
|
|
2082
|
+
}
|
|
2083
|
+
return
|
|
2084
|
+
}
|
|
2085
|
+
|
|
1823
2086
|
// 📖 Changelog overlay: two-phase (index + details) with keyboard navigation
|
|
1824
2087
|
if (state.changelogOpen) {
|
|
1825
2088
|
const pageStep = Math.max(1, (state.terminalRows || 1) - 2)
|
|
@@ -2366,9 +2629,15 @@ export function createKeyHandler(ctx) {
|
|
|
2366
2629
|
|
|
2367
2630
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
2368
2631
|
|
|
2369
|
-
// 📖 Shift+R:
|
|
2370
|
-
if (key.name === 'r' && key.shift) {
|
|
2371
|
-
|
|
2632
|
+
// 📖 Shift+R: open the Smart Model Router dashboard from the main table.
|
|
2633
|
+
if (key.name === 'r' && key.shift && !key.ctrl && !key.meta) {
|
|
2634
|
+
openRouterDashboardOverlay(state)
|
|
2635
|
+
return
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
// 📖 Shift+T: open the Token Usage screen.
|
|
2639
|
+
if (key.name === 't' && key.shift && !key.ctrl && !key.meta) {
|
|
2640
|
+
openTokenUsageOverlay()
|
|
2372
2641
|
return
|
|
2373
2642
|
}
|
|
2374
2643
|
|
|
@@ -2383,7 +2652,7 @@ export function createKeyHandler(ctx) {
|
|
|
2383
2652
|
// 📖 T is reserved for tier filter cycling. Y toggles favorites display mode.
|
|
2384
2653
|
// 📖 X clears the active custom text filter.
|
|
2385
2654
|
// 📖 D is now reserved for provider filter cycling
|
|
2386
|
-
// 📖 Shift+R is reserved for reset view
|
|
2655
|
+
// 📖 Shift+R is reserved for the Router Dashboard; reset view remains in Ctrl+P.
|
|
2387
2656
|
const sortKeys = {
|
|
2388
2657
|
'r': 'rank', 'o': 'origin', 'm': 'model',
|
|
2389
2658
|
'l': 'ping', 'a': 'avg', 's': 'swe', 'c': 'ctx', 'h': 'condition', 'v': 'verdict', 'b': 'stability', 'u': 'uptime'
|
|
@@ -2502,6 +2771,28 @@ export function createKeyHandler(ctx) {
|
|
|
2502
2771
|
return
|
|
2503
2772
|
}
|
|
2504
2773
|
|
|
2774
|
+
if (key.shift && key.name === 'up') {
|
|
2775
|
+
const selected = state.visibleSorted?.[state.cursor]
|
|
2776
|
+
if (selected?.isFavorite) {
|
|
2777
|
+
reorderFavorite(state.config, selected.providerKey, selected.modelId, 'up')
|
|
2778
|
+
syncFavoriteFlags(state.results, state.config)
|
|
2779
|
+
applyTierFilter()
|
|
2780
|
+
refreshVisibleSorted({ resetCursor: false })
|
|
2781
|
+
}
|
|
2782
|
+
return
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2785
|
+
if (key.shift && key.name === 'down') {
|
|
2786
|
+
const selected = state.visibleSorted?.[state.cursor]
|
|
2787
|
+
if (selected?.isFavorite) {
|
|
2788
|
+
reorderFavorite(state.config, selected.providerKey, selected.modelId, 'down')
|
|
2789
|
+
syncFavoriteFlags(state.results, state.config)
|
|
2790
|
+
applyTierFilter()
|
|
2791
|
+
refreshVisibleSorted({ resetCursor: false })
|
|
2792
|
+
}
|
|
2793
|
+
return
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2505
2796
|
if (key.name === 'up' || key.name === 'k') {
|
|
2506
2797
|
// 📖 Main list wrap navigation: top -> bottom on Up / K (vim-style).
|
|
2507
2798
|
const count = state.visibleSorted.length
|
|
@@ -2527,12 +2818,9 @@ export function createKeyHandler(ctx) {
|
|
|
2527
2818
|
}
|
|
2528
2819
|
|
|
2529
2820
|
if (key.name === 'return') { // Enter
|
|
2530
|
-
// 📖 Use the cached visible+sorted array — guaranteed to match what's on screen
|
|
2531
2821
|
const selected = state.visibleSorted[state.cursor]
|
|
2532
|
-
if (!selected) return
|
|
2822
|
+
if (!selected) return
|
|
2533
2823
|
|
|
2534
|
-
// 📖 Incompatibility intercept — if the model can't run on the active tool,
|
|
2535
|
-
// 📖 show the fallback overlay instead of launching. Lets user switch tool or pick similar model.
|
|
2536
2824
|
if (!isModelCompatibleWithTool(selected.providerKey, state.mode)) {
|
|
2537
2825
|
const compatTools = getCompatibleTools(selected.providerKey)
|
|
2538
2826
|
const similarModels = findSimilarCompatibleModels(
|
|
@@ -2763,6 +3051,11 @@ export function createMouseEventHandler(ctx) {
|
|
|
2763
3051
|
}
|
|
2764
3052
|
return
|
|
2765
3053
|
}
|
|
3054
|
+
if (state.routerDashboardOpen) {
|
|
3055
|
+
const step = evt.type === 'scroll-up' ? -3 : 3
|
|
3056
|
+
state.routerDashboardScrollOffset = Math.max(0, (state.routerDashboardScrollOffset || 0) + step)
|
|
3057
|
+
return
|
|
3058
|
+
}
|
|
2766
3059
|
|
|
2767
3060
|
// 📖 Main table scroll: move cursor up/down with wrap-around
|
|
2768
3061
|
const count = state.visibleSorted.length
|
|
@@ -2843,6 +3136,11 @@ export function createMouseEventHandler(ctx) {
|
|
|
2843
3136
|
return
|
|
2844
3137
|
}
|
|
2845
3138
|
|
|
3139
|
+
if (state.routerDashboardOpen) {
|
|
3140
|
+
closeRouterDashboardOverlay(state)
|
|
3141
|
+
return
|
|
3142
|
+
}
|
|
3143
|
+
|
|
2846
3144
|
if (state.incompatibleFallbackOpen) {
|
|
2847
3145
|
// 📖 Incompatible fallback: click closes
|
|
2848
3146
|
state.incompatibleFallbackOpen = false
|
|
@@ -3004,6 +3302,8 @@ export function createMouseEventHandler(ctx) {
|
|
|
3004
3302
|
// 📖 Most are single-character keys; special cases like ctrl+p need special handling.
|
|
3005
3303
|
if (zone.key === 'ctrl+p') {
|
|
3006
3304
|
process.stdin.emit('keypress', '\x10', { name: 'p', ctrl: true, meta: false, shift: false })
|
|
3305
|
+
} else if (zone.key === 'shift+r') {
|
|
3306
|
+
process.stdin.emit('keypress', 'R', { name: 'r', ctrl: false, meta: false, shift: true })
|
|
3007
3307
|
} else {
|
|
3008
3308
|
process.stdin.emit('keypress', zone.key, { name: zone.key, ctrl: false, meta: false, shift: false })
|
|
3009
3309
|
}
|
package/src/kilo.js
CHANGED
|
@@ -9,6 +9,7 @@ import { loadKiloConfig, saveKiloConfig, getKiloConfigPath } from './kilo-config
|
|
|
9
9
|
import { getApiKey } from './config.js'
|
|
10
10
|
import { ENV_VAR_NAMES, OPENCODE_MODEL_MAP } from './provider-metadata.js'
|
|
11
11
|
import { resolveToolBinaryPath } from './tool-bootstrap.js'
|
|
12
|
+
import { sources } from '../sources.js'
|
|
12
13
|
|
|
13
14
|
// 📖 Map source model IDs to Kilo built-in IDs (same as OpenCode).
|
|
14
15
|
function getKiloModelId(providerKey, modelId) {
|
|
@@ -17,6 +18,21 @@ function getKiloModelId(providerKey, modelId) {
|
|
|
17
18
|
return OPENCODE_MODEL_MAP[providerKey]?.[modelId] || modelId
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
function buildOpenAiCompatibleProviderConfig(providerKey) {
|
|
22
|
+
const source = sources[providerKey]
|
|
23
|
+
const envVarName = ENV_VAR_NAMES[providerKey]
|
|
24
|
+
if (!source?.url || !envVarName) return null
|
|
25
|
+
const baseURL = source.url
|
|
26
|
+
.replace(/\/chat\/completions$/i, '')
|
|
27
|
+
.replace(/\/responses$/i, '')
|
|
28
|
+
return {
|
|
29
|
+
npm: '@ai-sdk/openai-compatible',
|
|
30
|
+
name: source.name || providerKey,
|
|
31
|
+
options: { baseURL, apiKey: `{env:${envVarName}}` },
|
|
32
|
+
models: {},
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
20
36
|
// 📖 spawnKilo: Resolve API keys + spawn kilo CLI with correct env.
|
|
21
37
|
async function spawnKilo(args, providerKey, fcmConfig) {
|
|
22
38
|
const envVarName = ENV_VAR_NAMES[providerKey]
|
|
@@ -120,7 +136,7 @@ export async function startKilo(model, fcmConfig) {
|
|
|
120
136
|
config.provider.codestral = {
|
|
121
137
|
npm: '@ai-sdk/openai-compatible',
|
|
122
138
|
name: 'Mistral Codestral',
|
|
123
|
-
options: { baseURL: 'https://api.mistral.ai/v1', apiKey: '{env:
|
|
139
|
+
options: { baseURL: 'https://api.mistral.ai/v1', apiKey: '{env:MISTRAL_API_KEY}' },
|
|
124
140
|
models: {}
|
|
125
141
|
}
|
|
126
142
|
} else if (providerKey === 'hyperbolic') {
|
|
@@ -179,6 +195,9 @@ export async function startKilo(model, fcmConfig) {
|
|
|
179
195
|
options: { baseURL: 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1', apiKey: '{env:OVH_AI_ENDPOINTS_ACCESS_TOKEN}' },
|
|
180
196
|
models: {}
|
|
181
197
|
}
|
|
198
|
+
} else {
|
|
199
|
+
const providerConfig = buildOpenAiCompatibleProviderConfig(providerKey)
|
|
200
|
+
if (providerConfig) config.provider[providerKey] = providerConfig
|
|
182
201
|
}
|
|
183
202
|
}
|
|
184
203
|
|
package/src/opencode.js
CHANGED
|
@@ -98,6 +98,21 @@ function getOpenCodeModelId(providerKey, modelId) {
|
|
|
98
98
|
return OPENCODE_MODEL_MAP[providerKey]?.[modelId] || modelId
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
function buildOpenAiCompatibleProviderConfig(providerKey) {
|
|
102
|
+
const source = sources[providerKey]
|
|
103
|
+
const envVarName = ENV_VAR_NAMES[providerKey]
|
|
104
|
+
if (!source?.url || !envVarName) return null
|
|
105
|
+
const baseURL = source.url
|
|
106
|
+
.replace(/\/chat\/completions$/i, '')
|
|
107
|
+
.replace(/\/responses$/i, '')
|
|
108
|
+
return {
|
|
109
|
+
npm: '@ai-sdk/openai-compatible',
|
|
110
|
+
name: source.name || providerKey,
|
|
111
|
+
options: { baseURL, apiKey: `{env:${envVarName}}` },
|
|
112
|
+
models: {},
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
101
116
|
// ─── ZAI proxy bridge ─────────────────────────────────────────────────────────
|
|
102
117
|
|
|
103
118
|
// 📖 createZaiProxy: Localhost reverse proxy bridging ZAI's non-standard API paths
|
|
@@ -434,7 +449,7 @@ export async function startOpenCode(model, fcmConfig) {
|
|
|
434
449
|
config.provider.codestral = {
|
|
435
450
|
npm: '@ai-sdk/openai-compatible',
|
|
436
451
|
name: 'Mistral Codestral',
|
|
437
|
-
options: { baseURL: 'https://api.mistral.ai/v1', apiKey: '{env:
|
|
452
|
+
options: { baseURL: 'https://api.mistral.ai/v1', apiKey: '{env:MISTRAL_API_KEY}' },
|
|
438
453
|
models: {}
|
|
439
454
|
}
|
|
440
455
|
} else if (providerKey === 'hyperbolic') {
|
|
@@ -514,6 +529,9 @@ export async function startOpenCode(model, fcmConfig) {
|
|
|
514
529
|
options: { baseURL: 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1', apiKey: '{env:OVH_AI_ENDPOINTS_ACCESS_TOKEN}' },
|
|
515
530
|
models: {}
|
|
516
531
|
}
|
|
532
|
+
} else {
|
|
533
|
+
const providerConfig = buildOpenAiCompatibleProviderConfig(providerKey)
|
|
534
|
+
if (providerConfig) config.provider[providerKey] = providerConfig
|
|
517
535
|
}
|
|
518
536
|
}
|
|
519
537
|
|
|
@@ -799,7 +817,7 @@ export async function startOpenCodeDesktop(model, fcmConfig) {
|
|
|
799
817
|
config.provider.codestral = {
|
|
800
818
|
npm: '@ai-sdk/openai-compatible',
|
|
801
819
|
name: 'Mistral Codestral',
|
|
802
|
-
options: { baseURL: 'https://api.mistral.ai/v1', apiKey: '{env:
|
|
820
|
+
options: { baseURL: 'https://api.mistral.ai/v1', apiKey: '{env:MISTRAL_API_KEY}' },
|
|
803
821
|
models: {}
|
|
804
822
|
}
|
|
805
823
|
} else if (providerKey === 'hyperbolic') {
|
|
@@ -879,6 +897,9 @@ export async function startOpenCodeDesktop(model, fcmConfig) {
|
|
|
879
897
|
options: { baseURL: 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1', apiKey: '{env:OVH_AI_ENDPOINTS_ACCESS_TOKEN}' },
|
|
880
898
|
models: {}
|
|
881
899
|
}
|
|
900
|
+
} else {
|
|
901
|
+
const providerConfig = buildOpenAiCompatibleProviderConfig(providerKey)
|
|
902
|
+
if (providerConfig) config.provider[providerKey] = providerConfig
|
|
882
903
|
}
|
|
883
904
|
}
|
|
884
905
|
|