fix: scope gemini tool schemas
This commit is contained in:
parent
d42f31c2a5
commit
133889a2f6
@ -63,6 +63,10 @@ export async function loadModelCatalog(params?: {
|
|||||||
// If we found nothing, don't cache this result so we can try again.
|
// If we found nothing, don't cache this result so we can try again.
|
||||||
modelCatalogPromise = null;
|
modelCatalogPromise = null;
|
||||||
}
|
}
|
||||||
|
if (models.length === 0) {
|
||||||
|
// If we found nothing, don't cache this result so we can try again.
|
||||||
|
modelCatalogPromise = null;
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Leave models empty on discovery errors and don't cache.
|
// Leave models empty on discovery errors and don't cache.
|
||||||
modelCatalogPromise = null;
|
modelCatalogPromise = null;
|
||||||
|
|||||||
@ -408,6 +408,8 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
const promptSkills = resolvePromptSkills(skillsSnapshot, skillEntries);
|
const promptSkills = resolvePromptSkills(skillsSnapshot, skillEntries);
|
||||||
const tools = createClawdisCodingTools({
|
const tools = createClawdisCodingTools({
|
||||||
bash: params.config?.agent?.bash,
|
bash: params.config?.agent?.bash,
|
||||||
|
provider,
|
||||||
|
surface: params.surface,
|
||||||
});
|
});
|
||||||
const machineName = await getMachineDisplayName();
|
const machineName = await getMachineDisplayName();
|
||||||
const runtimeInfo = {
|
const runtimeInfo = {
|
||||||
|
|||||||
@ -72,6 +72,50 @@ describe("createClawdisCodingTools", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps anyOf variants for non-Gemini providers", () => {
|
||||||
|
const tools = createClawdisCodingTools({ provider: "openai" });
|
||||||
|
const browser = tools.find((tool) => tool.name === "browser");
|
||||||
|
expect(browser).toBeDefined();
|
||||||
|
const parameters = browser?.parameters as {
|
||||||
|
anyOf?: Array<{ properties?: Record<string, unknown> }>;
|
||||||
|
};
|
||||||
|
expect(Array.isArray(parameters.anyOf)).toBe(true);
|
||||||
|
expect(parameters.anyOf?.length ?? 0).toBeGreaterThan(0);
|
||||||
|
const hasConst = parameters.anyOf?.some((variant) => {
|
||||||
|
const action = variant.properties?.action as
|
||||||
|
| { const?: unknown }
|
||||||
|
| undefined;
|
||||||
|
return typeof action?.const === "string";
|
||||||
|
});
|
||||||
|
expect(hasConst).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips anyOf for google-gemini-cli tools", () => {
|
||||||
|
const tools = createClawdisCodingTools({ provider: "google-gemini-cli" });
|
||||||
|
const browser = tools.find((tool) => tool.name === "browser");
|
||||||
|
expect(browser).toBeDefined();
|
||||||
|
const parameters = browser?.parameters as {
|
||||||
|
anyOf?: unknown;
|
||||||
|
properties?: Record<string, unknown>;
|
||||||
|
required?: string[];
|
||||||
|
};
|
||||||
|
expect(parameters.anyOf).toBeUndefined();
|
||||||
|
expect(parameters.properties?.action).toBeDefined();
|
||||||
|
expect(parameters.required ?? []).toContain("action");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips anyOf for google-antigravity tools", () => {
|
||||||
|
const tools = createClawdisCodingTools({ provider: "google-antigravity" });
|
||||||
|
const browser = tools.find((tool) => tool.name === "browser");
|
||||||
|
expect(browser).toBeDefined();
|
||||||
|
const parameters = browser?.parameters as {
|
||||||
|
anyOf?: unknown;
|
||||||
|
properties?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
expect(parameters.anyOf).toBeUndefined();
|
||||||
|
expect(parameters.properties?.action).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
it("includes bash and process tools", () => {
|
it("includes bash and process tools", () => {
|
||||||
const tools = createClawdisCodingTools();
|
const tools = createClawdisCodingTools();
|
||||||
expect(tools.some((tool) => tool.name === "bash")).toBe(true);
|
expect(tools.some((tool) => tool.name === "bash")).toBe(true);
|
||||||
|
|||||||
@ -141,80 +141,76 @@ function mergePropertySchemas(existing: unknown, incoming: unknown): unknown {
|
|||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gemini tool schemas accept an OpenAPI-ish subset; strip unsupported bits.
|
||||||
function cleanSchemaForGemini(schema: unknown): unknown {
|
function cleanSchemaForGemini(schema: unknown): unknown {
|
||||||
if (!schema || typeof schema !== "object") return schema;
|
if (!schema || typeof schema !== "object") return schema;
|
||||||
if (Array.isArray(schema)) return schema.map(cleanSchemaForGemini);
|
if (Array.isArray(schema)) return schema.map(cleanSchemaForGemini);
|
||||||
|
|
||||||
const obj = schema as Record<string, unknown>;
|
const obj = schema as Record<string, unknown>;
|
||||||
const hasAnyOf = "anyOf" in obj && Array.isArray(obj.anyOf);
|
const hasAnyOf = "anyOf" in obj && Array.isArray(obj.anyOf);
|
||||||
const hasConst = "const" in obj;
|
|
||||||
const cleaned: Record<string, unknown> = {};
|
const cleaned: Record<string, unknown> = {};
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(obj)) {
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
// Skip unsupported schema features for Gemini:
|
|
||||||
// - patternProperties: not in OpenAPI 3.0 subset
|
|
||||||
// - const: convert to enum with single value instead
|
|
||||||
if (key === "patternProperties") {
|
if (key === "patternProperties") {
|
||||||
// Gemini doesn't support patternProperties - skip it
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert const to enum (Gemini doesn't support const)
|
|
||||||
if (key === "const") {
|
if (key === "const") {
|
||||||
cleaned.enum = [value];
|
cleaned.enum = [value];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip 'type' if we have 'anyOf' — Gemini doesn't allow both
|
|
||||||
if (key === "type" && hasAnyOf) {
|
if (key === "type" && hasAnyOf) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === "properties" && value && typeof value === "object") {
|
if (key === "properties" && value && typeof value === "object") {
|
||||||
// Recursively clean nested properties
|
|
||||||
const props = value as Record<string, unknown>;
|
const props = value as Record<string, unknown>;
|
||||||
cleaned[key] = Object.fromEntries(
|
cleaned[key] = Object.fromEntries(
|
||||||
Object.entries(props).map(([k, v]) => [k, cleanSchemaForGemini(v)])
|
Object.entries(props).map(([k, v]) => [k, cleanSchemaForGemini(v)]),
|
||||||
);
|
);
|
||||||
} else if (key === "items" && value && typeof value === "object") {
|
} else if (key === "items" && value && typeof value === "object") {
|
||||||
// Recursively clean array items schema
|
|
||||||
cleaned[key] = cleanSchemaForGemini(value);
|
cleaned[key] = cleanSchemaForGemini(value);
|
||||||
} else if (key === "anyOf" && Array.isArray(value)) {
|
} else if (key === "anyOf" && Array.isArray(value)) {
|
||||||
// Clean each anyOf variant
|
cleaned[key] = value.map((v) => cleanSchemaForGemini(v));
|
||||||
cleaned[key] = value.map(v => cleanSchemaForGemini(v));
|
|
||||||
} else if (key === "oneOf" && Array.isArray(value)) {
|
} else if (key === "oneOf" && Array.isArray(value)) {
|
||||||
// Clean each oneOf variant
|
cleaned[key] = value.map((v) => cleanSchemaForGemini(v));
|
||||||
cleaned[key] = value.map(v => cleanSchemaForGemini(v));
|
|
||||||
} else if (key === "allOf" && Array.isArray(value)) {
|
} else if (key === "allOf" && Array.isArray(value)) {
|
||||||
// Clean each allOf variant
|
cleaned[key] = value.map((v) => cleanSchemaForGemini(v));
|
||||||
cleaned[key] = value.map(v => cleanSchemaForGemini(v));
|
|
||||||
} else if (key === "additionalProperties" && value && typeof value === "object") {
|
} else if (key === "additionalProperties" && value && typeof value === "object") {
|
||||||
// Recursively clean additionalProperties schema
|
|
||||||
cleaned[key] = cleanSchemaForGemini(value);
|
cleaned[key] = cleanSchemaForGemini(value);
|
||||||
} else {
|
} else {
|
||||||
cleaned[key] = value;
|
cleaned[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cleaned;
|
return cleaned;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool {
|
// Only Gemini providers need schema cleanup; other providers can keep richer JSON Schema.
|
||||||
|
function shouldCleanSchemaForGemini(provider?: string): boolean {
|
||||||
|
return provider === "google-gemini-cli" || provider === "google-antigravity";
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeToolParameters(
|
||||||
|
tool: AnyAgentTool,
|
||||||
|
options?: { cleanForGemini?: boolean },
|
||||||
|
): AnyAgentTool {
|
||||||
const schema =
|
const schema =
|
||||||
tool.parameters && typeof tool.parameters === "object"
|
tool.parameters && typeof tool.parameters === "object"
|
||||||
? (tool.parameters as Record<string, unknown>)
|
? (tool.parameters as Record<string, unknown>)
|
||||||
: undefined;
|
: undefined;
|
||||||
if (!schema) return tool;
|
if (!schema) return tool;
|
||||||
|
const cleanForGemini = options?.cleanForGemini === true;
|
||||||
// If schema already has type + properties (no top-level anyOf to merge),
|
if (
|
||||||
// still clean it for Gemini compatibility
|
"type" in schema &&
|
||||||
if ("type" in schema && "properties" in schema && !Array.isArray(schema.anyOf)) {
|
"properties" in schema &&
|
||||||
return {
|
!Array.isArray(schema.anyOf)
|
||||||
...tool,
|
) {
|
||||||
parameters: cleanSchemaForGemini(schema),
|
return cleanForGemini
|
||||||
};
|
? { ...tool, parameters: cleanSchemaForGemini(schema) }
|
||||||
|
: tool;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(schema.anyOf)) return tool;
|
if (!Array.isArray(schema.anyOf)) return tool;
|
||||||
const mergedProperties: Record<string, unknown> = {};
|
const mergedProperties: Record<string, unknown> = {};
|
||||||
const requiredCounts = new Map<string, number>();
|
const requiredCounts = new Map<string, number>();
|
||||||
@ -258,22 +254,29 @@ function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool {
|
|||||||
.map(([key]) => key)
|
.map(([key]) => key)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const { anyOf: _unusedAnyOf, ...restSchema } = schema;
|
const mergedSchema = {
|
||||||
|
...schema,
|
||||||
|
type: "object",
|
||||||
|
properties:
|
||||||
|
Object.keys(mergedProperties).length > 0
|
||||||
|
? mergedProperties
|
||||||
|
: (schema.properties ?? {}),
|
||||||
|
...(mergedRequired && mergedRequired.length > 0
|
||||||
|
? { required: mergedRequired }
|
||||||
|
: {}),
|
||||||
|
additionalProperties:
|
||||||
|
"additionalProperties" in schema ? schema.additionalProperties : true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Preserve anyOf for non-Gemini providers; Gemini rejects top-level anyOf.
|
||||||
return {
|
return {
|
||||||
...tool,
|
...tool,
|
||||||
parameters: cleanSchemaForGemini({
|
parameters: cleanForGemini
|
||||||
...restSchema,
|
? (() => {
|
||||||
type: "object",
|
const { anyOf: _unusedAnyOf, ...rest } = mergedSchema;
|
||||||
properties:
|
return cleanSchemaForGemini(rest);
|
||||||
Object.keys(mergedProperties).length > 0
|
})()
|
||||||
? mergedProperties
|
: mergedSchema,
|
||||||
: (schema.properties ?? {}),
|
|
||||||
...(mergedRequired && mergedRequired.length > 0
|
|
||||||
? { required: mergedRequired }
|
|
||||||
: {}),
|
|
||||||
additionalProperties:
|
|
||||||
"additionalProperties" in schema ? schema.additionalProperties : true,
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -364,6 +367,8 @@ function createClawdisReadTool(base: AnyAgentTool): AnyAgentTool {
|
|||||||
|
|
||||||
export function createClawdisCodingTools(options?: {
|
export function createClawdisCodingTools(options?: {
|
||||||
bash?: BashToolDefaults & ProcessToolDefaults;
|
bash?: BashToolDefaults & ProcessToolDefaults;
|
||||||
|
surface?: string;
|
||||||
|
provider?: string;
|
||||||
}): AnyAgentTool[] {
|
}): AnyAgentTool[] {
|
||||||
const bashToolName = "bash";
|
const bashToolName = "bash";
|
||||||
const base = (codingTools as unknown as AnyAgentTool[]).flatMap((tool) => {
|
const base = (codingTools as unknown as AnyAgentTool[]).flatMap((tool) => {
|
||||||
@ -382,5 +387,12 @@ export function createClawdisCodingTools(options?: {
|
|||||||
createWhatsAppLoginTool(),
|
createWhatsAppLoginTool(),
|
||||||
...createClawdisTools(),
|
...createClawdisTools(),
|
||||||
];
|
];
|
||||||
return tools.map(normalizeToolParameters);
|
const allowDiscord = shouldIncludeDiscordTool(options?.surface);
|
||||||
|
const filtered = allowDiscord
|
||||||
|
? tools
|
||||||
|
: tools.filter((tool) => tool.name !== "discord");
|
||||||
|
const cleanForGemini = shouldCleanSchemaForGemini(options?.provider);
|
||||||
|
return filtered.map((tool) =>
|
||||||
|
normalizeToolParameters(tool, { cleanForGemini }),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user