free-coding-models 0.5.29 → 0.5.30

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 (48) hide show
  1. package/README.md +1 -1
  2. package/changelog/v0.5.30.md +15 -0
  3. package/package.json +1 -1
  4. package/src/core/playground.js +6 -4
  5. package/src/core/router-daemon.js +25 -2
  6. package/src/core/sync-set.js +5 -3
  7. package/web/dist/assets/index-CsFt3qt5.css +1 -0
  8. package/web/dist/assets/index-I6N91rH7.js +40 -0
  9. package/web/dist/favicon.ico +0 -0
  10. package/web/dist/favicons/apple-touch-icon.png +0 -0
  11. package/web/dist/favicons/favicon-16x16.png +0 -0
  12. package/web/dist/favicons/favicon-192x192.png +0 -0
  13. package/web/dist/favicons/favicon-32x32.png +0 -0
  14. package/web/dist/favicons/favicon-48x48.png +0 -0
  15. package/web/dist/favicons/favicon-512x512.png +0 -0
  16. package/web/dist/favicons/favicon-96x96.png +0 -0
  17. package/web/dist/favicons/favicon.ico +0 -0
  18. package/web/dist/favicons/mstile-150x150.png +0 -0
  19. package/web/dist/favicons/mstile-310x150.png +0 -0
  20. package/web/dist/favicons/mstile-310x310.png +0 -0
  21. package/web/dist/favicons/mstile-512x512.png +0 -0
  22. package/web/dist/favicons/mstile-70x70.png +0 -0
  23. package/web/dist/index.html +2 -2
  24. package/web/public/favicon.ico +0 -0
  25. package/web/public/favicons/apple-touch-icon.png +0 -0
  26. package/web/public/favicons/favicon-16x16.png +0 -0
  27. package/web/public/favicons/favicon-192x192.png +0 -0
  28. package/web/public/favicons/favicon-32x32.png +0 -0
  29. package/web/public/favicons/favicon-48x48.png +0 -0
  30. package/web/public/favicons/favicon-512x512.png +0 -0
  31. package/web/public/favicons/favicon-96x96.png +0 -0
  32. package/web/public/favicons/favicon.ico +0 -0
  33. package/web/public/favicons/mstile-150x150.png +0 -0
  34. package/web/public/favicons/mstile-310x150.png +0 -0
  35. package/web/public/favicons/mstile-310x310.png +0 -0
  36. package/web/public/favicons/mstile-512x512.png +0 -0
  37. package/web/public/favicons/mstile-70x70.png +0 -0
  38. package/web/server.js +34 -4
  39. package/web/src/components/dashboard/ExpandedDetailRow.jsx +10 -113
  40. package/web/src/components/dashboard/ExpandedDetailRow.module.css +9 -0
  41. package/web/src/components/playground/PlaygroundChat.jsx +527 -0
  42. package/web/src/components/playground/PlaygroundChat.module.css +382 -0
  43. package/web/src/components/playground/PlaygroundView.jsx +104 -442
  44. package/web/src/components/playground/PlaygroundView.module.css +10 -0
  45. package/web/src/components/router/RouterView.jsx +144 -0
  46. package/web/src/components/router/RouterView.module.css +281 -0
  47. package/web/dist/assets/index-BMU58Jju.js +0 -41
  48. package/web/dist/assets/index-Dd1jOGEn.css +0 -1
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -48,8 +48,8 @@
48
48
  <link rel="preconnect" href="https://fonts.googleapis.com">
49
49
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
50
50
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
51
- <script type="module" crossorigin src="/assets/index-BMU58Jju.js"></script>
52
- <link rel="stylesheet" crossorigin href="/assets/index-Dd1jOGEn.css">
51
+ <script type="module" crossorigin src="/assets/index-I6N91rH7.js"></script>
52
+ <link rel="stylesheet" crossorigin href="/assets/index-CsFt3qt5.css">
53
53
  </head>
54
54
  <body>
55
55
  <div id="root"></div>
