diff --git a/packages/opencode/src/provider/error.ts b/packages/opencode/src/provider/error.ts index 0db03576e92..dff008fd8b9 100644 --- a/packages/opencode/src/provider/error.ts +++ b/packages/opencode/src/provider/error.ts @@ -19,6 +19,7 @@ export namespace ProviderError { /context window exceeds limit/i, // MiniMax /exceeded model token limit/i, // Kimi For Coding, Moonshot /context[_ ]length[_ ]exceeded/i, // Generic fallback + /request entity too large/i, // HTTP 413 ] function isOpenAiErrorRetryable(e: APICallError) { @@ -165,7 +166,7 @@ export namespace ProviderError { export function parseAPICallError(input: { providerID: string; error: APICallError }): ParsedAPICallError { const m = message(input.providerID, input.error) - if (isOverflow(m)) { + if (isOverflow(m) || input.error.statusCode === 413) { return { type: "context_overflow", message: m, diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 9245426057c..1ad3387685d 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -104,6 +104,7 @@ export namespace SessionCompaction { sessionID: string abort: AbortSignal auto: boolean + overflow?: boolean }) { const userMessage = input.messages.findLast((m) => m.info.id === input.parentID)!.info as MessageV2.User const agent = await Agent.get("compaction") @@ -185,7 +186,7 @@ When constructing the summary, try to stick to this template: tools: {}, system: [], messages: [ - ...MessageV2.toModelMessages(input.messages, model), + ...MessageV2.toModelMessages(input.messages, model, { stripMedia: true }), { role: "user", content: [ @@ -199,6 +200,15 @@ When constructing the summary, try to stick to this template: model, }) + if (result === "compact") { + processor.message.error = new MessageV2.ContextOverflowError({ + message: "Session too large to compact - context exceeds model limit even after stripping media", + }).toObject() + processor.message.finish = "error" + await Session.updateMessage(processor.message) + return "stop" + } + if (result === "continue" && input.auto) { const continueMsg = await Session.updateMessage({ id: Identifier.ascending("message"), @@ -210,13 +220,17 @@ When constructing the summary, try to stick to this template: agent: userMessage.agent, model: userMessage.model, }) + const text = + (input.overflow + ? "The previous request exceeded the provider's size limit due to large media attachments. The conversation was compacted and media files were removed from context. If the user was asking about attached images or files, explain that the attachments were too large to process and suggest they try again with smaller or fewer files.\n\n" + : "") + "Continue if you have next steps, or stop and ask for clarification if you are unsure how to proceed." await Session.updatePart({ id: Identifier.ascending("part"), messageID: continueMsg.id, sessionID: input.sessionID, type: "text", synthetic: true, - text: "Continue if you have next steps, or stop and ask for clarification if you are unsure how to proceed.", + text, time: { start: Date.now(), end: Date.now(), @@ -237,6 +251,7 @@ When constructing the summary, try to stick to this template: modelID: z.string(), }), auto: z.boolean(), + overflow: z.boolean().optional(), }), async (input) => { const msg = await Session.updateMessage({ @@ -255,6 +270,7 @@ When constructing the summary, try to stick to this template: sessionID: msg.sessionID, type: "compaction", auto: input.auto, + overflow: input.overflow, }) }, ) diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 178751a2227..9281a29c4bb 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -196,6 +196,7 @@ export namespace MessageV2 { export const CompactionPart = PartBase.extend({ type: z.literal("compaction"), auto: z.boolean(), + overflow: z.boolean().optional(), }).meta({ ref: "CompactionPart", }) @@ -488,7 +489,11 @@ export namespace MessageV2 { }) export type WithParts = z.infer - export function toModelMessages(input: WithParts[], model: Provider.Model): ModelMessage[] { + export function toModelMessages( + input: WithParts[], + model: Provider.Model, + options?: { stripMedia?: boolean }, + ): ModelMessage[] { const result: UIMessage[] = [] const toolNames = new Set() // Track media from tool results that need to be injected as user messages @@ -562,13 +567,22 @@ export namespace MessageV2 { text: part.text, }) // text/plain and directory files are converted into text parts, ignore them - if (part.type === "file" && part.mime !== "text/plain" && part.mime !== "application/x-directory") - userMessage.parts.push({ - type: "file", - url: part.url, - mediaType: part.mime, - filename: part.filename, - }) + if (part.type === "file" && part.mime !== "text/plain" && part.mime !== "application/x-directory") { + const isMedia = part.mime.startsWith("image/") || part.mime === "application/pdf" + if (options?.stripMedia && isMedia) { + userMessage.parts.push({ + type: "text", + text: `[Attached ${part.mime}: ${part.filename ?? "file"}]`, + }) + } else { + userMessage.parts.push({ + type: "file", + url: part.url, + mediaType: part.mime, + filename: part.filename, + }) + } + } if (part.type === "compaction") { userMessage.parts.push({ @@ -618,7 +632,7 @@ export namespace MessageV2 { toolNames.add(part.tool) if (part.state.status === "completed") { const outputText = part.state.time.compacted ? "[Old tool result content cleared]" : part.state.output - const attachments = part.state.time.compacted ? [] : (part.state.attachments ?? []) + const attachments = part.state.time.compacted || options?.stripMedia ? [] : (part.state.attachments ?? []) // For providers that don't support media in tool results, extract media files // (images, PDFs) to be sent as a separate user message diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index e7532d20073..f85dc0b46fe 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -354,27 +354,32 @@ export namespace SessionProcessor { }) const error = MessageV2.fromError(e, { providerID: input.model.providerID }) if (MessageV2.ContextOverflowError.isInstance(error)) { - // TODO: Handle context overflow error - } - const retry = SessionRetry.retryable(error) - if (retry !== undefined) { - attempt++ - const delay = SessionRetry.delay(attempt, error.name === "APIError" ? error : undefined) - SessionStatus.set(input.sessionID, { - type: "retry", - attempt, - message: retry, - next: Date.now() + delay, + needsCompaction = true + Bus.publish(Session.Event.Error, { + sessionID: input.sessionID, + error, }) - await SessionRetry.sleep(delay, input.abort).catch(() => {}) - continue + } else { + const retry = SessionRetry.retryable(error) + if (retry !== undefined) { + attempt++ + const delay = SessionRetry.delay(attempt, error.name === "APIError" ? error : undefined) + SessionStatus.set(input.sessionID, { + type: "retry", + attempt, + message: retry, + next: Date.now() + delay, + }) + await SessionRetry.sleep(delay, input.abort).catch(() => {}) + continue + } + input.assistantMessage.error = error + Bus.publish(Session.Event.Error, { + sessionID: input.assistantMessage.sessionID, + error: input.assistantMessage.error, + }) + SessionStatus.set(input.sessionID, { type: "idle" }) } - input.assistantMessage.error = error - Bus.publish(Session.Event.Error, { - sessionID: input.assistantMessage.sessionID, - error: input.assistantMessage.error, - }) - SessionStatus.set(input.sessionID, { type: "idle" }) } if (snapshot) { const patch = await Snapshot.patch(snapshot) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 75bd3c9dfac..4f77920cc98 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -533,6 +533,7 @@ export namespace SessionPrompt { abort, sessionID, auto: task.auto, + overflow: task.overflow, }) if (result === "stop") break continue @@ -707,6 +708,7 @@ export namespace SessionPrompt { agent: lastUser.agent, model: lastUser.model, auto: true, + overflow: !processor.message.finish, }) } continue