Problem
Local LLM requests via openai-completions (and other provider transports) are killed at exactly 60 seconds, despite having timeoutSeconds: 900 (15 min) configured in openclaw.json.
Root Cause
Timeout propagation gap in the guarded fetch path.
OpenClaw correctly sets a global undici dispatcher timeout via ensureGlobalUndiciStreamTimeouts() in attempt-http-runtime.ts. However, the LLM transport path (openai-transport-stream.ts, anthropic-transport-stream.ts) uses buildGuardedModelFetch(), which calls fetchWithSsrFGuard().
fetchWithSsrFGuard() creates isolated dispatchers per-request for SSRF security (DNS pinning, private network isolation). These isolated dispatchers call createHttp1Agent(), createHttp1EnvHttpProxyAgent(), createHttp1ProxyAgent(), or createPinnedDispatcher() from undici-runtime.ts — and none of them currently accept or apply the timeoutMs parameter. They fall back to undici's hardcoded default of 60s for bodyTimeout and headersTimeout.
The 15-minute AbortSignal is correctly created (via buildTimeoutAbortSignal()), but undici kills the connection at 60s before the AbortSignal fires at 15 min.
The Flow
```
openclaw.json → timeoutSeconds: 900 (15 min)
↓
attempt.ts → params.timeoutMs = 900000
↓
attempt-http-runtime.ts → ensureGlobalUndiciStreamTimeouts({ timeoutMs }) ✅ (global dispatcher, 15 min)
↓
openai-transport-stream.ts → buildGuardedModelFetch(model) ❌ (no timeoutMs passed)
↓
provider-transport-fetch.ts → fetchWithSsrFGuard({ ..., timeoutMs: undefined }) ❌ (passed to AbortSignal only)
↓
fetch-guard.ts → createHttp1Agent() / createPinnedDispatcher() ❌ (no timeoutMs)
↓
undici-runtime.ts → new Agent() with no bodyTimeout/headersTimeout → undici default 60s ❌
```
Investigation
Proposed Fix
Thread `timeoutMs` through the dispatcher creation chain:
- `src/infra/net/undici-runtime.ts`: `createHttp1Agent()`, `createHttp1EnvHttpProxyAgent()`, `createHttp1ProxyAgent()` accept `timeoutMs` and apply `bodyTimeout`/`headersTimeout` to dispatcher options.
- `src/infra/net/ssrf.ts`: `createPinnedDispatcher()` accepts `timeoutMs` and passes it through to HTTP/1 dispatcher creation.
- `src/infra/net/fetch-guard.ts`: `fetchWithSsrFGuard()` reads timeout from `params.timeoutMs` or falls back to the global dispatcher's `bodyTimeout` via `getGlobalDispatcher()`. All dispatcher creation calls receive the timeout value.
- `src/agents/provider-transport-fetch.ts`: `buildGuardedModelFetch()` accepts optional `timeoutMs` and passes it to `fetchWithSsrFGuard()`.
Minimal Patch (4 files)
See PR #XXX (link after created).
Risks
- Higher timeouts mean connections stay open longer (expected for slow LLM operations)
- All changes are additive (optional parameters), fully backward compatible
- No changes to public API surface
Verification
After applying the fix:
- Build passes: `pnpm run build`
- Local LLM requests complete without 60s timeout
- Guarded fetch still creates per-request isolated dispatchers (SSRF protection unchanged)
- Global dispatcher timeout still works for non-guarded requests
Problem
Local LLM requests via
openai-completions(and other provider transports) are killed at exactly 60 seconds, despite havingtimeoutSeconds: 900(15 min) configured inopenclaw.json.Root Cause
Timeout propagation gap in the guarded fetch path.
OpenClaw correctly sets a global undici dispatcher timeout via
ensureGlobalUndiciStreamTimeouts()inattempt-http-runtime.ts. However, the LLM transport path (openai-transport-stream.ts,anthropic-transport-stream.ts) usesbuildGuardedModelFetch(), which callsfetchWithSsrFGuard().fetchWithSsrFGuard()creates isolated dispatchers per-request for SSRF security (DNS pinning, private network isolation). These isolated dispatchers callcreateHttp1Agent(),createHttp1EnvHttpProxyAgent(),createHttp1ProxyAgent(), orcreatePinnedDispatcher()fromundici-runtime.ts— and none of them currently accept or apply thetimeoutMsparameter. They fall back to undici's hardcoded default of 60s forbodyTimeoutandheadersTimeout.The 15-minute
AbortSignalis correctly created (viabuildTimeoutAbortSignal()), but undici kills the connection at 60s before the AbortSignal fires at 15 min.The Flow
```
openclaw.json → timeoutSeconds: 900 (15 min)
↓
attempt.ts → params.timeoutMs = 900000
↓
attempt-http-runtime.ts → ensureGlobalUndiciStreamTimeouts({ timeoutMs }) ✅ (global dispatcher, 15 min)
↓
openai-transport-stream.ts → buildGuardedModelFetch(model) ❌ (no timeoutMs passed)
↓
provider-transport-fetch.ts → fetchWithSsrFGuard({ ..., timeoutMs: undefined }) ❌ (passed to AbortSignal only)
↓
fetch-guard.ts → createHttp1Agent() / createPinnedDispatcher() ❌ (no timeoutMs)
↓
undici-runtime.ts → new Agent() with no bodyTimeout/headersTimeout → undici default 60s ❌
```
Investigation
Proposed Fix
Thread `timeoutMs` through the dispatcher creation chain:
Minimal Patch (4 files)
See PR #XXX (link after created).
Risks
Verification
After applying the fix: