diff --git a/packages/runtime/src/models/providers/builtin-providers.ts b/packages/runtime/src/models/providers/builtin-providers.ts index 57b80c7f..a8e574ce 100644 --- a/packages/runtime/src/models/providers/builtin-providers.ts +++ b/packages/runtime/src/models/providers/builtin-providers.ts @@ -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"; @@ -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. */ diff --git a/packages/runtime/src/models/providers/minimax-cn.ts b/packages/runtime/src/models/providers/minimax-cn.ts new file mode 100644 index 00000000..0cca12af --- /dev/null +++ b/packages/runtime/src/models/providers/minimax-cn.ts @@ -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(), + }); +} diff --git a/packages/runtime/src/models/providers/minimax.models.ts b/packages/runtime/src/models/providers/minimax.models.ts new file mode 100644 index 00000000..e332d550 --- /dev/null +++ b/packages/runtime/src/models/providers/minimax.models.ts @@ -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">, +}; diff --git a/packages/runtime/src/models/providers/minimax.ts b/packages/runtime/src/models/providers/minimax.ts new file mode 100644 index 00000000..04a1f56e --- /dev/null +++ b/packages/runtime/src/models/providers/minimax.ts @@ -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(), + }); +} diff --git a/packages/runtime/tests/models/providers/minimax/minimax.test.ts b/packages/runtime/tests/models/providers/minimax/minimax.test.ts new file mode 100644 index 00000000..6cf1bd87 --- /dev/null +++ b/packages/runtime/tests/models/providers/minimax/minimax.test.ts @@ -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"); + } + }); +});