From 386cb0466c2c036da555bf42dca3e8cd5ec44a31 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Tue, 19 May 2026 15:59:37 +0800 Subject: [PATCH 1/3] fix: widen LLM connect timeout for reasoning models The 30s first-progress watchdog in session/llm.ts aborts reasoning-model streams whose first observable provider event arrives later than the ceiling. This is reproducible with OpenAI gpt-5.5 on long sessions and was missed by #729 (which only fixed the timer-start moment). Inject a 120s connect timeout via a new ProviderTransform.streamTimeouts helper, gated on model.capabilities.reasoning. Apply it at the two production llm.stream() call sites (processor main response + prompt title generation) with helper-first spread order so any caller-provided StreamInput.connectTimeoutMs still wins. Three contract tests in transform.test.ts: - policy floor: helper output exceeds CONNECT_STREAM_TIMEOUT_MS - routing: reasoning emits override, non-reasoning emits empty - caller override precedence: explicit StreamInput value wins Out of scope, tracked separately on the issue: - SessionRetry.policy.retryable() does not classify local timeouts - watchdog architecture rewrite (typed errors, wall-clock budget) - mid-stream "terminated" errors (separate incident, separate PR) Refs #755 --- packages/opencode/src/provider/transform.ts | 9 ++++ packages/opencode/src/session/processor.ts | 7 ++- packages/opencode/src/session/prompt.ts | 1 + .../opencode/test/provider/transform.test.ts | 54 +++++++++++++++++++ 4 files changed, 70 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 7bfee4869..4686a2ae4 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -1085,6 +1085,13 @@ export function variants(model: Provider.Model): Record handleEvent(event)), diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 2f4620ef3..0a08e4fca 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -462,6 +462,7 @@ export const layer = Layer.effect( titleGenerationProgress.set(input.session.id, { startedAt }) const titleExit = yield* llm .stream({ + ...ProviderTransform.streamTimeouts(mdl), agent: ag, user: firstInfo, system: [], diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 2804820a2..8d0a1e064 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test" import { ProviderTransform } from "../../src/provider" import { ModelID, ProviderID } from "../../src/provider/schema" +import { LLM } from "../../src/session/llm" describe("ProviderTransform.options - setCacheKey", () => { const sessionID = "test-session-123" @@ -4720,3 +4721,56 @@ describe("ProviderTransform.variants", () => { }) }) }) + +describe("ProviderTransform.streamTimeouts", () => { + const baseModel = { + id: "test/test-model", + providerID: "test", + api: { + id: "test-model", + url: "https://api.test.com", + npm: "@ai-sdk/openai", + }, + name: "Test Model", + capabilities: { + temperature: true, + reasoning: false, + attachment: false, + toolcall: true, + input: { text: true, audio: false, image: false, video: false, pdf: false }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: false, + }, + cost: { input: 0, output: 0, cache: { read: 0, write: 0 } }, + limit: { context: 100_000, output: 8_192 }, + status: "active", + options: {}, + headers: {}, + release_date: "2024-01-01", + } as any + + const reasoningModel = { + ...baseModel, + capabilities: { ...baseModel.capabilities, reasoning: true }, + } + const nonReasoningModel = baseModel + + test("policy floor: reasoning model connect timeout strictly exceeds default", () => { + const result = ProviderTransform.streamTimeouts(reasoningModel) + expect(result.connectTimeoutMs).toBeDefined() + expect(result.connectTimeoutMs!).toBeGreaterThan(LLM.CONNECT_STREAM_TIMEOUT_MS) + }) + + test("routing contract: reasoning model emits override, non-reasoning model emits empty", () => { + expect(ProviderTransform.streamTimeouts(reasoningModel).connectTimeoutMs).toBeGreaterThan(0) + expect(ProviderTransform.streamTimeouts(nonReasoningModel).connectTimeoutMs).toBeUndefined() + }) + + // Mirrors the helper-first spread order at the production call sites so a + // caller-provided override on StreamInput still wins after the helper is applied. + test("caller override precedence: explicit connectTimeoutMs wins over helper", () => { + const callerInput = { connectTimeoutMs: 5_000 } + const merged = { ...ProviderTransform.streamTimeouts(reasoningModel), ...callerInput } + expect(merged.connectTimeoutMs).toBe(5_000) + }) +}) From d44b7eddda2a9cff65678c6d00b5599d4d817a04 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Tue, 19 May 2026 16:06:29 +0800 Subject: [PATCH 2/3] test: tighten reasoning timeout policy floor to 90s MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Crosscheck flagged the original >30s assertion as too loose — a regression that dropped the helper value to 31s would still pass. Add a >=90_000 lower bound; 90s is the lowest ceiling considered for reasoning models in #755 discussion, so this floor codifies the policy direction without pinning the chosen 120s constant. Refs #755 --- packages/opencode/test/provider/transform.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 8d0a1e064..85287d448 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -4755,10 +4755,14 @@ describe("ProviderTransform.streamTimeouts", () => { } const nonReasoningModel = baseModel - test("policy floor: reasoning model connect timeout strictly exceeds default", () => { + // Floor of 90_000ms guards against a regression like 31s that would still + // exceed the 30s default but defeat the purpose of the widened ceiling. + // 90s is the lowest value considered for reasoning models in #755. + test("policy floor: reasoning model connect timeout meets minimum ceiling", () => { const result = ProviderTransform.streamTimeouts(reasoningModel) expect(result.connectTimeoutMs).toBeDefined() expect(result.connectTimeoutMs!).toBeGreaterThan(LLM.CONNECT_STREAM_TIMEOUT_MS) + expect(result.connectTimeoutMs!).toBeGreaterThanOrEqual(90_000) }) test("routing contract: reasoning model emits override, non-reasoning model emits empty", () => { From a8268e75077e19bb44c3a326e21201bff58c625d Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Tue, 19 May 2026 16:29:19 +0800 Subject: [PATCH 3/3] docs: note streamTimeouts must be applied at every llm.stream call site MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GPT Pro pre-merge review noted the helper-spread convention is only enforceable by code reading today. Add JSDoc so future readers see the "spread at every call site" expectation at the helper definition. Not a test addition — three internal/external reviewers agreed adding a heavy integration test for a 2-call-site contract is overkill. Refs #755 --- packages/opencode/src/provider/transform.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 4686a2ae4..a0a492652 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -1087,6 +1087,10 @@ export function variants(model: Provider.Model): Record