Skip to content

fix(schema): strip null from enum arrays in tool schemas for strict validators#3727

Open
Crazy-LittleBoy wants to merge 6 commits into
tailcallhq:mainfrom
Crazy-LittleBoy:fix/kimi-null-enum-schema
Open

fix(schema): strip null from enum arrays in tool schemas for strict validators#3727
Crazy-LittleBoy wants to merge 6 commits into
tailcallhq:mainfrom
Crazy-LittleBoy:fix/kimi-null-enum-schema

Conversation

@Crazy-LittleBoy

Copy link
Copy Markdown

Problem

When using Kimi (Moonshot) models via Anthropic-compatible or OpenAI-compatible API endpoints, requests fail with:

400 Bad Request: parameters is not a valid moonshot flavored json schema,
details: <At path 'properties.output_mode.enum': enum value (null)
does not match any type in [string]>

Root Cause

schemars' AddNullable transform adds "nullable": true and removes "null" from the type keyword, but does not remove null values from enum arrays. This produces invalid JSON Schema where enum contains null while type is e.g. "string" — a contradiction that strict validators (Moonshot/Kimi) reject.

The existing cleanup in enforce_strict_schema only ran in strict mode, which is gated to specific provider IDs (FIREWORKS_AI, XAI, kimi_coding, etc.). Generic OpenAI-compatible and Anthropic-compatible endpoints never triggered the cleanup.

Fix (3 layers, architectural root cause)

1. Domain Layer — NormalizeNullable transform

  • New transform in forge_domain/src/tools/definition/tool_definition.rs
  • Replaces AddNullable in ToolCatalog::schema()
  • Delegates type normalization to AddNullable, then recursively strips null from enum arrays when nullable: true is set
  • The nullable marker alone fully conveys nullability (OpenAPI 3.0 convention); null in enum is redundant

2. App Layer — enforce_strict_schema defense-in-depth

  • Null-stripping from enum moved outside the strict_mode gate in forge_app/src/utils.rs
  • Guards external MCP tool schemas that bypass NormalizeNullable
  • In strict mode: null stripped first, then anyOf conversion (unchanged behavior)
  • In non-strict mode: null stripped, nullable marker preserved

3. Anthropic Pipeline — tool schema normalization gap fill

  • EnforceStrictObjectSchema in forge_app/src/dto/anthropic/transforms/enforce_schema.rs now normalizes tools[*].input_schema
  • Previously only processed output_format schema, completely skipping tool schemas

Testing

  • forge_domain: 611 tests pass (3 new tests for NormalizeNullable)
  • forge_app: 723 tests pass (2 new tests for non-strict null stripping + Anthropic tool normalization)
  • Snapshot updated: null removed from output_mode enum in tool definition snapshot
  • All existing strict-mode tests (Fireworks, OpenCode Zen, xAI, kimi_coding) pass unchanged

Co-Authored-By: ForgeCode noreply@forgecode.dev

@CLAassistant

CLAassistant commented Jul 21, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@github-actions github-actions Bot added the type: fix Iterations on existing features or infrastructure. label Jul 21, 2026
…alidators

Schemars' AddNullable adds "nullable": true and removes "null" from the
type keyword, but does not remove null values from enum arrays. This
produced invalid JSON Schema (e.g. type:string with null in enum) that
strict validators like Moonshot/Kimi reject with 400 Bad Request.

Fix at three layers:

1. Domain layer: new NormalizeNullable transform replaces AddNullable in
   ToolCatalog::schema(). It delegates type normalization to AddNullable,
   then recursively strips null from enum arrays when nullable is set.

2. App layer: enforce_strict_schema now strips null from enum regardless
   of strict mode (previously strict-only). This guards external MCP
   schemas that bypass NormalizeNullable.

3. Anthropic pipeline: EnforceStrictObjectSchema now normalizes
   tools[*].input_schema via enforce_strict_schema(schema, false),
   closing a gap where tool schemas were never sanitized.

Co-Authored-By: ForgeCode <noreply@forgecode.dev>
@Crazy-LittleBoy
Crazy-LittleBoy force-pushed the fix/kimi-null-enum-schema branch from aa0482b to e43291e Compare July 21, 2026 05:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: fix Iterations on existing features or infrastructure.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants