@tonyclaw/llm-inspector 1.7.9 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/index-DdJSLfxK.css +1 -0
  3. package/.output/public/assets/index-DyKLPMPn.js +97 -0
  4. package/.output/public/assets/main-Cu0oTDfX.js +17 -0
  5. package/.output/server/_libs/dequal.mjs +27 -0
  6. package/.output/server/_libs/swr.mjs +938 -0
  7. package/.output/server/_libs/use-sync-external-store.mjs +64 -1
  8. package/.output/server/_ssr/{index-CAIDMqNv.mjs → index-COIATcfa.mjs} +186 -88
  9. package/.output/server/_ssr/index.mjs +2 -2
  10. package/.output/server/_ssr/{router-CsCLdrXq.mjs → router-CwmgKXBJ.mjs} +259 -74
  11. package/.output/server/{_tanstack-start-manifest_v-BF6ge6dS.mjs → _tanstack-start-manifest_v-C7hQOzvX.mjs} +1 -1
  12. package/.output/server/index.mjs +23 -23
  13. package/README.md +8 -209
  14. package/package.json +2 -1
  15. package/src/components/ProxyViewer.tsx +2 -0
  16. package/src/components/ProxyViewerContainer.tsx +10 -1
  17. package/src/components/providers/ProviderCard.tsx +57 -48
  18. package/src/components/providers/ProviderForm.tsx +21 -0
  19. package/src/components/providers/ProviderLogo.tsx +6 -1
  20. package/src/components/providers/ProvidersPanel.tsx +29 -34
  21. package/src/components/providers/SettingsDialog.tsx +5 -3
  22. package/src/components/proxy-viewer/LogEntry.tsx +7 -0
  23. package/src/components/proxy-viewer/ResponseView.tsx +32 -6
  24. package/src/lib/useProviders.ts +30 -0
  25. package/src/proxy/chunkStorage.ts +4 -6
  26. package/src/proxy/formats/anthropic/schemas.ts +9 -0
  27. package/src/proxy/formats/anthropic/stream.ts +11 -0
  28. package/src/proxy/formats/openai/stream.ts +15 -0
  29. package/src/proxy/handler.ts +34 -27
  30. package/src/proxy/logIndex.ts +52 -7
  31. package/src/proxy/logger.ts +60 -10
  32. package/src/proxy/providers.ts +5 -0
  33. package/src/proxy/schemas.ts +2 -0
  34. package/src/proxy/socketTracker.ts +90 -36
  35. package/src/proxy/store.ts +24 -14
  36. package/src/routes/__root.tsx +4 -1
  37. package/src/routes/api/providers.$providerId.ts +1 -0
  38. package/src/routes/api/providers.ts +2 -0
  39. package/.output/public/assets/index-B3RwBPLW.css +0 -1
  40. package/.output/public/assets/index-CB8ZIeEk.js +0 -97
  41. package/.output/public/assets/main-BrU8NdGQ.js +0 -17
@@ -1,13 +1,14 @@
1
1
  import { c as createRouter, a as createRootRoute, b as createFileRoute, l as lazyRouteComponent, O as Outlet, H as HeadContent, S as Scripts } from "../_libs/tanstack__react-router.mjs";
2
2
  import { j as jsxRuntimeExports } from "../_libs/react.mjs";
3
+ import { S as SWRConfig } from "../_libs/swr.mjs";
3
4
  import { mkdirSync, writeFileSync, renameSync, copyFileSync, unlinkSync, existsSync, readFileSync } from "node:fs";
4
- import path, { join, dirname, isAbsolute } from "node:path";
5
- import { readFile, mkdir, writeFile, appendFile } from "node:fs/promises";
5
+ import path, { join, isAbsolute, dirname } from "node:path";
6
+ import { mkdir, appendFile, readFile, writeFile } from "node:fs/promises";
6
7
  import { C as Conf } from "../_libs/conf.mjs";
7
8
  import { randomUUID } from "crypto";
8
9
  import { exec } from "node:child_process";
9
10
  import { promisify } from "node:util";
10
- 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";
11
12
  import "../_libs/tiny-warning.mjs";
12
13
  import "../_libs/tanstack__router-core.mjs";
13
14
  import "../_libs/cookie-es.mjs";
@@ -22,6 +23,8 @@ import "util";
22
23
  import "async_hooks";
23
24
  import "stream";
24
25
  import "../_libs/isbot.mjs";
26
+ import "../_libs/use-sync-external-store.mjs";
27
+ import "../_libs/dequal.mjs";
25
28
  import "node:process";
26
29
  import "node:crypto";
27
30
  import "node:assert";
@@ -41,7 +44,7 @@ import "../_libs/debounce-fn.mjs";
41
44
  import "../_libs/mimic-function.mjs";
