Skip to content

fix: propagate timeoutMs to guarded dispatchers (local LLM 60s timeout) #70829

Description

@DranboFieldston

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:

  1. `src/infra/net/undici-runtime.ts`: `createHttp1Agent()`, `createHttp1EnvHttpProxyAgent()`, `createHttp1ProxyAgent()` accept `timeoutMs` and apply `bodyTimeout`/`headersTimeout` to dispatcher options.
  2. `src/infra/net/ssrf.ts`: `createPinnedDispatcher()` accepts `timeoutMs` and passes it through to HTTP/1 dispatcher creation.
  3. `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.
  4. `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:

  1. Build passes: `pnpm run build`
  2. Local LLM requests complete without 60s timeout
  3. Guarded fetch still creates per-request isolated dispatchers (SSRF protection unchanged)
  4. Global dispatcher timeout still works for non-guarded requests

Metadata

Metadata

Assignees

Labels

dedupe:parentPrimary canonical item in dedupe cluster

Type

No type

Fields

Priority

None yet

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions