openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
386 lines (385 loc) • 15.4 kB
JavaScript
import { c as isSecretRef } from "./types.secrets-_0JOMGE5.js";
import { i as COPILOT_INTEGRATION_ID, s as buildCopilotIdeHeaders } from "./copilot-dynamic-headers-CF9zPORX.js";
import { r as resolveProviderRequestCapabilities } from "./provider-attribution-GEHCfopl.js";
import { a as normalizeModelRef } from "./model-selection-normalize-roKDxQ9_.js";
import { r as ensureOpenClawModelsJson } from "./models-config-SUxEzcTF.js";
import "./model-selection-CA_65iXi.js";
import { o as requireApiKey } from "./model-auth-runtime-shared-ZF0jyHxm.js";
import { t as complete } from "./stream-4H1GSjKe.js";
import { a as getApiKeyForModel, d as resolveApiKeyForProvider } from "./model-auth-DVfmdW0b.js";
import { r as minimaxUnderstandImage, t as isMinimaxVlmModel } from "./minimax-vlm-B_N5SJb0.js";
import { i as hasImageReasoningOnlyResponse, t as coerceImageAssistantText } from "./image-tool.helpers-4xqkDHbG.js";
import { n as normalizeMediaProviderId } from "./provider-id-DSbuCFIb.js";
import { n as resolveModelAsync } from "./model-Cp9whgWq.js";
import { t as registerProviderStreamForModel } from "./provider-stream-FsgXx-aW.js";
import { o as resolveCopilotApiToken } from "./provider-auth-DAOC_qI9.js";
//#region src/media-understanding/image.ts
function resolveImageToolMaxTokens(modelMaxTokens, requestedMaxTokens = 4096) {
if (typeof modelMaxTokens !== "number" || !Number.isFinite(modelMaxTokens) || modelMaxTokens <= 0) return requestedMaxTokens;
return Math.min(requestedMaxTokens, modelMaxTokens);
}
function isRecord(value) {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function isNativeResponsesReasoningPayload(model) {
if (model.api !== "openai-responses" && model.api !== "azure-openai-responses" && model.api !== "openai-chatgpt-responses") return false;
return resolveProviderRequestCapabilities({
provider: model.provider,
api: model.api,
baseUrl: model.baseUrl,
capability: "image",
transport: "media-understanding"
}).usesKnownNativeOpenAIRoute;
}
function formatModelInputCapabilities(input) {
return input && input.length > 0 ? input.join(", ") : "none";
}
function removeReasoningInclude(value) {
if (!Array.isArray(value)) return value;
const next = value.filter((entry) => entry !== "reasoning.encrypted_content");
return next.length > 0 ? next : void 0;
}
function disableReasoningForImageRetryPayload(payload, model) {
if (!isRecord(payload)) return;
const next = { ...payload };
delete next.reasoning;
delete next.reasoning_effort;
const include = removeReasoningInclude(next.include);
if (include === void 0) delete next.include;
else next.include = include;
if (isNativeResponsesReasoningPayload(model)) next.reasoning = { effort: "none" };
return next;
}
function isImageModelNoTextError(err) {
return err instanceof Error && /^Image model returned no text\b/.test(err.message);
}
function isPromiseLike(value) {
return Boolean(value) && typeof value.then === "function";
}
function composeImageDescriptionPayloadHandlers(first, second) {
if (!first) return second;
if (!second) return first;
return (payload, payloadModel) => {
const runSecond = (firstResult) => {
const secondResult = second(firstResult === void 0 ? payload : firstResult, payloadModel);
const coerceResult = (resolvedSecond) => resolvedSecond === void 0 ? firstResult : resolvedSecond;
return isPromiseLike(secondResult) ? Promise.resolve(secondResult).then(coerceResult) : coerceResult(secondResult);
};
const firstResult = first(payload, payloadModel);
if (isPromiseLike(firstResult)) return Promise.resolve(firstResult).then(runSecond);
return runSecond(firstResult);
};
}
async function resolveImageRuntime(params) {
const resolvedRef = normalizeModelRef(params.provider, params.model);
if ((await resolveModelAsync(resolvedRef.provider, resolvedRef.model, params.agentDir, params.cfg, {
allowBundledStaticCatalogFallback: true,
skipAgentDiscovery: true,
skipProviderRuntimeHooks: true,
...params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}
})).model?.input?.includes("image")) {
const normalizedResolved = await resolveModelAsync(resolvedRef.provider, resolvedRef.model, params.agentDir, params.cfg, {
allowBundledStaticCatalogFallback: true,
skipAgentDiscovery: true,
...params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}
});
if (normalizedResolved.model?.input?.includes("image")) return await prepareResolvedImageRuntime(params, normalizedResolved.model, normalizedResolved.authStorage);
}
const modelsOptions = params.workspaceDir ? { workspaceDir: params.workspaceDir } : void 0;
await ensureOpenClawModelsJson(params.cfg, params.agentDir, modelsOptions);
const resolved = await resolveModelAsync(resolvedRef.provider, resolvedRef.model, params.agentDir, params.cfg, {
allowBundledStaticCatalogFallback: true,
...params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}
});
const { authStorage } = resolved;
const { model } = resolved;
if (!model) throw new Error(`Unknown model: ${resolvedRef.provider}/${resolvedRef.model}`);
if (!model.input?.includes("image")) {
if (isMinimaxVlmModel(resolvedRef.provider, resolvedRef.model)) throw new Error(`Unknown model: ${resolvedRef.provider}/${resolvedRef.model}`);
throw new Error(`Model does not support images: ${params.provider}/${params.model} (resolved ${model.provider}/${model.id} input: ${formatModelInputCapabilities(model.input)})`);
}
return await prepareResolvedImageRuntime(params, model, authStorage);
}
async function prepareResolvedImageRuntime(params, resolvedModel, authStorage) {
let model = resolvedModel;
let apiKey = requireApiKey(await getApiKeyForModel({
model,
cfg: params.cfg,
agentDir: params.agentDir,
...params.workspaceDir ? { workspaceDir: params.workspaceDir } : {},
profileId: params.profile,
preferredProfile: params.preferredProfile,
store: params.authStore
}), model.provider);
if (model.provider === "github-copilot") {
const copilotToken = await resolveCopilotApiToken({ githubToken: apiKey });
apiKey = copilotToken.token;
const runtimeBaseUrl = copilotToken.baseUrl?.trim();
if (runtimeBaseUrl) model = {
...model,
baseUrl: runtimeBaseUrl
};
}
authStorage.setRuntimeApiKey(model.provider, apiKey);
return {
apiKey,
model
};
}
function buildImageContext(prompt, images, opts) {
const imageContent = images.map((image) => ({
type: "image",
data: image.buffer.toString("base64"),
mimeType: image.mime ?? "image/jpeg"
}));
const content = opts?.promptInUserContent ? [{
type: "text",
text: prompt
}, ...imageContent] : imageContent;
return {
...opts?.promptInUserContent ? {} : { systemPrompt: prompt },
messages: [{
role: "user",
content,
timestamp: Date.now()
}]
};
}
function shouldPlaceImagePromptInUserContent(model) {
if (model.provider === "github-copilot") return true;
const capabilities = resolveProviderRequestCapabilities({
provider: model.provider,
api: model.api,
baseUrl: model.baseUrl,
capability: "image",
transport: "media-understanding"
});
return capabilities.endpointClass === "openrouter" || model.provider.toLowerCase() === "openrouter" && capabilities.endpointClass === "default";
}
function buildImageRequestHeaders(model) {
if (model.provider !== "github-copilot") return;
return {
...buildCopilotIdeHeaders(),
"Copilot-Integration-Id": COPILOT_INTEGRATION_ID,
"Openai-Organization": "github-copilot",
"x-initiator": "user",
"Copilot-Vision-Request": "true"
};
}
async function describeImagesWithMinimax(params) {
const responses = [];
for (const [index, image] of params.images.entries()) {
const prompt = params.images.length > 1 ? `${params.prompt}\n\nDescribe image ${index + 1} of ${params.images.length} independently.` : params.prompt;
const text = await minimaxUnderstandImage({
apiKey: params.apiKey,
provider: params.provider,
prompt,
imageDataUrl: `data:${image.mime ?? "image/jpeg"};base64,${image.buffer.toString("base64")}`,
modelBaseUrl: params.modelBaseUrl,
timeoutMs: params.timeoutMs
});
responses.push(params.images.length > 1 ? `Image ${index + 1}:\n${text.trim()}` : text.trim());
}
return {
text: responses.join("\n\n").trim(),
model: params.modelId
};
}
function isUnknownModelError(err) {
return err instanceof Error && /^Unknown model:/i.test(err.message);
}
function resolveConfiguredProviderBaseUrl(cfg, provider) {
const direct = cfg.models?.providers?.[provider];
if (typeof direct?.baseUrl === "string" && direct.baseUrl.trim()) return direct.baseUrl.trim();
const normalizedProvider = normalizeMediaProviderId(provider);
const normalized = cfg.models?.providers?.[normalizedProvider];
if (typeof normalized?.baseUrl === "string" && normalized.baseUrl.trim()) {
if (isMinimaxCnAlias(provider) && !isMinimaxCnBaseUrl(normalized.baseUrl)) return;
return normalized.baseUrl.trim();
}
}
function isMinimaxCnAlias(provider) {
const normalized = provider.trim().toLowerCase();
return normalized === "minimax-cn" || normalized === "minimax-portal-cn";
}
function isMinimaxCnBaseUrl(baseUrl) {
const trimmed = baseUrl.trim();
if (!trimmed) return false;
try {
return new URL(/^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`).hostname.toLowerCase() === "api.minimaxi.com";
} catch {
return false;
}
}
function hasConfiguredProviderApiKey(cfg, provider) {
const apiKey = cfg.models?.providers?.[provider]?.apiKey;
return typeof apiKey === "string" && apiKey.trim().length > 0 || isSecretRef(apiKey);
}
function resolveMinimaxVlmAuthProvider(cfg, provider) {
if (!isMinimaxCnAlias(provider) || hasConfiguredProviderApiKey(cfg, provider)) return provider;
return normalizeMediaProviderId(provider);
}
async function resolveMinimaxVlmFallbackRuntime(params) {
const authProvider = resolveMinimaxVlmAuthProvider(params.cfg, params.provider);
return {
apiKey: requireApiKey(await resolveApiKeyForProvider({
provider: authProvider,
cfg: params.cfg,
profileId: params.profile,
preferredProfile: params.preferredProfile,
agentDir: params.agentDir,
...params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}
}), authProvider),
modelBaseUrl: resolveConfiguredProviderBaseUrl(params.cfg, params.provider)
};
}
function resolveImageDescriptionTimeoutMs(timeoutMs, startedAtMs) {
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0) return;
return Math.max(1, Math.floor(timeoutMs - (Date.now() - startedAtMs)));
}
async function withImageDescriptionTimeout(params) {
if (params.timeoutMs === void 0) return await params.task;
let timeout;
try {
return await Promise.race([params.task, new Promise((_, reject) => {
timeout = setTimeout(() => {
params.controller.abort();
reject(/* @__PURE__ */ new Error(`image description timed out after ${params.timeoutMs}ms`));
}, params.timeoutMs);
})]);
} finally {
if (timeout) clearTimeout(timeout);
}
}
async function describeImagesWithModelInternal(params, options = {}) {
const prompt = params.prompt ?? "Describe the image.";
const startedAtMs = Date.now();
const controller = new AbortController();
let apiKey;
let model;
try {
const resolved = await withImageDescriptionTimeout({
controller,
timeoutMs: resolveImageDescriptionTimeoutMs(params.timeoutMs, startedAtMs),
task: resolveImageRuntime(params)
});
apiKey = resolved.apiKey;
model = resolved.model;
} catch (err) {
if (!isMinimaxVlmModel(params.provider, params.model) || !isUnknownModelError(err)) throw err;
const fallback = await resolveMinimaxVlmFallbackRuntime(params);
return await describeImagesWithMinimax({
apiKey: fallback.apiKey,
provider: params.provider,
modelId: params.model,
modelBaseUrl: fallback.modelBaseUrl,
prompt,
timeoutMs: params.timeoutMs,
images: params.images
});
}
if (isMinimaxVlmModel(model.provider, model.id)) return await describeImagesWithMinimax({
apiKey,
provider: model.provider,
modelId: model.id,
modelBaseUrl: model.baseUrl,
prompt,
timeoutMs: params.timeoutMs,
images: params.images
});
const providerStreamFn = registerProviderStreamForModel({
model,
cfg: params.cfg,
agentDir: params.agentDir,
...params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}
});
const context = buildImageContext(prompt, params.images, { promptInUserContent: shouldPlaceImagePromptInUserContent(model) });
const maxTokens = resolveImageToolMaxTokens(model.maxTokens, params.maxTokens);
const completeImage = async (onPayload) => {
const payloadHandler = composeImageDescriptionPayloadHandlers(onPayload, options.onPayload);
const timeoutMs = resolveImageDescriptionTimeoutMs(params.timeoutMs, startedAtMs);
const headers = buildImageRequestHeaders(model);
const streamOptions = {
apiKey,
maxTokens,
signal: controller.signal,
...timeoutMs !== void 0 ? { timeoutMs } : {},
...headers ? { headers } : {},
...payloadHandler ? { onPayload: payloadHandler } : {}
};
return await withImageDescriptionTimeout({
controller,
timeoutMs,
task: providerStreamFn ? (async () => await (await providerStreamFn(model, context, streamOptions)).result())() : complete(model, context, streamOptions)
});
};
const message = await completeImage();
try {
return {
text: coerceImageAssistantText({
message,
provider: model.provider,
model: model.id
}),
model: model.id
};
} catch (err) {
if (!isImageModelNoTextError(err) || !hasImageReasoningOnlyResponse(message)) throw err;
}
return {
text: coerceImageAssistantText({
message: await completeImage(disableReasoningForImageRetryPayload),
provider: model.provider,
model: model.id
}),
model: model.id
};
}
async function describeImagesWithModel(params) {
return await describeImagesWithModelInternal(params);
}
async function describeImagesWithModelPayloadTransform(params, onPayload) {
return await describeImagesWithModelInternal(params, { onPayload });
}
async function describeImageWithModel(params) {
return await describeImagesWithModel({
images: [{
buffer: params.buffer,
fileName: params.fileName,
mime: params.mime
}],
model: params.model,
provider: params.provider,
prompt: params.prompt,
maxTokens: params.maxTokens,
timeoutMs: params.timeoutMs,
profile: params.profile,
preferredProfile: params.preferredProfile,
authStore: params.authStore,
agentDir: params.agentDir,
...params.workspaceDir ? { workspaceDir: params.workspaceDir } : {},
cfg: params.cfg
});
}
async function describeImageWithModelPayloadTransform(params, onPayload) {
return await describeImagesWithModelPayloadTransform({
images: [{
buffer: params.buffer,
fileName: params.fileName,
mime: params.mime
}],
model: params.model,
provider: params.provider,
prompt: params.prompt,
maxTokens: params.maxTokens,
timeoutMs: params.timeoutMs,
profile: params.profile,
preferredProfile: params.preferredProfile,
authStore: params.authStore,
agentDir: params.agentDir,
...params.workspaceDir ? { workspaceDir: params.workspaceDir } : {},
cfg: params.cfg
}, onPayload);
}
//#endregion
export { describeImageWithModel, describeImageWithModelPayloadTransform, describeImagesWithModel, describeImagesWithModelPayloadTransform };