free-coding-models 0.3.54 → 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 -49
- 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 +316 -13
- package/src/kilo.js +20 -1
- package/src/opencode.js +24 -3
- 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-D2ban2S-.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
|
|
@@ -307,7 +323,10 @@ export function createKeyHandler(ctx) {
|
|
|
307
323
|
}
|
|
308
324
|
|
|
309
325
|
function shouldCheckMissingTool(mode) {
|
|
310
|
-
|
|
326
|
+
// 📖 opencode-desktop doesn't have a binary check (it uses 'open -a').
|
|
327
|
+
// 📖 opencode-web, opencode, and kilo manage their own ENOENT errors in spawn handlers.
|
|
328
|
+
// 📖 xcode uses 'open -a Xcode' which doesn't need a binary path resolution.
|
|
329
|
+
return !['opencode-desktop', 'opencode-web', 'opencode', 'kilo', 'xcode'].includes(mode)
|
|
311
330
|
}
|
|
312
331
|
|
|
313
332
|
function getModelTelemetryFamily(providerKey) {
|
|
@@ -360,6 +379,40 @@ export function createKeyHandler(ctx) {
|
|
|
360
379
|
})
|
|
361
380
|
}
|
|
362
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
|
+
|
|
363
416
|
async function launchSelectedModel(selected, options = {}) {
|
|
364
417
|
const { uiAlreadyStopped = false } = options
|
|
365
418
|
userSelected = { modelId: selected.modelId, label: selected.label, tier: selected.tier, providerKey: selected.providerKey }
|
|
@@ -370,7 +423,9 @@ export function createKeyHandler(ctx) {
|
|
|
370
423
|
stopUi()
|
|
371
424
|
}
|
|
372
425
|
|
|
373
|
-
// 📖
|
|
426
|
+
// 📖 If router is enabled, push [selected, ...favorites] to daemon as the active set
|
|
427
|
+
await syncFavoritesToRouter(selected)
|
|
428
|
+
|
|
374
429
|
if (selected.status === 'timeout') {
|
|
375
430
|
console.log(chalk.yellow(` ⚠ Selected: ${selected.label} (currently timing out)`))
|
|
376
431
|
} else if (selected.status === 'down') {
|
|
@@ -925,6 +980,51 @@ export function createKeyHandler(ctx) {
|
|
|
925
980
|
}
|
|
926
981
|
}
|
|
927
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
|
+
|
|
928
1028
|
function cycleToolMode() {
|
|
929
1029
|
const modeOrder = getToolModeOrder()
|
|
930
1030
|
const currentIndex = modeOrder.indexOf(state.mode)
|
|
@@ -1013,6 +1113,7 @@ export function createKeyHandler(ctx) {
|
|
|
1013
1113
|
|| state.installEndpointsOpen
|
|
1014
1114
|
|| state.toolInstallPromptOpen
|
|
1015
1115
|
|| state.installedModelsOpen
|
|
1116
|
+
|| state.routerDashboardOpen
|
|
1016
1117
|
|| state.recommendOpen
|
|
1017
1118
|
|| state.feedbackOpen
|
|
1018
1119
|
|| state.helpVisible
|
|
@@ -1204,6 +1305,8 @@ export function createKeyHandler(ctx) {
|
|
|
1204
1305
|
case 'open-changelog': return openChangelogOverlay()
|
|
1205
1306
|
case 'open-feedback': return openFeedbackOverlay()
|
|
1206
1307
|
case 'open-recommend': return openRecommendOverlay()
|
|
1308
|
+
case 'open-router-dashboard': return openRouterDashboardOverlay(state)
|
|
1309
|
+
case 'open-token-usage': return openTokenUsageOverlay()
|
|
1207
1310
|
case 'open-install-endpoints': return openInstallEndpointsOverlay()
|
|
1208
1311
|
case 'open-installed-models': return openInstalledModelsOverlay()
|
|
1209
1312
|
case 'action-cycle-theme': return cycleGlobalTheme()
|
|
@@ -1337,6 +1440,59 @@ export function createKeyHandler(ctx) {
|
|
|
1337
1440
|
|
|
1338
1441
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
1339
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
|
+
|
|
1340
1496
|
// 📖 Install Endpoints overlay: provider → tool → connection → scope → optional model subset.
|
|
1341
1497
|
if (state.installEndpointsOpen) {
|
|
1342
1498
|
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
@@ -1817,6 +1973,116 @@ export function createKeyHandler(ctx) {
|
|
|
1817
1973
|
return
|
|
1818
1974
|
}
|
|
1819
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
|
+
|
|
1820
2086
|
// 📖 Changelog overlay: two-phase (index + details) with keyboard navigation
|
|
1821
2087
|
if (state.changelogOpen) {
|
|
1822
2088
|
const pageStep = Math.max(1, (state.terminalRows || 1) - 2)
|
|
@@ -2363,9 +2629,15 @@ export function createKeyHandler(ctx) {
|
|
|
2363
2629
|
|
|
2364
2630
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
2365
2631
|
|
|
2366
|
-
// 📖 Shift+R:
|
|
2367
|
-
if (key.name === 'r' && key.shift) {
|
|
2368
|
-
|
|
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()
|
|
2369
2641
|
return
|
|
2370
2642
|
}
|
|
2371
2643
|
|
|
@@ -2380,7 +2652,7 @@ export function createKeyHandler(ctx) {
|
|
|
2380
2652
|
// 📖 T is reserved for tier filter cycling. Y toggles favorites display mode.
|
|
2381
2653
|
// 📖 X clears the active custom text filter.
|
|
2382
2654
|
// 📖 D is now reserved for provider filter cycling
|
|
2383
|
-
// 📖 Shift+R is reserved for reset view
|
|
2655
|
+
// 📖 Shift+R is reserved for the Router Dashboard; reset view remains in Ctrl+P.
|
|
2384
2656
|
const sortKeys = {
|
|
2385
2657
|
'r': 'rank', 'o': 'origin', 'm': 'model',
|
|
2386
2658
|
'l': 'ping', 'a': 'avg', 's': 'swe', 'c': 'ctx', 'h': 'condition', 'v': 'verdict', 'b': 'stability', 'u': 'uptime'
|
|
@@ -2499,6 +2771,28 @@ export function createKeyHandler(ctx) {
|
|
|
2499
2771
|
return
|
|
2500
2772
|
}
|
|
2501
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
|
+
|
|
2502
2796
|
if (key.name === 'up' || key.name === 'k') {
|
|
2503
2797
|
// 📖 Main list wrap navigation: top -> bottom on Up / K (vim-style).
|
|
2504
2798
|
const count = state.visibleSorted.length
|
|
@@ -2524,12 +2818,9 @@ export function createKeyHandler(ctx) {
|
|
|
2524
2818
|
}
|
|
2525
2819
|
|
|
2526
2820
|
if (key.name === 'return') { // Enter
|
|
2527
|
-
// 📖 Use the cached visible+sorted array — guaranteed to match what's on screen
|
|
2528
2821
|
const selected = state.visibleSorted[state.cursor]
|
|
2529
|
-
if (!selected) return
|
|
2822
|
+
if (!selected) return
|
|
2530
2823
|
|
|
2531
|
-
// 📖 Incompatibility intercept — if the model can't run on the active tool,
|
|
2532
|
-
// 📖 show the fallback overlay instead of launching. Lets user switch tool or pick similar model.
|
|
2533
2824
|
if (!isModelCompatibleWithTool(selected.providerKey, state.mode)) {
|
|
2534
2825
|
const compatTools = getCompatibleTools(selected.providerKey)
|
|
2535
2826
|
const similarModels = findSimilarCompatibleModels(
|
|
@@ -2760,6 +3051,11 @@ export function createMouseEventHandler(ctx) {
|
|
|
2760
3051
|
}
|
|
2761
3052
|
return
|
|
2762
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
|
+
}
|
|
2763
3059
|
|
|
2764
3060
|
// 📖 Main table scroll: move cursor up/down with wrap-around
|
|
2765
3061
|
const count = state.visibleSorted.length
|
|
@@ -2840,6 +3136,11 @@ export function createMouseEventHandler(ctx) {
|
|
|
2840
3136
|
return
|
|
2841
3137
|
}
|
|
2842
3138
|
|
|
3139
|
+
if (state.routerDashboardOpen) {
|
|
3140
|
+
closeRouterDashboardOverlay(state)
|
|
3141
|
+
return
|
|
3142
|
+
}
|
|
3143
|
+
|
|
2843
3144
|
if (state.incompatibleFallbackOpen) {
|
|
2844
3145
|
// 📖 Incompatible fallback: click closes
|
|
2845
3146
|
state.incompatibleFallbackOpen = false
|
|
@@ -3001,6 +3302,8 @@ export function createMouseEventHandler(ctx) {
|
|
|
3001
3302
|
// 📖 Most are single-character keys; special cases like ctrl+p need special handling.
|
|
3002
3303
|
if (zone.key === 'ctrl+p') {
|
|
3003
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 })
|
|
3004
3307
|
} else {
|
|
3005
3308
|
process.stdin.emit('keypress', zone.key, { name: zone.key, ctrl: false, meta: false, shift: false })
|
|
3006
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
|
|
@@ -203,7 +218,7 @@ async function spawnOpenCode(args, providerKey, fcmConfig, existingZaiProxy = nu
|
|
|
203
218
|
if (zaiProxy) zaiProxy.close()
|
|
204
219
|
if (err.code === 'ENOENT') {
|
|
205
220
|
console.error(chalk.red('\n X Could not find "opencode" -- is it installed and in your PATH?'))
|
|
206
|
-
console.error(chalk.dim(' Install: npm i -g opencode or see https://opencode.ai'))
|
|
221
|
+
console.error(chalk.dim(' Install: npm i -g opencode-ai or see https://opencode.ai'))
|
|
207
222
|
resolve(1)
|
|
208
223
|
} else {
|
|
209
224
|
reject(err)
|
|
@@ -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
|
|