free-coding-models 0.3.55 → 0.3.57

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.
Files changed (75) hide show
  1. package/CHANGELOG.md +55 -56
  2. package/README.md +214 -160
  3. package/bin/free-coding-models.js +46 -0
  4. package/package.json +2 -2
  5. package/sources.js +134 -310
  6. package/src/analysis.js +23 -10
  7. package/src/app.js +66 -27
  8. package/src/cache.js +1 -1
  9. package/src/cli-help.js +9 -0
  10. package/src/command-palette.js +15 -13
  11. package/src/config.js +201 -35
  12. package/src/constants.js +4 -4
  13. package/src/endpoint-installer.js +45 -1
  14. package/src/favorites.js +22 -0
  15. package/src/graphify-out/cache/089db1c1def873cf6d112f1590da4490e61e691aff0db41e006aa2fb15ba0656.json +1 -0
  16. package/src/graphify-out/cache/0b510b53cf1a1393fb52b1fc3bbbf88b63938e961ec5b82119a2e9715fee8bd7.json +1 -0
  17. package/src/graphify-out/cache/0ec9a95a326bde58e0316889018b278062d06d494d0f31ba177c9de71e5fed2d.json +1 -0
  18. package/src/graphify-out/cache/1548663a24a68dce740ebab1bd1d3091048c9604e9d067a1650a42a6d82541d4.json +1 -0
  19. package/src/graphify-out/cache/1783af63cb6d0dfb4d469009f71ac83a74ba0b33d48186ff2c6e63f9429e900a.json +1 -0
  20. package/src/graphify-out/cache/1e109f5eb5dc4fd285871c3613e32b6b14a8c225f4080ee34b51c7e1a1764571.json +1 -0
  21. package/src/graphify-out/cache/1eb24dbeb69b46c8bc1caf925df2f2a964af0f33aea143adf8ddf88e017db6ca.json +1 -0
  22. package/src/graphify-out/cache/21e1bcfed11685e8347243f9d8516072dda183266a4bfe22c52fb31753a446c8.json +1 -0
  23. package/src/graphify-out/cache/2327473478b9c4b1940bf7ef66c9ee960b3cba8d5302e56b625df8274246e0b4.json +1 -0
  24. package/src/graphify-out/cache/25955b81fd25454c8fa90fb71a47db8d1215cf621beb8ff3cbd580aaf011b4f3.json +1 -0
  25. package/src/graphify-out/cache/2739677f19c702f88f3de0a0bac475066adbda98709907ad3de967aef689f86d.json +1 -0
  26. package/src/graphify-out/cache/2bba03422f6b3ee7f5b5d29cc90314a064d259e5822a176657bda3e04505cf00.json +1 -0
  27. package/src/graphify-out/cache/2ddf1d2c6d10147b0402446bc71a7988187b79b6210dd7e7250be8c555b9ff35.json +1 -0
  28. package/src/graphify-out/cache/2ee07457a5767c95a57f8e9eb95b28f800044f35666e0715e9d88ad1103a092e.json +1 -0
  29. package/src/graphify-out/cache/2fe9f75dc2951c417f2c8dd22749092cf550dc67599f1c8d1866900dc6e9154e.json +1 -0
  30. package/src/graphify-out/cache/41c4b7c27e7fc3e2948d3a4bf95a72de2ed9a6f0463994babdce8ed2cc84598c.json +1 -0
  31. package/src/graphify-out/cache/5028defd54b7fbd3c7e444973e493de036e097e9b1d2a7cae7f19b88d68aacde.json +1 -0
  32. package/src/graphify-out/cache/5b133aba3fb16410c5b1fdbd1730039fc7fa1ac93abd99d7be08f60da70fc8d4.json +1 -0
  33. package/src/graphify-out/cache/74252e5b0978d85ab3421a3de1a9384aa282ffd2be2cfe7db2530139089f4275.json +1 -0
  34. package/src/graphify-out/cache/7695ebeea056095edd14332963cc43354ef3a097caf46f1e28d0f01369642901.json +1 -0
  35. package/src/graphify-out/cache/777aa7085c395a935c6556bbde182cd871edb61f3a685ed8068ec0c8f6fb0075.json +1 -0
  36. package/src/graphify-out/cache/82a723881980e82273c113def8315533d7da28827e300413d9ad30f27b7407df.json +1 -0
  37. package/src/graphify-out/cache/86b87c9603e6cd188f42c7eed3b86c291d48a781c223a707e74f3e7ed0c02a21.json +1 -0
  38. package/src/graphify-out/cache/890fead9a78cadaed560a2d2453916121fa605c3e43a334910ac4bc951a9ef6d.json +1 -0
  39. package/src/graphify-out/cache/89d3ea66f52783caa775ef9a30923d7d6225e1d8ae9e962f4741b8c7785dab1e.json +1 -0
  40. package/src/graphify-out/cache/8cc82cd9edce41f0e1c092f14a94fd52bf847addf3237b616dc5a9e505bd05bd.json +1 -0
  41. package/src/graphify-out/cache/93ba2e25e3ff7ad525f397902345fbd375df7315de7b402e20cc803c14eccde8.json +1 -0
  42. package/src/graphify-out/cache/99beed29580b9c7bfecfee794cb3d8e535fcf0eb3b92113108f88bdd0a8e79b3.json +1 -0
  43. package/src/graphify-out/cache/aeeb931fa477c65ce2e51d8149957350fa54225c613222bbbe8448998d1afd3d.json +1 -0
  44. package/src/graphify-out/cache/baf91bef5b5ecb2a476433b6cc0c48c563c54ee2d07fc3c192e543685e3e7222.json +1 -0
  45. package/src/graphify-out/cache/bd98b94ac4e9b92b6336d47b26e0366b51a4eaf0711d722f05f98dfae23ab42b.json +1 -0
  46. package/src/graphify-out/cache/bfcb51e9328e9cbfbee4f6fee0f56635d7b03488addc9f6c4e4b190b70a73362.json +1 -0
  47. package/src/graphify-out/cache/c0d3dabeb093aa758c49eadf41b87ecc96a16c1449c2670aaf48cbfc891d8da6.json +1 -0
  48. package/src/graphify-out/cache/c20d6630236f473c1406068c3ae205853e649b216495c93dfec055dd222c55cf.json +1 -0
  49. package/src/graphify-out/cache/c22b9122816bebce0a2f79af41a986559d01e00163dbcd579c5755621b4cb483.json +1 -0
  50. package/src/graphify-out/cache/ca556ec14453ddb8f9e0c5a832dac90d77111b9bad5f8c2d80d272e2e7a06371.json +1 -0
  51. package/src/graphify-out/cache/d6dbc9135dfa35a756b3b09b06700e4bc229fdccba11bb963f2ba44028e0bbae.json +1 -0
  52. package/src/graphify-out/cache/e1cf71276f1779d0fa075f79bd7c8a9fd0b8eef6932ac043137451b7c7fa7cbe.json +1 -0
  53. package/src/graphify-out/cache/e4b3be14494467df2d2ed389bc4f18f099021cb5bc355b901fa88387b2d8b8a2.json +1 -0
  54. package/src/graphify-out/cache/eaea0dded097f6f9553b654220046c6ec0c9be592a5973d906564ee60af34e0d.json +1 -0
  55. package/src/graphify-out/cache/ef07d0cd2675d1f79d2a2fdbf3bc3319687638751e9ce89b0d0d97ed1cd9f7e1.json +1 -0
  56. package/src/graphify-out/cache/f81272d6eb8aaff9e96d5a1d9f06777db70ac3652a646b951ded51f79871d733.json +1 -0
  57. package/src/graphify-out/cache/f9619dd92186f75a6dbda937e0c606647153918524cdb5763f956e6ec2a9e386.json +1 -0
  58. package/src/graphify-out/cache/fd88b1b2ff4bfcae08559d9c2aaeeb9a3f1e2f5cd8928762c311196956c170a5.json +1 -0
  59. package/src/key-handler.js +322 -114
  60. package/src/kilo.js +20 -1
  61. package/src/opencode.js +23 -2
  62. package/src/overlays.js +199 -98
  63. package/src/provider-metadata.js +26 -17
  64. package/src/quota-capabilities.js +6 -10
  65. package/src/render-helpers.js +38 -8
  66. package/src/render-table.js +119 -248
  67. package/src/router-daemon.js +1986 -0
  68. package/src/router-dashboard.js +902 -0
  69. package/src/sync-set.js +479 -0
  70. package/src/theme.js +4 -0
  71. package/src/tool-launchers.js +1 -0
  72. package/src/tool-metadata.js +6 -2
  73. package/src/utils.js +30 -6
  74. package/web/dist/assets/{index-C03JjCgA.js → index-DKHCzbK1.js} +2 -2
  75. package/web/dist/index.html +1 -1
