@marimo-team/frontend 0.24.1-dev59 → 0.24.1-dev60
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-D6_pLs0B.js → CellStatus-DeTZJKkI.js} +1 -1
- package/dist/assets/{ConnectedDataExplorerComponent-DZnvpjue.js → ConnectedDataExplorerComponent-CsYDGDdB.js} +1 -1
- package/dist/assets/{JsonOutput-D0JlHn1Z.js → JsonOutput-CucHb-Xd.js} +11 -11
- package/dist/assets/{MarimoErrorOutput-CYOWXi9k.js → MarimoErrorOutput-9BDdSrft.js} +2 -2
- package/dist/assets/{RenderHTML-qnAqp9Q0.js → RenderHTML-9AmE10az.js} +1 -1
- package/dist/assets/{RunButton-BDkLTxbY.js → RunButton-B1OO4yPE.js} +1 -1
- package/dist/assets/{merge-DxlVKwwO.js → _baseMerge-DLqSVasz.js} +1 -1
- package/dist/assets/{add-cell-with-ai-DVGZNNUv.js → add-cell-with-ai-B382kx-8.js} +8 -8
- package/dist/assets/{add-connection-dialog-CnRoNzHS.js → add-connection-dialog-CCgzVaXE.js} +1 -1
- package/dist/assets/{agent-panel-BTY3oeHD.js → agent-panel-tC3KSuFA.js} +1 -1
- package/dist/assets/ai-model-dropdown-BwTphSMd.js +3 -0
- package/dist/assets/{app-config-button-CB6s4qUy.js → app-config-button-C3LicTtk.js} +1 -1
- package/dist/assets/{blob-Dza7uUlT.js → blob-EfjbJ__b.js} +1 -1
- package/dist/assets/{cell-editor-D3YsOG7a.js → cell-editor-CR6poe2J.js} +7 -7
- package/dist/assets/{cell-link-DqJmxVu3.js → cell-link-q2lv7md_.js} +1 -1
- package/dist/assets/{cells-DfJw5iuV.js → cells-v0dAmt79.js} +18 -18
- package/dist/assets/{chat-display-NTNQQyoE.js → chat-display-C-KzQ4Sl.js} +1 -1
- package/dist/assets/{chat-panel-Bow0QKrA.js → chat-panel-BM8a5kkg.js} +3 -3
- package/dist/assets/{chat-ui-Cg69zAqt.js → chat-ui-B10QUDuE.js} +1 -1
- package/dist/assets/{chunk-5RXB4S5H-DHd0BPMu.js → chunk-5RXB4S5H-CUU6y4KG.js} +1 -1
- package/dist/assets/{chunk-GF5L2VYU-DidcU72l.js → chunk-GF5L2VYU-sioILS_d.js} +1 -1
- package/dist/assets/{chunk-J7OUQ5F2-BYO9AUEf.js → chunk-J7OUQ5F2-CO4p8tJJ.js} +2 -2
- package/dist/assets/{chunk-JQJVKLGR-BO9dy1YE.js → chunk-JQJVKLGR-BVs0Ec6r.js} +1 -1
- package/dist/assets/{classDiagram-JCYQIIEL-DXciwtwD.js → classDiagram-JCYQIIEL-DoI0wWRt.js} +1 -1
- package/dist/assets/{classDiagram-v2-OCEON4UE-DXciwtwD.js → classDiagram-v2-OCEON4UE-DoI0wWRt.js} +1 -1
- package/dist/assets/{column-preview-BdNXfBsJ.js → column-preview-DahZx7i3.js} +1 -1
- package/dist/assets/command-palette-DLDqpso1.js +1 -0
- package/dist/assets/{common-D_BAMDHd.js → common-C-hdr6As.js} +1 -1
- package/dist/assets/{components-D3x9DfK0.js → components-Cnt-rCdC.js} +1 -1
- package/dist/assets/{components-DwH_Yb1N.js → components-ib4asSo7.js} +1 -1
- package/dist/assets/config-Cl3eMWxb.js +1 -0
- package/dist/assets/{config-CUBl5qBg.js → config-DaNyNpZL.js} +1 -1
- package/dist/assets/dagre-B79OegKz.js +1 -0
- package/dist/assets/{dagre-VZM6K2ZE-Bztx5p0h.js → dagre-VZM6K2ZE-Bp1Dxcme.js} +1 -1
- package/dist/assets/{datasource-B9lSU_L0.js → datasource-Bq9EwG3Q.js} +1 -1
- package/dist/assets/{dependency-graph-panel-3-8ygeBR.js → dependency-graph-panel-DLgUYv2G.js} +1 -1
- package/dist/assets/{documentation-panel-BzJcTCN3.js → documentation-panel-BIqV8dCY.js} +1 -1
- package/dist/assets/{download-4OuOU5F0.js → download-D-WORhyV.js} +1 -1
- package/dist/assets/{edit-page-ds4edlkF.js → edit-page-rjLWDywQ.js} +6 -6
- package/dist/assets/{erDiagram-JOGREHBK-D7AqNG0H.js → erDiagram-JOGREHBK-L1YPulwW.js} +1 -1
- package/dist/assets/{error-entries-BCuZmxgk.js → error-entries-DOcZqCs5.js} +1 -1
- package/dist/assets/{error-panel-Dk-wFymb.js → error-panel-xPtHRph4.js} +1 -1
- package/dist/assets/{file-explorer-panel-Bprnrp3k.js → file-explorer-panel-YhKO1Kb8.js} +1 -1
- package/dist/assets/{file-icons-BnE-lUIr.js → file-icons-BI3Xijjz.js} +1 -1
- package/dist/assets/{file-name-input-B-KJyr-U.js → file-name-input-B0v9VVzS.js} +1 -1
- package/dist/assets/{floating-outline-BebYvSvL.js → floating-outline-fI7KiW7l.js} +1 -1
- package/dist/assets/{flowDiagram-UKHOOZJN-uIR0bHjt.js → flowDiagram-UKHOOZJN-dULvoGtY.js} +1 -1
- package/dist/assets/{glide-data-editor-BQNVc0zc.js → glide-data-editor-CSrQxrfb.js} +1 -1
- package/dist/assets/{home-page-zNwFmOPQ.js → home-page-BMzFHuuq.js} +1 -1
- package/dist/assets/{index-B36dhEV6.js → index-B2a_xizI.js} +8 -8
- package/dist/assets/{kiosk-mode-B2WU6CYc.js → kiosk-mode-CjT3sqy8.js} +1 -1
- package/dist/assets/{layout-iNJ-1JI2.js → layout-Dqce0OL2.js} +5 -5
- package/dist/assets/{logs-panel-CXu4QQwD.js → logs-panel-BlKmgk9O.js} +1 -1
- package/dist/assets/{markdown-renderer-BUBFaUWx.js → markdown-renderer-BHFj8yJ6.js} +1 -1
- package/dist/assets/{mermaid-CwvTqKGz.js → mermaid-BmbD1i8H.js} +3 -3
- package/dist/assets/{mindmap-definition-FAOFIHXS-DhzRwTNz.js → mindmap-definition-FAOFIHXS-aLgvJUwH.js} +1 -1
- package/dist/assets/{name-cell-input-DbRrAlCe.js → name-cell-input-9y9cdffD.js} +1 -1
- package/dist/assets/{outline-panel-16ugwsMh.js → outline-panel-BkyU6F0r.js} +1 -1
- package/dist/assets/{packages-panel-8OASpr5S.js → packages-panel-DGuBWpad.js} +1 -1
- package/dist/assets/{pair-with-agent-modal-CBIL9_6g.js → pair-with-agent-modal-TbFSfxnC.js} +1 -1
- package/dist/assets/{panel-accordion-state-786YBa3I.js → panel-accordion-state-C2vAFKpP.js} +1 -1
- package/dist/assets/panels-ZCSZ8vcg.js +4 -0
- package/dist/assets/{process-output-NwNkpkfY.js → process-output-DmEilKB6.js} +1 -1
- package/dist/assets/{readonly-code-display-Ce9Vb5Wf.js → readonly-code-display-2N0THiJK.js} +1 -1
- package/dist/assets/{readonly-python-code-CQd3Bopx.js → readonly-python-code-DyB_sdtx.js} +1 -1
- package/dist/assets/{renderShortcut-D9XYVNe3.js → renderShortcut-CS4ccH3x.js} +1 -1
- package/dist/assets/{requirementDiagram-LRYGKXZP-DcON47aG.js → requirementDiagram-LRYGKXZP-BOaR1V0o.js} +1 -1
- package/dist/assets/{reveal-component-C4pF9OE7.js → reveal-component-DmwU8_uO.js} +3 -3
- package/dist/assets/{run-page-Bo57bjbp.js → run-page-C-fKIihG.js} +1 -1
- package/dist/assets/{scratchpad-panel-vBIq9BfA.js → scratchpad-panel-D_0HEFCV.js} +1 -1
- package/dist/assets/{session-panel-BdXUO63q.js → session-panel-BAqFjL9-.js} +1 -1
- package/dist/assets/{skeleton-B5iAa5n_.js → skeleton-BUOlBrWg.js} +1 -1
- package/dist/assets/{snippets-panel-BI6SMTkf.js → snippets-panel-C4dcsNOG.js} +1 -1
- package/dist/assets/{state-DmlDv6kf.js → state-C8pDImyF.js} +3 -3
- package/dist/assets/{state-CZxxcsV7.js → state-Du63Xg9M.js} +1 -1
- package/dist/assets/{stateDiagram-OKZ733FA-CdIMP-aE.js → stateDiagram-OKZ733FA-BZg2y6qd.js} +1 -1
- package/dist/assets/{stateDiagram-v2-UEYNNEHI-DaZnzLJO.js → stateDiagram-v2-UEYNNEHI-C1g5a4e0.js} +1 -1
- package/dist/assets/{swimlanesDiagram-ULZ7WXOC-DHUJK_QJ.js → swimlanesDiagram-ULZ7WXOC-Cbwe8OlR.js} +1 -1
- package/dist/assets/{switch-CBR-ykv3.js → switch-27TbonxF.js} +1 -1
- package/dist/assets/{terminal-OC_FnEas.js → terminal-DOqqA9Ha.js} +1 -1
- package/dist/assets/{tracing-Bs9V1VRy.js → tracing-DPTkPFxO.js} +1 -1
- package/dist/assets/tracing-panel-3SQN2w-o.js +2 -0
- package/dist/assets/{tree-actions-A2eVufTn.js → tree-actions-Bn9POqng.js} +1 -1
- package/dist/assets/{useAddCell-BBv-60Hs.js → useAddCell-DtdPi8nq.js} +1 -1
- package/dist/assets/{useBoolean-DfDMEVCI.js → useBoolean-DWBCpKfT.js} +1 -1
- package/dist/assets/{useCellActionButton-DgA6C9_m.js → useCellActionButton-BS300A54.js} +1 -1
- package/dist/assets/{useDeleteCell-CqYCNkza.js → useDeleteCell-Ha8ut0yy.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-DhGw5Y2R.js → useDependencyPanelTab-DsrUeu8t.js} +1 -1
- package/dist/assets/useNotebookActions-DdgIE9oe.js +10 -0
- package/dist/assets/{useRunCells-BwYUZxV6.js → useRunCells-CIALOcRX.js} +1 -1
- package/dist/assets/{useSplitCell-D0ws6n0g.js → useSplitCell-dEuZjk85.js} +1 -1
- package/dist/assets/{useTheme-BRnykVDX.js → useTheme-D3zvxdF3.js} +1 -1
- package/dist/assets/utils-CUZQNAWJ.js +1 -0
- package/dist/assets/{vega-component-CzgE5HEs.js → vega-component-DCOCpf2B.js} +1 -1
- package/dist/index.html +29 -29
- package/package.json +1 -1
- package/src/components/ai/__tests__/ai-utils.test.ts +64 -66
- package/src/components/ai/ai-model-dropdown.tsx +5 -1
- package/src/components/ai/ai-utils.ts +37 -21
- package/src/components/app-config/ai-config.tsx +86 -24
- package/src/components/app-config/user-config-form.tsx +2 -3
- package/src/core/ai/config.ts +2 -3
- package/src/core/config/__tests__/config-schema.test.ts +31 -0
- package/src/core/config/config-schema.ts +1 -0
- package/src/core/config/config.ts +15 -2
- package/dist/assets/ai-model-dropdown-D2q0Cm0R.js +0 -3
- package/dist/assets/command-palette-QFmIvDcT.js +0 -1
- package/dist/assets/config-DcB2swfZ.js +0 -1
- package/dist/assets/dagre-Bv00Sosz.js +0 -1
- package/dist/assets/panels-BbkuRtEi.js +0 -4
- package/dist/assets/tracing-panel-CMnYFlDr.js +0 -2
- package/dist/assets/useNotebookActions-DGuDG0Px.js +0 -10
- package/dist/assets/utils-R_pHe-RB.js +0 -1
|
@@ -46,6 +46,13 @@ vi.mock("@marimo-team/llm-info/models.json", () => {
|
|
|
46
46
|
description: "Ollama Llama 2 model",
|
|
47
47
|
}),
|
|
48
48
|
],
|
|
49
|
+
marimo: [
|
|
50
|
+
make({
|
|
51
|
+
name: "Best for chat",
|
|
52
|
+
model: "best-for-chat",
|
|
53
|
+
description: "marimo hosted chat model",
|
|
54
|
+
}),
|
|
55
|
+
],
|
|
49
56
|
};
|
|
50
57
|
|
|
51
58
|
return { models };
|
|
@@ -54,8 +61,9 @@ vi.mock("@marimo-team/llm-info/models.json", () => {
|
|
|
54
61
|
// Must import after mock
|
|
55
62
|
import {
|
|
56
63
|
autoPopulateModels,
|
|
57
|
-
getConfiguredProvider,
|
|
58
64
|
getRecommendedModel,
|
|
65
|
+
listConfiguredProviders,
|
|
66
|
+
listModelsForAiSettings,
|
|
59
67
|
} from "../ai-utils";
|
|
60
68
|
|
|
61
69
|
describe("ai-utils", () => {
|
|
@@ -63,82 +71,72 @@ describe("ai-utils", () => {
|
|
|
63
71
|
vi.clearAllMocks();
|
|
64
72
|
});
|
|
65
73
|
|
|
66
|
-
describe("
|
|
67
|
-
it("
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
it("should return undefined when AI config has no credentials", () => {
|
|
73
|
-
const config: UserConfig = {
|
|
74
|
-
ai: {},
|
|
75
|
-
} as UserConfig;
|
|
76
|
-
expect(getConfiguredProvider(config.ai)).toBeUndefined();
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("should return openai when OpenAI API key is set", () => {
|
|
80
|
-
const config: UserConfig = {
|
|
81
|
-
ai: {
|
|
74
|
+
describe("listConfiguredProviders", () => {
|
|
75
|
+
it("returns known providers with credentials, then custom providers with a base URL", () => {
|
|
76
|
+
expect(
|
|
77
|
+
listConfiguredProviders({
|
|
78
|
+
wandb: { api_key: "unused" },
|
|
82
79
|
open_ai: { api_key: "sk-test" },
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
ai: {
|
|
91
|
-
anthropic: { api_key: "sk-ant-test" },
|
|
92
|
-
},
|
|
93
|
-
} as UserConfig;
|
|
94
|
-
expect(getConfiguredProvider(config.ai)).toBe("anthropic");
|
|
80
|
+
open_ai_compatible: { api_key: "unused", base_url: "https://x" },
|
|
81
|
+
custom_providers: {
|
|
82
|
+
groq: { base_url: "https://api.groq.com" },
|
|
83
|
+
empty: {},
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
86
|
+
).toEqual(["openai", "wandb", "marimo", "groq"]);
|
|
95
87
|
});
|
|
96
88
|
|
|
97
|
-
it("
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
},
|
|
102
|
-
|
|
103
|
-
expect(getConfiguredProvider(config.ai)).toBe("google");
|
|
89
|
+
it("treats marimo as configured when open_ai_compatible has a base URL", () => {
|
|
90
|
+
expect(
|
|
91
|
+
listConfiguredProviders({
|
|
92
|
+
open_ai_compatible: { api_key: "unused", base_url: "https://x" },
|
|
93
|
+
}),
|
|
94
|
+
).toEqual(["marimo"]);
|
|
104
95
|
});
|
|
105
96
|
|
|
106
|
-
it("
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
97
|
+
it("omits openai-compatible as its own provider id", () => {
|
|
98
|
+
expect(
|
|
99
|
+
listConfiguredProviders({
|
|
100
|
+
open_ai: {},
|
|
101
|
+
open_ai_compatible: { api_key: "unused" },
|
|
102
|
+
}),
|
|
103
|
+
).toEqual([]);
|
|
113
104
|
});
|
|
105
|
+
});
|
|
114
106
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
107
|
+
describe("listModelsForAiSettings", () => {
|
|
108
|
+
const entries: [string, number][] = [
|
|
109
|
+
["openai", 1],
|
|
110
|
+
["anthropic", 2],
|
|
111
|
+
["wandb", 3],
|
|
112
|
+
["marimo", 4],
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
it("returns all groups when provider config is allowed", () => {
|
|
116
|
+
expect(
|
|
117
|
+
listModelsForAiSettings(entries, {
|
|
118
|
+
allow_provider_config: true,
|
|
119
|
+
wandb: { api_key: "unused" },
|
|
120
|
+
}),
|
|
121
|
+
).toEqual(entries);
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
-
it("
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
124
|
+
it("filters to configured providers when provider config is locked", () => {
|
|
125
|
+
expect(
|
|
126
|
+
listModelsForAiSettings(entries, {
|
|
127
|
+
allow_provider_config: false,
|
|
128
|
+
wandb: { api_key: "unused" },
|
|
129
|
+
}),
|
|
130
|
+
).toEqual([["wandb", 3]]);
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
-
it("
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
} as unknown as UserConfig;
|
|
141
|
-
expect(getConfiguredProvider(config.ai)).toBe("my_provider");
|
|
133
|
+
it("keeps marimo models when locked with an openai-compatible gateway", () => {
|
|
134
|
+
expect(
|
|
135
|
+
listModelsForAiSettings(entries, {
|
|
136
|
+
allow_provider_config: false,
|
|
137
|
+
open_ai_compatible: { base_url: "https://ai.marimo.app/molab/v1" },
|
|
138
|
+
}),
|
|
139
|
+
).toEqual([["marimo", 4]]);
|
|
142
140
|
});
|
|
143
141
|
});
|
|
144
142
|
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
} from "../ui/dropdown-menu";
|
|
34
34
|
import { Tooltip } from "../ui/tooltip";
|
|
35
35
|
import { AiProviderIcon } from "./ai-provider-icon";
|
|
36
|
+
import { listModelsForAiSettings } from "./ai-utils";
|
|
36
37
|
import { getCurrentRoleTooltip, getTagColour } from "./display-helpers";
|
|
37
38
|
|
|
38
39
|
interface AIModelDropdownProps {
|
|
@@ -84,7 +85,10 @@ export const AIModelDropdown = ({
|
|
|
84
85
|
].filter(Boolean),
|
|
85
86
|
displayedModels: ai?.models?.displayed_models,
|
|
86
87
|
});
|
|
87
|
-
const modelsByProvider =
|
|
88
|
+
const modelsByProvider = listModelsForAiSettings(
|
|
89
|
+
aiModelRegistry.getListModelsByProvider(),
|
|
90
|
+
ai,
|
|
91
|
+
);
|
|
88
92
|
|
|
89
93
|
const activeModel =
|
|
90
94
|
forRole === "autocomplete"
|
|
@@ -24,39 +24,55 @@ const CREDENTIAL_CHECKERS: Record<KnownProviderId, CredentialChecker> = {
|
|
|
24
24
|
"opencode-go": (ai) => Boolean(ai?.opencode_go?.api_key),
|
|
25
25
|
bedrock: (ai) => Boolean(ai?.bedrock?.region_name),
|
|
26
26
|
ollama: (ai) => Boolean(ai?.ollama?.base_url),
|
|
27
|
-
//
|
|
27
|
+
// No user-configurable credentials in the UI.
|
|
28
28
|
deepseek: () => false,
|
|
29
|
-
marimo
|
|
29
|
+
// Hosted marimo models (marimo.app / molab) route through open_ai_compatible.
|
|
30
|
+
marimo: (ai) => Boolean(ai?.open_ai_compatible?.base_url),
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
|
-
*
|
|
34
|
+
* Known providers with credentials, then custom providers with a base URL.
|
|
34
35
|
*/
|
|
35
|
-
export function
|
|
36
|
-
|
|
37
|
-
): ProviderId
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
export function listConfiguredProviders(
|
|
37
|
+
ai: AiConfig | undefined,
|
|
38
|
+
): ProviderId[] {
|
|
39
|
+
const knownProviders = KNOWN_PROVIDERS.filter((provider) =>
|
|
40
|
+
CREDENTIAL_CHECKERS[provider](ai),
|
|
41
|
+
);
|
|
42
|
+
const customProviders = Object.entries(ai?.custom_providers ?? {})
|
|
43
|
+
.filter(([, providerConfig]) => Boolean(providerConfig?.base_url))
|
|
44
|
+
.map(([name]) => name);
|
|
45
|
+
return [...knownProviders, ...customProviders];
|
|
46
|
+
}
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Keep provider groups whose provider has credentials configured.
|
|
50
|
+
*/
|
|
51
|
+
export function filterToConfiguredProviders<T>(
|
|
52
|
+
entries: readonly [ProviderId, T][],
|
|
53
|
+
ai: AiConfig | undefined,
|
|
54
|
+
): [ProviderId, T][] {
|
|
55
|
+
const configured = new Set<string>(listConfiguredProviders(ai));
|
|
56
|
+
return entries.filter(([provider]) => configured.has(provider));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* When provider setup is locked, only show models for configured providers.
|
|
61
|
+
*/
|
|
62
|
+
export function listModelsForAiSettings<T>(
|
|
63
|
+
entries: readonly [ProviderId, T][],
|
|
64
|
+
ai: AiConfig | undefined,
|
|
65
|
+
): [ProviderId, T][] {
|
|
66
|
+
if (ai?.allow_provider_config !== false) {
|
|
67
|
+
return [...entries];
|
|
53
68
|
}
|
|
69
|
+
return filterToConfiguredProviders(entries, ai);
|
|
54
70
|
}
|
|
55
71
|
|
|
56
72
|
export function getRecommendedModel(
|
|
57
73
|
config: UserConfig["ai"],
|
|
58
74
|
): string | undefined {
|
|
59
|
-
const provider =
|
|
75
|
+
const provider = listConfiguredProviders(config)[0];
|
|
60
76
|
if (!provider) {
|
|
61
77
|
return undefined;
|
|
62
78
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import { useAtom } from "jotai";
|
|
3
|
+
import { useAtom, useAtomValue } from "jotai";
|
|
4
4
|
import {
|
|
5
5
|
BotIcon,
|
|
6
6
|
BrainIcon,
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
} from "@/core/ai/ids/ids";
|
|
44
44
|
import { type AiModel, AiModelRegistry } from "@/core/ai/model-registry";
|
|
45
45
|
import { CopilotConfig } from "@/core/codemirror/copilot/copilot-config";
|
|
46
|
+
import { resolvedMarimoConfigAtom } from "@/core/config/config";
|
|
46
47
|
import { DEFAULT_AI_MODEL, type UserConfig } from "@/core/config/config-schema";
|
|
47
48
|
import { isWasm } from "@/core/wasm/utils";
|
|
48
49
|
import { cn } from "@/utils/cn";
|
|
@@ -53,6 +54,10 @@ import {
|
|
|
53
54
|
AiProviderIcon,
|
|
54
55
|
type AiProviderIconProps,
|
|
55
56
|
} from "../ai/ai-provider-icon";
|
|
57
|
+
import {
|
|
58
|
+
listConfiguredProviders,
|
|
59
|
+
listModelsForAiSettings,
|
|
60
|
+
} from "../ai/ai-utils";
|
|
56
61
|
import { getTagColour } from "../ai/display-helpers";
|
|
57
62
|
import {
|
|
58
63
|
Accordion,
|
|
@@ -575,6 +580,11 @@ export const AiCodeCompletionConfig: React.FC<AiConfigProps> = ({
|
|
|
575
580
|
);
|
|
576
581
|
};
|
|
577
582
|
|
|
583
|
+
function useAllowProviderConfig(): boolean {
|
|
584
|
+
const ai = useAtomValue(resolvedMarimoConfigAtom).ai;
|
|
585
|
+
return ai?.allow_provider_config !== false;
|
|
586
|
+
}
|
|
587
|
+
|
|
578
588
|
const AccordionFormItem = ({
|
|
579
589
|
title,
|
|
580
590
|
triggerClassName,
|
|
@@ -867,17 +877,55 @@ export const CustomProvidersConfig: React.FC<AiConfigProps> = ({
|
|
|
867
877
|
);
|
|
868
878
|
};
|
|
869
879
|
|
|
880
|
+
const LockedProvidersList: React.FC = () => {
|
|
881
|
+
const resolvedAi = useAtomValue(resolvedMarimoConfigAtom).ai;
|
|
882
|
+
const providers = listConfiguredProviders(resolvedAi);
|
|
883
|
+
|
|
884
|
+
return (
|
|
885
|
+
<SettingGroup>
|
|
886
|
+
<p className="text-sm text-muted-secondary">
|
|
887
|
+
AI providers are configured for this environment. You cannot add your
|
|
888
|
+
own keys or custom providers.
|
|
889
|
+
</p>
|
|
890
|
+
{providers.length === 0 ? (
|
|
891
|
+
<p className="text-sm text-muted-foreground">
|
|
892
|
+
No AI providers are configured.
|
|
893
|
+
</p>
|
|
894
|
+
) : (
|
|
895
|
+
<div className="flex flex-col divide-y border rounded-md">
|
|
896
|
+
{providers.map((provider) => (
|
|
897
|
+
<div key={provider} className="flex items-center gap-2 px-3 py-2">
|
|
898
|
+
<AiProviderIcon provider={provider} className="h-4 w-4" />
|
|
899
|
+
<span className="text-sm font-medium">
|
|
900
|
+
{getProviderLabel(provider)}
|
|
901
|
+
</span>
|
|
902
|
+
<span className="ml-auto px-1 rounded bg-muted text-xs font-medium border">
|
|
903
|
+
Configured
|
|
904
|
+
</span>
|
|
905
|
+
</div>
|
|
906
|
+
))}
|
|
907
|
+
</div>
|
|
908
|
+
)}
|
|
909
|
+
</SettingGroup>
|
|
910
|
+
);
|
|
911
|
+
};
|
|
912
|
+
|
|
870
913
|
export const AiProvidersConfig: React.FC<AiConfigProps> = ({
|
|
871
914
|
form,
|
|
872
915
|
config,
|
|
873
916
|
onSubmit,
|
|
874
917
|
}) => {
|
|
875
918
|
const isWasmRuntime = isWasm();
|
|
919
|
+
const allowProviderConfig = useAllowProviderConfig();
|
|
876
920
|
|
|
877
921
|
const hasValue = (name: FieldPath<UserConfig>) => {
|
|
878
922
|
return !!form.getValues(name);
|
|
879
923
|
};
|
|
880
924
|
|
|
925
|
+
if (!allowProviderConfig) {
|
|
926
|
+
return <LockedProvidersList />;
|
|
927
|
+
}
|
|
928
|
+
|
|
881
929
|
return (
|
|
882
930
|
<SettingGroup>
|
|
883
931
|
<p className="text-sm text-muted-secondary">
|
|
@@ -1508,6 +1556,8 @@ export const AiModelDisplayConfig: React.FC<AiConfigProps> = ({
|
|
|
1508
1556
|
form,
|
|
1509
1557
|
onSubmit,
|
|
1510
1558
|
}) => {
|
|
1559
|
+
const resolvedAi = useAtomValue(resolvedMarimoConfigAtom).ai;
|
|
1560
|
+
|
|
1511
1561
|
const customModels = useWatch({
|
|
1512
1562
|
control: form.control,
|
|
1513
1563
|
name: "ai.models.custom_models",
|
|
@@ -1518,10 +1568,14 @@ export const AiModelDisplayConfig: React.FC<AiConfigProps> = ({
|
|
|
1518
1568
|
name: "ai.custom_providers",
|
|
1519
1569
|
}) as Record<string, CustomProviderConfig> | undefined;
|
|
1520
1570
|
|
|
1521
|
-
const customProviderNames = useMemo(
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1571
|
+
const customProviderNames = useMemo(() => {
|
|
1572
|
+
return [
|
|
1573
|
+
...new Set([
|
|
1574
|
+
...Object.keys(customProviders || {}),
|
|
1575
|
+
...Object.keys(resolvedAi?.custom_providers || {}),
|
|
1576
|
+
]),
|
|
1577
|
+
];
|
|
1578
|
+
}, [customProviders, resolvedAi]);
|
|
1525
1579
|
|
|
1526
1580
|
const aiModelRegistry = useMemo(
|
|
1527
1581
|
() =>
|
|
@@ -1538,7 +1592,10 @@ export const AiModelDisplayConfig: React.FC<AiConfigProps> = ({
|
|
|
1538
1592
|
}) as QualifiedModelId[];
|
|
1539
1593
|
const currentDisplayedModelsSet = new Set(currentDisplayedModels);
|
|
1540
1594
|
const modelsByProvider = aiModelRegistry.getGroupedModelsByProvider();
|
|
1541
|
-
const listModelsByProvider =
|
|
1595
|
+
const listModelsByProvider = listModelsForAiSettings(
|
|
1596
|
+
aiModelRegistry.getListModelsByProvider(),
|
|
1597
|
+
resolvedAi,
|
|
1598
|
+
);
|
|
1542
1599
|
|
|
1543
1600
|
const toggleModelDisplay = useEvent((modelId: QualifiedModelId) => {
|
|
1544
1601
|
const newModels = currentDisplayedModelsSet.has(modelId)
|
|
@@ -1676,6 +1733,9 @@ export const AddModelForm: React.FC<{
|
|
|
1676
1733
|
};
|
|
1677
1734
|
|
|
1678
1735
|
const providerClassName = "w-40 truncate";
|
|
1736
|
+
const knownProviders = KNOWN_PROVIDERS.filter(
|
|
1737
|
+
(p) => p !== "marimo" && !customProviderNames.includes(p),
|
|
1738
|
+
);
|
|
1679
1739
|
|
|
1680
1740
|
const providerSelect = (
|
|
1681
1741
|
<div className="flex flex-col gap-2">
|
|
@@ -1718,14 +1778,14 @@ export const AddModelForm: React.FC<{
|
|
|
1718
1778
|
</div>
|
|
1719
1779
|
</SelectItem>
|
|
1720
1780
|
))}
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1781
|
+
{knownProviders.length > 0 && (
|
|
1782
|
+
<p className="px-2 py-1 text-xs text-muted-secondary font-medium mt-1">
|
|
1783
|
+
Built-in Providers
|
|
1784
|
+
</p>
|
|
1785
|
+
)}
|
|
1724
1786
|
</>
|
|
1725
1787
|
)}
|
|
1726
|
-
{
|
|
1727
|
-
(p) => p !== "marimo" && !customProviderNames.includes(p),
|
|
1728
|
-
).map((p) => (
|
|
1788
|
+
{knownProviders.map((p) => (
|
|
1729
1789
|
<SelectItem key={p} value={p}>
|
|
1730
1790
|
<div className="flex items-center gap-2">
|
|
1731
1791
|
<AiProviderIcon provider={p} className="h-4 w-4" />
|
|
@@ -1733,18 +1793,20 @@ export const AddModelForm: React.FC<{
|
|
|
1733
1793
|
</div>
|
|
1734
1794
|
</SelectItem>
|
|
1735
1795
|
))}
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
<
|
|
1741
|
-
<
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1796
|
+
<>
|
|
1797
|
+
<p className="px-2 py-1 text-xs text-muted-secondary font-medium mt-1">
|
|
1798
|
+
Other
|
|
1799
|
+
</p>
|
|
1800
|
+
<SelectItem value="custom">
|
|
1801
|
+
<div className="flex items-center gap-2">
|
|
1802
|
+
<AiProviderIcon
|
|
1803
|
+
provider="openai-compatible"
|
|
1804
|
+
className="h-4 w-4"
|
|
1805
|
+
/>
|
|
1806
|
+
<span>Enter provider name</span>
|
|
1807
|
+
</div>
|
|
1808
|
+
</SelectItem>
|
|
1809
|
+
</>
|
|
1748
1810
|
</SelectGroup>
|
|
1749
1811
|
</SelectContent>
|
|
1750
1812
|
</Select>
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
4
4
|
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
|
|
5
|
-
import { merge } from "lodash-es";
|
|
6
5
|
import {
|
|
7
6
|
AlertTriangleIcon,
|
|
8
7
|
BrainIcon,
|
|
@@ -35,7 +34,7 @@ import { NumberField } from "@/components/ui/number-field";
|
|
|
35
34
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
36
35
|
import { KEYMAP_PRESETS } from "@/core/codemirror/keymaps/keymaps";
|
|
37
36
|
import { capabilitiesAtom } from "@/core/config/capabilities";
|
|
38
|
-
import { useUserConfig } from "@/core/config/config";
|
|
37
|
+
import { mergeConfig, useUserConfig } from "@/core/config/config";
|
|
39
38
|
import {
|
|
40
39
|
PackageManagerNames,
|
|
41
40
|
type UserConfig,
|
|
@@ -203,7 +202,7 @@ export const UserConfigForm: React.FC = () => {
|
|
|
203
202
|
await saveUserConfig({ config: dirtyValues });
|
|
204
203
|
// Only apply the changed keys; this avoids stale request responses
|
|
205
204
|
// overwriting newer config changes.
|
|
206
|
-
setConfig((prev) =>
|
|
205
|
+
setConfig((prev) => mergeConfig(prev, dirtyValues));
|
|
207
206
|
};
|
|
208
207
|
const onSubmit = useDebouncedCallback(onSubmitNotDebounced, FORM_DEBOUNCE);
|
|
209
208
|
|
package/src/core/ai/config.ts
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import type { Role } from "@marimo-team/llm-info";
|
|
4
4
|
import { useSetAtom } from "jotai";
|
|
5
|
-
import { merge } from "lodash-es";
|
|
6
5
|
import type { QualifiedModelId } from "@/core/ai/ids/ids";
|
|
7
|
-
import { userConfigAtom } from "@/core/config/config";
|
|
6
|
+
import { mergeConfig, userConfigAtom } from "@/core/config/config";
|
|
8
7
|
import type {
|
|
9
8
|
AIModelKey,
|
|
10
9
|
CopilotMode,
|
|
@@ -41,7 +40,7 @@ export const useAIConfigActions = () => {
|
|
|
41
40
|
const saveConfig = async (aiConfig: AiConfigPatch) => {
|
|
42
41
|
const newConfig = { ai: aiConfig };
|
|
43
42
|
await saveUserConfig({ config: newConfig }).then(() => {
|
|
44
|
-
setUserConfig((prev) =>
|
|
43
|
+
setUserConfig((prev) => mergeConfig(prev, newConfig));
|
|
45
44
|
});
|
|
46
45
|
};
|
|
47
46
|
|
|
@@ -5,6 +5,7 @@ import { expect, test } from "vitest";
|
|
|
5
5
|
import {
|
|
6
6
|
configOverridesAtom,
|
|
7
7
|
connectionTransportTypeAtom,
|
|
8
|
+
mergeConfig,
|
|
8
9
|
resolvedMarimoConfigAtom,
|
|
9
10
|
userConfigAtom,
|
|
10
11
|
} from "../config";
|
|
@@ -46,6 +47,7 @@ test("default UserConfig - empty", () => {
|
|
|
46
47
|
expect(defaultConfig).toMatchInlineSnapshot(`
|
|
47
48
|
{
|
|
48
49
|
"ai": {
|
|
50
|
+
"allow_provider_config": true,
|
|
49
51
|
"custom_providers": {},
|
|
50
52
|
"enabled": true,
|
|
51
53
|
"inline_tooltip": false,
|
|
@@ -120,6 +122,7 @@ test("default UserConfig - one level", () => {
|
|
|
120
122
|
expect(defaultConfig).toMatchInlineSnapshot(`
|
|
121
123
|
{
|
|
122
124
|
"ai": {
|
|
125
|
+
"allow_provider_config": true,
|
|
123
126
|
"custom_providers": {},
|
|
124
127
|
"enabled": true,
|
|
125
128
|
"inline_tooltip": false,
|
|
@@ -315,3 +318,31 @@ test("connectionTransportTypeAtom reads server.transport", () => {
|
|
|
315
318
|
});
|
|
316
319
|
expect(store.get(connectionTransportTypeAtom)).toBe("sse");
|
|
317
320
|
});
|
|
321
|
+
|
|
322
|
+
test("mergeConfig replaces arrays instead of merging by index", () => {
|
|
323
|
+
const prev = {
|
|
324
|
+
ai: {
|
|
325
|
+
models: {
|
|
326
|
+
displayed_models: ["wandb/a", "wandb/b", "wandb/c"],
|
|
327
|
+
custom_models: ["wandb/custom"],
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
const patch = {
|
|
332
|
+
ai: {
|
|
333
|
+
models: {
|
|
334
|
+
displayed_models: ["wandb/a"],
|
|
335
|
+
custom_models: [],
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
expect(mergeConfig(prev, patch)).toEqual({
|
|
341
|
+
ai: {
|
|
342
|
+
models: {
|
|
343
|
+
displayed_models: ["wandb/a"],
|
|
344
|
+
custom_models: [],
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
});
|
|
@@ -169,6 +169,7 @@ export const UserConfigSchema = z
|
|
|
169
169
|
max_tokens: z.number().int().positive().nullable().optional(),
|
|
170
170
|
mode: z.enum(COPILOT_MODES).prefault("manual"),
|
|
171
171
|
inline_tooltip: z.boolean().prefault(false),
|
|
172
|
+
allow_provider_config: z.boolean().prefault(true),
|
|
172
173
|
open_ai: AiConfigSchema.optional(),
|
|
173
174
|
anthropic: AiConfigSchema.optional(),
|
|
174
175
|
google: AiConfigSchema.optional(),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
|
|
3
|
-
import {
|
|
3
|
+
import { mergeWith } from "lodash-es";
|
|
4
4
|
import { OverridingHotkeyProvider } from "../hotkeys/hotkeys";
|
|
5
5
|
import { type Platform, resolvePlatform } from "../hotkeys/shortcuts";
|
|
6
6
|
import { store } from "../state/jotai";
|
|
@@ -11,6 +11,19 @@ import {
|
|
|
11
11
|
type UserConfig,
|
|
12
12
|
} from "./config-schema";
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Deep-merge config objects, replacing arrays instead of merging them by
|
|
16
|
+
* index. Lodash `merge` keeps leftover tail items. Python `deep_merge` replaces arrays.
|
|
17
|
+
*/
|
|
18
|
+
export function mergeConfig<T extends object>(...sources: object[]): T {
|
|
19
|
+
return mergeWith({}, ...sources, (_obj: unknown, src: unknown) => {
|
|
20
|
+
if (Array.isArray(src)) {
|
|
21
|
+
return [...src];
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}) as T;
|
|
25
|
+
}
|
|
26
|
+
|
|
14
27
|
/**
|
|
15
28
|
* Atom for storing the user config.
|
|
16
29
|
*/
|
|
@@ -21,7 +34,7 @@ export const configOverridesAtom = atom<{}>({});
|
|
|
21
34
|
export const resolvedMarimoConfigAtom = atom<UserConfig>((get) => {
|
|
22
35
|
const overrides = get(configOverridesAtom);
|
|
23
36
|
const userConfig = get(userConfigAtom);
|
|
24
|
-
return
|
|
37
|
+
return mergeConfig(userConfig, overrides);
|
|
25
38
|
});
|
|
26
39
|
|
|
27
40
|
export const autoInstantiateAtom = atom((get) => {
|