diff --git a/open-sse/translator/helpers/claudeHelper.js b/open-sse/translator/helpers/claudeHelper.js index a08b6bc5..762f5b03 100644 --- a/open-sse/translator/helpers/claudeHelper.js +++ b/open-sse/translator/helpers/claudeHelper.js @@ -76,6 +76,8 @@ export function fixToolUseOrdering(messages) { return merged; } +const CLAUDE_FORMAT_PROVIDERS_WITHOUT_OUTPUT_CONFIG = new Set(["minimax", "minimax-cn"]); + // Prepare request for Claude format endpoints // - Cleanup cache_control // - Filter empty messages @@ -83,6 +85,12 @@ export function fixToolUseOrdering(messages) { // - Fix tool_use/tool_result ordering // - Apply cloaking (billing header + fake user ID) for OAuth tokens export function prepareClaudeRequest(body, provider = null, apiKey = null, connectionId = null) { + // MiniMax exposes a Claude-compatible endpoint but rejects Anthropic's extended + // structured output parameter with a generic 400 "invalid params" response. + if (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 if (body.system && Array.isArray(body.system)) { body.system = body.system.map((block, i) => { diff --git a/tests/unit/translator-request-normalization.test.js b/tests/unit/translator-request-normalization.test.js index 0a67a91d..7165c9fa 100644 --- a/tests/unit/translator-request-normalization.test.js +++ b/tests/unit/translator-request-normalization.test.js @@ -96,6 +96,74 @@ describe("request normalization", () => { expect(userMessage.content).toBe("hello\nworld"); }); + it("translateRequest strips unsupported Anthropic output_config for MiniMax Claude-compatible endpoints", () => { + const body = { + 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, + }, + }, + }, + }; + + const result = translateRequest( + FORMATS.CLAUDE, + FORMATS.CLAUDE, + "MiniMax-M2.7", + JSON.parse(JSON.stringify(body)), + true, + null, + "minimax", + ); + + expect(result.output_config).toBeUndefined(); + expect(result.messages[0].content[0].text).toBe("continue"); + }); + + it("translateRequest preserves output_config for Anthropic Claude", () => { + const body = { + model: "claude-sonnet-4.5", + system: [{ type: "text", text: "You are helpful." }], + messages: [ + { + role: "user", + content: [{ type: "text", text: "continue" }], + }, + ], + max_tokens: 1024, + output_config: { + format: { type: "json_schema", schema: { type: "object" } }, + }, + }; + + const result = translateRequest( + FORMATS.CLAUDE, + FORMATS.CLAUDE, + "claude-sonnet-4.5", + JSON.parse(JSON.stringify(body)), + true, + null, + "claude", + ); + + expect(result.output_config).toEqual(body.output_config); + }); + it("parseSSELine supports provider raw NDJSON stream lines", () => { const raw = JSON.stringify({ model: "gpt-oss:120b",