openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
316 lines (315 loc) • 11.7 kB
JavaScript
import { u as resolveAgentDir, v as resolveDefaultAgentDir } from "./agent-scope-config-DcbEhP0R.js";
import "./agent-scope-DbtJyKUL.js";
import { t as hasHttpUrlPrefix } from "./url-protocol-OU3K-ySz.js";
import { l as kindFromMime, u as mimeTypeFromFilePath } from "./mime-CVpcq9ju.js";
import { n as normalizeMediaProviderId } from "./provider-id-DSbuCFIb.js";
import { n as DEFAULT_MAX_BYTES } from "./defaults.constants-Cof2g8lZ.js";
import { t as describeImageWithModel } from "./image-runtime-BOIoCq-0.js";
import { n as buildMediaUnderstandingRegistry, r as getMediaUnderstandingProvider } from "./pdf-extract-D4eXXKMg.js";
import { a as resolveModelEntries, i as resolveMediaRuntimeTimeoutMs, n as resolveMaxBytes } from "./resolve-DyQF7rKc.js";
import { c as normalizeImageDescriptionInput, i as normalizeDecisionReason, n as findDecisionReason } from "./runner.entries-B6FlBFiO.js";
import { a as createMediaAttachmentCache, i as runCapability, o as normalizeMediaAttachments, t as buildProviderRegistry } from "./runner-3MXvZszK.js";
import path from "node:path";
//#region src/media-understanding/runtime.ts
const KIND_BY_CAPABILITY = {
audio: "audio.transcription",
image: "image.description",
video: "video.description"
};
function resolveDecisionFailureReason(decision) {
return normalizeDecisionReason(findDecisionReason(decision, "failed"));
}
function buildFileContext(params) {
const scopeFields = {
...params.scopeContext?.sessionKey ? { SessionKey: params.scopeContext.sessionKey } : {},
...params.scopeContext?.channel ? {
Provider: params.scopeContext.channel,
Surface: params.scopeContext.channel
} : {},
...params.scopeContext?.chatType ? { ChatType: params.scopeContext.chatType } : {}
};
const remoteRef = params.mediaUrl ?? (isRemoteMediaReference(params.filePath) ? params.filePath.trim() : void 0);
const extensionMime = remoteRef ? mimeTypeFromFilePath(remoteRef) : void 0;
const extensionKind = kindFromMime(extensionMime);
const mediaType = params.mime ?? (remoteRef && params.capability && extensionKind === params.capability ? `${params.capability}/*` : extensionMime) ?? (remoteRef && params.capability ? `${params.capability}/*` : void 0);
if (remoteRef) return {
media: [{
url: remoteRef,
contentType: mediaType
}],
...scopeFields
};
return {
media: [{
path: params.filePath,
contentType: mediaType
}],
...scopeFields
};
}
function isRemoteMediaReference(value) {
return hasHttpUrlPrefix(value.trim());
}
function concreteMime(mime) {
const normalized = mime?.trim();
if (!normalized || normalized.endsWith("/*")) return;
return normalized;
}
function resolveFileLocalRoots(filePath) {
return isRemoteMediaReference(filePath) ? void 0 : [path.dirname(filePath)];
}
function basenameFromMediaReference(value) {
if (isRemoteMediaReference(value)) try {
const url = new URL(value);
return path.basename(url.pathname) || "image";
} catch {}
return path.basename(value);
}
function hasStructuredImageInput(input) {
return input.some((entry) => entry.type === "image");
}
/** Runs media understanding for one local file or remote URL and returns the first matching output. */
async function runMediaUnderstandingFile(params) {
const requestPrompt = params.prompt?.trim();
const requestTimeoutSeconds = typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs) && params.timeoutMs > 0 ? Math.ceil(params.timeoutMs / 1e3) : void 0;
const cfg = requestPrompt || requestTimeoutSeconds !== void 0 ? {
...params.cfg,
tools: {
...params.cfg.tools,
media: {
...params.cfg.tools?.media,
[params.capability]: {
...params.cfg.tools?.media?.[params.capability],
...requestPrompt ? {
prompt: requestPrompt,
_requestPromptOverride: requestPrompt
} : {},
...requestTimeoutSeconds !== void 0 ? { timeoutSeconds: requestTimeoutSeconds } : {}
}
}
}
} : params.cfg;
const ctx = buildFileContext({
...params,
capability: params.capability,
scopeContext: params.scopeContext
});
const attachments = normalizeMediaAttachments(ctx);
const decisionBase = {
capability: params.capability,
attachments: [],
...params.capability === "image" ? { nativeVisionActive: false } : {}
};
if (attachments.length === 0) return {
text: void 0,
decision: {
...decisionBase,
outcome: "no-attachment",
attachmentDispositions: {}
}
};
const config = cfg.tools?.media?.[params.capability];
if (config?.enabled === false) return {
text: void 0,
provider: void 0,
model: void 0,
output: void 0,
decision: {
...decisionBase,
outcome: "disabled",
attachmentDispositions: Object.fromEntries(attachments.map((attachment) => [attachment.index, { kind: "capability-disabled" }]))
}
};
const providerRegistry = buildProviderRegistry(void 0, cfg);
const agentDir = params.agentDir ?? (params.agentId ? resolveAgentDir(cfg, params.agentId) : void 0);
const cache = createMediaAttachmentCache(attachments, {
localPathRoots: params.mediaUrl ? void 0 : resolveFileLocalRoots(params.filePath),
ssrfPolicy: cfg.tools?.web?.fetch?.ssrfPolicy
});
try {
const result = await runCapability({
capability: params.capability,
cfg,
ctx,
attachments: cache,
media: attachments,
...params.agentId ? { agentId: params.agentId } : {},
...agentDir ? { agentDir } : {},
...params.workspaceDir ? { workspaceDir: params.workspaceDir } : {},
providerRegistry,
config,
activeModel: params.activeModel
});
if (result.outputs.length === 0 && result.decision.outcome === "failed") throw new Error(resolveDecisionFailureReason(result.decision) ?? `${params.capability} understanding failed`);
const output = result.outputs.find((entry) => entry.kind === KIND_BY_CAPABILITY[params.capability]);
const fileResult = {
text: output?.text?.trim() || void 0,
provider: output?.provider,
model: output?.model,
output
};
if (result.decision) fileResult.decision = result.decision;
return fileResult;
} finally {
await cache.cleanup();
}
}
/** Describes one image file or URL through the configured image-understanding pipeline. */
async function describeImageFile(params) {
return await runMediaUnderstandingFile({
...params,
capability: "image"
});
}
/** Reads and normalizes image input once before explicit-model fallback attempts. */
async function prepareImageDescriptionInput(params) {
const timeoutMs = resolveMediaRuntimeTimeoutMs(params.timeoutMs);
const image = await readImageDescriptionInput({
filePath: params.filePath,
mediaUrl: params.mediaUrl,
mime: params.mime,
cfg: params.cfg,
timeoutMs
});
const normalizedImage = await normalizeImageDescriptionInput({
buffer: image.buffer,
fileName: image.fileName,
mime: image.mime,
maxBytes: DEFAULT_MAX_BYTES.image
});
return {
buffer: normalizedImage.buffer,
fileName: image.fileName,
mime: normalizedImage.mime
};
}
/** Describes a prepared image with an explicit provider/model. */
async function describePreparedImageWithModel(params) {
const timeoutMs = resolveMediaRuntimeTimeoutMs(params.timeoutMs);
const describeImage = buildProviderRegistry(void 0, params.cfg).get(normalizeMediaProviderId(params.provider))?.describeImage ?? describeImageWithModel;
const agentDir = params.agentDir ?? (params.agentId ? resolveAgentDir(params.cfg, params.agentId) : resolveDefaultAgentDir(params.cfg));
return await describeImage({
buffer: params.image.buffer,
fileName: params.image.fileName,
mime: params.image.mime,
provider: params.provider,
model: params.model,
prompt: params.prompt,
maxTokens: params.maxTokens,
timeoutMs,
cfg: params.cfg,
...params.agentId ? { agentId: params.agentId } : {},
agentDir,
...params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}
});
}
/** Describes one image with an explicit provider/model, bypassing configured media model selection. */
async function describeImageFileWithModel(params) {
const image = await prepareImageDescriptionInput(params);
return await describePreparedImageWithModel({
...params,
image
});
}
async function readImageDescriptionInput(params) {
const attachments = normalizeMediaAttachments(buildFileContext({
...params,
capability: "image"
}));
const cache = createMediaAttachmentCache(attachments, {
localPathRoots: params.mediaUrl ? void 0 : resolveFileLocalRoots(params.filePath),
ssrfPolicy: params.cfg.tools?.web?.fetch?.ssrfPolicy
});
try {
const media = await cache.getBuffer({
attachmentIndex: 0,
maxBytes: DEFAULT_MAX_BYTES.image,
timeoutMs: params.timeoutMs
});
return {
buffer: media.buffer,
fileName: media.fileName || basenameFromMediaReference(params.mediaUrl ?? params.filePath),
mime: media.mime ?? concreteMime(params.mime)
};
} finally {
await cache.cleanup();
}
}
/** Runs provider-backed structured extraction for multimodal text/image input. */
async function extractStructuredWithModel(params) {
const timeoutMs = resolveMediaRuntimeTimeoutMs(params.timeoutMs);
if (!hasStructuredImageInput(params.input)) throw new Error("Structured extraction requires at least one image input.");
const provider = getMediaUnderstandingProvider(params.provider, buildMediaUnderstandingRegistry(void 0, params.cfg));
if (!provider?.extractStructured) throw new Error(`Provider does not support structured extraction: ${params.provider}`);
return await provider.extractStructured({
input: params.input,
instructions: params.instructions,
schemaName: params.schemaName,
jsonSchema: params.jsonSchema,
jsonMode: params.jsonMode,
provider: params.provider,
model: params.model,
profile: params.profile,
preferredProfile: params.preferredProfile,
authStore: params.authStore,
timeoutMs,
cfg: params.cfg,
agentDir: params.agentDir ?? ""
});
}
/** Describes one video file or URL through the configured video-understanding pipeline. */
async function describeVideoFile(params) {
return await runMediaUnderstandingFile({
...params,
capability: "video"
});
}
/** Prepares the largest input that any configured transcription fallback can accept. */
async function resolveAudioInputBudget(params) {
const { cfg } = params;
const config = cfg.tools?.media?.audio;
if (config?.enabled === false) return { enabled: false };
const capability = "audio";
const entries = resolveModelEntries({
cfg,
capability,
config,
providerRegistry: buildProviderRegistry(void 0, cfg)
}).map(({ entry }) => entry);
const candidates = entries.length > 0 ? entries : [{}];
return {
enabled: true,
maxBytes: Math.max(...candidates.map((entry) => resolveMaxBytes({
cfg,
capability,
config,
entry
})))
};
}
/** Transcribes one audio file or URL through the configured audio-understanding pipeline. */
async function transcribeAudioFile(params) {
const cfg = params.language || params.prompt ? {
...params.cfg,
tools: {
...params.cfg.tools,
media: {
...params.cfg.tools?.media,
audio: {
...params.cfg.tools?.media?.audio,
...params.language ? { _requestLanguageOverride: params.language } : {},
...params.prompt ? { _requestPromptOverride: params.prompt } : {},
...params.language ? { language: params.language } : {},
...params.prompt ? { prompt: params.prompt } : {}
}
}
}
} : params.cfg;
return await runMediaUnderstandingFile({
...params,
cfg,
capability: "audio"
});
}
//#endregion
export { extractStructuredWithModel as a, runMediaUnderstandingFile as c, describeVideoFile as i, transcribeAudioFile as l, describeImageFileWithModel as n, prepareImageDescriptionInput as o, describePreparedImageWithModel as r, resolveAudioInputBudget as s, describeImageFile as t };