@tonyclaw/agent-inspector 3.0.25 → 3.0.26

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-BnvOPwpe.js → CompareDrawer-SZRUDQc9.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-BJvwOrSi.js +4234 -0
  4. package/.output/public/assets/{ReplayDialog-BJgoQWdW.js → ReplayDialog-DgfmqiM3.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-CpBZEHnM.js → RequestAnatomy-CS6c7Qfn.js} +1 -1
  6. package/.output/public/assets/{ResponseView-CWmyRb2M.js → ResponseView-C0Ri_5gm.js} +2 -2
  7. package/.output/public/assets/{StreamingChunkSequence-BWhC8bwx.js → StreamingChunkSequence-7dYcHWyZ.js} +1 -1
  8. package/.output/public/assets/{_sessionId-Cq-2OqUU.js → _sessionId-D-V84cgw.js} +1 -1
  9. package/.output/public/assets/{index-BKi9IRtV.js → index-CSIL7-MK.js} +3 -3
  10. package/.output/public/assets/index-CmeF5XXS.css +1 -0
  11. package/.output/public/assets/{index-Cf8O0Oos.js → index-DgFWCxOs.js} +1 -1
  12. package/.output/public/assets/{index-DdgTJ4QJ.js → index-DrVJsks2.js} +1 -1
  13. package/.output/public/assets/{json-viewer-Bqu_4hmm.js → json-viewer-C9Ji3gYN.js} +1 -1
  14. package/.output/server/_libs/three.mjs +26016 -0
  15. package/.output/server/{_sessionId-Ba7H3Gjw.mjs → _sessionId-BQ6YQOO1.mjs} +7 -6
  16. package/.output/server/_ssr/{CompareDrawer-aH59Ab6x.mjs → CompareDrawer-CqNzIeMR.mjs} +7 -6
  17. package/.output/server/_ssr/{ProxyViewerContainer-By-I0tJv.mjs → ProxyViewerContainer-CMC7Vsbw.mjs} +785 -311
  18. package/.output/server/_ssr/{ReplayDialog-BO4Vy0et.mjs → ReplayDialog-D9p_LaTr.mjs} +8 -7
  19. package/.output/server/_ssr/{RequestAnatomy-FtOr7vSS.mjs → RequestAnatomy-B4ge3qNZ.mjs} +7 -6
  20. package/.output/server/_ssr/{ResponseView-cvw6ajue.mjs → ResponseView-BCqMKApI.mjs} +7 -6
  21. package/.output/server/_ssr/{StreamingChunkSequence-Oc8S2kD6.mjs → StreamingChunkSequence-BA4zTLE4.mjs} +7 -6
  22. package/.output/server/_ssr/{index-BDRlQheR.mjs → index-CzxhDjlq.mjs} +7 -6
  23. package/.output/server/_ssr/index.mjs +2 -2
  24. package/.output/server/_ssr/{json-viewer-Cd6trhQr.mjs → json-viewer-B3qV6iEJ.mjs} +7 -6
  25. package/.output/server/_ssr/{router-bofP-Ekl.mjs → router-CZe_R31M.mjs} +4 -4
  26. package/.output/server/{_tanstack-start-manifest_v-BPuf86qB.mjs → _tanstack-start-manifest_v-B50VTLrH.mjs} +1 -1
  27. package/.output/server/index.mjs +68 -68
  28. package/package.json +3 -1
  29. package/src/components/inspector-pet/InspectorPet.tsx +66 -51
  30. package/src/components/inspector-pet/InspectorPetCrab3D.tsx +455 -0
  31. package/src/components/inspector-pet/inspectorPetLogic.ts +28 -43
  32. package/src/components/providers/ProviderForm.tsx +238 -163
  33. package/src/components/providers/ProvidersPanel.tsx +16 -6
  34. package/src/components/providers/providerFormCapabilities.ts +36 -0
  35. package/styles/globals.css +0 -61
  36. package/.output/public/assets/ProxyViewerContainer-DcqFqo1m.js +0 -127
  37. package/.output/public/assets/index-BMYuDL5X.css +0 -1
@@ -1,7 +1,6 @@
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";
@@ -14,6 +13,7 @@ import {
14
13
  type UpstreamUrlPreview,
15
14
  } from "../../lib/upstreamUrl";
16
15
  import { formatContextWindowInput, parseContextWindowTokensInput } from "../../lib/utils";
16
+ import { deriveProviderCapabilities, endpointForCapability } from "./providerFormCapabilities";
17
17
 
18
18
  const ZHIPU_PROVIDER_KEYWORDS = ["zhipu", "zhipuai", "bigmodel", "z.ai", "zai", "glm"];
19
19
  const ZHIPU_CODING_PROVIDER_KEYWORDS = [
@@ -325,6 +325,7 @@ function EndpointUrlPreview({
325
325
  }
326
326
 
327
327
  export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps): JSX.Element {
328
+ const initialCapabilities = deriveProviderCapabilities(provider);
328
329
  const [name, setName] = useState(provider?.name ?? "");
329
330
  const [apiKey, setApiKey] = useState(provider?.apiKey ?? "");
330
331
  const [showApiKey, setShowApiKey] = useState(false);
@@ -339,7 +340,12 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
339
340
  initialModels !== undefined && initialModels.length > 0 ? initialModels : [""],
340
341
  ),
341
342
  );
