@tonyclaw/llm-inspector 1.7.6 → 1.7.7

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.
@@ -14,4 +14,4 @@ Error generating stack: `+l.message+`
14
14
  `)}else{const p=o.indexOf(`
15
15
  `);if(p>=0){const S=o.slice(0,p).trim();o=o.slice(p+1),S.length>0&&(m=JSON.parse(S),f=!0)}}}return(async()=>{try{for(;;){const{value:y,done:h}=await r.read();y&&(o+=y);const p=o.lastIndexOf(`
16
16
  `);if(p>=0){const S=o.slice(0,p);o=o.slice(p+1);const b=S.split(`
17
- `).filter(Boolean);for(const _ of b)try{u(JSON.parse(_))}catch(R){i?.(`Invalid JSON line: ${_}`,R)}}if(h)break}}catch(y){i?.("Stream processing error:",y)}})(),u(m)}async function _E({jsonStream:a,onMessage:u,onError:i}){const r=a.getReader(),{value:o,done:f}=await r.read();if(f||!o)throw new Error("Stream ended before first object");const m=JSON.parse(o);return(async()=>{try{for(;;){const{value:y,done:h}=await r.read();if(h)break;if(y)try{u(JSON.parse(y))}catch(p){i?.(`Invalid JSON: ${y}`,p)}}}catch(y){i?.("Stream processing error:",y)}})(),u(m)}function EE(a){const u="/_serverFn/"+a;return Object.assign((...o)=>{const f=Lp()?.serverFns?.fetch;return gE(u,o,f??fetch)},{url:u,serverFnMeta:{id:a},[Ao]:!0})}const RE={key:"$TSS/serverfn",test:a=>typeof a!="function"||!(Ao in a)?!1:!!a[Ao],toSerializable:({serverFnMeta:a})=>({functionId:a.id}),fromSerializable:({functionId:a})=>EE(a)},TE="/assets/index-B3RwBPLW.css",Hp=G_({head:()=>({meta:[{charSet:"utf-8"},{name:"viewport",content:"width=device-width, initial-scale=1"},{title:"llm-inspector"}],links:[{rel:"stylesheet",href:TE}]}),component:AE});function AE(){return k.jsx(OE,{children:k.jsx(Cp,{})})}function OE({children:a}){return k.jsxs("html",{lang:"en",className:"dark",children:[k.jsx("head",{children:k.jsx(aE,{})}),k.jsxs("body",{children:[a,k.jsx(lE,{})]})]})}const ME="modulepreload",zE=function(a){return"/"+a},zy={},xE=function(u,i,r){let o=Promise.resolve();if(i&&i.length>0){let h=function(p){return Promise.all(p.map(S=>Promise.resolve(S).then(b=>({status:"fulfilled",value:b}),b=>({status:"rejected",reason:b}))))};document.getElementsByTagName("link");const m=document.querySelector("meta[property=csp-nonce]"),y=m?.nonce||m?.getAttribute("nonce");o=h(i.map(p=>{if(p=zE(p),p in zy)return;zy[p]=!0;const S=p.endsWith(".css"),b=S?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${p}"]${b}`))return;const _=document.createElement("link");if(_.rel=S?"stylesheet":ME,S||(_.as="script"),_.crossOrigin="",_.href=p,y&&_.setAttribute("nonce",y),document.head.appendChild(_),S)return new Promise((R,D)=>{_.addEventListener("load",R),_.addEventListener("error",()=>D(new Error(`Unable to preload CSS for ${p}`)))})}))}function f(m){const y=new Event("vite:preloadError",{cancelable:!0});if(y.payload=m,window.dispatchEvent(y),!y.defaultPrevented)throw m}return o.then(m=>{for(const y of m||[])y.status==="rejected"&&f(y.reason);return u().catch(f)})},wE=()=>xE(()=>import("./index-BKkFFKAM.js"),[]),CE=To("/")({component:V_(wE,"component")}),DE=CE.update({id:"/",path:"/",getParentRoute:()=>Hp}),UE={IndexRoute:DE},LE=Hp._addFileChildren(UE);function NE(){return P_({routeTree:LE,scrollRestoration:!1})}async function BE(){const a=await NE();let u;return u=[],window.__TSS_START_OPTIONS__={serializationAdapters:u},u.push(RE),a.options.serializationAdapters&&u.push(...a.options.serializationAdapters),a.update({basepath:"",serializationAdapters:u}),a.state.matches.length||await uE(a),a}async function jE(){const a=await BE();return window.$_TSR?.h(),a}let So;function HE(){return So||(So=jE()),k.jsx(p_,{promise:So,children:a=>k.jsx(I_,{router:a})})}nt.startTransition(()=>{h0.hydrateRoot(document,k.jsx(nt.StrictMode,{children:k.jsx(HE,{})}))});export{Ul as R,Mp as a,s0 as b,qE as c,XE as d,xy as g,k as j,nt as r};
17
+ `).filter(Boolean);for(const _ of b)try{u(JSON.parse(_))}catch(R){i?.(`Invalid JSON line: ${_}`,R)}}if(h)break}}catch(y){i?.("Stream processing error:",y)}})(),u(m)}async function _E({jsonStream:a,onMessage:u,onError:i}){const r=a.getReader(),{value:o,done:f}=await r.read();if(f||!o)throw new Error("Stream ended before first object");const m=JSON.parse(o);return(async()=>{try{for(;;){const{value:y,done:h}=await r.read();if(h)break;if(y)try{u(JSON.parse(y))}catch(p){i?.(`Invalid JSON: ${y}`,p)}}}catch(y){i?.("Stream processing error:",y)}})(),u(m)}function EE(a){const u="/_serverFn/"+a;return Object.assign((...o)=>{const f=Lp()?.serverFns?.fetch;return gE(u,o,f??fetch)},{url:u,serverFnMeta:{id:a},[Ao]:!0})}const RE={key:"$TSS/serverfn",test:a=>typeof a!="function"||!(Ao in a)?!1:!!a[Ao],toSerializable:({serverFnMeta:a})=>({functionId:a.id}),fromSerializable:({functionId:a})=>EE(a)},TE="/assets/index-B3RwBPLW.css",Hp=G_({head:()=>({meta:[{charSet:"utf-8"},{name:"viewport",content:"width=device-width, initial-scale=1"},{title:"llm-inspector"}],links:[{rel:"stylesheet",href:TE}]}),component:AE});function AE(){return k.jsx(OE,{children:k.jsx(Cp,{})})}function OE({children:a}){return k.jsxs("html",{lang:"en",className:"dark",children:[k.jsx("head",{children:k.jsx(aE,{})}),k.jsxs("body",{children:[a,k.jsx(lE,{})]})]})}const ME="modulepreload",zE=function(a){return"/"+a},zy={},xE=function(u,i,r){let o=Promise.resolve();if(i&&i.length>0){let h=function(p){return Promise.all(p.map(S=>Promise.resolve(S).then(b=>({status:"fulfilled",value:b}),b=>({status:"rejected",reason:b}))))};document.getElementsByTagName("link");const m=document.querySelector("meta[property=csp-nonce]"),y=m?.nonce||m?.getAttribute("nonce");o=h(i.map(p=>{if(p=zE(p),p in zy)return;zy[p]=!0;const S=p.endsWith(".css"),b=S?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${p}"]${b}`))return;const _=document.createElement("link");if(_.rel=S?"stylesheet":ME,S||(_.as="script"),_.crossOrigin="",_.href=p,y&&_.setAttribute("nonce",y),document.head.appendChild(_),S)return new Promise((R,D)=>{_.addEventListener("load",R),_.addEventListener("error",()=>D(new Error(`Unable to preload CSS for ${p}`)))})}))}function f(m){const y=new Event("vite:preloadError",{cancelable:!0});if(y.payload=m,window.dispatchEvent(y),!y.defaultPrevented)throw m}return o.then(m=>{for(const y of m||[])y.status==="rejected"&&f(y.reason);return u().catch(f)})},wE=()=>xE(()=>import("./index-Drusqil7.js"),[]),CE=To("/")({component:V_(wE,"component")}),DE=CE.update({id:"/",path:"/",getParentRoute:()=>Hp}),UE={IndexRoute:DE},LE=Hp._addFileChildren(UE);function NE(){return P_({routeTree:LE,scrollRestoration:!1})}async function BE(){const a=await NE();let u;return u=[],window.__TSS_START_OPTIONS__={serializationAdapters:u},u.push(RE),a.options.serializationAdapters&&u.push(...a.options.serializationAdapters),a.update({basepath:"",serializationAdapters:u}),a.state.matches.length||await uE(a),a}async function jE(){const a=await BE();return window.$_TSR?.h(),a}let So;function HE(){return So||(So=jE()),k.jsx(p_,{promise:So,children:a=>k.jsx(I_,{router:a})})}nt.startTransition(()=>{h0.hydrateRoot(document,k.jsx(nt.StrictMode,{children:k.jsx(HE,{})}))});export{Ul as R,Mp as a,s0 as b,qE as c,XE as d,xy as g,k as j,nt as r};
@@ -1,5 +1,5 @@
1
1
  import { r as reactExports, j as jsxRuntimeExports, a as React } from "../_libs/react.mjs";
