fix(opencode): default chunk timeout to five minutes (#46890)
This commit is contained in:
parent
68abdce1a0
commit
4eb29a64f0
6 changed files with 95 additions and 11 deletions
|
|
@ -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)],
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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 } } } } },
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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]: {
|
||||
|
|
|
|||
|
|
@ -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": {}
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
Loading…
Reference in a new issue