fix(media-understanding): restore image description token default

Restore the describeImageWithModel default token budget to the helper-level 4096-token default instead of forcing 512 before resolution.

Add regression coverage for the default and for smaller model caps, and record the user-facing fix in the changelog.

Co-authored-by: scotthuang <scotthuang@tencent.com>
This commit is contained in:
scotthuang
2026-05-24 09:42:18 +08:00
committed by GitHub
parent 0cba872e38
commit 5dcbd385f7
3 changed files with 76 additions and 2 deletions
+1
View File
@@ -271,6 +271,7 @@ Docs: https://docs.openclaw.ai
- PDF tool: time out idle remote PDF body reads after 120 seconds so stalled remote documents return an error instead of wedging the session. Fixes #68649. (#84768) Thanks @luoyanglang.
- Diagnostics/OpenTelemetry plugin: suppress handled OTLP exporter promise rejections so collector shutdowns no longer crash the Gateway. (#81085) Thanks @luoyanglang.
- Agents/exec: omit raw command text and env values from denied exec failure logs while keeping safe correlation metadata. Fixes #85049. (#85140) Thanks @joshavant.
- Media-understanding: restore the 4096-token default for image descriptions so reasoning-capable vision models no longer truncate before returning text, while preserving smaller model caps. (#84932) Thanks @scotthuang.
- Media/audio: skip empty structured sherpa-onnx transcripts instead of treating the raw JSON payload as spoken text. (#84667) Thanks @TurboTheTurtle.
- Agents/exec: preserve inherited XDG base-directory environment values for subprocesses while still rejecting agent-supplied XDG overrides. Fixes #84854. (#85139) Thanks @joshavant.
- Node/Linux: keep `OPENCLAW_GATEWAY_TOKEN` out of generated systemd unit files by writing node service token values to a node-specific env file. (#84408)
+74 -1
View File
@@ -811,7 +811,7 @@ describe("describeImageWithModel", () => {
expect(context.messages).toHaveLength(1);
expect(Object.keys(options).toSorted()).toEqual(["apiKey", "maxTokens", "signal", "timeoutMs"]);
expect(options.apiKey).toBe("oauth-test");
expect(options.maxTokens).toBe(512);
expect(options.maxTokens).toBe(4096);
expect(options.signal).toBeInstanceOf(AbortSignal);
expect(options.timeoutMs).toBeGreaterThan(0);
expect(options.timeoutMs).toBeLessThanOrEqual(1000);
@@ -1290,4 +1290,77 @@ describe("describeImageWithModel", () => {
expect(contentTypes).not.toContain("text");
expect(contentTypes).toContain("image");
});
it("defaults image-describe maxTokens to 4096 for reasoning-capable VLMs", async () => {
discoverModelsMock.mockReturnValue({
find: vi.fn(() => ({
api: "openai-completions",
provider: "agent-plan",
id: "doubao-seed-2.0-pro",
input: ["text", "image"],
baseUrl: "https://ark.cn-beijing.volces.com/api/plan/v3",
})),
});
completeMock.mockResolvedValue({
role: "assistant",
api: "openai-completions",
provider: "agent-plan",
model: "doubao-seed-2.0-pro",
stopReason: "stop",
timestamp: Date.now(),
content: [{ type: "text", text: "ok" }],
});
await describeImageWithModel({
cfg: {},
agentDir: "/tmp/openclaw-agent",
provider: "agent-plan",
model: "doubao-seed-2.0-pro",
buffer: Buffer.from("png-bytes"),
fileName: "image.png",
mime: "image/png",
prompt: "Describe the image.",
timeoutMs: 1000,
});
const [, , options] = requireFirstMockCall(completeMock, "image completion");
expect(options.maxTokens).toBe(4096);
});
it("caps image-describe maxTokens by the resolved model's own maxTokens", async () => {
discoverModelsMock.mockReturnValue({
find: vi.fn(() => ({
api: "openai-completions",
provider: "fake",
id: "small-vlm",
input: ["text", "image"],
baseUrl: "https://example.test",
maxTokens: 1024,
})),
});
completeMock.mockResolvedValue({
role: "assistant",
api: "openai-completions",
provider: "fake",
model: "small-vlm",
stopReason: "stop",
timestamp: Date.now(),
content: [{ type: "text", text: "ok" }],
});
await describeImageWithModel({
cfg: {},
agentDir: "/tmp/openclaw-agent",
provider: "fake",
model: "small-vlm",
buffer: Buffer.from("png-bytes"),
fileName: "image.png",
mime: "image/png",
prompt: "Describe the image.",
timeoutMs: 1000,
});
const [, , options] = requireFirstMockCall(completeMock, "image completion");
expect(options.maxTokens).toBe(1024);
});
});
+1 -1
View File
@@ -518,7 +518,7 @@ async function describeImagesWithModelInternal(
promptInUserContent: shouldPlaceImagePromptInUserContent(model),
});
const maxTokens = resolveImageToolMaxTokens(model.maxTokens, params.maxTokens ?? 512);
const maxTokens = resolveImageToolMaxTokens(model.maxTokens, params.maxTokens);
const completeImage = async (onPayload?: ProviderStreamOptions["onPayload"]) => {
const payloadHandler = composeImageDescriptionPayloadHandlers(onPayload, options.onPayload);
const timeoutMs = resolveImageDescriptionTimeoutMs(params.timeoutMs, startedAtMs);