2
- import { C as CapturedLogSchema, a as parseRequest, p as parseOpenAIResponse, I as InspectorResponseSchema } from "./router-tRLZo2WT.mjs";
2
+ import { C as CapturedLogSchema, a as parseRequest, p as parseOpenAIResponse, I as InspectorResponseSchema } from "./router-CZSteFqT.mjs";
3
3
  import { u as useVirtualizer } from "../_libs/tanstack__react-virtual.mjs";
4
4
  import { J as JSZip } from "../_libs/jszip.mjs";
5
5
  import { c as clsx } from "../_libs/clsx.mjs";
@@ -2179,6 +2179,7 @@ function ProviderCard({
2179
2179
  provider,
2180
2180
  testResults,
2181
2181
  isTesting,
2182
+ testingTimeLeft,
2182
2183
  onEdit,
2183
2184
  onDelete,
2184
2185
  onTest
@@ -2245,7 +2246,7 @@ function ProviderCard({
2245
2246
  disabled: isTesting ?? false,
2246
2247
  children: [
2247
2248
  /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCw, { className: `size-3 ${isTesting ?? false ? "animate-spin" : ""}` }),
2248
- isTesting ?? false ? "Testing..." : "Test"
2249
+ isTesting ?? false ? testingTimeLeft !== void 0 ? `Testing (${testingTimeLeft}s)` : "Testing..." : "Test"
2249
2250
  ]
2250
2251
  }
2251
2252
  ),
@@ -2496,16 +2497,44 @@ function ProviderForm({ provider, onSubmit, onCancel }) {
2496
2497
  ] })
