@tonyclaw/llm-inspector 1.6.3 → 1.7.1

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/src/cli.ts CHANGED
@@ -1,89 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import { spawn, execSync } from "node:child_process";
2
+ import { spawn } from "node:child_process";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { dirname, join } from "node:path";
5
- import { existsSync } from "node:fs";
6
5
 
7
6
  const __filename = fileURLToPath(import.meta.url);
8
7
  const __dirname = dirname(__filename);
9
8
 
10
9
  const DEFAULT_PORT = 25947;
11
10
 
12
- // Find bun executable
13
- const findBun = (): string | null => {
14
- // Try running `where bun` (Windows) or `which bun` (Unix) to find in PATH
15
- try {
16
- const cmd = process.platform === "win32" ? "where bun" : "which bun";
17
- const output = execSync(cmd, { encoding: "utf8", timeout: 5000 }).trim();
18
- if (!output) return null;
19
-
20
- // Take first line and handle Windows .cmd shim
21
- const firstLine = output.split("\n")[0] ?? "";
22
- const firstPath = firstLine.trim();
23
- if (!firstPath) return null;
24
-
25
- if (process.platform === "win32" && firstPath.endsWith(".cmd")) {
26
- // npm shim - find actual exe
27
- const dir = join(firstPath, "..");
28
- const actualPath = join(dir, "node_modules", "bun", "bin", "bun.exe");
29
- if (existsSync(actualPath)) {
30
- return actualPath;
31
- }
32
- // Also try .exe directly in same directory
33
- const exePath = join(dir, "bun.exe");
34
- if (existsSync(exePath)) {
35
- return exePath;
36
- }
37
- }
38
- if (existsSync(firstPath)) {
39
- return firstPath;
40
- }
41
- } catch {
42
- // Command failed, continue with other methods
43
- }
44
-
45
- // Check PATH directories directly
46
- const pathEnv = process.env.PATH ?? "";
47
- const pathDirs = pathEnv.split(process.platform === "win32" ? ";" : ":");
48
-
49
- for (const dir of pathDirs) {
50
- const bunPath = join(dir, process.platform === "win32" ? "bun.exe" : "bun");
51
- if (existsSync(bunPath)) {
52
- // On Windows, npm shim is a script, not the actual exe
53
- if (process.platform === "win32" && !bunPath.endsWith(".exe")) {
54
- const actualPath = join(dir, "node_modules", "bun", "bin", "bun.exe");
55
- if (existsSync(actualPath)) {
56
- return actualPath;
57
- }
58
- }
59
- return bunPath;
60
- }
61
- }
62
-
63
- // Common Windows paths
64
- if (process.platform === "win32") {
65
- const localAppData = process.env.LOCALAPPDATA ?? "";
66
- const appData = process.env.APPDATA ?? "";
67
- const userProfile = process.env.USERPROFILE ?? "";
68
-
69
- const commonPaths = [
70
- join(localAppData, "bun", "bin", "bun.exe"),
71
- join(appData, "bun", "bin", "bun.exe"),
72
- join(userProfile, "AppData", "Local", "bun", "bin", "bun.exe"),
73
- join(userProfile, "AppData", "Roaming", "npm", "node_modules", "bun", "bin", "bun.exe"),
74
- join(userProfile, "AppData", "Roaming", "npm", "bun.exe"),
75
- join(userProfile, "AppData", "Roaming", "npm", "bun"),
76
- ];
77
- for (const bunPath of commonPaths) {
78
- if (existsSync(bunPath)) {
79
- return bunPath;
80
- }
81
- }
82
- }
83
-
84
- return null;
85
- };
86
-
87
11
  const envPort = process.env["PORT"];
88
12
  const portDefault = envPort !== undefined ? Number(envPort) : DEFAULT_PORT;
89
13
 
@@ -157,22 +81,15 @@ if (open) {
157
81
  openBrowser(url);
158
82
  }
159
83
 
160
- // Find bun and start server
161
- const bunPath = findBun();
162
- if (bunPath === null) {
163
- console.error("\nError: bun is not installed or not in PATH.");
164
- console.error("Please install bun from https://bun.sh");
165
- process.exit(1);
166
- }
167
-
168
84
  // Compute server path
169
85
  const outputDir = __dirname;
170
86
  const serverPath = join(outputDir, "../.output/server/index.mjs");
171
87
 
