diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts index 5de1ef8dc1b..cac1089876a 100644 --- a/packages/core/src/session/compaction.ts +++ b/packages/core/src/session/compaction.ts @@ -32,7 +32,7 @@ import { SessionModelRequest } from "./model-request.js" import { SessionProviderContext } from "./provider-context.js" import { SessionRunnerRetry } from "./runner/retry.js" import { toLLMMessages } from "./runner/to-llm-message.js" -import { toSessionError } from "./to-session-error.js" +import { contentFilterError, toSessionError } from "./to-session-error.js" import { SessionUsage } from "./usage.js" export type Settings = { @@ -540,10 +540,7 @@ export const layer = Layer.effect( message: "Compaction summary reached the output token limit", }) case "content-filter": - return unusable({ - type: "provider.content-filter", - message: "Compaction summary was blocked by the provider", - }) + return unusable(contentFilterError("Compaction summary was blocked by the provider", event.reason)) default: return Effect.succeed(streamed) } diff --git a/packages/core/src/session/runner/publish-llm-event.ts b/packages/core/src/session/runner/publish-llm-event.ts index 988afb70b0e..bceffb61202 100644 --- a/packages/core/src/session/runner/publish-llm-event.ts +++ b/packages/core/src/session/runner/publish-llm-event.ts @@ -12,6 +12,7 @@ import { SessionSchema } from "../schema.js" import { SessionError } from "@opencode/schema/session-error" import { Money } from "@opencode/schema/money" import { SessionUsage } from "../usage.js" +import { contentFilterError } from "../to-session-error.js" import type { Tool } from "../../tool.js" type Input = { @@ -543,15 +544,7 @@ export const createLLMEventPublisher = (bus: Pick, inp } if (event.reason.normalized === "content-filter") { providerFailed = true - yield* failAssistant({ - type: "provider.content-filter", - message: [ - `Provider blocked the response${event.reason.category === undefined ? "" : ` (${event.reason.category})`}`, - event.reason.explanation, - ] - .filter(Boolean) - .join(": "), - }) + yield* failAssistant(contentFilterError("Provider blocked the response", event.reason)) return } return diff --git a/packages/core/src/session/to-session-error.ts b/packages/core/src/session/to-session-error.ts index 46db0670389..d8eb0f683af 100644 --- a/packages/core/src/session/to-session-error.ts +++ b/packages/core/src/session/to-session-error.ts @@ -1,4 +1,4 @@ -import { AIError, ToolFailure } from "@opencode/ai" +import { AIError, ToolFailure, type FinishReasonDetails } from "@opencode/ai" import { Tool } from "@opencode/schema/tool" import { SessionError } from "@opencode/schema/session-error" import { Permission } from "../permission.js" @@ -85,6 +85,15 @@ export function toSessionError(cause: unknown): SessionError.Error { return { type: "unknown", message: cause instanceof Error ? cause.message : String(cause) } } +export function contentFilterError(summary: string, reason: FinishReasonDetails): SessionError.Error { + return { + type: "provider.content-filter", + message: [reason.category === undefined ? summary : `${summary} (${reason.category})`, reason.explanation] + .filter(Boolean) + .join(": "), + } +} + function providerError(type: string, reason: AIError["reason"]): SessionError.Error { const status = reason.http?.status return { diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index ce6058c3720..68c8f3eb135 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -2716,6 +2716,32 @@ describe("SessionRunnerLLM", () => { ) }) + scenario("explains a compaction blocked by the provider", function* (s) { + yield* s.llm.push(TestLLM.text("Earlier answer", "history")) + yield* s.runPrompt("Earlier question") + yield* s.llm.push( + TestLLM.complete({ + reason: { + normalized: "content-filter", + raw: "refusal", + category: "cyber", + explanation: "This request was declined because it could enable cyber harm.", + }, + }), + ) + const compaction = yield* s.session.compact({ sessionID }) + yield* s.resume + + expect((yield* s.messages).find((message) => message.id === compaction.id)).toMatchObject({ + status: "failed", + error: { + type: "provider.content-filter", + message: + "Compaction summary was blocked by the provider (cyber): This request was declined because it could enable cyber harm.", + }, + }) + }) + for (const header of [false, true]) { scenario(`stops compaction retries through the ${header ? "provider header" : "retry hook"}`, function* (s) { yield* s.llm.push(TestLLM.text("Earlier answer", "history"))