free-coding-models 0.5.26 → 0.5.28

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.
@@ -200,7 +200,8 @@ const BASE_COMMAND_TREE = [
200
200
  ],
201
201
  },
202
202
  { id: 'action-cycle-theme', label: 'Cycle theme', shortcut: 'G', icon: '🌗', description: 'Switch dark/light/auto', keywords: ['theme', 'dark', 'light', 'auto'] },
203
- { id: 'action-reset-view', label: 'Reset view', shortcut: 'N', icon: '🔄', description: 'Reset filters and sort', keywords: ['reset', 'view', 'sort', 'filters'] },
203
+ { id: 'action-reset-view', label: 'Reset view', shortcut: 'N', icon: '\u{1F504}', description: 'Reset filters and sort', keywords: ['reset', 'view', 'sort', 'filters'] },
204
+ { id: 'action-probe-404', label: 'Probe 404 Models', shortcut: 'Ctrl+Shift+P', icon: '\u{1F50D}', description: 'Test all configured models for 404/410 errors. Auto-hides broken models.', keywords: ['probe', '404', 'broken', 'dead', 'health', 'check', 'test', 'verify'] },
204
205
  ],
205
206
  },
206
207
  // 📖 Pages - directly at root level, not in submenu
@@ -107,6 +107,11 @@ const PROVIDER_AUTH_ENDPOINTS = {
107
107
  zai: null, // 📖 ZAI undocumented; use ping only
108
108
  googleai: null, // 📖 Google AI Studio has no OpenAI-compatible /models; use ping
109
109
  'opencode-zen': null, // 📖 OpenCode Zen uses OpenCode auth only; use ping
110
+ kilo: { url: 'https://api.kilo.ai/api/gateway/models', method: 'GET' },
111
+ llm7: { url: 'https://api.llm7.io/v1/models', method: 'GET' },
112
+ routeway: { url: 'https://api.routeway.ai/v1/models', method: 'GET' },
113
+ novita: { url: 'https://api.novita.ai/openai/v1/models', method: 'GET' },
114
+ 'ollama-cloud': { url: 'https://ollama.com/v1/models', method: 'GET' },
110
115
  }
111
116
 
112
117
  // 📖 Sleep helper kept local to this module so the Settings key test flow can
@@ -787,6 +792,42 @@ export function createKeyHandler(ctx) {
787
792
  })
788
793
  }
789
794
 
