@tonyclaw/llm-inspector 1.8.0 → 1.9.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.
Files changed (30) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{index-DH3FOgcK.js → index-BmEH5jeO.js} +18 -18
  3. package/.output/public/assets/{index-BLVa7n9b.css → index-DdJSLfxK.css} +1 -1
  4. package/.output/public/assets/{main-Beo3LJDa.js → main-GVpFMVGE.js} +1 -1
  5. package/.output/server/_ssr/{index-HkueJ4Un.mjs → index-D0d6QxPt.mjs} +85 -31
  6. package/.output/server/_ssr/index.mjs +2 -2
  7. package/.output/server/_ssr/{router-DTswxb7l.mjs → router-D9uLXa9A.mjs} +287 -67
  8. package/.output/server/{_tanstack-start-manifest_v-DhUuivt-.mjs → _tanstack-start-manifest_v-ByfnNZV_.mjs} +1 -1
  9. package/.output/server/index.mjs +26 -26
  10. package/README.md +8 -209
  11. package/package.json +1 -1
  12. package/src/components/ProxyViewerContainer.tsx +10 -1
  13. package/src/components/providers/ProviderCard.tsx +19 -15
  14. package/src/components/providers/ProviderForm.tsx +21 -0
  15. package/src/components/proxy-viewer/LogEntry.tsx +7 -0
  16. package/src/components/proxy-viewer/ResponseView.tsx +32 -6
  17. package/src/components/proxy-viewer/StreamingChunkSequence.tsx +12 -3
  18. package/src/proxy/chunkStorage.ts +4 -6
  19. package/src/proxy/formats/anthropic/schemas.ts +9 -0
  20. package/src/proxy/formats/anthropic/stream.ts +11 -0
  21. package/src/proxy/formats/openai/stream.ts +15 -0
  22. package/src/proxy/handler.ts +44 -27
  23. package/src/proxy/logIndex.ts +73 -7
  24. package/src/proxy/logger.ts +62 -6
  25. package/src/proxy/providers.ts +5 -0
  26. package/src/proxy/schemas.ts +2 -0
  27. package/src/proxy/socketTracker.ts +90 -36
  28. package/src/proxy/store.ts +32 -18
  29. package/src/routes/api/providers.$providerId.ts +1 -0
  30. package/src/routes/api/providers.ts +2 -0
@@ -2,13 +2,13 @@ import { c as createRouter, a as createRootRoute, b as createFileRoute, l as laz
2
2
  import { j as jsxRuntimeExports } from "../_libs/react.mjs";
3
3
  import { S as SWRConfig } from "../_libs/swr.mjs";
4
4
  import { mkdirSync, writeFileSync, renameSync, copyFileSync, unlinkSync, existsSync, readFileSync } from "node:fs";
5
- import path, { join, dirname, isAbsolute } from "node:path";
6
- import { readFile, mkdir, writeFile, appendFile } from "node:fs/promises";
5
+ import path, { join, isAbsolute, dirname } from "node:path";
6
+ import { mkdir, appendFile, readFile, writeFile, readdir, stat, unlink } from "node:fs/promises";
7
7
  import { C as Conf } from "../_libs/conf.mjs";
8
8
  import { randomUUID } from "crypto";
9
9
  import { exec } from "node:child_process";
10
10
  import { promisify } from "node:util";
11
- import { o as object, _ as _enum, s as string, u as union, a as array, n as number, d as discriminatedUnion, l as literal, b as boolean, r as record, c as lazy, e as _null, f as unknown } from "../_libs/zod.mjs";
11
+ import { o as object, s as string, _ as _enum, u as union, a as array, n as number, d as discriminatedUnion, l as literal, b as boolean, r as record, c as lazy, e as _null, f as unknown } from "../_libs/zod.mjs";
12
12
  import "../_libs/tiny-warning.mjs";
13
13
  import "../_libs/tanstack__router-core.mjs";
14
14
  import "../_libs/cookie-es.mjs";
@@ -44,7 +44,7 @@ import "../_libs/debounce-fn.mjs";
44
44
  import "../_libs/mimic-function.mjs";
45
45
  import "../_libs/semver.mjs";
46
46
  import "../_libs/uint8array-extras.mjs";
47
- const appCss = "/assets/index-BLVa7n9b.css";
47
+ const appCss = "/assets/index-DdJSLfxK.css";
48
48
  const Route$g = createRootRoute({
49
49
  head: () => ({
50
50
  meta: [
@@ -68,12 +68,13 @@ function RootDocument({ children }) {
68
68
  ] })
69
69
  ] });
70
70
  }
71
- const $$splitComponentImporter = () => import("./index-HkueJ4Un.mjs");
71
+ const $$splitComponentImporter = () => import("./index-D0d6QxPt.mjs");
72
72
  const Route$f = createFileRoute("/")({
73
73
  component: lazyRouteComponent($$splitComponentImporter, "component")
74
74
  });
