@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
package/src/utils/file-utils.ts
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs/promises';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import * as os from 'os';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Ensure a directory exists, creating it if necessary
|
|
7
|
-
*/
|
|
8
|
-
export async function ensureDir(dirPath: string): Promise<void> {
|
|
9
|
-
try {
|
|
10
|
-
await fs.mkdir(dirPath, { recursive: true, mode: 0o755 });
|
|
11
|
-
} catch (error) {
|
|
12
|
-
// Ignore error if directory already exists
|
|
13
|
-
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') {
|
|
14
|
-
throw error;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Write a file atomically (write to temp, then rename)
|
|
21
|
-
*/
|
|
22
|
-
export async function writeFileAtomic(filePath: string, content: string): Promise<void> {
|
|
23
|
-
const tempPath = `${filePath}.tmp`;
|
|
24
|
-
await fs.writeFile(tempPath, content, 'utf-8');
|
|
25
|
-
await fs.rename(tempPath, filePath);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Write JSON to a file atomically
|
|
30
|
-
*/
|
|
31
|
-
export async function writeJsonAtomic(filePath: string, data: any): Promise<void> {
|
|
32
|
-
const content = JSON.stringify(data, null, 2);
|
|
33
|
-
await writeFileAtomic(filePath, content);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Read and parse JSON file
|
|
38
|
-
*/
|
|
39
|
-
export async function readJson<T>(filePath: string): Promise<T> {
|
|
40
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
41
|
-
return JSON.parse(content);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Check if a file exists
|
|
46
|
-
*/
|
|
47
|
-
export async function fileExists(filePath: string): Promise<boolean> {
|
|
48
|
-
try {
|
|
49
|
-
await fs.access(filePath);
|
|
50
|
-
return true;
|
|
51
|
-
} catch {
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Get the llamacpp config directory (~/.llamacpp)
|
|
58
|
-
*/
|
|
59
|
-
export function getConfigDir(): string {
|
|
60
|
-
return path.join(os.homedir(), '.llamacpp');
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Get the servers directory (~/.llamacpp/servers)
|
|
65
|
-
*/
|
|
66
|
-
export function getServersDir(): string {
|
|
67
|
-
return path.join(getConfigDir(), 'servers');
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Get the logs directory (~/.llamacpp/logs)
|
|
72
|
-
*/
|
|
73
|
-
export function getLogsDir(): string {
|
|
74
|
-
return path.join(getConfigDir(), 'logs');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Get the global config file path
|
|
79
|
-
*/
|
|
80
|
-
export function getGlobalConfigPath(): string {
|
|
81
|
-
return path.join(getConfigDir(), 'config.json');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Get the default models directory (~/.llamacpp/models)
|
|
86
|
-
*/
|
|
87
|
-
export function getModelsDir(): string {
|
|
88
|
-
return path.join(getConfigDir(), 'models');
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Get the LaunchAgents directory
|
|
93
|
-
*/
|
|
94
|
-
export function getLaunchAgentsDir(): string {
|
|
95
|
-
return path.join(os.homedir(), 'Library', 'LaunchAgents');
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Expand tilde (~) in path to home directory
|
|
100
|
-
*/
|
|
101
|
-
export function expandHome(filePath: string): string {
|
|
102
|
-
if (filePath.startsWith('~/')) {
|
|
103
|
-
return path.join(os.homedir(), filePath.slice(2));
|
|
104
|
-
}
|
|
105
|
-
return filePath;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Parse Metal (GPU) memory allocation from llama-server stderr logs
|
|
110
|
-
* Looks for line: "load_tensors: Metal_Mapped model buffer size = 11120.23 MiB"
|
|
111
|
-
* Returns memory in MB, or null if not found
|
|
112
|
-
*/
|
|
113
|
-
export async function parseMetalMemoryFromLog(stderrPath: string): Promise<number | null> {
|
|
114
|
-
try {
|
|
115
|
-
// Check if log file exists
|
|
116
|
-
if (!(await fileExists(stderrPath))) {
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Open file for reading
|
|
121
|
-
const fileHandle = await fs.open(stderrPath, 'r');
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
// Read first 256KB (Metal allocation happens early during model loading)
|
|
125
|
-
const buffer = Buffer.alloc(256 * 1024);
|
|
126
|
-
const { bytesRead } = await fileHandle.read(buffer, 0, buffer.length, 0);
|
|
127
|
-
const content = buffer.toString('utf-8', 0, bytesRead);
|
|
128
|
-
const lines = content.split('\n');
|
|
129
|
-
|
|
130
|
-
// Look for Metal_Mapped buffer size
|
|
131
|
-
for (const line of lines) {
|
|
132
|
-
const match = line.match(/Metal_Mapped model buffer size\s*=\s*([\d.]+)\s*MiB/);
|
|
133
|
-
if (match) {
|
|
134
|
-
const sizeInMB = parseFloat(match[1]);
|
|
135
|
-
return isNaN(sizeInMB) ? null : sizeInMB;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return null;
|
|
140
|
-
} finally {
|
|
141
|
-
await fileHandle.close();
|
|
142
|
-
}
|
|
143
|
-
} catch {
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Format bytes to human-readable size
|
|
3
|
-
* Example: 1900000000 → "1.9 GB"
|
|
4
|
-
*/
|
|
5
|
-
export function formatBytes(bytes: number): string {
|
|
6
|
-
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
7
|
-
let size = bytes;
|
|
8
|
-
let unitIndex = 0;
|
|
9
|
-
|
|
10
|
-
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
11
|
-
size /= 1024;
|
|
12
|
-
unitIndex++;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Format a date to a human-readable string
|
|
20
|
-
* Example: "Nov 20, 2025 10:30 AM"
|
|
21
|
-
*/
|
|
22
|
-
export function formatDate(date: Date | string): string {
|
|
23
|
-
const d = typeof date === 'string' ? new Date(date) : date;
|
|
24
|
-
return d.toLocaleString('en-US', {
|
|
25
|
-
month: 'short',
|
|
26
|
-
day: 'numeric',
|
|
27
|
-
year: 'numeric',
|
|
28
|
-
hour: 'numeric',
|
|
29
|
-
minute: '2-digit',
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Format a date to a short string (no time)
|
|
35
|
-
* Example: "Nov 20"
|
|
36
|
-
*/
|
|
37
|
-
export function formatDateShort(date: Date | string): string {
|
|
38
|
-
const d = typeof date === 'string' ? new Date(date) : date;
|
|
39
|
-
return d.toLocaleString('en-US', {
|
|
40
|
-
month: 'short',
|
|
41
|
-
day: 'numeric',
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Calculate uptime from start time
|
|
47
|
-
* Example: "2d 4h" or "45m" or "30s"
|
|
48
|
-
*/
|
|
49
|
-
export function formatUptime(startTime: string): string {
|
|
50
|
-
const start = new Date(startTime).getTime();
|
|
51
|
-
const now = Date.now();
|
|
52
|
-
const seconds = Math.floor((now - start) / 1000);
|
|
53
|
-
|
|
54
|
-
if (seconds < 60) return `${seconds}s`;
|
|
55
|
-
|
|
56
|
-
const minutes = Math.floor(seconds / 60);
|
|
57
|
-
if (minutes < 60) return `${minutes}m`;
|
|
58
|
-
|
|
59
|
-
const hours = Math.floor(minutes / 60);
|
|
60
|
-
if (hours < 24) return `${hours}h`;
|
|
61
|
-
|
|
62
|
-
const days = Math.floor(hours / 24);
|
|
63
|
-
const remainingHours = hours % 24;
|
|
64
|
-
return remainingHours > 0 ? `${days}d ${remainingHours}h` : `${days}d`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Truncate a string to a maximum length
|
|
69
|
-
*/
|
|
70
|
-
export function truncate(str: string, maxLength: number): string {
|
|
71
|
-
if (str.length <= maxLength) return str;
|
|
72
|
-
return str.slice(0, maxLength - 3) + '...';
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Pad a string to a specific length
|
|
77
|
-
*/
|
|
78
|
-
export function pad(str: string, length: number, char = ' '): string {
|
|
79
|
-
return str.padEnd(length, char);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Format context size to human-readable format
|
|
84
|
-
* Uses "k" suffix for clean multiples of 1024 (e.g., 32768 → "32k")
|
|
85
|
-
* Falls back to full number with "tokens" for non-standard sizes
|
|
86
|
-
*/
|
|
87
|
-
export function formatContextSize(tokens: number): string {
|
|
88
|
-
// Check if it's a clean multiple of 1024
|
|
89
|
-
if (tokens % 1024 === 0) {
|
|
90
|
-
const k = tokens / 1024;
|
|
91
|
-
// Only use "k" format for reasonable sizes (1k to 1024k i.e., up to 1M)
|
|
92
|
-
if (k >= 1 && k <= 1024) {
|
|
93
|
-
return `${k}k`;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
// For non-standard sizes or very large values, show full number
|
|
97
|
-
return `${tokens.toLocaleString()} tokens`;
|
|
98
|
-
}
|
package/src/utils/log-parser.ts
DELETED
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Parse and consolidate verbose llama-server logs into compact single-line format
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
interface CompactLogEntry {
|
|
6
|
-
timestamp: string;
|
|
7
|
-
method: string;
|
|
8
|
-
endpoint: string;
|
|
9
|
-
ip: string;
|
|
10
|
-
status: number;
|
|
11
|
-
userMessage: string;
|
|
12
|
-
tokensIn: number;
|
|
13
|
-
tokensOut: number;
|
|
14
|
-
responseTimeMs: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export class LogParser {
|
|
18
|
-
private buffer: string[] = [];
|
|
19
|
-
private isBuffering = false;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Health check endpoints to filter out by default
|
|
23
|
-
* These are polled frequently by the TUI and generate excessive log noise
|
|
24
|
-
*/
|
|
25
|
-
private static readonly HEALTH_CHECK_ENDPOINTS = ['/health', '/slots', '/props'];
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Check if a log line represents a health check request
|
|
29
|
-
*/
|
|
30
|
-
isHealthCheckRequest(line: string): boolean {
|
|
31
|
-
return LogParser.HEALTH_CHECK_ENDPOINTS.some(ep => line.includes(`GET ${ep} `));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Check if line is a request status line (contains method/endpoint/status, no JSON)
|
|
36
|
-
* Handles both old and new formats:
|
|
37
|
-
* - Old: log_server_r: request: POST /v1/chat/completions 127.0.0.1 200
|
|
38
|
-
* - New: log_server_r: done request: POST /v1/messages 172.16.0.114 200
|
|
39
|
-
*/
|
|
40
|
-
private isRequestStatusLine(line: string): boolean {
|
|
41
|
-
return (
|
|
42
|
-
(line.includes('log_server_r: request:') || line.includes('log_server_r: done request:')) &&
|
|
43
|
-
!line.includes('{') &&
|
|
44
|
-
/(?:done )?request: (POST|GET|PUT|DELETE)/.test(line)
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Process log lines and output compact format
|
|
50
|
-
*/
|
|
51
|
-
processLine(line: string, callback: (compactLine: string) => void): void {
|
|
52
|
-
// Check if this is a request status line (no JSON, has method/endpoint/status)
|
|
53
|
-
// Handles both old format (request:) and new format (done request:)
|
|
54
|
-
if (this.isRequestStatusLine(line)) {
|
|
55
|
-
// Check if this is the start of verbose format (status line before JSON)
|
|
56
|
-
// or a simple single-line log
|
|
57
|
-
if (this.isBuffering) {
|
|
58
|
-
// We're already buffering, so this is a new request - process previous buffer
|
|
59
|
-
const compactLine = this.consolidateRequest(this.buffer);
|
|
60
|
-
if (compactLine) {
|
|
61
|
-
callback(compactLine);
|
|
62
|
-
}
|
|
63
|
-
this.buffer = [];
|
|
64
|
-
this.isBuffering = false;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Start buffering (might be verbose or simple)
|
|
68
|
-
this.isBuffering = true;
|
|
69
|
-
this.buffer = [line];
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// If we're buffering, collect lines
|
|
74
|
-
if (this.isBuffering) {
|
|
75
|
-
this.buffer.push(line);
|
|
76
|
-
|
|
77
|
-
// Check if we have a complete request (found response line in verbose mode)
|
|
78
|
-
if (line.includes('log_server_r: response:')) {
|
|
79
|
-
const compactLine = this.consolidateRequest(this.buffer);
|
|
80
|
-
if (compactLine) {
|
|
81
|
-
callback(compactLine);
|
|
82
|
-
}
|
|
83
|
-
this.buffer = [];
|
|
84
|
-
this.isBuffering = false;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Flush any buffered simple format logs
|
|
91
|
-
* Call this at the end of processing to handle simple logs that don't have response lines
|
|
92
|
-
*/
|
|
93
|
-
flush(callback: (compactLine: string) => void): void {
|
|
94
|
-
if (this.isBuffering && this.buffer.length > 0) {
|
|
95
|
-
// If we only have one line, it's a simple format log
|
|
96
|
-
if (this.buffer.length === 1) {
|
|
97
|
-
const simpleLine = this.parseSimpleFormat(this.buffer[0]);
|
|
98
|
-
if (simpleLine) {
|
|
99
|
-
callback(simpleLine);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
this.buffer = [];
|
|
103
|
-
this.isBuffering = false;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Parse simple single-line format (non-verbose mode)
|
|
109
|
-
* Handles both old and new formats:
|
|
110
|
-
* - Old: srv log_server_r: request: POST /v1/chat/completions 127.0.0.1 200
|
|
111
|
-
* - New: srv log_server_r: done request: POST /v1/messages 172.16.0.114 200
|
|
112
|
-
*/
|
|
113
|
-
private parseSimpleFormat(line: string): string | null {
|
|
114
|
-
try {
|
|
115
|
-
const timestamp = this.extractTimestamp(line);
|
|
116
|
-
// Match both "request:" and "done request:" formats
|
|
117
|
-
const requestMatch = line.match(/(?:done )?request: (POST|GET|PUT|DELETE) ([^\s]+) ([^\s]+) (\d+)/);
|
|
118
|
-
if (!requestMatch) return null;
|
|
119
|
-
|
|
120
|
-
const [, method, endpoint, ip, status] = requestMatch;
|
|
121
|
-
|
|
122
|
-
// Simple format doesn't include message/token details
|
|
123
|
-
return `${timestamp} ${method} ${endpoint} ${ip} ${status}`;
|
|
124
|
-
} catch (error) {
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Consolidate buffered request/response lines into single line
|
|
131
|
-
* Handles both old and new llama.cpp log formats
|
|
132
|
-
*/
|
|
133
|
-
private consolidateRequest(lines: string[]): string | null {
|
|
134
|
-
try {
|
|
135
|
-
// Parse first line: timestamp and request info
|
|
136
|
-
// Match both "request:" and "done request:" formats
|
|
137
|
-
const firstLine = lines[0];
|
|
138
|
-
const timestamp = this.extractTimestamp(firstLine);
|
|
139
|
-
const requestMatch = firstLine.match(/(?:done )?request: (POST|GET|PUT|DELETE) (\/[^\s]+) ([^\s]+) (\d+)/);
|
|
140
|
-
if (!requestMatch) return null;
|
|
141
|
-
|
|
142
|
-
const [, method, endpoint, ip, status] = requestMatch;
|
|
143
|
-
|
|
144
|
-
// Parse request JSON (line with JSON body)
|
|
145
|
-
const requestLine = lines.find((l) => l.includes('log_server_r: request:') && l.includes('{'));
|
|
146
|
-
|
|
147
|
-
let userMessage = '';
|
|
148
|
-
if (requestLine) {
|
|
149
|
-
const requestJson = this.extractJson(requestLine);
|
|
150
|
-
if (requestJson) {
|
|
151
|
-
userMessage = this.extractUserMessage(requestJson);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Parse response JSON (may be empty in new format)
|
|
156
|
-
const responseLine = lines.find((l) => l.includes('log_server_r: response:'));
|
|
157
|
-
let tokensIn = 0;
|
|
158
|
-
let tokensOut = 0;
|
|
159
|
-
let responseTimeMs = 0;
|
|
160
|
-
|
|
161
|
-
if (responseLine) {
|
|
162
|
-
const responseJson = this.extractJson(responseLine);
|
|
163
|
-
if (responseJson) {
|
|
164
|
-
tokensIn = responseJson.usage?.prompt_tokens || 0;
|
|
165
|
-
tokensOut = responseJson.usage?.completion_tokens || 0;
|
|
166
|
-
responseTimeMs = this.extractResponseTime(responseJson);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Format compact line (works even without response data)
|
|
171
|
-
return this.formatCompactLine({
|
|
172
|
-
timestamp,
|
|
173
|
-
method,
|
|
174
|
-
endpoint,
|
|
175
|
-
ip,
|
|
176
|
-
status: parseInt(status, 10),
|
|
177
|
-
userMessage,
|
|
178
|
-
tokensIn,
|
|
179
|
-
tokensOut,
|
|
180
|
-
responseTimeMs,
|
|
181
|
-
});
|
|
182
|
-
} catch (error) {
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Extract timestamp from log line
|
|
189
|
-
*/
|
|
190
|
-
private extractTimestamp(line: string): string {
|
|
191
|
-
// Look for timestamp format like [2025-12-09 10:13:45]
|
|
192
|
-
const match = line.match(/\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/);
|
|
193
|
-
if (match) {
|
|
194
|
-
return match[1]; // Return as-is: 2025-12-09 10:13:45
|
|
195
|
-
}
|
|
196
|
-
// If no timestamp in logs, use current time in same format
|
|
197
|
-
const now = new Date();
|
|
198
|
-
return now.toISOString().substring(0, 19).replace('T', ' '); // 2025-12-09 10:13:45
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Extract JSON from log line
|
|
203
|
-
*/
|
|
204
|
-
private extractJson(line: string): any {
|
|
205
|
-
const jsonStart = line.indexOf('{');
|
|
206
|
-
if (jsonStart === -1) return null;
|
|
207
|
-
|
|
208
|
-
try {
|
|
209
|
-
const jsonStr = line.substring(jsonStart);
|
|
210
|
-
return JSON.parse(jsonStr);
|
|
211
|
-
} catch {
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Extract first user message from request JSON
|
|
218
|
-
* Handles both string content and array content formats:
|
|
219
|
-
* - String: {"role":"user","content":"Hello"}
|
|
220
|
-
* - Array: {"role":"user","content":[{"type":"text","text":"Hello"}]}
|
|
221
|
-
*/
|
|
222
|
-
private extractUserMessage(requestJson: any): string {
|
|
223
|
-
const messages = requestJson.messages || [];
|
|
224
|
-
const userMsg = messages.find((m: any) => m.role === 'user');
|
|
225
|
-
if (!userMsg || !userMsg.content) return '';
|
|
226
|
-
|
|
227
|
-
let content: string;
|
|
228
|
-
|
|
229
|
-
// Handle array format (e.g., Claude/Anthropic API style)
|
|
230
|
-
if (Array.isArray(userMsg.content)) {
|
|
231
|
-
const textPart = userMsg.content.find((p: any) => p.type === 'text');
|
|
232
|
-
content = textPart?.text || '';
|
|
233
|
-
} else {
|
|
234
|
-
content = userMsg.content;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Clean and truncate to first 50 characters
|
|
238
|
-
content = content.replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
|
|
239
|
-
return content.length > 50 ? content.substring(0, 47) + '...' : content;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Extract response time from response JSON
|
|
244
|
-
*/
|
|
245
|
-
private extractResponseTime(responseJson: any): number {
|
|
246
|
-
// Check __verbose.timings first (has total time)
|
|
247
|
-
const verboseTimings = responseJson.__verbose?.timings;
|
|
248
|
-
if (verboseTimings) {
|
|
249
|
-
const promptMs = verboseTimings.prompt_ms || 0;
|
|
250
|
-
const predictedMs = verboseTimings.predicted_ms || 0;
|
|
251
|
-
return Math.round(promptMs + predictedMs);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Fallback to top-level timings
|
|
255
|
-
const timings = responseJson.timings;
|
|
256
|
-
if (timings) {
|
|
257
|
-
const promptMs = timings.prompt_ms || 0;
|
|
258
|
-
const predictedMs = timings.predicted_ms || 0;
|
|
259
|
-
return Math.round(promptMs + predictedMs);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
return 0;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Format compact log line
|
|
267
|
-
*/
|
|
268
|
-
private formatCompactLine(entry: CompactLogEntry): string {
|
|
269
|
-
return [
|
|
270
|
-
entry.timestamp,
|
|
271
|
-
entry.method,
|
|
272
|
-
entry.endpoint,
|
|
273
|
-
entry.ip,
|
|
274
|
-
entry.status,
|
|
275
|
-
`"${entry.userMessage}"`,
|
|
276
|
-
entry.tokensIn,
|
|
277
|
-
entry.tokensOut,
|
|
278
|
-
entry.responseTimeMs,
|
|
279
|
-
].join(' ');
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Export singleton instance
|
|
284
|
-
export const logParser = new LogParser();
|
package/src/utils/log-utils.ts
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs/promises';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import { fileExists, getLogsDir } from './file-utils';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Get the size of a file in bytes
|
|
7
|
-
*/
|
|
8
|
-
export async function getFileSize(filePath: string): Promise<number> {
|
|
9
|
-
try {
|
|
10
|
-
const stats = await fs.stat(filePath);
|
|
11
|
-
return stats.size;
|
|
12
|
-
} catch {
|
|
13
|
-
return 0;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Format bytes to human-readable size
|
|
19
|
-
*/
|
|
20
|
-
export function formatFileSize(bytes: number): string {
|
|
21
|
-
if (bytes === 0) return '0 B';
|
|
22
|
-
const k = 1024;
|
|
23
|
-
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
24
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
25
|
-
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Rotate a log file with timestamp
|
|
30
|
-
* Renames current log to <name>.YYYY-MM-DD-HH-MM-SS.<ext>
|
|
31
|
-
* Returns the new archived filename
|
|
32
|
-
*/
|
|
33
|
-
export async function rotateLogFile(logPath: string): Promise<string> {
|
|
34
|
-
if (!(await fileExists(logPath))) {
|
|
35
|
-
throw new Error(`Log file does not exist: ${logPath}`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Get file size before rotation
|
|
39
|
-
const size = await getFileSize(logPath);
|
|
40
|
-
if (size === 0) {
|
|
41
|
-
throw new Error('Log file is empty, nothing to rotate');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Generate timestamp
|
|
45
|
-
const timestamp = new Date()
|
|
46
|
-
.toISOString()
|
|
47
|
-
.replace(/T/, '-')
|
|
48
|
-
.replace(/:/g, '-')
|
|
49
|
-
.replace(/\..+/, '');
|
|
50
|
-
|
|
51
|
-
// Parse path components
|
|
52
|
-
const dir = path.dirname(logPath);
|
|
53
|
-
const ext = path.extname(logPath);
|
|
54
|
-
const basename = path.basename(logPath, ext);
|
|
55
|
-
|
|
56
|
-
// New archived filename
|
|
57
|
-
const archivedPath = path.join(dir, `${basename}.${timestamp}${ext}`);
|
|
58
|
-
|
|
59
|
-
// Rename current log to archived version
|
|
60
|
-
await fs.rename(logPath, archivedPath);
|
|
61
|
-
|
|
62
|
-
return archivedPath;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Clear (truncate) a log file to zero bytes
|
|
67
|
-
*/
|
|
68
|
-
export async function clearLogFile(logPath: string): Promise<void> {
|
|
69
|
-
if (!(await fileExists(logPath))) {
|
|
70
|
-
throw new Error(`Log file does not exist: ${logPath}`);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Truncate file to 0 bytes
|
|
74
|
-
await fs.truncate(logPath, 0);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Auto-rotate log files if they exceed threshold
|
|
79
|
-
* Returns true if rotation occurred, false otherwise
|
|
80
|
-
*/
|
|
81
|
-
export async function autoRotateIfNeeded(
|
|
82
|
-
stdoutPath: string,
|
|
83
|
-
stderrPath: string,
|
|
84
|
-
thresholdMB: number = 100
|
|
85
|
-
): Promise<{ rotated: boolean; files: string[] }> {
|
|
86
|
-
const thresholdBytes = thresholdMB * 1024 * 1024;
|
|
87
|
-
const rotatedFiles: string[] = [];
|
|
88
|
-
|
|
89
|
-
// Check stdout
|
|
90
|
-
if (await fileExists(stdoutPath)) {
|
|
91
|
-
const stdoutSize = await getFileSize(stdoutPath);
|
|
92
|
-
if (stdoutSize > thresholdBytes) {
|
|
93
|
-
const archived = await rotateLogFile(stdoutPath);
|
|
94
|
-
rotatedFiles.push(archived);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Check stderr
|
|
99
|
-
if (await fileExists(stderrPath)) {
|
|
100
|
-
const stderrSize = await getFileSize(stderrPath);
|
|
101
|
-
if (stderrSize > thresholdBytes) {
|
|
102
|
-
const archived = await rotateLogFile(stderrPath);
|
|
103
|
-
rotatedFiles.push(archived);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
rotated: rotatedFiles.length > 0,
|
|
109
|
-
files: rotatedFiles,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Get information about archived log files for a server
|
|
115
|
-
* Returns count and total size of timestamped archived logs
|
|
116
|
-
*/
|
|
117
|
-
export async function getArchivedLogInfo(serverId: string): Promise<{
|
|
118
|
-
count: number;
|
|
119
|
-
totalSize: number;
|
|
120
|
-
}> {
|
|
121
|
-
const logsDir = getLogsDir();
|
|
122
|
-
let count = 0;
|
|
123
|
-
let totalSize = 0;
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
const files = await fs.readdir(logsDir);
|
|
127
|
-
|
|
128
|
-
// Pattern matches: server-id.YYYY-MM-DD-HH-MM-SS.{stdout,stderr}
|
|
129
|
-
const pattern = new RegExp(`^${serverId}\\.(\\d{4}-\\d{2}-\\d{2}-\\d{2}-\\d{2}-\\d{2})\\.(stdout|stderr)$`);
|
|
130
|
-
|
|
131
|
-
for (const file of files) {
|
|
132
|
-
if (pattern.test(file)) {
|
|
133
|
-
count++;
|
|
134
|
-
const filePath = path.join(logsDir, file);
|
|
135
|
-
totalSize += await getFileSize(filePath);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
} catch {
|
|
139
|
-
// Directory doesn't exist or can't be read
|
|
140
|
-
return { count: 0, totalSize: 0 };
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return { count, totalSize };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Delete all archived log files for a server
|
|
148
|
-
* Returns count and total size of deleted files
|
|
149
|
-
*/
|
|
150
|
-
export async function deleteArchivedLogs(serverId: string): Promise<{
|
|
151
|
-
count: number;
|
|
152
|
-
totalSize: number;
|
|
153
|
-
}> {
|
|
154
|
-
const logsDir = getLogsDir();
|
|
155
|
-
let count = 0;
|
|
156
|
-
let totalSize = 0;
|
|
157
|
-
|
|
158
|
-
try {
|
|
159
|
-
const files = await fs.readdir(logsDir);
|
|
160
|
-
|
|
161
|
-
// Pattern matches: server-id.YYYY-MM-DD-HH-MM-SS.{stdout,stderr}
|
|
162
|
-
const pattern = new RegExp(`^${serverId}\\.(\\d{4}-\\d{2}-\\d{2}-\\d{2}-\\d{2}-\\d{2})\\.(stdout|stderr)$`);
|
|
163
|
-
|
|
164
|
-
for (const file of files) {
|
|
165
|
-
if (pattern.test(file)) {
|
|
166
|
-
const filePath = path.join(logsDir, file);
|
|
167
|
-
const size = await getFileSize(filePath);
|
|
168
|
-
await fs.unlink(filePath);
|
|
169
|
-
count++;
|
|
170
|
-
totalSize += size;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
} catch (error) {
|
|
174
|
-
throw new Error(`Failed to delete archived logs: ${(error as Error).message}`);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return { count, totalSize };
|
|
178
|
-
}
|