fix(tools): strip xAI-unsupported JSON Schema keywords from tool definitions

xAI rejects minLength, maxLength, minItems, maxItems, minContains, and
maxContains in tool schemas with a 502 error instead of ignoring them.
This causes all requests to fail when any tool definition includes these
validation-constraint keywords (e.g. sessions_spawn uses maxLength and
maxItems on its attachment fields).

Add stripXaiUnsupportedKeywords() in schema/clean-for-xai.ts, mirroring
the existing cleanSchemaForGemini() pattern. Apply it in normalizeToolParameters()
when the provider is xai directly, or openrouter with an x-ai/* model id.

Fixes tool calls for x-ai/grok-* models both direct and via OpenRouter.
This commit is contained in:
Jason Separovic
2026-03-02 09:59:49 -08:00
committed by Peter Steinberger
parent da05395c2a
commit 00347bda75
4 changed files with 221 additions and 12 deletions

View File

@@ -1,5 +1,6 @@
import type { AnyAgentTool } from "./pi-tools.types.js";
import { cleanSchemaForGemini } from "./schema/clean-for-gemini.js";
import { isXaiProvider, stripXaiUnsupportedKeywords } from "./schema/clean-for-xai.js";
function extractEnumValues(schema: unknown): unknown[] | undefined {
if (!schema || typeof schema !== "object") {
@@ -64,7 +65,7 @@ function mergePropertySchemas(existing: unknown, incoming: unknown): unknown {
export function normalizeToolParameters(
tool: AnyAgentTool,
options?: { modelProvider?: string },
options?: { modelProvider?: string; modelId?: string },
): AnyAgentTool {
const schema =
tool.parameters && typeof tool.parameters === "object"
@@ -79,6 +80,7 @@ export function normalizeToolParameters(
// - OpenAI rejects function tool schemas unless the *top-level* is `type: "object"`.
// (TypeBox root unions compile to `{ anyOf: [...] }` without `type`).
// - Anthropic expects full JSON Schema draft 2020-12 compliance.
// - xAI rejects validation-constraint keywords (minLength, maxLength, etc.) outright.
//
// Normalize once here so callers can always pass `tools` through unchanged.
@@ -86,13 +88,24 @@ export function normalizeToolParameters(
options?.modelProvider?.toLowerCase().includes("google") ||
options?.modelProvider?.toLowerCase().includes("gemini");
const isAnthropicProvider = options?.modelProvider?.toLowerCase().includes("anthropic");
const isXai = isXaiProvider(options?.modelProvider, options?.modelId);
function applyProviderCleaning(s: unknown): unknown {
if (isGeminiProvider && !isAnthropicProvider) {
return cleanSchemaForGemini(s);
}
if (isXai) {
return stripXaiUnsupportedKeywords(s);
}
return s;
}
// If schema already has type + properties (no top-level anyOf to merge),
// clean it for Gemini compatibility (but only if using Gemini, not Anthropic)
// clean it for Gemini/xAI compatibility as appropriate.
if ("type" in schema && "properties" in schema && !Array.isArray(schema.anyOf)) {
return {
...tool,
parameters: isGeminiProvider && !isAnthropicProvider ? cleanSchemaForGemini(schema) : schema,
parameters: applyProviderCleaning(schema),
};
}
@@ -107,10 +120,7 @@ export function normalizeToolParameters(
const schemaWithType = { ...schema, type: "object" };
return {
...tool,
parameters:
isGeminiProvider && !isAnthropicProvider
? cleanSchemaForGemini(schemaWithType)
: schemaWithType,
parameters: applyProviderCleaning(schemaWithType),
};
}
@@ -184,10 +194,7 @@ export function normalizeToolParameters(
// - OpenAI rejects schemas without top-level `type: "object"`.
// - Anthropic accepts proper JSON Schema with constraints.
// Merging properties preserves useful enums like `action` while keeping schemas portable.
parameters:
isGeminiProvider && !isAnthropicProvider
? cleanSchemaForGemini(flattenedSchema)
: flattenedSchema,
parameters: applyProviderCleaning(flattenedSchema),
};
}