172
- // Start server with bun
173
- const serverProcess = spawn(bunPath, [serverPath], {
88
+ // Start server with node
89
+ const serverProcess = spawn(process.execPath, [serverPath], {
174
90
  stdio: ["ignore", "inherit", "inherit"],
175
91
  detached: true,
92
+ env: { ...process.env },
176
93
  });
177
94
 
178
95
  serverProcess.unref();
@@ -10,6 +10,14 @@ import {
10
10
  XCircle,
11
11
  Minus,
12
12
  ExternalLink,
13
+ AlertCircle,
14
+ Wifi,
15
+ WifiOff,
16
+ Lock,
17
+ Gauge,
18
+ Server,
19
+ HelpCircle,
20
+ Clock,
13
21
  } from "lucide-react";
14
22
  import type { ProviderConfig } from "../../proxy/providers";
15
23
 
@@ -18,9 +26,26 @@ const KNOWN_PROVIDER_DOCS: Record<string, string> = {
18
26
  deepseek: "https://api-docs.deepseek.com/zh-cn/",
19
27
  };
20
28
 
29
+ type ErrorType =
30
+ | "timeout"
31
+ | "network_unreachable"
32
+ | "connection_refused"
33
+ | "auth_failed"
34
+ | "rate_limited"
35
+ | "server_error"
36
+ | "invalid_response"
37
+ | "unknown";
38
+
39
+ type EnhancedError = {
40
+ message: string;
41
+ type: ErrorType;
42
+ details?: string;
43
+ hint?: string;
44
+ };
45
+
21
46
  type TestResult = {
22
47
  success: boolean;
23
- error?: string;
48
+ error?: EnhancedError;
24
49
  };
25
50
 
26
51
  type NotConfigured = { notConfigured: true };
@@ -53,7 +78,37 @@ function hasSuccessField(result: TestResult | NotConfigured): result is TestResu
53
78
  return Object.prototype.hasOwnProperty.call(result, "success");
54
79
  }
55
80
 
56
- function TestStatus({ result }: { result: TestResult | NotConfigured }): JSX.Element {
81
+ function getErrorIcon(type: ErrorType): JSX.Element {
82
+ const iconProps = { className: "size-3", strokeWidth: 2 };
83
+ switch (type) {
84
+ case "timeout":
85
+ return <Clock {...iconProps} />;
86
+ case "network_unreachable":
87
+ return <WifiOff {...iconProps} />;
88
+ case "connection_refused":
89
+ return <Wifi {...iconProps} />;
90
+ case "auth_failed":
91
+ return <Lock {...iconProps} />;
92
+ case "rate_limited":
93
+ return <Gauge {...iconProps} />;
94
+ case "server_error":
95
+ return <Server {...iconProps} />;
96
+ case "invalid_response":
97
+ return <HelpCircle {...iconProps} />;
98
+ case "unknown":
99
+ return <AlertCircle {...iconProps} />;
100
+ default:
101
+ return <AlertCircle {...iconProps} />;
102
+ }
103
+ }
104
+
105
+ function TestStatus({
106
+ result,
107
+ isTesting,
108
+ }: {
109
+ result: TestResult | NotConfigured;
110
+ isTesting?: boolean;
111
+ }): JSX.Element {
57
112
  if (!hasSuccessField(result)) {
58
113
  return (
59
114
  <div className="flex items-center gap-1 text-xs text-muted-foreground">
@@ -70,10 +125,26 @@ function TestStatus({ result }: { result: TestResult | NotConfigured }): JSX.Ele
70
125
  </div>
71
126
  );
72
127
  }
128
+
129
+ const error = result.error;
130
+ const errorMessage = error?.message ?? "Connection failed";
131
+ const errorHint = error?.hint;
132
+ const errorType = error?.type ?? "unknown";
133
+
73
134
  return (
74
- <div className="flex items-center gap-1 text-xs text-red-600" title={result.error}>
75
- <XCircle className="size-3" />
76
- <span className="truncate">{result.error}</span>
135
+ <div className="flex flex-col gap-1 min-w-0">
136
+ <div
137
+ className="flex items-center gap-1 text-xs text-red-600 min-w-0"
138
+ title={error?.details ?? errorMessage}
139
+ >
140
+ {getErrorIcon(errorType)}
141
+ <span className="truncate">{errorMessage}</span>
142
+ </div>
143
+ {errorHint !== undefined && (
144
+ <div className="text-xs text-muted-foreground pl-4 truncate" title={errorHint}>
145
+ {errorHint}
146
+ </div>
147
+ )}
77
148
  </div>
78
149
  );
79
150
  }
@@ -135,7 +206,9 @@ export function ProviderCard({
135
206
  <span className="font-medium">Anthropic:</span>{" "}
136
207
  <span className="truncate">{provider.anthropicBaseUrl}</span>
137
208
  </div>
138
- {testResults && <TestStatus result={testResults.anthropic.nonStreaming} />}
209
+ {testResults && (
210
+ <TestStatus result={testResults.anthropic.nonStreaming} isTesting={isTesting} />
211
+ )}
139
212
  </div>
140
213
  )}
141
214
 
@@ -145,7 +218,9 @@ export function ProviderCard({
145
218
  <span className="font-medium">OpenAI:</span>{" "}
146
219
  <span className="truncate">{provider.openaiBaseUrl}</span>
147
220
  </div>
148
- {testResults && <TestStatus result={testResults.openai.nonStreaming} />}
221
+ {testResults && (
222
+ <TestStatus result={testResults.openai.nonStreaming} isTesting={isTesting} />
223
+ )}
149
224
  </div>
150
225
  )}
151
226
 
@@ -9,9 +9,24 @@ type ConfigPathsResponse = {
9
9
  providerConfig: string;
10
10
  };
11
11
 
12
+ type EnhancedError = {
13
+ message: string;
14
+ type:
15
+ | "timeout"
16
+ | "network_unreachable"
17
+ | "connection_refused"
18
+ | "auth_failed"
19
+ | "rate_limited"
20
+ | "server_error"
21
+ | "invalid_response"
22
+ | "unknown";
23
+ details?: string;
24
+ hint?: string;
25
+ };
26
+
12
27
  type TestResult = {
13
28
  success: boolean;
14
- error?: string;
29
+ error?: EnhancedError;
15
30
  };
16
31
 
17
32
  type NotConfigured = { notConfigured: true };
@@ -88,14 +103,23 @@ export function ProvidersPanel(): JSX.Element {
88
103
  name: string;
89
104
  apiKey: string;
90
105
  model?: string;
91
- anthropicBaseUrl?: string;
92
- openaiBaseUrl?: string;
106
+ format: "anthropic" | "openai";
107
+ baseUrl?: string;
93
108
  }): void {
94
109
  void (async () => {
110
+ // Convert baseUrl to format-specific URL
111
+ const payload = {
112
+ name: data.name,
113
+ apiKey: data.apiKey,
114
+ model: data.model,
115
+ format: data.format,
116
+ anthropicBaseUrl: data.format === "anthropic" ? data.baseUrl : undefined,
117
+ openaiBaseUrl: data.format === "openai" ? data.baseUrl : undefined,
118
+ };
95
119
  const res = await fetch("/api/providers", {
96
120
  method: "POST",
97
121
  headers: { "Content-Type": "application/json" },
98
- body: JSON.stringify(data),
122
+ body: JSON.stringify(payload),
99
123
  });
100
124
  if (!res.ok) {
101
125
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
@@ -116,15 +140,24 @@ export function ProvidersPanel(): JSX.Element {
116
140
  name: string;
117
141
  apiKey: string;
118
142
  model?: string;
119
- anthropicBaseUrl?: string;
120
- openaiBaseUrl?: string;
143
+ format: "anthropic" | "openai";
144
+ baseUrl?: string;
121
145
  }): void {
122
146
  if (!editingProvider) return;
123
147
  void (async () => {
148
+ // Convert baseUrl to format-specific URL
149
+ const payload = {
150
+ name: data.name,
151
+ apiKey: data.apiKey,
152
+ model: data.model,
153
+ format: data.format,
154
+ anthropicBaseUrl: data.format === "anthropic" ? data.baseUrl : undefined,
155
+ openaiBaseUrl: data.format === "openai" ? data.baseUrl : undefined,
156
+ };
124
157
  const res = await fetch(`/api/providers/${editingProvider.id}`, {
125
158
  method: "PUT",
126
159
  headers: { "Content-Type": "application/json" },
127
- body: JSON.stringify(data),
160
+ body: JSON.stringify(payload),
128
161
  });
129
162
  if (!res.ok) {
130
163
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions