fix(ollama): retry non-visible reasoning turns

This commit is contained in:
Peter Steinberger
2026-04-27 06:19:15 +01:00
parent 32b1f0ce74
commit b94ad7c9d8
3 changed files with 114 additions and 2 deletions

View File

@@ -56,6 +56,7 @@ Docs: https://docs.openclaw.ai
- Providers/Ollama: add provider-scoped model request timeouts, thread them through guarded fetch connect/header/body/abort handling, and document `params.keep_alive` for cold local models so first-turn Ollama loads no longer require global agent timeout changes. Fixes #64541 and #68796; supersedes #65143 and #66511. Thanks @LittleJakub, @Juankcba, @uninhibite-scholar, and @yfge.
- Providers/Ollama: preserve explicit configured model input modalities when merging discovered provider metadata so custom vision models keep image support instead of silently dropping attachments. Fixes #39690; carries forward #39785. Thanks @Skrblik and @Mriris.
- Providers/Ollama: estimate native Ollama transcript usage when `/api/chat` omits prompt/eval counters while preserving exact zero counters, keeping local model runs visible in usage surfaces. Carries forward #39112. Thanks @TylonHH.
- Agents/Ollama: retry native Ollama turns that finish without user-visible text, including unsigned thinking-only responses, so constrained reasoning turns can continue instead of surfacing an empty reply. Carries forward #66552 and #61223. Thanks @yfge and @L3G.
- Providers/PDF/Ollama: add bounded network timeouts for Ollama model pulls and native Anthropic/Gemini PDF analysis requests so unresponsive provider endpoints no longer hang sessions indefinitely. Fixes #54142; supersedes #54144 and #54145. Thanks @jinduwang1001-max and @arkyu2077.
- LLM Task/Ollama: accept model overrides that already include the selected provider prefix, avoiding doubled ids such as `ollama/ollama/llama3.2:latest`, and live-verify local Ollama JSON tasks return parsed output. Fixes #50052. Thanks @ralphy-maplebots and @Hollychou924.
- Memory/doctor: treat Ollama memory embeddings as key-optional so `openclaw doctor` no longer warns about a missing API key when the gateway reports embeddings are ready. Fixes #46584. Thanks @fengly78.

View File

@@ -976,6 +976,103 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => {
expect(retryInstruction).toBe(REASONING_ONLY_RETRY_INSTRUCTION);
});
it("does not apply planning-only or ack fast paths to Ollama runs", () => {
const retryInstruction = resolvePlanningOnlyRetryInstruction({
provider: "ollama",
modelId: "gemma4:31b",
prompt: "Please inspect the code, make the change, and run the checks.",
aborted: false,
timedOut: false,
attempt: makeAttemptResult({
assistantTexts: ["I'll inspect the code, make the change, and run the checks."],
}),
});
const ackInstruction = resolveAckExecutionFastPathInstruction({
provider: "ollama",
modelId: "gemma4:31b",
prompt: "go ahead",
});
expect(retryInstruction).toBeNull();
expect(ackInstruction).toBeNull();
});
it("retries signed reasoning-only Ollama turns with a visible-answer continuation instruction", () => {
const retryInstruction = resolveReasoningOnlyRetryInstruction({
provider: "ollama",
modelId: "gemma4:31b",
aborted: false,
timedOut: false,
attempt: makeAttemptResult({
assistantTexts: [],
lastAssistant: {
role: "assistant",
stopReason: "end_turn",
provider: "ollama",
model: "gemma4:31b",
content: [
{
type: "thinking",
thinking: "internal reasoning",
thinkingSignature: JSON.stringify({ id: "ollama_rs_helper", type: "reasoning" }),
},
],
} as unknown as EmbeddedRunAttemptResult["lastAssistant"],
}),
});
expect(retryInstruction).toBe(REASONING_ONLY_RETRY_INSTRUCTION);
});
it("retries unsigned-thinking Ollama turns via the empty-response path", () => {
const retryInstruction = resolveEmptyResponseRetryInstruction({
provider: "ollama",
modelId: "gemma4:31b",
payloadCount: 0,
aborted: false,
timedOut: false,
attempt: makeAttemptResult({
assistantTexts: [],
lastAssistant: {
role: "assistant",
stopReason: "end_turn",
provider: "ollama",
model: "gemma4:31b",
content: [
{
type: "thinking",
thinking: "internal reasoning",
},
],
} as unknown as EmbeddedRunAttemptResult["lastAssistant"],
}),
});
expect(retryInstruction).toBe(EMPTY_RESPONSE_RETRY_INSTRUCTION);
});
it("retries generic empty Ollama turns without visible text", () => {
const retryInstruction = resolveEmptyResponseRetryInstruction({
provider: "ollama",
modelId: "gemma4:31b",
payloadCount: 0,
aborted: false,
timedOut: false,
attempt: makeAttemptResult({
assistantTexts: [],
lastAssistant: {
role: "assistant",
stopReason: "end_turn",
provider: "ollama",
model: "gemma4:31b",
content: [{ type: "text", text: "" }],
} as unknown as EmbeddedRunAttemptResult["lastAssistant"],
}),
});
expect(retryInstruction).toBe(EMPTY_RESPONSE_RETRY_INSTRUCTION);
});
it("treats exact NO_REPLY as a deliberate silent assistant reply", () => {
const incompleteTurnText = resolveIncompleteTurnPayloadText({
payloadCount: 0,

View File

@@ -109,6 +109,7 @@ const GEMINI_INCOMPLETE_TURN_PROVIDER_IDS = new Set([
"google-gemini-cli",
]);
const GEMINI_INCOMPLETE_TURN_MODEL_ID_PATTERN = /^gemini(?:[.-]|$)/;
const OLLAMA_INCOMPLETE_TURN_PROVIDER_ID_PATTERN = /^ollama(?:-|$)/;
const DEFAULT_PLANNING_ONLY_RETRY_LIMIT = 1;
const STRICT_AGENTIC_PLANNING_ONLY_RETRY_LIMIT = 2;
// Allow one immediate continuation plus one follow-up continuation before
@@ -498,7 +499,7 @@ export function resolveReasoningOnlyRetryInstruction(params: {
}
if (
!shouldApplyPlanningOnlyRetryGuard({
!shouldApplyNonVisibleTurnRetryGuard({
provider: params.provider,
modelId: params.modelId,
executionContract: params.executionContract,
@@ -544,7 +545,7 @@ export function resolveEmptyResponseRetryInstruction(params: {
}
if (
shouldApplyPlanningOnlyRetryGuard({
shouldApplyNonVisibleTurnRetryGuard({
provider: params.provider,
modelId: params.modelId,
executionContract: params.executionContract,
@@ -573,6 +574,19 @@ function shouldApplyPlanningOnlyRetryGuard(params: {
});
}
function shouldApplyNonVisibleTurnRetryGuard(params: {
provider?: string;
modelId?: string;
executionContract?: string;
}): boolean {
if (shouldApplyPlanningOnlyRetryGuard(params)) {
return true;
}
return OLLAMA_INCOMPLETE_TURN_PROVIDER_ID_PATTERN.test(
normalizeLowercaseStringOrEmpty(params.provider ?? ""),
);
}
function isIncompleteTurnRecoverySupportedProviderModel(params: {
provider?: string;
modelId?: string;