@tonyclaw/llm-inspector 1.7.6 → 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.
@@ -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,111 @@ 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
+ // Create abort controller for this test request
145
+ const controller = new AbortController();
146
+
147
+ // Start countdown
148
+ let remaining = TEST_TIMEOUT_SECONDS;
149
+ setTestingTimeLeft(providerId, remaining);
150
+ const intervalId = setInterval(() => {
151
+ remaining--;
152
+ setTestingTimeLeft(providerId, remaining);
153
+ if (remaining <= 0) {
154
+ clearInterval(intervalId);
155
+ // Abort the fetch request when time runs out
156
+ controller.abort();
157
+ }
158
+ }, 1000);
159
+
160
+ try {
161
+ const res = await fetch(`/api/providers/${providerId}/test`, {
162
+ method: "POST",
163
+ signal: controller.signal,
164
+ });
165
+ if (res.ok) {
166
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
167
+ const results = (await res.json()) as TestResults;
168
+ if (onTestResultsChange) {
169
+ onTestResultsChange(providerId, results);
170
+ } else {
171
+ setInternalTestResults((prev) => ({ ...prev, [providerId]: results }));
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
+ }
221
+ }
222
+ } finally {
223
+ clearInterval(intervalId);
224
+ setTestingTimeLeft(providerId, undefined);
225
+ if (onTestingProvidersChange) {
226
+ onTestingProvidersChange(providerId, false);
227
+ } else {
228
+ setInternalTestingProviders((prev) => {
229
+ const next = new Set(prev);
230
+ next.delete(providerId);
231
+ return next;
232
+ });
233
+ }
234
+ }
235
+ },
236
+ [onTestingProvidersChange, onTestResultsChange, setTestingTimeLeft],
237
+ );
101
238
 
102
239
  function handleAddProvider(data: {
103
240
  name: string;
@@ -279,6 +416,7 @@ export function ProvidersPanel(): JSX.Element {
279
416
  provider={provider}
280
417
  testResults={testResults[provider.id]}
281
418
  isTesting={testingProviders.has(provider.id)}
419
+ testingTimeLeft={testingTimeLeft[provider.id]}
282
420
  onEdit={(p) => setEditingProvider(p)}
283
421
  onDelete={handleDeleteProvider}
284
422
  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>