@tonyclaw/llm-inspector 1.13.0 → 1.14.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-DEUddp_2.css +1 -0
- package/.output/public/assets/index-ax85pt2A.js +105 -0
- package/.output/public/assets/{main-C3tLo75s.js → main-BzK2SzIB.js} +3 -3
- package/.output/server/_libs/cfworker__json-schema.mjs +1 -0
- package/.output/server/_libs/lucide-react.mjs +60 -54
- package/.output/server/_libs/modelcontextprotocol__server.mjs +9738 -0
- package/.output/server/_libs/zod.mjs +79 -16
- package/.output/server/_ssr/{index-C8VC13EA.mjs → index-Cso39vJc.mjs} +715 -178
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-D5ccnemB.mjs → router-B8X3GXM2.mjs} +669 -39
- package/.output/server/_tanstack-start-manifest_v-vO4aM6jK.mjs +4 -0
- package/.output/server/index.mjs +29 -29
- package/README.md +98 -0
- package/package.json +3 -1
- package/src/components/providers/ProviderCard.tsx +28 -5
- package/src/components/providers/ProviderForm.tsx +172 -66
- package/src/components/providers/ProvidersPanel.tsx +73 -47
- package/src/components/proxy-viewer/CompareDrawer.tsx +583 -91
- package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +74 -4
- package/src/lib/mask.ts +4 -0
- package/src/lib/providerContract.ts +1 -0
- package/src/lib/serverPort.ts +41 -0
- package/src/mcp/loopback.ts +76 -0
- package/src/mcp/previewExtractor.ts +166 -0
- package/src/mcp/server.ts +320 -0
- package/src/mcp/toolHandlers.ts +259 -0
- package/src/proxy/formats/openai/schemas.ts +19 -0
- package/src/proxy/handler.ts +23 -2
- package/src/proxy/openaiOrphanToolStrip.ts +148 -0
- package/src/proxy/providers.ts +16 -4
- package/src/proxy/schemas.ts +1 -0
- package/src/routes/api/mcp.ts +25 -0
- package/src/routes/api/providers.$providerId.ts +1 -0
- package/src/routes/api/providers.ts +6 -4
- package/.output/public/assets/index-B0anmGQr.css +0 -1
- package/.output/public/assets/index-H_thmL2_.js +0 -105
- package/.output/server/_tanstack-start-manifest_v-DUbXa1lt.mjs +0 -4
|
@@ -8,7 +8,8 @@ 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 {
|
|
11
|
+
import { M as McpServer, W as WebStandardStreamableHTTPServerTransport } from "../_libs/modelcontextprotocol__server.mjs";
|
|
12
|
+
import { d as object, _ as _enum, b as string, u as union, a as array, c as boolean, n as number, g as discriminatedUnion, l as literal, r as record, h as _null, k as lazy, e as unknown } from "../_libs/zod.mjs";
|
|
12
13
|
import "../_libs/tiny-warning.mjs";
|
|
13
14
|
import "../_libs/tanstack__router-core.mjs";
|
|
14
15
|
import "../_libs/cookie-es.mjs";
|
|
@@ -44,8 +45,8 @@ import "../_libs/debounce-fn.mjs";
|
|
|
44
45
|
import "../_libs/mimic-function.mjs";
|
|
45
46
|
import "../_libs/semver.mjs";
|
|
46
47
|
import "../_libs/uint8array-extras.mjs";
|
|
47
|
-
const appCss = "/assets/index-
|
|
48
|
-
const Route$
|
|
48
|
+
const appCss = "/assets/index-DEUddp_2.css";
|
|
49
|
+
const Route$i = createRootRoute({
|
|
49
50
|
head: () => ({
|
|
50
51
|
meta: [
|
|
51
52
|
{ charSet: "utf-8" },
|
|
@@ -68,8 +69,8 @@ function RootDocument({ children }) {
|
|
|
68
69
|
] })
|
|
69
70
|
] });
|
|
70
71
|
}
|
|
71
|
-
const $$splitComponentImporter = () => import("./index-
|
|
72
|
-
const Route$
|
|
72
|
+
const $$splitComponentImporter = () => import("./index-Cso39vJc.mjs");
|
|
73
|
+
const Route$h = createFileRoute("/")({
|
|
73
74
|
component: lazyRouteComponent($$splitComponentImporter, "component")
|
|
74
75
|
});
|
|
75
76
|
const LOG_DIR_ENV = process.env["LOG_DIR"];
|
|
@@ -546,9 +547,19 @@ const OpenAIFunctionCall = object({
|
|
|
546
547
|
name: string(),
|
|
547
548
|
arguments: string()
|
|
548
549
|
});
|
|
550
|
+
const OpenAIToolCallSchema = object({
|
|
551
|
+
index: number().optional(),
|
|
552
|
+
id: string().optional(),
|
|
553
|
+
type: literal("function").optional(),
|
|
554
|
+
function: object({
|
|
555
|
+
name: string().optional(),
|
|
556
|
+
arguments: string().optional()
|
|
557
|
+
})
|
|
558
|
+
});
|
|
549
559
|
OpenAIMessage.extend({
|
|
550
560
|
content: union([string(), array(object({ type: literal("text"), text: string() }))]).optional(),
|
|
551
|
-
function_call: OpenAIFunctionCall.optional()
|
|
561
|
+
function_call: OpenAIFunctionCall.optional(),
|
|
562
|
+
tool_calls: array(OpenAIToolCallSchema).optional()
|
|
552
563
|
});
|
|
553
564
|
const OpenAIToolDefinition = object({
|
|
554
565
|
type: literal("function"),
|
|
@@ -603,7 +614,8 @@ const OpenAIChoice = object({
|
|
|
603
614
|
// Some providers use 'thinking' field in message
|
|
604
615
|
think: string().optional(),
|
|
605
616
|
// MiniMax uses 'think' field in message
|
|
606
|
-
function_call: object({ name: string(), arguments: string() }).nullable().optional()
|
|
617
|
+
function_call: object({ name: string(), arguments: string() }).nullable().optional(),
|
|
618
|
+
tool_calls: array(OpenAIToolCallSchema).optional()
|
|
607
619
|
}).optional(),
|
|
608
620
|
delta: OpenAIChoiceDelta.optional(),
|
|
609
621
|
finish_reason: string().nullable()
|
|
@@ -1602,6 +1614,7 @@ const ProviderConfigSchema = object({
|
|
|
1602
1614
|
openaiBaseUrl: string().optional(),
|
|
1603
1615
|
authHeader: _enum(["bearer", "x-api-key"]).optional().default("bearer"),
|
|
1604
1616
|
apiDocsUrl: string().optional(),
|
|
1617
|
+
source: _enum(["company", "personal"]).optional(),
|
|
1605
1618
|
createdAt: string(),
|
|
1606
1619
|
updatedAt: string()
|
|
1607
1620
|
});
|
|
@@ -1683,22 +1696,23 @@ function getProvider(id) {
|
|
|
1683
1696
|
function normalizeApiKey(apiKey) {
|
|
1684
1697
|
return apiKey.replace(/^Bearer\s+/i, "").trim();
|
|
1685
1698
|
}
|
|
1686
|
-
function addProvider(name, apiKey, format, baseUrl, model, authHeader, apiDocsUrl) {
|
|
1699
|
+
function addProvider(name, apiKey, format, baseUrl, model, authHeader, apiDocsUrl, anthropicBaseUrl, openaiBaseUrl, source) {
|
|
1687
1700
|
const providers = getProviders();
|
|
1688
1701
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1689
1702
|
const newProvider = {
|
|
1690
1703
|
id: randomUUID(),
|
|
1691
1704
|
name,
|
|
1692
1705
|
apiKey: normalizeApiKey(apiKey),
|
|
1693
|
-
format,
|
|
1706
|
+
format: format ?? (anthropicBaseUrl !== void 0 ? "anthropic" : openaiBaseUrl !== void 0 ? "openai" : void 0),
|
|
1694
1707
|
baseUrl,
|
|
1695
1708
|
model,
|
|
1696
1709
|
authHeader: authHeader ?? "bearer",
|
|
1697
1710
|
apiDocsUrl,
|
|
1698
1711
|
createdAt: now,
|
|
1699
1712
|
updatedAt: now,
|
|
1700
|
-
anthropicBaseUrl:
|
|
1701
|
-
openaiBaseUrl:
|
|
1713
|
+
anthropicBaseUrl: anthropicBaseUrl ?? "",
|
|
1714
|
+
openaiBaseUrl: openaiBaseUrl ?? "",
|
|
1715
|
+
source
|
|
1702
1716
|
};
|
|
1703
1717
|
providers.push(newProvider);
|
|
1704
1718
|
store.set("providers", providers);
|
|
@@ -1721,7 +1735,8 @@ function updateProvider(id, updates) {
|
|
|
1721
1735
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1722
1736
|
// Handle format-specific URLs
|
|
1723
1737
|
anthropicBaseUrl: updates.anthropicBaseUrl !== void 0 ? updates.anthropicBaseUrl : existing.anthropicBaseUrl,
|
|
1724
|
-
openaiBaseUrl: updates.openaiBaseUrl !== void 0 ? updates.openaiBaseUrl : existing.openaiBaseUrl
|
|
1738
|
+
openaiBaseUrl: updates.openaiBaseUrl !== void 0 ? updates.openaiBaseUrl : existing.openaiBaseUrl,
|
|
1739
|
+
source: updates.source !== void 0 ? updates.source : existing.source
|
|
1725
1740
|
};
|
|
1726
1741
|
const index = providers.findIndex((p) => p.id === id);
|
|
1727
1742
|
providers[index] = updated;
|
|
@@ -2056,7 +2071,7 @@ function isClaudeCodeBillingHeaderBlock(text) {
|
|
|
2056
2071
|
const trimmed = text.trimStart();
|
|
2057
2072
|
return trimmed.toLowerCase().startsWith(BILLING_HEADER_PREFIX);
|
|
2058
2073
|
}
|
|
2059
|
-
function getOwnProperty(obj, key) {
|
|
2074
|
+
function getOwnProperty$1(obj, key) {
|
|
2060
2075
|
const desc = Object.getOwnPropertyDescriptor(obj, key);
|
|
2061
2076
|
return desc?.value;
|
|
2062
2077
|
}
|
|
@@ -2066,8 +2081,8 @@ function isObjectWithSystem(value) {
|
|
|
2066
2081
|
}
|
|
2067
2082
|
function isBillingHeaderTextBlock(block) {
|
|
2068
2083
|
if (block === null || typeof block !== "object" || Array.isArray(block)) return false;
|
|
2069
|
-
const typeVal = getOwnProperty(block, "type");
|
|
2070
|
-
const textVal = getOwnProperty(block, "text");
|
|
2084
|
+
const typeVal = getOwnProperty$1(block, "type");
|
|
2085
|
+
const textVal = getOwnProperty$1(block, "text");
|
|
2071
2086
|
if (typeof typeVal !== "string" || typeVal !== "text") return false;
|
|
2072
2087
|
if (typeof textVal !== "string") return false;
|
|
2073
2088
|
return isClaudeCodeBillingHeaderBlock(textVal);
|
|
@@ -2105,6 +2120,82 @@ function stripClaudeCodeBillingHeader(rawBody) {
|
|
|
2105
2120
|
}
|
|
2106
2121
|
return { body: JSON.stringify(parsed), removed };
|
|
2107
2122
|
}
|
|
2123
|
+
const ROLE_ASSISTANT = "assistant";
|
|
2124
|
+
const ROLE_TOOL = "tool";
|
|
2125
|
+
function getOwnProperty(obj, key) {
|
|
2126
|
+
const desc = Object.getOwnPropertyDescriptor(obj, key);
|
|
2127
|
+
return desc?.value;
|
|
2128
|
+
}
|
|
2129
|
+
function isPlainObject(value) {
|
|
2130
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2131
|
+
}
|
|
2132
|
+
function isObjectWithMessages(value) {
|
|
2133
|
+
if (!isPlainObject(value)) return false;
|
|
2134
|
+
if (!Object.prototype.hasOwnProperty.call(value, "messages")) return false;
|
|
2135
|
+
const messages = getOwnProperty(value, "messages");
|
|
2136
|
+
return Array.isArray(messages);
|
|
2137
|
+
}
|
|
2138
|
+
function collectAssistantToolCallIds(message, into) {
|
|
2139
|
+
const toolCalls = getOwnProperty(message, "tool_calls");
|
|
2140
|
+
if (!Array.isArray(toolCalls)) return;
|
|
2141
|
+
for (const tc of toolCalls) {
|
|
2142
|
+
if (!isPlainObject(tc)) continue;
|
|
2143
|
+
const id = getOwnProperty(tc, "id");
|
|
2144
|
+
if (typeof id === "string" && id.length > 0) {
|
|
2145
|
+
into.add(id);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
function findOrphanToolMessageIndices(messages) {
|
|
2150
|
+
const knownToolCallIds = /* @__PURE__ */ new Set();
|
|
2151
|
+
const indices = [];
|
|
2152
|
+
const orphanIds = [];
|
|
2153
|
+
for (let i = 0; i < messages.length; i++) {
|
|
2154
|
+
const msg = messages[i];
|
|
2155
|
+
if (!isPlainObject(msg)) continue;
|
|
2156
|
+
const role = getOwnProperty(msg, "role");
|
|
2157
|
+
if (role === ROLE_ASSISTANT) {
|
|
2158
|
+
collectAssistantToolCallIds(msg, knownToolCallIds);
|
|
2159
|
+
continue;
|
|
2160
|
+
}
|
|
2161
|
+
if (role === ROLE_TOOL) {
|
|
2162
|
+
const id = getOwnProperty(msg, "tool_call_id");
|
|
2163
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
2164
|
+
indices.push(i);
|
|
2165
|
+
orphanIds.push(null);
|
|
2166
|
+
continue;
|
|
2167
|
+
}
|
|
2168
|
+
if (!knownToolCallIds.has(id)) {
|
|
2169
|
+
indices.push(i);
|
|
2170
|
+
orphanIds.push(id);
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
return { indices, orphanIds };
|
|
2175
|
+
}
|
|
2176
|
+
function stripOpenAIOrphanToolMessages(rawBody) {
|
|
2177
|
+
let parsed;
|
|
2178
|
+
try {
|
|
2179
|
+
parsed = JSON.parse(rawBody);
|
|
2180
|
+
} catch {
|
|
2181
|
+
return { body: rawBody, removed: 0, orphanIds: [] };
|
|
2182
|
+
}
|
|
2183
|
+
if (!isObjectWithMessages(parsed)) {
|
|
2184
|
+
return { body: rawBody, removed: 0, orphanIds: [] };
|
|
2185
|
+
}
|
|
2186
|
+
const messages = parsed.messages;
|
|
2187
|
+
const { indices, orphanIds } = findOrphanToolMessageIndices(messages);
|
|
2188
|
+
if (indices.length === 0) {
|
|
2189
|
+
return { body: rawBody, removed: 0, orphanIds: [] };
|
|
2190
|
+
}
|
|
2191
|
+
const dropSet = new Set(indices);
|
|
2192
|
+
const kept = [];
|
|
2193
|
+
for (let i = 0; i < messages.length; i++) {
|
|
2194
|
+
if (!dropSet.has(i)) kept.push(messages[i]);
|
|
2195
|
+
}
|
|
2196
|
+
parsed.messages = kept;
|
|
2197
|
+
return { body: JSON.stringify(parsed), removed: indices.length, orphanIds };
|
|
2198
|
+
}
|
|
2108
2199
|
function describeApiRoute(apiPath) {
|
|
2109
2200
|
const endpointPath = apiPath.split("?")[0] ?? "";
|
|
2110
2201
|
const isChatCompletions = endpointPath === PATH_CHAT_COMPLETIONS || endpointPath === PATH_V1_CHAT_COMPLETIONS;
|
|
@@ -2196,10 +2287,13 @@ function buildFileLogEntry(log, upstreamUrl) {
|
|
|
2196
2287
|
}
|
|
2197
2288
|
function parseRequestPath(req, url) {
|
|
2198
2289
|
const route = describeApiRoute(getProxyApiPath(url));
|
|
2199
|
-
const
|
|
2290
|
+
const isPost = req.method === "POST";
|
|
2291
|
+
const isChatCompletions = isPost && route.isChatCompletions;
|
|
2292
|
+
const isMessages = isPost && (route.endpointPath === PATH_V1_MESSAGES || route.isChatCompletions);
|
|
2200
2293
|
return {
|
|
2201
2294
|
apiPath: route.apiPath,
|
|
2202
2295
|
isMessages,
|
|
2296
|
+
isChatCompletions,
|
|
2203
2297
|
normalizedPath: route.normalizedPath
|
|
2204
2298
|
};
|
|
2205
2299
|
}
|
|
@@ -2299,6 +2393,15 @@ async function handleProxy(req) {
|
|
|
2299
2393
|
bodyToForward = stripped.body;
|
|
2300
2394
|
}
|
|
2301
2395
|
}
|
|
2396
|
+
if (bodyToForward !== null && parsed.isChatCompletions) {
|
|
2397
|
+
const stripped = stripOpenAIOrphanToolMessages(bodyToForward);
|
|
2398
|
+
if (stripped.removed > 0) {
|
|
2399
|
+
logger.warn(
|
|
2400
|
+
`[handler] Dropped ${stripped.removed} orphan OpenAI tool message(s) with tool_call_id(s) ${JSON.stringify(stripped.orphanIds)} — the client sent a tool result with no matching assistant.tool_calls`
|
|
2401
|
+
);
|
|
2402
|
+
bodyToForward = stripped.body;
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2302
2405
|
const { model, sessionId } = extractRequestMetadata(requestBody, req.headers);
|
|
2303
2406
|
const matchedProviderConfig = model !== null ? findProviderByModel(model) : null;
|
|
2304
2407
|
const route = describeApiRoute(parsed.apiPath);
|
|
@@ -2372,7 +2475,7 @@ async function handleProxy(req) {
|
|
|
2372
2475
|
}
|
|
2373
2476
|
return handleStreamingResponse(upstreamRes, req, startTime, formatHandler, upstreamUrl, log);
|
|
2374
2477
|
}
|
|
2375
|
-
const Route$
|
|
2478
|
+
const Route$g = createFileRoute("/proxy/$")({
|
|
2376
2479
|
server: {
|
|
2377
2480
|
handlers: {
|
|
2378
2481
|
GET: ({ request }) => handleProxy(request),
|
|
@@ -2384,7 +2487,7 @@ const Route$f = createFileRoute("/proxy/$")({
|
|
|
2384
2487
|
}
|
|
2385
2488
|
}
|
|
2386
2489
|
});
|
|
2387
|
-
const Route$
|
|
2490
|
+
const Route$f = createFileRoute("/api/sessions")({
|
|
2388
2491
|
server: {
|
|
2389
2492
|
handlers: {
|
|
2390
2493
|
GET: () => Response.json(getSessions())
|
|
@@ -2394,14 +2497,15 @@ const Route$e = createFileRoute("/api/sessions")({
|
|
|
2394
2497
|
const ProviderInputSchema = object({
|
|
2395
2498
|
name: string().min(1, "Name is required"),
|
|
2396
2499
|
apiKey: string().min(1, "API key is required"),
|
|
2397
|
-
format: _enum(["anthropic", "openai"]),
|
|
2500
|
+
format: _enum(["anthropic", "openai"]).optional(),
|
|
2398
2501
|
anthropicBaseUrl: string().optional(),
|
|
2399
2502
|
openaiBaseUrl: string().optional(),
|
|
2400
2503
|
model: string().min(1, "Model is required"),
|
|
2401
2504
|
authHeader: _enum(["bearer", "x-api-key"]).optional().default("bearer"),
|
|
2402
|
-
apiDocsUrl: string().optional()
|
|
2505
|
+
apiDocsUrl: string().optional(),
|
|
2506
|
+
source: _enum(["company", "personal"]).optional()
|
|
2403
2507
|
});
|
|
2404
|
-
const Route$
|
|
2508
|
+
const Route$e = createFileRoute("/api/providers")({
|
|
2405
2509
|
server: {
|
|
2406
2510
|
handlers: {
|
|
2407
2511
|
GET: () => {
|
|
@@ -2416,23 +2520,542 @@ const Route$d = createFileRoute("/api/providers")({
|
|
|
2416
2520
|
parsed.data.name,
|
|
2417
2521
|
parsed.data.apiKey,
|
|
2418
2522
|
parsed.data.format,
|
|
2419
|
-
|
|
2523
|
+
void 0,
|
|
2524
|
+
// baseUrl (legacy) — use format-specific URLs instead
|
|
2420
2525
|
parsed.data.model,
|
|
2421
2526
|
parsed.data.authHeader,
|
|
2422
|
-
parsed.data.apiDocsUrl
|
|
2527
|
+
parsed.data.apiDocsUrl,
|
|
2528
|
+
parsed.data.anthropicBaseUrl,
|
|
2529
|
+
parsed.data.openaiBaseUrl,
|
|
2530
|
+
parsed.data.source
|
|
2423
2531
|
);
|
|
2424
2532
|
return Response.json(newProvider, { status: 201 });
|
|
2425
2533
|
}
|
|
2426
2534
|
}
|
|
2427
2535
|
}
|
|
2428
2536
|
});
|
|
2429
|
-
const Route$
|
|
2537
|
+
const Route$d = createFileRoute("/api/models")({
|
|
2430
2538
|
server: {
|
|
2431
2539
|
handlers: {
|
|
2432
2540
|
GET: () => Response.json(getModels())
|
|
2433
2541
|
}
|
|
2434
2542
|
}
|
|
2435
2543
|
});
|
|
2544
|
+
let overridePort = null;
|
|
2545
|
+
function setCurrentPort(port) {
|
|
2546
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
2547
|
+
throw new Error(`setCurrentPort: invalid port ${port}`);
|
|
2548
|
+
}
|
|
2549
|
+
overridePort = port;
|
|
2550
|
+
}
|
|
2551
|
+
function getCurrentPort() {
|
|
2552
|
+
if (overridePort !== null) return overridePort;
|
|
2553
|
+
const envPort = process.env["PORT"];
|
|
2554
|
+
if (envPort !== void 0 && envPort !== "") {
|
|
2555
|
+
const n = Number(envPort);
|
|
2556
|
+
if (Number.isInteger(n) && n > 0 && n <= 65535) return n;
|
|
2557
|
+
}
|
|
2558
|
+
throw new Error(
|
|
2559
|
+
"Inspector server port not initialized: PORT env var is unset and setCurrentPort() has not been called"
|
|
2560
|
+
);
|
|
2561
|
+
}
|
|
2562
|
+
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
2563
|
+
class LoopbackTimeoutError extends Error {
|
|
2564
|
+
constructor(path2, timeoutMs) {
|
|
2565
|
+
super(`Loopback call to ${path2} timed out after ${timeoutMs}ms`);
|
|
2566
|
+
this.path = path2;
|
|
2567
|
+
this.timeoutMs = timeoutMs;
|
|
2568
|
+
this.name = "LoopbackTimeoutError";
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
async function callApi(path2, options = {}) {
|
|
2572
|
+
if (!path2.startsWith("/")) {
|
|
2573
|
+
throw new Error(`callApi: path must start with '/', got: ${path2}`);
|
|
2574
|
+
}
|
|
2575
|
+
const port = getCurrentPort();
|
|
2576
|
+
const url = `http://127.0.0.1:${port}${path2}`;
|
|
2577
|
+
const { timeoutMs = DEFAULT_TIMEOUT_MS, signal: userSignal, ...rest } = options;
|
|
2578
|
+
const controller = new AbortController();
|
|
2579
|
+
const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
|
|
2580
|
+
if (userSignal !== void 0 && userSignal !== null) {
|
|
2581
|
+
if (userSignal.aborted) {
|
|
2582
|
+
controller.abort();
|
|
2583
|
+
} else {
|
|
2584
|
+
userSignal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
try {
|
|
2588
|
+
return await fetch(url, { ...rest, signal: controller.signal });
|
|
2589
|
+
} catch (err) {
|
|
2590
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
2591
|
+
throw new LoopbackTimeoutError(path2, timeoutMs);
|
|
2592
|
+
}
|
|
2593
|
+
throw err;
|
|
2594
|
+
} finally {
|
|
2595
|
+
clearTimeout(timeoutHandle);
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
const PREVIEW_MAX_CHARS = 500;
|
|
2599
|
+
function truncate(text) {
|
|
2600
|
+
return text.length <= PREVIEW_MAX_CHARS ? text : text.slice(0, PREVIEW_MAX_CHARS);
|
|
2601
|
+
}
|
|
2602
|
+
function firstAnthropicText(content) {
|
|
2603
|
+
if (typeof content === "string") {
|
|
2604
|
+
return content.length > 0 ? content : null;
|
|
2605
|
+
}
|
|
2606
|
+
for (const block of content) {
|
|
2607
|
+
if (block.type === "text" && typeof block.text === "string" && block.text.length > 0) {
|
|
2608
|
+
return block.text;
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
return null;
|
|
2612
|
+
}
|
|
2613
|
+
function firstOpenAIText(content) {
|
|
2614
|
+
if (content === null || content === void 0) return null;
|
|
2615
|
+
if (typeof content === "string") {
|
|
2616
|
+
return content.length > 0 ? content : null;
|
|
2617
|
+
}
|
|
2618
|
+
for (const part of content) {
|
|
2619
|
+
if (part.type === "text" && typeof part.text === "string" && part.text.length > 0) {
|
|
2620
|
+
return part.text;
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
return null;
|
|
2624
|
+
}
|
|
2625
|
+
function extractLastUserMessagePreview(log) {
|
|
2626
|
+
if (log.apiFormat === "unknown") return null;
|
|
2627
|
+
if (log.rawRequestBody === null) return null;
|
|
2628
|
+
let json;
|
|
2629
|
+
try {
|
|
2630
|
+
json = JSON.parse(log.rawRequestBody);
|
|
2631
|
+
} catch {
|
|
2632
|
+
return null;
|
|
2633
|
+
}
|
|
2634
|
+
if (log.apiFormat === "anthropic") {
|
|
2635
|
+
const parsed = AnthropicRequestSchema.safeParse(json);
|
|
2636
|
+
if (!parsed.success) return null;
|
|
2637
|
+
for (let i = parsed.data.messages.length - 1; i >= 0; i--) {
|
|
2638
|
+
const msg = parsed.data.messages[i];
|
|
2639
|
+
if (msg && msg.role === "user") {
|
|
2640
|
+
const text = firstAnthropicText(msg.content);
|
|
2641
|
+
return text === null ? null : truncate(text);
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
return null;
|
|
2645
|
+
}
|
|
2646
|
+
if (log.apiFormat === "openai") {
|
|
2647
|
+
const parsed = OpenAIRequestSchema.safeParse(json);
|
|
2648
|
+
if (!parsed.success) return null;
|
|
2649
|
+
for (let i = parsed.data.messages.length - 1; i >= 0; i--) {
|
|
2650
|
+
const msg = parsed.data.messages[i];
|
|
2651
|
+
if (msg && msg.role === "user") {
|
|
2652
|
+
const text = firstOpenAIText(msg.content);
|
|
2653
|
+
return text === null ? null : truncate(text);
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
return null;
|
|
2657
|
+
}
|
|
2658
|
+
return null;
|
|
2659
|
+
}
|
|
2660
|
+
function extractResponsePreview(log) {
|
|
2661
|
+
if (log.apiFormat === "unknown") return null;
|
|
2662
|
+
if (log.responseText === null) return null;
|
|
2663
|
+
let json;
|
|
2664
|
+
try {
|
|
2665
|
+
json = JSON.parse(log.responseText);
|
|
2666
|
+
} catch {
|
|
2667
|
+
return null;
|
|
2668
|
+
}
|
|
2669
|
+
if (log.apiFormat === "anthropic") {
|
|
2670
|
+
const parsed = AnthropicResponseSchema$1.safeParse(json);
|
|
2671
|
+
if (!parsed.success) return null;
|
|
2672
|
+
for (const block of parsed.data.content) {
|
|
2673
|
+
if (block.type === "text" && typeof block.text === "string" && block.text.length > 0) {
|
|
2674
|
+
return truncate(block.text);
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
return null;
|
|
2678
|
+
}
|
|
2679
|
+
if (log.apiFormat === "openai") {
|
|
2680
|
+
const parsed = OpenAIResponseSchema$1.safeParse(json);
|
|
2681
|
+
if (!parsed.success) return null;
|
|
2682
|
+
for (const choice of parsed.data.choices) {
|
|
2683
|
+
const msg = choice.message;
|
|
2684
|
+
if (!msg) continue;
|
|
2685
|
+
const direct = firstOpenAIText(msg.content);
|
|
2686
|
+
if (direct !== null) return truncate(direct);
|
|
2687
|
+
const fallback = msg.reasoning_content ?? msg.thinking ?? msg.think;
|
|
2688
|
+
if (typeof fallback === "string" && fallback.length > 0) return truncate(fallback);
|
|
2689
|
+
}
|
|
2690
|
+
return null;
|
|
2691
|
+
}
|
|
2692
|
+
return null;
|
|
2693
|
+
}
|
|
2694
|
+
const LogsListResponseSchema = object({
|
|
2695
|
+
logs: array(CapturedLogSchema).optional(),
|
|
2696
|
+
total: number().optional(),
|
|
2697
|
+
offset: number().optional(),
|
|
2698
|
+
limit: number().optional()
|
|
2699
|
+
});
|
|
2700
|
+
const PAGINATION_DEFAULTS = { offset: 0, limit: 3 };
|
|
2701
|
+
const LIMIT_HARD_CAP = 5;
|
|
2702
|
+
function clampLimit(input) {
|
|
2703
|
+
if (input === void 0) return PAGINATION_DEFAULTS.limit;
|
|
2704
|
+
if (!Number.isFinite(input) || input < 1) return 1;
|
|
2705
|
+
return Math.min(Math.floor(input), LIMIT_HARD_CAP);
|
|
2706
|
+
}
|
|
2707
|
+
function textJson(data) {
|
|
2708
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
2709
|
+
}
|
|
2710
|
+
function toolError(message) {
|
|
2711
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
2712
|
+
}
|
|
2713
|
+
function buildLogSummary(log) {
|
|
2714
|
+
const hasError = log.error !== null && log.error !== void 0 && log.error.length > 0 || log.responseStatus !== null && log.responseStatus !== void 0 && log.responseStatus >= 400;
|
|
2715
|
+
return {
|
|
2716
|
+
id: log.id,
|
|
2717
|
+
timestamp: log.timestamp,
|
|
2718
|
+
provider: log.providerName ?? null,
|
|
2719
|
+
model: log.model,
|
|
2720
|
+
apiFormat: log.apiFormat,
|
|
2721
|
+
method: log.method,
|
|
2722
|
+
path: log.path,
|
|
2723
|
+
status: log.responseStatus,
|
|
2724
|
+
isStreaming: log.streaming,
|
|
2725
|
+
hasError,
|
|
2726
|
+
latencyMs: log.elapsedMs,
|
|
2727
|
+
tokens: {
|
|
2728
|
+
input: log.inputTokens,
|
|
2729
|
+
output: log.outputTokens,
|
|
2730
|
+
cacheCreate: log.cacheCreationInputTokens,
|
|
2731
|
+
cacheRead: log.cacheReadInputTokens
|
|
2732
|
+
},
|
|
2733
|
+
sessionId: log.sessionId,
|
|
2734
|
+
clientPid: log.clientPid ?? null,
|
|
2735
|
+
lastUserMessagePreview: extractLastUserMessagePreview(log),
|
|
2736
|
+
responsePreview: extractResponsePreview(log),
|
|
2737
|
+
hasChunks: log.streamingChunks !== void 0 && log.streamingChunks !== null || log.streamingChunksPath !== null && log.streamingChunksPath !== void 0,
|
|
2738
|
+
hasRawRequestBody: log.rawRequestBody !== null && log.rawRequestBody !== void 0
|
|
2739
|
+
};
|
|
2740
|
+
}
|
|
2741
|
+
async function listLogsImpl(callApi2, args) {
|
|
2742
|
+
const offset = args.offset ?? PAGINATION_DEFAULTS.offset;
|
|
2743
|
+
const limit = clampLimit(args.limit);
|
|
2744
|
+
const params = new URLSearchParams({ offset: String(offset), limit: String(limit) });
|
|
2745
|
+
if (args.sessionId !== void 0 && args.sessionId.length > 0) {
|
|
2746
|
+
params.set("sessionId", args.sessionId);
|
|
2747
|
+
}
|
|
2748
|
+
if (args.model !== void 0 && args.model.length > 0) {
|
|
2749
|
+
params.set("model", args.model);
|
|
2750
|
+
}
|
|
2751
|
+
const res = await callApi2(`/api/logs?${params.toString()}`);
|
|
2752
|
+
if (!res.ok) return toolError(`GET /api/logs returned ${res.status}`);
|
|
2753
|
+
const rawBody = await res.json();
|
|
2754
|
+
const parsed = LogsListResponseSchema.safeParse(rawBody);
|
|
2755
|
+
if (!parsed.success) return toolError("GET /api/logs returned an unparseable shape");
|
|
2756
|
+
const logs = parsed.data.logs ?? [];
|
|
2757
|
+
return textJson(logs.map(buildLogSummary));
|
|
2758
|
+
}
|
|
2759
|
+
async function getLogImpl(callApi2, id) {
|
|
2760
|
+
const res = await callApi2(`/api/logs/${id}`);
|
|
2761
|
+
if (!res.ok) return toolError(`GET /api/logs/${id} returned ${res.status}`);
|
|
2762
|
+
const rawBody = await res.json();
|
|
2763
|
+
const parsed = CapturedLogSchema.safeParse(rawBody);
|
|
2764
|
+
if (!parsed.success) {
|
|
2765
|
+
return toolError(`GET /api/logs/${id} returned an unparseable CapturedLog`);
|
|
2766
|
+
}
|
|
2767
|
+
const { streamingChunks: _chunks, ...rest } = parsed.data;
|
|
2768
|
+
return textJson(rest);
|
|
2769
|
+
}
|
|
2770
|
+
async function getLogChunksImpl(callApi2, id) {
|
|
2771
|
+
const res = await callApi2(`/api/logs/${id}/chunks`);
|
|
2772
|
+
if (!res.ok) return toolError(`GET /api/logs/${id}/chunks returned ${res.status}`);
|
|
2773
|
+
return textJson(await res.json());
|
|
2774
|
+
}
|
|
2775
|
+
async function listSessionsImpl(callApi2) {
|
|
2776
|
+
const res = await callApi2("/api/sessions");
|
|
2777
|
+
if (!res.ok) return toolError(`GET /api/sessions returned ${res.status}`);
|
|
2778
|
+
return textJson(await res.json());
|
|
2779
|
+
}
|
|
2780
|
+
async function listModelsImpl(callApi2) {
|
|
2781
|
+
const res = await callApi2("/api/models");
|
|
2782
|
+
if (!res.ok) return toolError(`GET /api/models returned ${res.status}`);
|
|
2783
|
+
return textJson(await res.json());
|
|
2784
|
+
}
|
|
2785
|
+
async function listProvidersImpl(callApi2) {
|
|
2786
|
+
const res = await callApi2("/api/providers");
|
|
2787
|
+
if (!res.ok) return toolError(`GET /api/providers returned ${res.status}`);
|
|
2788
|
+
return textJson(await res.json());
|
|
2789
|
+
}
|
|
2790
|
+
async function getProviderImpl(callApi2, id) {
|
|
2791
|
+
const res = await callApi2(`/api/providers/${encodeURIComponent(id)}`);
|
|
2792
|
+
if (!res.ok) return toolError(`GET /api/providers/${id} returned ${res.status}`);
|
|
2793
|
+
return textJson(await res.json());
|
|
2794
|
+
}
|
|
2795
|
+
async function replayLogImpl(callApi2, args) {
|
|
2796
|
+
const logRes = await callApi2(`/api/logs/${args.id}`);
|
|
2797
|
+
if (!logRes.ok) return toolError(`GET /api/logs/${args.id} returned ${logRes.status}`);
|
|
2798
|
+
const logRaw = await logRes.json();
|
|
2799
|
+
const logParsed = CapturedLogSchema.safeParse(logRaw);
|
|
2800
|
+
if (!logParsed.success) {
|
|
2801
|
+
return toolError(`GET /api/logs/${args.id} returned an unparseable CapturedLog`);
|
|
2802
|
+
}
|
|
2803
|
+
const log = logParsed.data;
|
|
2804
|
+
if (log.rawRequestBody === null) return toolError("Log has no rawRequestBody to replay");
|
|
2805
|
+
const replayRes = await callApi2(`/api/logs/${args.id}/replay`, {
|
|
2806
|
+
method: "POST",
|
|
2807
|
+
headers: { "content-type": "application/json" },
|
|
2808
|
+
body: JSON.stringify({ modifiedBody: log.rawRequestBody })
|
|
2809
|
+
});
|
|
2810
|
+
if (!replayRes.ok) {
|
|
2811
|
+
return toolError(`POST /api/logs/${args.id}/replay returned ${replayRes.status}`);
|
|
2812
|
+
}
|
|
2813
|
+
return textJson(await replayRes.json());
|
|
2814
|
+
}
|
|
2815
|
+
async function addProviderImpl(callApi2, provider) {
|
|
2816
|
+
const res = await callApi2("/api/providers", {
|
|
2817
|
+
method: "POST",
|
|
2818
|
+
headers: { "content-type": "application/json" },
|
|
2819
|
+
body: JSON.stringify(provider)
|
|
2820
|
+
});
|
|
2821
|
+
if (!res.ok) return toolError(`POST /api/providers returned ${res.status}`);
|
|
2822
|
+
return textJson(await res.json());
|
|
2823
|
+
}
|
|
2824
|
+
async function updateProviderImpl(callApi2, input) {
|
|
2825
|
+
const { id, ...patch } = input;
|
|
2826
|
+
const res = await callApi2(`/api/providers/${encodeURIComponent(id)}`, {
|
|
2827
|
+
method: "PUT",
|
|
2828
|
+
headers: { "content-type": "application/json" },
|
|
2829
|
+
body: JSON.stringify(patch)
|
|
2830
|
+
});
|
|
2831
|
+
if (!res.ok) return toolError(`PUT /api/providers/${id} returned ${res.status}`);
|
|
2832
|
+
return textJson(await res.json());
|
|
2833
|
+
}
|
|
2834
|
+
async function testProviderImpl(callApi2, id) {
|
|
2835
|
+
const res = await callApi2(`/api/providers/${encodeURIComponent(id)}/test`, {
|
|
2836
|
+
method: "POST"
|
|
2837
|
+
});
|
|
2838
|
+
if (!res.ok) return toolError(`POST /api/providers/${id}/test returned ${res.status}`);
|
|
2839
|
+
return textJson(await res.json());
|
|
2840
|
+
}
|
|
2841
|
+
async function safeCall(fn) {
|
|
2842
|
+
try {
|
|
2843
|
+
return await fn();
|
|
2844
|
+
} catch (err) {
|
|
2845
|
+
if (err instanceof LoopbackTimeoutError) {
|
|
2846
|
+
return toolError(`Internal loopback call timed out after ${err.timeoutMs}ms: ${err.path}`);
|
|
2847
|
+
}
|
|
2848
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2849
|
+
return toolError(`Tool execution failed: ${message}`);
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
let initialized = null;
|
|
2853
|
+
let initPromise = null;
|
|
2854
|
+
function buildServer() {
|
|
2855
|
+
const server = new McpServer(
|
|
2856
|
+
{ name: "llm-inspector", version: "1.0.0" },
|
|
2857
|
+
{ capabilities: { tools: {} } }
|
|
2858
|
+
);
|
|
2859
|
+
registerTools(server);
|
|
2860
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
2861
|
+
sessionIdGenerator: void 0
|
|
2862
|
+
// stateless — see module docstring
|
|
2863
|
+
});
|
|
2864
|
+
void server.connect(transport);
|
|
2865
|
+
return { server, transport };
|
|
2866
|
+
}
|
|
2867
|
+
async function getServer() {
|
|
2868
|
+
if (initialized !== null) return initialized;
|
|
2869
|
+
if (initPromise === null) {
|
|
2870
|
+
initPromise = Promise.resolve(buildServer()).then((pair) => {
|
|
2871
|
+
initialized = pair;
|
|
2872
|
+
return pair;
|
|
2873
|
+
});
|
|
2874
|
+
}
|
|
2875
|
+
return initPromise;
|
|
2876
|
+
}
|
|
2877
|
+
async function handleMcpRequest(request) {
|
|
2878
|
+
try {
|
|
2879
|
+
const url = new URL(request.url);
|
|
2880
|
+
const port = Number(url.port);
|
|
2881
|
+
if (port > 0) setCurrentPort(port);
|
|
2882
|
+
} catch {
|
|
2883
|
+
}
|
|
2884
|
+
const { transport } = await getServer();
|
|
2885
|
+
return transport.handleRequest(request);
|
|
2886
|
+
}
|
|
2887
|
+
const TOOL_LIST_LOGS_DESC = `List recent captured LLM proxy logs in reverse-chronological order with parsed previews. Useful for "what did I just send?" discovery.
|
|
2888
|
+
|
|
2889
|
+
REFLEXIVE-LOOP WARNING: this list includes the agent's own recent /proxy calls. If results appear to be the agent's own tool calls (e.g., the agent called inspector_list_logs and now sees itself in the results), filter by clientPid (your own PID) or by timestamp on the client side.
|
|
2890
|
+
|
|
2891
|
+
Parameters:
|
|
2892
|
+
- offset (number, default 0): skip this many entries from the start of the filtered list
|
|
2893
|
+
- limit (number, default 3, hard-clamped to 5): how many summaries to return
|
|
2894
|
+
- sessionId (string, optional): filter by session/affinity id
|
|
2895
|
+
- model (string, optional): filter by model name
|
|
2896
|
+
|
|
2897
|
+
Returns: array of "thick summary" objects, each containing:
|
|
2898
|
+
- id, timestamp, provider, model, apiFormat, method, path, status, isStreaming,
|
|
2899
|
+
hasError, latencyMs, tokens { input, output, cacheCreate, cacheRead },
|
|
2900
|
+
sessionId, clientPid,
|
|
2901
|
+
- lastUserMessagePreview (string|null, ≤500 chars) — first text block of the last user message, parsed via the format-specific schema (NOT a raw body slice)
|
|
2902
|
+
- responsePreview (string|null, ≤500 chars) — first text block of the assistant response
|
|
2903
|
+
- hasChunks, hasRawRequestBody (booleans)
|
|
2904
|
+
|
|
2905
|
+
Either preview field is null when the body is unknown format, unparseable, or the relevant content is non-text (e.g., image-only).`;
|
|
2906
|
+
const PROVIDER_WRITE_WARNING = "⚠ This tool mutates provider configuration. Confirm with the user before invoking.";
|
|
2907
|
+
function registerTools(server) {
|
|
2908
|
+
server.registerTool(
|
|
2909
|
+
"inspector_list_logs",
|
|
2910
|
+
{
|
|
2911
|
+
title: "List captured LLM logs",
|
|
2912
|
+
description: TOOL_LIST_LOGS_DESC,
|
|
2913
|
+
inputSchema: object({
|
|
2914
|
+
offset: number().int().nonnegative().optional().describe("Skip this many entries."),
|
|
2915
|
+
limit: number().int().positive().optional().describe("How many summaries to return. Hard-capped at 5."),
|
|
2916
|
+
sessionId: string().optional().describe("Filter by session/affinity id."),
|
|
2917
|
+
model: string().optional().describe("Filter by model name.")
|
|
2918
|
+
})
|
|
2919
|
+
},
|
|
2920
|
+
(args) => safeCall(() => listLogsImpl(callApi, args))
|
|
2921
|
+
);
|
|
2922
|
+
server.registerTool(
|
|
2923
|
+
"inspector_get_log",
|
|
2924
|
+
{
|
|
2925
|
+
title: "Get a single captured log by id",
|
|
2926
|
+
description: "Returns the full CapturedLog for the given id, including rawRequestBody and responseText with no truncation. SSE streaming chunks are NOT included — use inspector_get_log_chunks for those. Returns an error if the id does not exist.",
|
|
2927
|
+
inputSchema: object({
|
|
2928
|
+
id: number().int().positive().describe("The log id.")
|
|
2929
|
+
})
|
|
2930
|
+
},
|
|
2931
|
+
({ id }) => safeCall(() => getLogImpl(callApi, id))
|
|
2932
|
+
);
|
|
2933
|
+
server.registerTool(
|
|
2934
|
+
"inspector_get_log_chunks",
|
|
2935
|
+
{
|
|
2936
|
+
title: "Get SSE streaming chunks for a captured log",
|
|
2937
|
+
description: "Returns the SSE chunks array for a streaming log, in the order they were received. Returns an error if the log has no chunks (i.e. was non-streaming or the log id is unknown).",
|
|
2938
|
+
inputSchema: object({
|
|
2939
|
+
id: number().int().positive().describe("The log id.")
|
|
2940
|
+
})
|
|
2941
|
+
},
|
|
2942
|
+
({ id }) => safeCall(() => getLogChunksImpl(callApi, id))
|
|
2943
|
+
);
|
|
2944
|
+
server.registerTool(
|
|
2945
|
+
"inspector_list_sessions",
|
|
2946
|
+
{
|
|
2947
|
+
title: "List known session ids",
|
|
2948
|
+
description: "Returns the array of session ids that have been observed in captured logs. Useful for discovering what sessionId values to pass to inspector_list_logs.",
|
|
2949
|
+
inputSchema: object({})
|
|
2950
|
+
},
|
|
2951
|
+
() => safeCall(() => listSessionsImpl(callApi))
|
|
2952
|
+
);
|
|
2953
|
+
server.registerTool(
|
|
2954
|
+
"inspector_list_models",
|
|
2955
|
+
{
|
|
2956
|
+
title: "List distinct model names seen in captured logs",
|
|
2957
|
+
description: "Returns the array of distinct model names observed across all captured logs.",
|
|
2958
|
+
inputSchema: object({})
|
|
2959
|
+
},
|
|
2960
|
+
() => safeCall(() => listModelsImpl(callApi))
|
|
2961
|
+
);
|
|
2962
|
+
server.registerTool(
|
|
2963
|
+
"inspector_list_providers",
|
|
2964
|
+
{
|
|
2965
|
+
title: "List configured LLM providers",
|
|
2966
|
+
description: "Returns the full ProviderConfig array, including apiKey in PLAINTEXT. MCP is localhost-only — any process that can call /api/mcp can also read <dataDir>/providers.json directly, so the apiKey is not redacted. Do not expose this MCP server to non-trusted local processes.",
|
|
2967
|
+
inputSchema: object({})
|
|
2968
|
+
},
|
|
2969
|
+
() => safeCall(() => listProvidersImpl(callApi))
|
|
2970
|
+
);
|
|
2971
|
+
server.registerTool(
|
|
2972
|
+
"inspector_get_provider",
|
|
2973
|
+
{
|
|
2974
|
+
title: "Get a single provider by id",
|
|
2975
|
+
description: "Returns the full ProviderConfig for the given id, including apiKey in PLAINTEXT (same posture as inspector_list_providers).",
|
|
2976
|
+
inputSchema: object({
|
|
2977
|
+
id: string().describe("The provider id.")
|
|
2978
|
+
})
|
|
2979
|
+
},
|
|
2980
|
+
({ id }) => safeCall(() => getProviderImpl(callApi, id))
|
|
2981
|
+
);
|
|
2982
|
+
server.registerTool(
|
|
2983
|
+
"inspector_replay_log",
|
|
2984
|
+
{
|
|
2985
|
+
title: "Replay a captured log against its provider",
|
|
2986
|
+
description: "Re-sends the captured request body to the upstream LLM and returns the response summary: success flag, status, responseText, input/output token counts, elapsedMs, and whether the response was streaming. Useful for re-running a request after a fix or a transient failure. Returns an error if the log id is unknown or the upstream call fails.",
|
|
2987
|
+
inputSchema: object({
|
|
2988
|
+
id: number().int().positive().describe("The log id to replay.")
|
|
2989
|
+
})
|
|
2990
|
+
},
|
|
2991
|
+
(args) => safeCall(() => replayLogImpl(callApi, args))
|
|
2992
|
+
);
|
|
2993
|
+
server.registerTool(
|
|
2994
|
+
"inspector_add_provider",
|
|
2995
|
+
{
|
|
2996
|
+
title: "Add a new LLM provider",
|
|
2997
|
+
description: `${PROVIDER_WRITE_WARNING}
|
|
2998
|
+
|
|
2999
|
+
Persists a new provider to <dataDir>/providers.json. Required fields: name, apiKey, format (anthropic|openai), model. Optional: baseUrl, authHeader (default: bearer), apiDocsUrl.`,
|
|
3000
|
+
inputSchema: object({
|
|
3001
|
+
name: string().min(1),
|
|
3002
|
+
apiKey: string().min(1),
|
|
3003
|
+
format: _enum(["anthropic", "openai"]),
|
|
3004
|
+
model: string().min(1),
|
|
3005
|
+
anthropicBaseUrl: string().optional(),
|
|
3006
|
+
openaiBaseUrl: string().optional(),
|
|
3007
|
+
authHeader: _enum(["bearer", "x-api-key"]).optional(),
|
|
3008
|
+
apiDocsUrl: string().optional()
|
|
3009
|
+
})
|
|
3010
|
+
},
|
|
3011
|
+
(provider) => safeCall(() => addProviderImpl(callApi, provider))
|
|
3012
|
+
);
|
|
3013
|
+
server.registerTool(
|
|
3014
|
+
"inspector_update_provider",
|
|
3015
|
+
{
|
|
3016
|
+
title: "Update an existing provider",
|
|
3017
|
+
description: `${PROVIDER_WRITE_WARNING} Updating with nonsense values effectively soft-deletes a provider. Confirm with the user before invoking.
|
|
3018
|
+
|
|
3019
|
+
PATCH-style update: only the fields you supply are changed. Returns the updated ProviderConfig.`,
|
|
3020
|
+
inputSchema: object({
|
|
3021
|
+
id: string().describe("The provider id to update."),
|
|
3022
|
+
name: string().min(1).optional(),
|
|
3023
|
+
apiKey: string().min(1).optional(),
|
|
3024
|
+
format: _enum(["anthropic", "openai"]).optional(),
|
|
3025
|
+
model: string().min(1).optional(),
|
|
3026
|
+
baseUrl: string().min(1).optional(),
|
|
3027
|
+
authHeader: _enum(["bearer", "x-api-key"]).optional(),
|
|
3028
|
+
anthropicBaseUrl: string().optional(),
|
|
3029
|
+
openaiBaseUrl: string().optional(),
|
|
3030
|
+
apiDocsUrl: string().optional()
|
|
3031
|
+
})
|
|
3032
|
+
},
|
|
3033
|
+
(input) => safeCall(() => updateProviderImpl(callApi, input))
|
|
3034
|
+
);
|
|
3035
|
+
server.registerTool(
|
|
3036
|
+
"inspector_test_provider",
|
|
3037
|
+
{
|
|
3038
|
+
title: "Test a provider's connectivity",
|
|
3039
|
+
description: "Runs the same connectivity test the UI's 'Test' button does, against both Anthropic and OpenAI endpoints if configured. Returns the per-endpoint success/failure result. Returns an error if the provider id is unknown.",
|
|
3040
|
+
inputSchema: object({
|
|
3041
|
+
id: string().describe("The provider id to test.")
|
|
3042
|
+
})
|
|
3043
|
+
},
|
|
3044
|
+
({ id }) => safeCall(() => testProviderImpl(callApi, id))
|
|
3045
|
+
);
|
|
3046
|
+
}
|
|
3047
|
+
const Route$c = createFileRoute("/api/mcp")({
|
|
3048
|
+
server: {
|
|
3049
|
+
handlers: {
|
|
3050
|
+
// The SDK may issue either POST, GET, or DELETE. TanStack Start only
|
|
3051
|
+
// requires us to declare the methods we accept; routing by method is
|
|
3052
|
+
// handled inside the transport.
|
|
3053
|
+
POST: ({ request }) => handleMcpRequest(request),
|
|
3054
|
+
GET: ({ request }) => handleMcpRequest(request),
|
|
3055
|
+
DELETE: ({ request }) => handleMcpRequest(request)
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
});
|
|
2436
3059
|
const Route$b = createFileRoute("/api/logs")({
|
|
2437
3060
|
server: {
|
|
2438
3061
|
handlers: {
|
|
@@ -2563,7 +3186,8 @@ const ProviderUpdateSchema = object({
|
|
|
2563
3186
|
authHeader: _enum(["bearer", "x-api-key"]).optional(),
|
|
2564
3187
|
anthropicBaseUrl: string().optional(),
|
|
2565
3188
|
openaiBaseUrl: string().optional(),
|
|
2566
|
-
apiDocsUrl: string().optional()
|
|
3189
|
+
apiDocsUrl: string().optional(),
|
|
3190
|
+
source: _enum(["company", "personal"]).optional()
|
|
2567
3191
|
});
|
|
2568
3192
|
const Route$6 = createFileRoute("/api/providers/$providerId")({
|
|
2569
3193
|
server: {
|
|
@@ -3472,45 +4096,50 @@ const Route = createFileRoute("/api/logs/$id/chunks")({
|
|
|
3472
4096
|
}
|
|
3473
4097
|
}
|
|
3474
4098
|
});
|
|
3475
|
-
const IndexRoute = Route$
|
|
4099
|
+
const IndexRoute = Route$h.update({
|
|
3476
4100
|
id: "/",
|
|
3477
4101
|
path: "/",
|
|
3478
|
-
getParentRoute: () => Route$
|
|
4102
|
+
getParentRoute: () => Route$i
|
|
3479
4103
|
});
|
|
3480
|
-
const ProxySplatRoute = Route$
|
|
4104
|
+
const ProxySplatRoute = Route$g.update({
|
|
3481
4105
|
id: "/proxy/$",
|
|
3482
4106
|
path: "/proxy/$",
|
|
3483
|
-
getParentRoute: () => Route$
|
|
4107
|
+
getParentRoute: () => Route$i
|
|
3484
4108
|
});
|
|
3485
|
-
const ApiSessionsRoute = Route$
|
|
4109
|
+
const ApiSessionsRoute = Route$f.update({
|
|
3486
4110
|
id: "/api/sessions",
|
|
3487
4111
|
path: "/api/sessions",
|
|
3488
|
-
getParentRoute: () => Route$
|
|
4112
|
+
getParentRoute: () => Route$i
|
|
3489
4113
|
});
|
|
3490
|
-
const ApiProvidersRoute = Route$
|
|
4114
|
+
const ApiProvidersRoute = Route$e.update({
|
|
3491
4115
|
id: "/api/providers",
|
|
3492
4116
|
path: "/api/providers",
|
|
3493
|
-
getParentRoute: () => Route$
|
|
4117
|
+
getParentRoute: () => Route$i
|
|
3494
4118
|
});
|
|
3495
|
-
const ApiModelsRoute = Route$
|
|
4119
|
+
const ApiModelsRoute = Route$d.update({
|
|
3496
4120
|
id: "/api/models",
|
|
3497
4121
|
path: "/api/models",
|
|
3498
|
-
getParentRoute: () => Route$
|
|
4122
|
+
getParentRoute: () => Route$i
|
|
4123
|
+
});
|
|
4124
|
+
const ApiMcpRoute = Route$c.update({
|
|
4125
|
+
id: "/api/mcp",
|
|
4126
|
+
path: "/api/mcp",
|
|
4127
|
+
getParentRoute: () => Route$i
|
|
3499
4128
|
});
|
|
3500
4129
|
const ApiLogsRoute = Route$b.update({
|
|
3501
4130
|
id: "/api/logs",
|
|
3502
4131
|
path: "/api/logs",
|
|
3503
|
-
getParentRoute: () => Route$
|
|
4132
|
+
getParentRoute: () => Route$i
|
|
3504
4133
|
});
|
|
3505
4134
|
const ApiHealthRoute = Route$a.update({
|
|
3506
4135
|
id: "/api/health",
|
|
3507
4136
|
path: "/api/health",
|
|
3508
|
-
getParentRoute: () => Route$
|
|
4137
|
+
getParentRoute: () => Route$i
|
|
3509
4138
|
});
|
|
3510
4139
|
const ApiConfigRoute = Route$9.update({
|
|
3511
4140
|
id: "/api/config",
|
|
3512
4141
|
path: "/api/config",
|
|
3513
|
-
getParentRoute: () => Route$
|
|
4142
|
+
getParentRoute: () => Route$i
|
|
3514
4143
|
});
|
|
3515
4144
|
const ApiProvidersImportRoute = Route$8.update({
|
|
3516
4145
|
id: "/import",
|
|
@@ -3594,12 +4223,13 @@ const rootRouteChildren = {
|
|
|
3594
4223
|
ApiConfigRoute: ApiConfigRouteWithChildren,
|
|
3595
4224
|
ApiHealthRoute,
|
|
3596
4225
|
ApiLogsRoute: ApiLogsRouteWithChildren,
|
|
4226
|
+
ApiMcpRoute,
|
|
3597
4227
|
ApiModelsRoute,
|
|
3598
4228
|
ApiProvidersRoute: ApiProvidersRouteWithChildren,
|
|
3599
4229
|
ApiSessionsRoute,
|
|
3600
4230
|
ProxySplatRoute
|
|
3601
4231
|
};
|
|
3602
|
-
const routeTree = Route$
|
|
4232
|
+
const routeTree = Route$i._addFileChildren(rootRouteChildren)._addFileTypes();
|
|
3603
4233
|
function getRouter() {
|
|
3604
4234
|
const router2 = createRouter({
|
|
3605
4235
|
routeTree,
|