@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.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{index-DH3FOgcK.js → index-BmEH5jeO.js} +18 -18
- package/.output/public/assets/{index-BLVa7n9b.css → index-DdJSLfxK.css} +1 -1
- package/.output/public/assets/{main-Beo3LJDa.js → main-GVpFMVGE.js} +1 -1
- package/.output/server/_ssr/{index-HkueJ4Un.mjs → index-D0d6QxPt.mjs} +85 -31
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-DTswxb7l.mjs → router-D9uLXa9A.mjs} +287 -67
- package/.output/server/{_tanstack-start-manifest_v-DhUuivt-.mjs → _tanstack-start-manifest_v-ByfnNZV_.mjs} +1 -1
- package/.output/server/index.mjs +26 -26
- package/README.md +8 -209
- package/package.json +1 -1
- package/src/components/ProxyViewerContainer.tsx +10 -1
- package/src/components/providers/ProviderCard.tsx +19 -15
- package/src/components/providers/ProviderForm.tsx +21 -0
- package/src/components/proxy-viewer/LogEntry.tsx +7 -0
- package/src/components/proxy-viewer/ResponseView.tsx +32 -6
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +12 -3
- package/src/proxy/chunkStorage.ts +4 -6
- package/src/proxy/formats/anthropic/schemas.ts +9 -0
- package/src/proxy/formats/anthropic/stream.ts +11 -0
- package/src/proxy/formats/openai/stream.ts +15 -0
- package/src/proxy/handler.ts +44 -27
- package/src/proxy/logIndex.ts +73 -7
- package/src/proxy/logger.ts +62 -6
- package/src/proxy/providers.ts +5 -0
- package/src/proxy/schemas.ts +2 -0
- package/src/proxy/socketTracker.ts +90 -36
- package/src/proxy/store.ts +32 -18
- package/src/routes/api/providers.$providerId.ts +1 -0
- 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,
|
|
6
|
-
import {
|
|
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,
|
|
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-
|
|
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-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
const
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
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
|
-
|
|
1653
|
-
|
|
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
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
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("
|
|
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(
|
|
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
|
|
1853
|
-
|
|
2060
|
+
if (log.responseText === null) {
|
|
2061
|
+
logger.info(`[handler] Streaming client aborted: ${log.method} ${log.path}`);
|
|
1854
2062
|
log.elapsedMs = Date.now() - startTime;
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
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 =
|
|
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-
|
|
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
|
};
|