free-coding-models 0.5.15 → 0.5.16

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.
@@ -48,10 +48,14 @@
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-CUYQh5_t.js"></script>
52
- <link rel="stylesheet" crossorigin href="/assets/index-BRqzsHVw.css">
51
+ <script type="module" crossorigin src="/assets/index-vsysCImz.js"></script>
52
+ <link rel="stylesheet" crossorigin href="/assets/index-avN2GM3H.css">
53
53
  </head>
54
54
  <body>
55
55
  <div id="root"></div>
56
+ <!-- 📖 PostHog analytics — privacy-first, self-hosted option available -->
57
+ <script>
58
+ !function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.",".js."),function(t,e,o,n){var i,s;for(n=function(t,e){return t[e]},s=e.split("."),i=0;i<s.length-1;i++)n=n(s[i],t);(a=n[s.pop()])&&a.apply(t,[].slice.call(arguments,0))}(e,"5.0.0")},e.init('phc_PLACEHOLDER_REPLACE_WITH_YOUR_KEY',{api_host:'https://us.i.posthog.com'}))}(document,window.posthog||[]);
59
+ </script>
56
60
  </body>
57
61
  </html>
package/web/index.html CHANGED
@@ -52,5 +52,9 @@
52
52
  <body>
53
53
  <div id="root"></div>
54
54
  <script type="module" src="/src/main.jsx"></script>
55
+ <!-- 📖 PostHog analytics — privacy-first, self-hosted option available -->
56
+ <script>
57
+ !function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.",".js."),function(t,e,o,n){var i,s;for(n=function(t,e){return t[e]},s=e.split("."),i=0;i<s.length-1;i++)n=n(s[i],t);(a=n[s.pop()])&&a.apply(t,[].slice.call(arguments,0))}(e,"5.0.0")},e.init('phc_PLACEHOLDER_REPLACE_WITH_YOUR_KEY',{api_host:'https://us.i.posthog.com'}))}(document,window.posthog||[]);
58
+ </script>
55
59
  </body>
56
60
  </html>
package/web/server.js CHANGED
@@ -45,7 +45,7 @@ import {
45
45
  getAvg, getVerdict, getUptime, getP95, getJitter,
46
46
  getStabilityScore,
47
47
  } from '../src/core/utils.js'
48
- import { benchmarkModel, BENCHMARK_TIMEOUT_MS } from '../src/core/benchmark.js'
48
+ import { benchmarkModel, BENCHMARK_TIMEOUT_MS, BENCHMARK_PROMPT } from '../src/core/benchmark.js'
49
49
  import { getInstallTargetModes, installProviderEndpoints, getConfiguredInstallableProviders, getProviderCatalogModels } from '../src/core/endpoint-installer.js'
50
50
  import { isModelCompatibleWithTool } from '../src/core/tool-metadata.js'
51
51
  import { sendUsageTelemetry } from '../src/core/telemetry.js'
@@ -1030,6 +1030,171 @@ async function handleRequest(req, res) {
1030
1030
  return
1031
1031
  }
1032
1032
 
