@tonyclaw/agent-inspector 3.0.25 → 3.0.27

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 (81) hide show
  1. package/.output/cli.js +80 -11
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/{CompareDrawer-BnvOPwpe.js → CompareDrawer-BVCAAbbu.js} +1 -1
  4. package/.output/public/assets/ProxyViewerContainer-CYOXB81n.js +4234 -0
  5. package/.output/public/assets/{ReplayDialog-BJgoQWdW.js → ReplayDialog-DMX23O5G.js} +1 -1
  6. package/.output/public/assets/{RequestAnatomy-CpBZEHnM.js → RequestAnatomy-S17pYDRc.js} +1 -1
  7. package/.output/public/assets/{ResponseView-CWmyRb2M.js → ResponseView-Cf1XrawD.js} +2 -2
  8. package/.output/public/assets/{StreamingChunkSequence-BWhC8bwx.js → StreamingChunkSequence-BdOrVlCi.js} +1 -1
  9. package/.output/public/assets/{_sessionId-Cq-2OqUU.js → _sessionId-BnKGmJrR.js} +1 -1
  10. package/.output/public/assets/{index-Cf8O0Oos.js → index-9fMLCU4d.js} +1 -1
  11. package/.output/public/assets/{index-BKi9IRtV.js → index-B1atk19i.js} +3 -3
  12. package/.output/public/assets/index-CmeF5XXS.css +1 -0
  13. package/.output/public/assets/{index-DdgTJ4QJ.js → index-gY3I-7-D.js} +1 -1
  14. package/.output/public/assets/{json-viewer-Bqu_4hmm.js → json-viewer-CpyYpPjx.js} +1 -1
  15. package/.output/server/_libs/lucide-react.mjs +6 -6
  16. package/.output/server/_libs/three.mjs +26016 -0
  17. package/.output/server/{_sessionId-Ba7H3Gjw.mjs → _sessionId-eA_h2_s6.mjs} +9 -6
  18. package/.output/server/_ssr/{CompareDrawer-aH59Ab6x.mjs → CompareDrawer-K2k9G7r1.mjs} +10 -7
  19. package/.output/server/_ssr/{ProxyViewerContainer-By-I0tJv.mjs → ProxyViewerContainer-CbyBpCAj.mjs} +804 -363
  20. package/.output/server/_ssr/{ReplayDialog-BO4Vy0et.mjs → ReplayDialog-DacI2Iv0.mjs} +10 -7
  21. package/.output/server/_ssr/{RequestAnatomy-FtOr7vSS.mjs → RequestAnatomy-gh-pAdkO.mjs} +9 -6
  22. package/.output/server/_ssr/{ResponseView-cvw6ajue.mjs → ResponseView-BRJKN50-.mjs} +10 -7
  23. package/.output/server/_ssr/{StreamingChunkSequence-Oc8S2kD6.mjs → StreamingChunkSequence-BQLVEuWY.mjs} +9 -6
  24. package/.output/server/_ssr/{index-BDRlQheR.mjs → index-DuSWrjbH.mjs} +9 -6
  25. package/.output/server/_ssr/index.mjs +2 -2
  26. package/.output/server/_ssr/{json-viewer-Cd6trhQr.mjs → json-viewer-DNs1ZjKI.mjs} +9 -6
  27. package/.output/server/_ssr/{router-bofP-Ekl.mjs → router-Dgx6-ot3.mjs} +713 -218
  28. package/.output/server/{_tanstack-start-manifest_v-BPuf86qB.mjs → _tanstack-start-manifest_v-CoGjnkQD.mjs} +1 -1
  29. package/.output/server/index.mjs +65 -65
  30. package/README.md +8 -0
  31. package/package.json +6 -5
  32. package/src/cli.ts +43 -9
  33. package/src/components/inspector-pet/InspectorPet.tsx +66 -51
  34. package/src/components/inspector-pet/InspectorPetCrab3D.tsx +455 -0
  35. package/src/components/inspector-pet/inspectorPetLogic.ts +28 -43
  36. package/src/components/providers/ProviderCard.tsx +3 -36
  37. package/src/components/providers/ProviderForm.tsx +251 -175
  38. package/src/components/providers/ProvidersPanel.tsx +18 -8
  39. package/src/components/providers/SettingsDialog.tsx +3 -3
  40. package/src/components/providers/providerFormCapabilities.ts +36 -0
  41. package/src/components/proxy-viewer/ProviderLogoStack.tsx +0 -3
  42. package/src/contracts/anthropic.ts +0 -5
  43. package/src/contracts/index.ts +0 -4
  44. package/src/contracts/log.ts +0 -1
  45. package/src/contracts/openai.ts +0 -3
  46. package/src/knowledge/openclawClient.ts +22 -9
  47. package/src/knowledge/openclawGatewayClient.ts +20 -13
  48. package/src/lib/alertContract.ts +0 -3
  49. package/src/lib/ecosystemContract.ts +0 -3
  50. package/src/lib/export-logs.ts +1 -4
  51. package/src/lib/safeFetch.ts +100 -0
  52. package/src/lib/ssrfGuard.ts +133 -0
  53. package/src/lib/stopReason.ts +1 -4
  54. package/src/lib/unknown.ts +27 -0
  55. package/src/lib/useProviders.ts +1 -1
  56. package/src/mcp/mode.ts +3 -9
  57. package/src/mcp/server.ts +5 -14
  58. package/src/mcp/toolHandlers.ts +45 -54
  59. package/src/proxy/constants.ts +17 -0
  60. package/src/proxy/formats/anthropic/handler.ts +5 -1
  61. package/src/proxy/formats/anthropic/schemas.ts +1 -7
  62. package/src/proxy/formats/index.ts +1 -1
  63. package/src/proxy/formats/openai/schemas.ts +0 -3
  64. package/src/proxy/formats/registry.ts +0 -3
  65. package/src/proxy/handler.ts +91 -12
  66. package/src/proxy/identityProxy.ts +74 -1
  67. package/src/proxy/logImporter.ts +82 -12
  68. package/src/proxy/logIndex.ts +1 -4
  69. package/src/proxy/providerSecretStore.ts +181 -0
  70. package/src/proxy/providers.ts +84 -11
  71. package/src/proxy/schemas.ts +1 -8
  72. package/src/proxy/sqliteLogIndex.ts +0 -16
  73. package/src/proxy/store.ts +1 -5
  74. package/src/proxy/toolSchemaWarnings.ts +1 -9
  75. package/src/routes/api/logs.$id.replay.ts +17 -5
  76. package/src/routes/api/providers.$providerId.model-metadata.ts +11 -1
  77. package/src/routes/api/providers.$providerId.ts +18 -5
  78. package/src/routes/api/providers.ts +18 -7
  79. package/styles/globals.css +0 -61
  80. package/.output/public/assets/ProxyViewerContainer-DcqFqo1m.js +0 -127
  81. package/.output/public/assets/index-BMYuDL5X.css +0 -1
