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, }), ).toStrictEqual([]); }); 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], }), ).toStrictEqual([]); }); 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, }), ).toStrictEqual([]); }); 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).toStrictEqual([]); }); 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", }, }); }); });