@tonyclaw/agent-inspector 3.0.4 → 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 (32) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-Bs2kzUSp.js → CompareDrawer-Co-35_Xw.js} +1 -1
  3. package/.output/public/assets/{ProxyViewerContainer-BUnlrfiS.js → ProxyViewerContainer-DPv1rBo7.js} +28 -28
  4. package/.output/public/assets/{ReplayDialog-B6xy7pl5.js → ReplayDialog-DghbvKdy.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-BSvSmpCO.js → RequestAnatomy-CRXE5v8X.js} +1 -1
  6. package/.output/public/assets/{ResponseView-BFnUva7Z.js → ResponseView-CzxoQRjp.js} +2 -2
  7. package/.output/public/assets/{StreamingChunkSequence-u7ZaJOqn.js → StreamingChunkSequence-DnFyF_Hr.js} +1 -1
  8. package/.output/public/assets/{_sessionId-CH4nsMOA.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-B5HS6AxR.js → index-Dl1oki9E.js} +1 -1
  12. package/.output/public/assets/{json-viewer-5Tj5vkqe.js → json-viewer-B84f7oiG.js} +1 -1
  13. package/.output/public/assets/{main-ChxZWG8K.js → main-DViDnJ9X.js} +2 -2
  14. package/.output/server/_libs/lucide-react.mjs +2 -2
  15. package/.output/server/{_sessionId-B3r-VBdM.mjs → _sessionId-C43vEAWe.mjs} +2 -2
  16. package/.output/server/_ssr/{CompareDrawer-DAYrKHKe.mjs → CompareDrawer-DOL6q0jD.mjs} +2 -2
  17. package/.output/server/_ssr/{ProxyViewerContainer-CYmNmQK9.mjs → ProxyViewerContainer-S9izw3ND.mjs} +262 -146
  18. package/.output/server/_ssr/{ReplayDialog-CdKvdXxk.mjs → ReplayDialog-tqthwJh0.mjs} +3 -3
  19. package/.output/server/_ssr/{RequestAnatomy-BtmTUq7O.mjs → RequestAnatomy-CgWDjFPR.mjs} +2 -2
  20. package/.output/server/_ssr/{ResponseView-DuD_z5hd.mjs → ResponseView-Dk8Op622.mjs} +2 -2
  21. package/.output/server/_ssr/{StreamingChunkSequence-DpIV2wt_.mjs → StreamingChunkSequence-AlS7PK8f.mjs} +2 -2
  22. package/.output/server/_ssr/{index-B4_8vH8k.mjs → index-BGwvMN2M.mjs} +2 -2
  23. package/.output/server/_ssr/index.mjs +2 -2
  24. package/.output/server/_ssr/{json-viewer-gcNJEB7y.mjs → json-viewer-CKHkh5U3.mjs} +2 -2
  25. package/.output/server/_ssr/{router-CYjIsElo.mjs → router-BJMaHkr1.mjs} +9 -9
  26. package/.output/server/{_tanstack-start-manifest_v-BRdOxY6q.mjs → _tanstack-start-manifest_v-C_HvRzgP.mjs} +1 -1
  27. package/.output/server/index.mjs +65 -65
  28. package/package.json +1 -1
  29. package/src/components/providers/ProviderCard.tsx +342 -206
  30. package/src/lib/stopReason.ts +4 -0
  31. package/.output/public/assets/index-BqVL284D.js +0 -1
  32. package/.output/public/assets/index-DJW6qbnH.css +0 -1
@@ -319,6 +319,165 @@ function ProviderTestCoverage({ provider }: { provider: ProviderConfig }): JSX.E
319
319
  );
320
320
  }
