@appkit/llamacpp-cli 2.0.0 → 2.1.0

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 (229) hide show
  1. package/README.md +271 -277
  2. package/dist/cli.js +133 -23
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/admin/config.d.ts +1 -1
  5. package/dist/commands/admin/config.js +5 -5
  6. package/dist/commands/admin/config.js.map +1 -1
  7. package/dist/commands/admin/log-config.d.ts +11 -0
  8. package/dist/commands/admin/log-config.d.ts.map +1 -0
  9. package/dist/commands/admin/log-config.js +159 -0
  10. package/dist/commands/admin/log-config.js.map +1 -0
  11. package/dist/commands/admin/logs.d.ts +2 -3
  12. package/dist/commands/admin/logs.d.ts.map +1 -1
  13. package/dist/commands/admin/logs.js +6 -48
  14. package/dist/commands/admin/logs.js.map +1 -1
  15. package/dist/commands/admin/status.d.ts.map +1 -1
  16. package/dist/commands/admin/status.js +1 -0
  17. package/dist/commands/admin/status.js.map +1 -1
  18. package/dist/commands/config.d.ts +1 -0
  19. package/dist/commands/config.d.ts.map +1 -1
  20. package/dist/commands/config.js +63 -196
  21. package/dist/commands/config.js.map +1 -1
  22. package/dist/commands/create.d.ts +3 -2
  23. package/dist/commands/create.d.ts.map +1 -1
  24. package/dist/commands/create.js +24 -97
  25. package/dist/commands/create.js.map +1 -1
  26. package/dist/commands/delete.d.ts.map +1 -1
  27. package/dist/commands/delete.js +7 -24
  28. package/dist/commands/delete.js.map +1 -1
  29. package/dist/commands/internal/server-wrapper.d.ts +15 -0
  30. package/dist/commands/internal/server-wrapper.d.ts.map +1 -0
  31. package/dist/commands/internal/server-wrapper.js +126 -0
  32. package/dist/commands/internal/server-wrapper.js.map +1 -0
  33. package/dist/commands/logs-all.d.ts +0 -2
  34. package/dist/commands/logs-all.d.ts.map +1 -1
  35. package/dist/commands/logs-all.js +1 -61
  36. package/dist/commands/logs-all.js.map +1 -1
  37. package/dist/commands/logs.d.ts +2 -5
  38. package/dist/commands/logs.d.ts.map +1 -1
  39. package/dist/commands/logs.js +104 -120
  40. package/dist/commands/logs.js.map +1 -1
  41. package/dist/commands/migrate-labels.d.ts +12 -0
  42. package/dist/commands/migrate-labels.d.ts.map +1 -0
  43. package/dist/commands/migrate-labels.js +160 -0
  44. package/dist/commands/migrate-labels.js.map +1 -0
  45. package/dist/commands/ps.d.ts.map +1 -1
  46. package/dist/commands/ps.js +2 -1
  47. package/dist/commands/ps.js.map +1 -1
  48. package/dist/commands/rm.d.ts.map +1 -1
  49. package/dist/commands/rm.js +22 -48
  50. package/dist/commands/rm.js.map +1 -1
  51. package/dist/commands/router/config.d.ts +1 -1
  52. package/dist/commands/router/config.js +6 -6
  53. package/dist/commands/router/config.js.map +1 -1
  54. package/dist/commands/router/logs.d.ts +2 -4
  55. package/dist/commands/router/logs.d.ts.map +1 -1
  56. package/dist/commands/router/logs.js +34 -189
  57. package/dist/commands/router/logs.js.map +1 -1
  58. package/dist/commands/router/status.d.ts.map +1 -1
  59. package/dist/commands/router/status.js +1 -0
  60. package/dist/commands/router/status.js.map +1 -1
  61. package/dist/commands/server-show.d.ts.map +1 -1
  62. package/dist/commands/server-show.js +3 -0
  63. package/dist/commands/server-show.js.map +1 -1
  64. package/dist/commands/start.d.ts.map +1 -1
  65. package/dist/commands/start.js +21 -72
  66. package/dist/commands/start.js.map +1 -1
  67. package/dist/commands/stop.d.ts.map +1 -1
  68. package/dist/commands/stop.js +10 -26
  69. package/dist/commands/stop.js.map +1 -1
  70. package/dist/launchers/llamacpp-admin +8 -0
  71. package/dist/launchers/llamacpp-router +8 -0
  72. package/dist/launchers/llamacpp-server +8 -0
  73. package/dist/lib/admin-manager.d.ts +4 -0
  74. package/dist/lib/admin-manager.d.ts.map +1 -1
  75. package/dist/lib/admin-manager.js +42 -18
  76. package/dist/lib/admin-manager.js.map +1 -1
  77. package/dist/lib/admin-server.d.ts +48 -1
  78. package/dist/lib/admin-server.d.ts.map +1 -1
  79. package/dist/lib/admin-server.js +632 -238
  80. package/dist/lib/admin-server.js.map +1 -1
  81. package/dist/lib/config-generator.d.ts +1 -0
  82. package/dist/lib/config-generator.d.ts.map +1 -1
  83. package/dist/lib/config-generator.js +12 -5
  84. package/dist/lib/config-generator.js.map +1 -1
  85. package/dist/lib/keyboard-manager.d.ts +162 -0
  86. package/dist/lib/keyboard-manager.d.ts.map +1 -0
  87. package/dist/lib/keyboard-manager.js +247 -0
  88. package/dist/lib/keyboard-manager.js.map +1 -0
  89. package/dist/lib/label-migration.d.ts +65 -0
  90. package/dist/lib/label-migration.d.ts.map +1 -0
  91. package/dist/lib/label-migration.js +458 -0
  92. package/dist/lib/label-migration.js.map +1 -0
  93. package/dist/lib/launchctl-manager.d.ts +9 -0
  94. package/dist/lib/launchctl-manager.d.ts.map +1 -1
  95. package/dist/lib/launchctl-manager.js +65 -19
  96. package/dist/lib/launchctl-manager.js.map +1 -1
  97. package/dist/lib/log-management-service.d.ts +51 -0
  98. package/dist/lib/log-management-service.d.ts.map +1 -0
  99. package/dist/lib/log-management-service.js +124 -0
  100. package/dist/lib/log-management-service.js.map +1 -0
  101. package/dist/lib/log-workers.d.ts +70 -0
  102. package/dist/lib/log-workers.d.ts.map +1 -0
  103. package/dist/lib/log-workers.js +217 -0
  104. package/dist/lib/log-workers.js.map +1 -0
  105. package/dist/lib/model-downloader.d.ts +9 -1
  106. package/dist/lib/model-downloader.d.ts.map +1 -1
  107. package/dist/lib/model-downloader.js +98 -1
  108. package/dist/lib/model-downloader.js.map +1 -1
  109. package/dist/lib/model-management-service.d.ts +60 -0
  110. package/dist/lib/model-management-service.d.ts.map +1 -0
  111. package/dist/lib/model-management-service.js +246 -0
  112. package/dist/lib/model-management-service.js.map +1 -0
  113. package/dist/lib/model-management-service.test.d.ts +2 -0
  114. package/dist/lib/model-management-service.test.d.ts.map +1 -0
  115. package/dist/lib/model-management-service.test.js.map +1 -0
  116. package/dist/lib/model-scanner.d.ts +15 -3
  117. package/dist/lib/model-scanner.d.ts.map +1 -1
  118. package/dist/lib/model-scanner.js +174 -17
  119. package/dist/lib/model-scanner.js.map +1 -1
  120. package/dist/lib/openapi-spec.d.ts +1335 -0
  121. package/dist/lib/openapi-spec.d.ts.map +1 -0
  122. package/dist/lib/openapi-spec.js +1017 -0
  123. package/dist/lib/openapi-spec.js.map +1 -0
  124. package/dist/lib/router-logger.d.ts +1 -1
  125. package/dist/lib/router-logger.d.ts.map +1 -1
  126. package/dist/lib/router-logger.js +13 -11
  127. package/dist/lib/router-logger.js.map +1 -1
  128. package/dist/lib/router-manager.d.ts +4 -0
  129. package/dist/lib/router-manager.d.ts.map +1 -1
  130. package/dist/lib/router-manager.js +30 -18
  131. package/dist/lib/router-manager.js.map +1 -1
  132. package/dist/lib/router-server.d.ts.map +1 -1
  133. package/dist/lib/router-server.js +22 -12
  134. package/dist/lib/router-server.js.map +1 -1
  135. package/dist/lib/server-config-service.d.ts +51 -0
  136. package/dist/lib/server-config-service.d.ts.map +1 -0
  137. package/dist/lib/server-config-service.js +310 -0
  138. package/dist/lib/server-config-service.js.map +1 -0
  139. package/dist/lib/server-config-service.test.d.ts +2 -0
  140. package/dist/lib/server-config-service.test.d.ts.map +1 -0
  141. package/dist/lib/server-config-service.test.js.map +1 -0
  142. package/dist/lib/server-lifecycle-service.d.ts +172 -0
  143. package/dist/lib/server-lifecycle-service.d.ts.map +1 -0
  144. package/dist/lib/server-lifecycle-service.js +619 -0
  145. package/dist/lib/server-lifecycle-service.js.map +1 -0
  146. package/dist/lib/state-manager.d.ts +18 -1
  147. package/dist/lib/state-manager.d.ts.map +1 -1
  148. package/dist/lib/state-manager.js +51 -2
  149. package/dist/lib/state-manager.js.map +1 -1
  150. package/dist/lib/status-checker.d.ts +11 -4
  151. package/dist/lib/status-checker.d.ts.map +1 -1
  152. package/dist/lib/status-checker.js +34 -1
  153. package/dist/lib/status-checker.js.map +1 -1
  154. package/dist/lib/validation-service.d.ts +43 -0
  155. package/dist/lib/validation-service.d.ts.map +1 -0
  156. package/dist/lib/validation-service.js +112 -0
  157. package/dist/lib/validation-service.js.map +1 -0
  158. package/dist/lib/validation-service.test.d.ts +2 -0
  159. package/dist/lib/validation-service.test.d.ts.map +1 -0
  160. package/dist/lib/validation-service.test.js.map +1 -0
  161. package/dist/scripts/http-log-filter.sh +8 -0
  162. package/dist/tui/ConfigApp.d.ts.map +1 -1
  163. package/dist/tui/ConfigApp.js +222 -184
  164. package/dist/tui/ConfigApp.js.map +1 -1
  165. package/dist/tui/HistoricalMonitorApp.d.ts.map +1 -1
  166. package/dist/tui/HistoricalMonitorApp.js +12 -0
  167. package/dist/tui/HistoricalMonitorApp.js.map +1 -1
  168. package/dist/tui/ModelsApp.d.ts.map +1 -1
  169. package/dist/tui/ModelsApp.js +93 -17
  170. package/dist/tui/ModelsApp.js.map +1 -1
  171. package/dist/tui/MonitorApp.d.ts.map +1 -1
  172. package/dist/tui/MonitorApp.js +1 -3
  173. package/dist/tui/MonitorApp.js.map +1 -1
  174. package/dist/tui/MultiServerMonitorApp.d.ts +3 -3
  175. package/dist/tui/MultiServerMonitorApp.d.ts.map +1 -1
  176. package/dist/tui/MultiServerMonitorApp.js +724 -508
  177. package/dist/tui/MultiServerMonitorApp.js.map +1 -1
  178. package/dist/tui/RootNavigator.d.ts.map +1 -1
  179. package/dist/tui/RootNavigator.js +17 -1
  180. package/dist/tui/RootNavigator.js.map +1 -1
  181. package/dist/tui/RouterApp.d.ts +6 -0
  182. package/dist/tui/RouterApp.d.ts.map +1 -0
  183. package/dist/tui/RouterApp.js +928 -0
  184. package/dist/tui/RouterApp.js.map +1 -0
  185. package/dist/tui/SearchApp.d.ts.map +1 -1
  186. package/dist/tui/SearchApp.js +27 -6
  187. package/dist/tui/SearchApp.js.map +1 -1
  188. package/dist/tui/shared/modal-controller.d.ts +65 -0
  189. package/dist/tui/shared/modal-controller.d.ts.map +1 -0
  190. package/dist/tui/shared/modal-controller.js +625 -0
  191. package/dist/tui/shared/modal-controller.js.map +1 -0
  192. package/dist/tui/shared/overlay-utils.d.ts +7 -0
  193. package/dist/tui/shared/overlay-utils.d.ts.map +1 -0
  194. package/dist/tui/shared/overlay-utils.js +54 -0
  195. package/dist/tui/shared/overlay-utils.js.map +1 -0
  196. package/dist/types/admin-config.d.ts +15 -2
  197. package/dist/types/admin-config.d.ts.map +1 -1
  198. package/dist/types/model-info.d.ts +5 -0
  199. package/dist/types/model-info.d.ts.map +1 -1
  200. package/dist/types/router-config.d.ts +2 -2
  201. package/dist/types/router-config.d.ts.map +1 -1
  202. package/dist/types/server-config.d.ts +8 -0
  203. package/dist/types/server-config.d.ts.map +1 -1
  204. package/dist/types/server-config.js +25 -0
  205. package/dist/types/server-config.js.map +1 -1
  206. package/dist/utils/http-log-filter.d.ts +10 -0
  207. package/dist/utils/http-log-filter.d.ts.map +1 -0
  208. package/dist/utils/http-log-filter.js +84 -0
  209. package/dist/utils/http-log-filter.js.map +1 -0
  210. package/dist/utils/log-parser.d.ts.map +1 -1
  211. package/dist/utils/log-parser.js +7 -4
  212. package/dist/utils/log-parser.js.map +1 -1
  213. package/dist/utils/log-utils.d.ts +59 -4
  214. package/dist/utils/log-utils.d.ts.map +1 -1
  215. package/dist/utils/log-utils.js +150 -11
  216. package/dist/utils/log-utils.js.map +1 -1
  217. package/dist/utils/shard-utils.d.ts +72 -0
  218. package/dist/utils/shard-utils.d.ts.map +1 -0
  219. package/dist/utils/shard-utils.js +168 -0
  220. package/dist/utils/shard-utils.js.map +1 -0
  221. package/package.json +18 -4
  222. package/src/launchers/llamacpp-admin +8 -0
  223. package/src/launchers/llamacpp-router +8 -0
  224. package/src/launchers/llamacpp-server +8 -0
  225. package/web/dist/assets/index-Byhoy86V.css +1 -0
  226. package/web/dist/assets/index-HSrgvray.js +50 -0
  227. package/web/dist/index.html +2 -2
  228. package/web/dist/assets/index-Bin89Lwr.css +0 -1
  229. package/web/dist/assets/index-CVmonw3T.js +0 -17
