fix(opencode): default header timeout to five minutes (#46903)

This commit is contained in:
Aiden Cline 2026-09-02 16:00:54 -05:00 committed by GitHub
parent 4eb29a64f0
commit b04697366f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 47 additions and 40 deletions

View file

@ -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({

View file

@ -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"]

View file

@ -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 }) },
)
}),
)

View file

@ -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
/**

View file

@ -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": [

View file

@ -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.