package/src/kilo.js CHANGED
@@ -9,6 +9,7 @@ import { loadKiloConfig, saveKiloConfig, getKiloConfigPath } from './kilo-config
9
9
  import { getApiKey } from './config.js'
10
10
  import { ENV_VAR_NAMES, OPENCODE_MODEL_MAP } from './provider-metadata.js'
11
11
  import { resolveToolBinaryPath } from './tool-bootstrap.js'
12
+ import { sources } from '../sources.js'
12
13
 
13
14
  // šŸ“– Map source model IDs to Kilo built-in IDs (same as OpenCode).
14
15
  function getKiloModelId(providerKey, modelId) {
@@ -17,6 +18,21 @@ function getKiloModelId(providerKey, modelId) {
17
18
  return OPENCODE_MODEL_MAP[providerKey]?.[modelId] || modelId
18
19
  }
19
20
 
21
+ function buildOpenAiCompatibleProviderConfig(providerKey) {
22
+ const source = sources[providerKey]
23
+ const envVarName = ENV_VAR_NAMES[providerKey]
24
+ if (!source?.url || !envVarName) return null
25
+ const baseURL = source.url
26
+ .replace(/\/chat\/completions$/i, '')
27
+ .replace(/\/responses$/i, '')
28
+ return {
29
+ npm: '@ai-sdk/openai-compatible',
30
+ name: source.name || providerKey,
31
+ options: { baseURL, apiKey: `{env:${envVarName}}` },
32
+ models: {},
33
+ }
34
+ }
35
+
20
36
  // šŸ“– spawnKilo: Resolve API keys + spawn kilo CLI with correct env.
21
37
  async function spawnKilo(args, providerKey, fcmConfig) {
22
38
  const envVarName = ENV_VAR_NAMES[providerKey]
@@ -120,7 +136,7 @@ export async function startKilo(model, fcmConfig) {
120
136
  config.provider.codestral = {
121
137
  npm: '@ai-sdk/openai-compatible',
122
138
  name: 'Mistral Codestral',
123
- options: { baseURL: 'https://api.mistral.ai/v1', apiKey: '{env:CODESTRAL_API_KEY}' },
139
+ options: { baseURL: 'https://api.mistral.ai/v1', apiKey: '{env:MISTRAL_API_KEY}' },
124
140
  models: {}
125
141
  }
126
142
  } else if (providerKey === 'hyperbolic') {
@@ -179,6 +195,9 @@ export async function startKilo(model, fcmConfig) {
179
195
  options: { baseURL: 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1', apiKey: '{env:OVH_AI_ENDPOINTS_ACCESS_TOKEN}' },
180
196
  models: {}
181
197
  }
198
+ } else {
199
+ const providerConfig = buildOpenAiCompatibleProviderConfig(providerKey)
200
+ if (providerConfig) config.provider[providerKey] = providerConfig
182
201
  }
183
202
  }
184
203
 
package/src/opencode.js CHANGED
@@ -98,6 +98,21 @@ function getOpenCodeModelId(providerKey, modelId) {
98
98
  return OPENCODE_MODEL_MAP[providerKey]?.[modelId] || modelId
99
99
  }
100
100
 
101
+ function buildOpenAiCompatibleProviderConfig(providerKey) {
102
+ const source = sources[providerKey]
103
+ const envVarName = ENV_VAR_NAMES[providerKey]
104
+ if (!source?.url || !envVarName) return null
105
+ const baseURL = source.url
106
+ .replace(/\/chat\/completions$/i, '')
107
+ .replace(/\/responses$/i, '')
108
+ return {
109
+ npm: '@ai-sdk/openai-compatible',
110
+ name: source.name || providerKey,
111
+ options: { baseURL, apiKey: `{env:${envVarName}}` },
112
+ models: {},
113
+ }
114
+ }
115
+
101
116
  // ─── ZAI proxy bridge ─────────────────────────────────────────────────────────
102
117
 
103
118
  // šŸ“– createZaiProxy: Localhost reverse proxy bridging ZAI's non-standard API paths
@@ -434,7 +449,7 @@ export async function startOpenCode(model, fcmConfig) {
434
449
  config.provider.codestral = {
435
450
  npm: '@ai-sdk/openai-compatible',
436
451
  name: 'Mistral Codestral',
437
- options: { baseURL: 'https://api.mistral.ai/v1', apiKey: '{env:CODESTRAL_API_KEY}' },
452
+ options: { baseURL: 'https://api.mistral.ai/v1', apiKey: '{env:MISTRAL_API_KEY}' },
438
453
  models: {}
439
454
  }
440
455
  } else if (providerKey === 'hyperbolic') {
@@ -514,6 +529,9 @@ export async function startOpenCode(model, fcmConfig) {
514
529
  options: { baseURL: 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1', apiKey: '{env:OVH_AI_ENDPOINTS_ACCESS_TOKEN}' },
515
530
  models: {}
516
531
  }
532
+ } else {
533
+ const providerConfig = buildOpenAiCompatibleProviderConfig(providerKey)
534
+ if (providerConfig) config.provider[providerKey] = providerConfig
517
535
  }
518
536
  }
519
537
 
@@ -799,7 +817,7 @@ export async function startOpenCodeDesktop(model, fcmConfig) {
799
817
  config.provider.codestral = {
800
818
  npm: '@ai-sdk/openai-compatible',
801
819
  name: 'Mistral Codestral',
802
- options: { baseURL: 'https://api.mistral.ai/v1', apiKey: '{env:CODESTRAL_API_KEY}' },
820
+ options: { baseURL: 'https://api.mistral.ai/v1', apiKey: '{env:MISTRAL_API_KEY}' },
803
821
  models: {}
804
822
  }
805
823
  } else if (providerKey === 'hyperbolic') {
@@ -879,6 +897,9 @@ export async function startOpenCodeDesktop(model, fcmConfig) {
879
897
  options: { baseURL: 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1', apiKey: '{env:OVH_AI_ENDPOINTS_ACCESS_TOKEN}' },
880
898
  models: {}
881
899
  }
900
+ } else {
901
+ const providerConfig = buildOpenAiCompatibleProviderConfig(providerKey)
902
+ if (providerConfig) config.provider[providerKey] = providerConfig
882
903
  }
883
904
  }
884
905
 
package/src/overlays.js CHANGED
@@ -4,17 +4,16 @@
4
4
  *
5
5
  * @details
6
6
  * This module centralizes all overlay rendering in one place:
7
- * - Settings, Install Endpoints, Command Palette, Help, Smart Recommend, Feedback, Changelog
7
+ * - Settings, Install Endpoints, Command Palette, Help, Smart Recommend, Changelog, Router Dashboard
8
8
  * - Settings diagnostics for provider key tests, including wrapped retry/error details
9
9
  * - Recommend analysis timer orchestration and progress updates
10
10
  *
11
11
  * The factory pattern keeps stateful UI logic isolated while still
12
12
  * allowing the main CLI to control shared state and dependencies.
13
13
  *
14
- * šŸ“– Feedback overlay (I key) combines feature requests + bug reports in one left-aligned input
15
- *
16
14
  * → Functions:
17
15
  * - `createOverlayRenderers` — returns renderer + analysis helpers + overlayLayout
16
+ * - `renderRouterDashboard` — mounts the Smart Model Router dashboard renderer
18
17
  *
19
18
  * @exports { createOverlayRenderers }
20
19
  * @see ./key-handler.js — handles keypresses for all overlay interactions
@@ -22,6 +21,7 @@
22
21
 
23
22
  import { loadChangelog } from './changelog-loader.js'
24
23
  import { buildCliHelpLines } from './cli-help.js'
24
+ import { renderRouterDashboard as renderRouterDashboardOverlay } from './router-dashboard.js'
25
25
  import { themeColors, getThemeStatusLabel, getProviderRgb } from './theme.js'
26
26
 
27
27
  export function createOverlayRenderers(state, deps) {
@@ -297,12 +297,16 @@ export function createOverlayRenderers(state, deps) {
297
297
  }
298
298
  lines.push('')
299
299
 
300
- // šŸ“– Footer with credits
300
+ // šŸ“– Footer with credits + community links — Discord and Buy me a coffee
301
+ // šŸ“– live here (and in the onboarding) instead of the main TUI footer to
302
+ // šŸ“– keep the table chrome lean.
301
303
  lines.push('')
302
304
  lines.push(
303
305
  themeColors.dim(' ') +
304
306
  themeColors.footerLove('Made with šŸ’– & ā˜• by ') +
305
307
  themeColors.link('\x1b]8;;https://github.com/vava-nessa\x1b\\vava-nessa\x1b]8;;\x1b\\') +
308
+ themeColors.dim(' • šŸ’¬ ') +
309
+ themeColors.footerDiscord('\x1b]8;;https://discord.gg/ZTNFHvvCkU\x1b\\Join the Discord\x1b]8;;\x1b\\') +
306
310
  themeColors.dim(' • ā˜• ') +
307
311
  themeColors.footerCoffee('\x1b]8;;https://buymeacoffee.com/vavanessadev\x1b\\Buy me a coffee\x1b]8;;\x1b\\') +
308
312
  themeColors.dim(' • ') +
@@ -866,8 +870,9 @@ export function createOverlayRenderers(state, deps) {
866
870
  // šŸ“– Branding header
867
871
  lines.push(` ${themeColors.accent('šŸš€')} ${themeColors.accentBold('free-coding-models')} ${themeColors.dim(`v${LOCAL_VERSION}`)}`)
868
872
  lines.push(` ${heading('ā“ Help & Keyboard Shortcuts')}`)
873
+ lines.push(` ${themeColors.successBold('šŸ”‘ Yellow = active key')}`)
869
874
  lines.push('')
870
- lines.push(` ${hint('— ↑↓ / PgUp / PgDn / Home / End scroll • K or Esc close')}`)
875
+ lines.push(` ${hint('— ↑↓ / PgUp / PgDn / Home / End scroll • K or ')}${themeColors.successBold('Esc close')}`)
871
876
  lines.push(` ${heading('Columns')}`)
872
877
  lines.push('')
873
878
  lines.push(` ${label('Rank')} SWE-bench rank (1 = best coding score) ${hint('Sort:')} ${key('R')}`)
@@ -882,7 +887,7 @@ export function createOverlayRenderers(state, deps) {
882
887
  lines.push(` ${label('CTX')} Context window size (128k, 200k, 256k, 1m, etc.) ${hint('Sort:')} ${key('C')}`)
883
888
  lines.push(` ${hint('Bigger context = the model can read more of your codebase at once without forgetting.')}`)
884
889
  lines.push('')
885
- lines.push(` ${label('Model')} Model name (⭐ = favorited) ${hint('Sort:')} ${key('M')} ${hint('Favorite:')} ${key('F')}`)
890
+ lines.push(` ${label('Model')} Model name (1ļøāƒ£2ļøāƒ£3ļøāƒ£ = favorite order) ${hint('Sort:')} ${key('M')} ${hint('Favorite:')} ${key('F')}`)
886
891
  lines.push(` ${hint('Star the ones you like. Press Y to switch between pinned mode and normal filter/sort mode.')}`)
887
892
  lines.push('')
888
893
  lines.push(` ${label('Provider')} Provider source (NIM, Groq, Cerebras, etc.) ${hint('Sort:')} ${key('O')} ${hint('Cycle:')} ${key('D')}`)
@@ -923,17 +928,18 @@ export function createOverlayRenderers(state, deps) {
923
928
  lines.push(` ${key('Ctrl+P')} Open āš”ļø command palette ${hint('(search and run actions quickly)')}`)
924
929
  lines.push(` ${key('E')} Toggle configured models only ${hint('(enabled by default)')}`)
925
930
  lines.push(` ${key('Z')} Cycle tool mode ${hint('(šŸ“¦ OpenCode → Ļ€ Pi → 🪼 jcode → šŸ“¦ Desktop → šŸ¦ž OpenClaw → šŸ’˜ Crush → 🪿 Goose → šŸ›  Aider → šŸ‰ Qwen → 🤲 OpenHands → ⚔ Amp → 🦘 Rovo → ā™Š Gemini)')}`)
926
- lines.push(` ${key('F')} Toggle favorite on selected row ${hint('(⭐ persisted across sessions)')}`)
931
+ lines.push(` ${key('F')} Toggle favorite on selected row ${hint('(1ļøāƒ£2ļøāƒ£3ļøāƒ£ = router fallback order, capped at šŸ”Ÿ)')}`)
932
+ lines.push(` ${key('⇧↑/⇧↓')} Reorder selected favorite up/down ${hint('(changes router priority)')}`)
927
933
  lines.push(` ${key('Y')} Toggle favorites mode ${hint('(Pinned + always visible ↔ Normal filter/sort behavior)')}`)
928
934
  lines.push(` ${key('X')} Clear active text filter ${hint('(remove custom query applied from āš”ļø Command Palette)')}`)
929
935
  lines.push(` ${key('Q')} Smart Recommend ${hint('(šŸŽÆ find the best model for your task — questionnaire + live analysis)')}`)
930
936
  lines.push(` ${key('G')} Cycle theme ${hint('(auto → dark → light)')}`)
931
- lines.push(` ${themeColors.errorBold('I')} Feedback, bugs & requests ${hint('(šŸ“ send anonymous feedback, bug reports, or feature requests)')}`)
937
+
932
938
  lines.push(` ${key('P')} Open settings ${hint('(manage API keys, provider toggles, updates, legacy cleanup)')}`)
933
939
  // šŸ“– Profile system removed - API keys now persist permanently across all sessions
934
- lines.push(` ${key('Shift+R')} Reset view settings ${hint('(tier filter, sort, provider filter → defaults)')}`)
940
+ lines.push(` ${key('Ctrl+P')} Reset view settings ${hint('(search "Reset view" in the command palette)')}`)
935
941
  lines.push(` ${key('N')} Changelog ${hint('(šŸ“‹ browse all versions, Enter to view details)')}`)
936
- lines.push(` ${key('Ctrl+H')} / ${key('Esc')} Show/hide this help`)
942
+ lines.push(` ${key('I')} / ${key('Esc')} Show/hide this help`)
937
943
  lines.push(` ${key('Ctrl+C')} Exit`)
938
944
  lines.push('')
939
945
  lines.push(` ${heading('Settings (P)')}`)
@@ -1166,92 +1172,6 @@ export function createOverlayRenderers(state, deps) {
1166
1172
  }, PING_RATE)
1167
1173
  }
1168
1174
 
1169
- // ─── Feedback overlay renderer ────────────────────────────────────────────
1170
- // šŸ“– renderFeedback: Draw the overlay for anonymous Discord feedback.
1171
- // šŸ“– Shows an input field where users can type feedback, bug reports, or any comments.
1172
- function renderFeedback() {
1173
- const EL = '\x1b[K'
1174
- const lines = []
1175
-
1176
- // šŸ“– Calculate available space for multi-line input (dynamic based on terminal width)
1177
- const maxInputWidth = state.terminalCols - 8 // 8 = padding (4 spaces each side)
1178
- const maxInputLines = 10 // Show up to 10 lines of input
1179
-
1180
- // šŸ“– Split buffer into lines for display (with wrapping)
1181
- const wrapText = (text, width) => {
1182
- const words = text.split(' ')
1183
- const lines = []
1184
- let currentLine = ''
1185
-
1186
- for (const word of words) {
1187
- const testLine = currentLine ? currentLine + ' ' + word : word
1188
- if (testLine.length <= width) {
1189
- currentLine = testLine
1190
- } else {
1191
- if (currentLine) lines.push(currentLine)
1192
- currentLine = word
1193
- }
1194
- }
1195
- if (currentLine) lines.push(currentLine)
1196
- return lines
1197
- }
1198
-
1199
- const inputLines = wrapText(state.bugReportBuffer, maxInputWidth)
1200
- const displayLines = inputLines.slice(0, maxInputLines)
1201
-
1202
- // šŸ“– Branding header
1203
- lines.push('')
1204
- lines.push(` ${themeColors.accent('šŸš€')} ${themeColors.accentBold('free-coding-models')} ${themeColors.dim(`v${LOCAL_VERSION}`)}`)
1205
- lines.push(` ${themeColors.successBold('šŸ“ Feedback, bugs & requests')}`)
1206
- lines.push('')
1207
- lines.push(themeColors.dim(" — don't hesitate to send us feedback, bug reports, or just your feeling about the app"))
1208
- lines.push('')
1209
-
1210
- // šŸ“– Status messages (if any)
1211
- if (state.bugReportStatus === 'sending') {
1212
- lines.push(` ${themeColors.warning('ā³ Sending...')}`)
1213
- lines.push('')
1214
- } else if (state.bugReportStatus === 'success') {
1215
- lines.push(` ${themeColors.successBold('āœ… Successfully sent!')} ${themeColors.dim('Closing overlay in 3 seconds...')}`)
1216
- lines.push('')
1217
- lines.push(` ${themeColors.dim('Thank you for your feedback! It has been sent to the project team.')}`)
1218
- lines.push('')
1219
- } else if (state.bugReportStatus === 'error') {
1220
- lines.push(` ${themeColors.error('āŒ Error:')} ${themeColors.warning(state.bugReportError || 'Failed to send')}`)
1221
- lines.push(` ${themeColors.dim('Press Backspace to edit, or Esc to close')}`)
1222
- lines.push('')
1223
- } else {
1224
- lines.push(` ${themeColors.dim('Type your feedback below. Press Enter to send, Esc to cancel.')}`)
1225
- lines.push(` ${themeColors.dim('Your message will be sent anonymously to the project team.')}`)
1226
- lines.push('')
1227
- }
1228
-
1229
- // šŸ“– Simple input area – left-aligned, framed by horizontal lines
1230
- lines.push(` ${themeColors.info('Message')} (${state.bugReportBuffer.length}/500 chars)`)
1231
- lines.push(` ${themeColors.dim('─'.repeat(maxInputWidth))}`)
1232
- // šŸ“– Input lines — left-aligned, or placeholder when empty
1233
- if (displayLines.length > 0) {
1234
- for (const line of displayLines) {
1235
- lines.push(` ${line}`)
1236
- }
1237
- // šŸ“– Show cursor on last line
1238
- if (state.bugReportStatus === 'idle' || state.bugReportStatus === 'error') {
1239
- lines[lines.length - 1] += themeColors.accentBold('ā–')
1240
- }
1241
- } else {
1242
- const placeholderBR = state.bugReportStatus === 'idle' ? chalk.italic.rgb(...getProviderRgb('googleai'))('Type your message here...') : ''
1243
- lines.push(` ${placeholderBR}${themeColors.accentBold('ā–')}`)
1244
- }
1245
- lines.push(` ${themeColors.dim('─'.repeat(maxInputWidth))}`)
1246
- lines.push('')
1247
- lines.push(themeColors.dim(' Enter Send • Esc Cancel • Backspace Delete'))
1248
-
1249
- // šŸ“– Apply overlay tint and return
1250
- const tintedLines = tintOverlayLines(lines, themeColors.overlayBgFeedback, state.terminalCols)
1251
- const cleared = tintedLines.map(l => l + EL)
1252
- return cleared.join('\n')
1253
- }
1254
-
1255
1175
  // ─── Changelog overlay renderer ───────────────────────────────────────────
1256
1176
  // šŸ“– renderChangelog: Two-phase overlay — index of all versions or details of one version
1257
1177
  function renderChangelog() {
@@ -1394,6 +1314,10 @@ export function createOverlayRenderers(state, deps) {
1394
1314
  if (state.recommendPingTimer) { clearInterval(state.recommendPingTimer); state.recommendPingTimer = null }
1395
1315
  }
1396
1316
 
1317
+ function renderRouterDashboard() {
1318
+ return renderRouterDashboardOverlay(state, { LOCAL_VERSION })
1319
+ }
1320
+
1397
1321
  // ─── Incompatible fallback overlay ─────────────────────────────────────────
1398
1322
  // šŸ“– renderIncompatibleFallback shows when user presses Enter on a model that
1399
1323
  // šŸ“– is NOT compatible with the active tool. Two sections:
@@ -1490,6 +1414,181 @@ export function createOverlayRenderers(state, deps) {
1490
1414
  return cleared.join('\n')
1491
1415
  }
1492
1416
 
1417
+
1418
+ // ─── Token Usage screen renderer ───────────────────────────────────────────
1419
+ // šŸ“– renderTokenUsage: shows today/all-time breakdowns, by-model breakdown,
1420
+ // šŸ“– and a 7-day bar chart. Triggered by Shift+T from the main table.
1421
+ // šŸ“– Data fetched from GET /stats/tokens on the daemon.
1422
+ function renderTokenUsage() {
1423
+ const EL = '\x1b[K'
1424
+ const lines = []
1425
+ const cursorLineByRow = {}
1426
+
1427
+ lines.push('')
1428
+ lines.push(` ${themeColors.accent('šŸš€')} ${themeColors.accentBold('free-coding-models')} ${themeColors.dim(`v${LOCAL_VERSION}`)}`)
1429
+ lines.push(` ${themeColors.textBold('šŸ“Š Token Usage')} ${themeColors.dim('Shift+T from main table')}`)
1430
+ lines.push('')
1431
+
1432
+ const data = state.tokenUsageData
1433
+
1434
+ if (state.tokenUsageError) {
1435
+ lines.push(` ${themeColors.warning(state.tokenUsageError)}`)
1436
+ lines.push('')
1437
+ lines.push(themeColors.dim(' Press Shift+S to start the router daemon first, then reopen this screen.'))
1438
+ lines.push(themeColors.dim(' Esc to return to the main table'))
1439
+ const { visible, offset } = sliceOverlayLines(lines, state.tokenUsageScrollOffset, state.terminalRows)
1440
+ state.tokenUsageScrollOffset = offset
1441
+ const tintedLines = tintOverlayLines(visible, themeColors.overlayBgSettings, state.terminalCols)
1442
+ return tintedLines.map((l) => l + EL).join('\n')
1443
+ }
1444
+
1445
+ if (!data) {
1446
+ lines.push(themeColors.dim(' Loading token stats...'))
1447
+ const { visible, offset } = sliceOverlayLines(lines, state.tokenUsageScrollOffset, state.terminalRows)
1448
+ state.tokenUsageScrollOffset = offset
1449
+ const tintedLines = tintOverlayLines(visible, themeColors.overlayBgSettings, state.terminalCols)
1450
+ return tintedLines.map((l) => l + EL).join('\n')
1451
+ }
1452
+
1453
+ const today = data.today || {}
1454
+ const allTime = data.all_time || {}
1455
+ const dailyData = data.daily || {}
1456
+
1457
+ const todayTotal = today.total_tokens || 0
1458
+ const todayPrompt = today.prompt_tokens || 0
1459
+ const todayCompletion = today.completion_tokens || 0
1460
+ const todayReq = today.requests || 0
1461
+ const allTimeTotal = allTime.total_tokens || 0
1462
+ const allTimeReq = allTime.requests || 0
1463
+ const firstTracked = allTime.first_tracked || null
1464
+
1465
+ lines.push(` ${themeColors.textBold('TODAY')} ${themeColors.dim(new Date().toISOString().slice(0, 10))} ${themeColors.dim('|')} ${themeColors.textBold('ALL TIME')}`)
1466
+ lines.push(` ${themeColors.dim('─'.repeat(40))} ${themeColors.dim('─'.repeat(30))}`)
1467
+ lines.push(` ${themeColors.textBold('Total:')} ${themeColors.info(formatTokenTotalCompact(todayTotal))} tok ${themeColors.dim('│')} ${themeColors.textBold('Total:')} ${themeColors.info(formatTokenTotalCompact(allTimeTotal))} tok`)
1468
+ lines.push(` ${themeColors.textBold('Prompt:')} ${themeColors.dim(formatTokenTotalCompact(todayPrompt))} tok ${themeColors.dim('│')} ${themeColors.textBold('Requests:')} ${themeColors.dim(String(allTimeReq))}`)
1469
+ lines.push(` ${themeColors.textBold('Completion:')} ${themeColors.dim(formatTokenTotalCompact(todayCompletion))} tok ${themeColors.dim('│')} ${themeColors.textBold('Since:')} ${themeColors.dim(firstTracked ? new Date(firstTracked).toLocaleDateString() : '—')}`)
1470
+ lines.push(` ${themeColors.textBold('Requests:')} ${themeColors.dim(String(todayReq))} ${themeColors.dim('│')}`)
1471
+
1472
+ const byModel = today.by_model || {}
1473
+ const sortedModels = Object.entries(byModel)
1474
+ .map(([key, val]) => {
1475
+ // šŸ“– val can be a number (legacy) or { total, prompt, completion } object
1476
+ const total = (val && typeof val === 'object' && !Array.isArray(val)) ? (val.total || 0) : Number(val) || 0
1477
+ return { key, total }
1478
+ })
1479
+ .filter((m) => m.total > 0)
1480
+ .sort((a, b) => b.total - a.total)
1481
+ .slice(0, 8)
1482
+
1483
+ lines.push('')
1484
+ lines.push(` ${themeColors.textBold('TOP MODELS TODAY')}`)
1485
+ if (sortedModels.length === 0) {
1486
+ lines.push(themeColors.dim(' No usage tracked yet today.'))
1487
+ } else {
1488
+ const maxTotal = sortedModels[0]?.total || 1
1489
+ for (const m of sortedModels) {
1490
+ const barLen = Math.max(2, Math.round((m.total / maxTotal) * 28))
1491
+ const bar = themeColors.success('ā–ˆ'.repeat(barLen)) + themeColors.dim('ā–‘'.repeat(28 - barLen))
1492
+ const pct = todayTotal > 0 ? Math.round((m.total / todayTotal) * 100) : 0
1493
+ lines.push(` ${bar} ${themeColors.textBold(formatTokenTotalCompact(m.total))} tok ${themeColors.dim(`${pct}% ${m.key}`)}`)
1494
+ }
1495
+ }
1496
+
1497
+ lines.push('')
1498
+ lines.push(` ${themeColors.textBold('LAST 7 DAYS')}`)
1499
+ const dayLabels = []
1500
+ const dayTotals = []
1501
+ for (let i = 6; i >= 0; i--) {
1502
+ const d = new Date()
1503
+ d.setDate(d.getDate() - i)
1504
+ const key = d.toISOString().slice(0, 10)
1505
+ const dayData = dailyData[key]
1506
+ const total = dayData?.total_tokens || 0
1507
+ dayLabels.push(d.toLocaleDateString('en-US', { weekday: 'short' }))
1508
+ dayTotals.push(total)
1509
+ }
1510
+ const maxDay = Math.max(...dayTotals, 1)
1511
+ lines.push(` ${dayLabels.map((l, i) => themeColors.dim(padEndDisplay(l, 6))).join(' ')}`)
1512
+ const barHeights = [14, 10, 7, 4]
1513
+ for (const bh of barHeights) {
1514
+ const row = dayTotals.map((t) => {
1515
+ const filled = Math.round((t / maxDay) * bh)
1516
+ const bar = themeColors.info('ā–ˆ'.repeat(filled)) + themeColors.dim('ā–‘'.repeat(bh - filled))
1517
+ return padEndDisplay(bar, 6)
1518
+ })
1519
+ lines.push(` ${row.join(' ')}`)
1520
+ }
1521
+ const totalRow = dayTotals.map((t) => padEndDisplay(themeColors.textBold(formatTokenTotalCompact(t)), 6))
1522
+ lines.push(` ${totalRow.join(' ')}`)
1523
+
1524
+ lines.push('')
1525
+ lines.push(themeColors.dim(' Esc Back to main table'))
1526
+
1527
+ const { visible, offset } = sliceOverlayLines(lines, state.tokenUsageScrollOffset, state.terminalRows)
1528
+ state.tokenUsageScrollOffset = offset
1529
+ const tintedLines = tintOverlayLines(visible, themeColors.overlayBgSettings, state.terminalCols)
1530
+ return tintedLines.map((l) => l + EL).join('\n')
1531
+ }
1532
+
1533
+ // ─── Router Onboarding overlay renderer ─────────────────────────────────────
1534
+ // šŸ“– renderRouterOnboarding: shown on first launch (no config.router) or
1535
+ // šŸ“– first launch after upgrade (existing config but router.onboardingSeen !== true).
1536
+ // šŸ“– Two options: Enable (Y) or Not now (N). Phase 6 — Smart Model Router.
1537
+ function renderRouterOnboarding() {
1538
+ const EL = '\x1b[K'
1539
+ const lines = []
1540
+ const cursorLineByRow = {}
1541
+
1542
+ lines.push('')
1543
+ lines.push(` ${themeColors.accent('šŸš€')} ${themeColors.accentBold('free-coding-models')} ${themeColors.dim(`v${LOCAL_VERSION}`)}`)
1544
+ lines.push(` ${themeColors.textBold('šŸ”€ Smart Router Available!')}`)
1545
+ lines.push('')
1546
+ lines.push(themeColors.dim(' FCM can run a background daemon that automatically'))
1547
+ lines.push(themeColors.dim(' routes your requests to the fastest healthy model —'))
1548
+ lines.push(themeColors.dim(' with zero manual intervention after initial setup.'))
1549
+ lines.push('')
1550
+
1551
+ const options = [
1552
+ { label: 'Yes, enable the router', hint: 'Recommended — creates default set and starts daemon', key: 'Y' },
1553
+ { label: 'Not now', hint: 'You can enable it later from the TUI', key: 'N' },
1554
+ ]
1555
+
1556
+ if (state.routerOnboardingPhase === 'loading') {
1557
+ lines.push(themeColors.info(' Enabling router, please wait...'))
1558
+ } else if (state.routerOnboardingPhase === 'success') {
1559
+ lines.push(themeColors.success(' āœ… Router enabled! Dashboard opening...'))
1560
+ lines.push(themeColors.dim(' Setup complete. Return to the main table to continue.'))
1561
+ } else if (state.routerOnboardingPhase === 'error') {
1562
+ lines.push(themeColors.error(` āŒ ${state.routerOnboardingError || 'Failed to enable router'}`))
1563
+ lines.push(themeColors.dim(' Press Esc or Enter to continue to the main table'))
1564
+ } else {
1565
+ for (let i = 0; i < options.length; i++) {
1566
+ const opt = options[i]
1567
+ const isCursor = i === state.routerOnboardingCursor
1568
+ const keyLabel = themeColors.hotkey(` ${opt.key}]`)
1569
+ const row = `${bullet(isCursor)}${keyLabel} ${isCursor ? themeColors.textBold(opt.label) : themeColors.text(opt.label)}`
1570
+ cursorLineByRow[i] = lines.length
1571
+ lines.push(isCursor ? themeColors.bgCursorSettingsList(row) : row)
1572
+ lines.push(themeColors.dim(` ${opt.hint}`))
1573
+ lines.push('')
1574
+ }
1575
+ lines.push(themeColors.dim(' ↑↓ Navigate • Enter Select • Esc Skip for now'))
1576
+ lines.push('')
1577
+ lines.push(
1578
+ themeColors.dim(' šŸ’¬ ') +
1579
+ themeColors.footerDiscord('\x1b]8;;https://discord.gg/ZTNFHvvCkU\x1b\\Join the Discord community\x1b]8;;\x1b\\') +
1580
+ themeColors.dim(' • Get help, share feedback, follow updates')
1581
+ )
1582
+ }
1583
+
1584
+ const targetLine = cursorLineByRow[state.routerOnboardingCursor] ?? 0
1585
+ state.routerOnboardingScrollOffset = keepOverlayTargetVisible(state.routerOnboardingScrollOffset, targetLine, lines.length, state.terminalRows)
1586
+ const { visible, offset } = sliceOverlayLines(lines, state.routerOnboardingScrollOffset, state.terminalRows)
1587
+ state.routerOnboardingScrollOffset = offset
1588
+ const tintedLines = tintOverlayLines(visible, themeColors.overlayBgSettings, state.terminalCols)
1589
+ return tintedLines.map((l) => l + EL).join('\n')
1590
+ }
1591
+
1493
1592
  return {
1494
1593
  renderSettings,
1495
1594
  renderInstallEndpoints,
@@ -1497,12 +1596,14 @@ export function createOverlayRenderers(state, deps) {
1497
1596
  renderCommandPalette,
1498
1597
  renderHelp,
1499
1598
  renderRecommend,
1500
- renderFeedback,
1501
1599
  renderChangelog,
1502
1600
  renderInstalledModels,
1601
+ renderRouterDashboard,
1503
1602
  renderIncompatibleFallback,
1603
+ renderTokenUsage,
1604
+ renderRouterOnboarding,
1504
1605
  startRecommendAnalysis,
1505
1606
  stopRecommendAnalysis,
1506
- overlayLayout, // šŸ“– Mouse support: exposes cursor-to-line maps for click handling
1607
+ overlayLayout,
1507
1608
  }
1508
1609
  }
@@ -45,22 +45,17 @@ export const ENV_VAR_NAMES = {
45
45
  cerebras: 'CEREBRAS_API_KEY',
46
46
  sambanova: 'SAMBANOVA_API_KEY',
47
47
  openrouter: 'OPENROUTER_API_KEY',
48
- huggingface:'HUGGINGFACE_API_KEY',
49
- replicate: 'REPLICATE_API_TOKEN',
50
- deepinfra: 'DEEPINFRA_API_KEY',
51
- fireworks: 'FIREWORKS_API_KEY',
52
- codestral: 'CODESTRAL_API_KEY',
53
- hyperbolic: 'HYPERBOLIC_API_KEY',
48
+ 'github-models': 'GITHUB_TOKEN',
49
+ mistral: 'MISTRAL_API_KEY',
50
+ codestral: 'MISTRAL_API_KEY',
54
51
  scaleway: 'SCALEWAY_API_KEY',
55
52
  googleai: 'GOOGLE_API_KEY',
56
- siliconflow:'SILICONFLOW_API_KEY',
57
- together: 'TOGETHER_API_KEY',
58
53
  cloudflare: 'CLOUDFLARE_API_TOKEN',
59
- perplexity: 'PERPLEXITY_API_KEY',
60
54
  zai: 'ZAI_API_KEY',
61
55
  gemini: 'GEMINI_API_KEY',
62
- chutes: 'CHUTES_API_KEY',
63
56
  ovhcloud: 'OVH_AI_ENDPOINTS_ACCESS_TOKEN',
57
+ qwen: 'DASHSCOPE_API_KEY',
58
+ 'opencode-zen': 'OPENCODE_ZEN_API_KEY',
64
59
  }
65
60
 
66
61
  // šŸ“– OPENCODE_MODEL_MAP: sparse table of model IDs that differ between sources.js and OpenCode's
@@ -103,7 +98,7 @@ export const PROVIDER_METADATA = {
103
98
  color: chalk.rgb(255, 224, 178),
104
99
  signupUrl: 'https://cloud.sambanova.ai/apis',
105
100
  signupHint: 'SambaCloud portal → Create API key',
106
- rateLimits: 'Dev tier generous quota',
101
+ rateLimits: 'Small developer quota; useful for light coding and smoke tests',
107
102
  },
108
103
  openrouter: {
109
104
  label: 'OpenRouter',
@@ -113,6 +108,20 @@ export const PROVIDER_METADATA = {
113
108
  rateLimits: 'Free on :free: 50/day <$10, 1000/day ≄$10 (20 req/min)',
114
109
  detailedLimits: 'No credits (or <$10) → 50 requests/day (20 req/min)\n≄ $10 in credits → 1000 requests/day (20 req/min)\n• Free models (:free) never consume credits\n• Failed requests count toward quota\n• Quota resets daily at midnight UTC\n• Free-tier models may be rate-limited during peak hours',
115
110
  },
111
+ 'github-models': {
112
+ label: 'GitHub Models',
113
+ color: chalk.rgb(183, 201, 255),
114
+ signupUrl: 'https://models.github.ai',
115
+ signupHint: 'Use a GitHub token with Models access (GITHUB_TOKEN works in GitHub contexts)',
116
+ rateLimits: 'Quota depends on GitHub/Copilot tier; no separate provider billing',
117
+ },
118
+ mistral: {
119
+ label: 'Mistral LP',
120
+ color: chalk.rgb(255, 196, 120),
121
+ signupUrl: 'https://console.mistral.ai/api-keys',
122
+ signupHint: 'La Plateforme → API keys (MISTRAL_API_KEY)',
123
+ rateLimits: 'Experiment plan: free evaluation tier with limited RPS/TPM/monthly tokens',
124
+ },
116
125
  huggingface: {
117
126
  label: 'Hugging Face Inference',
118
127
  color: chalk.rgb(255, 245, 157),
@@ -146,9 +155,9 @@ export const PROVIDER_METADATA = {
146
155
  codestral: {
147
156
  label: 'Mistral Codestral',
148
157
  color: chalk.rgb(248, 187, 208),
149
- signupUrl: 'https://codestral.mistral.ai',
150
- signupHint: 'API Keys → Create',
151
- rateLimits: '30 req/min, 2000/day',
158
+ signupUrl: 'https://console.mistral.ai/api-keys',
159
+ signupHint: 'La Plateforme → API keys (MISTRAL_API_KEY; CODESTRAL_API_KEY also works)',
160
+ rateLimits: 'Codestral free access: 30 req/min, 2000/day',
152
161
  },
153
162
  hyperbolic: {
154
163
  label: 'Hyperbolic',
@@ -169,7 +178,7 @@ export const PROVIDER_METADATA = {
169
178
  color: chalk.rgb(187, 222, 251),
170
179
  signupUrl: 'https://aistudio.google.com/apikey',
171
180
  signupHint: 'Get API key',
172
- rateLimits: '14.4K req/day, 30/min',
181
+ rateLimits: 'Gemini free quotas vary by model and region',
173
182
  },
174
183
  siliconflow: {
175
184
  label: 'SiliconFlow',
@@ -211,7 +220,7 @@ export const PROVIDER_METADATA = {
211
220
  color: chalk.rgb(174, 213, 255),
212
221
  signupUrl: 'https://z.ai',
213
222
  signupHint: 'Sign up and generate an API key',
214
- rateLimits: 'Free tier (generous quota)',
223
+ rateLimits: 'Free tier: Flash models only in this catalog',
215
224
  },
216
225
  iflow: {
217
226
  label: 'iFlow',
@@ -240,7 +249,7 @@ export const PROVIDER_METADATA = {
240
249
  color: chalk.rgb(66, 165, 245), // blue
241
250
  signupUrl: 'https://github.com/google-gemini/gemini-cli',
242
251
  signupHint: 'Install: npm install -g @google/gemini-cli',
243
- rateLimits: 'Free tier: 1,000 req/day (personal Google account, no credit card)',
252
+ rateLimits: 'Free tier: 1,000 req/day with personal Google account',
244
253
  cliOnly: true,
245
254
  },
246
255
  'opencode-zen': {
@@ -29,7 +29,7 @@
29
29
  * @property {'header'|'endpoint'|'unknown'} telemetryType
30
30
  * @property {boolean} [supportsEndpoint]
31
31
  * @property {'percent'|'ok'} usageDisplay
32
- * @property {'rolling'|'daily'|'unknown'|'none'} resetCadence
32
+ * @property {'rolling'|'daily'|'monthly'|'unknown'|'none'} resetCadence
33
33
  */
34
34
 
35
35
  /** @type {Record<string, ProviderCapability>} */
@@ -39,26 +39,22 @@ export const PROVIDER_CAPABILITIES = {
39
39
  groq: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'daily' },
40
40
  cerebras: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
41
41
  sambanova: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
42
- deepinfra: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
43
- fireworks: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
44
- together: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
45
- hyperbolic: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
42
+ 'github-models': { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
43
+ mistral: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'monthly' },
46
44
  scaleway: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
47
45
  googleai: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'daily' },
48
46
  codestral: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'daily' },
49
- perplexity: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
50
47
  qwen: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
48
+ ovhcloud: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
51
49
 
52
50
  // Providers that have a dedicated usage/credits endpoint
53
51
  openrouter: { telemetryType: 'endpoint', supportsEndpoint: true, usageDisplay: 'percent', resetCadence: 'unknown' },
54
- siliconflow: { telemetryType: 'endpoint', supportsEndpoint: true, usageDisplay: 'ok', resetCadence: 'unknown' },
55
52
 
56
53
  // Providers with no reliable quota signal
57
- huggingface: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'none' },
58
- replicate: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'none' },
59
54
  cloudflare: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'daily' },
60
55
  zai: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'none' },
61
- iflow: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'none' },
56
+ gemini: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'daily' },
57
+ 'opencode-zen': { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'unknown' },
62
58
  }
63
59
 
64
60
  /** Fallback for unrecognized providers */