openclaw - 💡(How to fix) Fix fix: normalizeDeepSeekSchema drops all but first anyOf variant for const unions [5 pull requests]

Official PRs (…)
ON THIS PAGE

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Root Cause

In normalizeDeepSeekSchema() (line ~498 of the bundled provider-tools file):

const selected = nonNullVariants[0] ?? normalizedVariants[0];
const merged = { ...selected, ...normalized };

When a property has anyOf: [{const:"A"}, {const:"B"}, ..., {const:"G"}] (7 const literals from Typebox Union[Literal]), the function:

  1. Skips copying the anyOf key (line 485-489)
  2. Takes only nonNullVariants[0] — the first const value
  3. Merges it with remaining properties

Result: The LLM sees {const: "A", type: "string", description: "..."} instead of {anyOf: [{const:"A"}, ..., {const:"G"}]}.

The function was designed to handle nullable unions (e.g. {anyOf: [{type:"string"}, {type:"null"}]}{type:"string", nullable:true}), but incorrectly applies the same "pick first" logic to unions of string literal consts.

Fix Action

Fixed

Code Example

const selected = nonNullVariants[0] ?? normalizedVariants[0];
const merged = { ...selected, ...normalized };

---

mode: Type.Union([
  Type.Literal("overwrite"),
  Type.Literal("append"),
  Type.Literal("replace_range"),
  Type.Literal("replace_all"),
  Type.Literal("insert_before"),
  Type.Literal("insert_after"),
  Type.Literal("delete_range"),
], { description: '更新模式(必填)' })

---

{"anyOf": [{"const":"overwrite","type":"string"}, {"const":"append","type":"string"}, ...]}

---

{"const": "overwrite", "type": "string", "description": "更新模式(必填)"}

---

// In normalizeDeepSeekSchema, after computing nonNullVariants:
const allConstStrings = nonNullVariants.every(
  v => v && typeof v === "object" && "const" in v && typeof v.const === "string"
);
if (allConstStrings) {
  const enumValues = nonNullVariants.map(v => v.const);
  return {
    type: "string",
    enum: enumValues,
    ...normalized  // preserve description, etc.
  };
}
// ... existing nullable union handling
RAW_BUFFERClick to expand / collapse

Bug Description

normalizeDeepSeekSchema in src/plugin-sdk/provider-tools.ts collapses anyOf unions of const literals to only the first variant. This makes multi-option tool parameters (like mode with 7 enum values) appear as a single const to the LLM, causing validation errors when the model tries to use any option other than the first.

Root Cause

In normalizeDeepSeekSchema() (line ~498 of the bundled provider-tools file):

const selected = nonNullVariants[0] ?? normalizedVariants[0];
const merged = { ...selected, ...normalized };

When a property has anyOf: [{const:"A"}, {const:"B"}, ..., {const:"G"}] (7 const literals from Typebox Union[Literal]), the function:

  1. Skips copying the anyOf key (line 485-489)
  2. Takes only nonNullVariants[0] — the first const value
  3. Merges it with remaining properties

Result: The LLM sees {const: "A", type: "string", description: "..."} instead of {anyOf: [{const:"A"}, ..., {const:"G"}]}.

The function was designed to handle nullable unions (e.g. {anyOf: [{type:"string"}, {type:"null"}]}{type:"string", nullable:true}), but incorrectly applies the same "pick first" logic to unions of string literal consts.

Reproduction

Any plugin tool with a Type.Union([Type.Literal("a"), Type.Literal("b"), ...]) parameter, used with a DeepSeek model.

Concrete example: The openclaw-lark plugin's feishu_update_doc tool defines:

mode: Type.Union([
  Type.Literal("overwrite"),
  Type.Literal("append"),
  Type.Literal("replace_range"),
  Type.Literal("replace_all"),
  Type.Literal("insert_before"),
  Type.Literal("insert_after"),
  Type.Literal("delete_range"),
], { description: '更新模式(必填)' })

Typebox correctly generates:

{"anyOf": [{"const":"overwrite","type":"string"}, {"const":"append","type":"string"}, ...]}

But after normalizeDeepSeekSchema, the LLM only sees:

{"const": "overwrite", "type": "string", "description": "更新模式(必填)"}

All 6 other modes (append, replace_range, replace_all, insert_before, insert_after, delete_range) are invisible to the model.

Impact

  • Tool parameters using Union[Literal] (Typebox) or equivalent JSON Schema anyOf with const values are silently broken for DeepSeek models
  • Models can only use the first enum value; all others cause Validation failed: mode: must be equal to constant
  • The same issue affects any plugin that uses const unions for enum-like parameters

Suggested Fix

When all non-null variants are const string literals, convert to enum instead of picking the first:

// In normalizeDeepSeekSchema, after computing nonNullVariants:
const allConstStrings = nonNullVariants.every(
  v => v && typeof v === "object" && "const" in v && typeof v.const === "string"
);
if (allConstStrings) {
  const enumValues = nonNullVariants.map(v => v.const);
  return {
    type: "string",
    enum: enumValues,
    ...normalized  // preserve description, etc.
  };
}
// ... existing nullable union handling

This preserves the full set of allowed values while staying compatible with DeepSeek (which supports enum in JSON Schema).

Environment

  • OpenClaw version: 2026.5.20
  • Model: deepseek-v4-flash (via openai-completions API)
  • Plugin: openclaw-lark (feishu_update_doc tool)

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING