@tonyclaw/agent-inspector 3.0.3 → 3.0.5

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.
Files changed (37) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-CufpM7_P.js → CompareDrawer-Co-35_Xw.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-DPv1rBo7.js +106 -0
  4. package/.output/public/assets/{ReplayDialog-ClDmxv57.js → ReplayDialog-DghbvKdy.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-DcuIghmp.js → RequestAnatomy-CRXE5v8X.js} +1 -1
  6. package/.output/public/assets/{ResponseView-C7Tl47j3.js → ResponseView-CzxoQRjp.js} +2 -2
  7. package/.output/public/assets/{StreamingChunkSequence-DBK_R540.js → StreamingChunkSequence-DnFyF_Hr.js} +1 -1
  8. package/.output/public/assets/{_sessionId-D8Ke-EcI.js → _sessionId-DHRhWzKe.js} +1 -1
  9. package/.output/public/assets/index-D8cruW0P.css +1 -0
  10. package/.output/public/assets/index-DZyTpd2w.js +1 -0
  11. package/.output/public/assets/{index-CpWH7b2O.js → index-Dl1oki9E.js} +1 -1
  12. package/.output/public/assets/{json-viewer-BEifdKM_.js → json-viewer-B84f7oiG.js} +1 -1
  13. package/.output/public/assets/{main-DDfExS3_.js → main-DViDnJ9X.js} +2 -2
  14. package/.output/server/_libs/lucide-react.mjs +2 -2
  15. package/.output/server/{_sessionId-Di7z893V.mjs → _sessionId-C43vEAWe.mjs} +2 -2
  16. package/.output/server/_ssr/{CompareDrawer-ABv-8FMB.mjs → CompareDrawer-DOL6q0jD.mjs} +2 -2
  17. package/.output/server/_ssr/{ProxyViewerContainer-CGX-1aUr.mjs → ProxyViewerContainer-S9izw3ND.mjs} +572 -272
  18. package/.output/server/_ssr/{ReplayDialog-DD31ftzG.mjs → ReplayDialog-tqthwJh0.mjs} +3 -3
  19. package/.output/server/_ssr/{RequestAnatomy-54kNHAjH.mjs → RequestAnatomy-CgWDjFPR.mjs} +2 -2
  20. package/.output/server/_ssr/{ResponseView-CRnibtlX.mjs → ResponseView-Dk8Op622.mjs} +2 -2
  21. package/.output/server/_ssr/{StreamingChunkSequence-Cb5WGfMm.mjs → StreamingChunkSequence-AlS7PK8f.mjs} +2 -2
  22. package/.output/server/_ssr/{index-BJ-8DomR.mjs → index-BGwvMN2M.mjs} +2 -2
  23. package/.output/server/_ssr/index.mjs +2 -2
  24. package/.output/server/_ssr/{json-viewer-DB5188I7.mjs → json-viewer-CKHkh5U3.mjs} +2 -2
  25. package/.output/server/_ssr/{router-BKf70uDr.mjs → router-BJMaHkr1.mjs} +9 -9
  26. package/.output/server/{_tanstack-start-manifest_v-DUuf3Rgf.mjs → _tanstack-start-manifest_v-C_HvRzgP.mjs} +1 -1
  27. package/.output/server/index.mjs +66 -66
  28. package/package.json +1 -1
  29. package/src/components/ProxyViewer.tsx +92 -25
  30. package/src/components/ProxyViewerContainer.tsx +154 -55
  31. package/src/components/providers/ProviderCard.tsx +413 -230
  32. package/src/components/proxy-viewer/ConversationGroup.tsx +1 -1
  33. package/src/components/proxy-viewer/ConversationHeader.tsx +4 -5
  34. package/src/lib/stopReason.ts +4 -0
  35. package/.output/public/assets/ProxyViewerContainer-Dppr2zGE.js +0 -106
  36. package/.output/public/assets/index-BskTYcAH.css +0 -1
  37. package/.output/public/assets/index-DRHfTEM5.js +0 -1
@@ -275,6 +275,209 @@ function ProviderEndpointSummary({
275
275
  );
276
276
  }
277
277
 
