DashboardCodeSession 0CTXksJacZC8

Session 0CTXksJacZC8

Full ID: 0CTXksJacZC8T7eij
Messages: 16
Distillations: 0
Time range: 2026-06-05 08:45:17 — 2026-06-05 08:48:47

Conversation (16 messages)

Keep getting invalid params, 400 (2013) when I try to use MiniMax

gateway · 2026-06-05 08:45:17

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.

claude-opus-4-6 · 2026-06-05 08:45:17 · ~85 tokens
result
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. ...
gateway · 2026-06-05 08:48:06

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:

claude-opus-4-6 · 2026-06-05 08:48:06 · ~159 tokens
result
<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) ...
gateway · 2026-06-05 08:48:11

Now let me see how buildAnthropicRequest is called from pipeline.ts — specifically what cache options are passed:

claude-opus-4-6 · 2026-06-05 08:48:11 · ~39 tokens
result
<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 ...
gateway · 2026-06-05 08:48:16

Let me see the Anthropic path specifically:

claude-opus-4-6 · 2026-06-05 08:48:16 · ~15 tokens
result
<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. ...
gateway · 2026-06-05 08:48:23

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.

claude-opus-4-6 · 2026-06-05 08:48:23 · ~212 tokens
result
<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>
gateway · 2026-06-05 08:48:29

The approach is clear. The simplest and most robust fix:

  1. When upstream is NOT native Anthropic, pass cache = undefined to buildAnthropicRequest — this disables cache_control breakpoints and sends system as a plain string
  2. Strip anthropic-beta header for non-native upstreams
  3. Strip Anthropic-specific metadata keys (thinking, metadata) for non-native upstreams

Let me check what metadata keys are Anthropic-specific:

claude-opus-4-6 · 2026-06-05 08:48:29 · ~146 tokens
result
<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>
gateway · 2026-06-05 08:48:35

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:

claude-opus-4-6 · 2026-06-05 08:48:36 · ~57 tokens
result
[
  {
    "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"
  }
]
gateway · 2026-06-05 08:48:47

The fix point is in forwardToUpstream where buildAnthropicRequest is called. I need to:

  1. Detect if upstream is native Anthropic (api.anthropic.com)
  2. If not: pass cache = undefined (disables cache_control, uses string system prompt)
  3. Strip anthropic-beta header from the result
  4. Strip Anthropic-specific metadata keys from the request before building

Let me also check what the ANTHROPIC_ONLY_METADATA keys would be — thinking is the main one:

claude-opus-4-6 · 2026-06-05 08:48:47 · ~156 tokens