mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 15:30:39 +00:00
Revert "fix: flatten remaining anyOf/oneOf in Gemini schema cleaning"
This reverts commit 06b961b037.
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import type { AgentMessage, AgentTool } from "@mariozechner/pi-agent-core";
|
||||
import type { SessionManager } from "@mariozechner/pi-coding-agent";
|
||||
import type { TSchema } from "@sinclair/typebox";
|
||||
import { EventEmitter } from "node:events";
|
||||
import type { TranscriptPolicy } from "../transcript-policy.js";
|
||||
import { registerUnhandledRejectionHandler } from "../../infra/unhandled-rejections.js";
|
||||
import {
|
||||
hasInterSessionUserProvenance,
|
||||
@@ -20,7 +21,6 @@ import {
|
||||
stripToolResultDetails,
|
||||
sanitizeToolUseResultPairing,
|
||||
} from "../session-transcript-repair.js";
|
||||
import type { TranscriptPolicy } from "../transcript-policy.js";
|
||||
import { resolveTranscriptPolicy } from "../transcript-policy.js";
|
||||
import { log } from "./logger.js";
|
||||
import { describeUnknownError } from "./utils.js";
|
||||
@@ -245,7 +245,11 @@ export function sanitizeToolsForGoogle<
|
||||
tools: AgentTool<TSchemaType, TResult>[];
|
||||
provider: string;
|
||||
}): AgentTool<TSchemaType, TResult>[] {
|
||||
if (params.provider !== "google-antigravity" && params.provider !== "google-gemini-cli") {
|
||||
// google-antigravity serves Anthropic models (e.g. claude-opus-4-6-thinking),
|
||||
// NOT Gemini. Applying Gemini schema cleaning strips JSON Schema keywords
|
||||
// (minimum, maximum, format, etc.) that Anthropic's API requires for
|
||||
// draft 2020-12 compliance. Only clean for actual Gemini providers.
|
||||
if (params.provider !== "google-gemini-cli") {
|
||||
return params.tools;
|
||||
}
|
||||
return params.tools.map((tool) => {
|
||||
|
||||
@@ -62,7 +62,10 @@ function mergePropertySchemas(existing: unknown, incoming: unknown): unknown {
|
||||
return existing;
|
||||
}
|
||||
|
||||
export function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool {
|
||||
export function normalizeToolParameters(
|
||||
tool: AnyAgentTool,
|
||||
options?: { modelProvider?: string },
|
||||
): AnyAgentTool {
|
||||
const schema =
|
||||
tool.parameters && typeof tool.parameters === "object"
|
||||
? (tool.parameters as Record<string, unknown>)
|
||||
@@ -75,15 +78,23 @@ export function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool {
|
||||
// - Gemini rejects several JSON Schema keywords, so we scrub those.
|
||||
// - OpenAI rejects function tool schemas unless the *top-level* is `type: "object"`.
|
||||
// (TypeBox root unions compile to `{ anyOf: [...] }` without `type`).
|
||||
// - Anthropic (google-antigravity) expects full JSON Schema draft 2020-12 compliance.
|
||||
//
|
||||
// Normalize once here so callers can always pass `tools` through unchanged.
|
||||
|
||||
const isGeminiProvider =
|
||||
options?.modelProvider?.toLowerCase().includes("google") ||
|
||||
options?.modelProvider?.toLowerCase().includes("gemini");
|
||||
const isAnthropicProvider =
|
||||
options?.modelProvider?.toLowerCase().includes("anthropic") ||
|
||||
options?.modelProvider?.toLowerCase().includes("google-antigravity");
|
||||
|
||||
// If schema already has type + properties (no top-level anyOf to merge),
|
||||
// still clean it for Gemini compatibility
|
||||
// clean it for Gemini compatibility (but only if using Gemini, not Anthropic)
|
||||
if ("type" in schema && "properties" in schema && !Array.isArray(schema.anyOf)) {
|
||||
return {
|
||||
...tool,
|
||||
parameters: cleanSchemaForGemini(schema),
|
||||
parameters: isGeminiProvider && !isAnthropicProvider ? cleanSchemaForGemini(schema) : schema,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -95,9 +106,13 @@ export function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool {
|
||||
!Array.isArray(schema.anyOf) &&
|
||||
!Array.isArray(schema.oneOf)
|
||||
) {
|
||||
const schemaWithType = { ...schema, type: "object" };
|
||||
return {
|
||||
...tool,
|
||||
parameters: cleanSchemaForGemini({ ...schema, type: "object" }),
|
||||
parameters:
|
||||
isGeminiProvider && !isAnthropicProvider
|
||||
? cleanSchemaForGemini(schemaWithType)
|
||||
: schemaWithType,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -154,26 +169,34 @@ export function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool {
|
||||
: undefined;
|
||||
|
||||
const nextSchema: Record<string, unknown> = { ...schema };
|
||||
const flattenedSchema = {
|
||||
type: "object",
|
||||
...(typeof nextSchema.title === "string" ? { title: nextSchema.title } : {}),
|
||||
...(typeof nextSchema.description === "string" ? { description: nextSchema.description } : {}),
|
||||
properties:
|
||||
Object.keys(mergedProperties).length > 0 ? mergedProperties : (schema.properties ?? {}),
|
||||
...(mergedRequired && mergedRequired.length > 0 ? { required: mergedRequired } : {}),
|
||||
additionalProperties: "additionalProperties" in schema ? schema.additionalProperties : true,
|
||||
};
|
||||
|
||||
return {
|
||||
...tool,
|
||||
// Flatten union schemas into a single object schema:
|
||||
// - Gemini doesn't allow top-level `type` together with `anyOf`.
|
||||
// - OpenAI rejects schemas without top-level `type: "object"`.
|
||||
// - Anthropic accepts proper JSON Schema with constraints.
|
||||
// Merging properties preserves useful enums like `action` while keeping schemas portable.
|
||||
parameters: cleanSchemaForGemini({
|
||||
type: "object",
|
||||
...(typeof nextSchema.title === "string" ? { title: nextSchema.title } : {}),
|
||||
...(typeof nextSchema.description === "string"
|
||||
? { description: nextSchema.description }
|
||||
: {}),
|
||||
properties:
|
||||
Object.keys(mergedProperties).length > 0 ? mergedProperties : (schema.properties ?? {}),
|
||||
...(mergedRequired && mergedRequired.length > 0 ? { required: mergedRequired } : {}),
|
||||
additionalProperties: "additionalProperties" in schema ? schema.additionalProperties : true,
|
||||
}),
|
||||
parameters:
|
||||
isGeminiProvider && !isAnthropicProvider
|
||||
? cleanSchemaForGemini(flattenedSchema)
|
||||
: flattenedSchema,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use normalizeToolParameters with modelProvider instead.
|
||||
* This function should only be used for Gemini providers.
|
||||
*/
|
||||
export function cleanToolSchemaForGemini(schema: Record<string, unknown>): unknown {
|
||||
return cleanSchemaForGemini(schema);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@ import {
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { ToolLoopDetectionConfig } from "../config/types.tools.js";
|
||||
import type { ModelAuthMode } from "./model-auth.js";
|
||||
import type { AnyAgentTool } from "./pi-tools.types.js";
|
||||
import type { SandboxContext } from "./sandbox.js";
|
||||
import { logWarn } from "../logger.js";
|
||||
import { getPluginToolMeta } from "../plugins/tools.js";
|
||||
import { isSubagentSessionKey } from "../routing/session-key.js";
|
||||
@@ -20,7 +23,6 @@ import {
|
||||
type ProcessToolDefaults,
|
||||
} from "./bash-tools.js";
|
||||
import { listChannelAgentTools } from "./channel-tools.js";
|
||||
import type { ModelAuthMode } from "./model-auth.js";
|
||||
import { createOpenClawTools } from "./openclaw-tools.js";
|
||||
import { wrapToolWithAbortSignal } from "./pi-tools.abort.js";
|
||||
import { wrapToolWithBeforeToolCallHook } from "./pi-tools.before-tool-call.js";
|
||||
@@ -43,8 +45,6 @@ import {
|
||||
wrapToolParamNormalization,
|
||||
} from "./pi-tools.read.js";
|
||||
import { cleanToolSchemaForGemini, normalizeToolParameters } from "./pi-tools.schema.js";
|
||||
import type { AnyAgentTool } from "./pi-tools.types.js";
|
||||
import type { SandboxContext } from "./sandbox.js";
|
||||
import { getSubagentDepthFromSessionStore } from "./subagent-depth.js";
|
||||
import {
|
||||
applyToolPolicyPipeline,
|
||||
@@ -474,7 +474,10 @@ export function createOpenClawCodingTools(options?: {
|
||||
});
|
||||
// Always normalize tool JSON Schemas before handing them to pi-agent/pi-ai.
|
||||
// Without this, some providers (notably OpenAI) will reject root-level union schemas.
|
||||
const normalized = subagentFiltered.map(normalizeToolParameters);
|
||||
// Provider-specific cleaning: Gemini needs constraint keywords stripped, but Anthropic expects them.
|
||||
const normalized = subagentFiltered.map((tool) =>
|
||||
normalizeToolParameters(tool, { modelProvider: options?.modelProvider }),
|
||||
);
|
||||
const withHooks = normalized.map((tool) =>
|
||||
wrapToolWithBeforeToolCallHook(tool, {
|
||||
agentId,
|
||||
|
||||
@@ -207,36 +207,6 @@ function simplifyUnionVariants(params: { obj: Record<string, unknown>; variants:
|
||||
return { variants: stripped ? nonNullVariants : variants };
|
||||
}
|
||||
|
||||
function flattenUnionFallback(
|
||||
obj: Record<string, unknown>,
|
||||
value: unknown,
|
||||
): Record<string, unknown> | undefined {
|
||||
if (!Array.isArray(value)) {
|
||||
return undefined;
|
||||
}
|
||||
const variants = (value as Record<string, unknown>[]).filter((v) => v && typeof v === "object");
|
||||
const types = new Set(variants.map((v) => v.type).filter(Boolean));
|
||||
if (variants.length === 1) {
|
||||
const merged: Record<string, unknown> = { ...variants[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 = variants[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;
|
||||
}
|
||||
|
||||
function cleanSchemaForGeminiWithDefs(
|
||||
schema: unknown,
|
||||
defs: SchemaDefs | undefined,
|
||||
@@ -369,20 +339,6 @@ function cleanSchemaForGeminiWithDefs(
|
||||
}
|
||||
}
|
||||
|
||||
// Cloud Code Assist API also rejects anyOf/oneOf in nested schemas.
|
||||
// If simplifyUnionVariants couldn't reduce the union above, flatten it
|
||||
// here as a fallback: pick the first variant's type or use a permissive
|
||||
// schema so the tool declaration is accepted.
|
||||
const flattenedAnyOf = flattenUnionFallback(cleaned, cleaned.anyOf);
|
||||
if (flattenedAnyOf) {
|
||||
return flattenedAnyOf;
|
||||
}
|
||||
|
||||
const flattenedOneOf = flattenUnionFallback(cleaned, cleaned.oneOf);
|
||||
if (flattenedOneOf) {
|
||||
return flattenedOneOf;
|
||||
}
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user