@@ -1,11 +1,9 @@
1
1
  import { type JSX, useState, useEffect, useRef } from "react";
2
2
  import { Button } from "../ui/button";
3
3
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select";
4
- import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
5
4
  import { Eye, EyeOff, Copy, Check, ChevronDown } from "lucide-react";
6
5
  import type { ProviderConfig, ProviderModelMetadata } from "../../lib/providerContract";
7
6
  import { copyTextToClipboard } from "../../lib/clipboard";
8
- import { maskApiKey } from "../../lib/mask";
9
7
  import {
10
8
  PATH_V1_CHAT_COMPLETIONS,
11
9
  PATH_V1_MESSAGES,
@@ -14,6 +12,7 @@ import {
14
12
  type UpstreamUrlPreview,
15
13
  } from "../../lib/upstreamUrl";
16
14
  import { formatContextWindowInput, parseContextWindowTokensInput } from "../../lib/utils";
15
+ import { deriveProviderCapabilities, endpointForCapability } from "./providerFormCapabilities";
17
16
 
18
17
  const ZHIPU_PROVIDER_KEYWORDS = ["zhipu", "zhipuai", "bigmodel", "z.ai", "zai", "glm"];
19
18
  const ZHIPU_CODING_PROVIDER_KEYWORDS = [
@@ -325,8 +324,9 @@ function EndpointUrlPreview({
325
324
  }
326
325
 
327
326
  export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps): JSX.Element {
327
+ const initialCapabilities = deriveProviderCapabilities(provider);
328
328
  const [name, setName] = useState(provider?.name ?? "");
329
- const [apiKey, setApiKey] = useState(provider?.apiKey ?? "");
329
+ const [apiKey, setApiKey] = useState("");
330
330
  const [showApiKey, setShowApiKey] = useState(false);
331
331
  const [copied, setCopied] = useState(false);
332
332
  const initialModels = provider?.models;
@@ -339,7 +339,12 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
339
339
  initialModels !== undefined && initialModels.length > 0 ? initialModels : [""],
340
340
  ),
341
341
  );
342
- const [activeTab, setActiveTab] = useState<"anthropic" | "openai">("anthropic");
342
+ const [supportsAnthropic, setSupportsAnthropic] = useState(initialCapabilities.anthropic);
343
+ const [supportsOpenai, setSupportsOpenai] = useState(initialCapabilities.openai);
344
+ const [supportsOpenaiChat, setSupportsOpenaiChat] = useState(initialCapabilities.openaiChat);
345
+ const [supportsOpenaiResponses, setSupportsOpenaiResponses] = useState(
346
+ initialCapabilities.openaiResponses,
347
+ );
343
348
  const [anthropicBaseUrl, setAnthropicBaseUrl] = useState(
344
349
  stripEndpointSuffix(provider?.anthropicBaseUrl, ANTHROPIC_MESSAGES_ENDPOINT),
345
350
  );
@@ -405,10 +410,16 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
405
410
  useEffect(() => {
406
411
  if (provider) {
407
412
  setName(provider.name);
408
- setApiKey(provider.apiKey);
413
+ setApiKey("");
414
+ setShowApiKey(false);
409
415
  const nextModels = (provider.models?.length ?? 0) > 0 ? provider.models : [""];
410
416
  setModels(nextModels);
411
417
  setModelMetadataDrafts(createMetadataDrafts(provider, nextModels));
418
+ const nextCapabilities = deriveProviderCapabilities(provider);
419
+ setSupportsAnthropic(nextCapabilities.anthropic);
420
+ setSupportsOpenai(nextCapabilities.openai);
421
+ setSupportsOpenaiChat(nextCapabilities.openaiChat);
422
+ setSupportsOpenaiResponses(nextCapabilities.openaiResponses);
412
423
  setAnthropicBaseUrl(
413
424
  stripEndpointSuffix(provider.anthropicBaseUrl, ANTHROPIC_MESSAGES_ENDPOINT),
414
425
  );
@@ -471,20 +482,38 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
471
482
  if (!name.trim()) {
472
483
  newErrors.name = "Name is required";
473
484
  }
474
- if (!apiKey.trim()) {
485
+ if (provider === undefined && !apiKey.trim()) {
475
486
  newErrors.apiKey = "API key is required";
476
487
  }
477
488
  if (models.length === 0 || models.every((m) => !m.trim())) {
478
489
  newErrors.models = "At least one model is required";
479
490
  }
480
- if (anthropicBaseUrl.trim() && !isValidUrl(anthropicBaseUrl.trim())) {
481
- newErrors.anthropicBaseUrl = "Invalid URL format";
491
+ if (!supportsAnthropic && !supportsOpenai) {
492
+ newErrors.capabilities = "Select at least one API capability";
493
+ }
494
+ if (supportsOpenai && !supportsOpenaiChat && !supportsOpenaiResponses) {
495
+ newErrors.openaiCapabilities = "Select Chat Completions, Responses, or both";
496
+ }
497
+ if (supportsAnthropic) {
498
+ if (anthropicBaseUrl.trim() === "") {
499
+ newErrors.anthropicBaseUrl = "Anthropic base URL is required";
500
+ } else if (!isValidUrl(anthropicBaseUrl.trim())) {
501
+ newErrors.anthropicBaseUrl = "Invalid URL format";
502
+ }
482
503
  }
483
- if (openaiBaseUrl.trim() && !isValidUrl(openaiBaseUrl.trim())) {
484
- newErrors.openaiBaseUrl = "Invalid URL format";
504
+ if (supportsOpenai && supportsOpenaiChat) {
505
+ if (openaiBaseUrl.trim() === "") {
506
+ newErrors.openaiBaseUrl = "OpenAI Chat base URL is required";
507
+ } else if (!isValidUrl(openaiBaseUrl.trim())) {
508
+ newErrors.openaiBaseUrl = "Invalid URL format";
509
+ }
485
510
  }
486
- if (openaiResponsesBaseUrl.trim() && !isValidUrl(openaiResponsesBaseUrl.trim())) {
487
- newErrors.openaiResponsesBaseUrl = "Invalid URL format";
511
+ if (supportsOpenai && supportsOpenaiResponses) {
512
+ if (openaiResponsesBaseUrl.trim() === "") {
513
+ newErrors.openaiResponsesBaseUrl = "OpenAI Responses base URL is required";
514
+ } else if (!isValidUrl(openaiResponsesBaseUrl.trim())) {
515
+ newErrors.openaiResponsesBaseUrl = "Invalid URL format";
516
+ }
488
517
  }
489
518
  if (modelMetadataUrl.trim() && !isValidUrl(modelMetadataUrl.trim())) {
490
519
  newErrors.modelMetadataUrl = "Invalid URL format";
@@ -504,9 +533,6 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
504
533
  "Output limit should not exceed context window";
505
534
  }
506
535
  });
507
- if (!anthropicBaseUrl.trim() && !openaiBaseUrl.trim() && !openaiResponsesBaseUrl.trim()) {
508
- newErrors.format = "At least one format URL (Anthropic or OpenAI) is required";
509
- }
510
536
  setErrors(newErrors);
511
537
  return Object.keys(newErrors).length === 0;
512
538
  }
@@ -631,14 +657,27 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
631
657
  e.preventDefault();
632
658
  if (!validate()) return;
633
659
  setIsSubmitting(true);
660
+ const clearDisabledEndpoints = provider !== undefined;
634
661
  try {
635
662
  onSubmit({
636
663
  name: name.trim(),
637
664
  apiKey: apiKey.trim(),
638
665
  models: models.map((m) => m.trim()).filter((m) => m !== ""),
639
- anthropicBaseUrl: anthropicBaseUrl.trim() || undefined,
640
- openaiBaseUrl: openaiBaseUrl.trim() || undefined,
641
- openaiResponsesBaseUrl: openaiResponsesBaseUrl.trim() || undefined,
666
+ anthropicBaseUrl: endpointForCapability(
667
+ supportsAnthropic,
668
+ anthropicBaseUrl,
669
+ clearDisabledEndpoints,
670
+ ),
671
+ openaiBaseUrl: endpointForCapability(
672
+ supportsOpenai && supportsOpenaiChat,
673
+ openaiBaseUrl,
674
+ clearDisabledEndpoints,
675
+ ),
676
+ openaiResponsesBaseUrl: endpointForCapability(
677
+ supportsOpenai && supportsOpenaiResponses,
678
+ openaiResponsesBaseUrl,
679
+ clearDisabledEndpoints,
680
+ ),
642
681
  apiDocsUrl: apiDocsUrl.trim() || undefined,
643
682
  modelMetadataUrl: modelMetadataUrl.trim() || undefined,
644
683
  modelMetadata: buildModelMetadata(),
@@ -666,6 +705,191 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
666
705
  {errors.name !== undefined && <p className="text-xs text-destructive">{errors.name}</p>}
667
706
  </div>
668
707
 
708
+ <fieldset className="space-y-3">
709
+ <legend className="text-sm font-medium">
710
+ Capabilities <span className="text-destructive">*</span>
711
+ </legend>
712
+ <div className="divide-y divide-border overflow-hidden rounded-md border border-border">
713
+ <div className={supportsOpenai ? "bg-primary/[0.04] px-3 py-3" : "px-3 py-3"}>
714
+ <label
715
+ htmlFor="provider-capability-openai"
716
+ className="flex cursor-pointer items-start gap-3"
717
+ >
718
+ <input
719
+ id="provider-capability-openai"
720
+ type="checkbox"
721
+ checked={supportsOpenai}
722
+ onChange={(event) => setSupportsOpenai(event.target.checked)}
723
+ className="mt-0.5 size-4 shrink-0 accent-primary"
724
+ />
725
+ <span className="min-w-0">
726
+ <span className="block text-sm font-medium">OpenAI</span>
727
+ <span className="block text-xs text-muted-foreground">
728
+ Chat Completions and Responses
729
+ </span>
730
+ </span>
731
+ </label>
732
+ {supportsOpenai && (
733
+ <div className="ml-7 mt-3 flex flex-wrap gap-x-5 gap-y-2 border-t border-border/70 pt-3">
734
+ <label
735
+ htmlFor="provider-capability-openai-chat"
736
+ className="flex cursor-pointer items-center gap-2 text-sm"
737
+ >
738
+ <input
739
+ id="provider-capability-openai-chat"
740
+ type="checkbox"
741
+ checked={supportsOpenaiChat}
742
+ onChange={(event) => setSupportsOpenaiChat(event.target.checked)}
743
+ className="size-4 shrink-0 accent-primary"
744
+ />
745
+ Chat Completions
746
+ </label>
747
+ <label
748
+ htmlFor="provider-capability-openai-responses"
749
+ className="flex cursor-pointer items-center gap-2 text-sm"
750
+ >
751
+ <input
752
+ id="provider-capability-openai-responses"
753
+ type="checkbox"
754
+ checked={supportsOpenaiResponses}
755
+ onChange={(event) => setSupportsOpenaiResponses(event.target.checked)}
756
+ className="size-4 shrink-0 accent-primary"
757
+ />
758
+ Responses
759
+ </label>
760
+ </div>
761
+ )}
762
+ {errors.openaiCapabilities !== undefined && supportsOpenai && (
763
+ <p className="ml-7 mt-2 text-xs text-destructive">{errors.openaiCapabilities}</p>
764
+ )}
765
+ </div>
766
+ <label
767
+ htmlFor="provider-capability-anthropic"
768
+ className={`flex cursor-pointer items-start gap-3 px-3 py-3 ${
769
+ supportsAnthropic ? "bg-primary/[0.04]" : ""
770
+ }`}
771
+ >
772
+ <input
773
+ id="provider-capability-anthropic"
774
+ type="checkbox"
775
+ checked={supportsAnthropic}
776
+ onChange={(event) => setSupportsAnthropic(event.target.checked)}
777
+ className="mt-0.5 size-4 shrink-0 accent-primary"
778
+ />
779
+ <span className="min-w-0">
780
+ <span className="block text-sm font-medium">Anthropic</span>
781
+ <span className="block text-xs text-muted-foreground">Messages</span>
782
+ </span>
783
+ </label>
784
+ </div>
785
+ {errors.capabilities !== undefined && (
786
+ <p className="text-xs text-destructive">{errors.capabilities}</p>
787
+ )}
788
+ </fieldset>
789
+
790
+ {(supportsAnthropic ||
791
+ (supportsOpenai && (supportsOpenaiChat || supportsOpenaiResponses))) && (
792
+ <section className="space-y-5 border-l border-border pl-4" aria-label="API endpoints">
793
+ <h3 className="text-sm font-medium">Endpoint Configuration</h3>
794
+
795
+ {supportsAnthropic && (
796
+ <div className="space-y-2">
797
+ <label htmlFor="provider-anthropic-base-url" className="text-sm font-medium">
798
+ Anthropic Messages Base URL <span className="text-destructive">*</span>
799
+ </label>
800
+ <div className="border border-input bg-background hover:bg-accent hover:text-accent-foreground flex min-w-0 overflow-hidden rounded-md focus-within:border-ring focus-within:outline-ring focus-within:ring-[3px]">
801
+ <input
802
+ id="provider-anthropic-base-url"
803
+ type="text"
804
+ value={anthropicBaseUrl}
805
+ onChange={(event) => {
806
+ setManualAnthropicUrlOverride(true);
807
+ setAnthropicBaseUrl(event.target.value);
808
+ }}
809
+ placeholder="https://api.anthropic.com"
810
+ className="min-w-0 flex-1 bg-transparent px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
811
+ />
812
+ <span className="shrink-0 select-none border-l border-white/10 bg-white/[0.04] px-3 py-2 font-mono text-xs text-muted-foreground">
813
+ {ANTHROPIC_MESSAGES_ENDPOINT}
814
+ </span>
815
+ </div>
816
+ {errors.anthropicBaseUrl !== undefined && (
817
+ <p className="text-xs text-destructive">{errors.anthropicBaseUrl}</p>
818
+ )}
819
+ <EndpointUrlPreview
820
+ baseUrl={anthropicBaseUrl}
821
+ endpoint={ANTHROPIC_MESSAGES_ENDPOINT}
822
+ providerName={name}
823
+ />
824
+ </div>
825
+ )}
826
+
827
+ {supportsOpenai && supportsOpenaiChat && (
828
+ <div className="space-y-2">
829
+ <label htmlFor="provider-openai-base-url" className="text-sm font-medium">
830
+ OpenAI Chat Base URL <span className="text-destructive">*</span>
831
+ </label>
832
+ <div className="border border-input bg-background hover:bg-accent hover:text-accent-foreground flex min-w-0 overflow-hidden rounded-md focus-within:border-ring focus-within:outline-ring focus-within:ring-[3px]">
833
+ <input
834
+ id="provider-openai-base-url"
835
+ type="text"
836
+ value={openaiBaseUrl}
837
+ onChange={(event) => {
838
+ setManualOpenaiUrlOverride(true);
839
+ setOpenaiBaseUrl(event.target.value);
840
+ }}
841
+ placeholder="https://api.openai.com"
842
+ className="min-w-0 flex-1 bg-transparent px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
843
+ />
844
+ <span className="shrink-0 select-none border-l border-white/10 bg-white/[0.04] px-3 py-2 font-mono text-xs text-muted-foreground">
845
+ {OPENAI_CHAT_COMPLETIONS_ENDPOINT}
846
+ </span>
847
+ </div>
848
+ {errors.openaiBaseUrl !== undefined && (
849
+ <p className="text-xs text-destructive">{errors.openaiBaseUrl}</p>
850
+ )}
851
+ <EndpointUrlPreview
852
+ baseUrl={openaiBaseUrl}
853
+ endpoint={OPENAI_CHAT_COMPLETIONS_ENDPOINT}
854
+ providerName={name}
855
+ />
856
+ </div>
857
+ )}
858
+
859
+ {supportsOpenai && supportsOpenaiResponses && (
860
+ <div className="space-y-2">
861
+ <label htmlFor="provider-openai-responses-base-url" className="text-sm font-medium">
862
+ OpenAI Responses Base URL <span className="text-destructive">*</span>
863
+ </label>
864
+ <div className="border border-input bg-background hover:bg-accent hover:text-accent-foreground flex min-w-0 overflow-hidden rounded-md focus-within:border-ring focus-within:outline-ring focus-within:ring-[3px]">
865
+ <input
866
+ id="provider-openai-responses-base-url"
867
+ type="text"
868
+ value={openaiResponsesBaseUrl}
869
+ onChange={(event) => {
870
+ setManualOpenaiResponsesUrlOverride(true);
871
+ setOpenaiResponsesBaseUrl(event.target.value);
872
+ }}
873
+ placeholder="https://api.openai.com"
874
+ className="min-w-0 flex-1 bg-transparent px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
875
+ />
876
+ <span className="shrink-0 select-none border-l border-white/10 bg-white/[0.04] px-3 py-2 font-mono text-xs text-muted-foreground">
877
+ {OPENAI_RESPONSES_ENDPOINT}
878
+ </span>
879
+ </div>
880
+ {errors.openaiResponsesBaseUrl !== undefined && (
881
+ <p className="text-xs text-destructive">{errors.openaiResponsesBaseUrl}</p>
882
+ )}
883
+ <EndpointUrlPreview
884
+ baseUrl={openaiResponsesBaseUrl}
885
+ endpoint={OPENAI_RESPONSES_ENDPOINT}
886
+ providerName={name}
887
+ />
888
+ </div>
889
+ )}
890
+ </section>
891
+ )}
892
+
669
893
  <div className="space-y-2">
670
894
  <label htmlFor="provider-source" className="text-sm font-medium">
671
895
  Token Source
@@ -726,19 +950,15 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
726
950
 
727
951
  <div className="space-y-2">
728
952
  <label htmlFor="provider-apikey" className="text-sm font-medium">
729
- API Key <span className="text-destructive">*</span>
953
+ API Key {provider === undefined && <span className="text-destructive">*</span>}
730
954
  </label>
731
955
  <div className="flex items-center gap-2">
732
956
  <input
733
957
  id="provider-apikey"
734
- type="text"
735
- value={showApiKey || apiKey.length === 0 ? apiKey : maskApiKey(apiKey)}
958
+ type={showApiKey ? "text" : "password"}
959
+ value={apiKey}
736
960
  onChange={(e) => setApiKey(e.target.value)}
737
- onFocus={() => {
738
- if (!showApiKey && apiKey.length > 0) setShowApiKey(true);
739
- }}
740
- placeholder="sk-ant-..."
741
- readOnly={!showApiKey && apiKey.length > 0}
961
+ placeholder={provider === undefined ? "sk-ant-..." : "Leave blank to keep current key"}
742
962
  className="flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50"
743
963
  />
744
964
  <button
@@ -758,6 +978,11 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
758
978
  {copied ? <Check className="size-4 text-cyan-300" /> : <Copy className="size-4" />}
759
979
  </button>
760
980
  </div>
981
+ {provider !== undefined && apiKey.trim() === "" && (
982
+ <p className="text-xs text-muted-foreground">
983
+ A key is configured. Enter a new value only to rotate it.
984
+ </p>
985
+ )}
761
986
  {errors.apiKey !== undefined && <p className="text-xs text-destructive">{errors.apiKey}</p>}
762
987
  </div>
763
988
 
@@ -924,155 +1149,6 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
924
1149
  {errors.models !== undefined && <p className="text-xs text-destructive">{errors.models}</p>}
925
1150
  </div>
926
1151
 
927
- <div className="space-y-2">
928
- <div className="border border-border bg-muted/40 flex gap-1 rounded-[8px] p-1">
929
- <TooltipProvider>
930
- <Tooltip>
931
- <TooltipTrigger asChild>
932
- <button
933
- type="button"
934
- onClick={() => setActiveTab("anthropic")}
935
- className={`px-3 py-2 text-sm font-medium border-b-2 transition-colors ${
936
- activeTab === "anthropic"
937
- ? "border-primary text-primary"
938
- : "border-transparent text-muted-foreground hover:text-foreground"
939
- }`}
940
- >
941
- Anthropic Format
942
- </button>
943
- </TooltipTrigger>
944
- <TooltipContent>Anthropic Messages API format</TooltipContent>
945
- </Tooltip>
946
- </TooltipProvider>
947
- <TooltipProvider>
948
- <Tooltip>
949
- <TooltipTrigger asChild>
950
- <button
951
- type="button"
952
- onClick={() => setActiveTab("openai")}
953
- className={`px-3 py-2 text-sm font-medium border-b-2 transition-colors ${
954
- activeTab === "openai"
955
- ? "border-primary text-primary"
956
- : "border-transparent text-muted-foreground hover:text-foreground"
957
- }`}
958
- >
959
- OpenAI Format
960
- </button>
961
- </TooltipTrigger>
962
- <TooltipContent>OpenAI Chat Completions API format</TooltipContent>
963
- </Tooltip>
964
- </TooltipProvider>
965
- </div>
966
- {errors.format !== undefined && <p className="text-xs text-destructive">{errors.format}</p>}
967
- </div>
968
-
969
- {activeTab === "anthropic" && (
970
- <div className="space-y-2">
971
- <label htmlFor="provider-anthropic-base-url" className="text-sm font-medium">
972
- Anthropic Base URL
973
- </label>
974
- <div className="border border-input bg-background hover:bg-accent hover:text-accent-foreground flex min-w-0 overflow-hidden rounded-md focus-within:border-ring focus-within:outline-ring focus-within:ring-[3px]">
975
- <input
976
- id="provider-anthropic-base-url"
977
- type="text"
978
- value={anthropicBaseUrl}
979
- onChange={(e) => {
980
- setManualAnthropicUrlOverride(true);
981
- setAnthropicBaseUrl(e.target.value);
982
- }}
983
- placeholder="https://api.anthropic.com"
984
- className="min-w-0 flex-1 bg-transparent px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
985
- />
986
- <span className="shrink-0 select-none border-l border-white/10 bg-white/[0.04] px-3 py-2 font-mono text-xs text-muted-foreground">
987
- {ANTHROPIC_MESSAGES_ENDPOINT}
988
- </span>
989
- </div>
990
- {errors.anthropicBaseUrl !== undefined && (
991
- <p className="text-xs text-destructive">{errors.anthropicBaseUrl}</p>
992
- )}
993
- <EndpointUrlPreview
994
- baseUrl={anthropicBaseUrl}
995
- endpoint={ANTHROPIC_MESSAGES_ENDPOINT}
996
- providerName={name}
997
- />
998
- <p className="text-xs text-muted-foreground">
999
- Anthropic-compatible base URL. Leave empty if this provider does not support Anthropic
1000
- format.
1001
- </p>
1002
- </div>
1003
- )}
1004
-
1005
- {activeTab === "openai" && (
1006
- <div className="space-y-4">
1007
- <div className="space-y-2">
1008
- <label htmlFor="provider-openai-base-url" className="text-sm font-medium">
1009
- OpenAI Chat Base URL
1010
- </label>
1011
- <div className="border border-input bg-background hover:bg-accent hover:text-accent-foreground flex min-w-0 overflow-hidden rounded-md focus-within:border-ring focus-within:outline-ring focus-within:ring-[3px]">
1012
- <input
1013
- id="provider-openai-base-url"
1014
- type="text"
1015
- value={openaiBaseUrl}
1016
- onChange={(e) => {
1017
- setManualOpenaiUrlOverride(true);
1018
- setOpenaiBaseUrl(e.target.value);
1019
- }}
1020
- placeholder="https://api.openai.com"
1021
- className="min-w-0 flex-1 bg-transparent px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
1022
- />
1023
- <span className="shrink-0 select-none border-l border-white/10 bg-white/[0.04] px-3 py-2 font-mono text-xs text-muted-foreground">
1024
- {OPENAI_CHAT_COMPLETIONS_ENDPOINT}
1025
- </span>
1026
- </div>
1027
- {errors.openaiBaseUrl !== undefined && (
1028
- <p className="text-xs text-destructive">{errors.openaiBaseUrl}</p>
1029
- )}
1030
- <EndpointUrlPreview
1031
- baseUrl={openaiBaseUrl}
1032
- endpoint={OPENAI_CHAT_COMPLETIONS_ENDPOINT}
1033
- providerName={name}
1034
- />
1035
- <p className="text-xs text-muted-foreground">
1036
- OpenAI Chat Completions-compatible base URL. Leave empty if this provider only
1037
- supports the Responses API.
1038
- </p>
1039
- </div>
1040
- <div className="space-y-2">
1041
- <label htmlFor="provider-openai-responses-base-url" className="text-sm font-medium">
1042
- OpenAI Responses Base URL
1043
- </label>
1044
- <div className="border border-input bg-background hover:bg-accent hover:text-accent-foreground flex min-w-0 overflow-hidden rounded-md focus-within:border-ring focus-within:outline-ring focus-within:ring-[3px]">
1045
- <input
1046
- id="provider-openai-responses-base-url"
1047
- type="text"
1048
- value={openaiResponsesBaseUrl}
1049
- onChange={(e) => {
1050
- setManualOpenaiResponsesUrlOverride(true);
1051
- setOpenaiResponsesBaseUrl(e.target.value);
1052
- }}
1053
- placeholder="https://api.openai.com"
1054
- className="min-w-0 flex-1 bg-transparent px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
1055
- />
1056
- <span className="shrink-0 select-none border-l border-white/10 bg-white/[0.04] px-3 py-2 font-mono text-xs text-muted-foreground">
1057
- {OPENAI_RESPONSES_ENDPOINT}
1058
- </span>
1059
- </div>
1060
- {errors.openaiResponsesBaseUrl !== undefined && (
1061
- <p className="text-xs text-destructive">{errors.openaiResponsesBaseUrl}</p>
1062
- )}
1063
- <EndpointUrlPreview
1064
- baseUrl={openaiResponsesBaseUrl}
1065
- endpoint={OPENAI_RESPONSES_ENDPOINT}
1066
- providerName={name}
1067
- />
1068
- <p className="text-xs text-muted-foreground">
1069
- Optional Responses-compatible base URL. Set only when the provider supports the
1070
- Responses API.
1071
- </p>
1072
- </div>
1073
- </div>
1074
- )}
1075
-
1076
1152
  <div className="space-y-2">
1077
1153
  <label htmlFor="provider-api-docs-url" className="text-sm font-medium">
1078
1154
  API Docs URL
@@ -70,15 +70,25 @@ type ProviderFormData = {
70
70
  source?: "company" | "personal";
71
71
  };
72
72
 
73
- function createProviderPayload(data: ProviderFormData) {
73
+ function endpointPayloadValue(
74
+ value: string | undefined,
75
+ preserveEmptyEndpoints: boolean,
76
+ ): string | undefined {
77
+ if ((value?.length ?? 0) > 0) return value;
78
+ return preserveEmptyEndpoints && value !== undefined ? "" : undefined;
79
+ }
80
+
81
+ function createProviderPayload(data: ProviderFormData, preserveEmptyEndpoints = false) {
74
82
  return {
75
83
  name: data.name,
76
- apiKey: data.apiKey,
84
+ ...(data.apiKey.trim() === "" ? {} : { apiKey: data.apiKey }),
77
85
  models: data.models,
78
- anthropicBaseUrl: (data.anthropicBaseUrl?.length ?? 0) > 0 ? data.anthropicBaseUrl : undefined,
79
- openaiBaseUrl: (data.openaiBaseUrl?.length ?? 0) > 0 ? data.openaiBaseUrl : undefined,
80
- openaiResponsesBaseUrl:
81
- (data.openaiResponsesBaseUrl?.length ?? 0) > 0 ? data.openaiResponsesBaseUrl : undefined,
86
+ anthropicBaseUrl: endpointPayloadValue(data.anthropicBaseUrl, preserveEmptyEndpoints),
87
+ openaiBaseUrl: endpointPayloadValue(data.openaiBaseUrl, preserveEmptyEndpoints),
88
+ openaiResponsesBaseUrl: endpointPayloadValue(
89
+ data.openaiResponsesBaseUrl,
90
+ preserveEmptyEndpoints,
91
+ ),
82
92
  apiDocsUrl: (data.apiDocsUrl?.length ?? 0) > 0 ? data.apiDocsUrl : undefined,
83
93
  modelMetadataUrl: (data.modelMetadataUrl?.length ?? 0) > 0 ? data.modelMetadataUrl : undefined,
84
94
  modelMetadata: data.modelMetadata,
@@ -408,7 +418,7 @@ export function ProvidersPanel({
408
418
  {
409
419
  method: "PUT",
410
420
  headers: { "Content-Type": "application/json" },
411
- body: JSON.stringify(createProviderPayload(data)),
421
+ body: JSON.stringify(createProviderPayload(data, true)),
412
422
  },
413
423
  () => "Failed to update provider",
414
424
  );
@@ -420,7 +430,7 @@ export function ProvidersPanel({
420
430
  refreshProviders();
421
431
  // Only run connection test when critical fields (apiKey, model, base URLs) changed
422
432
  const criticalFieldsChanged =
423
- (data.apiKey ?? "") !== (editingProvider.apiKey ?? "") ||
433
+ data.apiKey.trim() !== "" ||
424
434
  JSON.stringify(data.models) !== JSON.stringify(editingProvider.models) ||
425
435
  (data.anthropicBaseUrl ?? "") !== (editingProvider.anthropicBaseUrl ?? "") ||
426
436
  (data.openaiBaseUrl ?? "") !== (editingProvider.openaiBaseUrl ?? "") ||
@@ -471,9 +471,9 @@ function McpSettingsTab(): JSX.Element {
471
471
  value: `agent-inspector-mcp doctor --url ${endpoint}`,
472
472
  },
473
473
  {
474
- id: "readonly",
475
- label: "Readonly mode",
476
- value: "AGENT_INSPECTOR_MCP_READONLY=1 agent-inspector",
474
+ id: "writes",
475
+ label: "Enable write tools",
476
+ value: "AGENT_INSPECTOR_MCP_WRITES=1 agent-inspector",
477
477
  },
478
478
  {
479
479
  id: "capabilities",
@@ -0,0 +1,36 @@
1
+ import type { ProviderConfig } from "../../lib/providerContract";
2
+
3
+ export type ProviderCapabilitySelection = {
4
+ anthropic: boolean;
5
+ openai: boolean;
6
+ openaiChat: boolean;
7
+ openaiResponses: boolean;
8
+ };
9
+
10
+ function hasEndpoint(value: string | undefined): boolean {
11
+ return value !== undefined && value.trim() !== "";
12
+ }
13
+
14
+ export function deriveProviderCapabilities(
15
+ provider: ProviderConfig | undefined,
16
+ ): ProviderCapabilitySelection {
17
+ const anthropic = hasEndpoint(provider?.anthropicBaseUrl);
18
+ const openaiChat = hasEndpoint(provider?.openaiBaseUrl);
19
+ const openaiResponses = hasEndpoint(provider?.openaiResponsesBaseUrl);
20
+ return {
21
+ anthropic,
22
+ openai: openaiChat || openaiResponses,
23
+ openaiChat,
24
+ openaiResponses,
25
+ };
26
+ }
27
+
28
+ export function endpointForCapability(
29
+ enabled: boolean,
30
+ value: string,
31
+ clearWhenDisabled: boolean,
32
+ ): string | undefined {
33
+ if (!enabled) return clearWhenDisabled ? "" : undefined;
34
+ const trimmed = value.trim();
35
+ return trimmed === "" ? undefined : trimmed;
36
+ }
@@ -5,9 +5,6 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/
5
5
 
6
6
  const MAX_VISIBLE_LOGOS = 3;
7
7
 
8
- export const ROW_CHEVRON_SLOT_CLASS =
9
- "flex h-8 w-5 shrink-0 items-center justify-center text-muted-foreground";
10
-
11
8
  export type ProviderLogoStackItem = {
12
9
  provider: Provider;
13
10
  tooltip: string;
@@ -228,8 +228,3 @@ export const SseEventSchema = z.discriminatedUnion("type", [
228
228
 
229
229
  export type AnthropicResponse = z.infer<typeof AnthropicResponseSchema>;
230
230
  export type ResponseContentBlockType = z.infer<typeof ResponseContentBlock>;
231
-
232
- // Backward-compatible aliases (originally named Inspector*)
233
- export const InspectorRequestSchema = AnthropicRequestSchema;
234
- export const InspectorResponseSchema = AnthropicResponseSchema;
235
- export type InspectorResponse = AnthropicResponse;
@@ -12,7 +12,6 @@ export type {
12
12
  CapturedLog,
13
13
  LogBodyChunk,
14
14
  LogBodyPart,
15
- RequestFormat,
16
15
  StreamingChunk,
17
16
  TokenUsage,
18
17
  } from "./log";
@@ -46,9 +45,6 @@ export {
46
45
  } from "./openai";
47
46
  export type {
48
47
  OpenAIResponse,
49
- OpenAIResponsesRequest,
50
48
  OpenAIResponsesResponse,
51
- OpenAIResponsesSSEvent,
52
- OpenAIResponsesOutputItem,
53
49
  OpenAIToolCall,
54
50
  } from "./openai";