fix(agents): skip default idle watchdog for local provider streams

This commit is contained in:
openperf
2026-05-03 01:33:41 +08:00
parent 1507c6dac7
commit a740ff1b53
3 changed files with 94 additions and 0 deletions

View File

@@ -2141,6 +2141,7 @@ export async function runEmbeddedAttempt(
trigger: params.trigger,
runTimeoutMs: params.timeoutMs !== configuredRunTimeoutMs ? params.timeoutMs : undefined,
modelRequestTimeoutMs: (params.model as { requestTimeoutMs?: number }).requestTimeoutMs,
model: params.model as { baseUrl?: string },
});
if (idleTimeoutMs > 0) {
activeSession.agent.streamFn = streamWithIdleTimeout(

View File

@@ -85,6 +85,51 @@ describe("resolveLlmIdleTimeoutMs", () => {
const cfg = { agents: { defaults: { timeoutSeconds: 300 } } } as OpenClawConfig;
expect(resolveLlmIdleTimeoutMs({ cfg, trigger: "cron" })).toBe(DEFAULT_LLM_IDLE_TIMEOUT_MS);
});
it.each([
"http://localhost:11434",
"http://127.0.0.1:11434",
"http://0.0.0.0:11434",
"http://[::1]:11434",
"http://my-rig.local:11434",
"http://10.0.0.5:11434",
"http://172.16.5.10:11434",
"http://192.168.1.20:11434",
])("disables the default idle watchdog for local provider baseUrl %s", (baseUrl) => {
expect(resolveLlmIdleTimeoutMs({ model: { baseUrl } })).toBe(0);
});
it("keeps the default idle watchdog for remote provider baseUrls", () => {
expect(resolveLlmIdleTimeoutMs({ model: { baseUrl: "https://api.openai.com/v1" } })).toBe(
DEFAULT_LLM_IDLE_TIMEOUT_MS,
);
expect(resolveLlmIdleTimeoutMs({ model: { baseUrl: "https://ollama.com" } })).toBe(
DEFAULT_LLM_IDLE_TIMEOUT_MS,
);
});
it("ignores malformed baseUrl and keeps the default idle watchdog", () => {
expect(resolveLlmIdleTimeoutMs({ model: { baseUrl: "not-a-url" } })).toBe(
DEFAULT_LLM_IDLE_TIMEOUT_MS,
);
expect(resolveLlmIdleTimeoutMs({ model: { baseUrl: "" } })).toBe(DEFAULT_LLM_IDLE_TIMEOUT_MS);
});
it("still honors an explicit provider request timeout for local providers", () => {
expect(
resolveLlmIdleTimeoutMs({
model: { baseUrl: "http://127.0.0.1:11434" },
modelRequestTimeoutMs: 600_000,
}),
).toBe(600_000);
});
it("still applies agents.defaults.timeoutSeconds cap for local providers", () => {
const cfg = { agents: { defaults: { timeoutSeconds: 30 } } } as OpenClawConfig;
expect(resolveLlmIdleTimeoutMs({ cfg, model: { baseUrl: "http://127.0.0.1:11434" } })).toBe(
30_000,
);
});
});
describe("streamWithIdleTimeout", () => {

View File

@@ -15,6 +15,43 @@ export const DEFAULT_LLM_IDLE_TIMEOUT_MS = DEFAULT_LLM_IDLE_TIMEOUT_SECONDS * 10
*/
const MAX_SAFE_TIMEOUT_MS = 2_147_000_000;
/**
* Detects loopback / private-network / `.local` base URLs. Local providers
* (Ollama, LM Studio, llama.cpp) legitimately stay silent for many minutes
* during prompt evaluation and thinking, so the network-silence-as-hang
* heuristic that motivates the default idle watchdog does not apply.
*/
function isLocalProviderBaseUrl(baseUrl: string): boolean {
let host: string;
try {
host = new URL(baseUrl).hostname.toLowerCase();
} catch {
return false;
}
if (host.startsWith("[") && host.endsWith("]")) {
host = host.slice(1, -1);
}
if (
host === "localhost" ||
host === "127.0.0.1" ||
host === "0.0.0.0" ||
host === "::1" ||
host === "::ffff:7f00:1" ||
host === "::ffff:127.0.0.1" ||
host.endsWith(".local")
) {
return true;
}
const octets = host.split(".").map((part) => Number.parseInt(part, 10));
if (octets.length !== 4 || octets.some((p) => !Number.isInteger(p) || p < 0 || p > 255)) {
return false;
}
const [a, b] = octets;
return (
a === 10 || (a === 172 && b !== undefined && b >= 16 && b <= 31) || (a === 192 && b === 168)
);
}
/**
* Resolves the LLM idle timeout from configuration.
* @returns Idle timeout in milliseconds, or 0 to disable
@@ -24,6 +61,7 @@ export function resolveLlmIdleTimeoutMs(params?: {
trigger?: EmbeddedRunTrigger;
runTimeoutMs?: number;
modelRequestTimeoutMs?: number;
model?: { baseUrl?: string };
}): number {
const clampTimeoutMs = (valueMs: number) => Math.min(Math.floor(valueMs), MAX_SAFE_TIMEOUT_MS);
const clampImplicitTimeoutMs = (valueMs: number) =>
@@ -72,6 +110,16 @@ export function resolveLlmIdleTimeoutMs(params?: {
return 0;
}
// The default watchdog is a network-silence-as-hang guard for cloud providers.
// Local providers can legitimately stream nothing for many minutes during
// prompt evaluation or thinking, so falling back to the default would abort
// valid local runs. Honor it only when the user has not opted out via the
// baseUrl pointing at loopback / private-network / `.local`.
const baseUrl = params?.model?.baseUrl;
if (typeof baseUrl === "string" && baseUrl.length > 0 && isLocalProviderBaseUrl(baseUrl)) {
return 0;
}
return DEFAULT_LLM_IDLE_TIMEOUT_MS;
}