fix: expose union tool parameters

This commit is contained in:
Peter Steinberger
2025-12-24 21:48:20 +00:00
parent 1b69c94f76
commit 79870472e1
2 changed files with 57 additions and 66 deletions

View File

@@ -1,71 +1,21 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import sharp from "sharp";
import { describe, expect, it } from "vitest";
import { createClawdisCodingTools } from "./pi-tools.js";
const PNG_1x1 =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/woAAn8B9FD5fHAAAAAASUVORK5CYII=";
describe("createClawdisCodingTools", () => {
it("sniffs mime from bytes when extension lies", async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-pi-"));
const filePath = path.join(tmpDir, "image.jpg"); // actually PNG bytes
await fs.writeFile(filePath, Buffer.from(PNG_1x1, "base64"));
const read = createClawdisCodingTools().find((t) => t.name === "read");
expect(read).toBeTruthy();
if (!read) throw new Error("read tool missing");
const res = await read.execute("toolCallId", { path: filePath });
const image = res.content.find(
(b): b is { type: "image"; mimeType: string } =>
!!b &&
typeof b === "object" &&
(b as Record<string, unknown>).type === "image" &&
typeof (b as Record<string, unknown>).mimeType === "string",
);
expect(image?.mimeType).toBe("image/png");
});
it("downscales oversized images for LLM safety", async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-pi-"));
const filePath = path.join(tmpDir, "oversized.png");
const buf = await sharp({
create: {
width: 2001,
height: 10,
channels: 3,
background: { r: 0, g: 0, b: 0 },
},
})
.png()
.toBuffer();
await fs.writeFile(filePath, buf);
const read = createClawdisCodingTools().find((t) => t.name === "read");
expect(read).toBeTruthy();
if (!read) throw new Error("read tool missing");
const res = await read.execute("toolCallId", { path: filePath });
const image = res.content.find(
(b): b is { type: "image"; mimeType: string; data: string } =>
!!b &&
typeof b === "object" &&
(b as Record<string, unknown>).type === "image" &&
typeof (b as Record<string, unknown>).mimeType === "string" &&
typeof (b as Record<string, unknown>).data === "string",
);
expect(image).toBeTruthy();
if (!image) throw new Error("image block missing");
const decoded = Buffer.from(image.data, "base64");
const meta = await sharp(decoded).metadata();
expect(meta.width).toBeLessThanOrEqual(2000);
expect(meta.height).toBeLessThanOrEqual(2000);
it("merges properties for union tool schemas", () => {
const tools = createClawdisCodingTools();
const browser = tools.find((tool) => tool.name === "clawdis_browser");
expect(browser).toBeDefined();
const parameters = browser?.parameters as {
anyOf?: unknown[];
properties?: Record<string, unknown>;
required?: string[];
};
expect(parameters.anyOf?.length ?? 0).toBeGreaterThan(0);
expect(parameters.properties?.action).toBeDefined();
expect(parameters.properties?.controlUrl).toBeDefined();
expect(parameters.properties?.targetUrl).toBeDefined();
expect(parameters.properties?.request).toBeDefined();
expect(parameters.required ?? []).toContain("action");
});
});

View File

@@ -107,12 +107,53 @@ function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool {
if (!schema) return tool;
if ("type" in schema && "properties" in schema) return tool;
if (!Array.isArray(schema.anyOf)) return tool;
const mergedProperties: Record<string, unknown> = {};
const requiredCounts = new Map<string, number>();
let objectVariants = 0;
for (const entry of schema.anyOf) {
if (!entry || typeof entry !== "object") continue;
const props = (entry as { properties?: unknown }).properties;
if (!props || typeof props !== "object") continue;
objectVariants += 1;
for (const [key, value] of Object.entries(
props as Record<string, unknown>,
)) {
if (!(key in mergedProperties)) mergedProperties[key] = value;
}
const required = Array.isArray((entry as { required?: unknown }).required)
? (entry as { required: unknown[] }).required
: [];
for (const key of required) {
if (typeof key !== "string") continue;
requiredCounts.set(key, (requiredCounts.get(key) ?? 0) + 1);
}
}
const baseRequired = Array.isArray(schema.required)
? schema.required.filter((key) => typeof key === "string")
: undefined;
const mergedRequired =
baseRequired && baseRequired.length > 0
? baseRequired
: objectVariants > 0
? Array.from(requiredCounts.entries())
.filter(([, count]) => count === objectVariants)
.map(([key]) => key)
: undefined;
return {
...tool,
parameters: {
...schema,
type: "object",
properties: schema.properties ?? {},
properties:
Object.keys(mergedProperties).length > 0
? mergedProperties
: schema.properties ?? {},
...(mergedRequired && mergedRequired.length > 0
? { required: mergedRequired }
: {}),
additionalProperties:
"additionalProperties" in schema ? schema.additionalProperties : true,
} as unknown as TSchema,