42
45
  import "../_libs/semver.mjs";
43
46
  import "../_libs/uint8array-extras.mjs";
44
- const appCss = "/assets/index-B3RwBPLW.css";
47
+ const appCss = "/assets/index-DdJSLfxK.css";
45
48
  const Route$g = createRootRoute({
46
49
  head: () => ({
47
50
  meta: [
@@ -60,17 +63,18 @@ function RootDocument({ children }) {
60
63
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("html", { lang: "en", className: "dark", children: [
61
64
  /* @__PURE__ */ jsxRuntimeExports.jsx("head", { children: /* @__PURE__ */ jsxRuntimeExports.jsx(HeadContent, {}) }),
62
65
  /* @__PURE__ */ jsxRuntimeExports.jsxs("body", { children: [
63
- children,
66
+ /* @__PURE__ */ jsxRuntimeExports.jsx(SWRConfig, { value: { revalidateOnFocus: false, revalidateIfStale: false }, children }),
64
67
  /* @__PURE__ */ jsxRuntimeExports.jsx(Scripts, {})
65
68
  ] })
66
69
  ] });
67
70
  }
68
- const $$splitComponentImporter = () => import("./index-CAIDMqNv.mjs");
71
+ const $$splitComponentImporter = () => import("./index-COIATcfa.mjs");
69
72
  const Route$f = createFileRoute("/")({
70
73
  component: lazyRouteComponent($$splitComponentImporter, "component")
71
74
  });
72
75
  const LOG_DIR_ENV = process.env["LOG_DIR"];
73
76
  Number(process.env["LOG_RETENTION_DAYS"] ?? "7");
77
+ const LOG_FILE_ENV = process.env["LLM_INSPECTOR_LOG_FILE"];
74
78
  function getUserDataDir() {
75
79
  if (process.platform === "win32") {
76
80
  return process.env["APPDATA"] ?? path.join(process.env["USERPROFILE"] ?? "C:\\", ".llm-inspector");
@@ -95,18 +99,59 @@ function getLogFilePath() {
95
99
  const dd = String(date.getUTCDate()).padStart(2, "0");
96
100
  return path.join(resolveLogDir(), `${yyyy}-${mm}-${dd}.jsonl`);
97
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 writeAppLog(message) {
110
+ try {
111
+ const logPath = getInspectorLogPath();
112
+ const logDirPath = path.dirname(logPath);
113
+ await mkdir(logDirPath, { recursive: true });
114
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
115
+ await appendFile(logPath, `[${timestamp}] ${message}
116
+ `, "utf-8");
117
+ } catch {
118
+ }
119
+ }
120
+ const logger = {
121
+ debug(message, ...args) {
122
+ const msg = args.length > 0 ? `${message} ${args.map(String).join(" ")}` : message;
123
+ void writeAppLog(`[DEBUG] ${msg}`);
124
+ },
125
+ info(message, ...args) {
126
+ const msg = args.length > 0 ? `${message} ${args.map(String).join(" ")}` : message;
127
+ void writeAppLog(`[INFO] ${msg}`);
128
+ },
129
+ warn(message, ...args) {
130
+ const msg = args.length > 0 ? `${message} ${args.map(String).join(" ")}` : message;
131
+ void writeAppLog(`[WARN] ${msg}`);
132
+ },
133
+ error(message, ...args) {
134
+ const msg = args.length > 0 ? `${message} ${args.map(String).join(" ")}` : message;
135
+ void writeAppLog(`[ERROR] ${msg}`);
136
+ }
137
+ };
138
+ const MAX_BUFFER_SIZE = 1e3;
98
139
  let writeBuffer = [];
99
140
  let writeScheduled = false;
141
+ let isFlushing = false;
100
142
  async function flushWriteBuffer() {
101
143
  if (writeBuffer.length === 0) return;
144
+ if (isFlushing) return;
145
+ isFlushing = true;
102
146
  const toWrite = writeBuffer.join("");
103
147
  writeBuffer = [];
104
148
  try {
105
149
  const filePath = getLogFilePath();
106
150
  await mkdir(path.dirname(filePath), { recursive: true });
107
151
  await appendFile(filePath, toWrite, "utf-8");
108
- } catch (err) {
109
- console.error("[logger] Failed to write log entries:", err);
152
+ } catch {
153
+ } finally {
154
+ isFlushing = false;
110
155
  }
111
156
  }
112
157
  function scheduleFlush() {
@@ -120,7 +165,11 @@ function scheduleFlush() {
120
165
  function appendLogEntry(entry) {
121
166
  const line = JSON.stringify(entry) + "\n";
122
167
  writeBuffer.push(line);
123
- scheduleFlush();
168
+ if (writeBuffer.length >= MAX_BUFFER_SIZE) {
169
+ void flushWriteBuffer();
170
+ } else {
171
+ scheduleFlush();
172
+ }
124
173
  }
125
174
  const INDEX_VERSION = 1;
126
175
  const INDEX_FILE = "logs.idx";
@@ -159,7 +208,7 @@ async function loadIndex() {
159
208
  }
160
209
  return cachedIndex;
161
210
  } catch (err) {
162
- console.error("[logIndex] Failed to load index:", err);
211
+ logger.error("[logIndex] Failed to load index:", String(err));
163
212
  cachedIndex = createEmptyIndex();
164
213
  return cachedIndex;
165
214
  }
@@ -174,7 +223,7 @@ async function saveIndex(index) {
174
223
  try {
175
224
  await writeFile(indexPath, JSON.stringify(index), "utf-8");
176
225
  } catch (err) {
177
- console.error("[logIndex] Failed to save index:", err);
226
+ logger.error("[logIndex] Failed to save index:", String(err));
178
227
  }
179
228
  }
180
229
  async function addToIndex(id, file, lineStart, lineEnd) {
@@ -183,15 +232,40 @@ async function addToIndex(id, file, lineStart, lineEnd) {
183
232
  if (id > index.maxId) {
184
233
  index.maxId = id;
185
234
  }
235
+ scheduleIndexFlush();
236
+ }
237
+ let indexFlushScheduled = false;
238
+ async function flushIndexAsync() {
239
+ indexFlushScheduled = false;
240
+ const index = await loadIndex();
186
241
  await saveIndex(index);
187
242
  }
243
+ function scheduleIndexFlush() {
244
+ if (indexFlushScheduled) return;
245
+ indexFlushScheduled = true;
246
+ setTimeout(() => {
247
+ void flushIndexAsync();
248
+ }, 1e3);
249
+ }
188
250
  async function findInIndex(id) {
189
251
  const index = await loadIndex();
190
252
  return index.entries[id] ?? null;
191
253
  }
254
+ let idGenerationLock = false;
192
255
  async function getNextLogId() {
193
- const index = await loadIndex();
194
- return index.maxId + 1;
256
+ while (idGenerationLock) {
257
+ await new Promise((resolve) => setTimeout(resolve, 0));
258
+ }
259
+ idGenerationLock = true;
260
+ try {
261
+ const index = await loadIndex();
262
+ const nextId = index.maxId + 1;
263
+ index.maxId = nextId;
264
+ cachedIndex = index;
265
+ return nextId;
266
+ } finally {
267
+ idGenerationLock = false;
268
+ }
195
269
  }
196
270
  function getCurrentLogFile() {
197
271
  const now = /* @__PURE__ */ new Date();
@@ -372,6 +446,13 @@ const SseMessageStopEvent = object({
372
446
  const SsePingEvent = object({
373
447
  type: literal("ping")
374
448
  });
449
+ const SseErrorEvent = object({
450
+ type: literal("error"),
451
+ error: object({
452
+ type: string(),
453
+ message: string()
454
+ })
455
+ });
375
456
  const SseEventSchema = discriminatedUnion("type", [
376
457
  SseMessageStartEvent,
377
458
  SseContentBlockStartEvent,
@@ -379,7 +460,8 @@ const SseEventSchema = discriminatedUnion("type", [
379
460
  SseContentBlockStopEvent,
380
461
  SseMessageDeltaEvent,
381
462
  SseMessageStopEvent,
382
- SsePingEvent
463
+ SsePingEvent,
464
+ SseErrorEvent
383
465
  ]);
384
466
  const InspectorRequestSchema = AnthropicRequestSchema;
385
467
  const InspectorResponseSchema = AnthropicResponseSchema$1;
@@ -538,7 +620,9 @@ const CapturedLogSchema = object({
538
620
  clientCwd: string().nullable().optional(),
539
621
  clientProjectFolder: string().nullable().optional(),
540
622
  streamingChunks: StreamingChunksArraySchema.optional(),
541
- streamingChunksPath: string().nullable().optional()
623
+ streamingChunksPath: string().nullable().optional(),
624
+ /** Error message from streaming response (e.g., SSE error event) */
625
+ error: string().nullable().optional()
542
626
  });
543
627
  const RequestModelSchema = object({
544
628
  model: string()
@@ -598,13 +682,13 @@ function writeChunks(logId, chunks, truncated) {
598
682
  try {
599
683
  mkdirSync(dir, { recursive: true });
600
684
  } catch (err) {
601
- console.error("[chunkStorage] Failed to create chunks directory:", err);
685
+ logger.error("[chunkStorage] Failed to create chunks directory:", String(err));
602
686
  }
603
687
  const data = { chunks, truncated };
604
688
  try {
605
689
  writeFileSync(tempPath, JSON.stringify(data), "utf-8");
606
690
  } catch (err) {
607
- console.error("[chunkStorage] Failed to write chunks temp file:", err);
691
+ logger.error("[chunkStorage] Failed to write chunks temp file:", String(err));
608
692
  return targetPath;
609
693
  }
610
694
  try {
@@ -615,7 +699,7 @@ function writeChunks(logId, chunks, truncated) {
615
699
  copyFileSync(tempPath, targetPath);
616
700
  unlinkSync(tempPath);
617
701
  } catch (copyErr) {
618
- console.error("[chunkStorage] Failed to copy chunks file:", copyErr);
702
+ logger.error("[chunkStorage] Failed to copy chunks file:", String(copyErr));
619
703
  }
620
704
  }
621
705
  return targetPath;
@@ -788,12 +872,12 @@ async function getLogById(id) {
788
872
  }
789
873
  if (lastMatch !== null) return lastMatch;
790
874
  } catch (err) {
791
- console.error("[store] Failed to read log from disk:", err);
875
+ logger.error("[store] Failed to read log from disk:", String(err));
792
876
  }
793
877
  return null;
794
878
  }
795
879
  function getFilteredLogs(sessionId, model) {
796
- const cachedLogs = Array.from(memoryCache.values()).sort((a, b) => a.id - b.id);
880
+ const cachedLogs = Array.from(memoryCache.values());
797
881
  return cachedLogs.filter((l) => {
798
882
  if (sessionId !== void 0 && l.sessionId !== sessionId) return false;
799
883
  if (model !== void 0 && l.model !== model) return false;
@@ -1006,6 +1090,15 @@ function extractAnthropicStream(raw, log, fallbackModel, collectChunks) {
1006
1090
  stopSequence = data.delta.stop_sequence ?? null;
1007
1091
  outputTokens = data.usage.output_tokens;
1008
1092
  log.outputTokens = outputTokens;
1093
+ if (data.usage.cache_creation_input_tokens !== void 0) {
1094
+ log.cacheCreationInputTokens = data.usage.cache_creation_input_tokens;
1095
+ }
1096
+ if (data.usage.cache_read_input_tokens !== void 0) {
1097
+ log.cacheReadInputTokens = data.usage.cache_read_input_tokens;
1098
+ }
1099
+ break;
1100
+ case "error":
1101
+ log.error = data.error.message;
1009
1102
  break;
1010
1103
  case "content_block_stop":
1011
1104
  case "message_stop":
@@ -1182,6 +1275,16 @@ function extractOpenAIStream(raw, log, fallbackModel, collectChunks = true) {
1182
1275
  if (dataStr === "[DONE]") break;
1183
1276
  try {
1184
1277
  const parsed = JSON.parse(dataStr);
1278
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1279
+ const errorDesc = Object.getOwnPropertyDescriptor(parsed, "error");
1280
+ if (errorDesc !== void 0 && typeof errorDesc.value === "object" && errorDesc.value !== null) {
1281
+ const msgDesc = Object.getOwnPropertyDescriptor(errorDesc.value, "message");
1282
+ if (msgDesc !== void 0 && typeof msgDesc.value === "string") {
1283
+ log.error = msgDesc.value;
1284
+ }
1285
+ continue;
1286
+ }
1287
+ }
1185
1288
  const chunkResult = OpenAISSERawChunkSchema.safeParse(parsed);
1186
1289
  if (!chunkResult.success) continue;
1187
1290
  const chunk = chunkResult.data;
@@ -1392,6 +1495,8 @@ const ProviderConfigSchema = object({
1392
1495
  openaiBaseUrl: string().optional(),
1393
1496
  /** Auth header to use: "bearer" (default) or "x-api-key" */
1394
1497
  authHeader: _enum(["bearer", "x-api-key"]).optional().default("bearer"),
1498
+ /** API documentation URL */
1499
+ apiDocsUrl: string().optional(),
1395
1500
  createdAt: string(),
1396
1501
  updatedAt: string()
1397
1502
  });
@@ -1470,7 +1575,7 @@ function getProvider(id) {
1470
1575
  function normalizeApiKey(apiKey) {
1471
1576
  return apiKey.replace(/^Bearer\s+/i, "").trim();
1472
1577
  }
1473
- function addProvider(name, apiKey, format, baseUrl, model, authHeader) {
1578
+ function addProvider(name, apiKey, format, baseUrl, model, authHeader, apiDocsUrl) {
1474
1579
  const providers = getProviders();
1475
1580
  const now = (/* @__PURE__ */ new Date()).toISOString();
1476
1581
  const newProvider = {
@@ -1481,6 +1586,7 @@ function addProvider(name, apiKey, format, baseUrl, model, authHeader) {
1481
1586
  baseUrl,
1482
1587
  model,
1483
1588
  authHeader: authHeader ?? "bearer",
1589
+ apiDocsUrl,
1484
1590
  createdAt: now,
1485
1591
  updatedAt: now,
1486
1592
  anthropicBaseUrl: format === "anthropic" && baseUrl !== void 0 ? baseUrl : "",
@@ -1502,6 +1608,7 @@ function updateProvider(id, updates) {
1502
1608
  format: updates.format ?? existing.format,
1503
1609
  baseUrl: updates.baseUrl !== void 0 ? updates.baseUrl : existing.baseUrl,
1504
1610
  authHeader: updates.authHeader ?? existing.authHeader,
1611
+ apiDocsUrl: updates.apiDocsUrl ?? existing.apiDocsUrl,
1505
1612
  createdAt: existing.createdAt,
1506
1613
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1507
1614
  // Handle format-specific URLs
@@ -1633,45 +1740,99 @@ function extractRemotePort(request) {
1633
1740
  return null;
1634
1741
  }
1635
1742
  async function lookupPidByPort(port) {
1743
+ const platform = process.platform;
1636
1744
  try {
1637
- const { stdout } = await execAsync(`netstat -aon | findstr :${port} | findstr ESTABLISHED`, {
1638
- windowsHide: true
1639
- });
1640
- const lines = stdout.trim().split("\n").filter(Boolean);
1641
- for (const line of lines) {
1642
- const parts = line.trim().split(/\s+/);
1643
- const lastPart = parts[parts.length - 1];
1644
- if (lastPart !== void 0) {
1645
- const pid = parseInt(lastPart, 10);
1646
- if (!isNaN(pid) && pid > 0) return pid;
1745
+ if (platform === "win32") {
1746
+ const { stdout } = await execAsync(
1747
+ `powershell -NoProfile -Command "Get-NetTCPConnection -LocalPort ${port} -State Established | Select-Object -ExpandProperty OwningProcess"`,
1748
+ { windowsHide: true }
1749
+ );
1750
+ const pid = parseInt(stdout.trim(), 10);
1751
+ return !isNaN(pid) && pid > 0 ? pid : null;
1752
+ } else if (platform === "darwin") {
1753
+ const { stdout } = await execAsync(`lsof -i :${port} -sTCP:ESTABLISHED -t 2>/dev/null`, {
1754
+ shell: "/bin/sh"
1755
+ });
1756
+ const pid = parseInt(stdout.trim(), 10);
1757
+ return !isNaN(pid) && pid > 0 ? pid : null;
1758
+ } else {
1759
+ try {
1760
+ const { stdout: stdout2 } = await execAsync(
1761
+ `ss -tlnp 'sport = :${port}' 2>/dev/null | grep ESTAB | awk '{print $6}' | grep -o 'pid=[0-9]*' | cut -d= -f2`,
1762
+ { shell: "/bin/sh" }
1763
+ );
1764
+ const pid2 = parseInt(stdout2.trim(), 10);
1765
+ if (!isNaN(pid2) && pid2 > 0) return pid2;
1766
+ } catch {
1647
1767
  }
1768
+ const { stdout } = await execAsync(
1769
+ `netstat -tan 2>/dev/null | grep ':${port}' | grep ESTABLISHED | awk '{print $7}' | cut -d/ -f1`,
1770
+ { shell: "/bin/sh" }
1771
+ );
1772
+ const pid = parseInt(stdout.trim(), 10);
1773
+ return !isNaN(pid) && pid > 0 ? pid : null;
1648
1774
  }
1649
- return null;
1650
- } catch {
1775
+ } catch (err) {
1776
+ logger.debug(`[socketTracker] Failed to lookup PID for port ${port}:`, String(err));
1651
1777
  return null;
1652
1778
  }
1653
1779
  }
1654
1780
  async function lookupProcessInfo(pid) {
1781
+ const platform = process.platform;
1655
1782
  try {
1656
- const { stdout } = await execAsync(
1657
- `wmic process where processid=${pid} get commandline /value`,
1658
- { windowsHide: true }
1659
- );
1660
- const lines = stdout.trim().split("\n").filter(Boolean);
1661
- for (const line of lines) {
1662
- if (line.includes("=")) {
1663
- const commandLine = (line.split("=")[1] ?? "").trim();
1783
+ if (platform === "win32") {
1784
+ const { stdout } = await execAsync(
1785
+ `wmic process where processid=${pid} get commandline /value`,
1786
+ { windowsHide: true }
1787
+ );
1788
+ const lines = stdout.trim().split("\n").filter(Boolean);
1789
+ for (const line of lines) {
1790
+ if (line.includes("=")) {
1791
+ const commandLine = (line.split("=")[1] ?? "").trim();
1792
+ if (commandLine) {
1793
+ const exeMatch = commandLine.match(/^"([^"]+)"/) || commandLine.match(/^([^\s]+)/);
1794
+ if (exeMatch && exeMatch[1] !== void 0) {
1795
+ const exePath = exeMatch[1];
1796
+ const exeDir = exePath.substring(0, exePath.lastIndexOf("\\"));
1797
+ return {
1798
+ cwd: exeDir,
1799
+ projectFolder: exeDir.split("\\").pop() ?? null
1800
+ };
1801
+ }
1802
+ }
1803
+ }
1804
+ }
1805
+ } else {
1806
+ try {
1807
+ const { stdout } = await execAsync(`cat /proc/${pid}/cmdline 2>/dev/null | tr '\\0' ' '`, {
1808
+ shell: "/bin/sh"
1809
+ });
1810
+ const commandLine = stdout.trim();
1664
1811
  if (commandLine) {
1665
1812
  const exeMatch = commandLine.match(/^"([^"]+)"/) || commandLine.match(/^([^\s]+)/);
1666
1813
  if (exeMatch && exeMatch[1] !== void 0) {
1667
1814
  const exePath = exeMatch[1];
1668
- const exeDir = exePath.substring(0, exePath.lastIndexOf("\\"));
1815
+ const exeDir = exePath.substring(0, exePath.lastIndexOf("/"));
1669
1816
  return {
1670
1817
  cwd: exeDir,
1671
- projectFolder: exeDir.split("\\").pop() ?? null
1818
+ projectFolder: exeDir.split("/").pop() ?? null
1672
1819
  };
1673
1820
  }
1674
1821
  }
1822
+ } catch {
1823
+ }
1824
+ if (platform === "darwin") {
1825
+ const { stdout } = await execAsync(`ps -p ${pid} -o comm= 2>/dev/null`, {
1826
+ shell: "/bin/sh"
1827
+ });
1828
+ const exePath = stdout.trim();
1829
+ if (exePath) {
1830
+ const exeDir = exePath.substring(0, exePath.lastIndexOf("/"));
1831
+ return {
1832
+ cwd: exeDir,
1833
+ projectFolder: exePath.split("/").pop() ?? null
1834
+ };
1835
+ }
1675
1836
  }
1676
1837
  }
1677
1838
  return { cwd: null, projectFolder: null };
@@ -1744,7 +1905,8 @@ function buildFileLogEntry(log, upstreamUrl) {
1744
1905
  clientCwd: log.clientCwd,
1745
1906
  clientProjectFolder: log.clientProjectFolder,
1746
1907
  streamingChunks: log.streamingChunks,
1747
- streamingChunksPath: log.streamingChunksPath
1908
+ streamingChunksPath: log.streamingChunksPath,
1909
+ error: log.error
1748
1910
  };
1749
1911
  }
1750
1912
  function parseRequestPath(req, url) {
@@ -1837,7 +1999,7 @@ function handleStreamingResponse(upstreamRes, req, startTime, formatHandler, ups
1837
1999
  );
1838
2000
  log.streamingChunksPath = chunkPath;
1839
2001
  }
1840
- appendLogEntry({ ...buildFileLogEntry(log, upstreamUrl), error: null });
2002
+ appendLogEntry(buildFileLogEntry(log, upstreamUrl));
1841
2003
  emitLogUpdate(log);
1842
2004
  }
1843
2005
  });
@@ -1846,17 +2008,22 @@ function handleStreamingResponse(upstreamRes, req, startTime, formatHandler, ups
1846
2008
  }
1847
2009
  const loggedStream = upstreamRes.body.pipeThrough(transform);
1848
2010
  req.signal?.addEventListener("abort", () => {
1849
- if (log.responseText === null && chunks.length > 0) {
1850
- const full = chunks.join("");
2011
+ if (log.responseText === null) {
2012
+ logger.info(`[handler] Streaming client aborted: ${log.method} ${log.path}`);
1851
2013
  log.elapsedMs = Date.now() - startTime;
1852
- log.responseText = formatHandler.extractStream(full, log, log.model ?? void 0, true);
1853
- if (log.streamingChunks && log.streamingChunks.chunks.length > 0) {
1854
- const chunkPath = writeChunks(
1855
- log.id,
1856
- log.streamingChunks.chunks,
1857
- log.streamingChunks.truncated
1858
- );
1859
- log.streamingChunksPath = chunkPath;
2014
+ if (chunks.length > 0) {
2015
+ const full = chunks.join("");
2016
+ log.responseText = formatHandler.extractStream(full, log, log.model ?? void 0, true);
2017
+ if (log.streamingChunks && log.streamingChunks.chunks.length > 0) {
2018
+ const chunkPath = writeChunks(
2019
+ log.id,
2020
+ log.streamingChunks.chunks,
2021
+ log.streamingChunks.truncated
2022
+ );
2023
+ log.streamingChunksPath = chunkPath;
2024
+ }
2025
+ } else {
2026
+ log.responseText = "Client aborted";
1860
2027
  }
1861
2028
  appendLogEntry({ ...buildFileLogEntry(log, upstreamUrl), error: "Client aborted" });
1862
2029
  emitLogUpdate(log);
@@ -1878,7 +2045,7 @@ async function handleProxy(req) {
1878
2045
  requestBody = await req.text();
1879
2046
  }
1880
2047
  const model = requestBody !== null ? extractModelFromBody(requestBody) : null;
1881
- const matchedProviderConfig = findProviderByModelFromConfig(requestBody);
2048
+ const matchedProviderConfig = model !== null ? findProviderByModel(model) : null;
1882
2049
  const upstreamBase = selectUpstreamBase$1(parsed.isChatCompletions, matchedProviderConfig);
1883
2050
  const upstreamUrl = buildUpstreamUrl$1(upstreamBase, parsed.normalizedPath);
1884
2051
  const upstreamHost = getHostFromUrl$1(upstreamBase);
@@ -1888,6 +2055,7 @@ async function handleProxy(req) {
1888
2055
  injectAuthHeaders$1(upstreamHeaders, matchedProviderConfig);
1889
2056
  const provider = model !== null ? registry.findProvider(model) : null;
1890
2057
  if (model === null || provider === null) {
2058
+ logger.warn(`[handler] Unsupported provider: model=${model}`);
1891
2059
  return new Response("Forbidden: unsupported provider", { status: STATUS_FORBIDDEN });
1892
2060
  }
1893
2061
  let formatHandler;
@@ -1919,10 +2087,19 @@ async function handleProxy(req) {
1919
2087
  upstreamRes = await fetch(upstreamUrl, {
1920
2088
  method: req.method,
1921
2089
  headers: upstreamHeaders,
1922
- body: requestBody
2090
+ body: requestBody,
2091
+ signal: req.signal
1923
2092
  });
1924
2093
  } catch (err) {
1925
2094
  log.elapsedMs = Date.now() - startTime;
2095
+ if (err instanceof Error && err.name === "AbortError") {
2096
+ logger.info(`[handler] Client aborted: ${req.method} ${parsed.apiPath}`);
2097
+ log.responseStatus = 499;
2098
+ log.responseText = "Client aborted";
2099
+ appendLogEntry({ ...buildFileLogEntry(log, upstreamUrl), error: "Client aborted" });
2100
+ return new Response("Client aborted", { status: 499 });
2101
+ }
2102
+ logger.error(`[handler] Proxy error: ${req.method} ${parsed.apiPath}`, String(err));
1926
2103
  log.responseStatus = STATUS_BAD_GATEWAY;
1927
2104
  log.responseText = String(err);
1928
2105
  appendLogEntry({ ...buildFileLogEntry(log, upstreamUrl), error: String(err) });
@@ -1942,12 +2119,6 @@ async function handleProxy(req) {
1942
2119
  }
1943
2120
  return handleStreamingResponse(upstreamRes, req, startTime, formatHandler, upstreamUrl, log);
1944
2121
  }
1945
- function findProviderByModelFromConfig(requestBody) {
1946
- if (requestBody === null) return null;
1947
- const model = extractModelFromBody(requestBody);
1948
- if (model === null) return null;
1949
- return findProviderByModel(model);
1950
- }
1951
2122
  const Route$e = createFileRoute("/proxy/$")({
1952
2123
  server: {
1953
2124
  handlers: {
@@ -1973,7 +2144,8 @@ const ProviderInputSchema = object({
1973
2144
  format: _enum(["anthropic", "openai"]),
1974
2145
  baseUrl: string().min(1, "Base URL is required"),
1975
2146
  model: string().min(1, "Model is required"),
1976
- authHeader: _enum(["bearer", "x-api-key"]).optional().default("bearer")
2147
+ authHeader: _enum(["bearer", "x-api-key"]).optional().default("bearer"),
2148
+ apiDocsUrl: string().optional()
1977
2149
  });
1978
2150
  const Route$c = createFileRoute("/api/providers")({
1979
2151
  server: {
@@ -1992,7 +2164,8 @@ const Route$c = createFileRoute("/api/providers")({
1992
2164
  parsed.data.format,
1993
2165
  parsed.data.baseUrl,
1994
2166
  parsed.data.model,
1995
- parsed.data.authHeader
2167
+ parsed.data.authHeader,
2168
+ parsed.data.apiDocsUrl
1996
2169
  );
1997
2170
  return Response.json(newProvider, { status: 201 });
1998
2171
  }
@@ -2100,7 +2273,8 @@ const ProviderUpdateSchema = object({
2100
2273
  model: string().min(1, "Model is required").optional(),
2101
2274
  authHeader: _enum(["bearer", "x-api-key"]).optional(),
2102
2275
  anthropicBaseUrl: string().optional(),
2103
- openaiBaseUrl: string().optional()
2276
+ openaiBaseUrl: string().optional(),
2277
+ apiDocsUrl: string().optional()
2104
2278
  });
2105
2279
  const Route$6 = createFileRoute("/api/providers/$providerId")({
2106
2280
  server: {
@@ -2306,7 +2480,7 @@ function truncateErrorDetails(details) {
2306
2480
  }
2307
2481
  return details.slice(0, MAX_ERROR_DETAILS_LENGTH) + "...";
2308
2482
  }
2309
- function createErrorResult(error, latencyMs, streaming, responseStatus) {
2483
+ function createErrorResult(error, latencyMs, streaming, responseStatus, requestHeaders) {
2310
2484
  const { type, details } = classifyError(error, responseStatus);
2311
2485
  return {
2312
2486
  success: false,
@@ -2317,7 +2491,8 @@ function createErrorResult(error, latencyMs, streaming, responseStatus) {
2317
2491
  hint: ERROR_HINTS[type]
2318
2492
  },
2319
2493
  latencyMs,
2320
- streaming
2494
+ streaming,
2495
+ requestHeaders
2321
2496
  };
2322
2497
  }
2323
2498
  function getErrorMessage(type) {
@@ -2394,7 +2569,8 @@ async function testEndpoint(baseUrl, apiKey, path2, model, isOpenAI) {
2394
2569
  `HTTP ${response.status}: ${response.statusText}`,
2395
2570
  latencyMs,
2396
2571
  false,
2397
- response.status
2572
+ response.status,
2573
+ requestHeaders
2398
2574
  );
2399
2575
  }
2400
2576
  const responseText = await response.text();
@@ -2451,7 +2627,7 @@ async function testEndpoint(baseUrl, apiKey, path2, model, isOpenAI) {
2451
2627
  }
2452
2628
  } catch (err) {
2453
2629
  clearTimeout(timeoutId);
2454
- return createErrorResult(err, Date.now() - startTime, false);
2630
+ return createErrorResult(err, Date.now() - startTime, false, void 0, requestHeaders);
2455
2631
  }
2456
2632
  }
2457
2633
  async function testStreamingEndpoint(baseUrl, apiKey, path2, model, isOpenAI) {
@@ -2483,7 +2659,8 @@ async function testStreamingEndpoint(baseUrl, apiKey, path2, model, isOpenAI) {
2483
2659
  `HTTP ${response.status}: ${response.statusText}`,
2484
2660
  latencyMs,
2485
2661
  true,
2486
- response.status
2662
+ response.status,
2663
+ requestHeaders
2487
2664
  );
2488
2665
  }
2489
2666
  const chunks = [];
@@ -2511,7 +2688,9 @@ async function testStreamingEndpoint(baseUrl, apiKey, path2, model, isOpenAI) {
2511
2688
  return createErrorResult(
2512
2689
  "Response too large (exceeded 10MB limit)",
2513
2690
  Date.now() - startTime,
2514
- true
2691
+ true,
2692
+ void 0,
2693
+ requestHeaders
2515
2694
  );
2516
2695
  }
2517
2696
  const decoded = decoder.decode(value, { stream: true });
@@ -2523,7 +2702,13 @@ async function testStreamingEndpoint(baseUrl, apiKey, path2, model, isOpenAI) {
2523
2702
  chunks.push(finalChunk);
2524
2703
  }
2525
2704
  } catch (readErr) {
2526
- return createErrorResult(`Stream read error: ${readErr}`, latencyMs, true);
2705
+ return createErrorResult(
2706
+ `Stream read error: ${readErr}`,
2707
+ latencyMs,
2708
+ true,
2709
+ void 0,
2710
+ requestHeaders
2711
+ );
2527
2712
  }
2528
2713
  const fullResponse = chunks.join("");
2529
2714
  const mockLog = {
@@ -2607,7 +2792,7 @@ async function testStreamingEndpoint(baseUrl, apiKey, path2, model, isOpenAI) {
2607
2792
  }
2608
2793
  } catch (err) {
2609
2794
  clearTimeout(timeoutId);
2610
- return createErrorResult(err, Date.now() - startTime, true);
2795
+ return createErrorResult(err, Date.now() - startTime, true, void 0, requestHeaders);
2611
2796
  }
2612
2797
  }
2613
2798
  function createTestLogEntry(providerName, path2, body, upstreamUrl, result, isTest) {