From f1069d24bcfdd9c46251c828f3d28edd1ec4e26c Mon Sep 17 00:00:00 2001 From: zerob13 Date: Tue, 16 Jun 2026 10:11:36 +0800 Subject: [PATCH 1/2] fix(model): handle minimax m3 --- docs/issues/minimax-model-matching/plan.md | 7 +++ docs/issues/minimax-model-matching/spec.md | 27 +++++++++ docs/issues/minimax-model-matching/tasks.md | 6 ++ .../presenter/configPresenter/modelConfig.ts | 27 +++++++-- .../aiSdk/providerOptionsMapper.ts | 28 +++++++++ .../aiSdkProviderOptionsMapper.test.ts | 51 ++++++++++++++++ .../presenter/providerDbModelConfig.test.ts | 60 +++++++++++++++++++ 7 files changed, 202 insertions(+), 4 deletions(-) create mode 100644 docs/issues/minimax-model-matching/plan.md create mode 100644 docs/issues/minimax-model-matching/spec.md create mode 100644 docs/issues/minimax-model-matching/tasks.md diff --git a/docs/issues/minimax-model-matching/plan.md b/docs/issues/minimax-model-matching/plan.md new file mode 100644 index 000000000..cc381ffa2 --- /dev/null +++ b/docs/issues/minimax-model-matching/plan.md @@ -0,0 +1,7 @@ +# Plan + +1. Fix provider DB model ID comparison in `ModelConfigHelper`. +2. Add a narrow MiniMax-M3 config correction for interleaved thinking and the documented context + window. +3. Add MiniMax-M3 adaptive thinking provider options for Anthropic-compatible runtime. +4. Cover the behavior with focused unit tests. diff --git a/docs/issues/minimax-model-matching/spec.md b/docs/issues/minimax-model-matching/spec.md new file mode 100644 index 000000000..f089a45c7 --- /dev/null +++ b/docs/issues/minimax-model-matching/spec.md @@ -0,0 +1,27 @@ +# MiniMax model matching and thinking + +## Problem + +MiniMax provider models with mixed-case IDs such as `MiniMax-M3` and `MiniMax-M2.5` can miss +provider DB derived configuration because model config lookup lowercases the requested model ID but +compares it with the raw DB model ID. + +MiniMax-M3 also requires explicit Anthropic-compatible `thinking: { type: "adaptive" }` to emit +thinking blocks. Current Anthropic-compatible provider options only send `thinking` for official +Anthropic adaptive reasoning or budget-based thinking. + +## Acceptance Criteria + +- Provider DB model lookup matches model IDs case-insensitively while keeping provider matching + strict. +- MiniMax mixed-case model IDs inherit provider DB context, output, multimodal, tool-call, and + reasoning defaults. +- MiniMax-M3 defaults to interleaved thinking compatibility even if the provider DB source has not + caught up. +- MiniMax-M3 sends Anthropic-compatible adaptive thinking when reasoning is enabled. +- Reasoning-disabled MiniMax-M3 requests do not send adaptive thinking. + +## Non-goals + +- Do not route MiniMax through Anthropic provider capability semantics. +- Do not change Claude or generic Anthropic proxy behavior. diff --git a/docs/issues/minimax-model-matching/tasks.md b/docs/issues/minimax-model-matching/tasks.md new file mode 100644 index 000000000..9e519f31b --- /dev/null +++ b/docs/issues/minimax-model-matching/tasks.md @@ -0,0 +1,6 @@ +# Tasks + +- [x] Add provider DB case-insensitive matching coverage. +- [x] Fix `ModelConfigHelper` matching and MiniMax-M3 defaults. +- [x] Add MiniMax-M3 provider options coverage. +- [x] Run focused tests. diff --git a/src/main/presenter/configPresenter/modelConfig.ts b/src/main/presenter/configPresenter/modelConfig.ts index 39b97aae5..9993330ed 100644 --- a/src/main/presenter/configPresenter/modelConfig.ts +++ b/src/main/presenter/configPresenter/modelConfig.ts @@ -34,6 +34,20 @@ import type { StoreLike } from './storeLike' const SPECIAL_CONCAT_CHAR = '-_-' const MODEL_CONFIG_META_KEY = '__meta__' +const MINIMAX_M3_CONTEXT_LENGTH = 1_000_000 + +const normalizeProviderDbModelId = (modelId: string | undefined): string | undefined => { + const normalized = modelId ? modelId.toLowerCase() : modelId + return normalized ? normalized.replace(/^models\//, '') : normalized +} + +const isMiniMaxProviderId = (providerId: string | undefined): boolean => { + const normalized = providerId?.trim().toLowerCase() + return normalized === 'minimax' || normalized === 'minimax-cn' +} + +const isMiniMaxM3Model = (providerId: string | undefined, modelId: string): boolean => + isMiniMaxProviderId(providerId) && modelId.trim().toLowerCase() === 'minimax-m3' const normalizeVerbosityValue = ( portrait: ReasoningPortrait | null, @@ -178,10 +192,13 @@ export class ModelConfigHelper { portrait, portrait?.verbosity ?? model.reasoning?.verbosity ) + const contextLimit = isMiniMaxM3Model(providerId, model.id) + ? Math.max(model.limit?.context ?? 0, MINIMAX_M3_CONTEXT_LENGTH) + : model.limit?.context return this.applyProviderSpecificPolicies(providerId, model.id, { maxTokens: resolveDerivedModelMaxTokens(model.limit?.output), - contextLength: resolveModelContextLength(model.limit?.context), + contextLength: resolveModelContextLength(contextLimit), timeout: DEFAULT_MODEL_TIMEOUT, temperature: 0.6, topP: undefined, @@ -199,7 +216,9 @@ export class ModelConfigHelper { ? ApiEndpointType.AudioSpeech : ApiEndpointType.Chat, thinkingBudget, - forceInterleavedThinkingCompat, + forceInterleavedThinkingCompat: isMiniMaxM3Model(providerId, model.id) + ? true + : forceInterleavedThinkingCompat, reasoningEffort, reasoningVisibility, verbosity, @@ -455,7 +474,7 @@ export class ModelConfigHelper { ) { for (let i = 0; i < providerEntry.models.length; i += 1) { const candidate = providerEntry.models[i] - if (candidate && candidate.id === normModelId) { + if (candidate && normalizeProviderDbModelId(candidate.id) === normModelId) { finalConfig = this.buildConfigFromProviderModel(candidate, resolvedProviderId) break } @@ -472,7 +491,7 @@ export class ModelConfigHelper { for (let j = 0; j < candidateProvider.models.length; j += 1) { const candidateModel = candidateProvider.models[j] - if (candidateModel && candidateModel.id === normModelId) { + if (candidateModel && normalizeProviderDbModelId(candidateModel.id) === normModelId) { finalConfig = this.buildConfigFromProviderModel(candidateModel, candidateProvider.id) break } diff --git a/src/main/presenter/llmProviderPresenter/aiSdk/providerOptionsMapper.ts b/src/main/presenter/llmProviderPresenter/aiSdk/providerOptionsMapper.ts index c87f3b239..39fbbec8d 100644 --- a/src/main/presenter/llmProviderPresenter/aiSdk/providerOptionsMapper.ts +++ b/src/main/presenter/llmProviderPresenter/aiSdk/providerOptionsMapper.ts @@ -118,6 +118,23 @@ function supportsGrokReasoningEffort(modelId: string): boolean { ) } +function normalizeMiniMaxModelId(modelId: string): string { + const normalized = modelId.trim().toLowerCase() + return normalized.includes('/') ? normalized.slice(normalized.lastIndexOf('/') + 1) : normalized +} + +function supportsMiniMaxAdaptiveThinking( + providerId: string, + capabilityProviderId: string, + modelId: string +): boolean { + const providerIds = [providerId, capabilityProviderId].map((id) => id.trim().toLowerCase()) + return ( + providerIds.some((id) => id === 'minimax' || id === 'minimax-cn') && + normalizeMiniMaxModelId(modelId) === 'minimax-m3' + ) +} + export function buildProviderOptions( params: BuildProviderOptionsParams ): ProviderOptionsMappingResult { @@ -264,6 +281,17 @@ export function buildProviderOptions( type: 'adaptive', display: resolvedVisibility } + } else if ( + supportsMiniMaxAdaptiveThinking( + params.providerId, + params.capabilityProviderId, + params.modelId + ) && + reasoningEnabled + ) { + config.thinking = { + type: 'adaptive' + } } else if (reasoningEnabled && params.modelConfig.thinkingBudget !== undefined) { config.thinking = { type: 'enabled', diff --git a/test/main/presenter/llmProviderPresenter/aiSdkProviderOptionsMapper.test.ts b/test/main/presenter/llmProviderPresenter/aiSdkProviderOptionsMapper.test.ts index 080b583cb..cc4572e2b 100644 --- a/test/main/presenter/llmProviderPresenter/aiSdkProviderOptionsMapper.test.ts +++ b/test/main/presenter/llmProviderPresenter/aiSdkProviderOptionsMapper.test.ts @@ -313,6 +313,57 @@ describe('AI SDK provider options', () => { expect(result.providerOptions?.anthropic).not.toHaveProperty('effort') }) + it('maps MiniMax-M3 reasoning to Anthropic-compatible adaptive thinking', () => { + mockGetReasoningPortrait.mockReturnValue({ + supported: true, + defaultEnabled: true + }) + + const result = buildProviderOptions({ + providerId: 'minimax', + capabilityProviderId: 'minimax', + providerOptionsKey: 'anthropic', + apiType: 'anthropic', + modelId: 'MiniMax-M3', + modelConfig: { + reasoning: true + } as any, + tools: [], + messages: [] + }) + + expect(result.providerOptions?.anthropic).toEqual({ + toolStreaming: false, + thinking: { + type: 'adaptive' + } + }) + }) + + it('does not send MiniMax-M3 adaptive thinking when reasoning is disabled', () => { + mockGetReasoningPortrait.mockReturnValue({ + supported: true, + defaultEnabled: true + }) + + const result = buildProviderOptions({ + providerId: 'minimax', + capabilityProviderId: 'minimax', + providerOptionsKey: 'anthropic', + apiType: 'anthropic', + modelId: 'MiniMax-M3', + modelConfig: { + reasoning: false + } as any, + tools: [], + messages: [] + }) + + expect(result.providerOptions?.anthropic).toEqual({ + toolStreaming: false + }) + }) + it('keeps aws bedrock anthropic routes on the compatible reasoning dialect', () => { const result = buildProviderOptions({ providerId: 'aws-bedrock', diff --git a/test/main/presenter/providerDbModelConfig.test.ts b/test/main/presenter/providerDbModelConfig.test.ts index e76551a52..65d1f0e3f 100644 --- a/test/main/presenter/providerDbModelConfig.test.ts +++ b/test/main/presenter/providerDbModelConfig.test.ts @@ -175,6 +175,42 @@ describe('Provider DB strict matching + user overrides', () => { } } ] + }, + minimax: { + id: 'minimax', + name: 'MiniMax', + models: [ + { + id: 'MiniMax-M2.5', + limit: { context: 204800, output: 131072 }, + modalities: { input: ['text'], output: ['text'] }, + tool_call: true, + reasoning: { + supported: true, + default: true + }, + extra_capabilities: { + reasoning: { + supported: true + } + } + }, + { + id: 'MiniMax-M3', + limit: { context: 512000, output: 128000 }, + modalities: { input: ['text', 'image', 'video'], output: ['text'] }, + tool_call: true, + reasoning: { + supported: true, + default: true + }, + extra_capabilities: { + reasoning: { + supported: true + } + } + } + ] } } } @@ -325,6 +361,30 @@ describe('Provider DB strict matching + user overrides', () => { expect(cfg.maxTokens).toBe(2000) }) + it('matches mixed-case provider DB model IDs case-insensitively', () => { + const helper = new ModelConfigHelper('1.0.0') + + const cfg = helper.getModelConfig('minimax-m2.5', 'minimax') + + expect(cfg.contextLength).toBe(204800) + expect(cfg.maxTokens).toBe(32000) + expect(cfg.functionCall).toBe(true) + expect(cfg.reasoning).toBe(true) + }) + + it('applies MiniMax-M3 provider defaults when the provider DB cache is stale', () => { + const helper = new ModelConfigHelper('1.0.0') + + const cfg = helper.getModelConfig('minimax-m3', 'minimax') + + expect(cfg.contextLength).toBe(1_000_000) + expect(cfg.maxTokens).toBe(32000) + expect(cfg.vision).toBe(true) + expect(cfg.functionCall).toBe(true) + expect(cfg.reasoning).toBe(true) + expect(cfg.forceInterleavedThinkingCompat).toBe(true) + }) + it('prefers portrait defaults over legacy reasoning defaults', () => { const helper = new ModelConfigHelper('1.0.0') From dc36aa52d442160c4f9a547143b656bc233f8b9b Mon Sep 17 00:00:00 2001 From: zerob13 Date: Tue, 16 Jun 2026 10:47:13 +0800 Subject: [PATCH 2/2] fix(model): preserve minimax m3 context --- docs/issues/minimax-model-matching/spec.md | 2 +- .../presenter/configPresenter/modelConfig.ts | 12 ++++++- .../presenter/providerDbModelConfig.test.ts | 33 +++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/docs/issues/minimax-model-matching/spec.md b/docs/issues/minimax-model-matching/spec.md index f089a45c7..9f3960894 100644 --- a/docs/issues/minimax-model-matching/spec.md +++ b/docs/issues/minimax-model-matching/spec.md @@ -3,7 +3,7 @@ ## Problem MiniMax provider models with mixed-case IDs such as `MiniMax-M3` and `MiniMax-M2.5` can miss -provider DB derived configuration because model config lookup lowercases the requested model ID but +provider DB-derived configuration because model config lookup lowercases the requested model ID but compares it with the raw DB model ID. MiniMax-M3 also requires explicit Anthropic-compatible `thinking: { type: "adaptive" }` to emit diff --git a/src/main/presenter/configPresenter/modelConfig.ts b/src/main/presenter/configPresenter/modelConfig.ts index 9993330ed..6e193de5c 100644 --- a/src/main/presenter/configPresenter/modelConfig.ts +++ b/src/main/presenter/configPresenter/modelConfig.ts @@ -168,7 +168,17 @@ export class ModelConfigHelper { return config } - return applyMoonshotKimiReasoningTemperaturePolicy(providerId, modelId, config) + const policyConfig = applyMoonshotKimiReasoningTemperaturePolicy(providerId, modelId, config) + + if (!isMiniMaxM3Model(providerId, modelId)) { + return policyConfig + } + + return { + ...policyConfig, + contextLength: Math.max(policyConfig.contextLength ?? 0, MINIMAX_M3_CONTEXT_LENGTH), + forceInterleavedThinkingCompat: true + } } private buildConfigFromProviderModel(model: ProviderModel, providerId: string): ModelConfig { diff --git a/test/main/presenter/providerDbModelConfig.test.ts b/test/main/presenter/providerDbModelConfig.test.ts index 65d1f0e3f..b68d01ccd 100644 --- a/test/main/presenter/providerDbModelConfig.test.ts +++ b/test/main/presenter/providerDbModelConfig.test.ts @@ -385,6 +385,39 @@ describe('Provider DB strict matching + user overrides', () => { expect(cfg.forceInterleavedThinkingCompat).toBe(true) }) + it('keeps MiniMax-M3 context floor after provider cache merge', () => { + const helper = new ModelConfigHelper('1.0.0') + const helperAny = helper as any + const providerCacheKey = helperAny.generateCacheKey('minimax', 'minimax-m3') + + helper.importConfigs( + { + [providerCacheKey]: { + id: 'minimax-m3', + providerId: 'minimax', + source: 'provider', + config: { + maxTokens: 32000, + contextLength: 512000, + temperature: 0.6, + vision: true, + functionCall: true, + reasoning: true, + type: ModelType.Chat, + isUserDefined: false + } + } + }, + false + ) + + const cfg = helper.getModelConfig('minimax-m3', 'minimax') + + expect(cfg.contextLength).toBe(1_000_000) + expect(cfg.forceInterleavedThinkingCompat).toBe(true) + expect(cfg.isUserDefined).toBe(false) + }) + it('prefers portrait defaults over legacy reasoning defaults', () => { const helper = new ModelConfigHelper('1.0.0')