278
+ function providerHasEndpoint(value: string | undefined): value is string {
279
+ return value !== undefined && value !== "";
280
+ }
281
+
282
+ function providerTestTargets(provider: ProviderConfig): string[] {
283
+ const targets: string[] = [];
284
+ if (providerHasEndpoint(provider.anthropicBaseUrl)) {
285
+ targets.push("Anthropic");
286
+ }
287
+ if (providerHasEndpoint(provider.openaiBaseUrl)) {
288
+ targets.push("Chat");
289
+ }
290
+ if (providerHasEndpoint(provider.openaiResponsesBaseUrl)) {
291
+ targets.push("Responses");
292
+ }
293
+ return targets;
294
+ }
295
+
296
+ function ProviderTestCoverage({ provider }: { provider: ProviderConfig }): JSX.Element {
297
+ const targets = providerTestTargets(provider);
298
+ if (targets.length === 0) {
299
+ return (
300
+ <div className="text-xs text-muted-foreground">
301
+ Test requires at least one configured endpoint.
302
+ </div>
303
+ );
304
+ }
305
+
306
+ return (
307
+ <div className="flex min-w-0 flex-wrap items-center gap-1.5 text-xs text-muted-foreground">
308
+ <span>Test covers</span>
309
+ {targets.map((target) => (
310
+ <span
311
+ key={target}
312
+ className="rounded border border-border/70 bg-muted/25 px-1.5 py-0.5 text-foreground/80"
313
+ >
314
+ {target}
315
+ </span>
316
+ ))}
317
+ <span>non-stream + stream</span>
318
+ </div>
319
+ );
320
+ }
321
+
322
+ type ProviderTestSummary = {
323
+ total: number;
324
+ passed: number;
325
+ failed: number;
326
+ testing: number;
327
+ };
328
+
329
+ function configuredFormatResults(
330
+ provider: ProviderConfig,
331
+ testResults: TestResults | undefined,
332
+ ): ProviderFormatTestResults[] {
333
+ if (testResults === undefined) return [];
334
+ const results: ProviderFormatTestResults[] = [];
335
+ if (providerHasEndpoint(provider.anthropicBaseUrl)) {
336
+ results.push(testResults.anthropic);
337
+ }
338
+ if (providerHasEndpoint(provider.openaiBaseUrl)) {
339
+ results.push(testResults.openaiChat);
340
+ }
341
+ if (providerHasEndpoint(provider.openaiResponsesBaseUrl)) {
342
+ results.push(testResults.openaiResponses);
343
+ }
344
+ return results;
345
+ }
346
+
347
+ function summarizeTestResults(
348
+ provider: ProviderConfig,
349
+ testResults: TestResults | undefined,
350
+ ): ProviderTestSummary {
351
+ const summary: ProviderTestSummary = { total: 0, passed: 0, failed: 0, testing: 0 };
352
+ for (const results of configuredFormatResults(provider, testResults)) {
353
+ for (const state of [results.nonStreaming, results.streaming]) {
354
+ if (isNotConfiguredState(state)) continue;
355
+ summary.total += 1;
356
+ if (hasSuccessField(state)) {
357
+ if (state.success) {
358
+ summary.passed += 1;
359
+ } else {
360
+ summary.failed += 1;
361
+ }
362
+ } else {
363
+ summary.testing += 1;
364
+ }
365
+ }
366
+ }
367
+ return summary;
368
+ }
369
+
370
+ function ProviderTestPill({
371
+ provider,
372
+ testResults,
373
+ isTesting,
374
+ }: {
375
+ provider: ProviderConfig;
376
+ testResults: TestResults | undefined;
377
+ isTesting: boolean;
378
+ }): JSX.Element {
379
+ const targets = providerTestTargets(provider);
380
+ const summary = summarizeTestResults(provider, testResults);
381
+
382
+ if (isTesting) {
383
+ return (
384
+ <span className="inline-flex items-center gap-1 rounded border border-cyan-300/25 bg-cyan-300/[0.08] px-2 py-1 text-xs text-cyan-100">
385
+ <RotateCw className="size-3 animate-spin" />
386
+ Testing
387
+ </span>
388
+ );
389
+ }
390
+
391
+ if (targets.length === 0) {
392
+ return (
393
+ <span className="rounded border border-border/70 bg-muted/20 px-2 py-1 text-xs text-muted-foreground">
394
+ No endpoint
395
+ </span>
396
+ );
397
+ }
398
+
399
+ if (testResults === undefined || summary.total === 0) {
400
+ return (
401
+ <span className="rounded border border-border/70 bg-muted/20 px-2 py-1 text-xs text-muted-foreground">
402
+ Not tested
403
+ </span>
404
+ );
405
+ }
406
+
407
+ if (summary.failed > 0) {
408
+ return (
409
+ <span className="inline-flex items-center gap-1 rounded border border-red-400/25 bg-red-500/[0.08] px-2 py-1 text-xs text-red-200">
410
+ <AlertCircle className="size-3" />
411
+ {summary.passed}/{summary.total} passed
412
+ </span>
413
+ );
414
+ }
415
+
416
+ if (summary.testing > 0 || summary.passed < summary.total) {
417
+ return (
418
+ <span className="rounded border border-amber-300/25 bg-amber-400/[0.08] px-2 py-1 text-xs text-amber-100">
419
+ Partial {summary.passed}/{summary.total}
420
+ </span>
421
+ );
422
+ }
423
+
424
+ return (
425
+ <span className="inline-flex items-center gap-1 rounded border border-cyan-300/25 bg-cyan-300/[0.08] px-2 py-1 text-xs text-cyan-100">
426
+ <CheckCircle className="size-3" />
427
+ OK {summary.passed}/{summary.total}
428
+ </span>
429
+ );
430
+ }
431
+
432
+ function compactModelLabel(provider: ProviderConfig): string {
433
+ const models = provider.models ?? [];
434
+ if (models.length === 0) return "No models configured";
435
+ const visible = models.slice(0, 2).join(", ");
436
+ const hiddenCount = models.length - 2;
437
+ return hiddenCount > 0 ? `${visible} +${hiddenCount} more` : visible;
438
+ }
439
+
440
+ function ModelChip({ provider, model }: { provider: ProviderConfig; model: string }): JSX.Element {
441
+ const metadata = findProviderModelMetadata(provider, model);
442
+ const sourceLink = getModelSourceLink(metadata?.sourceUrl);
443
+ return (
444
+ <span className="inline-flex items-center gap-1 rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground">
445
+ <span>{model}</span>
446
+ {metadata !== null && metadata.contextWindow !== undefined && (
447
+ <span className="font-mono text-foreground/80">
448
+ {formatContextWindowTokens(metadata.contextWindow)}
449
+ </span>
450
+ )}
451
+ {sourceLink !== null && (
452
+ <TooltipProvider>
453
+ <Tooltip>
454
+ <TooltipTrigger asChild>
455
+ <a
456
+ href={sourceLink.url}
457
+ target="_blank"
458
+ rel="noopener noreferrer"
459
+ className="inline-flex items-center gap-0.5 rounded border border-border/70 bg-background/70 px-1 font-mono text-[10px] leading-4 text-muted-foreground transition-colors hover:text-foreground"
460
+ aria-label={sourceLink.title}
461
+ >
462
+ {sourceLink.label}
463
+ </a>
464
+ </TooltipTrigger>
465
+ <TooltipContent>{sourceLink.title}</TooltipContent>
466
+ </Tooltip>
467
+ </TooltipProvider>
468
+ )}
469
+ </span>
470
+ );
471
+ }
472
+
473
+ function modelResultsLabel(testResults: TestResults | undefined): string | null {
474
+ const models = testResults?.models;
475
+ if (models === undefined) return null;
476
+ const count = Object.keys(models).length;
477
+ if (count === 0) return null;
478
+ return `${count} model${count === 1 ? "" : "s"} tested`;
479
+ }
480
+
278
481
  function getErrorIcon(type: ErrorType): JSX.Element {
279
482
  const iconProps = { className: "size-3", strokeWidth: 2 };
280
483
  switch (type) {
@@ -476,6 +679,7 @@ export function ProviderCard({
476
679
  }: ProviderCardProps): JSX.Element {
477
680
  const [showApiKey, setShowApiKey] = useState(false);
478
681
  const [copied, setCopied] = useState(false);
682
+ const [showDetails, setShowDetails] = useState(false);
479
683
  const [showModelResults, setShowModelResults] = useState(false);
480
684
 
481
685
  function handleCopy() {
@@ -486,7 +690,6 @@ export function ProviderCard({
486
690
  });
487
691
  }
488
692
 
489
- // Get docs URL: use provider.apiDocsUrl if configured, otherwise check KNOWN_PROVIDER_DOCS
490
693
  const docsUrl =
491
694
  provider.apiDocsUrl ??
492
695
  Object.entries(KNOWN_PROVIDER_DOCS).find(([keyword]) =>
@@ -496,6 +699,8 @@ export function ProviderCard({
496
699
  const canRefreshMetadata =
497
700
  provider.modelMetadataUrl !== undefined && provider.modelMetadataUrl.trim() !== "";
498
701
  const metadataImportLabel = canRefreshMetadata ? "Refresh" : "Import defaults";
702
+ const isTestingValue = isTesting ?? false;
703
+ const testedModelsLabel = modelResultsLabel(testResults);
499
704
  const providerSourceBadge =
500
705
  provider.source === "company" ? (
501
706
  <TooltipProvider>
@@ -523,90 +728,53 @@ export function ProviderCard({
523
728
 
524
729
  return (
525
730
  <div
526
- className={`border border-border bg-card shadow-sm flex flex-col gap-3 rounded-[8px] p-4 transition-all duration-500 ${isHighlighted === true ? "ring-2 ring-primary shadow-md" : ""}`}
731
+ className={`flex flex-col gap-3 rounded-[8px] border border-border bg-card p-4 shadow-sm transition-all duration-500 ${isHighlighted === true ? "ring-2 ring-primary shadow-md" : ""}`}
527
732
  >
528
733
  <div className="flex items-start justify-between gap-2">
529
- <div className="flex items-center gap-2 min-w-0">
530
- <span className="font-medium truncate">{provider.name}</span>
531
- {providerSourceBadge}
532
- {providerSourceBadge === null && provider.source === "company" && (
533
- <TooltipProvider>
534
- <Tooltip>
535
- <TooltipTrigger asChild>
536
- <span className="text-xs px-1.5 py-0.5 rounded bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 shrink-0">
537
- 公司
538
- </span>
539
- </TooltipTrigger>
540
- <TooltipContent>Company-provided API key</TooltipContent>
541
- </Tooltip>
542
- </TooltipProvider>
543
- )}
544
- {providerSourceBadge === null && provider.source === "personal" && (
545
- <TooltipProvider>
546
- <Tooltip>
547
- <TooltipTrigger asChild>
548
- <span className="text-xs px-1.5 py-0.5 rounded bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400 shrink-0">
549
- 个人
550
- </span>
551
- </TooltipTrigger>
552
- <TooltipContent>Your personal API key</TooltipContent>
553
- </Tooltip>
554
- </TooltipProvider>
734
+ <div className="min-w-0 space-y-1">
735
+ <div className="flex min-w-0 items-center gap-2">
736
+ <span className="truncate font-medium">{provider.name}</span>
737
+ {providerSourceBadge}
738
+ </div>
739
+ <div className="truncate text-xs text-muted-foreground">
740
+ {compactModelLabel(provider)}
741
+ </div>
742
+ </div>
743
+ <div className="flex shrink-0 items-center gap-2">
744
+ <ProviderTestPill
745
+ provider={provider}
746
+ testResults={testResults}
747
+ isTesting={isTestingValue}
748
+ />
749
+ {docsUrl !== undefined && (
750
+ <a
751
+ href={docsUrl}
752
+ target="_blank"
753
+ rel="noopener noreferrer"
754
+ className="flex items-center gap-1 text-xs text-muted-foreground transition-colors hover:text-foreground"
755
+ title="View API documentation"
756
+ >
757
+ <ExternalLink className="size-3" />
758
+ <span className="sr-only">Docs</span>
759
+ </a>
555
760
  )}
556
761
  </div>
557
- {docsUrl !== undefined && (
558
- <a
559
- href={docsUrl}
560
- target="_blank"
561
- rel="noopener noreferrer"
562
- className="text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1 text-xs"
563
- title="View API documentation"
564
- >
565
- <ExternalLink className="size-3" />
566
- <span className="sr-only">Docs</span>
567
- </a>
568
- )}
569
762
  </div>
570
763
 
571
- {provider.models !== undefined && provider.models.length > 0 && (
572
- <div className="flex flex-wrap gap-1">
573
- {provider.models.map((m) => {
574
- const metadata = findProviderModelMetadata(provider, m);
575
- const sourceLink = getModelSourceLink(metadata?.sourceUrl);
576
- return (
577
- <span
578
- key={m}
579
- className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground"
580
- >
581
- <span>{m}</span>
582
- {metadata !== null && metadata.contextWindow !== undefined && (
583
- <span className="font-mono text-foreground/80">
584
- {formatContextWindowTokens(metadata.contextWindow)}
585
- </span>
586
- )}
587
- {sourceLink !== null && (
588
- <TooltipProvider>
589
- <Tooltip>
590
- <TooltipTrigger asChild>
591
- <a
592
- href={sourceLink.url}
593
- target="_blank"
594
- rel="noopener noreferrer"
595
- className="inline-flex items-center gap-0.5 rounded border border-border/70 bg-background/70 px-1 font-mono text-[10px] leading-4 text-muted-foreground transition-colors hover:text-foreground"
596
- aria-label={sourceLink.title}
597
- >
598
- {sourceLink.label}
599
- </a>
600
- </TooltipTrigger>
601
- <TooltipContent>{sourceLink.title}</TooltipContent>
602
- </Tooltip>
603
- </TooltipProvider>
604
- )}
605
- </span>
606
- );
607
- })}
764
+ <div className="flex flex-col gap-2 text-xs text-muted-foreground sm:flex-row sm:items-center sm:justify-between">
765
+ <ProviderTestCoverage provider={provider} />
766
+ <div className="flex flex-wrap items-center gap-2">
767
+ <span>Key configured</span>
768
+ <span>{provider.authHeader === "x-api-key" ? "x-api-key" : "Bearer"}</span>
769
+ {testedModelsLabel !== null && <span>{testedModelsLabel}</span>}
770
+ {testResults?.testedAt !== undefined && (
771
+ <span className="inline-flex items-center gap-1">
772
+ <Clock className="size-3" />
773
+ {formatTimeAgo(testResults.testedAt)}
774
+ </span>
775
+ )}
608
776
  </div>
609
- )}
777
+ </div>
610
778
 
611
779
  {!hasContextMetadata && (
612
780
  <div className="flex items-center justify-between gap-2 rounded-md border border-amber-500/25 bg-amber-500/6 px-2 py-1.5 text-xs text-muted-foreground">
@@ -638,179 +806,194 @@ export function ProviderCard({
638
806
  </div>
639
807
  )}
640
808
 
641
- <div className="flex items-center gap-2">
642
- <code className="text-xs text-muted-foreground bg-muted px-2 py-1 rounded flex-1 truncate">
643
- {showApiKey ? provider.apiKey : maskApiKey(provider.apiKey)}
644
- </code>
645
- <button
646
- type="button"
647
- onClick={() => setShowApiKey((s) => !s)}
648
- className="text-muted-foreground hover:text-foreground transition-colors p-1"
649
- aria-label={showApiKey ? "Hide API key" : "Show API key"}
650
- >
651
- {showApiKey ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
652
- </button>
653
- <button
654
- type="button"
655
- onClick={handleCopy}
656
- className="text-muted-foreground hover:text-foreground transition-colors p-1"
657
- aria-label="Copy API key"
658
- >
659
- {copied ? <Check className="size-4 text-cyan-300" /> : <Copy className="size-4" />}
660
- </button>
661
- </div>
809
+ {showDetails && (
810
+ <div className="space-y-3 border-t border-border/60 pt-3">
811
+ <div className="flex items-center gap-2">
812
+ <code className="flex-1 truncate rounded bg-muted px-2 py-1 text-xs text-muted-foreground">
813
+ {showApiKey ? provider.apiKey : maskApiKey(provider.apiKey)}
814
+ </code>
815
+ <button
816
+ type="button"
817
+ onClick={() => setShowApiKey((s) => !s)}
818
+ className="p-1 text-muted-foreground transition-colors hover:text-foreground"
819
+ aria-label={showApiKey ? "Hide API key" : "Show API key"}
820
+ >
821
+ {showApiKey ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
822
+ </button>
823
+ <button
824
+ type="button"
825
+ onClick={handleCopy}
826
+ className="p-1 text-muted-foreground transition-colors hover:text-foreground"
827
+ aria-label="Copy API key"
828
+ >
829
+ {copied ? <Check className="size-4 text-cyan-300" /> : <Copy className="size-4" />}
830
+ </button>
831
+ </div>
662
832
 
663
- {provider.anthropicBaseUrl !== undefined && provider.anthropicBaseUrl !== "" && (
664
- <ProviderEndpointSummary
665
- label="Anthropic"
666
- baseUrl={provider.anthropicBaseUrl}
667
- endpoint={PATH_V1_MESSAGES}
668
- providerName={provider.name}
669
- >
670
- {testResults !== undefined && (
671
- <ProviderFormatTestStatus results={testResults.anthropic} />
833
+ {provider.models !== undefined && provider.models.length > 0 && (
834
+ <div className="flex flex-wrap gap-1">
835
+ {provider.models.map((model) => (
836
+ <ModelChip key={model} provider={provider} model={model} />
837
+ ))}
838
+ </div>
672
839
  )}
673
- </ProviderEndpointSummary>
674
- )}
675
840
 
676
- {provider.openaiBaseUrl !== undefined && provider.openaiBaseUrl !== "" && (
677
- <ProviderEndpointSummary
678
- label="OpenAI Chat"
679
- baseUrl={provider.openaiBaseUrl}
680
- endpoint={PATH_V1_CHAT_COMPLETIONS}
681
- providerName={provider.name}
682
- >
683
- {testResults !== undefined && (
684
- <ProviderFormatTestStatus results={testResults.openaiChat} />
841
+ {providerHasEndpoint(provider.anthropicBaseUrl) && (
842
+ <ProviderEndpointSummary
843
+ label="Anthropic"
844
+ baseUrl={provider.anthropicBaseUrl}
845
+ endpoint={PATH_V1_MESSAGES}
846
+ providerName={provider.name}
847
+ >
848
+ {testResults !== undefined && (
849
+ <ProviderFormatTestStatus results={testResults.anthropic} />
850
+ )}
851
+ </ProviderEndpointSummary>
685
852
  )}
686
- </ProviderEndpointSummary>
687
- )}
688
853
 
689
- {provider.openaiResponsesBaseUrl !== undefined && provider.openaiResponsesBaseUrl !== "" && (
690
- <ProviderEndpointSummary
691
- label="OpenAI Responses"
692
- baseUrl={provider.openaiResponsesBaseUrl}
693
- endpoint={PATH_V1_RESPONSES}
694
- providerName={provider.name}
695
- >
696
- {testResults !== undefined && (
697
- <ProviderFormatTestStatus results={testResults.openaiResponses} />
854
+ {providerHasEndpoint(provider.openaiBaseUrl) && (
855
+ <ProviderEndpointSummary
856
+ label="Chat"
857
+ baseUrl={provider.openaiBaseUrl}
858
+ endpoint={PATH_V1_CHAT_COMPLETIONS}
859
+ providerName={provider.name}
860
+ >
861
+ {testResults !== undefined && (
862
+ <ProviderFormatTestStatus results={testResults.openaiChat} />
863
+ )}
864
+ </ProviderEndpointSummary>
698
865
  )}
699
- </ProviderEndpointSummary>
700
- )}
701
866
 
702
- {testResults?.testedAt !== undefined && (
703
- <div className="text-xs text-muted-foreground flex items-center gap-1">
704
- <Clock className="size-3" />
705
- <span>Tested {formatTimeAgo(testResults.testedAt)}</span>
706
- </div>
707
- )}
708
- {testResults?.models !== undefined && Object.keys(testResults.models).length > 0 && (
709
- <div className="border-t pt-2">
710
- <button
711
- type="button"
712
- onClick={() => setShowModelResults((value) => !value)}
713
- className="text-xs text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1"
714
- >
715
- {showModelResults ? (
716
- <ChevronDown className="size-3" />
717
- ) : (
718
- <ChevronRight className="size-3" />
719
- )}
720
- Model Test Results ({Object.keys(testResults.models).length})
721
- </button>
722
- {showModelResults && (
723
- <div className="mt-2 overflow-x-auto">
724
- <table className="w-full text-xs border-collapse">
725
- <thead>
726
- <tr className="border-b border-border">
727
- <th className="text-left py-1 px-2 font-medium text-muted-foreground">Model</th>
728
- {provider.anthropicBaseUrl !== undefined &&
729
- provider.anthropicBaseUrl !== "" && (
730
- <th className="text-left py-1 px-2 font-medium text-muted-foreground">
731
- Anthropic
732
- </th>
733
- )}
734
- {provider.openaiBaseUrl !== undefined && provider.openaiBaseUrl !== "" && (
735
- <th className="text-left py-1 px-2 font-medium text-muted-foreground">
736
- Chat
737
- </th>
738
- )}
739
- {provider.openaiResponsesBaseUrl !== undefined &&
740
- provider.openaiResponsesBaseUrl !== "" && (
741
- <th className="text-left py-1 px-2 font-medium text-muted-foreground">
742
- Responses
867
+ {providerHasEndpoint(provider.openaiResponsesBaseUrl) && (
868
+ <ProviderEndpointSummary
869
+ label="Responses"
870
+ baseUrl={provider.openaiResponsesBaseUrl}
871
+ endpoint={PATH_V1_RESPONSES}
872
+ providerName={provider.name}
873
+ >
874
+ {testResults !== undefined && (
875
+ <ProviderFormatTestStatus results={testResults.openaiResponses} />
876
+ )}
877
+ </ProviderEndpointSummary>
878
+ )}
879
+
880
+ {testResults?.models !== undefined && Object.keys(testResults.models).length > 0 && (
881
+ <div>
882
+ <button
883
+ type="button"
884
+ onClick={() => setShowModelResults((value) => !value)}
885
+ className="flex items-center gap-1 text-xs text-muted-foreground transition-colors hover:text-foreground"
886
+ >
887
+ {showModelResults ? (
888
+ <ChevronDown className="size-3" />
889
+ ) : (
890
+ <ChevronRight className="size-3" />
891
+ )}
892
+ Model Test Results ({Object.keys(testResults.models).length})
893
+ </button>
894
+ {showModelResults && (
895
+ <div className="mt-2 overflow-x-auto">
896
+ <table className="w-full border-collapse text-xs">
897
+ <thead>
898
+ <tr className="border-b border-border">
899
+ <th className="px-2 py-1 text-left font-medium text-muted-foreground">
900
+ Model
743
901
  </th>
744
- )}
745
- </tr>
746
- </thead>
747
- <tbody>
748
- {Object.entries(testResults.models).map(([modelName, modelResult]) => (
749
- <tr key={modelName} className="border-b border-border/50 last:border-0">
750
- <td className="py-1 px-2 font-medium">{modelName}</td>
751
- {provider.anthropicBaseUrl !== undefined &&
752
- provider.anthropicBaseUrl !== "" && (
753
- <td className="py-1 px-2 align-top">
754
- <ProviderFormatTestStatus results={modelResult.anthropic} />
755
- </td>
902
+ {providerHasEndpoint(provider.anthropicBaseUrl) && (
903
+ <th className="px-2 py-1 text-left font-medium text-muted-foreground">
904
+ Anthropic
905
+ </th>
756
906
  )}
757
- {provider.openaiBaseUrl !== undefined && provider.openaiBaseUrl !== "" && (
758
- <td className="py-1 px-2 align-top">
759
- <ProviderFormatTestStatus results={modelResult.openaiChat} />
760
- </td>
761
- )}
762
- {provider.openaiResponsesBaseUrl !== undefined &&
763
- provider.openaiResponsesBaseUrl !== "" && (
764
- <td className="py-1 px-2 align-top">
765
- <ProviderFormatTestStatus results={modelResult.openaiResponses} />
766
- </td>
907
+ {providerHasEndpoint(provider.openaiBaseUrl) && (
908
+ <th className="px-2 py-1 text-left font-medium text-muted-foreground">
909
+ Chat
910
+ </th>
767
911
  )}
768
- </tr>
769
- ))}
770
- </tbody>
771
- </table>
912
+ {providerHasEndpoint(provider.openaiResponsesBaseUrl) && (
913
+ <th className="px-2 py-1 text-left font-medium text-muted-foreground">
914
+ Responses
915
+ </th>
916
+ )}
917
+ </tr>
918
+ </thead>
919
+ <tbody>
920
+ {Object.entries(testResults.models).map(([modelName, modelResult]) => (
921
+ <tr key={modelName} className="border-b border-border/50 last:border-0">
922
+ <td className="px-2 py-1 font-medium">{modelName}</td>
923
+ {providerHasEndpoint(provider.anthropicBaseUrl) && (
924
+ <td className="px-2 py-1 align-top">
925
+ <ProviderFormatTestStatus results={modelResult.anthropic} />
926
+ </td>
927
+ )}
928
+ {providerHasEndpoint(provider.openaiBaseUrl) && (
929
+ <td className="px-2 py-1 align-top">
930
+ <ProviderFormatTestStatus results={modelResult.openaiChat} />
931
+ </td>
932
+ )}
933
+ {providerHasEndpoint(provider.openaiResponsesBaseUrl) && (
934
+ <td className="px-2 py-1 align-top">
935
+ <ProviderFormatTestStatus results={modelResult.openaiResponses} />
936
+ </td>
937
+ )}
938
+ </tr>
939
+ ))}
940
+ </tbody>
941
+ </table>
942
+ </div>
943
+ )}
772
944
  </div>
773
945
  )}
774
946
  </div>
775
947
  )}
776
948
 
777
- <div className="flex gap-2 pt-1 border-t">
778
- {onTest !== undefined && (
949
+ <div className="flex flex-col gap-2 border-t pt-1 sm:flex-row sm:items-center sm:justify-between">
950
+ <button
951
+ type="button"
952
+ onClick={() => setShowDetails((value) => !value)}
953
+ className="inline-flex items-center gap-1 text-xs text-muted-foreground transition-colors hover:text-foreground"
954
+ aria-expanded={showDetails}
955
+ >
956
+ {showDetails ? <ChevronDown className="size-3" /> : <ChevronRight className="size-3" />}
957
+ Details
958
+ </button>
959
+ <div className="flex gap-2">
960
+ {onTest !== undefined && (
961
+ <Button
962
+ variant="outline"
963
+ size="sm"
964
+ onClick={() => onTest(provider.id)}
965
+ className="text-xs h-7 gap-1"
966
+ disabled={isTestingValue}
967
+ >
968
+ <RotateCw className={`size-3 ${isTestingValue ? "animate-spin" : ""}`} />
969
+ {isTestingValue
970
+ ? testingTimeLeft !== undefined
971
+ ? `Testing (${testingTimeLeft}s)`
972
+ : "Testing..."
973
+ : "Test"}
974
+ </Button>
975
+ )}
779
976
  <Button
780
977
  variant="outline"
781
978
  size="sm"
782
- onClick={() => onTest(provider.id)}
979
+ onClick={() => onEdit(provider)}
783
980
  className="text-xs h-7 gap-1"
784
- disabled={isTesting ?? false}
981
+ disabled={isTestingValue}
785
982
  >
786
- <RotateCw className={`size-3 ${(isTesting ?? false) ? "animate-spin" : ""}`} />
787
- {(isTesting ?? false)
788
- ? testingTimeLeft !== undefined
789
- ? `Testing (${testingTimeLeft}s)`
790
- : "Testing..."
791
- : "Test"}
983
+ <Pencil className="size-3" />
984
+ Edit
792
985
  </Button>
793
- )}
794
- <Button
795
- variant="outline"
796
- size="sm"
797
- onClick={() => onEdit(provider)}
798
- className="text-xs h-7 gap-1"
799
- disabled={isTesting ?? false}
800
- >
801
- <Pencil className="size-3" />
802
- Edit
803
- </Button>
804
- <Button
805
- variant="outline"
806
- size="sm"
807
- onClick={() => onDelete(provider.id)}
808
- className="text-xs h-7 gap-1 text-destructive hover:text-destructive"
809
- disabled={isTesting ?? false}
810
- >
811
- <Trash2 className="size-3" />
812
- Delete
813
- </Button>
986
+ <Button
987
+ variant="outline"
988
+ size="sm"
989
+ onClick={() => onDelete(provider.id)}
990
+ className="text-xs h-7 gap-1 text-destructive hover:text-destructive"
991
+ disabled={isTestingValue}
992
+ >
993
+ <Trash2 className="size-3" />
994
+ Delete
995
+ </Button>
996
+ </div>
814
997
  </div>
815
998
  </div>
816
999
  );