Back to Cherry Studio

Chat Attachments

docs/references/ai/chat-attachments.md

2.0.06.7 KB
Original Source

Chat Attachments

How a user's attached files reach the model on a chat turn.

One rule, per attachment: if the provider+model can take it as a native input, send the native file; otherwise send its extracted text, inlined and capped. The read_file tool only exists to page the overflow of large text — it is never the only way the model sees content.

This is deliberate: visibility must not depend on the model choosing to call a tool. A weak (or non-tool-calling) model still sees every attachment, and a provider that handles a modality natively keeps doing so — no capability regression.

Routing matrix

Decided per file part in prepareChatMessages (src/main/ai/messages/attachmentRouting.ts):

AttachmentNative whenWhat the model receives
imagemodel is visionnative image part (inline)
imagenon-vision, OCR finds textOCR text, inline (capped)
imagenon-vision, no OCR text (or OCR unconfigured/failed)native image part (inline base64)
pdfprovider+model native PDFnative PDF part (inline)
pdfotherwiseextracted text, inline (capped)
office (docx/xlsx/pptx/odf)extracted text, inline (capped)
text / codedecoded text, inline (capped)
extensionlessdecoded text when content is text; otherwise unsupported note
audiomodel is audio-capablenative audio part (inline)
audiootherwiseshort note ("can't process audio")
videomodel is video-capablenative video part (inline)
videootherwiseshort note ("can't process video")
other (binary: zip/exe/…)short note ("unsupported file type")
  • Native → the file part is left in place and materialized as a data: URL by materializeNativeFilePart (src/main/ai/messages/fileProcessor.ts), which also normalizes the mediaType to the on-disk MIME. The provider gets the real file as a user-message part. (The function is named for the boundary: provider File-API upload for large files would slot in behind the same signature.)
  • Binary / unsupported types are not auto-decoded — they'd inline as mojibake — so they get a short note instead.
  • A non-vision image only degrades to OCR text when OCR actually finds text. Otherwise (empty OCR result, unconfigured or failed OCR) the native image is forwarded anyway — the provider decides what it can do with it, so a model whose vision capability is under-declared still sees the picture.
  • Any per-file failure (missing entry, parse error, failed materialization) degrades to a [could not read this file]. note rather than dropping the file or failing the request.
  • Non-native → the file part is replaced by its extracted text (see the cap below). The internal fileEntryId is never written into the prompt.

Only fileEntryId-backed (first-party chat) images enter the OCR path. Gateway / external file parts (no fileEntryId) are still eagerly materialized, but an image is omitted for a non-vision model because it was never an explicit OCR fallback. Other gateway/external file types keep their existing behavior.

The cap (the only context guard)

Extracted text is bounded so multi-turn context stays in control:

  • text ≤ cap → inlined in full.
  • text > cap → inline the first cap chars + a trailer:
    • tool-capable model: [truncated N/total — call read_file("name", offset=N) for more]
    • otherwise: [truncated N/total]

Default cap ≈ 8k chars/file (tunable).

read_file — text-only overflow tool

src/main/ai/tools/fileLookup.ts + tools/adapters/aiSdk/builtin/ReadFileTool.ts.

  • Input { filename, offset?, limit? }. The filename is the model-facing handle (unique, normalized — see collectFileAttachments), resolved to an entry id against a per-request allow-list. The model never sees or guesses entry ids, and can only read files attached to the current conversation.
  • Returns text only (extracted / OCR), paginated. Errors are sanitized to filename-level messages; details are logged, not returned.
  • Exposed to tool-capable models whenever the request carries first-party file attachments (applies: scope.hasFileAttachments). It pages over-cap text; when everything inlines within the cap the model simply never needs to call it.
  • Claude Code exposes the same read operation through the Cherry Assistant-only assistant-files MCP server. Its model-facing handles are stable, opaque hashes of FileEntry ids. The server rebuilds its allow-list from the current session transcript when each tool call runs, so deleting a message revokes access immediately. Ordinary Claude Code Agents receive neither this server nor its attachment manifest.
  • Because native media is kept inline (never routed through the tool), read_file carries no media result — no toModelOutput base64 re-read, no resend re-materialization.

Cherry Assistant also gets approval-gated save_attachment from the same session-scoped server. It writes only new paths inside the session workspace and never overwrites an existing file. No attachment state or write tools are added to the shared chat runtime or to ordinary Agents.

Extraction & OCR

ConcernOwner
office/pdf/text → textextractDocumentText (src/main/ai/messages/attachmentTextExtraction.ts)
image → text (non-vision)FileProcessingService.ocrImage (src/main/features/fileProcessing/)

ai/ reaches OCR through the FileProcessingService rather than deep-importing the feature, keeping processor/handler internals in that domain. Both extractDocumentText and the OCR path are path-free and cache their result by content version (30 min), so the eager every-turn pass over history doesn't re-extract or re-OCR the same file. extractDocumentText reads bytes through FileManager.read (PDF via pdf-parse, office via officeparser/word-extractor, known text extensions via decodeTextWithAutoEncoding, and extensionless text via decodeTextBufferIfText) and dispatches on the FileEntry canonical ext.

Capability resolution

resolveNativeFileSupport (src/main/ai/runtime/aiSdk/params/nativeFileSupport.ts) derives the "native" column from (provider, model): image/audio/video ride on the model capability alone (isVision / isAudio / isVideo, @shared/utils/model), while PDF additionally requires a first-party provider (supportsNativePdf). There is no pdf-compatibility middleware — native PDFs pass through inline, non-native PDFs go through extraction.

Invariants

  • Content visibility never depends on a tool call.
  • fileEntryId never reaches the model (filename in, filename out).
  • Native modalities keep provider-native handling.
  • Per-turn context is bounded by the cap.