Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/issues/minimax-model-matching/plan.md
Original file line number Diff line number Diff line change
@@ -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.
27 changes: 27 additions & 0 deletions docs/issues/minimax-model-matching/spec.md
Original file line number Diff line number Diff line change
@@ -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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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.
6 changes: 6 additions & 0 deletions docs/issues/minimax-model-matching/tasks.md
Original file line number Diff line number Diff line change
@@ -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.
39 changes: 34 additions & 5 deletions src/main/presenter/configPresenter/modelConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -154,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 {
Expand All @@ -178,10 +202,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),
Comment on lines +205 to +211

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

MiniMax-M3 context floor can be silently bypassed by provider cache merge.

You compute the M3 floor here, but Line 534 later prefers storedConfig.contextLength for source: 'provider' entries. A stale provider cache value (e.g., 512000) will override the new 1_000_000 minimum and break the intended fallback behavior.

Proposed fix
-        contextLength: storedConfig.contextLength ?? finalConfig.contextLength,
+        contextLength:
+          isMiniMaxM3Model(providerId, modelId)
+            ? Math.max(
+                storedConfig.contextLength ?? 0,
+                finalConfig.contextLength ?? MINIMAX_M3_CONTEXT_LENGTH
+              )
+            : storedConfig.contextLength ?? finalConfig.contextLength,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/presenter/configPresenter/modelConfig.ts` around lines 195 - 201,
The MiniMax-M3 context floor computed at the isMiniMaxM3Model check is being
silently overridden by stale provider cache values during the merge at the
provider source preference logic (line 534). Ensure that when merging
provider-specific configurations, the M3 context floor is not bypassed by cached
values. Either prevent the storedConfig.contextLength preference from applying
to M3 models, or reapply the M3 floor constraint after the provider cache merge
in the applyProviderSpecificPolicies method to guarantee the floor remains
intact regardless of cached state.

timeout: DEFAULT_MODEL_TIMEOUT,
temperature: 0.6,
topP: undefined,
Expand All @@ -199,7 +226,9 @@ export class ModelConfigHelper {
? ApiEndpointType.AudioSpeech
: ApiEndpointType.Chat,
thinkingBudget,
forceInterleavedThinkingCompat,
forceInterleavedThinkingCompat: isMiniMaxM3Model(providerId, model.id)
? true
: forceInterleavedThinkingCompat,
reasoningEffort,
reasoningVisibility,
verbosity,
Expand Down Expand Up @@ -455,7 +484,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
}
Expand All @@ -472,7 +501,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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
93 changes: 93 additions & 0 deletions test/main/presenter/providerDbModelConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
]
}
}
}
Expand Down Expand Up @@ -325,6 +361,63 @@ 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('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')

Expand Down