1033
+ case '/api/benchmark-stream': {
1034
+ if (req.method !== 'POST') {
1035
+ res.writeHead(405)
1036
+ res.end('Method Not Allowed')
1037
+ return
1038
+ }
1039
+
1040
+ let body
1041
+ try {
1042
+ body = await readJsonBody(req)
1043
+ } catch {
1044
+ res.writeHead(400)
1045
+ res.end('Invalid JSON')
1046
+ return
1047
+ }
1048
+
1049
+ const result = getResult(body.providerKey, body.modelId)
1050
+ if (!result) {
1051
+ sendJson(res, 404, { error: 'Model not found' })
1052
+ return
1053
+ }
1054
+
1055
+ const source = sources[result.providerKey]
1056
+ const apiKey = getApiKey(config, result.providerKey)
1057
+ if (!apiKey) {
1058
+ sendJson(res, 401, { error: 'No API key configured' })
1059
+ return
1060
+ }
1061
+
1062
+ const key = getResultKey(result)
1063
+ benchmarkRunning.add(key)
1064
+ broadcastUpdate({ immediate: true })
1065
+
1066
+ // 📖 Set SSE headers for streaming response to the browser
1067
+ res.writeHead(200, {
1068
+ 'Content-Type': 'text/event-stream',
1069
+ 'Cache-Control': 'no-cache',
1070
+ 'Connection': 'keep-alive',
1071
+ 'X-Accel-Buffering': 'no',
1072
+ })
1073
+
1074
+ try {
1075
+ // Build the upstream URL — append /v1/chat/completions if needed
1076
+ let upstreamUrl = source?.url || result.url
1077
+ if (!upstreamUrl.includes('/chat/completions')) {
1078
+ upstreamUrl = upstreamUrl.replace(/\/+$/, '') + '/v1/chat/completions'
1079
+ }
1080
+
1081
+ // 📖 ZAI provider: strip the "zai/" prefix from modelId for the API
1082
+ let apiModelId = result.modelId
1083
+ if (result.providerKey === 'zai' && apiModelId.startsWith('zai/')) {
1084
+ apiModelId = apiModelId.slice(4)
1085
+ }
1086
+
1087
+ const upstreamHeaders = {
1088
+ 'Content-Type': 'application/json',
1089
+ 'Authorization': `Bearer ${apiKey}`,
1090
+ }
1091
+
1092
+ // 📖 OpenRouter requires HTTP-Referer and X-Title headers
1093
+ if (result.providerKey === 'openrouter') {
1094
+ upstreamHeaders['HTTP-Referer'] = 'https://github.com/vava-nessa/free-coding-models'
1095
+ upstreamHeaders['X-Title'] = 'free-coding-models'
1096
+ }
1097
+
1098
+ const reqBody = {
1099
+ model: apiModelId,
1100
+ messages: [{ role: 'user', content: BENCHMARK_PROMPT }],
1101
+ max_tokens: 140,
1102
+ temperature: 0,
1103
+ stream: true,
1104
+ }
1105
+
1106
+ const resp = await fetch(upstreamUrl, {
1107
+ method: 'POST',
1108
+ headers: upstreamHeaders,
1109
+ body: JSON.stringify(reqBody),
1110
+ signal: AbortSignal.timeout(20000),
1111
+ })
1112
+
1113
+ if (!resp.ok) {
1114
+ let message = `HTTP ${resp.status}`
1115
+ try {
1116
+ const errJson = await resp.json()
1117
+ message = errJson?.error?.message || errJson?.error || errJson?.message || message
1118
+ } catch { /* keep default */ }
1119
+ res.write('event: error\ndata: ' + JSON.stringify({ error: message }) + '\n\n')
1120
+ res.end()
1121
+ benchmarkRunning.delete(key)
1122
+ broadcastUpdate({ immediate: true })
1123
+ return
1124
+ }
1125
+
1126
+ // 📖 Parse SSE stream from the provider — extract tokens and measure TPS
1127
+ const reader = resp.body.getReader()
1128
+ const decoder = new TextDecoder()
1129
+ const t0 = performance.now()
1130
+ let tokenCount = 0
1131
+ let fullText = ''
1132
+ let buffer = ''
1133
+
1134
+ while (true) {
1135
+ const { done, value } = await reader.read()
1136
+ if (done) break
1137
+
1138
+ buffer += decoder.decode(value, { stream: true })
1139
+ const lines = buffer.split('\n')
1140
+ buffer = lines.pop() || '' // keep incomplete line in buffer
1141
+
1142
+ for (const line of lines) {
1143
+ const trimmed = line.trim()
1144
+ if (!trimmed.startsWith('data: ')) continue
1145
+ const payload = trimmed.slice(6)
1146
+ if (payload === '[DONE]') continue
1147
+
1148
+ let parsed
1149
+ try { parsed = JSON.parse(payload) } catch { continue }
1150
+
1151
+ const delta = parsed?.choices?.[0]?.delta?.content
1152
+ if (!delta) continue
1153
+
1154
+ fullText += delta
1155
+ tokenCount++
1156
+ res.write('event: token\ndata: ' + JSON.stringify({
1157
+ token: delta,
1158
+ totalMs: Math.round(performance.now() - t0),
1159
+ tokens: tokenCount,
1160
+ tps: Math.round(tokenCount / ((performance.now() - t0) / 1000) * 10) / 10,
1161
+ text: fullText,
1162
+ }) + '\n\n')
1163
+ }
1164
+ }
1165
+
1166
+ const totalMs = Math.round(performance.now() - t0)
1167
+ const tps = tokenCount / (totalMs / 1000)
1168
+
1169
+ res.write('event: done\ndata: ' + JSON.stringify({
1170
+ totalMs,
1171
+ outputTokens: tokenCount,
1172
+ tokensPerSecond: Math.round(tps * 10) / 10,
1173
+ answerPreview: fullText.slice(0, 100),
1174
+ }) + '\n\n')
1175
+ res.end()
1176
+
1177
+ // 📖 Update shared benchmark state so the dashboard reflects the result
1178
+ const benchmarkResult = {
1179
+ ok: true,
1180
+ totalMs,
1181
+ outputTokens: tokenCount,
1182
+ tokensPerSecond: tps,
1183
+ answerPreview: fullText.slice(0, 60),
1184
+ }
1185
+ benchmarkResults.set(key, benchmarkResult)
1186
+ benchmarkRunning.delete(key)
1187
+ updateHealthFromBenchmark(result, benchmarkResult)
1188
+ broadcastUpdate({ immediate: true })
1189
+ } catch (err) {
1190
+ res.write('event: error\ndata: ' + JSON.stringify({ error: err?.message || 'Stream failed' }) + '\n\n')
1191
+ res.end()
1192
+ benchmarkRunning.delete(key)
1193
+ broadcastUpdate({ immediate: true })
1194
+ }
1195
+ return
1196
+ }
1197
+
1033
1198
  case '/api/global-benchmark': {
1034
1199
  if (req.method === 'GET') {
1035
1200
  sendJson(res, 200, {
package/web/src/App.jsx CHANGED
@@ -77,6 +77,31 @@ export default function App() {
77
77
  const [toasts, setToasts] = useState([])
78
78
  const lastActivityRef = useRef(Date.now())
79
79
 
80
+ // 📖 PostHog: track app_web_start on mount
81
+ useEffect(() => {
82
+ try {
83
+ if (typeof window !== 'undefined' && window.posthog?.capture) {
84
+ window.posthog.capture('app_web_start', {
85
+ version: __APP_VERSION__ || 'unknown',
86
+ timestamp: new Date().toISOString(),
87
+ })
88
+ }
89
+ } catch {}
90
+ }, [])
91
+
92
+ // 📖 PostHog: track app_router_start when router opens
93
+ const handleRouterOpen = useCallback(() => {
94
+ setRouterOpen(true)
95
+ try {
96
+ if (typeof window !== 'undefined' && window.posthog?.capture) {
97
+ window.posthog.capture('app_router_start', {
98
+ version: __APP_VERSION__ || 'unknown',
99
+ timestamp: new Date().toISOString(),
100
+ })
101
+ }
102
+ } catch {}
103
+ }, [])
104
+
80
105
  // ── Toast helpers ────────────────────────────────────────────────────────
81
106
  const addToast = useCallback((message, type = 'info') => {
82
107
  const id = ++toastIdCounter
@@ -248,7 +273,7 @@ export default function App() {
248
273
  if (viewId === 'help') { setHelpOpen(true); return }
249
274
  if (viewId === 'changelog') { setChangelogOpen(true); setChangelogDefaultVersion(null); return }
250
275
  if (viewId === 'recommend') { setRecommendOpen(true); return }
251
- if (viewId === 'router') { setRouterOpen(true); return }
276
+ if (viewId === 'router') { handleRouterOpen(); return }
252
277
  if (viewId === 'playground') { setPlaygroundOpen(true); return }
253
278
  if (viewId === 'install-endpoints') { setInstallEndpointsOpen(true); return }
254
279
  if (viewId === 'installed-models') { setInstalledModelsOpen(true); return }
@@ -379,6 +404,10 @@ export default function App() {
379
404
  sortDirection={sortDirection}
380
405
  onSort={toggleSort}
381
406
  toolMode={toolMode}
407
+ onToast={addToast}
408
+ onSetToolMode={setToolMode}
409
+ onCycleToolMode={cycleToolMode}
410
+ onOpenFallback={(model) => setIncompatibleRequest({ model, toolMode })}
382
411
  />
383
412
  </main>
384
413
  )}
@@ -403,19 +432,7 @@ export default function App() {
403
432
  </div>
404
433
  </div>
405
434
 
406
- <DetailPanel
407
- model={selectedModel}
408
- onClose={handleCloseDetail}
409
- favorites={favorites}
410
- onBenchmark={handleBenchmarkRow}
411
- onLaunch={handleInstallEndpoint}
412
- toolMode={toolMode}
413
- onSetToolMode={setToolMode}
414
- onCycleToolMode={cycleToolMode}
415
- models={models}
416
- onOpenFallback={(model) => setIncompatibleRequest({ model, toolMode })}
417
- onToast={addToast}
418
- />
435
+ {/* 📖 DetailPanel side panel replaced by expand row in ModelTable */}
419
436
 
420
437
  {exportOpen && (
421
438
  <ExportModal