From 0bbb0eb735586eef6cceb5696c798c42ee165dc8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 25 Apr 2026 18:25:13 +0100 Subject: [PATCH] fix(image): honor generation timeout config --- docs/.generated/config-baseline.sha256 | 4 +- docs/cli/infer.md | 1 + docs/providers/openrouter.md | 3 +- docs/tools/image-generation.md | 5 ++ src/agents/tools/image-generate-tool.test.ts | 61 +++++++++++++++++++ src/agents/tools/image-generate-tool.ts | 2 +- src/agents/tools/model-config.helpers.ts | 6 +- src/cli/capability-cli.test.ts | 36 +++++++++++ src/cli/capability-cli.ts | 6 ++ src/config/model-input.ts | 12 ++++ src/config/schema.base.generated.ts | 48 +++++++++++++++ src/config/schema.help.ts | 2 + src/config/schema.labels.ts | 1 + src/config/types.agents-shared.ts | 2 + src/config/zod-schema.agent-defaults.test.ts | 22 +++++++ src/config/zod-schema.agent-model.ts | 1 + src/image-generation/runtime.test.ts | 42 +++++++++++++ src/image-generation/runtime.ts | 6 +- .../media-generation/runtime-module-mocks.ts | 10 +++ 19 files changed, 264 insertions(+), 6 deletions(-) diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256 index 8cd430eeadb..5ea41f5501d 100644 --- a/docs/.generated/config-baseline.sha256 +++ b/docs/.generated/config-baseline.sha256 @@ -1,4 +1,4 @@ -9a012a9c87b9010683289dc7d68ba5446a4b78beedf381e2c5f9d486f25a9213 config-baseline.json -6128d6eff8c28d17194d1ae9ee7f72abae48da1c6476ab16e6378f1898e4373a config-baseline.core.json +439ff58a4a54f0f4bda959239f382cc3b2f94a282680dcd89bd3f8c93e0f07d0 config-baseline.json +6ef86147534d12aa5ac7a9cf208b4627177090c92479a71dfd1791096d20353b config-baseline.core.json 7cd9c908f066c143eab2a201efbc9640f483ab28bba92ddeca1d18cc2b528bc3 config-baseline.channel.json 7825b56a5b3fcdbe2e09ef8fe5d9f12ac3598435afebe20413051e45b0d1968e config-baseline.plugin.json diff --git a/docs/cli/infer.md b/docs/cli/infer.md index bbb3f6a6dea..f5dbd32fdda 100644 --- a/docs/cli/infer.md +++ b/docs/cli/infer.md @@ -156,6 +156,7 @@ Use `image` for generation, edit, and description. ```bash openclaw infer image generate --prompt "friendly lobster illustration" --json openclaw infer image generate --prompt "cinematic product photo of headphones" --json +openclaw infer image generate --prompt "slow image backend" --timeout-ms 180000 --json openclaw infer image describe --file ./photo.jpg --json openclaw infer image describe --file ./ui-screenshot.png --model openai/gpt-4.1-mini --json openclaw infer image describe --file ./photo.jpg --model ollama/qwen2.5vl:7b --json diff --git a/docs/providers/openrouter.md b/docs/providers/openrouter.md index 8b66a950871..8725c62b0f6 100644 --- a/docs/providers/openrouter.md +++ b/docs/providers/openrouter.md @@ -71,13 +71,14 @@ OpenRouter can also back the `image_generate` tool. Use an OpenRouter image mode defaults: { imageGenerationModel: { primary: "openrouter/google/gemini-3.1-flash-image-preview", + timeoutMs: 180_000, }, }, }, } ``` -OpenClaw sends image requests to OpenRouter's chat completions image API with `modalities: ["image", "text"]`. Gemini image models receive supported `aspectRatio` and `resolution` hints through OpenRouter's `image_config`. +OpenClaw sends image requests to OpenRouter's chat completions image API with `modalities: ["image", "text"]`. Gemini image models receive supported `aspectRatio` and `resolution` hints through OpenRouter's `image_config`. Use `agents.defaults.imageGenerationModel.timeoutMs` for slower OpenRouter image models; the `image_generate` tool's per-call `timeoutMs` parameter still wins. ## Text-to-speech diff --git a/docs/tools/image-generation.md b/docs/tools/image-generation.md index ab646b6c618..e4ee385ee68 100644 --- a/docs/tools/image-generation.md +++ b/docs/tools/image-generation.md @@ -24,6 +24,8 @@ The tool only appears when at least one image generation provider is available. defaults: { imageGenerationModel: { primary: "openai/gpt-image-2", + // Optional default provider request timeout for image_generate. + timeoutMs: 180_000, }, }, }, @@ -150,6 +152,7 @@ Tool results report the applied settings. When OpenClaw remaps geometry during p defaults: { imageGenerationModel: { primary: "openai/gpt-image-2", + timeoutMs: 180_000, fallbacks: [ "openrouter/google/gemini-3.1-flash-image-preview", "google/gemini-3.1-flash-image-preview", @@ -185,6 +188,8 @@ Notes: `agents.defaults.mediaGenerationAutoProviderFallback: false` if you want image generation to use only the explicit `model`, `primary`, and `fallbacks` entries. +- Set `agents.defaults.imageGenerationModel.timeoutMs` for slow image backends. + A per-call `timeoutMs` tool parameter overrides the configured default. - Use `action: "list"` to inspect the currently registered providers, their default models, and auth env-var hints. diff --git a/src/agents/tools/image-generate-tool.test.ts b/src/agents/tools/image-generate-tool.test.ts index d03a510e3fd..bc395916b5f 100644 --- a/src/agents/tools/image-generate-tool.test.ts +++ b/src/agents/tools/image-generate-tool.test.ts @@ -478,6 +478,67 @@ describe("createImageGenerateTool", () => { expect(text).toContain("MEDIA:/tmp/generated-2.png"); }); + it("uses configured timeoutMs for image generation and lets calls override it", async () => { + stubImageGenerationProviders(); + const generateImage = vi.spyOn(imageGenerationRuntime, "generateImage").mockResolvedValue({ + provider: "openai", + model: "gpt-image-1", + attempts: [], + ignoredOverrides: [], + images: [ + { + buffer: Buffer.from("png-out"), + mimeType: "image/png", + fileName: "cat.png", + }, + ], + }); + vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue({ + path: "/tmp/generated.png", + id: "generated.png", + size: 7, + contentType: "image/png", + }); + + const tool = requireImageGenerateTool( + createImageGenerateTool({ + config: { + agents: { + defaults: { + imageGenerationModel: { + primary: "openai/gpt-image-1", + timeoutMs: 180_000, + }, + }, + }, + }, + }), + ); + + const defaultResult = await tool.execute("call-timeout-default", { + prompt: "A cat wearing sunglasses", + }); + const overrideResult = await tool.execute("call-timeout-override", { + prompt: "A cat wearing sunglasses", + timeoutMs: 12_345, + }); + + expect(generateImage).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + timeoutMs: 180_000, + }), + ); + expect(generateImage).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + timeoutMs: 12_345, + }), + ); + expect(defaultResult.details).toMatchObject({ timeoutMs: 180_000 }); + expect(overrideResult.details).toMatchObject({ timeoutMs: 12_345 }); + }); + it("forwards output hints and OpenAI provider options", async () => { const generateImage = vi.spyOn(imageGenerationRuntime, "generateImage").mockResolvedValue({ provider: "openai", diff --git a/src/agents/tools/image-generate-tool.ts b/src/agents/tools/image-generate-tool.ts index 7b616814517..2a04fb47961 100644 --- a/src/agents/tools/image-generate-tool.ts +++ b/src/agents/tools/image-generate-tool.ts @@ -638,7 +638,7 @@ export function createImageGenerateTool(options?: { const size = readStringParam(params, "size"); const aspectRatio = normalizeAspectRatio(readStringParam(params, "aspectRatio")); const explicitResolution = normalizeResolution(readStringParam(params, "resolution")); - const timeoutMs = readGenerationTimeoutMs(params); + const timeoutMs = readGenerationTimeoutMs(params) ?? imageGenerationModelConfig.timeoutMs; const quality = normalizeQuality(readStringParam(params, "quality")); const outputFormat = normalizeOutputFormat(readStringParam(params, "outputFormat")); const providerOptions = normalizeProviderOptions(params); diff --git a/src/agents/tools/model-config.helpers.ts b/src/agents/tools/model-config.helpers.ts index a3121e32c43..b6f262860d1 100644 --- a/src/agents/tools/model-config.helpers.ts +++ b/src/agents/tools/model-config.helpers.ts @@ -1,6 +1,7 @@ import { resolveAgentModelFallbackValues, resolveAgentModelPrimaryValue, + resolveAgentModelTimeoutMsValue, } from "../../config/model-input.js"; import type { AgentModelConfig } from "../../config/types.agents-shared.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; @@ -13,7 +14,7 @@ import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js"; import { resolveEnvApiKey } from "../model-auth.js"; import { resolveConfiguredModelRef } from "../model-selection.js"; -export type ToolModelConfig = { primary?: string; fallbacks?: string[] }; +export type ToolModelConfig = { primary?: string; fallbacks?: string[]; timeoutMs?: number }; export function hasToolModelConfig(model: ToolModelConfig | undefined): boolean { return Boolean( @@ -53,9 +54,11 @@ export function hasAuthForProvider(params: { provider: string; agentDir?: string export function coerceToolModelConfig(model?: AgentModelConfig): ToolModelConfig { const primary = resolveAgentModelPrimaryValue(model); const fallbacks = resolveAgentModelFallbackValues(model); + const timeoutMs = resolveAgentModelTimeoutMsValue(model); return { ...(primary?.trim() ? { primary: primary.trim() } : {}), ...(fallbacks.length > 0 ? { fallbacks } : {}), + ...(timeoutMs !== undefined ? { timeoutMs } : {}), }; } @@ -94,5 +97,6 @@ export function buildToolModelConfigFromCandidates(params: { return { primary: deduped[0], ...(deduped.length > 1 ? { fallbacks: deduped.slice(1) } : {}), + ...(params.explicit.timeoutMs !== undefined ? { timeoutMs: params.explicit.timeoutMs } : {}), }; } diff --git a/src/cli/capability-cli.test.ts b/src/cli/capability-cli.test.ts index 6bc9006ff13..4f845283558 100644 --- a/src/cli/capability-cli.test.ts +++ b/src/cli/capability-cli.test.ts @@ -517,6 +517,42 @@ describe("capability cli", () => { ); }); + it("passes image generation timeout through to runtime", async () => { + mocks.generateImage.mockResolvedValue({ + provider: "openai", + model: "gpt-image-1", + attempts: [], + images: [ + { + buffer: Buffer.from("png-bytes"), + mimeType: "image/png", + fileName: "provider-output.png", + }, + ], + }); + + await runRegisteredCli({ + register: registerCapabilityCli as (program: Command) => void, + argv: [ + "capability", + "image", + "generate", + "--prompt", + "friendly lobster", + "--timeout-ms", + "180000", + "--json", + ], + }); + + expect(mocks.generateImage).toHaveBeenCalledWith( + expect.objectContaining({ + prompt: "friendly lobster", + timeoutMs: 180000, + }), + ); + }); + it("streams url-only generated videos to --output paths", async () => { mocks.generateVideo.mockResolvedValue({ provider: "vydra", diff --git a/src/cli/capability-cli.ts b/src/cli/capability-cli.ts index a7088bd4983..ea97cedd9e1 100644 --- a/src/cli/capability-cli.ts +++ b/src/cli/capability-cli.ts @@ -704,6 +704,7 @@ async function runImageGenerate(params: { resolution?: "1K" | "2K" | "4K"; file?: string[]; output?: string; + timeoutMs?: number; }) { const cfg = loadConfig(); const agentDir = resolveAgentDir(cfg, resolveDefaultAgentId(cfg)); @@ -727,6 +728,7 @@ async function runImageGenerate(params: { size: params.size, aspectRatio: params.aspectRatio, resolution: params.resolution, + timeoutMs: params.timeoutMs, inputImages, }); const outputs = await Promise.all( @@ -1436,6 +1438,7 @@ export function registerCapabilityCli(program: Command) { .option("--size ", "Size hint like 1024x1024") .option("--aspect-ratio ", "Aspect ratio hint like 16:9") .option("--resolution ", "Resolution hint: 1K, 2K, or 4K") + .option("--timeout-ms ", "Provider request timeout in milliseconds") .option("--output ", "Output path") .option("--json", "Output JSON", false) .action(async (opts) => { @@ -1448,6 +1451,7 @@ export function registerCapabilityCli(program: Command) { size: opts.size as string | undefined, aspectRatio: opts.aspectRatio as string | undefined, resolution: opts.resolution as "1K" | "2K" | "4K" | undefined, + timeoutMs: parseOptionalFiniteNumber(opts.timeoutMs, "--timeout-ms"), output: opts.output as string | undefined, }); emitJsonOrText(defaultRuntime, Boolean(opts.json), result, formatEnvelopeForText); @@ -1460,6 +1464,7 @@ export function registerCapabilityCli(program: Command) { .requiredOption("--file ", "Input file", collectOption, []) .requiredOption("--prompt ", "Prompt text") .option("--model ", "Model override") + .option("--timeout-ms ", "Provider request timeout in milliseconds") .option("--output ", "Output path") .option("--json", "Output JSON", false) .action(async (opts) => { @@ -1470,6 +1475,7 @@ export function registerCapabilityCli(program: Command) { prompt: String(opts.prompt), model: opts.model as string | undefined, file: files, + timeoutMs: parseOptionalFiniteNumber(opts.timeoutMs, "--timeout-ms"), output: opts.output as string | undefined, }); emitJsonOrText(defaultRuntime, Boolean(opts.json), result, formatEnvelopeForText); diff --git a/src/config/model-input.ts b/src/config/model-input.ts index ffb8ffcf605..9645bfdc1be 100644 --- a/src/config/model-input.ts +++ b/src/config/model-input.ts @@ -4,6 +4,7 @@ import type { AgentModelConfig } from "./types.agents-shared.js"; type AgentModelListLike = { primary?: string; fallbacks?: string[]; + timeoutMs?: number; }; export function resolveAgentModelPrimaryValue(model?: AgentModelConfig): string | undefined { @@ -17,6 +18,17 @@ export function resolveAgentModelFallbackValues(model?: AgentModelConfig): strin return Array.isArray(model.fallbacks) ? model.fallbacks : []; } +export function resolveAgentModelTimeoutMsValue(model?: AgentModelConfig): number | undefined { + if (!model || typeof model !== "object") { + return undefined; + } + return typeof model.timeoutMs === "number" && + Number.isFinite(model.timeoutMs) && + model.timeoutMs > 0 + ? Math.floor(model.timeoutMs) + : undefined; +} + export function toAgentModelListLike(model?: AgentModelConfig): AgentModelListLike | undefined { if (typeof model === "string") { const primary = normalizeOptionalString(model); diff --git a/src/config/schema.base.generated.ts b/src/config/schema.base.generated.ts index cabf999a0da..f3cc7e0e516 100644 --- a/src/config/schema.base.generated.ts +++ b/src/config/schema.base.generated.ts @@ -3199,6 +3199,11 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { description: "Ordered fallback models (provider/model). Used when the primary model fails.", }, + timeoutMs: { + type: "integer", + exclusiveMinimum: 0, + maximum: 9007199254740991, + }, }, additionalProperties: false, }, @@ -3226,6 +3231,11 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { title: "Image Model Fallbacks", description: "Ordered fallback image models (provider/model).", }, + timeoutMs: { + type: "integer", + exclusiveMinimum: 0, + maximum: 9007199254740991, + }, }, additionalProperties: false, }, @@ -3253,6 +3263,14 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { title: "Image Generation Model Fallbacks", description: "Ordered fallback image-generation models (provider/model).", }, + timeoutMs: { + type: "integer", + exclusiveMinimum: 0, + maximum: 9007199254740991, + title: "Image Generation Timeout (ms)", + description: + "Default provider request timeout in milliseconds for image_generate calls. Per-call timeoutMs overrides this.", + }, }, additionalProperties: false, }, @@ -3280,6 +3298,11 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { title: "Video Generation Model Fallbacks", description: "Ordered fallback video-generation models (provider/model).", }, + timeoutMs: { + type: "integer", + exclusiveMinimum: 0, + maximum: 9007199254740991, + }, }, additionalProperties: false, }, @@ -3307,6 +3330,11 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { title: "Music Generation Model Fallbacks", description: "Ordered fallback music-generation models (provider/model).", }, + timeoutMs: { + type: "integer", + exclusiveMinimum: 0, + maximum: 9007199254740991, + }, }, additionalProperties: false, }, @@ -3340,6 +3368,11 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { title: "PDF Model Fallbacks", description: "Ordered fallback PDF models (provider/model).", }, + timeoutMs: { + type: "integer", + exclusiveMinimum: 0, + maximum: 9007199254740991, + }, }, additionalProperties: false, }, @@ -5333,6 +5366,11 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { type: "string", }, }, + timeoutMs: { + type: "integer", + exclusiveMinimum: 0, + maximum: 9007199254740991, + }, }, additionalProperties: false, }, @@ -5956,6 +5994,11 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { type: "string", }, }, + timeoutMs: { + type: "integer", + exclusiveMinimum: 0, + maximum: 9007199254740991, + }, }, additionalProperties: false, }, @@ -26089,6 +26132,11 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { help: "Ordered fallback image-generation models (provider/model).", tags: ["reliability", "media"], }, + "agents.defaults.imageGenerationModel.timeoutMs": { + label: "Image Generation Timeout (ms)", + help: "Default provider request timeout in milliseconds for image_generate calls. Per-call timeoutMs overrides this.", + tags: ["performance", "media"], + }, "agents.defaults.videoGenerationModel.primary": { label: "Video Generation Model", help: "Optional video-generation model (provider/model) used by the shared video generation capability.", diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index 23afe539e55..48df41d1875 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -1209,6 +1209,8 @@ export const FIELD_HELP: Record = { "Optional image-generation model (provider/model) used by the shared image generation capability.", "agents.defaults.imageGenerationModel.fallbacks": "Ordered fallback image-generation models (provider/model).", + "agents.defaults.imageGenerationModel.timeoutMs": + "Default provider request timeout in milliseconds for image_generate calls. Per-call timeoutMs overrides this.", "agents.defaults.videoGenerationModel.primary": "Optional video-generation model (provider/model) used by the shared video generation capability.", "agents.defaults.videoGenerationModel.fallbacks": diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index 4b77148e99f..dd57a710349 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -549,6 +549,7 @@ export const FIELD_LABELS: Record = { "agents.defaults.imageModel.fallbacks": "Image Model Fallbacks", "agents.defaults.imageGenerationModel.primary": "Image Generation Model", "agents.defaults.imageGenerationModel.fallbacks": "Image Generation Model Fallbacks", + "agents.defaults.imageGenerationModel.timeoutMs": "Image Generation Timeout (ms)", "agents.defaults.videoGenerationModel.primary": "Video Generation Model", "agents.defaults.videoGenerationModel.fallbacks": "Video Generation Model Fallbacks", "agents.defaults.musicGenerationModel.primary": "Music Generation Model", diff --git a/src/config/types.agents-shared.ts b/src/config/types.agents-shared.ts index cd692a139e1..0e5572c4325 100644 --- a/src/config/types.agents-shared.ts +++ b/src/config/types.agents-shared.ts @@ -12,6 +12,8 @@ export type AgentModelConfig = primary?: string; /** Per-agent model fallbacks (provider/model). */ fallbacks?: string[]; + /** Optional provider request timeout in milliseconds for capabilities that support it. */ + timeoutMs?: number; }; export type AgentEmbeddedHarnessConfig = { diff --git a/src/config/zod-schema.agent-defaults.test.ts b/src/config/zod-schema.agent-defaults.test.ts index 7e4d2dc3dab..f49eff633f4 100644 --- a/src/config/zod-schema.agent-defaults.test.ts +++ b/src/config/zod-schema.agent-defaults.test.ts @@ -25,6 +25,28 @@ describe("agent defaults schema", () => { ).not.toThrow(); }); + it("accepts imageGenerationModel timeoutMs", () => { + const defaults = AgentDefaultsSchema.parse({ + imageGenerationModel: { + primary: "openrouter/openai/gpt-5.4-image-2", + timeoutMs: 180_000, + }, + })!; + + expect(defaults.imageGenerationModel).toEqual({ + primary: "openrouter/openai/gpt-5.4-image-2", + timeoutMs: 180_000, + }); + expect(() => + AgentDefaultsSchema.parse({ + imageGenerationModel: { + primary: "openrouter/openai/gpt-5.4-image-2", + timeoutMs: 0, + }, + }), + ).toThrow(); + }); + it("accepts mediaGenerationAutoProviderFallback", () => { expect(() => AgentDefaultsSchema.parse({ diff --git a/src/config/zod-schema.agent-model.ts b/src/config/zod-schema.agent-model.ts index 3a6bac05c24..3902d6f18bd 100644 --- a/src/config/zod-schema.agent-model.ts +++ b/src/config/zod-schema.agent-model.ts @@ -6,6 +6,7 @@ export const AgentModelSchema = z.union([ .object({ primary: z.string().optional(), fallbacks: z.array(z.string()).optional(), + timeoutMs: z.number().int().positive().optional(), }) .strict(), ]); diff --git a/src/image-generation/runtime.test.ts b/src/image-generation/runtime.test.ts index e6eb1e98f83..f3706e8bb01 100644 --- a/src/image-generation/runtime.test.ts +++ b/src/image-generation/runtime.test.ts @@ -80,6 +80,48 @@ describe("image-generation runtime", () => { expect(result.ignoredOverrides).toEqual([]); }); + it("uses configured image-generation timeout when the call omits timeoutMs", async () => { + let seenTimeoutMs: number | undefined; + mocks.resolveAgentModelPrimaryValue.mockReturnValue("image-plugin/img-v1"); + const provider: ImageGenerationProvider = { + id: "image-plugin", + capabilities: { + generate: {}, + edit: { enabled: false }, + }, + async generateImage(req: { timeoutMs?: number }) { + seenTimeoutMs = req.timeoutMs; + return { + images: [ + { + buffer: Buffer.from("png-bytes"), + mimeType: "image/png", + fileName: "sample.png", + }, + ], + model: "img-v1", + }; + }, + }; + mocks.getImageGenerationProvider.mockReturnValue(provider); + + await generateImage({ + cfg: { + agents: { + defaults: { + imageGenerationModel: { + primary: "image-plugin/img-v1", + timeoutMs: 180_000, + }, + }, + }, + } as OpenClawConfig, + prompt: "draw a cat", + }); + + expect(seenTimeoutMs).toBe(180_000); + }); + it("auto-detects and falls through to another configured image-generation provider by default", async () => { mocks.getImageGenerationProvider.mockImplementation((providerId: string) => { if (providerId === "openai") { diff --git a/src/image-generation/runtime.ts b/src/image-generation/runtime.ts index 1ac50d4f008..0264eac45e8 100644 --- a/src/image-generation/runtime.ts +++ b/src/image-generation/runtime.ts @@ -1,5 +1,6 @@ import { describeFailoverError, isFailoverError } from "../agents/failover-error.js"; import type { FallbackAttempt } from "../agents/model-fallback.types.js"; +import { resolveAgentModelTimeoutMsValue } from "../config/model-input.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { formatErrorMessage } from "../infra/errors.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; @@ -34,6 +35,9 @@ export function listRuntimeImageGenerationProviders(params?: { config?: OpenClaw export async function generateImage( params: GenerateImageParams, ): Promise { + const timeoutMs = + params.timeoutMs ?? + resolveAgentModelTimeoutMsValue(params.cfg.agents?.defaults?.imageGenerationModel); const candidates = resolveCapabilityModelCandidates({ cfg: params.cfg, modelConfig: params.cfg.agents?.defaults?.imageGenerationModel, @@ -89,7 +93,7 @@ export async function generateImage( quality: sanitized.quality, outputFormat: sanitized.outputFormat, inputImages: params.inputImages, - ...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs } : {}), + ...(timeoutMs !== undefined ? { timeoutMs } : {}), providerOptions: params.providerOptions, }); if (!Array.isArray(result.images) || result.images.length === 0) { diff --git a/test/helpers/media-generation/runtime-module-mocks.ts b/test/helpers/media-generation/runtime-module-mocks.ts index 218cbf453eb..46bbdb81888 100644 --- a/test/helpers/media-generation/runtime-module-mocks.ts +++ b/test/helpers/media-generation/runtime-module-mocks.ts @@ -58,6 +58,15 @@ const mediaRuntimeMocks = vi.hoisted(() => { resolveEnvApiKey: vi.fn(() => undefined), resolveAgentModelFallbackValues: vi.fn<(value: unknown) => string[]>(() => []), resolveAgentModelPrimaryValue: vi.fn<(value: unknown) => string | undefined>(() => undefined), + resolveAgentModelTimeoutMsValue: vi.fn<(value: unknown) => number | undefined>((value) => { + if (!value || typeof value !== "object" || !("timeoutMs" in value)) { + return undefined; + } + const timeoutMs = (value as { timeoutMs?: unknown }).timeoutMs; + return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 + ? Math.floor(timeoutMs) + : undefined; + }), resolveProviderAuthEnvVarCandidates: vi.fn(() => ({})), debug, warn, @@ -81,6 +90,7 @@ vi.mock("../../../src/agents/model-auth-env.js", () => ({ vi.mock("../../../src/config/model-input.js", () => ({ resolveAgentModelFallbackValues: mediaRuntimeMocks.resolveAgentModelFallbackValues, resolveAgentModelPrimaryValue: mediaRuntimeMocks.resolveAgentModelPrimaryValue, + resolveAgentModelTimeoutMsValue: mediaRuntimeMocks.resolveAgentModelTimeoutMsValue, })); vi.mock("../../../src/logging/subsystem.js", () => ({ createSubsystemLogger: mediaRuntimeMocks.createSubsystemLogger,