From b04697366f05419e9bd7a92f841813dd976161c9 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Wed, 2 Sep 2026 16:00:54 -0500 Subject: [PATCH] fix(opencode): default header timeout to five minutes (#46903) --- packages/core/src/v1/config/provider.ts | 4 +- packages/opencode/src/provider/provider.ts | 2 +- .../test/provider/header-timeout.test.ts | 74 ++++++++++--------- packages/sdk/js/src/v2/gen/types.gen.ts | 2 +- packages/sdk/openapi.json | 2 +- packages/web/src/content/docs/config.mdx | 3 +- 6 files changed, 47 insertions(+), 40 deletions(-) diff --git a/packages/core/src/v1/config/provider.ts b/packages/core/src/v1/config/provider.ts index 2421e13de4..5b6a8133c4 100644 --- a/packages/core/src/v1/config/provider.ts +++ b/packages/core/src/v1/config/provider.ts @@ -108,11 +108,11 @@ export const Info = Schema.Struct({ headerTimeout: Schema.optional( Schema.Union([PositiveInt, Schema.Literal(false)]).annotate({ description: - "Timeout in milliseconds to wait for response headers. Provider integrations may set defaults. Set to false to disable timeout.", + "Timeout in milliseconds to wait for response headers (default: 300000). Set to false to disable timeout.", }), ).annotate({ description: - "Timeout in milliseconds to wait for response headers. Provider integrations may set defaults. Set to false to disable timeout.", + "Timeout in milliseconds to wait for response headers (default: 300000). Set to false to disable timeout.", }), chunkTimeout: Schema.optional( Schema.Union([PositiveInt, Schema.Literal(false)]).annotate({ diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index dc43fbfbdd..72d5a7a593 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -1793,7 +1793,7 @@ const layer = Layer.effect( const customFetch = options["fetch"] const chunkTimeout = options["chunkTimeout"] ?? 300_000 - const headerTimeout = options["headerTimeout"] + const headerTimeout = options["headerTimeout"] ?? 300_000 delete options["chunkTimeout"] delete options["headerTimeout"] diff --git a/packages/opencode/test/provider/header-timeout.test.ts b/packages/opencode/test/provider/header-timeout.test.ts index e0dd19e471..38b884dc7f 100644 --- a/packages/opencode/test/provider/header-timeout.test.ts +++ b/packages/opencode/test/provider/header-timeout.test.ts @@ -48,40 +48,46 @@ it.live("headerTimeout does not abort delayed SSE body after headers arrive", () }), ) -it.live("default chunkTimeout is applied at fetch without changing provider options", () => - Effect.gen(function* () { - const server = yield* Effect.acquireRelease( - Effect.promise(() => delayedBodyServer(250)), - (server) => Effect.sync(() => server.server.close()), - ) +for (const timeout of ["chunkTimeout", "headerTimeout"] as const) { + it.live(`default ${timeout} is applied at fetch without changing provider options`, () => + Effect.gen(function* () { + const server = yield* Effect.acquireRelease( + Effect.promise(() => delayedBodyServer(250)), + (server) => Effect.sync(() => server.server.close()), + ) - yield* provideTmpdirInstance( - () => - Effect.gen(function* () { - const provider = yield* Provider.Service - const configured = yield* provider.getProvider(ProviderV2.ID.make("test")) - const signals: (AbortSignal | null | undefined)[] = [] - configured.options.fetch = (input: RequestInfo | URL, init?: RequestInit) => { - signals.push(init?.signal) - return fetch(input, init) - } - const model = yield* provider.getModel(ProviderV2.ID.make("test"), ModelV2.ID.make("test-model")) - const language = yield* provider.getLanguage(model) - yield* Effect.acquireRelease( - Effect.promise(() => - language.doStream({ prompt: [{ role: "user", content: [{ type: "text", text: "hello" }] }] }), - ), - (result) => Effect.promise(() => result.stream.cancel()), - ) + yield* provideTmpdirInstance( + () => + Effect.gen(function* () { + const provider = yield* Provider.Service + const configured = yield* provider.getProvider(ProviderV2.ID.make("test")) + const signals: (AbortSignal | null | undefined)[] = [] + configured.options.fetch = (input: RequestInfo | URL, init?: RequestInit) => { + signals.push(init?.signal) + return fetch(input, init) + } + const model = yield* provider.getModel(ProviderV2.ID.make("test"), ModelV2.ID.make("test-model")) + const language = yield* provider.getLanguage(model) + yield* Effect.acquireRelease( + Effect.promise(() => + language.doStream({ prompt: [{ role: "user", content: [{ type: "text", text: "hello" }] }] }), + ), + (result) => Effect.promise(() => result.stream.cancel()), + ) - expect(signals).toHaveLength(1) - expect(signals[0]).toBeInstanceOf(AbortSignal) - expect(configured.options.chunkTimeout).toBeUndefined() - }), - { config: providerConfig(server.url) }, - ) - }), -) + expect(signals).toHaveLength(1) + expect(signals[0]).toBeInstanceOf(AbortSignal) + expect(configured.options[timeout]).toBeUndefined() + }), + { + config: providerConfig(server.url, { + [timeout === "chunkTimeout" ? "headerTimeout" : "chunkTimeout"]: false, + }), + }, + ) + }), + ) +} it.live("configured chunkTimeout raises a retryable response stream error when SSE body stalls", () => Effect.gen(function* () { @@ -178,7 +184,7 @@ it.live("headerTimeout aborts when response headers do not arrive", () => }), ) -it.live("headerTimeout is opt-in for non-OpenAI providers", () => +it.live("headerTimeout can be disabled with false for non-OpenAI providers", () => Effect.gen(function* () { const server = yield* Effect.acquireRelease( Effect.promise(() => delayedHeaderServer(100)), @@ -197,7 +203,7 @@ it.live("headerTimeout is opt-in for non-OpenAI providers", () => expect(yield* Effect.promise(() => result.text)).toBe("ok") }), - { config: providerConfig(server.url) }, + { config: providerConfig(server.url, { headerTimeout: false }) }, ) }), ) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 23d1b19649..f06c20cc41 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1751,7 +1751,7 @@ export type ProviderConfig = { */ timeout?: number | false /** - * Timeout in milliseconds to wait for response headers. Provider integrations may set defaults. Set to false to disable timeout. + * Timeout in milliseconds to wait for response headers (default: 300000). Set to false to disable timeout. */ headerTimeout?: number | false /** diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index d9f7579936..e66d140505 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -20780,7 +20780,7 @@ "enum": [false] } ], - "description": "Timeout in milliseconds to wait for response headers. Provider integrations may set defaults. Set to false to disable timeout." + "description": "Timeout in milliseconds to wait for response headers (default: 300000). Set to false to disable timeout." }, "chunkTimeout": { "anyOf": [ diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index 70dc8851d3..22c0640f66 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -374,7 +374,7 @@ You can configure the providers and models you want to use in your OpenCode conf The `small_model` option configures a separate model for lightweight tasks like title generation. By default, OpenCode tries to use a cheaper model if one is available from your provider, otherwise it falls back to your main model. -Provider options can include `timeout`, `chunkTimeout`, and `setCacheKey`: +Provider options can include `timeout`, `headerTimeout`, `chunkTimeout`, and `setCacheKey`: ```json title="opencode.json" { @@ -392,6 +392,7 @@ Provider options can include `timeout`, `chunkTimeout`, and `setCacheKey`: ``` - `timeout` - Request timeout in milliseconds (default: 300000). Set to `false` to disable. +- `headerTimeout` - Timeout in milliseconds to wait for response headers (default: 300000, or 5 minutes). This timer stops once headers arrive and does not limit the streamed response body. Set to `false` to disable. - `chunkTimeout` - Timeout in milliseconds between streamed response chunks (default: 300000, or 5 minutes). If no chunk arrives in time, the request is aborted. Set to `false` to disable. - `setCacheKey` - Ensure a cache key is always set for designated provider.