From d5d4e40f995fb44357fd50e315ff6e2581465dae Mon Sep 17 00:00:00 2001 From: Octopus Date: Sat, 14 Mar 2026 10:51:15 -0500 Subject: [PATCH] feat: add MiniMax provider support - Add MiniMax chat model provider with OpenAI-compatible API - Support MiniMax-M2.5 and MiniMax-M2.5-highspeed models (204K context) - Add MiniMaxIcon to icons component - Register provider in types, registry, utils, and models - Clamp temperature to (0, 1] range per MiniMax API constraints - Add unit tests for provider metadata and request execution --- apps/sim/components/icons.tsx | 15 + apps/sim/providers/minimax/index.test.ts | 153 ++++++ apps/sim/providers/minimax/index.ts | 564 +++++++++++++++++++++++ apps/sim/providers/minimax/utils.ts | 14 + apps/sim/providers/models.ts | 42 ++ apps/sim/providers/registry.ts | 2 + apps/sim/providers/types.ts | 1 + apps/sim/providers/utils.ts | 1 + 8 files changed, 792 insertions(+) create mode 100644 apps/sim/providers/minimax/index.test.ts create mode 100644 apps/sim/providers/minimax/index.ts create mode 100644 apps/sim/providers/minimax/utils.ts diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 0db577837dc..ca675b5eeb7 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -2783,6 +2783,21 @@ export const GroqIcon = (props: SVGProps) => ( ) +export const MiniMaxIcon = (props: SVGProps) => ( + + MiniMax + + + +) + export const DeepseekIcon = (props: SVGProps) => ( DeepSeek diff --git a/apps/sim/providers/minimax/index.test.ts b/apps/sim/providers/minimax/index.test.ts new file mode 100644 index 00000000000..4b9faf35d85 --- /dev/null +++ b/apps/sim/providers/minimax/index.test.ts @@ -0,0 +1,153 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockCreate, MockOpenAI } = vi.hoisted(() => { + const mockCreate = vi.fn() + const MockOpenAI = vi.fn().mockImplementation(() => ({ + chat: { + completions: { + create: mockCreate, + }, + }, + })) + return { mockCreate, MockOpenAI } +}) + +vi.mock('openai', () => ({ + default: MockOpenAI, +})) + +vi.mock('@/tools', () => ({ + executeTool: vi.fn(), +})) + +import { minimaxProvider } from '@/providers/minimax' + +describe('MiniMax Provider', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('provider metadata', () => { + it('has correct id and name', () => { + expect(minimaxProvider.id).toBe('minimax') + expect(minimaxProvider.name).toBe('MiniMax') + }) + + it('has correct version', () => { + expect(minimaxProvider.version).toBe('1.0.0') + }) + + it('has models defined', () => { + expect(minimaxProvider.models).toBeDefined() + expect(minimaxProvider.models.length).toBeGreaterThan(0) + }) + + it('has default model set', () => { + expect(minimaxProvider.defaultModel).toBe('MiniMax-M2.5') + }) + }) + + describe('executeRequest', () => { + it('throws when API key is missing', async () => { + await expect( + minimaxProvider.executeRequest({ + model: 'MiniMax-M2.5', + messages: [{ role: 'user', content: 'Hello' }], + }) + ).rejects.toThrow('API key is required for MiniMax') + }) + + it('creates OpenAI client with correct base URL', async () => { + mockCreate.mockResolvedValue({ + choices: [{ message: { content: 'Hello!', tool_calls: undefined } }], + usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }, + }) + + await minimaxProvider.executeRequest({ + model: 'MiniMax-M2.5', + apiKey: 'test-key', + messages: [{ role: 'user', content: 'Hello' }], + }) + + expect(MockOpenAI).toHaveBeenCalledWith({ + apiKey: 'test-key', + baseURL: 'https://api.minimax.io/v1', + }) + }) + + it('returns content from response', async () => { + mockCreate.mockResolvedValue({ + choices: [{ message: { content: 'Hello from MiniMax!', tool_calls: undefined } }], + usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }, + }) + + const result = await minimaxProvider.executeRequest({ + model: 'MiniMax-M2.5', + apiKey: 'test-key', + messages: [{ role: 'user', content: 'Hello' }], + }) + + expect(result).toHaveProperty('content', 'Hello from MiniMax!') + }) + + it('clamps temperature to valid range', async () => { + mockCreate.mockResolvedValue({ + choices: [{ message: { content: 'ok', tool_calls: undefined } }], + usage: { prompt_tokens: 5, completion_tokens: 2, total_tokens: 7 }, + }) + + await minimaxProvider.executeRequest({ + model: 'MiniMax-M2.5', + apiKey: 'test-key', + temperature: 0, + messages: [{ role: 'user', content: 'Hi' }], + }) + + const callArgs = mockCreate.mock.calls[0][0] + expect(callArgs.temperature).toBeGreaterThan(0) + expect(callArgs.temperature).toBeLessThanOrEqual(1) + }) + + it('includes system prompt when provided', async () => { + mockCreate.mockResolvedValue({ + choices: [{ message: { content: 'ok', tool_calls: undefined } }], + usage: { prompt_tokens: 10, completion_tokens: 2, total_tokens: 12 }, + }) + + await minimaxProvider.executeRequest({ + model: 'MiniMax-M2.5', + apiKey: 'test-key', + systemPrompt: 'You are a helpful assistant', + messages: [{ role: 'user', content: 'Hi' }], + }) + + const callArgs = mockCreate.mock.calls[0][0] + expect(callArgs.messages[0]).toEqual({ + role: 'system', + content: 'You are a helpful assistant', + }) + }) + + it('returns token usage information', async () => { + mockCreate.mockResolvedValue({ + choices: [{ message: { content: 'Hello!', tool_calls: undefined } }], + usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }, + }) + + const result = await minimaxProvider.executeRequest({ + model: 'MiniMax-M2.5', + apiKey: 'test-key', + messages: [{ role: 'user', content: 'Hello' }], + }) + + expect(result).toHaveProperty('tokens') + const response = result as any + expect(response.tokens.input).toBe(10) + expect(response.tokens.output).toBe(5) + expect(response.tokens.total).toBe(15) + }) + }) +}) diff --git a/apps/sim/providers/minimax/index.ts b/apps/sim/providers/minimax/index.ts new file mode 100644 index 00000000000..07d4aa7ba6b --- /dev/null +++ b/apps/sim/providers/minimax/index.ts @@ -0,0 +1,564 @@ +import { createLogger } from '@sim/logger' +import OpenAI from 'openai' +import type { StreamingExecution } from '@/executor/types' +import { MAX_TOOL_ITERATIONS } from '@/providers' +import { createReadableStreamFromMiniMaxStream } from '@/providers/minimax/utils' +import { getProviderDefaultModel, getProviderModels } from '@/providers/models' +import type { + ProviderConfig, + ProviderRequest, + ProviderResponse, + TimeSegment, +} from '@/providers/types' +import { ProviderError } from '@/providers/types' +import { + calculateCost, + prepareToolExecution, + prepareToolsWithUsageControl, + trackForcedToolUsage, +} from '@/providers/utils' +import { executeTool } from '@/tools' + +const logger = createLogger('MiniMaxProvider') + +export const minimaxProvider: ProviderConfig = { + id: 'minimax', + name: 'MiniMax', + description: "MiniMax's chat models", + version: '1.0.0', + models: getProviderModels('minimax'), + defaultModel: getProviderDefaultModel('minimax'), + + executeRequest: async ( + request: ProviderRequest + ): Promise => { + if (!request.apiKey) { + throw new Error('API key is required for MiniMax') + } + + const providerStartTime = Date.now() + const providerStartTimeISO = new Date(providerStartTime).toISOString() + + try { + const minimax = new OpenAI({ + apiKey: request.apiKey, + baseURL: 'https://api.minimax.io/v1', + }) + + const allMessages = [] + + if (request.systemPrompt) { + allMessages.push({ + role: 'system', + content: request.systemPrompt, + }) + } + + if (request.context) { + allMessages.push({ + role: 'user', + content: request.context, + }) + } + + if (request.messages) { + allMessages.push(...request.messages) + } + + const tools = request.tools?.length + ? request.tools.map((tool) => ({ + type: 'function', + function: { + name: tool.id, + description: tool.description, + parameters: tool.parameters, + }, + })) + : undefined + + const payload: any = { + model: request.model, + messages: allMessages, + } + + // MiniMax temperature must be in (0.0, 1.0] - clamp accordingly + if (request.temperature !== undefined) { + payload.temperature = Math.min(Math.max(request.temperature, 0.01), 1.0) + } + if (request.maxTokens != null) payload.max_tokens = request.maxTokens + + let preparedTools: ReturnType | null = null + + if (tools?.length) { + preparedTools = prepareToolsWithUsageControl(tools, request.tools, logger, 'minimax') + const { tools: filteredTools, toolChoice } = preparedTools + + if (filteredTools?.length && toolChoice) { + payload.tools = filteredTools + payload.tool_choice = toolChoice + + logger.info('MiniMax request configuration:', { + toolCount: filteredTools.length, + toolChoice: + typeof toolChoice === 'string' + ? toolChoice + : toolChoice.type === 'function' + ? `force:${toolChoice.function.name}` + : toolChoice.type === 'tool' + ? `force:${toolChoice.name}` + : toolChoice.type === 'any' + ? `force:${toolChoice.any?.name || 'unknown'}` + : 'unknown', + model: request.model, + }) + } + } + + if (request.stream && (!tools || tools.length === 0)) { + logger.info('Using streaming response for MiniMax request (no tools)') + + const streamResponse = await minimax.chat.completions.create( + { + ...payload, + stream: true, + }, + request.abortSignal ? { signal: request.abortSignal } : undefined + ) + + const streamingResult = { + stream: createReadableStreamFromMiniMaxStream( + streamResponse as any, + (content, usage) => { + streamingResult.execution.output.content = content + streamingResult.execution.output.tokens = { + input: usage.prompt_tokens, + output: usage.completion_tokens, + total: usage.total_tokens, + } + + const costResult = calculateCost( + request.model, + usage.prompt_tokens, + usage.completion_tokens + ) + streamingResult.execution.output.cost = { + input: costResult.input, + output: costResult.output, + total: costResult.total, + } + } + ), + execution: { + success: true, + output: { + content: '', + model: request.model, + tokens: { input: 0, output: 0, total: 0 }, + toolCalls: undefined, + providerTiming: { + startTime: providerStartTimeISO, + endTime: new Date().toISOString(), + duration: Date.now() - providerStartTime, + timeSegments: [ + { + type: 'model', + name: 'Streaming response', + startTime: providerStartTime, + endTime: Date.now(), + duration: Date.now() - providerStartTime, + }, + ], + }, + cost: { input: 0, output: 0, total: 0 }, + }, + logs: [], + metadata: { + startTime: providerStartTimeISO, + endTime: new Date().toISOString(), + duration: Date.now() - providerStartTime, + }, + isStreaming: true, + }, + } + + return streamingResult as StreamingExecution + } + + const initialCallTime = Date.now() + const originalToolChoice = payload.tool_choice + const forcedTools = preparedTools?.forcedTools || [] + let usedForcedTools: string[] = [] + + let currentResponse = await minimax.chat.completions.create( + payload, + request.abortSignal ? { signal: request.abortSignal } : undefined + ) + const firstResponseTime = Date.now() - initialCallTime + + let content = currentResponse.choices[0]?.message?.content || '' + + if (content) { + content = content.replace(/```json\n?|\n?```/g, '') + content = content.trim() + } + + const tokens = { + input: currentResponse.usage?.prompt_tokens || 0, + output: currentResponse.usage?.completion_tokens || 0, + total: currentResponse.usage?.total_tokens || 0, + } + const toolCalls = [] + const toolResults = [] + const currentMessages = [...allMessages] + let iterationCount = 0 + let hasUsedForcedTool = false + let modelTime = firstResponseTime + let toolsTime = 0 + + const timeSegments: TimeSegment[] = [ + { + type: 'model', + name: 'Initial response', + startTime: initialCallTime, + endTime: initialCallTime + firstResponseTime, + duration: firstResponseTime, + }, + ] + + if ( + typeof originalToolChoice === 'object' && + currentResponse.choices[0]?.message?.tool_calls + ) { + const toolCallsResponse = currentResponse.choices[0].message.tool_calls + const result = trackForcedToolUsage( + toolCallsResponse, + originalToolChoice, + logger, + 'minimax', + forcedTools, + usedForcedTools + ) + hasUsedForcedTool = result.hasUsedForcedTool + usedForcedTools = result.usedForcedTools + } + + try { + while (iterationCount < MAX_TOOL_ITERATIONS) { + if (currentResponse.choices[0]?.message?.content) { + content = currentResponse.choices[0].message.content + } + + const toolCallsInResponse = currentResponse.choices[0]?.message?.tool_calls + if (!toolCallsInResponse || toolCallsInResponse.length === 0) { + break + } + + const toolsStartTime = Date.now() + + const toolExecutionPromises = toolCallsInResponse.map(async (toolCall) => { + const toolCallStartTime = Date.now() + const toolName = toolCall.function.name + + try { + const toolArgs = JSON.parse(toolCall.function.arguments) + const tool = request.tools?.find((t) => t.id === toolName) + + if (!tool) return null + + const { toolParams, executionParams } = prepareToolExecution(tool, toolArgs, request) + const result = await executeTool(toolName, executionParams) + const toolCallEndTime = Date.now() + + return { + toolCall, + toolName, + toolParams, + result, + startTime: toolCallStartTime, + endTime: toolCallEndTime, + duration: toolCallEndTime - toolCallStartTime, + } + } catch (error) { + const toolCallEndTime = Date.now() + logger.error('Error processing tool call:', { error, toolName }) + + return { + toolCall, + toolName, + toolParams: {}, + result: { + success: false, + output: undefined, + error: error instanceof Error ? error.message : 'Tool execution failed', + }, + startTime: toolCallStartTime, + endTime: toolCallEndTime, + duration: toolCallEndTime - toolCallStartTime, + } + } + }) + + const executionResults = await Promise.allSettled(toolExecutionPromises) + + currentMessages.push({ + role: 'assistant', + content: null, + tool_calls: toolCallsInResponse.map((tc) => ({ + id: tc.id, + type: 'function', + function: { + name: tc.function.name, + arguments: tc.function.arguments, + }, + })), + }) + + for (const settledResult of executionResults) { + if (settledResult.status === 'rejected' || !settledResult.value) continue + + const { toolCall, toolName, toolParams, result, startTime, endTime, duration } = + settledResult.value + + timeSegments.push({ + type: 'tool', + name: toolName, + startTime: startTime, + endTime: endTime, + duration: duration, + }) + + let resultContent: any + if (result.success) { + toolResults.push(result.output) + resultContent = result.output + } else { + resultContent = { + error: true, + message: result.error || 'Tool execution failed', + tool: toolName, + } + } + + toolCalls.push({ + name: toolName, + arguments: toolParams, + startTime: new Date(startTime).toISOString(), + endTime: new Date(endTime).toISOString(), + duration: duration, + result: resultContent, + success: result.success, + }) + + currentMessages.push({ + role: 'tool', + tool_call_id: toolCall.id, + content: JSON.stringify(resultContent), + }) + } + + const thisToolsTime = Date.now() - toolsStartTime + toolsTime += thisToolsTime + + const nextPayload = { + ...payload, + messages: currentMessages, + } + + if ( + typeof originalToolChoice === 'object' && + hasUsedForcedTool && + forcedTools.length > 0 + ) { + const remainingTools = forcedTools.filter((tool) => !usedForcedTools.includes(tool)) + + if (remainingTools.length > 0) { + nextPayload.tool_choice = { + type: 'function', + function: { name: remainingTools[0] }, + } + logger.info(`Forcing next tool: ${remainingTools[0]}`) + } else { + nextPayload.tool_choice = 'auto' + logger.info('All forced tools have been used, switching to auto tool_choice') + } + } + + const nextModelStartTime = Date.now() + currentResponse = await minimax.chat.completions.create( + nextPayload, + request.abortSignal ? { signal: request.abortSignal } : undefined + ) + + if ( + typeof nextPayload.tool_choice === 'object' && + currentResponse.choices[0]?.message?.tool_calls + ) { + const toolCallsResponse = currentResponse.choices[0].message.tool_calls + const result = trackForcedToolUsage( + toolCallsResponse, + nextPayload.tool_choice, + logger, + 'minimax', + forcedTools, + usedForcedTools + ) + hasUsedForcedTool = result.hasUsedForcedTool + usedForcedTools = result.usedForcedTools + } + + const nextModelEndTime = Date.now() + const thisModelTime = nextModelEndTime - nextModelStartTime + + timeSegments.push({ + type: 'model', + name: `Model response (iteration ${iterationCount + 1})`, + startTime: nextModelStartTime, + endTime: nextModelEndTime, + duration: thisModelTime, + }) + + modelTime += thisModelTime + + if (currentResponse.choices[0]?.message?.content) { + content = currentResponse.choices[0].message.content + content = content.replace(/```json\n?|\n?```/g, '') + content = content.trim() + } + + if (currentResponse.usage) { + tokens.input += currentResponse.usage.prompt_tokens || 0 + tokens.output += currentResponse.usage.completion_tokens || 0 + tokens.total += currentResponse.usage.total_tokens || 0 + } + + iterationCount++ + } + } catch (error) { + logger.error('Error in MiniMax request:', { error }) + } + + const providerEndTime = Date.now() + const providerEndTimeISO = new Date(providerEndTime).toISOString() + const totalDuration = providerEndTime - providerStartTime + + if (request.stream) { + logger.info('Using streaming for final MiniMax response after tool processing') + + const streamingPayload = { + ...payload, + messages: currentMessages, + tool_choice: 'auto', + stream: true, + } + + const streamResponse = await minimax.chat.completions.create( + streamingPayload, + request.abortSignal ? { signal: request.abortSignal } : undefined + ) + + const accumulatedCost = calculateCost(request.model, tokens.input, tokens.output) + + const streamingResult = { + stream: createReadableStreamFromMiniMaxStream( + streamResponse as any, + (content, usage) => { + streamingResult.execution.output.content = content + streamingResult.execution.output.tokens = { + input: tokens.input + usage.prompt_tokens, + output: tokens.output + usage.completion_tokens, + total: tokens.total + usage.total_tokens, + } + + const streamCost = calculateCost( + request.model, + usage.prompt_tokens, + usage.completion_tokens + ) + streamingResult.execution.output.cost = { + input: accumulatedCost.input + streamCost.input, + output: accumulatedCost.output + streamCost.output, + total: accumulatedCost.total + streamCost.total, + } + } + ), + execution: { + success: true, + output: { + content: '', + model: request.model, + tokens: { + input: tokens.input, + output: tokens.output, + total: tokens.total, + }, + toolCalls: + toolCalls.length > 0 + ? { + list: toolCalls, + count: toolCalls.length, + } + : undefined, + providerTiming: { + startTime: providerStartTimeISO, + endTime: new Date().toISOString(), + duration: Date.now() - providerStartTime, + modelTime: modelTime, + toolsTime: toolsTime, + firstResponseTime: firstResponseTime, + iterations: iterationCount + 1, + timeSegments: timeSegments, + }, + cost: { + input: accumulatedCost.input, + output: accumulatedCost.output, + total: accumulatedCost.total, + }, + }, + logs: [], + metadata: { + startTime: providerStartTimeISO, + endTime: new Date().toISOString(), + duration: Date.now() - providerStartTime, + }, + isStreaming: true, + }, + } + + return streamingResult as StreamingExecution + } + + return { + content, + model: request.model, + tokens, + toolCalls: toolCalls.length > 0 ? toolCalls : undefined, + toolResults: toolResults.length > 0 ? toolResults : undefined, + timing: { + startTime: providerStartTimeISO, + endTime: providerEndTimeISO, + duration: totalDuration, + modelTime: modelTime, + toolsTime: toolsTime, + firstResponseTime: firstResponseTime, + iterations: iterationCount + 1, + timeSegments: timeSegments, + }, + } + } catch (error) { + const providerEndTime = Date.now() + const providerEndTimeISO = new Date(providerEndTime).toISOString() + const totalDuration = providerEndTime - providerStartTime + + logger.error('Error in MiniMax request:', { + error, + duration: totalDuration, + }) + + throw new ProviderError(error instanceof Error ? error.message : String(error), { + startTime: providerStartTimeISO, + endTime: providerEndTimeISO, + duration: totalDuration, + }) + } + }, +} diff --git a/apps/sim/providers/minimax/utils.ts b/apps/sim/providers/minimax/utils.ts new file mode 100644 index 00000000000..2b8b946bb5e --- /dev/null +++ b/apps/sim/providers/minimax/utils.ts @@ -0,0 +1,14 @@ +import type { ChatCompletionChunk } from 'openai/resources/chat/completions' +import type { CompletionUsage } from 'openai/resources/completions' +import { createOpenAICompatibleStream } from '@/providers/utils' + +/** + * Creates a ReadableStream from a MiniMax streaming response. + * Uses the shared OpenAI-compatible streaming utility. + */ +export function createReadableStreamFromMiniMaxStream( + minimaxStream: AsyncIterable, + onComplete?: (content: string, usage: CompletionUsage) => void +): ReadableStream { + return createOpenAICompatibleStream(minimaxStream, 'MiniMax', onComplete) +} diff --git a/apps/sim/providers/models.ts b/apps/sim/providers/models.ts index 37f973198ce..11cc10ca928 100644 --- a/apps/sim/providers/models.ts +++ b/apps/sim/providers/models.ts @@ -16,6 +16,7 @@ import { DeepseekIcon, GeminiIcon, GroqIcon, + MiniMaxIcon, MistralIcon, OllamaIcon, OpenAIIcon, @@ -1185,6 +1186,47 @@ export const PROVIDER_DEFINITIONS: Record = { }, ], }, + minimax: { + id: 'minimax', + name: 'MiniMax', + description: "MiniMax's chat models", + defaultModel: 'MiniMax-M2.5', + modelPatterns: [/^minimax/i], + icon: MiniMaxIcon, + capabilities: { + toolUsageControl: true, + }, + models: [ + { + id: 'MiniMax-M2.5', + pricing: { + input: 0.3, + cachedInput: 0.03, + output: 1.2, + updatedAt: '2025-06-01', + }, + capabilities: { + temperature: { min: 0.01, max: 1 }, + maxOutputTokens: 192000, + }, + contextWindow: 204800, + }, + { + id: 'MiniMax-M2.5-highspeed', + pricing: { + input: 0.6, + cachedInput: 0.03, + output: 2.4, + updatedAt: '2025-06-01', + }, + capabilities: { + temperature: { min: 0.01, max: 1 }, + maxOutputTokens: 192000, + }, + contextWindow: 204800, + }, + ], + }, xai: { id: 'xai', name: 'xAI', diff --git a/apps/sim/providers/registry.ts b/apps/sim/providers/registry.ts index 3f7be20c947..6d21d40df8d 100644 --- a/apps/sim/providers/registry.ts +++ b/apps/sim/providers/registry.ts @@ -7,6 +7,7 @@ import { cerebrasProvider } from '@/providers/cerebras' import { deepseekProvider } from '@/providers/deepseek' import { googleProvider } from '@/providers/google' import { groqProvider } from '@/providers/groq' +import { minimaxProvider } from '@/providers/minimax' import { mistralProvider } from '@/providers/mistral' import { ollamaProvider } from '@/providers/ollama' import { openaiProvider } from '@/providers/openai' @@ -28,6 +29,7 @@ const providerRegistry: Record = { xai: xAIProvider, cerebras: cerebrasProvider, groq: groqProvider, + minimax: minimaxProvider, vllm: vllmProvider, mistral: mistralProvider, 'azure-openai': azureOpenAIProvider, diff --git a/apps/sim/providers/types.ts b/apps/sim/providers/types.ts index af5362c3c75..c7f1827273a 100644 --- a/apps/sim/providers/types.ts +++ b/apps/sim/providers/types.ts @@ -12,6 +12,7 @@ export type ProviderId = | 'cerebras' | 'groq' | 'mistral' + | 'minimax' | 'ollama' | 'openrouter' | 'vllm' diff --git a/apps/sim/providers/utils.ts b/apps/sim/providers/utils.ts index 82ca33e1070..a8648907410 100644 --- a/apps/sim/providers/utils.ts +++ b/apps/sim/providers/utils.ts @@ -144,6 +144,7 @@ export const providers: Record = { cerebras: buildProviderMetadata('cerebras'), groq: buildProviderMetadata('groq'), mistral: buildProviderMetadata('mistral'), + minimax: buildProviderMetadata('minimax'), bedrock: buildProviderMetadata('bedrock'), openrouter: buildProviderMetadata('openrouter'), }