free-coding-models 0.3.62 → 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 +13 -2
- package/src/endpoint-installer.js +17 -0
- package/src/favorites.js +22 -1
- package/src/key-handler.js +108 -23
- package/src/overlays.js +6 -4
- package/src/render-table.js +1 -1
- package/src/router-daemon.js +11 -9
- package/src/router-dashboard.js +99 -22
- package/web/dist/assets/{index-A4ph-qbA.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.
|
|
@@ -579,7 +582,8 @@ export async function runApp(cliArgs, config) {
|
|
|
579
582
|
const scheduleNextPing = () => {
|
|
580
583
|
clearTimeout(state.pingIntervalObj)
|
|
581
584
|
const elapsed = Date.now() - state.lastPingTime
|
|
582
|
-
const
|
|
585
|
+
const interval = state.routerDashboardOpen ? 1000 : state.pingInterval
|
|
586
|
+
const delay = Math.max(0, interval - elapsed)
|
|
583
587
|
state.pingIntervalObj = setTimeout(runPingCycle, delay)
|
|
584
588
|
}
|
|
585
589
|
|
|
@@ -1224,6 +1228,13 @@ if (unconfiguredHide) {
|
|
|
1224
1228
|
}
|
|
1225
1229
|
|
|
1226
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
|
+
|
|
1227
1238
|
pingModel(r).catch(() => {
|
|
1228
1239
|
// Individual ping failures don't crash the loop
|
|
1229
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,
|
|
@@ -1439,7 +1440,8 @@ export function createKeyHandler(ctx) {
|
|
|
1439
1440
|
if (state.routerDashboardOpen) {
|
|
1440
1441
|
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
1441
1442
|
const favorites = Array.isArray(state.config?.favorites) ? state.config.favorites : []
|
|
1442
|
-
|
|
1443
|
+
// 📖 maxCursor accounts for the favorites list + 2 buttons (Start/Stop Daemon and Install Endpoint)
|
|
1444
|
+
const maxCursor = Math.max(0, favorites.length + 1)
|
|
1443
1445
|
const pageStep = Math.max(1, (state.terminalRows || 1) - 4)
|
|
1444
1446
|
|
|
1445
1447
|
if (key.name === 'escape') {
|
|
@@ -1447,10 +1449,10 @@ export function createKeyHandler(ctx) {
|
|
|
1447
1449
|
return
|
|
1448
1450
|
}
|
|
1449
1451
|
|
|
1450
|
-
// 📖
|
|
1451
|
-
if (key.
|
|
1452
|
-
|
|
1453
|
-
|
|
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) {
|
|
1454
1456
|
const favKey = favorites[cursorIdx]
|
|
1455
1457
|
if (favKey) {
|
|
1456
1458
|
const slashIdx = favKey.indexOf('/')
|
|
@@ -1466,10 +1468,10 @@ export function createKeyHandler(ctx) {
|
|
|
1466
1468
|
return
|
|
1467
1469
|
}
|
|
1468
1470
|
|
|
1469
|
-
// 📖
|
|
1470
|
-
if (key.
|
|
1471
|
-
|
|
1472
|
-
|
|
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) {
|
|
1473
1475
|
const favKey = favorites[cursorIdx]
|
|
1474
1476
|
if (favKey) {
|
|
1475
1477
|
const slashIdx = favKey.indexOf('/')
|
|
@@ -1477,7 +1479,7 @@ export function createKeyHandler(ctx) {
|
|
|
1477
1479
|
const modelId = slashIdx >= 0 ? favKey.slice(slashIdx + 1) : favKey
|
|
1478
1480
|
const moved = reorderFavorite(state.config, providerKey, modelId, 'down')
|
|
1479
1481
|
if (moved) {
|
|
1480
|
-
state.routerDashboardCursorIndex = Math.min(
|
|
1482
|
+
state.routerDashboardCursorIndex = Math.min(favorites.length - 1, cursorIdx + 1)
|
|
1481
1483
|
syncFavoriteFlags(state.results, state.config)
|
|
1482
1484
|
}
|
|
1483
1485
|
}
|
|
@@ -1485,17 +1487,56 @@ export function createKeyHandler(ctx) {
|
|
|
1485
1487
|
return
|
|
1486
1488
|
}
|
|
1487
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
|
+
|
|
1488
1533
|
// 📖 ↑/↓: navigate the favorites list cursor
|
|
1489
1534
|
if (key.name === 'up' || key.name === 'k') {
|
|
1490
|
-
|
|
1491
|
-
state.routerDashboardCursorIndex = Math.max(0, (state.routerDashboardCursorIndex ?? 0) - 1)
|
|
1492
|
-
}
|
|
1535
|
+
state.routerDashboardCursorIndex = Math.max(0, (state.routerDashboardCursorIndex ?? 0) - 1)
|
|
1493
1536
|
return
|
|
1494
1537
|
}
|
|
1495
1538
|
if (key.name === 'down' || key.name === 'j') {
|
|
1496
|
-
|
|
1497
|
-
state.routerDashboardCursorIndex = Math.min(maxCursor, (state.routerDashboardCursorIndex ?? 0) + 1)
|
|
1498
|
-
}
|
|
1539
|
+
state.routerDashboardCursorIndex = Math.min(maxCursor, (state.routerDashboardCursorIndex ?? 0) + 1)
|
|
1499
1540
|
return
|
|
1500
1541
|
}
|
|
1501
1542
|
if (key.name === 'pageup') {
|
|
@@ -1527,7 +1568,7 @@ export function createKeyHandler(ctx) {
|
|
|
1527
1568
|
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
1528
1569
|
|
|
1529
1570
|
const providerChoices = getConfiguredInstallableProviders(state.config)
|
|
1530
|
-
const toolChoices = getInstallTargetModes()
|
|
1571
|
+
const toolChoices = getInstallTargetModes().filter(t => !(state.installEndpointsProviderKey === 'fcm_router' && t === 'fcm_router'))
|
|
1531
1572
|
const modelChoices = state.installEndpointsProviderKey
|
|
1532
1573
|
? getProviderCatalogModels(state.installEndpointsProviderKey)
|
|
1533
1574
|
: []
|
|
@@ -1569,10 +1610,19 @@ export function createKeyHandler(ctx) {
|
|
|
1569
1610
|
if (key.name === 'escape') {
|
|
1570
1611
|
state.installEndpointsErrorMsg = null
|
|
1571
1612
|
if (state.installEndpointsPhase === 'providers' || state.installEndpointsPhase === 'result') {
|
|
1613
|
+
const wasFcmRouter = state.installEndpointsProviderKey === 'fcm_router'
|
|
1572
1614
|
resetInstallEndpointsOverlay()
|
|
1615
|
+
if (wasFcmRouter) {
|
|
1616
|
+
state.routerDashboardOpen = true
|
|
1617
|
+
}
|
|
1573
1618
|
return
|
|
1574
1619
|
}
|
|
1575
1620
|
if (state.installEndpointsPhase === 'tools') {
|
|
1621
|
+
if (state.installEndpointsProviderKey === 'fcm_router') {
|
|
1622
|
+
resetInstallEndpointsOverlay()
|
|
1623
|
+
state.routerDashboardOpen = true
|
|
1624
|
+
return
|
|
1625
|
+
}
|
|
1576
1626
|
state.installEndpointsPhase = 'providers'
|
|
1577
1627
|
state.installEndpointsCursor = 0
|
|
1578
1628
|
state.installEndpointsScrollOffset = 0
|
|
@@ -1617,10 +1667,25 @@ export function createKeyHandler(ctx) {
|
|
|
1617
1667
|
if (!selectedToolMode) return
|
|
1618
1668
|
state.installEndpointsToolMode = selectedToolMode
|
|
1619
1669
|
state.installEndpointsConnectionMode = 'direct'
|
|
1620
|
-
|
|
1621
|
-
state.
|
|
1622
|
-
|
|
1623
|
-
|
|
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
|
+
}
|
|
1624
1689
|
}
|
|
1625
1690
|
return
|
|
1626
1691
|
}
|
|
@@ -1695,7 +1760,11 @@ export function createKeyHandler(ctx) {
|
|
|
1695
1760
|
|
|
1696
1761
|
if (state.installEndpointsPhase === 'result') {
|
|
1697
1762
|
if (key.name === 'return' || key.name === 'y') {
|
|
1763
|
+
const wasFcmRouter = state.installEndpointsProviderKey === 'fcm_router'
|
|
1698
1764
|
resetInstallEndpointsOverlay()
|
|
1765
|
+
if (wasFcmRouter) {
|
|
1766
|
+
state.routerDashboardOpen = true
|
|
1767
|
+
}
|
|
1699
1768
|
}
|
|
1700
1769
|
return
|
|
1701
1770
|
}
|
|
@@ -2593,10 +2662,26 @@ export function createKeyHandler(ctx) {
|
|
|
2593
2662
|
|
|
2594
2663
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
2595
2664
|
|
|
2596
|
-
// 📖 Shift+R
|
|
2597
|
-
// 📖
|
|
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.
|
|
2598
2667
|
if (key.name === 'r' && key.shift && !key.ctrl && !key.meta) {
|
|
2668
|
+
if (state.routerDashboardOpen) {
|
|
2669
|
+
state.routerDashboardScrollOffset = 0
|
|
2670
|
+
return
|
|
2671
|
+
}
|
|
2599
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
|
+
}
|
|
2600
2685
|
return
|
|
2601
2686
|
}
|
|
2602
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
|
@@ -825,7 +825,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
825
825
|
const starLink = '⭐ ' + themeColors.link('\x1b]8;;https://github.com/vava-nessa/free-coding-models\x1b\\GitHub\x1b]8;;\x1b\\')
|
|
826
826
|
lines.push(
|
|
827
827
|
' ' + paletteLabel + themeColors.dim(` • `) + starLink + themeColors.dim(` • `) +
|
|
828
|
-
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\\')
|
|
829
829
|
)
|
|
830
830
|
|
|
831
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
|
|