fix(opencode): surface content-filter finish reason as visible error (#31745)

Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
This commit is contained in:
Kevin Dawkins 2026-06-11 08:41:11 -07:00 committed by GitHub
commit e2527db3c7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 110 additions and 0 deletions

View file

@ -1355,6 +1355,18 @@ export const layer = Layer.effect(
const finished = handle.message.finish && !["tool-calls", "unknown"].includes(handle.message.finish)
if (finished && !handle.message.error) {
// Surface any content-filter finish (e.g. Anthropic stop_reason:
// refusal) as an error. These turns may have produced no visible
// output at all — previously the session went idle silently — or
// partial text that was cut off by the provider's filter.
if (handle.message.finish === "content-filter") {
handle.message.error = new SessionV1.ContentFilterError({
message: "The response was blocked by the provider's content filter",
}).toObject()
yield* sessions.updateMessage(handle.message)
yield* events.publish(Session.Event.Error, { sessionID, error: handle.message.error })
return "break" as const
}
if (format.type === "json_schema") {
handle.message.error = new SessionV1.StructuredOutputError({
message: "Model did not produce structured output",

View file

@ -493,6 +493,14 @@ export class Reply {
return this
}
contentFilter() {
this.#finish = "content_filter"
this.#hang = false
this.#error = undefined
this.#reset = false
return this
}
toolCalls() {
this.#finish = "tool_calls"
this.#hang = false

View file

@ -506,6 +506,52 @@ it.instance("loop calls LLM and returns assistant message", () =>
}),
)
it.instance("loop surfaces content-filter finishes as session errors", () =>
Effect.gen(function* () {
const { llm } = yield* useServerConfig(providerCfg)
const events = yield* EventV2Bridge.Service
const prompt = yield* SessionPrompt.Service
const sessions = yield* Session.Service
const chat = yield* sessions.create({ title: "Pinned" })
const errors: NonNullable<SessionV1.Assistant["error"]>[] = []
const expected = {
name: "ContentFilterError",
data: { message: "The response was blocked by the provider's content filter" },
} satisfies NonNullable<SessionV1.Assistant["error"]>
const off = yield* events.listen((event) => {
if (event.type !== Session.Event.Error.type) return Effect.void
const data = event.data as typeof Session.Event.Error.data.Type
if (data.sessionID === chat.id && data.error) errors.push(data.error)
return Effect.void
})
yield* prompt.prompt({
sessionID: chat.id,
agent: "build",
noReply: true,
parts: [{ type: "text", text: "hello" }],
})
yield* llm.push(reply().text("partial response").contentFilter())
const result = yield* prompt.loop({ sessionID: chat.id })
const stored = yield* MessageV2.get({ sessionID: chat.id, messageID: result.info.id })
yield* off
expect(yield* llm.hits).toHaveLength(1)
expect(result.info.role).toBe("assistant")
expect(stored.info.role).toBe("assistant")
if (result.info.role === "assistant" && stored.info.role === "assistant") {
expect(result.info.finish).toBe("content-filter")
expect(result.info.error).toEqual(expected)
expect(stored.info.error).toEqual(result.info.error)
expect(errors).toContainEqual(expected)
}
expect(result.parts).toEqual(
expect.arrayContaining([expect.objectContaining({ type: "text", text: "partial response" })]),
)
}),
)
it.instance("loop stops provider overflow instead of auto-compacting when disabled", () =>
Effect.gen(function* () {
const { llm } = yield* useServerConfig((url) => ({