mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:10:44 +00:00
fix: keep Gemini thinking streams active (#76080) (thanks @zhangguiping-xydt)
This commit is contained in:
@@ -277,6 +277,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Providers/Google: fix Gemini 2.5 Flash-Lite `reasoning: "minimal"` rejections by raising its thinking-budget floor to 512 while preserving the existing Gemini 2.5 Pro and Flash minimal presets. (#70629) Thanks @ericberic.
|
||||
- Agents/status: resolve `session_status(sessionKey="current")` for sparse channel-plugin sessions after literal current lookups miss, so Scope, Slack, Discord, and other plugin-driven agents avoid retrying through `Unknown sessionKey: current`. Fixes #74141. (#72306) Thanks @bittoby.
|
||||
- Cron: retry recurring wake-now main-session jobs through temporary heartbeat busy skips before recording success, so queued cron events no longer appear as ok ghost runs while the main lane is still busy. Fixes #75964. (#76083) Thanks @kshetrajna12 and @xuruiray.
|
||||
- Providers/Google: keep Gemini thinking-signature-only stream chunks active during reasoning, so Gemini 3.1 Pro Preview replies no longer hit idle timeouts before visible text. Fixes #76071. (#76080) Thanks @marcoschierhorn and @zhangguiping-xydt.
|
||||
|
||||
## 2026.4.30
|
||||
|
||||
|
||||
@@ -768,7 +768,7 @@ describe("google transport stream", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("emits a thinking_signature event for thoughtSignature-only parts to keep the stream active", async () => {
|
||||
it("emits thinking activity for thoughtSignature-only parts to keep the stream active", async () => {
|
||||
guardedFetchMock.mockResolvedValueOnce(
|
||||
buildSseResponse([
|
||||
{
|
||||
@@ -810,12 +810,28 @@ describe("google transport stream", () => {
|
||||
{ reasoning: "high" },
|
||||
),
|
||||
);
|
||||
const events = [];
|
||||
for await (const event of stream) {
|
||||
events.push(event);
|
||||
}
|
||||
const result = await stream.result();
|
||||
|
||||
expect(result.content).toEqual([
|
||||
{ type: "thinking", thinking: "draft", thinkingSignature: "sig_2" },
|
||||
{ type: "text", text: "answer" },
|
||||
]);
|
||||
expect(events.map((event) => event.type)).toEqual([
|
||||
"start",
|
||||
"thinking_start",
|
||||
"thinking_delta",
|
||||
"thinking_delta",
|
||||
"thinking_end",
|
||||
"text_start",
|
||||
"text_delta",
|
||||
"text_end",
|
||||
"done",
|
||||
]);
|
||||
expect(events[3]).toMatchObject({ type: "thinking_delta", delta: "" });
|
||||
});
|
||||
|
||||
it("starts a thinking block for thoughtSignature-only parts that arrive before any text", async () => {
|
||||
|
||||
@@ -797,8 +797,11 @@ function createGoogleTransportStreamFn(kind: GoogleTransportApi): StreamFn {
|
||||
const candidate = chunk.candidates?.[0];
|
||||
if (candidate?.content?.parts) {
|
||||
for (const part of candidate.content.parts) {
|
||||
if (typeof part.text === "string") {
|
||||
const isThinking = part.thought === true;
|
||||
const hasThoughtSignature =
|
||||
typeof part.thoughtSignature === "string" && part.thoughtSignature.length > 0;
|
||||
const hasText = typeof part.text === "string";
|
||||
if (hasText || (hasThoughtSignature && !part.functionCall)) {
|
||||
const isThinking = part.thought === true || !hasText;
|
||||
const currentBlock = output.content[currentBlockIndex];
|
||||
if (
|
||||
currentBlockIndex < 0 ||
|
||||
@@ -829,7 +832,8 @@ function createGoogleTransportStreamFn(kind: GoogleTransportApi): StreamFn {
|
||||
}
|
||||
const activeBlock = output.content[currentBlockIndex];
|
||||
if (activeBlock?.type === "thinking") {
|
||||
activeBlock.thinking += part.text;
|
||||
const delta = hasText ? part.text : "";
|
||||
activeBlock.thinking += delta;
|
||||
activeBlock.thinkingSignature = retainThoughtSignature(
|
||||
activeBlock.thinkingSignature,
|
||||
part.thoughtSignature,
|
||||
@@ -837,7 +841,7 @@ function createGoogleTransportStreamFn(kind: GoogleTransportApi): StreamFn {
|
||||
stream.push({
|
||||
type: "thinking_delta",
|
||||
contentIndex: currentBlockIndex,
|
||||
delta: part.text,
|
||||
delta,
|
||||
partial: output as never,
|
||||
});
|
||||
} else if (activeBlock?.type === "text") {
|
||||
@@ -894,45 +898,6 @@ function createGoogleTransportStreamFn(kind: GoogleTransportApi): StreamFn {
|
||||
partial: output as never,
|
||||
});
|
||||
}
|
||||
// Gemini 3+ models can emit thoughtSignature-only parts during the
|
||||
// thinking phase before user-visible text arrives. Emit a stream event
|
||||
// so that idle-timeout wrappers detect model activity and don't kill
|
||||
// the stream prematurely.
|
||||
if (
|
||||
typeof part.thoughtSignature === "string" &&
|
||||
part.thoughtSignature.length > 0 &&
|
||||
typeof part.text !== "string" &&
|
||||
!part.functionCall
|
||||
) {
|
||||
if (
|
||||
currentBlockIndex < 0 ||
|
||||
output.content[currentBlockIndex]?.type !== "thinking"
|
||||
) {
|
||||
if (currentBlockIndex >= 0) {
|
||||
pushTextBlockEnd(stream, output, currentBlockIndex);
|
||||
}
|
||||
output.content.push({ type: "thinking", thinking: "" });
|
||||
currentBlockIndex = output.content.length - 1;
|
||||
stream.push({
|
||||
type: "thinking_start",
|
||||
contentIndex: currentBlockIndex,
|
||||
partial: output as never,
|
||||
});
|
||||
}
|
||||
const activeBlock = output.content[currentBlockIndex];
|
||||
if (activeBlock?.type === "thinking") {
|
||||
activeBlock.thinkingSignature = retainThoughtSignature(
|
||||
activeBlock.thinkingSignature,
|
||||
part.thoughtSignature,
|
||||
);
|
||||
}
|
||||
stream.push({
|
||||
type: "thinking_signature",
|
||||
contentIndex: currentBlockIndex,
|
||||
signature: part.thoughtSignature,
|
||||
partial: output as never,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof candidate?.finishReason === "string") {
|
||||
|
||||
Reference in New Issue
Block a user