fix(opencode): default chunk timeout to five minutes (#46890)

This commit is contained in:
Aiden Cline 2026-09-02 15:33:15 -05:00 committed by GitHub
parent 68abdce1a0
commit 4eb29a64f0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 95 additions and 11 deletions

View file

@ -114,9 +114,14 @@ export const Info = Schema.Struct({
description:
"Timeout in milliseconds to wait for response headers. Provider integrations may set defaults. Set to false to disable timeout.",
}),
chunkTimeout: Schema.optional(PositiveInt).annotate({
chunkTimeout: Schema.optional(
Schema.Union([PositiveInt, Schema.Literal(false)]).annotate({
description:
"Timeout in milliseconds between streamed SSE chunks for this provider (default: 300000). If no chunk arrives within this window, the request is aborted. Set to false to disable timeout.",
}),
).annotate({
description:
"Timeout in milliseconds between streamed SSE chunks for this provider. If no chunk arrives within this window, the request is aborted.",
"Timeout in milliseconds between streamed SSE chunks for this provider (default: 300000). If no chunk arrives within this window, the request is aborted. Set to false to disable timeout.",
}),
}),
[Schema.Record(Schema.String, Schema.Any)],

View file

@ -1792,7 +1792,7 @@ const layer = Layer.effect(
if (existing) return existing
const customFetch = options["fetch"]
const chunkTimeout = options["chunkTimeout"]
const chunkTimeout = options["chunkTimeout"] ?? 300_000
const headerTimeout = options["headerTimeout"]
delete options["chunkTimeout"]
delete options["headerTimeout"]

View file

@ -13,6 +13,8 @@ import { Env } from "@/env"
import { Plugin } from "@/plugin"
import { Provider } from "@/provider/provider"
import { ProviderError } from "@/provider/error"
import { MessageV2 } from "@/session/message-v2"
import { SessionRetry } from "@/session/retry"
afterEach(async () => {
await disposeAllInstances()
@ -46,7 +48,42 @@ it.live("headerTimeout does not abort delayed SSE body after headers arrive", ()
}),
)
it.live("chunkTimeout raises a response stream error when SSE body stalls", () =>
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()),
)
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) },
)
}),
)
it.live("configured chunkTimeout raises a retryable response stream error when SSE body stalls", () =>
Effect.gen(function* () {
const server = yield* Effect.acquireRelease(
Effect.promise(() => delayedBodyServer(250)),
@ -74,12 +111,41 @@ it.live("chunkTimeout raises a response stream error when SSE body stalls", () =
}
})
expect(error).toBeInstanceOf(ProviderError.ResponseStreamError)
expect(
SessionRetry.retryable(MessageV2.fromError(error, { providerID: model.providerID }), model.providerID),
).toEqual({ message: "SSE read timed out" })
}),
{ config: providerConfig(server.url, { chunkTimeout: 50 }) },
)
}),
)
it.live("chunkTimeout can be disabled with false", () =>
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"))
expect(configured.options.chunkTimeout).toBe(false)
const model = yield* provider.getModel(ProviderV2.ID.make("test"), ModelV2.ID.make("test-model"))
const result = streamText({
model: yield* provider.getLanguage(model),
messages: [{ role: "user", content: "hello" }],
})
expect(yield* Effect.promise(() => result.text)).toBe("late")
}),
{ config: providerConfig(server.url, { chunkTimeout: false }) },
)
}),
)
it.live("headerTimeout aborts when response headers do not arrive", () =>
Effect.gen(function* () {
const server = yield* Effect.acquireRelease(
@ -136,7 +202,7 @@ it.live("headerTimeout is opt-in for non-OpenAI providers", () =>
}),
)
it.live("OpenAI Codex headerTimeout default can be disabled by config", () =>
it.live("OpenAI Codex header and chunk timeout defaults can be disabled by config", () =>
Effect.gen(function* () {
yield* withAuthContent(
Effect.gen(function* () {
@ -146,8 +212,9 @@ it.live("OpenAI Codex headerTimeout default can be disabled by config", () =>
const provider = yield* Provider.Service
const openai = yield* provider.getProvider(ProviderV2.ID.openai)
expect(openai.options.headerTimeout).toBe(false)
expect(openai.options.chunkTimeout).toBe(false)
}),
{ config: { provider: { openai: { options: { headerTimeout: false } } } } },
{ config: { provider: { openai: { options: { headerTimeout: false, chunkTimeout: false } } } } },
)
}),
)

View file

@ -1754,8 +1754,11 @@ export type ProviderConfig = {
* Timeout in milliseconds to wait for response headers. Provider integrations may set defaults. Set to false to disable timeout.
*/
headerTimeout?: number | false
chunkTimeout?: number
[key: string]: unknown | string | boolean | number | false | number | false | number | undefined
/**
* Timeout in milliseconds between streamed SSE chunks for this provider (default: 300000). If no chunk arrives within this window, the request is aborted. Set to false to disable timeout.
*/
chunkTimeout?: number | false
[key: string]: unknown | string | boolean | number | false | number | false | number | false | undefined
}
models?: {
[key: string]: {

View file

@ -20783,8 +20783,17 @@
"description": "Timeout in milliseconds to wait for response headers. Provider integrations may set defaults. Set to false to disable timeout."
},
"chunkTimeout": {
"type": "integer",
"exclusiveMinimum": 0
"anyOf": [
{
"type": "integer",
"exclusiveMinimum": 0
},
{
"type": "boolean",
"enum": [false]
}
],
"description": "Timeout in milliseconds between streamed SSE chunks for this provider (default: 300000). If no chunk arrives within this window, the request is aborted. Set to false to disable timeout."
}
},
"additionalProperties": {}

View file

@ -392,7 +392,7 @@ Provider options can include `timeout`, `chunkTimeout`, and `setCacheKey`:
```
- `timeout` - Request timeout in milliseconds (default: 300000). Set to `false` to disable.
- `chunkTimeout` - Timeout in milliseconds between streamed response chunks. If no chunk arrives in time, the request is aborted.
- `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.
You can also configure [local models](/docs/models#local). [Learn more](/docs/models).