Skip to content

fix: strip null enum values unconditionally in enforce_strict_schema - #3768

Open
Wang-tianhao wants to merge 1 commit into
tailcallhq:mainfrom
Wang-tianhao:fix/strip-null-enum-values
Open

fix: strip null enum values unconditionally in enforce_strict_schema#3768
Wang-tianhao wants to merge 1 commit into
tailcallhq:mainfrom
Wang-tianhao:fix/strip-null-enum-values

Conversation

@Wang-tianhao

Copy link
Copy Markdown

Summary

Fixes #3767

When kimi-k3 is routed through OpenRouter, every request fails with a 400 Bad Request because the tool schema contains an invalid null value inside the output_mode enum array, which Moonshot AI rejects:

Invalid request: tools.function.parameters is not a valid moonshot flavored json schema,
details: <At path 'properties.output_mode.enum': enum value (<nil>) does not match any type in [string]>

Root Cause

  1. ToolCatalog::schema (crates/forge_domain/src/tools/catalog.rs:893) applies schemars' AddNullable transform, which appends null to enum arrays for Option<Enum> fields.
  2. enforce_strict_schema (crates/forge_app/src/utils.rs) only stripped those null entries inside the strict_mode branch.
  3. The EnforceStrictToolSchema transformer (crates/forge_app/src/dto/openai/transformers/pipeline.rs:102-112) is only enabled for the direct MOONSHOT / KIMI_CODING provider IDs — not for OPEN_ROUTER. So when kimi is accessed via OpenRouter, the null enum value survives and is sent to Moonshot, which rejects it.

Fix (Approach B)

Hoist the null-stripping logic out of the strict_mode branch so it runs unconditionally. The anyOf conversion (and nullable removal) remains strict-mode-only.

This makes the non-strict path safe for all OpenAI-compatible providers, not just the ones explicitly gated into strict mode. A null literal inside a typed enum array is a non-standard representation that multiple providers reject, so stripping it unconditionally is the more robust behavior.

Changes

  • crates/forge_app/src/utils.rs: moved the enum_values.retain(|v| !v.is_null()) call to run before the strict_mode/nullable check.
  • Added test test_null_enum_values_stripped_in_non_strict_mode verifying null is removed in non-strict mode while nullable: true is preserved.

Test Plan

  • cargo test -p forge_app --lib utils::tests — 64 passed (including new test)
  • cargo test -p forge_app --lib dto::openai::transformers — 106 passed
  • cargo test -p forge_repo --lib provider::openai_responses — 139 passed (including catalog tool snapshot tests)
  • cargo test -p forge_app — full suite green

Alternative Considered

Approach A (narrower) would add || when_model("kimi")(request) to the strict_schema condition in pipeline.rs. That only fixes kimi specifically and leaves the latent issue for any other OpenAI-compatible provider that rejects null enum values. Approach B is broader and prevents the class of bug entirely.

When kimi-k3 is routed through OpenRouter, the EnforceStrictToolSchema
transformer is not applied (it only matches the direct MOONSHOT/KIMI_CODING
provider IDs). As a result, the tool schema sent to Moonshot retains a null
literal inside the output_mode enum array, which Moonshot rejects with:

  400 Bad Request: enum value (<nil>) does not match any type in [string]

The null entry originates from schemars' AddNullable transform applied in
ToolCatalog::schema (catalog.rs:893) for Option<Enum> fields.

Fix: hoist the null-stripping logic out of the strict_mode branch so it runs
unconditionally. The anyOf conversion remains strict-mode-only. This makes the
non-strict path safe for all OpenAI-compatible providers, not just the ones
explicitly gated into strict mode.

Fixes tailcallhq#3767
@github-actions github-actions Bot added the type: fix Iterations on existing features or infrastructure. label Jul 27, 2026
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.

Bug: kimi-k3 via OpenRouter fails with 400 — null enum value in tool schema rejected by Moonshot

1 participant