From 7e702bdf8978c3ab95de0bc3a3ebeeaf075e3d2c Mon Sep 17 00:00:00 2001 From: octo-patch Date: Mon, 16 Mar 2026 21:43:12 +0800 Subject: [PATCH 1/2] feat: add MiniMax as a direct LLM provider - Add MiniMax-M2.5 and MiniMax-M2.5-highspeed models to the model selector - Route MiniMax models directly via @ai-sdk/openai (OpenAI-compatible API) instead of through the AI Gateway, enabling users to use their own API key - Add MINIMAX_API_KEY environment variable support - Add MiniMax provider group in the model selector UI - Add integration tests for MiniMax API - Update README to mention MiniMax support --- .env.example | 4 + README.md | 11 ++- components/multimodal-input.tsx | 1 + lib/ai/models.ts | 13 +++ lib/ai/providers.ts | 15 ++++ package.json | 1 + pnpm-lock.yaml | 69 ++++++++++++++- tests/e2e/model-selector.test.ts | 2 +- tests/minimax-integration.test.ts | 137 ++++++++++++++++++++++++++++++ 9 files changed, 249 insertions(+), 4 deletions(-) create mode 100644 tests/minimax-integration.test.ts diff --git a/.env.example b/.env.example index 42bdcf2c91..18a97bc55b 100644 --- a/.env.example +++ b/.env.example @@ -11,6 +11,10 @@ AUTH_SECRET=**** AI_GATEWAY_API_KEY=**** +# Optional: MiniMax API key for using MiniMax models directly +# Get your API key at https://platform.minimax.io +MINIMAX_API_KEY=**** + # Instructions to create a Vercel Blob Store here: https://vercel.com/docs/vercel-blob BLOB_READ_WRITE_TOKEN=**** diff --git a/README.md b/README.md index 0c54ca8e7a..c4cb9bafe6 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ - [AI SDK](https://ai-sdk.dev/docs/introduction) - Unified API for generating text, structured objects, and tool calls with LLMs - Hooks for building dynamic chat and generative user interfaces - - Supports OpenAI, Anthropic, Google, xAI, and other model providers via AI Gateway + - Supports OpenAI, Anthropic, Google, xAI, MiniMax, and other model providers - [shadcn/ui](https://ui.shadcn.com) - Styling with [Tailwind CSS](https://tailwindcss.com) - Component primitives from [Radix UI](https://radix-ui.com) for accessibility and flexibility @@ -36,7 +36,7 @@ ## Model Providers -This template uses the [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) to access multiple AI models through a unified interface. The default model is [OpenAI](https://openai.com) GPT-4.1 Mini, with support for Anthropic, Google, and xAI models. +This template uses the [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) to access multiple AI models through a unified interface. The default model is [OpenAI](https://openai.com) GPT-4.1 Mini, with support for Anthropic, Google, xAI, and [MiniMax](https://platform.minimax.io) models. ### AI Gateway Authentication @@ -44,6 +44,13 @@ This template uses the [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) t **For non-Vercel deployments**: You need to provide an AI Gateway API key by setting the `AI_GATEWAY_API_KEY` environment variable in your `.env.local` file. +### MiniMax + +[MiniMax](https://platform.minimax.io) models are accessed directly via the OpenAI-compatible API (not via AI Gateway). Set the `MINIMAX_API_KEY` environment variable to enable MiniMax models. Available models: + +- **MiniMax M2.5** – Peak performance with 204K context window +- **MiniMax M2.5 Highspeed** – Same performance, faster and more agile + With the [AI SDK](https://ai-sdk.dev/docs/introduction), you can also switch to direct LLM providers like [OpenAI](https://openai.com), [Anthropic](https://anthropic.com), [Cohere](https://cohere.com/), and [many more](https://ai-sdk.dev/providers/ai-sdk-providers) with just a few lines of code. ## Deploy Your Own diff --git a/components/multimodal-input.tsx b/components/multimodal-input.tsx index ed62df19c9..f883246a02 100644 --- a/components/multimodal-input.tsx +++ b/components/multimodal-input.tsx @@ -482,6 +482,7 @@ function PureModelSelectorCompact({ openai: "OpenAI", google: "Google", xai: "xAI", + minimax: "MiniMax", reasoning: "Reasoning", }; diff --git a/lib/ai/models.ts b/lib/ai/models.ts index fb5cc70725..cb4ad10dfd 100644 --- a/lib/ai/models.ts +++ b/lib/ai/models.ts @@ -49,6 +49,19 @@ export const chatModels: ChatModel[] = [ provider: "xai", description: "Fast with 30K context", }, + // MiniMax + { + id: "minimax/MiniMax-M2.5", + name: "MiniMax M2.5", + provider: "minimax", + description: "Peak performance with 204K context window", + }, + { + id: "minimax/MiniMax-M2.5-highspeed", + name: "MiniMax M2.5 Highspeed", + provider: "minimax", + description: "Same performance, faster and more agile", + }, // Reasoning models (extended thinking) { id: "anthropic/claude-3.7-sonnet-thinking", diff --git a/lib/ai/providers.ts b/lib/ai/providers.ts index a93fc8a4b8..e516ca08f3 100644 --- a/lib/ai/providers.ts +++ b/lib/ai/providers.ts @@ -1,4 +1,5 @@ import { gateway } from "@ai-sdk/gateway"; +import { createOpenAI } from "@ai-sdk/openai"; import { customProvider, extractReasoningMiddleware, @@ -6,6 +7,14 @@ import { } from "ai"; import { isTestEnvironment } from "../constants"; +function getMiniMaxProvider() { + return createOpenAI({ + name: "minimax", + apiKey: process.env.MINIMAX_API_KEY, + baseURL: process.env.MINIMAX_BASE_URL ?? "https://api.minimax.io/v1", + }); +} + const THINKING_SUFFIX_REGEX = /-thinking$/; export const myProvider = isTestEnvironment @@ -32,6 +41,12 @@ export function getLanguageModel(modelId: string) { return myProvider.languageModel(modelId); } + // MiniMax models use direct API (not via AI Gateway) + if (modelId.startsWith("minimax/")) { + const minimax = getMiniMaxProvider(); + return minimax(modelId.replace("minimax/", "")); + } + const isReasoningModel = modelId.endsWith("-thinking") || (modelId.includes("reasoning") && !modelId.includes("non-reasoning")); diff --git a/package.json b/package.json index 15224e88e2..3997cf79e6 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ }, "dependencies": { "@ai-sdk/gateway": "^3.0.15", + "@ai-sdk/openai": "^3.0.41", "@ai-sdk/provider": "^3.0.3", "@ai-sdk/react": "3.0.39", "@codemirror/lang-javascript": "^6.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a4228af3f8..bd1f4bfcfc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@ai-sdk/gateway': specifier: ^3.0.15 version: 3.0.15(zod@3.25.76) + '@ai-sdk/openai': + specifier: ^3.0.41 + version: 3.0.41(zod@3.25.76) '@ai-sdk/provider': specifier: ^3.0.3 version: 3.0.3 @@ -312,6 +315,18 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/openai@3.0.41': + resolution: {integrity: sha512-IZ42A+FO+vuEQCVNqlnAPYQnnUpUfdJIwn1BEDOBywiEHa23fw7PahxVtlX9zm3/zMvTW4JKPzWyvAgDu+SQ2A==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@4.0.19': + resolution: {integrity: sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/provider-utils@4.0.7': resolution: {integrity: sha512-ItzTdBxRLieGz1GHPwl9X3+HKfwTfFd9MdIa91aXRnOjUVRw68ENjAGKm3FcXGsBLkXDLaFWgjbTVdXe2igs2w==} engines: {node: '>=18'} @@ -322,6 +337,10 @@ packages: resolution: {integrity: sha512-qGPYdoAuECaUXPrrz0BPX1SacZQuJ6zky0aakxpW89QW1hrY0eF4gcFm/3L9Pk8C5Fwe+RvBf2z7ZjDhaPjnlg==} engines: {node: '>=18'} + '@ai-sdk/provider@3.0.8': + resolution: {integrity: sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==} + engines: {node: '>=18'} + '@ai-sdk/react@3.0.39': resolution: {integrity: sha512-Q/39hAazwxItCbqEDWC4pa3+HXLVvTzeu/zu7ghANqRNCxzmqBb/GYEIU/wiYNRS05hP6R1l9bA9S4U6sx0iTA==} engines: {node: '>=18'} @@ -372,24 +391,28 @@ packages: engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [musl] '@biomejs/cli-linux-arm64@2.3.11': resolution: {integrity: sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [glibc] '@biomejs/cli-linux-x64-musl@2.3.11': resolution: {integrity: sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [musl] '@biomejs/cli-linux-x64@2.3.11': resolution: {integrity: sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [glibc] '@biomejs/cli-win32-arm64@2.3.11': resolution: {integrity: sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw==} @@ -1083,89 +1106,105 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -1263,24 +1302,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@next/swc-linux-arm64-musl@16.0.10': resolution: {integrity: sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@next/swc-linux-x64-gnu@16.0.10': resolution: {integrity: sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@next/swc-linux-x64-musl@16.0.10': resolution: {integrity: sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@next/swc-win32-arm64-msvc@16.0.10': resolution: {integrity: sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg==} @@ -2243,24 +2286,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.16': resolution: {integrity: sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.16': resolution: {integrity: sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.16': resolution: {integrity: sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.16': resolution: {integrity: sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==} @@ -2466,7 +2513,6 @@ packages: '@vercel/postgres@0.10.0': resolution: {integrity: sha512-fSD23DxGND40IzSkXjcFcxr53t3Tiym59Is0jSYIFpG4/0f0KO9SGtcp1sXiebvPaGe7N/tU05cH4yt2S6/IPg==} engines: {node: '>=18.14'} - deprecated: '@vercel/postgres is deprecated. If you are setting up a new database, you can choose an alternate storage solution from the Vercel Marketplace. If you had an existing Vercel Postgres database, it should have been migrated to Neon as a native Vercel integration. You can find more details and the guide to migrate to Neon''s SDKs here: https://neon.com/docs/guides/vercel-postgres-transition-guide' '@xyflow/react@12.10.0': resolution: {integrity: sha512-eOtz3whDMWrB4KWVatIBrKuxECHqip6PfA8fTpaS2RUGVpiEAe+nqDKsLqkViVWxDGreq0lWX71Xth/SPAzXiw==} @@ -3113,24 +3159,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} @@ -4119,6 +4169,19 @@ snapshots: '@vercel/oidc': 3.1.0 zod: 3.25.76 + '@ai-sdk/openai@3.0.41(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.19(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/provider-utils@4.0.19(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.6 + zod: 3.25.76 + '@ai-sdk/provider-utils@4.0.7(zod@3.25.76)': dependencies: '@ai-sdk/provider': 3.0.3 @@ -4130,6 +4193,10 @@ snapshots: dependencies: json-schema: 0.4.0 + '@ai-sdk/provider@3.0.8': + dependencies: + json-schema: 0.4.0 + '@ai-sdk/react@3.0.39(react@19.0.1)(zod@3.25.76)': dependencies: '@ai-sdk/provider-utils': 4.0.7(zod@3.25.76) diff --git a/tests/e2e/model-selector.test.ts b/tests/e2e/model-selector.test.ts index 548aba93ca..ddb8d551e6 100644 --- a/tests/e2e/model-selector.test.ts +++ b/tests/e2e/model-selector.test.ts @@ -1,6 +1,6 @@ import { expect, test } from "@playwright/test"; -const MODEL_BUTTON_REGEX = /Gemini|Claude|GPT|Grok/i; +const MODEL_BUTTON_REGEX = /Gemini|Claude|GPT|Grok|MiniMax/i; test.describe("Model Selector", () => { test.beforeEach(async ({ page }) => { diff --git a/tests/minimax-integration.test.ts b/tests/minimax-integration.test.ts new file mode 100644 index 0000000000..de8f0e0948 --- /dev/null +++ b/tests/minimax-integration.test.ts @@ -0,0 +1,137 @@ +/** + * Integration test for MiniMax provider. + * Run with: MINIMAX_API_KEY= npx tsx tests/minimax-integration.test.ts + */ + +const API_KEY = process.env.MINIMAX_API_KEY; +const BASE_URL = process.env.MINIMAX_BASE_URL || "https://api.minimax.io/v1"; + +async function testBasicChat() { + console.log("Testing MiniMax basic chat..."); + const response = await fetch(`${BASE_URL}/chat/completions`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${API_KEY}`, + }, + body: JSON.stringify({ + model: "MiniMax-M2.5", + messages: [{ role: "user", content: 'Say "MiniMax test passed"' }], + max_tokens: 20, + temperature: 1.0, + }), + }); + + if (!response.ok) { + throw new Error(`Chat API error: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + const content = data.choices?.[0]?.message?.content; + if (!content) { + throw new Error("No content in response"); + } + console.log(` Response: ${content}`); + console.log(" PASSED"); +} + +async function testStreaming() { + console.log("Testing MiniMax streaming..."); + const response = await fetch(`${BASE_URL}/chat/completions`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${API_KEY}`, + }, + body: JSON.stringify({ + model: "MiniMax-M2.5", + messages: [{ role: "user", content: "Count 1 to 3" }], + max_tokens: 50, + stream: true, + temperature: 1.0, + }), + }); + + if (!response.ok) { + throw new Error( + `Streaming API error: ${response.status} ${response.statusText}` + ); + } + + const reader = response.body!.getReader(); + const decoder = new TextDecoder(); + let chunks = 0; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + const text = decoder.decode(value, { stream: true }); + if (text.includes("data:")) chunks++; + } + + if (chunks < 1) { + throw new Error("Expected multiple streaming chunks"); + } + console.log(` Received ${chunks} chunks`); + console.log(" PASSED"); +} + +async function testModelList() { + console.log("Testing model list includes MiniMax..."); + + // Dynamically import models to verify they're correctly defined + const models = await import("../lib/ai/models"); + + const minimaxModels = models.chatModels.filter((m) => + m.id.startsWith("minimax/") + ); + + if (minimaxModels.length !== 2) { + throw new Error(`Expected 2 MiniMax models, got ${minimaxModels.length}`); + } + + const hasM25 = minimaxModels.some((m) => m.id === "minimax/MiniMax-M2.5"); + const hasHighspeed = minimaxModels.some( + (m) => m.id === "minimax/MiniMax-M2.5-highspeed" + ); + + if (!hasM25 || !hasHighspeed) { + throw new Error("Missing expected MiniMax models"); + } + + if (!models.allowedModelIds.has("minimax/MiniMax-M2.5")) { + throw new Error("MiniMax-M2.5 not in allowedModelIds"); + } + + const minimaxGroup = models.modelsByProvider["minimax"]; + if (!minimaxGroup || minimaxGroup.length !== 2) { + throw new Error("MiniMax not properly grouped in modelsByProvider"); + } + + console.log(" Models:", minimaxModels.map((m) => m.id).join(", ")); + console.log(" PASSED"); +} + +async function main() { + console.log("=== MiniMax Integration Tests ===\n"); + + // Model list test (no API key needed) + await testModelList(); + + if (!API_KEY) { + console.log( + "\nSkipping API tests (set MINIMAX_API_KEY to run full tests)" + ); + return; + } + + await testBasicChat(); + await testStreaming(); + + console.log("\n=== All tests passed ==="); +} + +main().catch((error) => { + console.error("\nTEST FAILED:", error.message); + process.exit(1); +}); From 525bfbab5d7595505d1aed0b57c523d5d773a866 Mon Sep 17 00:00:00 2001 From: octo-patch Date: Thu, 19 Mar 2026 01:31:07 +0800 Subject: [PATCH 2/2] feat: upgrade MiniMax default model to M2.7 - Add MiniMax-M2.7 and MiniMax-M2.7-highspeed to model list - Place M2.7 models before existing M2.5 models - Keep all previous models as alternatives - Update integration tests to verify 4 models with M2.7 first - Update README with M2.7 model descriptions --- README.md | 2 ++ lib/ai/models.ts | 12 ++++++++++++ tests/minimax-integration.test.ts | 29 ++++++++++++++++++++--------- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index c4cb9bafe6..83ebb5f95a 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,8 @@ This template uses the [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) t [MiniMax](https://platform.minimax.io) models are accessed directly via the OpenAI-compatible API (not via AI Gateway). Set the `MINIMAX_API_KEY` environment variable to enable MiniMax models. Available models: +- **MiniMax M2.7** – Latest flagship model with enhanced reasoning and coding +- **MiniMax M2.7 Highspeed** – High-speed version of M2.7 for low-latency scenarios - **MiniMax M2.5** – Peak performance with 204K context window - **MiniMax M2.5 Highspeed** – Same performance, faster and more agile diff --git a/lib/ai/models.ts b/lib/ai/models.ts index cb4ad10dfd..ef1c706827 100644 --- a/lib/ai/models.ts +++ b/lib/ai/models.ts @@ -50,6 +50,18 @@ export const chatModels: ChatModel[] = [ description: "Fast with 30K context", }, // MiniMax + { + id: "minimax/MiniMax-M2.7", + name: "MiniMax M2.7", + provider: "minimax", + description: "Latest flagship model with enhanced reasoning and coding", + }, + { + id: "minimax/MiniMax-M2.7-highspeed", + name: "MiniMax M2.7 Highspeed", + provider: "minimax", + description: "High-speed version of M2.7 for low-latency scenarios", + }, { id: "minimax/MiniMax-M2.5", name: "MiniMax M2.5", diff --git a/tests/minimax-integration.test.ts b/tests/minimax-integration.test.ts index de8f0e0948..b802c69598 100644 --- a/tests/minimax-integration.test.ts +++ b/tests/minimax-integration.test.ts @@ -15,7 +15,7 @@ async function testBasicChat() { Authorization: `Bearer ${API_KEY}`, }, body: JSON.stringify({ - model: "MiniMax-M2.5", + model: "MiniMax-M2.7", messages: [{ role: "user", content: 'Say "MiniMax test passed"' }], max_tokens: 20, temperature: 1.0, @@ -44,7 +44,7 @@ async function testStreaming() { Authorization: `Bearer ${API_KEY}`, }, body: JSON.stringify({ - model: "MiniMax-M2.5", + model: "MiniMax-M2.7", messages: [{ role: "user", content: "Count 1 to 3" }], max_tokens: 50, stream: true, @@ -86,25 +86,36 @@ async function testModelList() { m.id.startsWith("minimax/") ); - if (minimaxModels.length !== 2) { - throw new Error(`Expected 2 MiniMax models, got ${minimaxModels.length}`); + if (minimaxModels.length !== 4) { + throw new Error(`Expected 4 MiniMax models, got ${minimaxModels.length}`); } + const hasM27 = minimaxModels.some((m) => m.id === "minimax/MiniMax-M2.7"); + const hasM27Highspeed = minimaxModels.some( + (m) => m.id === "minimax/MiniMax-M2.7-highspeed" + ); const hasM25 = minimaxModels.some((m) => m.id === "minimax/MiniMax-M2.5"); - const hasHighspeed = minimaxModels.some( + const hasM25Highspeed = minimaxModels.some( (m) => m.id === "minimax/MiniMax-M2.5-highspeed" ); - if (!hasM25 || !hasHighspeed) { + if (!hasM27 || !hasM27Highspeed || !hasM25 || !hasM25Highspeed) { throw new Error("Missing expected MiniMax models"); } - if (!models.allowedModelIds.has("minimax/MiniMax-M2.5")) { - throw new Error("MiniMax-M2.5 not in allowedModelIds"); + // M2.7 should come before M2.5 in the list + const m27Index = minimaxModels.findIndex((m) => m.id === "minimax/MiniMax-M2.7"); + const m25Index = minimaxModels.findIndex((m) => m.id === "minimax/MiniMax-M2.5"); + if (m27Index > m25Index) { + throw new Error("MiniMax-M2.7 should appear before MiniMax-M2.5 in model list"); + } + + if (!models.allowedModelIds.has("minimax/MiniMax-M2.7")) { + throw new Error("MiniMax-M2.7 not in allowedModelIds"); } const minimaxGroup = models.modelsByProvider["minimax"]; - if (!minimaxGroup || minimaxGroup.length !== 2) { + if (!minimaxGroup || minimaxGroup.length !== 4) { throw new Error("MiniMax not properly grouped in modelsByProvider"); }