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 { stripUnsupportedModalities } from "../translator/concerns/modality.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";
|
||||
|
||||
/**
|
||||
@@ -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)
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ export default {
|
||||
headers: { ...CLAUDE_API_HEADERS },
|
||||
quirks: {
|
||||
dropOutputConfig: true,
|
||||
requireClaudeToolType: true,
|
||||
},
|
||||
reasoningInject: {
|
||||
scope: "all",
|
||||
|
||||
@@ -22,6 +22,7 @@ export default {
|
||||
headers: { ...CLAUDE_API_HEADERS },
|
||||
quirks: {
|
||||
dropOutputConfig: true,
|
||||
requireClaudeToolType: true,
|
||||
},
|
||||
reasoningInject: {
|
||||
scope: "all",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Tool call helper functions for translator
|
||||
|
||||
import { FORMATS } from "../formats.js";
|
||||
|
||||
// Anthropic tool_use.id must match: ^[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" });
|
||||
}
|
||||
|
||||
// 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