@tonyclaw/llm-inspector 1.7.8 → 1.7.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/index-CB8ZIeEk.js +97 -0
- package/.output/public/assets/{main-Bxc5pKCu.js → main-BrU8NdGQ.js} +1 -1
- package/.output/server/_libs/lucide-react.mjs +98 -91
- package/.output/server/_libs/zod.mjs +1 -0
- package/.output/server/_ssr/{index-hNquJMfH.mjs → index-CAIDMqNv.mjs} +116 -21
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-MmnX-LYh.mjs → router-CsCLdrXq.mjs} +241 -43
- package/.output/server/_tanstack-start-manifest_v-BF6ge6dS.mjs +4 -0
- package/.output/server/index.mjs +20 -20
- package/package.json +1 -1
- package/src/components/providers/ProvidersPanel.tsx +120 -14
- package/src/components/proxy-viewer/ConversationGroup.tsx +3 -3
- package/src/components/proxy-viewer/LogEntry.tsx +6 -3
- package/src/components/proxy-viewer/LogEntryHeader.tsx +3 -2
- package/src/proxy/formats/anthropic/stream.ts +3 -2
- package/src/proxy/formats/openai/stream.ts +3 -2
- package/src/proxy/handler.ts +5 -0
- package/src/proxy/providers.ts +98 -0
- package/src/routes/api/providers.export.ts +26 -0
- package/src/routes/api/providers.import.ts +47 -0
- package/.output/public/assets/index-C8o6bEv6.js +0 -97
- package/.output/server/_tanstack-start-manifest_v-CYKtU_9S.mjs +0 -4
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type JSX, useState, useEffect, useCallback, useRef } from "react";
|
|
2
|
+
import { z } from "zod";
|
|
2
3
|
import { Button } from "../ui/button";
|
|
3
|
-
import { Plus, AlertCircle, Copy, Check } from "lucide-react";
|
|
4
|
+
import { Plus, AlertCircle, Copy, Check, Download, Upload } from "lucide-react";
|
|
4
5
|
import { ProviderCard } from "./ProviderCard";
|
|
5
6
|
import { ProviderForm } from "./ProviderForm";
|
|
6
7
|
import type { ProviderConfig } from "../../proxy/providers";
|
|
@@ -134,6 +135,17 @@ export function ProvidersPanel({
|
|
|
134
135
|
|
|
135
136
|
const runTest = useCallback(
|
|
136
137
|
async (providerId: string): Promise<void> => {
|
|
138
|
+
// Clear previous test results when starting a new test
|
|
139
|
+
const resetResults: TestResults = {
|
|
140
|
+
anthropic: { nonStreaming: { notConfigured: true }, streaming: { notConfigured: true } },
|
|
141
|
+
openai: { nonStreaming: { notConfigured: true }, streaming: { notConfigured: true } },
|
|
142
|
+
};
|
|
143
|
+
if (onTestResultsChange) {
|
|
144
|
+
onTestResultsChange(providerId, resetResults);
|
|
145
|
+
} else {
|
|
146
|
+
setInternalTestResults((prev) => ({ ...prev, [providerId]: resetResults }));
|
|
147
|
+
}
|
|
148
|
+
|
|
137
149
|
// Use callback form if available, otherwise direct set
|
|
138
150
|
if (onTestingProvidersChange) {
|
|
139
151
|
onTestingProvidersChange(providerId, true);
|
|
@@ -196,7 +208,8 @@ export function ProvidersPanel({
|
|
|
196
208
|
}
|
|
197
209
|
} catch (err) {
|
|
198
210
|
// Check if this was an abort (timeout)
|
|
199
|
-
|
|
211
|
+
const isAbort = err instanceof Error && err.name === "AbortError";
|
|
212
|
+
if (isAbort) {
|
|
200
213
|
const timeoutResult: TestResults = {
|
|
201
214
|
anthropic: {
|
|
202
215
|
nonStreaming: {
|
|
@@ -219,17 +232,25 @@ export function ProvidersPanel({
|
|
|
219
232
|
setInternalTestResults((prev) => ({ ...prev, [providerId]: timeoutResult }));
|
|
220
233
|
}
|
|
221
234
|
}
|
|
235
|
+
// If it's not an abort error, the test results won't be updated
|
|
236
|
+
// which means the previous results will persist (or it will show "Not configured" reset state)
|
|
222
237
|
} finally {
|
|
238
|
+
// Always clear the countdown and testing state, even if an error occurs
|
|
223
239
|
clearInterval(intervalId);
|
|
224
240
|
setTestingTimeLeft(providerId, undefined);
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
241
|
+
// Ensure testingProviders state is cleared - wrap in try to guarantee execution
|
|
242
|
+
try {
|
|
243
|
+
if (onTestingProvidersChange) {
|
|
244
|
+
onTestingProvidersChange(providerId, false);
|
|
245
|
+
} else {
|
|
246
|
+
setInternalTestingProviders((prev) => {
|
|
247
|
+
const next = new Set(prev);
|
|
248
|
+
next.delete(providerId);
|
|
249
|
+
return next;
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
} catch {
|
|
253
|
+
// Ignore errors in state updates to ensure we always clean up
|
|
233
254
|
}
|
|
234
255
|
}
|
|
235
256
|
},
|
|
@@ -328,6 +349,74 @@ export function ProvidersPanel({
|
|
|
328
349
|
})();
|
|
329
350
|
}
|
|
330
351
|
|
|
352
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
353
|
+
|
|
354
|
+
function handleExport(includeKeys: boolean): void {
|
|
355
|
+
const url = `/api/providers/export${includeKeys ? "?includeKeys=true" : ""}`;
|
|
356
|
+
void (async () => {
|
|
357
|
+
try {
|
|
358
|
+
const res = await fetch(url);
|
|
359
|
+
if (!res.ok) {
|
|
360
|
+
setError("Failed to export providers");
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const blob = await res.blob();
|
|
364
|
+
const downloadUrl = URL.createObjectURL(blob);
|
|
365
|
+
const a = document.createElement("a");
|
|
366
|
+
a.href = downloadUrl;
|
|
367
|
+
a.download =
|
|
368
|
+
res.headers.get("Content-Disposition")?.match(/filename="(.+)"/)?.[1] ?? "providers.json";
|
|
369
|
+
document.body.appendChild(a);
|
|
370
|
+
a.click();
|
|
371
|
+
document.body.removeChild(a);
|
|
372
|
+
URL.revokeObjectURL(downloadUrl);
|
|
373
|
+
} catch {
|
|
374
|
+
setError("Failed to export providers");
|
|
375
|
+
}
|
|
376
|
+
})();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function handleImportClick(): void {
|
|
380
|
+
fileInputRef.current?.click();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function handleFileChange(e: React.ChangeEvent<HTMLInputElement>): void {
|
|
384
|
+
const file = e.target.files?.[0];
|
|
385
|
+
if (!file) return;
|
|
386
|
+
|
|
387
|
+
void (async () => {
|
|
388
|
+
try {
|
|
389
|
+
const text = await file.text();
|
|
390
|
+
const res = await fetch("/api/providers/import", {
|
|
391
|
+
method: "POST",
|
|
392
|
+
headers: { "Content-Type": "application/json" },
|
|
393
|
+
body: JSON.stringify(text),
|
|
394
|
+
});
|
|
395
|
+
const ImportResponseSchema = z.object({
|
|
396
|
+
success: z.boolean().optional(),
|
|
397
|
+
imported: z.number().optional(),
|
|
398
|
+
message: z.string().optional(),
|
|
399
|
+
errors: z.array(z.string()).optional(),
|
|
400
|
+
});
|
|
401
|
+
const data = ImportResponseSchema.parse(await res.json());
|
|
402
|
+
if (res.ok && data.imported !== undefined && data.imported > 0) {
|
|
403
|
+
await fetchProviders();
|
|
404
|
+
// Show success message via error state temporarily
|
|
405
|
+
setError(null);
|
|
406
|
+
// Use a ref or state to show success - for now just refresh list
|
|
407
|
+
} else if (data.errors && data.errors.length > 0) {
|
|
408
|
+
setError(data.errors.join("; "));
|
|
409
|
+
} else {
|
|
410
|
+
setError(data.message ?? "Import failed");
|
|
411
|
+
}
|
|
412
|
+
} catch {
|
|
413
|
+
setError("Failed to import providers");
|
|
414
|
+
}
|
|
415
|
+
// Reset file input
|
|
416
|
+
e.target.value = "";
|
|
417
|
+
})();
|
|
418
|
+
}
|
|
419
|
+
|
|
331
420
|
// Only show loading if we have no providers at all (prevents flashing when reopening Settings during test)
|
|
332
421
|
if (isLoading && providers.length === 0) {
|
|
333
422
|
return (
|
|
@@ -361,10 +450,27 @@ export function ProvidersPanel({
|
|
|
361
450
|
<div className="space-y-4">
|
|
362
451
|
<div className="flex items-center justify-between">
|
|
363
452
|
<h3 className="text-lg font-medium">Providers</h3>
|
|
364
|
-
<
|
|
365
|
-
<
|
|
366
|
-
|
|
367
|
-
|
|
453
|
+
<div className="flex items-center gap-2">
|
|
454
|
+
<Button variant="outline" size="sm" onClick={() => handleExport(false)} className="gap-1">
|
|
455
|
+
<Download className="size-3" />
|
|
456
|
+
Export
|
|
457
|
+
</Button>
|
|
458
|
+
<Button variant="outline" size="sm" onClick={handleImportClick} className="gap-1">
|
|
459
|
+
<Upload className="size-3" />
|
|
460
|
+
Import
|
|
461
|
+
</Button>
|
|
462
|
+
<input
|
|
463
|
+
type="file"
|
|
464
|
+
ref={fileInputRef}
|
|
465
|
+
accept=".json"
|
|
466
|
+
onChange={handleFileChange}
|
|
467
|
+
style={{ display: "none" }}
|
|
468
|
+
/>
|
|
469
|
+
<Button onClick={() => setShowForm(true)} size="sm" className="gap-1">
|
|
470
|
+
<Plus className="size-4" />
|
|
471
|
+
Add Provider
|
|
472
|
+
</Button>
|
|
473
|
+
</div>
|
|
368
474
|
</div>
|
|
369
475
|
|
|
370
476
|
{configPath !== null && (
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
1
|
+
import { useState, memo } from "react";
|
|
2
2
|
import type { JSX } from "react";
|
|
3
3
|
import type { CapturedLog } from "../../proxy/schemas";
|
|
4
4
|
import {
|
|
@@ -26,7 +26,7 @@ function computeStats(logs: CapturedLog[]): {
|
|
|
26
26
|
return { totalInputTokens: totalInput, totalOutputTokens: totalOutput };
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
export function ConversationGroup({
|
|
29
|
+
export const ConversationGroup = memo(function ConversationGroup({
|
|
30
30
|
group,
|
|
31
31
|
viewMode = "simple",
|
|
32
32
|
}: ConversationGroupProps): JSX.Element {
|
|
@@ -65,4 +65,4 @@ export function ConversationGroup({
|
|
|
65
65
|
)}
|
|
66
66
|
</div>
|
|
67
67
|
);
|
|
68
|
-
}
|
|
68
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Check, Copy, RotateCcw } from "lucide-react";
|
|
2
2
|
import type { JSX } from "react";
|
|
3
|
-
import { useMemo, useState } from "react";
|
|
3
|
+
import { useMemo, useState, memo } from "react";
|
|
4
4
|
import { cn } from "../../lib/utils";
|
|
5
5
|
import { type CapturedLog, parseRequest } from "../../proxy/schemas";
|
|
6
6
|
import { Button } from "../ui/button";
|
|
@@ -49,7 +49,10 @@ function CopyButton({
|
|
|
49
49
|
);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export
|
|
52
|
+
export const LogEntry = memo(function LogEntry({
|
|
53
|
+
log,
|
|
54
|
+
viewMode = "simple",
|
|
55
|
+
}: LogEntryProps): JSX.Element {
|
|
53
56
|
const [expanded, setExpanded] = useState<boolean>(false);
|
|
54
57
|
const [requestCopied, setRequestCopied] = useState<boolean>(false);
|
|
55
58
|
const [responseCopied, setResponseCopied] = useState<boolean>(false);
|
|
@@ -222,4 +225,4 @@ export function LogEntry({ log, viewMode = "simple" }: LogEntryProps): JSX.Eleme
|
|
|
222
225
|
<ReplayDialog log={log} open={replayOpen} onOpenChange={setReplayOpen} />
|
|
223
226
|
</>
|
|
224
227
|
);
|
|
225
|
-
}
|
|
228
|
+
});
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
Zap,
|
|
13
13
|
} from "lucide-react";
|
|
14
14
|
import type { JSX } from "react";
|
|
15
|
+
import { memo } from "react";
|
|
15
16
|
import { cn, formatTokens, getStatusCategory, type StatusCategory } from "../../lib/utils";
|
|
16
17
|
import type { CapturedLog, InspectorRequest } from "../../proxy/schemas";
|
|
17
18
|
import { Badge } from "../ui/badge";
|
|
@@ -42,7 +43,7 @@ export type LogEntryHeaderProps = {
|
|
|
42
43
|
onToggle: () => void;
|
|
43
44
|
};
|
|
44
45
|
|
|
45
|
-
export function LogEntryHeader({
|
|
46
|
+
export const LogEntryHeader = memo(function LogEntryHeader({
|
|
46
47
|
log,
|
|
47
48
|
parsedRequest,
|
|
48
49
|
expanded,
|
|
@@ -247,4 +248,4 @@ export function LogEntryHeader({
|
|
|
247
248
|
)}
|
|
248
249
|
</div>
|
|
249
250
|
);
|
|
250
|
-
}
|
|
251
|
+
});
|
|
@@ -63,9 +63,10 @@ export function extractAnthropicStream(
|
|
|
63
63
|
const chunks: Array<{ index: number; timestamp: number; type: string; data: JsonValue }> = [];
|
|
64
64
|
|
|
65
65
|
for (const line of raw.split("\n")) {
|
|
66
|
-
|
|
66
|
+
const trimmedLine = line.trim();
|
|
67
|
+
if (!trimmedLine.startsWith("data: ")) continue;
|
|
67
68
|
try {
|
|
68
|
-
const json: unknown = JSON.parse(
|
|
69
|
+
const json: unknown = JSON.parse(trimmedLine.slice(6));
|
|
69
70
|
const parsed = SseEventSchema.safeParse(json);
|
|
70
71
|
if (!parsed.success) continue;
|
|
71
72
|
const data = parsed.data;
|
|
@@ -35,8 +35,9 @@ export function extractOpenAIStream(
|
|
|
35
35
|
const chunks: Array<{ index: number; timestamp: number; type: string; data: JsonValue }> = [];
|
|
36
36
|
|
|
37
37
|
for (const line of raw.split("\n")) {
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
const trimmedLine = line.trim();
|
|
39
|
+
if (!trimmedLine.startsWith("data: ")) continue;
|
|
40
|
+
const dataStr = trimmedLine.slice(6);
|
|
40
41
|
if (dataStr === "[DONE]") break;
|
|
41
42
|
|
|
42
43
|
try {
|
package/src/proxy/handler.ts
CHANGED
|
@@ -127,6 +127,11 @@ function parseRequestPath(req: Request, url: URL): ParsedRequestPath {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
function buildUpstreamUrl(upstreamBase: string, normalizedPath: string): string {
|
|
130
|
+
// Handle case where upstreamBase already ends with /v1 and normalizedPath starts with /v1/
|
|
131
|
+
// to avoid double /v1/v1/ duplication
|
|
132
|
+
if (upstreamBase.endsWith("/v1") && normalizedPath.startsWith("/v1/")) {
|
|
133
|
+
return upstreamBase + normalizedPath.slice(3); // Remove leading /v1 from path
|
|
134
|
+
}
|
|
130
135
|
return upstreamBase + normalizedPath;
|
|
131
136
|
}
|
|
132
137
|
|
package/src/proxy/providers.ts
CHANGED
|
@@ -202,6 +202,104 @@ export function deleteProvider(id: string): boolean {
|
|
|
202
202
|
return true;
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Export all providers as a JSON string (without API keys for security).
|
|
207
|
+
*/
|
|
208
|
+
export function exportProviders(): string {
|
|
209
|
+
const providers = getProviders();
|
|
210
|
+
// Mask API keys for security
|
|
211
|
+
const safeProviders = providers.map(({ apiKey, ...rest }) => ({
|
|
212
|
+
...rest,
|
|
213
|
+
apiKey: maskApiKey(apiKey),
|
|
214
|
+
}));
|
|
215
|
+
return JSON.stringify(safeProviders, null, 2);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Export all providers as a JSON string including API keys (user must opt-in).
|
|
220
|
+
*/
|
|
221
|
+
export function exportProvidersWithKeys(): string {
|
|
222
|
+
return JSON.stringify(getProviders(), null, 2);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Import providers from a JSON string.
|
|
227
|
+
* Returns { imported: number, errors: string[] }
|
|
228
|
+
*/
|
|
229
|
+
export function importProviders(json: string): { imported: number; errors: string[] } {
|
|
230
|
+
const errors: string[] = [];
|
|
231
|
+
let imported = 0;
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const parsed: unknown = JSON.parse(json);
|
|
235
|
+
|
|
236
|
+
// Use Zod to validate the structure
|
|
237
|
+
const ImportSchema = z.union([
|
|
238
|
+
z.array(ProviderConfigSchema),
|
|
239
|
+
z.object({
|
|
240
|
+
providers: z.array(ProviderConfigSchema),
|
|
241
|
+
}),
|
|
242
|
+
]);
|
|
243
|
+
|
|
244
|
+
const parseResult = ImportSchema.safeParse(parsed);
|
|
245
|
+
if (!parseResult.success) {
|
|
246
|
+
return {
|
|
247
|
+
imported: 0,
|
|
248
|
+
errors: [`Invalid format: ${parseResult.error.message}`],
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const providerArray = Array.isArray(parseResult.data)
|
|
253
|
+
? parseResult.data
|
|
254
|
+
: parseResult.data.providers;
|
|
255
|
+
|
|
256
|
+
for (let i = 0; i < providerArray.length; i++) {
|
|
257
|
+
const item = providerArray[i];
|
|
258
|
+
const result = ProviderConfigSchema.safeParse(item);
|
|
259
|
+
|
|
260
|
+
if (!result.success) {
|
|
261
|
+
errors.push(`Provider ${i + 1}: ${result.error.message}`);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const provider = result.data;
|
|
266
|
+
const existing = getProviders().find(
|
|
267
|
+
(p) => p.name === provider.name && p.anthropicBaseUrl === provider.anthropicBaseUrl,
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
if (existing) {
|
|
271
|
+
errors.push(`Provider "${provider.name}" already exists, skipping`);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Generate new ID and add
|
|
276
|
+
const newProvider: ProviderConfig = {
|
|
277
|
+
...provider,
|
|
278
|
+
id: randomUUID(),
|
|
279
|
+
createdAt: new Date().toISOString(),
|
|
280
|
+
updatedAt: new Date().toISOString(),
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const providers = getProviders();
|
|
284
|
+
providers.push(newProvider);
|
|
285
|
+
store.set("providers", providers);
|
|
286
|
+
imported++;
|
|
287
|
+
}
|
|
288
|
+
} catch (err) {
|
|
289
|
+
return {
|
|
290
|
+
imported: 0,
|
|
291
|
+
errors: [`Failed to parse JSON: ${err instanceof Error ? err.message : String(err)}`],
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return { imported, errors };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function maskApiKey(apiKey: string): string {
|
|
299
|
+
if (apiKey.length <= 8) return "••••••••";
|
|
300
|
+
return apiKey.slice(0, 4) + "••••••••" + apiKey.slice(-4);
|
|
301
|
+
}
|
|
302
|
+
|
|
205
303
|
/**
|
|
206
304
|
* Converts display model name to API usage name based on provider-specific rules.
|
|
207
305
|
* Default behavior: returns model as-is.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { exportProviders, exportProvidersWithKeys } from "../../proxy/providers";
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute("/api/providers/export")({
|
|
5
|
+
server: {
|
|
6
|
+
handlers: {
|
|
7
|
+
GET: ({ request }: { request: Request }) => {
|
|
8
|
+
const url = new URL(request.url);
|
|
9
|
+
const includeKeys = url.searchParams.get("includeKeys") === "true";
|
|
10
|
+
|
|
11
|
+
const json = includeKeys ? exportProvidersWithKeys() : exportProviders();
|
|
12
|
+
|
|
13
|
+
const filename = includeKeys
|
|
14
|
+
? `llm-inspector-providers-${Date.now()}.json`
|
|
15
|
+
: `llm-inspector-providers-safe-${Date.now()}.json`;
|
|
16
|
+
|
|
17
|
+
return new Response(json, {
|
|
18
|
+
headers: {
|
|
19
|
+
"Content-Type": "application/json",
|
|
20
|
+
"Content-Disposition": `attachment; filename="${filename}"`,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { importProviders } from "../../proxy/providers";
|
|
4
|
+
|
|
5
|
+
const ImportRequestSchema = z.union([z.string(), z.object({ providers: z.unknown() })]);
|
|
6
|
+
|
|
7
|
+
export const Route = createFileRoute("/api/providers/import")({
|
|
8
|
+
server: {
|
|
9
|
+
handlers: {
|
|
10
|
+
POST: async ({ request }: { request: Request }) => {
|
|
11
|
+
try {
|
|
12
|
+
const rawBody = await request.text();
|
|
13
|
+
let jsonContent: string;
|
|
14
|
+
try {
|
|
15
|
+
const parsedBody: unknown = JSON.parse(rawBody);
|
|
16
|
+
jsonContent = typeof parsedBody === "string" ? parsedBody : JSON.stringify(parsedBody);
|
|
17
|
+
} catch {
|
|
18
|
+
jsonContent = rawBody;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!jsonContent || jsonContent.trim() === "") {
|
|
22
|
+
return Response.json({ error: "No JSON content provided" }, { status: 400 });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const result = importProviders(jsonContent);
|
|
26
|
+
|
|
27
|
+
return Response.json({
|
|
28
|
+
success: result.imported > 0,
|
|
29
|
+
imported: result.imported,
|
|
30
|
+
errors: result.errors,
|
|
31
|
+
message:
|
|
32
|
+
result.imported > 0
|
|
33
|
+
? `Successfully imported ${result.imported} provider(s)`
|
|
34
|
+
: result.errors.length > 0
|
|
35
|
+
? `Import completed with ${result.errors.length} error(s)`
|
|
36
|
+
: "No providers imported",
|
|
37
|
+
});
|
|
38
|
+
} catch (err) {
|
|
39
|
+
return Response.json(
|
|
40
|
+
{ error: `Failed to import: ${err instanceof Error ? err.message : String(err)}` },
|
|
41
|
+
{ status: 400 },
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
});
|