mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:50:43 +00:00
fix(tools): preserve tool availability contracts
This commit is contained in:
@@ -230,12 +230,9 @@ describe("createImageGenerateTool", () => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("defers image-generation model resolution until execution", async () => {
|
||||
it("returns null when no image-generation model can be inferred", () => {
|
||||
stubImageGenerationProviders();
|
||||
const tool = requireImageGenerateTool(createImageGenerateTool({ config: {} }));
|
||||
await expect(tool.execute("tool-call-1", { prompt: "draw a chart" })).rejects.toThrow(
|
||||
"No image-generation model configured.",
|
||||
);
|
||||
expect(createImageGenerateTool({ config: {} })).toBeNull();
|
||||
});
|
||||
|
||||
it("tells agents how to request transparent OpenAI backgrounds", () => {
|
||||
|
||||
@@ -565,7 +565,18 @@ export function createImageGenerateTool(options?: {
|
||||
workspaceDir?: string;
|
||||
sandbox?: ImageGenerateSandboxConfig;
|
||||
fsPolicy?: ToolFsPolicy;
|
||||
}): AnyAgentTool {
|
||||
}): AnyAgentTool | null {
|
||||
const cfg = options?.config ?? getRuntimeConfig();
|
||||
const imageGenerationModelConfig = resolveImageGenerationModelConfigForTool({
|
||||
cfg,
|
||||
agentDir: options?.agentDir,
|
||||
});
|
||||
if (!imageGenerationModelConfig) {
|
||||
return null;
|
||||
}
|
||||
const effectiveCfg =
|
||||
applyImageGenerationModelConfigDefaults(cfg, imageGenerationModelConfig) ?? cfg;
|
||||
const remoteMediaSsrfPolicy = resolveRemoteMediaSsrfPolicy(effectiveCfg);
|
||||
const sandboxConfig =
|
||||
options?.sandbox && options.sandbox.root.trim()
|
||||
? {
|
||||
@@ -584,9 +595,8 @@ export function createImageGenerateTool(options?: {
|
||||
execute: async (_toolCallId, args) => {
|
||||
const params = args as Record<string, unknown>;
|
||||
const action = resolveAction(params);
|
||||
const cfg = options?.config ?? getRuntimeConfig();
|
||||
if (action === "list") {
|
||||
const runtimeProviders = listRuntimeImageGenerationProviders({ config: cfg });
|
||||
const runtimeProviders = listRuntimeImageGenerationProviders({ config: effectiveCfg });
|
||||
const providers = runtimeProviders.map((provider) =>
|
||||
Object.assign(
|
||||
{ id: provider.id },
|
||||
@@ -597,7 +607,7 @@ export function createImageGenerateTool(options?: {
|
||||
configured: isCapabilityProviderConfigured({
|
||||
providers: runtimeProviders,
|
||||
provider,
|
||||
cfg,
|
||||
cfg: effectiveCfg,
|
||||
agentDir: options?.agentDir,
|
||||
}),
|
||||
authEnvVars: getImageGenerationProviderAuthEnvVars(provider.id),
|
||||
@@ -647,16 +657,6 @@ export function createImageGenerateTool(options?: {
|
||||
};
|
||||
}
|
||||
|
||||
const imageGenerationModelConfig = resolveImageGenerationModelConfigForTool({
|
||||
cfg,
|
||||
agentDir: options?.agentDir,
|
||||
});
|
||||
if (!imageGenerationModelConfig) {
|
||||
throw new ToolInputError("No image-generation model configured.");
|
||||
}
|
||||
const effectiveCfg =
|
||||
applyImageGenerationModelConfigDefaults(cfg, imageGenerationModelConfig) ?? cfg;
|
||||
const remoteMediaSsrfPolicy = resolveRemoteMediaSsrfPolicy(effectiveCfg);
|
||||
const prompt = readStringParam(params, "prompt", { required: true });
|
||||
const imageInputs = normalizeReferenceImages(params);
|
||||
const model = readStringParam(params, "model");
|
||||
|
||||
@@ -618,20 +618,13 @@ describe("image tool implicit imageModel config", () => {
|
||||
__testing.setProviderDepsForTest();
|
||||
});
|
||||
|
||||
it("defers image model pairing until execution", async () => {
|
||||
it("stays disabled without auth when no pairing is possible", async () => {
|
||||
await withTempAgentDir(async (agentDir) => {
|
||||
const cfg: OpenClawConfig = {
|
||||
agents: { defaults: { model: { primary: "openai/gpt-5.4" } } },
|
||||
};
|
||||
expect(resolveImageModelConfigForTool({ cfg, agentDir })).toBeNull();
|
||||
const tool = createImageTool({ config: cfg, agentDir });
|
||||
expect(tool).not.toBeNull();
|
||||
await expect(
|
||||
tool?.execute("tool-call-1", {
|
||||
image: `data:image/png;base64,${ONE_PIXEL_PNG_B64}`,
|
||||
prompt: "describe it",
|
||||
}),
|
||||
).rejects.toThrow("No image model configured.");
|
||||
expect(createImageTool({ config: cfg, agentDir })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -380,6 +380,15 @@ export function createImageTool(options?: {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const imageModelConfig = resolveImageModelConfigForTool({
|
||||
cfg: options?.config,
|
||||
agentDir,
|
||||
});
|
||||
if (!imageModelConfig) {
|
||||
return null;
|
||||
}
|
||||
const remoteMediaSsrfPolicy = resolveRemoteMediaSsrfPolicy(options?.config);
|
||||
|
||||
// If model has native vision, images in the prompt are auto-injected
|
||||
// so this tool is only needed when image wasn't provided in the prompt
|
||||
const description = options?.modelHasVision
|
||||
@@ -453,14 +462,6 @@ export function createImageTool(options?: {
|
||||
record,
|
||||
DEFAULT_PROMPT,
|
||||
);
|
||||
const imageModelConfig = resolveImageModelConfigForTool({
|
||||
cfg: options?.config,
|
||||
agentDir,
|
||||
});
|
||||
if (!imageModelConfig) {
|
||||
throw new Error("No image model configured.");
|
||||
}
|
||||
const remoteMediaSsrfPolicy = resolveRemoteMediaSsrfPolicy(options?.config);
|
||||
const maxBytesMb = typeof record.maxBytesMb === "number" ? record.maxBytesMb : undefined;
|
||||
const maxBytes = pickMaxBytes(options?.config, maxBytesMb);
|
||||
|
||||
|
||||
@@ -129,12 +129,9 @@ describe("createMusicGenerateTool", () => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("defers music-generation model resolution until execution", async () => {
|
||||
it("returns null when no music-generation config or auth-backed provider is available", () => {
|
||||
vi.spyOn(musicGenerationRuntime, "listRuntimeMusicGenerationProviders").mockReturnValue([]);
|
||||
const tool = createMusicGenerateTool({ config: asConfig({}) });
|
||||
await expect(tool.execute("tool-call-1", { prompt: "make a loop" })).rejects.toThrow(
|
||||
"No music-generation model configured.",
|
||||
);
|
||||
expect(createMusicGenerateTool({ config: asConfig({}) })).toBeNull();
|
||||
});
|
||||
|
||||
it("registers when music-generation config is present", () => {
|
||||
|
||||
@@ -493,7 +493,16 @@ export function createMusicGenerateTool(options?: {
|
||||
sandbox?: MusicGenerateSandboxConfig;
|
||||
fsPolicy?: ToolFsPolicy;
|
||||
scheduleBackgroundWork?: MusicGenerateBackgroundScheduler;
|
||||
}): AnyAgentTool {
|
||||
}): AnyAgentTool | null {
|
||||
const cfg: OpenClawConfig = options?.config ?? getRuntimeConfig();
|
||||
const musicGenerationModelConfig = resolveMusicGenerationModelConfigForTool({
|
||||
cfg,
|
||||
agentDir: options?.agentDir,
|
||||
});
|
||||
if (!musicGenerationModelConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sandboxConfig = options?.sandbox
|
||||
? {
|
||||
root: options.sandbox.root,
|
||||
@@ -514,26 +523,17 @@ export function createMusicGenerateTool(options?: {
|
||||
execute: async (_toolCallId, rawArgs) => {
|
||||
const args = rawArgs as Record<string, unknown>;
|
||||
const action = resolveAction(args);
|
||||
const cfg: OpenClawConfig = options?.config ?? getRuntimeConfig();
|
||||
const effectiveCfg =
|
||||
applyMusicGenerationModelConfigDefaults(cfg, musicGenerationModelConfig) ?? cfg;
|
||||
|
||||
if (action === "list") {
|
||||
return createMusicGenerateListActionResult(cfg);
|
||||
return createMusicGenerateListActionResult(effectiveCfg);
|
||||
}
|
||||
|
||||
if (action === "status") {
|
||||
return createMusicGenerateStatusActionResult(options?.agentSessionKey);
|
||||
}
|
||||
|
||||
const musicGenerationModelConfig = resolveMusicGenerationModelConfigForTool({
|
||||
cfg,
|
||||
agentDir: options?.agentDir,
|
||||
});
|
||||
if (!musicGenerationModelConfig) {
|
||||
throw new ToolInputError("No music-generation model configured.");
|
||||
}
|
||||
const effectiveCfg =
|
||||
applyMusicGenerationModelConfigDefaults(cfg, musicGenerationModelConfig) ?? cfg;
|
||||
|
||||
const duplicateGuardResult = createMusicGenerateDuplicateGuardResult(
|
||||
options?.agentSessionKey,
|
||||
);
|
||||
|
||||
@@ -257,6 +257,11 @@ export function createPdfTool(options?: {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pdfModelConfig = resolvePdfModelConfigForTool({ cfg: options?.config, agentDir });
|
||||
if (!pdfModelConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const maxBytesMbDefault = (
|
||||
options?.config?.agents?.defaults as Record<string, unknown> | undefined
|
||||
)?.pdfMaxBytesMb;
|
||||
@@ -303,10 +308,6 @@ export function createPdfTool(options?: {
|
||||
record,
|
||||
DEFAULT_PROMPT,
|
||||
);
|
||||
const pdfModelConfig = resolvePdfModelConfigForTool({ cfg: options?.config, agentDir });
|
||||
if (!pdfModelConfig) {
|
||||
throw new Error("No PDF model configured.");
|
||||
}
|
||||
const maxBytesMbRaw = typeof record.maxBytesMb === "number" ? record.maxBytesMb : undefined;
|
||||
const maxBytesMb =
|
||||
typeof maxBytesMbRaw === "number" && Number.isFinite(maxBytesMbRaw) && maxBytesMbRaw > 0
|
||||
|
||||
@@ -93,13 +93,10 @@ describe("createVideoGenerateTool", () => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("defers video-generation model resolution until execution", async () => {
|
||||
it("returns null when no video-generation config or auth-backed provider is available", () => {
|
||||
vi.spyOn(videoGenerationRuntime, "listRuntimeVideoGenerationProviders").mockReturnValue([]);
|
||||
|
||||
const tool = createVideoGenerateTool({ config: asConfig({}) });
|
||||
await expect(tool.execute("tool-call-1", { prompt: "make a clip" })).rejects.toThrow(
|
||||
"No video-generation model configured.",
|
||||
);
|
||||
expect(createVideoGenerateTool({ config: asConfig({}) })).toBeNull();
|
||||
});
|
||||
|
||||
it("registers when video-generation config is present", () => {
|
||||
|
||||
@@ -800,7 +800,16 @@ export function createVideoGenerateTool(options?: {
|
||||
sandbox?: VideoGenerateSandboxConfig;
|
||||
fsPolicy?: ToolFsPolicy;
|
||||
scheduleBackgroundWork?: VideoGenerateBackgroundScheduler;
|
||||
}): AnyAgentTool {
|
||||
}): AnyAgentTool | null {
|
||||
const cfg: OpenClawConfig = options?.config ?? getRuntimeConfig();
|
||||
const videoGenerationModelConfig = resolveVideoGenerationModelConfigForTool({
|
||||
cfg,
|
||||
agentDir: options?.agentDir,
|
||||
});
|
||||
if (!videoGenerationModelConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sandboxConfig = options?.sandbox
|
||||
? {
|
||||
root: options.sandbox.root,
|
||||
@@ -821,27 +830,18 @@ export function createVideoGenerateTool(options?: {
|
||||
execute: async (_toolCallId, rawArgs) => {
|
||||
const args = rawArgs as Record<string, unknown>;
|
||||
const action = resolveAction(args);
|
||||
const cfg: OpenClawConfig = options?.config ?? getRuntimeConfig();
|
||||
const effectiveCfg =
|
||||
applyVideoGenerationModelConfigDefaults(cfg, videoGenerationModelConfig) ?? cfg;
|
||||
const remoteMediaSsrfPolicy = resolveRemoteMediaSsrfPolicy(effectiveCfg);
|
||||
|
||||
if (action === "list") {
|
||||
return createVideoGenerateListActionResult(cfg);
|
||||
return createVideoGenerateListActionResult(effectiveCfg);
|
||||
}
|
||||
|
||||
if (action === "status") {
|
||||
return createVideoGenerateStatusActionResult(options?.agentSessionKey);
|
||||
}
|
||||
|
||||
const videoGenerationModelConfig = resolveVideoGenerationModelConfigForTool({
|
||||
cfg,
|
||||
agentDir: options?.agentDir,
|
||||
});
|
||||
if (!videoGenerationModelConfig) {
|
||||
throw new ToolInputError("No video-generation model configured.");
|
||||
}
|
||||
const effectiveCfg =
|
||||
applyVideoGenerationModelConfigDefaults(cfg, videoGenerationModelConfig) ?? cfg;
|
||||
const remoteMediaSsrfPolicy = resolveRemoteMediaSsrfPolicy(effectiveCfg);
|
||||
|
||||
const duplicateGuardResult = createVideoGenerateDuplicateGuardResult(
|
||||
options?.agentSessionKey,
|
||||
);
|
||||
|
||||
@@ -14,6 +14,51 @@ const WebSearchSchema = {
|
||||
type: "number",
|
||||
description: "Number of results to return.",
|
||||
minimum: 1,
|
||||
maximum: 20,
|
||||
},
|
||||
country: {
|
||||
type: "string",
|
||||
description: "2-letter country code for region-specific results.",
|
||||
},
|
||||
language: {
|
||||
type: "string",
|
||||
description: "ISO 639-1 language code for results.",
|
||||
},
|
||||
freshness: {
|
||||
type: "string",
|
||||
description: "Filter by time: day, week, month, or year.",
|
||||
},
|
||||
date_after: {
|
||||
type: "string",
|
||||
description: "Only results published after this date (YYYY-MM-DD).",
|
||||
},
|
||||
date_before: {
|
||||
type: "string",
|
||||
description: "Only results published before this date (YYYY-MM-DD).",
|
||||
},
|
||||
search_lang: {
|
||||
type: "string",
|
||||
description: "Brave search result language code.",
|
||||
},
|
||||
ui_lang: {
|
||||
type: "string",
|
||||
description: "Brave UI locale code in language-region format.",
|
||||
},
|
||||
domain_filter: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Perplexity native Search API domain filter.",
|
||||
},
|
||||
max_tokens: {
|
||||
type: "number",
|
||||
description: "Perplexity native Search API total content budget.",
|
||||
minimum: 1,
|
||||
maximum: 1000000,
|
||||
},
|
||||
max_tokens_per_page: {
|
||||
type: "number",
|
||||
description: "Perplexity native Search API max tokens extracted per page.",
|
||||
minimum: 1,
|
||||
},
|
||||
},
|
||||
} satisfies Record<string, unknown>;
|
||||
|
||||
Reference in New Issue
Block a user