free-coding-models 0.3.60 → 0.3.63
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 +15 -5
- package/package.json +1 -1
- package/src/app.js +14 -2
- package/src/endpoint-installer.js +17 -0
- package/src/favorites.js +22 -1
- package/src/key-handler.js +143 -23
- package/src/overlays.js +6 -4
- package/src/render-table.js +5 -3
- package/src/router-daemon.js +11 -9
- package/src/router-dashboard.js +214 -59
- package/web/dist/assets/{index-BGUOjKTu.js → index-B_N6UmGP.js} +1 -1
- package/web/dist/index.html +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
|
-
## [0.3.
|
|
1
|
+
## [0.3.63] - 2026-05-05
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
|
|
5
|
+
- **Router daemon auth error false positives** — Fixed `health.every([])` returning `true` for empty arrays, which caused all models to be incorrectly marked as AUTH_ERROR when the daemon had no health data yet. Added explicit `health.length > 0` guards before `.every()` checks.
|
|
6
|
+
- **Daemon API key priority** — `getApiKeyForProvider` no longer falls back to shell env vars when a config key exists. The daemon now exclusively uses keys from `~/.free-coding-models.json`, preventing test/fake keys in shell env from overriding real configured keys.
|
|
2
7
|
|
|
3
8
|
### Changed
|
|
4
9
|
|
|
5
|
-
- **
|
|
6
|
-
- **
|
|
10
|
+
- **Shift+R now launches OpenCode** — Pressing Shift+R opens the Router Dashboard AND launches OpenCode with the currently selected model from the main table. If dashboard is already open, just resets scroll. Previously Shift+R only opened the dashboard without launching anything.
|
|
11
|
+
- **Favorites sync to router on launch** — When a model is launched from the TUI, it and the user's full favorites chain are synced to the daemon as the active set via `/sets/fast-coding`.
|
|
12
|
+
- **Router Dashboard install flow** — The "Install Router Endpoint to CLI Tool" button now opens the Install Endpoints overlay directly with `fcm_router` pre-selected, skipping the provider selection phase.
|
|
7
13
|
|
|
8
|
-
###
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **README_ROUTER.md** — New comprehensive documentation for the FCM Router daemon, covering setup, endpoints, routing behavior, and tool configuration.
|
|
17
|
+
|
|
18
|
+
### Changed (general)
|
|
9
19
|
|
|
10
|
-
- **
|
|
20
|
+
- **CTX column gradient improved** — Context window colorization now goes from red (32k) → orange (64k) → yellow (128k) → green (256k) → cyan/teal fluo (400k) → bold cyan+underline (1M+) so the biggest context windows stand out visually.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.63",
|
|
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/src/app.js
CHANGED
|
@@ -111,7 +111,7 @@ import { resolveCloudflareUrl, buildPingRequest, ping, extractQuotaPercent, getP
|
|
|
111
111
|
import { runFiableMode, filterByTierOrExit, fetchOpenRouterFreeModels } from '../src/analysis.js'
|
|
112
112
|
import { PROVIDER_METADATA, ENV_VAR_NAMES, isWindows, isMac } from '../src/provider-metadata.js'
|
|
113
113
|
import { parseTelemetryEnv, isTelemetryDebugEnabled, telemetryDebug, ensureTelemetryConfig, getTelemetryDistinctId, getTelemetrySystem, getTelemetryTerminal, isTelemetryEnabled, sendUsageTelemetry } from '../src/telemetry.js'
|
|
114
|
-
import { ensureFavoritesConfig, toFavoriteKey, syncFavoriteFlags, toggleFavoriteModel, reorderFavorite } from '../src/favorites.js'
|
|
114
|
+
import { ensureFavoritesConfig, toFavoriteKey, syncFavoriteFlags, toggleFavoriteModel, reorderFavorite, pruneOrphanedFavorites } from '../src/favorites.js'
|
|
115
115
|
import { checkForUpdateDetailed, checkForUpdate, runUpdate, promptUpdateNotification, fetchLastReleaseDate } from './updater.js'
|
|
116
116
|
import { promptApiKey } from '../src/setup.js'
|
|
117
117
|
import { syncShellEnv, ensureShellRcSource, promptShellEnvMigration, removeShellEnv } from '../src/shell-env.js'
|
|
@@ -372,6 +372,9 @@ export async function runApp(cliArgs, config) {
|
|
|
372
372
|
hidden: false, // 📖 Simple flag to hide/show models
|
|
373
373
|
}))
|
|
374
374
|
syncFavoriteFlags(results, config)
|
|
375
|
+
// 📖 Garbage-collect favorites that reference models no longer in sources.js,
|
|
376
|
+
// 📖 so the router dashboard only shows real, launchable models.
|
|
377
|
+
pruneOrphanedFavorites(results, config)
|
|
375
378
|
|
|
376
379
|
// 📖 Load usage data from token-stats.json and attach usagePercent to each result row.
|
|
377
380
|
// 📖 usagePercent is the quota percent remaining (0–100). undefined = no data available.
|
|
@@ -544,6 +547,7 @@ export async function runApp(cliArgs, config) {
|
|
|
544
547
|
routerDashboardNoticeTimer: null,
|
|
545
548
|
routerOnboardingScrollOffset: 0,
|
|
546
549
|
routerDashboardEverOpened: false, // 📖 Set to true the first time dashboard opens (used for upgrade-path telemetry)
|
|
550
|
+
routerDashboardCursorIndex: 0, // 📖 Cursor index for navigating the favorites list in router dashboard
|
|
547
551
|
// 📖 Custom text filter (Ctrl+P palette → type text → Enter). Ephemeral — not saved to config.
|
|
548
552
|
customTextFilter: null, // 📖 Active free-text filter string (null = off). Matches model name, ctx, provider key/name.
|
|
549
553
|
}
|
|
@@ -578,7 +582,8 @@ export async function runApp(cliArgs, config) {
|
|
|
578
582
|
const scheduleNextPing = () => {
|
|
579
583
|
clearTimeout(state.pingIntervalObj)
|
|
580
584
|
const elapsed = Date.now() - state.lastPingTime
|
|
581
|
-
const
|
|
585
|
+
const interval = state.routerDashboardOpen ? 1000 : state.pingInterval
|
|
586
|
+
const delay = Math.max(0, interval - elapsed)
|
|
582
587
|
state.pingIntervalObj = setTimeout(runPingCycle, delay)
|
|
583
588
|
}
|
|
584
589
|
|
|
@@ -1223,6 +1228,13 @@ if (unconfiguredHide) {
|
|
|
1223
1228
|
}
|
|
1224
1229
|
|
|
1225
1230
|
state.results.forEach(r => {
|
|
1231
|
+
// 📖 When router dashboard is open, ONLY ping favorites every second
|
|
1232
|
+
// 📖 to prevent massive rate limiting across the entire 90+ model catalog.
|
|
1233
|
+
if (state.routerDashboardOpen) {
|
|
1234
|
+
const favKey = `${r.providerKey}/${r.modelId}`
|
|
1235
|
+
if (!state.config.favorites.includes(favKey)) return
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1226
1238
|
pingModel(r).catch(() => {
|
|
1227
1239
|
// Individual ping failures don't crash the loop
|
|
1228
1240
|
})
|
|
@@ -136,10 +136,12 @@ function getManagedProviderId(providerKey) {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
function getProviderLabel(providerKey) {
|
|
139
|
+
if (providerKey === 'fcm_router') return 'Smart Router Daemon'
|
|
139
140
|
return PROVIDER_METADATA[providerKey]?.label || sources[providerKey]?.name || providerKey
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
function getManagedProviderLabel(providerKey) {
|
|
144
|
+
if (providerKey === 'fcm_router') return 'FCM Smart Router'
|
|
143
145
|
return `FCM ${getProviderLabel(providerKey)}`
|
|
144
146
|
}
|
|
145
147
|
|
|
@@ -157,6 +159,10 @@ function getDefaultMaxTokens(contextWindow) {
|
|
|
157
159
|
}
|
|
158
160
|
|
|
159
161
|
function resolveProviderBaseUrl(providerKey) {
|
|
162
|
+
if (providerKey === 'fcm_router') {
|
|
163
|
+
return `http://localhost:${process.env.FCM_ROUTER_PORT || '19280'}/v1`
|
|
164
|
+
}
|
|
165
|
+
|
|
160
166
|
const providerUrl = sources[providerKey]?.url
|
|
161
167
|
if (!providerUrl) return null
|
|
162
168
|
|
|
@@ -173,6 +179,9 @@ function resolveProviderBaseUrl(providerKey) {
|
|
|
173
179
|
}
|
|
174
180
|
|
|
175
181
|
function resolveGooseBaseUrl(providerKey) {
|
|
182
|
+
if (providerKey === 'fcm_router') {
|
|
183
|
+
return `http://localhost:${process.env.FCM_ROUTER_PORT || '19280'}/v1`
|
|
184
|
+
}
|
|
176
185
|
const providerUrl = sources[providerKey]?.url
|
|
177
186
|
if (!providerUrl) return null
|
|
178
187
|
if (providerKey === 'cloudflare') {
|
|
@@ -184,6 +193,7 @@ function resolveGooseBaseUrl(providerKey) {
|
|
|
184
193
|
}
|
|
185
194
|
|
|
186
195
|
function getDirectInstallSupport(providerKey) {
|
|
196
|
+
if (providerKey === 'fcm_router') return { supported: true, reason: null }
|
|
187
197
|
if (!sources[providerKey]) {
|
|
188
198
|
return { supported: false, reason: 'Unknown provider' }
|
|
189
199
|
}
|
|
@@ -220,6 +230,12 @@ function buildCatalogModel(modelId, label, tier, sweScore, ctx) {
|
|
|
220
230
|
}
|
|
221
231
|
|
|
222
232
|
export function getProviderCatalogModels(providerKey) {
|
|
233
|
+
if (providerKey === 'fcm_router') {
|
|
234
|
+
return [
|
|
235
|
+
buildCatalogModel('fcm', 'FCM Smart Router', 'S+', 100, '200k')
|
|
236
|
+
]
|
|
237
|
+
}
|
|
238
|
+
|
|
223
239
|
const seen = new Set()
|
|
224
240
|
return MODELS
|
|
225
241
|
.filter((entry) => entry[5] === providerKey)
|
|
@@ -252,6 +268,7 @@ export function getInstallTargetModes() {
|
|
|
252
268
|
}
|
|
253
269
|
|
|
254
270
|
function requireConfiguredProviderKey(config, providerKey) {
|
|
271
|
+
if (providerKey === 'fcm_router') return 'fcm-local'
|
|
255
272
|
const apiKey = getApiKey(config, providerKey)
|
|
256
273
|
if (!apiKey) {
|
|
257
274
|
throw new Error(`No configured API key found for ${getProviderLabel(providerKey)}`)
|
package/src/favorites.js
CHANGED
|
@@ -21,9 +21,10 @@
|
|
|
21
21
|
* → toFavoriteKey(providerKey, modelId) — Build the canonical "providerKey/modelId" string
|
|
22
22
|
* → syncFavoriteFlags(results, config) — Attach isFavorite/favoriteRank to result rows
|
|
23
23
|
* → toggleFavoriteModel(config, providerKey, modelId) — Add/remove favorite and persist
|
|
24
|
+
* → pruneOrphanedFavorites(results, config) — Remove favorites referencing models no longer in sources
|
|
24
25
|
*
|
|
25
26
|
* @exports
|
|
26
|
-
* ensureFavoritesConfig, toFavoriteKey, syncFavoriteFlags, toggleFavoriteModel
|
|
27
|
+
* ensureFavoritesConfig, toFavoriteKey, syncFavoriteFlags, toggleFavoriteModel, pruneOrphanedFavorites
|
|
27
28
|
*
|
|
28
29
|
* @see src/config.js — load/save helpers keep favorite persistence atomic and merge-safe
|
|
29
30
|
* @see bin/free-coding-models.js — calls syncFavoriteFlags on startup and toggleFavoriteModel on F key
|
|
@@ -125,3 +126,23 @@ export function reorderFavorite(config, providerKey, modelId, direction) {
|
|
|
125
126
|
if (saveResult.success) replaceConfigContents(config, latestConfig)
|
|
126
127
|
return true
|
|
127
128
|
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 📖 Remove favorites that reference models no longer present in the active sources.
|
|
132
|
+
* 📖 Called once at startup so the router dashboard does not show stale/removed models.
|
|
133
|
+
* 📖 Persists immediately if any orphaned entries are found.
|
|
134
|
+
* @param {Array<Record<string, unknown>>} results — the full result rows from sources
|
|
135
|
+
* @param {Record<string, unknown>} config
|
|
136
|
+
* @returns {number} count of removed orphaned entries
|
|
137
|
+
*/
|
|
138
|
+
export function pruneOrphanedFavorites(results, config) {
|
|
139
|
+
ensureFavoritesConfig(config)
|
|
140
|
+
const validKeys = new Set(results.map(r => toFavoriteKey(r.providerKey, r.modelId)))
|
|
141
|
+
const before = config.favorites.length
|
|
142
|
+
config.favorites = config.favorites.filter(key => validKeys.has(key))
|
|
143
|
+
const removed = before - config.favorites.length
|
|
144
|
+
if (removed > 0) {
|
|
145
|
+
saveConfig(config, { replaceFavorites: true })
|
|
146
|
+
}
|
|
147
|
+
return removed
|
|
148
|
+
}
|
package/src/key-handler.js
CHANGED
|
@@ -269,6 +269,7 @@ export function createKeyHandler(ctx) {
|
|
|
269
269
|
installProviderEndpoints,
|
|
270
270
|
syncFavoriteFlags,
|
|
271
271
|
toggleFavoriteModel,
|
|
272
|
+
reorderFavorite,
|
|
272
273
|
sortResultsWithPinnedFavorites,
|
|
273
274
|
adjustScrollOffset,
|
|
274
275
|
applyTierFilter,
|
|
@@ -1434,22 +1435,108 @@ export function createKeyHandler(ctx) {
|
|
|
1434
1435
|
|
|
1435
1436
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
1436
1437
|
|
|
1437
|
-
// 📖 Router Dashboard
|
|
1438
|
-
// 📖
|
|
1438
|
+
// 📖 Router Dashboard: ↑↓ navigate favorites list, Ctrl+↑↓ reorder,
|
|
1439
|
+
// 📖 I cycles health check speed, C clears log, Esc goes back.
|
|
1439
1440
|
if (state.routerDashboardOpen) {
|
|
1440
1441
|
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
1442
|
+
const favorites = Array.isArray(state.config?.favorites) ? state.config.favorites : []
|
|
1443
|
+
// 📖 maxCursor accounts for the favorites list + 2 buttons (Start/Stop Daemon and Install Endpoint)
|
|
1444
|
+
const maxCursor = Math.max(0, favorites.length + 1)
|
|
1441
1445
|
const pageStep = Math.max(1, (state.terminalRows || 1) - 4)
|
|
1442
1446
|
|
|
1443
1447
|
if (key.name === 'escape') {
|
|
1444
1448
|
closeRouterDashboardOverlay(state)
|
|
1445
1449
|
return
|
|
1446
1450
|
}
|
|
1451
|
+
|
|
1452
|
+
// 📖 Shift+↑: move the selected favorite UP in fallback priority
|
|
1453
|
+
if (key.shift && key.name === 'up') {
|
|
1454
|
+
const cursorIdx = state.routerDashboardCursorIndex ?? 0
|
|
1455
|
+
if (favorites.length > 0 && cursorIdx < favorites.length) {
|
|
1456
|
+
const favKey = favorites[cursorIdx]
|
|
1457
|
+
if (favKey) {
|
|
1458
|
+
const slashIdx = favKey.indexOf('/')
|
|
1459
|
+
const providerKey = slashIdx >= 0 ? favKey.slice(0, slashIdx) : favKey
|
|
1460
|
+
const modelId = slashIdx >= 0 ? favKey.slice(slashIdx + 1) : favKey
|
|
1461
|
+
const moved = reorderFavorite(state.config, providerKey, modelId, 'up')
|
|
1462
|
+
if (moved) {
|
|
1463
|
+
state.routerDashboardCursorIndex = Math.max(0, cursorIdx - 1)
|
|
1464
|
+
syncFavoriteFlags(state.results, state.config)
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
return
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// 📖 Shift+↓: move the selected favorite DOWN in fallback priority
|
|
1472
|
+
if (key.shift && key.name === 'down') {
|
|
1473
|
+
const cursorIdx = state.routerDashboardCursorIndex ?? 0
|
|
1474
|
+
if (favorites.length > 0 && cursorIdx < favorites.length) {
|
|
1475
|
+
const favKey = favorites[cursorIdx]
|
|
1476
|
+
if (favKey) {
|
|
1477
|
+
const slashIdx = favKey.indexOf('/')
|
|
1478
|
+
const providerKey = slashIdx >= 0 ? favKey.slice(0, slashIdx) : favKey
|
|
1479
|
+
const modelId = slashIdx >= 0 ? favKey.slice(slashIdx + 1) : favKey
|
|
1480
|
+
const moved = reorderFavorite(state.config, providerKey, modelId, 'down')
|
|
1481
|
+
if (moved) {
|
|
1482
|
+
state.routerDashboardCursorIndex = Math.min(favorites.length - 1, cursorIdx + 1)
|
|
1483
|
+
syncFavoriteFlags(state.results, state.config)
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
return
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// 📖 S: Toggle daemon start/stop
|
|
1491
|
+
if (key.name === 's') {
|
|
1492
|
+
const isRunning = state.routerDashboardStatus === 'ready' || state.routerDashboardStatus === 'partial'
|
|
1493
|
+
const binPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'free-coding-models.js')
|
|
1494
|
+
const args = isRunning ? ['--daemon-stop'] : ['--daemon-bg']
|
|
1495
|
+
|
|
1496
|
+
state.routerDashboardStatus = 'loading'
|
|
1497
|
+
const child = spawn('node', [binPath, ...args], {
|
|
1498
|
+
detached: true,
|
|
1499
|
+
stdio: 'ignore',
|
|
1500
|
+
})
|
|
1501
|
+
child.unref()
|
|
1502
|
+
return
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// 📖 Enter/Return: Toggle daemon or open Install Endpoints if cursor is on a button
|
|
1506
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
1507
|
+
const btnCursor = favorites.length
|
|
1508
|
+
const installBtnCursor = favorites.length + 1
|
|
1509
|
+
|
|
1510
|
+
if ((state.routerDashboardCursorIndex ?? 0) === btnCursor) {
|
|
1511
|
+
const isRunning = state.routerDashboardStatus === 'ready' || state.routerDashboardStatus === 'partial'
|
|
1512
|
+
const binPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'free-coding-models.js')
|
|
1513
|
+
const args = isRunning ? ['--daemon-stop'] : ['--daemon-bg']
|
|
1514
|
+
|
|
1515
|
+
state.routerDashboardStatus = 'loading'
|
|
1516
|
+
const child = spawn('node', [binPath, ...args], {
|
|
1517
|
+
detached: true,
|
|
1518
|
+
stdio: 'ignore',
|
|
1519
|
+
})
|
|
1520
|
+
child.unref()
|
|
1521
|
+
} else if ((state.routerDashboardCursorIndex ?? 0) === installBtnCursor) {
|
|
1522
|
+
state.routerDashboardOpen = false
|
|
1523
|
+
state.installEndpointsOpen = true
|
|
1524
|
+
state.installEndpointsPhase = 'tools' // skip the provider selection phase
|
|
1525
|
+
state.installEndpointsCursor = 0
|
|
1526
|
+
state.installEndpointsProviderKey = 'fcm_router' // special provider key handled by endpoint-installer
|
|
1527
|
+
state.installEndpointsScrollOffset = 0
|
|
1528
|
+
state.installEndpointsErrorMsg = null
|
|
1529
|
+
}
|
|
1530
|
+
return
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// 📖 ↑/↓: navigate the favorites list cursor
|
|
1447
1534
|
if (key.name === 'up' || key.name === 'k') {
|
|
1448
|
-
state.
|
|
1535
|
+
state.routerDashboardCursorIndex = Math.max(0, (state.routerDashboardCursorIndex ?? 0) - 1)
|
|
1449
1536
|
return
|
|
1450
1537
|
}
|
|
1451
1538
|
if (key.name === 'down' || key.name === 'j') {
|
|
1452
|
-
state.
|
|
1539
|
+
state.routerDashboardCursorIndex = Math.min(maxCursor, (state.routerDashboardCursorIndex ?? 0) + 1)
|
|
1453
1540
|
return
|
|
1454
1541
|
}
|
|
1455
1542
|
if (key.name === 'pageup') {
|
|
@@ -1461,29 +1548,18 @@ export function createKeyHandler(ctx) {
|
|
|
1461
1548
|
return
|
|
1462
1549
|
}
|
|
1463
1550
|
if (key.name === 'home') {
|
|
1551
|
+
state.routerDashboardCursorIndex = 0
|
|
1464
1552
|
state.routerDashboardScrollOffset = 0
|
|
1465
1553
|
return
|
|
1466
1554
|
}
|
|
1467
|
-
if (key.name === 's') {
|
|
1468
|
-
try { await cycleRouterDashboardActiveSet(state) } catch {}
|
|
1469
|
-
return
|
|
1470
|
-
}
|
|
1471
1555
|
if (key.name === 'i') {
|
|
1472
1556
|
try { await cycleRouterDashboardProbeMode(state) } catch {}
|
|
1473
1557
|
return
|
|
1474
1558
|
}
|
|
1475
|
-
if (key.name === 'r') {
|
|
1476
|
-
restartRouterDashboardDaemon(state)
|
|
1477
|
-
return
|
|
1478
|
-
}
|
|
1479
1559
|
if (key.name === 'c') {
|
|
1480
1560
|
clearRouterDashboardRequestLog(state)
|
|
1481
1561
|
return
|
|
1482
1562
|
}
|
|
1483
|
-
if (key.name === 'p') {
|
|
1484
|
-
toggleRouterDashboardProbePause(state)
|
|
1485
|
-
return
|
|
1486
|
-
}
|
|
1487
1563
|
return
|
|
1488
1564
|
}
|
|
1489
1565
|
|
|
@@ -1492,7 +1568,7 @@ export function createKeyHandler(ctx) {
|
|
|
1492
1568
|
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
1493
1569
|
|
|
1494
1570
|
const providerChoices = getConfiguredInstallableProviders(state.config)
|
|
1495
|
-
const toolChoices = getInstallTargetModes()
|
|
1571
|
+
const toolChoices = getInstallTargetModes().filter(t => !(state.installEndpointsProviderKey === 'fcm_router' && t === 'fcm_router'))
|
|
1496
1572
|
const modelChoices = state.installEndpointsProviderKey
|
|
1497
1573
|
? getProviderCatalogModels(state.installEndpointsProviderKey)
|
|
1498
1574
|
: []
|
|
@@ -1534,10 +1610,19 @@ export function createKeyHandler(ctx) {
|
|
|
1534
1610
|
if (key.name === 'escape') {
|
|
1535
1611
|
state.installEndpointsErrorMsg = null
|
|
1536
1612
|
if (state.installEndpointsPhase === 'providers' || state.installEndpointsPhase === 'result') {
|
|
1613
|
+
const wasFcmRouter = state.installEndpointsProviderKey === 'fcm_router'
|
|
1537
1614
|
resetInstallEndpointsOverlay()
|
|
1615
|
+
if (wasFcmRouter) {
|
|
1616
|
+
state.routerDashboardOpen = true
|
|
1617
|
+
}
|
|
1538
1618
|
return
|
|
1539
1619
|
}
|
|
1540
1620
|
if (state.installEndpointsPhase === 'tools') {
|
|
1621
|
+
if (state.installEndpointsProviderKey === 'fcm_router') {
|
|
1622
|
+
resetInstallEndpointsOverlay()
|
|
1623
|
+
state.routerDashboardOpen = true
|
|
1624
|
+
return
|
|
1625
|
+
}
|
|
1541
1626
|
state.installEndpointsPhase = 'providers'
|
|
1542
1627
|
state.installEndpointsCursor = 0
|
|
1543
1628
|
state.installEndpointsScrollOffset = 0
|
|
@@ -1582,10 +1667,25 @@ export function createKeyHandler(ctx) {
|
|
|
1582
1667
|
if (!selectedToolMode) return
|
|
1583
1668
|
state.installEndpointsToolMode = selectedToolMode
|
|
1584
1669
|
state.installEndpointsConnectionMode = 'direct'
|
|
1585
|
-
|
|
1586
|
-
state.
|
|
1587
|
-
|
|
1588
|
-
|
|
1670
|
+
|
|
1671
|
+
if (state.installEndpointsProviderKey === 'fcm_router') {
|
|
1672
|
+
state.installEndpointsScope = 'all'
|
|
1673
|
+
try {
|
|
1674
|
+
await runInstallEndpointsFlow()
|
|
1675
|
+
} catch (error) {
|
|
1676
|
+
state.installEndpointsResult = {
|
|
1677
|
+
type: 'error',
|
|
1678
|
+
title: 'Install failed',
|
|
1679
|
+
lines: [error instanceof Error ? error.message : String(error)],
|
|
1680
|
+
}
|
|
1681
|
+
state.installEndpointsPhase = 'result'
|
|
1682
|
+
}
|
|
1683
|
+
} else {
|
|
1684
|
+
state.installEndpointsPhase = 'scope'
|
|
1685
|
+
state.installEndpointsCursor = 0
|
|
1686
|
+
state.installEndpointsScrollOffset = 0
|
|
1687
|
+
state.installEndpointsErrorMsg = null
|
|
1688
|
+
}
|
|
1589
1689
|
}
|
|
1590
1690
|
return
|
|
1591
1691
|
}
|
|
@@ -1660,7 +1760,11 @@ export function createKeyHandler(ctx) {
|
|
|
1660
1760
|
|
|
1661
1761
|
if (state.installEndpointsPhase === 'result') {
|
|
1662
1762
|
if (key.name === 'return' || key.name === 'y') {
|
|
1763
|
+
const wasFcmRouter = state.installEndpointsProviderKey === 'fcm_router'
|
|
1663
1764
|
resetInstallEndpointsOverlay()
|
|
1765
|
+
if (wasFcmRouter) {
|
|
1766
|
+
state.routerDashboardOpen = true
|
|
1767
|
+
}
|
|
1664
1768
|
}
|
|
1665
1769
|
return
|
|
1666
1770
|
}
|
|
@@ -2558,10 +2662,26 @@ export function createKeyHandler(ctx) {
|
|
|
2558
2662
|
|
|
2559
2663
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
2560
2664
|
|
|
2561
|
-
// 📖 Shift+R
|
|
2562
|
-
// 📖
|
|
2665
|
+
// 📖 Shift+R: Open Router Dashboard AND launch OpenCode with the selected model.
|
|
2666
|
+
// 📖 If the dashboard is already open, just bring it to front.
|
|
2563
2667
|
if (key.name === 'r' && key.shift && !key.ctrl && !key.meta) {
|
|
2668
|
+
if (state.routerDashboardOpen) {
|
|
2669
|
+
state.routerDashboardScrollOffset = 0
|
|
2670
|
+
return
|
|
2671
|
+
}
|
|
2564
2672
|
openRouterDashboardOverlay(state)
|
|
2673
|
+
// 📖 If a model is selected in the main table, launch OpenCode with it after opening dashboard
|
|
2674
|
+
const selected = state.visibleSorted?.[state.cursor]
|
|
2675
|
+
if (selected && selected.providerKey && selected.modelId) {
|
|
2676
|
+
const launchModel = {
|
|
2677
|
+
modelId: selected.modelId,
|
|
2678
|
+
label: selected.label,
|
|
2679
|
+
tier: selected.tier,
|
|
2680
|
+
providerKey: selected.providerKey,
|
|
2681
|
+
}
|
|
2682
|
+
// 📖 Launch asynchronously — don't await, dashboard renders while OpenCode starts
|
|
2683
|
+
void startOpenCode(launchModel, state.config)
|
|
2684
|
+
}
|
|
2565
2685
|
return
|
|
2566
2686
|
}
|
|
2567
2687
|
|
package/src/overlays.js
CHANGED
|
@@ -343,7 +343,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
343
343
|
const lines = []
|
|
344
344
|
const cursorLineByRow = {}
|
|
345
345
|
const providerChoices = getConfiguredInstallableProviders(state.config)
|
|
346
|
-
const toolChoices = getInstallTargetModes()
|
|
346
|
+
const toolChoices = getInstallTargetModes().filter(t => !(state.installEndpointsProviderKey === 'fcm_router' && t === 'fcm_router'))
|
|
347
347
|
const totalSteps = 4
|
|
348
348
|
const scopeChoices = [
|
|
349
349
|
{
|
|
@@ -357,9 +357,11 @@ export function createOverlayRenderers(state, deps) {
|
|
|
357
357
|
hint: 'Choose a smaller curated subset for a cleaner model picker.',
|
|
358
358
|
},
|
|
359
359
|
]
|
|
360
|
-
const selectedProviderLabel = state.installEndpointsProviderKey
|
|
361
|
-
?
|
|
362
|
-
:
|
|
360
|
+
const selectedProviderLabel = state.installEndpointsProviderKey === 'fcm_router'
|
|
361
|
+
? 'Smart Router Daemon'
|
|
362
|
+
: state.installEndpointsProviderKey
|
|
363
|
+
? (sources[state.installEndpointsProviderKey]?.name || state.installEndpointsProviderKey)
|
|
364
|
+
: '—'
|
|
363
365
|
|
|
364
366
|
// 📖 Resolve tool label from metadata instead of hard-coded switch
|
|
365
367
|
const selectedToolLabel = state.installEndpointsToolMode
|
package/src/render-table.js
CHANGED
|
@@ -503,9 +503,11 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
503
503
|
? themeColors.metricWarn(ctxRaw.padEnd(W_CTX))
|
|
504
504
|
: numK <= 128
|
|
505
505
|
? chalk.rgb(200, 180, 50).bold(ctxRaw.padEnd(W_CTX))
|
|
506
|
-
: numK <=
|
|
506
|
+
: numK <= 256
|
|
507
507
|
? chalk.rgb(100, 200, 80).bold(ctxRaw.padEnd(W_CTX))
|
|
508
|
-
:
|
|
508
|
+
: numK <= 400
|
|
509
|
+
? chalk.rgb(0, 255, 200).bold(ctxRaw.padEnd(W_CTX))
|
|
510
|
+
: chalk.rgb(0, 255, 255).bold.underline(ctxRaw.padEnd(W_CTX))
|
|
509
511
|
} else {
|
|
510
512
|
ctxCell = themeColors.dim(ctxRaw.padEnd(W_CTX))
|
|
511
513
|
}
|
|
@@ -823,7 +825,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
823
825
|
const starLink = '⭐ ' + themeColors.link('\x1b]8;;https://github.com/vava-nessa/free-coding-models\x1b\\GitHub\x1b]8;;\x1b\\')
|
|
824
826
|
lines.push(
|
|
825
827
|
' ' + paletteLabel + themeColors.dim(` • `) + starLink + themeColors.dim(` • `) +
|
|
826
|
-
chalk.rgb(255, 168, 209).bold('\x1b]8;;https://x.com/vavanessadev\x1b\\
|
|
828
|
+
chalk.rgb(255, 168, 209).bold('\x1b]8;;https://x.com/vavanessadev\x1b\\Follow @vavanessadev on X for updates and support\x1b]8;;\x1b\\')
|
|
827
829
|
)
|
|
828
830
|
|
|
829
831
|
if (versionStatus.isOutdated) {
|
package/src/router-daemon.js
CHANGED
|
@@ -637,7 +637,8 @@ class RouterRuntime {
|
|
|
637
637
|
reloadConfigFromDisk() {
|
|
638
638
|
try {
|
|
639
639
|
const nextConfig = loadConfig()
|
|
640
|
-
|
|
640
|
+
// 📖 Always rebuild the router set from favorites so UI toggles apply dynamically
|
|
641
|
+
ensureRouterConfigForDaemon(nextConfig, true)
|
|
641
642
|
this.config = nextConfig
|
|
642
643
|
this.refreshRouteState()
|
|
643
644
|
this.scheduleProbeLoop()
|
|
@@ -649,12 +650,10 @@ class RouterRuntime {
|
|
|
649
650
|
}
|
|
650
651
|
|
|
651
652
|
getApiKeyForProvider(providerKey) {
|
|
652
|
-
// 📖 Router background startup should work without inherited shell env, so
|
|
653
|
-
// 📖 config keys are primary. Env is only a fallback for headless sessions.
|
|
654
653
|
const configured = this.config?.apiKeys?.[providerKey]
|
|
655
654
|
if (Array.isArray(configured)) return configured.find(Boolean) || null
|
|
656
655
|
if (typeof configured === 'string' && configured.trim()) return configured.trim()
|
|
657
|
-
return
|
|
656
|
+
return null
|
|
658
657
|
}
|
|
659
658
|
|
|
660
659
|
getSet(setName = null) {
|
|
@@ -1052,15 +1051,18 @@ class RouterRuntime {
|
|
|
1052
1051
|
let errorCode = 'all_models_unavailable'
|
|
1053
1052
|
let errorType = 'service_unavailable'
|
|
1054
1053
|
if (health.length > 0) {
|
|
1055
|
-
|
|
1054
|
+
const allAuthError = health.length > 0 && health.every((h) => h.state === 'AUTH_ERROR')
|
|
1055
|
+
const allAuthOrQuota = health.length > 0 && health.every((h) => h.state === 'AUTH_ERROR' || quotaExhausted.includes(h.key))
|
|
1056
|
+
const allStaleOrUnsupported = health.every((h) => h.state === 'STALE' || h.state === 'UNSUPPORTED')
|
|
1057
|
+
if (allAuthError) {
|
|
1056
1058
|
statusCode = 401
|
|
1057
1059
|
errorCode = 'invalid_api_key'
|
|
1058
1060
|
errorType = 'invalid_request_error'
|
|
1059
|
-
} else if (
|
|
1061
|
+
} else if (allAuthOrQuota) {
|
|
1060
1062
|
statusCode = 429
|
|
1061
1063
|
errorCode = 'insufficient_quota'
|
|
1062
1064
|
errorType = 'insufficient_quota'
|
|
1063
|
-
} else if (
|
|
1065
|
+
} else if (allStaleOrUnsupported) {
|
|
1064
1066
|
statusCode = 400
|
|
1065
1067
|
errorCode = 'invalid_model'
|
|
1066
1068
|
errorType = 'invalid_request_error'
|
|
@@ -1808,7 +1810,7 @@ export function createRouterRuntimeForTest({ config, port = 0, logger = null, to
|
|
|
1808
1810
|
})
|
|
1809
1811
|
}
|
|
1810
1812
|
|
|
1811
|
-
function ensureRouterConfigForDaemon(config) {
|
|
1813
|
+
function ensureRouterConfigForDaemon(config, skipSave = false) {
|
|
1812
1814
|
// 📖 Always rebuild from favorites or defaults — no more manual set management
|
|
1813
1815
|
const favSet = buildRouterSetFromFavorites(config)
|
|
1814
1816
|
const activeSet = favSet || buildDefaultRouterSet(config)
|
|
@@ -1819,7 +1821,7 @@ function ensureRouterConfigForDaemon(config) {
|
|
|
1819
1821
|
activeSet: activeSet.name,
|
|
1820
1822
|
sets: { [activeSet.name]: activeSet },
|
|
1821
1823
|
})
|
|
1822
|
-
saveConfig(config)
|
|
1824
|
+
if (!skipSave) saveConfig(config)
|
|
1823
1825
|
return config.router
|
|
1824
1826
|
}
|
|
1825
1827
|
|