Skip to content

Desktop hits 5-minute Headers Timeout Error with slow local providers #26602

Description

@osamahaltassan

Description

OpenCode Desktop can abort local OpenAI-compatible provider requests after exactly 5 minutes with:

Cannot connect to API: Headers Timeout Error

This happens even when the provider config sets "timeout": false or a much larger timeout. The provider is still working, but OpenCode disconnects and then retries.

This is reproducible with Ollama on CPU when a large prompt takes longer than 300 seconds before response headers or the first streamed token.

Plugins

None

OpenCode version

Current Desktop / dev branch

Steps to reproduce

  1. Configure Desktop with a local OpenAI-compatible provider such as Ollama.
  2. Set the provider timeout to false or a value greater than 300000.
  3. Send a prompt/model request that takes more than 5 minutes before the first response.
  4. Observe Desktop fail with Headers Timeout Error and retry while the provider is still computing.

Screenshot and/or share link

N/A

Operating System

Windows 11

Terminal

N/A

Activity

  1. github-actions commented on May 10, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

  2. osamahaltassan commented on May 10, 2026

    @osamahaltassan
    Author

    Checked #15555 and #20119. They are related, but I think this issue is a different request path.

    Those issues describe SDK client.session.prompt() timing out while a Node SDK client waits for the OpenCode server response. This Desktop case uses the local Desktop/sidecar flow: the UI calls prompt_async, the local API returns immediately, and the long wait happens later when OpenCode's Node/Electron sidecar calls the OpenAI-compatible provider.

    So the shared cause is Node/Undici's 300s headersTimeout, but the failing fetch is different:

    PR #26599 targets the provider fetch path in packages/opencode/src/provider/provider.ts, so it does not claim to close the SDK-client timeout issues.

  3. lolhunter121 commented on May 20, 2026

    @lolhunter121

    Same for Windows client. Right 300 seconds and he drop session

  4. C485 commented on May 25, 2026

    @C485

    Can we push this fix ASAP? I had to go down to models like 4b because of the size of the context that is used the initial response takes more than 5 minutes.

  5. RX-LOST commented on Jun 8, 2026

    @RX-LOST

    I still have this issue. Even with the newest update, my local modes like devstral small 2 take too long to process context, and opencode terminates it.

  6. C485 commented on Jun 8, 2026

    @C485

    I still have this issue. Even with the newest update, my local modes like devstral small 2 take too long to process context, and opencode terminates it.

    For now you need to use opencode cli, they wont fix it soon

  7. bashrusakh commented on Jun 9, 2026

    @bashrusakh

    hey wont fix it soon

    why? its clearly a High / P1 bug. opencode gui unusible with local models - 5 minutes its not a big deal for 27-32b qwen to proccess 128-256k ctx and give answer. it can be waaay longer

  8. osamahaltassan commented on Jun 10, 2026

    @osamahaltassan
    Author

    PR automatically closed
    #26599 (comment)

  9. Lemtsu commented on Jul 7, 2026

    @Lemtsu

    it's still not fixed

  10. bashrusakh commented on Jul 7, 2026

    @bashrusakh

    it's still not fixed

    #33535

  11. stevbev commented on Aug 16, 2026

    @stevbev

    @Hona Can this issue be prioritized so that #33535 can be merged to resolve this severe problem? Not passing the timeouts from the config makes the opencode desktop useless when using local models.

    Cheers mate!

  12. malek262 commented on Sep 12, 2026

    @malek262

    Still reproducing on the current release (1.18.30, Linux), and it is not limited to local models or to one provider.

    I run a local LLM router (key rotation and fallback toward cloud providers) and OpenCode talks to it directly on localhost, so I can see both sides of every request. I hit this with Alibaba Model Studio (qwen3.8-max, reasoning_effort medium) and with Perplexity's agent API (claude family reasoning models). The common factor is any upstream that thinks silently for more than 300s before the first byte. While the upstream has produced no response headers there is nothing for the router to forward, and Desktop kills the request at exactly five minutes.

    Router side logs from one evening (time from request receipt to client socket close; not a single response byte had been sent on any of these):

    2026-09-11 17:01  300002 ms
    2026-09-11 18:16  300008 ms
    2026-09-11 18:49  300010 ms
    2026-09-12 20:00  300002 ms
    2026-09-12 20:06  299974 ms
    2026-09-12 20:32  299992 ms
    

    Every abort lands at 300.0s within about 20ms. After each death the client retries (the next attempt starts about 2s after the previous one died, which matches SessionRetry with RETRY_INITIAL_DELAY = 2000 and RETRY_MAX_RETRIES = 5), and the retry dies the same way. A single prompt can burn half an hour across a chain of identical attempts before the user sees an error.

    The provider UA on these requests reports runtime/node.js/24, so it is the Electron sidecar running the Node server bundle, where global fetch is undici.

    Two layers are involved on 1.18.30:

    1. With default config, OpenCode's own timer fires first. In packages/opencode/src/provider/provider.ts:

      const headerTimeout = options["headerTimeout"] ?? 300_000
      

      which aborts with ProviderError.HeaderTimeoutError ("Provider response headers timed out after 300000ms"). That part is documented and intentional.

    2. The escape hatch does not work on Desktop. Raising the value or setting "headerTimeout": false only removes OpenCode's timer. The sidecar's fetch is still Node's undici with its default 300s headersTimeout, and nothing in the codebase configures a dispatcher (there is no setGlobalDispatcher call anywhere; the timeout: false passed in provider.ts is a Bun only option, the ts-ignore right above it links to an oven-sh/bun issue). So on Desktop a request can never wait more than 300s for response headers, regardless of config, while the CLI on Bun honors the setting. That is the asymmetry people in this thread were told to work around by using the CLI.

    The same wall also appears mid stream, which is the clearest confirmation that this is a transport level inactivity timer and not an application timeout: with slow reasoning models, a stream that has already started dies the same way when no bytes arrive for 300s (undici's bodyTimeout). When my router emits an SSE comment line (": keepalive") every 15s, the same streams survive the silence and complete normally.

    Same root cause as the original report (the "Headers Timeout Error" there is undici's HeadersTimeoutError), observed here from the server side. Closed weight reasoning models routinely take minutes before the first streamed byte now, so the 300s wall gets hit in normal daily use on Desktop, with remote providers as well as local models.

    PR #33535 had the right fix (map the provider timeout options to an undici Agent with headersTimeout: 0 / bodyTimeout: 0 when running on Node) but it was closed by the automated cleanup without a review. Is there appetite to land something like that? I can test a build against the router setup above and report back.

  13. Sugaroverdose commented on Sep 14, 2026

    @Sugaroverdose

    Three data points from a Magic Context (dreamer task) setup that may update the state of this thread:

    1. The fix PR is dead. #33535 ("apply undici headersTimeout for long-running requests", the SDK client→server dispatcher fix) was closed without merging on 2026-07-23. The 08-16 comment in this thread still asks for it to be prioritized as if it were alive — as of today there is no active fix path on this issue, and the repro was confirmed on 1.18.30 on 09-12. This issue currently needs a fix, not a re-prioritized zombie PR.

    2. Configured provider timeouts do not reach this layer. The desktop bundle contains no headersTimeout/bodyTimeout/Dispatcher configuration; the SDK's caller-side timeout/AbortSignal options are cancellation only. So a user who configures, e.g., a 3 000 s provider request timeout still dies at ~302 s — the configured wait budget never applies to the headers phase of the non-streaming session.prompt RPC (whose headers arrive only at turn completion).

    3. A new failure class: multi-call agentic turns die mid-turn, not just single slow calls. In our environment a 150-call agentic task (wall time 5–19 min depending on concurrent load — the server's decode rate is 200+ tok/s; the variance is admission queuing) dies at ~302 s with only a small fraction of the work committed, and the driver's retry re-scans the whole scope. Same HeadersTimeoutError signature, but it turns a long-but-successful run into a partial-failure loop.

    For reference, the stopgap we filed on the Magic Context side (per-category task scoping so runs fit the window as it exists today) is cortexkit/magic-context#451. The durable fix remains applying a configurable/extended undici headers+body timeout in the SDK dispatcher, as #33535 attempted.

  14. bashrusakh commented on Sep 15, 2026

    @bashrusakh
    1. The fix PR is dead. #33535 ("apply undici headersTimeout for long-running requests", the SDK client→server dispatcher fix) was closed without merging on 2026-07-23. The 08-16 comment in this thread still asks for it to be prioritized as if it were alive — as of today there is no active fix path on this issue, and the repro was confirmed on 1.18.30 on 09-12. This issue currently needs a fix, not a re-prioritized zombie PR.

    its not dead - its closed without review by opencode strange policy.
    it can be re-opened any time, or asked to rebase to current main if needed. BUT - no-one (inside opencode team) cares

  15. hidp123 commented on Sep 17, 2026

    @hidp123

    Any update on this? fix will help tremendously.

  16. hidp123 commented on Sep 17, 2026

    @hidp123

    A patch that worked for me to extend the timeout time, made using qwen3.8-max, based on a previous patch made using same for cline:

    <#
    .SYNOPSIS
        OpenCode Desktop Timeout Patcher for LM Studio (v1.4)
    .DESCRIPTION
        Fixes the hardcoded 5-minute timeout in OpenCode Desktop when using slow local models.
        Auto-detects install location (even if already patched), extracts app.asar, and injects an undici dispatcher.
    #>
    
    $ErrorActionPreference = "Stop"
    $TimeoutMs   = 7200000   # 120 minutes
    $PatchDir    = Join-Path $env:USERPROFILE ".opencode-patch"
    $PatchTag    = "/* === opencode lmstudio timeout patch === */"
    $PatchEndTag = "/* === end opencode lmstudio timeout patch === */"
    
    function Write-Step { param($m) Write-Host "`n==> $m" -ForegroundColor Cyan }
    function Write-Ok   { param($m) Write-Host "    OK: $m" -ForegroundColor Green }
    function Write-Warn { param($m) Write-Host "    WARN: $m" -ForegroundColor Yellow }
    function Write-Skip { param($m) Write-Host "    SKIP: $m" -ForegroundColor DarkYellow }
    function Write-Err  { param($m) Write-Host "    ERR: $m" -ForegroundColor Red }
    
    # --- 1. Locate OpenCode Desktop Installation ---
    Write-Step "Locating OpenCode Desktop Installation"
    $searchRoots = @(
        (Join-Path $env:LOCALAPPDATA "Programs"),
        $env:ProgramFiles,
        ${env:ProgramFiles(x86)}
    ) | Where-Object { $_ -and (Test-Path $_) }
    
    $installDirs = @()
    foreach ($root in $searchRoots) {
        $hits = Get-ChildItem $root -Directory -ErrorAction SilentlyContinue | 
                Where-Object { $_.Name -match 'opencode' } |
                Where-Object { 
                    $resPath = Join-Path $_.FullName 'resources'
                    # FIX: Check for app.asar OR app.asar.orig so it works on re-runs
                    (Test-Path $resPath) -and (
                        (Test-Path (Join-Path $resPath 'app.asar')) -or 
                        (Test-Path (Join-Path $resPath 'app.asar.orig'))
                    )
                }
        if ($hits) { $installDirs += $hits }
    }
    
    if ($installDirs.Count -eq 0) {
        Write-Err "OpenCode Desktop installation not found."
        exit 1
    }
    
    $installDir = $installDirs | Sort-Object { 
        $asar = Join-Path $_.FullName 'resources\app.asar'
        $orig = Join-Path $_.FullName 'resources\app.asar.orig'
        if (Test-Path $asar) { (Get-Item $asar).LastWriteTime } elseif (Test-Path $orig) { (Get-Item $orig).LastWriteTime } else { 0 }
    } -Descending | Select-Object -First 1
    
    $resDir = Join-Path $installDir.FullName "resources"
    $asarFile = Join-Path $resDir "app.asar"
    $appDir = Join-Path $resDir "app"
    
    Write-Ok "Found: $($installDir.FullName)"
    
    # --- 2. Extract app.asar ---
    Write-Step "Extracting app.asar"
    if (Test-Path $appDir) {
        Write-Skip "app\ folder already exists."
    } else {
        if (-not (Test-Path $asarFile)) {
            Write-Err "app\ folder missing and app.asar not found to extract from."
            exit 1
        }
        if (-not (Get-Command npx -ErrorAction SilentlyContinue)) {
            Write-Err "npx not found. Please install Node.js."
            exit 1
        }
        Push-Location $resDir
        try {
            & npx --yes "@electron/asar" extract "app.asar" "app"
            if ($LASTEXITCODE -ne 0) { throw "Extraction failed" }
        } finally { Pop-Location }
        Write-Ok "Extracted to: $appDir"
    }
    
    $appPkg = Join-Path $appDir "package.json"
    if (-not (Test-Path $appPkg)) {
        Write-Err "app/package.json missing after extraction."
        exit 1
    }
    
    $asarBackup = Join-Path $resDir "app.asar.orig"
    if (Test-Path $asarFile) {
        if (-not (Test-Path $asarBackup)) {
            Rename-Item $asarFile $asarBackup
            Write-Ok "Backed up: app.asar -> app.asar.orig"
        } else {
            Remove-Item $asarFile -Force -ErrorAction SilentlyContinue
        }
    }
    
    $unpackedDir = Join-Path $resDir "app.asar.unpacked"
    if (Test-Path $unpackedDir) {
        Copy-Item "$unpackedDir\*" $appDir -Recurse -Force -ErrorAction SilentlyContinue
    }
    
    # --- 3. Locate Target File ---
    Write-Step "Locating target bundle"
    $sidecarFile = Join-Path $appDir "out\main\sidecar.js"
    $indexFile   = Join-Path $appDir "out\main\index.js"
    
    $targetFile = $null
    if (Test-Path $sidecarFile) { $targetFile = $sidecarFile; Write-Ok "Target: sidecar.js" }
    elseif (Test-Path $indexFile) { $targetFile = $indexFile; Write-Warn "Falling back to index.js" }
    else { Write-Err "Could not find sidecar.js or index.js"; exit 1 }
    
    # --- 4. Ensure undici is installed (BOM-less JSON) ---
    Write-Step "Ensuring undici is installed"
    if (-not (Test-Path $PatchDir)) { New-Item -ItemType Directory -Path $PatchDir | Out-Null }
    $undiciDir = Join-Path $PatchDir "node_modules\undici"
    
    $clineUndici = Join-Path $env:USERPROFILE ".cline-patch\node_modules\undici"
    if (-not (Test-Path $undiciDir) -and (Test-Path $clineUndici)) {
        Copy-Item $clineUndici $undiciDir -Recurse -Force
        Write-Ok "Reused undici from .cline-patch"
    }
    
    if (-not (Test-Path $undiciDir)) {
        Push-Location $PatchDir
        try {
            $pkgJsonPath = Join-Path $PatchDir "package.json"
            if (-not (Test-Path $pkgJsonPath)) {
                $pkgJsonContent = "{`"name`": `"opencode-patch`", `"version`": `"1.0.0`", `"private`": true}"
                [System.IO.File]::WriteAllText($pkgJsonPath, $pkgJsonContent, [System.Text.UTF8Encoding]::new($false))
            }
            npm install undici --no-fund --no-audit
            if ($LASTEXITCODE -ne 0) { throw "npm install failed" }
        } finally { Pop-Location }
    }
    Write-Ok "undici ready"
    
    # --- 5. Patch the Bundle ---
    Write-Step "Patching $targetFile"
    $content = [System.IO.File]::ReadAllText($targetFile)
    
    $patchRegex = '(?s)/\* === opencode lmstudio timeout patch === \*/.*?/\* === end opencode lmstudio timeout patch === \*/\r?\n?'
    $importRegex = '(?m)^import \{ createRequire as __oc_createRequire \} from "node:module";\r?\n?'
    
    $existingTimeout = $null
    if ($content -match $patchRegex) {
        if ($matches[0] -match 'headersTimeout:\s*(\d+)') { $existingTimeout = [int]$matches[1] }
    }
    
    $isEsm = $content -match '(?m)^import\s'
    $hasCreateRequire = $content -match 'import\s+.*?\bcreateRequire\b'
    
    if ($null -ne $existingTimeout -and $existingTimeout -eq $TimeoutMs) {
        Write-Skip "Already patched with $TimeoutMs ms"
    } else {
        if ($null -ne $existingTimeout) {
            Write-Warn "Updating existing patch to $TimeoutMs ms"
            $content = $content -replace $patchRegex, ''
            $content = $content -replace $importRegex, ''
        } else {
            $bak = $targetFile + ".bak"
            $i = 1
            while (Test-Path $bak) { $bak = $targetFile + ".bak$i"; $i++ }
            Copy-Item $targetFile $bak -Force
            Write-Ok "Backup: $bak"
        }
    
        $jsLines = @(
            $PatchTag,
            'try {',
            '  const __oc_require = (typeof require === "function") ? require : (typeof __oc_createRequire === "function" ? __oc_createRequire(import.meta.url) : null);',
            '  if (!__oc_require) throw new Error("No require available");',
            '  const path = __oc_require("path");',
            '  const os = __oc_require("os");',
            '  const undiciPath = path.join(os.homedir(), ".opencode-patch", "node_modules", "undici");',
            '  const { fetch: undiciFetch, Agent } = __oc_require(undiciPath);',
            '  const longAgent = new Agent({',
            '    headersTimeout: __TIMEOUT__,',
            '    bodyTimeout:    __TIMEOUT__,',
            '    connectTimeout: 60000,',
            '  });',
            '  const origFetch = globalThis.fetch;',
            '  const isLocal = (input) => {',
            '    try {',
            '      const u = typeof input === "string" ? input',
            '              : input instanceof URL ? input.href',
            '              : input && typeof input.url === "string" ? input.url',
            '              : String(input);',
            '      return /^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(:\d+)?(\/|$)/.test(u);',
            '    } catch { return false; }',
            '  };',
            '  globalThis.fetch = function opencodeTimeoutFetch(input, init) {',
            '    if (isLocal(input)) {',
            '      return undiciFetch(input, Object.assign({}, init, { dispatcher: longAgent }));',
            '    }',
            '    return origFetch(input, init);',
            '  };',
            '  if (origFetch && typeof origFetch.preconnect === "function") {',
            '    globalThis.fetch.preconnect = origFetch.preconnect.bind(origFetch);',
            '  }',
            '} catch (_e) { /* patch failed; fall back to default behavior */ }',
            $PatchEndTag
        )
    
        $patchBlock = ($jsLines -join "`r`n") -replace '__TIMEOUT__', $TimeoutMs
    
        if ($isEsm -and -not $hasCreateRequire) {
            $patchBlock = "import { createRequire as __oc_createRequire } from `"node:module`";`r`n" + $patchBlock
        }
    
        if ($content.StartsWith("#!")) {
            $nl = $content.IndexOf("`n")
            $newContent = $content.Substring(0, $nl + 1) + $patchBlock + "`r`n" + $content.Substring($nl + 1)
        } else {
            $newContent = $patchBlock + "`r`n" + $content
        }
    
        [System.IO.File]::WriteAllText($targetFile, $newContent, [System.Text.UTF8Encoding]::new($false))
        Write-Ok "Patched with timeout = $TimeoutMs ms"
    }
    
    Write-Host "`n==========================================" -ForegroundColor Green
    Write-Host "All done! Relaunch OpenCode Desktop." -ForegroundColor Green
    Write-Host "To revert: delete resources\app and rename app.asar.orig to app.asar" -ForegroundColor Yellow
    
  17. huornlmj commented on Oct 7, 2026

    @huornlmj

    It's even worse in Opencode v2

  18. Yedigaryan commented on Oct 7, 2026

    @Yedigaryan

    I rebased #50656 on current dev. It sets undici's headersTimeout and bodyTimeout from the provider timeout option on Node (V1 provider, V2 aisdk.ts, and the SDK client). Bun is unchanged. The two earlier attempts at this (#26599, #33535) were closed by the automated PR cleanup, not on review.

    I've only verified it with unit tests so far, not in Desktop against a slow local provider. If you're hitting the 300s timeout, I'd appreciate a note on whether the PR fixes it for you. The cleanup bot closes PRs with fewer than 2 positive reactions after a month, so a 👍 on #50656 also helps.

  19. Yedigaryan commented on Oct 12, 2026

    @Yedigaryan

    Update: #50656 was closed when dev moved to V2, so I ported the fix and opened it again as #54793.

    It targets the V2 provider request paths. On Node, the built-in fetch applies undici's default 300s headersTimeout and bodyTimeout, which is the cutoff at about 5 minutes in this issue. The PR sends provider requests through undici's own fetch with those two timers lifted. V2 already enforces the configured headerTimeout, chunkTimeout and timeout itself, so those settings keep working and the socket-level default stops cutting the request first. Bun is unchanged, since its fetch has no such default.

    I checked it on Node 22 against a local server that delays its response. The built-in fetch fails at 301s with UND_ERR_HEADERS_TIMEOUT, and the new path completes a 620s response. I have not tried it in Desktop against a real slow local model, so a test from someone who hits this would help, especially with huornlmj's V2 case.

    This PR does not touch the SDK client to server path that #33535 targeted. If the error still shows up with #54793 applied, that is a separate path and I would like to hear about it.

    Reactions on the PR help, since the cleanup bot closes PRs with fewer than 2 after a month.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions