@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.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/index-DdJSLfxK.css +1 -0
- package/.output/public/assets/index-DyKLPMPn.js +97 -0
- package/.output/public/assets/main-Cu0oTDfX.js +17 -0
- package/.output/server/_libs/dequal.mjs +27 -0
- package/.output/server/_libs/swr.mjs +938 -0
- package/.output/server/_libs/use-sync-external-store.mjs +64 -1
- package/.output/server/_ssr/{index-CAIDMqNv.mjs → index-COIATcfa.mjs} +186 -88
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-CsCLdrXq.mjs → router-CwmgKXBJ.mjs} +259 -74
- package/.output/server/{_tanstack-start-manifest_v-BF6ge6dS.mjs → _tanstack-start-manifest_v-C7hQOzvX.mjs} +1 -1
- package/.output/server/index.mjs +23 -23
- package/README.md +8 -209
- package/package.json +2 -1
- package/src/components/ProxyViewer.tsx +2 -0
- package/src/components/ProxyViewerContainer.tsx +10 -1
- package/src/components/providers/ProviderCard.tsx +57 -48
- package/src/components/providers/ProviderForm.tsx +21 -0
- package/src/components/providers/ProviderLogo.tsx +6 -1
- package/src/components/providers/ProvidersPanel.tsx +29 -34
- package/src/components/providers/SettingsDialog.tsx +5 -3
- package/src/components/proxy-viewer/LogEntry.tsx +7 -0
- package/src/components/proxy-viewer/ResponseView.tsx +32 -6
- package/src/lib/useProviders.ts +30 -0
- 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 +34 -27
- package/src/proxy/logIndex.ts +52 -7
- package/src/proxy/logger.ts +60 -10
- 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 +24 -14
- package/src/routes/__root.tsx +4 -1
- package/src/routes/api/providers.$providerId.ts +1 -0
- package/src/routes/api/providers.ts +2 -0
- package/.output/public/assets/index-B3RwBPLW.css +0 -1
- package/.output/public/assets/index-CB8ZIeEk.js +0 -97
- 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,
|
|
5
|
-
import {
|
|
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,
|
|
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-
|
|
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-
|
|
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
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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())
|
|
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
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
const
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
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
|
-
|
|
1650
|
-
|
|
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
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
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("
|
|
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(
|
|
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
|
|
1850
|
-
|
|
2011
|
+
if (log.responseText === null) {
|
|
2012
|
+
logger.info(`[handler] Streaming client aborted: ${log.method} ${log.path}`);
|
|
1851
2013
|
log.elapsedMs = Date.now() - startTime;
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
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 =
|
|
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(
|
|
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) {
|