@tonyclaw/llm-inspector 1.7.7 → 1.7.8

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-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};
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-C8o6bEv6.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-CZSteFqT.mjs";
2
+ import { C as CapturedLogSchema, a as parseRequest, p as parseOpenAIResponse, I as InspectorResponseSchema } from "./router-MmnX-LYh.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";
@@ -2567,6 +2567,7 @@ function ProvidersPanel({
2567
2567
  } else {
2568
2568
  setInternalTestingProviders((prev) => new Set(prev).add(providerId));
2569
2569
  }
2570
+ const controller = new AbortController();
2570
2571
  let remaining = TEST_TIMEOUT_SECONDS;
2571
2572
  setTestingTimeLeft(providerId, remaining);
2572
2573
  const intervalId = setInterval(() => {
@@ -2574,10 +2575,14 @@ function ProvidersPanel({
2574
2575
  setTestingTimeLeft(providerId, remaining);
2575
2576
  if (remaining <= 0) {
2576
2577
  clearInterval(intervalId);
2578
+ controller.abort();
2577
2579
  }
2578
2580
  }, 1e3);
2579
2581
  try {
2580
- const res = await fetch(`/api/providers/${providerId}/test`, { method: "POST" });
2582
+ const res = await fetch(`/api/providers/${providerId}/test`, {
2583
+ method: "POST",
2584
+ signal: controller.signal
2585
+ });
2581
2586
  if (res.ok) {
2582
2587
  const results = await res.json();
2583
2588
  if (onTestResultsChange) {
@@ -2585,6 +2590,52 @@ function ProvidersPanel({
2585
2590
  } else {
2586
2591
  setInternalTestResults((prev) => ({ ...prev, [providerId]: results }));
2587
2592
  }
2593
+ } else {
2594
+ const errorResult = {
2595
+ anthropic: {
2596
+ nonStreaming: {
2597
+ success: false,
2598
+ error: { message: `HTTP ${res.status}: ${res.statusText}`, type: "server_error" }
2599
+ },
2600
+ streaming: { notConfigured: true }
2601
+ },
2602
+ openai: {
2603
+ nonStreaming: {
2604
+ success: false,
2605
+ error: { message: `HTTP ${res.status}: ${res.statusText}`, type: "server_error" }
2606
+ },
2607
+ streaming: { notConfigured: true }
2608
+ }
2609
+ };
2610
+ if (onTestResultsChange) {
2611
+ onTestResultsChange(providerId, errorResult);
2612
+ } else {
2613
+ setInternalTestResults((prev) => ({ ...prev, [providerId]: errorResult }));
2614
+ }
2615
+ }
2616
+ } catch (err) {
2617
+ if (err instanceof Error && err.name === "AbortError") {
2618
+ const timeoutResult = {
2619
+ anthropic: {
2620
+ nonStreaming: {
2621
+ success: false,
2622
+ error: { message: "Request timed out", type: "timeout" }
2623
+ },
2624
+ streaming: { notConfigured: true }
2625
+ },
2626
+ openai: {
2627
+ nonStreaming: {
2628
+ success: false,
2629
+ error: { message: "Request timed out", type: "timeout" }
2630
+ },
2631
+ streaming: { notConfigured: true }
2632
+ }
2633
+ };
2634
+ if (onTestResultsChange) {
2635
+ onTestResultsChange(providerId, timeoutResult);
2636
+ } else {
2637
+ setInternalTestResults((prev) => ({ ...prev, [providerId]: timeoutResult }));
2638
+ }
2588
2639
  }
2589
2640
  } finally {
2590
2641
  clearInterval(intervalId);
@@ -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-DgsS3z4y.mjs");
200
+ const { tsrStartManifest } = await import("../_tanstack-start-manifest_v-CYKtU_9S.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-CZSteFqT.mjs").then((n) => n.r);
769
+ const routerEntry = await import("./router-MmnX-LYh.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-DbpsE3Y4.mjs");
68
+ const $$splitComponentImporter = () => import("./index-hNquJMfH.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-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" });
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-Bxc5pKCu.js"], "assets": [] }, "/": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/index.tsx", "assets": [], "preloads": ["/assets/index-C8o6bEv6.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-Bxc5pKCu.js" });
2
2
  export {
3
3
  tsrStartManifest
4
4
  };
@@ -100,51 +100,51 @@ const assets = {
100
100
  "/assets/index-B3RwBPLW.css": {
101
101
  "type": "text/css; charset=utf-8",
102
102
  "etag": '"10c74-aXacU4DRFVsUwcC5jHnjoPRSlTA"',
103
- "mtime": "2026-06-03T12:05:46.053Z",
103
+ "mtime": "2026-06-03T12:12:14.334Z",
104
104
  "size": 68724,
105
105
  "path": "../public/assets/index-B3RwBPLW.css"
106
106
  },
107
107
  "/assets/alibaba-TTwafVwX.svg": {
108
108
  "type": "image/svg+xml",
109
109
  "etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
110
- "mtime": "2026-06-03T12:05:46.051Z",
110
+ "mtime": "2026-06-03T12:12:14.334Z",
111
111
  "size": 5915,
112
112
  "path": "../public/assets/alibaba-TTwafVwX.svg"
113
113
  },
114
- "/assets/main-DYXtPYU8.js": {
114
+ "/assets/minimax-BPMzvuL-.jpeg": {
115
+ "type": "image/jpeg",
116
+ "etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
117
+ "mtime": "2026-06-03T12:12:14.334Z",
118
+ "size": 6918,
119
+ "path": "../public/assets/minimax-BPMzvuL-.jpeg"
120
+ },
121
+ "/assets/main-Bxc5pKCu.js": {
115
122
  "type": "text/javascript; charset=utf-8",
116
- "etag": '"4db57-3IoaJ0zzvDUU7WQZuVFYW7QEzGQ"',
117
- "mtime": "2026-06-03T12:05:46.053Z",
123
+ "etag": '"4db57-Hyx+4GtcEuVuTT+HldUhPQTOcL8"',
124
+ "mtime": "2026-06-03T12:12:14.334Z",
118
125
  "size": 318295,
119
- "path": "../public/assets/main-DYXtPYU8.js"
126
+ "path": "../public/assets/main-Bxc5pKCu.js"
120
127
  },
121
128
  "/assets/qwen-CONDcHqt.png": {
122
129
  "type": "image/png",
123
130
  "etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
124
- "mtime": "2026-06-03T12:05:46.053Z",
131
+ "mtime": "2026-06-03T12:12:14.334Z",
125
132
  "size": 357059,
126
133
  "path": "../public/assets/qwen-CONDcHqt.png"
127
134
  },
128
- "/assets/minimax-BPMzvuL-.jpeg": {
129
- "type": "image/jpeg",
130
- "etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
131
- "mtime": "2026-06-03T12:05:46.053Z",
132
- "size": 6918,
133
- "path": "../public/assets/minimax-BPMzvuL-.jpeg"
134
- },
135
135
  "/assets/zhipuai-BPNAnxo-.svg": {
136
136
  "type": "image/svg+xml",
137
137
  "etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
138
- "mtime": "2026-06-03T12:05:46.053Z",
138
+ "mtime": "2026-06-03T12:12:14.334Z",
139
139
  "size": 11256,
140
140
  "path": "../public/assets/zhipuai-BPNAnxo-.svg"
141
141
  },
142
- "/assets/index-Drusqil7.js": {
142
+ "/assets/index-C8o6bEv6.js": {
143
143
  "type": "text/javascript; charset=utf-8",
144
- "etag": '"8368b-K/ZW3jyne5SeFVJGXXNOEllhQyM"',
145
- "mtime": "2026-06-03T12:05:46.054Z",
146
- "size": 538251,
147
- "path": "../public/assets/index-Drusqil7.js"
144
+ "etag": '"83962-iIMvsvRc3BmqDsudqDxFOFuC3xg"',
145
+ "mtime": "2026-06-03T12:12:14.334Z",
146
+ "size": 538978,
147
+ "path": "../public/assets/index-C8o6bEv6.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.7",
3
+ "version": "1.7.8",
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",
@@ -141,6 +141,9 @@ export function ProvidersPanel({
141
141
  setInternalTestingProviders((prev) => new Set(prev).add(providerId));
142
142
  }
143
143
 
144
+ // Create abort controller for this test request
145
+ const controller = new AbortController();
146
+
144
147
  // Start countdown
145
148
  let remaining = TEST_TIMEOUT_SECONDS;
146
149
  setTestingTimeLeft(providerId, remaining);
@@ -149,11 +152,16 @@ export function ProvidersPanel({
149
152
  setTestingTimeLeft(providerId, remaining);
150
153
  if (remaining <= 0) {
151
154
  clearInterval(intervalId);
155
+ // Abort the fetch request when time runs out
156
+ controller.abort();
152
157
  }
153
158
  }, 1000);
154
159
 
155
160
  try {
156
- const res = await fetch(`/api/providers/${providerId}/test`, { method: "POST" });
161
+ const res = await fetch(`/api/providers/${providerId}/test`, {
162
+ method: "POST",
163
+ signal: controller.signal,
164
+ });
157
165
  if (res.ok) {
158
166
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
159
167
  const results = (await res.json()) as TestResults;
@@ -162,6 +170,54 @@ export function ProvidersPanel({
162
170
  } else {
163
171
  setInternalTestResults((prev) => ({ ...prev, [providerId]: results }));
164
172
  }
173
+ } else {
174
+ // Non-ok response, create error result
175
+ const errorResult: TestResults = {
176
+ anthropic: {
177
+ nonStreaming: {
178
+ success: false,
179
+ error: { message: `HTTP ${res.status}: ${res.statusText}`, type: "server_error" },
180
+ },
181
+ streaming: { notConfigured: true },
182
+ },
183
+ openai: {
184
+ nonStreaming: {
185
+ success: false,
186
+ error: { message: `HTTP ${res.status}: ${res.statusText}`, type: "server_error" },
187
+ },
188
+ streaming: { notConfigured: true },
189
+ },
190
+ };
191
+ if (onTestResultsChange) {
192
+ onTestResultsChange(providerId, errorResult);
193
+ } else {
194
+ setInternalTestResults((prev) => ({ ...prev, [providerId]: errorResult }));
195
+ }
196
+ }
197
+ } catch (err) {
198
+ // Check if this was an abort (timeout)
199
+ if (err instanceof Error && err.name === "AbortError") {
200
+ const timeoutResult: TestResults = {
201
+ anthropic: {
202
+ nonStreaming: {
203
+ success: false,
204
+ error: { message: "Request timed out", type: "timeout" },
205
+ },
206
+ streaming: { notConfigured: true },
207
+ },
208
+ openai: {
209
+ nonStreaming: {
210
+ success: false,
211
+ error: { message: "Request timed out", type: "timeout" },
212
+ },
213
+ streaming: { notConfigured: true },
214
+ },
215
+ };
216
+ if (onTestResultsChange) {
217
+ onTestResultsChange(providerId, timeoutResult);
218
+ } else {
219
+ setInternalTestResults((prev) => ({ ...prev, [providerId]: timeoutResult }));
220
+ }
165
221
  }
166
222
  } finally {
167
223
  clearInterval(intervalId);