Skip to content

feat(openai): support adjusting thinking level for MiniMax-M3#3432

Closed
lightfront wants to merge 1 commit into
esengine:main-v2from
lightfront:feat/minimax-thinking-level
Closed

feat(openai): support adjusting thinking level for MiniMax-M3#3432
lightfront wants to merge 1 commit into
esengine:main-v2from
lightfront:feat/minimax-thinking-level

Conversation

@lightfront

Copy link
Copy Markdown
Contributor

MiniMax-M3 exposes a single binary thinking knob (adaptive | disabled) on its OpenAI-compatible endpoint at api.minimaxi.com. Today our provider unconditionally emits reasoning_effort, which M3 rejects — so users with a MiniMax provider can't actually configure the thinking level even though the EffortCapability plumbing implies they can.

This PR teaches the openai provider and the EffortCapability layer to recognize api.minimaxi.com as a separate wire shape, mirroring the existing DeepSeek handling.

Wire-format change

For a base_url of api.minimaxi.com (or any *.minimaxi.com subdomain), buildRequest now:

  • emits thinking.type = "adaptive" when /effort auto (the empty-effort case, which is also the M3 model default)
  • emits thinking.type = "adaptive" for the explicit adaptive level
  • emits thinking.type = "disabled" for the disabled level
  • omits reasoning_effort entirely — M3 has no level scale and rejects the field

This is in addition to the existing DeepSeek path, which still emits both thinking.type = "enabled" and reasoning_effort = high|max.

User-facing EffortCapability change

M3 entries now expose /effort auto|adaptive|disabled with default adaptive. Legacy level names from other vendors resolve to a valid M3 level so stale /effort values don't error:

Input Resolved to Why
auto (empty, → adaptive on the wire) Don't override the M3 default
adaptive adaptive M3's "thinking on"
disabled disabled M3's "thinking off"
off disabled Retired DeepSeek level meaning "no thinking" — M3 actually supports this mode
low / medium / high adaptive OpenAI-style "low depth" maps to M3's "thinking on"
xhigh / max disabled "max depth" can't be expressed on M3, fall through to off

Unknown values get a MiniMax-specific usage hint (/effort auto|adaptive|disabled) instead of the generic message.

Refactor bundled in

The host-matching logic was duplicated across internal/provider/openai/openai.go (private *BaseURL helpers) and internal/config/effort.go (private *Entry wrappers). Adding a new gateway would have required updating both files in lockstep, with no compiler-level guarantee they stay in sync.

I extracted the shared primitive into a new internal/provider/openai/host.go:

func IsDeepSeek(baseURL string) bool {
    return matchesVendorHost(baseURL, "deepseek.com", "api.deepseek.com")
}

func IsMiniMax(baseURL string) bool {
    return matchesVendorHost(baseURL, "minimaxi.com", "api.minimaxi.com")
}

The *Entry wrappers in effort.go now just gate on the openai kind and delegate. net/url is no longer imported in effort.go or openai.go.

The new host_test.go pins the matching rule (canonical hostname exact match, plus any subdomain of the apex) directly at the source.

Tests

  • TestBuildRequestMiniMaxThinking — wire-shape coverage for all three levels (auto/adaptive/disabled), plus assertion that reasoning_effort is omitted
  • TestNewMiniMaxEffortValidation — boot-time validation of accepted and rejected effort values
  • TestNewMiniMaxSetsFlag — base-URL detection (with and without /v1 suffix)
  • TestIsMiniMaxEntry — host detection edge cases (subdomain variants accepted, bare apex rejected, wrong-spelling rejected)
  • TestEffortCapabilityMiniMax/levels and default for M3 entries
  • TestNormalizeEffortMiniMax — full table of legacy level remaps
  • TestNormalizeEffortMiniMaxRejectsGarbage — MiniMax-specific error message
  • TestEffectiveEffortMiniMaxEffectiveEffort resolution
  • TestIsDeepSeek / TestIsMiniMax (new in host_test.go) — host-matching primitives

All MiniMax and DeepSeek tests pass; the existing TestRealDeepSeekCacheProbe and the 18+ other openai tests are unchanged and still green.

Out of scope / not changed

  • The <think>…</think> block in content is handled by think.go (already in upstream from 19bf1872). This PR doesn't touch it.
  • The HTTP client, retry logic, and SSE parsing are unchanged.
  • Other OpenAI-compatible gateways (MiMo, vLLM, llama.cpp) fall into the default branch and are unaffected.

Test results

$ go vet ./...
(clean)

$ go test ./internal/provider/openai/ ./internal/config/ -run 'IsDeepSeek|IsMiniMax|MiniMax|DeepSeek|Effort'
ok  reasonix/internal/provider/openai  12.117s
ok  reasonix/internal/config           0.747s

@github-actions github-actions Bot added v2 Go rewrite (1.x) — main-v2 branch, active development config Configuration & setup (internal/config) provider Model providers & selection (internal/provider) labels Jun 7, 2026
MiniMax-M3 exposes a single binary thinking knob (adaptive|disabled)
on its OpenAI-compatible endpoint at api.minimaxi.com, but our
provider unconditionally emits reasoning_effort — which M3 rejects.
Detect api.minimaxi.com as a separate wire shape alongside
api.deepseek.com and:

  - emit thinking.type=adaptive|disabled in buildRequest
  - omit reasoning_effort entirely (M3 has no level scale)
  - translate /effort auto to 'adaptive' (the M3 default, since M3
    ships with thinking on out of the box and 'auto' semantically
    means 'don't override the model default')
  - map legacy level names from other vendors so a stale /effort
    value still resolves to a valid M3 level:
      off        → disabled  (retired DeepSeek 'no thinking' — M3
                              actually supports 'thinking off')
      low/medium/high → adaptive
      xhigh/max  → disabled
  - in NormalizeEffort, surface a MiniMax-specific usage hint
    (auto|adaptive|disabled) when an unknown level is supplied

internal/config/effort.go mirrors the change: M3 entries now expose
/levels=[auto,adaptive,disabled] with default=adaptive. Tests cover
the wire shape (TestBuildRequestMiniMaxThinking) and the
config-layer remap (TestNormalizeEffortMiniMax,
TestNormalizeEffortMiniMaxRejectsGarbage,
TestEffectiveEffortMiniMax).

Refactor: the host-matching logic was duplicated across
internal/provider/openai/openai.go (private *BaseURL helpers) and
internal/config/effort.go (private *Entry wrappers). Extract the
shared primitive into internal/provider/openai/host.go as
matchesVendorHost, plus thin exported wrappers IsDeepSeek and
IsMiniMax. The *Entry wrappers in effort.go now just gate on the
openai kind and delegate. Adding a new gateway is now a one-line
change in host.go rather than a four-line change across two files.

Add internal/provider/openai/host_test.go pinning the matching
rule (canonical hostname exact match, plus any subdomain of the
apex) directly at the source.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

config Configuration & setup (internal/config) provider Model providers & selection (internal/provider) v2 Go rewrite (1.x) — main-v2 branch, active development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant