diff --git a/__tests__/unit/services/providers/minimaxDiscovery.test.ts b/__tests__/unit/services/providers/minimaxDiscovery.test.ts new file mode 100644 index 000000000..165f16099 --- /dev/null +++ b/__tests__/unit/services/providers/minimaxDiscovery.test.ts @@ -0,0 +1,81 @@ +import { fetchModelsFromServer, testEndpointAndGetModels } from '../../../../src/stores/remoteServerHelpers'; +import type { RemoteServer } from '../../../../src/types'; + +jest.mock('../../../../src/utils/logger', () => ({ + __esModule: true, + default: { log: jest.fn(), warn: jest.fn(), error: jest.fn() }, +})); +jest.mock('../../../../src/services/offGridDesktopModels', () => ({ + readOffGridDesktopModelState: jest.fn(async () => null), +})); +jest.mock('../../../../src/services/httpClient', () => ({ + testEndpoint: jest.fn(async () => ({ success: true, latency: 1 })), + detectServerType: jest.fn(async () => null), +})); +jest.mock('../../../../src/stores/remoteModelCapabilities', () => ({ + isGenerativeModel: () => true, + fetchModelCapabilities: jest.fn(async () => ({ contextLength: 4096, supportsVision: false })), + RemoteAuthenticationError: class extends Error {}, +})); + +const directModels = [ + { id: 'MiniMax-M2.7', kind: 'chat' }, + { id: 'MiniMax-M3', kind: 'chat' }, + { id: 'MiniMax-M3.1-Flash', kind: 'chat' }, + { id: 'MiniMax-image', kind: 'image' }, + { id: 'MiniMax-speech', kind: 'speech' }, +]; + +describe('MiniMax discovery', () => { + const originalFetch = global.fetch; + afterEach(() => { global.fetch = originalFetch; }); + const serve = (data: unknown[]) => { + global.fetch = jest.fn(async () => ({ + ok: true, status: 200, json: async () => ({ object: 'list', data }), + })) as unknown as typeof fetch; + }; + + it.each(['https://api.minimax.io/v1', 'https://api.minimaxi.com/v1/'])( + 'refreshes ordinary text choices at %s without changing saved IDs or media', + async endpoint => { + serve(directModels); + const server = { id: 'saved', endpoint, mediaModels: { text: 'MiniMax-M2.7' } } as RemoteServer; + const models = await fetchModelsFromServer(server); + expect(models.map(model => model.id)).toEqual(['MiniMax-M3.1-Flash', 'MiniMax-M3']); + expect(models[0].capabilities).toMatchObject({ + maxContextLength: 1_000_000, supportsVision: true, + supportsToolCalling: true, thinkingLevelsOnly: true, acceptsThinkingKwarg: false, + }); + expect(server.mediaModels?.text).toBe('MiniMax-M2.7'); + const result = await testEndpointAndGetModels(endpoint); + expect(result.modelCatalog?.text?.map(model => model.id)).toEqual(['MiniMax-M3.1-Flash', 'MiniMax-M3']); + expect(result.mediaModels).toEqual({ text: 'MiniMax-M3.1-Flash', image: 'MiniMax-image', voice: 'MiniMax-speech' }); + }, + ); + + it('keeps gateway metadata, non-MiniMax models and media while ordering current text models', async () => { + const data = [ + { id: 'other/model', context_length: 12345 }, + { id: 'minimax/minimax-m2.7' }, + { id: 'minimax/minimax-m3', context_length: 524288 }, + { id: 'minimax/minimax-m3.1-flash:free', context_length: 1000000 }, + ].map(model => ({ ...model, architecture: { input_modalities: ['text', 'image'], output_modalities: ['text'] }, supported_parameters: ['tools'] })); + serve([...data, { id: 'minimax/media', architecture: { output_modalities: ['image'] } }]); + const endpoint = 'https://openrouter.ai/api/v1'; + const models = await fetchModelsFromServer({ id: 'gateway', endpoint } as RemoteServer); + expect(models.map(model => model.id)).toEqual(['other/model', 'minimax/minimax-m3.1-flash:free', 'minimax/minimax-m3']); + expect(models[2].capabilities.maxContextLength).toBe(524288); + expect(models[1].capabilities).toMatchObject({ + supportsThinking: true, thinkingLevelsOnly: true, acceptsThinkingKwarg: false, + }); + const result = await testEndpointAndGetModels(endpoint); + expect(result.modelCatalog?.text?.map(model => model.id)).toEqual(models.map(model => model.id)); + expect(result.modelCatalog?.image?.map(model => model.id)).toEqual(['minimax/media']); + }); + + it('leaves custom endpoint model lists alone', async () => { + serve(directModels); + const models = await fetchModelsFromServer({ id: 'custom', endpoint: 'https://custom.example/v1' } as RemoteServer); + expect(models.map(model => model.id)).toEqual(['MiniMax-M2.7', 'MiniMax-M3', 'MiniMax-M3.1-Flash']); + }); +}); diff --git a/__tests__/unit/services/providers/minimaxForm.test.ts b/__tests__/unit/services/providers/minimaxForm.test.ts new file mode 100644 index 000000000..4e6ef3918 --- /dev/null +++ b/__tests__/unit/services/providers/minimaxForm.test.ts @@ -0,0 +1,58 @@ +import React from 'react'; +import { act, create } from 'react-test-renderer'; +import { useRemoteServerForm } from '../../../../src/components/RemoteServerEditor/useRemoteServerForm'; +import { remoteServerManager } from '../../../../src/services/remoteServerManager'; +import type { RemoteServer } from '../../../../src/types'; + +jest.mock('../../../../src/services/remoteServerManager', () => ({ + remoteServerManager: { + getApiKey: jest.fn(async () => null), + testConnectionByEndpoint: jest.fn(async () => ({ + success: true, models: [{ id: 'MiniMax-M3' }], + })), + }, +})); +jest.mock('../../../../src/stores', () => ({ useRemoteServerStore: {} })); +jest.mock('../../../../src/services/httpClient', () => ({ isPrivateNetworkEndpoint: () => false })); +jest.mock('../../../../src/components/CustomAlert', () => ({ initialAlertState: {}, showAlert: jest.fn() })); + +// Hooks are mounted so edits between renders exercise callback capture and updater ordering. +describe('MiniMax editor defaults', () => { + it.each(['', 'MiniMax-M2.7', 'custom-model'])('preserves explicit selection %s when testing', async selection => { + let form!: ReturnType; + function Harness() { + form = useRemoteServerForm({ visible: true, onClose: jest.fn() }); + return null; + } + let renderer!: ReturnType; + await act(async () => { renderer = create(React.createElement(Harness)); }); + await act(async () => { + form.setName('MiniMax'); + form.setEndpoint('https://api.minimax.io/v1'); + }); + await act(async () => { form.setTextModelId(selection); }); + await act(async () => { await form.handleTestConnection(); }); + expect(form.textModelId).toBe(selection || 'MiniMax-M3.1-Flash'); + expect(form.providerType).toBe('minimax'); + expect(remoteServerManager.testConnectionByEndpoint).toHaveBeenCalled(); + await act(async () => { renderer.unmount(); }); + }); + + it('preserves an existing provider type on a custom endpoint', async () => { + const server = { + id: 'saved', name: 'Custom', endpoint: 'https://custom.example/anthropic', + providerType: 'anthropic', mediaModels: { text: 'saved-model' }, + } as RemoteServer; + let form!: ReturnType; + function Harness() { + form = useRemoteServerForm({ server, visible: true, onClose: jest.fn() }); + return null; + } + let renderer!: ReturnType; + await act(async () => { renderer = create(React.createElement(Harness)); }); + await act(async () => { await form.handleTestConnection(); }); + expect(form.providerType).toBe('anthropic'); + expect(form.textModelId).toBe('saved-model'); + await act(async () => { renderer.unmount(); }); + }); +}); diff --git a/__tests__/unit/services/providers/minimaxTransport.test.ts b/__tests__/unit/services/providers/minimaxTransport.test.ts new file mode 100644 index 000000000..90220b26e --- /dev/null +++ b/__tests__/unit/services/providers/minimaxTransport.test.ts @@ -0,0 +1,88 @@ +import { createOpenAIProvider } from '../../../../src/services/providers/openAICompatibleProvider'; +import { MINIMAX_DEFAULT_MODEL, MINIMAX_ENDPOINTS, MINIMAX_MODELS, isMiniMaxEndpoint } from '../../../../src/services/providers/minimax'; +import type { Message } from '../../../../src/types'; + +jest.mock('../../../../src/utils/logger', () => ({ + __esModule: true, + default: { log: jest.fn(), warn: jest.fn(), error: jest.fn() }, +})); +jest.mock('../../../../src/stores/appStore', () => ({ + useAppStore: { getState: () => ({ settings: {} }) }, +})); + +// Exercise the real provider, message builder, JSON serialization and SSE reader. +// Only the native network boundary is replaced; no request reaches an API. +class RecordingXHR { + static requests: Array<{ url: string; body: Record }> = []; + readyState = 0; + status = 200; + responseText = 'data: {"choices":[{"delta":{"content":"ok"},"finish_reason":"stop"}]}\n\n'; + responseURL = ''; + onreadystatechange?: () => void; + open(_method: string, url: string) { this.responseURL = url; } + setRequestHeader() {} + abort() {} + send(body: string) { + RecordingXHR.requests.push({ url: this.responseURL, body: JSON.parse(body) }); + this.readyState = 4; + this.onreadystatechange?.(); + } +} + +describe('MiniMax serialized remote requests', () => { + const originalXHR = global.XMLHttpRequest; + beforeEach(() => { + RecordingXHR.requests = []; + global.XMLHttpRequest = RecordingXHR as unknown as typeof XMLHttpRequest; + }); + afterEach(() => { global.XMLHttpRequest = originalXHR; }); + + it('offers Flash first and M3 without treating other protocols as this transport', () => { + expect(MINIMAX_MODELS.map(model => model.id)).toEqual(['MiniMax-M3.1-Flash', 'MiniMax-M3']); + expect(isMiniMaxEndpoint(MINIMAX_ENDPOINTS.global.anthropic)).toBe(false); + expect(isMiniMaxEndpoint('https://api.minimax.io.example/v1')).toBe(false); + expect(isMiniMaxEndpoint('invalid')).toBe(false); + }); + + it.each(Object.values(MINIMAX_ENDPOINTS).map(region => region.openai))( + 'serializes default, saved and custom models at %s without losing tool/image history', + async endpoint => { + const messages = [ + { role: 'user', content: 'Describe', attachments: [{ type: 'image', uri: 'data:image/png;base64,AAAA' }] }, + { role: 'assistant', content: '', reasoningDetails: [{ type: 'reasoning.text', text: 'kept' }], toolCalls: [{ id: 'call-1', name: 'lookup', arguments: '{}' }] }, + { role: 'tool', content: 'result', toolCallId: 'call-1' }, + ] as Message[]; + for (const modelId of [MINIMAX_DEFAULT_MODEL, 'MiniMax-M3', 'MiniMax-M2.7', 'custom-model']) { + const provider = createOpenAIProvider('test', endpoint, { modelId }); + provider.updateCapabilities({ supportsVision: true, supportsThinking: true }); + const callbacks = { onToken: jest.fn(), onComplete: jest.fn(), onError: jest.fn() }; + await provider.generate(messages, { enableThinking: false }, callbacks); + expect(callbacks.onError).not.toHaveBeenCalled(); + expect(callbacks.onComplete).toHaveBeenCalledWith(expect.objectContaining({ content: 'ok' })); + const request = RecordingXHR.requests[RecordingXHR.requests.length - 1]; + expect(request.url).toBe(`${endpoint}/chat/completions`); + expect(request.body.model).toBe(modelId); + expect(request.body.messages[0].content[1].image_url.url).toBe('data:image/png;base64,AAAA'); + expect(request.body.messages[1].tool_calls[0].id).toBe('call-1'); + expect(request.body.messages[1].reasoning_details).toEqual(messages[1].reasoningDetails); + expect(request.body.messages[2].tool_call_id).toBe('call-1'); + expect(request.body.thinking).toEqual(modelId === 'MiniMax-M3' ? { type: 'disabled' } : undefined); + expect(request.body).not.toHaveProperty('reasoning_effort'); + expect(request.body).not.toHaveProperty('service_tier'); + expect(request.body).not.toHaveProperty('reasoning_split'); + expect(request.body).not.toHaveProperty('chat_template_kwargs'); + } + }, + ); + + it('uses adaptive M3 thinking by default and preserves non-MiniMax requests', async () => { + for (const endpoint of ['https://api.minimax.io/v1', 'https://custom.example/v1']) { + const provider = createOpenAIProvider('test', endpoint, { modelId: 'MiniMax-M3' }); + const onError = jest.fn(); + await provider.generate([], {}, { onToken: jest.fn(), onComplete: jest.fn(), onError }); + expect(onError).not.toHaveBeenCalled(); + expect(RecordingXHR.requests[RecordingXHR.requests.length - 1].body.thinking) + .toEqual(endpoint.includes('minimax.io') ? { type: 'adaptive' } : undefined); + } + }); +}); diff --git a/src/components/RemoteServerEditor/useRemoteServerForm.ts b/src/components/RemoteServerEditor/useRemoteServerForm.ts index de3e7503a..9ecad7f1a 100644 --- a/src/components/RemoteServerEditor/useRemoteServerForm.ts +++ b/src/components/RemoteServerEditor/useRemoteServerForm.ts @@ -10,6 +10,10 @@ import { } from '../../types'; import { isPrivateNetworkEndpoint } from '../../services/httpClient'; import { AlertState, initialAlertState, showAlert } from '../CustomAlert'; +import { + isMiniMaxEndpoint, + MINIMAX_DEFAULT_MODEL, +} from '../../services/providers/minimax'; interface FormOptions { server?: RemoteServer; @@ -55,6 +59,7 @@ export function useRemoteServerForm({ }: FormOptions) { const [name, setName] = useState(''); const [endpoint, setEndpoint] = useState(''); + const [providerType, setProviderType] = useState('openai-compatible'); const [apiKey, setApiKey] = useState(''); const [notes, setNotes] = useState(''); const [textModelId, setTextModelId] = useState(''); @@ -82,6 +87,7 @@ export function useRemoteServerForm({ if (server) { setName(server.name); setEndpoint(server.endpoint); + setProviderType(server.providerType); setNotes(server.notes || ''); setTextModelId(server.mediaModels?.text || ''); setImageModelId(server.mediaModels?.image || ''); @@ -100,6 +106,7 @@ export function useRemoteServerForm({ // Reset form for new server setName(''); setEndpoint(''); + setProviderType('openai-compatible'); setApiKey(''); setNotes(''); setTextModelId(''); @@ -191,6 +198,10 @@ export function useRemoteServerForm({ const handleTestConnection = useCallback(async () => { if (!validateForm()) return; setIsTesting(true); + const minimax = isMiniMaxEndpoint(endpoint); + setProviderType(current => + minimax ? 'minimax' : current === 'minimax' ? 'openai-compatible' : current, + ); setTestResult(null); setDiscoveredModels([]); setModelCatalog({}); @@ -202,7 +213,13 @@ export function useRemoteServerForm({ apiKey || undefined, ); if (result.success) { - applySuccessfulConnection(result); + applySuccessfulConnection(minimax + ? { + ...result, + mediaModels: { ...result.mediaModels, text: MINIMAX_DEFAULT_MODEL }, + } + : result, + ); } else { const triedUrl = `${endpoint.replace(/\/+$/, '')}/v1/models`; setTestResult({ @@ -262,6 +279,7 @@ export function useRemoteServerForm({ await remoteServerManager.updateServer(server.id, { name, endpoint, + providerType, notes, apiKey, mediaModels: desktopManaged ? server.mediaModels : mediaModels, @@ -292,7 +310,7 @@ export function useRemoteServerForm({ const newServer = await remoteServerManager.addServer({ name, endpoint, - providerType: 'openai-compatible', + providerType, notes: notes || undefined, apiKey: apiKey || undefined, mediaModels: desktopManaged ? confirmedMediaModels : mediaModels, @@ -336,6 +354,7 @@ export function useRemoteServerForm({ apiKey, notes, textModelId, + providerType, imageModelId, transcriptionModelId, voiceModelId, @@ -370,6 +389,7 @@ export function useRemoteServerForm({ name, setName, endpoint, + providerType, setEndpoint, apiKey, setApiKey, diff --git a/src/services/providers/index.ts b/src/services/providers/index.ts index 4674ca89b..0a0590870 100644 --- a/src/services/providers/index.ts +++ b/src/services/providers/index.ts @@ -13,5 +13,15 @@ export { localProvider } from './localProvider'; // OpenAI-compatible provider ; +// MiniMax regional endpoint and model presets +export { + MINIMAX_PROVIDER, + MINIMAX_ENDPOINTS, + MINIMAX_MODELS, + MINIMAX_DEFAULT_MODEL, + isMiniMaxEndpoint, + miniMaxRegion, +} from './minimax'; + // Registry -export { providerRegistry } from './registry'; \ No newline at end of file +export { providerRegistry } from './registry'; diff --git a/src/services/providers/minimax.ts b/src/services/providers/minimax.ts new file mode 100644 index 000000000..09d482911 --- /dev/null +++ b/src/services/providers/minimax.ts @@ -0,0 +1,82 @@ +/** MiniMax remote provider presets. + * + * MiniMax exposes OpenAI-compatible and Anthropic-compatible transports in + * separate regional endpoints. Keeping the endpoints and model defaults in a + * single registry lets callers present a provider choice without duplicating + * routing facts throughout the remote-server UI. + */ +export const MINIMAX_PROVIDER = 'MiniMax' as const; + +export const MINIMAX_ENDPOINTS = { + global: { + openai: 'https://api.minimax.io/v1', + anthropic: 'https://api.minimax.io/anthropic', + }, + china: { + openai: 'https://api.minimaxi.com/v1', + anthropic: 'https://api.minimaxi.com/anthropic', + }, +} as const; + +export const MINIMAX_MODELS = [ + { id: 'MiniMax-M3.1-Flash', contextWindow: 1_000_000 }, + { id: 'MiniMax-M3', contextWindow: 1_000_000 }, +] as const; + +export const MINIMAX_DEFAULT_MODEL = MINIMAX_MODELS[0].id; + +export type MiniMaxRegion = keyof typeof MINIMAX_ENDPOINTS; + +export const MINIMAX_OPENROUTER_MODELS = [ + 'minimax/minimax-m3.1-flash:free', + 'minimax/minimax-m3', +] as const; + +/** Filter ordinary text choices only; saved/custom model IDs are never rewritten. */ +export function currentMiniMaxTextModels( + endpoint: string, + models: T[], +): T[] { + const direct = isMiniMaxEndpoint(endpoint); + let gateway = false; + try { + gateway = new URL(endpoint).hostname === 'openrouter.ai'; + } catch { + return models; + } + if (!direct && !gateway) return models; + const ids: readonly string[] = direct + ? MINIMAX_MODELS.map(model => model.id) + : MINIMAX_OPENROUTER_MODELS; + const prefix = direct ? 'MiniMax-' : 'minimax/'; + const current = models.filter(model => ids.includes(model.id)); + current.sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id)); + let next = 0; + return models + .filter(model => !model.id.startsWith(prefix) || ids.includes(model.id)) + .map(model => ids.includes(model.id) ? current[next++] : model); +} + +export function isMiniMaxEndpoint(endpoint: string): boolean { + try { + const url = new URL(endpoint); + // Only recognize the transport implemented by the remote provider. Other + // paths (including saved Anthropic endpoints) remain explicit custom routes. + return url.protocol === 'https:' && + (url.hostname === 'api.minimax.io' || url.hostname === 'api.minimaxi.com') && + ['', '/', '/v1', '/v1/'].includes(url.pathname); + } catch { + return false; + } +} + +export function miniMaxRegion(endpoint: string): MiniMaxRegion | null { + try { + const hostname = new URL(endpoint).hostname; + if (hostname === 'api.minimax.io') return 'global'; + if (hostname === 'api.minimaxi.com') return 'china'; + } catch { + // Invalid endpoints are handled by the editor's URL validation. + } + return null; +} diff --git a/src/services/providers/openAICompatibleProvider.ts b/src/services/providers/openAICompatibleProvider.ts index 96ed9ed07..a042e1c56 100644 --- a/src/services/providers/openAICompatibleProvider.ts +++ b/src/services/providers/openAICompatibleProvider.ts @@ -24,6 +24,7 @@ import type { OpenAIStreamState, } from './openAICompatibleTypes'; import { remoteAuthorizationHeaders } from '../remoteTransportPolicy'; +import { isMiniMaxEndpoint, MINIMAX_DEFAULT_MODEL } from './minimax'; export type { OpenAIChatMessage, OpenAIConfig } from './openAICompatibleTypes'; @@ -97,6 +98,9 @@ export class OpenAICompatibleProvider implements LLMProvider { model: this.config.modelId, messages: openaiMessages, stream: true, + ...(isMiniMaxEndpoint(this.config.endpoint) && this.config.modelId === 'MiniMax-M3' + ? { thinking: { type: thinkingEnabled ? 'adaptive' : 'disabled' } } + : {}), ...(options.temperature !== undefined && { temperature: options.temperature }), // max_tokens intentionally omitted — the remote server owns output limits. // A client-side cap (default 1024) silently truncates reasoning models that @@ -141,6 +145,9 @@ export class OpenAICompatibleProvider implements LLMProvider { try { const openaiMessages = await this.buildOpenAIMessages(messages, options); const thinkingEnabled = + (isMiniMaxEndpoint(this.config.endpoint) && this.config.modelId === MINIMAX_DEFAULT_MODEL) || + (new URL(this.config.endpoint).hostname === 'openrouter.ai' && + this.config.modelId === 'minimax/minimax-m3.1-flash:free') || this.modelCapabilities.thinkingLevelsOnly || options.enableThinking !== false; diff --git a/src/stores/remoteServerHelpers.ts b/src/stores/remoteServerHelpers.ts index c492d0b2a..577af4f25 100644 --- a/src/stores/remoteServerHelpers.ts +++ b/src/stores/remoteServerHelpers.ts @@ -30,6 +30,12 @@ import { } from '../services/remoteTransportPolicy'; import { readOffGridDesktopModelState } from '../services/offGridDesktopModels'; import { displayModelName } from '../utils/displayModelName'; +import { + currentMiniMaxTextModels, + isMiniMaxEndpoint, + MINIMAX_MODELS, + MINIMAX_OPENROUTER_MODELS, +} from '../services/providers/minimax'; /** Timeout for model discovery fetches (non-critical, background operation) */ const DISCOVERY_FETCH_TIMEOUT_MS = 5000; @@ -142,6 +148,7 @@ async function fetchGatewayModelCatalog( result[category] = options; } } + if (result.text) result.text = currentMiniMaxTextModels(url, result.text); return result; } catch { return {}; @@ -391,9 +398,9 @@ export async function fetchModelsFromServer( // OpenAI format: { object: "list", data: [{ id, object, owned_by, ... }] } if ((data?.object === 'list' || isOpenRouter) && Array.isArray(data.data)) { - const generativeModels = data.data.filter( + const generativeModels = currentMiniMaxTextModels(url, data.data.filter( (model: { id: string; kind?: unknown }) => isTextModel(model), - ); + )); const modelInfos = await Promise.all( generativeModels.map((model: { id: string; @@ -407,10 +414,20 @@ export async function fetchModelsFromServer( contextLength: model.context_length ?? 4096, supportsVision: model.architecture?.input_modalities?.includes('image') === true, supportsToolCalling: model.supported_parameters?.includes('tools') === true, - supportsThinking: !!model.reasoning, - thinkingLevelsOnly: model.reasoning?.mandatory === true, + supportsThinking: !!model.reasoning || MINIMAX_OPENROUTER_MODELS.some(id => id === model.id), + thinkingLevelsOnly: model.id === MINIMAX_OPENROUTER_MODELS[0] || model.reasoning?.mandatory === true, + acceptsThinkingKwarg: MINIMAX_OPENROUTER_MODELS.some(id => id === model.id) ? false : undefined, } - : fetchModelCapabilities(probeBase, model.id, nameDetect, server.apiKey), + : isMiniMaxEndpoint(url) && MINIMAX_MODELS.some(preset => preset.id === model.id) + ? { + contextLength: 1_000_000, + supportsVision: true, + supportsToolCalling: true, + supportsThinking: true, + thinkingLevelsOnly: model.id === 'MiniMax-M3.1-Flash', + acceptsThinkingKwarg: false, + } + : fetchModelCapabilities(probeBase, model.id, nameDetect, server.apiKey), ), ); return generativeModels.map( diff --git a/src/types/remoteServer.ts b/src/types/remoteServer.ts index 12187019c..cc4ee2043 100644 --- a/src/types/remoteServer.ts +++ b/src/types/remoteServer.ts @@ -6,7 +6,7 @@ */ /** Provider types supported by the system */ -type RemoteProviderType = 'openai-compatible' | 'anthropic'; +export type RemoteProviderType = 'openai-compatible' | 'anthropic' | 'minimax'; /** Optional OpenAI-compatible media models served by this endpoint. */ export interface RemoteMediaModelIds {