free-coding-models 0.5.6 → 0.5.8
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 +14 -9
- package/changelog/v0.5.7.md +20 -0
- package/package.json +1 -1
- package/src/core/endpoint-installer.js +3 -3
- package/src/core/tool-launchers.js +2 -2
- package/web/dist/assets/index-DKmmiDip.css +1 -0
- package/web/dist/assets/index-DzVt42Nf.js +39 -0
- package/web/dist/index.html +2 -2
- package/web/server.js +386 -1
- package/web/src/App.jsx +145 -26
- package/web/src/components/analytics/AnalyticsView.jsx +4 -0
- package/web/src/components/analytics/TokenUsagePanel.jsx +105 -0
- package/web/src/components/analytics/TokenUsagePanel.module.css +126 -0
- package/web/src/components/atoms/TierBadge.module.css +3 -3
- package/web/src/components/dashboard/DetailPanel.jsx +29 -4
- package/web/src/components/dashboard/DetailPanel.module.css +26 -0
- package/web/src/components/dashboard/FilterBar.jsx +17 -2
- package/web/src/components/dashboard/FilterBar.module.css +17 -0
- package/web/src/components/dashboard/ModelTable.jsx +17 -5
- package/web/src/components/dashboard/ModelTable.module.css +14 -1
- package/web/src/components/help/HelpView.jsx +1 -1
- package/web/src/components/install/InstallEndpointsView.jsx +278 -0
- package/web/src/components/install/InstallEndpointsView.module.css +351 -0
- package/web/src/components/installed/InstalledModelsView.jsx +89 -0
- package/web/src/components/installed/InstalledModelsView.module.css +200 -0
- package/web/src/components/launch/IncompatibleFallbackModal.jsx +69 -0
- package/web/src/components/launch/LaunchButton.jsx +30 -0
- package/web/src/components/launch/LaunchButton.module.css +35 -0
- package/web/src/components/launch/LaunchModal.module.css +125 -0
- package/web/src/components/layout/Header.jsx +70 -11
- package/web/src/components/layout/Header.module.css +62 -2
- package/web/src/components/recommend/RecommendView.jsx +121 -0
- package/web/src/components/recommend/RecommendView.module.css +55 -0
- package/web/src/components/router/RouterView.jsx +286 -0
- package/web/src/components/router/RouterView.module.css +357 -0
- package/web/src/components/tools/ToolPicker.jsx +86 -0
- package/web/src/components/tools/ToolPicker.module.css +84 -0
- package/web/src/global.css +23 -0
- package/web/src/hooks/urlState.constants.js +3 -0
- package/web/src/hooks/useInstalledModels.js +42 -0
- package/web/src/hooks/useRecommend.js +67 -0
- package/web/src/hooks/useRouterDashboard.js +133 -0
- package/web/src/hooks/useTokenUsage.js +103 -0
- package/web/src/hooks/useToolMode.js +77 -0
- package/web/src/hooks/useUrlState.js +4 -3
- package/web/src/utils/m3.js +55 -0
- package/web/dist/assets/index-Blp9QJev.js +0 -39
- package/web/dist/assets/index-Cz_aCLTR.css +0 -1
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-DzVt42Nf.js"></script>
|
|
52
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DKmmiDip.css">
|
|
53
53
|
</head>
|
|
54
54
|
<body>
|
|
55
55
|
<div id="root"></div>
|
package/web/server.js
CHANGED
|
@@ -46,6 +46,17 @@ import {
|
|
|
46
46
|
getStabilityScore,
|
|
47
47
|
} from '../src/core/utils.js'
|
|
48
48
|
import { benchmarkModel, BENCHMARK_TIMEOUT_MS } from '../src/core/benchmark.js'
|
|
49
|
+
import { getInstallTargetModes, installProviderEndpoints, getConfiguredInstallableProviders, getProviderCatalogModels } from '../src/core/endpoint-installer.js'
|
|
50
|
+
import { isModelCompatibleWithTool } from '../src/core/tool-metadata.js'
|
|
51
|
+
import { sendUsageTelemetry } from '../src/core/telemetry.js'
|
|
52
|
+
import { getRouterDaemonStatus, startRouterDaemonBackground, stopRouterDaemon, ROUTER_TOKENS_PATH } from '../src/core/router-daemon.js'
|
|
53
|
+
import { scanAllToolConfigs, softDeleteModel } from '../src/core/installed-models-manager.js'
|
|
54
|
+
import {
|
|
55
|
+
TASK_TYPES,
|
|
56
|
+
PRIORITY_TYPES,
|
|
57
|
+
CONTEXT_BUDGETS,
|
|
58
|
+
getTopRecommendations,
|
|
59
|
+
} from '../src/core/utils.js'
|
|
49
60
|
import {
|
|
50
61
|
PING_MODE_INTERVALS,
|
|
51
62
|
PING_MODE_CYCLE,
|
|
@@ -511,6 +522,108 @@ function runWithConcurrency(tasks, concurrency) {
|
|
|
511
522
|
})
|
|
512
523
|
}
|
|
513
524
|
|
|
525
|
+
const TOOL_MODE_ORDER = getInstallTargetModes()
|
|
526
|
+
const TOOL_MODES = new Set(TOOL_MODE_ORDER)
|
|
527
|
+
const DAEMON_PROXY_TIMEOUT_MS = 5000
|
|
528
|
+
|
|
529
|
+
// ─── Router daemon proxy helper ────────────────────────────────────────────
|
|
530
|
+
async function proxyToDaemon(path, options = {}) {
|
|
531
|
+
const port = await readDaemonPort()
|
|
532
|
+
if (!port) return null
|
|
533
|
+
try {
|
|
534
|
+
const url = `http://127.0.0.1:${port}${path}`
|
|
535
|
+
const resp = await fetch(url, { ...options, signal: AbortSignal.timeout(DAEMON_PROXY_TIMEOUT_MS) })
|
|
536
|
+
return { ok: resp.ok, status: resp.status, data: await resp.json().catch(() => null) }
|
|
537
|
+
} catch { return null }
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function readTokenFile() {
|
|
541
|
+
try {
|
|
542
|
+
if (!existsSync(ROUTER_TOKENS_PATH)) return null
|
|
543
|
+
return JSON.parse(readFileSync(ROUTER_TOKENS_PATH, 'utf8'))
|
|
544
|
+
} catch { return null }
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function normalizeToolMode(mode) {
|
|
548
|
+
return typeof mode === 'string' && TOOL_MODES.has(mode) ? mode : 'opencode'
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function getPreferredToolMode() {
|
|
552
|
+
return normalizeToolMode(config.settings?.preferredToolMode)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function persistPreferredToolMode(mode) {
|
|
556
|
+
const normalized = normalizeToolMode(mode)
|
|
557
|
+
if (!config.settings || typeof config.settings !== 'object') config.settings = {}
|
|
558
|
+
config.settings.preferredToolMode = normalized
|
|
559
|
+
const saveResult = saveConfig(config)
|
|
560
|
+
return { mode: normalized, saveResult }
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function getEndpointModel(providerKey, modelId) {
|
|
564
|
+
const result = getResult(providerKey, modelId)
|
|
565
|
+
if (!result) return null
|
|
566
|
+
return {
|
|
567
|
+
providerKey: result.providerKey,
|
|
568
|
+
modelId: result.modelId,
|
|
569
|
+
label: result.label,
|
|
570
|
+
tier: result.tier,
|
|
571
|
+
sweScore: result.sweScore,
|
|
572
|
+
ctx: result.ctx,
|
|
573
|
+
status: result.status,
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async function readDaemonPort() {
|
|
578
|
+
try {
|
|
579
|
+
const raw = readFileSync(`${process.env.HOME}/.free-coding-models-daemon.port`, 'utf8').trim()
|
|
580
|
+
if (/^\d+$/.test(raw)) return Number(raw)
|
|
581
|
+
} catch {}
|
|
582
|
+
return null
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async function syncFavoritesToRouter(selected) {
|
|
586
|
+
if (config?.router?.enabled !== true) return
|
|
587
|
+
const selKey = `${selected.providerKey}/${selected.modelId}`
|
|
588
|
+
const favorites = Array.isArray(config.favorites) ? config.favorites : []
|
|
589
|
+
const chain = [selKey, ...favorites.filter((entry) => entry !== selKey)]
|
|
590
|
+
const models = chain.map((entry, index) => {
|
|
591
|
+
const slashIdx = entry.indexOf('/')
|
|
592
|
+
const provider = slashIdx >= 0 ? entry.slice(0, slashIdx) : '?'
|
|
593
|
+
const model = slashIdx >= 0 ? entry.slice(slashIdx + 1) : entry
|
|
594
|
+
return { provider, model, priority: index + 1 }
|
|
595
|
+
})
|
|
596
|
+
try {
|
|
597
|
+
const port = await readDaemonPort()
|
|
598
|
+
if (!port) return
|
|
599
|
+
const baseUrl = `http://127.0.0.1:${port}`
|
|
600
|
+
const setPayload = { name: 'fast-coding', models, created: new Date().toISOString() }
|
|
601
|
+
await fetch(`${baseUrl}/sets/fast-coding`, {
|
|
602
|
+
method: 'PUT',
|
|
603
|
+
headers: { 'Content-Type': 'application/json' },
|
|
604
|
+
body: JSON.stringify(setPayload),
|
|
605
|
+
})
|
|
606
|
+
await fetch(`${baseUrl}/sets/fast-coding/activate`, { method: 'POST' })
|
|
607
|
+
} catch {}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
async function installEndpointForMode(mode, model) {
|
|
611
|
+
return installProviderEndpoints(config, model.providerKey, mode, {
|
|
612
|
+
scope: 'selected',
|
|
613
|
+
modelIds: [model.modelId],
|
|
614
|
+
})
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function buildRecommendReason(result, answers) {
|
|
618
|
+
const bits = []
|
|
619
|
+
if (result.tier) bits.push(`${result.tier} tier`)
|
|
620
|
+
if (result.sweScore && result.sweScore !== '—') bits.push(`${result.sweScore} SWE`)
|
|
621
|
+
if (result.ctx) bits.push(`${result.ctx} context`)
|
|
622
|
+
if (result.status === 'up') bits.push('currently up')
|
|
623
|
+
const priority = PRIORITY_TYPES[answers.priority]?.label || 'balanced'
|
|
624
|
+
return `${bits.join(' · ') || 'Strong catalog fit'} for ${priority.toLowerCase()} priority.`
|
|
625
|
+
}
|
|
626
|
+
|
|
514
627
|
async function handleRequest(req, res) {
|
|
515
628
|
res.setHeader('X-FCM-Server', SERVER_SIGNATURE)
|
|
516
629
|
res.setHeader('Access-Control-Allow-Origin', '*')
|
|
@@ -628,6 +741,110 @@ async function handleRequest(req, res) {
|
|
|
628
741
|
sendJson(res, 200, getConfigPayload())
|
|
629
742
|
return
|
|
630
743
|
|
|
744
|
+
// ── M3: shared tool mode — same preferredToolMode setting as the TUI Z cycle ──
|
|
745
|
+
case '/api/tool-mode': {
|
|
746
|
+
if (req.method === 'GET') {
|
|
747
|
+
sendJson(res, 200, { mode: getPreferredToolMode(), tools: TOOL_MODE_ORDER })
|
|
748
|
+
return
|
|
749
|
+
}
|
|
750
|
+
if (req.method !== 'POST') { res.writeHead(405); res.end('Method Not Allowed'); return }
|
|
751
|
+
const body = await readJsonBody(req)
|
|
752
|
+
if (!TOOL_MODES.has(body?.mode)) { sendJson(res, 422, { error: 'Invalid tool mode' }); return }
|
|
753
|
+
const { mode, saveResult } = persistPreferredToolMode(body.mode)
|
|
754
|
+
if (!saveResult.success) { sendJson(res, 500, { error: saveResult.error || 'Failed to save tool mode' }); return }
|
|
755
|
+
noteUserActivity()
|
|
756
|
+
sendJson(res, 200, { mode })
|
|
757
|
+
return
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// ── M3: install selected model endpoint into a tool config, no process spawn ──
|
|
761
|
+
case '/api/install-endpoint':
|
|
762
|
+
case '/api/launch': {
|
|
763
|
+
if (req.method !== 'POST') { res.writeHead(405); res.end('Method Not Allowed'); return }
|
|
764
|
+
const body = await readJsonBody(req)
|
|
765
|
+
const mode = normalizeToolMode(body?.toolMode || body?.mode || getPreferredToolMode())
|
|
766
|
+
const model = getEndpointModel(body?.providerKey, body?.modelId)
|
|
767
|
+
if (!model) { sendJson(res, 404, { error: 'Model not found' }); return }
|
|
768
|
+
if (!isModelCompatibleWithTool(model.providerKey, mode)) {
|
|
769
|
+
sendJson(res, 422, { error: 'Model is incompatible with selected tool', code: 'incompatible_model', mode, model })
|
|
770
|
+
return
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const { saveResult } = persistPreferredToolMode(mode)
|
|
774
|
+
if (!saveResult.success) { sendJson(res, 500, { error: saveResult.error || 'Failed to persist tool mode' }); return }
|
|
775
|
+
noteUserActivity()
|
|
776
|
+
try {
|
|
777
|
+
const installResult = await installEndpointForMode(mode, model)
|
|
778
|
+
void syncFavoritesToRouter(model)
|
|
779
|
+
void sendUsageTelemetry(config, { noTelemetry: false }, {
|
|
780
|
+
event: 'app_action',
|
|
781
|
+
mode,
|
|
782
|
+
properties: {
|
|
783
|
+
source: 'web',
|
|
784
|
+
action_type: 'install_endpoint',
|
|
785
|
+
tool_mode: mode,
|
|
786
|
+
provider: model.providerKey,
|
|
787
|
+
model_id: model.modelId,
|
|
788
|
+
model_label: model.label,
|
|
789
|
+
model_tier: model.tier,
|
|
790
|
+
},
|
|
791
|
+
})
|
|
792
|
+
sendJson(res, 200, { configured: true, mode, model, installResult })
|
|
793
|
+
} catch (err) {
|
|
794
|
+
sendJson(res, 422, {
|
|
795
|
+
error: err?.message || 'Failed to install endpoint',
|
|
796
|
+
code: 'endpoint_install_failed',
|
|
797
|
+
mode,
|
|
798
|
+
model,
|
|
799
|
+
})
|
|
800
|
+
}
|
|
801
|
+
return
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// ── M3: Smart Recommend — wraps the same core scoring engine as the TUI ──
|
|
805
|
+
case '/api/recommend': {
|
|
806
|
+
if (req.method !== 'POST') { res.writeHead(405); res.end('Method Not Allowed'); return }
|
|
807
|
+
const body = await readJsonBody(req)
|
|
808
|
+
const answers = body?.answers || {}
|
|
809
|
+
const taskType = answers.taskType
|
|
810
|
+
const priority = answers.priority
|
|
811
|
+
const contextBudget = answers.contextBudget
|
|
812
|
+
if (!TASK_TYPES[taskType] || !PRIORITY_TYPES[priority] || !CONTEXT_BUDGETS[contextBudget]) {
|
|
813
|
+
sendJson(res, 422, { error: 'Invalid recommendation answers' })
|
|
814
|
+
return
|
|
815
|
+
}
|
|
816
|
+
const top3 = getTopRecommendations(results, taskType, priority, contextBudget, 3)
|
|
817
|
+
.map(({ result, score }) => ({
|
|
818
|
+
result: serializeModel(result),
|
|
819
|
+
score,
|
|
820
|
+
reason: buildRecommendReason(result, answers),
|
|
821
|
+
}))
|
|
822
|
+
void sendUsageTelemetry(config, { noTelemetry: false }, {
|
|
823
|
+
event: 'app_action',
|
|
824
|
+
mode: getPreferredToolMode(),
|
|
825
|
+
properties: { source: 'web', action_type: 'smart_recommend', taskType, priority, contextBudget },
|
|
826
|
+
})
|
|
827
|
+
sendJson(res, 200, { top3, answers: { taskType, priority, contextBudget } })
|
|
828
|
+
return
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// ── M3: web telemetry mirror — never blocks UX and never exposes secrets ──
|
|
832
|
+
case '/api/telemetry/event': {
|
|
833
|
+
if (req.method !== 'POST') { res.writeHead(405); res.end('Method Not Allowed'); return }
|
|
834
|
+
const body = await readJsonBody(req)
|
|
835
|
+
const event = typeof body?.event === 'string' && body.event.trim() ? body.event.trim() : 'app_action'
|
|
836
|
+
const properties = body?.properties && typeof body.properties === 'object' && !Array.isArray(body.properties)
|
|
837
|
+
? body.properties
|
|
838
|
+
: {}
|
|
839
|
+
void sendUsageTelemetry(config, { noTelemetry: false }, {
|
|
840
|
+
event,
|
|
841
|
+
mode: getPreferredToolMode(),
|
|
842
|
+
properties: { ...properties, source: 'web' },
|
|
843
|
+
})
|
|
844
|
+
sendJson(res, 200, { ok: true })
|
|
845
|
+
return
|
|
846
|
+
}
|
|
847
|
+
|
|
631
848
|
case '/api/events':
|
|
632
849
|
res.writeHead(200, {
|
|
633
850
|
'Content-Type': 'text/event-stream',
|
|
@@ -900,7 +1117,175 @@ async function handleRequest(req, res) {
|
|
|
900
1117
|
return
|
|
901
1118
|
}
|
|
902
1119
|
|
|
903
|
-
|
|
1120
|
+
// ── M4: Router dashboard endpoints ────────────────────────────────────
|
|
1121
|
+
case '/api/router/status': {
|
|
1122
|
+
try {
|
|
1123
|
+
const status = await getRouterDaemonStatus()
|
|
1124
|
+
sendJson(res, 200, status)
|
|
1125
|
+
} catch (err) {
|
|
1126
|
+
sendJson(res, 200, { ok: false, running: false, error: err.message })
|
|
1127
|
+
}
|
|
1128
|
+
return
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
case '/api/router/stats': {
|
|
1132
|
+
const proxy = await proxyToDaemon('/stats')
|
|
1133
|
+
if (proxy?.ok) { sendJson(res, 200, proxy.data); return }
|
|
1134
|
+
sendJson(res, 200, { ok: false, running: false, error: 'Daemon not reachable' })
|
|
1135
|
+
return
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
case '/api/router/tokens': {
|
|
1139
|
+
// 📖 Try daemon first (live data), fall back to reading the token file
|
|
1140
|
+
const proxy = await proxyToDaemon('/stats/tokens')
|
|
1141
|
+
if (proxy?.ok) { sendJson(res, 200, proxy.data); return }
|
|
1142
|
+
const fileData = readTokenFile()
|
|
1143
|
+
if (fileData) { sendJson(res, 200, fileData); return }
|
|
1144
|
+
sendJson(res, 200, { daily: {}, all_time: { total_tokens: 0, prompt_tokens: 0, completion_tokens: 0, requests: 0 } })
|
|
1145
|
+
return
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
case '/api/router/start': {
|
|
1149
|
+
if (req.method !== 'POST') { res.writeHead(405); res.end('Method Not Allowed'); return }
|
|
1150
|
+
try {
|
|
1151
|
+
const result = await startRouterDaemonBackground()
|
|
1152
|
+
noteUserActivity()
|
|
1153
|
+
sendJson(res, 200, result)
|
|
1154
|
+
} catch (err) {
|
|
1155
|
+
sendJson(res, 500, { ok: false, error: err.message })
|
|
1156
|
+
}
|
|
1157
|
+
return
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
case '/api/router/stop': {
|
|
1161
|
+
if (req.method !== 'POST') { res.writeHead(405); res.end('Method Not Allowed'); return }
|
|
1162
|
+
try {
|
|
1163
|
+
const result = await stopRouterDaemon()
|
|
1164
|
+
sendJson(res, 200, result)
|
|
1165
|
+
} catch (err) {
|
|
1166
|
+
sendJson(res, 500, { ok: false, error: err.message })
|
|
1167
|
+
}
|
|
1168
|
+
return
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
case '/api/router/sets': {
|
|
1172
|
+
// 📖 Proxy set operations to the daemon
|
|
1173
|
+
if (req.method === 'GET') {
|
|
1174
|
+
const proxy = await proxyToDaemon('/sets')
|
|
1175
|
+
if (proxy?.ok) { sendJson(res, 200, proxy.data); return }
|
|
1176
|
+
// 📖 Fallback: read sets from config directly
|
|
1177
|
+
const routerConfig = config.router || {}
|
|
1178
|
+
sendJson(res, 200, { activeSet: routerConfig.activeSet || 'fast-coding', sets: routerConfig.sets || {} })
|
|
1179
|
+
return
|
|
1180
|
+
}
|
|
1181
|
+
if (req.method === 'POST') {
|
|
1182
|
+
const proxy = await proxyToDaemon('/sets', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(await readJsonBody(req)) })
|
|
1183
|
+
if (proxy?.ok) { sendJson(res, 200, proxy.data); return }
|
|
1184
|
+
if (proxy?.status === 201) { sendJson(res, 201, proxy.data); return }
|
|
1185
|
+
sendJson(res, proxy?.status || 502, proxy?.data || { error: 'Daemon not reachable' })
|
|
1186
|
+
return
|
|
1187
|
+
}
|
|
1188
|
+
res.writeHead(405); res.end('Method Not Allowed')
|
|
1189
|
+
return
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
case '/api/router/probe-mode': {
|
|
1193
|
+
if (req.method !== 'POST') { res.writeHead(405); res.end('Method Not Allowed'); return }
|
|
1194
|
+
const body = await readJsonBody(req)
|
|
1195
|
+
const proxy = await proxyToDaemon('/daemon/probe-mode', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
|
|
1196
|
+
if (proxy?.ok) { sendJson(res, 200, proxy.data); return }
|
|
1197
|
+
sendJson(res, proxy?.status || 502, proxy?.data || { error: 'Daemon not reachable' })
|
|
1198
|
+
return
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
case '/api/router/quick-setup': {
|
|
1202
|
+
// 📖 Return router connection info for quick clipboard copy
|
|
1203
|
+
const port = await readDaemonPort()
|
|
1204
|
+
const routerConfig = config.router || {}
|
|
1205
|
+
const activeSetName = routerConfig.activeSet || 'fast-coding'
|
|
1206
|
+
const baseUrl = port ? `http://127.0.0.1:${port}/v1` : null
|
|
1207
|
+
sendJson(res, 200, {
|
|
1208
|
+
running: !!port,
|
|
1209
|
+
port: port || null,
|
|
1210
|
+
baseUrl,
|
|
1211
|
+
model: 'fcm',
|
|
1212
|
+
activeSet: activeSetName,
|
|
1213
|
+
apiKey: 'not-needed',
|
|
1214
|
+
})
|
|
1215
|
+
return
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// ── M4: Installed Models — scan tool configs + soft-delete ────────────
|
|
1219
|
+
case '/api/installed-models': {
|
|
1220
|
+
if (req.method !== 'GET') { res.writeHead(405); res.end('Method Not Allowed'); return }
|
|
1221
|
+
const results = scanAllToolConfigs()
|
|
1222
|
+
sendJson(res, 200, { results })
|
|
1223
|
+
return
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// ── M4: Install Endpoints wizard — full provider install into tool ────
|
|
1227
|
+
case '/api/install-endpoints/providers': {
|
|
1228
|
+
if (req.method !== 'GET') { res.writeHead(405); res.end('Method Not Allowed'); return }
|
|
1229
|
+
const providers = getConfiguredInstallableProviders(config)
|
|
1230
|
+
sendJson(res, 200, { providers })
|
|
1231
|
+
return
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
case '/api/install-endpoints/catalog': {
|
|
1235
|
+
const catProvider = url.searchParams.get('provider')
|
|
1236
|
+
if (!catProvider) { sendJson(res, 400, { error: 'Missing ?provider= parameter' }); return }
|
|
1237
|
+
const models = getProviderCatalogModels(catProvider)
|
|
1238
|
+
sendJson(res, 200, { provider: catProvider, models })
|
|
1239
|
+
return
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
case '/api/install-endpoints/wizard': {
|
|
1243
|
+
if (req.method !== 'POST') { res.writeHead(405); res.end('Method Not Allowed'); return }
|
|
1244
|
+
const body = await readJsonBody(req)
|
|
1245
|
+
const wizProvider = body?.providerKey
|
|
1246
|
+
const wizTool = body?.toolMode
|
|
1247
|
+
const wizScope = body?.scope || 'all'
|
|
1248
|
+
const wizModelIds = body?.modelIds || []
|
|
1249
|
+
if (!wizProvider || !wizTool) {
|
|
1250
|
+
sendJson(res, 400, { error: 'Missing providerKey or toolMode' }); return
|
|
1251
|
+
}
|
|
1252
|
+
noteUserActivity()
|
|
1253
|
+
try {
|
|
1254
|
+
const installResult = installProviderEndpoints(config, wizProvider, wizTool, {
|
|
1255
|
+
scope: wizScope,
|
|
1256
|
+
modelIds: wizModelIds,
|
|
1257
|
+
})
|
|
1258
|
+
void sendUsageTelemetry(config, { noTelemetry: false }, {
|
|
1259
|
+
event: 'app_action',
|
|
1260
|
+
mode: wizTool,
|
|
1261
|
+
properties: {
|
|
1262
|
+
source: 'web',
|
|
1263
|
+
action_type: 'install_endpoints_wizard',
|
|
1264
|
+
provider: wizProvider,
|
|
1265
|
+
tool_mode: wizTool,
|
|
1266
|
+
scope: wizScope,
|
|
1267
|
+
model_count: installResult.modelCount || 0,
|
|
1268
|
+
},
|
|
1269
|
+
})
|
|
1270
|
+
sendJson(res, 200, { success: true, ...installResult })
|
|
1271
|
+
} catch (err) {
|
|
1272
|
+
sendJson(res, 422, { error: err.message, code: 'install_failed' })
|
|
1273
|
+
}
|
|
1274
|
+
return
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// 📖 M4: soft-delete an installed model (pattern match — must be before default:)
|
|
1278
|
+
// 📖 Path: /api/installed-models/:tool/:model/disable
|
|
1279
|
+
default: {
|
|
1280
|
+
const disableMatch = url.pathname.match(/^\/api\/installed-models\/([^/]+)\/([^/]+)\/disable$/)
|
|
1281
|
+
if (disableMatch && req.method === 'POST') {
|
|
1282
|
+
const toolMode = decodeURIComponent(disableMatch[1])
|
|
1283
|
+
const modelId = decodeURIComponent(disableMatch[2])
|
|
1284
|
+
const result = softDeleteModel(toolMode, modelId)
|
|
1285
|
+
sendJson(res, result.success ? 200 : 422, result)
|
|
1286
|
+
return
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
904
1289
|
// 📖 Serve Vite's /assets/* bundle, and our static favicon set that
|
|
905
1290
|
// 📖 Vite copies verbatim from web/public/ into web/dist/. The legacy
|
|
906
1291
|
// 📖 /favicon.ico lives at web/public/favicon.ico (root of public/).
|