2497
2498
  ] });
2498
2499
  }
2499
- function ProvidersPanel() {
2500
- const [providers, setProviders] = reactExports.useState([]);
2500
+ function ProvidersPanel({
2501
+ externalProviders,
2502
+ externalTestResults,
2503
+ externalTestingProviders,
2504
+ externalTestingTimeLeft,
2505
+ onProvidersChange,
2506
+ onTestResultsChange,
2507
+ onTestingProvidersChange,
2508
+ onTestingTimeLeftChange
2509
+ }) {
2510
+ const [internalProviders, setInternalProviders] = reactExports.useState([]);
2501
2511
  const [isLoading, setIsLoading] = reactExports.useState(true);
2502
2512
  const [showForm, setShowForm] = reactExports.useState(false);
2503
2513
  const [editingProvider, setEditingProvider] = reactExports.useState();
2504
2514
  const [error, setError] = reactExports.useState(null);
2505
- const [testResults, setTestResults] = reactExports.useState({});
2506
- const [testingProviders, setTestingProviders] = reactExports.useState(/* @__PURE__ */ new Set());
2515
+ const [internalTestResults, setInternalTestResults] = reactExports.useState({});
2516
+ const [internalTestingProviders, setInternalTestingProviders] = reactExports.useState(/* @__PURE__ */ new Set());
2517
+ const [internalTestingTimeLeft, setInternalTestingTimeLeft] = reactExports.useState(
2518
+ {}
2519
+ );
2507
2520
  const [configPath, setConfigPath] = reactExports.useState(null);
2508
2521
  const [configPathCopied, setConfigPathCopied] = reactExports.useState(false);
2522
+ const providers = externalProviders ?? internalProviders;
2523
+ const setProviders = onProvidersChange ?? setInternalProviders;
2524
+ const testResults = externalTestResults ?? internalTestResults;
2525
+ const testingProviders = externalTestingProviders ?? internalTestingProviders;
2526
+ const testingTimeLeft = externalTestingTimeLeft ?? internalTestingTimeLeft;
2527
+ const setTestingTimeLeft = onTestingTimeLeftChange ? (id, seconds) => onTestingTimeLeftChange(id, seconds) : (id, seconds) => {
2528
+ if (seconds === void 0) {
2529
+ setInternalTestingTimeLeft((prev) => {
2530
+ const next = { ...prev };
2531
+ delete next[id];
2532
+ return next;
2533
+ });
2534
+ } else {
2535
+ setInternalTestingTimeLeft((prev) => ({ ...prev, [id]: seconds }));
2536
+ }
2537
+ };
2509
2538
  const fetchProviders = reactExports.useCallback(async () => {
2510
2539
  try {
2511
2540
  const providersRes = await fetch("/api/providers");
@@ -2530,22 +2559,49 @@ function ProvidersPanel() {
2530
2559
  }
2531
2560
  })();
2532
2561
  }, [fetchProviders]);
2533
- const runTest = reactExports.useCallback(async (providerId) => {
2534
- setTestingProviders((prev) => new Set(prev).add(providerId));
2535
- try {
2536
- const res = await fetch(`/api/providers/${providerId}/test`, { method: "POST" });
2537
- if (res.ok) {
2538
- const results = await res.json();
2539
- setTestResults((prev) => ({ ...prev, [providerId]: results }));
2562
+ const TEST_TIMEOUT_SECONDS = 30;
2563
+ const runTest = reactExports.useCallback(
2564
+ async (providerId) => {
2565
+ if (onTestingProvidersChange) {
2566
+ onTestingProvidersChange(providerId, true);
2567
+ } else {
2568
+ setInternalTestingProviders((prev) => new Set(prev).add(providerId));
2540
2569
  }
2541
- } finally {
2542
- setTestingProviders((prev) => {
2543
- const next = new Set(prev);
2544
- next.delete(providerId);
2545
- return next;
2546
- });
2547
- }
2548
- }, []);
2570
+ let remaining = TEST_TIMEOUT_SECONDS;
2571
+ setTestingTimeLeft(providerId, remaining);
2572
+ const intervalId = setInterval(() => {
2573
+ remaining--;
2574
+ setTestingTimeLeft(providerId, remaining);
2575
+ if (remaining <= 0) {
2576
+ clearInterval(intervalId);
2577
+ }
2578
+ }, 1e3);
2579
+ try {
2580
+ const res = await fetch(`/api/providers/${providerId}/test`, { method: "POST" });
2581
+ if (res.ok) {
2582
+ const results = await res.json();
2583
+ if (onTestResultsChange) {
2584
+ onTestResultsChange(providerId, results);
2585
+ } else {
2586
+ setInternalTestResults((prev) => ({ ...prev, [providerId]: results }));
2587
+ }
2588
+ }
2589
+ } finally {
2590
+ clearInterval(intervalId);
2591
+ setTestingTimeLeft(providerId, void 0);
2592
+ if (onTestingProvidersChange) {
2593
+ onTestingProvidersChange(providerId, false);
2594
+ } else {
2595
+ setInternalTestingProviders((prev) => {
2596
+ const next = new Set(prev);
2597
+ next.delete(providerId);
2598
+ return next;
2599
+ });
2600
+ }
2601
+ }
2602
+ },
2603
+ [onTestingProvidersChange, onTestResultsChange, setTestingTimeLeft]
2604
+ );
2549
2605
  function handleAddProvider(data) {
2550
2606
  void (async () => {
2551
2607
  const payload = {
@@ -2675,6 +2731,7 @@ function ProvidersPanel() {
2675
2731
  provider,
2676
2732
  testResults: testResults[provider.id],
2677
2733
  isTesting: testingProviders.has(provider.id),
2734
+ testingTimeLeft: testingTimeLeft[provider.id],
2678
2735
  onEdit: (p) => setEditingProvider(p),
2679
2736
  onDelete: handleDeleteProvider,
2680
2737
  onTest: (id) => {
@@ -2688,6 +2745,40 @@ function ProvidersPanel() {
2688
2745
  function SettingsDialog() {
2689
2746
  const [open, setOpen] = reactExports.useState(false);
2690
2747
  const [activeTab, setActiveTab] = reactExports.useState("providers");
2748
+ const [providers, setProviders] = reactExports.useState([]);
2749
+ const [testResults, setTestResults] = reactExports.useState({});
2750
+ const [testingProviders, setTestingProviders] = reactExports.useState(/* @__PURE__ */ new Set());
2751
+ const [testingTimeLeft, setTestingTimeLeft] = reactExports.useState({});
2752
+ const handleTestResultsChange = reactExports.useCallback(
2753
+ (providerId, results) => {
2754
+ setTestResults((prev) => ({ ...prev, [providerId]: results }));
2755
+ },
2756
+ []
2757
+ );
2758
+ const handleTestingProvidersChange = reactExports.useCallback((providerId, isTesting) => {
2759
+ setTestingProviders((prev) => {
2760
+ const next = new Set(prev);
2761
+ if (isTesting) {
2762
+ next.add(providerId);
2763
+ } else {
2764
+ next.delete(providerId);
2765
+ }
2766
+ return next;
2767
+ });
2768
+ }, []);
2769
+ const handleTestingTimeLeftChange = reactExports.useCallback(
2770
+ (providerId, seconds) => {
2771
+ setTestingTimeLeft((prev) => {
2772
+ if (seconds === void 0) {
2773
+ const next = { ...prev };
2774
+ delete next[providerId];
2775
+ return next;
2776
+ }
2777
+ return { ...prev, [providerId]: seconds };
2778
+ });
2779
+ },
2780
+ []
2781
+ );
2691
2782
  return /* @__PURE__ */ jsxRuntimeExports.jsxs(Dialog, { open, onOpenChange: setOpen, children: [
2692
2783
  /* @__PURE__ */ jsxRuntimeExports.jsx(DialogTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Button, { variant: "ghost", size: "icon", className: "size-8", children: [
2693
2784
  /* @__PURE__ */ jsxRuntimeExports.jsx(Settings, { className: "size-4" }),
@@ -2697,7 +2788,19 @@ function SettingsDialog() {
2697
2788
  /* @__PURE__ */ jsxRuntimeExports.jsx(DialogHeader, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(DialogTitle, { children: "Settings" }) }),
2698
2789
  /* @__PURE__ */ jsxRuntimeExports.jsxs(Tabs, { value: activeTab, onValueChange: setActiveTab, className: "flex-1 overflow-hidden", children: [
2699
2790
  /* @__PURE__ */ jsxRuntimeExports.jsx(TabsList, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(TabsTrigger, { value: "providers", children: "Providers" }) }),
2700
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-4 overflow-y-auto flex-1", children: /* @__PURE__ */ jsxRuntimeExports.jsx(TabsContent, { value: "providers", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ProvidersPanel, {}) }) })
2791
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-4 overflow-y-auto flex-1", children: /* @__PURE__ */ jsxRuntimeExports.jsx(TabsContent, { value: "providers", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
2792
+ ProvidersPanel,
2793
+ {
2794
+ externalProviders: providers,
2795
+ externalTestResults: testResults,
2796
+ externalTestingProviders: testingProviders,
2797
+ externalTestingTimeLeft: testingTimeLeft,
2798
+ onProvidersChange: setProviders,
2799
+ onTestResultsChange: handleTestResultsChange,
2800
+ onTestingProvidersChange: handleTestingProvidersChange,
2801
+ onTestingTimeLeftChange: handleTestingTimeLeftChange
2802
+ }
2803
+ ) }) })
2701
2804
  ] })
2702
2805
  ] })
2703
2806
  ] });
@@ -197,7 +197,7 @@ function getResponse() {
197
197
  return event.res;
198
198
  }
199
199
  async function getStartManifest(matchedRoutes) {
200
- const { tsrStartManifest } = await import("../_tanstack-start-manifest_v-_KPj4BcN.mjs");
200
+ const { tsrStartManifest } = await import("../_tanstack-start-manifest_v-DgsS3z4y.mjs");
201
201
  const startManifest = tsrStartManifest();
202
202
  const rootRoute = startManifest.routes[rootRouteId] = startManifest.routes[rootRouteId] || {};
203
203
  rootRoute.assets = rootRoute.assets || [];
@@ -766,7 +766,7 @@ let entriesPromise;
766
766
  let baseManifestPromise;
767
767
  let cachedFinalManifestPromise;
768
768
  async function loadEntries() {
769
- const routerEntry = await import("./router-tRLZo2WT.mjs").then((n) => n.r);
769
+ const routerEntry = await import("./router-CZSteFqT.mjs").then((n) => n.r);
770
770
  const startEntry = await import("./start-HYkvq4Ni.mjs");
771
771
  return { startEntry, routerEntry };
772
772
  }
@@ -65,7 +65,7 @@ function RootDocument({ children }) {
65
65
  ] })
66
66
  ] });
