Dashboard › Code › Session 0CTXksJacZC8
0CTXksJacZC8T7eijKeep getting invalid params, 400 (2013) when I try to use MiniMax
That's a MiniMax API error, not a Lore error — so traffic is reaching MiniMax now (routing works!). The 400 with code 2013 suggests a parameter issue in the request body. Let me investigate what could cause this when the gateway translates the request.
task_id: ses_1690ab872ffeV1HoPc1gHDWg5v (for resuming to continue this task if needed) <task_result> I now have a thorough understanding of the full request flow. Here are my complete findings: --- ## Investigation Results: MiniMax "invalid params, 400 (2013)" Error ### Summary There is **no code path that strips Anthropic-specific features when forwarding to non-Anthropic providers using the Anthropic protocol**. The gateway treats MiniMax identically to `api.anthropic.com`. Multiple Anthropic-specific fields and headers are sent that MiniMax's Anthropic-compatible endpoint likely does not support. --- ### Finding 1: `cache_control` breakpoints on system, messages, and tools **File:** `/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/lucky-cactus/packages/gateway/src/translate/anthropic.ts` `buildAnthropicRequest()` injects `cache_control` objects in **three places**: 1. ...
The investigation is clear. The main culprit is that the gateway applies all Anthropic-specific features (cache_control, thinking, beta headers, metadata passthrough) unconditionally to any provider using the Anthropic protocol — even third-party compatible endpoints like MiniMax that don't support these extensions.
The fix needs to strip Anthropic-specific extensions when forwarding to non-native-Anthropic endpoints. Let me look at where the best place to gate this is:
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/lucky-cactus/packages/gateway/src/translate/anthropic.ts</path>
<type>file</type>
<content>
340: export function buildAnthropicRequest(
341: req: GatewayRequest,
342: cache?: AnthropicCacheOptions,
343: ): {
344: url: string;
345: headers: Record<string, string>;
346: body: unknown;
347: } {
348: // --- Headers ---
349: const headers: Record<string, string> = {
350: "content-type": "application/json",
351: "anthropic-version": ANTHROPIC_VERSION,
352: };
353:
354: // Forward auth from the original request (API key or OAuth Bearer)
355: const cred = extractAuth(req.rawHeaders);
356: if (cred) {
357: Object.assign(headers, authHeaders(cred));
358: }
359:
360: // Forward anthropic-beta if present (enables features like extended thinking)
361: const beta =
362: req.rawHeaders["anthropic-beta"] || req.rawHeaders["Anthropic-Beta"] || "";
363: if (beta) ...
Now let me see how buildAnthropicRequest is called from pipeline.ts — specifically what cache options are passed:
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/lucky-cactus/packages/gateway/src/pipeline.ts</path>
<type>file</type>
<content>
1594: // Inject LTM into system prompt for non-Anthropic paths.
1595: // Anthropic handles LTM via separate system blocks in buildAnthropicRequest;
1596: // OpenAI paths receive a single system string, so we concatenate here.
1597: const ltmParts = [cache?.stableLtmSystem, cache?.ltmSystem].filter(Boolean);
1598: const reqWithLtm = ltmParts.length
1599: ? {
1600: ...req,
1601: system: [req.system, ...ltmParts].filter(Boolean).join("\n\n"),
1602: }
1603: : req;
1604: const result = buildOpenAIResponsesUpstreamRequest(
1605: reqWithLtm,
1606: effectiveUpstreamBase,
1607: );
1608: url = result.url;
1609: headers = result.headers;
1610: body = result.body;
1611: } else if (effectiveProtocol === "openai") {
1612: // Inject LTM ...
Let me see the Anthropic path specifically:
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/lucky-cactus/packages/gateway/src/pipeline.ts</path>
<type>file</type>
<content>
1624: url = result.url;
1625: headers = result.headers;
1626: body = result.body;
1627: } else {
1628: const result = buildAnthropicRequest(req, cache);
1629: url = `${effectiveUpstreamBase}${result.url}`;
1630: headers = result.headers;
1631: body = result.body;
1632: }
1633:
1634: let serializedBody = JSON.stringify(body);
1635:
1636: // Re-sign the billing header cch after body reconstruction.
1637: // buildAnthropicRequest completely rebuilds the body (different JSON key
1638: // ordering, cache_control wrappers, toAnthropicBlock transforms) which
1639: // invalidates the client's original cch signature. resignBody detects
1640: // billing headers and re-signs with our known seed + version. ...
Now I see the full picture. The cleanest fix is: when the upstream is NOT native Anthropic (api.anthropic.com), pass cache = undefined to buildAnthropicRequest and strip anthropic-beta headers. This disables all cache_control breakpoints and the system block array format, falling back to the simple string system prompt path.
Let me also check what happens with the thinking metadata passthrough — that needs to be stripped too for non-native upstreams.
The key insight: we already have effectiveUpstreamBase resolved. We can check if it's api.anthropic.com to decide whether to enable Anthropic-specific extensions.
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/lucky-cactus/packages/gateway/src/translate/anthropic.ts</path> <type>file</type> <content> 20: 21: const ANTHROPIC_VERSION = "2023-06-01"; 22: 23: // --------------------------------------------------------------------------- 24: // Fields the gateway reads/writes — everything else goes into `metadata` 25: // --------------------------------------------------------------------------- 26: 27: /** Top-level body fields that are extracted into `GatewayRequest` fields. */ 28: const KNOWN_BODY_FIELDS = new Set([ 29: "model", 30: "system", 31: "messages", 32: "tools", 33: "max_tokens", 34: "stream", (Showing lines 20-34 of 586. Use offset=35 to continue.) </content>
The approach is clear. The simplest and most robust fix:
cache = undefined to buildAnthropicRequest — this disables cache_control breakpoints and sends system as a plain stringanthropic-beta header for non-native upstreamsthinking, metadata) for non-native upstreamsLet me check what metadata keys are Anthropic-specific:
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/lucky-cactus/packages/gateway/src/translate/anthropic.ts</path>
<type>file</type>
<content>
216: const rawTools = Array.isArray(raw.tools) ? raw.tools : [];
217: const tools: GatewayTool[] = rawTools.map((t: Record<string, unknown>) => ({
218: name: String(t.name ?? ""),
219: description: String(t.description ?? ""),
220: inputSchema: (t.input_schema as Record<string, unknown>) ?? {},
221: }));
222:
223: // --- Metadata: everything the gateway doesn't explicitly process ---
224: const metadata: Record<string, unknown> = {};
225: for (const [key, value] of Object.entries(raw)) {
226: if (!KNOWN_BODY_FIELDS.has(key)) {
227: metadata[key] = value;
228: }
229: }
230:
231: return {
232: protocol: "anthropic",
233: model,
234: system,
235: messages,
(Showing lines 216-235 of 586. Use offset=236 to continue.)
</content>
Now I have the full picture. The cleanest approach is to detect whether the upstream is native Anthropic in the pipeline and adjust accordingly. Let me implement this:
[
{
"content": "Strip Anthropic-specific extensions (cache_control, beta headers, thinking) for non-native Anthropic upstreams like MiniMax",
"status": "in_progress",
"priority": "high"
},
{
"content": "Run typecheck, tests, lint",
"status": "pending",
"priority": "high"
},
{
"content": "Push and verify CI",
"status": "pending",
"priority": "high"
}
]
The fix point is in forwardToUpstream where buildAnthropicRequest is called. I need to:
api.anthropic.com)cache = undefined (disables cache_control, uses string system prompt)anthropic-beta header from the resultLet me also check what the ANTHROPIC_ONLY_METADATA keys would be — thinking is the main one: