From c61cdc30aa8f8a95d5ec9086baa512d4a0438a44 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Tue, 11 Aug 2026 07:25:28 -0300 Subject: [PATCH] fix(providers): switch minimax from claude to openai format so images work (#9463) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(providers): switch minimax from claude to openai format so images work The Anthropic-compatible /anthropic/v1/messages endpoint rejects image input with 403. MiniMax's OpenAI-compatible /v1/chat/completions endpoint supports image_url natively for MiniMax-M3. - minimax + minimax-cn: format claude→openai, baseUrl→/v1/chat/completions - Remove Anthropic-Version header + ?beta=true suffix (not needed for openai) - Remove minimax/minimax-cn from ?beta=true executor case - Update cache-control tests (openai format uses different caching path) - Fix reasoning-split test names (no longer claude format) TDD: 2 registry tests assert format=openai (red→green). Refs: Hermes Agent #15715, MiniMax OpenAI-compatible API docs. * fix(sse): re-align stream-readiness-policy tests with minimax's openai format PR #9463 switched minimax/minimax-cn from claude to openai format so images work. The stream-readiness bump for Claude-format replicas is keyed off the registry's format field (single source of truth), so minimax legitimately falls out of that group now. Swap the "Claude-format replica" test fixtures to agentrouter (still format: "claude") and add explicit coverage that minimax no longer gets the claude_format_heavy_reasoning bump. --------- Co-authored-by: diegosouzapw --- .../providers/registry/minimax/cn/index.ts | 8 ++--- .../providers/registry/minimax/index.ts | 8 ++--- open-sse/executors/default.ts | 2 -- .../cache-control-claude-providers.test.ts | 12 ++++--- tests/unit/minimax-m3-model-registry.test.ts | 20 +++++++++++ .../responsesanitizer-reasoning-split.test.ts | 4 +-- tests/unit/stream-readiness-policy.test.ts | 35 ++++++++++++++----- 7 files changed, 60 insertions(+), 29 deletions(-) diff --git a/open-sse/config/providers/registry/minimax/cn/index.ts b/open-sse/config/providers/registry/minimax/cn/index.ts index 8046b9e9d6..91981768d3 100644 --- a/open-sse/config/providers/registry/minimax/cn/index.ts +++ b/open-sse/config/providers/registry/minimax/cn/index.ts @@ -1,18 +1,14 @@ import type { RegistryEntry } from "../../../shared.ts"; -import { getAnthropicCompatHeaders, ANTHROPIC_VERSION_HEADER } from "../../../shared.ts"; export const minimax_cnProvider: RegistryEntry = { id: "minimax-cn", alias: "minimax-cn", // unique alias (was colliding with minimax) - format: "claude", + format: "openai", executor: "default", - baseUrl: "https://api.minimaxi.com/anthropic/v1/messages", + baseUrl: "https://api.minimaxi.com/v1/chat/completions", modelsUrl: "https://api.minimaxi.com/v1/models", - urlSuffix: "?beta=true", authType: "apikey", authHeader: "bearer", - headers: getAnthropicCompatHeaders(), - ensureThinkingSignature: true, models: [ // Keep parity with minimax to ensure model discovery works for minimax-cn connections. // #3110: MiniMax M3 — frontier coding model with 1M context diff --git a/open-sse/config/providers/registry/minimax/index.ts b/open-sse/config/providers/registry/minimax/index.ts index 6f1f80f51f..54fc7b3058 100644 --- a/open-sse/config/providers/registry/minimax/index.ts +++ b/open-sse/config/providers/registry/minimax/index.ts @@ -1,18 +1,14 @@ import type { RegistryEntry } from "../../shared.ts"; -import { getAnthropicCompatHeaders, ANTHROPIC_VERSION_HEADER } from "../../shared.ts"; export const minimaxProvider: RegistryEntry = { id: "minimax", alias: "minimax", - format: "claude", + format: "openai", executor: "default", - baseUrl: "https://api.minimax.io/anthropic/v1/messages", + baseUrl: "https://api.minimax.io/v1/chat/completions", modelsUrl: "https://api.minimax.io/v1/models", - urlSuffix: "?beta=true", authType: "apikey", authHeader: "bearer", - headers: getAnthropicCompatHeaders(), - ensureThinkingSignature: true, models: [ // T12/T28: MiniMax default upgraded from M2.5 to M2.7 // #3110: MiniMax M3 — frontier coding model with 1M context diff --git a/open-sse/executors/default.ts b/open-sse/executors/default.ts index 8814c4262a..52dd139871 100644 --- a/open-sse/executors/default.ts +++ b/open-sse/executors/default.ts @@ -358,8 +358,6 @@ export class DefaultExecutor extends BaseExecutor { case "glm": case "glmt": case "kimi-coding": - case "minimax": - case "minimax-cn": return `${this.config.baseUrl}?beta=true`; case "agentrouter": return this.usesClaudeCodeProtocol(credentials) diff --git a/tests/unit/cache-control-claude-providers.test.ts b/tests/unit/cache-control-claude-providers.test.ts index ed50593e33..5f8a96255c 100644 --- a/tests/unit/cache-control-claude-providers.test.ts +++ b/tests/unit/cache-control-claude-providers.test.ts @@ -17,8 +17,10 @@ describe("Cache Control Policy - Claude Protocol Providers", () => { // These should be detected via targetFormat assert.equal(providerSupportsCaching("bailian-coding-plan", "claude"), true); assert.equal(providerSupportsCaching("glm", "claude"), true); - assert.equal(providerSupportsCaching("minimax", "claude"), true); - assert.equal(providerSupportsCaching("minimax-cn", "claude"), true); + // minimax/minimax-cn use openai format (#3110 / image 403 fix); + // caching support for their OpenAI-compatible endpoint is TBD + assert.equal(providerSupportsCaching("minimax", "openai"), false); + assert.equal(providerSupportsCaching("minimax-cn", "openai"), false); assert.equal(providerSupportsCaching("kimi-coding", "claude"), true); // #3955 — OpenAI / Codex use automatic prefix caching (no cache_control needed). @@ -66,15 +68,17 @@ describe("Cache Control Policy - Claude Protocol Providers", () => { true ); + // minimax now uses openai format — caching behavior may differ; + // cache_control preservation depends on whether it joins CACHING_PROVIDERS assert.equal( shouldPreserveCacheControl({ userAgent: claudeCodeUA, isCombo: false, targetProvider: "minimax", - targetFormat: "claude", + targetFormat: "openai", settings: { alwaysPreserveClientCache: "auto" }, }), - true + false ); }); diff --git a/tests/unit/minimax-m3-model-registry.test.ts b/tests/unit/minimax-m3-model-registry.test.ts index 2aae2b3a69..d1e1ec74be 100644 --- a/tests/unit/minimax-m3-model-registry.test.ts +++ b/tests/unit/minimax-m3-model-registry.test.ts @@ -73,6 +73,26 @@ describe("MiniMax M3 model registration (#3110)", () => { assert.equal(m3.contextLength, 1_048_576); }); + it("minimax uses openai format (not claude) so images work via /v1/chat/completions", () => { + const entry = REGISTRY.minimax; + assert.ok(entry, "minimax registry entry must exist"); + assert.equal( + entry.format, + "openai", + "minimax must use openai format — the Anthropic-compatible /anthropic/v1/messages endpoint rejects images with 403; images work on the OpenAI-compatible /v1/chat/completions endpoint. See Hermes Agent #15715." + ); + }); + + it("minimax-cn uses openai format (not claude) so images work via /v1/chat/completions", () => { + const entry = REGISTRY["minimax-cn"]; + assert.ok(entry, "minimax-cn registry entry must exist"); + assert.equal( + entry.format, + "openai", + "minimax-cn must use openai format — parity with minimax; the Anthropic endpoint on api.minimaxi.com also rejects images." + ); + }); + it("nvidia provider does NOT list minimaxai/minimax-m3 (removed in #3329 — 404 upstream)", () => { const entry = REGISTRY.nvidia; assert.ok(entry, "nvidia registry entry must exist"); diff --git a/tests/unit/responsesanitizer-reasoning-split.test.ts b/tests/unit/responsesanitizer-reasoning-split.test.ts index dde3d50cd6..70ee456d7d 100644 --- a/tests/unit/responsesanitizer-reasoning-split.test.ts +++ b/tests/unit/responsesanitizer-reasoning-split.test.ts @@ -137,12 +137,12 @@ describe("responseSanitizer/reasoning — MiniMax M3 textual reasoning-tag route }); describe("responseSanitizer/reasoning — MiniMax M3 fix regression guards", () => { - it("direct minimax tier (claude format) stays unaffected", () => { + it("direct minimax tier (openai format) stays unaffected for textual reasoning tags", () => { assert.equal(isTextualReasoningTagNativeRoute("minimax", "minimax-m3"), false); assert.equal(shouldParseTextualReasoningTags("minimax", "MiniMax-M3"), false); }); - it("direct minimax-cn tier (claude format) stays unaffected", () => { + it("direct minimax-cn tier (openai format) stays unaffected for textual reasoning tags", () => { assert.equal(isTextualReasoningTagNativeRoute("minimax-cn", "minimax-m3"), false); assert.equal(shouldParseTextualReasoningTags("minimax-cn", "MiniMax-M3"), false); }); diff --git a/tests/unit/stream-readiness-policy.test.ts b/tests/unit/stream-readiness-policy.test.ts index 1c5abe908e..0292f24b1d 100644 --- a/tests/unit/stream-readiness-policy.test.ts +++ b/tests/unit/stream-readiness-policy.test.ts @@ -145,14 +145,14 @@ test("preserves zero timeout so readiness checks can be disabled", () => { assert.deepEqual(result.reasons, ["disabled"]); }); -test("bumps small requests to third-party Claude-format replicas (Minimax M3, ZAI, bailian, agentrouter) — guards against #3825-class false 504s on long reasoning warm-ups", () => { - // Provider registry lists Minimax with `format: "claude"` — the readiness budget +test("bumps small requests to third-party Claude-format replicas (agentrouter, ZAI, bailian) — guards against #3825-class false 504s on long reasoning warm-ups", () => { + // Provider registry lists agentrouter with `format: "claude"` — the readiness budget // must fire UNCONDITIONALLY for those replicas, like the codex_gpt_5_5_high // bump, because their reasoning warm-ups routinely exceed the default 80s window. const result = resolveStreamReadinessTimeout({ baseTimeoutMs: 80_000, - provider: "minimax", - model: "MiniMax-M3", + provider: "agentrouter", + model: "claude-opus-4-8", body: { messages: items(3), tools: tools(2) }, }); @@ -163,6 +163,23 @@ test("bumps small requests to third-party Claude-format replicas (Minimax M3, ZA ); }); +test("does NOT bump Minimax (M3) — #3110 moved it from claude to openai format so images work, and the readiness bump is keyed off the registry's `format: \"claude\"` field", () => { + // Minimax's replica quirk (long reasoning warm-up) hasn't changed, but this + // policy intentionally keys off the translator format, not the provider + // name — the registry is the single source of truth (see isClaudeFormatReasoningProvider + // doc comment). Now that minimax routes through the OpenAI translator, it no + // longer matches, mirroring the OpenAI/non-Claude exclusion below. + const result = resolveStreamReadinessTimeout({ + baseTimeoutMs: 80_000, + provider: "minimax", + model: "MiniMax-M3", + body: { messages: items(3), tools: tools(2) }, + }); + + assert.equal(result.timeoutMs, 80_000); + assert.ok(!result.reasons.includes("claude_format_heavy_reasoning")); +}); + test("bumps ZAI (claude-format replica) readiness budget the same way", () => { const result = resolveStreamReadinessTimeout({ baseTimeoutMs: 80_000, @@ -213,13 +230,13 @@ test("does NOT double-bump when codex-high reasoning and Claude-format replica b // Claude-format providers later, the readiness bump must not stack. const result = resolveStreamReadinessTimeout({ baseTimeoutMs: 80_000, - provider: "minimax", - model: "MiniMax-M3-high", + provider: "agentrouter", + model: "claude-opus-4-8-high", body: { messages: items(3), tools: tools(2), reasoning_effort: "high" }, }); // Should be bumped by exactly one reason — claude_format_heavy_reasoning — - // because minimax is not a codex provider, the codex_* path never fires. + // because agentrouter is not a codex provider, the codex_* path never fires. assert.equal(result.timeoutMs, 110_000); assert.ok(result.reasons.includes("claude_format_heavy_reasoning")); assert.ok(!result.reasons.includes("codex_gpt_5_5_high_reasoning")); @@ -229,8 +246,8 @@ test("caps Claude-format replica bump at the configured maxTimeoutMs", () => { const result = resolveStreamReadinessTimeout({ baseTimeoutMs: 80_000, maxTimeoutMs: 100_000, - provider: "minimax", - model: "MiniMax-M3", + provider: "agentrouter", + model: "claude-opus-4-8", body: { messages: items(500), tools: tools(20), instructions: "x".repeat(800_000) }, });