diff --git a/packages/browseros-agent/apps/app/lib/llm-providers/providerIcons.tsx b/packages/browseros-agent/apps/app/lib/llm-providers/providerIcons.tsx index 4c1feb3eb5..dd39ab9dd6 100644 --- a/packages/browseros-agent/apps/app/lib/llm-providers/providerIcons.tsx +++ b/packages/browseros-agent/apps/app/lib/llm-providers/providerIcons.tsx @@ -5,6 +5,7 @@ import { Gemini, Kimi, LmStudio, + Minimax, Ollama, OpenAI, OpenRouter, @@ -33,6 +34,7 @@ const providerIconMap: Record = { bedrock: Bedrock, browseros: null, moonshot: Kimi, + minimax: Minimax, 'chatgpt-pro': OpenAI, 'github-copilot': Github, 'qwen-code': Qwen, diff --git a/packages/browseros-agent/apps/app/lib/llm-providers/providerTemplates.test.ts b/packages/browseros-agent/apps/app/lib/llm-providers/providerTemplates.test.ts index b03ad1df9f..629cc1230e 100644 --- a/packages/browseros-agent/apps/app/lib/llm-providers/providerTemplates.test.ts +++ b/packages/browseros-agent/apps/app/lib/llm-providers/providerTemplates.test.ts @@ -13,4 +13,19 @@ describe('providerTemplates', () => { contextWindow: 1050000, }) }) + + it('defaults MiniMax to M3 on the international endpoint with a 1M context', () => { + const template = providerTemplates.find( + (provider) => provider.id === 'minimax', + ) + + expect(template).toMatchObject({ + name: 'MiniMax', + defaultBaseUrl: 'https://api.minimax.io/v1', + defaultModelId: 'MiniMax-M3', + contextWindow: 1000000, + setupGuideUrl: + 'https://platform.minimax.io/docs/api-reference/api-overview', + }) + }) }) diff --git a/packages/browseros-agent/apps/app/lib/llm-providers/providerTemplates.ts b/packages/browseros-agent/apps/app/lib/llm-providers/providerTemplates.ts index 7d14defafb..2e8aa0f625 100644 --- a/packages/browseros-agent/apps/app/lib/llm-providers/providerTemplates.ts +++ b/packages/browseros-agent/apps/app/lib/llm-providers/providerTemplates.ts @@ -91,6 +91,16 @@ export const providerTemplates: ProviderTemplate[] = [ apiKeyUrl: 'https://platform.moonshot.ai/console/api-keys', setupGuideUrl: 'https://platform.moonshot.ai/console/api-keys', }, + { + id: 'minimax', + name: 'MiniMax', + defaultBaseUrl: 'https://api.minimax.io/v1', + defaultModelId: 'MiniMax-M3', + supportsImages: true, + contextWindow: 1000000, + setupGuideUrl: + 'https://platform.minimax.io/docs/api-reference/api-overview', + }, enrichTemplate('openai', { defaultModelId: 'gpt-5', apiKeyUrl: 'https://platform.openai.com/api-keys', @@ -163,6 +173,7 @@ export const providerTypeOptions: { value: ProviderType; label: string }[] = [ { value: 'codex', label: 'Codex' }, { value: 'claude-code', label: 'Claude Code' }, { value: 'moonshot', label: 'Moonshot AI' }, + { value: 'minimax', label: 'MiniMax' }, { value: 'anthropic', label: 'Anthropic' }, { value: 'openai', label: 'OpenAI' }, { value: 'openai-compatible', label: 'OpenAI Compatible' }, @@ -198,6 +209,7 @@ const DEFAULT_BASE_URLS: Record = { 'claude-code': '', 'acp-custom': '', moonshot: 'https://api.moonshot.ai/v1', + minimax: 'https://api.minimax.io/v1', anthropic: 'https://api.anthropic.com/v1', openai: 'https://api.openai.com/v1', 'openai-compatible': '', diff --git a/packages/browseros-agent/apps/app/lib/llm-providers/types.ts b/packages/browseros-agent/apps/app/lib/llm-providers/types.ts index d764c17ced..ba96728e73 100644 --- a/packages/browseros-agent/apps/app/lib/llm-providers/types.ts +++ b/packages/browseros-agent/apps/app/lib/llm-providers/types.ts @@ -11,6 +11,7 @@ export type ProviderType = | 'bedrock' | 'browseros' | 'moonshot' + | 'minimax' | 'chatgpt-pro' | 'github-copilot' | 'qwen-code' diff --git a/packages/browseros-agent/apps/app/screens/ai-settings/models.test.ts b/packages/browseros-agent/apps/app/screens/ai-settings/models.test.ts index fc239dad7a..84fba4f64e 100644 --- a/packages/browseros-agent/apps/app/screens/ai-settings/models.test.ts +++ b/packages/browseros-agent/apps/app/screens/ai-settings/models.test.ts @@ -30,3 +30,27 @@ describe('ChatGPT models', () => { expect(getModelContextLength('chatgpt-pro', 'gpt-5.1')).toBe(400000) }) }) + +describe('MiniMax models', () => { + it('offers current models with their supported capabilities', () => { + const models = getModelsForProvider('minimax') + + expect(models).toEqual([ + { + modelId: 'MiniMax-M3', + contextLength: 1000000, + supportsImages: true, + supportsReasoning: true, + supportsToolCall: true, + }, + { + modelId: 'MiniMax-M2.7', + contextLength: 204800, + supportsReasoning: true, + supportsToolCall: true, + }, + ]) + expect(getModelContextLength('minimax', 'MiniMax-M3')).toBe(1000000) + expect(getModelContextLength('minimax', 'MiniMax-M2.7')).toBe(204800) + }) +}) diff --git a/packages/browseros-agent/apps/app/screens/ai-settings/models.ts b/packages/browseros-agent/apps/app/screens/ai-settings/models.ts index ea073a8bc0..0b6c9a539e 100644 --- a/packages/browseros-agent/apps/app/screens/ai-settings/models.ts +++ b/packages/browseros-agent/apps/app/screens/ai-settings/models.ts @@ -36,6 +36,21 @@ const CUSTOM_PROVIDER_MODELS: Partial> = { { modelId: 'qwen3-coder-flash', contextLength: 1000000 }, { modelId: 'qwen3.5-plus', contextLength: 1000000 }, ], + minimax: [ + { + modelId: 'MiniMax-M3', + contextLength: 1000000, + supportsImages: true, + supportsReasoning: true, + supportsToolCall: true, + }, + { + modelId: 'MiniMax-M2.7', + contextLength: 204800, + supportsReasoning: true, + supportsToolCall: true, + }, + ], } function fromModelsDevModel(m: ModelsDevModel): ModelInfo { diff --git a/packages/browseros-agent/apps/app/screens/ai-settings/provider-form-schema.ts b/packages/browseros-agent/apps/app/screens/ai-settings/provider-form-schema.ts index 60d0b8adf9..dda3fc4c53 100644 --- a/packages/browseros-agent/apps/app/screens/ai-settings/provider-form-schema.ts +++ b/packages/browseros-agent/apps/app/screens/ai-settings/provider-form-schema.ts @@ -3,6 +3,7 @@ import { isLocalRuntimeProviderType } from '../../lib/llm-providers/provider-run const providerTypeEnum = z.enum([ 'moonshot', + 'minimax', 'anthropic', 'openai', 'openai-compatible', diff --git a/packages/browseros-agent/apps/server/src/agent/provider-factory.ts b/packages/browseros-agent/apps/server/src/agent/provider-factory.ts index e2cd2ffe99..f4bce4f3a2 100644 --- a/packages/browseros-agent/apps/server/src/agent/provider-factory.ts +++ b/packages/browseros-agent/apps/server/src/agent/provider-factory.ts @@ -20,6 +20,7 @@ import { import { resolveAcpSpawnCommand } from '../lib/agents/host-acp/launcher' import { getBrowserosDir } from '../lib/browseros-dir' import { createBrowserOSFetch } from '../lib/browseros-fetch' +import { createMinimaxProvider } from '../lib/clients/llm/minimax-provider' import { createMockBrowserOSLanguageModel, shouldUseMockBrowserOSLLM, @@ -413,6 +414,12 @@ function createMoonshotFactory( }) } +function createMinimaxFactory( + config: ResolvedAgentConfig, +): (modelId: string) => unknown { + return createMinimaxProvider(config) +} + function createQwenCodeFactory( config: ResolvedAgentConfig, ): (modelId: string) => unknown { @@ -459,6 +466,7 @@ const PROVIDER_FACTORIES: Record = { [LLM_PROVIDERS.BROWSEROS]: createBrowserOSFactory, [LLM_PROVIDERS.OPENAI_COMPATIBLE]: createOpenAICompatibleFactory, [LLM_PROVIDERS.MOONSHOT]: createMoonshotFactory, + [LLM_PROVIDERS.MINIMAX]: createMinimaxFactory, [LLM_PROVIDERS.CHATGPT_PRO]: createChatGPTProFactory, [LLM_PROVIDERS.GITHUB_COPILOT]: createGitHubCopilotFactory, [LLM_PROVIDERS.QWEN_CODE]: createQwenCodeFactory, diff --git a/packages/browseros-agent/apps/server/src/lib/clients/llm/minimax-provider.ts b/packages/browseros-agent/apps/server/src/lib/clients/llm/minimax-provider.ts new file mode 100644 index 0000000000..5052382fdf --- /dev/null +++ b/packages/browseros-agent/apps/server/src/lib/clients/llm/minimax-provider.ts @@ -0,0 +1,36 @@ +/** + * @license + * Copyright 2025 BrowserOS + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { createAnthropic } from '@ai-sdk/anthropic' +import { createOpenAICompatible } from '@ai-sdk/openai-compatible' +import type { LanguageModel } from 'ai' + +interface MinimaxProviderConfig { + apiKey?: string + baseUrl?: string +} + +export function createMinimaxProvider( + config: MinimaxProviderConfig, +): (modelId: string) => LanguageModel { + if (!config.baseUrl) throw new Error('MiniMax provider requires baseUrl') + if (!config.apiKey) throw new Error('MiniMax provider requires apiKey') + + const baseUrl = config.baseUrl.replace(/\/+$/, '') + if (baseUrl.endsWith('/anthropic')) { + return createAnthropic({ + name: 'minimax', + baseURL: `${baseUrl}/v1`, + apiKey: config.apiKey, + }) + } + + return createOpenAICompatible({ + name: 'minimax', + baseURL: baseUrl, + apiKey: config.apiKey, + }) +} diff --git a/packages/browseros-agent/apps/server/src/lib/clients/llm/provider.ts b/packages/browseros-agent/apps/server/src/lib/clients/llm/provider.ts index a3d8a8af3a..e3a61828a8 100644 --- a/packages/browseros-agent/apps/server/src/lib/clients/llm/provider.ts +++ b/packages/browseros-agent/apps/server/src/lib/clients/llm/provider.ts @@ -21,6 +21,7 @@ import { logger } from '../../logger' import { createOpenRouterCompatibleFetch } from '../../openrouter-fetch' import { createCodexFetch } from '../oauth/codex-fetch' import { createCopilotFetch } from '../oauth/copilot-fetch' +import { createMinimaxProvider } from './minimax-provider' import { createMockBrowserOSLanguageModel, shouldUseMockBrowserOSLLM, @@ -152,6 +153,10 @@ function createMoonshotModel(config: ResolvedLLMConfig): LanguageModel { })(config.model) } +function createMinimaxModel(config: ResolvedLLMConfig): LanguageModel { + return createMinimaxProvider(config)(config.model) +} + function createQwenCodeModel(config: ResolvedLLMConfig): LanguageModel { if (!config.apiKey) throw new Error('Qwen Code requires OAuth authentication') return createOpenAICompatible({ @@ -192,6 +197,7 @@ const PROVIDER_FACTORIES: Record = { [LLM_PROVIDERS.BROWSEROS]: createBrowserOSModel, [LLM_PROVIDERS.OPENAI_COMPATIBLE]: createOpenAICompatibleModel, [LLM_PROVIDERS.MOONSHOT]: createMoonshotModel, + [LLM_PROVIDERS.MINIMAX]: createMinimaxModel, [LLM_PROVIDERS.CHATGPT_PRO]: createChatGPTProModel, [LLM_PROVIDERS.GITHUB_COPILOT]: createGitHubCopilotModel, [LLM_PROVIDERS.QWEN_CODE]: createQwenCodeModel, diff --git a/packages/browseros-agent/apps/server/tests/lib/clients/llm/provider.test.ts b/packages/browseros-agent/apps/server/tests/lib/clients/llm/provider.test.ts new file mode 100644 index 0000000000..f064044d37 --- /dev/null +++ b/packages/browseros-agent/apps/server/tests/lib/clients/llm/provider.test.ts @@ -0,0 +1,128 @@ +/** + * @license + * Copyright 2025 BrowserOS + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { afterEach, describe, expect, it } from 'bun:test' +import { LLM_PROVIDERS } from '@browseros/shared/schemas/llm' +import { generateText } from 'ai' +import { createLLMProvider } from '../../../../src/lib/clients/llm/provider' + +const originalFetch = globalThis.fetch + +afterEach(() => { + globalThis.fetch = originalFetch +}) + +function requestUrl(input: RequestInfo | URL): string { + if (typeof input === 'string') return input + if (input instanceof URL) return input.href + return input.url +} + +function responseBody(protocol: 'anthropic' | 'openai', model: string) { + if (protocol === 'anthropic') { + return { + id: 'msg_test', + type: 'message', + role: 'assistant', + content: [{ type: 'text', text: 'Hello' }], + model, + stop_reason: 'end_turn', + stop_sequence: null, + usage: { input_tokens: 1, output_tokens: 1 }, + } + } + + return { + id: 'chatcmpl_test', + object: 'chat.completion', + created: 0, + model, + choices: [ + { + index: 0, + message: { role: 'assistant', content: 'Hello' }, + finish_reason: 'stop', + }, + ], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + } +} + +const endpointCases = [ + { + name: 'global OpenAI', + baseUrl: 'https://api.minimax.io/v1', + requestUrl: 'https://api.minimax.io/v1/chat/completions', + protocol: 'openai', + model: 'MiniMax-M3', + }, + { + name: 'China OpenAI', + baseUrl: 'https://api.minimaxi.com/v1', + requestUrl: 'https://api.minimaxi.com/v1/chat/completions', + protocol: 'openai', + model: 'MiniMax-M2.7', + }, + { + name: 'global Anthropic', + baseUrl: 'https://api.minimax.io/anthropic', + requestUrl: 'https://api.minimax.io/anthropic/v1/messages', + protocol: 'anthropic', + model: 'MiniMax-M3', + }, + { + name: 'China Anthropic', + baseUrl: 'https://api.minimaxi.com/anthropic', + requestUrl: 'https://api.minimaxi.com/anthropic/v1/messages', + protocol: 'anthropic', + model: 'MiniMax-M2.7', + }, +] as const + +describe('createLLMProvider', () => { + it('creates an OpenAI-compatible model for MiniMax', () => { + const model = createLLMProvider({ + provider: LLM_PROVIDERS.MINIMAX, + model: 'MiniMax-M3', + apiKey: 'test-key', + baseUrl: 'https://api.minimax.io/v1', + }) + + expect(model.provider).toBe('minimax.chat') + expect(model.modelId).toBe('MiniMax-M3') + }) + + it('throws when the MiniMax provider is missing an apiKey', () => { + expect(() => + createLLMProvider({ + provider: LLM_PROVIDERS.MINIMAX, + model: 'MiniMax-M3', + baseUrl: 'https://api.minimax.io/v1', + }), + ).toThrow('MiniMax provider requires apiKey') + }) + + for (const endpoint of endpointCases) { + it(`sends requests to the ${endpoint.name} endpoint`, async () => { + let capturedUrl = '' + globalThis.fetch = (async (input: RequestInfo | URL) => { + capturedUrl = requestUrl(input) + return Response.json(responseBody(endpoint.protocol, endpoint.model)) + }) as typeof globalThis.fetch + + const model = createLLMProvider({ + provider: LLM_PROVIDERS.MINIMAX, + model: endpoint.model, + apiKey: 'test-key', + baseUrl: endpoint.baseUrl, + }) + const result = await generateText({ model, prompt: 'Hello' }) + + expect(result.text).toBe('Hello') + expect(capturedUrl).toBe(endpoint.requestUrl) + }) + } +}) diff --git a/packages/browseros-agent/packages/shared/src/schemas/llm.ts b/packages/browseros-agent/packages/shared/src/schemas/llm.ts index 3de65f6972..a79ecf1bd6 100644 --- a/packages/browseros-agent/packages/shared/src/schemas/llm.ts +++ b/packages/browseros-agent/packages/shared/src/schemas/llm.ts @@ -24,6 +24,7 @@ export const LLM_PROVIDERS = { BROWSEROS: 'browseros', OPENAI_COMPATIBLE: 'openai-compatible', MOONSHOT: 'moonshot', + MINIMAX: 'minimax', CHATGPT_PRO: 'chatgpt-pro', GITHUB_COPILOT: 'github-copilot', QWEN_CODE: 'qwen-code', @@ -49,6 +50,7 @@ export const LLMProviderSchema: z.ZodEnum< 'browseros', 'openai-compatible', 'moonshot', + 'minimax', 'chatgpt-pro', 'github-copilot', 'qwen-code', @@ -69,6 +71,7 @@ export const LLMProviderSchema: z.ZodEnum< LLM_PROVIDERS.BROWSEROS, LLM_PROVIDERS.OPENAI_COMPATIBLE, LLM_PROVIDERS.MOONSHOT, + LLM_PROVIDERS.MINIMAX, LLM_PROVIDERS.CHATGPT_PRO, LLM_PROVIDERS.GITHUB_COPILOT, LLM_PROVIDERS.QWEN_CODE,