fix(tools): scope Claude tool type defaulting to gateways that need it (#3905)
defaultClaudeToolType() stamped tools[].type = "custom" onto every
Claude-format request carrying tools since e08ac6da. That satisfied
MiniMax (error 2013) but broke Anthropic-compatible endpoints that only
accept the legacy typeless tool shape. DeepSeek's endpoint
(api.deepseek.com/anthropic/v1/messages) whitelists its tool `type` enum
to the web_search_* variants and answers HTTP 400 "unknown variant
`custom`", so every Claude Code request routed to a DeepSeek connection
failed and surfaced as a persistent 503.
Run the defaulting only when the target provider declares the new
requireClaudeToolType quirk (MiniMax, MiniMax-CN). Add
shouldDefaultClaudeToolType(provider, finalFormat, tools, PROVIDERS) in
translator/concerns/toolCall.js so the gate is unit-testable, and cover
MiniMax keeping the explicit type, DeepSeek/Anthropic staying typeless,
non-Claude formats and tool-less requests never defaulting.
Effectively a no-op for MiniMax and a restore of the pre-e08ac6da
behaviour everywhere else. Another strict gateway now only needs the same
one-line quirk instead of a global behavioural change.
This commit is contained in:
@@ -28,7 +28,7 @@ import { compressWithPxpipe } from "../rtk/pxpipe.js";
|
|||||||
import { getCapabilitiesForModel } from "../providers/capabilities.js";
|
import { getCapabilitiesForModel } from "../providers/capabilities.js";
|
||||||
import { stripUnsupportedModalities } from "../translator/concerns/modality.js";
|
import { stripUnsupportedModalities } from "../translator/concerns/modality.js";
|
||||||
import { prefetchRemoteImages } from "../translator/concerns/prefetch.js";
|
import { prefetchRemoteImages } from "../translator/concerns/prefetch.js";
|
||||||
import { defaultClaudeToolType } from "../translator/concerns/toolCall.js";
|
import { defaultClaudeToolType, shouldDefaultClaudeToolType } from "../translator/concerns/toolCall.js";
|
||||||
import { resolveSessionId } from "../utils/sessionManager.js";
|
import { resolveSessionId } from "../utils/sessionManager.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -243,7 +243,11 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
|
|||||||
|
|
||||||
// Claude tool schema requires `type` to be explicitly set; strict gateways (e.g., MiniMax)
|
// Claude tool schema requires `type` to be explicitly set; strict gateways (e.g., MiniMax)
|
||||||
// reject legacy payloads that omit it with HTTP 400. Default to "custom" when missing.
|
// reject legacy payloads that omit it with HTTP 400. Default to "custom" when missing.
|
||||||
if (finalFormat === FORMATS.CLAUDE && Array.isArray(translatedBody.tools)) {
|
// Provider-scoped via quirks (shouldDefaultClaudeToolType): only gateways that declare
|
||||||
|
// requireClaudeToolType get the explicit type. Applying it unconditionally breaks
|
||||||
|
// Claude-format endpoints that only accept the legacy typeless tool shape — DeepSeek's
|
||||||
|
// Anthropic-compatible endpoint 400s with "unknown variant `custom`" (#3905).
|
||||||
|
if (shouldDefaultClaudeToolType(provider, finalFormat, translatedBody.tools, PROVIDERS)) {
|
||||||
translatedBody.tools = defaultClaudeToolType(translatedBody.tools);
|
translatedBody.tools = defaultClaudeToolType(translatedBody.tools);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export default {
|
|||||||
headers: { ...CLAUDE_API_HEADERS },
|
headers: { ...CLAUDE_API_HEADERS },
|
||||||
quirks: {
|
quirks: {
|
||||||
dropOutputConfig: true,
|
dropOutputConfig: true,
|
||||||
|
requireClaudeToolType: true,
|
||||||
},
|
},
|
||||||
reasoningInject: {
|
reasoningInject: {
|
||||||
scope: "all",
|
scope: "all",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export default {
|
|||||||
headers: { ...CLAUDE_API_HEADERS },
|
headers: { ...CLAUDE_API_HEADERS },
|
||||||
quirks: {
|
quirks: {
|
||||||
dropOutputConfig: true,
|
dropOutputConfig: true,
|
||||||
|
requireClaudeToolType: true,
|
||||||
},
|
},
|
||||||
reasoningInject: {
|
reasoningInject: {
|
||||||
scope: "all",
|
scope: "all",
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// Tool call helper functions for translator
|
// Tool call helper functions for translator
|
||||||
|
|
||||||
|
import { FORMATS } from "../formats.js";
|
||||||
|
|
||||||
// Anthropic tool_use.id must match: ^[a-zA-Z0-9_-]+$
|
// Anthropic tool_use.id must match: ^[a-zA-Z0-9_-]+$
|
||||||
const TOOL_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
const TOOL_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
||||||
|
|
||||||
@@ -165,3 +167,16 @@ export function defaultClaudeToolType(tools) {
|
|||||||
return tools.map(tool => tool?.type ? tool : { ...tool, type: "custom" });
|
return tools.map(tool => tool?.type ? tool : { ...tool, type: "custom" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Whether Claude-format tools need explicit `type` defaulting before dispatch.
|
||||||
|
// Only gateways that declare the `requireClaudeToolType` quirk (MiniMax) reject typeless
|
||||||
|
// tools. Applying the default globally breaks Claude-format endpoints that only accept the
|
||||||
|
// legacy typeless tool shape — DeepSeek's Anthropic-compatible endpoint answers HTTP 400
|
||||||
|
// "unknown variant `custom`" and every Claude Code request routed there fails (#3905).
|
||||||
|
export function shouldDefaultClaudeToolType(provider, finalFormat, tools, PROVIDERS) {
|
||||||
|
return (
|
||||||
|
finalFormat === FORMATS.CLAUDE
|
||||||
|
&& Array.isArray(tools)
|
||||||
|
&& PROVIDERS?.[provider]?.quirks?.requireClaudeToolType === true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
33
tests/translator/bugs-3905-deepseek-tool-type.test.js
Normal file
33
tests/translator/bugs-3905-deepseek-tool-type.test.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// Regression for #3905: defaultClaudeToolType() (type:"custom") must only run for
|
||||||
|
// gateways that declare the requireClaudeToolType quirk (MiniMax). Claude-format
|
||||||
|
// endpoints that only accept the legacy typeless tool shape — e.g. DeepSeek's
|
||||||
|
// Anthropic-compatible endpoint, which answers HTTP 400 "unknown variant `custom`" —
|
||||||
|
// must never receive tools[].type = "custom".
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { PROVIDERS } from "../../open-sse/providers/index.js";
|
||||||
|
import { FORMATS } from "../../open-sse/translator/formats.js";
|
||||||
|
import { shouldDefaultClaudeToolType } from "../../open-sse/translator/concerns/toolCall.js";
|
||||||
|
|
||||||
|
const tools = [{ name: "get_weather", description: "weather", input_schema: { type: "object" } }];
|
||||||
|
|
||||||
|
describe("Claude tool `type` defaulting is provider-scoped (#3905)", () => {
|
||||||
|
it("runs only for providers declaring requireClaudeToolType", () => {
|
||||||
|
expect(shouldDefaultClaudeToolType("minimax", FORMATS.CLAUDE, tools, PROVIDERS)).toBe(true);
|
||||||
|
expect(shouldDefaultClaudeToolType("minimax-cn", FORMATS.CLAUDE, tools, PROVIDERS)).toBe(true);
|
||||||
|
// Endpoints accepting only the legacy typeless shape must NOT get type:"custom".
|
||||||
|
expect(shouldDefaultClaudeToolType("deepseek", FORMATS.CLAUDE, tools, PROVIDERS)).toBe(false);
|
||||||
|
expect(shouldDefaultClaudeToolType("claude", FORMATS.CLAUDE, tools, PROVIDERS)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("never applies outside Claude-format requests or without tools", () => {
|
||||||
|
expect(shouldDefaultClaudeToolType("minimax", FORMATS.OPENAI, tools, PROVIDERS)).toBe(false);
|
||||||
|
expect(shouldDefaultClaudeToolType("minimax", FORMATS.CLAUDE, undefined, PROVIDERS)).toBe(false);
|
||||||
|
expect(shouldDefaultClaudeToolType("minimax", FORMATS.CLAUDE, null, PROVIDERS)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("declares the quirk only on the MiniMax providers (registry tripwire)", () => {
|
||||||
|
expect(PROVIDERS.minimax?.quirks?.requireClaudeToolType).toBe(true);
|
||||||
|
expect(PROVIDERS["minimax-cn"]?.quirks?.requireClaudeToolType).toBe(true);
|
||||||
|
expect(PROVIDERS.deepseek?.quirks?.requireClaudeToolType).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user