free-coding-models 0.5.28 → 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.
- package/README.md +1 -1
- package/bin/free-coding-models.js +10 -2
- package/changelog/v0.5.29.md +19 -0
- package/changelog/v0.5.30.md +15 -0
- package/package.json +1 -1
- package/sources.js +3 -2
- package/src/core/config.js +1 -14
- package/src/core/endpoint-installer.js +5 -9
- package/src/core/legacy-proxy-cleanup.js +2 -0
- package/src/core/model-merger.js +4 -13
- package/src/core/playground.js +6 -4
- package/src/core/router-daemon.js +101 -43
- package/src/core/router-dashboard.js +63 -25
- package/src/core/shared-helpers.js +117 -0
- package/src/core/sync-set.js +11 -24
- package/src/core/tool-launchers.js +24 -92
- package/src/core/utils.js +26 -100
- package/src/tui/app.js +2 -2
- package/src/tui/command-palette.js +1 -0
- package/src/tui/key-handler.js +77 -20
- package/src/tui/render-table.js +3 -3
- package/web/dist/assets/index-CsFt3qt5.css +1 -0
- package/web/dist/assets/index-I6N91rH7.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 +34 -4
- 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 +268 -56
- package/web/src/components/router/RouterView.module.css +416 -1
- package/src/core/product-flags.js +0 -9
- package/web/dist/assets/index-BRFowJv5.css +0 -1
- package/web/dist/assets/index-Pr3waI0-.js +0 -41
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-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>
|
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
|
|
@@ -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
|
-
|
|
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 {
|
|
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;
|