75
75
  const LOG_DIR_ENV = process.env["LOG_DIR"];
76
- Number(process.env["LOG_RETENTION_DAYS"] ?? "7");
76
+ const RETENTION_DAYS = Number(process.env["LOG_RETENTION_DAYS"] ?? "7");
77
+ const LOG_FILE_ENV = process.env["LLM_INSPECTOR_LOG_FILE"];
77
78
  function getUserDataDir() {
78
79
  if (process.platform === "win32") {
79
80
  return process.env["APPDATA"] ?? path.join(process.env["USERPROFILE"] ?? "C:\\", ".llm-inspector");
@@ -98,10 +99,72 @@ function getLogFilePath() {
98
99
  const dd = String(date.getUTCDate()).padStart(2, "0");
99
100
  return path.join(resolveLogDir(), `${yyyy}-${mm}-${dd}.jsonl`);
100
101
  }
102
+ function getInspectorLogPath() {
103
+ if (LOG_FILE_ENV !== void 0) {
104
+ return LOG_FILE_ENV;
105
+ }
106
+ const base = getUserDataDir();
107
+ return path.join(base, ".llm-inspector", "logs", "inspector.log");
108
+ }
109
+ async function initLogger() {
110
+ const dir = resolveLogDir();
111
+ const retentionMs = RETENTION_DAYS * 24 * 60 * 60 * 1e3;
112
+ const cutoff = Date.now() - retentionMs;
113
+ try {
114
+ const entries = await readdir(dir);
115
+ for (const entry of entries) {
116
+ if (!entry.endsWith(".jsonl")) continue;
117
+ const fullPath = path.join(dir, entry);
118
+ try {
119
+ const s = await stat(fullPath);
120
+ if (s.mtimeMs < cutoff) {
121
+ await unlink(fullPath);
122
+ }
123
+ } catch {
124
+ }
125
+ }
126
+ } catch (err) {
127
+ console.error("[logger] Failed to initialize log directory:", err);
128
+ }
129
+ }
130
+ async function writeAppLog(message) {
131
+ try {
132
+ const logPath = getInspectorLogPath();
133
+ const logDirPath = path.dirname(logPath);
134
+ await mkdir(logDirPath, { recursive: true });
135
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
136
+ await appendFile(logPath, `[${timestamp}] ${message}
137
+ `, "utf-8");
138
+ } catch (err) {
139
+ console.error(`[logger] Failed to write to ${getInspectorLogPath()}:`, err);
140
+ }
141
+ }
142
+ const logger = {
143
+ debug(message, ...args) {
144
+ const msg = args.length > 0 ? `${message} ${args.map(String).join(" ")}` : message;
145
+ void writeAppLog(`[DEBUG] ${msg}`);
146
+ },
147
+ info(message, ...args) {
148
+ const msg = args.length > 0 ? `${message} ${args.map(String).join(" ")}` : message;
149
+ void writeAppLog(`[INFO] ${msg}`);
150
+ },
151
+ warn(message, ...args) {
152
+ const msg = args.length > 0 ? `${message} ${args.map(String).join(" ")}` : message;
153
+ void writeAppLog(`[WARN] ${msg}`);
154
+ },
155
+ error(message, ...args) {
156
+ const msg = args.length > 0 ? `${message} ${args.map(String).join(" ")}` : message;
157
+ void writeAppLog(`[ERROR] ${msg}`);
158
+ }
159
+ };
160
+ const MAX_BUFFER_SIZE = 1e3;
101
161
  let writeBuffer = [];
102
162
  let writeScheduled = false;
163
+ let isFlushing = false;
103
164
  async function flushWriteBuffer() {
104
165
  if (writeBuffer.length === 0) return;
166
+ if (isFlushing) return;
167
+ isFlushing = true;
105
168
  const toWrite = writeBuffer.join("");
106
169
  writeBuffer = [];
107
170
  try {
@@ -109,7 +172,9 @@ async function flushWriteBuffer() {
109
172
  await mkdir(path.dirname(filePath), { recursive: true });
110
173
  await appendFile(filePath, toWrite, "utf-8");
111
174
  } catch (err) {
112
- console.error("[logger] Failed to write log entries:", err);
175
+ console.error("[logger] Failed to flush write buffer:", err);
176
+ } finally {
177
+ isFlushing = false;
113
178
  }
114
179
  }
115
180
  function scheduleFlush() {
@@ -123,7 +188,11 @@ function scheduleFlush() {
123
188
  function appendLogEntry(entry) {
124
189
  const line = JSON.stringify(entry) + "\n";
125
190
  writeBuffer.push(line);
126
- scheduleFlush();
191
+ if (writeBuffer.length >= MAX_BUFFER_SIZE) {
192
+ void flushWriteBuffer();
193
+ } else {
194
+ scheduleFlush();
195
+ }
127
196
  }
128
197
  const INDEX_VERSION = 1;
129
198
  const INDEX_FILE = "logs.idx";
@@ -162,7 +231,7 @@ async function loadIndex() {
162
231
  }
163
232
  return cachedIndex;
164
233
  } catch (err) {
165
- console.error("[logIndex] Failed to load index:", err);
234
+ logger.error("[logIndex] Failed to load index:", String(err));
166
235
  cachedIndex = createEmptyIndex();
167
236
  return cachedIndex;
168
237
  }
@@ -177,7 +246,7 @@ async function saveIndex(index) {
177
246
  try {
178
247
  await writeFile(indexPath, JSON.stringify(index), "utf-8");
179
248
  } catch (err) {
180
- console.error("[logIndex] Failed to save index:", err);
249
+ logger.error("[logIndex] Failed to save index:", String(err));
181
250
  }
182
251
  }
183
252
  async function addToIndex(id, file, lineStart, lineEnd) {
@@ -186,15 +255,58 @@ async function addToIndex(id, file, lineStart, lineEnd) {
186
255
  if (id > index.maxId) {
187
256
  index.maxId = id;
188
257
  }
258
+ scheduleIndexFlush();
259
+ }
260
+ let indexFlushScheduled = false;
261
+ async function flushIndexAsync() {
262
+ indexFlushScheduled = false;
263
+ const index = await loadIndex();
189
264
  await saveIndex(index);
190
265
  }
266
+ function scheduleIndexFlush() {
267
+ if (indexFlushScheduled) return;
268
+ indexFlushScheduled = true;
269
+ setTimeout(() => {
270
+ void flushIndexAsync();
271
+ }, 1e3);
272
+ }
191
273
  async function findInIndex(id) {
192
274
  const index = await loadIndex();
193
275
  return index.entries[id] ?? null;
194
276
  }
277
+ let idGenerationPromise = null;
278
+ let releaseLock = null;
279
+ async function acquireLock() {
280
+ if (releaseLock === null) {
281
+ idGenerationPromise = new Promise((resolve) => {
282
+ releaseLock = resolve;
283
+ });
284
+ } else {
285
+ await idGenerationPromise;
286
+ idGenerationPromise = new Promise((resolve) => {
287
+ releaseLock = resolve;
288
+ });
289
+ }
290
+ }
291
+ function releaseLockFn() {
292
+ if (releaseLock) {
293
+ const resolve = releaseLock;
294
+ releaseLock = null;
295
+ idGenerationPromise = null;
296
+ resolve();
297
+ }
298
+ }
195
299
  async function getNextLogId() {
196
- const index = await loadIndex();
197
- return index.maxId + 1;
300
+ await acquireLock();
301
+ try {
302
+ const index = await loadIndex();
303
+ const nextId = index.maxId + 1;
304
+ index.maxId = nextId;
305
+ cachedIndex = index;
306
+ return nextId;
307
+ } finally {
308
+ releaseLockFn();
309
+ }
198
310
  }
199
311
  function getCurrentLogFile() {
200
312
  const now = /* @__PURE__ */ new Date();
@@ -375,6 +487,13 @@ const SseMessageStopEvent = object({
375
487
  const SsePingEvent = object({
376
488
  type: literal("ping")
377
489
  });
490
+ const SseErrorEvent = object({
491
+ type: literal("error"),
492
+ error: object({
493
+ type: string(),
494
+ message: string()
495
+ })
496
+ });
378
497
  const SseEventSchema = discriminatedUnion("type", [
379
498
  SseMessageStartEvent,
380
499
  SseContentBlockStartEvent,
@@ -382,7 +501,8 @@ const SseEventSchema = discriminatedUnion("type", [
382
501
  SseContentBlockStopEvent,
383
502
  SseMessageDeltaEvent,
384
503
  SseMessageStopEvent,
385
- SsePingEvent
504
+ SsePingEvent,
505
+ SseErrorEvent
386
506
  ]);
387
507
  const InspectorRequestSchema = AnthropicRequestSchema;
388
508
  const InspectorResponseSchema = AnthropicResponseSchema$1;
@@ -541,7 +661,9 @@ const CapturedLogSchema = object({
541
661
  clientCwd: string().nullable().optional(),
542
662
  clientProjectFolder: string().nullable().optional(),
543
663
  streamingChunks: StreamingChunksArraySchema.optional(),
544
- streamingChunksPath: string().nullable().optional()
664
+ streamingChunksPath: string().nullable().optional(),
665
+ /** Error message from streaming response (e.g., SSE error event) */
666
+ error: string().nullable().optional()
545
667
  });
546
668
  const RequestModelSchema = object({
547
669
  model: string()
@@ -601,13 +723,13 @@ function writeChunks(logId, chunks, truncated) {
601
723
  try {
602
724
  mkdirSync(dir, { recursive: true });
603
725
  } catch (err) {
604
- console.error("[chunkStorage] Failed to create chunks directory:", err);
726
+ logger.error("[chunkStorage] Failed to create chunks directory:", String(err));
605
727
  }
606
728
  const data = { chunks, truncated };
607
729
  try {
608
730
  writeFileSync(tempPath, JSON.stringify(data), "utf-8");
609
731
  } catch (err) {
610
- console.error("[chunkStorage] Failed to write chunks temp file:", err);
732
+ logger.error("[chunkStorage] Failed to write chunks temp file:", String(err));
611
733
  return targetPath;
612
734
  }
613
735
  try {
@@ -618,7 +740,7 @@ function writeChunks(logId, chunks, truncated) {
618
740
  copyFileSync(tempPath, targetPath);
619
741
  unlinkSync(tempPath);
620
742
  } catch (copyErr) {
621
- console.error("[chunkStorage] Failed to copy chunks file:", copyErr);
743
+ logger.error("[chunkStorage] Failed to copy chunks file:", String(copyErr));
622
744
  }
623
745
  }
624
746
  return targetPath;
@@ -791,13 +913,12 @@ async function getLogById(id) {
791
913
  }
792
914
  if (lastMatch !== null) return lastMatch;
793
915
  } catch (err) {
794
- console.error("[store] Failed to read log from disk:", err);
916
+ logger.error("[store] Failed to read log from disk:", String(err));
795
917
  }
796
918
  return null;
797
919
  }
798
920
  function getFilteredLogs(sessionId, model) {
799
- const cachedLogs = Array.from(memoryCache.values()).sort((a, b) => a.id - b.id);
800
- return cachedLogs.filter((l) => {
921
+ return [...memoryCache.values()].filter((l) => {
801
922
  if (sessionId !== void 0 && l.sessionId !== sessionId) return false;
802
923
  if (model !== void 0 && l.model !== model) return false;
803
924
  return true;
@@ -834,13 +955,17 @@ function onLogUpdate(handler) {
834
955
  };
835
956
  }
836
957
  function emitLogUpdate(log) {
958
+ const failedHandlers = [];
837
959
  for (const handler of sseHandlers) {
838
960
  try {
839
961
  handler(log);
840
962
  } catch {
841
- sseHandlers.delete(handler);
963
+ failedHandlers.push(handler);
842
964
  }
843
965
  }
966
+ for (const handler of failedHandlers) {
967
+ sseHandlers.delete(handler);
968
+ }
844
969
  }
845
970
  const DEFAULT_UPSTREAM$1 = "https://api.anthropic.com";
846
971
  const DEFAULT_OPENAI_UPSTREAM$1 = "https://api.openai.com/v1";
@@ -1009,6 +1134,15 @@ function extractAnthropicStream(raw, log, fallbackModel, collectChunks) {
1009
1134
  stopSequence = data.delta.stop_sequence ?? null;
1010
1135
  outputTokens = data.usage.output_tokens;
1011
1136
  log.outputTokens = outputTokens;
1137
+ if (data.usage.cache_creation_input_tokens !== void 0) {
1138
+ log.cacheCreationInputTokens = data.usage.cache_creation_input_tokens;
1139
+ }
1140
+ if (data.usage.cache_read_input_tokens !== void 0) {
1141
+ log.cacheReadInputTokens = data.usage.cache_read_input_tokens;
1142
+ }
1143
+ break;
1144
+ case "error":
1145
+ log.error = data.error.message;
1012
1146
  break;
1013
1147
  case "content_block_stop":
1014
1148
  case "message_stop":
@@ -1185,6 +1319,16 @@ function extractOpenAIStream(raw, log, fallbackModel, collectChunks = true) {
1185
1319
  if (dataStr === "[DONE]") break;
1186
1320
  try {
1187
1321
  const parsed = JSON.parse(dataStr);
1322
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1323
+ const errorDesc = Object.getOwnPropertyDescriptor(parsed, "error");
1324
+ if (errorDesc !== void 0 && typeof errorDesc.value === "object" && errorDesc.value !== null) {
1325
+ const msgDesc = Object.getOwnPropertyDescriptor(errorDesc.value, "message");
1326
+ if (msgDesc !== void 0 && typeof msgDesc.value === "string") {
1327
+ log.error = msgDesc.value;
1328
+ }
1329
+ continue;
1330
+ }
1331
+ }
1188
1332
  const chunkResult = OpenAISSERawChunkSchema.safeParse(parsed);
1189
1333
  if (!chunkResult.success) continue;
1190
1334
  const chunk = chunkResult.data;
@@ -1395,6 +1539,8 @@ const ProviderConfigSchema = object({
1395
1539
  openaiBaseUrl: string().optional(),
1396
1540
  /** Auth header to use: "bearer" (default) or "x-api-key" */
1397
1541
  authHeader: _enum(["bearer", "x-api-key"]).optional().default("bearer"),
1542
+ /** API documentation URL */
1543
+ apiDocsUrl: string().optional(),
1398
1544
  createdAt: string(),
1399
1545
  updatedAt: string()
1400
1546
  });
@@ -1473,7 +1619,7 @@ function getProvider(id) {
1473
1619
  function normalizeApiKey(apiKey) {
1474
1620
  return apiKey.replace(/^Bearer\s+/i, "").trim();
1475
1621
  }
1476
- function addProvider(name, apiKey, format, baseUrl, model, authHeader) {
1622
+ function addProvider(name, apiKey, format, baseUrl, model, authHeader, apiDocsUrl) {
1477
1623
  const providers = getProviders();
1478
1624
  const now = (/* @__PURE__ */ new Date()).toISOString();
1479
1625
  const newProvider = {
@@ -1484,6 +1630,7 @@ function addProvider(name, apiKey, format, baseUrl, model, authHeader) {
1484
1630
  baseUrl,
1485
1631
  model,
1486
1632
  authHeader: authHeader ?? "bearer",
1633
+ apiDocsUrl,
1487
1634
  createdAt: now,
1488
1635
  updatedAt: now,
1489
1636
  anthropicBaseUrl: format === "anthropic" && baseUrl !== void 0 ? baseUrl : "",
@@ -1505,6 +1652,7 @@ function updateProvider(id, updates) {
1505
1652
  format: updates.format ?? existing.format,
1506
1653
  baseUrl: updates.baseUrl !== void 0 ? updates.baseUrl : existing.baseUrl,
1507
1654
  authHeader: updates.authHeader ?? existing.authHeader,
1655
+ apiDocsUrl: updates.apiDocsUrl ?? existing.apiDocsUrl,
1508
1656
  createdAt: existing.createdAt,
1509
1657
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1510
1658
  // Handle format-specific URLs
@@ -1636,45 +1784,99 @@ function extractRemotePort(request) {
1636
1784
  return null;
1637
1785
  }
1638
1786
  async function lookupPidByPort(port) {
1787
+ const platform = process.platform;
1639
1788
  try {
1640
- const { stdout } = await execAsync(`netstat -aon | findstr :${port} | findstr ESTABLISHED`, {
1641
- windowsHide: true
1642
- });
1643
- const lines = stdout.trim().split("\n").filter(Boolean);
1644
- for (const line of lines) {
1645
- const parts = line.trim().split(/\s+/);
1646
- const lastPart = parts[parts.length - 1];
1647
- if (lastPart !== void 0) {
1648
- const pid = parseInt(lastPart, 10);
1649
- if (!isNaN(pid) && pid > 0) return pid;
1789
+ if (platform === "win32") {
1790
+ const { stdout } = await execAsync(
1791
+ `powershell -NoProfile -Command "Get-NetTCPConnection -LocalPort ${port} -State Established | Select-Object -ExpandProperty OwningProcess"`,
1792
+ { windowsHide: true }
1793
+ );
1794
+ const pid = parseInt(stdout.trim(), 10);
1795
+ return !isNaN(pid) && pid > 0 ? pid : null;
1796
+ } else if (platform === "darwin") {
1797
+ const { stdout } = await execAsync(`lsof -i :${port} -sTCP:ESTABLISHED -t 2>/dev/null`, {
1798
+ shell: "/bin/sh"
1799
+ });
1800
+ const pid = parseInt(stdout.trim(), 10);
1801
+ return !isNaN(pid) && pid > 0 ? pid : null;
1802
+ } else {
1803
+ try {
1804
+ const { stdout: stdout2 } = await execAsync(
1805
+ `ss -tlnp 'sport = :${port}' 2>/dev/null | grep ESTAB | awk '{print $6}' | grep -o 'pid=[0-9]*' | cut -d= -f2`,
1806
+ { shell: "/bin/sh" }
1807
+ );
1808
+ const pid2 = parseInt(stdout2.trim(), 10);
1809
+ if (!isNaN(pid2) && pid2 > 0) return pid2;
1810
+ } catch {
1650
1811
  }
1812
+ const { stdout } = await execAsync(
1813
+ `netstat -tan 2>/dev/null | grep ':${port}' | grep ESTABLISHED | awk '{print $7}' | cut -d/ -f1`,
1814
+ { shell: "/bin/sh" }
1815
+ );
1816
+ const pid = parseInt(stdout.trim(), 10);
1817
+ return !isNaN(pid) && pid > 0 ? pid : null;
1651
1818
  }
1652
- return null;
1653
- } catch {
1819
+ } catch (err) {
1820
+ logger.debug(`[socketTracker] Failed to lookup PID for port ${port}:`, String(err));
1654
1821
  return null;
1655
1822
  }
1656
1823
  }
1657
1824
  async function lookupProcessInfo(pid) {
1825
+ const platform = process.platform;
1658
1826
  try {
1659
- const { stdout } = await execAsync(
1660
- `wmic process where processid=${pid} get commandline /value`,
1661
- { windowsHide: true }
1662
- );
1663
- const lines = stdout.trim().split("\n").filter(Boolean);
1664
- for (const line of lines) {
1665
- if (line.includes("=")) {
1666
- const commandLine = (line.split("=")[1] ?? "").trim();
1827
+ if (platform === "win32") {
1828
+ const { stdout } = await execAsync(
1829
+ `wmic process where processid=${pid} get commandline /value`,
1830
+ { windowsHide: true }
1831
+ );
1832
+ const lines = stdout.trim().split("\n").filter(Boolean);
1833
+ for (const line of lines) {
1834
+ if (line.includes("=")) {
1835
+ const commandLine = (line.split("=")[1] ?? "").trim();
1836
+ if (commandLine) {
1837
+ const exeMatch = commandLine.match(/^"([^"]+)"/) || commandLine.match(/^([^\s]+)/);
1838
+ if (exeMatch && exeMatch[1] !== void 0) {
1839
+ const exePath = exeMatch[1];
1840
+ const exeDir = exePath.substring(0, exePath.lastIndexOf("\\"));
1841
+ return {
1842
+ cwd: exeDir,
1843
+ projectFolder: exeDir.split("\\").pop() ?? null
1844
+ };
1845
+ }
1846
+ }
1847
+ }
1848
+ }
1849
+ } else {
1850
+ try {
1851
+ const { stdout } = await execAsync(`cat /proc/${pid}/cmdline 2>/dev/null | tr '\\0' ' '`, {
1852
+ shell: "/bin/sh"
1853
+ });
1854
+ const commandLine = stdout.trim();
1667
1855
  if (commandLine) {
1668
1856
  const exeMatch = commandLine.match(/^"([^"]+)"/) || commandLine.match(/^([^\s]+)/);
1669
1857
  if (exeMatch && exeMatch[1] !== void 0) {
1670
1858
  const exePath = exeMatch[1];
1671
- const exeDir = exePath.substring(0, exePath.lastIndexOf("\\"));
1859
+ const exeDir = exePath.substring(0, exePath.lastIndexOf("/"));
1672
1860
  return {
1673
1861
  cwd: exeDir,
1674
- projectFolder: exeDir.split("\\").pop() ?? null
1862
+ projectFolder: exeDir.split("/").pop() ?? null
1675
1863
  };
1676
1864
  }
1677
1865
  }
1866
+ } catch {
1867
+ }
1868
+ if (platform === "darwin") {
1869
+ const { stdout } = await execAsync(`ps -p ${pid} -o comm= 2>/dev/null`, {
1870
+ shell: "/bin/sh"
1871
+ });
1872
+ const exePath = stdout.trim();
1873
+ if (exePath) {
1874
+ const exeDir = exePath.substring(0, exePath.lastIndexOf("/"));
1875
+ return {
1876
+ cwd: exeDir,
1877
+ projectFolder: exePath.split("/").pop() ?? null
1878
+ };
1879
+ }
1678
1880
  }
1679
1881
  }
1680
1882
  return { cwd: null, projectFolder: null };
@@ -1700,6 +1902,11 @@ async function getClientInfo(request) {
1700
1902
  setCache(port, info);
1701
1903
  return info;
1702
1904
  }
1905
+ initLogger().then(() => {
1906
+ logger.info("Proxy handler initialized");
1907
+ }).catch((err) => {
1908
+ console.error("[handler] Logger initialization failed:", err);
1909
+ });
1703
1910
  function buildProxyHeaders(originalHeaders) {
1704
1911
  const rawHeaders = {};
1705
1912
  const headers = new Headers();
@@ -1747,7 +1954,8 @@ function buildFileLogEntry(log, upstreamUrl) {
1747
1954
  clientCwd: log.clientCwd,
1748
1955
  clientProjectFolder: log.clientProjectFolder,
1749
1956
  streamingChunks: log.streamingChunks,
1750
- streamingChunksPath: log.streamingChunksPath
1957
+ streamingChunksPath: log.streamingChunksPath,
1958
+ error: log.error
1751
1959
  };
1752
1960
  }
1753
1961
  function parseRequestPath(req, url) {
@@ -1840,7 +2048,7 @@ function handleStreamingResponse(upstreamRes, req, startTime, formatHandler, ups
1840
2048
  );
1841
2049
  log.streamingChunksPath = chunkPath;
1842
2050
  }
1843
- appendLogEntry({ ...buildFileLogEntry(log, upstreamUrl), error: null });
2051
+ appendLogEntry(buildFileLogEntry(log, upstreamUrl));
1844
2052
  emitLogUpdate(log);
1845
2053
  }
1846
2054
  });
@@ -1849,17 +2057,22 @@ function handleStreamingResponse(upstreamRes, req, startTime, formatHandler, ups
1849
2057
  }
1850
2058
  const loggedStream = upstreamRes.body.pipeThrough(transform);
1851
2059
  req.signal?.addEventListener("abort", () => {
1852
- if (log.responseText === null && chunks.length > 0) {
1853
- const full = chunks.join("");
2060
+ if (log.responseText === null) {
2061
+ logger.info(`[handler] Streaming client aborted: ${log.method} ${log.path}`);
1854
2062
  log.elapsedMs = Date.now() - startTime;
1855
- log.responseText = formatHandler.extractStream(full, log, log.model ?? void 0, true);
1856
- if (log.streamingChunks && log.streamingChunks.chunks.length > 0) {
1857
- const chunkPath = writeChunks(
1858
- log.id,
1859
- log.streamingChunks.chunks,
1860
- log.streamingChunks.truncated
1861
- );
1862
- log.streamingChunksPath = chunkPath;
2063
+ if (chunks.length > 0) {
2064
+ const full = chunks.join("");
2065
+ log.responseText = formatHandler.extractStream(full, log, log.model ?? void 0, true);
2066
+ if (log.streamingChunks && log.streamingChunks.chunks.length > 0) {
2067
+ const chunkPath = writeChunks(
2068
+ log.id,
2069
+ log.streamingChunks.chunks,
2070
+ log.streamingChunks.truncated
2071
+ );
2072
+ log.streamingChunksPath = chunkPath;
2073
+ }
2074
+ } else {
2075
+ log.responseText = "Client aborted";
1863
2076
  }
1864
2077
  appendLogEntry({ ...buildFileLogEntry(log, upstreamUrl), error: "Client aborted" });
1865
2078
  emitLogUpdate(log);
@@ -1881,7 +2094,7 @@ async function handleProxy(req) {
1881
2094
  requestBody = await req.text();
1882
2095
  }
1883
2096
  const model = requestBody !== null ? extractModelFromBody(requestBody) : null;
1884
- const matchedProviderConfig = findProviderByModelFromConfig(requestBody);
2097
+ const matchedProviderConfig = model !== null ? findProviderByModel(model) : null;
1885
2098
  const upstreamBase = selectUpstreamBase$1(parsed.isChatCompletions, matchedProviderConfig);
1886
2099
  const upstreamUrl = buildUpstreamUrl$1(upstreamBase, parsed.normalizedPath);
1887
2100
  const upstreamHost = getHostFromUrl$1(upstreamBase);
@@ -1891,6 +2104,7 @@ async function handleProxy(req) {
1891
2104
  injectAuthHeaders$1(upstreamHeaders, matchedProviderConfig);
1892
2105
  const provider = model !== null ? registry.findProvider(model) : null;
1893
2106
  if (model === null || provider === null) {
2107
+ logger.warn(`[handler] Unsupported provider: model=${model}`);
1894
2108
  return new Response("Forbidden: unsupported provider", { status: STATUS_FORBIDDEN });
1895
2109
  }
1896
2110
  let formatHandler;
@@ -1922,10 +2136,19 @@ async function handleProxy(req) {
1922
2136
  upstreamRes = await fetch(upstreamUrl, {
1923
2137
  method: req.method,
1924
2138
  headers: upstreamHeaders,
1925
- body: requestBody
2139
+ body: requestBody,
2140
+ signal: req.signal
1926
2141
  });
1927
2142
  } catch (err) {
1928
2143
  log.elapsedMs = Date.now() - startTime;
2144
+ if (err instanceof Error && err.name === "AbortError") {
2145
+ logger.info(`[handler] Client aborted: ${req.method} ${parsed.apiPath}`);
2146
+ log.responseStatus = 499;
2147
+ log.responseText = "Client aborted";
2148
+ appendLogEntry({ ...buildFileLogEntry(log, upstreamUrl), error: "Client aborted" });
2149
+ return new Response("Client aborted", { status: 499 });
2150
+ }
2151
+ logger.error(`[handler] Proxy error: ${req.method} ${parsed.apiPath}`, String(err));
1929
2152
  log.responseStatus = STATUS_BAD_GATEWAY;
1930
2153
  log.responseText = String(err);
1931
2154
  appendLogEntry({ ...buildFileLogEntry(log, upstreamUrl), error: String(err) });
@@ -1945,12 +2168,6 @@ async function handleProxy(req) {
1945
2168
  }
1946
2169
  return handleStreamingResponse(upstreamRes, req, startTime, formatHandler, upstreamUrl, log);
1947
2170
  }
1948
- function findProviderByModelFromConfig(requestBody) {
1949
- if (requestBody === null) return null;
1950
- const model = extractModelFromBody(requestBody);
1951
- if (model === null) return null;
1952
- return findProviderByModel(model);
1953
- }
1954
2171
  const Route$e = createFileRoute("/proxy/$")({
1955
2172
  server: {
1956
2173
  handlers: {
@@ -1976,7 +2193,8 @@ const ProviderInputSchema = object({
1976
2193
  format: _enum(["anthropic", "openai"]),
1977
2194
  baseUrl: string().min(1, "Base URL is required"),
1978
2195
  model: string().min(1, "Model is required"),
1979
- authHeader: _enum(["bearer", "x-api-key"]).optional().default("bearer")
2196
+ authHeader: _enum(["bearer", "x-api-key"]).optional().default("bearer"),
2197
+ apiDocsUrl: string().optional()
1980
2198
  });
1981
2199
  const Route$c = createFileRoute("/api/providers")({
1982
2200
  server: {
@@ -1995,7 +2213,8 @@ const Route$c = createFileRoute("/api/providers")({
1995
2213
  parsed.data.format,
1996
2214
  parsed.data.baseUrl,
1997
2215
  parsed.data.model,
1998
- parsed.data.authHeader
2216
+ parsed.data.authHeader,
2217
+ parsed.data.apiDocsUrl
1999
2218
  );
2000
2219
  return Response.json(newProvider, { status: 201 });
2001
2220
  }
@@ -2103,7 +2322,8 @@ const ProviderUpdateSchema = object({
2103
2322
  model: string().min(1, "Model is required").optional(),
2104
2323
  authHeader: _enum(["bearer", "x-api-key"]).optional(),
2105
2324
  anthropicBaseUrl: string().optional(),
2106
- openaiBaseUrl: string().optional()
2325
+ openaiBaseUrl: string().optional(),
2326
+ apiDocsUrl: string().optional()
2107
2327
  });
2108
2328
  const Route$6 = createFileRoute("/api/providers/$providerId")({
2109
2329
  server: {
@@ -1,4 +1,4 @@
1
- const tsrStartManifest = () => ({ "routes": { "__root__": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/__root.tsx", "children": ["/", "/api/health", "/api/logs", "/api/models", "/api/providers", "/api/sessions", "/proxy/$", "/api/config/paths"], "preloads": ["/assets/main-Beo3LJDa.js"], "assets": [] }, "/": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/index.tsx", "assets": [], "preloads": ["/assets/index-DH3FOgcK.js"] }, "/api/health": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/health.ts" }, "/api/logs": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.ts", "children": ["/api/logs/$id", "/api/logs/stream"] }, "/api/models": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/models.ts" }, "/api/providers": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.ts", "children": ["/api/providers/$providerId", "/api/providers/export", "/api/providers/import"] }, "/api/sessions": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/sessions.ts" }, "/proxy/$": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/proxy/$.ts" }, "/api/config/paths": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/config.paths.ts" }, "/api/logs/$id": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.ts", "children": ["/api/logs/$id/chunks", "/api/logs/$id/replay"] }, "/api/logs/stream": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.stream.ts" }, "/api/providers/$providerId": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.$providerId.ts", "children": ["/api/providers/$providerId/test"] }, "/api/providers/export": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.export.ts" }, "/api/providers/import": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.import.ts" }, "/api/logs/$id/chunks": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.chunks.ts" }, "/api/logs/$id/replay": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.replay.ts" }, "/api/providers/$providerId/test": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.$providerId.test.ts" } }, "clientEntry": "/assets/main-Beo3LJDa.js" });
1
+ const tsrStartManifest = () => ({ "routes": { "__root__": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/__root.tsx", "children": ["/", "/api/health", "/api/logs", "/api/models", "/api/providers", "/api/sessions", "/proxy/$", "/api/config/paths"], "preloads": ["/assets/main-GVpFMVGE.js"], "assets": [] }, "/": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/index.tsx", "assets": [], "preloads": ["/assets/index-BmEH5jeO.js"] }, "/api/health": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/health.ts" }, "/api/logs": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.ts", "children": ["/api/logs/$id", "/api/logs/stream"] }, "/api/models": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/models.ts" }, "/api/providers": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.ts", "children": ["/api/providers/$providerId", "/api/providers/export", "/api/providers/import"] }, "/api/sessions": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/sessions.ts" }, "/proxy/$": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/proxy/$.ts" }, "/api/config/paths": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/config.paths.ts" }, "/api/logs/$id": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.ts", "children": ["/api/logs/$id/chunks", "/api/logs/$id/replay"] }, "/api/logs/stream": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.stream.ts" }, "/api/providers/$providerId": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.$providerId.ts", "children": ["/api/providers/$providerId/test"] }, "/api/providers/export": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.export.ts" }, "/api/providers/import": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.import.ts" }, "/api/logs/$id/chunks": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.chunks.ts" }, "/api/logs/$id/replay": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.replay.ts" }, "/api/providers/$providerId/test": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.$providerId.test.ts" } }, "clientEntry": "/assets/main-GVpFMVGE.js" });
2
2
  export {
3
3
  tsrStartManifest
4
4
  };