diff --git a/src/adapters/openai-chat.ts b/src/adapters/openai-chat.ts index 978a7a573..46b889648 100644 --- a/src/adapters/openai-chat.ts +++ b/src/adapters/openai-chat.ts @@ -522,6 +522,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd messages, stream: parsed.stream, }; + if (modelInList(provider.reasoningSplitModels, parsed.modelId)) body.reasoning_split = true; const maxTokens = resolveMaxTokens(provider, parsed); const openRouterRouting = resolveOpenRouterRouting(provider, parsed.modelId); if (openRouterRouting) body.provider = openRouterProviderPayload(openRouterRouting); @@ -545,9 +546,10 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd const budget = thinkingBudgetForEffort(parsed, reasoningEffort, maxTokens); if (budget !== undefined) body.thinking_budget = budget; } else if (modelInList(provider.thinkingToggleModels, parsed.modelId)) { - // Vendor thinking-toggle wire (MiMo v2.x, GLM 5/5.1): the mapped value is the toggle - // state, sent as `thinking: {type}` — these models ignore/reject reasoning_effort. - if (reasoningEffort === "enabled" || reasoningEffort === "disabled") { + // Vendor thinking-toggle wire: the mapped value is sent as `thinking: {type}` because + // these models ignore/reject reasoning_effort. Most use enabled/disabled; MiniMax-M3 + // uses adaptive/disabled. + if (reasoningEffort === "enabled" || reasoningEffort === "disabled" || reasoningEffort === "adaptive") { body.thinking = { type: reasoningEffort }; } } else { @@ -679,13 +681,12 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd } const delta = choices[0].delta; if (delta) { - if (typeof delta.content === "string" && delta.content.length > 0) { - yield { type: "text_delta", text: delta.content }; - } - if (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0) { yield { type: "reasoning_raw_delta", text: delta.reasoning_content }; } + if (typeof delta.content === "string" && delta.content.length > 0) { + yield { type: "text_delta", text: delta.content }; + } const toolCalls = delta.tool_calls as { index?: number; id?: string; function?: { name?: string; arguments?: string } }[] | undefined; if (toolCalls) { @@ -780,12 +781,12 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd } const msg = choices[0].message; - if (typeof msg.content === "string") { - events.push({ type: "text_delta", text: msg.content }); - } if (typeof msg.reasoning_content === "string" && msg.reasoning_content.length > 0) { events.push({ type: "reasoning_raw_delta", text: msg.reasoning_content }); } + if (typeof msg.content === "string") { + events.push({ type: "text_delta", text: msg.content }); + } const toolCalls = msg.tool_calls as { id: string; function: { name: string; arguments: string } }[] | undefined; if (toolCalls) { for (const tc of toolCalls) { diff --git a/src/oauth/index.ts b/src/oauth/index.ts index a8382accc..44939ffdb 100644 --- a/src/oauth/index.ts +++ b/src/oauth/index.ts @@ -492,6 +492,7 @@ const OAUTH_RECONCILE_FIELDS: (keyof OcxProviderConfig)[] = [ "noPenaltyModels", "autoToolChoiceOnlyModels", "preserveReasoningContentModels", + "reasoningSplitModels", ]; export function reconcileOAuthProviders(config: OcxConfig): boolean { diff --git a/src/oauth/login-cli.ts b/src/oauth/login-cli.ts index 86a37e0fa..50b85a0b6 100644 --- a/src/oauth/login-cli.ts +++ b/src/oauth/login-cli.ts @@ -86,6 +86,7 @@ export function providerConfigFromKeyLoginProvider(def: KeyLoginProvider, key: s ...(def.noPenaltyModels ? { noPenaltyModels: [...def.noPenaltyModels] } : {}), ...(def.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...def.autoToolChoiceOnlyModels] } : {}), ...(def.preserveReasoningContentModels ? { preserveReasoningContentModels: [...def.preserveReasoningContentModels] } : {}), + ...(def.reasoningSplitModels ? { reasoningSplitModels: [...def.reasoningSplitModels] } : {}), ...(def.escapeBuiltinToolNames !== undefined ? { escapeBuiltinToolNames: def.escapeBuiltinToolNames } : {}), }; } diff --git a/src/providers/derive.ts b/src/providers/derive.ts index 7277d5e87..fce744e88 100644 --- a/src/providers/derive.ts +++ b/src/providers/derive.ts @@ -27,6 +27,7 @@ export interface DerivedKeyLoginProvider { noPenaltyModels?: string[]; autoToolChoiceOnlyModels?: string[]; preserveReasoningContentModels?: string[]; + reasoningSplitModels?: string[]; thinkingToggleModels?: string[]; thinkingBudgetModels?: string[]; escapeBuiltinToolNames?: boolean; @@ -113,6 +114,7 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon ...(entry.parallelToolCalls !== undefined ? { parallelToolCalls: entry.parallelToolCalls } : {}), ...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}), ...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}), + ...(entry.reasoningSplitModels ? { reasoningSplitModels: [...entry.reasoningSplitModels] } : {}), ...(entry.thinkingToggleModels ? { thinkingToggleModels: [...entry.thinkingToggleModels] } : {}), ...(entry.thinkingBudgetModels ? { thinkingBudgetModels: [...entry.thinkingBudgetModels] } : {}), ...(entry.escapeBuiltinToolNames !== undefined ? { escapeBuiltinToolNames: entry.escapeBuiltinToolNames } : {}), @@ -153,6 +155,7 @@ export function deriveKeyLoginMap(): Record { ...(entry.noPenaltyModels ? { noPenaltyModels: [...entry.noPenaltyModels] } : {}), ...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}), ...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}), + ...(entry.reasoningSplitModels ? { reasoningSplitModels: [...entry.reasoningSplitModels] } : {}), ...(entry.thinkingToggleModels ? { thinkingToggleModels: [...entry.thinkingToggleModels] } : {}), ...(entry.thinkingBudgetModels ? { thinkingBudgetModels: [...entry.thinkingBudgetModels] } : {}), ...(entry.escapeBuiltinToolNames !== undefined ? { escapeBuiltinToolNames: entry.escapeBuiltinToolNames } : {}), @@ -224,6 +227,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig if (prov.parallelToolCalls === undefined && seed.parallelToolCalls !== undefined) prov.parallelToolCalls = seed.parallelToolCalls; if (!prov.autoToolChoiceOnlyModels && seed.autoToolChoiceOnlyModels) prov.autoToolChoiceOnlyModels = [...seed.autoToolChoiceOnlyModels]; if (!prov.preserveReasoningContentModels && seed.preserveReasoningContentModels) prov.preserveReasoningContentModels = [...seed.preserveReasoningContentModels]; + if (!prov.reasoningSplitModels && seed.reasoningSplitModels) prov.reasoningSplitModels = [...seed.reasoningSplitModels]; if (!prov.thinkingToggleModels && seed.thinkingToggleModels) prov.thinkingToggleModels = [...seed.thinkingToggleModels]; if (!prov.thinkingBudgetModels && seed.thinkingBudgetModels) prov.thinkingBudgetModels = [...seed.thinkingBudgetModels]; if (prov.escapeBuiltinToolNames === undefined && seed.escapeBuiltinToolNames !== undefined) prov.escapeBuiltinToolNames = seed.escapeBuiltinToolNames; diff --git a/src/providers/registry.ts b/src/providers/registry.ts index 71140fa72..46420117d 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -71,6 +71,7 @@ export interface ProviderRegistryEntry { promptCacheKey?: boolean; autoToolChoiceOnlyModels?: string[]; preserveReasoningContentModels?: string[]; + reasoningSplitModels?: string[]; thinkingToggleModels?: string[]; thinkingBudgetModels?: string[]; escapeBuiltinToolNames?: boolean; @@ -92,7 +93,7 @@ export type ProviderConfigSeed = Pick< | "modelMaxInputTokens" | "defaultMaxOutputTokens" | "modelMaxOutputTokens" | "reasoningEfforts" | "modelReasoningEfforts" | "modelDefaultReasoningEfforts" | "reasoningEffortMap" | "modelReasoningEffortMap" | "noVisionModels" | "noReasoningModels" | "noTemperatureModels" | "noTopPModels" | "noPenaltyModels" - | "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "escapeBuiltinToolNames" + | "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "reasoningSplitModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "escapeBuiltinToolNames" | "googleMode" | "project" | "location" | "headers" >; @@ -117,6 +118,16 @@ const MINIMAX_MODELS = [ const MINIMAX_MODEL_CONTEXT_WINDOWS: Record = Object.fromEntries( MINIMAX_MODELS.map(id => [id, id === "MiniMax-M3" ? 1_000_000 : 204_800]), ); +const MINIMAX_M3_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"]; +const MINIMAX_M3_REASONING_EFFORT_MAP: Record = { + none: "disabled", + minimal: "disabled", + low: "disabled", + medium: "adaptive", + high: "adaptive", + xhigh: "adaptive", + max: "adaptive", +}; const OPENAI_GPT56_MODELS = ["gpt-5.6", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]; const OPENAI_GPT56_PRO_MODELS = ["gpt-5.6-sol-pro", "gpt-5.6-terra-pro", "gpt-5.6-luna-pro"]; const OPENAI_API_GPT56_CONTEXT_WINDOW = 1_050_000; @@ -920,12 +931,24 @@ export const PROVIDER_REGISTRY: 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, + modelReasoningEfforts: { "MiniMax-M3": MINIMAX_M3_REASONING_EFFORTS }, + modelDefaultReasoningEfforts: { "MiniMax-M3": "medium" }, + modelReasoningEffortMap: { "MiniMax-M3": MINIMAX_M3_REASONING_EFFORT_MAP }, + preserveReasoningContentModels: MINIMAX_MODELS, + reasoningSplitModels: MINIMAX_MODELS, + thinkingToggleModels: ["MiniMax-M3"], jawcodeBundle: "minimax", metadataModelIdNormalize: "case-insensitive", note: "Subscription Key or API Key", }, { 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, + modelReasoningEfforts: { "MiniMax-M3": MINIMAX_M3_REASONING_EFFORTS }, + modelDefaultReasoningEfforts: { "MiniMax-M3": "medium" }, + modelReasoningEffortMap: { "MiniMax-M3": MINIMAX_M3_REASONING_EFFORT_MAP }, + preserveReasoningContentModels: MINIMAX_MODELS, + reasoningSplitModels: MINIMAX_MODELS, + thinkingToggleModels: ["MiniMax-M3"], jawcodeBundle: "minimax", metadataModelIdNormalize: "case-insensitive", note: "中国区 Subscription Key", }, { diff --git a/src/router.ts b/src/router.ts index 618f43b34..6e8850bcf 100644 --- a/src/router.ts +++ b/src/router.ts @@ -150,6 +150,7 @@ function routedProviderConfig(providerName: string, provider: OcxProviderConfig) const noPenaltyModels = mergeStringArray(registryEntry.noPenaltyModels, provider.noPenaltyModels); const autoToolChoiceOnlyModels = mergeStringArray(registryEntry.autoToolChoiceOnlyModels, provider.autoToolChoiceOnlyModels); const preserveReasoningContentModels = mergeStringArray(registryEntry.preserveReasoningContentModels, provider.preserveReasoningContentModels); + const reasoningSplitModels = mergeStringArray(registryEntry.reasoningSplitModels, provider.reasoningSplitModels); const thinkingToggleModels = mergeStringArray(registryEntry.thinkingToggleModels, provider.thinkingToggleModels); const thinkingBudgetModels = mergeStringArray(registryEntry.thinkingBudgetModels, provider.thinkingBudgetModels); const registryBaseUrlIsTemplate = /\{[^}]*\}/.test(registryEntry.baseUrl); @@ -203,6 +204,7 @@ function routedProviderConfig(providerName: string, provider: OcxProviderConfig) ...(noPenaltyModels ? { noPenaltyModels } : {}), ...(autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels } : {}), ...(preserveReasoningContentModels ? { preserveReasoningContentModels } : {}), + ...(reasoningSplitModels ? { reasoningSplitModels } : {}), ...(thinkingToggleModels ? { thinkingToggleModels } : {}), ...(thinkingBudgetModels ? { thinkingBudgetModels } : {}), }; diff --git a/src/server/auth-cors.ts b/src/server/auth-cors.ts index af36826f9..fad1fbce4 100644 --- a/src/server/auth-cors.ts +++ b/src/server/auth-cors.ts @@ -320,6 +320,7 @@ export function safeConfigDTO(config: OcxConfig): unknown { "noPenaltyModels", "autoToolChoiceOnlyModels", "preserveReasoningContentModels", + "reasoningSplitModels", "escapeBuiltinToolNames", ] as const) { copyIfDefined(dto, provider, key); diff --git a/src/types.ts b/src/types.ts index d4422034b..9701e9f25 100644 --- a/src/types.ts +++ b/src/types.ts @@ -881,7 +881,12 @@ export interface OcxProviderConfig { /** Model ids that expect prior assistant `reasoning_content` to be preserved in chat history. */ preserveReasoningContentModels?: string[]; /** - * Model ids whose reasoning is a vendor `thinking: {type: enabled|disabled}` toggle on the + * Model ids whose OpenAI-compatible chat endpoint accepts `reasoning_split: true` and returns + * thinking separately in `reasoning_content` / `reasoning_details` instead of visible content. + */ + reasoningSplitModels?: string[]; + /** + * Model ids whose reasoning is a vendor `thinking: {type}` toggle on the * chat-completions wire (MiMo v2.x, GLM 5/5.1 style), NOT an OpenAI `reasoning_effort` ladder. * The openai-chat adapter translates the mapped effort into the thinking toggle for these. */ diff --git a/tests/adapter-usage.test.ts b/tests/adapter-usage.test.ts index 70b532bea..ef6ac810f 100644 --- a/tests/adapter-usage.test.ts +++ b/tests/adapter-usage.test.ts @@ -18,18 +18,17 @@ describe("adapter reasoning and usage details", () => { }, }))); - expect(events).toContainEqual({ type: "reasoning_raw_delta", text: "raw thoughts" }); - expect(events).toContainEqual({ type: "text_delta", text: "answer" }); - expect(events?.at(-1)).toEqual({ - type: "done", - usage: { inputTokens: 11, outputTokens: 7, cachedInputTokens: 5, reasoningOutputTokens: 3 }, - }); + expect(events).toEqual([ + { type: "reasoning_raw_delta", text: "raw thoughts" }, + { type: "text_delta", text: "answer" }, + { type: "done", usage: { inputTokens: 11, outputTokens: 7, cachedInputTokens: 5, reasoningOutputTokens: 3 } }, + ]); }); test("OpenAI-compatible streaming maps reasoning_content and usage details", async () => { const adapter = createOpenAIChatAdapter(provider); const response = new Response([ - "data: {\"choices\":[{\"delta\":{\"reasoning_content\":\"raw stream\"}}]}\n\n", + "data: {\"choices\":[{\"delta\":{\"reasoning_content\":\"raw stream\",\"content\":\"answer\"}}]}\n\n", "data: {\"usage\":{\"prompt_tokens\":9,\"completion_tokens\":4,\"prompt_tokens_details\":{\"cached_tokens\":2},\"completion_tokens_details\":{\"reasoning_tokens\":1}}}\n\n", "data: [DONE]\n\n", ].join("")); @@ -37,11 +36,11 @@ describe("adapter reasoning and usage details", () => { const events = []; for await (const event of adapter.parseStream(response)) events.push(event); - expect(events).toContainEqual({ type: "reasoning_raw_delta", text: "raw stream" }); - expect(events.at(-1)).toEqual({ - type: "done", - usage: { inputTokens: 9, outputTokens: 4, cachedInputTokens: 2, reasoningOutputTokens: 1 }, - }); + expect(events).toEqual([ + { type: "reasoning_raw_delta", text: "raw stream" }, + { type: "text_delta", text: "answer" }, + { type: "done", usage: { inputTokens: 9, outputTokens: 4, cachedInputTokens: 2, reasoningOutputTokens: 1 } }, + ]); }); test("OpenAI-compatible non-OpenAI providers receive the tool catalog nudge", async () => { diff --git a/tests/minimax-reasoning-split.test.ts b/tests/minimax-reasoning-split.test.ts new file mode 100644 index 000000000..9712519a5 --- /dev/null +++ b/tests/minimax-reasoning-split.test.ts @@ -0,0 +1,125 @@ +import { describe, expect, test } from "bun:test"; +import { createOpenAIChatAdapter } from "../src/adapters/openai-chat"; +import { enrichProviderFromRegistry } from "../src/providers/derive"; +import { routeModel } from "../src/router"; +import type { OcxConfig, OcxParsedRequest, OcxProviderConfig } from "../src/types"; + +type ReasoningEffort = OcxParsedRequest["options"]["reasoning"]; + +function parsed(modelId: string, reasoning?: ReasoningEffort): OcxParsedRequest { + return { + modelId, + context: { messages: [{ role: "user", content: "ping", timestamp: 0 }] }, + stream: false, + options: reasoning ? { reasoning } : {}, + }; +} + +function body(provider: OcxProviderConfig, modelId: string, reasoning?: ReasoningEffort): Record { + const request = createOpenAIChatAdapter(provider).buildRequest(parsed(modelId, reasoning)); + return JSON.parse(request.body as string) as Record; +} + +function minimaxRoute(modelId = "MiniMax-M3", provider: Partial = {}) { + const config: OcxConfig = { + port: 10100, + defaultProvider: "minimax", + providers: { + minimax: { + adapter: "openai-chat", + baseUrl: "https://api.minimax.io/v1", + apiKey: "test-key", + ...provider, + }, + }, + }; + return routeModel(config, `minimax/${modelId}`); +} + +describe("MiniMax split reasoning", () => { + test("M3 maps Codex effort to MiniMax adaptive/disabled and separates reasoning", () => { + const route = minimaxRoute(); + + expect(body(route.provider, route.modelId, "low")).toMatchObject({ + model: "MiniMax-M3", + reasoning_split: true, + thinking: { type: "disabled" }, + }); + expect(body(route.provider, route.modelId, "medium")).toMatchObject({ + model: "MiniMax-M3", + reasoning_split: true, + thinking: { type: "adaptive" }, + }); + expect(body(route.provider, route.modelId, "high")).toMatchObject({ + reasoning_split: true, + thinking: { type: "adaptive" }, + }); + expect(body(route.provider, route.modelId, "high")).not.toHaveProperty("reasoning_effort"); + }); + + test("all MiniMax M-series models request split reasoning and preserve it in history", () => { + const route = minimaxRoute("MiniMax-M2.7"); + const request = createOpenAIChatAdapter(route.provider).buildRequest({ + modelId: route.modelId, + context: { + messages: [ + { role: "user", content: "first", timestamp: 0 }, + { + role: "assistant", + timestamp: 1, + content: [ + { type: "thinking", thinking: "prior reasoning" }, + { type: "text", text: "prior answer" }, + ], + }, + { role: "user", content: "continue", timestamp: 2 }, + ], + }, + stream: false, + options: {}, + }); + const requestBody = JSON.parse(request.body as string) as { + reasoning_split?: boolean; + messages: Array>; + }; + + expect(requestBody.reasoning_split).toBe(true); + expect(requestBody.messages[1]?.reasoning_content).toBe("prior reasoning"); + }); + + test("routing merges registry capabilities while explicit user effort mappings win", () => { + const route = minimaxRoute("MiniMax-M3", { + reasoningSplitModels: ["user-split-model"], + modelReasoningEffortMap: { "MiniMax-M3": { medium: "disabled" } }, + }); + + expect(route.provider.reasoningSplitModels).toEqual(expect.arrayContaining(["MiniMax-M3", "user-split-model"])); + expect(route.provider.modelReasoningEffortMap?.["MiniMax-M3"]).toMatchObject({ + medium: "disabled", + high: "adaptive", + }); + }); + + test("registry enrichment never replaces explicit split-reasoning fields", () => { + const provider: OcxProviderConfig = { + adapter: "openai-chat", + baseUrl: "https://api.minimax.io/v1", + reasoningSplitModels: ["only-user-model"], + modelReasoningEffortMap: { "MiniMax-M3": { medium: "disabled" } }, + }; + + enrichProviderFromRegistry("minimax", provider); + + expect(provider.reasoningSplitModels).toEqual(["only-user-model"]); + expect(provider.modelReasoningEffortMap).toEqual({ "MiniMax-M3": { medium: "disabled" } }); + }); + + test("unconfigured OpenAI-compatible providers remain unchanged", () => { + const provider: OcxProviderConfig = { + adapter: "openai-chat", + baseUrl: "https://example.test/v1", + }; + + expect(body(provider, "example-model", "high")).not.toHaveProperty("reasoning_split"); + }); +}); diff --git a/tests/openai-chat-eof.test.ts b/tests/openai-chat-eof.test.ts index 6dbac2582..696151637 100644 --- a/tests/openai-chat-eof.test.ts +++ b/tests/openai-chat-eof.test.ts @@ -85,6 +85,17 @@ describe("openai-chat stream EOF fail-closed", () => { expect(events.some(e => e.type === "error")).toBe(false); }); + test("empty deltas followed by a finish-only chunk complete without phantom output", async () => { + const response = new Response([ + 'data: {"choices":[{"delta":{"content":"","reasoning_content":""}}]}\n\n', + 'data: {"choices":[{"delta":{}}]}\n\n', + 'data: {"choices":[{"finish_reason":"stop"}]}\n\n', + ].join("")); + const events = await collect(createOpenAIChatAdapter(provider).parseStream(response)); + + expect(events).toEqual([{ type: "done", usage: undefined }]); + }); + test("final frame WITHOUT a trailing newline still emits its content and is accepted as done", async () => { // No trailing "\n" — the terminal frame stays in the buffer and is only seen at EOF. Its // content must NOT be dropped (regression guard: the EOF flush must run the full delta path). diff --git a/tests/provider-registry-parity.test.ts b/tests/provider-registry-parity.test.ts index 0196b004d..a4b7fe55e 100644 --- a/tests/provider-registry-parity.test.ts +++ b/tests/provider-registry-parity.test.ts @@ -234,6 +234,12 @@ describe("provider registry parity", () => { expect(entry?.defaultModel).toBe("MiniMax-M3"); expect(entry?.models).toEqual(minimaxModels); expect(entry?.modelContextWindows?.["MiniMax-M3"]).toBe(1_000_000); + expect(entry?.modelReasoningEfforts?.["MiniMax-M3"]).toEqual(["low", "medium", "high", "xhigh", "max"]); + expect(entry?.modelDefaultReasoningEfforts?.["MiniMax-M3"]).toBe("medium"); + expect(entry?.modelReasoningEffortMap?.["MiniMax-M3"]).toMatchObject({ low: "disabled", medium: "adaptive", high: "adaptive" }); + expect(entry?.preserveReasoningContentModels).toEqual(minimaxModels); + expect(entry?.reasoningSplitModels).toEqual(minimaxModels); + expect(entry?.thinkingToggleModels).toEqual(["MiniMax-M3"]); for (const modelId of minimaxModels.slice(1)) { expect(entry?.modelContextWindows?.[modelId]).toBe(204_800); }