fix(opencode): retry raw network finish errors (#43813)

Co-authored-by: Aiden <rekram1-node@users.noreply.github.com>
This commit is contained in:
opencode-agent[bot] 2026-08-21 02:17:35 -05:00 committed by GitHub
parent 1e4c153aed
commit e0b9e68a68
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 125 additions and 0 deletions

View file

@ -2,6 +2,7 @@ import { FinishReason, LLMEvent, ProviderMetadata, ToolResultValue } from "@open
import { Effect, Schema } from "effect"
import { type streamText } from "ai"
import { errorMessage } from "@/util/error"
import { ProviderError } from "@/provider/error"
type Result = Awaited<ReturnType<typeof streamText>>
type AISDKEvent = Result["fullStream"] extends AsyncIterable<infer T> ? T : never
@ -85,6 +86,8 @@ export function toLLMEvents(
return Effect.succeed([LLMEvent.stepStart({ index: state.step })])
case "finish-step":
if (event.rawFinishReason === "network_error")
return Effect.fail(new ProviderError.ResponseStreamError("Provider finish_reason: network_error"))
return Effect.sync(() => {
const original = providerMetadata(event.providerMetadata)
const metadata =

View file

@ -27,6 +27,7 @@ import { ModelV2 } from "@opencode-ai/core/model"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
import { ProviderError } from "@/provider/error"
type ConfigModel = NonNullable<NonNullable<ConfigV1.Info["provider"]>[string]["models"]>[string]
@ -832,6 +833,70 @@ describe("session.llm.stream", () => {
},
)
it.instance(
"surfaces network_error finish reasons as retryable stream failures",
() =>
Effect.gen(function* () {
const fixture = loadFixture(vivgridFixture.providerID, vivgridFixture.modelID)
const request = waitRequest(
"/chat/completions",
createEventResponse(
[
{
id: "chatcmpl-network-error",
object: "chat.completion.chunk",
choices: [{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: "network_error" }],
},
],
true,
),
)
const resolved = yield* Provider.use.getModel(
ProviderV2.ID.make(vivgridFixture.providerID),
ModelV2.ID.make(fixture.model.id),
)
const sessionID = SessionID.make("session-test-network-error")
const agent = {
name: "test",
mode: "primary",
options: {},
permission: [{ permission: "*", pattern: "*", action: "allow" }],
} satisfies Agent.Info
const user = {
id: MessageID.make("msg_user-network-error"),
sessionID,
role: "user",
time: { created: Date.now() },
agent: agent.name,
model: { providerID: ProviderV2.ID.make(vivgridFixture.providerID), modelID: resolved.id },
} satisfies SessionV1.User
const error = yield* drain({
user,
sessionID,
model: resolved,
agent,
system: ["You are a helpful assistant."],
messages: [{ role: "user", content: "Hello" }],
tools: {},
}).pipe(Effect.flip)
yield* Effect.promise(() => request)
if (!(error instanceof ProviderError.ResponseStreamError)) throw error
expect(error.message).toBe("Provider finish_reason: network_error")
}),
{
config: () => ({
enabled_providers: [vivgridFixture.providerID],
provider: {
[vivgridFixture.providerID]: {
options: { apiKey: "test-key", baseURL: `${state.server!.url.origin}/v1` },
},
},
}),
},
)
const cerebrasFixture = { providerID: "cerebras", modelID: "gpt-oss-120b" }
it.instance(
"replays Cerebras assistant reasoning using the provider-supported field",

View file

@ -651,6 +651,63 @@ it.live("session.processor effect tests retry OpenAI-compatible midstream server
),
)
it.live("session.processor effect tests retry network_error finish reasons", () =>
provideTmpdirServer(
({ dir, llm }) =>
Effect.gen(function* () {
const { processors, session, provider } = yield* boot()
yield* llm.push(
raw({
chunks: [
{
id: "chatcmpl-network-error",
object: "chat.completion.chunk",
choices: [{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: "network_error" }],
},
],
}),
)
yield* llm.text("after retry")
const chat = yield* session.create({})
const parent = yield* user(chat.id, "retry network error")
const msg = yield* assistant(chat.id, parent.id, path.resolve(dir))
const mdl = yield* provider.getModel(ref.providerID, ref.modelID)
const handle = yield* processors.create({
assistantMessage: msg,
sessionID: chat.id,
model: mdl,
})
const value = yield* handle.process({
user: {
id: parent.id,
sessionID: chat.id,
role: "user",
time: parent.time,
agent: parent.agent,
model: { providerID: ref.providerID, modelID: ref.modelID },
} satisfies SessionV1.User,
sessionID: chat.id,
model: mdl,
agent: agent(),
system: [],
messages: [{ role: "user", content: "retry network error" }],
tools: {},
})
const parts = yield* MessageV2.parts(msg.id)
expect(value).toBe("continue")
expect(yield* llm.calls).toBe(2)
expect(parts.some((part) => part.type === "text" && part.text === "after retry")).toBe(true)
expect(handle.message.error).toBeUndefined()
}),
{ config: (url) => providerCfg(url) },
),
)
it.live("session.processor effect tests publish retry status updates", () =>
provideTmpdirServer(
({ dir, llm }) =>