@@ -41,14 +41,18 @@ const path = __importStar(require("path"));
41
41
  const fs = __importStar(require("fs/promises"));
42
42
  const file_utils_1 = require("../utils/file-utils");
43
43
  const state_manager_1 = require("./state-manager");
44
- const launchctl_manager_1 = require("./launchctl-manager");
45
44
  const model_scanner_1 = require("./model-scanner");
46
- const config_generator_1 = require("./config-generator");
47
- const port_manager_1 = require("./port-manager");
48
45
  const status_checker_1 = require("./status-checker");
46
+ const server_lifecycle_service_1 = require("./server-lifecycle-service");
47
+ const server_config_service_1 = require("./server-config-service");
48
+ const model_management_service_1 = require("./model-management-service");
49
49
  const model_search_1 = require("./model-search");
50
50
  const download_job_manager_1 = require("./download-job-manager");
51
51
  const router_manager_1 = require("./router-manager");
52
+ const admin_manager_1 = require("./admin-manager");
53
+ const log_management_service_1 = require("./log-management-service");
54
+ const log_workers_1 = require("./log-workers");
55
+ const openapi_spec_1 = require("./openapi-spec");
52
56
  /**
53
57
  * Admin HTTP server - REST API for managing llama.cpp servers
54
58
  */
