fix: flatten nested anyOf/oneOf in Gemini schema cleaning (openclaw#22825) thanks @Oceanswave

Verified:
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: Oceanswave <760674+Oceanswave@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
Sean McLellan
2026-02-21 14:09:42 -05:00
committed by GitHub
parent f9108120c2
commit 00b98a368a
2 changed files with 55 additions and 0 deletions

View File

@@ -129,6 +129,7 @@ Docs: https://docs.openclaw.ai
- Agents/Subagents: restore announce-chain delivery to agent injection, defer nested announce output until descendant follow-up content is ready, and prevent descendant deferrals from consuming announce retry budget so deep chains do not drop final completions. (#22223) Thanks @tyler6204.
- Agents/System Prompt: label allowlisted senders as authorized senders to avoid implying ownership. Thanks @thewilloftheshadow.
- Agents/Tool display: fix exec cwd suffix inference so `pushd ... && popd ... && <command>` does not keep stale `(in <dir>)` context in summaries. (#21925) Thanks @Lukavyi.
- Agents/Google: flatten residual nested `anyOf`/`oneOf` unions in Gemini tool-schema cleanup so Cloud Code Assist no longer rejects unsupported union keywords that survive earlier simplification. (#22825) Thanks @Oceanswave.
- Tools/web_search: handle xAI Responses API payloads that emit top-level `output_text` blocks (without a `message` wrapper) so Grok web_search no longer returns `No response` for those results. (#20508) Thanks @echoVic.
- Agents/Failover: treat non-default override runs as direct fallback-to-configured-primary (skip configured fallback chain), normalize default-model detection for provider casing/whitespace, and add regression coverage for override/auth error paths. (#18820) Thanks @Glucksberg.
- Docker/Build: include `ownerDisplay` in `CommandsSchema` object-level defaults so Docker `pnpm build` no longer fails with `TS2769` during plugin SDK d.ts generation. (#22558) Thanks @obviyus.

View File

@@ -339,9 +339,63 @@ function cleanSchemaForGeminiWithDefs(
}
}
// Cloud Code Assist API rejects anyOf/oneOf in nested schemas even after
// simplifyUnionVariants runs above. Flatten remaining unions as a fallback:
// pick the common type or use the first variant's type so the tool
// declaration is accepted by Google's validation layer.
if (cleaned.anyOf && Array.isArray(cleaned.anyOf)) {
const flattened = flattenUnionFallback(cleaned, cleaned.anyOf);
if (flattened) {
return flattened;
}
}
if (cleaned.oneOf && Array.isArray(cleaned.oneOf)) {
const flattened = flattenUnionFallback(cleaned, cleaned.oneOf);
if (flattened) {
return flattened;
}
}
return cleaned;
}
/**
* Last-resort flattening for anyOf/oneOf arrays that could not be simplified
* by `simplifyUnionVariants`. Picks a representative type so the schema is
* accepted by Google's restricted JSON Schema validation.
*/
function flattenUnionFallback(
obj: Record<string, unknown>,
variants: unknown[],
): Record<string, unknown> | undefined {
const objects = variants.filter(
(v): v is Record<string, unknown> => !!v && typeof v === "object",
);
if (objects.length === 0) {
return undefined;
}
const types = new Set(objects.map((v) => v.type).filter(Boolean));
if (objects.length === 1) {
const merged: Record<string, unknown> = { ...objects[0] };
copySchemaMeta(obj, merged);
return merged;
}
if (types.size === 1) {
const merged: Record<string, unknown> = { type: Array.from(types)[0] };
copySchemaMeta(obj, merged);
return merged;
}
const first = objects[0];
if (first?.type) {
const merged: Record<string, unknown> = { type: first.type };
copySchemaMeta(obj, merged);
return merged;
}
const merged: Record<string, unknown> = {};
copySchemaMeta(obj, merged);
return merged;
}
export function cleanSchemaForGemini(schema: unknown): unknown {
if (!schema || typeof schema !== "object") {
return schema;