@appkit/llamacpp-cli 1.12.0 → 1.13.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.
- package/README.md +294 -168
- package/dist/cli.js +35 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/launch/claude.d.ts +6 -0
- package/dist/commands/launch/claude.d.ts.map +1 -0
- package/dist/commands/launch/claude.js +277 -0
- package/dist/commands/launch/claude.js.map +1 -0
- package/dist/lib/integration-checker.d.ts +26 -0
- package/dist/lib/integration-checker.d.ts.map +1 -0
- package/dist/lib/integration-checker.js +77 -0
- package/dist/lib/integration-checker.js.map +1 -0
- package/dist/lib/router-manager.d.ts +4 -0
- package/dist/lib/router-manager.d.ts.map +1 -1
- package/dist/lib/router-manager.js +10 -0
- package/dist/lib/router-manager.js.map +1 -1
- package/dist/lib/router-server.d.ts +13 -0
- package/dist/lib/router-server.d.ts.map +1 -1
- package/dist/lib/router-server.js +267 -7
- package/dist/lib/router-server.js.map +1 -1
- package/dist/types/integration-config.d.ts +28 -0
- package/dist/types/integration-config.d.ts.map +1 -0
- package/dist/types/integration-config.js +3 -0
- package/dist/types/integration-config.js.map +1 -0
- package/package.json +10 -2
- package/web/dist/assets/index-Bin89Lwr.css +1 -0
- package/web/dist/assets/index-CVmonw3T.js +17 -0
- package/web/{index.html → dist/index.html} +2 -1
- package/.versionrc.json +0 -16
- package/CHANGELOG.md +0 -213
- package/docs/images/.gitkeep +0 -1
- package/docs/images/web-ui-servers.png +0 -0
- package/src/cli.ts +0 -523
- package/src/commands/admin/config.ts +0 -121
- package/src/commands/admin/logs.ts +0 -91
- package/src/commands/admin/restart.ts +0 -26
- package/src/commands/admin/start.ts +0 -27
- package/src/commands/admin/status.ts +0 -84
- package/src/commands/admin/stop.ts +0 -16
- package/src/commands/config-global.ts +0 -38
- package/src/commands/config.ts +0 -323
- package/src/commands/create.ts +0 -183
- package/src/commands/delete.ts +0 -74
- package/src/commands/list.ts +0 -37
- package/src/commands/logs-all.ts +0 -251
- package/src/commands/logs.ts +0 -345
- package/src/commands/monitor.ts +0 -110
- package/src/commands/ps.ts +0 -84
- package/src/commands/pull.ts +0 -44
- package/src/commands/rm.ts +0 -107
- package/src/commands/router/config.ts +0 -116
- package/src/commands/router/logs.ts +0 -256
- package/src/commands/router/restart.ts +0 -36
- package/src/commands/router/start.ts +0 -60
- package/src/commands/router/status.ts +0 -119
- package/src/commands/router/stop.ts +0 -33
- package/src/commands/run.ts +0 -233
- package/src/commands/search.ts +0 -107
- package/src/commands/server-show.ts +0 -161
- package/src/commands/show.ts +0 -207
- package/src/commands/start.ts +0 -101
- package/src/commands/stop.ts +0 -39
- package/src/commands/tui.ts +0 -25
- package/src/lib/admin-manager.ts +0 -435
- package/src/lib/admin-server.ts +0 -1243
- package/src/lib/config-generator.ts +0 -130
- package/src/lib/download-job-manager.ts +0 -213
- package/src/lib/history-manager.ts +0 -172
- package/src/lib/launchctl-manager.ts +0 -225
- package/src/lib/metrics-aggregator.ts +0 -257
- package/src/lib/model-downloader.ts +0 -328
- package/src/lib/model-scanner.ts +0 -157
- package/src/lib/model-search.ts +0 -114
- package/src/lib/models-dir-setup.ts +0 -46
- package/src/lib/port-manager.ts +0 -80
- package/src/lib/router-logger.ts +0 -201
- package/src/lib/router-manager.ts +0 -414
- package/src/lib/router-server.ts +0 -538
- package/src/lib/state-manager.ts +0 -206
- package/src/lib/status-checker.ts +0 -113
- package/src/lib/system-collector.ts +0 -315
- package/src/tui/ConfigApp.ts +0 -1085
- package/src/tui/HistoricalMonitorApp.ts +0 -587
- package/src/tui/ModelsApp.ts +0 -368
- package/src/tui/MonitorApp.ts +0 -386
- package/src/tui/MultiServerMonitorApp.ts +0 -1833
- package/src/tui/RootNavigator.ts +0 -74
- package/src/tui/SearchApp.ts +0 -511
- package/src/tui/SplashScreen.ts +0 -149
- package/src/types/admin-config.ts +0 -25
- package/src/types/global-config.ts +0 -26
- package/src/types/history-types.ts +0 -39
- package/src/types/model-info.ts +0 -8
- package/src/types/monitor-types.ts +0 -162
- package/src/types/router-config.ts +0 -25
- package/src/types/server-config.ts +0 -46
- package/src/utils/downsample-utils.ts +0 -128
- package/src/utils/file-utils.ts +0 -146
- package/src/utils/format-utils.ts +0 -98
- package/src/utils/log-parser.ts +0 -284
- package/src/utils/log-utils.ts +0 -178
- package/src/utils/process-utils.ts +0 -316
- package/src/utils/prompt-utils.ts +0 -47
- package/test-load.sh +0 -100
- package/tsconfig.json +0 -20
- package/web/eslint.config.js +0 -23
- package/web/llamacpp-web-dist.tar.gz +0 -0
- package/web/package-lock.json +0 -4017
- package/web/package.json +0 -38
- package/web/postcss.config.js +0 -6
- package/web/src/App.css +0 -42
- package/web/src/App.tsx +0 -86
- package/web/src/assets/react.svg +0 -1
- package/web/src/components/ApiKeyPrompt.tsx +0 -71
- package/web/src/components/CreateServerModal.tsx +0 -372
- package/web/src/components/DownloadProgress.tsx +0 -123
- package/web/src/components/Nav.tsx +0 -89
- package/web/src/components/RouterConfigModal.tsx +0 -240
- package/web/src/components/SearchModal.tsx +0 -306
- package/web/src/components/ServerConfigModal.tsx +0 -291
- package/web/src/hooks/useApi.ts +0 -259
- package/web/src/index.css +0 -42
- package/web/src/lib/api.ts +0 -226
- package/web/src/main.tsx +0 -10
- package/web/src/pages/Dashboard.tsx +0 -103
- package/web/src/pages/Models.tsx +0 -258
- package/web/src/pages/Router.tsx +0 -270
- package/web/src/pages/RouterLogs.tsx +0 -201
- package/web/src/pages/ServerLogs.tsx +0 -553
- package/web/src/pages/Servers.tsx +0 -358
- package/web/src/types/api.ts +0 -140
- package/web/tailwind.config.js +0 -31
- package/web/tsconfig.app.json +0 -28
- package/web/tsconfig.json +0 -7
- package/web/tsconfig.node.json +0 -26
- package/web/vite.config.ts +0 -25
- /package/web/{public → dist}/vite.svg +0 -0
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import * as os from 'os';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import { ServerConfig, sanitizeModelName } from '../types/server-config';
|
|
4
|
-
import { getLogsDir, getLaunchAgentsDir } from '../utils/file-utils';
|
|
5
|
-
import { stateManager } from './state-manager';
|
|
6
|
-
|
|
7
|
-
export interface ServerOptions {
|
|
8
|
-
port?: number;
|
|
9
|
-
host?: string;
|
|
10
|
-
threads?: number;
|
|
11
|
-
ctxSize?: number;
|
|
12
|
-
gpuLayers?: number;
|
|
13
|
-
embeddings?: boolean;
|
|
14
|
-
jinja?: boolean;
|
|
15
|
-
verbose?: boolean;
|
|
16
|
-
customFlags?: string[];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface SmartDefaults {
|
|
20
|
-
threads: number;
|
|
21
|
-
ctxSize: number;
|
|
22
|
-
gpuLayers: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export class ConfigGenerator {
|
|
26
|
-
/**
|
|
27
|
-
* Calculate smart defaults based on model size
|
|
28
|
-
*/
|
|
29
|
-
calculateSmartDefaults(modelSizeBytes: number): SmartDefaults {
|
|
30
|
-
const sizeGB = modelSizeBytes / (1024 ** 3);
|
|
31
|
-
|
|
32
|
-
// Context size based on model size
|
|
33
|
-
let ctxSize: number;
|
|
34
|
-
if (sizeGB < 1) {
|
|
35
|
-
ctxSize = 2048; // < 1GB: small context
|
|
36
|
-
} else if (sizeGB < 3) {
|
|
37
|
-
ctxSize = 4096; // 1-3GB: medium
|
|
38
|
-
} else if (sizeGB < 6) {
|
|
39
|
-
ctxSize = 8192; // 3-6GB: large
|
|
40
|
-
} else {
|
|
41
|
-
ctxSize = 16384; // 6GB+: very large
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// GPU layers - always max for Metal (macOS)
|
|
45
|
-
const gpuLayers = 60; // llama.cpp auto-detects optimal value
|
|
46
|
-
|
|
47
|
-
// Threads - use half of available cores (better performance)
|
|
48
|
-
const cpuCount = os.cpus().length;
|
|
49
|
-
const threads = Math.max(4, Math.floor(cpuCount / 2));
|
|
50
|
-
|
|
51
|
-
return { threads, ctxSize, gpuLayers };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Generate server configuration
|
|
56
|
-
*/
|
|
57
|
-
async generateConfig(
|
|
58
|
-
modelPath: string,
|
|
59
|
-
modelName: string,
|
|
60
|
-
modelSize: number,
|
|
61
|
-
port: number,
|
|
62
|
-
options?: ServerOptions
|
|
63
|
-
): Promise<ServerConfig> {
|
|
64
|
-
// Calculate smart defaults
|
|
65
|
-
const smartDefaults = this.calculateSmartDefaults(modelSize);
|
|
66
|
-
|
|
67
|
-
// Apply user overrides
|
|
68
|
-
const host = options?.host ?? '127.0.0.1'; // Default to localhost (secure)
|
|
69
|
-
const threads = options?.threads ?? smartDefaults.threads;
|
|
70
|
-
const ctxSize = options?.ctxSize ?? smartDefaults.ctxSize;
|
|
71
|
-
const gpuLayers = options?.gpuLayers ?? smartDefaults.gpuLayers;
|
|
72
|
-
const embeddings = options?.embeddings ?? true;
|
|
73
|
-
const jinja = options?.jinja ?? true;
|
|
74
|
-
const verbose = options?.verbose ?? true; // Default to true (HTTP request logging)
|
|
75
|
-
const customFlags = options?.customFlags; // Optional custom flags
|
|
76
|
-
|
|
77
|
-
// Generate server ID
|
|
78
|
-
const id = sanitizeModelName(modelName);
|
|
79
|
-
|
|
80
|
-
// Generate paths
|
|
81
|
-
const label = `com.llama.${id}`;
|
|
82
|
-
const plistPath = path.join(getLaunchAgentsDir(), `${label}.plist`);
|
|
83
|
-
const logsDir = getLogsDir();
|
|
84
|
-
const stdoutPath = path.join(logsDir, `${id}.stdout`);
|
|
85
|
-
const stderrPath = path.join(logsDir, `${id}.stderr`);
|
|
86
|
-
|
|
87
|
-
const config: ServerConfig = {
|
|
88
|
-
id,
|
|
89
|
-
modelPath,
|
|
90
|
-
modelName,
|
|
91
|
-
port,
|
|
92
|
-
host,
|
|
93
|
-
threads,
|
|
94
|
-
ctxSize,
|
|
95
|
-
gpuLayers,
|
|
96
|
-
embeddings,
|
|
97
|
-
jinja,
|
|
98
|
-
verbose,
|
|
99
|
-
customFlags,
|
|
100
|
-
status: 'stopped',
|
|
101
|
-
createdAt: new Date().toISOString(),
|
|
102
|
-
plistPath,
|
|
103
|
-
label,
|
|
104
|
-
stdoutPath,
|
|
105
|
-
stderrPath,
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
return config;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Merge global defaults with user options
|
|
113
|
-
*/
|
|
114
|
-
async mergeWithGlobalDefaults(options?: ServerOptions): Promise<Partial<ServerOptions>> {
|
|
115
|
-
const globalConfig = await stateManager.loadGlobalConfig();
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
host: options?.host ?? '127.0.0.1',
|
|
119
|
-
threads: options?.threads ?? globalConfig.defaults.threads,
|
|
120
|
-
ctxSize: options?.ctxSize ?? globalConfig.defaults.ctxSize,
|
|
121
|
-
gpuLayers: options?.gpuLayers ?? globalConfig.defaults.gpuLayers,
|
|
122
|
-
embeddings: options?.embeddings ?? true,
|
|
123
|
-
jinja: options?.jinja ?? true,
|
|
124
|
-
verbose: options?.verbose ?? true,
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Export singleton instance
|
|
130
|
-
export const configGenerator = new ConfigGenerator();
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import * as path from 'path';
|
|
2
|
-
import { modelDownloader, DownloadProgress } from './model-downloader';
|
|
3
|
-
import { stateManager } from './state-manager';
|
|
4
|
-
|
|
5
|
-
export type DownloadJobStatus = 'pending' | 'downloading' | 'completed' | 'failed' | 'cancelled';
|
|
6
|
-
|
|
7
|
-
export interface DownloadJob {
|
|
8
|
-
id: string;
|
|
9
|
-
repo: string;
|
|
10
|
-
filename: string;
|
|
11
|
-
status: DownloadJobStatus;
|
|
12
|
-
progress: {
|
|
13
|
-
downloaded: number;
|
|
14
|
-
total: number;
|
|
15
|
-
percentage: number;
|
|
16
|
-
speed: string;
|
|
17
|
-
} | null;
|
|
18
|
-
error?: string;
|
|
19
|
-
createdAt: string;
|
|
20
|
-
completedAt?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface InternalJob extends DownloadJob {
|
|
24
|
-
abortController: AbortController;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Manages download jobs with progress tracking and cancellation support
|
|
29
|
-
*/
|
|
30
|
-
class DownloadJobManager {
|
|
31
|
-
private jobs: Map<string, InternalJob> = new Map();
|
|
32
|
-
private jobCounter = 0;
|
|
33
|
-
private cleanupInterval: NodeJS.Timeout | null = null;
|
|
34
|
-
|
|
35
|
-
constructor() {
|
|
36
|
-
// Auto-cleanup completed/failed jobs after 5 minutes
|
|
37
|
-
this.cleanupInterval = setInterval(() => this.cleanupOldJobs(), 60000);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Create a new download job
|
|
42
|
-
*/
|
|
43
|
-
createJob(repo: string, filename: string): string {
|
|
44
|
-
const id = `download-${Date.now()}-${++this.jobCounter}`;
|
|
45
|
-
const abortController = new AbortController();
|
|
46
|
-
|
|
47
|
-
const job: InternalJob = {
|
|
48
|
-
id,
|
|
49
|
-
repo,
|
|
50
|
-
filename,
|
|
51
|
-
status: 'pending',
|
|
52
|
-
progress: null,
|
|
53
|
-
createdAt: new Date().toISOString(),
|
|
54
|
-
abortController,
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
this.jobs.set(id, job);
|
|
58
|
-
|
|
59
|
-
// Start download asynchronously
|
|
60
|
-
this.startDownload(job);
|
|
61
|
-
|
|
62
|
-
return id;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Get a job by ID
|
|
67
|
-
*/
|
|
68
|
-
getJob(id: string): DownloadJob | null {
|
|
69
|
-
const job = this.jobs.get(id);
|
|
70
|
-
if (!job) return null;
|
|
71
|
-
|
|
72
|
-
// Return public job info (without abortController)
|
|
73
|
-
return this.toPublicJob(job);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* List all jobs
|
|
78
|
-
*/
|
|
79
|
-
listJobs(): DownloadJob[] {
|
|
80
|
-
return Array.from(this.jobs.values()).map(job => this.toPublicJob(job));
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Cancel a download job
|
|
85
|
-
*/
|
|
86
|
-
cancelJob(id: string): boolean {
|
|
87
|
-
const job = this.jobs.get(id);
|
|
88
|
-
if (!job) return false;
|
|
89
|
-
|
|
90
|
-
if (job.status === 'pending' || job.status === 'downloading') {
|
|
91
|
-
job.abortController.abort();
|
|
92
|
-
job.status = 'cancelled';
|
|
93
|
-
job.completedAt = new Date().toISOString();
|
|
94
|
-
return true;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Delete a job from the list
|
|
102
|
-
*/
|
|
103
|
-
deleteJob(id: string): boolean {
|
|
104
|
-
const job = this.jobs.get(id);
|
|
105
|
-
if (!job) return false;
|
|
106
|
-
|
|
107
|
-
// Cancel if still running
|
|
108
|
-
if (job.status === 'pending' || job.status === 'downloading') {
|
|
109
|
-
job.abortController.abort();
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
this.jobs.delete(id);
|
|
113
|
-
return true;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Start the download process for a job
|
|
118
|
-
*/
|
|
119
|
-
private async startDownload(job: InternalJob): Promise<void> {
|
|
120
|
-
job.status = 'downloading';
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
const modelsDir = await stateManager.getModelsDirectory();
|
|
124
|
-
|
|
125
|
-
await modelDownloader.downloadModel(
|
|
126
|
-
job.repo,
|
|
127
|
-
job.filename,
|
|
128
|
-
(progress: DownloadProgress) => {
|
|
129
|
-
job.progress = {
|
|
130
|
-
downloaded: progress.downloaded,
|
|
131
|
-
total: progress.total,
|
|
132
|
-
percentage: progress.percentage,
|
|
133
|
-
speed: progress.speed,
|
|
134
|
-
};
|
|
135
|
-
},
|
|
136
|
-
modelsDir,
|
|
137
|
-
{
|
|
138
|
-
silent: true,
|
|
139
|
-
signal: job.abortController.signal,
|
|
140
|
-
}
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
// Only mark as completed if not cancelled
|
|
144
|
-
if (job.status === 'downloading') {
|
|
145
|
-
job.status = 'completed';
|
|
146
|
-
job.completedAt = new Date().toISOString();
|
|
147
|
-
// Ensure progress shows 100%
|
|
148
|
-
if (job.progress) {
|
|
149
|
-
job.progress.percentage = 100;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
} catch (error) {
|
|
153
|
-
// Check if this was a cancellation (status may have been set by cancelJob)
|
|
154
|
-
const currentStatus = job.status as DownloadJobStatus;
|
|
155
|
-
if (currentStatus === 'cancelled') {
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const message = (error as Error).message;
|
|
160
|
-
if (message.includes('cancelled') || message.includes('interrupted')) {
|
|
161
|
-
job.status = 'cancelled';
|
|
162
|
-
} else {
|
|
163
|
-
job.status = 'failed';
|
|
164
|
-
job.error = message;
|
|
165
|
-
}
|
|
166
|
-
job.completedAt = new Date().toISOString();
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Convert internal job to public job (strips internal fields)
|
|
172
|
-
*/
|
|
173
|
-
private toPublicJob(job: InternalJob): DownloadJob {
|
|
174
|
-
const { abortController, ...publicJob } = job;
|
|
175
|
-
return publicJob;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Clean up old completed/failed jobs
|
|
180
|
-
*/
|
|
181
|
-
private cleanupOldJobs(): void {
|
|
182
|
-
const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
|
|
183
|
-
|
|
184
|
-
for (const [id, job] of this.jobs.entries()) {
|
|
185
|
-
if (
|
|
186
|
-
job.completedAt &&
|
|
187
|
-
new Date(job.completedAt).getTime() < fiveMinutesAgo
|
|
188
|
-
) {
|
|
189
|
-
this.jobs.delete(id);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Cleanup on shutdown
|
|
196
|
-
*/
|
|
197
|
-
shutdown(): void {
|
|
198
|
-
if (this.cleanupInterval) {
|
|
199
|
-
clearInterval(this.cleanupInterval);
|
|
200
|
-
this.cleanupInterval = null;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Cancel all active downloads
|
|
204
|
-
for (const job of this.jobs.values()) {
|
|
205
|
-
if (job.status === 'pending' || job.status === 'downloading') {
|
|
206
|
-
job.abortController.abort();
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Export singleton instance
|
|
213
|
-
export const downloadJobManager = new DownloadJobManager();
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile, access, rename } from 'fs/promises';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
import { homedir } from 'os';
|
|
4
|
-
import { ServerMetrics, SystemMetrics } from '../types/monitor-types.js';
|
|
5
|
-
import { HistoryData, HistorySnapshot, TIME_WINDOW_HOURS, TimeWindow } from '../types/history-types.js';
|
|
6
|
-
|
|
7
|
-
export class HistoryManager {
|
|
8
|
-
private serverId: string;
|
|
9
|
-
private historyDir: string;
|
|
10
|
-
private historyPath: string;
|
|
11
|
-
private readonly MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
12
|
-
|
|
13
|
-
constructor(serverId: string) {
|
|
14
|
-
this.serverId = serverId;
|
|
15
|
-
this.historyDir = join(homedir(), '.llamacpp', 'history');
|
|
16
|
-
this.historyPath = join(this.historyDir, `${serverId}.json`);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Append a new snapshot to history (with auto-pruning)
|
|
21
|
-
*/
|
|
22
|
-
async appendSnapshot(serverMetrics: ServerMetrics, systemMetrics?: SystemMetrics): Promise<void> {
|
|
23
|
-
try {
|
|
24
|
-
// Ensure history directory exists
|
|
25
|
-
await mkdir(this.historyDir, { recursive: true });
|
|
26
|
-
|
|
27
|
-
// Load existing history
|
|
28
|
-
const historyData = await this.loadHistoryData();
|
|
29
|
-
|
|
30
|
-
// Create new snapshot
|
|
31
|
-
const snapshot: HistorySnapshot = {
|
|
32
|
-
timestamp: Date.now(),
|
|
33
|
-
server: {
|
|
34
|
-
healthy: serverMetrics.healthy,
|
|
35
|
-
uptime: serverMetrics.uptime,
|
|
36
|
-
activeSlots: serverMetrics.activeSlots,
|
|
37
|
-
idleSlots: serverMetrics.idleSlots,
|
|
38
|
-
totalSlots: serverMetrics.totalSlots,
|
|
39
|
-
avgPromptSpeed: serverMetrics.avgPromptSpeed,
|
|
40
|
-
avgGenerateSpeed: serverMetrics.avgGenerateSpeed,
|
|
41
|
-
processMemory: serverMetrics.processMemory,
|
|
42
|
-
processCpuUsage: serverMetrics.processCpuUsage,
|
|
43
|
-
},
|
|
44
|
-
system: systemMetrics ? {
|
|
45
|
-
gpuUsage: systemMetrics.gpuUsage,
|
|
46
|
-
cpuUsage: systemMetrics.cpuUsage,
|
|
47
|
-
aneUsage: systemMetrics.aneUsage,
|
|
48
|
-
temperature: systemMetrics.temperature,
|
|
49
|
-
memoryUsed: systemMetrics.memoryUsed,
|
|
50
|
-
memoryTotal: systemMetrics.memoryTotal,
|
|
51
|
-
} : undefined,
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
// Append new snapshot
|
|
55
|
-
historyData.snapshots.push(snapshot);
|
|
56
|
-
|
|
57
|
-
// Prune old snapshots (keep only last 24h)
|
|
58
|
-
historyData.snapshots = this.pruneOldSnapshots(historyData.snapshots, this.MAX_AGE_MS);
|
|
59
|
-
|
|
60
|
-
// Atomic write: write to temp file in same directory, then rename
|
|
61
|
-
// This prevents read collisions during concurrent access
|
|
62
|
-
// IMPORTANT: temp file MUST be in same directory as destination for rename to work across filesystems
|
|
63
|
-
const tempPath = join(this.historyDir, `.${this.serverId}-${Date.now()}.tmp`);
|
|
64
|
-
await writeFile(tempPath, JSON.stringify(historyData, null, 2), 'utf-8');
|
|
65
|
-
await rename(tempPath, this.historyPath);
|
|
66
|
-
} catch (error) {
|
|
67
|
-
// Silent failure - don't interrupt monitoring
|
|
68
|
-
// Don't throw - just return silently to avoid polluting console
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Load all snapshots within specified time window
|
|
75
|
-
*/
|
|
76
|
-
async loadHistory(windowHours: number): Promise<HistorySnapshot[]> {
|
|
77
|
-
// Retry logic for file I/O collisions during concurrent read/write
|
|
78
|
-
const maxRetries = 3;
|
|
79
|
-
let lastError: Error | null = null;
|
|
80
|
-
|
|
81
|
-
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
82
|
-
try {
|
|
83
|
-
const historyData = await this.loadHistoryData();
|
|
84
|
-
return this.filterByTimeWindow(historyData.snapshots, windowHours);
|
|
85
|
-
} catch (error) {
|
|
86
|
-
lastError = error as Error;
|
|
87
|
-
// Wait briefly before retry (exponential backoff)
|
|
88
|
-
if (attempt < maxRetries - 1) {
|
|
89
|
-
await new Promise(resolve => setTimeout(resolve, 50 * Math.pow(2, attempt)));
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// All retries failed - throw error so it can be handled upstream
|
|
95
|
-
throw new Error(`Failed to load history after ${maxRetries} attempts: ${lastError?.message || 'Unknown error'}`);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Load history for specific time window type
|
|
100
|
-
*/
|
|
101
|
-
async loadHistoryByWindow(window: TimeWindow): Promise<HistorySnapshot[]> {
|
|
102
|
-
return this.loadHistory(TIME_WINDOW_HOURS[window]);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Get file path for server history
|
|
107
|
-
*/
|
|
108
|
-
getHistoryPath(): string {
|
|
109
|
-
return this.historyPath;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Check if history file exists
|
|
114
|
-
*/
|
|
115
|
-
async hasHistory(): Promise<boolean> {
|
|
116
|
-
try {
|
|
117
|
-
await access(this.historyPath);
|
|
118
|
-
return true;
|
|
119
|
-
} catch {
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Clear all history for server
|
|
126
|
-
*/
|
|
127
|
-
async clearHistory(): Promise<void> {
|
|
128
|
-
const emptyHistory: HistoryData = {
|
|
129
|
-
serverId: this.serverId,
|
|
130
|
-
snapshots: [],
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
await mkdir(this.historyDir, { recursive: true });
|
|
134
|
-
|
|
135
|
-
// Atomic write - temp file in same directory as destination
|
|
136
|
-
const tempPath = join(this.historyDir, `.${this.serverId}-${Date.now()}.tmp`);
|
|
137
|
-
await writeFile(tempPath, JSON.stringify(emptyHistory, null, 2), 'utf-8');
|
|
138
|
-
await rename(tempPath, this.historyPath);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Load full history data from file
|
|
143
|
-
*/
|
|
144
|
-
private async loadHistoryData(): Promise<HistoryData> {
|
|
145
|
-
try {
|
|
146
|
-
const content = await readFile(this.historyPath, 'utf-8');
|
|
147
|
-
return JSON.parse(content) as HistoryData;
|
|
148
|
-
} catch (error) {
|
|
149
|
-
// File doesn't exist or is corrupted, return empty history
|
|
150
|
-
return {
|
|
151
|
-
serverId: this.serverId,
|
|
152
|
-
snapshots: [],
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Prune snapshots older than maxAge
|
|
159
|
-
*/
|
|
160
|
-
private pruneOldSnapshots(snapshots: HistorySnapshot[], maxAgeMs: number): HistorySnapshot[] {
|
|
161
|
-
const cutoff = Date.now() - maxAgeMs;
|
|
162
|
-
return snapshots.filter(s => s.timestamp >= cutoff);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Filter snapshots by time window
|
|
167
|
-
*/
|
|
168
|
-
private filterByTimeWindow(snapshots: HistorySnapshot[], windowHours: number): HistorySnapshot[] {
|
|
169
|
-
const cutoff = Date.now() - (windowHours * 60 * 60 * 1000);
|
|
170
|
-
return snapshots.filter(s => s.timestamp >= cutoff);
|
|
171
|
-
}
|
|
172
|
-
}
|