67
67
  }
68
- const $$splitComponentImporter = () => import("./index-DAvMem8_.mjs");
68
+ const $$splitComponentImporter = () => import("./index-DbpsE3Y4.mjs");
69
69
  const Route$d = createFileRoute("/")({
70
70
  component: lazyRouteComponent($$splitComponentImporter, "component")
71
71
  });
@@ -1,4 +1,4 @@
1
- const tsrStartManifest = () => ({ "routes": { "__root__": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/__root.tsx", "children": ["/", "/api/health", "/api/logs", "/api/models", "/api/providers", "/api/sessions", "/proxy/$", "/api/config/paths"], "preloads": ["/assets/main-B3Cmykkm.js"], "assets": [] }, "/": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/index.tsx", "assets": [], "preloads": ["/assets/index-BKkFFKAM.js"] }, "/api/health": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/health.ts" }, "/api/logs": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.ts", "children": ["/api/logs/$id", "/api/logs/stream"] }, "/api/models": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/models.ts" }, "/api/providers": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.ts", "children": ["/api/providers/$providerId"] }, "/api/sessions": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/sessions.ts" }, "/proxy/$": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/proxy/$.ts" }, "/api/config/paths": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/config.paths.ts" }, "/api/logs/$id": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.ts", "children": ["/api/logs/$id/chunks", "/api/logs/$id/replay"] }, "/api/logs/stream": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.stream.ts" }, "/api/providers/$providerId": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.$providerId.ts", "children": ["/api/providers/$providerId/test"] }, "/api/logs/$id/chunks": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.chunks.ts" }, "/api/logs/$id/replay": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.replay.ts" }, "/api/providers/$providerId/test": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.$providerId.test.ts" } }, "clientEntry": "/assets/main-B3Cmykkm.js" });
1
+ const tsrStartManifest = () => ({ "routes": { "__root__": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/__root.tsx", "children": ["/", "/api/health", "/api/logs", "/api/models", "/api/providers", "/api/sessions", "/proxy/$", "/api/config/paths"], "preloads": ["/assets/main-DYXtPYU8.js"], "assets": [] }, "/": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/index.tsx", "assets": [], "preloads": ["/assets/index-Drusqil7.js"] }, "/api/health": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/health.ts" }, "/api/logs": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.ts", "children": ["/api/logs/$id", "/api/logs/stream"] }, "/api/models": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/models.ts" }, "/api/providers": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.ts", "children": ["/api/providers/$providerId"] }, "/api/sessions": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/sessions.ts" }, "/proxy/$": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/proxy/$.ts" }, "/api/config/paths": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/config.paths.ts" }, "/api/logs/$id": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.ts", "children": ["/api/logs/$id/chunks", "/api/logs/$id/replay"] }, "/api/logs/stream": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.stream.ts" }, "/api/providers/$providerId": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.$providerId.ts", "children": ["/api/providers/$providerId/test"] }, "/api/logs/$id/chunks": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.chunks.ts" }, "/api/logs/$id/replay": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.replay.ts" }, "/api/providers/$providerId/test": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.$providerId.test.ts" } }, "clientEntry": "/assets/main-DYXtPYU8.js" });
2
2
  export {
3
3
  tsrStartManifest
4
4
  };
