@marimo-team/frontend 0.19.3-dev47 → 0.19.3-dev48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{CellStatus-b7Yo2X9j.js → CellStatus--kUu6N2K.js} +1 -1
- package/dist/assets/{ConnectedDataExplorerComponent-Cr6-n9Em.js → ConnectedDataExplorerComponent-BKJwCHu7.js} +1 -1
- package/dist/assets/{JsonOutput-C8Eo1zBR.js → JsonOutput-BSGE-MRo.js} +1 -1
- package/dist/assets/{MarimoErrorOutput-CXBGzjO2.js → MarimoErrorOutput-CX0SCJOZ.js} +1 -1
- package/dist/assets/{RenderHTML-SoetmcW2.js → RenderHTML-Do_PVqRy.js} +1 -1
- package/dist/assets/{add-cell-with-ai-D2qS3Nos.js → add-cell-with-ai-manh7kBT.js} +1 -1
- package/dist/assets/{add-database-form-BBkiGMZ_.js → add-database-form-CgkV0MRs.js} +1 -1
- package/dist/assets/{agent-panel-BzV4XUTo.js → agent-panel-D-OmT-rw.js} +1 -1
- package/dist/assets/{ai-model-dropdown-CrMTCgo7.js → ai-model-dropdown-DzyBY5VA.js} +1 -1
- package/dist/assets/{app-config-button-9izWmQ0X.js → app-config-button-4H0CJmIo.js} +1 -1
- package/dist/assets/{cell-editor-Do6lWWk9.js → cell-editor-RHFZmO74.js} +1 -1
- package/dist/assets/{cell-link-BP7_Ns0N.js → cell-link-Dqj_nfXA.js} +1 -1
- package/dist/assets/{cells-Cv9PtwL9.js → cells-BNQUQiDS.js} +1 -1
- package/dist/assets/{chat-components-Be6BPrbT.js → chat-components-CWiXtKu6.js} +1 -1
- package/dist/assets/{chat-display-BRKfnhbm.js → chat-display-CGnOamQG.js} +1 -1
- package/dist/assets/{chat-panel-71zcilvi.js → chat-panel-CVFlznA-.js} +1 -1
- package/dist/assets/{client-CGOlSEYr.js → client-CDjmJmVw.js} +1 -1
- package/dist/assets/{column-preview-MC6VOHbd.js → column-preview-CKxT2s-S.js} +1 -1
- package/dist/assets/{command-n_oMaKjl.js → command-YPFTinLj.js} +1 -1
- package/dist/assets/{command-palette-DfZNcw7W.js → command-palette-FssCj6Ds.js} +1 -1
- package/dist/assets/{common-MUZIZluQ.js → common-DJkPpBxC.js} +1 -1
- package/dist/assets/{config-DFDEcYvy.js → config-D6nhy4FA.js} +1 -1
- package/dist/assets/{datasource-CEsMStKs.js → datasource-DerBLc6V.js} +1 -1
- package/dist/assets/{dependency-graph-panel-CNTGbfLZ.js → dependency-graph-panel-Vd-OsVLa.js} +1 -1
- package/dist/assets/{documentation-panel-Cb9AHO2C.js → documentation-panel-xG2-zpwg.js} +1 -1
- package/dist/assets/{download-24bI2vH0.js → download-B6EJS7Ar.js} +1 -1
- package/dist/assets/{edit-page-DSuXLdcn.js → edit-page-DoH9XFiW.js} +3 -3
- package/dist/assets/{error-panel-CpYH0GfR.js → error-panel-BxBpZYvt.js} +1 -1
- package/dist/assets/{es-BITbuY9w.js → es-BoHEdemq.js} +1 -1
- package/dist/assets/{file-explorer-panel-CdA81LHh.js → file-explorer-panel-C9K0vIPl.js} +1 -1
- package/dist/assets/{floating-outline-BbJ4ldyu.js → floating-outline-DCrTuu2G.js} +1 -1
- package/dist/assets/{focus-D1y1tXyC.js → focus-DM53w5BH.js} +1 -1
- package/dist/assets/{form-BAtvsPJL.js → form-BcKfhfZc.js} +1 -1
- package/dist/assets/{glide-data-editor-Dv8ZW9dk.js → glide-data-editor-CRb9AiCG.js} +1 -1
- package/dist/assets/{globals-C6OH39EA.js → globals-Bf30kOQF.js} +1 -1
- package/dist/assets/{home-page-B_YprqxM.js → home-page-CCegkRxN.js} +1 -1
- package/dist/assets/{index-VUoDw_Qb.js → index-BVybQnue.js} +6 -6
- package/dist/assets/index-DDc_1b-N.css +2 -0
- package/dist/assets/{kiosk-mode-DfyjlR7p.js → kiosk-mode-P-NYHJID.js} +1 -1
- package/dist/assets/{layout-9uQoV-6h.js → layout-DT91GUei.js} +1 -1
- package/dist/assets/links-D529u6GQ.js +1 -0
- package/dist/assets/{logs-panel-svcirwjp.js → logs-panel-C2dfrRig.js} +1 -1
- package/dist/assets/{markdown-renderer-DlVqlHOL.js → markdown-renderer-BPnVa0ym.js} +1 -1
- package/dist/assets/{mermaid-BPkO79lo.js → mermaid--ZwxKP7u.js} +1 -1
- package/dist/assets/{mode-PeuS_Lp-.js → mode-Dq8MKjNR.js} +1 -1
- package/dist/assets/{name-cell-input-YMoA0SQj.js → name-cell-input-BaEPC7ON.js} +1 -1
- package/dist/assets/{outline-panel-RKJ5Mqrt.js → outline-panel-Cca864H0.js} +1 -1
- package/dist/assets/{packages-panel-BuiAGEBw.js → packages-panel-BwDA3cjR.js} +1 -1
- package/dist/assets/{panels-BKsZUDjc.js → panels-BzlLZfye.js} +1 -1
- package/dist/assets/{process-output-KJWsSvCT.js → process-output-Dn1rOp26.js} +1 -1
- package/dist/assets/{readonly-python-code-HPlG_YPX.js → readonly-python-code-CXeF74Iq.js} +1 -1
- package/dist/assets/{renderShortcut-D0Pei-OA.js → renderShortcut-eU5Hsfml.js} +1 -1
- package/dist/assets/{run-page-CBDzVDX3.js → run-page-CM_n6pXD.js} +1 -1
- package/dist/assets/{scratchpad-panel-CarbQVYs.js → scratchpad-panel-XCkVY3Hp.js} +1 -1
- package/dist/assets/{session-panel-Cv14Ehfm.js → session-panel-BDt6Y_mU.js} +1 -1
- package/dist/assets/{snippets-panel-OAdQXQ93.js → snippets-panel-K-JKJQBf.js} +1 -1
- package/dist/assets/state-BTHUBVxX.js +1 -0
- package/dist/assets/{state-xh6GqNrp.js → state-DWRZTH2y.js} +1 -1
- package/dist/assets/{switch-DPeh0R76.js → switch-RowEjq0T.js} +1 -1
- package/dist/assets/{terminal-BbAhzgnR.js → terminal-BhbNfCNw.js} +1 -1
- package/dist/assets/{textarea-wbzgrXvB.js → textarea-Di1KKcL4.js} +1 -1
- package/dist/assets/{tracing-Bh3EJxAS.js → tracing-nvbrZdpf.js} +1 -1
- package/dist/assets/{tracing-panel-BzSQ7qvB.js → tracing-panel-CTXJaO-A.js} +2 -2
- package/dist/assets/{types-B8Qb1FfB.js → types-CT2U5Ljy.js} +1 -1
- package/dist/assets/{useAddCell-DBGvrN8K.js → useAddCell-COb93CUl.js} +1 -1
- package/dist/assets/{useBoolean-CyOFPk5r.js → useBoolean-B_S7yTZz.js} +1 -1
- package/dist/assets/{useCellActionButton-BlS_HKk-.js → useCellActionButton-D5Zt1dDz.js} +1 -1
- package/dist/assets/{useDeleteCell-BvQIJfpI.js → useDeleteCell-DHF_xvAh.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-BqEhbPr2.js → useDependencyPanelTab-D59iW_MD.js} +1 -1
- package/dist/assets/{useNotebookActions-D1Woz3AV.js → useNotebookActions-Ck-yX5ub.js} +1 -1
- package/dist/assets/{useRunCells-B9Xr4tcH.js → useRunCells-CKEmgeKM.js} +1 -1
- package/dist/assets/{useSplitCell-7xBW3b8-.js → useSplitCell-D9YiO-z5.js} +1 -1
- package/dist/assets/{useTheme-DfP1CWaW.js → useTheme-CNj0G_ol.js} +1 -1
- package/dist/assets/{utilities.esm-xahhGpny.js → utilities.esm-DG4qccZc.js} +1 -1
- package/dist/assets/utils-pfqq9IdB.js +1 -0
- package/dist/assets/{vega-component-dUiiVmIx.js → vega-component-C1voDf5W.js} +1 -1
- package/dist/index.html +37 -37
- package/package.json +1 -1
- package/src/components/app-config/ai-config.tsx +316 -24
- package/src/components/app-config/user-config-form.tsx +9 -2
- package/src/core/ai/ids/ids.ts +12 -4
- package/src/core/config/__tests__/config-schema.test.ts +36 -0
- package/src/core/config/config-schema.ts +1 -0
- package/dist/assets/index-C30GhE0W.css +0 -2
- package/dist/assets/links-DbDrjRnm.js +0 -1
- package/dist/assets/state-DYG6kYly.js +0 -1
- package/dist/assets/utils-CJJIceVn.js +0 -1
|
@@ -34,7 +34,8 @@ import { Textarea } from "@/components/ui/textarea";
|
|
|
34
34
|
import type { SupportedRole } from "@/core/ai/config";
|
|
35
35
|
import {
|
|
36
36
|
AiModelId,
|
|
37
|
-
|
|
37
|
+
KNOWN_PROVIDERS,
|
|
38
|
+
type KnownProviderId,
|
|
38
39
|
type ProviderId,
|
|
39
40
|
type QualifiedModelId,
|
|
40
41
|
type ShortModelId,
|
|
@@ -90,6 +91,11 @@ interface AiProviderTitleProps {
|
|
|
90
91
|
children: React.ReactNode;
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
interface CustomProviderConfig {
|
|
95
|
+
api_key?: string;
|
|
96
|
+
base_url?: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
93
99
|
export const AiProviderTitle: React.FC<AiProviderTitleProps> = ({
|
|
94
100
|
provider,
|
|
95
101
|
children,
|
|
@@ -555,15 +561,18 @@ const AccordionFormItem = ({
|
|
|
555
561
|
provider,
|
|
556
562
|
children,
|
|
557
563
|
isConfigured,
|
|
564
|
+
value,
|
|
558
565
|
}: {
|
|
559
566
|
title: string;
|
|
560
567
|
triggerClassName?: string;
|
|
561
568
|
provider: AiProviderIconProps["provider"];
|
|
562
569
|
children: React.ReactNode;
|
|
563
570
|
isConfigured: boolean;
|
|
571
|
+
/** Custom value for the accordion item. Defaults to provider. */
|
|
572
|
+
value?: string;
|
|
564
573
|
}) => {
|
|
565
574
|
return (
|
|
566
|
-
<AccordionItem value={provider}>
|
|
575
|
+
<AccordionItem value={value ?? provider}>
|
|
567
576
|
<AccordionTrigger className={triggerClassName}>
|
|
568
577
|
<AiProviderTitle provider={provider}>
|
|
569
578
|
{title}
|
|
@@ -581,9 +590,228 @@ const AccordionFormItem = ({
|
|
|
581
590
|
);
|
|
582
591
|
};
|
|
583
592
|
|
|
593
|
+
export const CustomProvidersConfig: React.FC<AiConfigProps> = ({
|
|
594
|
+
form,
|
|
595
|
+
config,
|
|
596
|
+
onSubmit,
|
|
597
|
+
}) => {
|
|
598
|
+
const [isAddingProvider, setIsAddingProvider] = useState(false);
|
|
599
|
+
const [newProviderName, setNewProviderName] = useState("");
|
|
600
|
+
const [newProviderApiKey, setNewProviderApiKey] = useState("");
|
|
601
|
+
const [newProviderBaseUrl, setNewProviderBaseUrl] = useState("");
|
|
602
|
+
|
|
603
|
+
const providerNameInputId = useId();
|
|
604
|
+
const apiKeyInputId = useId();
|
|
605
|
+
const baseUrlInputId = useId();
|
|
606
|
+
|
|
607
|
+
const normalizedName = newProviderName.toLowerCase().replaceAll(/\s+/g, "_");
|
|
608
|
+
const customProviders = form.watch("ai.custom_providers");
|
|
609
|
+
const isDuplicate =
|
|
610
|
+
KNOWN_PROVIDERS.includes(normalizedName as KnownProviderId) ||
|
|
611
|
+
(customProviders && Object.keys(customProviders).includes(normalizedName));
|
|
612
|
+
|
|
613
|
+
const hasValidValues =
|
|
614
|
+
normalizedName.trim() && newProviderBaseUrl.trim() && !isDuplicate;
|
|
615
|
+
|
|
616
|
+
const resetForm = () => {
|
|
617
|
+
setNewProviderName("");
|
|
618
|
+
setNewProviderApiKey("");
|
|
619
|
+
setNewProviderBaseUrl("");
|
|
620
|
+
setIsAddingProvider(false);
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
return (
|
|
624
|
+
<FormField
|
|
625
|
+
control={form.control}
|
|
626
|
+
name="ai.custom_providers"
|
|
627
|
+
render={({ field }) => {
|
|
628
|
+
const customProviders = (field.value || {}) as Record<
|
|
629
|
+
string,
|
|
630
|
+
CustomProviderConfig
|
|
631
|
+
>;
|
|
632
|
+
const customProviderEntries = Object.entries(customProviders);
|
|
633
|
+
|
|
634
|
+
const addProvider = () => {
|
|
635
|
+
if (!hasValidValues) {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
field.onChange({
|
|
639
|
+
...customProviders,
|
|
640
|
+
[normalizedName]: {
|
|
641
|
+
api_key: newProviderApiKey || undefined,
|
|
642
|
+
base_url: newProviderBaseUrl,
|
|
643
|
+
},
|
|
644
|
+
});
|
|
645
|
+
onSubmit(form.getValues());
|
|
646
|
+
resetForm();
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
const removeProvider = (providerName: string) => {
|
|
650
|
+
const { [providerName]: _, ...rest } = customProviders;
|
|
651
|
+
// Reset to clear nested dirty state, then set new value
|
|
652
|
+
form.resetField("ai.custom_providers");
|
|
653
|
+
form.setValue("ai.custom_providers", rest, { shouldDirty: true });
|
|
654
|
+
onSubmit(form.getValues());
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
const providerForm = (
|
|
658
|
+
<div className="flex flex-col gap-3 p-4 border border-border rounded-md bg-muted/20">
|
|
659
|
+
<div className="flex flex-col gap-1.5">
|
|
660
|
+
<Label htmlFor={providerNameInputId}>Provider Name</Label>
|
|
661
|
+
<Input
|
|
662
|
+
id={providerNameInputId}
|
|
663
|
+
placeholder="e.g., together, groq, mistral"
|
|
664
|
+
value={newProviderName}
|
|
665
|
+
onChange={(e) => setNewProviderName(e.target.value)}
|
|
666
|
+
/>
|
|
667
|
+
{isDuplicate && (
|
|
668
|
+
<p className="text-xs text-destructive">
|
|
669
|
+
A provider with this name already exists.
|
|
670
|
+
</p>
|
|
671
|
+
)}
|
|
672
|
+
{newProviderName && (
|
|
673
|
+
<p className="text-xs text-muted-secondary">
|
|
674
|
+
Use models with prefix:{" "}
|
|
675
|
+
<Kbd className="inline text-xs">{normalizedName}/</Kbd>
|
|
676
|
+
</p>
|
|
677
|
+
)}
|
|
678
|
+
</div>
|
|
679
|
+
|
|
680
|
+
<div className="flex flex-col gap-1.5">
|
|
681
|
+
<Label htmlFor={baseUrlInputId}>
|
|
682
|
+
Base URL <span className="text-destructive">*</span>
|
|
683
|
+
</Label>
|
|
684
|
+
<Input
|
|
685
|
+
id={baseUrlInputId}
|
|
686
|
+
placeholder="e.g., https://api.together.xyz/v1"
|
|
687
|
+
value={newProviderBaseUrl}
|
|
688
|
+
onChange={(e) => setNewProviderBaseUrl(e.target.value)}
|
|
689
|
+
/>
|
|
690
|
+
</div>
|
|
691
|
+
|
|
692
|
+
<div className="flex flex-col gap-1.5">
|
|
693
|
+
<Label htmlFor={apiKeyInputId}>API Key (optional)</Label>
|
|
694
|
+
<Input
|
|
695
|
+
id={apiKeyInputId}
|
|
696
|
+
placeholder="sk-..."
|
|
697
|
+
type="password"
|
|
698
|
+
value={newProviderApiKey}
|
|
699
|
+
onChange={(e) => setNewProviderApiKey(e.target.value)}
|
|
700
|
+
/>
|
|
701
|
+
</div>
|
|
702
|
+
|
|
703
|
+
<div className="flex gap-2 mt-1">
|
|
704
|
+
<Button
|
|
705
|
+
onClick={addProvider}
|
|
706
|
+
disabled={!hasValidValues}
|
|
707
|
+
size="xs"
|
|
708
|
+
>
|
|
709
|
+
Add Provider
|
|
710
|
+
</Button>
|
|
711
|
+
<Button variant="outline" onClick={resetForm} size="xs">
|
|
712
|
+
Cancel
|
|
713
|
+
</Button>
|
|
714
|
+
</div>
|
|
715
|
+
</div>
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
const renderAccordionItem = ({
|
|
719
|
+
providerName,
|
|
720
|
+
providerConfig,
|
|
721
|
+
onRemove,
|
|
722
|
+
}: {
|
|
723
|
+
providerName: string;
|
|
724
|
+
providerConfig: CustomProviderConfig;
|
|
725
|
+
onRemove: (name: string) => void;
|
|
726
|
+
}) => {
|
|
727
|
+
const displayName = Strings.startCase(providerName);
|
|
728
|
+
const isConfigured =
|
|
729
|
+
!!providerConfig.api_key || !!providerConfig.base_url;
|
|
730
|
+
|
|
731
|
+
return (
|
|
732
|
+
<AccordionFormItem
|
|
733
|
+
key={`custom-${providerName}`}
|
|
734
|
+
title={displayName}
|
|
735
|
+
provider={providerName}
|
|
736
|
+
value={`custom-${providerName}`}
|
|
737
|
+
isConfigured={isConfigured}
|
|
738
|
+
>
|
|
739
|
+
<ApiKey
|
|
740
|
+
form={form}
|
|
741
|
+
config={config}
|
|
742
|
+
name={
|
|
743
|
+
`ai.custom_providers.${providerName}.api_key` as FieldPath<UserConfig>
|
|
744
|
+
}
|
|
745
|
+
placeholder="sk-..."
|
|
746
|
+
testId={`custom-provider-${providerName}-api-key`}
|
|
747
|
+
/>
|
|
748
|
+
<BaseUrl
|
|
749
|
+
form={form}
|
|
750
|
+
config={config}
|
|
751
|
+
name={
|
|
752
|
+
`ai.custom_providers.${providerName}.base_url` as FieldPath<UserConfig>
|
|
753
|
+
}
|
|
754
|
+
placeholder="https://api.example.com/v1"
|
|
755
|
+
testId={`custom-provider-${providerName}-base-url`}
|
|
756
|
+
/>
|
|
757
|
+
<Button
|
|
758
|
+
variant="destructive"
|
|
759
|
+
size="xs"
|
|
760
|
+
onClick={(e) => {
|
|
761
|
+
e.stopPropagation();
|
|
762
|
+
e.preventDefault();
|
|
763
|
+
onRemove(providerName);
|
|
764
|
+
}}
|
|
765
|
+
className="w-fit self-end"
|
|
766
|
+
>
|
|
767
|
+
<Trash2Icon className="h-4 w-4 mr-2" />
|
|
768
|
+
Remove Provider
|
|
769
|
+
</Button>
|
|
770
|
+
</AccordionFormItem>
|
|
771
|
+
);
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
return (
|
|
775
|
+
<SettingGroup>
|
|
776
|
+
<SettingSubtitle>Custom Providers</SettingSubtitle>
|
|
777
|
+
<p className="text-sm text-muted-secondary">
|
|
778
|
+
Add your own OpenAI-compatible provider. Once added, you can
|
|
779
|
+
configure models in the AI Models tab.
|
|
780
|
+
</p>
|
|
781
|
+
|
|
782
|
+
{customProviderEntries.length > 0 && (
|
|
783
|
+
<Accordion type="multiple" className="-mt-4">
|
|
784
|
+
{customProviderEntries.map(([name, providerConfig]) =>
|
|
785
|
+
renderAccordionItem({
|
|
786
|
+
providerName: name,
|
|
787
|
+
providerConfig,
|
|
788
|
+
onRemove: removeProvider,
|
|
789
|
+
}),
|
|
790
|
+
)}
|
|
791
|
+
</Accordion>
|
|
792
|
+
)}
|
|
793
|
+
|
|
794
|
+
{isAddingProvider ? (
|
|
795
|
+
providerForm
|
|
796
|
+
) : (
|
|
797
|
+
<AddButton
|
|
798
|
+
className="self-start"
|
|
799
|
+
isFormOpen={isAddingProvider}
|
|
800
|
+
setIsFormOpen={setIsAddingProvider}
|
|
801
|
+
label="Add Provider"
|
|
802
|
+
/>
|
|
803
|
+
)}
|
|
804
|
+
</SettingGroup>
|
|
805
|
+
);
|
|
806
|
+
}}
|
|
807
|
+
/>
|
|
808
|
+
);
|
|
809
|
+
};
|
|
810
|
+
|
|
584
811
|
export const AiProvidersConfig: React.FC<AiConfigProps> = ({
|
|
585
812
|
form,
|
|
586
813
|
config,
|
|
814
|
+
onSubmit,
|
|
587
815
|
}) => {
|
|
588
816
|
const isWasmRuntime = isWasm();
|
|
589
817
|
|
|
@@ -903,13 +1131,17 @@ export const AiProvidersConfig: React.FC<AiConfigProps> = ({
|
|
|
903
1131
|
</AccordionFormItem>
|
|
904
1132
|
|
|
905
1133
|
<AccordionFormItem
|
|
906
|
-
title="OpenAI-Compatible"
|
|
1134
|
+
title="OpenAI-Compatible (Legacy)"
|
|
907
1135
|
provider="openai-compatible"
|
|
908
1136
|
isConfigured={
|
|
909
1137
|
hasValue("ai.open_ai_compatible.api_key") &&
|
|
910
1138
|
hasValue("ai.open_ai_compatible.base_url")
|
|
911
1139
|
}
|
|
912
1140
|
>
|
|
1141
|
+
<p className="text-sm text-amber-600 dark:text-amber-400 mb-2">
|
|
1142
|
+
Consider using Custom Providers instead, which allows you to add
|
|
1143
|
+
multiple providers with distinct names.
|
|
1144
|
+
</p>
|
|
913
1145
|
<ApiKey
|
|
914
1146
|
form={form}
|
|
915
1147
|
config={config}
|
|
@@ -933,6 +1165,8 @@ export const AiProvidersConfig: React.FC<AiConfigProps> = ({
|
|
|
933
1165
|
/>
|
|
934
1166
|
</AccordionFormItem>
|
|
935
1167
|
</Accordion>
|
|
1168
|
+
|
|
1169
|
+
<CustomProvidersConfig form={form} config={config} onSubmit={onSubmit} />
|
|
936
1170
|
</SettingGroup>
|
|
937
1171
|
);
|
|
938
1172
|
};
|
|
@@ -1133,6 +1367,16 @@ export const AiModelDisplayConfig: React.FC<AiConfigProps> = ({
|
|
|
1133
1367
|
name: "ai.models.custom_models",
|
|
1134
1368
|
}) as QualifiedModelId[];
|
|
1135
1369
|
|
|
1370
|
+
const customProviders = useWatch({
|
|
1371
|
+
control: form.control,
|
|
1372
|
+
name: "ai.custom_providers",
|
|
1373
|
+
}) as Record<string, CustomProviderConfig> | undefined;
|
|
1374
|
+
|
|
1375
|
+
const customProviderNames = useMemo(
|
|
1376
|
+
() => Object.keys(customProviders || {}),
|
|
1377
|
+
[customProviders],
|
|
1378
|
+
);
|
|
1379
|
+
|
|
1136
1380
|
const aiModelRegistry = useMemo(
|
|
1137
1381
|
() =>
|
|
1138
1382
|
AiModelRegistry.create({
|
|
@@ -1212,6 +1456,7 @@ export const AiModelDisplayConfig: React.FC<AiConfigProps> = ({
|
|
|
1212
1456
|
<AddModelForm
|
|
1213
1457
|
form={form}
|
|
1214
1458
|
customModels={customModels}
|
|
1459
|
+
customProviderNames={customProviderNames}
|
|
1215
1460
|
onSubmit={onSubmit}
|
|
1216
1461
|
/>
|
|
1217
1462
|
</SettingGroup>
|
|
@@ -1221,8 +1466,9 @@ export const AiModelDisplayConfig: React.FC<AiConfigProps> = ({
|
|
|
1221
1466
|
export const AddModelForm: React.FC<{
|
|
1222
1467
|
form: UseFormReturn<UserConfig>;
|
|
1223
1468
|
customModels: QualifiedModelId[];
|
|
1469
|
+
customProviderNames: string[];
|
|
1224
1470
|
onSubmit: (values: UserConfig) => void;
|
|
1225
|
-
}> = ({ form, customModels, onSubmit }) => {
|
|
1471
|
+
}> = ({ form, customModels, customProviderNames, onSubmit }) => {
|
|
1226
1472
|
const [isFormOpen, setIsFormOpen] = useState(false);
|
|
1227
1473
|
const [modelAdded, setModelAdded] = useState(false);
|
|
1228
1474
|
const [provider, setProvider] = useState<ProviderId | "custom" | null>(null);
|
|
@@ -1293,23 +1539,46 @@ export const AddModelForm: React.FC<{
|
|
|
1293
1539
|
</SelectTrigger>
|
|
1294
1540
|
<SelectContent>
|
|
1295
1541
|
<SelectGroup>
|
|
1542
|
+
{customProviderNames.length > 0 && (
|
|
1543
|
+
<>
|
|
1544
|
+
<p className="px-2 py-1 text-xs text-muted-secondary font-medium">
|
|
1545
|
+
Custom Providers
|
|
1546
|
+
</p>
|
|
1547
|
+
{customProviderNames.map((p) => (
|
|
1548
|
+
<SelectItem key={p} value={p}>
|
|
1549
|
+
<div className="flex items-center gap-2">
|
|
1550
|
+
<AiProviderIcon provider={p} className="h-4 w-4" />
|
|
1551
|
+
<span>{Strings.startCase(p)}</span>
|
|
1552
|
+
</div>
|
|
1553
|
+
</SelectItem>
|
|
1554
|
+
))}
|
|
1555
|
+
<p className="px-2 py-1 text-xs text-muted-secondary font-medium mt-1">
|
|
1556
|
+
Built-in Providers
|
|
1557
|
+
</p>
|
|
1558
|
+
</>
|
|
1559
|
+
)}
|
|
1560
|
+
{KNOWN_PROVIDERS.filter(
|
|
1561
|
+
(p) => p !== "marimo" && !customProviderNames.includes(p),
|
|
1562
|
+
).map((p) => (
|
|
1563
|
+
<SelectItem key={p} value={p}>
|
|
1564
|
+
<div className="flex items-center gap-2">
|
|
1565
|
+
<AiProviderIcon provider={p} className="h-4 w-4" />
|
|
1566
|
+
<span>{getProviderLabel(p)}</span>
|
|
1567
|
+
</div>
|
|
1568
|
+
</SelectItem>
|
|
1569
|
+
))}
|
|
1570
|
+
<p className="px-2 py-1 text-xs text-muted-secondary font-medium mt-1">
|
|
1571
|
+
Other
|
|
1572
|
+
</p>
|
|
1296
1573
|
<SelectItem value="custom">
|
|
1297
1574
|
<div className="flex items-center gap-2">
|
|
1298
1575
|
<AiProviderIcon
|
|
1299
1576
|
provider="openai-compatible"
|
|
1300
1577
|
className="h-4 w-4"
|
|
1301
1578
|
/>
|
|
1302
|
-
<span>
|
|
1579
|
+
<span>Enter provider name</span>
|
|
1303
1580
|
</div>
|
|
1304
1581
|
</SelectItem>
|
|
1305
|
-
{PROVIDERS.filter((p) => p !== "marimo").map((p) => (
|
|
1306
|
-
<SelectItem key={p} value={p}>
|
|
1307
|
-
<div className="flex items-center gap-2">
|
|
1308
|
-
<AiProviderIcon provider={p} className="h-4 w-4" />
|
|
1309
|
-
<span>{getProviderLabel(p)}</span>
|
|
1310
|
-
</div>
|
|
1311
|
-
</SelectItem>
|
|
1312
|
-
))}
|
|
1313
1582
|
</SelectGroup>
|
|
1314
1583
|
</SelectContent>
|
|
1315
1584
|
</Select>
|
|
@@ -1379,17 +1648,12 @@ export const AddModelForm: React.FC<{
|
|
|
1379
1648
|
<div>
|
|
1380
1649
|
{isFormOpen && inputForm}
|
|
1381
1650
|
<div className="flex flex-row text-sm">
|
|
1382
|
-
<
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
disabled={isFormOpen}
|
|
1389
|
-
>
|
|
1390
|
-
<PlusIcon className="h-4 w-4 mr-2 mb-0.5" />
|
|
1391
|
-
Add Model
|
|
1392
|
-
</Button>
|
|
1651
|
+
<AddButton
|
|
1652
|
+
isFormOpen={isFormOpen}
|
|
1653
|
+
setIsFormOpen={setIsFormOpen}
|
|
1654
|
+
label="Add Model"
|
|
1655
|
+
className="pl-2"
|
|
1656
|
+
/>
|
|
1393
1657
|
{modelAdded && (
|
|
1394
1658
|
<div className="flex items-center gap-1 text-green-700 bg-green-500/10 px-2 py-1 rounded-md ml-auto">
|
|
1395
1659
|
✓ Model added
|
|
@@ -1400,6 +1664,34 @@ export const AddModelForm: React.FC<{
|
|
|
1400
1664
|
);
|
|
1401
1665
|
};
|
|
1402
1666
|
|
|
1667
|
+
const AddButton = ({
|
|
1668
|
+
isFormOpen,
|
|
1669
|
+
setIsFormOpen,
|
|
1670
|
+
label,
|
|
1671
|
+
className,
|
|
1672
|
+
}: {
|
|
1673
|
+
isFormOpen: boolean;
|
|
1674
|
+
setIsFormOpen: (isOpen: boolean) => void;
|
|
1675
|
+
label: string;
|
|
1676
|
+
className?: string;
|
|
1677
|
+
}) => {
|
|
1678
|
+
return (
|
|
1679
|
+
<Button
|
|
1680
|
+
onClick={(e) => {
|
|
1681
|
+
e.stopPropagation();
|
|
1682
|
+
e.preventDefault();
|
|
1683
|
+
setIsFormOpen(true);
|
|
1684
|
+
}}
|
|
1685
|
+
variant="link"
|
|
1686
|
+
disabled={isFormOpen}
|
|
1687
|
+
className={cn("px-0", className)}
|
|
1688
|
+
>
|
|
1689
|
+
<PlusIcon className="h-4 w-4 mr-2 mb-0.5" />
|
|
1690
|
+
{label}
|
|
1691
|
+
</Button>
|
|
1692
|
+
);
|
|
1693
|
+
};
|
|
1694
|
+
|
|
1403
1695
|
export const AiConfig: React.FC<AiConfigProps> = ({
|
|
1404
1696
|
form,
|
|
1405
1697
|
config,
|
|
@@ -71,12 +71,19 @@ export function getDirtyValues<T extends FieldValues>(
|
|
|
71
71
|
const result: Partial<T> = {};
|
|
72
72
|
for (const key of Object.keys(dirtyFields) as (keyof T)[]) {
|
|
73
73
|
const dirty = dirtyFields[key];
|
|
74
|
+
const value = values[key];
|
|
75
|
+
|
|
76
|
+
// Skip if the value no longer exists (e.g., deleted from a record)
|
|
77
|
+
if (value === undefined) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
74
81
|
if (dirty === true) {
|
|
75
|
-
result[key] =
|
|
82
|
+
result[key] = value;
|
|
76
83
|
} else if (typeof dirty === "object" && dirty !== null) {
|
|
77
84
|
// Nested object - recurse
|
|
78
85
|
const nested = getDirtyValues(
|
|
79
|
-
|
|
86
|
+
value as FieldValues,
|
|
80
87
|
dirty as Partial<Record<string, unknown>>,
|
|
81
88
|
);
|
|
82
89
|
if (Object.keys(nested).length > 0) {
|
package/src/core/ai/ids/ids.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import type { TypedString } from "@/utils/typed";
|
|
4
4
|
|
|
5
|
-
export const
|
|
5
|
+
export const KNOWN_PROVIDERS = [
|
|
6
6
|
"openai",
|
|
7
7
|
"anthropic",
|
|
8
8
|
"google",
|
|
@@ -15,7 +15,13 @@ export const PROVIDERS = [
|
|
|
15
15
|
"wandb",
|
|
16
16
|
"marimo",
|
|
17
17
|
] as const;
|
|
18
|
-
export type
|
|
18
|
+
export type KnownProviderId = (typeof KNOWN_PROVIDERS)[number];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Provider ID can be a known provider or a custom string
|
|
22
|
+
* The (string & {}) pattern allows any string while still providing autocomplete for known providers
|
|
23
|
+
*/
|
|
24
|
+
export type ProviderId = KnownProviderId | (string & {});
|
|
19
25
|
|
|
20
26
|
export type ShortModelId = TypedString<"ShortModelId">;
|
|
21
27
|
|
|
@@ -68,6 +74,8 @@ function guessProviderId(id: string): ProviderId {
|
|
|
68
74
|
return "ollama";
|
|
69
75
|
}
|
|
70
76
|
|
|
71
|
-
export function isKnownAIProvider(
|
|
72
|
-
|
|
77
|
+
export function isKnownAIProvider(
|
|
78
|
+
providerId: string,
|
|
79
|
+
): providerId is KnownProviderId {
|
|
80
|
+
return (KNOWN_PROVIDERS as readonly string[]).includes(providerId);
|
|
73
81
|
}
|
|
@@ -45,6 +45,7 @@ test("default UserConfig - empty", () => {
|
|
|
45
45
|
expect(defaultConfig).toMatchInlineSnapshot(`
|
|
46
46
|
{
|
|
47
47
|
"ai": {
|
|
48
|
+
"custom_providers": {},
|
|
48
49
|
"inline_tooltip": false,
|
|
49
50
|
"mode": "manual",
|
|
50
51
|
"models": {
|
|
@@ -114,6 +115,7 @@ test("default UserConfig - one level", () => {
|
|
|
114
115
|
expect(defaultConfig).toMatchInlineSnapshot(`
|
|
115
116
|
{
|
|
116
117
|
"ai": {
|
|
118
|
+
"custom_providers": {},
|
|
117
119
|
"inline_tooltip": false,
|
|
118
120
|
"mode": "manual",
|
|
119
121
|
"models": {
|
|
@@ -198,6 +200,40 @@ test("default UserConfig with additional information", () => {
|
|
|
198
200
|
);
|
|
199
201
|
});
|
|
200
202
|
|
|
203
|
+
test("UserConfig with custom_providers", () => {
|
|
204
|
+
const config = UserConfigSchema.parse({
|
|
205
|
+
ai: {
|
|
206
|
+
custom_providers: {
|
|
207
|
+
my_provider: {
|
|
208
|
+
api_key: "test-key",
|
|
209
|
+
base_url: "https://api.example.com/v1",
|
|
210
|
+
},
|
|
211
|
+
another_provider: {
|
|
212
|
+
base_url: "https://api.another.com/v1",
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(config.ai?.custom_providers).toEqual({
|
|
219
|
+
my_provider: {
|
|
220
|
+
api_key: "test-key",
|
|
221
|
+
base_url: "https://api.example.com/v1",
|
|
222
|
+
},
|
|
223
|
+
another_provider: {
|
|
224
|
+
base_url: "https://api.another.com/v1",
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("UserConfig custom_providers defaults to empty object", () => {
|
|
230
|
+
const config = UserConfigSchema.parse({
|
|
231
|
+
ai: {},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
expect(config.ai?.custom_providers).toEqual({});
|
|
235
|
+
});
|
|
236
|
+
|
|
201
237
|
test("resolvedMarimoConfigAtom overrides correctly and does not mutate the original array", () => {
|
|
202
238
|
const initialUserConfig = {
|
|
203
239
|
completion: {
|
|
@@ -175,6 +175,7 @@ export const UserConfigSchema = z
|
|
|
175
175
|
aws_secret_access_key: z.string().optional(),
|
|
176
176
|
})
|
|
177
177
|
.optional(),
|
|
178
|
+
custom_providers: z.record(z.string(), AiConfigSchema).prefault({}),
|
|
178
179
|
models: AiModelsSchema.prefault({
|
|
179
180
|
displayed_models: [],
|
|
180
181
|
custom_models: [],
|