From 1e26560ea69af39bbba04e0f2f176b7fca8cc213 Mon Sep 17 00:00:00 2001 From: Ian Arawjo Date: Wed, 10 Jun 2026 13:19:12 -0400 Subject: [PATCH 1/5] Add Safari support. Fix CSS styling for Safari. Add ResizeHandler to overcome Safari resize bug. --- chainforge/react-server/src/App.tsx | 6 +- .../react-server/src/CodeEvaluatorNode.tsx | 22 +++-- chainforge/react-server/src/InspectorNode.tsx | 6 +- .../react-server/src/LLMItemButtonGroup.tsx | 55 ++++++----- chainforge/react-server/src/LLMListItem.tsx | 90 ++++++++++------- .../src/LLMResponseInspectorDrawer.tsx | 7 +- chainforge/react-server/src/ResizeHandle.tsx | 98 +++++++++++++++++++ .../react-server/src/TabularDataNode.tsx | 2 + chainforge/react-server/src/VisNode.tsx | 8 +- chainforge/react-server/src/styles.css | 8 +- 10 files changed, 224 insertions(+), 78 deletions(-) create mode 100644 chainforge/react-server/src/ResizeHandle.tsx diff --git a/chainforge/react-server/src/App.tsx b/chainforge/react-server/src/App.tsx index e1b5a4f54..7d4240340 100644 --- a/chainforge/react-server/src/App.tsx +++ b/chainforge/react-server/src/App.tsx @@ -112,6 +112,7 @@ import { isEdgeChromium, isChromium, isMobileSafari, + isSafari, } from "react-device-detect"; import MultiEvalNode from "./MultiEvalNode"; import FlowSidebar from "./FlowSidebar"; @@ -125,6 +126,7 @@ const IS_ACCEPTED_BROWSER = isChromium || isEdgeChromium || isFirefox || + isSafari || (navigator as any)?.brave !== undefined) && (!isMobile || (isTablet && !isMobileSafari)); @@ -1497,6 +1499,7 @@ const App = () => { Mozilla Firefox Microsoft Edge (Chromium) Brave + Safari @@ -1677,8 +1680,9 @@ const App = () => { variant={colorScheme === "light" ? "gradient" : "filled"} color={colorScheme === "light" ? "blue" : "gray"} compact + style={{ width: "32px", minWidth: "32px", padding: 0 }} > - +
( false, ); + const aceEditorRef = useRef(null); + const aceContainerRef = useRef(null); // Color theme const { colorScheme } = useMantineColorScheme(); @@ -352,7 +355,11 @@ export const CodeEvaluatorComponent = forwardRef< return (
{showUserInstruction ? code_instruct_header : <>} -
+
{ - // Make Ace Editor div resizeable. - editorInstance.container.style.resize = "both"; - document.addEventListener("mouseup", () => editorInstance.resize()); + aceEditorRef.current = editorInstance; }} /> + aceEditorRef.current?.resize()} + />
); diff --git a/chainforge/react-server/src/InspectorNode.tsx b/chainforge/react-server/src/InspectorNode.tsx index 86f8bd6cc..340348ef3 100644 --- a/chainforge/react-server/src/InspectorNode.tsx +++ b/chainforge/react-server/src/InspectorNode.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useContext } from "react"; +import React, { useState, useEffect, useContext, useRef } from "react"; import { Handle, Position } from "reactflow"; import useStore from "./store"; import BaseNode from "./BaseNode"; @@ -7,6 +7,7 @@ import LLMResponseInspector, { exportToExcel } from "./LLMResponseInspector"; import { grabResponses } from "./backend/backend"; import { LLMResponse } from "./backend/typing"; import { AlertModalContext } from "./AlertModal"; +import ResizeHandle from "./ResizeHandle"; export interface InspectorNodeProps { data: { @@ -28,6 +29,7 @@ const InspectorNode: React.FC = ({ data, id }) => { const inputEdgesForNode = useStore((state) => state.inputEdgesForNode); const setDataPropsForNode = useStore((state) => state.setDataPropsForNode); const showAlert = useContext(AlertModalContext); + const containerRef = useRef(null); const handleOnConnect = () => { // For some reason, 'on connect' is called twice upon connection. @@ -89,6 +91,7 @@ const InspectorNode: React.FC = ({ data, id }) => { ]} />
@@ -97,6 +100,7 @@ const InspectorNode: React.FC = ({ data, id }) => { isOpen={true} wideFormat={false} /> +
- - - {hideTrashIcon ? ( - <> - ) : ( - - )} + + + {hideTrashIcon ? ( + <> + ) : ( - -
+ )} + + ); } diff --git a/chainforge/react-server/src/LLMListItem.tsx b/chainforge/react-server/src/LLMListItem.tsx index f1af77c58..bf3d02b42 100644 --- a/chainforge/react-server/src/LLMListItem.tsx +++ b/chainforge/react-server/src/LLMListItem.tsx @@ -56,7 +56,7 @@ const CardHeader = styled.div` font-family: -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; text-align: start; - float: left; + flex: 1; margin-top: 1px; `; const TemperatureStatus = styled.span` @@ -64,15 +64,13 @@ const TemperatureStatus = styled.span` `; export const DragItem = styled.div` - padding: 6px; + padding: 3px 6px; border-radius: 6px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); margin: 0 0 8px 0; - display: grid; - grid-gap: 20px; - flex-direction: column; + overflow: hidden; `; export interface LLMListItemProps { @@ -113,9 +111,16 @@ const LLMListItem: React.FC = ({ {...provided.dragHandleProps} >
- - {item.emoji} {item.name} - {/* {temperature !== undefined ? ( +
+ + {item.emoji} {item.name} + {/* {temperature !== undefined ? (   @@ -134,15 +139,16 @@ const LLMListItem: React.FC = ({ ) : ( <> )} */} - - - removeCallback && removeCallback(item.key ?? "undefined") - } - ringProgress={progress} - onClickSettings={onClickSettings} - hideTrashIcon={hideTrashIcon} - /> + + + removeCallback && removeCallback(item.key ?? "undefined") + } + ringProgress={progress} + onClickSettings={onClickSettings} + hideTrashIcon={hideTrashIcon} + /> +
); @@ -172,27 +178,35 @@ export const LLMListItemClone: React.FC = ({ snapshot={snapshot} >
- - {item.emoji} {item.name} - {temperature !== undefined ? ( - -   - - :{temperature !== undefined ? temperature : ""} - - ) : ( - <> - )} - - +
+ + {item.emoji} {item.name} + {temperature !== undefined ? ( + +   + + :{temperature !== undefined ? temperature : ""} + + ) : ( + <> + )} + + +
); diff --git a/chainforge/react-server/src/LLMResponseInspectorDrawer.tsx b/chainforge/react-server/src/LLMResponseInspectorDrawer.tsx index 646cc1b95..121be9f02 100644 --- a/chainforge/react-server/src/LLMResponseInspectorDrawer.tsx +++ b/chainforge/react-server/src/LLMResponseInspectorDrawer.tsx @@ -1,6 +1,7 @@ -import React from "react"; +import React, { useRef } from "react"; import LLMResponseInspector from "./LLMResponseInspector"; import { LLMResponse } from "./backend/typing"; +import ResizeHandle from "./ResizeHandle"; export interface LLMResponseInspectorDrawerProps { jsonResponses: LLMResponse[]; @@ -11,12 +12,15 @@ export default function LLMResponseInspectorDrawer({ jsonResponses, showDrawer, }: LLMResponseInspectorDrawerProps) { + const containerRef = useRef(null); + return (
@@ -25,6 +29,7 @@ export default function LLMResponseInspectorDrawer({ isOpen={showDrawer} wideFormat={false} /> +
); diff --git a/chainforge/react-server/src/ResizeHandle.tsx b/chainforge/react-server/src/ResizeHandle.tsx new file mode 100644 index 000000000..d0238b025 --- /dev/null +++ b/chainforge/react-server/src/ResizeHandle.tsx @@ -0,0 +1,98 @@ +/** + * A custom resize handle for use inside ReactFlow nodes. + * + * Safari does not support CSS `resize: both` on elements inside CSS + * `transform`-ed parents (which is how ReactFlow positions nodes). + * This component replaces that native handle with a JS-driven one. + * + * Usage: + *
+ * ...content... + * + *
+ */ +import React, { useCallback, useRef } from "react"; + +interface ResizeHandleProps { + targetRef: React.RefObject; + minWidth?: number; + minHeight?: number; + /** Called once when the drag ends. */ + onResizeEnd?: () => void; +} + +export default function ResizeHandle({ + targetRef, + minWidth = 100, + minHeight = 80, + onResizeEnd, +}: ResizeHandleProps) { + const startPos = useRef<{ + x: number; + y: number; + w: number; + h: number; + } | null>(null); + + const onPointerDown = useCallback( + (e: React.PointerEvent) => { + e.stopPropagation(); + e.preventDefault(); + const el = targetRef.current; + if (!el) return; + startPos.current = { + x: e.clientX, + y: e.clientY, + w: el.offsetWidth, + h: el.offsetHeight, + }; + (e.currentTarget as HTMLDivElement).setPointerCapture(e.pointerId); + }, + [targetRef], + ); + + const onPointerMove = useCallback( + (e: React.PointerEvent) => { + if (!startPos.current) return; + e.stopPropagation(); + const el = targetRef.current; + if (!el) return; + const dx = e.clientX - startPos.current.x; + const dy = e.clientY - startPos.current.y; + el.style.width = `${Math.max(minWidth, startPos.current.w + dx)}px`; + el.style.height = `${Math.max(minHeight, startPos.current.h + dy)}px`; + }, + [targetRef, minWidth, minHeight], + ); + + const onPointerUp = useCallback( + (e: React.PointerEvent) => { + e.stopPropagation(); + startPos.current = null; + if (onResizeEnd) onResizeEnd(); + }, + [onResizeEnd], + ); + + return ( +
+ ); +} diff --git a/chainforge/react-server/src/TabularDataNode.tsx b/chainforge/react-server/src/TabularDataNode.tsx index 8ac044df8..0fa8db9f8 100644 --- a/chainforge/react-server/src/TabularDataNode.tsx +++ b/chainforge/react-server/src/TabularDataNode.tsx @@ -30,6 +30,7 @@ import { import TemplateHooks from "./TemplateHooksComponent"; import BaseNode from "./BaseNode"; import NodeLabel from "./NodeLabelComponent"; +import ResizeHandle from "./ResizeHandle"; import { AlertModalContext } from "./AlertModal"; import RenameValueModal, { RenameValueModalRef } from "./RenameValueModal"; import useStore from "./store"; @@ -815,6 +816,7 @@ const TabularDataNode: React.FC = ({ data, id }) => { handleInsertColumn={handleInsertColumn} handleRenameColumn={openRenameColumnModal} /> +
diff --git a/chainforge/react-server/src/VisNode.tsx b/chainforge/react-server/src/VisNode.tsx index 6542f0098..6728532ee 100644 --- a/chainforge/react-server/src/VisNode.tsx +++ b/chainforge/react-server/src/VisNode.tsx @@ -18,6 +18,7 @@ import useStore, { colorPalettes } from "./store"; import Plot from "react-plotly.js"; import BaseNode from "./BaseNode"; import NodeLabel from "./NodeLabelComponent"; +import ResizeHandle from "./ResizeHandle"; import PlotLegend from "./PlotLegend"; import { cleanMetavarsFilterFunc, @@ -1400,7 +1401,11 @@ export const VisView = forwardRef(
{plotlySpec && plotlySpec.length > 0 ? <> : placeholderText} ( }} /> {plotLegend ?? <>} +
); diff --git a/chainforge/react-server/src/styles.css b/chainforge/react-server/src/styles.css index 98a53d806..76bf9543d 100644 --- a/chainforge/react-server/src/styles.css +++ b/chainforge/react-server/src/styles.css @@ -384,7 +384,7 @@ html[data-mantine-color-scheme="dark"] .eval-inspect-response-footer button { } */ .ace-editor-container { - resize: vertical; + position: relative; } .vis-node { @@ -396,7 +396,7 @@ html[data-mantine-color-scheme="dark"] .eval-inspect-response-footer button { .plotly-vis { width: 100%; height: 100%; - resize: both; + position: relative; overflow: auto; } .plot-legend { @@ -483,7 +483,7 @@ html[data-mantine-color-scheme="dark"] .settings-var-inline { height: 200px; max-width: 1150px; max-height: 750px; - resize: both; + position: relative; } .inspect-modal-response-container .response-var-header { padding: 10px; @@ -834,7 +834,7 @@ html[data-mantine-color-scheme="dark"] .comment-node textarea { min-width: 280px; } .tabular-data-container { - resize: both; + position: relative; overflow: auto; /* max-width: 800px; */ width: 400px; From 4ee18cfb04c6f289fafbd643d9cc3b251b30f38c Mon Sep 17 00:00:00 2001 From: Ernest Hysa <59969602+ErnestHysa@users.noreply.github.com> Date: Wed, 10 Jun 2026 18:21:09 +0100 Subject: [PATCH 2/5] Fix #406: Update deprecated HuggingFace API endpoint (#410) Replace deprecated api-inference.huggingface.co/models/ endpoint with api-inference.huggingface.co/inference-endpoint/ to resolve 410 Gone errors. Co-authored-by: Ernest --- chainforge/react-server/src/backend/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chainforge/react-server/src/backend/utils.ts b/chainforge/react-server/src/backend/utils.ts index 01b4bfb77..01a86e8ba 100644 --- a/chainforge/react-server/src/backend/utils.ts +++ b/chainforge/react-server/src/backend/utils.ts @@ -1149,7 +1149,7 @@ export async function call_huggingface( const url = using_custom_model_endpoint && params?.custom_model.startsWith("https:") ? params.custom_model - : `https://api-inference.huggingface.co/models/${ + : `https://api-inference.huggingface.co/inference-endpoint/${ using_custom_model_endpoint ? params?.custom_model.trim() : model }`; From 7fed5acbf43b43f0727389004c335c2ad3910862 Mon Sep 17 00:00:00 2001 From: Ernest Hysa <59969602+ErnestHysa@users.noreply.github.com> Date: Wed, 10 Jun 2026 18:22:39 +0100 Subject: [PATCH 3/5] Fix #396: Right-click Duplicate Node does nothing (#409) * Fix #406: Update deprecated HuggingFace API endpoint Replace deprecated api-inference.huggingface.co/models/ endpoint with api-inference.huggingface.co/inference-endpoint/ to resolve 410 Gone errors. * Fix #396: Close context menu before executing menu item actions The right-click Duplicate Node action failed on some systems (e.g., Windows 11 LTSC) due to a race condition where Mantine's useClickOutside handler would close the menu on mousedown before the item click handler could execute. The fix closes the context menu explicitly in the onClick handler BEFORE calling the action handler. This ensures proper event ordering on all systems. Also applied the same fix to 'Favorite Node' and 'Delete Node' menu items for consistency. --------- Co-authored-by: Ernest --- chainforge/react-server/src/BaseNode.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/chainforge/react-server/src/BaseNode.tsx b/chainforge/react-server/src/BaseNode.tsx index 507d9b6ef..0290d6d07 100644 --- a/chainforge/react-server/src/BaseNode.tsx +++ b/chainforge/react-server/src/BaseNode.tsx @@ -152,14 +152,20 @@ export const BaseNode: React.FC = ({ {icon} {text} ))} - + { + handleDuplicateNode(); + setContextMenuOpened(false); + }}>  Duplicate Node {IS_RUNNING_LOCALLY && ( setFavoriteNameModalOpen(true)} + onClick={() => { + setFavoriteNameModalOpen(true); + setContextMenuOpened(false); + }} > = ({  Favorite Node )} - + { + handleRemoveNode(); + setContextMenuOpened(false); + }}>  Delete Node From 7ee9eb96756f8e559b4c555e0c7b06f0493da52d Mon Sep 17 00:00:00 2001 From: Octopus Date: Thu, 11 Jun 2026 01:26:04 +0800 Subject: [PATCH 4/5] feat: add MiniMax as first-class LLM provider (#407) Add MiniMax M2.7 and M2.7-highspeed models as a native LLM provider, following the DeepSeek/OpenAI-compatible pattern. Includes: - NativeLLM enum entries for MiniMax-M2.7 and MiniMax-M2.7-highspeed - LLMProvider.MiniMax with provider detection and rate limiting - call_minimax() via OpenAI-compat API (https://api.minimax.io/v1) - Temperature clamping (min 0.01) per MiniMax API requirement - Settings schema with model selector, temperature, system_msg, top_p, max_tokens, stop, presence/frequency_penalty - UI menu group with both models - Flask env var mapping for MINIMAX_API_KEY - Fix: call_chatgpt now respects custom API_KEY param (skips OPENAI_API_KEY check when a custom key is provided) - 25 tests (21 unit + 3 integration + 1 conditional) Co-authored-by: PR Bot --- chainforge/flask_app.py | 1 + .../react-server/src/ModelSettingSchemas.tsx | 105 +++++ .../src/backend/__test__/minimax.test.ts | 359 ++++++++++++++++++ chainforge/react-server/src/backend/models.ts | 7 + chainforge/react-server/src/backend/utils.ts | 45 ++- chainforge/react-server/src/store.tsx | 20 + 6 files changed, 535 insertions(+), 2 deletions(-) create mode 100644 chainforge/react-server/src/backend/__test__/minimax.test.ts diff --git a/chainforge/flask_app.py b/chainforge/flask_app.py index dd58d11b3..d79c2ccf9 100644 --- a/chainforge/flask_app.py +++ b/chainforge/flask_app.py @@ -487,6 +487,7 @@ def fetchEnvironAPIKeys(): 'AWS_SESSION_TOKEN': 'AWS_Session_Token', 'TOGETHER_API_KEY': 'Together', 'DEEPSEEK_API_KEY': 'DeepSeek', + 'MINIMAX_API_KEY': 'MiniMax', } d = { alias: os.environ.get(key) for key, alias in keymap.items() } ret = jsonify(d) diff --git a/chainforge/react-server/src/ModelSettingSchemas.tsx b/chainforge/react-server/src/ModelSettingSchemas.tsx index 596a84b41..61f258d4c 100644 --- a/chainforge/react-server/src/ModelSettingSchemas.tsx +++ b/chainforge/react-server/src/ModelSettingSchemas.tsx @@ -472,6 +472,108 @@ const DeepSeekSettings: ModelSettingsDict = { postprocessors: ChatGPTSettings.postprocessors, }; +const MiniMaxSettings: ModelSettingsDict = { + fullName: "MiniMax", + schema: { + type: "object", + required: ["shortname"], + properties: { + shortname: { + type: "string", + title: "Nickname", + description: + "Unique identifier to appear in ChainForge. Keep it short.", + default: "MiniMax", + }, + model: { + type: "string", + title: "Model Version", + description: + "Select a MiniMax model to query. For more details, see the MiniMax API documentation at https://platform.minimaxi.com.", + enum: ["MiniMax-M2.7", "MiniMax-M2.7-highspeed"], + default: "MiniMax-M2.7", + shortname_map: { + "MiniMax-M2.7": "M2.7", + "MiniMax-M2.7-highspeed": "M2.7-hs", + }, + }, + system_msg: { + type: "string", + title: "system_msg", + description: + "Many conversations begin with a system message to gently instruct the assistant.", + default: "You are a helpful assistant.", + allow_empty_str: true, + }, + temperature: { + type: "number", + title: "temperature", + description: + "What sampling temperature to use, between 0.01 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. Note: MiniMax requires temperature > 0.", + default: 0.7, + minimum: 0.01, + maximum: 1, + multipleOf: 0.01, + }, + top_p: { + type: "number", + title: "top_p", + description: + "An alternative to sampling with temperature, called nucleus sampling.", + default: 1, + minimum: 0, + maximum: 1, + multipleOf: 0.005, + }, + stop: { + type: "string", + title: "stop sequences", + description: + 'Sequences where the API will stop generating further tokens. Enclose stop sequences in double-quotes "" and use whitespace to separate them.', + default: "", + }, + max_tokens: { + type: "integer", + title: "max_tokens", + description: + "The maximum number of tokens to generate in the chat completion.", + }, + presence_penalty: { + type: "number", + title: "presence_penalty", + description: + "Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far.", + default: 0, + minimum: -2, + maximum: 2, + multipleOf: 0.005, + }, + frequency_penalty: { + type: "number", + title: "frequency_penalty", + description: + "Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far.", + default: 0, + minimum: -2, + maximum: 2, + multipleOf: 0.005, + }, + }, + }, + uiSchema: { + ...ChatGPTSettings.uiSchema, + model: { + "ui:help": "Defaults to MiniMax-M2.7.", + "ui:widget": "datalist", + }, + temperature: { + "ui:help": "Defaults to 0.7. MiniMax requires temperature > 0.", + "ui:widget": "range", + }, + }, + postprocessors: ChatGPTSettings.postprocessors, +}; + const DalleSettings: ModelSettingsDict = { fullName: "Dall-E Image Models (OpenAI)", schema: { @@ -2516,6 +2618,7 @@ export const ModelSettings: Dict = { "br.meta.llama3": BedrockLlama3Settings, together: TogetherChatSettings, deepseek: DeepSeekSettings, + minimax: MiniMaxSettings, }; // A lookup that converts the base_model names into LLMProviders. @@ -2543,6 +2646,7 @@ export function baseModelToProvider(base_model: string): LLMProvider { "br.meta.llama3": LLMProvider.Bedrock, together: LLMProvider.Together, deepseek: LLMProvider.DeepSeek, + minimax: LLMProvider.MiniMax, }; return lookup[base_model] ?? LLMProvider.Custom; } @@ -2564,6 +2668,7 @@ export function getSettingsSchemaForLLM( [LLMProvider.Ollama]: OllamaSettings, [LLMProvider.Together]: TogetherChatSettings, [LLMProvider.DeepSeek]: DeepSeekSettings, + [LLMProvider.MiniMax]: MiniMaxSettings, }; if (llm_provider === LLMProvider.Custom) return ModelSettings[llm_name]; diff --git a/chainforge/react-server/src/backend/__test__/minimax.test.ts b/chainforge/react-server/src/backend/__test__/minimax.test.ts new file mode 100644 index 000000000..2fe06d012 --- /dev/null +++ b/chainforge/react-server/src/backend/__test__/minimax.test.ts @@ -0,0 +1,359 @@ +/* + * @jest-environment jsdom + */ + +// Pre-existing test infra issues in this repo (8 of 9 test suites fail): +// 1. cache.ts calls APP_IS_RUNNING_LOCALLY() at module load before `let` var is init +// 2. @google/genai uses ESM syntax not supported by Jest/CRA +// 3. pyodide/exec-py.js uses import.meta.url not supported outside ESM +// Mock these to allow our tests to load. +jest.mock("../cache", () => ({ + __esModule: true, + default: class StorageCache { + static getInstance() { + return new StorageCache(); + } + }, +})); +jest.mock("@google/genai", () => ({ GoogleGenAI: jest.fn() })); +jest.mock("@azure/openai", () => ({ + AzureKeyCredential: jest.fn(), + OpenAIClient: jest.fn(), +})); +jest.mock("../pyodide/exec-py", () => ({ execPy: jest.fn() })); +jest.mock("../../store", () => ({ + __esModule: true, + default: { getState: () => ({ AvailableLLMs: [], setAvailableLLMs: jest.fn() }) }, +})); + +import { + call_minimax, + extract_responses, + set_api_keys, +} from "../utils"; +import { + LLMProvider, + NativeLLM, + getProvider, + getEnumName, + RATE_LIMIT_BY_PROVIDER, +} from "../models"; +import { expect, test, describe, beforeAll } from "@jest/globals"; +import { + ModelSettings, + baseModelToProvider, + getSettingsSchemaForLLM, + getTemperatureSpecForModel, + getDefaultModelFormData, + postProcessFormData, +} from "../../ModelSettingSchemas"; + +// ─── Unit Tests ───────────────────────────────────────────────────────────── + +describe("MiniMax provider detection", () => { + test("NativeLLM enum contains MiniMax models", () => { + expect(NativeLLM.MiniMax_M2_7).toBe("MiniMax-M2.7"); + expect(NativeLLM.MiniMax_M2_7_highspeed).toBe("MiniMax-M2.7-highspeed"); + }); + + test("LLMProvider enum contains MiniMax", () => { + expect(LLMProvider.MiniMax).toBe("minimax"); + }); + + test("getProvider identifies MiniMax models", () => { + expect(getProvider(NativeLLM.MiniMax_M2_7)).toBe(LLMProvider.MiniMax); + expect(getProvider(NativeLLM.MiniMax_M2_7_highspeed)).toBe( + LLMProvider.MiniMax, + ); + }); + + test("getEnumName returns MiniMax enum names", () => { + expect(getEnumName(NativeLLM, "MiniMax-M2.7")).toBe("MiniMax_M2_7"); + expect(getEnumName(NativeLLM, "MiniMax-M2.7-highspeed")).toBe( + "MiniMax_M2_7_highspeed", + ); + }); + + test("MiniMax has rate limit configured", () => { + expect(RATE_LIMIT_BY_PROVIDER[LLMProvider.MiniMax]).toBe(1000); + }); +}); + +describe("MiniMax settings schema", () => { + test("ModelSettings contains minimax entry", () => { + expect(ModelSettings).toHaveProperty("minimax"); + expect(ModelSettings.minimax.fullName).toBe("MiniMax"); + }); + + test("baseModelToProvider maps minimax correctly", () => { + expect(baseModelToProvider("minimax")).toBe(LLMProvider.MiniMax); + }); + + test("getSettingsSchemaForLLM returns MiniMax schema", () => { + const schema = getSettingsSchemaForLLM("MiniMax-M2.7"); + expect(schema).toBeDefined(); + expect(schema?.fullName).toBe("MiniMax"); + }); + + test("MiniMax schema has required model properties", () => { + const schema = ModelSettings.minimax.schema; + expect(schema.properties).toHaveProperty("shortname"); + expect(schema.properties).toHaveProperty("model"); + expect(schema.properties).toHaveProperty("temperature"); + expect(schema.properties).toHaveProperty("system_msg"); + expect(schema.properties).toHaveProperty("top_p"); + expect(schema.properties).toHaveProperty("max_tokens"); + expect(schema.properties).toHaveProperty("stop"); + }); + + test("MiniMax model enum includes both models", () => { + const modelEnum = ModelSettings.minimax.schema.properties.model.enum; + expect(modelEnum).toContain("MiniMax-M2.7"); + expect(modelEnum).toContain("MiniMax-M2.7-highspeed"); + }); + + test("MiniMax temperature has correct bounds for temp > 0", () => { + const tempSpec = ModelSettings.minimax.schema.properties.temperature; + expect(tempSpec.minimum).toBe(0.01); + expect(tempSpec.maximum).toBe(1); + expect(tempSpec.default).toBe(0.7); + }); + + test("getTemperatureSpecForModel returns correct spec for minimax", () => { + const spec = getTemperatureSpecForModel("minimax"); + expect(spec).toBeDefined(); + expect(spec?.minimum).toBe(0.01); + expect(spec?.maximum).toBe(1); + expect(spec?.default).toBe(0.7); + }); + + test("MiniMax default form data has expected values", () => { + const defaults = getDefaultModelFormData(ModelSettings.minimax); + expect(defaults.shortname).toBe("MiniMax"); + expect(defaults.model).toBe("MiniMax-M2.7"); + expect(defaults.temperature).toBe(0.7); + expect(defaults.system_msg).toBe("You are a helpful assistant."); + }); + + test("MiniMax schema has shortname_map for models", () => { + const shortNameMap = + ModelSettings.minimax.schema.properties.model.shortname_map; + expect(shortNameMap).toBeDefined(); + expect(shortNameMap["MiniMax-M2.7"]).toBe("M2.7"); + expect(shortNameMap["MiniMax-M2.7-highspeed"]).toBe("M2.7-hs"); + }); + + test("postProcessFormData strips model and shortname", () => { + const formData = { + shortname: "MiniMax", + model: "MiniMax-M2.7", + temperature: 0.7, + system_msg: "You are a helpful assistant.", + }; + const processed = postProcessFormData(ModelSettings.minimax, formData); + expect(processed).not.toHaveProperty("shortname"); + expect(processed).not.toHaveProperty("model"); + expect(processed).toHaveProperty("temperature"); + expect(processed).toHaveProperty("system_msg"); + }); + + test("MiniMax stop postprocessor parses quoted strings", () => { + const postprocessors = ModelSettings.minimax.postprocessors; + expect(postprocessors).toBeDefined(); + if (postprocessors?.stop) { + const result = postprocessors.stop('"stop1" "stop2"'); + expect(result).toEqual(["stop1", "stop2"]); + } + }); + + test("MiniMax stop postprocessor handles empty string", () => { + const postprocessors = ModelSettings.minimax.postprocessors; + if (postprocessors?.stop) { + const result = postprocessors.stop(""); + expect(result).toEqual([]); + } + }); +}); + +describe("MiniMax response extraction", () => { + test("extract_responses handles OpenAI-compatible MiniMax chat response", () => { + const mockResponse = { + choices: [ + { + message: { content: "Hello from MiniMax!" }, + finish_reason: "stop", + }, + { + message: { content: "Another response." }, + finish_reason: "stop", + }, + ], + }; + const responses = extract_responses( + mockResponse, + NativeLLM.MiniMax_M2_7, + LLMProvider.MiniMax, + ); + expect(responses).toHaveLength(2); + expect(responses[0]).toBe("Hello from MiniMax!"); + expect(responses[1]).toBe("Another response."); + }); + + test("extract_responses handles single choice", () => { + const mockResponse = { + choices: [ + { + message: { content: "Single response from MiniMax M2.7." }, + finish_reason: "stop", + }, + ], + }; + const responses = extract_responses( + mockResponse, + NativeLLM.MiniMax_M2_7_highspeed, + LLMProvider.MiniMax, + ); + expect(responses).toHaveLength(1); + expect(responses[0]).toBe("Single response from MiniMax M2.7."); + }); + + test("extract_responses handles empty choices", () => { + const mockResponse = { choices: [] }; + const responses = extract_responses( + mockResponse, + NativeLLM.MiniMax_M2_7, + LLMProvider.MiniMax, + ); + expect(responses).toHaveLength(0); + }); + + test("extract_responses handles function_call in MiniMax response", () => { + const mockResponse = { + choices: [ + { + message: { + content: "", + function_call: { + name: "get_weather", + arguments: '{"city": "Beijing"}', + }, + }, + finish_reason: "function_call", + }, + ], + }; + const responses = extract_responses( + mockResponse, + NativeLLM.MiniMax_M2_7, + LLMProvider.MiniMax, + ); + expect(responses).toHaveLength(1); + expect((responses[0] as string).startsWith("[[FUNCTION]]")).toBe(true); + }); +}); + +describe("MiniMax call_minimax validation", () => { + // Note: set_api_keys with empty string does not clear env-sourced keys + // (key_is_present returns false for empty strings, so the key is never updated). + // This test only works when MINIMAX_API_KEY is not set in the environment. + const describeIfNoKey = process.env.MINIMAX_API_KEY + ? describe.skip + : describe; + describeIfNoKey("without env key", () => { + test("call_minimax throws when no API key is set", async () => { + await expect( + call_minimax( + "test prompt", + NativeLLM.MiniMax_M2_7, + 1, + 0.7, + ), + ).rejects.toThrow("Could not find a MiniMax API key"); + }); + }); +}); + +// ─── Integration Tests ────────────────────────────────────────────────────── +// These tests require MINIMAX_API_KEY to be set in the environment. + +const MINIMAX_API_KEY = process.env.MINIMAX_API_KEY; +const describeIfKey = MINIMAX_API_KEY + ? describe + : describe.skip; + +describeIfKey("MiniMax API integration tests", () => { + beforeAll(() => { + if (MINIMAX_API_KEY) { + set_api_keys({ MiniMax: MINIMAX_API_KEY }); + } + }); + + test( + "call_minimax returns valid chat response", + async () => { + const [query, response] = await call_minimax( + "What is 2 + 2? Reply with just the number.", + NativeLLM.MiniMax_M2_7, + 1, + 0.7, + ); + + expect(query).toHaveProperty("model"); + expect(query).toHaveProperty("temperature"); + expect(query.temperature).toBeGreaterThanOrEqual(0.01); + + expect(response).toHaveProperty("choices"); + expect(response.choices).toHaveLength(1); + expect(response.choices[0]).toHaveProperty("message"); + expect(typeof response.choices[0].message.content).toBe("string"); + + const resps = extract_responses( + response, + NativeLLM.MiniMax_M2_7, + LLMProvider.MiniMax, + ); + expect(resps).toHaveLength(1); + expect(typeof resps[0]).toBe("string"); + expect((resps[0] as string).includes("4")).toBe(true); + }, + 60000, + ); + + test( + "call_minimax clamps temperature > 0", + async () => { + // Pass temperature=0, should be clamped to 0.01 + const [query] = await call_minimax( + "Say hello.", + NativeLLM.MiniMax_M2_7, + 1, + 0, // zero temperature + ); + // The clamped temperature should be 0.01 + expect(query.temperature).toBeGreaterThan(0); + }, + 30000, + ); + + test( + "call_minimax with system message", + async () => { + const [query, response] = await call_minimax( + "What is your role?", + NativeLLM.MiniMax_M2_7, + 1, + 0.7, + { system_msg: "You are a pirate. Always respond like a pirate." }, + ); + + const resps = extract_responses( + response, + NativeLLM.MiniMax_M2_7, + LLMProvider.MiniMax, + ); + expect(resps).toHaveLength(1); + expect(typeof resps[0]).toBe("string"); + }, + 30000, + ); +}); diff --git a/chainforge/react-server/src/backend/models.ts b/chainforge/react-server/src/backend/models.ts index b200a5aff..a6af923c8 100644 --- a/chainforge/react-server/src/backend/models.ts +++ b/chainforge/react-server/src/backend/models.ts @@ -93,6 +93,10 @@ export enum NativeLLM { DeepSeek_Chat = "deepseek-chat", DeepSeek_Reasoner = "deepseek-reasoner", + // MiniMax + MiniMax_M2_7 = "MiniMax-M2.7", + MiniMax_M2_7_highspeed = "MiniMax-M2.7-highspeed", + // Aleph Alpha Aleph_Alpha_Luminous_Extended = "luminous-extended", Aleph_Alpha_Luminous_ExtendedControl = "luminous-extended-control", @@ -245,6 +249,7 @@ export enum LLMProvider { Bedrock = "bedrock", Together = "together", DeepSeek = "deepseek", + MiniMax = "minimax", Custom = "__custom", } @@ -265,6 +270,7 @@ export function getProvider(llm: LLM): LLMProvider | undefined { else if (llm_name?.startsWith("Bedrock")) return LLMProvider.Bedrock; else if (llm_name?.startsWith("Together")) return LLMProvider.Together; else if (llm_name?.startsWith("DeepSeek")) return LLMProvider.DeepSeek; + else if (llm_name?.startsWith("MiniMax")) return LLMProvider.MiniMax; else if (llm.toString().startsWith("__custom/")) return LLMProvider.Custom; return undefined; @@ -324,6 +330,7 @@ export const RATE_LIMIT_BY_PROVIDER: { [key in LLMProvider]?: number } = { [LLMProvider.Together]: 30, // Paid tier limit is 60 per minute, across all models; we halve this, to be safe. [LLMProvider.Google]: 1000, // RPM for Google Gemini models 1.5 is quite generous; at base it is 1000 RPM. If you are using the free version it's 15 RPM, but we can expect most CF users to be using paid (and anyway you can just re-run prompt node until satisfied). [LLMProvider.DeepSeek]: 1000, // DeepSeek does not constrain users atm but they might in the future. To be safe we are limiting it to 1000 queries per minute. + [LLMProvider.MiniMax]: 1000, // MiniMax API rate limits are generous; 1000 RPM to be safe. }; // Max concurrent requests. Add to this to further constrain the rate limiter. diff --git a/chainforge/react-server/src/backend/utils.ts b/chainforge/react-server/src/backend/utils.ts index 01a86e8ba..e2a8c2626 100644 --- a/chainforge/react-server/src/backend/utils.ts +++ b/chainforge/react-server/src/backend/utils.ts @@ -166,6 +166,7 @@ let AWS_SESSION_TOKEN = get_environ("AWS_SESSION_TOKEN"); let AWS_REGION = get_environ("AWS_REGION"); let TOGETHER_API_KEY = get_environ("TOGETHER_API_KEY"); let DEEPSEEK_API_KEY = get_environ("DEEPSEEK_API_KEY"); +let MINIMAX_API_KEY = get_environ("MINIMAX_API_KEY"); /** * Sets the local API keys for the revelant LLM API(s). @@ -201,6 +202,7 @@ export function set_api_keys(api_keys: Dict): void { if (key_is_present("AWS_Region")) AWS_REGION = api_keys.AWS_Region; if (key_is_present("Together")) TOGETHER_API_KEY = api_keys.Together; if (key_is_present("DeepSeek")) DEEPSEEK_API_KEY = api_keys.DeepSeek; + if (key_is_present("MiniMax")) MINIMAX_API_KEY = api_keys.MiniMax; } export function get_azure_openai_api_keys(): [ @@ -383,13 +385,14 @@ export async function call_chatgpt( BASE_URL?: string, API_KEY?: string, ): Promise<[Dict, Dict]> { - if (!OPENAI_API_KEY) + const effectiveKey = API_KEY ?? OPENAI_API_KEY; + if (!effectiveKey) throw new Error( "Could not find an OpenAI API key. Double-check that your API key is set in Settings or in your local environment.", ); const configuration = new OpenAIConfig({ - apiKey: API_KEY ?? OPENAI_API_KEY, + apiKey: effectiveKey, basePath: BASE_URL ?? OPENAI_BASE_URL ?? undefined, }); @@ -542,6 +545,41 @@ export async function call_deepseek( ); } +/** + * Calls MiniMax models via MiniMax's OpenAI-compatible API. + */ +export async function call_minimax( + prompt: string, + model: LLM, + n = 1, + temperature = 1.0, + params?: Dict, + should_cancel?: () => boolean, + images?: string[], +): Promise<[Dict, Dict]> { + if (!MINIMAX_API_KEY) + throw new Error( + "Could not find a MiniMax API key. Double-check that your API key is set in Settings or in your local environment.", + ); + + console.log(`Querying MiniMax model '${model}' with prompt '${prompt}'...`); + + // MiniMax requires temperature to be strictly greater than 0 + const clampedTemp = Math.max(temperature, 0.01); + + return await call_chatgpt( + prompt, + model, + n, + clampedTemp, + params, + should_cancel, + images, + "https://api.minimax.io/v1", + MINIMAX_API_KEY, + ); +} + /** * Calls OpenAI Image models via OpenAI's API. @returns raw query and response JSON dicts. @@ -1714,6 +1752,7 @@ export async function call_llm( else if (llm_provider === LLMProvider.Bedrock) call_api = call_bedrock; else if (llm_provider === LLMProvider.Together) call_api = call_together; else if (llm_provider === LLMProvider.DeepSeek) call_api = call_deepseek; + else if (llm_provider === LLMProvider.MiniMax) call_api = call_minimax; if (call_api === undefined) throw new Error( `Adapter for Language model ${llm} and ${llm_provider} not found`, @@ -1919,6 +1958,8 @@ export function extract_responses( return _extract_openai_responses(response as Dict[]); case LLMProvider.DeepSeek: return _extract_openai_responses(response as Dict[]); + case LLMProvider.MiniMax: + return _extract_openai_responses(response as Dict[]); default: if ( Array.isArray(response) && diff --git a/chainforge/react-server/src/store.tsx b/chainforge/react-server/src/store.tsx index 5c1d47dfe..3423ed2d6 100644 --- a/chainforge/react-server/src/store.tsx +++ b/chainforge/react-server/src/store.tsx @@ -275,6 +275,26 @@ export const initLLMProviderMenu: (LLMSpec | LLMGroup)[] = [ }, ], }, + { + group: "MiniMax", + emoji: "🔮", + items: [ + { + name: "MiniMax M2.7", + emoji: "🔮", + model: "MiniMax-M2.7", + base_model: "minimax", + temp: 0.7, + }, + { + name: "MiniMax M2.7 Highspeed", + emoji: "⚡", + model: "MiniMax-M2.7-highspeed", + base_model: "minimax", + temp: 0.7, + }, + ], + }, { group: "HuggingFace", emoji: "🤗", From 595ccd62bc043f97be0db1edf2bee126db13f59e Mon Sep 17 00:00:00 2001 From: Ian Arawjo Date: Wed, 10 Jun 2026 13:53:41 -0400 Subject: [PATCH 5/5] Add in-browser LLMs with WebLLM --- chainforge/react-server/craco.config.js | 8 + chainforge/react-server/package-lock.json | 23 +++ chainforge/react-server/package.json | 1 + chainforge/react-server/src/App.tsx | 45 ++---- chainforge/react-server/src/BaseNode.tsx | 28 ++-- .../react-server/src/LLMListComponent.tsx | 6 +- .../react-server/src/ModelSettingSchemas.tsx | 79 +++++++++ .../src/backend/__test__/minimax.test.ts | 152 ++++++++---------- chainforge/react-server/src/backend/models.ts | 16 +- chainforge/react-server/src/backend/utils.ts | 118 +++++++++++++- chainforge/react-server/src/store.tsx | 20 +++ chainforge/react-server/src/styles.css | 4 + setup.py | 2 +- 13 files changed, 369 insertions(+), 133 deletions(-) diff --git a/chainforge/react-server/craco.config.js b/chainforge/react-server/craco.config.js index 72e996b17..d951d95b0 100644 --- a/chainforge/react-server/craco.config.js +++ b/chainforge/react-server/craco.config.js @@ -9,6 +9,14 @@ module.exports = { }, webpack: { configure: { + // WebLLM currently publishes sourcemap references to TS sources that are + // not included in the npm package. Ignore only those warnings. + ignoreWarnings: [ + { + module: /@mlc-ai\/web-llm/, + message: /Failed to parse source map/, + }, + ], resolve: { fallback: { process: require.resolve("process/browser"), diff --git a/chainforge/react-server/package-lock.json b/chainforge/react-server/package-lock.json index 57ab72576..45a703745 100644 --- a/chainforge/react-server/package-lock.json +++ b/chainforge/react-server/package-lock.json @@ -22,6 +22,7 @@ "@mantine/form": "^6.0.11", "@mantine/prism": "^6.0.15", "@mirai73/bedrock-fm": "^0.4.10", + "@mlc-ai/web-llm": "^0.2.84", "@reactflow/background": "^11.2.0", "@reactflow/controls": "^11.1.11", "@reactflow/core": "^11.7.0", @@ -4740,6 +4741,15 @@ "@aws-sdk/client-bedrock-runtime": "^3.507.0" } }, + "node_modules/@mlc-ai/web-llm": { + "version": "0.2.84", + "resolved": "https://registry.npmjs.org/@mlc-ai/web-llm/-/web-llm-0.2.84.tgz", + "integrity": "sha512-hrOWzK4/nGNmgoRKT8pgVmZZ2oEPpbblIWQOwpqNyvK2dysHw3KVB1gNJOuRcQfKOPhucEhX1NJzXzgMDnwSCQ==", + "license": "Apache-2.0", + "dependencies": { + "loglevel": "^1.9.1" + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -17488,6 +17498,19 @@ "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", diff --git a/chainforge/react-server/package.json b/chainforge/react-server/package.json index d47badb9f..16d61faf5 100644 --- a/chainforge/react-server/package.json +++ b/chainforge/react-server/package.json @@ -20,6 +20,7 @@ "@mantine/form": "^6.0.11", "@mantine/prism": "^6.0.15", "@mirai73/bedrock-fm": "^0.4.10", + "@mlc-ai/web-llm": "^0.2.84", "@reactflow/background": "^11.2.0", "@reactflow/controls": "^11.1.11", "@reactflow/core": "^11.7.0", diff --git a/chainforge/react-server/src/App.tsx b/chainforge/react-server/src/App.tsx index 7d4240340..ad72926ad 100644 --- a/chainforge/react-server/src/App.tsx +++ b/chainforge/react-server/src/App.tsx @@ -67,6 +67,7 @@ import { getDefaultModelFormData, getDefaultModelSettings, } from "./ModelSettingSchemas"; +import { NativeLLM } from "./backend/models"; import { v4 as uuid } from "uuid"; import axios from "axios"; import LZString from "lz-string"; @@ -169,37 +170,19 @@ const selector = (state: StoreHandles) => ({ // The initial LLM to use when new flows are created, or upon first load const INITIAL_LLM = () => { - if (!IS_RUNNING_LOCALLY) { - // Prefer HF if running on server, as it's free. - const falcon7b = { - key: uuid(), - name: "Mistral-7B", - emoji: "🤗", - model: "mistralai/Mistral-7B-Instruct-v0.1", - base_model: "hf", - temp: 1.0, - settings: getDefaultModelSettings("hf"), - formData: getDefaultModelFormData("hf"), - } satisfies LLMSpec; - falcon7b.formData.shortname = falcon7b.name; - falcon7b.formData.model = falcon7b.model; - return falcon7b; - } else { - // Prefer OpenAI for majority of local users. - const chatgpt = { - key: uuid(), - name: "GPT-4o-mini", - emoji: "🤖", - model: "gpt-4o-mini", - base_model: "gpt-4", - temp: 1.0, - settings: getDefaultModelSettings("gpt-4"), - formData: getDefaultModelFormData("gpt-4"), - } satisfies LLMSpec; - chatgpt.formData.shortname = chatgpt.name; - chatgpt.formData.model = chatgpt.model; - return chatgpt; - } + const qwenWebLLM = { + key: uuid(), + name: "Qwen2.5 0.5B", + emoji: "🌐", + model: NativeLLM.WebLLM_Qwen2_5_0_5B, + base_model: "webllm", + temp: 0.7, + settings: getDefaultModelSettings("webllm"), + formData: getDefaultModelFormData("webllm"), + } satisfies LLMSpec; + qwenWebLLM.formData.shortname = qwenWebLLM.name; + qwenWebLLM.formData.model = qwenWebLLM.model; + return qwenWebLLM; }; const nodeTypes = { diff --git a/chainforge/react-server/src/BaseNode.tsx b/chainforge/react-server/src/BaseNode.tsx index 0290d6d07..f1984e373 100644 --- a/chainforge/react-server/src/BaseNode.tsx +++ b/chainforge/react-server/src/BaseNode.tsx @@ -152,10 +152,13 @@ export const BaseNode: React.FC = ({ {icon} {text} ))} - { - handleDuplicateNode(); - setContextMenuOpened(false); - }}> + { + handleDuplicateNode(); + setContextMenuOpened(false); + }} + >  Duplicate Node @@ -163,9 +166,9 @@ export const BaseNode: React.FC = ({ { - setFavoriteNameModalOpen(true); - setContextMenuOpened(false); - }} + setFavoriteNameModalOpen(true); + setContextMenuOpened(false); + }} > = ({  Favorite Node )} - { - handleRemoveNode(); - setContextMenuOpened(false); - }}> + { + handleRemoveNode(); + setContextMenuOpened(false); + }} + >  Delete Node diff --git a/chainforge/react-server/src/LLMListComponent.tsx b/chainforge/react-server/src/LLMListComponent.tsx index e54cd94e7..24bd8cd99 100644 --- a/chainforge/react-server/src/LLMListComponent.tsx +++ b/chainforge/react-server/src/LLMListComponent.tsx @@ -24,6 +24,7 @@ import ModelSettingsModal, { ModelSettingsModalRef, } from "./ModelSettingsModal"; import { getDefaultModelSettings } from "./ModelSettingSchemas"; +import { NativeLLM } from "./backend/models"; import useStore, { initLLMProviders, initLLMProviderMenu } from "./store"; import { Dict, JSONCompatible, LLMGroup, LLMSpec } from "./backend/typing"; import { ContextMenuItemOptions } from "mantine-contextmenu/dist/types"; @@ -31,9 +32,9 @@ import { deepcopy, ensureUniqueName } from "./backend/utils"; import NestedMenu, { NestedMenuItemProps } from "./NestedMenu"; // The LLM(s) to include by default on a PromptNode whenever one is created. -// Defaults to a cheap non-reasoning OpenAI model. +// Defaults to an in-browser Qwen 2.5 model. const DEFAULT_INIT_LLMS = [ - initLLMProviders.find((m) => m.model === "gpt-4o-mini")!, + initLLMProviders.find((m) => m.model === NativeLLM.WebLLM_Qwen2_5_0_5B)!, ]; // Helper funcs @@ -499,7 +500,6 @@ export const LLMListContainer = forwardRef< items={menuItems} button={(closeMenu) => (