mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-20 00:03:30 +02:00
321 lines
9.0 KiB
TypeScript
321 lines
9.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
applyXaiModelCompat,
|
|
buildProviderToolCompatFamilyHooks,
|
|
findOpenAIStrictSchemaViolations,
|
|
inspectGeminiToolSchemas,
|
|
inspectOpenAIToolSchemas,
|
|
normalizeGeminiToolSchemas,
|
|
normalizeOpenAIToolSchemas,
|
|
resolveXaiModelCompatPatch,
|
|
} from "./provider-tools.js";
|
|
|
|
describe("buildProviderToolCompatFamilyHooks", () => {
|
|
function normalizeOpenAIParameters(parameters: unknown): unknown {
|
|
const hooks = buildProviderToolCompatFamilyHooks("openai");
|
|
const tools = [{ name: "demo", description: "", parameters }] as never;
|
|
const normalized = hooks.normalizeToolSchemas({
|
|
provider: "openai",
|
|
modelId: "gpt-5.4",
|
|
modelApi: "openai-responses",
|
|
model: {
|
|
provider: "openai",
|
|
api: "openai-responses",
|
|
baseUrl: "https://api.openai.com/v1",
|
|
id: "gpt-5.4",
|
|
} as never,
|
|
tools,
|
|
});
|
|
return normalized[0]?.parameters;
|
|
}
|
|
|
|
it("covers the tool compat family matrix", () => {
|
|
const cases = [
|
|
{
|
|
family: "gemini" as const,
|
|
normalizeToolSchemas: normalizeGeminiToolSchemas,
|
|
inspectToolSchemas: inspectGeminiToolSchemas,
|
|
},
|
|
{
|
|
family: "openai" as const,
|
|
normalizeToolSchemas: normalizeOpenAIToolSchemas,
|
|
inspectToolSchemas: inspectOpenAIToolSchemas,
|
|
},
|
|
];
|
|
|
|
for (const testCase of cases) {
|
|
const hooks = buildProviderToolCompatFamilyHooks(testCase.family);
|
|
|
|
expect(hooks.normalizeToolSchemas).toBe(testCase.normalizeToolSchemas);
|
|
expect(hooks.inspectToolSchemas).toBe(testCase.inspectToolSchemas);
|
|
}
|
|
});
|
|
|
|
it("normalizes parameter-free and typed-object schemas for the openai family", () => {
|
|
const hooks = buildProviderToolCompatFamilyHooks("openai");
|
|
const tools = [
|
|
{ name: "ping", description: "", parameters: {} },
|
|
{ name: "exec", description: "", parameters: { type: "object" } },
|
|
] as never;
|
|
|
|
const normalized = hooks.normalizeToolSchemas({
|
|
provider: "openai",
|
|
modelId: "gpt-5.4",
|
|
modelApi: "openai-responses",
|
|
model: {
|
|
provider: "openai",
|
|
api: "openai-responses",
|
|
baseUrl: "https://api.openai.com/v1",
|
|
id: "gpt-5.4",
|
|
} as never,
|
|
tools,
|
|
});
|
|
|
|
expect(normalized.map((tool) => tool.parameters)).toEqual([
|
|
{ type: "object", properties: {}, required: [], additionalProperties: false },
|
|
{ type: "object", properties: {}, required: [], additionalProperties: false },
|
|
]);
|
|
expect(
|
|
hooks.inspectToolSchemas({
|
|
provider: "openai",
|
|
modelId: "gpt-5.4",
|
|
modelApi: "openai-responses",
|
|
model: {
|
|
provider: "openai",
|
|
api: "openai-responses",
|
|
baseUrl: "https://api.openai.com/v1",
|
|
id: "gpt-5.4",
|
|
} as never,
|
|
tools,
|
|
}),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it("preserves explicit empty properties maps when normalizing strict openai schemas", () => {
|
|
const hooks = buildProviderToolCompatFamilyHooks("openai");
|
|
const parameters = {
|
|
type: "object",
|
|
properties: {},
|
|
};
|
|
const tools = [{ name: "ping", description: "", parameters }] as never;
|
|
|
|
const normalized = hooks.normalizeToolSchemas({
|
|
provider: "openai",
|
|
modelId: "gpt-5.4",
|
|
modelApi: "openai-responses",
|
|
model: {
|
|
provider: "openai",
|
|
api: "openai-responses",
|
|
baseUrl: "https://api.openai.com/v1",
|
|
id: "gpt-5.4",
|
|
} as never,
|
|
tools,
|
|
});
|
|
|
|
expect(normalized[0]?.parameters).toEqual({
|
|
type: "object",
|
|
properties: {},
|
|
required: [],
|
|
additionalProperties: false,
|
|
});
|
|
});
|
|
|
|
it("preserves nested schemas and annotation objects while normalizing strict openai schemas", () => {
|
|
const cases = [
|
|
{
|
|
name: "property schema",
|
|
parameters: {
|
|
type: "object",
|
|
properties: { payload: {} },
|
|
required: ["payload"],
|
|
additionalProperties: false,
|
|
},
|
|
},
|
|
{
|
|
name: "schema maps",
|
|
parameters: {
|
|
type: "object",
|
|
properties: { mode: { $defs: { nested: {} }, dependentSchemas: { flag: {} } } },
|
|
required: ["mode"],
|
|
additionalProperties: false,
|
|
},
|
|
},
|
|
{
|
|
name: "nested schema arrays",
|
|
parameters: {
|
|
type: "object",
|
|
properties: { mode: { anyOf: [{}], prefixItems: [{}] } },
|
|
required: ["mode"],
|
|
additionalProperties: false,
|
|
},
|
|
},
|
|
{
|
|
name: "annotation objects",
|
|
parameters: {
|
|
type: "object",
|
|
properties: { mode: { type: "string", default: {}, const: {}, examples: [{}] } },
|
|
required: ["mode"],
|
|
additionalProperties: false,
|
|
},
|
|
},
|
|
];
|
|
|
|
for (const testCase of cases) {
|
|
expect(normalizeOpenAIParameters(testCase.parameters), testCase.name).toEqual(
|
|
testCase.parameters,
|
|
);
|
|
}
|
|
});
|
|
|
|
it("does not tighten or warn for permissive object schemas that use strict:false", () => {
|
|
const hooks = buildProviderToolCompatFamilyHooks("openai");
|
|
const permissiveParameters = {
|
|
type: "object",
|
|
properties: {
|
|
action: { type: "string" },
|
|
schedule: { type: "string" },
|
|
},
|
|
required: ["action"],
|
|
additionalProperties: true,
|
|
};
|
|
const permissiveTool = {
|
|
name: "cron",
|
|
description: "",
|
|
parameters: permissiveParameters,
|
|
} as never;
|
|
|
|
const normalized = hooks.normalizeToolSchemas({
|
|
provider: "openai",
|
|
modelId: "gpt-5.4",
|
|
modelApi: "openai-responses",
|
|
model: {
|
|
provider: "openai",
|
|
api: "openai-responses",
|
|
baseUrl: "https://api.openai.com/v1",
|
|
id: "gpt-5.4",
|
|
} as never,
|
|
tools: [permissiveTool],
|
|
});
|
|
|
|
expect(normalized[0]?.parameters).toEqual(permissiveParameters);
|
|
expect(findOpenAIStrictSchemaViolations(permissiveParameters, "cron.parameters")).toEqual(
|
|
expect.arrayContaining([
|
|
"cron.parameters.required.schedule",
|
|
"cron.parameters.additionalProperties",
|
|
]),
|
|
);
|
|
expect(
|
|
hooks.inspectToolSchemas({
|
|
provider: "openai",
|
|
modelId: "gpt-5.4",
|
|
modelApi: "openai-responses",
|
|
model: {
|
|
provider: "openai",
|
|
api: "openai-responses",
|
|
baseUrl: "https://api.openai.com/v1",
|
|
id: "gpt-5.4",
|
|
} as never,
|
|
tools: [permissiveTool],
|
|
}),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it("skips openai strict-tool normalization on non-native routes", () => {
|
|
const hooks = buildProviderToolCompatFamilyHooks("openai");
|
|
const tools = [{ name: "ping", description: "", parameters: {} }] as never;
|
|
|
|
expect(
|
|
hooks.normalizeToolSchemas({
|
|
provider: "openai",
|
|
modelId: "gpt-5.4",
|
|
modelApi: "openai-completions",
|
|
model: {
|
|
provider: "openai",
|
|
api: "openai-completions",
|
|
baseUrl: "https://example.com/v1",
|
|
id: "gpt-5.4",
|
|
} as never,
|
|
tools,
|
|
}),
|
|
).toBe(tools);
|
|
expect(
|
|
hooks.inspectToolSchemas({
|
|
provider: "openai",
|
|
modelId: "gpt-5.4",
|
|
modelApi: "openai-completions",
|
|
model: {
|
|
provider: "openai",
|
|
api: "openai-completions",
|
|
baseUrl: "https://example.com/v1",
|
|
id: "gpt-5.4",
|
|
} as never,
|
|
tools,
|
|
}),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it("suppresses openai strict-schema diagnostics because transport falls back to strict false", () => {
|
|
const hooks = buildProviderToolCompatFamilyHooks("openai");
|
|
|
|
const diagnostics = hooks.inspectToolSchemas({
|
|
provider: "openai-codex",
|
|
modelId: "gpt-5.4",
|
|
modelApi: "openai-codex-responses",
|
|
model: {
|
|
provider: "openai-codex",
|
|
api: "openai-codex-responses",
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
id: "gpt-5.4",
|
|
} as never,
|
|
tools: [
|
|
{
|
|
name: "exec",
|
|
description: "",
|
|
parameters: {
|
|
type: "object",
|
|
properties: {
|
|
mode: {
|
|
anyOf: [{ type: "string" }, { type: "number" }],
|
|
},
|
|
cwd: { type: "string" },
|
|
},
|
|
required: ["mode"],
|
|
additionalProperties: true,
|
|
},
|
|
} as never,
|
|
],
|
|
});
|
|
|
|
expect(diagnostics).toEqual([]);
|
|
});
|
|
|
|
it("covers the shared xAI tool compat patch", () => {
|
|
const patch = resolveXaiModelCompatPatch();
|
|
|
|
expect(patch).toMatchObject({
|
|
toolSchemaProfile: "xai",
|
|
nativeWebSearchTool: true,
|
|
toolCallArgumentsEncoding: "html-entities",
|
|
});
|
|
expect(patch.unsupportedToolSchemaKeywords).toEqual(
|
|
expect.arrayContaining(["minLength", "maxLength", "minItems", "maxItems"]),
|
|
);
|
|
|
|
expect(
|
|
applyXaiModelCompat({
|
|
id: "grok-4",
|
|
compat: {
|
|
supportsUsageInStreaming: true,
|
|
},
|
|
}),
|
|
).toMatchObject({
|
|
compat: {
|
|
supportsUsageInStreaming: true,
|
|
toolSchemaProfile: "xai",
|
|
nativeWebSearchTool: true,
|
|
toolCallArgumentsEncoding: "html-entities",
|
|
},
|
|
});
|
|
});
|
|
});
|