diff --git a/CHANGELOG.md b/CHANGELOG.md index bfd86674940..771e51305ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/extensions/google/transport-stream.test.ts b/extensions/google/transport-stream.test.ts index e118951c0e4..4e40b5a4683 100644 --- a/extensions/google/transport-stream.test.ts +++ b/extensions/google/transport-stream.test.ts @@ -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: " " }] }]); + }); }); diff --git a/extensions/google/transport-stream.ts b/extensions/google/transport-stream.ts index 6db1cb6f167..9ba85ce9eaa 100644 --- a/extensions/google/transport-stream.ts +++ b/extensions/google/transport-stream.ts @@ -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; }