import { cleanSchemaForGemini, GEMINI_UNSUPPORTED_SCHEMA_KEYWORDS, } from "../agents/schema/clean-for-gemini.js"; import type { ModelCompatConfig } from "../config/types.models.js"; import { applyModelCompatPatch } from "../plugins/provider-model-compat.js"; import type { AnyAgentTool, ProviderNormalizeToolSchemasContext, ProviderToolSchemaDiagnostic, } from "./plugin-entry.js"; // Shared provider-tool helpers for plugin-owned schema compatibility rewrites. export { cleanSchemaForGemini, GEMINI_UNSUPPORTED_SCHEMA_KEYWORDS }; export const XAI_TOOL_SCHEMA_PROFILE = "xai"; export const HTML_ENTITY_TOOL_CALL_ARGUMENTS_ENCODING = "html-entities"; export const XAI_UNSUPPORTED_SCHEMA_KEYWORDS = new Set([ "minLength", "maxLength", "minItems", "maxItems", "minContains", "maxContains", ]); export function stripUnsupportedSchemaKeywords( schema: unknown, unsupportedKeywords: ReadonlySet, ): unknown { if (!schema || typeof schema !== "object") { return schema; } if (Array.isArray(schema)) { return schema.map((entry) => stripUnsupportedSchemaKeywords(entry, unsupportedKeywords)); } const obj = schema as Record; const cleaned: Record = {}; for (const [key, value] of Object.entries(obj)) { if (unsupportedKeywords.has(key)) { continue; } if (key === "properties" && value && typeof value === "object" && !Array.isArray(value)) { cleaned[key] = Object.fromEntries( Object.entries(value as Record).map(([childKey, childValue]) => [ childKey, stripUnsupportedSchemaKeywords(childValue, unsupportedKeywords), ]), ); continue; } if (key === "items" && value && typeof value === "object") { cleaned[key] = Array.isArray(value) ? value.map((entry) => stripUnsupportedSchemaKeywords(entry, unsupportedKeywords)) : stripUnsupportedSchemaKeywords(value, unsupportedKeywords); continue; } if ((key === "anyOf" || key === "oneOf" || key === "allOf") && Array.isArray(value)) { cleaned[key] = value.map((entry) => stripUnsupportedSchemaKeywords(entry, unsupportedKeywords), ); continue; } cleaned[key] = value; } return cleaned; } export function stripXaiUnsupportedKeywords(schema: unknown): unknown { return stripUnsupportedSchemaKeywords(schema, XAI_UNSUPPORTED_SCHEMA_KEYWORDS); } export function resolveXaiModelCompatPatch(): ModelCompatConfig { return { toolSchemaProfile: XAI_TOOL_SCHEMA_PROFILE, unsupportedToolSchemaKeywords: Array.from(XAI_UNSUPPORTED_SCHEMA_KEYWORDS), nativeWebSearchTool: true, toolCallArgumentsEncoding: HTML_ENTITY_TOOL_CALL_ARGUMENTS_ENCODING, }; } export function applyXaiModelCompat(model: T): T { return applyModelCompatPatch( model as T & { compat?: ModelCompatConfig }, resolveXaiModelCompatPatch(), ) as T; } export function findUnsupportedSchemaKeywords( schema: unknown, path: string, unsupportedKeywords: ReadonlySet, ): string[] { if (!schema || typeof schema !== "object") { return []; } if (Array.isArray(schema)) { return schema.flatMap((item, index) => findUnsupportedSchemaKeywords(item, `${path}[${index}]`, unsupportedKeywords), ); } const record = schema as Record; const violations: string[] = []; const properties = record.properties && typeof record.properties === "object" && !Array.isArray(record.properties) ? (record.properties as Record) : undefined; if (properties) { for (const [key, value] of Object.entries(properties)) { violations.push( ...findUnsupportedSchemaKeywords(value, `${path}.properties.${key}`, unsupportedKeywords), ); } } for (const [key, value] of Object.entries(record)) { if (key === "properties") { continue; } if (unsupportedKeywords.has(key)) { violations.push(`${path}.${key}`); } if (value && typeof value === "object") { violations.push( ...findUnsupportedSchemaKeywords(value, `${path}.${key}`, unsupportedKeywords), ); } } return violations; } export function normalizeGeminiToolSchemas( ctx: ProviderNormalizeToolSchemasContext, ): AnyAgentTool[] { return ctx.tools.map((tool) => { if (!tool.parameters || typeof tool.parameters !== "object") { return tool; } return { ...tool, parameters: cleanSchemaForGemini(tool.parameters as Record), }; }); } export function inspectGeminiToolSchemas( ctx: ProviderNormalizeToolSchemasContext, ): ProviderToolSchemaDiagnostic[] { return ctx.tools.flatMap((tool, toolIndex) => { const violations = findUnsupportedSchemaKeywords( tool.parameters, `${tool.name}.parameters`, GEMINI_UNSUPPORTED_SCHEMA_KEYWORDS, ); if (violations.length === 0) { return []; } return [{ toolName: tool.name, toolIndex, violations }]; }); } export type ProviderToolCompatFamily = "gemini"; export function buildProviderToolCompatFamilyHooks(family: ProviderToolCompatFamily): { normalizeToolSchemas: (ctx: ProviderNormalizeToolSchemasContext) => AnyAgentTool[]; inspectToolSchemas: (ctx: ProviderNormalizeToolSchemasContext) => ProviderToolSchemaDiagnostic[]; } { switch (family) { case "gemini": return { normalizeToolSchemas: normalizeGeminiToolSchemas, inspectToolSchemas: inspectGeminiToolSchemas, }; } }