mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-20 14:12:59 +03:00
fix(chatcore): default Claude tool type to "custom" when missing (#5662)
Integrated into release/v3.8.43. Port from 9router#2196. Co-authored-by: warelik <warelik@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
cf25de44d4
commit
66374642f9
@@ -62,6 +62,7 @@ import { checkHeapPressureGuard } from "../utils/heapPressure.ts";
|
||||
import { normalizeHeaders } from "../utils/headers.ts";
|
||||
import { resolveChatCoreRequestFormat } from "./chatCore/requestFormat.ts";
|
||||
import { resolveChatCoreTargetFormat } from "./chatCore/targetFormat.ts";
|
||||
import { defaultClaudeToolType } from "./chatCore/claudeToolDefaults.ts";
|
||||
import { injectSystemPrompt, injectCustomSystemPrompt } from "../services/systemPrompt.ts";
|
||||
import { translateRequest, needsTranslation } from "../translator/index.ts";
|
||||
import { FORMATS } from "../translator/formats.ts";
|
||||
@@ -1876,6 +1877,16 @@ export async function handleChatCore({
|
||||
}
|
||||
}
|
||||
|
||||
// Claude: strict Anthropic-compatible gateways (e.g. MiniMax) reject tool
|
||||
// definitions that omit the required `type` discriminator with HTTP 400. Default
|
||||
// a missing `type` to "custom" before dispatch, mirroring Anthropic's own
|
||||
// inference, so legacy Claude-format tool payloads survive strict gateways (#2195).
|
||||
if (targetFormat === FORMATS.CLAUDE && Array.isArray(translatedBody.tools)) {
|
||||
translatedBody.tools = defaultClaudeToolType(
|
||||
translatedBody.tools
|
||||
) as typeof translatedBody.tools;
|
||||
}
|
||||
|
||||
// Extract toolNameMap for response translation (Claude OAuth)
|
||||
const translatedToolNameMap = translatedBody._toolNameMap;
|
||||
const nativeClaudeToolNameMap = isClaudePassthrough
|
||||
|
||||
24
open-sse/handlers/chatCore/claudeToolDefaults.ts
Normal file
24
open-sse/handlers/chatCore/claudeToolDefaults.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// Claude (Anthropic Messages) tool-definition normalization for outbound requests.
|
||||
|
||||
type UnknownRecord = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Claude's tool schema requires every tool to carry an explicit `type` discriminator
|
||||
* (e.g. "custom", "computer_20241022", "bash_20241022"). Anthropic's own API infers
|
||||
* "custom" when it's omitted, but strict Anthropic-compatible gateways (e.g. MiniMax)
|
||||
* enforce the documented schema and reject payloads whose tools lack `type` with
|
||||
* HTTP 400. Default a missing `type` to "custom" so legacy Claude-format tool
|
||||
* definitions survive strict gateways, while leaving any tool that already declares a
|
||||
* type (incl. built-in tool types) untouched. (port from 9router#2195)
|
||||
*
|
||||
* Non-array input is returned unchanged; defaulted entries are new objects so the
|
||||
* caller's original tool objects are not mutated.
|
||||
*/
|
||||
export function defaultClaudeToolType(tools: unknown): unknown {
|
||||
if (!Array.isArray(tools)) return tools;
|
||||
return tools.map((tool) =>
|
||||
tool && typeof tool === "object" && !Array.isArray(tool) && (tool as UnknownRecord).type
|
||||
? tool
|
||||
: { type: "custom", ...(tool as UnknownRecord) }
|
||||
);
|
||||
}
|
||||
64
tests/unit/claude-tool-type-default-2195.test.ts
Normal file
64
tests/unit/claude-tool-type-default-2195.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
// Port of 9router#2196 (fixes #2195): Claude's tool schema requires each tool to
|
||||
// carry an explicit `type` discriminator. Anthropic's first-party API infers
|
||||
// "custom" when omitted, but strict Anthropic-compatible gateways (e.g. MiniMax)
|
||||
// reject the payload with HTTP 400. defaultClaudeToolType() backfills the missing
|
||||
// `type` so legacy Claude-format tool definitions survive strict gateways.
|
||||
|
||||
const { defaultClaudeToolType } = await import(
|
||||
"../../open-sse/handlers/chatCore/claudeToolDefaults.ts"
|
||||
);
|
||||
|
||||
test("backfills type:'custom' on a Claude tool missing the type field", () => {
|
||||
const tools = [
|
||||
{ name: "get_weather", description: "Get weather", input_schema: { type: "object" } },
|
||||
];
|
||||
const out = defaultClaudeToolType(tools) as Array<Record<string, unknown>>;
|
||||
assert.equal(out[0].type, "custom");
|
||||
// Other fields are preserved untouched.
|
||||
assert.equal(out[0].name, "get_weather");
|
||||
assert.equal(out[0].description, "Get weather");
|
||||
assert.deepEqual(out[0].input_schema, { type: "object" });
|
||||
});
|
||||
|
||||
test("leaves tools that already declare a type untouched", () => {
|
||||
const tools = [
|
||||
{ type: "custom", name: "a", input_schema: {} },
|
||||
{ type: "computer_20241022", name: "computer" },
|
||||
{ type: "bash_20241022", name: "bash" },
|
||||
];
|
||||
const out = defaultClaudeToolType(tools) as Array<Record<string, unknown>>;
|
||||
assert.deepEqual(
|
||||
out.map((t) => t.type),
|
||||
["custom", "computer_20241022", "bash_20241022"]
|
||||
);
|
||||
// Non-custom built-in tool types must be preserved, not overwritten.
|
||||
assert.equal(out[1].type, "computer_20241022");
|
||||
});
|
||||
|
||||
test("normalizes a mixed array — only the type-less entries get defaulted", () => {
|
||||
const tools = [
|
||||
{ type: "computer_20241022", name: "computer" },
|
||||
{ name: "get_weather", input_schema: {} },
|
||||
];
|
||||
const out = defaultClaudeToolType(tools) as Array<Record<string, unknown>>;
|
||||
assert.equal(out[0].type, "computer_20241022");
|
||||
assert.equal(out[1].type, "custom");
|
||||
});
|
||||
|
||||
test("returns non-array input unchanged (no tools / undefined)", () => {
|
||||
assert.equal(defaultClaudeToolType(undefined), undefined);
|
||||
assert.equal(defaultClaudeToolType(null), null);
|
||||
const obj = { not: "an array" };
|
||||
assert.equal(defaultClaudeToolType(obj), obj);
|
||||
});
|
||||
|
||||
test("does not mutate the original tool objects (returns new entries for defaulted tools)", () => {
|
||||
const original = { name: "x", input_schema: {} };
|
||||
const tools = [original];
|
||||
const out = defaultClaudeToolType(tools) as Array<Record<string, unknown>>;
|
||||
assert.equal(original.type, undefined, "original tool must stay untouched");
|
||||
assert.equal(out[0].type, "custom");
|
||||
});
|
||||
Reference in New Issue
Block a user