@tonyclaw/llm-inspector 1.11.4 → 1.11.6

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.
@@ -1,5 +1,5 @@
1
1
  import { r as reactExports, j as jsxRuntimeExports, R as React } from "../_libs/react.mjs";
2
- import { C as CapturedLogSchema, R as RuntimeConfigSchema, a as parseRequest, s as stripClaudeCodeBillingHeader, p as parseOpenAIResponse, I as InspectorResponseSchema } from "./router-DRA0j7Zv.mjs";
2
+ import { C as CapturedLogSchema, R as RuntimeConfigSchema, a as parseRequest, s as stripClaudeCodeBillingHeader, p as parseOpenAIResponse, I as InspectorResponseSchema } from "./router-BOZDxiEC.mjs";
3
3
  import { u as useSWR, a as useSWRConfig } from "../_libs/swr.mjs";
4
4
  import { u as useVirtualizer } from "../_libs/tanstack__react-virtual.mjs";
5
5
  import { J as JSZip } from "../_libs/jszip.mjs";
@@ -255,7 +255,7 @@ async function exportLogsAsZip(logs) {
255
255
  document.body.removeChild(anchor);
256
256
  URL.revokeObjectURL(url);
257
257
  }
258
- const version = "1.11.4";
258
+ const version = "1.11.6";
259
259
  const packageJson = {
260
260
  version
261
261
  };
@@ -2799,9 +2799,50 @@ function TestStatus({ result }) {
2799
2799
  ] });
2800
2800
  }
2801
2801
  if (result.success) {
2802
+ const tokenParts = [];
2803
+ if (result.inputTokens !== void 0) {
2804
+ tokenParts.push(/* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [
2805
+ result.inputTokens,
2806
+ " in"
2807
+ ] }, "in"));
2808
+ }
2809
+ if (result.outputTokens !== void 0) {
2810
+ tokenParts.push(/* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [
2811
+ result.outputTokens,
2812
+ " out"
2813
+ ] }, "out"));
2814
+ }
2815
+ if (result.cacheCreationInputTokens !== void 0 && result.cacheCreationInputTokens > 0) {
2816
+ tokenParts.push(
2817
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono tabular-nums text-emerald-400", children: [
2818
+ "+",
2819
+ result.cacheCreationInputTokens,
2820
+ " cache"
2821
+ ] }, "cache-create")
2822
+ );
2823
+ }
2824
+ if (result.cacheReadInputTokens !== void 0 && result.cacheReadInputTokens > 0) {
2825
+ tokenParts.push(
2826
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono tabular-nums text-purple-400", children: [
2827
+ "~",
2828
+ result.cacheReadInputTokens,
2829
+ " cached"
2830
+ ] }, "cache-read")
2831
+ );
2832
+ }
2833
+ const displayTokens = [];
2834
+ for (let i = 0; i < tokenParts.length; i++) {
2835
+ if (i > 0) displayTokens.push(", ");
2836
+ displayTokens.push(tokenParts[i]);
2837
+ }
2802
2838
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1 text-xs text-green-600 shrink-0", children: [
2803
2839
  /* @__PURE__ */ jsxRuntimeExports.jsx(CircleCheckBig, { className: "size-3" }),
2804
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Connected" })
2840
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Connected" }),
2841
+ tokenParts.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-muted-foreground", children: [
2842
+ "(",
2843
+ displayTokens,
2844
+ ")"
2845
+ ] })
2805
2846
  ] });
2806
2847
  }
2807
2848
  const error = result.error;
@@ -3835,12 +3876,14 @@ function ProxyViewer({
3835
3876
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "max-w-[1200px] mx-auto flex flex-col h-screen", style: { maxHeight: "100vh" }, children: [
3836
3877
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-4 mb-4 px-6 pt-6", children: [
3837
3878
  /* @__PURE__ */ jsxRuntimeExports.jsxs("h1", { className: "text-lg font-bold flex-1 flex items-center gap-2", children: [
3838
- /* @__PURE__ */ jsxRuntimeExports.jsx(CrabLogo, { className: "size-10 text-amber-500" }),
3839
- "LLM Inspector"
3840
- ] }),
3841
- /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-muted-foreground text-xs font-mono", children: [
3842
- "v",
3843
- packageJson.version
3879
+ /* @__PURE__ */ jsxRuntimeExports.jsx(CrabLogo, { className: "size-10 text-amber-500 self-center" }),
3880
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-baseline gap-2", children: [
3881
+ "LLM Inspector",
3882
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-xs text-muted-foreground font-mono", children: [
3883
+ "v",
3884
+ packageJson.version
3885
+ ] })
3886
+ ] })
3844
3887
  ] }),
3845
3888
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center border border-border rounded-md overflow-hidden", children: [
3846
3889
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -85,7 +85,7 @@ function getResponse() {
85
85
  }
86
86
  var HEADERS = { TSS_SHELL: "X-TSS_SHELL" };
