mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-19 13:23:50 +03:00
fix(translator): flatten root-level anyOf/oneOf/allOf in Claude tool schemas (#13561)
Anthropic's Messages API rejects a tool whose `input_schema` carries a composition keyword at the root with: tools.N.custom.input_schema: input_schema does not support oneOf, allOf, or anyOf at the top level The refusal happens before inference, so a single MCP/agent tool carrying a root-level union fails every request that ships the catalog, and combo failover cannot recover from it. Both conversion paths that build a Claude `input_schema` from a client payload now flatten such a root union into a plain object schema: object-compatible branches contribute their `properties` (root wins on a name collision), the root is pinned to `type: "object"`, and only `allOf` contributes `required` — `anyOf`/`oneOf` branch requirements are alternatives and promoting them would refuse calls the original schema accepts. Nested unions are untouched and clean schemas pass through unchanged. Closes #13552
This commit is contained in:
@@ -0,0 +1 @@
|
||||
- **fix(translator):** Claude tool `input_schema` with a root-level `anyOf` / `oneOf` / `allOf` is flattened into a plain object schema instead of being forwarded verbatim. Anthropic refuses such a tool before inference (`tools.N.custom.input_schema: input_schema does not support oneOf, allOf, or anyOf at the top level`), so a single MCP/agent tool carrying one made every request fail with no combo failover possible ([#13552](https://github.com/diegosouzapw/OmniRoute/issues/13552))
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "../config/claudeCodeCompatibleIdentity.ts";
|
||||
import { supportsClaudeMaxEffort, supportsXHighEffort } from "../config/providerModels.ts";
|
||||
import { prepareClaudeRequest } from "../translator/helpers/claudeHelper.ts";
|
||||
import { normalizeClaudeToolInputSchema } from "../translator/helpers/schemaCoercion.ts";
|
||||
import { signRequestBody } from "./claudeCodeCCH.ts";
|
||||
import { resolveClaudeCodeCompatibleAnthropicBeta } from "./claudeCodeCompatibleBeta.ts";
|
||||
import { remapToolNamesInRequest } from "./claudeCodeToolRemapper.ts";
|
||||
@@ -757,10 +758,13 @@ function convertClaudeCodeCompatibleTool(tool: unknown) {
|
||||
|
||||
const rawSchema = readRecord(toolData.parameters) ||
|
||||
readRecord(toolData.input_schema) || { type: "object", properties: {}, required: [] };
|
||||
const inputSchema =
|
||||
const withProperties =
|
||||
rawSchema.type === "object" && !readRecord(rawSchema.properties)
|
||||
? { ...rawSchema, properties: {} }
|
||||
: rawSchema;
|
||||
// Flatten a root-level anyOf/oneOf/allOf: Anthropic refuses it outright with
|
||||
// "input_schema does not support oneOf, allOf, or anyOf at the top level" (#13552).
|
||||
const inputSchema = normalizeClaudeToolInputSchema(withProperties);
|
||||
|
||||
const converted: Record<string, unknown> = {
|
||||
name,
|
||||
|
||||
@@ -629,13 +629,111 @@ export function stripInvalidSchemaConstructs(schema: unknown): unknown {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON Schema composition keywords Anthropic refuses at the *root* of a tool
|
||||
* `input_schema`. Nested occurrences (inside `properties`, `items`, `$defs`, …)
|
||||
* are valid and must be preserved.
|
||||
*/
|
||||
const CLAUDE_ROOT_UNION_KEYWORDS = ["anyOf", "oneOf", "allOf"] as const;
|
||||
|
||||
/** Whether a union branch may contribute object properties to the flattened root. */
|
||||
function claudeUnionBranchCanBeObject(branch: JsonRecord): boolean {
|
||||
const type = branch.type;
|
||||
if (type === undefined) return true;
|
||||
if (typeof type === "string") return type === "object";
|
||||
if (Array.isArray(type)) return type.includes("object");
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Append the string entries of `branchRequired` that are not recorded yet. */
|
||||
function mergeClaudeRequired(target: string[], seen: Set<string>, branchRequired: unknown): void {
|
||||
if (!Array.isArray(branchRequired)) return;
|
||||
for (const name of branchRequired) {
|
||||
if (typeof name !== "string" || seen.has(name)) continue;
|
||||
seen.add(name);
|
||||
target.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a tool schema carries a root-level `anyOf` / `oneOf` / `allOf`.
|
||||
*
|
||||
* @param schema - Candidate tool `input_schema` / `parameters` value.
|
||||
* @returns `true` when Anthropic would reject the schema's root shape.
|
||||
*/
|
||||
export function hasRootLevelSchemaUnion(schema: unknown): boolean {
|
||||
if (!isPlainObject(schema)) return false;
|
||||
return CLAUDE_ROOT_UNION_KEYWORDS.some((keyword) => hasOwn(schema, keyword));
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten a root-level `anyOf` / `oneOf` / `allOf` into a plain object schema.
|
||||
*
|
||||
* Anthropic's Messages API rejects a tool whose `input_schema` root carries a
|
||||
* composition keyword with
|
||||
* `tools.N.custom.input_schema: input_schema does not support oneOf, allOf, or
|
||||
* anyOf at the top level` (#13552). The request is refused *before* inference,
|
||||
* so a single such tool from an MCP/agent client fails every request that
|
||||
* carries the catalog — combo failover cannot recover from it either.
|
||||
*
|
||||
* The flattening mirrors the union handling CLIProxyAPI applies on the same
|
||||
* wire hop: object-compatible branches contribute their `properties` (first
|
||||
* branch wins on a name collision), the root is pinned to `type: "object"`, and
|
||||
* only `allOf` — whose branches all apply at once — contributes `required`.
|
||||
* `anyOf` / `oneOf` branch requirements are alternatives, so promoting them
|
||||
* would refuse calls the original schema accepts.
|
||||
*
|
||||
* Schemas without a root union are returned untouched, and nested unions are
|
||||
* never rewritten.
|
||||
*
|
||||
* @param schema - Tool `input_schema` as received from the client.
|
||||
* @returns An Anthropic-compatible schema, or the input when nothing to do.
|
||||
*/
|
||||
export function normalizeClaudeToolInputSchema(schema: unknown): unknown {
|
||||
if (!hasRootLevelSchemaUnion(schema)) return schema;
|
||||
|
||||
const source = schema as JsonRecord;
|
||||
const result: JsonRecord = { ...source };
|
||||
const properties: JsonRecord = isPlainObject(source.properties) ? { ...source.properties } : {};
|
||||
const required: string[] = [];
|
||||
const requiredSeen = new Set<string>();
|
||||
mergeClaudeRequired(required, requiredSeen, source.required);
|
||||
|
||||
for (const keyword of CLAUDE_ROOT_UNION_KEYWORDS) {
|
||||
if (!hasOwn(result, keyword)) continue;
|
||||
const branches = result[keyword];
|
||||
// Dropped even when malformed: the keyword itself is what Anthropic refuses.
|
||||
delete result[keyword];
|
||||
if (!Array.isArray(branches)) continue;
|
||||
|
||||
for (const branch of branches) {
|
||||
if (!isPlainObject(branch) || !claudeUnionBranchCanBeObject(branch)) continue;
|
||||
if (isPlainObject(branch.properties)) {
|
||||
for (const [name, propertySchema] of Object.entries(branch.properties)) {
|
||||
if (!hasOwn(properties, name)) properties[name] = propertySchema;
|
||||
}
|
||||
}
|
||||
if (keyword === "allOf") mergeClaudeRequired(required, requiredSeen, branch.required);
|
||||
}
|
||||
}
|
||||
|
||||
result.type = "object";
|
||||
result.properties = properties;
|
||||
if (required.length > 0) result.required = required;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function sanitizeClaudeToolSchema(schema: unknown): unknown {
|
||||
// stripInvalidSchemaConstructs now also coerces numeric-string constraints, so
|
||||
// it is the single pass for the Claude path. We deliberately do NOT compose
|
||||
// coerceSchemaNumericFields: it strips the valid `default` keyword (Fix #1782,
|
||||
// a translator concern) which on the native / passthrough surface would
|
||||
// silently alter tool schemas that were previously forwarded verbatim.
|
||||
return stripInvalidSchemaConstructs(schema);
|
||||
//
|
||||
// The root-union flattening runs last so it sees the already-repaired shape
|
||||
// (e.g. an index-keyed `anyOf` object coerced back into an array).
|
||||
return normalizeClaudeToolInputSchema(stripInvalidSchemaConstructs(schema));
|
||||
}
|
||||
|
||||
export function sanitizeClaudeToolSchemas(tools: unknown): unknown {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { FORMATS } from "../formats.ts";
|
||||
// CLAUDE_SYSTEM_PROMPT import removed — no longer injected unconditionally (#1966/#2130)
|
||||
import { supportsClaudeMaxEffort, supportsXHighEffort } from "../../config/providerModels.ts";
|
||||
import { adjustMaxTokens } from "../helpers/maxTokensHelper.ts";
|
||||
import { sanitizeToolId } from "../helpers/schemaCoercion.ts";
|
||||
import { normalizeClaudeToolInputSchema, sanitizeToolId } from "../helpers/schemaCoercion.ts";
|
||||
import { safeParseJSON } from "../helpers/jsonUtil.ts";
|
||||
import {
|
||||
applyKimiCodingThinking,
|
||||
@@ -437,10 +437,13 @@ export function openaiToClaudeRequest(model, body, stream, credentials = null) {
|
||||
// MCP tools (e.g. pencil, computer_use) may omit properties on object-type schemas.
|
||||
const rawSchema: Record<string, unknown> = toolData.parameters ||
|
||||
toolData.input_schema || { type: "object", properties: {}, required: [] };
|
||||
const normalizedSchema =
|
||||
const withProperties =
|
||||
rawSchema.type === "object" && !rawSchema.properties
|
||||
? { ...rawSchema, properties: {} }
|
||||
: rawSchema;
|
||||
// Flatten a root-level anyOf/oneOf/allOf: Anthropic refuses it outright with
|
||||
// "input_schema does not support oneOf, allOf, or anyOf at the top level" (#13552).
|
||||
const normalizedSchema = normalizeClaudeToolInputSchema(withProperties);
|
||||
|
||||
return {
|
||||
name: toolName,
|
||||
|
||||
320
tests/unit/claude-tool-schema-root-unions.test.ts
Normal file
320
tests/unit/claude-tool-schema-root-unions.test.ts
Normal file
@@ -0,0 +1,320 @@
|
||||
/**
|
||||
* Root-level `anyOf` / `oneOf` / `allOf` in a Claude tool `input_schema` (#13552).
|
||||
*
|
||||
* Anthropic's Messages API refuses such a tool before inference with
|
||||
* `tools.N.custom.input_schema: input_schema does not support oneOf, allOf, or
|
||||
* anyOf at the top level`, so one offending tool in a client's catalog fails
|
||||
* every request that carries it — combo failover cannot recover from a request
|
||||
* that never reaches a model.
|
||||
*
|
||||
* These tests pin the flattening itself plus both conversion paths that build a
|
||||
* Claude `input_schema` from a client payload: the OpenAI→Claude translator and
|
||||
* the Claude-Code-compatible bridge.
|
||||
*/
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
hasRootLevelSchemaUnion,
|
||||
normalizeClaudeToolInputSchema,
|
||||
sanitizeClaudeToolSchema,
|
||||
} from "../../open-sse/translator/helpers/schemaCoercion.ts";
|
||||
import { openaiToClaudeRequest } from "../../open-sse/translator/request/openai-to-claude.ts";
|
||||
import { buildClaudeCodeCompatibleRequest } from "../../open-sse/services/claudeCodeCompatible.ts";
|
||||
|
||||
type AnyRecord = Record<string, unknown>;
|
||||
|
||||
const ROOT_UNION_KEYWORDS = ["anyOf", "oneOf", "allOf"] as const;
|
||||
|
||||
const assertNoRootUnion = (schema: unknown, label: string): void => {
|
||||
for (const keyword of ROOT_UNION_KEYWORDS) {
|
||||
assert.equal(
|
||||
Object.prototype.hasOwnProperty.call(schema as AnyRecord, keyword),
|
||||
false,
|
||||
`${label} still carries a root-level ${keyword}`
|
||||
);
|
||||
}
|
||||
assert.equal((schema as AnyRecord).type, "object", `${label} root type is not "object"`);
|
||||
};
|
||||
|
||||
describe("hasRootLevelSchemaUnion", () => {
|
||||
it("detects each composition keyword at the root", () => {
|
||||
for (const keyword of ROOT_UNION_KEYWORDS) {
|
||||
assert.equal(hasRootLevelSchemaUnion({ type: "object", [keyword]: [] }), true, keyword);
|
||||
}
|
||||
});
|
||||
|
||||
it("ignores a union nested inside a property", () => {
|
||||
const schema = {
|
||||
type: "object",
|
||||
properties: { pin: { anyOf: [{ type: "boolean" }, { type: "object" }] } },
|
||||
};
|
||||
assert.equal(hasRootLevelSchemaUnion(schema), false);
|
||||
});
|
||||
|
||||
it("ignores non-object schemas", () => {
|
||||
assert.equal(hasRootLevelSchemaUnion(null), false);
|
||||
assert.equal(hasRootLevelSchemaUnion("[MaxDepth]"), false);
|
||||
assert.equal(hasRootLevelSchemaUnion([{ anyOf: [] }]), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeClaudeToolInputSchema", () => {
|
||||
it("flattens a root anyOf that carries the whole schema", () => {
|
||||
const result = normalizeClaudeToolInputSchema({
|
||||
anyOf: [
|
||||
{ type: "object", properties: { a: { type: "string" } } },
|
||||
{ type: "object", properties: { b: { type: "integer" } } },
|
||||
],
|
||||
}) as AnyRecord;
|
||||
|
||||
assertNoRootUnion(result, "flattened anyOf");
|
||||
assert.deepEqual(result.properties, {
|
||||
a: { type: "string" },
|
||||
b: { type: "integer" },
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps a nested union while removing the root one", () => {
|
||||
const result = normalizeClaudeToolInputSchema({
|
||||
type: "object",
|
||||
properties: { nested: { oneOf: [{ type: "string" }, { type: "number" }] } },
|
||||
oneOf: [
|
||||
{ properties: { a: { type: "string" } }, required: ["a"] },
|
||||
{ properties: { b: { type: "string" } }, required: ["b"] },
|
||||
],
|
||||
}) as AnyRecord;
|
||||
|
||||
assertNoRootUnion(result, "flattened oneOf");
|
||||
const properties = result.properties as AnyRecord;
|
||||
assert.deepEqual(properties.nested, { oneOf: [{ type: "string" }, { type: "number" }] });
|
||||
assert.deepEqual(properties.a, { type: "string" });
|
||||
assert.deepEqual(properties.b, { type: "string" });
|
||||
});
|
||||
|
||||
it("does not promote alternative branch requirements of anyOf/oneOf", () => {
|
||||
// `a` and `b` are alternatives: requiring both would refuse calls the
|
||||
// original schema accepts.
|
||||
const result = normalizeClaudeToolInputSchema({
|
||||
type: "object",
|
||||
properties: { a: { type: "string" }, b: { type: "string" } },
|
||||
anyOf: [{ required: ["a"] }, { required: ["b"] }],
|
||||
}) as AnyRecord;
|
||||
|
||||
assertNoRootUnion(result, "anyOf requirements");
|
||||
assert.equal("required" in result, false);
|
||||
});
|
||||
|
||||
it("merges properties and requirements of a root allOf", () => {
|
||||
// Every allOf branch applies at once, so its requirements are cumulative.
|
||||
const result = normalizeClaudeToolInputSchema({
|
||||
type: "object",
|
||||
properties: { base: { type: "boolean" } },
|
||||
required: ["base"],
|
||||
allOf: [
|
||||
{ type: "object", properties: { a: { type: "string" } }, required: ["a"] },
|
||||
{ properties: { b: { type: "integer" } }, required: ["a", "b"] },
|
||||
],
|
||||
}) as AnyRecord;
|
||||
|
||||
assertNoRootUnion(result, "flattened allOf");
|
||||
assert.deepEqual(Object.keys(result.properties as AnyRecord).sort(), ["a", "b", "base"]);
|
||||
assert.deepEqual(result.required, ["base", "a", "b"]);
|
||||
});
|
||||
|
||||
it("keeps the root object's own property when a branch redeclares it", () => {
|
||||
const result = normalizeClaudeToolInputSchema({
|
||||
type: "object",
|
||||
properties: { shared: { type: "string", description: "root wins" } },
|
||||
allOf: [{ properties: { shared: { type: "integer" } } }],
|
||||
}) as AnyRecord;
|
||||
|
||||
assert.deepEqual((result.properties as AnyRecord).shared, {
|
||||
type: "string",
|
||||
description: "root wins",
|
||||
});
|
||||
});
|
||||
|
||||
it("skips branches that cannot contribute object properties", () => {
|
||||
const result = normalizeClaudeToolInputSchema({
|
||||
type: "object",
|
||||
properties: { keep: { type: "string" } },
|
||||
anyOf: [
|
||||
{ type: "string" },
|
||||
{ type: "null" },
|
||||
{ type: ["object", "null"], properties: { fromUnionType: { type: "boolean" } } },
|
||||
],
|
||||
}) as AnyRecord;
|
||||
|
||||
assertNoRootUnion(result, "mixed branches");
|
||||
assert.deepEqual(Object.keys(result.properties as AnyRecord).sort(), ["fromUnionType", "keep"]);
|
||||
});
|
||||
|
||||
it("drops a malformed root union instead of forwarding it", () => {
|
||||
const result = normalizeClaudeToolInputSchema({
|
||||
type: "object",
|
||||
properties: { a: { type: "string" } },
|
||||
allOf: "not-an-array",
|
||||
}) as AnyRecord;
|
||||
|
||||
assertNoRootUnion(result, "malformed allOf");
|
||||
assert.deepEqual(result.properties, { a: { type: "string" } });
|
||||
});
|
||||
|
||||
it("preserves unrelated keywords and leaves a clean schema untouched", () => {
|
||||
const clean = {
|
||||
type: "object",
|
||||
properties: { query: { type: "string" } },
|
||||
required: ["query"],
|
||||
additionalProperties: false,
|
||||
};
|
||||
assert.equal(normalizeClaudeToolInputSchema(clean), clean);
|
||||
|
||||
const result = normalizeClaudeToolInputSchema({
|
||||
title: "Search",
|
||||
additionalProperties: false,
|
||||
$defs: { Ref: { type: "string" } },
|
||||
oneOf: [{ type: "object", properties: { q: { type: "string" } } }],
|
||||
}) as AnyRecord;
|
||||
|
||||
assertNoRootUnion(result, "keyword preservation");
|
||||
assert.equal(result.title, "Search");
|
||||
assert.equal(result.additionalProperties, false);
|
||||
assert.deepEqual(result.$defs, { Ref: { type: "string" } });
|
||||
});
|
||||
|
||||
it("does not mutate the schema it was given", () => {
|
||||
const input = {
|
||||
type: "object",
|
||||
properties: { a: { type: "string" } },
|
||||
anyOf: [{ type: "object", properties: { b: { type: "string" } } }],
|
||||
};
|
||||
const snapshot = structuredClone(input);
|
||||
|
||||
normalizeClaudeToolInputSchema(input);
|
||||
|
||||
assert.deepEqual(input, snapshot);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sanitizeClaudeToolSchema (native OAuth / passthrough surface)", () => {
|
||||
it("flattens a root union after repairing invalid constructs", () => {
|
||||
const result = sanitizeClaudeToolSchema({
|
||||
type: "object",
|
||||
properties: { a: { type: "string", enum: { "0": "x", "1": "y" } } },
|
||||
anyOf: [{ type: "object", properties: { b: { type: "string" } } }],
|
||||
}) as AnyRecord;
|
||||
|
||||
assertNoRootUnion(result, "sanitized schema");
|
||||
const properties = result.properties as AnyRecord;
|
||||
assert.deepEqual((properties.a as AnyRecord).enum, ["x", "y"]);
|
||||
assert.deepEqual(properties.b, { type: "string" });
|
||||
});
|
||||
|
||||
it("flattens an index-keyed root union object", () => {
|
||||
// stripInvalidSchemaConstructs coerces the index-keyed object back into an
|
||||
// array first, so the flattening still sees real branches.
|
||||
const result = sanitizeClaudeToolSchema({
|
||||
type: "object",
|
||||
oneOf: { "0": { type: "object", properties: { a: { type: "string" } } } },
|
||||
}) as AnyRecord;
|
||||
|
||||
assertNoRootUnion(result, "index-keyed oneOf");
|
||||
assert.deepEqual(result.properties, { a: { type: "string" } });
|
||||
});
|
||||
});
|
||||
|
||||
describe("openaiToClaudeRequest — tool input_schema", () => {
|
||||
const toolWithRootUnion = {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "kolonie_operator_agent",
|
||||
description: "Repro of the reported failure",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: { act: { type: "string" } },
|
||||
required: ["act"],
|
||||
anyOf: [{ type: "object", properties: { delegationId: { type: "string" } } }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const baseBody = {
|
||||
messages: [{ role: "user", content: "Say OK" }],
|
||||
max_tokens: 32,
|
||||
};
|
||||
|
||||
it("flattens a root union before the request reaches Anthropic", () => {
|
||||
const result = openaiToClaudeRequest(
|
||||
"claude-opus-5",
|
||||
{ ...baseBody, tools: [toolWithRootUnion] },
|
||||
false
|
||||
) as AnyRecord;
|
||||
|
||||
const tool = (result.tools as AnyRecord[])[0];
|
||||
const schema = tool.input_schema as AnyRecord;
|
||||
assertNoRootUnion(schema, "translated tool");
|
||||
assert.deepEqual(Object.keys(schema.properties as AnyRecord).sort(), ["act", "delegationId"]);
|
||||
assert.deepEqual(schema.required, ["act"]);
|
||||
});
|
||||
|
||||
it("leaves a nested union and an ordinary schema untouched", () => {
|
||||
const result = openaiToClaudeRequest(
|
||||
"claude-opus-5",
|
||||
{
|
||||
...baseBody,
|
||||
tools: [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "ordinary",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: { pin: { anyOf: [{ type: "boolean" }, { type: "object" }] } },
|
||||
required: ["pin"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
false
|
||||
) as AnyRecord;
|
||||
|
||||
const schema = (result.tools as AnyRecord[])[0].input_schema as AnyRecord;
|
||||
assert.deepEqual(schema.properties, {
|
||||
pin: { anyOf: [{ type: "boolean" }, { type: "object" }] },
|
||||
});
|
||||
assert.deepEqual(schema.required, ["pin"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildClaudeCodeCompatibleRequest — tool input_schema", () => {
|
||||
it("flattens a root union on the Claude-Code-compatible bridge", () => {
|
||||
const body = buildClaudeCodeCompatibleRequest({
|
||||
model: "claude-opus-5",
|
||||
normalizedBody: {
|
||||
model: "claude-opus-5",
|
||||
messages: [{ role: "user", content: "Say OK" }],
|
||||
max_tokens: 32,
|
||||
tools: [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "bridge_tool",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: { base: { type: "string" } },
|
||||
required: ["base"],
|
||||
allOf: [{ properties: { extra: { type: "string" } }, required: ["extra"] }],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}) as AnyRecord;
|
||||
|
||||
const schema = (body.tools as AnyRecord[])[0].input_schema as AnyRecord;
|
||||
assertNoRootUnion(schema, "bridge tool");
|
||||
assert.deepEqual(Object.keys(schema.properties as AnyRecord).sort(), ["base", "extra"]);
|
||||
assert.deepEqual(schema.required, ["base", "extra"]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user