mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 12:00:44 +00:00
fix(agents): skip default idle watchdog for local provider streams
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user