795
+ // 📖 toggleAutoHideBrokenModels: Toggle auto-hiding of 404/410 models from probe.
796
+ // 📖 When disabling, unhide all previously hidden models so they become visible again.
797
+ function toggleAutoHideBrokenModels() {
798
+ if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
799
+ const wasEnabled = state.config.settings.autoHideBrokenModels !== false
800
+ state.config.settings.autoHideBrokenModels = !wasEnabled
801
+
802
+ if (!wasEnabled) {
803
+ // 📖 Just enabled — models that were hidden before enabling will be picked up on next probe
804
+ } else {
805
+ // 📖 Just disabled — unhide all probe-hidden models so user can see them again
806
+ if (state.config.hiddenModels instanceof Set && state.config.hiddenModels.size > 0) {
807
+ const keysToUnhide = [...state.config.hiddenModels]
808
+ state.config.hiddenModels.clear()
809
+ for (const key of keysToUnhide) {
810
+ const [pk, ...rest] = key.split('/')
811
+ const modelId = rest.join('/')
812
+ const result = state.results.find(r => r.providerKey === pk && r.modelId === modelId)
813
+ if (result) result.hidden = false
814
+ }
815
+ }
816
+ }
817
+
818
+ saveConfig(state.config)
819
+ const hiddenCount = state.config.hiddenModels instanceof Set ? state.config.hiddenModels.size : 0
820
+ state.settingsSyncStatus = {
821
+ type: 'success',
822
+ msg: state.config.settings.autoHideBrokenModels
823
+ ? `✅ Auto-hide enabled — broken models (404/410) from Ctrl+Shift+P probe are hidden automatically.${hiddenCount ? ` (${hiddenCount} currently hidden)` : ''}`
824
+ : '✅ Auto-hide disabled — all models are visible. Previously hidden models have been unhidden.',
825
+ }
826
+ trackAppAction('auto_hide_broken_models_toggled', {
827
+ enabled: state.config.settings.autoHideBrokenModels !== false,
828
+ })
829
+ }
830
+
790
831
  function toggleShellEnv() {
791
832
  if (!state.config.settings) state.config.settings = {}
792
833
  const currentlyEnabled = state.config.settings.shellEnvEnabled === true
@@ -1127,6 +1168,96 @@ export function createKeyHandler(ctx) {
1127
1168
  return Promise.all(workers).then(() => results)
1128
1169
  }
1129
1170
 
1171
+ // 📖 runBrokenModelProbe: Probe all configured models for 404/broken endpoints.
1172
+ // 📖 Sends a real chat-completion request to each model with an API key.
1173
+ // 📖 Models returning 404 or 410 are auto-hidden when setting is enabled.
1174
+ // 📖 Results flash live in the TUI table — models flip status in real time.
1175
+ async function runBrokenModelProbe(state) {
1176
+ if (state.probeRunning) return
1177
+
1178
+ // 📖 Only probe models where the user has an API key configured
1179
+ const probeable = state.results.filter(r => {
1180
+ const apiKey = getApiKey(state.config, r.providerKey)
1181
+ return !!apiKey
1182
+ })
1183
+
1184
+ if (probeable.length === 0) return
1185
+
1186
+ state.probeRunning = true
1187
+ state.probeTotal = probeable.length
1188
+ state.probeCompleted = 0
1189
+ state.probeHiddenCount = 0
1190
+
1191
+ const autoHide = state.config.settings?.autoHideBrokenModels !== false
1192
+ const HTTP_CODES_404 = new Set([404, '404'])
1193
+ const HTTP_CODES_GONE = new Set([410, '410'])
1194
+
1195
+ const tasks = probeable.map(r => async () => {
1196
+ const apiKey = getApiKey(state.config, r.providerKey) ?? null
1197
+ const providerUrl = sources[r.providerKey]?.url ?? null
1198
+ if (!apiKey || !providerUrl) {
1199
+ state.probeCompleted++
1200
+ return { model: r, ok: false, reason: 'no_key' }
1201
+ }
1202
+
1203
+ try {
1204
+ const { code } = await ping(apiKey, r.modelId, r.providerKey, providerUrl)
1205
+ state.probeCompleted++
1206
+
1207
+ if (HTTP_CODES_404.has(code) || HTTP_CODES_GONE.has(code)) {
1208
+ // 📖 Model is broken (404/410) — mark it
1209
+ r.status = 'down'
1210
+ r.httpCode = String(code)
1211
+
1212
+ if (autoHide) {
1213
+ const modelKey = `${r.providerKey}/${r.modelId}`
1214
+ if (!state.config.hiddenModels) state.config.hiddenModels = new Set()
1215
+ state.config.hiddenModels.add(modelKey)
1216
+ r.hidden = true
1217
+ state.probeHiddenCount++
1218
+ }
1219
+
1220
+ return { model: r, ok: false, code, reason: 'broken' }
1221
+ } else if (code === '200') {
1222
+ // 📖 Model is alive — unhide if it was previously hidden by probe
1223
+ const modelKey = `${r.providerKey}/${r.modelId}`
1224
+ if (state.config.hiddenModels?.has(modelKey)) {
1225
+ state.config.hiddenModels.delete(modelKey)
1226
+ r.hidden = false
1227
+ }
1228
+ r.status = 'up'
1229
+ return { model: r, ok: true, code }
1230
+ } else {
1231
+ // 📖 Other codes (401, 429, etc.) — leave status unchanged, don't hide
1232
+ return { model: r, ok: false, code, reason: 'other' }
1233
+ }
1234
+ } catch (err) {
1235
+ state.probeCompleted++
1236
+ return { model: r, ok: false, reason: 'error', error: err?.message }
1237
+ }
1238
+ })
1239
+
1240
+ await runWithConcurrency(tasks, 5)
1241
+
1242
+ // 📖 Persist hidden models set to config
1243
+ if (autoHide && state.probeHiddenCount > 0) {
1244
+ saveConfig(state.config)
1245
+ }
1246
+
1247
+ state.probeRunning = false
1248
+ // 📖 Keep totals visible for a few seconds so user can see results
1249
+ setTimeout(() => {
1250
+ if (!state.probeRunning) {
1251
+ state.probeTotal = 0
1252
+ state.probeCompleted = 0
1253
+ // 📖 Don't reset probeHiddenCount immediately — user needs to see how many were hidden
1254
+ setTimeout(() => {
1255
+ state.probeHiddenCount = 0
1256
+ }, 5000)
1257
+ }
1258
+ }, 3000)
1259
+ }
1260
+
1130
1261
  // 📖 runGlobalBenchmark: Benchmark all visible models with up to 5 concurrent requests.
1131
1262
  // 📖 Results are stored in state.benchmarkResults (same format as individual benchmarks).
1132
1263
  async function runGlobalBenchmark(state) {
@@ -1481,6 +1612,7 @@ export function createKeyHandler(ctx) {
1481
1612
  case 'action-toggle-favorite': return toggleFavoriteOnSelectedRow()
1482
1613
  case 'action-toggle-favorite-mode': return toggleFavoritesDisplayMode()
1483
1614
  case 'action-reset-view': return resetViewSettings()
1615
+ case 'action-probe-404': return runBrokenModelProbe(state)
1484
1616
  default:
1485
1617
  return
1486
1618
  }
@@ -1507,13 +1639,22 @@ export function createKeyHandler(ctx) {
1507
1639
  }
1508
1640
 
1509
1641
  // 📖 Ctrl+U: Global AI Speed Benchmark (benchmark all visible models, 5 concurrent)
1510
- // 📖 Also handles the raw \x15 byte as a fallback for terminals where readline doesn't
1511
- // 📖 set key.ctrl properly (same pattern as Ctrl+C → \x03 fallback).
1642
+ // Also handles the raw \x15 byte as a fallback for terminals where readline doesn't
1643
+ // set key.ctrl properly (same pattern as Ctrl+C → \x03 fallback).
1512
1644
  if ((key.ctrl && key.name === 'u') || str === '\x15') {
1513
1645
  await runGlobalBenchmark(state)
1514
1646
  return
1515
1647
  }
1516
1648
 
1649
+ // 📖 Ctrl+Shift+P: Probe all configured models for 404/broken endpoints.
1650
+ // 📖 Sends a real chat-completion request to every model that has an API key.
1651
+ // 📖 Models returning 404/410 are auto-hidden (when setting is enabled).
1652
+ // 📖 Results flash live in the table — models flip from down→up in real time.
1653
+ if (key.ctrl && key.shift && key.name === 'p') {
1654
+ await runBrokenModelProbe(state)
1655
+ return
1656
+ }
1657
+
1517
1658
  // 📖 Command palette captures the keyboard while active.
1518
1659
  if (state.commandPaletteOpen) {
1519
1660
  if (key.ctrl && key.name === 'c') { exit(0); return }
@@ -2529,7 +2670,8 @@ export function createKeyHandler(ctx) {
2529
2670
  const themeRowIdx = updateRowIdx + 1
2530
2671
  const favoritesModeRowIdx = themeRowIdx + 1
2531
2672
  const startupAiSpeedScanRowIdx = favoritesModeRowIdx + 1
2532
- const cleanupLegacyProxyRowIdx = startupAiSpeedScanRowIdx + 1
2673
+ const autoHideBrokenModelsRowIdx = startupAiSpeedScanRowIdx + 1
2674
+ const cleanupLegacyProxyRowIdx = autoHideBrokenModelsRowIdx + 1
2533
2675
  const changelogViewRowIdx = cleanupLegacyProxyRowIdx + 1
2534
2676
  const shellEnvRowIdx = changelogViewRowIdx + 1
2535
2677
  // 📖 Profile system removed - API keys now persist permanently across all sessions
@@ -2691,6 +2833,12 @@ export function createKeyHandler(ctx) {
2691
2833
  return
2692
2834
  }
2693
2835
 
2836
+ // 📖 Auto-hide broken models toggle
2837
+ if (state.settingsCursor === autoHideBrokenModelsRowIdx) {
2838
+ toggleAutoHideBrokenModels()
2839
+ return
2840
+ }
2841
+
2694
2842
  if (state.settingsCursor === cleanupLegacyProxyRowIdx) {
2695
2843
  runLegacyProxyCleanup()
2696
2844
  return
@@ -2748,6 +2896,11 @@ export function createKeyHandler(ctx) {
2748
2896
  toggleStartupAiSpeedScan()
2749
2897
  return
2750
2898
  }
2899
+ // 📖 Auto-hide broken models toggle (space)
2900
+ if (state.settingsCursor === autoHideBrokenModelsRowIdx) {
2901
+ toggleAutoHideBrokenModels()
2902
+ return
2903
+ }
2751
2904
  // 📖 Profile system removed - API keys now persist permanently across all sessions
2752
2905
 
2753
2906
  // 📖 Toggle enabled/disabled for selected provider
@@ -2765,6 +2918,7 @@ export function createKeyHandler(ctx) {
2765
2918
  || state.settingsCursor === themeRowIdx
2766
2919
  || state.settingsCursor === favoritesModeRowIdx
2767
2920
  || state.settingsCursor === startupAiSpeedScanRowIdx
2921
+ || state.settingsCursor === autoHideBrokenModelsRowIdx
2768
2922
  || state.settingsCursor === cleanupLegacyProxyRowIdx
2769
2923
  || state.settingsCursor === changelogViewRowIdx
2770
2924
  ) return
@@ -24,6 +24,7 @@ import { buildCliHelpLines } from './cli-help.js'
24
24
  import { renderRouterDashboard as renderRouterDashboardOverlay } from '../core/router-dashboard.js'
25
25
  import { renderPlayground as renderPlaygroundOverlay } from '../core/playground.js'
26
26
  import { themeColors, getThemeStatusLabel, getProviderRgb } from './theme.js'
27
+ import { getProviderBillingNote, getProviderLabelWithBilling } from '../core/provider-metadata.js'
27
28
 
28
29
  export function createOverlayRenderers(state, deps) {
29
30
  const {
@@ -120,7 +121,8 @@ export function createOverlayRenderers(state, deps) {
120
121
  const themeRowIdx = updateRowIdx + 1
121
122
  const favoritesModeRowIdx = themeRowIdx + 1
122
123
  const startupAiSpeedScanRowIdx = favoritesModeRowIdx + 1
123
- const cleanupLegacyProxyRowIdx = startupAiSpeedScanRowIdx + 1
124
+ const autoHideBrokenModelsRowIdx = startupAiSpeedScanRowIdx + 1
125
+ const cleanupLegacyProxyRowIdx = autoHideBrokenModelsRowIdx + 1
124
126
  const changelogViewRowIdx = cleanupLegacyProxyRowIdx + 1
125
127
  const shellEnvRowIdx = changelogViewRowIdx + 1
126
128
  const EL = '\x1b[K'
@@ -183,13 +185,16 @@ export function createOverlayRenderers(state, deps) {
183
185
  else if (testResult === 'rate_limited') testBadge = themeColors.warning('[Rate limit ⏳]')
184
186
  else if (testResult === 'no_callable_model') testBadge = chalk.rgb(...getProviderRgb('openrouter'))('[No model ⚠]')
185
187
  else if (testResult === 'fail') testBadge = themeColors.error('[Test ❌]')
186
- // 📖 No truncation of rate limits - overlay now uses 100% terminal width
187
- const rateSummary = themeColors.dim(meta.rateLimits || 'No limit info')
188
+ // 📖 No truncation of rate limits - overlay now uses 100% terminal width.
189
+ // 📖 Paid/credits-required providers get an explicit money marker + parenthesized detail.
190
+ const billingNote = getProviderBillingNote(pk)
191
+ const rateSummary = themeColors.dim(`${meta.rateLimits || 'No limit info'}${billingNote ? ` ${billingNote}` : ''}`)
188
192
 
189
193
  const enabledBadge = enabled ? themeColors.successBold('✅') : themeColors.errorBold('❌')
190
194
  // 📖 Color provider names the same way as in the main table
191
195
  const providerRgb = PROVIDER_COLOR[pk] ?? [105, 190, 245]
192
- const providerName = chalk.bold.rgb(...providerRgb)((meta.label || src.name || pk).slice(0, 22).padEnd(22))
196
+ const providerLabel = getProviderLabelWithBilling(pk, src.name || pk)
197
+ const providerName = chalk.bold.rgb(...providerRgb)(providerLabel.slice(0, 24).padEnd(24))
193
198
 
194
199
  const row = `${bullet(isCursor)}[ ${enabledBadge} ] ${providerName} ${padEndDisplay(keyDisplay, 30)} ${testBadge} ${rateSummary}`
195
200
  cursorLineByRow[i] = lines.length
@@ -205,9 +210,12 @@ export function createOverlayRenderers(state, deps) {
205
210
  const setupStatus = selectedKey ? themeColors.success('API key detected ✅') : themeColors.warning('API key missing ⚠')
206
211
  // 📖 Color the provider name in the setup instructions header
207
212
  const selectedProviderRgb = PROVIDER_COLOR[selectedProviderKey] ?? [105, 190, 245]
208
- const coloredProviderName = chalk.bold.rgb(...selectedProviderRgb)(selectedMeta.label || selectedSource.name || selectedProviderKey)
209
- lines.push(` ${themeColors.textBold('Setup Instructions')} ${coloredProviderName}`)
213
+ const selectedProviderLabel = getProviderLabelWithBilling(selectedProviderKey, selectedSource.name || selectedProviderKey)
214
+ const selectedBillingNote = getProviderBillingNote(selectedProviderKey)
215
+ const coloredProviderName = chalk.bold.rgb(...selectedProviderRgb)(selectedProviderLabel)
216
+ lines.push(` ${themeColors.textBold('Setup Instructions')} — ${coloredProviderName}${selectedBillingNote ? ' ' + themeColors.warning(selectedBillingNote) : ''}`)
210
217
  lines.push(themeColors.dim(` 1) Create a ${selectedMeta.label || selectedSource.name} account: ${selectedMeta.signupUrl || 'signup link missing'}`))
218
+ if (selectedBillingNote) lines.push(themeColors.warning(` 💰 Paid provider note: ${selectedBillingNote}`))
211
219
  lines.push(themeColors.dim(` 2) ${selectedMeta.signupHint || 'Generate an API key and paste it with Enter on this row'}`))
212
220
  lines.push(themeColors.dim(` 3) Press ${themeColors.hotkey('T')} to test your key. Status: ${setupStatus}`))
213
221
  if (selectedProviderKey === 'cloudflare') {
@@ -270,6 +278,16 @@ export function createOverlayRenderers(state, deps) {
270
278
  cursorLineByRow[startupAiSpeedScanRowIdx] = lines.length
271
279
  lines.push(state.settingsCursor === startupAiSpeedScanRowIdx ? themeColors.bgCursorSettingsList(startupAiSpeedScanRow) : startupAiSpeedScanRow)
272
280
 
281
+ // 📖 Auto-hide broken models row: toggles auto-hiding of 404/410 models from probe.
282
+ const autoHideEnabled = state.config.settings?.autoHideBrokenModels !== false
283
+ const hiddenCount = state.config.hiddenModels instanceof Set ? state.config.hiddenModels.size : 0
284
+ const autoHideStatus = autoHideEnabled
285
+ ? themeColors.successBold(`✅ Enabled (${hiddenCount} hidden)`)
286
+ : themeColors.errorBold('❌ Disabled')
287
+ const autoHideRow = `${bullet(state.settingsCursor === autoHideBrokenModelsRowIdx)}${themeColors.textBold('Auto-hide Broken Models').padEnd(44)} ${autoHideStatus}`
288
+ cursorLineByRow[autoHideBrokenModelsRowIdx] = lines.length
289
+ lines.push(state.settingsCursor === autoHideBrokenModelsRowIdx ? themeColors.bgCursorSettingsList(autoHideRow) : autoHideRow)
290
+
273
291
  if (updateState === 'error' && state.settingsUpdateError) {
274
292
  lines.push(themeColors.error(` ${state.settingsUpdateError}`))
275
293
  }
@@ -943,6 +961,7 @@ export function createOverlayRenderers(state, deps) {
943
961
  lines.push(` ${key('Ctrl+P')} Open ⚡️ command palette ${hint('(search and run actions quickly)')}`)
944
962
  lines.push(` ${key('Ctrl+A')} AI Speed Test ${hint('(benchmark selected model → time + TPS)')}`)
945
963
  lines.push(` ${key('Ctrl+U')} Global AI Speed Test ${hint('(benchmark all models; Settings can auto-run it on startup)')}`)
964
+ lines.push(` ${key('Ctrl+Shift+P')} Probe 404 Models ${hint('(test all configured models; auto-hide broken 404/410)')}`)
946
965
  lines.push(` ${key('E')} Cycle filter mode ${hint('(Normal → Configured only → Usable only)')}`)
947
966
  lines.push(` ${key('Z')} Cycle tool mode ${hint('(📦 OpenCode → π Pi → 🪼 jcode → 📦 Desktop → 🦞 OpenClaw → 💘 Crush → 🪿 Goose → 🛠 Aider → 🐉 Qwen → 🤲 OpenHands → ⚡ Amp)')}`)
948
967
  lines.push(` ${key('F')} Toggle favorite on selected row ${hint('(1️⃣2️⃣3️⃣ = router fallback order, capped at 🔟)')}`)
@@ -192,6 +192,10 @@ export function renderTable({
192
192
  benchmarkResults = {},
193
193
  benchmarkRunning = new Set(),
194
194
  headerFlashColumn = null,
195
+ probeRunning = false,
196
+ probeTotal = 0,
197
+ probeCompleted = 0,
198
+ probeHiddenCount = 0,
195
199
  } = _) {
196
200
  // 📖 Filter out hidden models for display
197
201
  const visibleResults = results.filter(r => !r.hidden)
@@ -1157,13 +1161,25 @@ export function renderTable({
1157
1161
  const speedTestLabel = chalk.bgRgb(...currentPalette().badgeSpeedTestBg).rgb(...currentPalette().badgeSpeedTestFg).bold(' NEW ⭐️ Ctrl+A 🤖 AI Speed Test ')
1158
1162
  const globalBenchmarkLabel = chalk.bgRgb(...currentPalette().badgeBenchmarkBg).rgb(...currentPalette().badgeBenchmarkFg).bold(' NEW Ctrl+U : Global AI Speed Test (Uses a lot of requests!) ')
1159
1163
 
1160
- // 📖 Line 3: Speed Test (Ctrl+A) + Global Benchmark (Ctrl+U) + Last release
1161
- if (releaseLabel || speedTestLabel || globalBenchmarkLabel) {
1164
+ // 📖 Probe badge: show progress when 404 probe is running or recently completed
1165
+ let probeLabel = ''
1166
+ if (probeRunning) {
1167
+ const pct = probeTotal > 0 ? Math.round((probeCompleted / probeTotal) * 100) : 0
1168
+ const bar = '█'.repeat(Math.floor(pct / 5)) + '░'.repeat(20 - Math.floor(pct / 5))
1169
+ probeLabel = chalk.bgRgb(180, 40, 40).rgb(255, 255, 255).bold(` 🔍 Probe ${bar} ${probeCompleted}/${probeTotal} `)
1170
+ } else if (probeHiddenCount > 0 && probeTotal > 0) {
1171
+ probeLabel = chalk.bgRgb(120, 60, 60).rgb(255, 200, 200).bold(` 🔍 Probe done: ${probeHiddenCount} broken model${probeHiddenCount > 1 ? 's' : ''} hidden `)
1172
+ }
1173
+
1174
+ // 📖 Line 3: Speed Test + Global Benchmark + Probe + Last release
1175
+ if (releaseLabel || speedTestLabel || globalBenchmarkLabel || probeLabel) {
1162
1176
  const parts = [
1163
1177
  { text: ' ', key: null },
1164
1178
  { text: speedTestLabel, key: 'a' },
1165
1179
  { text: ' ', key: null },
1166
1180
  { text: globalBenchmarkLabel, key: 'u' },
1181
+ { text: probeLabel ? ' ' : '', key: null },
1182
+ { text: probeLabel, key: null },
1167
1183
  { text: ' ', key: null },
1168
1184
  { text: releaseLabel, key: null },
1169
1185
  ]
package/src/tui/theme.js CHANGED
@@ -188,6 +188,11 @@ const PROVIDER_PALETTES = {
188
188
  zai: [150, 208, 255],
189
189
  iflow: [211, 229, 101],
190
190
  'opencode-zen': [185, 146, 255],
191
+ kilo: [120, 255, 190],
192
+ llm7: [180, 255, 140],
193
+ routeway: [130, 210, 255],
194
+ novita: [255, 185, 120],
195
+ 'ollama-cloud': [230, 230, 230],
191
196
  },
192
197
  light: {
193
198
  nvidia: [0, 126, 73],
@@ -213,6 +218,11 @@ const PROVIDER_PALETTES = {
213
218
  zai: [0, 104, 171],
214
219
  iflow: [107, 130, 0],
215
220
  'opencode-zen': [108, 58, 183],
221
+ kilo: [0, 130, 82],
222
+ llm7: [73, 130, 0],
223
+ routeway: [0, 105, 180],
224
+ novita: [173, 84, 0],
225
+ 'ollama-cloud': [88, 88, 88],
216
226
  },
217
227
  }
218
228
 
@@ -58,7 +58,7 @@ export function createTuiFilters(state, { sources, getApiKey, PROVIDER_METADATA
58
58
  // 📖 CLI-only tools and Zen models don't need traditional API keys —
59
59
  // 📖 they authenticate via their own CLI login flow, so "configured only" should never hide them.
60
60
  const providerMeta = PROVIDER_METADATA[r.providerKey]
61
- const noKeyNeeded = providerMeta?.cliOnly || providerMeta?.zenOnly
61
+ const noKeyNeeded = providerMeta?.cliOnly || providerMeta?.zenOnly || providerMeta?.noKeyNeeded
62
62
  // 📖 E toggles "Show only configured & working models":
63
63
  // 📖 hide models where provider has no key, or where the health status is noauth/auth_error (but keep timeout and 429)
64
64
  const badHealth = r.status === 'noauth' || r.status === 'auth_error'
@@ -280,6 +280,12 @@ export function createTuiState({
280
280
  globalBenchmarkTotal: 0,
281
281
  globalBenchmarkCompleted: 0,
282
282
 
283
+ // 📖 404 Probe state (Ctrl+Shift+P)
284
+ probeRunning: false,
285
+ probeTotal: 0,
286
+ probeCompleted: 0,
287
+ probeHiddenCount: 0,
288
+
283
289
  // 📖 Header click flash animation: briefly highlights the clicked column header
284
290
  // 📖 with an inverse/bright style for ~250ms (3 frames at 12 FPS).
285
291
  headerFlashColumn: null, // 📖 Column name being flashed (null = no flash active)