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/analysis.js
CHANGED
|
@@ -138,32 +138,45 @@ export function filterByTierOrExit(results, tierLetter) {
|
|
|
138
138
|
const OPENROUTER_TIER_MAP = {
|
|
139
139
|
'qwen/qwen3-coder': ['S+', '70.6%'],
|
|
140
140
|
'mistralai/devstral-2': ['S+', '72.2%'],
|
|
141
|
-
'
|
|
142
|
-
'
|
|
141
|
+
'minimax/minimax-m2.5': ['S+', '74.0%'],
|
|
142
|
+
'z-ai/glm-4.5-air': ['S+', '72.0%'],
|
|
143
|
+
'tencent/hy3-preview': ['S+', '70.0%'],
|
|
144
|
+
'poolside/laguna-m.1': ['S+', '70.0%'],
|
|
145
|
+
'poolside/laguna-xs.2': ['S+', '70.0%'],
|
|
143
146
|
'qwen/qwen3-next-80b-a3b-instruct': ['S', '65.0%'],
|
|
144
147
|
'openai/gpt-oss-120b': ['S', '60.0%'],
|
|
148
|
+
'inclusionai/ling-2.6-1t': ['S', '60.0%'],
|
|
149
|
+
'nvidia/nemotron-3-super-120b-a12b': ['A+', '56.0%'],
|
|
150
|
+
'nvidia/nemotron-3-nano-omni-30b-a3b-reasoning': ['A+', '52.0%'],
|
|
145
151
|
'openai/gpt-oss-20b': ['A', '42.0%'],
|
|
146
152
|
'nvidia/nemotron-3-nano-30b-a3b': ['A', '43.0%'],
|
|
147
153
|
'meta-llama/llama-3.3-70b-instruct': ['A-', '39.5%'],
|
|
148
|
-
'
|
|
154
|
+
'google/gemma-4-31b-it': ['A', '45.0%'],
|
|
155
|
+
'google/gemma-4-26b-a4b-it': ['A-', '38.0%'],
|
|
149
156
|
'google/gemma-3-27b-it': ['A-', '36.0%'],
|
|
150
157
|
'google/gemma-3-12b-it': ['B+', '30.0%'],
|
|
151
158
|
'google/gemma-3-4b-it': ['B', '22.0%'],
|
|
152
159
|
'google/gemma-3n-e4b-it': ['B', '22.0%'],
|
|
153
160
|
'google/gemma-3n-e2b-it': ['B', '18.0%'],
|
|
154
161
|
'meta-llama/llama-3.2-3b-instruct': ['B', '20.0%'],
|
|
155
|
-
'mistralai/mistral-small-3.1-24b-instruct': ['A-', '35.0%'],
|
|
156
|
-
'qwen/qwen3-4b': ['B', '22.0%'],
|
|
157
162
|
'nousresearch/hermes-3-llama-3.1-405b': ['A', '40.0%'],
|
|
158
163
|
'nvidia/nemotron-nano-9b-v2': ['B+', '28.0%'],
|
|
159
164
|
'nvidia/nemotron-nano-12b-v2-vl': ['B+', '30.0%'],
|
|
160
|
-
'z-ai/glm-4.5-air': ['A-', '38.0%'],
|
|
161
|
-
'arcee-ai/trinity-large-preview': ['A', '40.0%'],
|
|
162
|
-
'arcee-ai/trinity-mini': ['B+', '28.0%'],
|
|
163
|
-
'upstage/solar-pro-3': ['A-', '35.0%'],
|
|
164
165
|
'cognitivecomputations/dolphin-mistral-24b-venice-edition': ['B+', '28.0%'],
|
|
165
166
|
'liquid/lfm-2.5-1.2b-thinking': ['B', '18.0%'],
|
|
166
167
|
'liquid/lfm-2.5-1.2b-instruct': ['B', '18.0%'],
|
|
168
|
+
'openrouter/free': ['B', '25.0%'],
|
|
169
|
+
'openrouter/owl-alpha': ['A+', '50.0%'],
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isOpenRouterFreeModel(model) {
|
|
173
|
+
if (!model?.id) return false
|
|
174
|
+
if (model.id.endsWith(':free')) return true
|
|
175
|
+
const promptPrice = Number(model.pricing?.prompt)
|
|
176
|
+
const completionPrice = Number(model.pricing?.completion)
|
|
177
|
+
return Number.isFinite(promptPrice) && Number.isFinite(completionPrice)
|
|
178
|
+
&& promptPrice === 0
|
|
179
|
+
&& completionPrice === 0
|
|
167
180
|
}
|
|
168
181
|
|
|
169
182
|
// 📖 fetchOpenRouterFreeModels: Fetch live free models from OpenRouter API.
|
|
@@ -186,7 +199,7 @@ export async function fetchOpenRouterFreeModels() {
|
|
|
186
199
|
const json = await res.json()
|
|
187
200
|
if (!json.data || !Array.isArray(json.data)) return null
|
|
188
201
|
|
|
189
|
-
const freeModels = json.data.filter(
|
|
202
|
+
const freeModels = json.data.filter(isOpenRouterFreeModel)
|
|
190
203
|
|
|
191
204
|
return freeModels.map(m => {
|
|
192
205
|
const baseId = m.id.replace(/:free$/, '')
|
package/src/app.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* - Direct mode flags plus an in-app Z-cycle for the public launcher set
|
|
20
20
|
* - Automatic config detection and model setup for both tools
|
|
21
21
|
* - JSON config stored in ~/.free-coding-models.json (auto-migrates from old plain-text)
|
|
22
|
-
* - Multi-provider support via sources.js (NIM/Groq/Cerebras/
|
|
22
|
+
* - Multi-provider support via sources.js (NIM/Groq/Cerebras/GitHub Models/Mistral/OpenRouter/... — extensible)
|
|
23
23
|
* - Settings screen (P key) to manage API keys, provider toggles, manual updates, and provider-key diagnostics
|
|
24
24
|
* - Install Endpoints flow (Settings / Command Palette) to push provider catalogs into OpenCode, OpenClaw, Crush, and Goose
|
|
25
25
|
* - Favorites system: toggle with F, switch pinning mode with Y, persist between sessions
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
* ⚙️ Configuration:
|
|
60
60
|
* - API keys stored per-provider in ~/.free-coding-models.json (0600 perms)
|
|
61
61
|
* - Old ~/.free-coding-models plain-text auto-migrated as nvidia key on first run
|
|
62
|
-
* - Env vars override config: NVIDIA_API_KEY, GROQ_API_KEY, CEREBRAS_API_KEY, OPENROUTER_API_KEY,
|
|
62
|
+
* - Env vars override config: NVIDIA_API_KEY, GROQ_API_KEY, CEREBRAS_API_KEY, OPENROUTER_API_KEY, GITHUB_TOKEN, MISTRAL_API_KEY, SCALEWAY_API_KEY, GOOGLE_API_KEY, CLOUDFLARE_API_TOKEN, DASHSCOPE_API_KEY, ZAI_API_KEY, etc.
|
|
63
63
|
* - ZAI (z.ai) uses a non-standard base path; cloudflare needs CLOUDFLARE_ACCOUNT_ID in env.
|
|
64
64
|
* - Cloudflare Workers AI requires both CLOUDFLARE_API_TOKEN (or CLOUDFLARE_API_KEY) and CLOUDFLARE_ACCOUNT_ID
|
|
65
65
|
* - Models loaded from sources.js — all provider/model definitions are centralized there
|
|
@@ -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, sendBugReport } from '../src/telemetry.js'
|
|
114
|
-
import { ensureFavoritesConfig, toFavoriteKey, syncFavoriteFlags, toggleFavoriteModel } from '../src/favorites.js'
|
|
114
|
+
import { ensureFavoritesConfig, toFavoriteKey, syncFavoriteFlags, toggleFavoriteModel, reorderFavorite } 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'
|
|
@@ -123,6 +123,7 @@ import { startOpenClaw } from '../src/openclaw.js'
|
|
|
123
123
|
import { createOverlayRenderers } from '../src/overlays.js'
|
|
124
124
|
import { createKeyHandler, createMouseEventHandler } from '../src/key-handler.js'
|
|
125
125
|
import { createMouseHandler, containsMouseSequence } from '../src/mouse.js'
|
|
126
|
+
import { stopRouterDashboardClient } from '../src/router-dashboard.js'
|
|
126
127
|
import { getToolModeOrder, getToolMeta } from '../src/tool-metadata.js'
|
|
127
128
|
import { startExternalTool } from '../src/tool-launchers.js'
|
|
128
129
|
import { getToolInstallPlan, installToolWithPlan, isToolInstalled } from '../src/tool-bootstrap.js'
|
|
@@ -241,7 +242,11 @@ export async function runApp(cliArgs, config) {
|
|
|
241
242
|
config.settings.shellEnvEnabled = false
|
|
242
243
|
saveConfig(config)
|
|
243
244
|
}
|
|
244
|
-
|
|
245
|
+
if (choice === 'skip') {
|
|
246
|
+
if (!config.settings) config.settings = {}
|
|
247
|
+
config.settings.shellEnvEnabled = false
|
|
248
|
+
saveConfig(config)
|
|
249
|
+
}
|
|
245
250
|
}
|
|
246
251
|
|
|
247
252
|
// 📖 Default mode: use the last persisted launcher choice when valid,
|
|
@@ -522,6 +527,31 @@ export async function runApp(cliArgs, config) {
|
|
|
522
527
|
installedModelsScrollOffset: 0, // 📖 Vertical scroll offset for overlay viewport
|
|
523
528
|
installedModelsData: [], // 📖 Cached scan results
|
|
524
529
|
installedModelsErrorMsg: null, // 📖 Error or status message
|
|
530
|
+
// 📖 Router Dashboard overlay state (Shift+R opens it).
|
|
531
|
+
routerDashboardOpen: false,
|
|
532
|
+
routerDashboardStatus: 'idle', // 📖 idle | loading | ready | partial | stopped | stale | unreachable | malformed
|
|
533
|
+
routerDashboardBaseUrl: null,
|
|
534
|
+
routerDashboardPort: null,
|
|
535
|
+
routerDashboardHealth: null,
|
|
536
|
+
routerDashboardStats: null,
|
|
537
|
+
routerDashboardError: null,
|
|
538
|
+
routerDashboardScrollOffset: 0,
|
|
539
|
+
routerDashboardEvents: [],
|
|
540
|
+
routerDashboardLiveRequests: [],
|
|
541
|
+
routerDashboardClearedAt: 0,
|
|
542
|
+
routerDashboardLastUpdatedAt: null,
|
|
543
|
+
routerDashboardLastRefreshStartedAt: null,
|
|
544
|
+
routerDashboardPollTimer: null,
|
|
545
|
+
routerDashboardEventAbort: null,
|
|
546
|
+
routerDashboardEventStatus: 'idle',
|
|
547
|
+
routerDashboardEventError: null,
|
|
548
|
+
routerDashboardNotice: null,
|
|
549
|
+
routerDashboardNoticeTimer: null,
|
|
550
|
+
routerOnboardingScrollOffset: 0,
|
|
551
|
+
// 📖 Router upgrade banner (shown once to existing users who haven't seen router)
|
|
552
|
+
routerUpgradeBannerShownAt: 0, // 📖 Timestamp when banner was shown (0 = not shown)
|
|
553
|
+
routerUpgradeBannerDismissedAt: 0, // 📖 Timestamp when banner was dismissed (0 = not dismissed)
|
|
554
|
+
routerDashboardEverOpened: false, // 📖 Set to true the first time dashboard opens (used for upgrade-path telemetry)
|
|
525
555
|
// 📖 Custom text filter (Ctrl+P palette → type text → Enter). Ephemeral — not saved to config.
|
|
526
556
|
customTextFilter: null, // 📖 Active free-text filter string (null = off). Matches model name, ctx, provider key/name.
|
|
527
557
|
}
|
|
@@ -725,6 +755,7 @@ export async function runApp(cliArgs, config) {
|
|
|
725
755
|
clearInterval(ticker)
|
|
726
756
|
clearTimeout(state.pingIntervalObj)
|
|
727
757
|
clearInterval(state.versionRecheckTimer)
|
|
758
|
+
stopRouterDashboardClient(state)
|
|
728
759
|
process.stdout.write(ALT_LEAVE)
|
|
729
760
|
if (process.stdout.isTTY) {
|
|
730
761
|
process.stdout.flush && process.stdout.flush()
|
|
@@ -798,6 +829,7 @@ export async function runApp(cliArgs, config) {
|
|
|
798
829
|
if (ticker) clearInterval(ticker)
|
|
799
830
|
clearTimeout(state.pingIntervalObj)
|
|
800
831
|
clearInterval(state.versionRecheckTimer)
|
|
832
|
+
stopRouterDashboardClient(state)
|
|
801
833
|
if (onKeyPress) process.stdin.removeListener('keypress', onKeyPress)
|
|
802
834
|
if (onMouseData) process.stdin.removeListener('data', onMouseData)
|
|
803
835
|
if (process.stdin.isTTY && resetRawMode) process.stdin.setRawMode(false)
|
|
@@ -864,6 +896,7 @@ export async function runApp(cliArgs, config) {
|
|
|
864
896
|
installProviderEndpoints,
|
|
865
897
|
syncFavoriteFlags,
|
|
866
898
|
toggleFavoriteModel,
|
|
899
|
+
reorderFavorite,
|
|
867
900
|
sortResultsWithPinnedFavorites,
|
|
868
901
|
adjustScrollOffset,
|
|
869
902
|
applyTierFilter,
|
|
@@ -1015,7 +1048,7 @@ export async function runApp(cliArgs, config) {
|
|
|
1015
1048
|
refreshAutoPingMode()
|
|
1016
1049
|
state.frame++
|
|
1017
1050
|
// 📖 Cache visible+sorted models each frame so Enter handler always matches the display
|
|
1018
|
-
if (!state.settingsOpen && !state.installEndpointsOpen && !state.toolInstallPromptOpen && !state.incompatibleFallbackOpen && !state.recommendOpen && !state.feedbackOpen && !state.changelogOpen && !state.installedModelsOpen && !state.commandPaletteOpen) {
|
|
1051
|
+
if (!state.settingsOpen && !state.installEndpointsOpen && !state.toolInstallPromptOpen && !state.incompatibleFallbackOpen && !state.recommendOpen && !state.feedbackOpen && !state.changelogOpen && !state.installedModelsOpen && !state.routerDashboardOpen && !state.commandPaletteOpen) {
|
|
1019
1052
|
const visible = state.results.filter(r => !r.hidden)
|
|
1020
1053
|
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection, {
|
|
1021
1054
|
pinFavorites: state.favoritesPinnedAndSticky,
|
|
@@ -1058,7 +1091,12 @@ export async function runApp(cliArgs, config) {
|
|
|
1058
1091
|
state.lastReleaseDate,
|
|
1059
1092
|
state.footerHidden,
|
|
1060
1093
|
state.verdictFilterMode,
|
|
1061
|
-
state.healthFilterMode
|
|
1094
|
+
state.healthFilterMode,
|
|
1095
|
+
state.routerFooterRunning,
|
|
1096
|
+
state.routerFooterActiveSet,
|
|
1097
|
+
state.routerFooterTodayTokens,
|
|
1098
|
+
state.routerFooterAllTimeTokens,
|
|
1099
|
+
state.routerFooterRequests
|
|
1062
1100
|
)
|
|
1063
1101
|
}
|
|
1064
1102
|
tableContent = state.commandPaletteFrozenTable
|
|
@@ -1096,10 +1134,21 @@ export async function runApp(cliArgs, config) {
|
|
|
1096
1134
|
state.lastReleaseDate,
|
|
1097
1135
|
state.footerHidden,
|
|
1098
1136
|
state.verdictFilterMode,
|
|
1099
|
-
state.healthFilterMode
|
|
1137
|
+
state.healthFilterMode,
|
|
1138
|
+
state.routerFooterRunning,
|
|
1139
|
+
state.routerFooterActiveSet,
|
|
1140
|
+
state.routerFooterTodayTokens,
|
|
1141
|
+
state.routerFooterAllTimeTokens,
|
|
1142
|
+
state.routerFooterRequests
|
|
1100
1143
|
)
|
|
1101
1144
|
}
|
|
1102
1145
|
|
|
1146
|
+
// 📖 Router upgrade banner: inline notification for existing users not yet seen router
|
|
1147
|
+
if (!state.routerOnboardingOpen && !state.settingsOpen && !state.installEndpointsOpen && !state.toolInstallPromptOpen && !state.installedModelsOpen && !state.routerDashboardOpen && !state.tokenUsageOpen && !state.commandPaletteOpen && !state.recommendOpen && !state.feedbackOpen && !state.helpVisible && !state.changelogOpen && !state.incompatibleFallbackOpen) {
|
|
1148
|
+
const banner = overlays.renderRouterUpgradeBanner()
|
|
1149
|
+
if (banner) tableContent = banner + '\n' + tableContent
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1103
1152
|
const content = state.settingsOpen
|
|
1104
1153
|
? overlays.renderSettings()
|
|
1105
1154
|
: state.installEndpointsOpen
|
|
@@ -1108,6 +1157,12 @@ export async function runApp(cliArgs, config) {
|
|
|
1108
1157
|
? overlays.renderToolInstallPrompt()
|
|
1109
1158
|
: state.installedModelsOpen
|
|
1110
1159
|
? overlays.renderInstalledModels()
|
|
1160
|
+
: state.routerDashboardOpen
|
|
1161
|
+
? overlays.renderRouterDashboard()
|
|
1162
|
+
: state.tokenUsageOpen
|
|
1163
|
+
? overlays.renderTokenUsage()
|
|
1164
|
+
: state.routerOnboardingOpen
|
|
1165
|
+
? overlays.renderRouterOnboarding()
|
|
1111
1166
|
: state.incompatibleFallbackOpen
|
|
1112
1167
|
? overlays.renderIncompatibleFallback()
|
|
1113
1168
|
: state.commandPaletteOpen
|
|
@@ -1228,6 +1283,57 @@ export async function runApp(cliArgs, config) {
|
|
|
1228
1283
|
} catch {}
|
|
1229
1284
|
}, VERSION_RECHECK_INTERVAL_MS)
|
|
1230
1285
|
|
|
1286
|
+
// 📖 Router footer stats: poll daemon every 30s so the main table footer always
|
|
1287
|
+
// 📖 shows live token counts and daemon status even when the Router Dashboard is closed.
|
|
1288
|
+
const ROUTER_FOOTER_POLL_INTERVAL_MS = 30_000
|
|
1289
|
+
const ROUTER_FOOTER_FETCH_TIMEOUT_MS = 1200
|
|
1290
|
+
|
|
1291
|
+
async function fetchRouterFooterStats() {
|
|
1292
|
+
try {
|
|
1293
|
+
const controller = new AbortController()
|
|
1294
|
+
const timer = setTimeout(() => controller.abort(), ROUTER_FOOTER_FETCH_TIMEOUT_MS)
|
|
1295
|
+
const pidPath = `${process.env.HOME}/.free-coding-models-daemon.pid`
|
|
1296
|
+
const portPath = `${process.env.HOME}/.free-coding-models-daemon.port`
|
|
1297
|
+
let port = 19280
|
|
1298
|
+
try {
|
|
1299
|
+
const { readFileSync: rfs } = await import('node:fs')
|
|
1300
|
+
const savedPort = rfs(portPath, 'utf8').trim()
|
|
1301
|
+
if (/^\d+$/.test(savedPort)) port = Number(savedPort)
|
|
1302
|
+
} catch {}
|
|
1303
|
+
const res = await globalThis.fetch(`http://127.0.0.1:${port}/stats`, {
|
|
1304
|
+
signal: controller.signal,
|
|
1305
|
+
})
|
|
1306
|
+
clearTimeout(timer)
|
|
1307
|
+
if (!res.ok) { state.routerFooterRunning = false; return }
|
|
1308
|
+
const raw = await res.json()
|
|
1309
|
+
const tokens = raw.tokens || {}
|
|
1310
|
+
const today = tokens.today || {}
|
|
1311
|
+
const allTime = tokens.all_time || {}
|
|
1312
|
+
state.routerFooterRunning = true
|
|
1313
|
+
state.routerFooterActiveSet = raw.activeSet || null
|
|
1314
|
+
state.routerFooterTodayTokens = today.total_tokens || 0
|
|
1315
|
+
state.routerFooterAllTimeTokens = allTime.total_tokens || 0
|
|
1316
|
+
state.routerFooterRequests = today.requests || 0
|
|
1317
|
+
state.routerFooterLastFetchAt = Date.now()
|
|
1318
|
+
} catch {
|
|
1319
|
+
state.routerFooterRunning = false
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
state.routerFooterPollTimer = setInterval(() => {
|
|
1324
|
+
void fetchRouterFooterStats()
|
|
1325
|
+
}, ROUTER_FOOTER_POLL_INTERVAL_MS)
|
|
1326
|
+
void fetchRouterFooterStats() // 📖 Initial fetch immediately so footer is populated on first render
|
|
1327
|
+
|
|
1328
|
+
// 📖 Router ON by default — no onboarding prompt, just auto-enable silently.
|
|
1329
|
+
const routerCfg = state.config?.router
|
|
1330
|
+
if (!routerCfg || routerCfg.onboardingSeen !== true || routerCfg.enabled !== true) {
|
|
1331
|
+
if (!state.config.router) state.config.router = {}
|
|
1332
|
+
state.config.router.enabled = true
|
|
1333
|
+
state.config.router.onboardingSeen = true
|
|
1334
|
+
saveConfig(state.config)
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1231
1337
|
// 📖 Keep interface running forever - user can select anytime or Ctrl+C to exit
|
|
1232
1338
|
// 📖 The pings continue running in background with dynamic interval
|
|
1233
1339
|
// 📖 User can press W to decrease interval (faster pings) or = to increase (slower)
|
package/src/cache.js
CHANGED
package/src/cli-help.js
CHANGED
|
@@ -37,6 +37,11 @@ const ANALYSIS_FLAGS = [
|
|
|
37
37
|
|
|
38
38
|
const CONFIG_FLAGS = [
|
|
39
39
|
{ flag: '--web', description: 'Launch the web dashboard in your browser' },
|
|
40
|
+
{ flag: '--daemon', description: 'Start the FCM Router daemon in the foreground' },
|
|
41
|
+
{ flag: '--daemon-bg', description: 'Start the FCM Router daemon in the background' },
|
|
42
|
+
{ flag: '--daemon-status', description: 'Print FCM Router daemon status JSON' },
|
|
43
|
+
{ flag: '--daemon-stop', description: 'Gracefully stop the FCM Router daemon' },
|
|
44
|
+
{ flag: '--sync-set [name]', description: 'Auto-discover and live-probe models into a router set' },
|
|
40
45
|
{ flag: '--no-telemetry', description: 'Disable anonymous telemetry for this run' },
|
|
41
46
|
{ flag: '--help, -h', description: 'Print this help and exit' },
|
|
42
47
|
]
|
|
@@ -44,6 +49,10 @@ const CONFIG_FLAGS = [
|
|
|
44
49
|
const EXAMPLES = [
|
|
45
50
|
'free-coding-models --help',
|
|
46
51
|
'free-coding-models --web',
|
|
52
|
+
'free-coding-models --daemon-bg',
|
|
53
|
+
'free-coding-models --daemon-status',
|
|
54
|
+
'free-coding-models --sync-set',
|
|
55
|
+
'free-coding-models --sync-set my-coding-set',
|
|
47
56
|
'free-coding-models --openclaw --tier S',
|
|
48
57
|
"free-coding-models --json | jq '.[0]'",
|
|
49
58
|
]
|
package/src/command-palette.js
CHANGED
|
@@ -16,6 +16,18 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { TOOL_METADATA, TOOL_MODE_ORDER } from './tool-metadata.js'
|
|
19
|
+
import { sources } from '../sources.js'
|
|
20
|
+
|
|
21
|
+
const PROVIDER_FILTER_COMMANDS = Object.entries(sources).map(([providerKey, source]) => {
|
|
22
|
+
const label = source?.name || providerKey
|
|
23
|
+
return {
|
|
24
|
+
id: `filter-provider-${providerKey.replace(/[^a-z0-9]+/gi, '-')}`,
|
|
25
|
+
label,
|
|
26
|
+
providerKey,
|
|
27
|
+
description: `${label} models`,
|
|
28
|
+
keywords: ['filter', 'provider', 'origin', providerKey, label.toLowerCase()],
|
|
29
|
+
}
|
|
30
|
+
})
|
|
19
31
|
|
|
20
32
|
const TOOL_MODE_DESCRIPTIONS = {
|
|
21
33
|
opencode: 'Launch in OpenCode CLI with the selected model.',
|
|
@@ -119,17 +131,7 @@ const BASE_COMMAND_TREE = [
|
|
|
119
131
|
children: [
|
|
120
132
|
{ id: 'filter-provider-cycle', label: 'Cycle provider', shortcut: 'D', description: 'Switch between providers', keywords: ['filter', 'provider', 'origin'] },
|
|
121
133
|
{ id: 'filter-provider-all', label: 'All providers', providerKey: null, description: 'Show all providers', keywords: ['filter', 'provider', 'all'] },
|
|
122
|
-
|
|
123
|
-
{ id: 'filter-provider-groq', label: 'Groq', providerKey: 'groq', description: 'Groq models', keywords: ['filter', 'provider', 'groq'] },
|
|
124
|
-
{ id: 'filter-provider-cerebras', label: 'Cerebras', providerKey: 'cerebras', description: 'Cerebras models', keywords: ['filter', 'provider', 'cerebras'] },
|
|
125
|
-
{ id: 'filter-provider-sambanova', label: 'SambaNova', providerKey: 'sambanova', description: 'SambaNova models', keywords: ['filter', 'provider', 'sambanova'] },
|
|
126
|
-
{ id: 'filter-provider-openrouter', label: 'OpenRouter', providerKey: 'openrouter', description: 'OpenRouter models', keywords: ['filter', 'provider', 'openrouter'] },
|
|
127
|
-
{ id: 'filter-provider-together', label: 'Together AI', providerKey: 'together', description: 'Together models', keywords: ['filter', 'provider', 'together'] },
|
|
128
|
-
{ id: 'filter-provider-deepinfra', label: 'DeepInfra', providerKey: 'deepinfra', description: 'DeepInfra models', keywords: ['filter', 'provider', 'deepinfra'] },
|
|
129
|
-
{ id: 'filter-provider-fireworks', label: 'Fireworks AI', providerKey: 'fireworks', description: 'Fireworks models', keywords: ['filter', 'provider', 'fireworks'] },
|
|
130
|
-
{ id: 'filter-provider-hyperbolic', label: 'Hyperbolic', providerKey: 'hyperbolic', description: 'Hyperbolic models', keywords: ['filter', 'provider', 'hyperbolic'] },
|
|
131
|
-
{ id: 'filter-provider-google', label: 'Google AI', providerKey: 'google', description: 'Google models', keywords: ['filter', 'provider', 'google'] },
|
|
132
|
-
{ id: 'filter-provider-huggingface', label: 'Hugging Face', providerKey: 'huggingface', description: 'Hugging Face models', keywords: ['filter', 'provider', 'huggingface'] },
|
|
134
|
+
...PROVIDER_FILTER_COMMANDS,
|
|
133
135
|
]
|
|
134
136
|
},
|
|
135
137
|
{
|
|
@@ -199,7 +201,7 @@ const BASE_COMMAND_TREE = [
|
|
|
199
201
|
],
|
|
200
202
|
},
|
|
201
203
|
{ id: 'action-cycle-theme', label: 'Cycle theme', shortcut: 'G', icon: '🌗', description: 'Switch dark/light/auto', keywords: ['theme', 'dark', 'light', 'auto'] },
|
|
202
|
-
{ id: 'action-reset-view', label: 'Reset view',
|
|
204
|
+
{ id: 'action-reset-view', label: 'Reset view', icon: '🔄', description: 'Reset filters and sort', keywords: ['reset', 'view', 'sort', 'filters'] },
|
|
203
205
|
],
|
|
204
206
|
},
|
|
205
207
|
// 📖 Pages - directly at root level, not in submenu
|
|
@@ -208,6 +210,8 @@ const BASE_COMMAND_TREE = [
|
|
|
208
210
|
{ id: 'open-changelog', label: 'Changelog', shortcut: 'N', icon: '📋', type: 'page', description: 'Version history', keywords: ['changelog', 'release'] },
|
|
209
211
|
{ id: 'open-feedback', label: 'Feedback', shortcut: 'I', icon: '📝', type: 'page', description: 'Report bugs or requests', keywords: ['feedback', 'bug', 'request'] },
|
|
210
212
|
{ id: 'open-recommend', label: 'Smart recommend', shortcut: 'Q', icon: '🎯', type: 'page', description: 'Find best model for task', keywords: ['recommend', 'best model'] },
|
|
213
|
+
{ id: 'open-router-dashboard', label: 'Router dashboard', shortcut: 'Shift+R', icon: '🔀', type: 'page', description: 'Inspect daemon health, circuits, tokens, and request log', keywords: ['router', 'daemon', 'dashboard', 'health', 'stats', 'tokens', 'circuit'] },
|
|
214
|
+
{ id: 'open-token-usage', label: 'Token usage', shortcut: 'Shift+T', icon: '📊', type: 'page', description: 'View token usage history, 7-day chart, today/all-time totals', keywords: ['token', 'usage', 'chart', 'history', 'router'] },
|
|
211
215
|
{ id: 'open-install-endpoints', label: 'Install endpoints', icon: '🔌', type: 'page', description: 'Install provider catalogs', keywords: ['install', 'endpoints', 'providers'] },
|
|
212
216
|
{ id: 'open-installed-models', label: 'Installed models', icon: '🗂️', type: 'page', description: 'View models configured in tools', keywords: ['installed', 'models', 'configured', 'tools', 'manager', 'goose', 'crush', 'aider'] },
|
|
213
217
|
]
|