mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-03 11:04:06 +00:00
fix(plugin-sdk): preserve string-const unions as flat enum for deepseek tool schemas (#86712)
Summary: - This PR changes DeepSeek provider tool-schema normalization to convert multi-value string const unions into flat string enums, with regression coverage for pure, nullable, and single-const union cases. - PR surface: Source +27, Tests +84. Total +111 across 2 files. - Reproducibility: yes. source-level reproduction is high confidence: current main selects only the first non-null anyOf/oneOf variant, and the linked source PR proof shows before/after output for that exact schema shape. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(plugin-sdk): preserve string-const unions as flat enum for deepse… Validation: - ClawSweeper review passed for head310d95e327. - Required merge gates passed before the squash merge. Prepared head SHA:310d95e327Review: https://github.com/openclaw/openclaw/pull/86712#issuecomment-4538892244 Co-authored-by: 1052326311 <1052326311@users.noreply.github.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -119,6 +119,90 @@ describe("buildProviderToolCompatFamilyHooks", () => {
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("preserves string-const unions as a flat enum for the deepseek family", () => {
|
||||
// Regression for https://github.com/openclaw/openclaw/issues/86468 —
|
||||
// Typebox `Type.Union([Type.Literal(...)])` collapses to anyOf of consts;
|
||||
// the previous normalizer kept only the first const, hiding every other
|
||||
// literal from the model.
|
||||
const hooks = buildProviderToolCompatFamilyHooks("deepseek");
|
||||
const tools = [
|
||||
{
|
||||
name: "feishu_update_doc",
|
||||
description: "",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
mode: {
|
||||
description: "更新模式(必填)",
|
||||
anyOf: [
|
||||
{ const: "overwrite", type: "string" },
|
||||
{ const: "append", type: "string" },
|
||||
{ const: "replace_range", type: "string" },
|
||||
],
|
||||
},
|
||||
optional_mode: {
|
||||
anyOf: [
|
||||
{ const: "a", type: "string" },
|
||||
{ const: "b", type: "string" },
|
||||
{ type: "null" },
|
||||
],
|
||||
},
|
||||
single_const: {
|
||||
anyOf: [{ const: "only", type: "string" }],
|
||||
},
|
||||
},
|
||||
required: ["mode"],
|
||||
},
|
||||
},
|
||||
] as never;
|
||||
|
||||
const normalized = hooks.normalizeToolSchemas({
|
||||
provider: "deepseek",
|
||||
modelId: "deepseek-v4-pro",
|
||||
modelApi: "openai-completions",
|
||||
model: {
|
||||
provider: "deepseek",
|
||||
api: "openai-completions",
|
||||
id: "deepseek-v4-pro",
|
||||
} as never,
|
||||
tools,
|
||||
});
|
||||
|
||||
expect(normalized[0]?.parameters).toEqual({
|
||||
type: "object",
|
||||
properties: {
|
||||
mode: {
|
||||
description: "更新模式(必填)",
|
||||
type: "string",
|
||||
enum: ["overwrite", "append", "replace_range"],
|
||||
},
|
||||
optional_mode: {
|
||||
type: "string",
|
||||
enum: ["a", "b"],
|
||||
nullable: true,
|
||||
},
|
||||
single_const: {
|
||||
const: "only",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
required: ["mode"],
|
||||
});
|
||||
expect(
|
||||
hooks.inspectToolSchemas({
|
||||
provider: "deepseek",
|
||||
modelId: "deepseek-v4-pro",
|
||||
modelApi: "openai-completions",
|
||||
model: {
|
||||
provider: "deepseek",
|
||||
api: "openai-completions",
|
||||
id: "deepseek-v4-pro",
|
||||
} as never,
|
||||
tools: normalized,
|
||||
}),
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("normalizes parameter-free and typed-object schemas for the openai family", () => {
|
||||
const hooks = buildProviderToolCompatFamilyHooks("openai");
|
||||
const tools = [
|
||||
|
||||
@@ -455,6 +455,25 @@ function normalizeDeepSeekSchema(schema: unknown): unknown {
|
||||
const variants = record[unionKey] as unknown[];
|
||||
const normalizedVariants = variants.map((entry) => normalizeDeepSeekSchema(entry));
|
||||
const nonNullVariants = normalizedVariants.filter((entry) => !isNullSchemaVariant(entry));
|
||||
const hasNullVariant = nonNullVariants.length < normalizedVariants.length;
|
||||
|
||||
// Preserve string-const unions as a flat string enum so DeepSeek tool
|
||||
// callers still see every allowed literal. Without this, a Typebox
|
||||
// `Type.Union([Type.Literal("a"), Type.Literal("b"), ...])` collapses to
|
||||
// only the first const and the model can never pick any other value.
|
||||
if (nonNullVariants.length > 1 && nonNullVariants.every((entry) => isStringConstVariant(entry))) {
|
||||
const enumValues = nonNullVariants.map((entry) => (entry as { const: string }).const);
|
||||
const merged: Record<string, unknown> = {
|
||||
...normalized,
|
||||
type: "string",
|
||||
enum: enumValues,
|
||||
};
|
||||
if (hasNullVariant) {
|
||||
merged.nullable = true;
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
const selected = nonNullVariants[0] ?? normalizedVariants[0];
|
||||
if (!selected || typeof selected !== "object" || Array.isArray(selected)) {
|
||||
return normalized;
|
||||
@@ -464,12 +483,20 @@ function normalizeDeepSeekSchema(schema: unknown): unknown {
|
||||
...(selected as Record<string, unknown>),
|
||||
...normalized,
|
||||
};
|
||||
if (nonNullVariants.length < normalizedVariants.length) {
|
||||
if (hasNullVariant) {
|
||||
merged.nullable = true;
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function isStringConstVariant(entry: unknown): entry is { const: string } {
|
||||
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
||||
return false;
|
||||
}
|
||||
const record = entry as Record<string, unknown>;
|
||||
return typeof record.const === "string";
|
||||
}
|
||||
|
||||
export function normalizeDeepSeekToolSchemas(
|
||||
ctx: ProviderNormalizeToolSchemasContext,
|
||||
): AnyAgentTool[] {
|
||||
|
||||
Reference in New Issue
Block a user