diff --git a/packages/types/src/__tests__/provider-settings.test.ts b/packages/types/src/__tests__/provider-settings.test.ts index 724fc20f34..45797ad2f5 100644 --- a/packages/types/src/__tests__/provider-settings.test.ts +++ b/packages/types/src/__tests__/provider-settings.test.ts @@ -1,3 +1,4 @@ +import { describe, expect, it } from "vitest" import { getApiProtocol } from "../provider-settings.js" describe("getApiProtocol", () => { @@ -88,6 +89,17 @@ describe("getApiProtocol", () => { }) }) + describe("MiniMax provider", () => { + it("should return 'anthropic' for MiniMax M3 and M2.x models", () => { + expect(getApiProtocol("minimax", "MiniMax-M3-1M")).toBe("anthropic") + expect(getApiProtocol("minimax", "MiniMax-M3-512k")).toBe("anthropic") + expect(getApiProtocol("minimax", "MiniMax-M2.7")).toBe("anthropic") + expect(getApiProtocol("minimax", "MiniMax-M2.5")).toBe("anthropic") + expect(getApiProtocol("minimax", "MiniMax-M2")).toBe("anthropic") + expect(getApiProtocol("minimax")).toBe("anthropic") + }) + }) + describe("Edge cases", () => { it("should return 'openai' when provider is undefined", () => { expect(getApiProtocol(undefined)).toBe("openai") diff --git a/packages/types/src/providers/minimax.ts b/packages/types/src/providers/minimax.ts index 2ac02cf9af..196983ec7d 100644 --- a/packages/types/src/providers/minimax.ts +++ b/packages/types/src/providers/minimax.ts @@ -5,7 +5,7 @@ import type { ModelInfo } from "../model.js" // https://platform.minimax.io/docs/guides/pricing-paygo // https://platform.minimax.io/docs/guides/pricing-tokenplan export type MinimaxModelId = keyof typeof minimaxModels -export const minimaxDefaultModelId: MinimaxModelId = "MiniMax-M2.7" +export const minimaxDefaultModelId: MinimaxModelId = "MiniMax-M3-1M" export const minimaxModels = { "MiniMax-M2.5": { @@ -68,6 +68,44 @@ export const minimaxModels = { description: "MiniMax M2.7 highspeed: same performance as M2.7 but with faster response (approximately 100 tps vs 60 tps). See pricing at https://platform.minimax.io/docs/guides/pricing-paygo. Requires TokenPlan High-Speed subscription for use with TokenPlan keys. Note: When using TokenPlan, usage is billed per request, not per token.", }, + "MiniMax-M3-512k": { + maxTokens: 65_536, + contextWindow: 524_288, + supportsImages: true, + supportsPromptCache: true, + includedTools: ["search_and_replace"], + excludedTools: ["apply_diff"], + preserveReasoning: true, + inputPrice: 0.3, + outputPrice: 1.2, + // M3 routes through the Anthropic Messages path on api.minimax.io/anthropic + // with client-side cache_control injection active, so cache_creation_input_tokens + // are reported and billed. Matches the MiniMax write price shared by the M2 + // family (same vendor/pricing tier: $0.3 in / $1.2 out / $0.06 cache read). + cacheWritesPrice: 0.375, + cacheReadsPrice: 0.06, + description: + "MiniMax M3 (512K context) — the latest MiniMax model with multimodal input, stronger agentic reasoning and tool use, exposed via a 512K-token context window. See pricing at https://platform.minimax.io/docs/guides/pricing-paygo. Note: When using TokenPlan, usage is billed per request, not per token.", + }, + "MiniMax-M3-1M": { + maxTokens: 131_072, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: true, + includedTools: ["search_and_replace"], + excludedTools: ["apply_diff"], + preserveReasoning: true, + inputPrice: 0.3, + outputPrice: 1.2, + // M3 routes through the Anthropic Messages path on api.minimax.io/anthropic + // with client-side cache_control injection active, so cache_creation_input_tokens + // are reported and billed. Matches the MiniMax write price shared by the M2 + // family (same vendor/pricing tier: $0.3 in / $1.2 out / $0.06 cache read). + cacheWritesPrice: 0.375, + cacheReadsPrice: 0.06, + description: + "MiniMax M3 (1M context) — the latest MiniMax model with multimodal input, stronger agentic reasoning and tool use, exposed via a 1M-token context window. See pricing at https://platform.minimax.io/docs/guides/pricing-paygo. Note: When using TokenPlan, usage is billed per request, not per token.", + }, "MiniMax-M2": { maxTokens: 16_384, contextWindow: 204_800, diff --git a/src/api/providers/__tests__/minimax.spec.ts b/src/api/providers/__tests__/minimax.spec.ts index d87ae1190b..e0a692d598 100644 --- a/src/api/providers/__tests__/minimax.spec.ts +++ b/src/api/providers/__tests__/minimax.spec.ts @@ -121,6 +121,50 @@ describe("MiniMaxHandler", () => { expect(model.info.cacheReadsPrice).toBe(0.03) }) + it("should return MiniMax-M3-512k model with correct configuration", () => { + const testModelId: MinimaxModelId = "MiniMax-M3-512k" + const handlerWithModel = new MiniMaxHandler({ + apiModelId: testModelId, + minimaxApiKey: "test-minimax-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual(minimaxModels[testModelId]) + expect(model.info.contextWindow).toBe(524_288) + expect(model.info.maxTokens).toBe(65_536) + expect(model.info.supportsImages).toBe(true) + expect(model.info.supportsPromptCache).toBe(true) + expect(model.info.inputPrice).toBe(0.3) + expect(model.info.outputPrice).toBe(1.2) + expect(model.info.cacheWritesPrice).toBe(0.375) + expect(model.info.cacheReadsPrice).toBe(0.06) + }) + + it("should return MiniMax-M3-1M model with correct configuration", () => { + const testModelId: MinimaxModelId = "MiniMax-M3-1M" + const handlerWithModel = new MiniMaxHandler({ + apiModelId: testModelId, + minimaxApiKey: "test-minimax-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual(minimaxModels[testModelId]) + expect(model.info.contextWindow).toBe(1_048_576) + expect(model.info.maxTokens).toBe(131_072) + expect(model.info.supportsImages).toBe(true) + expect(model.info.supportsPromptCache).toBe(true) + expect(model.info.inputPrice).toBe(0.3) + expect(model.info.outputPrice).toBe(1.2) + expect(model.info.cacheWritesPrice).toBe(0.375) + expect(model.info.cacheReadsPrice).toBe(0.06) + }) + + it(`should default to ${minimaxDefaultModelId} model`, () => { + const handlerDefault = new MiniMaxHandler({ minimaxApiKey: "test-minimax-api-key" }) + const model = handlerDefault.getModel() + expect(model.id).toBe(minimaxDefaultModelId) + }) + it("should return MiniMax-M2-Stable model with correct configuration", () => { const testModelId: MinimaxModelId = "MiniMax-M2-Stable" const handlerWithModel = new MiniMaxHandler({ @@ -193,10 +237,20 @@ describe("MiniMaxHandler", () => { expect(model.info).toEqual(minimaxModels[minimaxDefaultModelId]) }) - it("should default to MiniMax-M2.7 model", () => { + it(`should default to ${minimaxDefaultModelId} model`, () => { const handlerDefault = new MiniMaxHandler({ minimaxApiKey: "test-minimax-api-key" }) const model = handlerDefault.getModel() + expect(model.id).toBe(minimaxDefaultModelId) + }) + + it("should still resolve MiniMax-M2.7 when explicitly requested (back-compat)", () => { + const handlerWithModel = new MiniMaxHandler({ + apiModelId: "MiniMax-M2.7", + minimaxApiKey: "test-minimax-api-key", + }) + const model = handlerWithModel.getModel() expect(model.id).toBe("MiniMax-M2.7") + expect(model.info).toEqual(minimaxModels["MiniMax-M2.7"]) }) }) @@ -327,6 +381,251 @@ describe("MiniMaxHandler", () => { ) }) + // Regression guard for the "double-think" hang: MiniMax M3 on the Anthropic + // endpoint defaults thinking OFF, so we must explicitly enable adaptive + // thinking and never pass budget_tokens (M-series is a binary toggle). + // Ported from kilo-code opencode provider transform (lines 661, 1208-1211). + it("should enable adaptive thinking for MiniMax-M3 models on Anthropic endpoint", async () => { + for (const modelId of ["MiniMax-M3-512k", "MiniMax-M3-1M"] as const) { + mockCreate.mockResolvedValueOnce({ + [Symbol.asyncIterator]: () => ({ + async next() { + return { done: true } + }, + }), + }) + + const handlerForModel = new MiniMaxHandler({ + apiModelId: modelId, + minimaxApiKey: "test-minimax-api-key", + }) + const gen = handlerForModel.createMessage("test", []) + await gen.next() + + expect(mockCreate).toHaveBeenLastCalledWith( + expect.objectContaining({ + model: modelId, + thinking: { type: "adaptive" }, + }), + ) + // M-series is a binary toggle: budget_tokens must NEVER be sent. + const lastCall = mockCreate.mock.calls[mockCreate.mock.calls.length - 1][0] + expect(lastCall.thinking).not.toHaveProperty("budget_tokens") + expect(lastCall).not.toHaveProperty("budget_tokens") + } + }) + + it("should NOT enable adaptive thinking for MiniMax-M2.x models", async () => { + for (const modelId of ["MiniMax-M2", "MiniMax-M2.5", "MiniMax-M2.7"] as const) { + mockCreate.mockResolvedValueOnce({ + [Symbol.asyncIterator]: () => ({ + async next() { + return { done: true } + }, + }), + }) + + const handlerForModel = new MiniMaxHandler({ + apiModelId: modelId, + minimaxApiKey: "test-minimax-api-key", + }) + const gen = handlerForModel.createMessage("test", []) + await gen.next() + + const lastCall = mockCreate.mock.calls[mockCreate.mock.calls.length - 1][0] + expect(lastCall.thinking).toBeUndefined() + } + }) + + // Regression guard for the "hang" symptom: M-series sampling parameters + // must match kilo-code defaults (transform.ts lines 488-524). M2.x and M3 + // both use top_p=0.95; M2.x additionally uses top_k=40. + it("should pass MiniMax-M2 sampling params (top_p=0.95, top_k=40)", async () => { + mockCreate.mockResolvedValueOnce({ + [Symbol.asyncIterator]: () => ({ + async next() { + return { done: true } + }, + }), + }) + + const handlerForModel = new MiniMaxHandler({ + apiModelId: "MiniMax-M2.7", + minimaxApiKey: "test-minimax-api-key", + }) + const gen = handlerForModel.createMessage("test", []) + await gen.next() + + expect(mockCreate).toHaveBeenLastCalledWith( + expect.objectContaining({ + temperature: 1, + top_p: 0.95, + top_k: 40, + }), + ) + }) + + it("should pass MiniMax-M3 sampling params (top_p=0.95, no top_k)", async () => { + mockCreate.mockResolvedValueOnce({ + [Symbol.asyncIterator]: () => ({ + async next() { + return { done: true } + }, + }), + }) + + const handlerForModel = new MiniMaxHandler({ + apiModelId: "MiniMax-M3-1M", + minimaxApiKey: "test-minimax-api-key", + }) + const gen = handlerForModel.createMessage("test", []) + await gen.next() + + const lastCall = mockCreate.mock.calls[mockCreate.mock.calls.length - 1][0] + expect(lastCall).toMatchObject({ + temperature: 1, + top_p: 0.95, + }) + expect(lastCall).not.toHaveProperty("top_k") + }) + + // Regression guard for CR-3: the M-series double-think/hang fix depends + // on the exact `temperature: 1.0` default. Any user-supplied temperature + // must be ignored on the M-series path. + it("should ignore user-supplied temperature for MiniMax-M3 M-series request", async () => { + mockCreate.mockResolvedValueOnce({ + [Symbol.asyncIterator]: () => ({ + async next() { + return { done: true } + }, + }), + }) + + const handlerWithCustomTemp = new MiniMaxHandler({ + apiModelId: "MiniMax-M3-1M", + minimaxApiKey: "test-minimax-api-key", + // Attempt to override the M-series temperature hard-coded in + // `getMSeriesRequestParams`. If this propagates, the hang-fix + // contract is broken. + modelTemperature: 0.2 as any, + }) + + const messageGenerator = handlerWithCustomTemp.createMessage("system", []) + await messageGenerator.next() + + const lastCall = mockCreate.mock.calls[mockCreate.mock.calls.length - 1][0] + expect(lastCall).toMatchObject({ temperature: 1.0, top_p: 0.95 }) + }) + + it("should ignore user-supplied temperature for MiniMax-M2 M-series request", async () => { + mockCreate.mockResolvedValueOnce({ + [Symbol.asyncIterator]: () => ({ + async next() { + return { done: true } + }, + }), + }) + + const handlerWithCustomTemp = new MiniMaxHandler({ + apiModelId: "MiniMax-M2.7", + minimaxApiKey: "test-minimax-api-key", + modelTemperature: 0.2 as any, + }) + + const messageGenerator = handlerWithCustomTemp.createMessage("system", []) + await messageGenerator.next() + + const lastCall = mockCreate.mock.calls[mockCreate.mock.calls.length - 1][0] + expect(lastCall).toMatchObject({ temperature: 1.0, top_p: 0.95, top_k: 40 }) + }) + + // Regression guard for CR-2: the M-series request-param builder is shared + // by `createMessage` and `completePrompt`, so the M3 default still works + // for non-streaming single-turn completions. + it("should also apply adaptive thinking to MiniMax-M3 in completePrompt", async () => { + mockCreate.mockResolvedValueOnce({ + content: [{ type: "text", text: "ok" }], + }) + + const handler = new MiniMaxHandler({ + apiModelId: "MiniMax-M3-1M", + minimaxApiKey: "test-minimax-api-key", + }) + + await handler.completePrompt("hello") + + const lastCall = mockCreate.mock.calls[mockCreate.mock.calls.length - 1][0] + expect(lastCall).toMatchObject({ + model: "MiniMax-M3-1M", + max_tokens: 131_072, + temperature: 1.0, + top_p: 0.95, + thinking: { type: "adaptive" }, + }) + expect(lastCall).not.toHaveProperty("top_k") + expect(lastCall.thinking).not.toHaveProperty("budget_tokens") + }) + + it("should also apply M2 sampling params to completePrompt", async () => { + mockCreate.mockResolvedValueOnce({ + content: [{ type: "text", text: "ok" }], + }) + + const handler = new MiniMaxHandler({ + apiModelId: "MiniMax-M2.7", + minimaxApiKey: "test-minimax-api-key", + }) + + await handler.completePrompt("hello") + + const lastCall = mockCreate.mock.calls[mockCreate.mock.calls.length - 1][0] + expect(lastCall).toMatchObject({ + model: "MiniMax-M2.7", + max_tokens: 16_384, + temperature: 1.0, + top_p: 0.95, + top_k: 40, + }) + expect(lastCall.thinking).toBeUndefined() + }) + + // Regression guard for the new max_tokens follow-up: completePrompt must + // honor the selected model's registry `maxTokens` (not the legacy 16_384 + // cap). M3-1M's registry advertises 131_072; M2.x stays at 16_384. + it("should honor M3-1M 131_072 max_tokens in completePrompt (no legacy 16_384 cap)", async () => { + mockCreate.mockResolvedValueOnce({ + content: [{ type: "text", text: "ok" }], + }) + + const handler = new MiniMaxHandler({ + apiModelId: "MiniMax-M3-1M", + minimaxApiKey: "test-minimax-api-key", + }) + + await handler.completePrompt("hello") + + const lastCall = mockCreate.mock.calls[mockCreate.mock.calls.length - 1][0] + expect(lastCall.max_tokens).toBe(131_072) + expect(lastCall.max_tokens).not.toBe(16_384) + }) + + it("should honor M3-512k 65_536 max_tokens in completePrompt", async () => { + mockCreate.mockResolvedValueOnce({ + content: [{ type: "text", text: "ok" }], + }) + + const handler = new MiniMaxHandler({ + apiModelId: "MiniMax-M3-512k", + minimaxApiKey: "test-minimax-api-key", + }) + + await handler.completePrompt("hello") + + const lastCall = mockCreate.mock.calls[mockCreate.mock.calls.length - 1][0] + expect(lastCall.max_tokens).toBe(65_536) + expect(lastCall.max_tokens).not.toBe(16_384) + }) + it("should handle thinking blocks in stream", async () => { const thinkingContent = "Let me think about this..." @@ -434,6 +733,30 @@ describe("MiniMaxHandler", () => { expect(model.cacheReadsPrice).toBe(0.06) }) + it("should correctly configure MiniMax-M3-512k model properties", () => { + const model = minimaxModels["MiniMax-M3-512k"] + expect(model.maxTokens).toBe(65_536) + expect(model.contextWindow).toBe(524_288) + expect(model.supportsImages).toBe(true) + expect(model.supportsPromptCache).toBe(true) + expect(model.inputPrice).toBe(0.3) + expect(model.outputPrice).toBe(1.2) + expect(model.cacheWritesPrice).toBe(0.375) + expect(model.cacheReadsPrice).toBe(0.06) + }) + + it("should correctly configure MiniMax-M3-1M model properties", () => { + const model = minimaxModels["MiniMax-M3-1M"] + expect(model.maxTokens).toBe(131_072) + expect(model.contextWindow).toBe(1_048_576) + expect(model.supportsImages).toBe(true) + expect(model.supportsPromptCache).toBe(true) + expect(model.inputPrice).toBe(0.3) + expect(model.outputPrice).toBe(1.2) + expect(model.cacheWritesPrice).toBe(0.375) + expect(model.cacheReadsPrice).toBe(0.06) + }) + it("should correctly configure MiniMax-M2.7-highspeed model properties", () => { const model = minimaxModels["MiniMax-M2.7-highspeed"] expect(model.maxTokens).toBe(16_384) @@ -479,5 +802,16 @@ describe("MiniMaxHandler", () => { const model = minimaxModels["MiniMax-M2"] expect(model.contextWindow).toBe(204_800) }) + + // Regression guard: MiniMax M3 pricing is intentionally a single flat tier + // across the 512K and 1M context windows. Keep `longContextPricing` unset + // (not even `undefined`) so the existing token math is reused end-to-end. + it("should NOT add longContextPricing to MiniMax-M3-512k (flat pricing by design)", () => { + expect(minimaxModels["MiniMax-M3-512k"]).not.toHaveProperty("longContextPricing") + }) + + it("should NOT add longContextPricing to MiniMax-M3-1M (flat pricing by design)", () => { + expect(minimaxModels["MiniMax-M3-1M"]).not.toHaveProperty("longContextPricing") + }) }) }) diff --git a/src/api/providers/minimax.ts b/src/api/providers/minimax.ts index 93aa7ea8f1..44f1f7d9f3 100644 --- a/src/api/providers/minimax.ts +++ b/src/api/providers/minimax.ts @@ -3,7 +3,7 @@ import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming" import { CacheControlEphemeral } from "@anthropic-ai/sdk/resources" import OpenAI from "openai" -import { type MinimaxModelId, minimaxDefaultModelId, minimaxModels } from "@roo-code/types" +import { MINIMAX_DEFAULT_MAX_TOKENS, type MinimaxModelId, minimaxDefaultModelId, minimaxModels } from "@roo-code/types" import type { ApiHandlerOptions } from "../../shared/api" @@ -83,7 +83,7 @@ export class MiniMaxHandler extends BaseProvider implements SingleCompletionHand metadata?: ApiHandlerCreateMessageMetadata, ): ApiStream { const cacheControl: CacheControlEphemeral = { type: "ephemeral" } - const { id: modelId, info, maxTokens, temperature } = this.getModel() + const { id: modelId, info, maxTokens } = this.getModel() // MiniMax M2 models support prompt caching const supportsPromptCache = info.supportsPromptCache ?? false @@ -101,16 +101,18 @@ export class MiniMaxHandler extends BaseProvider implements SingleCompletionHand : { text: systemPrompt, type: "text" }, ] - // Prepare request parameters + // Prepare request parameters. `MINIMAX_DEFAULT_MAX_TOKENS` is the named + // fallback; using the constant keeps the `??` branch exercised in + // coverage and self-documenting at the call site. const requestParams: Anthropic.Messages.MessageCreateParams = { model: modelId, - max_tokens: maxTokens ?? 16_384, - temperature: temperature ?? 1.0, + max_tokens: maxTokens ?? MINIMAX_DEFAULT_MAX_TOKENS, system: systemBlocks, messages: supportsPromptCache ? this.addCacheControl(processedMessages, cacheControl) : processedMessages, stream: true, tools: convertOpenAIToolsToAnthropic(metadata?.tools ?? []), tool_choice: convertOpenAIToolChoice(metadata?.tool_choice), + ...this.getMSeriesRequestParams(modelId), } const stream = await this.client.messages.create(requestParams) @@ -289,15 +291,44 @@ export class MiniMaxHandler extends BaseProvider implements SingleCompletionHand } } + /** + * Build the M-series-specific request params shared by `createMessage` and + * `completePrompt`. The MiniMax Anthropic endpoint defaults `thinking` OFF, + * which makes M3 fall back to a single non-thinking turn and then re-think + * from scratch on the next turn — the "double-think" symptom. We hard-code + * `temperature: 1.0` (the regression-guarded default that the hang-fix + * depends on), per-family `top_p`/`top_k` matching kilo-code's opencode + * provider, and (for M3 only) enable adaptive thinking. We intentionally + * never set `budget_tokens` — M-series is a binary on/off toggle, not + * effort levels. + * + * The user-supplied temperature is intentionally ignored for the M-series + * path: the hang/double-think fix depends on the exact M-series defaults. + */ + private getMSeriesRequestParams(modelId: string): { + temperature: number + top_p: number + top_k?: number + thinking?: { type: "adaptive" } + } { + const isM3 = modelId.startsWith("MiniMax-M3") + return isM3 + ? { temperature: 1.0, top_p: 0.95, thinking: { type: "adaptive" } } + : { temperature: 1.0, top_p: 0.95, top_k: 40 } + } + async completePrompt(prompt: string) { - const { id: model, temperature } = this.getModel() + const { id: model, maxTokens } = this.getModel() + // Honor the selected model's `maxTokens` registry value (e.g. M3-1M + // advertises 131_072). `MINIMAX_DEFAULT_MAX_TOKENS` is the named + // fallback for any future model entry that omits one. const message = await this.client.messages.create({ model, - max_tokens: 16_384, - temperature: temperature ?? 1.0, + max_tokens: maxTokens ?? MINIMAX_DEFAULT_MAX_TOKENS, messages: [{ role: "user", content: prompt }], stream: false, + ...this.getMSeriesRequestParams(model), }) const content = message.content.find(({ type }) => type === "text") diff --git a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts index 9e84ec364b..b8d6a9d069 100644 --- a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts +++ b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts @@ -844,5 +844,39 @@ describe("useSelectedModel", () => { expect(result.current.id).toBe("MiniMax-M2.7") expect(result.current.info).toEqual(minimaxModels["MiniMax-M2.7"]) }) + + it("should resolve MiniMax-M3-512k with its 512K-context model info", () => { + const apiConfiguration: ProviderSettings = { + apiProvider: "minimax", + apiModelId: "MiniMax-M3-512k", + } + + const wrapper = createWrapper() + const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) + + expect(result.current.provider).toBe("minimax") + expect(result.current.id).toBe("MiniMax-M3-512k") + expect(result.current.info).toEqual(minimaxModels["MiniMax-M3-512k"]) + expect(result.current.info?.contextWindow).toBe(524_288) + expect(result.current.info?.maxTokens).toBe(65_536) + expect(result.current.info?.supportsImages).toBe(true) + }) + + it("should resolve MiniMax-M3-1M with its 1M-context model info", () => { + const apiConfiguration: ProviderSettings = { + apiProvider: "minimax", + apiModelId: "MiniMax-M3-1M", + } + + const wrapper = createWrapper() + const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) + + expect(result.current.provider).toBe("minimax") + expect(result.current.id).toBe("MiniMax-M3-1M") + expect(result.current.info).toEqual(minimaxModels["MiniMax-M3-1M"]) + expect(result.current.info?.contextWindow).toBe(1_048_576) + expect(result.current.info?.maxTokens).toBe(131_072) + expect(result.current.info?.supportsImages).toBe(true) + }) }) })