diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 983be1bbce..49c582e0a9 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -48,7 +48,7 @@ function wrapSSE(res: Response, ms: number, ctl: AbortController) { async pull(ctrl) { const part = await new Promise>>((resolve, reject) => { const id = setTimeout(() => { - const err = new Error("SSE read timed out") + const err = new ProviderError.ResponseStreamError("SSE read timed out") ctl.abort(err) void reader.cancel(err) reject(err) diff --git a/packages/opencode/test/provider/header-timeout.test.ts b/packages/opencode/test/provider/header-timeout.test.ts index 145a77280a..543bb5f30d 100644 --- a/packages/opencode/test/provider/header-timeout.test.ts +++ b/packages/opencode/test/provider/header-timeout.test.ts @@ -10,6 +10,7 @@ import { testProviderConfig } from "../lib/test-provider" import { Env } from "@/env" import { Plugin } from "@/plugin" import { Provider } from "@/provider/provider" +import { ProviderError } from "@/provider/error" import { ModelID, ProviderID } from "@/provider/schema" afterEach(async () => { @@ -58,6 +59,35 @@ it.live("headerTimeout does not abort delayed SSE body after headers arrive", () ), ) +it.live("chunkTimeout raises a response stream error when SSE body stalls", () => + provideTmpdirServer( + ({ llm }) => + Effect.gen(function* () { + yield* llm.push(reply().wait(Bun.sleep(250)).text("late").stop()) + + const provider = yield* Provider.Service + const model = yield* provider.getModel(ProviderID.make("test"), ModelID.make("test-model")) + const result = streamText({ + model: yield* provider.getLanguage(model), + onError() {}, + messages: [{ role: "user", content: "hello" }], + }) + + const error = yield* Effect.promise(async () => { + try { + for await (const part of result.fullStream) { + if (part.type === "error") return part.error + } + } catch (error) { + return error + } + }) + expect(error).toBeInstanceOf(ProviderError.ResponseStreamError) + }), + { config: (url) => providerConfig(url, { chunkTimeout: 50 }) }, + ), +) + it.live("headerTimeout aborts when response headers do not arrive", () => Effect.gen(function* () { const server = yield* Effect.acquireRelease(