Files
openclaw/src/image-generation/runtime.ts
Val Alexander 05c9492bff fix: reduce WebUI session latency churn (#76277) thanks @BunsDev
Reduce WebUI/Gateway latency churn by avoiding redundant session reloads, carrying session keys through transcript update events, and deferring explicit media provider discovery. Includes changelog attribution and closes the referenced runtime latency issues.
2026-05-02 18:39:06 -05:00

165 lines
5.9 KiB
TypeScript

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";
import {
buildMediaGenerationNormalizationMetadata,
buildNoCapabilityModelConfiguredMessage,
resolveCapabilityModelCandidates,
throwCapabilityGenerationFailure,
} from "../media-generation/runtime-shared.js";
import { getProviderEnvVars } from "../secrets/provider-env-vars.js";
import { parseImageGenerationModelRef } from "./model-ref.js";
import { resolveImageGenerationOverrides } from "./normalization.js";
import { getImageGenerationProvider, listImageGenerationProviders } from "./provider-registry.js";
import type { GenerateImageParams, GenerateImageRuntimeResult } from "./runtime-types.js";
import type { ImageGenerationResult } from "./types.js";
const log = createSubsystemLogger("image-generation");
export type ImageGenerationRuntimeDeps = {
getProvider?: typeof getImageGenerationProvider;
listProviders?: typeof listImageGenerationProviders;
getProviderEnvVars?: typeof getProviderEnvVars;
log?: Pick<typeof log, "warn">;
};
export type { GenerateImageParams, GenerateImageRuntimeResult } from "./runtime-types.js";
function buildNoImageGenerationModelConfiguredMessage(
cfg: OpenClawConfig,
deps: ImageGenerationRuntimeDeps,
): string {
const listProviders = deps.listProviders ?? listImageGenerationProviders;
return buildNoCapabilityModelConfiguredMessage({
capabilityLabel: "image-generation",
modelConfigKey: "imageGenerationModel",
providers: listProviders(cfg),
getProviderEnvVars: deps.getProviderEnvVars,
});
}
export function listRuntimeImageGenerationProviders(
params?: { config?: OpenClawConfig },
deps: ImageGenerationRuntimeDeps = {},
) {
return (deps.listProviders ?? listImageGenerationProviders)(params?.config);
}
export async function generateImage(
params: GenerateImageParams,
deps: ImageGenerationRuntimeDeps = {},
): Promise<GenerateImageRuntimeResult> {
const getProvider = deps.getProvider ?? getImageGenerationProvider;
const listProviders = deps.listProviders ?? listImageGenerationProviders;
const logger = deps.log ?? log;
const timeoutMs =
params.timeoutMs ??
resolveAgentModelTimeoutMsValue(params.cfg.agents?.defaults?.imageGenerationModel);
const candidates = resolveCapabilityModelCandidates({
cfg: params.cfg,
modelConfig: params.cfg.agents?.defaults?.imageGenerationModel,
modelOverride: params.modelOverride,
parseModelRef: parseImageGenerationModelRef,
agentDir: params.agentDir,
listProviders,
autoProviderFallback: params.autoProviderFallback,
});
if (candidates.length === 0) {
throw new Error(buildNoImageGenerationModelConfiguredMessage(params.cfg, deps));
}
const attempts: FallbackAttempt[] = [];
let lastError: unknown;
for (const candidate of candidates) {
const provider = getProvider(candidate.provider, params.cfg);
if (!provider) {
const error = `No image-generation provider registered for ${candidate.provider}`;
attempts.push({
provider: candidate.provider,
model: candidate.model,
error,
});
lastError = new Error(error);
logger.warn(
`image-generation candidate failed: ${candidate.provider}/${candidate.model}: ${error}`,
);
continue;
}
try {
const sanitized = resolveImageGenerationOverrides({
provider,
size: params.size,
aspectRatio: params.aspectRatio,
resolution: params.resolution,
quality: params.quality,
outputFormat: params.outputFormat,
background: params.background,
inputImages: params.inputImages,
});
const result: ImageGenerationResult = await provider.generateImage({
provider: candidate.provider,
model: candidate.model,
prompt: params.prompt,
cfg: params.cfg,
agentDir: params.agentDir,
authStore: params.authStore,
count: params.count,
size: sanitized.size,
aspectRatio: sanitized.aspectRatio,
resolution: sanitized.resolution,
quality: sanitized.quality,
outputFormat: sanitized.outputFormat,
background: sanitized.background,
inputImages: params.inputImages,
...(timeoutMs !== undefined ? { timeoutMs } : {}),
providerOptions: params.providerOptions,
});
if (!Array.isArray(result.images) || result.images.length === 0) {
throw new Error("Image generation provider returned no images.");
}
return {
images: result.images,
provider: candidate.provider,
model: result.model ?? candidate.model,
attempts,
normalization: sanitized.normalization,
metadata: {
...result.metadata,
...buildMediaGenerationNormalizationMetadata({
normalization: sanitized.normalization,
requestedSizeForDerivedAspectRatio: params.size,
}),
},
ignoredOverrides: sanitized.ignoredOverrides,
};
} catch (err) {
lastError = err;
const described = isFailoverError(err) ? describeFailoverError(err) : undefined;
attempts.push({
provider: candidate.provider,
model: candidate.model,
error: described?.message ?? formatErrorMessage(err),
reason: described?.reason,
status: described?.status,
code: described?.code,
});
logger.warn(
`image-generation candidate failed: ${candidate.provider}/${candidate.model}: ${
described?.message ?? formatErrorMessage(err)
}`,
);
}
}
return throwCapabilityGenerationFailure({
capabilityLabel: "image generation",
attempts,
lastError,
});
}