Skip to content
Closed
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
5 changes: 5 additions & 0 deletions docs-site/src/content/docs/guides/minimax.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ the protocol boundary it actually exposes:

## MiniMax Code

In the OpenCodex Models page, both MiniMax Coding Plan regions advertise image input for
`MiniMax-M3` and `MiniMax-M3.1-Flash-Preview`. A Combo's **Image / multimodal** switch is
available only when every selected model supports image input. The switch controls image
attachments; it does not enable video uploads.

Install and sign in to MiniMax Code using MiniMax's instructions first. Then start
OpenCodex and connect the reversible file integration:

Expand Down
4 changes: 4 additions & 0 deletions docs-site/src/content/docs/zh-cn/guides/minimax.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ MiniMax 发布两种不同的命令行产品。OpenCodex 在它们实际提供

## MiniMax Code

在 OpenCodex 的模型管理页面,MiniMax Coding Plan 两个区域的 `MiniMax-M3` 和
`MiniMax-M3.1-Flash-Preview` 均声明支持图片输入。只有所有选定模型均支持图片输入时,
Combo 的“图片 / 多模态”开关才可用。该开关控制图片附件,不提供视频上传能力。

先按照 MiniMax 的说明安装并登录 MiniMax Code,然后启动 OpenCodex 并连接可逆的文件集成:

```bash
Expand Down
1 change: 1 addition & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"desktop-app-server-shim-launcher.test.ts": "clients",
"desktop-chatgpt-config.test.ts": "clients",
"owner-registry-acl.test.ts": "config",
"minimax-combo-image-input.test.ts": "providers",
"claude-devin-output-order.test.ts": "claude-integration",
"codex-config-preservation.test.ts": "codex-integration",
"codex-credits.test.ts": "codex-integration",
Expand Down
3 changes: 3 additions & 0 deletions src/providers/registry/entries-extended.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
ZAI_GLM_5X_REASONING_EFFORTS,
MINIMAX_MODELS,
MINIMAX_MODEL_CONTEXT_WINDOWS,
MINIMAX_MODEL_INPUT_MODALITIES,
MINIMAX_M3_REASONING_EFFORTS,
MINIMAX_M3_REASONING_EFFORT_MAP,
MINIMAX_M31_FLASH_PREVIEW,
Expand Down Expand Up @@ -1024,6 +1025,7 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
id: "minimax", label: "MiniMax — Coding Plan", baseUrl: "https://api.minimax.io/v1", adapter: "openai-chat", authKind: "key",
dashboardUrl: "https://platform.minimax.io", defaultModel: "MiniMax-M3", models: MINIMAX_MODELS,
modelContextWindows: MINIMAX_MODEL_CONTEXT_WINDOWS,
modelInputModalities: MINIMAX_MODEL_INPUT_MODALITIES,
modelReasoningEfforts: { "MiniMax-M3": MINIMAX_M3_REASONING_EFFORTS, [MINIMAX_M31_FLASH_PREVIEW]: MINIMAX_M31_REASONING_EFFORTS },
modelDefaultReasoningEfforts: { "MiniMax-M3": "medium", [MINIMAX_M31_FLASH_PREVIEW]: MINIMAX_M31_DEFAULT_REASONING_EFFORT },
modelReasoningEffortMap: { "MiniMax-M3": MINIMAX_M3_REASONING_EFFORT_MAP },
Expand All @@ -1049,6 +1051,7 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
id: "minimax-cn", label: "MiniMax — Coding Plan (CN)", baseUrl: "https://api.minimaxi.com/v1", adapter: "openai-chat", authKind: "key",
dashboardUrl: "https://platform.minimaxi.com", defaultModel: "MiniMax-M3", models: MINIMAX_MODELS,
modelContextWindows: MINIMAX_MODEL_CONTEXT_WINDOWS,
modelInputModalities: MINIMAX_MODEL_INPUT_MODALITIES,
modelReasoningEfforts: { "MiniMax-M3": MINIMAX_M3_REASONING_EFFORTS, [MINIMAX_M31_FLASH_PREVIEW]: MINIMAX_M31_REASONING_EFFORTS },
modelDefaultReasoningEfforts: { "MiniMax-M3": "medium", [MINIMAX_M31_FLASH_PREVIEW]: MINIMAX_M31_DEFAULT_REASONING_EFFORT },
modelReasoningEffortMap: { "MiniMax-M3": MINIMAX_M3_REASONING_EFFORT_MAP },
Expand Down
5 changes: 5 additions & 0 deletions src/providers/registry/model-seeds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,11 @@ export const MINIMAX_REASONING_SPLIT_MODELS = MINIMAX_MODELS_BEFORE_M31;
export const MINIMAX_MODEL_CONTEXT_WINDOWS: Record<string, number> = Object.fromEntries(
MINIMAX_MODELS.map(id => [id, id === "MiniMax-M3" || id === MINIMAX_M31_FLASH_PREVIEW ? 1_000_000 : 204_800]),
);
/** MiniMax's OpenAI-compatible M3 endpoints accept image_url; keep video off Codex's input enum. */
export const MINIMAX_MODEL_INPUT_MODALITIES: Record<string, string[]> = {
"MiniMax-M3": ["text", "image"],
[MINIMAX_M31_FLASH_PREVIEW]: ["text", "image"],
};
export const MINIMAX_M3_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
/** Identity efforts on the wire; no map, so none omits the field instead of disabling thinking. */
export const MINIMAX_M31_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
Expand Down
7 changes: 3 additions & 4 deletions structure/providers-and-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,10 +354,9 @@ axis that outranks every source here, so it is where a deliberate text-only over
(`ocx provider edit <provider> --model <id> --text-only` writes it) and the one declaration a
restart cannot take back.

Roster additions share the blind spot when the vendor's `/models` omits the new id (MiniMax-M3.1-Flash-Preview):
`src/providers/stale-model-roster-migration.ts` replaces a saved roster only while it is byte-for-byte the previous
seed, filling the added id's window and default effort only inside records the row already has, in the same startup
pass; `CALLABLE_CONFIGURED_COMPATIBILITY_MODELS` (`src/codex/catalog/model-hints.ts`) keeps it in the live catalog.
When `/models` omits MiniMax-M3.1-Flash-Preview, `src/providers/stale-model-roster-migration.ts` replaces only a saved roster identical to the old seed and fills existing window and default-effort records during startup; `CALLABLE_CONFIGURED_COMPATIBILITY_MODELS` (`src/codex/catalog/model-hints.ts`) retains it in the live catalog.

Both MiniMax Coding Plan presets declare `text` and `image` for M3 and M3.1 in `src/providers/registry/model-seeds.ts`; registry enrichment fills missing saved declarations, so id-only live rows enable the Combo image switch when every target supports images. Explicit overrides still win; video is outside Codex's catalog input enum.

The BigModel Coding Plan Responses preset uses the separately documented
`https://open.bigmodel.cn/api/v1` transport and a static catalog. Its provider row
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/test-layout-expected.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"desktop-app-server-shim-launcher.test.ts": "clients",
"desktop-chatgpt-config.test.ts": "clients",
"owner-registry-acl.test.ts": "config",
"minimax-combo-image-input.test.ts": "providers",
"claude-devin-output-order.test.ts": "claude-integration",
"codex-config-preservation.test.ts": "codex-integration",
"codex-credits.test.ts": "codex-integration",
Expand Down
101 changes: 101 additions & 0 deletions tests/providers/minimax-combo-image-input.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { afterEach, describe, expect, test } from "bun:test";
import { comboImagesSupported } from "../../gui/src/combo-capabilities";
import { applyProviderConfigHints, deriveComboCatalogModel, gatherRoutedModels } from "../../src/codex/catalog";
import { clearModelCache } from "../../src/codex/model-cache";
import { enrichProviderFromRegistry, providerConfigSeed } from "../../src/providers/derive";
import { getProviderRegistryEntry } from "../../src/providers/registry";
import { listManagementModelRows } from "../../src/server/management/model-rows";
import { isModelVisionSidecarConsumer } from "../../src/vision/eligibility";
import type { CatalogModel, OcxConfig, OcxProviderConfig } from "../../src/types";

const M3 = "MiniMax-M3";
const PREVIEW = "MiniMax-M3.1-Flash-Preview";
const REGIONS = ["minimax", "minimax-cn"] as const;
const originalFetch = globalThis.fetch;