321
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
+
322
481
  function getErrorIcon(type: ErrorType): JSX.Element {
323
482
  const iconProps = { className: "size-3", strokeWidth: 2 };
324
483
  switch (type) {
@@ -520,6 +679,7 @@ export function ProviderCard({
520
679
  }: ProviderCardProps): JSX.Element {
521
680
  const [showApiKey, setShowApiKey] = useState(false);
522
681
  const [copied, setCopied] = useState(false);
682
+ const [showDetails, setShowDetails] = useState(false);
523
683
  const [showModelResults, setShowModelResults] = useState(false);
524
684
 
525
685
  function handleCopy() {
@@ -530,7 +690,6 @@ export function ProviderCard({
530
690
  });
531
691
  }
532
692
 
533
- // Get docs URL: use provider.apiDocsUrl if configured, otherwise check KNOWN_PROVIDER_DOCS
534
693
  const docsUrl =
535
694
  provider.apiDocsUrl ??
536
695
  Object.entries(KNOWN_PROVIDER_DOCS).find(([keyword]) =>
@@ -540,6 +699,8 @@ export function ProviderCard({
540
699
  const canRefreshMetadata =
541
700
  provider.modelMetadataUrl !== undefined && provider.modelMetadataUrl.trim() !== "";
542
701
  const metadataImportLabel = canRefreshMetadata ? "Refresh" : "Import defaults";
702
+ const isTestingValue = isTesting ?? false;
703
+ const testedModelsLabel = modelResultsLabel(testResults);
543
704
  const providerSourceBadge =
544
705
  provider.source === "company" ? (
545
706
  <TooltipProvider>
@@ -567,90 +728,53 @@ export function ProviderCard({
567
728
 
568
729
  return (
569
730
  <div
570
- 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" : ""}`}
571
732
  >
572
733
  <div className="flex items-start justify-between gap-2">
573
- <div className="flex items-center gap-2 min-w-0">
574
- <span className="font-medium truncate">{provider.name}</span>
575
- {providerSourceBadge}
576
- {providerSourceBadge === null && provider.source === "company" && (
577
- <TooltipProvider>
578
- <Tooltip>
579
- <TooltipTrigger asChild>
580
- <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">
581
- 公司
582
- </span>
583
- </TooltipTrigger>
584
- <TooltipContent>Company-provided API key</TooltipContent>
585
- </Tooltip>
586
- </TooltipProvider>
587
- )}
588
- {providerSourceBadge === null && provider.source === "personal" && (
589
- <TooltipProvider>
590
- <Tooltip>
591
- <TooltipTrigger asChild>
592
- <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">
593
- 个人
594
- </span>
595
- </TooltipTrigger>
596
- <TooltipContent>Your personal API key</TooltipContent>
597
- </Tooltip>
598
- </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>
599
760
  )}
600
761
  </div>
601
- {docsUrl !== undefined && (
602
- <a
603
- href={docsUrl}
604
- target="_blank"
605
- rel="noopener noreferrer"
606
- className="text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1 text-xs"
607
- title="View API documentation"
608
- >
609
- <ExternalLink className="size-3" />
610
- <span className="sr-only">Docs</span>
611
- </a>
612
- )}
613
762
  </div>
614
763
 
615
- {provider.models !== undefined && provider.models.length > 0 && (
616
- <div className="flex flex-wrap gap-1">
617
- {provider.models.map((m) => {
618
- const metadata = findProviderModelMetadata(provider, m);
619
- const sourceLink = getModelSourceLink(metadata?.sourceUrl);
620
- return (
621
- <span
622
- key={m}
623
- className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground"
624
- >
625
- <span>{m}</span>
626
- {metadata !== null && metadata.contextWindow !== undefined && (
627
- <span className="font-mono text-foreground/80">
628
- {formatContextWindowTokens(metadata.contextWindow)}
629
- </span>
630
- )}
631
- {sourceLink !== null && (
632
- <TooltipProvider>
633
- <Tooltip>
634
- <TooltipTrigger asChild>
635
- <a
636
- href={sourceLink.url}
637
- target="_blank"
638
- rel="noopener noreferrer"
639
- 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"
640
- aria-label={sourceLink.title}
641
- >
642
- {sourceLink.label}
643
- </a>
644
- </TooltipTrigger>
645
- <TooltipContent>{sourceLink.title}</TooltipContent>
646
- </Tooltip>
647
- </TooltipProvider>
648
- )}
649
- </span>
650
- );
651
- })}
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
+ )}
652
776
  </div>
653
- )}
777
+ </div>
654
778
 
655
779
  {!hasContextMetadata && (
656
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">
@@ -682,144 +806,156 @@ export function ProviderCard({
682
806
  </div>
683
807
  )}
684
808
 
685
- <div className="flex items-center gap-2">
686
- <code className="text-xs text-muted-foreground bg-muted px-2 py-1 rounded flex-1 truncate">
687
- {showApiKey ? provider.apiKey : maskApiKey(provider.apiKey)}
688
- </code>
689
- <button
690
- type="button"
691
- onClick={() => setShowApiKey((s) => !s)}
692
- className="text-muted-foreground hover:text-foreground transition-colors p-1"
693
- aria-label={showApiKey ? "Hide API key" : "Show API key"}
694
- >
695
- {showApiKey ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
696
- </button>
697
- <button
698
- type="button"
699
- onClick={handleCopy}
700
- className="text-muted-foreground hover:text-foreground transition-colors p-1"
701
- aria-label="Copy API key"
702
- >
703
- {copied ? <Check className="size-4 text-cyan-300" /> : <Copy className="size-4" />}
704
- </button>
705
- </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>
706
832
 
707
- {providerHasEndpoint(provider.anthropicBaseUrl) && (
708
- <ProviderEndpointSummary
709
- label="Test: Anthropic"
710
- baseUrl={provider.anthropicBaseUrl}
711
- endpoint={PATH_V1_MESSAGES}
712
- providerName={provider.name}
713
- >
714
- {testResults !== undefined && (
715
- <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>
716
839
  )}
717
- </ProviderEndpointSummary>
718
- )}
719
840
 
720
- {providerHasEndpoint(provider.openaiBaseUrl) && (
721
- <ProviderEndpointSummary
722
- label="Test: Chat"
723
- baseUrl={provider.openaiBaseUrl}
724
- endpoint={PATH_V1_CHAT_COMPLETIONS}
725
- providerName={provider.name}
726
- >
727
- {testResults !== undefined && (
728
- <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>
729
852
  )}
730
- </ProviderEndpointSummary>
731
- )}
732
853
 
733
- {providerHasEndpoint(provider.openaiResponsesBaseUrl) && (
734
- <ProviderEndpointSummary
735
- label="Test: Responses"
736
- baseUrl={provider.openaiResponsesBaseUrl}
737
- endpoint={PATH_V1_RESPONSES}
738
- providerName={provider.name}
739
- >
740
- {testResults !== undefined && (
741
- <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>
742
865
  )}
743
- </ProviderEndpointSummary>
744
- )}
745
866
 
746
- {testResults?.testedAt !== undefined && (
747
- <div className="text-xs text-muted-foreground flex items-center gap-1">
748
- <Clock className="size-3" />
749
- <span>Tested {formatTimeAgo(testResults.testedAt)}</span>
750
- </div>
751
- )}
752
- {testResults?.models !== undefined && Object.keys(testResults.models).length > 0 && (
753
- <div className="border-t pt-2">
754
- <button
755
- type="button"
756
- onClick={() => setShowModelResults((value) => !value)}
757
- className="text-xs text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1"
758
- >
759
- {showModelResults ? (
760
- <ChevronDown className="size-3" />
761
- ) : (
762
- <ChevronRight className="size-3" />
763
- )}
764
- Model Test Results ({Object.keys(testResults.models).length})
765
- </button>
766
- {showModelResults && (
767
- <div className="mt-2 overflow-x-auto">
768
- <table className="w-full text-xs border-collapse">
769
- <thead>
770
- <tr className="border-b border-border">
771
- <th className="text-left py-1 px-2 font-medium text-muted-foreground">Model</th>
772
- {provider.anthropicBaseUrl !== undefined &&
773
- provider.anthropicBaseUrl !== "" && (
774
- <th className="text-left py-1 px-2 font-medium text-muted-foreground">
775
- Anthropic
776
- </th>
777
- )}
778
- {provider.openaiBaseUrl !== undefined && provider.openaiBaseUrl !== "" && (
779
- <th className="text-left py-1 px-2 font-medium text-muted-foreground">
780
- Chat
781
- </th>
782
- )}
783
- {provider.openaiResponsesBaseUrl !== undefined &&
784
- provider.openaiResponsesBaseUrl !== "" && (
785
- <th className="text-left py-1 px-2 font-medium text-muted-foreground">
786
- 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
787
901
  </th>
788
- )}
789
- </tr>
790
- </thead>
791
- <tbody>
792
- {Object.entries(testResults.models).map(([modelName, modelResult]) => (
793
- <tr key={modelName} className="border-b border-border/50 last:border-0">
794
- <td className="py-1 px-2 font-medium">{modelName}</td>
795
- {provider.anthropicBaseUrl !== undefined &&
796
- provider.anthropicBaseUrl !== "" && (
797
- <td className="py-1 px-2 align-top">
798
- <ProviderFormatTestStatus results={modelResult.anthropic} />
799
- </td>
902
+ {providerHasEndpoint(provider.anthropicBaseUrl) && (
903
+ <th className="px-2 py-1 text-left font-medium text-muted-foreground">
904
+ Anthropic
905
+ </th>
906
+ )}
907
+ {providerHasEndpoint(provider.openaiBaseUrl) && (
908
+ <th className="px-2 py-1 text-left font-medium text-muted-foreground">
909
+ Chat
910
+ </th>
800
911
  )}
801
- {provider.openaiBaseUrl !== undefined && provider.openaiBaseUrl !== "" && (
802
- <td className="py-1 px-2 align-top">
803
- <ProviderFormatTestStatus results={modelResult.openaiChat} />
804
- </td>
805
- )}
806
- {provider.openaiResponsesBaseUrl !== undefined &&
807
- provider.openaiResponsesBaseUrl !== "" && (
808
- <td className="py-1 px-2 align-top">
809
- <ProviderFormatTestStatus results={modelResult.openaiResponses} />
810
- </td>
912
+ {providerHasEndpoint(provider.openaiResponsesBaseUrl) && (
913
+ <th className="px-2 py-1 text-left font-medium text-muted-foreground">
914
+ Responses
915
+ </th>
811
916
  )}
812
- </tr>
813
- ))}
814
- </tbody>
815
- </table>
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
+ )}
816
944
  </div>
817
945
  )}
818
946
  </div>
819
947
  )}
820
948
 
821
- <div className="flex flex-col gap-2 pt-1 border-t sm:flex-row sm:items-center sm:justify-between">
822
- <ProviderTestCoverage provider={provider} />
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>
823
959
  <div className="flex gap-2">
824
960
  {onTest !== undefined && (
825
961
  <Button
@@ -827,10 +963,10 @@ export function ProviderCard({
827
963
  size="sm"
828
964
  onClick={() => onTest(provider.id)}
829
965
  className="text-xs h-7 gap-1"
830
- disabled={isTesting ?? false}
966
+ disabled={isTestingValue}
831
967
  >
832
- <RotateCw className={`size-3 ${(isTesting ?? false) ? "animate-spin" : ""}`} />
833
- {(isTesting ?? false)
968
+ <RotateCw className={`size-3 ${isTestingValue ? "animate-spin" : ""}`} />
969
+ {isTestingValue
834
970
  ? testingTimeLeft !== undefined
835
971
  ? `Testing (${testingTimeLeft}s)`
836
972
  : "Testing..."
@@ -842,7 +978,7 @@ export function ProviderCard({
842
978
  size="sm"
843
979
  onClick={() => onEdit(provider)}
844
980
  className="text-xs h-7 gap-1"
845
- disabled={isTesting ?? false}
981
+ disabled={isTestingValue}
846
982
  >
847
983
  <Pencil className="size-3" />
848
984
  Edit
@@ -852,7 +988,7 @@ export function ProviderCard({
852
988
  size="sm"
853
989
  onClick={() => onDelete(provider.id)}
854
990
  className="text-xs h-7 gap-1 text-destructive hover:text-destructive"
855
- disabled={isTesting ?? false}
991
+ disabled={isTestingValue}
856
992
  >
857
993
  <Trash2 className="size-3" />
858
994
  Delete
@@ -11,6 +11,8 @@ function normalizeAnthropicStopReason(value: string): StopReason {
11
11
  case "end_turn":
12
12
  case "tool_use":
13
13
  return value;
14
+ case "max_tokens":
15
+ return "length";
14
16
  default:
15
17
  return null;
16
18
  }
@@ -21,6 +23,8 @@ function normalizeOpenAIFinishReason(value: string): StopReason {
21
23
  case "stop":
22
24
  case "length":
23
25
  return value;
26
+ case "max_tokens":
27
+ return "length";
24
28
  default:
25
29
  return null;
26
30
  }
@@ -1 +0,0 @@
1
- import{P as o}from"./ProxyViewerContainer-BUnlrfiS.js";import"./main-ChxZWG8K.js";const r=o;export{r as component};