diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index cce3c7014bc1..db32cd335fa9 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -392,6 +392,21 @@ function unsupportedParts(msgs: ModelMessage[], model: Provider.Model): ModelMes } } + // Check for empty base64 PDF data + if (part.type === "file" && part.mediaType === "application/pdf") { + const url = "url" in part ? String(part.url) : "" + if (url.startsWith("data:")) { + const match = url.match(/^data:([^;]+);base64,(.*)$/) + if (match && (!match[2] || match[2].length === 0)) { + const name = part.filename ? `"${part.filename}"` : "PDF" + return { + type: "text" as const, + text: `ERROR: ${name} file is empty or corrupted. Please provide a valid PDF file.`, + } + } + } + } + const mime = part.type === "image" ? String(part.image).split(";")[0].replace("data:", "") : part.mediaType const filename = part.type === "file" ? part.filename : undefined const modality = mimeToModality(mime) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 2a58cd425312..e82debe3d1d0 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -929,6 +929,25 @@ export const layer = Layer.effect( ] } + const readExit = yield* fsys.readFile(filepath).pipe(Effect.exit) + if (Exit.isFailure(readExit)) { + const error = Cause.squash(readExit.cause) + log.error("failed to read file attachment", { error, filepath }) + const message = error instanceof Error ? error.message : String(error) + yield* events.publish(Session.Event.Error, { + sessionID: input.sessionID, + error: new NamedError.Unknown({ message }).toObject(), + }) + return [ + { + messageID: info.id, + sessionID: input.sessionID, + type: "text", + synthetic: true, + text: `Failed to read file ${filepath}: ${message}`, + }, + ] + } return [ { messageID: info.id, @@ -942,9 +961,7 @@ export const layer = Layer.effect( messageID: info.id, sessionID: input.sessionID, type: "file", - url: - `data:${mime};base64,` + - Buffer.from(yield* fsys.readFile(filepath).pipe(Effect.catch(Effect.die))).toString("base64"), + url: `data:${mime};base64,${Buffer.from(readExit.value).toString("base64")}`, mime, filename: part.filename!, source: part.source, diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index 678ed4451048..1fb7fb2968a0 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -304,8 +304,17 @@ export const ReadTool = Tool.define< const isImage = SUPPORTED_IMAGE_MIMES.has(mime) if (isImage || isPdfAttachment(mime)) { - const bytes = yield* fs.readFile(filepath) - const msg = isPdfAttachment(mime) ? "PDF read successfully" : "Image read successfully" + const kind = isPdfAttachment(mime) ? "PDF" : "image" + const bytes = yield* fs.readFile(filepath).pipe( + Effect.catch((error) => + Effect.fail( + new Error( + `Cannot read ${kind} file: ${filepath} (${error instanceof Error ? error.message : String(error)})`, + ), + ), + ), + ) + const msg = `${kind === "PDF" ? "PDF" : "Image"} read successfully` return { title, output: msg,