





















@@ -0,0 +1,99 @@
1+import fs from "node:fs/promises";
2+import os from "node:os";
3+import path from "node:path";
4+import { describe, expect, it } from "vitest";
5+import type { OpenClawConfig } from "../../config/types.openclaw.js";
6+import { createImageTool } from "./image-tool.js";
7+8+const LIVE =
9+process.env.OPENCLAW_LIVE_TEST === "1" && process.env.OPENCLAW_LIVE_OLLAMA_IMAGE === "1";
10+const OLLAMA_BASE_URL =
11+process.env.OPENCLAW_LIVE_OLLAMA_BASE_URL?.trim() || "http://127.0.0.1:11434";
12+const OLLAMA_IMAGE_MODEL = process.env.OPENCLAW_LIVE_OLLAMA_IMAGE_MODEL?.trim() || "qwen2.5vl:7b";
13+14+function resolveLiveNumCtx(): number {
15+const parsed = Number.parseInt(process.env.OPENCLAW_LIVE_OLLAMA_IMAGE_NUM_CTX ?? "2048", 10);
16+return Number.isFinite(parsed) ? Math.max(512, parsed) : 2048;
17+}
18+19+const OLLAMA_IMAGE_NUM_CTX = resolveLiveNumCtx();
20+21+const VALID_RED_PNG_B64 =
22+"iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAGYktHRAD/AP8A/6C9p5MAAAAHdElNRQfqBBsGAQr00ED3AAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDI2LTA0LTI3VDA2OjAxOjEwKzAwOjAwPU3tXwAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyNi0wNC0yN1QwNjowMToxMCswMDowMEwQVeMAAAAodEVYdGRhdGU6dGltZXN0YW1wADIwMjYtMDQtMjdUMDY6MDE6MTArMDA6MDAbBXQ8AAAAeElEQVRo3u3awQnDQBAEwT2Q8w/YAikIP5rF1RFMca+FO8/s7rrnqjcA1BsA6g0A9QaAesOfA77zqTf8Blj/AgAAAAAAAJsDqAOoA6gDqAOoc9TXAdQB1AHUAdQB1AHUAdQB1AHU7Qc46gEAAAAANrcecGZ2f8B/ASYSQPlKoEJ/AAAAAElFTkSuQmCC";
23+24+async function withLiveImageWorkspace<T>(
25+run: (ctx: { agentDir: string; workspaceDir: string; imagePath: string }) => Promise<T>,
26+) {
27+const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-ollama-image-live-"));
28+try {
29+const agentDir = path.join(root, "agent");
30+const workspaceDir = path.join(root, "workspace");
31+await fs.mkdir(agentDir, { recursive: true });
32+await fs.mkdir(workspaceDir, { recursive: true });
33+const imagePath = path.join(workspaceDir, "red.png");
34+await fs.writeFile(imagePath, Buffer.from(VALID_RED_PNG_B64, "base64"));
35+return await run({ agentDir, workspaceDir, imagePath });
36+} finally {
37+await fs.rm(root, { recursive: true, force: true });
38+}
39+}
40+41+describe.skipIf(!LIVE)("image tool Ollama live", () => {
42+it("describes a local image through the explicit image tool", async () => {
43+process.env.OLLAMA_API_KEY ||= "ollama-local";
44+await withLiveImageWorkspace(async ({ agentDir, workspaceDir, imagePath }) => {
45+const cfg: OpenClawConfig = {
46+agents: {
47+defaults: {
48+imageModel: { primary: `ollama/${OLLAMA_IMAGE_MODEL}` },
49+},
50+},
51+models: {
52+providers: {
53+ollama: {
54+api: "ollama",
55+baseUrl: OLLAMA_BASE_URL,
56+apiKey: "ollama-local",
57+timeoutSeconds: 300,
58+models: [
59+{
60+id: OLLAMA_IMAGE_MODEL,
61+name: OLLAMA_IMAGE_MODEL,
62+input: ["text", "image"],
63+reasoning: false,
64+cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
65+contextWindow: 128_000,
66+maxTokens: 512,
67+params: { num_ctx: OLLAMA_IMAGE_NUM_CTX, keep_alive: "1m" },
68+},
69+],
70+},
71+},
72+},
73+tools: {
74+media: {
75+image: {
76+timeoutSeconds: 180,
77+models: [{ provider: "ollama", model: OLLAMA_IMAGE_MODEL, timeoutSeconds: 300 }],
78+},
79+},
80+},
81+};
82+const tool = createImageTool({ config: cfg, agentDir, workspaceDir });
83+expect(tool).not.toBeNull();
84+85+const result = await tool!.execute("live-ollama-image", {
86+prompt: "Describe this image in one short sentence.",
87+image: imagePath,
88+});
89+90+expect(result).toMatchObject({
91+content: [expect.objectContaining({ type: "text" })],
92+});
93+const text = (
94+result as { content?: Array<{ type?: string; text?: string }> }
95+).content?.[0]?.text?.trim();
96+expect(text?.length ?? 0).toBeGreaterThan(0);
97+});
98+}, 180_000);
99+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。