function seeded(region: typeof REGIONS[number]): OcxProviderConfig {
const entry = getProviderRegistryEntry(region);
if (!entry) throw new Error(`missing ${region} registry entry`);
return { ...providerConfigSeed(entry), apiKey: "test-key" };
}

function config(region: typeof REGIONS[number], provider = seeded(region)): OcxConfig {
return { port: 10100, defaultProvider: region, providers: { [region]: provider } } as OcxConfig;
}

afterEach(() => {
globalThis.fetch = originalFetch;
clearModelCache();
});

describe("MiniMax Coding Plan Combo image input", () => {
test("both presets seed M3 image input and enrich missing saved declarations without replacing overrides", () => {
for (const region of REGIONS) {
const fresh = seeded(region);
expect(fresh.modelInputModalities?.[M3]).toEqual(["text", "image"]);
expect(fresh.modelInputModalities?.[PREVIEW]).toEqual(["text", "image"]);
expect(fresh.modelInputModalities?.["MiniMax-M2.7"]).toBeUndefined();

const saved: OcxProviderConfig = {
adapter: "openai-chat", baseUrl: fresh.baseUrl,
modelInputModalities: { "MiniMax-M2.7": ["text"] },
modelCapabilities: { [M3]: { inputModalities: ["text"] } },
};
enrichProviderFromRegistry(region, saved);
expect(saved.modelInputModalities).toMatchObject({
[M3]: ["text", "image"], [PREVIEW]: ["text", "image"], "MiniMax-M2.7": ["text"],
});
expect(saved.modelCapabilities?.[M3]?.inputModalities).toEqual(["text"]);
expect(isModelVisionSidecarConsumer(saved, M3)).toBe(true);
expect(applyProviderConfigHints(region, saved, { provider: region, id: M3 }).inputModalities)
.toEqual(["text", "image"]); // explicit text-only uses the existing vision sidecar
expect(isModelVisionSidecarConsumer(fresh, M3)).toBe(false);
}
});

test("id-only discovery and a warm cache yield management rows accepted by the Combo picker", async () => {
globalThis.fetch = (async (input: RequestInfo | URL) => {
if (!String(input).includes("/models")) throw new Error("unexpected request");
return Response.json({ data: [{ id: M3 }] });
}) as typeof fetch;

for (const region of REGIONS) {
const live = config(region);
const first = await gatherRoutedModels(live);
const cached = await gatherRoutedModels(live);
for (const models of [first, cached]) {
const m3 = models.find(model => model.provider === region && model.id === M3);
expect(m3?.inputModalities).toEqual(["text", "image"]);
const rows = await listManagementModelRows(live, { models });
const row = rows.find(model => model.provider === region && model.id === M3);
expect(row?.inputModalities).toEqual(["text", "image"]);
expect(comboImagesSupported([{ provider: region, model: M3 }], rows)).toBe(true);
expect(comboImagesSupported(
[{ provider: region, model: M3 }, { provider: "other", model: "vision" }],
[...rows, { provider: "other", id: "vision", inputModalities: ["text", "image"] }],
)).toBe(true);
expect(comboImagesSupported(
[{ provider: region, model: M3 }, { provider: "other", model: "blind" }],
[...rows, { provider: "other", id: "blind", inputModalities: ["text"] }],
)).toBe(false);
}
}
});

test("static fallback and Combo catalog retain image unless the operator disables it", async () => {
for (const region of REGIONS) {
const provider = seeded(region);
provider.liveModels = false;
const models = await gatherRoutedModels(config(region, provider));
const member = models.find(model => model.provider === region && model.id === M3);
expect(member?.inputModalities).toEqual(["text", "image"]);
const combo = (imageInput: "auto" | "disabled") => ({
targets: [{ provider: region, model: M3 }], imageInput, defaultEffort: "medium",
}) as never;
expect(deriveComboCatalogModel("minimax-image", combo("auto"), [member as CatalogModel])?.inputModalities)
.toContain("image");
expect(deriveComboCatalogModel("minimax-image", combo("disabled"), [member as CatalogModel])?.inputModalities)
.toEqual(["text"]);
}
});
});
Loading