From 9708feddc90a1f26586c8e23b985b7ff02e28840 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Sat, 20 Jun 2026 20:25:50 -0300 Subject: [PATCH] fix(translator): strip Claude output_config before MiniMax (#4448) Integrated into release/v3.8.32 --- CHANGELOG.md | 1 + open-sse/translator/helpers/claudeHelper.ts | 18 +++++++ ...laude-helper-minimax-output-config.test.ts | 53 +++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 tests/unit/claude-helper-minimax-output-config.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 24180fbd2f..8ed8ef1584 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ _In development — bullets added per PR; finalized at release._ - **fix(usage):** reuse Gemini CLI project ID for quota checks (avoid re-discovery). (thanks @Delcado19) - **fix(dashboard):** surface manual config CTA when Claude CLI detection fails (remote deployments). (thanks @anuragg-saxenaa) - **fix(executors):** granular reasoning_effort handling for Claude models on GitHub Copilot. (thanks @baslr) +- **fix(translator):** strip Claude output_config before MiniMax (rejected upstream). (thanks @hiepau1231) - **fix(combo): round-robin members fail over faster under concurrency saturation via a configurable queue depth** — when a round-robin combo member was saturated, requests sat in the per-model semaphore's **unbounded** queue and only failed over to the next member after the full `queueTimeoutMs` (default 30s) elapsed — so a burst of agentic requests deep-queued one hot member instead of spilling to healthy ones. The per-model semaphore now accepts a bounded queue depth and emits `SEMAPHORE_QUEUE_FULL` once it is full (the round-robin loop already cascades on that code), so a configured low depth fails over immediately. A new `queueDepth` combo-config knob (global default / provider override / per-combo, default **20** for backward compatibility; **0** = never queue → fail over now) is exposed in Settings → Combo Defaults. ([#3872](https://github.com/diegosouzapw/OmniRoute/issues/3872) — thanks @KooshaPari) - **fix(pricing): align Claude Code (`cc`) pricing with current Anthropic per-MTok rates** — the `cc` provider block in the default pricing table had stale numbers across every Claude 4.x family entry — most visibly, `claude-opus-4-5-20251101` was billed at the deprecated Opus 4.1 rate (`input $15` / `output $75`), and `claude-haiku-4-5-20251001` was at half the current Haiku 4.5 rate. The `cached` (cache hit) and `cache_creation` (5-minute cache write) multipliers were also off across Opus 4.6/4.7/4.8, Sonnet 4.5/4.6, Haiku 4.5, and Fable 5. All eight entries now match the rates Anthropic publishes (input, 5m cache write at 1.25x input, cache hit at 0.1x input, output; reasoning billed at the output rate), so cost accounting on the dashboard and per-request usage events stop under- or over-reporting Claude Code spend. (thanks @chulanpro5) diff --git a/open-sse/translator/helpers/claudeHelper.ts b/open-sse/translator/helpers/claudeHelper.ts index 088c354b67..4c5a60e08b 100644 --- a/open-sse/translator/helpers/claudeHelper.ts +++ b/open-sse/translator/helpers/claudeHelper.ts @@ -2,6 +2,17 @@ import { DEFAULT_THINKING_CLAUDE_SIGNATURE } from "../../config/defaultThinkingSignature.ts"; import { lookupReasoning, recordReplay } from "../../services/reasoningCache.ts"; +// MiniMax exposes a Claude-compatible endpoint but rejects Anthropic's extended +// `output_config` parameter (used to steer reasoning effort and structured output) +// with a generic 400 "invalid params" response. Strip the entire field before +// dispatching Claude-shape requests to these providers. Anthropic Claude and +// other Claude-compatible upstreams that do accept it are unaffected. +// Ported from upstream decolua/9router#820 by @hiepau1231. +const CLAUDE_FORMAT_PROVIDERS_WITHOUT_OUTPUT_CONFIG = new Set([ + "minimax", + "minimax-cn", +]); + // Placeholder thinking text used as last-resort fallback when: // - Target upstream is a non-Anthropic Claude-shape provider // (kimi-coding, glmt, zai, …) that rejects redacted_thinking blobs @@ -211,6 +222,13 @@ export function prepareClaudeRequest( provider: string | null = null, preserveCacheControl = false ): ClaudeRequestBody { + // 0. Strip Anthropic `output_config` for providers that reject it on their + // Claude-compatible endpoints (MiniMax). Must run before any downstream + // processing so the field never reaches translateRequest/the executor. + if (provider && CLAUDE_FORMAT_PROVIDERS_WITHOUT_OUTPUT_CONFIG.has(provider)) { + delete body.output_config; + } + // 1. System: remove all cache_control, add only to last block with ttl 1h // In passthrough mode, preserve existing cache_control markers const supportsPromptCaching = diff --git a/tests/unit/claude-helper-minimax-output-config.test.ts b/tests/unit/claude-helper-minimax-output-config.test.ts new file mode 100644 index 0000000000..cb0d701aaa --- /dev/null +++ b/tests/unit/claude-helper-minimax-output-config.test.ts @@ -0,0 +1,53 @@ +// Port of upstream decolua/9router#820 by @hiepau1231. +// MiniMax exposes a Claude-compatible endpoint but rejects Anthropic's +// extended `output_config` parameter (used to steer reasoning effort and +// structured output) with a generic 400 "invalid params" response. +// `prepareClaudeRequest()` must strip the entire `output_config` for +// MiniMax providers, while preserving it verbatim for Anthropic Claude +// and other Claude-compatible upstreams that already accept it. +import { describe, test } from "node:test"; +import assert from "node:assert/strict"; +import { prepareClaudeRequest } from "../../open-sse/translator/helpers/claudeHelper.ts"; + +describe("prepareClaudeRequest output_config stripping for MiniMax", () => { + const buildBody = () => ({ + model: "MiniMax-M2.7", + system: [{ type: "text", text: "You are helpful." }], + messages: [{ role: "user", content: [{ type: "text", text: "continue" }] }], + max_tokens: 1024, + output_config: { + effort: "medium", + format: { + type: "json_schema", + schema: { + type: "object", + properties: { title: { type: "string" } }, + required: ["title"], + additionalProperties: false, + }, + }, + }, + }); + + test("strips output_config (effort + format) for minimax", () => { + const body = buildBody(); + const result = prepareClaudeRequest(body as any, "minimax"); + assert.equal(result.output_config, undefined); + // Sanity: rest of the request must still be intact. + assert.equal(result.messages?.[0]?.content?.[0]?.text, "continue"); + assert.equal(result.max_tokens, 1024); + }); + + test("strips output_config (effort + format) for minimax-cn", () => { + const body = buildBody(); + const result = prepareClaudeRequest(body as any, "minimax-cn"); + assert.equal(result.output_config, undefined); + }); + + test("preserves output_config for Anthropic Claude", () => { + const body = buildBody(); + const original = JSON.parse(JSON.stringify(body.output_config)); + const result = prepareClaudeRequest(body as any, "claude"); + assert.deepEqual(result.output_config, original); + }); +});