Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const hoisted = vi.hoisted(() => ({
completeMock: vi.fn(),
ensureOpenClawModelsJsonMock: vi.fn(async () => {}),
getApiKeyForModelMock: vi.fn(async () => ({
apiKey: "oauth-test", // pragma: allowlist secret
source: "test",
mode: "oauth",
})),
resolveApiKeyForProviderMock: vi.fn(async () => ({
apiKey: "oauth-test", // pragma: allowlist secret
source: "test",
mode: "oauth",
})),
requireApiKeyMock: vi.fn((auth: { apiKey?: string }) => auth.apiKey ?? ""),
setRuntimeApiKeyMock: vi.fn(),
discoverModelsMock: vi.fn(),
fetchMock: vi.fn(),
registerProviderStreamForModelMock: vi.fn(),
}));
const {
completeMock,
ensureOpenClawModelsJsonMock,
getApiKeyForModelMock,
resolveApiKeyForProviderMock,
requireApiKeyMock,
setRuntimeApiKeyMock,
discoverModelsMock,
fetchMock,
registerProviderStreamForModelMock,
} = hoisted;
vi.mock("@mariozechner/pi-ai", async () => {
const actual = await vi.importActual<typeof import("@mariozechner/pi-ai")>("@mariozechn er/pi-ai");
return {
...actual,
complete: completeMock,
};
});
vi.mock("../agents/models-config.js", async () => ({
...(await vi.importActual<typeof import("../agents/models-config.js")>(
"../agents/models-config.js",
)),
ensureOpenClawModelsJson: ensureOpenClawModelsJsonMock,
}));
vi.mock("../agents/model-auth.js", () => ({
getApiKeyForModel: getApiKeyForModelMock,
resolveApiKeyForProvider: resolveApiKeyForProviderMock,
requireApiKey: requireApiKeyMock,
}));
vi.mock("../agents/provider-stream.js", () => ({
registerProviderStreamForModel: registerProviderStreamForModelMock,
}));
vi.mock("../agents/pi-model-discovery-runtime.js", () => ({
discoverAuthStorage: () => ({
setRuntimeApiKey: setRuntimeApiKeyMock,
}),
discoverModels: discoverModelsMock,
}));
const { describeImageWithModel } = await import("./image.js");
describe("describeImageWithModel", () => {
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
beforeEach(() => {
vi.stubGlobal("fetch", fetchMock);
vi.clearAllMocks();
fetchMock.mockResolvedValue({
ok: true,
status: 200,
statusText: "OK",
headers: { get: vi.fn(() => null) },
json: vi.fn(async () => ({
base_resp: { status_code: 0 },
content: "portal ok",
})),
text: vi.fn(async () => ""),
});
discoverModelsMock.mockReturnValue({
find: vi.fn(() => ({
provider: "minimax-portal",
id: "MiniMax-VL-01",
input: ["text", "image"],
baseUrl: "https://api.minimax.io/anthropic",
})),
});
});
it("routes minimax-portal image models through the MiniMax VLM endpoint", async () => {
const authStore = { version: 1, profiles: {} };
const result = await describeImageWithModel({
cfg: {},
agentDir: "/tmp/openclaw-agent",
provider: "minimax-portal",
model: "MiniMax-VL-01",
buffer: Buffer.from("png-bytes"),
fileName: "image.png",
mime: "image/png",
prompt: "Describe the image.",
timeoutMs: 1000,
authStore,
});
expect(result).toEqual({
text: "portal ok",
model: "MiniMax-VL-01",
});
expect(ensureOpenClawModelsJsonMock).toHaveBeenCalled();
expect(getApiKeyForModelMock).toHaveBeenCalledWith(
expect.objectContaining({ store: authStore }),
);
expect(requireApiKeyMock).toHaveBeenCalled();
expect(setRuntimeApiKeyMock).toHaveBeenCalledWith("minimax-portal", "oauth-test");
expect(fetchMock).toHaveBeenCalledWith(
"https://api.minimax.io/v1/coding_plan/vlm",
expect.objectContaining({
method: "POST",
headers: {
Authorization: "Bearer oauth-test",
"Content-Type": "application/json",
"MM-API-Source": "OpenClaw",
},
body: JSON.stringify({
prompt: "Describe the image.",
image_url: `data:image/png;base64,${Buffer.from("png-bytes").toString("base64")}`,
}),
signal: expect.any(AbortSignal),
}),
);
expect(completeMock).not.toHaveBeenCalled();
});
it("uses generic completion for non-canonical minimax-portal image models", async () => {
discoverModelsMock.mockReturnValue({
find: vi.fn(() => ({
provider: "minimax-portal",
id: "custom-vision",
input: ["text", "image"],
baseUrl: "https://api.minimax.io/anthropic",
})),
});
completeMock.mockResolvedValue({
role: "assistant",
api: "anthropic-messages",
provider: "minimax-portal",
model: "custom-vision",
stopReason: "stop",
timestamp: Date.now(),
content: [{ type: "text", text: "generic ok" }],
});
const result = await describeImageWithModel({
cfg: {},
agentDir: "/tmp/openclaw-agent",
provider: "minimax-portal",
model: "custom-vision",
buffer: Buffer.from("png-bytes"),
fileName: "image.png",
mime: "image/png",
prompt: "Describe the image.",
timeoutMs: 1000,
});
expect(result).toEqual({
text: "generic ok",
model: "custom-vision",
});
expect(registerProviderStreamForModelMock).toHaveBeenCalledWith(
expect.objectContaining({
model: expect.objectContaining({
provider: "minimax-portal",
id: "custom-vision",
}),
cfg: {},
agentDir: "/tmp/openclaw-agent",
}),
);
expect(completeMock).toHaveBeenCalledOnce();
expect(fetchMock).not.toHaveBeenCalled();
});
it("passes image prompt as system instructions for codex image requests", async () => {
discoverModelsMock.mockReturnValue({
find: vi.fn(() => ({
provider: "openai-codex",
id: "gpt-5.4",
input: ["text", "image"],
baseUrl: "https://chatgpt.com/backend-api",
})),
});
completeMock.mockResolvedValue({
role: "assistant",
api: "openai-codex-responses",
provider: "openai-codex",
model: "gpt-5.4",
stopReason: "stop",
timestamp: Date.now(),
content: [{ type: "text", text: "codex ok" }],
});
const result = await describeImageWithModel({
cfg: {},
agentDir: "/tmp/openclaw-agent",
provider: "openai-codex",
model: "gpt-5.4",
buffer: Buffer.from("png-bytes"),
fileName: "image.png",
mime: "image/png",
prompt: "Describe the image.",
timeoutMs: 1000,
});
expect(result).toEqual({
text: "codex ok",
model: "gpt-5.4",
});
expect(completeMock).toHaveBeenCalledOnce();
expect(completeMock).toHaveBeenCalledWith(
expect.objectContaining({
provider: "openai-codex",
id: "gpt-5.4",
}),
expect.objectContaining({
systemPrompt: "Describe the image.",
messages: [
expect.objectContaining({
role: "user",
content: [
expect.objectContaining({
type: "image",
mimeType: "image/png",
}),
],
}),
],
}),
expect.any(Object),
);
const [, context] = completeMock.mock.calls[0] ?? [];
expect(context?.messages?.[0]?.content).toHaveLength(1);
});
it("places OpenRouter image prompts in user content before images", async () => {
discoverModelsMock.mockReturnValue({
find: vi.fn(() => ({
api: "openai-completions",
provider: "openrouter",
id: "google/gemini-2.5-flash",
input: ["text", "image"],
baseUrl: "https://openrouter.ai/api/v1",
})),
});
completeMock.mockResolvedValue({
role: "assistant",
api: "openai-completions",
provider: "openrouter",
model: "google/gemini-2.5-flash",
stopReason: "stop",
timestamp: Date.now(),
content: [{ type: "text", text: "openrouter ok" }],
});
const result = await describeImageWithModel({
cfg: {},
agentDir: "/tmp/openclaw-agent",
provider: "openrouter",
model: "google/gemini-2.5-flash",
buffer: Buffer.from("png-bytes"),
fileName: "image.png",
mime: "image/png",
prompt: "Describe the image.",
timeoutMs: 1000,
});
expect(result).toEqual({
text: "openrouter ok",
model: "google/gemini-2.5-flash",
});
const [, context] = completeMock.mock.calls[0] ?? [];
expect(context?.systemPrompt).toBeUndefined();
expect(context?.messages?.[0]?.content).toEqual([
{ type: "text", text: "Describe the image." },
expect.objectContaining({
type: "image",
mimeType: "image/png",
}),
]);
});
it.each([
{
name: "direct OpenAI Responses baseUrl",
provider: "openai",
model: {
api: "openai-responses",
provider: "openai",
id: "gpt-5.4-mini",
input: ["text", "image"],
baseUrl: "https://api.openai.com/v1",
},
expectedRetryPayload: {
reasoning: { effort: "none" },
},
},
{
name: "default OpenAI Responses route without explicit baseUrl",
provider: "openai",
model: {
api: "openai-responses",
provider: "openai",
id: "gpt-5.4-mini",
input: ["text", "image"],
},
expectedRetryPayload: {
reasoning: { effort: "none" },
},
},
{
name: "azure-openai provider using openai-responses api",
provider: "azure-openai",
model: {
api: "openai-responses",
provider: "azure-openai",
id: "gpt-5.4-mini",
input: ["text", "image"],
baseUrl: "https://myresource.openai.azure.com/openai/v1",
},
expectedRetryPayload: {
reasoning: { effort: "none" },
},
},
{
name: "proxy-like openai-responses route",
provider: "openai",
model: {
api: "openai-responses",
provider: "openai",
id: "gpt-5.4-mini",
input: ["text", "image"],
baseUrl: "https://proxy.example.com/v1",
},
expectedRetryPayload: {},
},
])(
"retries reasoning-only image responses with reasoning disabled for $name",
async ({ provider, model, expectedRetryPayload }) => {
discoverModelsMock.mockReturnValue({
find: vi.fn(() => model),
});
completeMock
.mockResolvedValueOnce({
role: "assistant",
api: model.api,
provider: model.provider,
model: model.id,
stopReason: "stop",
timestamp: Date.now(),
content: [
{
type: "thinking",
thinking: "internal image reasoning",
thinkingSignature: "reasoning_content",
},
],
})
.mockResolvedValueOnce({
role: "assistant",
api: model.api,
provider: model.provider,
model: model.id,
stopReason: "stop",
timestamp: Date.now(),
content: [{ type: "text", text: "retry ok" }],
});
const result = await describeImageWithModel({
cfg: {},
agentDir: "/tmp/openclaw-agent",
provider,
model: model.id,
buffer: Buffer.from("png-bytes"),
fileName: "image.png",
mime: "image/png",
prompt: "Describe the image.",
timeoutMs: 1000,
});
expect(result).toEqual({
text: "retry ok",
model: model.id,
});
expect(completeMock).toHaveBeenCalledTimes(2);
const [, , retryOptions] = completeMock.mock.calls[1] ?? [];
expect(retryOptions?.onPayload).toEqual(expect.any(Function));
const retryPayload = await retryOptions?.onPayload?.(
{
reasoning: { effort: "high", summary: "auto" },
reasoning_effort: "high",
include: ["reasoning.encrypted_content"],
},
completeMock.mock.calls[1]?.[0],
);
expect(retryPayload).toEqual(expectedRetryPayload);
},
);
it("normalizes deprecated google flash ids before lookup and keeps profile auth selection", async () => {
const findMock = vi.fn((provider: string, modelId: string) => {
expect(provider).toBe("google");
expect(modelId).toBe("gemini-3-flash-preview");
return {
provider: "google",
id: "gemini-3-flash-preview",
input: ["text", "image"],
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
};
});
discoverModelsMock.mockReturnValue({ find: findMock });
completeMock.mockResolvedValue({
role: "assistant",
api: "google-generative-ai",
provider: "google",
model: "gemini-3-flash-preview",
stopReason: "stop",
timestamp: Date.now(),
content: [{ type: "text", text: "flash ok" }],
});
const result = await describeImageWithModel({
cfg: {},
agentDir: "/tmp/openclaw-agent",
provider: "google",
model: "gemini-3.1-flash-preview",
profile: "google:default",
buffer: Buffer.from("png-bytes"),
fileName: "image.png",
mime: "image/png",
prompt: "Describe the image.",
timeoutMs: 1000,
});
expect(result).toEqual({
text: "flash ok",
model: "gemini-3-flash-preview",
});
expect(findMock).toHaveBeenCalledOnce();
expect(getApiKeyForModelMock).toHaveBeenCalledWith(
expect.objectContaining({
profileId: "google:default",
}),
);
expect(setRuntimeApiKeyMock).toHaveBeenCalledWith("google", "oauth-test");
});
it("normalizes gemini 3.1 flash-lite ids before lookup and keeps profile auth selection", async () => {
const findMock = vi.fn((provider: string, modelId: string) => {
expect(provider).toBe("google");
expect(modelId).toBe("gemini-3.1-flash-lite-preview");
return {
provider: "google",
id: "gemini-3.1-flash-lite-preview",
input: ["text", "image"],
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
};
});
discoverModelsMock.mockReturnValue({ find: findMock });
completeMock.mockResolvedValue({
role: "assistant",
api: "google-generative-ai",
provider: "google",
model: "gemini-3.1-flash-lite-preview",
stopReason: "stop",
timestamp: Date.now(),
content: [{ type: "text", text: "flash lite ok" }],
});
const result = await describeImageWithModel({
cfg: {},
agentDir: "/tmp/openclaw-agent",
provider: "google",
model: "gemini-3.1-flash-lite",
profile: "google:default",
buffer: Buffer.from("png-bytes"),
fileName: "image.png",
mime: "image/png",
prompt: "Describe the image.",
timeoutMs: 1000,
});
expect(result).toEqual({
text: "flash lite ok",
model: "gemini-3.1-flash-lite-preview",
});
expect(findMock).toHaveBeenCalledOnce();
expect(getApiKeyForModelMock).toHaveBeenCalledWith(
expect.objectContaining({
profileId: "google:default",
}),
);
expect(setRuntimeApiKeyMock).toHaveBeenCalledWith("google", "oauth-test");
});
});
¤ Dauer der Verarbeitung: 0.24 Sekunden
(vorverarbeitet am 2026-04-27)
¤
*© Formatika GbR, Deutschland
|
|