Files
openclaw/src/image-generation/runtime.ts

166 lines
6.0 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,
ssrfPolicy: params.ssrfPolicy,
});
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,
});
}