fix(opencode): retry raw network finish errors (#43813)
Co-authored-by: Aiden <rekram1-node@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
6c73cbafd1
commit
645491ce33
@@ -2,6 +2,7 @@ import { FinishReason, LLMEvent, ProviderMetadata, ToolResultValue } from "@open
|
|||||||
import { Effect, Schema } from "effect"
|
import { Effect, Schema } from "effect"
|
||||||
import { type streamText } from "ai"
|
import { type streamText } from "ai"
|
||||||
import { errorMessage } from "@/util/error"
|
import { errorMessage } from "@/util/error"
|
||||||
|
import { ProviderError } from "@/provider/error"
|
||||||
|
|
||||||
type Result = Awaited<ReturnType<typeof streamText>>
|
type Result = Awaited<ReturnType<typeof streamText>>
|
||||||
type AISDKEvent = Result["fullStream"] extends AsyncIterable<infer T> ? T : never
|
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 })])
|
return Effect.succeed([LLMEvent.stepStart({ index: state.step })])
|
||||||
|
|
||||||
case "finish-step":
|
case "finish-step":
|
||||||
|
if (event.rawFinishReason === "network_error")
|
||||||
|
return Effect.fail(new ProviderError.ResponseStreamError("Provider finish_reason: network_error"))
|
||||||
return Effect.sync(() => {
|
return Effect.sync(() => {
|
||||||
const original = providerMetadata(event.providerMetadata)
|
const original = providerMetadata(event.providerMetadata)
|
||||||
const metadata =
|
const metadata =
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { ModelV2 } from "@opencode-ai/core/model"
|
|||||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
|
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]
|
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" }
|
const cerebrasFixture = { providerID: "cerebras", modelID: "gpt-oss-120b" }
|
||||||
it.instance(
|
it.instance(
|
||||||
"replays Cerebras assistant reasoning using the provider-supported field",
|
"replays Cerebras assistant reasoning using the provider-supported field",
|
||||||
|
|||||||
@@ -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", () =>
|
it.live("session.processor effect tests publish retry status updates", () =>
|
||||||
provideTmpdirServer(
|
provideTmpdirServer(
|
||||||
({ dir, llm }) =>
|
({ dir, llm }) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user