Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
- **See every session in one place** — switch context without losing momentum.
- **Control everything keyboard-first** — every action has a shortcut, mouse optional.
- **Monitor progress from your phone** — scan a QR code, watch agents work over Wi-Fi or Tailscale.
- **Ask about code with any LLM** — the inline code Q&A feature supports [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (default) or [MiniMax](https://www.minimax.io/) M2.7 (204K context) — configurable in Settings.
- **Ask about code with any LLM** — the inline code Q&A feature supports [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (default) or [MiniMax](https://www.minimax.io/) M3 (1M context, image and video input) with M2.7 also supported — configurable in Settings.

<details>
<summary><strong>How does it compare?</strong></summary>
Expand Down
2 changes: 1 addition & 1 deletion electron/ipc/ask-code-minimax.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ describe('askAboutCodeMinimax', () => {
);
});

it('uses MiniMax-M2.7 model', async () => {
it('uses the default MiniMax model from the catalog', async () => {
const { win, messages } = makeMockWin();

mockFetch.mockResolvedValueOnce(makeStreamResponse('data: [DONE]\n\n'));
Expand Down
9 changes: 7 additions & 2 deletions electron/ipc/ask-code-minimax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,20 @@ import {
assertCanStart,
assertPromptWithinLimit,
} from './request-registry.js';
import {
DEFAULT_MINIMAX_MODEL_ID,
DEFAULT_MINIMAX_REGION,
minimaxChatCompletionsUrl,
} from './minimax-catalog.js';

interface MinimaxAskCodeRequest {
requestId: string;
channelId: string;
prompt: string;
}

const MINIMAX_API_URL = 'https://api.minimax.io/v1/chat/completions';
export const MINIMAX_MODEL = 'MiniMax-M2.7';
const MINIMAX_API_URL = minimaxChatCompletionsUrl(DEFAULT_MINIMAX_REGION);
export const MINIMAX_MODEL = DEFAULT_MINIMAX_MODEL_ID;

const activeRequests = new RequestRegistry<AbortController>({
maxConcurrent: ASK_CODE_MAX_CONCURRENT,
Expand Down
85 changes: 85 additions & 0 deletions electron/ipc/minimax-catalog.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { describe, it, expect } from 'vitest';
import {
DEFAULT_MINIMAX_MODEL_ID,
DEFAULT_MINIMAX_REGION,
MINIMAX_ENDPOINTS,
MINIMAX_MODELS,
getMinimaxEndpoint,
getMinimaxModel,
minimaxBaseUrl,
minimaxChatCompletionsUrl,
minimaxModelIds,
} from './minimax-catalog.js';

describe('minimax catalog models', () => {
it('retains M2.7 alongside M3', () => {
expect(minimaxModelIds()).toEqual(['MiniMax-M3', 'MiniMax-M2.7']);
});

it('defaults to M3', () => {
expect(DEFAULT_MINIMAX_MODEL_ID).toBe('MiniMax-M3');
expect(getMinimaxModel(DEFAULT_MINIMAX_MODEL_ID)).toBeDefined();
});

it('represents M3 with a 1M context window, image/video input and adaptive/disabled thinking', () => {
const m3 = getMinimaxModel('MiniMax-M3');
expect(m3).toBeDefined();
expect(m3?.contextWindow).toBe(1_000_000);
expect(m3?.inputModalities).toEqual(['text', 'image', 'video']);
expect(m3?.thinking).toEqual(['adaptive', 'disabled']);
});

it('represents M2.7 as a 204K text-only always-on model', () => {
const m27 = getMinimaxModel('MiniMax-M2.7');
expect(m27).toBeDefined();
expect(m27?.contextWindow).toBe(204_800);
expect(m27?.inputModalities).toEqual(['text']);
expect(m27?.thinking).toEqual(['always_on']);
});

it('does not clone the M2.7 profile onto M3', () => {
const m3 = getMinimaxModel('MiniMax-M3');
const m27 = getMinimaxModel('MiniMax-M2.7');
expect(m3?.contextWindow).not.toBe(m27?.contextWindow);
expect(m3?.inputModalities).not.toEqual(m27?.inputModalities);
});

it('returns undefined for unknown models', () => {
expect(getMinimaxModel('nope')).toBeUndefined();
});

it('exposes exactly the catalogued models', () => {
expect(MINIMAX_MODELS).toHaveLength(2);
});
});

describe('minimax catalog endpoints', () => {
it('exposes the global and China regions', () => {
expect(MINIMAX_ENDPOINTS.map((e) => e.region)).toEqual(['global_en', 'cn_zh']);
});

it('exposes both protocol base URLs for the global region', () => {
expect(minimaxBaseUrl('global_en', 'openai')).toBe('https://api.minimax.io/v1');
expect(minimaxBaseUrl('global_en', 'anthropic')).toBe('https://api.minimax.io/anthropic');
});

it('exposes both protocol base URLs for the China region', () => {
expect(minimaxBaseUrl('cn_zh', 'openai')).toBe('https://api.minimaxi.com/v1');
expect(minimaxBaseUrl('cn_zh', 'anthropic')).toBe('https://api.minimaxi.com/anthropic');
});

it('defaults to the global region', () => {
expect(DEFAULT_MINIMAX_REGION).toBe('global_en');
});

it('builds the chat completions URL from the region base URL', () => {
expect(minimaxChatCompletionsUrl('global_en')).toBe(
'https://api.minimax.io/v1/chat/completions',
);
expect(minimaxChatCompletionsUrl('cn_zh')).toBe('https://api.minimaxi.com/v1/chat/completions');
});

it('falls back to the global endpoint for the default region', () => {
expect(getMinimaxEndpoint(DEFAULT_MINIMAX_REGION).region).toBe('global_en');
});
});
110 changes: 110 additions & 0 deletions electron/ipc/minimax-catalog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* Catalog of the MiniMax models and regional API endpoints used by the inline
* "Ask about Code" provider.
*
* Centralizing this metadata keeps the provider from being pinned to a single
* model or a single endpoint: each model carries its own context window, input
* modalities and thinking modes, and each region exposes both of its
* supported protocol base URLs.
*/

/** Regions where the MiniMax platform is reachable. */
export type MinimaxRegion = 'global_en' | 'cn_zh';

/** Wire protocols each MiniMax endpoint speaks. */
export type MinimaxProtocol = 'openai' | 'anthropic';

/** Input modalities a MiniMax model can accept. */
export type MinimaxInputModality = 'text' | 'image' | 'video';

/** Thinking modes a MiniMax model supports. */
export type MinimaxThinkingMode = 'adaptive' | 'disabled' | 'always_on';

export interface MinimaxModel {
/** Model identifier sent as the `model` field on the wire. */
id: string;
/** Maximum number of tokens the model accepts in a single request. */
contextWindow: number;
/** Input modalities the model understands. */
inputModalities: MinimaxInputModality[];
/** Thinking modes the model exposes. */
thinking: MinimaxThinkingMode[];
}

export interface MinimaxEndpoint {
region: MinimaxRegion;
/** Base URL for the `openai` protocol; chat completions live under `${openaiBaseUrl}/chat/completions`. */
openaiBaseUrl: string;
/** Base URL for the `anthropic` protocol. */
anthropicBaseUrl: string;
/** Root of the platform documentation for this region. */
docsRoot: string;
}

/**
* Supported models, newest first. M3 carries a 1M-token context window and
* accepts image and video input with adaptive or disabled thinking; M2.7 is
* retained as a 204K-token text-only model with always-on thinking.
*/
export const MINIMAX_MODELS: MinimaxModel[] = [
{
id: 'MiniMax-M3',
contextWindow: 1_000_000,
inputModalities: ['text', 'image', 'video'],
thinking: ['adaptive', 'disabled'],
},
{
id: 'MiniMax-M2.7',
contextWindow: 204_800,
inputModalities: ['text'],
thinking: ['always_on'],
},
];

/** Regional endpoints and the protocol base URLs each one exposes. */
export const MINIMAX_ENDPOINTS: MinimaxEndpoint[] = [
{
region: 'global_en',
openaiBaseUrl: 'https://api.minimax.io/v1',
anthropicBaseUrl: 'https://api.minimax.io/anthropic',
docsRoot: 'https://platform.minimax.io/docs',
},
{
region: 'cn_zh',
openaiBaseUrl: 'https://api.minimaxi.com/v1',
anthropicBaseUrl: 'https://api.minimaxi.com/anthropic',
docsRoot: 'https://platform.minimaxi.com/docs',
},
];

/** Default model used by the inline code Q&A provider. */
export const DEFAULT_MINIMAX_MODEL_ID = 'MiniMax-M3';

/** Default region used by the inline code Q&A provider. */
export const DEFAULT_MINIMAX_REGION: MinimaxRegion = 'global_en';

/** Model ids in the catalog, newest first. */
export function minimaxModelIds(): string[] {
return MINIMAX_MODELS.map((model) => model.id);
}

/** Looks up a model by id, or `undefined` when it is not in the catalog. */
export function getMinimaxModel(id: string): MinimaxModel | undefined {
return MINIMAX_MODELS.find((model) => model.id === id);
}

/** Returns the endpoint for a region, falling back to the first (global) one. */
export function getMinimaxEndpoint(region: MinimaxRegion): MinimaxEndpoint {
return MINIMAX_ENDPOINTS.find((endpoint) => endpoint.region === region) ?? MINIMAX_ENDPOINTS[0];
}

/** Returns the base URL a region exposes for the given protocol. */
export function minimaxBaseUrl(region: MinimaxRegion, protocol: MinimaxProtocol): string {
const endpoint = getMinimaxEndpoint(region);
return protocol === 'anthropic' ? endpoint.anthropicBaseUrl : endpoint.openaiBaseUrl;
}

/** Builds the chat completions URL for a region. */
export function minimaxChatCompletionsUrl(region: MinimaxRegion): string {
return `${minimaxBaseUrl(region, 'openai')}/chat/completions`;
}
4 changes: 2 additions & 2 deletions src/components/SettingsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,7 @@ export function SettingsDialog(props: SettingsDialogProps) {
}}
>
<option value="claude">Claude Code (claude CLI)</option>
<option value="minimax">MiniMax (M2.7)</option>
<option value="minimax">MiniMax (M3)</option>
</select>
</label>
<Show when={store.askCodeProvider === 'minimax'}>
Expand Down Expand Up @@ -625,7 +625,7 @@ export function SettingsDialog(props: SettingsDialogProps) {
</Show>
<span style={{ 'font-size': '11px', color: theme.fgSubtle }}>
{store.askCodeProvider === 'minimax'
? 'Uses MiniMax M2.7 (204K context) via the OpenAI-compatible API — no Claude Code CLI required.'
? 'Uses MiniMax M3 (1M context; text, image and video input), with M2.7 also supported — no local CLI required.'
: 'Uses the claude CLI to answer questions about selected code. Requires Claude Code to be installed.'}
</span>
</div>
Expand Down
Loading