Bug Description
The LLM stream connection timeout (CONNECT_STREAM_TIMEOUT_MS = 30s) fires prematurely because the timeout timer starts before the HTTP request is actually sent.
Root Cause
In packages/opencode/src/session/llm.ts, the execution order is:
Line 448: request object created → arm() called → 30s timer starts ticking
Line 520: yield* run(...) ← async setup work begins here
The run() function (lines 78-377) performs several async operations after the timer has already started:
| Step |
Lines |
Operation |
| 1 |
92-100 |
yield* Effect.all(...) — concurrent fetch of provider, config, auth |
| 2 |
120-124 |
yield* plugin.trigger("experimental.chat.system.transform") |
| 3 |
168-186 |
yield* plugin.trigger("chat.params") |
| 4 |
188-200 |
yield* plugin.trigger("chat.headers") |
| 5 |
348 |
streamText({...}) — HTTP request actually starts here |
If steps 1-4 take ~5 seconds, the actual time waiting for the provider's first token is only ~25 seconds instead of the intended 30 seconds.
Symptoms
- Users report timeout errors before 30 seconds of actual waiting
- Error message:
LLM stream connection timed out after 30000ms without provider progress
- Affects providers with higher latency (e.g., domestic China APIs like Kimi, Xiaomi Token Plan)
- 12 occurrences found in a single user's database
Additional Issues
- No retry on timeout: The
SessionRetry.policy in retry.ts does not recognize connection timeout errors as retryable — only API errors (5xx, rate limit) are retried.
- Not configurable:
connectTimeoutMs parameter exists in StreamInput but is never passed from any configuration source.
Expected Behavior
The 30-second timeout should measure the time from when the HTTP request is sent to when the first provider progress event is received, not including internal setup time.
Suggested Fix
Move the arm() call to after run() returns, so the timeout only measures actual network + provider response time.
Bug Description
The LLM stream connection timeout (
CONNECT_STREAM_TIMEOUT_MS = 30s) fires prematurely because the timeout timer starts before the HTTP request is actually sent.Root Cause
In
packages/opencode/src/session/llm.ts, the execution order is:The
run()function (lines 78-377) performs several async operations after the timer has already started:yield* Effect.all(...)— concurrent fetch of provider, config, authyield* plugin.trigger("experimental.chat.system.transform")yield* plugin.trigger("chat.params")yield* plugin.trigger("chat.headers")streamText({...})— HTTP request actually starts hereIf steps 1-4 take ~5 seconds, the actual time waiting for the provider's first token is only ~25 seconds instead of the intended 30 seconds.
Symptoms
LLM stream connection timed out after 30000ms without provider progressAdditional Issues
SessionRetry.policyinretry.tsdoes not recognize connection timeout errors as retryable — only API errors (5xx, rate limit) are retried.connectTimeoutMsparameter exists inStreamInputbut is never passed from any configuration source.Expected Behavior
The 30-second timeout should measure the time from when the HTTP request is sent to when the first provider progress event is received, not including internal setup time.
Suggested Fix
Move the
arm()call to afterrun()returns, so the timeout only measures actual network + provider response time.