fix(translator): strip Claude output_config before MiniMax (#4448)

Integrated into release/v3.8.32
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-20 20:25:50 -03:00
committed by GitHub
parent 0403f29805
commit 9708feddc9
3 changed files with 72 additions and 0 deletions

View File

@@ -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)

View File

@@ -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<string>([
"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 =

View File

@@ -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);
});
});