@tonyclaw/llm-inspector 1.8.0 → 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 (29) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{index-BLVa7n9b.css → index-DdJSLfxK.css} +1 -1
  3. package/.output/public/assets/{index-DH3FOgcK.js → index-DyKLPMPn.js} +18 -18
  4. package/.output/public/assets/{main-Beo3LJDa.js → main-Cu0oTDfX.js} +1 -1
  5. package/.output/server/_ssr/{index-HkueJ4Un.mjs → index-COIATcfa.mjs} +74 -28
  6. package/.output/server/_ssr/index.mjs +2 -2
  7. package/.output/server/_ssr/{router-DTswxb7l.mjs → router-CwmgKXBJ.mjs} +236 -65
  8. package/.output/server/{_tanstack-start-manifest_v-DhUuivt-.mjs → _tanstack-start-manifest_v-C7hQOzvX.mjs} +1 -1
  9. package/.output/server/index.mjs +23 -23
  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/proxy/chunkStorage.ts +4 -6
  18. package/src/proxy/formats/anthropic/schemas.ts +9 -0
  19. package/src/proxy/formats/anthropic/stream.ts +11 -0
  20. package/src/proxy/formats/openai/stream.ts +15 -0
  21. package/src/proxy/handler.ts +34 -27
  22. package/src/proxy/logIndex.ts +52 -7
  23. package/src/proxy/logger.ts +60 -10
  24. package/src/proxy/providers.ts +5 -0
  25. package/src/proxy/schemas.ts +2 -0
  26. package/src/proxy/socketTracker.ts +90 -36
  27. package/src/proxy/store.ts +24 -14
  28. package/src/routes/api/providers.$providerId.ts +1 -0
  29. 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 } 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-COIATcfa.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
76
  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,18 +99,59 @@ 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 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;
101
139
  let writeBuffer = [];
102
140
  let writeScheduled = false;
141
+ let isFlushing = false;
103
142
  async function flushWriteBuffer() {
104
143
  if (writeBuffer.length === 0) return;
144
+ if (isFlushing) return;
145
+ isFlushing = true;
105
146
  const toWrite = writeBuffer.join("");
106
147
  writeBuffer = [];
107
148
  try {
108
149
  const filePath = getLogFilePath();
109
150
  await mkdir(path.dirname(filePath), { recursive: true });
110
151
  await appendFile(filePath, toWrite, "utf-8");
111
- } catch (err) {
112
- console.error("[logger] Failed to write log entries:", err);
152
+ } catch {
153
+ } finally {
154
+ isFlushing = false;
113
155
  }
114
156
  }
115
157
  function scheduleFlush() {
@@ -123,7 +165,11 @@ function scheduleFlush() {
123
165
  function appendLogEntry(entry) {
124
166
  const line = JSON.stringify(entry) + "\n";
125
167
  writeBuffer.push(line);
126
- scheduleFlush();
168
+ if (writeBuffer.length >= MAX_BUFFER_SIZE) {
169
+ void flushWriteBuffer();
170
+ } else {
171
+ scheduleFlush();
172
+ }
127
173
  }
128
174
  const INDEX_VERSION = 1;
129
175
  const INDEX_FILE = "logs.idx";
@@ -162,7 +208,7 @@ async function loadIndex() {
162
208
  }
163
209
  return cachedIndex;
164
210
  } catch (err) {
165
- console.error("[logIndex] Failed to load index:", err);
211
+ logger.error("[logIndex] Failed to load index:", String(err));
166
212
  cachedIndex = createEmptyIndex();
167
213
  return cachedIndex;
168
214
  }
@@ -177,7 +223,7 @@ async function saveIndex(index) {
177
223
  try {
178
224
  await writeFile(indexPath, JSON.stringify(index), "utf-8");
179
225
  } catch (err) {
180
- console.error("[logIndex] Failed to save index:", err);
226
+ logger.error("[logIndex] Failed to save index:", String(err));
181
227
  }
182
228
  }
183
229
  async function addToIndex(id, file, lineStart, lineEnd) {
@@ -186,15 +232,40 @@ async function addToIndex(id, file, lineStart, lineEnd) {
186
232
  if (id > index.maxId) {
187
233
  index.maxId = id;
188
234
  }
235
+ scheduleIndexFlush();
236
+ }
237
+ let indexFlushScheduled = false;
238
+ async function flushIndexAsync() {
239
+ indexFlushScheduled = false;
240
+ const index = await loadIndex();
189
241
  await saveIndex(index);
190
242
  }
243
+ function scheduleIndexFlush() {
244
+ if (indexFlushScheduled) return;
245
+ indexFlushScheduled = true;
246
+ setTimeout(() => {
247
+ void flushIndexAsync();
248
+ }, 1e3);
249
+ }
191
250
  async function findInIndex(id) {
192
251
  const index = await loadIndex();
193
252
  return index.entries[id] ?? null;
194
253
  }
254
+ let idGenerationLock = false;
195
255
  async function getNextLogId() {
196
- const index = await loadIndex();
197
- 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
+ }
198
269
  }
199
270
  function getCurrentLogFile() {
200
271
  const now = /* @__PURE__ */ new Date();
@@ -375,6 +446,13 @@ const SseMessageStopEvent = object({
375
446
  const SsePingEvent = object({
376
447
  type: literal("ping")
377
448
  });
449
+ const SseErrorEvent = object({
450
+ type: literal("error"),
451
+ error: object({
452
+ type: string(),
453
+ message: string()
454
+ })
455
+ });
378
456
  const SseEventSchema = discriminatedUnion("type", [
379
457
  SseMessageStartEvent,
380
458
  SseContentBlockStartEvent,
@@ -382,7 +460,8 @@ const SseEventSchema = discriminatedUnion("type", [
382
460
  SseContentBlockStopEvent,
383
461
  SseMessageDeltaEvent,
384
462
  SseMessageStopEvent,
385
- SsePingEvent
463
+ SsePingEvent,
464
+ SseErrorEvent
386
465
  ]);
387
466
  const InspectorRequestSchema = AnthropicRequestSchema;
388
467
  const InspectorResponseSchema = AnthropicResponseSchema$1;
@@ -541,7 +620,9 @@ const CapturedLogSchema = object({
541
620
  clientCwd: string().nullable().optional(),
542
621
  clientProjectFolder: string().nullable().optional(),
543
622
  streamingChunks: StreamingChunksArraySchema.optional(),
544
- 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()
545
626
  });
546
627
  const RequestModelSchema = object({
547
628
  model: string()
@@ -601,13 +682,13 @@ function writeChunks(logId, chunks, truncated) {
601
682
  try {
602
683
  mkdirSync(dir, { recursive: true });
603
684
  } catch (err) {
604
- console.error("[chunkStorage] Failed to create chunks directory:", err);
685
+ logger.error("[chunkStorage] Failed to create chunks directory:", String(err));
605
686
  }
606
687
  const data = { chunks, truncated };
607
688
  try {
608
689
  writeFileSync(tempPath, JSON.stringify(data), "utf-8");
609
690
  } catch (err) {
610
- console.error("[chunkStorage] Failed to write chunks temp file:", err);
691
+ logger.error("[chunkStorage] Failed to write chunks temp file:", String(err));
611
692
  return targetPath;
612
693
  }
613
694
  try {
@@ -618,7 +699,7 @@ function writeChunks(logId, chunks, truncated) {
618
699
  copyFileSync(tempPath, targetPath);
619
700
  unlinkSync(tempPath);
620
701
  } catch (copyErr) {
621
- console.error("[chunkStorage] Failed to copy chunks file:", copyErr);
702
+ logger.error("[chunkStorage] Failed to copy chunks file:", String(copyErr));
622
703
  }
623
704
  }
624
705
  return targetPath;
@@ -791,12 +872,12 @@ async function getLogById(id) {
791
872
  }
792
873
  if (lastMatch !== null) return lastMatch;
793
874
  } catch (err) {
794
- console.error("[store] Failed to read log from disk:", err);
875
+ logger.error("[store] Failed to read log from disk:", String(err));
795
876
  }
796
877
  return null;
797
878
  }
798
879
  function getFilteredLogs(sessionId, model) {
799
- const cachedLogs = Array.from(memoryCache.values()).sort((a, b) => a.id - b.id);
880
+ const cachedLogs = Array.from(memoryCache.values());
800
881
  return cachedLogs.filter((l) => {
801
882
  if (sessionId !== void 0 && l.sessionId !== sessionId) return false;
802
883
  if (model !== void 0 && l.model !== model) return false;
@@ -1009,6 +1090,15 @@ function extractAnthropicStream(raw, log, fallbackModel, collectChunks) {
1009
1090
  stopSequence = data.delta.stop_sequence ?? null;
1010
1091
  outputTokens = data.usage.output_tokens;
1011
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;
1012
1102
  break;
1013
1103
  case "content_block_stop":
1014
1104
  case "message_stop":
@@ -1185,6 +1275,16 @@ function extractOpenAIStream(raw, log, fallbackModel, collectChunks = true) {
1185
1275
  if (dataStr === "[DONE]") break;
1186
1276
  try {
1187
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
+ }
1188
1288
  const chunkResult = OpenAISSERawChunkSchema.safeParse(parsed);
1189
1289
  if (!chunkResult.success) continue;
1190
1290
  const chunk = chunkResult.data;
@@ -1395,6 +1495,8 @@ const ProviderConfigSchema = object({
1395
1495
  openaiBaseUrl: string().optional(),
1396
1496
  /** Auth header to use: "bearer" (default) or "x-api-key" */
1397
1497
  authHeader: _enum(["bearer", "x-api-key"]).optional().default("bearer"),
1498
+ /** API documentation URL */
1499
+ apiDocsUrl: string().optional(),
1398
1500
  createdAt: string(),
1399
1501
  updatedAt: string()
1400
1502
  });
@@ -1473,7 +1575,7 @@ function getProvider(id) {
1473
1575
  function normalizeApiKey(apiKey) {
1474
1576
  return apiKey.replace(/^Bearer\s+/i, "").trim();
1475
1577
  }
1476
- function addProvider(name, apiKey, format, baseUrl, model, authHeader) {
1578
+ function addProvider(name, apiKey, format, baseUrl, model, authHeader, apiDocsUrl) {
1477
1579
  const providers = getProviders();
1478
1580
  const now = (/* @__PURE__ */ new Date()).toISOString();
1479
1581
  const newProvider = {
@@ -1484,6 +1586,7 @@ function addProvider(name, apiKey, format, baseUrl, model, authHeader) {
1484
1586
  baseUrl,
1485
1587
  model,
1486
1588
  authHeader: authHeader ?? "bearer",
1589
+ apiDocsUrl,
1487
1590
  createdAt: now,
1488
1591
  updatedAt: now,
1489
1592
  anthropicBaseUrl: format === "anthropic" && baseUrl !== void 0 ? baseUrl : "",
@@ -1505,6 +1608,7 @@ function updateProvider(id, updates) {
1505
1608
  format: updates.format ?? existing.format,
1506
1609
  baseUrl: updates.baseUrl !== void 0 ? updates.baseUrl : existing.baseUrl,
1507
1610
  authHeader: updates.authHeader ?? existing.authHeader,
1611
+ apiDocsUrl: updates.apiDocsUrl ?? existing.apiDocsUrl,
1508
1612
  createdAt: existing.createdAt,
1509
1613
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1510
1614
  // Handle format-specific URLs
@@ -1636,45 +1740,99 @@ function extractRemotePort(request) {
1636
1740
  return null;
1637
1741
  }
1638
1742
  async function lookupPidByPort(port) {
1743
+ const platform = process.platform;
1639
1744
  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;
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 {
1650
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;
1651
1774
  }
1652
- return null;
1653
- } catch {
1775
+ } catch (err) {
1776
+ logger.debug(`[socketTracker] Failed to lookup PID for port ${port}:`, String(err));
1654
1777
  return null;
1655
1778
  }
1656
1779
  }
1657
1780
  async function lookupProcessInfo(pid) {
1781
+ const platform = process.platform;
1658
1782
  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();
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();
1667
1811
  if (commandLine) {
1668
1812
  const exeMatch = commandLine.match(/^"([^"]+)"/) || commandLine.match(/^([^\s]+)/);
1669
1813
  if (exeMatch && exeMatch[1] !== void 0) {
1670
1814
  const exePath = exeMatch[1];
1671
- const exeDir = exePath.substring(0, exePath.lastIndexOf("\\"));
1815
+ const exeDir = exePath.substring(0, exePath.lastIndexOf("/"));
1672
1816
  return {
1673
1817
  cwd: exeDir,
1674
- projectFolder: exeDir.split("\\").pop() ?? null
1818
+ projectFolder: exeDir.split("/").pop() ?? null
1675
1819
  };
1676
1820
  }
1677
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
+ }
1678
1836
  }
1679
1837
  }
1680
1838
  return { cwd: null, projectFolder: null };
@@ -1747,7 +1905,8 @@ function buildFileLogEntry(log, upstreamUrl) {
1747
1905
  clientCwd: log.clientCwd,
1748
1906
  clientProjectFolder: log.clientProjectFolder,
1749
1907
  streamingChunks: log.streamingChunks,
1750
- streamingChunksPath: log.streamingChunksPath
1908
+ streamingChunksPath: log.streamingChunksPath,
1909
+ error: log.error
1751
1910
  };
1752
1911
  }
1753
1912
  function parseRequestPath(req, url) {
@@ -1840,7 +1999,7 @@ function handleStreamingResponse(upstreamRes, req, startTime, formatHandler, ups
1840
1999
  );
1841
2000
  log.streamingChunksPath = chunkPath;
1842
2001
  }
1843
- appendLogEntry({ ...buildFileLogEntry(log, upstreamUrl), error: null });
2002
+ appendLogEntry(buildFileLogEntry(log, upstreamUrl));
1844
2003
  emitLogUpdate(log);
1845
2004
  }
1846
2005
  });
@@ -1849,17 +2008,22 @@ function handleStreamingResponse(upstreamRes, req, startTime, formatHandler, ups
1849
2008
  }
1850
2009
  const loggedStream = upstreamRes.body.pipeThrough(transform);
1851
2010
  req.signal?.addEventListener("abort", () => {
1852
- if (log.responseText === null && chunks.length > 0) {
1853
- const full = chunks.join("");
2011
+ if (log.responseText === null) {
2012
+ logger.info(`[handler] Streaming client aborted: ${log.method} ${log.path}`);
1854
2013
  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;
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";
1863
2027
  }
1864
2028
  appendLogEntry({ ...buildFileLogEntry(log, upstreamUrl), error: "Client aborted" });
1865
2029
  emitLogUpdate(log);
@@ -1881,7 +2045,7 @@ async function handleProxy(req) {
1881
2045
  requestBody = await req.text();
1882
2046
  }
1883
2047
  const model = requestBody !== null ? extractModelFromBody(requestBody) : null;
1884
- const matchedProviderConfig = findProviderByModelFromConfig(requestBody);
2048
+ const matchedProviderConfig = model !== null ? findProviderByModel(model) : null;
1885
2049
  const upstreamBase = selectUpstreamBase$1(parsed.isChatCompletions, matchedProviderConfig);
1886
2050
  const upstreamUrl = buildUpstreamUrl$1(upstreamBase, parsed.normalizedPath);
1887
2051
  const upstreamHost = getHostFromUrl$1(upstreamBase);
@@ -1891,6 +2055,7 @@ async function handleProxy(req) {
1891
2055
  injectAuthHeaders$1(upstreamHeaders, matchedProviderConfig);
1892
2056
  const provider = model !== null ? registry.findProvider(model) : null;
1893
2057
  if (model === null || provider === null) {
2058
+ logger.warn(`[handler] Unsupported provider: model=${model}`);
1894
2059
  return new Response("Forbidden: unsupported provider", { status: STATUS_FORBIDDEN });
1895
2060
  }
1896
2061
  let formatHandler;
@@ -1922,10 +2087,19 @@ async function handleProxy(req) {
1922
2087
  upstreamRes = await fetch(upstreamUrl, {
1923
2088
  method: req.method,
1924
2089
  headers: upstreamHeaders,
1925
- body: requestBody
2090
+ body: requestBody,
2091
+ signal: req.signal
1926
2092
  });
1927
2093
  } catch (err) {
1928
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));
1929
2103
  log.responseStatus = STATUS_BAD_GATEWAY;
1930
2104
  log.responseText = String(err);
1931
2105
  appendLogEntry({ ...buildFileLogEntry(log, upstreamUrl), error: String(err) });
@@ -1945,12 +2119,6 @@ async function handleProxy(req) {
1945
2119
  }
1946
2120
  return handleStreamingResponse(upstreamRes, req, startTime, formatHandler, upstreamUrl, log);
1947
2121
  }
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
2122
  const Route$e = createFileRoute("/proxy/$")({
1955
2123
  server: {
1956
2124
  handlers: {
@@ -1976,7 +2144,8 @@ const ProviderInputSchema = object({
1976
2144
  format: _enum(["anthropic", "openai"]),
1977
2145
  baseUrl: string().min(1, "Base URL is required"),
1978
2146
  model: string().min(1, "Model is required"),
1979
- authHeader: _enum(["bearer", "x-api-key"]).optional().default("bearer")
2147
+ authHeader: _enum(["bearer", "x-api-key"]).optional().default("bearer"),
2148
+ apiDocsUrl: string().optional()
1980
2149
  });
1981
2150
  const Route$c = createFileRoute("/api/providers")({
1982
2151
  server: {
@@ -1995,7 +2164,8 @@ const Route$c = createFileRoute("/api/providers")({
1995
2164
  parsed.data.format,
1996
2165
  parsed.data.baseUrl,
1997
2166
  parsed.data.model,
1998
- parsed.data.authHeader
2167
+ parsed.data.authHeader,
2168
+ parsed.data.apiDocsUrl
1999
2169
  );
2000
2170
  return Response.json(newProvider, { status: 201 });
2001
2171
  }
@@ -2103,7 +2273,8 @@ const ProviderUpdateSchema = object({
2103
2273
  model: string().min(1, "Model is required").optional(),
2104
2274
  authHeader: _enum(["bearer", "x-api-key"]).optional(),
2105
2275
  anthropicBaseUrl: string().optional(),
2106
- openaiBaseUrl: string().optional()
2276
+ openaiBaseUrl: string().optional(),
2277
+ apiDocsUrl: string().optional()
2107
2278
  });
2108
2279
  const Route$6 = createFileRoute("/api/providers/$providerId")({
2109
2280
  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-Cu0oTDfX.js"], "assets": [] }, "/": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/index.tsx", "assets": [], "preloads": ["/assets/index-DyKLPMPn.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-Cu0oTDfX.js" });
2
2
  export {
3
3
  tsrStartManifest
4
4
  };
@@ -100,51 +100,51 @@ const assets = {
100
100
  "/assets/alibaba-TTwafVwX.svg": {
101
101
  "type": "image/svg+xml",
102
102
  "etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
103
- "mtime": "2026-06-04T01:19:08.282Z",
103
+ "mtime": "2026-06-04T03:52:13.283Z",
104
104
  "size": 5915,
105
105
  "path": "../public/assets/alibaba-TTwafVwX.svg"
106
106
  },
107
+ "/assets/index-DdJSLfxK.css": {
108
+ "type": "text/css; charset=utf-8",
109
+ "etag": '"10da0-LYeZ5d/vwqh4bAnuP/9hr6Wka6g"',
110
+ "mtime": "2026-06-04T03:52:13.283Z",
111
+ "size": 69024,
112
+ "path": "../public/assets/index-DdJSLfxK.css"
113
+ },
107
114
  "/assets/minimax-BPMzvuL-.jpeg": {
108
115
  "type": "image/jpeg",
109
116
  "etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
110
- "mtime": "2026-06-04T01:19:08.282Z",
117
+ "mtime": "2026-06-04T03:52:13.278Z",
111
118
  "size": 6918,
112
119
  "path": "../public/assets/minimax-BPMzvuL-.jpeg"
113
120
  },
114
- "/assets/main-Beo3LJDa.js": {
115
- "type": "text/javascript; charset=utf-8",
116
- "etag": '"50591-/XG/Oh/RlDy7LRgwa0Uqfltq6fM"',
117
- "mtime": "2026-06-04T01:19:08.282Z",
118
- "size": 329105,
119
- "path": "../public/assets/main-Beo3LJDa.js"
120
- },
121
121
  "/assets/zhipuai-BPNAnxo-.svg": {
122
122
  "type": "image/svg+xml",
123
123
  "etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
124
- "mtime": "2026-06-04T01:19:08.282Z",
124
+ "mtime": "2026-06-04T03:52:13.283Z",
125
125
  "size": 11256,
126
126
  "path": "../public/assets/zhipuai-BPNAnxo-.svg"
127
127
  },
128
+ "/assets/main-Cu0oTDfX.js": {
129
+ "type": "text/javascript; charset=utf-8",
130
+ "etag": '"50591-/K5L0AnXTcumA7dHm7kmA5HXlCE"',
131
+ "mtime": "2026-06-04T03:52:13.283Z",
132
+ "size": 329105,
133
+ "path": "../public/assets/main-Cu0oTDfX.js"
134
+ },
128
135
  "/assets/qwen-CONDcHqt.png": {
129
136
  "type": "image/png",
130
137
  "etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
131
- "mtime": "2026-06-04T01:19:08.282Z",
138
+ "mtime": "2026-06-04T03:52:13.283Z",
132
139
  "size": 357059,
133
140
  "path": "../public/assets/qwen-CONDcHqt.png"
134
141
  },
135
- "/assets/index-BLVa7n9b.css": {
136
- "type": "text/css; charset=utf-8",
137
- "etag": '"10ce0-rnZGppItQl8rOmyih342MZyZcI0"',
138
- "mtime": "2026-06-04T01:19:08.282Z",
139
- "size": 68832,
140
- "path": "../public/assets/index-BLVa7n9b.css"
141
- },
142
- "/assets/index-DH3FOgcK.js": {
142
+ "/assets/index-DyKLPMPn.js": {
143
143
  "type": "text/javascript; charset=utf-8",
144
- "etag": '"8421c-gZ3NyuThEQLW3CYFr8ZxAzJ1zq0"',
145
- "mtime": "2026-06-04T01:19:08.283Z",
146
- "size": 541212,
147
- "path": "../public/assets/index-DH3FOgcK.js"
144
+ "etag": '"84a31-iF3/oGIdY77bqBWoGeM9Ctiyids"',
145
+ "mtime": "2026-06-04T03:52:13.284Z",
146
+ "size": 543281,
147
+ "path": "../public/assets/index-DyKLPMPn.js"
148
148
  }
149
149
  };
150
150
  function readAsset(id) {