Binary file
Binary file
package/web/server.js CHANGED
@@ -50,7 +50,7 @@ import { benchmarkModel, BENCHMARK_TIMEOUT_MS, BENCHMARK_PROMPT } from '../src/c
50
50
  import { getInstallTargetModes, installProviderEndpoints, getConfiguredInstallableProviders, getProviderCatalogModels } from '../src/core/endpoint-installer.js'
51
51
  import { isModelCompatibleWithTool } from '../src/core/tool-metadata.js'
52
52
  import { sendUsageTelemetry } from '../src/core/telemetry.js'
53
- import { getRouterDaemonStatus, startRouterDaemonBackground, stopRouterDaemon, ROUTER_TOKENS_PATH } from '../src/core/router-daemon.js'
53
+ import { getRouterDaemonStatus, startRouterDaemonBackground, stopRouterDaemon, ROUTER_TOKENS_PATH, getRouterPortPath } from '../src/core/router-daemon.js'
54
54
  import { scanAllToolConfigs, softDeleteModel } from '../src/core/installed-models-manager.js'
55
55
  import {
56
56
  TASK_TYPES,
@@ -590,7 +590,10 @@ function getEndpointModel(providerKey, modelId) {
590
590
 
591
591
  async function readDaemonPort() {
592
592
  try {
593
- const raw = readFileSync(`${process.env.HOME}/.free-coding-models-daemon.port`, 'utf8').trim()
593
+ // 📖 Use the dynamic port-path resolver so dev checkouts (FCM_DEV=1) read
594
+ // 📖 the `-dev` port file and find the dev daemon, instead of always
595
+ // 📖 reading the production file and missing it. Mirrors router-dashboard.js.
596
+ const raw = readFileSync(getRouterPortPath(), 'utf8').trim()
594
597
  if (/^\d+$/.test(raw)) return Number(raw)
595
598
  } catch {}
596
599
  return null
@@ -1460,6 +1463,26 @@ async function handleRequest(req, res) {
1460
1463
  return
1461
1464
  }
1462
1465
 
1466
+ // 📖 /api/router/probe-all — launch AI Latency benchmarks on the active
1467
+ // 📖 set's models (or an explicit list) inside the DAEMON process. Results
1468
+ // 📖 flow back through /api/router/stats (per-model `benchmark` field +
1469
+ // 📖 `globalBenchmark` progress), so the Router Dashboard's set list shows
1470
+ // 📖 live AI latency after a probe. Proxied to the daemon's
1471
+ // 📖 /api/global-benchmark (longer timeout — probing ~10 models takes time).
1472
+ case '/api/router/probe-all': {
1473
+ if (req.method !== 'POST') { res.writeHead(405); res.end('Method Not Allowed'); return }
1474
+ const body = await readJsonBody(req)
1475
+ const proxy = await proxyToDaemon('/api/global-benchmark', {
1476
+ method: 'POST',
1477
+ headers: { 'Content-Type': 'application/json' },
1478
+ body: JSON.stringify(body || {}),
1479
+ timeoutMs: 60000,
1480
+ })
1481
+ if (proxy?.ok || proxy?.status === 202) { sendJson(res, proxy?.status || 200, proxy.data); return }
1482
+ sendJson(res, proxy?.status || 502, proxy?.data || { error: 'Daemon not reachable' })
1483
+ return
1484
+ }
1485
+
1463
1486
  case '/api/router/catalog': {
1464
1487
  // 📖 Catalog of routeable models for the Web Router Dashboard's
1465
1488
  // 📖 "Add model" picker. Proxies the daemon when running, falls
@@ -1629,12 +1652,19 @@ async function handleRequest(req, res) {
1629
1652
  })
1630
1653
  if (wantsStream) {
1631
1654
  // 📖 Pipe SSE events straight from the upstream to the browser.
1632
- res.writeHead(upstreamResp.status, {
1655
+ // 📖 Forward the daemon's x-fcm-router-model header so the browser
1656
+ // 📖 knows WHICH model the priority-first router actually served
1657
+ // 📖 (the chunks only carry the bare model id, not provider/model).
1658
+ // 📖 See issue #120 — this makes the served-model badge work.
1659
+ const streamHeaders = {
1633
1660
  'Content-Type': 'text/event-stream',
1634
1661
  'Cache-Control': 'no-cache',
1635
1662
  'Connection': 'keep-alive',
1636
1663
  'X-Accel-Buffering': 'no',
1637
- })
1664
+ }
1665
+ const servedModel = upstreamResp.headers.get('x-fcm-router-model')
1666
+ if (servedModel) streamHeaders['x-fcm-router-model'] = servedModel
1667
+ res.writeHead(upstreamResp.status, streamHeaders)
1638
1668
  const reader = upstreamResp.body?.getReader()
1639
1669
  if (!reader) { res.end(); clearTimeout(timeout); return }
1640
1670
  try {
@@ -12,7 +12,7 @@
12
12
  * @functions ExpandedDetailRow → main component
13
13
  */
14
14
  import { useState, useRef, useCallback } from 'react'
15
- import { IconSend, IconPlayerPlayFilled, IconStar, IconStarFilled, IconLoader } from '@tabler/icons-react'
15
+ import { IconPlayerPlayFilled, IconStar, IconStarFilled, IconLoader } from '@tabler/icons-react'
16
16
  import TierBadge from '../atoms/TierBadge.jsx'
17
17
  import VerdictBadge from '../atoms/VerdictBadge.jsx'
18
18
  import StatusDot from '../atoms/StatusDot.jsx'
@@ -20,6 +20,7 @@ import StabilityCell from '../atoms/StabilityCell.jsx'
20
20
  import { formatAvg, pingClass } from '../../utils/format.js'
21
21
  import { sweClass } from '../../utils/ranks.js'
22
22
  import LaunchButton from '../launch/LaunchButton.jsx'
23
+ import PlaygroundChat from '../playground/PlaygroundChat.jsx'
23
24
  import styles from './ExpandedDetailRow.module.css'
24
25
 
25
26
  /**
@@ -61,12 +62,6 @@ export default function ExpandedDetailRow({
61
62
  onCycleToolMode,
62
63
  onOpenFallback,
63
64
  }) {
64
- // ─── Mini Playground state ───
65
- const [playgroundInput, setPlaygroundInput] = useState('')
66
- const [playgroundResponse, setPlaygroundResponse] = useState('')
67
- const [playgroundBusy, setPlaygroundBusy] = useState(false)
68
- const playgroundAbort = useRef(null)
69
-
70
65
  // ─── AI Latency benchmark state ───
71
66
  const [benchState, setBenchState] = useState('idle') // 'idle' | 'running' | 'done' | 'error'
72
67
  const [benchMetrics, setBenchMetrics] = useState({ latency: null, tokens: null, tps: null })
@@ -80,77 +75,6 @@ export default function ExpandedDetailRow({
80
75
  const avgData = formatAvg(model.avg)
81
76
  const avgCls = avgData.cls || pingClass(model.avg)
82
77
 
83
- // ─── Mini Playground: send a chat message ───
84
- const handlePlaygroundSend = useCallback(async () => {
85
- const text = playgroundInput.trim()
86
- if (!text || playgroundBusy) return
87
-
88
- setPlaygroundBusy(true)
89
- setPlaygroundResponse('')
90
- setPlaygroundInput('')
91
-
92
- // Cancel any previous request
93
- if (playgroundAbort.current) playgroundAbort.current.abort()
94
- const controller = new AbortController()
95
- playgroundAbort.current = controller
96
-
97
- try {
98
- const res = await fetch('/api/playground/chat', {
99
- method: 'POST',
100
- headers: { 'Content-Type': 'application/json' },
101
- body: JSON.stringify({
102
- model: `${model.providerKey}/${model.modelId}`,
103
- messages: [{ role: 'user', content: text }],
104
- stream: true,
105
- temperature: 0.7,
106
- }),
107
- signal: controller.signal,
108
- })
109
-
110
- if (!res.ok) {
111
- const errText = await res.text().catch(() => `HTTP ${res.status}`)
112
- setPlaygroundResponse(`Error: ${errText}`)
113
- return
114
- }
115
-
116
- const reader = res.body.getReader()
117
- const decoder = new TextDecoder()
118
- let accumulated = ''
119
-
120
- while (true) {
121
- const { done, value } = await reader.read()
122
- if (done) break
123
-
124
- const chunk = decoder.decode(value, { stream: true })
125
- // Parse SSE lines
126
- const lines = chunk.split('\n')
127
- for (const line of lines) {
128
- if (!line.startsWith('data: ')) continue
129
- const payload = line.slice(6).trim()
130
- if (payload === '[DONE]') continue
131
-
132
- try {
133
- const json = JSON.parse(payload)
134
- const content = json.choices?.[0]?.delta?.content
135
- if (content) {
136
- accumulated += content
137
- setPlaygroundResponse(accumulated)
138
- }
139
- } catch {
140
- // Non-JSON line — ignore
141
- }
142
- }
143
- }
144
- } catch (err) {
145
- if (err.name !== 'AbortError') {
146
- setPlaygroundResponse(`Error: ${err.message}`)
147
- }
148
- } finally {
149
- setPlaygroundBusy(false)
150
- playgroundAbort.current = null
151
- }
152
- }, [playgroundInput, playgroundBusy, model.providerKey, model.modelId])
153
-
154
78
  // ─── AI Latency: run benchmark ───
155
79
  const handleBenchStart = useCallback(async () => {
156
80
  if (benchState === 'running') return
@@ -349,47 +273,20 @@ export default function ExpandedDetailRow({
349
273
  </div>
350
274
  </div>
351
275
 
352
- {/* ═══════ Column 2: Mini Playground ═══════ */}
276
+ {/* ═══════ Column 2: Mini Playground (shared PlaygroundChat core) ═══════ */}
353
277
  <div className={styles.col}>
354
278
  <div className={styles.playgroundHeader}>
355
279
  💬 Mini Playground — {model.label}
356
280
  </div>
357
- <div className={styles.inputRow}>
358
- <input
359
- type="text"
360
- className={styles.inputField}
281
+ <div className={styles.playgroundWrap}>
282
+ <PlaygroundChat
283
+ model={`${model.providerKey}/${model.modelId}`}
284
+ variant="mini"
285
+ disabled={!model.hasApiKey}
361
286
  placeholder="Type a message…"
362
- value={playgroundInput}
363
- onChange={e => setPlaygroundInput(e.target.value)}
364
- onKeyDown={e => {
365
- if (e.key === 'Enter') handlePlaygroundSend()
366
- }}
367
- disabled={playgroundBusy}
287
+ emptyTitle={`Test ${model.label}`}
288
+ emptyHint="Send a message to test this model. Responses stream in real time with latency + TPS."
368
289
  />
369
- <button
370
- className={styles.sendBtn}
371
- onClick={handlePlaygroundSend}
372
- disabled={playgroundBusy || !playgroundInput.trim()}
373
- title="Send message"
374
- >
375
- {playgroundBusy
376
- ? <IconLoader size={14} stroke={1.8} className={styles.spinning} />
377
- : <IconSend size={14} stroke={1.8} />
378
- }
379
- </button>
380
- </div>
381
- <div className={styles.responseArea}>
382
- {playgroundBusy && !playgroundResponse ? (
383
- <span className={styles.responsePlaceholder}>
384
- Waiting for response…
385
- </span>
386
- ) : playgroundResponse ? (
387
- playgroundResponse
388
- ) : (
389
- <span className={styles.responsePlaceholder}>
390
- Send a message to test this model. Responses stream in real time.
391
- </span>
392
- )}
393
290
  </div>
394
291
  </div>
395
292
 
@@ -123,6 +123,15 @@
123
123
  margin-bottom: 2px;
124
124
  }
125
125
 
126
+ /* 📖 Wrapper that lets the shared PlaygroundChat fill the column height so its
127
+ transcript scrolls and the input stays pinned to the bottom. */
128
+ .playgroundWrap {
129
+ flex: 1;
130
+ display: flex;
131
+ flex-direction: column;
132
+ min-height: 140px;
133
+ }
134
+
126
135
  .inputRow {
127
136
  display: flex;
128
137
  gap: 6px;