free-coding-models 0.5.29 → 0.5.31
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/README.md +1 -1
- package/changelog/v0.5.30.md +15 -0
- package/changelog/v0.5.31.md +6 -0
- package/package.json +1 -1
- package/src/core/playground.js +6 -4
- package/src/core/router-daemon.js +25 -2
- package/src/core/sync-set.js +5 -3
- package/web/dist/assets/index-CsFt3qt5.css +1 -0
- package/web/dist/assets/index-D2n0DqkV.js +40 -0
- package/web/dist/favicon.ico +0 -0
- package/web/dist/favicons/apple-touch-icon.png +0 -0
- package/web/dist/favicons/favicon-16x16.png +0 -0
- package/web/dist/favicons/favicon-192x192.png +0 -0
- package/web/dist/favicons/favicon-32x32.png +0 -0
- package/web/dist/favicons/favicon-48x48.png +0 -0
- package/web/dist/favicons/favicon-512x512.png +0 -0
- package/web/dist/favicons/favicon-96x96.png +0 -0
- package/web/dist/favicons/favicon.ico +0 -0
- package/web/dist/favicons/mstile-150x150.png +0 -0
- package/web/dist/favicons/mstile-310x150.png +0 -0
- package/web/dist/favicons/mstile-310x310.png +0 -0
- package/web/dist/favicons/mstile-512x512.png +0 -0
- package/web/dist/favicons/mstile-70x70.png +0 -0
- package/web/dist/index.html +2 -2
- package/web/public/favicon.ico +0 -0
- package/web/public/favicons/apple-touch-icon.png +0 -0
- package/web/public/favicons/favicon-16x16.png +0 -0
- package/web/public/favicons/favicon-192x192.png +0 -0
- package/web/public/favicons/favicon-32x32.png +0 -0
- package/web/public/favicons/favicon-48x48.png +0 -0
- package/web/public/favicons/favicon-512x512.png +0 -0
- package/web/public/favicons/favicon-96x96.png +0 -0
- package/web/public/favicons/favicon.ico +0 -0
- package/web/public/favicons/mstile-150x150.png +0 -0
- package/web/public/favicons/mstile-310x150.png +0 -0
- package/web/public/favicons/mstile-310x310.png +0 -0
- package/web/public/favicons/mstile-512x512.png +0 -0
- package/web/public/favicons/mstile-70x70.png +0 -0
- package/web/server.js +73 -4
- package/web/src/App.jsx +1 -0
- package/web/src/components/dashboard/ExpandedDetailRow.jsx +10 -113
- package/web/src/components/dashboard/ExpandedDetailRow.module.css +9 -0
- package/web/src/components/playground/PlaygroundChat.jsx +527 -0
- package/web/src/components/playground/PlaygroundChat.module.css +382 -0
- package/web/src/components/playground/PlaygroundView.jsx +104 -442
- package/web/src/components/playground/PlaygroundView.module.css +10 -0
- package/web/src/components/router/RouterView.jsx +198 -1
- package/web/src/components/router/RouterView.module.css +281 -0
- package/web/dist/assets/index-BMU58Jju.js +0 -41
- package/web/dist/assets/index-Dd1jOGEn.css +0 -1
package/web/dist/favicon.ico
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/web/dist/index.html
CHANGED
|
@@ -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-
|
|
52
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
51
|
+
<script type="module" crossorigin src="/assets/index-D2n0DqkV.js"></script>
|
|
52
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CsFt3qt5.css">
|
|
53
53
|
</head>
|
|
54
54
|
<body>
|
|
55
55
|
<div id="root"></div>
|
package/web/public/favicon.ico
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
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
|
-
|
|
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
|
|
@@ -748,6 +751,45 @@ async function handleRequest(req, res) {
|
|
|
748
751
|
return
|
|
749
752
|
}
|
|
750
753
|
|
|
754
|
+
// 📖 /api/router/sets/:name — PUT to edit/rename/replace models in the set.
|
|
755
|
+
const setPutMatch = url.pathname.match(/^\/api\/router\/sets\/([^/]+)$/)
|
|
756
|
+
if (setPutMatch && req.method === 'PUT') {
|
|
757
|
+
const setName = decodeURIComponent(setPutMatch[1])
|
|
758
|
+
const body = await readJsonBody(req)
|
|
759
|
+
const proxy = await proxyToDaemon(`/sets/${encodeURIComponent(setName)}`, {
|
|
760
|
+
method: 'PUT',
|
|
761
|
+
headers: { 'Content-Type': 'application/json' },
|
|
762
|
+
body: JSON.stringify(body),
|
|
763
|
+
})
|
|
764
|
+
if (proxy?.ok) { sendJson(res, 200, proxy.data); return }
|
|
765
|
+
// 📖 Fallback: if daemon is offline, edit config directly so the UI still works.
|
|
766
|
+
if (!proxy || !proxy.ok) {
|
|
767
|
+
if (!config.router) config.router = {}
|
|
768
|
+
if (!config.router.sets) config.router.sets = {}
|
|
769
|
+
if (config.router.sets[setName]) {
|
|
770
|
+
const nextName = typeof body.name === 'string' && body.name.trim() ? body.name.trim() : setName
|
|
771
|
+
const nextSets = { ...config.router.sets }
|
|
772
|
+
delete nextSets[setName]
|
|
773
|
+
nextSets[nextName] = {
|
|
774
|
+
...config.router.sets[setName],
|
|
775
|
+
...body,
|
|
776
|
+
name: nextName,
|
|
777
|
+
models: Array.isArray(body.models) ? body.models : config.router.sets[setName].models,
|
|
778
|
+
}
|
|
779
|
+
config.router.sets = nextSets
|
|
780
|
+
if (config.router.activeSet === setName) {
|
|
781
|
+
config.router.activeSet = nextName
|
|
782
|
+
}
|
|
783
|
+
saveConfig(config)
|
|
784
|
+
broadcastUpdate({ immediate: true })
|
|
785
|
+
sendJson(res, 200, { set: config.router.sets[nextName], router: config.router })
|
|
786
|
+
return
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
sendJson(res, proxy?.status || 502, proxy?.data || { error: 'Daemon not reachable' })
|
|
790
|
+
return
|
|
791
|
+
}
|
|
792
|
+
|
|
751
793
|
// 📖 /api/router/sets/:name/sync — re-run the probe-based sync pipeline
|
|
752
794
|
// 📖 against the named set. Used by the Web Router Dashboard's "Sync
|
|
753
795
|
// 📖 best models" button so the user can rebuild a set with models
|
|
@@ -1460,6 +1502,26 @@ async function handleRequest(req, res) {
|
|
|
1460
1502
|
return
|
|
1461
1503
|
}
|
|
1462
1504
|
|
|
1505
|
+
// 📖 /api/router/probe-all — launch AI Latency benchmarks on the active
|
|
1506
|
+
// 📖 set's models (or an explicit list) inside the DAEMON process. Results
|
|
1507
|
+
// 📖 flow back through /api/router/stats (per-model `benchmark` field +
|
|
1508
|
+
// 📖 `globalBenchmark` progress), so the Router Dashboard's set list shows
|
|
1509
|
+
// 📖 live AI latency after a probe. Proxied to the daemon's
|
|
1510
|
+
// 📖 /api/global-benchmark (longer timeout — probing ~10 models takes time).
|
|
1511
|
+
case '/api/router/probe-all': {
|
|
1512
|
+
if (req.method !== 'POST') { res.writeHead(405); res.end('Method Not Allowed'); return }
|
|
1513
|
+
const body = await readJsonBody(req)
|
|
1514
|
+
const proxy = await proxyToDaemon('/api/global-benchmark', {
|
|
1515
|
+
method: 'POST',
|
|
1516
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1517
|
+
body: JSON.stringify(body || {}),
|
|
1518
|
+
timeoutMs: 60000,
|
|
1519
|
+
})
|
|
1520
|
+
if (proxy?.ok || proxy?.status === 202) { sendJson(res, proxy?.status || 200, proxy.data); return }
|
|
1521
|
+
sendJson(res, proxy?.status || 502, proxy?.data || { error: 'Daemon not reachable' })
|
|
1522
|
+
return
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1463
1525
|
case '/api/router/catalog': {
|
|
1464
1526
|
// 📖 Catalog of routeable models for the Web Router Dashboard's
|
|
1465
1527
|
// 📖 "Add model" picker. Proxies the daemon when running, falls
|
|
@@ -1629,12 +1691,19 @@ async function handleRequest(req, res) {
|
|
|
1629
1691
|
})
|
|
1630
1692
|
if (wantsStream) {
|
|
1631
1693
|
// 📖 Pipe SSE events straight from the upstream to the browser.
|
|
1632
|
-
|
|
1694
|
+
// 📖 Forward the daemon's x-fcm-router-model header so the browser
|
|
1695
|
+
// 📖 knows WHICH model the priority-first router actually served
|
|
1696
|
+
// 📖 (the chunks only carry the bare model id, not provider/model).
|
|
1697
|
+
// 📖 See issue #120 — this makes the served-model badge work.
|
|
1698
|
+
const streamHeaders = {
|
|
1633
1699
|
'Content-Type': 'text/event-stream',
|
|
1634
1700
|
'Cache-Control': 'no-cache',
|
|
1635
1701
|
'Connection': 'keep-alive',
|
|
1636
1702
|
'X-Accel-Buffering': 'no',
|
|
1637
|
-
}
|
|
1703
|
+
}
|
|
1704
|
+
const servedModel = upstreamResp.headers.get('x-fcm-router-model')
|
|
1705
|
+
if (servedModel) streamHeaders['x-fcm-router-model'] = servedModel
|
|
1706
|
+
res.writeHead(upstreamResp.status, streamHeaders)
|
|
1638
1707
|
const reader = upstreamResp.body?.getReader()
|
|
1639
1708
|
if (!reader) { res.end(); clearTimeout(timeout); return }
|
|
1640
1709
|
try {
|
package/web/src/App.jsx
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* @functions ExpandedDetailRow → main component
|
|
13
13
|
*/
|
|
14
14
|
import { useState, useRef, useCallback } from 'react'
|
|
15
|
-
import {
|
|
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.
|
|
358
|
-
<
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
363
|
-
|
|
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;
|