Files
openclaw/src/agents/tools/image-tool.helpers.ts
边黎安 a4c373935f fix(agents): fall back to agents.defaults.model when agent has no model config (#24210)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 0f272b102763736001a82cfda23f35ff2ee9cac8
Co-authored-by: bianbiandashen <16240681+bianbiandashen@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-02-23 03:18:55 -05:00

89 lines
3.0 KiB
TypeScript

import type { AssistantMessage } from "@mariozechner/pi-ai";
import type { OpenClawConfig } from "../../config/config.js";
import {
resolveAgentModelFallbackValues,
resolveAgentModelPrimaryValue,
} from "../../config/model-input.js";
import { extractAssistantText } from "../pi-embedded-utils.js";
export type ImageModelConfig = { primary?: string; fallbacks?: string[] };
export function decodeDataUrl(dataUrl: string): {
buffer: Buffer;
mimeType: string;
kind: "image";
} {
const trimmed = dataUrl.trim();
const match = /^data:([^;,]+);base64,([a-z0-9+/=\r\n]+)$/i.exec(trimmed);
if (!match) {
throw new Error("Invalid data URL (expected base64 data: URL).");
}
const mimeType = (match[1] ?? "").trim().toLowerCase();
if (!mimeType.startsWith("image/")) {
throw new Error(`Unsupported data URL type: ${mimeType || "unknown"}`);
}
const b64 = (match[2] ?? "").trim();
const buffer = Buffer.from(b64, "base64");
if (buffer.length === 0) {
throw new Error("Invalid data URL: empty payload.");
}
return { buffer, mimeType, kind: "image" };
}
export function coerceImageAssistantText(params: {
message: AssistantMessage;
provider: string;
model: string;
}): string {
const stop = params.message.stopReason;
const errorMessage = params.message.errorMessage?.trim();
if (stop === "error" || stop === "aborted") {
throw new Error(
errorMessage
? `Image model failed (${params.provider}/${params.model}): ${errorMessage}`
: `Image model failed (${params.provider}/${params.model})`,
);
}
if (errorMessage) {
throw new Error(`Image model failed (${params.provider}/${params.model}): ${errorMessage}`);
}
const text = extractAssistantText(params.message);
if (text.trim()) {
return text.trim();
}
throw new Error(`Image model returned no text (${params.provider}/${params.model}).`);
}
export function coerceImageModelConfig(cfg?: OpenClawConfig): ImageModelConfig {
const primary = resolveAgentModelPrimaryValue(cfg?.agents?.defaults?.imageModel);
const fallbacks = resolveAgentModelFallbackValues(cfg?.agents?.defaults?.imageModel);
return {
...(primary?.trim() ? { primary: primary.trim() } : {}),
...(fallbacks.length > 0 ? { fallbacks } : {}),
};
}
export function resolveProviderVisionModelFromConfig(params: {
cfg?: OpenClawConfig;
provider: string;
}): string | null {
const providerCfg = params.cfg?.models?.providers?.[params.provider] as unknown as
| { models?: Array<{ id?: string; input?: string[] }> }
| undefined;
const models = providerCfg?.models ?? [];
const preferMinimaxVl =
params.provider === "minimax"
? models.find(
(m) =>
(m?.id ?? "").trim() === "MiniMax-VL-01" &&
Array.isArray(m?.input) &&
m.input.includes("image"),
)
: null;
const picked =
preferMinimaxVl ??
models.find((m) => Boolean((m?.id ?? "").trim()) && m.input?.includes("image"));
const id = (picked?.id ?? "").trim();
return id ? `${params.provider}/${id}` : null;
}