@@ -67,6 +71,7 @@ class AdminServer {
67
71
  // Graceful shutdown
68
72
  process.on('SIGTERM', async () => {
69
73
  console.error('[Admin] Received SIGTERM, shutting down gracefully...');
74
+ await this.stopWorkers();
70
75
  this.server.close(() => {
71
76
  console.error('[Admin] Server closed');
72
77
  process.exit(0);
@@ -74,6 +79,7 @@ class AdminServer {
74
79
  });
75
80
  process.on('SIGINT', async () => {
76
81
  console.error('[Admin] Received SIGINT, shutting down gracefully...');
82
+ await this.stopWorkers();
77
83
  this.server.close(() => {
78
84
  console.error('[Admin] Server closed');
79
85
  process.exit(0);
@@ -82,12 +88,45 @@ class AdminServer {
82
88
  }
83
89
  async start() {
84
90
  await this.initialize();
91
+ // Start log management workers if configured
92
+ await this.startWorkers();
85
93
  this.server.listen(this.config.port, this.config.host, () => {
86
94
  console.error(`[Admin] Listening on http://${this.config.host}:${this.config.port}`);
87
95
  console.error(`[Admin] PID: ${process.pid}`);
88
96
  console.error(`[Admin] API Key: ${this.config.apiKey}`);
89
97
  });
90
98
  }
99
+ /**
100
+ * Start log management workers based on configuration
101
+ */
102
+ async startWorkers() {
103
+ const logConfig = this.config.logManagement;
104
+ if (!logConfig) {
105
+ // No log management configured, use defaults
106
+ return;
107
+ }
108
+ // Start auto-rotate worker
109
+ if (logConfig.autoRotate.enabled) {
110
+ this.autoRotateWorker = new log_workers_1.AutoRotateWorker(logConfig.autoRotate);
111
+ await this.autoRotateWorker.start();
112
+ }
113
+ // Start auto-delete worker
114
+ if (logConfig.autoDelete.enabled) {
115
+ this.autoDeleteWorker = new log_workers_1.AutoDeleteWorker(logConfig.autoDelete);
116
+ await this.autoDeleteWorker.start();
117
+ }
118
+ }
119
+ /**
120
+ * Stop log management workers
121
+ */
122
+ async stopWorkers() {
123
+ if (this.autoRotateWorker) {
124
+ await this.autoRotateWorker.stop();
125
+ }
126
+ if (this.autoDeleteWorker) {
127
+ await this.autoDeleteWorker.stop();
128
+ }
129
+ }
91
130
  /**
92
131
  * Main request handler
93
132
  */
@@ -114,6 +153,21 @@ class AdminServer {
114
153
  await this.handleHealth(req, res);
115
154
  return;
116
155
  }
156
+ // Swagger UI - OpenAPI spec (no auth required)
157
+ if (pathname === '/api-docs.json') {
158
+ this.sendJson(res, 200, openapi_spec_1.openApiSpec);
159
+ return;
160
+ }
161
+ // Swagger UI - HTML interface (no auth required)
162
+ if (pathname === '/api-docs' || pathname.startsWith('/api-docs/')) {
163
+ await this.handleSwaggerUI(req, res, pathname);
164
+ return;
165
+ }
166
+ // Proxy chat requests to router (production mode)
167
+ if (pathname.startsWith('/v1/')) {
168
+ await this.handleRouterProxy(req, res, pathname);
169
+ return;
170
+ }
117
171
  // Static files (no auth required)
118
172
  if (!pathname.startsWith('/api/')) {
119
173
  await this.handleStaticFile(req, res, pathname);
@@ -155,6 +209,10 @@ class AdminServer {
155
209
  const serverId = pathname.split('/')[3];
156
210
  await this.handleRestartServer(req, res, serverId);
157
211
  }
212
+ else if (pathname.match(/^\/api\/servers\/[^/]+\/slots$/) && method === 'GET') {
213
+ const serverId = pathname.split('/')[3];
214
+ await this.handleGetServerSlots(req, res, serverId);
215
+ }
158
216
  else if (pathname.match(/^\/api\/servers\/[^/]+\/logs$/) && method === 'GET') {
159
217
  const serverId = pathname.split('/')[3];
160
218
  await this.handleGetLogs(req, res, serverId, url);
@@ -214,6 +272,24 @@ class AdminServer {
214
272
  else if (pathname === '/api/router' && method === 'PATCH') {
215
273
  await this.handleUpdateRouter(req, res);
216
274
  }
275
+ else if (pathname === '/api/admin' && method === 'GET') {
276
+ await this.handleGetAdmin(req, res);
277
+ }
278
+ else if (pathname === '/api/admin/logs' && method === 'GET') {
279
+ await this.handleGetAdminLogs(req, res);
280
+ }
281
+ else if (pathname === '/api/admin/logs/rotate' && method === 'POST') {
282
+ await this.handleRotateLogs(req, res);
283
+ }
284
+ else if (pathname === '/api/admin/logs/clear-archived' && method === 'POST') {
285
+ await this.handleClearArchivedLogs(req, res);
286
+ }
287
+ else if (pathname === '/api/admin/logs/config' && method === 'PATCH') {
288
+ await this.handleUpdateLogConfig(req, res);
289
+ }
290
+ else if (pathname === '/api/admin/service-logs' && method === 'GET') {
291
+ await this.handleGetAdminServiceLogs(req, res, url);
292
+ }
217
293
  else {
218
294
  // API endpoint not found
219
295
  this.sendError(res, 404, 'Not Found', `Unknown endpoint: ${method} ${pathname}`, 'NOT_FOUND');
@@ -237,16 +313,68 @@ class AdminServer {
237
313
  timestamp: new Date().toISOString(),
238
314
  });
239
315
  }
316
+ /**
317
+ * Proxy router requests (production mode)
318
+ */
319
+ async handleRouterProxy(req, res, pathname) {
320
+ try {
321
+ // Check if router is running
322
+ const routerStatus = await router_manager_1.routerManager.getStatus();
323
+ if (!routerStatus || !routerStatus.status.isRunning) {
324
+ this.sendError(res, 503, 'Service Unavailable', 'Router is not running. Start router to use chat.', 'ROUTER_NOT_RUNNING');
325
+ return;
326
+ }
327
+ const { config } = routerStatus;
328
+ const routerHost = config.host || '127.0.0.1';
329
+ const routerPort = config.port || 9100;
330
+ // Parse URL to get query string
331
+ const url = new url_1.URL(req.url, `http://${req.headers.host}`);
332
+ const fullPath = pathname + url.search;
333
+ // Create proxy request
334
+ const options = {
335
+ hostname: routerHost,
336
+ port: routerPort,
337
+ path: fullPath,
338
+ method: req.method,
339
+ headers: {
340
+ ...req.headers,
341
+ host: `${routerHost}:${routerPort}`,
342
+ },
343
+ };
344
+ const proxyReq = http.request(options, (proxyRes) => {
345
+ // Copy status and headers
346
+ res.writeHead(proxyRes.statusCode || 500, proxyRes.headers);
347
+ // Pipe response (supports streaming SSE)
348
+ proxyRes.pipe(res);
349
+ });
350
+ proxyReq.on('error', (err) => {
351
+ console.error('[Admin] Router proxy error:', err);
352
+ if (!res.headersSent) {
353
+ this.sendError(res, 502, 'Bad Gateway', `Router connection failed: ${err.message}`, 'ROUTER_PROXY_ERROR');
354
+ }
355
+ });
356
+ // Pipe request body
357
+ req.pipe(proxyReq);
358
+ }
359
+ catch (error) {
360
+ console.error('[Admin] Error proxying to router:', error);
361
+ if (!res.headersSent) {
362
+ this.sendError(res, 500, 'Internal Server Error', error.message, 'ROUTER_PROXY_ERROR');
363
+ }
364
+ }
365
+ }
240
366
  /**
241
367
  * List all servers
242
368
  */
243
369
  async handleListServers(req, res) {
244
370
  const servers = await state_manager_1.stateManager.getAllServers();
245
- // Update status for each server
371
+ // Update status for each server (including health check)
246
372
  for (const server of servers) {
247
373
  const status = await status_checker_1.statusChecker.checkServer(server);
248
374
  server.status = status_checker_1.statusChecker.determineStatus(status, status.portListening);
249
375
  server.pid = status.pid || undefined;
376
+ // Add health check result (only meaningful if server is running)
377
+ server.healthy = server.status === 'running' ? status.healthy : undefined;
250
378
  }
251
379
  this.sendJson(res, 200, { servers });
252
380
  }
@@ -262,6 +390,8 @@ class AdminServer {
262
390
  const status = await status_checker_1.statusChecker.checkServer(server);
263
391
  server.status = status_checker_1.statusChecker.determineStatus(status, status.portListening);
264
392
  server.pid = status.pid || undefined;
393
+ // Add health check result (only meaningful if server is running)
394
+ server.healthy = server.status === 'running' ? status.healthy : undefined;
265
395
  this.sendJson(res, 200, { server, status });
266
396
  }
267
397
  /**
@@ -283,39 +413,6 @@ class AdminServer {
283
413
  return;
284
414
  }
285
415
  try {
286
- // Resolve model path
287
- const modelPath = await model_scanner_1.modelScanner.resolveModelPath(data.model);
288
- if (!modelPath) {
289
- this.sendError(res, 404, 'Not Found', `Model not found: ${data.model}`, 'MODEL_NOT_FOUND');
290
- return;
291
- }
292
- const modelName = path.basename(modelPath);
293
- // Check if server already exists
294
- const existingServer = await state_manager_1.stateManager.serverExistsForModel(modelPath);
295
- if (existingServer) {
296
- this.sendError(res, 409, 'Conflict', `Server already exists for model: ${modelName}`, 'SERVER_EXISTS');
297
- return;
298
- }
299
- // Get model size
300
- const modelSize = await model_scanner_1.modelScanner.getModelSize(modelName);
301
- if (!modelSize) {
302
- this.sendError(res, 500, 'Internal Server Error', 'Failed to read model file', 'MODEL_READ_ERROR');
303
- return;
304
- }
305
- // Determine port
306
- let port;
307
- if (data.port) {
308
- port_manager_1.portManager.validatePort(data.port);
309
- const available = await port_manager_1.portManager.isPortAvailable(data.port);
310
- if (!available) {
311
- this.sendError(res, 409, 'Conflict', `Port ${data.port} is already in use`, 'PORT_IN_USE');
312
- return;
313
- }
314
- port = data.port;
315
- }
316
- else {
317
- port = await port_manager_1.portManager.findAvailablePort();
318
- }
319
416
  // Parse custom flags if provided
320
417
  let customFlags;
321
418
  if (data.customFlags) {
@@ -323,8 +420,8 @@ class AdminServer {
323
420
  ? data.customFlags
324
421
  : data.customFlags.split(',').map((f) => f.trim()).filter((f) => f.length > 0);
325
422
  }
326
- // Generate configuration
327
- const serverConfig = await config_generator_1.configGenerator.generateConfig(modelPath, modelName, modelSize, port, {
423
+ // Create server using centralized service
424
+ const result = await server_lifecycle_service_1.serverLifecycleService.createServer(data.model, {
328
425
  port: data.port,
329
426
  host: data.host,
330
427
  threads: data.threads,
@@ -332,29 +429,25 @@ class AdminServer {
332
429
  gpuLayers: data.gpuLayers,
333
430
  verbose: data.verbose,
334
431
  customFlags,
432
+ alias: data.alias,
335
433
  });
336
- // Save configuration
337
- await state_manager_1.stateManager.saveServerConfig(serverConfig);
338
- // Create and start server
339
- await launchctl_manager_1.launchctlManager.createPlist(serverConfig);
340
- await launchctl_manager_1.launchctlManager.loadService(serverConfig.plistPath);
341
- await launchctl_manager_1.launchctlManager.startService(serverConfig.label);
342
- // Wait for startup
343
- const started = await launchctl_manager_1.launchctlManager.waitForServiceStart(serverConfig.label, 5000);
344
- if (!started) {
345
- this.sendError(res, 500, 'Internal Server Error', 'Server failed to start', 'START_FAILED');
434
+ if (!result.success) {
435
+ // Map common errors to appropriate HTTP status codes
436
+ if (result.error?.includes('not found')) {
437
+ this.sendError(res, 404, 'Not Found', result.error, 'MODEL_NOT_FOUND');
438
+ }
439
+ else if (result.error?.includes('already in use') || result.error?.includes('already used')) {
440
+ this.sendError(res, 409, 'Conflict', result.error, 'CONFLICT');
441
+ }
442
+ else if (result.error?.includes('Invalid alias')) {
443
+ this.sendError(res, 400, 'Bad Request', result.error, 'INVALID_ALIAS');
444
+ }
445
+ else {
446
+ this.sendError(res, 500, 'Internal Server Error', result.error || 'Server creation failed', 'CREATE_ERROR');
447
+ }
346
448
  return;
347
449
  }
348
- // Update status
349
- const status = await status_checker_1.statusChecker.checkServer(serverConfig);
350
- serverConfig.status = status_checker_1.statusChecker.determineStatus(status, status.portListening);
351
- serverConfig.pid = status.pid || undefined;
352
- await state_manager_1.stateManager.updateServerConfig(serverConfig.id, {
353
- status: serverConfig.status,
354
- pid: serverConfig.pid,
355
- lastStarted: new Date().toISOString(),
356
- });
357
- this.sendJson(res, 201, { server: serverConfig });
450
+ this.sendJson(res, 201, { server: result.server });
358
451
  }
359
452
  catch (error) {
360
453
  this.sendError(res, 500, 'Internal Server Error', error.message, 'CREATE_ERROR');
@@ -379,66 +472,56 @@ class AdminServer {
379
472
  return;
380
473
  }
381
474
  try {
382
- // Build updates object
383
- const updates = {};
384
- if (data.model !== undefined) {
385
- const modelPath = await model_scanner_1.modelScanner.resolveModelPath(data.model);
386
- if (!modelPath) {
387
- this.sendError(res, 404, 'Not Found', `Model not found: ${data.model}`, 'MODEL_NOT_FOUND');
388
- return;
389
- }
390
- updates.modelPath = modelPath;
391
- updates.modelName = path.basename(modelPath);
392
- }
393
- if (data.port !== undefined) {
394
- port_manager_1.portManager.validatePort(data.port);
395
- const available = await port_manager_1.portManager.isPortAvailable(data.port);
396
- if (!available && data.port !== server.port) {
397
- this.sendError(res, 409, 'Conflict', `Port ${data.port} is already in use`, 'PORT_IN_USE');
398
- return;
399
- }
400
- updates.port = data.port;
401
- }
402
- if (data.host !== undefined)
403
- updates.host = data.host;
404
- if (data.threads !== undefined)
405
- updates.threads = data.threads;
406
- if (data.ctxSize !== undefined)
407
- updates.ctxSize = data.ctxSize;
408
- if (data.gpuLayers !== undefined)
409
- updates.gpuLayers = data.gpuLayers;
410
- if (data.verbose !== undefined)
411
- updates.verbose = data.verbose;
475
+ // Parse custom flags
476
+ let customFlags;
412
477
  if (data.customFlags !== undefined) {
413
- updates.customFlags = Array.isArray(data.customFlags)
478
+ customFlags = Array.isArray(data.customFlags)
414
479
  ? data.customFlags
415
480
  : data.customFlags.split(',').map((f) => f.trim()).filter((f) => f.length > 0);
416
481
  }
417
- // Check if server is running
418
- const status = await status_checker_1.statusChecker.checkServer(server);
419
- const isRunning = status_checker_1.statusChecker.determineStatus(status, status.portListening) === 'running';
420
- // Apply updates
421
- await state_manager_1.stateManager.updateServerConfig(server.id, updates);
422
- // Regenerate plist with new config
423
- const updatedServer = await state_manager_1.stateManager.loadServerConfig(server.id);
424
- if (updatedServer) {
425
- await launchctl_manager_1.launchctlManager.createPlist(updatedServer);
426
- // Restart if requested and running
427
- if (data.restart && isRunning) {
428
- await launchctl_manager_1.launchctlManager.unloadService(updatedServer.plistPath);
429
- await launchctl_manager_1.launchctlManager.loadService(updatedServer.plistPath);
430
- await launchctl_manager_1.launchctlManager.startService(updatedServer.label);
431
- await launchctl_manager_1.launchctlManager.waitForServiceStart(updatedServer.label, 5000);
432
- const newStatus = await status_checker_1.statusChecker.checkServer(updatedServer);
433
- await state_manager_1.stateManager.updateServerConfig(updatedServer.id, {
434
- status: status_checker_1.statusChecker.determineStatus(newStatus, newStatus.portListening),
435
- pid: newStatus.pid || undefined,
436
- lastStarted: new Date().toISOString(),
437
- });
482
+ // Handle alias empty string/null -> null conversion
483
+ let aliasValue = data.alias;
484
+ if (data.alias === '' || data.alias === null) {
485
+ aliasValue = null; // null means remove alias
486
+ }
487
+ // Delegate to serverConfigService (FIX: now handles model migration properly)
488
+ const result = await server_config_service_1.serverConfigService.updateConfig({
489
+ serverId: server.id,
490
+ updates: {
491
+ model: data.model,
492
+ port: data.port,
493
+ host: data.host,
494
+ threads: data.threads,
495
+ ctxSize: data.ctxSize,
496
+ gpuLayers: data.gpuLayers,
497
+ verbose: data.verbose,
498
+ customFlags,
499
+ alias: aliasValue,
500
+ },
501
+ restartIfNeeded: data.restart === true,
502
+ });
503
+ if (!result.success) {
504
+ // Map common errors to appropriate HTTP status codes
505
+ if (result.error?.includes('not found')) {
506
+ this.sendError(res, 404, 'Not Found', result.error, 'NOT_FOUND');
507
+ }
508
+ else if (result.error?.includes('already in use') || result.error?.includes('already exists')) {
509
+ this.sendError(res, 409, 'Conflict', result.error, 'CONFLICT');
438
510
  }
511
+ else if (result.error?.includes('Invalid')) {
512
+ this.sendError(res, 400, 'Bad Request', result.error, 'VALIDATION_ERROR');
513
+ }
514
+ else {
515
+ this.sendError(res, 500, 'Internal Server Error', result.error || 'Update failed', 'UPDATE_ERROR');
516
+ }
517
+ return;
439
518
  }
440
- const finalServer = await state_manager_1.stateManager.loadServerConfig(server.id);
441
- this.sendJson(res, 200, { server: finalServer });
519
+ // Return updated server (with migration info if applicable)
520
+ this.sendJson(res, 200, {
521
+ server: result.server,
522
+ migrated: result.migrated,
523
+ oldServerId: result.oldServerId,
524
+ });
442
525
  }
443
526
  catch (error) {
444
527
  this.sendError(res, 500, 'Internal Server Error', error.message, 'UPDATE_ERROR');
@@ -453,58 +536,50 @@ class AdminServer {
453
536
  this.sendError(res, 404, 'Not Found', `Server not found: ${serverId}`, 'SERVER_NOT_FOUND');
454
537
  return;
455
538
  }
456
- try {
457
- // Stop server if running
458
- const status = await status_checker_1.statusChecker.checkServer(server);
459
- if (status_checker_1.statusChecker.determineStatus(status, status.portListening) === 'running') {
460
- await launchctl_manager_1.launchctlManager.unloadService(server.plistPath);
461
- await launchctl_manager_1.launchctlManager.waitForServiceStop(server.label, 5000);
539
+ const result = await server_lifecycle_service_1.serverLifecycleService.deleteServer(serverId);
540
+ if (!result.success) {
541
+ if (result.error?.includes('not found')) {
542
+ this.sendError(res, 404, 'Not Found', result.error, 'SERVER_NOT_FOUND');
462
543
  }
463
- // Delete plist and config
464
- await launchctl_manager_1.launchctlManager.deletePlist(server.plistPath);
465
- await state_manager_1.stateManager.deleteServerConfig(server.id);
466
- this.sendJson(res, 200, { success: true });
467
- }
468
- catch (error) {
469
- this.sendError(res, 500, 'Internal Server Error', error.message, 'DELETE_ERROR');
544
+ else if (result.error?.includes('already')) {
545
+ this.sendError(res, 409, 'Conflict', result.error, 'OPERATION_IN_PROGRESS');
546
+ }
547
+ else {
548
+ this.sendError(res, 500, 'Internal Server Error', result.error || 'Unknown error', 'DELETE_ERROR');
549
+ }
550
+ return;
470
551
  }
552
+ this.sendJson(res, 200, { success: true });
471
553
  }
472
554
  /**
473
555
  * Start server
474
556
  */
475
557
  async handleStartServer(req, res, serverId) {
476
- const server = await state_manager_1.stateManager.findServer(serverId);
477
- if (!server) {
478
- this.sendError(res, 404, 'Not Found', `Server not found: ${serverId}`, 'SERVER_NOT_FOUND');
479
- return;
480
- }
481
558
  try {
482
- const status = await status_checker_1.statusChecker.checkServer(server);
483
- if (status_checker_1.statusChecker.determineStatus(status, status.portListening) === 'running') {
484
- this.sendError(res, 409, 'Conflict', 'Server is already running', 'ALREADY_RUNNING');
485
- return;
486
- }
487
- // Recreate plist if missing
488
- if (!(await (0, file_utils_1.fileExists)(server.plistPath))) {
489
- await launchctl_manager_1.launchctlManager.createPlist(server);
490
- }
491
- await launchctl_manager_1.launchctlManager.loadService(server.plistPath);
492
- await launchctl_manager_1.launchctlManager.startService(server.label);
493
- const started = await launchctl_manager_1.launchctlManager.waitForServiceStart(server.label, 5000);
494
- if (!started) {
495
- this.sendError(res, 500, 'Internal Server Error', 'Server failed to start', 'START_FAILED');
559
+ // Use centralized lifecycle service
560
+ const result = await server_lifecycle_service_1.serverLifecycleService.startServer(serverId);
561
+ if (!result.success) {
562
+ // Map common errors to appropriate HTTP status codes
563
+ if (result.error?.includes('not found')) {
564
+ this.sendError(res, 404, 'Not Found', result.error, 'SERVER_NOT_FOUND');
565
+ }
566
+ else if (result.error?.includes('already running')) {
567
+ this.sendError(res, 409, 'Conflict', result.error, 'ALREADY_RUNNING');
568
+ }
569
+ else if (result.error?.includes('already starting')) {
570
+ this.sendError(res, 409, 'Conflict', result.error, 'OPERATION_IN_PROGRESS');
571
+ }
572
+ else {
573
+ this.sendError(res, 500, 'Internal Server Error', result.error || 'Unknown error', 'START_FAILED');
574
+ }
496
575
  return;
497
576
  }
498
- // Give server a moment to fully start before checking status
499
- await new Promise(resolve => setTimeout(resolve, 1000));
500
- const newStatus = await status_checker_1.statusChecker.checkServer(server);
501
- await state_manager_1.stateManager.updateServerConfig(server.id, {
502
- status: status_checker_1.statusChecker.determineStatus(newStatus, newStatus.portListening),
503
- pid: newStatus.pid || undefined,
504
- lastStarted: new Date().toISOString(),
577
+ // Return success with server details
578
+ this.sendJson(res, 200, {
579
+ server: result.server,
580
+ metalMemoryMB: result.metalMemoryMB,
581
+ rotatedLogs: result.rotatedLogs,
505
582
  });
506
- const updatedServer = await state_manager_1.stateManager.loadServerConfig(server.id);
507
- this.sendJson(res, 200, { server: updatedServer });
508
583
  }
509
584
  catch (error) {
510
585
  this.sendError(res, 500, 'Internal Server Error', error.message, 'START_ERROR');
@@ -514,26 +589,27 @@ class AdminServer {
514
589
  * Stop server
515
590
  */
516
591
  async handleStopServer(req, res, serverId) {
517
- const server = await state_manager_1.stateManager.findServer(serverId);
518
- if (!server) {
519
- this.sendError(res, 404, 'Not Found', `Server not found: ${serverId}`, 'SERVER_NOT_FOUND');
520
- return;
521
- }
522
592
  try {
523
- const status = await status_checker_1.statusChecker.checkServer(server);
524
- if (status_checker_1.statusChecker.determineStatus(status, status.portListening) !== 'running') {
525
- this.sendError(res, 409, 'Conflict', 'Server is not running', 'NOT_RUNNING');
593
+ // Use centralized lifecycle service
594
+ const result = await server_lifecycle_service_1.serverLifecycleService.stopServer(serverId);
595
+ if (!result.success) {
596
+ // Map common errors to appropriate HTTP status codes
597
+ if (result.error?.includes('not found')) {
598
+ this.sendError(res, 404, 'Not Found', result.error, 'SERVER_NOT_FOUND');
599
+ }
600
+ else if (result.error?.includes('already stopped')) {
601
+ this.sendError(res, 409, 'Conflict', result.error, 'NOT_RUNNING');
602
+ }
603
+ else if (result.error?.includes('already stopping')) {
604
+ this.sendError(res, 409, 'Conflict', result.error, 'OPERATION_IN_PROGRESS');
605
+ }
606
+ else {
607
+ this.sendError(res, 500, 'Internal Server Error', result.error || 'Unknown error', 'STOP_FAILED');
608
+ }
526
609
  return;
527
610
  }
528
- await launchctl_manager_1.launchctlManager.unloadService(server.plistPath);
529
- await launchctl_manager_1.launchctlManager.waitForServiceStop(server.label, 5000);
530
- await state_manager_1.stateManager.updateServerConfig(server.id, {
531
- status: 'stopped',
532
- pid: undefined,
533
- lastStopped: new Date().toISOString(),
534
- });
535
- const updatedServer = await state_manager_1.stateManager.loadServerConfig(server.id);
536
- this.sendJson(res, 200, { server: updatedServer });
611
+ // Return success with server details
612
+ this.sendJson(res, 200, { server: result.server });
537
613
  }
538
614
  catch (error) {
539
615
  this.sendError(res, 500, 'Internal Server Error', error.message, 'STOP_ERROR');
@@ -543,39 +619,75 @@ class AdminServer {
543
619
  * Restart server
544
620
  */
545
621
  async handleRestartServer(req, res, serverId) {
622
+ try {
623
+ // Use centralized lifecycle service
624
+ const result = await server_lifecycle_service_1.serverLifecycleService.restartServer(serverId);
625
+ if (!result.success) {
626
+ // Map common errors to appropriate HTTP status codes
627
+ if (result.error?.includes('not found')) {
628
+ this.sendError(res, 404, 'Not Found', result.error, 'SERVER_NOT_FOUND');
629
+ }
630
+ else if (result.error?.includes('Failed to stop')) {
631
+ this.sendError(res, 500, 'Internal Server Error', result.error, 'STOP_FAILED');
632
+ }
633
+ else {
634
+ this.sendError(res, 500, 'Internal Server Error', result.error || 'Unknown error', 'RESTART_FAILED');
635
+ }
636
+ return;
637
+ }
638
+ // Return success with server details
639
+ this.sendJson(res, 200, {
640
+ server: result.server,
641
+ metalMemoryMB: result.metalMemoryMB,
642
+ rotatedLogs: result.rotatedLogs,
643
+ });
644
+ }
645
+ catch (error) {
646
+ this.sendError(res, 500, 'Internal Server Error', error.message, 'RESTART_ERROR');
647
+ }
648
+ }
649
+ /**
650
+ * Get slot status from a running llama.cpp server
651
+ */
652
+ async handleGetServerSlots(req, res, serverId) {
546
653
  const server = await state_manager_1.stateManager.findServer(serverId);
547
654
  if (!server) {
548
655
  this.sendError(res, 404, 'Not Found', `Server not found: ${serverId}`, 'SERVER_NOT_FOUND');
549
656
  return;
550
657
  }
658
+ const emptySlots = { slots: [], activeSlots: 0, idleSlots: 0, totalSlots: 0 };
551
659
  try {
552
- // Stop if running
553
660
  const status = await status_checker_1.statusChecker.checkServer(server);
554
- if (status_checker_1.statusChecker.determineStatus(status, status.portListening) === 'running') {
555
- await launchctl_manager_1.launchctlManager.unloadService(server.plistPath);
556
- await launchctl_manager_1.launchctlManager.waitForServiceStop(server.label, 5000);
557
- }
558
- // Start
559
- await launchctl_manager_1.launchctlManager.loadService(server.plistPath);
560
- await launchctl_manager_1.launchctlManager.startService(server.label);
561
- const started = await launchctl_manager_1.launchctlManager.waitForServiceStart(server.label, 5000);
562
- if (!started) {
563
- this.sendError(res, 500, 'Internal Server Error', 'Server failed to start', 'START_FAILED');
661
+ if (status_checker_1.statusChecker.determineStatus(status, status.portListening) !== 'running') {
662
+ this.sendJson(res, 200, emptySlots);
564
663
  return;
565
664
  }
566
- // Give server a moment to fully start before checking status
567
- await new Promise(resolve => setTimeout(resolve, 1000));
568
- const newStatus = await status_checker_1.statusChecker.checkServer(server);
569
- await state_manager_1.stateManager.updateServerConfig(server.id, {
570
- status: status_checker_1.statusChecker.determineStatus(newStatus, newStatus.portListening),
571
- pid: newStatus.pid || undefined,
572
- lastStarted: new Date().toISOString(),
573
- });
574
- const updatedServer = await state_manager_1.stateManager.loadServerConfig(server.id);
575
- this.sendJson(res, 200, { server: updatedServer });
665
+ const host = server.host || '127.0.0.1';
666
+ const controller = new AbortController();
667
+ const timeout = setTimeout(() => controller.abort(), 3000);
668
+ try {
669
+ const response = await fetch(`http://${host}:${server.port}/slots`, { signal: controller.signal });
670
+ clearTimeout(timeout);
671
+ if (!response.ok) {
672
+ this.sendJson(res, 200, emptySlots);
673
+ return;
674
+ }
675
+ const slots = await response.json();
676
+ const activeSlots = slots.filter(s => s.is_processing).length;
677
+ this.sendJson(res, 200, {
678
+ slots,
679
+ activeSlots,
680
+ idleSlots: slots.length - activeSlots,
681
+ totalSlots: slots.length,
682
+ });
683
+ }
684
+ catch {
685
+ clearTimeout(timeout);
686
+ this.sendJson(res, 200, emptySlots);
687
+ }
576
688
  }
577
689
  catch (error) {
578
- this.sendError(res, 500, 'Internal Server Error', error.message, 'RESTART_ERROR');
690
+ this.sendError(res, 500, 'Internal Server Error', error.message, 'SLOTS_ERROR');
579
691
  }
580
692
  }
581
693
  /**
@@ -588,41 +700,66 @@ class AdminServer {
588
700
  return;
589
701
  }
590
702
  try {
591
- const type = url.searchParams.get('type') || 'both'; // stdout, stderr, or both
703
+ const type = url.searchParams.get('type') || 'activity'; // activity (default), system, http, stderr, stdout, or all
592
704
  const lines = parseInt(url.searchParams.get('lines') || '100');
705
+ // Support both new terminology and old parameter names (backward compatibility):
706
+ // - 'activity' (new) or 'http' (old) -> HTTP activity logs only
707
+ // - 'system' (new) -> stderr + stdout (system diagnostic logs, no http)
708
+ // - 'all' (old) -> everything (http + stderr + stdout)
709
+ // - 'stderr' (old) -> stderr only
710
+ // - 'stdout' (old) -> stdout only
711
+ let http = '';
593
712
  let stdout = '';
594
713
  let stderr = '';
595
- if ((type === 'stdout' || type === 'both') && (await (0, file_utils_1.fileExists)(server.stdoutPath))) {
714
+ // HTTP logs
715
+ if ((type === 'activity' || type === 'http' || type === 'all') && (await (0, file_utils_1.fileExists)(server.httpLogPath))) {
716
+ const content = await fs.readFile(server.httpLogPath, 'utf-8');
717
+ const logLines = content.split('\n');
718
+ http = logLines.slice(-lines).join('\n');
719
+ }
720
+ // Stdout logs
721
+ if ((type === 'system' || type === 'stdout' || type === 'all') && (await (0, file_utils_1.fileExists)(server.stdoutPath))) {
596
722
  const content = await fs.readFile(server.stdoutPath, 'utf-8');
597
723
  const logLines = content.split('\n');
598
724
  stdout = logLines.slice(-lines).join('\n');
599
725
  }
600
- if ((type === 'stderr' || type === 'both') && (await (0, file_utils_1.fileExists)(server.stderrPath))) {
726
+ // Stderr logs
727
+ if ((type === 'system' || type === 'stderr' || type === 'all') && (await (0, file_utils_1.fileExists)(server.stderrPath))) {
601
728
  const content = await fs.readFile(server.stderrPath, 'utf-8');
602
729
  const logLines = content.split('\n');
603
730
  stderr = logLines.slice(-lines).join('\n');
604
731
  }
605
- this.sendJson(res, 200, { stdout, stderr });
732
+ this.sendJson(res, 200, { http, stdout, stderr });
606
733
  }
607
734
  catch (error) {
608
735
  this.sendError(res, 500, 'Internal Server Error', error.message, 'LOGS_ERROR');
609
736
  }
610
737
  }
611
738
  /**
612
- * List models
739
+ * List models (handles sharded models correctly)
613
740
  */
614
741
  async handleListModels(req, res) {
615
742
  try {
616
743
  const models = await model_scanner_1.modelScanner.scanModels();
617
- const modelsWithServers = await Promise.all(models.map(async (model) => {
618
- const servers = await state_manager_1.stateManager.getAllServers();
619
- const usingServers = servers.filter(s => s.modelName === model.filename);
744
+ const allServers = await state_manager_1.stateManager.getAllServers();
745
+ const modelsWithServers = models.map((model) => {
746
+ // Find servers using this model (handles sharded models)
747
+ const usingServers = allServers.filter(server => {
748
+ if (model.isSharded && model.shardPaths) {
749
+ // Check if server uses any shard of this model
750
+ return model.shardPaths.includes(server.modelPath);
751
+ }
752
+ else {
753
+ // Single-file model: exact path match
754
+ return server.modelPath === model.path;
755
+ }
756
+ });
620
757
  return {
621
758
  ...model,
622
759
  serversUsing: usingServers.length,
623
760
  serverIds: usingServers.map(s => s.id),
624
761
  };
625
- }));
762
+ });
626
763
  this.sendJson(res, 200, { models: modelsWithServers });
627
764
  }
628
765
  catch (error) {
@@ -759,46 +896,38 @@ class AdminServer {
759
896
  }
760
897
  /**
761
898
  * Delete model
899
+ * FIX: Now uses modelManagementService which filters by modelPath (not modelName)
762
900
  */
763
901
  async handleDeleteModel(req, res, modelName, url) {
764
902
  try {
765
903
  const cascade = url.searchParams.get('cascade') === 'true';
766
- // Find servers using this model
767
- const servers = await state_manager_1.stateManager.getAllServers();
768
- const usingServers = servers.filter(s => s.modelName === modelName);
769
- // Block deletion if servers exist and cascade not specified
770
- if (usingServers.length > 0 && !cascade) {
771
- this.sendError(res, 409, 'Conflict', `Model is used by ${usingServers.length} server(s). Use ?cascade=true to delete model and servers.`, 'MODEL_IN_USE');
772
- return;
773
- }
774
- // Delete servers if cascade
775
- const deletedServers = [];
776
- if (cascade) {
777
- for (const server of usingServers) {
778
- const status = await status_checker_1.statusChecker.checkServer(server);
779
- if (status_checker_1.statusChecker.determineStatus(status, status.portListening) === 'running') {
780
- await launchctl_manager_1.launchctlManager.unloadService(server.plistPath);
781
- await launchctl_manager_1.launchctlManager.waitForServiceStop(server.label, 5000);
782
- }
783
- await launchctl_manager_1.launchctlManager.deletePlist(server.plistPath);
784
- await state_manager_1.stateManager.deleteServerConfig(server.id);
785
- deletedServers.push(server.id);
904
+ // Delegate to modelManagementService (FIX: now filters by modelPath correctly)
905
+ const result = await model_management_service_1.modelManagementService.deleteModel({
906
+ modelIdentifier: modelName,
907
+ cascade,
908
+ });
909
+ if (!result.success) {
910
+ // Map errors to appropriate HTTP status codes
911
+ if (result.error?.includes('not found')) {
912
+ this.sendError(res, 404, 'Not Found', result.error, 'MODEL_NOT_FOUND');
913
+ }
914
+ else if (result.error?.includes('used by')) {
915
+ this.sendError(res, 409, 'Conflict', result.error, 'MODEL_IN_USE');
916
+ }
917
+ else {
918
+ this.sendError(res, 500, 'Internal Server Error', result.error || 'Delete failed', 'DELETE_ERROR');
786
919
  }
787
- }
788
- // Delete model file
789
- const modelPath = await model_scanner_1.modelScanner.resolveModelPath(modelName);
790
- if (!modelPath) {
791
- this.sendError(res, 404, 'Not Found', `Model not found: ${modelName}`, 'MODEL_NOT_FOUND');
792
920
  return;
793
921
  }
794
- await fs.unlink(modelPath);
922
+ // Success - return deleted servers info
795
923
  this.sendJson(res, 200, {
796
924
  success: true,
797
- deletedServers: deletedServers.length > 0 ? deletedServers : undefined,
925
+ modelPath: result.modelPath,
926
+ deletedServers: result.deletedServers,
798
927
  });
799
928
  }
800
929
  catch (error) {
801
- this.sendError(res, 500, 'Internal Server Error', error.message, 'DELETE_MODEL_ERROR');
930
+ this.sendError(res, 500, 'Internal Server Error', error.message, 'DELETE_ERROR');
802
931
  }
803
932
  }
804
933
  /**
@@ -871,7 +1000,7 @@ class AdminServer {
871
1000
  config: {
872
1001
  port: config.port,
873
1002
  host: config.host,
874
- verbose: config.verbose,
1003
+ logging: config.logging,
875
1004
  requestTimeout: config.requestTimeout,
876
1005
  healthCheckInterval: config.healthCheckInterval,
877
1006
  },
@@ -887,6 +1016,41 @@ class AdminServer {
887
1016
  this.sendError(res, 500, 'Internal Server Error', error.message, 'ROUTER_STATUS_ERROR');
888
1017
  }
889
1018
  }
1019
+ /**
1020
+ * Get admin status
1021
+ */
1022
+ async handleGetAdmin(req, res) {
1023
+ try {
1024
+ const adminStatus = await admin_manager_1.adminManager.getStatus();
1025
+ if (!adminStatus) {
1026
+ this.sendJson(res, 200, {
1027
+ status: 'not_configured',
1028
+ config: null,
1029
+ isRunning: false,
1030
+ });
1031
+ return;
1032
+ }
1033
+ const { config, status } = adminStatus;
1034
+ this.sendJson(res, 200, {
1035
+ status: status.isRunning ? 'running' : 'stopped',
1036
+ config: {
1037
+ port: config.port,
1038
+ host: config.host,
1039
+ logging: config.logging,
1040
+ requestTimeout: config.requestTimeout,
1041
+ },
1042
+ pid: status.pid,
1043
+ isRunning: status.isRunning,
1044
+ apiKey: config.apiKey,
1045
+ createdAt: config.createdAt,
1046
+ lastStarted: config.lastStarted,
1047
+ lastStopped: config.lastStopped,
1048
+ });
1049
+ }
1050
+ catch (error) {
1051
+ this.sendError(res, 500, 'Internal Server Error', error.message, 'ADMIN_STATUS_ERROR');
1052
+ }
1053
+ }
890
1054
  /**
891
1055
  * Start router service
892
1056
  */
@@ -946,16 +1110,22 @@ class AdminServer {
946
1110
  this.sendError(res, 404, 'Not Found', 'Router not configured', 'ROUTER_NOT_FOUND');
947
1111
  return;
948
1112
  }
949
- const type = url.searchParams.get('type') || 'both'; // stdout, stderr, or both
1113
+ const type = url.searchParams.get('type') || 'both'; // activity, system, stdout, stderr, or both
950
1114
  const lines = parseInt(url.searchParams.get('lines') || '100');
1115
+ // Support both new terminology and old parameter names (backward compatibility):
1116
+ // - 'activity' (new) or 'stdout' (old) -> router activity logs
1117
+ // - 'system' (new) or 'stderr' (old) -> system diagnostic logs
1118
+ // - 'both' (old) -> both stdout + stderr
951
1119
  let stdout = '';
952
1120
  let stderr = '';
953
- if ((type === 'stdout' || type === 'both') && (await (0, file_utils_1.fileExists)(config.stdoutPath))) {
1121
+ // Activity logs (stdout)
1122
+ if ((type === 'activity' || type === 'stdout' || type === 'both') && (await (0, file_utils_1.fileExists)(config.stdoutPath))) {
954
1123
  const content = await fs.readFile(config.stdoutPath, 'utf-8');
955
1124
  const logLines = content.split('\n');
956
1125
  stdout = logLines.slice(-lines).join('\n');
957
1126
  }
958
- if ((type === 'stderr' || type === 'both') && (await (0, file_utils_1.fileExists)(config.stderrPath))) {
1127
+ // System logs (stderr)
1128
+ if ((type === 'system' || type === 'stderr' || type === 'both') && (await (0, file_utils_1.fileExists)(config.stderrPath))) {
959
1129
  const content = await fs.readFile(config.stderrPath, 'utf-8');
960
1130
  const logLines = content.split('\n');
961
1131
  stderr = logLines.slice(-lines).join('\n');
@@ -966,6 +1136,33 @@ class AdminServer {
966
1136
  this.sendError(res, 500, 'Internal Server Error', error.message, 'ROUTER_LOGS_ERROR');
967
1137
  }
968
1138
  }
1139
+ /**
1140
+ * Get admin service logs content
1141
+ */
1142
+ async handleGetAdminServiceLogs(req, res, url) {
1143
+ try {
1144
+ const type = url.searchParams.get('type') || 'both'; // activity, system, or both
1145
+ const lines = parseInt(url.searchParams.get('lines') || '100');
1146
+ let stdout = '';
1147
+ let stderr = '';
1148
+ // Activity logs (stdout)
1149
+ if ((type === 'activity' || type === 'both') && (await (0, file_utils_1.fileExists)(this.config.stdoutPath))) {
1150
+ const content = await fs.readFile(this.config.stdoutPath, 'utf-8');
1151
+ const logLines = content.split('\n');
1152
+ stdout = logLines.slice(-lines).join('\n');
1153
+ }
1154
+ // System logs (stderr)
1155
+ if ((type === 'system' || type === 'both') && (await (0, file_utils_1.fileExists)(this.config.stderrPath))) {
1156
+ const content = await fs.readFile(this.config.stderrPath, 'utf-8');
1157
+ const logLines = content.split('\n');
1158
+ stderr = logLines.slice(-lines).join('\n');
1159
+ }
1160
+ this.sendJson(res, 200, { stdout, stderr });
1161
+ }
1162
+ catch (error) {
1163
+ this.sendError(res, 500, 'Internal Server Error', error.message, 'ADMIN_SERVICE_LOGS_ERROR');
1164
+ }
1165
+ }
969
1166
  /**
970
1167
  * Update router configuration
971
1168
  */
@@ -979,14 +1176,15 @@ class AdminServer {
979
1176
  return;
980
1177
  }
981
1178
  // Validate updates
982
- const allowedFields = ['port', 'host', 'verbose', 'requestTimeout', 'healthCheckInterval'];
1179
+ const allowedFields = ['port', 'host', 'logging', 'requestTimeout', 'healthCheckInterval'];
983
1180
  const invalidFields = Object.keys(updates).filter(key => !allowedFields.includes(key));
984
1181
  if (invalidFields.length > 0) {
985
1182
  this.sendError(res, 400, 'Bad Request', `Invalid fields: ${invalidFields.join(', ')}`, 'INVALID_FIELDS');
986
1183
  return;
987
1184
  }
988
1185
  // Apply updates
989
- const needsRestart = updates.port !== undefined || updates.host !== undefined;
1186
+ // Restart needed for: port, host (changes plist), or logging (changes router behavior)
1187
+ const needsRestart = updates.port !== undefined || updates.host !== undefined || updates.logging !== undefined;
990
1188
  await router_manager_1.routerManager.updateConfig(updates);
991
1189
  // Regenerate plist if needed
992
1190
  if (needsRestart) {
@@ -1005,6 +1203,122 @@ class AdminServer {
1005
1203
  this.sendError(res, 500, 'Internal Server Error', error.message, 'ROUTER_UPDATE_ERROR');
1006
1204
  }
1007
1205
  }
1206
+ /**
1207
+ * Get admin log information
1208
+ */
1209
+ async handleGetAdminLogs(req, res) {
1210
+ try {
1211
+ const allLogs = await log_management_service_1.logManagementService.scanAllLogs();
1212
+ // Include log management config
1213
+ const logConfig = this.config.logManagement || {
1214
+ autoRotate: { enabled: true, intervalHours: 24, thresholdMB: 100 },
1215
+ autoDelete: { enabled: true, intervalHours: 24, afterDays: 30 },
1216
+ };
1217
+ // Include worker status
1218
+ const workerStatus = {
1219
+ autoRotate: {
1220
+ enabled: logConfig.autoRotate.enabled,
1221
+ running: this.autoRotateWorker?.isRunning() || false,
1222
+ lastRun: this.autoRotateWorker?.getLastRun()?.toISOString(),
1223
+ },
1224
+ autoDelete: {
1225
+ enabled: logConfig.autoDelete.enabled,
1226
+ running: this.autoDeleteWorker?.isRunning() || false,
1227
+ lastRun: this.autoDeleteWorker?.getLastRun()?.toISOString(),
1228
+ },
1229
+ };
1230
+ this.sendJson(res, 200, {
1231
+ ...allLogs,
1232
+ config: logConfig,
1233
+ workers: workerStatus,
1234
+ });
1235
+ }
1236
+ catch (error) {
1237
+ this.sendError(res, 500, 'Internal Server Error', error.message, 'ADMIN_LOGS_ERROR');
1238
+ }
1239
+ }
1240
+ /**
1241
+ * Rotate log files
1242
+ */
1243
+ async handleRotateLogs(req, res) {
1244
+ try {
1245
+ const body = await this.readBody(req);
1246
+ const { type, serverId, streams } = JSON.parse(body);
1247
+ // Validate request
1248
+ if (!type || !streams || !Array.isArray(streams)) {
1249
+ this.sendError(res, 400, 'Bad Request', 'Missing or invalid type/streams', 'INVALID_REQUEST');
1250
+ return;
1251
+ }
1252
+ if (type === 'server' && !serverId) {
1253
+ this.sendError(res, 400, 'Bad Request', 'Missing serverId for server type', 'INVALID_REQUEST');
1254
+ return;
1255
+ }
1256
+ const archivedFiles = await log_management_service_1.logManagementService.rotateLogs(type, serverId, streams);
1257
+ this.sendJson(res, 200, {
1258
+ success: true,
1259
+ message: `Rotated ${archivedFiles.length} log file(s)`,
1260
+ archivedFiles,
1261
+ });
1262
+ }
1263
+ catch (error) {
1264
+ this.sendError(res, 500, 'Internal Server Error', error.message, 'ROTATE_LOGS_ERROR');
1265
+ }
1266
+ }
1267
+ /**
1268
+ * Clear archived logs
1269
+ */
1270
+ async handleClearArchivedLogs(req, res) {
1271
+ try {
1272
+ const body = await this.readBody(req);
1273
+ const { serverId } = JSON.parse(body);
1274
+ const result = await log_management_service_1.logManagementService.clearArchivedLogs(serverId);
1275
+ this.sendJson(res, 200, {
1276
+ success: true,
1277
+ count: result.count,
1278
+ totalSize: result.totalSize,
1279
+ });
1280
+ }
1281
+ catch (error) {
1282
+ this.sendError(res, 500, 'Internal Server Error', error.message, 'CLEAR_ARCHIVED_ERROR');
1283
+ }
1284
+ }
1285
+ /**
1286
+ * Update log management configuration
1287
+ */
1288
+ async handleUpdateLogConfig(req, res) {
1289
+ try {
1290
+ const body = await this.readBody(req);
1291
+ const updates = JSON.parse(body);
1292
+ // Load current admin config
1293
+ const configPath = path.join((0, file_utils_1.getConfigDir)(), 'admin.json');
1294
+ const currentConfig = await (0, file_utils_1.readJson)(configPath);
1295
+ // Merge updates into existing config
1296
+ const newLogConfig = {
1297
+ autoRotate: {
1298
+ ...currentConfig.logManagement?.autoRotate || { enabled: true, intervalHours: 24, thresholdMB: 100 },
1299
+ ...updates.autoRotate,
1300
+ },
1301
+ autoDelete: {
1302
+ ...currentConfig.logManagement?.autoDelete || { enabled: true, intervalHours: 24, afterDays: 30 },
1303
+ ...updates.autoDelete,
1304
+ },
1305
+ };
1306
+ // Update admin config
1307
+ currentConfig.logManagement = newLogConfig;
1308
+ await (0, file_utils_1.writeJsonAtomic)(configPath, currentConfig);
1309
+ // Restart workers with new configuration
1310
+ await this.stopWorkers();
1311
+ this.config = currentConfig; // Update in-memory config
1312
+ await this.startWorkers();
1313
+ this.sendJson(res, 200, {
1314
+ success: true,
1315
+ config: newLogConfig,
1316
+ });
1317
+ }
1318
+ catch (error) {
1319
+ this.sendError(res, 500, 'Internal Server Error', error.message, 'UPDATE_LOG_CONFIG_ERROR');
1320
+ }
1321
+ }
1008
1322
  /**
1009
1323
  * Authenticate request via API key
1010
1324
  */
@@ -1052,6 +1366,86 @@ class AdminServer {
1052
1366
  /**
1053
1367
  * Serve static files from web/dist directory
1054
1368
  */
1369
+ /**
1370
+ * Serve Swagger UI for API documentation
1371
+ */
1372
+ async handleSwaggerUI(req, res, pathname) {
1373
+ try {
1374
+ // Get swagger-ui-dist directory
1375
+ const swaggerUiPath = require.resolve('swagger-ui-dist');
1376
+ const swaggerDistDir = path.dirname(swaggerUiPath);
1377
+ // Handle /api-docs -> redirect to /api-docs/
1378
+ if (pathname === '/api-docs') {
1379
+ res.writeHead(302, { Location: '/api-docs/' });
1380
+ res.end();
1381
+ return;
1382
+ }
1383
+ // Handle swagger-initializer.js with custom config
1384
+ if (pathname === '/api-docs/swagger-initializer.js') {
1385
+ const customInitializer = `
1386
+ window.onload = function() {
1387
+ window.ui = SwaggerUIBundle({
1388
+ url: '/api-docs.json',
1389
+ dom_id: '#swagger-ui',
1390
+ deepLinking: true,
1391
+ presets: [
1392
+ SwaggerUIBundle.presets.apis,
1393
+ SwaggerUIStandalonePreset
1394
+ ],
1395
+ plugins: [
1396
+ SwaggerUIBundle.plugins.DownloadUrl
1397
+ ],
1398
+ layout: "StandaloneLayout"
1399
+ });
1400
+ };
1401
+ `;
1402
+ res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8' });
1403
+ res.end(customInitializer);
1404
+ return;
1405
+ }
1406
+ // Determine which file to serve
1407
+ let filePath;
1408
+ if (pathname === '/api-docs/' || pathname === '/api-docs/index.html') {
1409
+ filePath = path.join(swaggerDistDir, 'index.html');
1410
+ }
1411
+ else {
1412
+ // Serve other swagger-ui assets
1413
+ const assetPath = pathname.replace('/api-docs/', '');
1414
+ filePath = path.join(swaggerDistDir, assetPath);
1415
+ }
1416
+ // Security: Ensure file is within swagger dist directory
1417
+ const resolvedPath = path.resolve(filePath);
1418
+ if (!resolvedPath.startsWith(swaggerDistDir)) {
1419
+ this.sendError(res, 403, 'Forbidden', 'Access denied', 'FORBIDDEN');
1420
+ return;
1421
+ }
1422
+ // Check if file exists
1423
+ if (!(await (0, file_utils_1.fileExists)(resolvedPath))) {
1424
+ this.sendError(res, 404, 'Not Found', `Swagger UI file not found: ${pathname}`, 'SWAGGER_NOT_FOUND');
1425
+ return;
1426
+ }
1427
+ // Determine content type
1428
+ const ext = path.extname(resolvedPath);
1429
+ const contentTypes = {
1430
+ '.html': 'text/html; charset=utf-8',
1431
+ '.js': 'application/javascript; charset=utf-8',
1432
+ '.css': 'text/css; charset=utf-8',
1433
+ '.json': 'application/json; charset=utf-8',
1434
+ '.png': 'image/png',
1435
+ '.svg': 'image/svg+xml',
1436
+ '.map': 'application/json',
1437
+ };
1438
+ const contentType = contentTypes[ext] || 'application/octet-stream';
1439
+ // Read and serve file
1440
+ const content = await fs.readFile(resolvedPath);
1441
+ res.writeHead(200, { 'Content-Type': contentType });
1442
+ res.end(content);
1443
+ }
1444
+ catch (error) {
1445
+ console.error('[Admin] Error serving Swagger UI:', error);
1446
+ this.sendError(res, 500, 'Internal Server Error', error.message, 'SWAGGER_ERROR');
1447
+ }
1448
+ }
1055
1449
  async handleStaticFile(req, res, pathname) {
1056
1450
  try {
1057
1451
  // Resolve web/dist directory relative to project root
@@ -1129,7 +1523,7 @@ class AdminServer {
1129
1523
  * Log request
1130
1524
  */
1131
1525
  logRequest(method, pathname) {
1132
- if (this.config.verbose) {
1526
+ if (this.config.logging) {
1133
1527
  console.log(`[Admin] ${method} ${pathname}`);
1134
1528
  }
1135
1529
  }
@@ -1137,7 +1531,7 @@ class AdminServer {
1137
1531
  * Log response
1138
1532
  */
1139
1533
  logResponse(method, pathname, statusCode, durationMs) {
1140
- if (this.config.verbose) {
1534
+ if (this.config.logging) {
1141
1535
  console.log(`[Admin] ${method} ${pathname} ${statusCode} ${durationMs}ms`);
1142
1536
  }
1143
1537
  }