Skip to content
Merged
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
4 changes: 2 additions & 2 deletions packages/runtime/src/models/providers/builtin-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import { deepseekProvider } from "@earendil-works/pi-ai/providers/deepseek";
import { googleProvider } from "@earendil-works/pi-ai/providers/google";
import { groqProvider } from "@earendil-works/pi-ai/providers/groq";
import { huggingfaceProvider } from "@earendil-works/pi-ai/providers/huggingface";
import { minimaxProvider } from "@earendil-works/pi-ai/providers/minimax";
import { minimaxCnProvider } from "@earendil-works/pi-ai/providers/minimax-cn";
import { moonshotaiProvider } from "@earendil-works/pi-ai/providers/moonshotai";
import { moonshotaiCnProvider } from "@earendil-works/pi-ai/providers/moonshotai-cn";
import { nvidiaProvider } from "@earendil-works/pi-ai/providers/nvidia";
Expand All @@ -23,6 +21,8 @@ import { zaiCodingCnProvider } from "@earendil-works/pi-ai/providers/zai-coding-
import { arkProvider } from "./ark";
import { arkAgentPlanProvider } from "./ark-agent-plan";
import { arkCodingPlanProvider } from "./ark-coding-plan";
import { minimaxProvider } from "./minimax";
import { minimaxCnProvider } from "./minimax-cn";
import { openaiCodexProvider } from "./openai-codex";

/** Static, non-`Provider` metadata for a builtin provider. */
Expand Down
19 changes: 19 additions & 0 deletions packages/runtime/src/models/providers/minimax-cn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {
createProvider,
envApiKeyAuth,
type Provider,
} from "@earendil-works/pi-ai";
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";

import { MINIMAX_CN_MODELS } from "./minimax.models";

export function minimaxCnProvider(): Provider<"openai-completions"> {
return createProvider({
id: "minimax-cn",
name: "MiniMax CN",
baseUrl: "https://api.minimaxi.com/v1",
auth: { apiKey: envApiKeyAuth("MiniMax CN API key", ["MINIMAX_CN_API_KEY"]) },
models: Object.values(MINIMAX_CN_MODELS),
api: openAICompletionsApi(),
});
}
100 changes: 100 additions & 0 deletions packages/runtime/src/models/providers/minimax.models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import type { Model } from "@earendil-works/pi-ai";

// The MiniMax chat API is OpenAI-compatible. Each region targets its
// OpenAI-compatible base URL and the OpenAI Completions API.
const OPENAI_BASE_URL = "https://api.minimax.io/v1";
const OPENAI_BASE_URL_CN = "https://api.minimaxi.com/v1";

// MiniMax-M3 supports adaptive thinking (effort-based, on by default) and can
// also be disabled. "off" is supported (null would block disabling) and every
// effort level maps to itself so the adapter forwards it verbatim.
const M3_THINKING_LEVEL_MAP = {
off: "off",
minimal: "minimal",
low: "low",
medium: "medium",
high: "high",
xhigh: "xhigh",
max: "max",
} as const;

// MiniMax-M2.7 is an always-on reasoning model: thinking cannot be disabled
// ("off" is null) and no effort selection is exposed.
const M2_7_THINKING_LEVEL_MAP = {
off: null,
} as const;

// The pi-ai `Model.input` field is typed `("text" | "image")[]`; MiniMax-M3
// also accepts video. Declare the wider literal here so the catalog records
// the real input modalities, then assert at the model boundary (see below).
const M3_INPUT = ["text", "image", "video"] as unknown as ("text" | "image")[];

/**
* Models for the global MiniMax provider.
*
* Refreshed to the OpenAI-compatible endpoint (`https://api.minimax.io/v1`)
* with MiniMax-M3 at $0.6/$2.4/$0.12 input/output/cache-read pricing, a 1M
* token context window, text/image/video input, and adaptive/disabled
* thinking. MiniMax-M2.7 stays an always-on reasoning text model.
*/
export const MINIMAX_MODELS = {
"MiniMax-M3": {
id: "MiniMax-M3",
provider: "minimax",
name: "MiniMax-M3",
api: "openai-completions",
baseUrl: OPENAI_BASE_URL,
reasoning: true,
thinkingLevelMap: M3_THINKING_LEVEL_MAP,
input: M3_INPUT,
cost: {
input: 0.6,
output: 2.4,
cacheRead: 0.12,
cacheWrite: 0,
},
contextWindow: 1000000,
maxTokens: 128000,
compat: {
thinkingFormat: "deepseek",
},
} satisfies Model<"openai-completions">,
"MiniMax-M2.7": {
id: "MiniMax-M2.7",
provider: "minimax",
name: "MiniMax-M2.7",
api: "openai-completions",
baseUrl: OPENAI_BASE_URL,
reasoning: true,
thinkingLevelMap: M2_7_THINKING_LEVEL_MAP,
input: ["text"],
cost: {
input: 0.3,
output: 1.2,
cacheRead: 0.06,
cacheWrite: 0.375,
},
contextWindow: 204800,
maxTokens: 131072,
compat: {
thinkingFormat: "deepseek",
},
} satisfies Model<"openai-completions">,
};

/**
* Models for the China MiniMax provider, mirroring the global catalog against
* the China OpenAI-compatible endpoint (`https://api.minimaxi.com/v1`).
*/
export const MINIMAX_CN_MODELS = {
"MiniMax-M3": {
...MINIMAX_MODELS["MiniMax-M3"],
provider: "minimax-cn",
baseUrl: OPENAI_BASE_URL_CN,
} satisfies Model<"openai-completions">,
"MiniMax-M2.7": {
...MINIMAX_MODELS["MiniMax-M2.7"],
provider: "minimax-cn",
baseUrl: OPENAI_BASE_URL_CN,
} satisfies Model<"openai-completions">,
};
19 changes: 19 additions & 0 deletions packages/runtime/src/models/providers/minimax.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {
createProvider,
envApiKeyAuth,
type Provider,
} from "@earendil-works/pi-ai";
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";

import { MINIMAX_MODELS } from "./minimax.models";

export function minimaxProvider(): Provider<"openai-completions"> {
return createProvider({
id: "minimax",
name: "MiniMax",
baseUrl: "https://api.minimax.io/v1",
auth: { apiKey: envApiKeyAuth("MiniMax API key", ["MINIMAX_API_KEY"]) },
models: Object.values(MINIMAX_MODELS),
api: openAICompletionsApi(),
});
}
48 changes: 48 additions & 0 deletions packages/runtime/tests/models/providers/minimax/minimax.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, expect, test } from "bun:test";

import { minimaxProvider } from "../../../../src/models/providers/minimax";
import { minimaxCnProvider } from "../../../../src/models/providers/minimax-cn";

const MODELS = ["MiniMax-M3", "MiniMax-M2.7"] as const;

describe("minimaxProvider", () => {
test("exposes the OpenAI-compatible global endpoint and the M3/M2.7 models", () => {
const provider = minimaxProvider();
const models = provider.getModels();

expect(provider.id).toBe("minimax");
expect(provider.baseUrl).toBe("https://api.minimax.io/v1");
expect(models.map((model) => model.id).sort()).toEqual([...MODELS].sort());

const m3 = models.find((model) => model.id === "MiniMax-M3");
expect(m3).toBeDefined();
expect(m3?.api).toBe("openai-completions");
expect(m3?.baseUrl).toBe("https://api.minimax.io/v1");
expect(m3?.provider).toBe("minimax");
// MiniMax-M3 accepts text, image, and video input.
expect(m3?.input.map(String)).toEqual(["text", "image", "video"]);
expect(m3?.contextWindow).toBe(1000000);
expect(m3?.cost).toEqual({
input: 0.6,
output: 2.4,
cacheRead: 0.12,
cacheWrite: 0,
});
});
});

describe("minimaxCnProvider", () => {
test("targets the China OpenAI-compatible endpoint for the same models", () => {
const provider = minimaxCnProvider();
const models = provider.getModels();

expect(provider.id).toBe("minimax-cn");
expect(provider.baseUrl).toBe("https://api.minimaxi.com/v1");
expect(models.map((model) => model.id).sort()).toEqual([...MODELS].sort());

for (const model of models) {
expect(model.provider).toBe("minimax-cn");
expect(model.baseUrl).toBe("https://api.minimaxi.com/v1");
}
});
});