@@ -97,54 +97,54 @@ const headers = ((m) => function headersRouteRule(event) {
97
97
  }
98
98
  });
99
99
  const assets = {
100
+ "/assets/index-B3RwBPLW.css": {
101
+ "type": "text/css; charset=utf-8",
102
+ "etag": '"10c74-aXacU4DRFVsUwcC5jHnjoPRSlTA"',
103
+ "mtime": "2026-06-03T12:05:46.053Z",
104
+ "size": 68724,
105
+ "path": "../public/assets/index-B3RwBPLW.css"
106
+ },
100
107
  "/assets/alibaba-TTwafVwX.svg": {
101
108
  "type": "image/svg+xml",
102
109
  "etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
103
- "mtime": "2026-06-03T11:15:10.948Z",
110
+ "mtime": "2026-06-03T12:05:46.051Z",
104
111
  "size": 5915,
105
112
  "path": "../public/assets/alibaba-TTwafVwX.svg"
106
113
  },
107
- "/assets/index-B3RwBPLW.css": {
108
- "type": "text/css; charset=utf-8",
109
- "etag": '"10c74-aXacU4DRFVsUwcC5jHnjoPRSlTA"',
110
- "mtime": "2026-06-03T11:15:10.948Z",
111
- "size": 68724,
112
- "path": "../public/assets/index-B3RwBPLW.css"
114
+ "/assets/main-DYXtPYU8.js": {
115
+ "type": "text/javascript; charset=utf-8",
116
+ "etag": '"4db57-3IoaJ0zzvDUU7WQZuVFYW7QEzGQ"',
117
+ "mtime": "2026-06-03T12:05:46.053Z",
118
+ "size": 318295,
119
+ "path": "../public/assets/main-DYXtPYU8.js"
120
+ },
121
+ "/assets/qwen-CONDcHqt.png": {
122
+ "type": "image/png",
123
+ "etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
124
+ "mtime": "2026-06-03T12:05:46.053Z",
125
+ "size": 357059,
126
+ "path": "../public/assets/qwen-CONDcHqt.png"
113
127
  },
114
128
  "/assets/minimax-BPMzvuL-.jpeg": {
115
129
  "type": "image/jpeg",
116
130
  "etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
117
- "mtime": "2026-06-03T11:15:10.948Z",
131
+ "mtime": "2026-06-03T12:05:46.053Z",
118
132
  "size": 6918,
119
133
  "path": "../public/assets/minimax-BPMzvuL-.jpeg"
120
134
  },
121
- "/assets/main-B3Cmykkm.js": {
122
- "type": "text/javascript; charset=utf-8",
123
- "etag": '"4db57-BoW6SyNc1bhvI2SIfQuoPlQ7pjU"',
124
- "mtime": "2026-06-03T11:15:10.950Z",
125
- "size": 318295,
126
- "path": "../public/assets/main-B3Cmykkm.js"
127
- },
128
135
  "/assets/zhipuai-BPNAnxo-.svg": {
129
136
  "type": "image/svg+xml",
130
137
  "etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
131
- "mtime": "2026-06-03T11:15:10.948Z",
138
+ "mtime": "2026-06-03T12:05:46.053Z",
132
139
  "size": 11256,
133
140
  "path": "../public/assets/zhipuai-BPNAnxo-.svg"
134
141
  },
135
- "/assets/qwen-CONDcHqt.png": {
136
- "type": "image/png",
137
- "etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
138
- "mtime": "2026-06-03T11:15:10.950Z",
139
- "size": 357059,
140
- "path": "../public/assets/qwen-CONDcHqt.png"
141
- },
142
- "/assets/index-BKkFFKAM.js": {
142
+ "/assets/index-Drusqil7.js": {
143
143
  "type": "text/javascript; charset=utf-8",
144
- "etag": '"83213-FJqU+YwY4BQ0ObBCm+z27w3dIRc"',
145
- "mtime": "2026-06-03T11:15:10.950Z",
146
- "size": 537107,
147
- "path": "../public/assets/index-BKkFFKAM.js"
144
+ "etag": '"8368b-K/ZW3jyne5SeFVJGXXNOEllhQyM"',
145
+ "mtime": "2026-06-03T12:05:46.054Z",
146
+ "size": 538251,
147
+ "path": "../public/assets/index-Drusqil7.js"
148
148
  }
149
149
  };
150
150
  function readAsset(id) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tonyclaw/llm-inspector",
3
- "version": "1.7.6",
3
+ "version": "1.7.7",
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",
@@ -63,6 +63,7 @@ type ProviderCardProps = {
63
63
  provider: ProviderConfig;
64
64
  testResults?: TestResults;
65
65
  isTesting?: boolean;
66
+ testingTimeLeft?: number;
66
67
  onEdit: (provider: ProviderConfig) => void;
67
68
  onDelete: (providerId: string) => void;
68
69
  onTest?: (providerId: string) => void;
@@ -152,6 +153,7 @@ export function ProviderCard({
152
153
  provider,
153
154
  testResults,
154
155
  isTesting,
156
+ testingTimeLeft,
155
157
  onEdit,
156
158
  onDelete,
157
159
  onTest,
@@ -233,7 +235,11 @@ export function ProviderCard({
233
235
  disabled={isTesting ?? false}
234
236
  >
235
237
  <RotateCw className={`size-3 ${(isTesting ?? false) ? "animate-spin" : ""}`} />
236
- {(isTesting ?? false) ? "Testing..." : "Test"}
238
+ {(isTesting ?? false)
239
+ ? testingTimeLeft !== undefined
240
+ ? `Testing (${testingTimeLeft}s)`
241
+ : "Testing..."
242
+ : "Test"}
237
243
  </Button>
238
244
  )}
239
245
  <Button
@@ -1,4 +1,4 @@
1
- import { type JSX, useState, useEffect, useCallback } from "react";
1
+ import { type JSX, useState, useEffect, useCallback, useRef } from "react";
2
2
  import { Button } from "../ui/button";
3
3
  import { Plus, AlertCircle, Copy, Check } from "lucide-react";
4
4
  import { ProviderCard } from "./ProviderCard";
@@ -36,22 +36,71 @@ type StreamingTestResults = {
36
36
  streaming: TestResult | NotConfigured;
37
37
  };
38
38
 
39
- type TestResults = {
39
+ export type TestResults = {
40
40
  anthropic: StreamingTestResults;
41
41
  openai: StreamingTestResults;
42
42
  };
43
43
 
44
- export function ProvidersPanel(): JSX.Element {
45
- const [providers, setProviders] = useState<ProviderConfig[]>([]);
44
+ type ProvidersPanelProps = {
45
+ externalProviders?: ProviderConfig[];
46
+ externalTestResults?: Record<string, TestResults>;
47
+ externalTestingProviders?: Set<string>;
48
+ externalTestingTimeLeft?: Record<string, number>;
49
+ onProvidersChange?: (providers: ProviderConfig[]) => void;
50
+ onTestResultsChange?: (providerId: string, results: TestResults) => void;
51
+ onTestingProvidersChange?: (providerId: string, isTesting: boolean) => void;
52
+ onTestingTimeLeftChange?: (providerId: string, seconds: number | undefined) => void;
53
+ };
54
+
55
+ export function ProvidersPanel({
56
+ externalProviders,
57
+ externalTestResults,
58
+ externalTestingProviders,
59
+ externalTestingTimeLeft,
60
+ onProvidersChange,
61
+ onTestResultsChange,
62
+ onTestingProvidersChange,
63
+ onTestingTimeLeftChange,
64
+ }: ProvidersPanelProps): JSX.Element {
65
+ const [internalProviders, setInternalProviders] = useState<ProviderConfig[]>([]);
46
66
  const [isLoading, setIsLoading] = useState(true);
47
67
  const [showForm, setShowForm] = useState(false);
48
68
  const [editingProvider, setEditingProvider] = useState<ProviderConfig | undefined>();
49
69
  const [error, setError] = useState<string | null>(null);
50
- const [testResults, setTestResults] = useState<Record<string, TestResults>>({});
51
- const [testingProviders, setTestingProviders] = useState<Set<string>>(new Set());
70
+ const [internalTestResults, setInternalTestResults] = useState<Record<string, TestResults>>({});
71
+ const [internalTestingProviders, setInternalTestingProviders] = useState<Set<string>>(new Set());
72
+ const [internalTestingTimeLeft, setInternalTestingTimeLeft] = useState<Record<string, number>>(
73
+ {},
74
+ );
52
75
  const [configPath, setConfigPath] = useState<string | null>(null);
53
76
  const [configPathCopied, setConfigPathCopied] = useState(false);
54
77
 
78
+ // Use external state if provided, otherwise use internal state
79
+ const providers = externalProviders ?? internalProviders;
80
+ const setProviders = onProvidersChange ?? setInternalProviders;
81
+ const testResults = externalTestResults ?? internalTestResults;
82
+ const setTestResults = onTestResultsChange
83
+ ? (id: string, results: TestResults) => onTestResultsChange(id, results)
84
+ : setInternalTestResults;
85
+ const testingProviders = externalTestingProviders ?? internalTestingProviders;
86
+ const setTestingProviders = onTestingProvidersChange
87
+ ? (id: string, isTesting: boolean) => onTestingProvidersChange(id, isTesting)
88
+ : setInternalTestingProviders;
89
+ const testingTimeLeft = externalTestingTimeLeft ?? internalTestingTimeLeft;
90
+ const setTestingTimeLeft = onTestingTimeLeftChange
91
+ ? (id: string, seconds: number | undefined) => onTestingTimeLeftChange(id, seconds)
92
+ : (id: string, seconds: number | undefined) => {
93
+ if (seconds === undefined) {
94
+ setInternalTestingTimeLeft((prev) => {
95
+ const next = { ...prev };
96
+ delete next[id];
97
+ return next;
98
+ });
99
+ } else {
100
+ setInternalTestingTimeLeft((prev) => ({ ...prev, [id]: seconds }));
101
+ }
102
+ };
103
+
55
104
  const fetchProviders = useCallback(async (): Promise<void> => {
56
105
  try {
57
106
  const providersRes = await fetch("/api/providers");
@@ -81,23 +130,55 @@ export function ProvidersPanel(): JSX.Element {
81
130
  })();
82
131
  }, [fetchProviders]);
83
132
 
84
- const runTest = useCallback(async (providerId: string): Promise<void> => {
85
- setTestingProviders((prev) => new Set(prev).add(providerId));
86
- try {
87
- const res = await fetch(`/api/providers/${providerId}/test`, { method: "POST" });
88
- if (res.ok) {
89
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
90
- const results = (await res.json()) as TestResults;
91
- setTestResults((prev) => ({ ...prev, [providerId]: results }));
133
+ const TEST_TIMEOUT_SECONDS = 30;
134
+
135
+ const runTest = useCallback(
136
+ async (providerId: string): Promise<void> => {
137
+ // Use callback form if available, otherwise direct set
138
+ if (onTestingProvidersChange) {
139
+ onTestingProvidersChange(providerId, true);
140
+ } else {
141
+ setInternalTestingProviders((prev) => new Set(prev).add(providerId));
92
142
  }
93
- } finally {
94
- setTestingProviders((prev) => {
95
- const next = new Set(prev);
96
- next.delete(providerId);
97
- return next;
98
- });
99
- }
100
- }, []);
143
+
144
+ // Start countdown
145
+ let remaining = TEST_TIMEOUT_SECONDS;
146
+ setTestingTimeLeft(providerId, remaining);
147
+ const intervalId = setInterval(() => {
148
+ remaining--;
149
+ setTestingTimeLeft(providerId, remaining);
150
+ if (remaining <= 0) {
151
+ clearInterval(intervalId);
152
+ }
153
+ }, 1000);
154
+
155
+ try {
156
+ const res = await fetch(`/api/providers/${providerId}/test`, { method: "POST" });
157
+ if (res.ok) {
158
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
159
+ const results = (await res.json()) as TestResults;
160
+ if (onTestResultsChange) {
161
+ onTestResultsChange(providerId, results);
162
+ } else {
163
+ setInternalTestResults((prev) => ({ ...prev, [providerId]: results }));
164
+ }
165
+ }
166
+ } finally {
167
+ clearInterval(intervalId);
168
+ setTestingTimeLeft(providerId, undefined);
169
+ if (onTestingProvidersChange) {
170
+ onTestingProvidersChange(providerId, false);
171
+ } else {
172
+ setInternalTestingProviders((prev) => {
173
+ const next = new Set(prev);
174
+ next.delete(providerId);
175
+ return next;
176
+ });
177
+ }
178
+ }
179
+ },
180
+ [onTestingProvidersChange, onTestResultsChange, setTestingTimeLeft],
181
+ );
101
182
 
102
183
  function handleAddProvider(data: {
103
184
  name: string;
@@ -279,6 +360,7 @@ export function ProvidersPanel(): JSX.Element {
279
360
  provider={provider}
280
361
  testResults={testResults[provider.id]}
281
362
  isTesting={testingProviders.has(provider.id)}
363
+ testingTimeLeft={testingTimeLeft[provider.id]}
282
364
  onEdit={(p) => setEditingProvider(p)}
283
365
  onDelete={handleDeleteProvider}
284
366
  onTest={(id: string) => {
@@ -1,13 +1,53 @@
1
- import { type JSX, useState } from "react";
1
+ import { type JSX, useState, useCallback } from "react";
2
2
  import { Settings } from "lucide-react";
3
3
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog";
4
4
  import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs";
5
5
  import { Button } from "../ui/button";
6
6
  import { ProvidersPanel } from "./ProvidersPanel";
7
+ import type { ProviderConfig } from "../../proxy/providers";
7
8
 
8
9
  export function SettingsDialog(): JSX.Element {
9
10
  const [open, setOpen] = useState(false);
10
11
  const [activeTab, setActiveTab] = useState("providers");
12
+ const [providers, setProviders] = useState<ProviderConfig[]>([]);
13
+ const [testResults, setTestResults] = useState<
14
+ Record<string, import("./ProvidersPanel").TestResults>
15
+ >({});
16
+ const [testingProviders, setTestingProviders] = useState<Set<string>>(new Set());
17
+ const [testingTimeLeft, setTestingTimeLeft] = useState<Record<string, number>>({});
18
+
19
+ const handleTestResultsChange = useCallback(
20
+ (providerId: string, results: import("./ProvidersPanel").TestResults) => {
21
+ setTestResults((prev) => ({ ...prev, [providerId]: results }));
22
+ },
23
+ [],
24
+ );
25
+
26
+ const handleTestingProvidersChange = useCallback((providerId: string, isTesting: boolean) => {
27
+ setTestingProviders((prev) => {
28
+ const next = new Set(prev);
29
+ if (isTesting) {
30
+ next.add(providerId);
31
+ } else {
32
+ next.delete(providerId);
33
+ }
34
+ return next;
35
+ });
36
+ }, []);
37
+
38
+ const handleTestingTimeLeftChange = useCallback(
39
+ (providerId: string, seconds: number | undefined) => {
40
+ setTestingTimeLeft((prev) => {
41
+ if (seconds === undefined) {
42
+ const next = { ...prev };
43
+ delete next[providerId];
44
+ return next;
45
+ }
46
+ return { ...prev, [providerId]: seconds };
47
+ });
48
+ },
49
+ [],
50
+ );
11
51
 
12
52
  return (
13
53
  <Dialog open={open} onOpenChange={setOpen}>
@@ -29,7 +69,16 @@ export function SettingsDialog(): JSX.Element {
29
69
 
30
70
  <div className="mt-4 overflow-y-auto flex-1">
31
71
  <TabsContent value="providers">
32
- <ProvidersPanel />
72
+ <ProvidersPanel
73
+ externalProviders={providers}
74
+ externalTestResults={testResults}
75
+ externalTestingProviders={testingProviders}
76
+ externalTestingTimeLeft={testingTimeLeft}
77
+ onProvidersChange={setProviders}
78
+ onTestResultsChange={handleTestResultsChange}
79
+ onTestingProvidersChange={handleTestingProvidersChange}
80
+ onTestingTimeLeftChange={handleTestingTimeLeftChange}
81
+ />
33
82
  </TabsContent>
34
83
  </div>
35
84
  </Tabs>