342
- const [activeTab, setActiveTab] = useState<"anthropic" | "openai">("anthropic");
343
+ const [supportsAnthropic, setSupportsAnthropic] = useState(initialCapabilities.anthropic);
344
+ const [supportsOpenai, setSupportsOpenai] = useState(initialCapabilities.openai);
345
+ const [supportsOpenaiChat, setSupportsOpenaiChat] = useState(initialCapabilities.openaiChat);
346
+ const [supportsOpenaiResponses, setSupportsOpenaiResponses] = useState(
347
+ initialCapabilities.openaiResponses,
348
+ );
343
349
  const [anthropicBaseUrl, setAnthropicBaseUrl] = useState(
344
350
  stripEndpointSuffix(provider?.anthropicBaseUrl, ANTHROPIC_MESSAGES_ENDPOINT),
345
351
  );
@@ -409,6 +415,11 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
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
  );
@@ -477,14 +488,32 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
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
@@ -924,155 +1148,6 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
924
1148
  {errors.models !== undefined && <p className="text-xs text-destructive">{errors.models}</p>}
925
1149
  </div>
926
1150
 
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
1151
  <div className="space-y-2">
1077
1152
  <label htmlFor="provider-api-docs-url" className="text-sm font-medium">
1078
1153
  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
84
  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
  );
@@ -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
+ }
@@ -45,8 +45,6 @@
45
45
  --animate-crab-appear: crab-appear 300ms ease-out;
46
46
  --animate-crab-settle: crab-settle 400ms ease-in-out;
47
47
  --animate-crab-piano-pop: crab-piano-pop 400ms cubic-bezier(0.34, 1.56, 0.64, 1) backwards;
48
- --animate-crab-short-leg-body: crab-short-leg-body 720ms ease-in-out infinite;
49
- --animate-crab-short-leg-step: crab-short-leg-step 360ms ease-in-out infinite;
50
48
  }
51
49
 
52
50
  :root {
@@ -222,34 +220,6 @@
222
220
  }
223
221
  }
224
222
 
225
- @keyframes crab-short-leg-body {
226
- 0%,
227
- 100% {
228
- transform: translateY(0) rotate(-1deg);
229
- }
230
- 25% {
231
- transform: translateY(1px) rotate(1.5deg);
232
- }
233
- 50% {
234
- transform: translateY(-1px) rotate(-1.5deg);
235
- }
236
- 75% {
237
- transform: translateY(1px) rotate(1deg);
238
- }
239
- }
240
-
241
- @keyframes crab-short-leg-step {
242
- 0%,
243
- 100% {
244
- transform: translateY(0) scaleY(0.72) rotate(var(--crab-leg-rest, 0deg));
245
- opacity: 0.42;
246
- }
247
- 50% {
248
- transform: translateY(3px) scaleY(1.15) rotate(var(--crab-leg-step, 0deg));
249
- opacity: 0.9;
250
- }
251
- }
252
-
253
223
  @keyframes inspector-pet-watch {
254
224
  0%,
255
225
  100% {
@@ -317,8 +287,6 @@
317
287
  .animate-crab-piano-pop {
318
288
  animation: none !important;
319
289
  }
320
- .floating-crab-body,
321
- .floating-crab-leg,
322
290
  .inspector-pet-active,
323
291
  .inspector-pet-attention,
324
292
  .inspector-pet-sleeping,
@@ -379,26 +347,6 @@
379
347
  font-size: var(--inspector-font-13);
380
348
  }
381
349
 
382
- .floating-crab-body {
383
- animation: var(--animate-crab-short-leg-body);
384
- transform-origin: 50% 74%;
385
- }
386
-
387
- .floating-crab-leg {
388
- animation: var(--animate-crab-short-leg-step);
389
- transform-origin: 50% 0%;
390
- }
391
-
392
- .floating-crab-leg-left {
393
- --crab-leg-rest: -10deg;
394
- --crab-leg-step: 14deg;
395
- }
396
-
397
- .floating-crab-leg-right {
398
- --crab-leg-rest: 10deg;
399
- --crab-leg-step: -14deg;
400
- }
401
-
402
350
  .inspector-pet-active {
403
351
  animation: inspector-pet-watch 1.25s ease-in-out infinite;
404
352
  }
@@ -419,15 +367,6 @@
419
367
  animation: inspector-pet-heart 720ms ease-out forwards;
420
368
  }
421
369
 
422
- [data-pet-mood="active"] .floating-crab-leg {
423
- animation-duration: 260ms;
424
- }
425
-
426
- [data-pet-mood="sleeping"] .floating-crab-leg {
427
- animation-duration: 1.8s;
428
- opacity: 0.3;
429
- }
430
-
431
370
  .inspector-scrollbar {
432
371
  scrollbar-color: color-mix(in srgb, var(--muted-foreground) 32%, transparent) transparent;
433
372
  scrollbar-width: thin;