


























@@ -85,6 +85,121 @@ describe("resolveLlmIdleTimeoutMs", () => {
8585const cfg = { agents: { defaults: { timeoutSeconds: 300 } } } as OpenClawConfig;
8686expect(resolveLlmIdleTimeoutMs({ cfg, trigger: "cron" })).toBe(DEFAULT_LLM_IDLE_TIMEOUT_MS);
8787});
88+89+it.each([
90+"http://localhost:11434",
91+"http://127.0.0.1:11434",
92+"http://127.0.0.2:11434",
93+"http://127.255.255.254:11434",
94+"http://0.0.0.0:11434",
95+"http://[::1]:11434",
96+"http://my-rig.local:11434",
97+"http://10.0.0.5:11434",
98+"http://172.16.5.10:11434",
99+"http://172.31.99.1:11434",
100+"http://192.168.1.20:11434",
101+"http://100.64.0.5:11434",
102+"http://100.127.255.254:11434",
103+// RFC 4193 IPv6 unique local (Tailscale IPv6 mesh fd7a:115c:a1e0::/48
104+// falls inside fc00::/7).
105+"http://[fc00::1]:11434",
106+"http://[fd00::1]:11434",
107+"http://[fd7a:115c:a1e0::dead:beef]:11434",
108+"http://[fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff]:11434",
109+// RFC 4291 IPv6 link-local.
110+"http://[fe80::1]:11434",
111+"http://[fe9a::1]:11434",
112+"http://[feab:cd::1]:11434",
113+"http://[febf::1]:11434",
114+])("disables the default idle watchdog for local provider baseUrl %s", (baseUrl) => {
115+expect(resolveLlmIdleTimeoutMs({ model: { baseUrl } })).toBe(0);
116+});
117+118+it.each([
119+"http://172.32.0.1:11434",
120+"http://192.169.1.1:11434",
121+"http://100.63.255.254:11434",
122+"http://100.128.0.1:11434",
123+])("keeps the default idle watchdog for non-private IPv4 baseUrl %s", (baseUrl) => {
124+expect(resolveLlmIdleTimeoutMs({ model: { baseUrl } })).toBe(DEFAULT_LLM_IDLE_TIMEOUT_MS);
125+});
126+127+// Node's URL parser normalizes every IPv4-mapped loopback form
128+// (`::ffff:127.0.0.1`, `::ffff:7F00:1`, mixed case, …) to the canonical
129+// `::ffff:7f00:1`. Exercise the user-facing input shapes here so the full
130+// parse → lowercase → bracket-strip → exact-match chain is regression-tested
131+// against future URL parser behavior, not just the canonical literal.
132+it.each([
133+"http://[::ffff:127.0.0.1]:11434",
134+"http://[::ffff:7f00:1]:11434",
135+"http://[::FFFF:127.0.0.1]:11434",
136+])("disables the default idle watchdog for IPv4-mapped loopback baseUrl %s", (baseUrl) => {
137+expect(resolveLlmIdleTimeoutMs({ model: { baseUrl } })).toBe(0);
138+});
139+140+it.each([
141+// Just outside fc00::/7 (fe.. and 00fc::/16 are not unique-local).
142+"http://[fec0::1]:11434",
143+"http://[fbff::1]:11434",
144+// Just outside fe80::/10 (fec0:: was deprecated site-local, fe7f:: not LL).
145+"http://[fe7f::1]:11434",
146+// Public IPv6.
147+"http://[2001:db8::1]:11434",
148+// Abbreviated `fc::1` expands to 00fc:0:0:...:1, first byte is 0x00, not
149+// 0xfc — outside fc00::/7. Strict first-hextet match keeps this remote.
150+"http://[fc::1]:11434",
151+// IPv4-mapped IPv6 outside loopback (private RFC 1918 in mapped form is
152+// intentionally not matched, mirroring the SSRF policy helper).
153+"http://[::ffff:10.0.0.5]:11434",
154+"http://[::ffff:192.168.1.20]:11434",
155+])("keeps the default idle watchdog for non-private IPv6 baseUrl %s", (baseUrl) => {
156+expect(resolveLlmIdleTimeoutMs({ model: { baseUrl } })).toBe(DEFAULT_LLM_IDLE_TIMEOUT_MS);
157+});
158+159+it.each([
160+"http://10.0.0.5evil:11434",
161+"http://127.0.0.1foo:11434",
162+"http://192.168.1.20attacker.com:11434",
163+"http://10.0.0.5.evil.com:11434",
164+"http://1.2.3.4.5:11434",
165+])(
166+"keeps the default idle watchdog for numeric-looking hostnames that are not IPv4 literals (%s)",
167+(baseUrl) => {
168+expect(resolveLlmIdleTimeoutMs({ model: { baseUrl } })).toBe(DEFAULT_LLM_IDLE_TIMEOUT_MS);
169+},
170+);
171+172+it("keeps the default idle watchdog for remote provider baseUrls", () => {
173+expect(resolveLlmIdleTimeoutMs({ model: { baseUrl: "https://api.openai.com/v1" } })).toBe(
174+DEFAULT_LLM_IDLE_TIMEOUT_MS,
175+);
176+expect(resolveLlmIdleTimeoutMs({ model: { baseUrl: "https://ollama.com" } })).toBe(
177+DEFAULT_LLM_IDLE_TIMEOUT_MS,
178+);
179+});
180+181+it("ignores malformed baseUrl and keeps the default idle watchdog", () => {
182+expect(resolveLlmIdleTimeoutMs({ model: { baseUrl: "not-a-url" } })).toBe(
183+DEFAULT_LLM_IDLE_TIMEOUT_MS,
184+);
185+expect(resolveLlmIdleTimeoutMs({ model: { baseUrl: "" } })).toBe(DEFAULT_LLM_IDLE_TIMEOUT_MS);
186+});
187+188+it("still honors an explicit provider request timeout for local providers", () => {
189+expect(
190+resolveLlmIdleTimeoutMs({
191+model: { baseUrl: "http://127.0.0.1:11434" },
192+modelRequestTimeoutMs: 600_000,
193+}),
194+).toBe(600_000);
195+});
196+197+it("still applies agents.defaults.timeoutSeconds cap for local providers", () => {
198+const cfg = { agents: { defaults: { timeoutSeconds: 30 } } } as OpenClawConfig;
199+expect(resolveLlmIdleTimeoutMs({ cfg, model: { baseUrl: "http://127.0.0.1:11434" } })).toBe(
200+30_000,
201+);
202+});
88203});
8920490205describe("streamWithIdleTimeout", () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。