fix(google): prevent empty contents error for gemini (#74465)

* fix(google): prevent empty contents error for gemini

* test(google): cover empty Gemini contents fallback

* docs(changelog): note Gemini empty content fallback

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Carl
2026-04-30 00:30:51 +08:00
committed by GitHub
parent df0074768c
commit 5e384fed6d
3 changed files with 59 additions and 4 deletions

View File

@@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Exec: reject invalid per-call `host` values instead of silently falling back to the default target, so hostname-like values fail before commands run. Fixes #74426. Thanks @scr00ge-00 and @vyctorbrzezowski.
- Google/Gemini: send non-empty placeholder content when a Gemini run is triggered with empty or filtered user content, avoiding `contents is not specified` API errors. Thanks @CaoYuhaoCarl.
- Build/Gateway: route restart, shutdown, respawn, diagnostics, command-queue cleanup, and runtime cleanup through one stable gateway lifecycle runtime entry so rebuilt packages do not strand long-running gateways on stale hashed chunks. Carries forward #73964. Thanks @pashpashpash.
- Memory/wiki: keep broad shared-source and generated related-link blocks from turning every page into a search hit, cap noisy backlinks, support all-term searches such as people-routing queries, and prefer readable page body snippets over generated metadata. Thanks @vincentkoc.
- Cron/Gateway: abort and bounded-clean up timed-out isolated agent turns before recording the timeout, so stale cron sessions cannot leave Discord or other chat lanes stuck in `processing` after a timeout. Thanks @vincentkoc.

View File

@@ -514,4 +514,54 @@ describe("google transport stream", () => {
expect(params.cachedContent).toBe("cachedContents/prebuilt-context");
});
it("uses a non-empty text placeholder for empty user text", () => {
const params = buildGoogleGenerativeAiParams(buildGeminiModel(), {
messages: [
{ role: "user", content: "", timestamp: 0 },
{
role: "user",
content: [{ type: "text", text: "" }],
timestamp: 1,
},
],
} as never);
expect(params.contents).toEqual([
{ role: "user", parts: [{ text: " " }] },
{ role: "user", parts: [{ text: " " }] },
]);
});
it("uses a text placeholder when user parts are filtered out for text-only models", () => {
const params = buildGoogleGenerativeAiParams(buildGeminiModel({ input: ["text"] }), {
messages: [
{
role: "user",
content: [{ type: "image", mimeType: "image/png", data: "png-bytes" }],
timestamp: 0,
},
],
} as never);
expect(params.contents).toEqual([{ role: "user", parts: [{ text: " " }] }]);
});
it("uses a user placeholder when converted Gemini contents would otherwise be empty", () => {
const params = buildGoogleGenerativeAiParams(buildGeminiModel(), {
messages: [
{
role: "assistant",
provider: "google",
api: "google-generative-ai",
model: "gemini-2.5-pro",
stopReason: "stop",
timestamp: 0,
content: [{ type: "text", text: " " }],
},
],
} as never);
expect(params.contents).toEqual([{ role: "user", parts: [{ text: " " }] }]);
});
});

View File

@@ -315,14 +315,14 @@ function convertGoogleMessages(model: GoogleTransportModel, context: Context) {
if (typeof msg.content === "string") {
contents.push({
role: "user",
parts: [{ text: sanitizeTransportPayloadText(msg.content) }],
parts: [{ text: sanitizeTransportPayloadText(msg.content) || " " }],
});
continue;
}
const parts = msg.content
.map((item) =>
item.type === "text"
? { text: sanitizeTransportPayloadText(item.text) }
? { text: sanitizeTransportPayloadText(item.text) || " " }
: {
inlineData: {
mimeType: item.mimeType,
@@ -331,9 +331,10 @@ function convertGoogleMessages(model: GoogleTransportModel, context: Context) {
},
)
.filter((item) => model.input.includes("image") || !("inlineData" in item));
if (parts.length > 0) {
contents.push({ role: "user", parts });
if (parts.length === 0) {
parts.push({ text: " " });
}
contents.push({ role: "user", parts });
continue;
}
@@ -437,6 +438,9 @@ function convertGoogleMessages(model: GoogleTransportModel, context: Context) {
}
}
}
if (contents.length === 0) {
contents.push({ role: "user", parts: [{ text: " " }] });
}
return contents;
}