87
87
  async function getStartManifest(matchedRoutes) {
88
- const { tsrStartManifest } = await import("../_tanstack-start-manifest_v-DKfW4gRz.mjs");
88
+ const { tsrStartManifest } = await import("../_tanstack-start-manifest_v-C_jssHYZ.mjs");
89
89
  const startManifest = tsrStartManifest();
90
90
  let routes = startManifest.routes;
91
91
  routes[rootRouteId];
@@ -1157,7 +1157,7 @@ var getBaseManifest = getProdBaseManifest;
1157
1157
  var createEarlyHintsForRequest = createEarlyHintsCollector;
1158
1158
  async function loadEntries() {
1159
1159
  const [routerEntry, startEntry, pluginAdapters] = await Promise.all([
1160
- import("./router-DRA0j7Zv.mjs").then((n) => n.r),
1160
+ import("./router-BOZDxiEC.mjs").then((n) => n.r),
1161
1161
  import("./start-HYkvq4Ni.mjs"),
1162
1162
  import("./empty-plugin-adapters-BFgPZ6_d.mjs")
1163
1163
  ]);
@@ -42,7 +42,7 @@ import "../_libs/debounce-fn.mjs";
42
42
  import "../_libs/mimic-function.mjs";
43
43
  import "../_libs/semver.mjs";
44
44
  import "../_libs/uint8array-extras.mjs";
45
- const appCss = "/assets/index-BpKPXEcb.css";
45
+ const appCss = "/assets/index-BGxRftQy.css";
46
46
  const Route$h = createRootRoute({
47
47
  head: () => ({
48
48
  meta: [
@@ -66,7 +66,7 @@ function RootDocument({ children }) {
66
66
  ] })
67
67
  ] });
68
68
  }
69
- const $$splitComponentImporter = () => import("./index-BWMVJy33.mjs");
69
+ const $$splitComponentImporter = () => import("./index-BzG8P6B8.mjs");
70
70
  const Route$g = createFileRoute("/")({
71
71
  component: lazyRouteComponent($$splitComponentImporter, "component")
72
72
  });
@@ -1031,11 +1031,8 @@ class FormatRegistryImpl {
1031
1031
  /** Get handler matching a request path */
1032
1032
  getByPath(path2) {
1033
1033
  const messagesPath = path2.split("?")[0] ?? "";
1034
- if (messagesPath === PATH_V1_MESSAGES) return this.handlers.get("anthropic");
1035
- if (messagesPath === PATH_V1_CHAT_COMPLETIONS || messagesPath === PATH_CHAT_COMPLETIONS) {
1036
- return this.handlers.get("openai");
1037
- }
1038
- return void 0;
1034
+ const format = this.pathMap.get(messagesPath);
1035
+ return format === void 0 ? void 0 : this.handlers.get(format);
1039
1036
  }
1040
1037
  /** Detect format from request body content */
1041
1038
  detectFormat(rawBody) {
@@ -1652,38 +1649,13 @@ const store = new Conf({
1652
1649
  });
1653
1650
  migrateFromLegacyConfLocation(store);
1654
1651
  function migrateProvider(p) {
1655
- const currentAnthropicUrl = p.anthropicBaseUrl ?? "";
1656
- const currentOpenaiUrl = p.openaiBaseUrl ?? "";
1657
- if (currentAnthropicUrl !== "" || currentOpenaiUrl !== "") {
1652
+ if (p.anthropicBaseUrl !== void 0 || p.openaiBaseUrl !== void 0) {
1658
1653
  return p;
1659
1654
  }
1660
- let format;
1661
- let baseUrl;
1662
- if (currentAnthropicUrl !== "" && currentOpenaiUrl !== "") {
1663
- format = p.format ?? "anthropic";
1664
- baseUrl = p.baseUrl !== void 0 && p.baseUrl !== "" ? p.baseUrl : currentAnthropicUrl;
1665
- } else if (currentOpenaiUrl !== "") {
1666
- format = "openai";
1667
- baseUrl = currentOpenaiUrl;
1668
- } else if (currentAnthropicUrl !== "") {
1669
- format = "anthropic";
1670
- baseUrl = currentAnthropicUrl;
1671
- } else if (p.format !== void 0 && p.baseUrl !== void 0 && p.baseUrl !== "") {
1672
- format = p.format;
1673
- baseUrl = p.baseUrl;
1674
- if (format === "openai") {
1675
- return { ...p, format, baseUrl, anthropicBaseUrl: "", openaiBaseUrl: p.baseUrl };
1676
- } else {
1677
- return { ...p, format, baseUrl, anthropicBaseUrl: p.baseUrl, openaiBaseUrl: "" };
1678
- }
1655
+ if (p.format === void 0 || p.baseUrl === void 0 || p.baseUrl === "") {
1656
+ return p;
1679
1657
  }
1680
- return {
1681
- ...p,
1682
- format,
1683
- baseUrl,
1684
- anthropicBaseUrl: currentAnthropicUrl,
1685
- openaiBaseUrl: currentOpenaiUrl
1686
- };
1658
+ return p.format === "openai" ? { ...p, anthropicBaseUrl: "", openaiBaseUrl: p.baseUrl } : { ...p, anthropicBaseUrl: p.baseUrl, openaiBaseUrl: "" };
1687
1659
  }
1688
1660
  function migrateProviders() {
1689
1661
  const providers = store.get("providers", []);
@@ -1856,6 +1828,13 @@ function findProviderByModel(model) {
1856
1828
  if (provider.model !== void 0 && provider.model !== "" && modelNormalized === normalizeModelName(provider.model)) {
1857
1829
  return provider;
1858
1830
  }
1831
+ if (provider.model !== void 0 && provider.model !== "") {
1832
+ const modelPart = modelNormalized.replace(normalizeModelName(provider.name), "");
1833
+ const concatenated = normalizeModelName(provider.name) + modelPart;
1834
+ if (modelNormalized === concatenated) {
1835
+ return provider;
1836
+ }
1837
+ }
1859
1838
  }
1860
1839
  return null;
1861
1840
  }
@@ -2130,6 +2109,52 @@ function stripClaudeCodeBillingHeader(rawBody) {
2130
2109
  }
2131
2110
  return { body: JSON.stringify(parsed), removed };
2132
2111
  }
2112
+ function describeApiRoute(apiPath) {
2113
+ const endpointPath = apiPath.split("?")[0] ?? "";
2114
+ const isChatCompletions = endpointPath === PATH_CHAT_COMPLETIONS || endpointPath === PATH_V1_CHAT_COMPLETIONS;
2115
+ const normalizedPath = isChatCompletions && !apiPath.startsWith("/v1/") ? `/v1${apiPath}` : apiPath;
2116
+ return { apiPath, endpointPath, isChatCompletions, normalizedPath };
2117
+ }
2118
+ function getProxyApiPath(url) {
2119
+ return url.pathname.replace(/^\/proxy/, "") + url.search;
2120
+ }
2121
+ function selectUpstreamBase(route, provider) {
2122
+ if (provider === null) {
2123
+ return route.isChatCompletions ? DEFAULT_OPENAI_UPSTREAM$1 : DEFAULT_UPSTREAM$1;
2124
+ }
2125
+ const formatSpecificUrl = route.isChatCompletions ? provider.openaiBaseUrl : provider.anthropicBaseUrl;
2126
+ if (formatSpecificUrl !== void 0 && formatSpecificUrl !== "") {
2127
+ return formatSpecificUrl;
2128
+ }
2129
+ if (provider.baseUrl !== void 0 && provider.baseUrl !== "") {
2130
+ return provider.baseUrl;
2131
+ }
2132
+ return provider.format === "openai" ? DEFAULT_OPENAI_UPSTREAM$1 : DEFAULT_UPSTREAM$1;
2133
+ }
2134
+ function buildUpstreamUrl(upstreamBase, normalizedPath) {
2135
+ const base = upstreamBase.endsWith("/") ? upstreamBase.slice(0, -1) : upstreamBase;
2136
+ if (base.endsWith("/v1") && normalizedPath.startsWith("/v1/")) {
2137
+ return base + normalizedPath.slice(3);
2138
+ }
2139
+ return base + normalizedPath;
2140
+ }
2141
+ function setUpstreamHost(headers, upstreamBase) {
2142
+ try {
2143
+ headers.set(HEADER_HOST, new URL(upstreamBase).host);
2144
+ } catch {
2145
+ headers.set(HEADER_HOST, "api.anthropic.com");
2146
+ }
2147
+ }
2148
+ function injectProviderAuth(headers, provider) {
2149
+ if (provider === null) return;
2150
+ const apiKey = provider.apiKey.replace(/^Bearer\s+/i, "").trim();
2151
+ if (provider.authHeader === AUTH_HEADER_X_API_KEY) {
2152
+ headers.set(HEADER_X_API_KEY, apiKey);
2153
+ headers.delete(HEADER_AUTHORIZATION);
2154
+ return;
2155
+ }
2156
+ headers.set(HEADER_AUTHORIZATION, `Bearer ${apiKey}`);
2157
+ }
2133
2158
  function buildProxyHeaders(originalHeaders) {
2134
2159
  const rawHeaders = {};
2135
2160
  const headers = new Headers();
@@ -2146,14 +2171,6 @@ function buildProxyHeaders(originalHeaders) {
2146
2171
  }
2147
2172
  return { headers, rawHeaders };
2148
2173
  }
2149
- function getHostFromUrl$1(urlStr) {
2150
- try {
2151
- const url = new URL(urlStr);
2152
- return url.host;
2153
- } catch {
2154
- return "api.anthropic.com";
2155
- }
2156
- }
2157
2174
  function buildFileLogEntry(log, upstreamUrl) {
2158
2175
  return {
2159
2176
  timestamp: log.timestamp,
@@ -2182,55 +2199,14 @@ function buildFileLogEntry(log, upstreamUrl) {
2182
2199
  };
2183
2200
  }
2184
2201
  function parseRequestPath(req, url) {
2185
- const apiPath = url.pathname.replace(/^\/proxy/, "") + url.search;
2186
- const messagesPath = apiPath.split("?")[0] ?? "";
2187
- const isChatCompletionsV1 = messagesPath === PATH_V1_CHAT_COMPLETIONS;
2188
- const isChatCompletions = messagesPath === PATH_CHAT_COMPLETIONS || isChatCompletionsV1;
2189
- const isMessages = req.method === "POST" && (messagesPath === PATH_V1_MESSAGES || messagesPath === PATH_V1_CHAT_COMPLETIONS || messagesPath === PATH_CHAT_COMPLETIONS);
2190
- const normalizedPath = isChatCompletions && !apiPath.startsWith("/v1/") ? "/v1" + apiPath : apiPath;
2202
+ const route = describeApiRoute(getProxyApiPath(url));
2203
+ const isMessages = req.method === "POST" && (route.endpointPath === PATH_V1_MESSAGES || route.isChatCompletions);
2191
2204
  return {
2192
- apiPath,
2193
- messagesPath,
2194
- isChatCompletionsV1,
2195
- isChatCompletions,
2205
+ apiPath: route.apiPath,
2196
2206
  isMessages,
2197
- normalizedPath
2207
+ normalizedPath: route.normalizedPath
2198
2208
  };
2199
2209
  }
2200
- function buildUpstreamUrl$1(upstreamBase, normalizedPath) {
2201
- const base = upstreamBase.endsWith("/") ? upstreamBase.slice(0, -1) : upstreamBase;
2202
- if (base.endsWith("/v1") && normalizedPath.startsWith("/v1/")) {
2203
- return base + normalizedPath.slice(3);
2204
- }
2205
- return base + normalizedPath;
2206
- }
2207
- function selectUpstreamBase$1(isChatCompletions, matchedProviderConfig) {
2208
- let upstreamBase;
2209
- if (matchedProviderConfig) {
2210
- if (isChatCompletions && matchedProviderConfig.openaiBaseUrl !== void 0 && matchedProviderConfig.openaiBaseUrl !== "") {
2211
- upstreamBase = matchedProviderConfig.openaiBaseUrl;
2212
- } else if (!isChatCompletions && matchedProviderConfig.anthropicBaseUrl !== void 0 && matchedProviderConfig.anthropicBaseUrl !== "") {
2213
- upstreamBase = matchedProviderConfig.anthropicBaseUrl;
2214
- } else if (matchedProviderConfig.baseUrl !== void 0 && matchedProviderConfig.baseUrl !== "") {
2215
- upstreamBase = matchedProviderConfig.baseUrl;
2216
- } else {
2217
- upstreamBase = matchedProviderConfig.format === "openai" ? DEFAULT_OPENAI_UPSTREAM$1 : DEFAULT_UPSTREAM$1;
2218
- }
2219
- } else {
2220
- upstreamBase = isChatCompletions ? DEFAULT_OPENAI_UPSTREAM$1 : DEFAULT_UPSTREAM$1;
2221
- }
2222
- return upstreamBase;
2223
- }
2224
- function injectAuthHeaders$1(upstreamHeaders, matchedProviderConfig) {
2225
- if (!matchedProviderConfig) return;
2226
- const apiKey = matchedProviderConfig.apiKey.replace(/^Bearer\s+/i, "").trim();
2227
- if (matchedProviderConfig.authHeader === AUTH_HEADER_X_API_KEY) {
2228
- upstreamHeaders.set(HEADER_X_API_KEY, apiKey);
2229
- upstreamHeaders.delete(HEADER_AUTHORIZATION);
2230
- } else {
2231
- upstreamHeaders.set(HEADER_AUTHORIZATION, `Bearer ${apiKey}`);
2232
- }
2233
- }
2234
2210
  function handleNonStreamingResponse(upstreamRes, responseBody, startTime, formatHandler, upstreamUrl, log) {
2235
2211
  const elapsedMs = Date.now() - startTime;
2236
2212
  const tokens = formatHandler.extractTokens(responseBody);
@@ -2329,26 +2305,19 @@ async function handleProxy(req) {
2329
2305
  }
2330
2306
  const { model, sessionId } = extractRequestMetadata(requestBody, req.headers);
2331
2307
  const matchedProviderConfig = model !== null ? findProviderByModel(model) : null;
2332
- const upstreamBase = selectUpstreamBase$1(parsed.isChatCompletions, matchedProviderConfig);
2333
- const upstreamUrl = buildUpstreamUrl$1(upstreamBase, parsed.normalizedPath);
2334
- const upstreamHost = getHostFromUrl$1(upstreamBase);
2308
+ const route = describeApiRoute(parsed.apiPath);
2309
+ const upstreamBase = selectUpstreamBase(route, matchedProviderConfig);
2310
+ const upstreamUrl = buildUpstreamUrl(upstreamBase, parsed.normalizedPath);
2335
2311
  const startTime = Date.now();
2336
2312
  const { headers: upstreamHeaders, rawHeaders } = buildProxyHeaders(req.headers);
2337
- upstreamHeaders.set(HEADER_HOST, upstreamHost);
2338
- injectAuthHeaders$1(upstreamHeaders, matchedProviderConfig);
2313
+ setUpstreamHost(upstreamHeaders, upstreamBase);
2314
+ injectProviderAuth(upstreamHeaders, matchedProviderConfig);
2339
2315
  const provider = model !== null ? registry.findProvider(model) : null;
2340
2316
  if (model === null || provider === null) {
2341
2317
  logger.warn(`[handler] Unsupported provider: model=${model}`);
2342
2318
  return new Response("Forbidden: unsupported provider", { status: STATUS_FORBIDDEN });
2343
2319
  }
2344
- let formatHandler;
2345
- if (parsed.isChatCompletions) {
2346
- formatHandler = formatRegistry.get("openai") ?? null;
2347
- } else if (matchedProviderConfig?.format) {
2348
- formatHandler = formatRegistry.get(matchedProviderConfig.format) ?? null;
2349
- } else {
2350
- formatHandler = formatForPath(parsed.apiPath);
2351
- }
2320
+ const formatHandler = formatForPath(parsed.apiPath);
2352
2321
  if (formatHandler === null) {
2353
2322
  return new Response("Forbidden: unsupported format", { status: STATUS_FORBIDDEN });
2354
2323
  }
@@ -2837,12 +2806,15 @@ const AnthropicResponseSchema = object({
2837
2806
  model: string().optional(),
2838
2807
  usage: object({
2839
2808
  input_tokens: number().optional(),
2840
- output_tokens: number().optional()
2809
+ output_tokens: number().optional(),
2810
+ cache_creation_input_tokens: number().optional(),
2811
+ cache_read_input_tokens: number().optional()
2841
2812
  }).optional(),
2842
2813
  content: array(
2843
2814
  discriminatedUnion("type", [
2844
2815
  object({ type: literal("text"), text: string() }),
2845
- object({ type: literal("thinking"), thinking: string() })
2816
+ object({ type: literal("thinking"), thinking: string() }),
2817
+ object({ type: literal("think"), thinking: string() })
2846
2818
  ])
2847
2819
  )
2848
2820
  });
@@ -2918,11 +2890,13 @@ async function testEndpoint(baseUrl, apiKey, path2, model, isOpenAI) {
2918
2890
  const responseModel = json.model ?? model;
2919
2891
  const inputTokens = json.usage?.input_tokens;
2920
2892
  const outputTokens = json.usage?.output_tokens;
2893
+ const cacheCreationInputTokens = json.usage?.cache_creation_input_tokens;
2894
+ const cacheReadInputTokens = json.usage?.cache_read_input_tokens;
2921
2895
  const content = [];
2922
2896
  for (const block of json.content ?? []) {
2923
2897
  if (block.type === "text" && block.text) {
2924
2898
  content.push({ type: "text", text: block.text });
2925
- } else if (block.type === "thinking" && block.thinking) {
2899
+ } else if ((block.type === "thinking" || block.type === "think") && block.thinking) {
2926
2900
  content.push({ type: "thinking", thinking: block.thinking });
2927
2901
  }
2928
2902
  }
@@ -2931,6 +2905,8 @@ async function testEndpoint(baseUrl, apiKey, path2, model, isOpenAI) {
2931
2905
  model: responseModel,
2932
2906
  inputTokens,
2933
2907
  outputTokens,
2908
+ cacheCreationInputTokens,
2909
+ cacheReadInputTokens,
2934
2910
  latencyMs,
2935
2911
  content: content.length > 0 ? content : void 0,
2936
2912
  rawResponse: responseText,
@@ -3056,6 +3032,8 @@ async function testStreamingEndpoint(baseUrl, apiKey, path2, model, isOpenAI) {
3056
3032
  let content = [];
3057
3033
  let inputTokens;
3058
3034
  let outputTokens;
3035
+ let cacheCreationInputTokens;
3036
+ let cacheReadInputTokens;
3059
3037
  let responseModel;
3060
3038
  if (isOpenAI) {
3061
3039
  const parsed = OpenAIResponseSchema.parse(JSON.parse(reconstructedJson));
@@ -3071,10 +3049,12 @@ async function testStreamingEndpoint(baseUrl, apiKey, path2, model, isOpenAI) {
3071
3049
  responseModel = typeof parsed.model === "string" && parsed.model !== "" ? parsed.model : void 0;
3072
3050
  inputTokens = parsed.usage?.input_tokens;
3073
3051
  outputTokens = parsed.usage?.output_tokens;
3052
+ cacheCreationInputTokens = parsed.usage?.cache_creation_input_tokens;
3053
+ cacheReadInputTokens = parsed.usage?.cache_read_input_tokens;
3074
3054
  for (const block of parsed.content ?? []) {
3075
3055
  if (block.type === "text" && block.text) {
3076
3056
  content.push({ type: "text", text: block.text });
3077
- } else if (block.type === "thinking" && block.thinking) {
3057
+ } else if ((block.type === "thinking" || block.type === "think") && block.thinking) {
3078
3058
  content.push({ type: "thinking", thinking: block.thinking });
3079
3059
  }
3080
3060
  }
@@ -3084,6 +3064,8 @@ async function testStreamingEndpoint(baseUrl, apiKey, path2, model, isOpenAI) {
3084
3064
  model: responseModel ?? model,
3085
3065
  inputTokens,
3086
3066
  outputTokens,
3067
+ cacheCreationInputTokens,
3068
+ cacheReadInputTokens,
3087
3069
  latencyMs,
3088
3070
  content: content.length > 0 ? content : void 0,
3089
3071
  rawResponse: reconstructedJson,
@@ -3178,8 +3160,8 @@ const Route$2 = createFileRoute("/api/providers/$providerId/test")({
3178
3160
  responseText: nonStreamingResult.rawResponse ?? JSON.stringify(nonStreamingResult),
3179
3161
  inputTokens: nonStreamingResult.inputTokens ?? null,
3180
3162
  outputTokens: nonStreamingResult.outputTokens ?? null,
3181
- cacheCreationInputTokens: null,
3182
- cacheReadInputTokens: null,
3163
+ cacheCreationInputTokens: nonStreamingResult.cacheCreationInputTokens ?? null,
3164
+ cacheReadInputTokens: nonStreamingResult.cacheReadInputTokens ?? null,
3183
3165
  elapsedMs: nonStreamingResult.latencyMs ?? 0,
3184
3166
  streaming: false,
3185
3167
  userAgent: "provider-test",
@@ -3224,8 +3206,8 @@ const Route$2 = createFileRoute("/api/providers/$providerId/test")({
3224
3206
  responseText: streamingResult.rawResponse ?? JSON.stringify(streamingResult),
3225
3207
  inputTokens: streamingResult.inputTokens ?? null,
3226
3208
  outputTokens: streamingResult.outputTokens ?? null,
3227
- cacheCreationInputTokens: null,
3228
- cacheReadInputTokens: null,
3209
+ cacheCreationInputTokens: streamingResult.cacheCreationInputTokens ?? null,
3210
+ cacheReadInputTokens: streamingResult.cacheReadInputTokens ?? null,
3229
3211
  elapsedMs: streamingResult.latencyMs ?? 0,
3230
3212
  streaming: true,
3231
3213
  streamingChunks: streamingResult.streamingChunks,
@@ -3350,45 +3332,6 @@ const Route$2 = createFileRoute("/api/providers/$providerId/test")({
3350
3332
  const ReplayRequestSchema = object({
3351
3333
  modifiedBody: string()
3352
3334
  });
3353
- function getHostFromUrl(urlStr) {
3354
- try {
3355
- const url = new URL(urlStr);
3356
- return url.host;
3357
- } catch {
3358
- return "api.anthropic.com";
3359
- }
3360
- }
3361
- function buildUpstreamUrl(upstreamBase, apiPath) {
3362
- const base = upstreamBase.endsWith("/") ? upstreamBase.slice(0, -1) : upstreamBase;
3363
- return base + apiPath;
3364
- }
3365
- function selectUpstreamBase(isChatCompletions, matchedProviderConfig) {
3366
- let upstreamBase;
3367
- if (matchedProviderConfig) {
3368
- if (isChatCompletions && matchedProviderConfig.openaiBaseUrl !== void 0 && matchedProviderConfig.openaiBaseUrl !== "") {
3369
- upstreamBase = matchedProviderConfig.openaiBaseUrl;
3370
- } else if (!isChatCompletions && matchedProviderConfig.anthropicBaseUrl !== void 0 && matchedProviderConfig.anthropicBaseUrl !== "") {
3371
- upstreamBase = matchedProviderConfig.anthropicBaseUrl;
3372
- } else if (matchedProviderConfig.baseUrl !== void 0 && matchedProviderConfig.baseUrl !== "") {
3373
- upstreamBase = matchedProviderConfig.baseUrl;
3374
- } else {
3375
- upstreamBase = matchedProviderConfig.format === "openai" ? DEFAULT_OPENAI_UPSTREAM$1 : DEFAULT_UPSTREAM$1;
3376
- }
3377
- } else {
3378
- upstreamBase = isChatCompletions ? DEFAULT_OPENAI_UPSTREAM$1 : DEFAULT_UPSTREAM$1;
3379
- }
3380
- return upstreamBase;
3381
- }
3382
- function injectAuthHeaders(upstreamHeaders, matchedProviderConfig) {
3383
- if (!matchedProviderConfig) return;
3384
- const apiKey = matchedProviderConfig.apiKey.replace(/^Bearer\s+/i, "").trim();
3385
- if (matchedProviderConfig.authHeader === AUTH_HEADER_X_API_KEY) {
3386
- upstreamHeaders.set(HEADER_X_API_KEY, apiKey);
3387
- upstreamHeaders.delete(HEADER_AUTHORIZATION);
3388
- } else {
3389
- upstreamHeaders.set(HEADER_AUTHORIZATION, `Bearer ${apiKey}`);
3390
- }
3391
- }
3392
3335
  const Route$1 = createFileRoute("/api/logs/$id/replay")({
3393
3336
  server: {
3394
3337
  handlers: {
@@ -3428,16 +3371,13 @@ const Route$1 = createFileRoute("/api/logs/$id/replay")({
3428
3371
  { status: STATUS_FORBIDDEN }
3429
3372
  );
3430
3373
  }
3431
- const apiPath = log.path;
3432
- const isChatCompletions = apiPath === PATH_V1_CHAT_COMPLETIONS || apiPath === PATH_CHAT_COMPLETIONS;
3433
- const normalizedPath = isChatCompletions && !apiPath.startsWith("/v1/") ? "/v1" + apiPath : apiPath;
3434
- const formatHandler = formatForPath(apiPath);
3374
+ const route = describeApiRoute(log.path);
3375
+ const formatHandler = formatForPath(route.apiPath);
3435
3376
  if (formatHandler === null) {
3436
3377
  return Response.json({ error: "Unsupported path" }, { status: STATUS_FORBIDDEN });
3437
3378
  }
3438
- const upstreamBase = selectUpstreamBase(isChatCompletions, matchedProviderConfig);
3439
- const upstreamUrl = buildUpstreamUrl(upstreamBase, normalizedPath);
3440
- const upstreamHost = getHostFromUrl(upstreamBase);
3379
+ const upstreamBase = selectUpstreamBase(route, matchedProviderConfig);
3380
+ const upstreamUrl = buildUpstreamUrl(upstreamBase, route.normalizedPath);
3441
3381
  const headers = new Headers();
3442
3382
  headers.set(HEADER_USER_AGENT, PROXY_IDENTITY);
3443
3383
  headers.set(HEADER_X_PROXY_IDENTITY, PROXY_IDENTITY);
@@ -3448,8 +3388,8 @@ const Route$1 = createFileRoute("/api/logs/$id/replay")({
3448
3388
  headers.set(name, value);
3449
3389
  }
3450
3390
  }
3451
- headers.set(HEADER_HOST, upstreamHost);
3452
- injectAuthHeaders(headers, matchedProviderConfig);
3391
+ setUpstreamHost(headers, upstreamBase);
3392
+ injectProviderAuth(headers, matchedProviderConfig);
3453
3393
  const startTime = Date.now();
3454
3394
  let upstreamRes;
3455
3395
  try {
@@ -3465,7 +3405,7 @@ const Route$1 = createFileRoute("/api/logs/$id/replay")({
3465
3405
  });
3466
3406
  }
3467
3407
  const elapsedMs = Date.now() - startTime;
3468
- const isStream = upstreamRes.headers.get(HEADER_CONTENT_TYPE)?.includes("text/event-stream") ?? false;
3408
+ const isStream = upstreamRes.headers.get(HEADER_CONTENT_TYPE)?.includes(CONTENT_TYPE_EVENT_STREAM) ?? false;
3469
3409
  if (isStream) {
3470
3410
  const chunks = [];
3471
3411
  const decoder = new TextDecoder();
@@ -1,4 +1,4 @@
1
- const tsrStartManifest = () => ({ routes: { __root__: { filePath: "C:/Users/claw/workspace/llm-inspector/src/routes/__root.tsx", children: ["/", "/api/config", "/api/health", "/api/logs", "/api/models", "/api/providers", "/api/sessions", "/proxy/$"], preloads: ["/assets/index-Do7wdaYZ.js"], scripts: [{ attrs: { type: "module", async: true, src: "/assets/index-Do7wdaYZ.js" } }] }, "/": { filePath: "C:/Users/claw/workspace/llm-inspector/src/routes/index.tsx", children: void 0, preloads: ["/assets/index-CjjXIYIt.js"] } } });
1
+ const tsrStartManifest = () => ({ routes: { __root__: { filePath: "C:/Users/claw/workspace/llm-inspector/src/routes/__root.tsx", children: ["/", "/api/config", "/api/health", "/api/logs", "/api/models", "/api/providers", "/api/sessions", "/proxy/$"], preloads: ["/assets/index-BLz26_QE.js"], scripts: [{ attrs: { type: "module", async: true, src: "/assets/index-BLz26_QE.js" } }] }, "/": { filePath: "C:/Users/claw/workspace/llm-inspector/src/routes/index.tsx", children: void 0, preloads: ["/assets/index-BHjxAEHw.js"] } } });
2
2
  export {
3
3
  tsrStartManifest
4
4
  };
@@ -38,51 +38,51 @@ const assets = {
38
38
  "/assets/alibaba-TTwafVwX.svg": {
39
39
  "type": "image/svg+xml",
40
40
  "etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
41
- "mtime": "2026-06-08T00:49:07.051Z",
41
+ "mtime": "2026-06-08T07:43:02.084Z",
42
42
  "size": 5915,
43
43
  "path": "../public/assets/alibaba-TTwafVwX.svg"
44
44
  },
45
45
  "/assets/minimax-BPMzvuL-.jpeg": {
46
46
  "type": "image/jpeg",
47
47
  "etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
48
- "mtime": "2026-06-08T00:49:07.048Z",
48
+ "mtime": "2026-06-08T07:43:02.087Z",
49
49
  "size": 6918,
50
50
  "path": "../public/assets/minimax-BPMzvuL-.jpeg"
51
51
  },
52
- "/assets/index-BpKPXEcb.css": {
52
+ "/assets/index-BGxRftQy.css": {
53
53
  "type": "text/css; charset=utf-8",
54
- "etag": '"11628-5GjLelHqc7BEfGB/7qUFPt5/gyM"',
55
- "mtime": "2026-06-08T00:49:07.051Z",
56
- "size": 71208,
57
- "path": "../public/assets/index-BpKPXEcb.css"
54
+ "etag": '"1166c-gg/EqTkMlOVIH7gFJHkZ+FF8BFg"',
55
+ "mtime": "2026-06-08T07:43:02.087Z",
56
+ "size": 71276,
57
+ "path": "../public/assets/index-BGxRftQy.css"
58
58
  },
59
- "/assets/zhipuai-BPNAnxo-.svg": {
60
- "type": "image/svg+xml",
61
- "etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
62
- "mtime": "2026-06-08T00:49:07.051Z",
63
- "size": 11256,
64
- "path": "../public/assets/zhipuai-BPNAnxo-.svg"
65
- },
66
- "/assets/index-Do7wdaYZ.js": {
59
+ "/assets/index-BLz26_QE.js": {
67
60
  "type": "text/javascript; charset=utf-8",
68
- "etag": '"5103c-aaFcAP7Accy+WRst7gdyeAfO198"',
69
- "mtime": "2026-06-08T00:49:07.051Z",
61
+ "etag": '"5103c-+JM4xmfobHv5x3cvc23X5jJh1dQ"',
62
+ "mtime": "2026-06-08T07:43:02.087Z",
70
63
  "size": 331836,
71
- "path": "../public/assets/index-Do7wdaYZ.js"
64
+ "path": "../public/assets/index-BLz26_QE.js"
72
65
  },
73
66
  "/assets/qwen-CONDcHqt.png": {
74
67
  "type": "image/png",
75
68
  "etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
76
- "mtime": "2026-06-08T00:49:07.051Z",
69
+ "mtime": "2026-06-08T07:43:02.087Z",
77
70
  "size": 357059,
78
71
  "path": "../public/assets/qwen-CONDcHqt.png"
79
72
  },
80
- "/assets/index-CjjXIYIt.js": {
73
+ "/assets/zhipuai-BPNAnxo-.svg": {
74
+ "type": "image/svg+xml",
75
+ "etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
76
+ "mtime": "2026-06-08T07:43:02.087Z",
77
+ "size": 11256,
78
+ "path": "../public/assets/zhipuai-BPNAnxo-.svg"
79
+ },
80
+ "/assets/index-BHjxAEHw.js": {
81
81
  "type": "text/javascript; charset=utf-8",
82
- "etag": '"8b905-rdaof1vYelA3ENck4MON8W7NPww"',
83
- "mtime": "2026-06-08T00:49:07.051Z",
84
- "size": 571653,
85
- "path": "../public/assets/index-CjjXIYIt.js"
82
+ "etag": '"8bc3c-HIRAa9iMtFjtXPHh5YlOmbmCEoQ"',
83
+ "mtime": "2026-06-08T07:43:02.087Z",
84
+ "size": 572476,
85
+ "path": "../public/assets/index-BHjxAEHw.js"
86
86
  }
87
87
  };
88
88
  function readAsset(id) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tonyclaw/llm-inspector",
3
- "version": "1.11.4",
3
+ "version": "1.11.6",
4
4
  "type": "module",
5
5
  "description": "LLM API proxy inspector — captures and displays requests/responses from AI coding tools in a web UI",
6
6
  "license": "MIT",
@@ -147,10 +147,12 @@ export function ProxyViewer({
147
147
  {/* Header */}
148
148
  <div className="flex items-center gap-4 mb-4 px-6 pt-6">
149
149
  <h1 className="text-lg font-bold flex-1 flex items-center gap-2">
150
- <CrabLogo className="size-10 text-amber-500" />
151
- LLM Inspector
150
+ <CrabLogo className="size-10 text-amber-500 self-center" />
151
+ <span className="flex items-baseline gap-2">
152
+ LLM Inspector
153
+ <span className="text-xs text-muted-foreground font-mono">v{packageJson.version}</span>
154
+ </span>
152
155
  </h1>
153
- <span className="text-muted-foreground text-xs font-mono">v{packageJson.version}</span>
154
156
  <div className="flex items-center border border-border rounded-md overflow-hidden">
155
157
  <button
156
158
  type="button"