Repository navigation
Conversation
…ile Parts The inherited supportedMimeTypes list routes files to "Upload to Provider" (LibreChat-AI#13550), but some types on it can never go inline: - Archives (zip, tar, gzip, epub, octet-stream) go to the provider only when the endpoint lists them itself, in BaseClient and child runs. - Gemini, native or behind an OpenAI-compatible gateway (detected by model name, as LibreChat-AI#16055 does for Claude), gets PDF and textual types plus types the endpoint lists; Office documents are skipped like the Anthropic filter from LibreChat-AI#14535. Textual application/* types (JSON, YAML, XML, SQL) go to Gemini as a text part; text/* stays inline. - application/sql, x-sh and xml go to other OpenAI-like providers as a text part unless the endpoint lists them: Azure OpenAI rejects them as a file part with a 400 that recurs on every later turn. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Gemini handling still drops an inherited textual MIME type, and one warning recommends an ineffective configuration fix.
Review effort: Balanced
Findings: 1
Open (2)
What changed in this PR
Prevents unsupported provider-uploaded files from producing recurring provider errors.
Changes:
- Filters inherited archive and Gemini-incompatible document types.
- Converts unsupported textual file parts into text blocks.
- Adds routing and encoder regression tests.
| File | Description |
|---|---|
packages/api/src/files/encode/utils.ts |
Adds provider document eligibility logic. |
packages/api/src/files/encode/processAttachments.spec.ts |
Tests inherited and explicit MIME routing. |
packages/api/src/files/encode/document.ts |
Adds Gemini filtering and text-part formatting. |
packages/api/src/files/encode/document.spec.ts |
Tests provider-specific document handling. |
packages/api/src/agents/files/encode.ts |
Applies filtering to child-run encoding. |
packages/api/src/agents/files/encode.spec.ts |
Tests child-run archive routing. |
api/app/clients/BaseClient.js |
Filters and warns about dropped attachments. |
api/app/clients/specs/BaseClient.test.js |
Tests BaseClient filtering and warnings. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| } else if (usesGeminiDocumentCapabilities(provider, model)) { | ||
| label = 'Gemini'; | ||
| isSupported = (file) => isAnthropicDocumentType(file.type) || isOptedIn(file.type ?? ''); |
There was a problem hiding this comment.
Fixed in 5ec8bb3. The Gemini filter and sendsAsTextWithoutOptIn now use isNativelyReadableText, the same classification that routes the file to the provider. So an inherited application/vnd.coffeescript (and application/x-yaml) goes to Gemini as a text part and is no longer dropped. New spec covers it for native Google and for Gemini behind a gateway. The Claude filter from #14535 still uses isAnthropicDocumentType. I left it as it is, because this PR does not change it.
| logger.warn( | ||
| `[BaseClient] Not sending "${file.filename}" (${file.type}) to the provider: list this type in the endpoint's own supportedMimeTypes to send it`, | ||
| ); |
There was a problem hiding this comment.
Agreed, fixed in 5ec8bb3. The warning now only says the type is not an inline document for this endpoint, and gives no configuration advice. You are right: for Claude and Bedrock, an explicitly listed archive is still filtered out.
The Gemini document filter and its text-part decision used the Anthropic text list, which lacks application/vnd.coffeescript and application/x-yaml. The router sends those to the provider as natively readable text, so the filter dropped an inherited CoffeeScript file. Both now use isNativelyReadableText, the classification that routes the file. The BaseClient skip warning no longer tells admins to list the type: for Claude and Bedrock an explicitly listed archive is still filtered out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A conversation breaks when an archive from an earlier message goes out as a file part on every later turn. The replay path (addPreviousAttachments → processAttachments) now has a test: the archive is skipped with a warning, the PDF from the same message is still sent, and message_file_map holds only the PDF. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed a3155b430d89fc097d1e885749bcf1ec58b796fc again, including the full base-to-head diff, upload admission, current-turn delivery, history replay, child-run encoding, and the existing inline review threads. The added archive-history regression test is useful, and the CoffeeScript classification and warning-wording findings from the earlier review are addressed.
I am requesting changes for the three inline findings:
- Make unsupported attachment omissions observable instead of reporting an accepted attachment while sending no file content.
- Apply the existing text budgets to the new raw-file-to-text fallback, including aggregate turn/child/replay accounting.
- Keep native Google/Vertex capability constraints separate from upload allowlists. This configuration finding is narrower than a blanket prohibition on overrides: intentional overrides for custom gateways should remain supported.
Verification: mapped consumers with the code graph at dev commit e9d1d330b1e1429b01f49b3a6c77764e2049f47d, then inspected the actual PR source. A dependency-light probe executed the actual document encoder, BaseClient methods, MIME classifiers, and attachment-stat collector, with storage/config lookup/SDK-validation adapters supplied. It reproduced the retained-but-empty Office attachment, archive omission, unbounded SQL text, and configured native-provider media output. These were source-level checks, not live provider or end-to-end tests. git diff --check passed. The focused document/child-run/BaseClient/AgentClient Jest suites and packages/api typecheck could not start because the shared dependency install currently lacks the local Jest and TypeScript executables. GitHub reports no CI checks for this head.
These are changes requested before merging this PR, not a request to hold the release for it.
| if (skipped.length) { | ||
| console.warn( | ||
| `Skipping attachment(s) unsupported by Claude document input: ${skipped.join(', ')}`, | ||
| `Skipping attachment(s) unsupported by ${label} document input: ${skipped.join(', ')}`, |
There was a problem hiding this comment.
[P1] Surface rejected/omitted attachments to the user
For a provider-chosen DOCX on Google with no explicit MIME list, BaseClient.processAttachments adds the file to allFiles before this filter drops it. The actual methods return [report.docx] while message.documents is undefined and storage is never read. The upload route still reports success, and current-turn persistence can retain the attachment even though the model receives none of its content. The inherited archive path similarly ends with only a server warning, and an archive-only child share can return no file message at all.
Please reject unsupported new/current-turn provider attachments through a user-visible, typed error, or return an omission outcome that the UI can display with localized copy. Historical replay should still recover rather than throwing on every later turn, but its omission must be explicit. Preserve historical references and tool-only uploads. Add coverage for the user-visible current-turn result, restored-history omission, and an archive-only child share; a warning assertion alone does not prove that the missing input is observable.
There was a problem hiding this comment.
Fixed in 8c83da1, with the typed-error route.
encodeAndFormatDocumentsnow returns the files it skipped inomitted(reasonunsupported_typeortext_limit).BaseClient.processAttachmentscollects them together with the provider-bound files that no encoder takes (the archive case).- Current turn:
processAttachmentsthrowsAgentAttachmentUnsupportedError(codeAGENT_ATTACHMENT_UNSUPPORTED, 415). The message names each file, for exampleThis model cannot read "report.docx" (…). Remove it, or upload it as text or to the code environment, and try again.isAgentAttachmentLimitErroraccepts it, and the code is inFATAL_AGENT_INITIALIZATION_CODES. So it takes the same path to the user as the existing attachment-limit errors, and the turn fails before the user message is saved. - History replay: the turn continues. The file is left out of
message_file_map, the message gets a text part that names the file and the reason, andlogger.warnrecords the omission with the message id. - Child share:
createRunFileMessageEncoderrejects a provider-bound file that no encoder takes invalidate(), before it reads bytes. An archive-only share now fails with the same error, and it no longer returns no message. Anomittedlist from the document encoder rejects the share too.
Tests: current-turn archive and encoder omission (BaseClient), the same archive through the real AgentClient.processAttachments, replay of an archive and of an encoder-omitted DOCX, and archive-only and mixed child shares.
Known limits, both from before this PR: the upload route still returns 200, and the rejection happens at send time, as with AgentAttachmentLimitError. agents/steering/media.ts also calls the document encoder and does not read omitted yet. One change in behaviour: a Bedrock skip is now reported, so a non-Bedrock type on the document path rejects the turn instead of being dropped.
| function formatTextDocumentBlock(filename: string, content: string): DocumentBlock { | ||
| return { | ||
| type: 'text', | ||
| text: `File: "${filename}"\n\n${Buffer.from(content, 'base64').toString('utf8')}`, |
There was a problem hiding this comment.
[P2] Enforce text budgets before emitting the fallback block
This decodes the complete file directly into a text block without applying fileTokenLimit or accounting for fileContextCharLimit. A legacy provider-chosen upload normally has no file.text, so collectAgentAttachmentStats counts zero extracted-text characters before this conversion. A source-level run with fileTokenLimit: 8, fileContextCharLimit: 100, and a roughly 2 MB SQL file emitted 2,000,029 characters and counted zero extracted-text characters. The byte-size limit is not a substitute for the configured text limits.
Please reuse the existing bounded-text processing and account for the resulting text in the turn's aggregate limits, including child delivery and history replay, before constructing/sending the block. Add a large-SQL regression test with deliberately small token/character budgets. This need not add another storage read.
There was a problem hiding this comment.
Fixed in 8c83da1. The text-part fallback and the plain-text source of a native Anthropic document now go through limitTextBlock:
fileTokenLimitper file (req.body.fileTokenLimit ?? fileConfig.fileTokenLimit), throughprocessTextWithTokenLimitwithcountTokens, the same call thatextractFileContextuses.fileContextCharLimitper request. AWeakMapkeyed byreqholds the decoded characters already sent, so the limit covers the current turn, history replay and child runs of one request together. The text is cut to the remaining characters before tokens are counted, so a 2 MB file is never tokenized whole. A file that gets no budget is reported astext_limit(current turn: typed error; replay: note and omission).
Your case is now a test: a 2 MB SQL file with fileTokenLimit: 8 and fileContextCharLimit: 100 gives one text block of ≤ 100 characters. A second test shows that the budget is shared across two encoder calls on one request.
Two choices to call out. First, this ledger is separate from extractedTextChars in assertAgentAttachmentLimits. Decoded fallback text and extracted text are each bounded by fileContextCharLimit, so their sum can reach twice the limit. Second, the fallback cuts the text to fit, where the admission check throws. A throw here would also fire on history replay and fail every later turn, which is the failure this PR removes. If you prefer one shared counter, I can seed the ledger from the admission stats.
| isSupported = (file) => | ||
| file.type === 'application/pdf' || | ||
| isNativelyReadableText(file.type ?? '') || | ||
| isOptedIn(file.type ?? ''); |
There was a problem hiding this comment.
[P2] Do not let native Google/Vertex upload allowlists disable safe encoding
The explicit opt-in escape hatch also applies to native Google/Vertex, not just an OpenAI-compatible gateway whose behavior an operator may intentionally override. With provider: vertexai and DOCX in that provider's allowlist, this admits the Office file and the actual encoder emits { type: 'media', mimeType: '<DOCX MIME>', ... }. With native Google and application/sql listed, optedIn also disables the text fallback at lines 147-148 and emits SQL as inline media. Those are the MIME-bearing payloads reported as rejected in #16472; allowing an upload does not change the native provider's capabilities.
Please retain the configurable gateway override, but make the known native Google/Vertex path convert these textual types to text and reject or otherwise handle unsupported Office content independently of the upload allowlist. Add explicit-allowlist SQL and Office cases for both native providers, while preserving gateway opt-in tests.
There was a problem hiding this comment.
Fixed in 8c83da1. On native Google/Vertex, isOptedIn is now always false, because the upload allowlist does not change what the native API accepts. As a result:
- A textual
application/*type (SQL, JSON, YAML, XML) always goes as a text part, even when the endpoint lists it. - Office files (DOCX, XLSX) are always omitted, and the omission is reported through the P1 path.
The opt-in is unchanged for Gemini behind an OpenAI-compatible gateway, where the operator controls what the gateway forwards. New tests: a listed XLSX and DOCX on both google and vertexai (omitted, no storage read), a listed SQL on both (text part, not media), and a listed XLSX for gemini-* through a gateway (file part). The existing test that expected a listed XLSX as media on native Google now expects the omission.
Addresses the three findings of the review on a3155b4. Unsupported attachments are now visible. The document encoder returns the files it skipped in `omitted` (reason `unsupported_type` or `text_limit`), and BaseClient collects them together with the provider-bound files that no encoder takes. On the current turn, processAttachments throws AgentAttachmentUnsupportedError (code AGENT_ATTACHMENT_UNSUPPORTED, 415). The error message names each file, it is an attachment error to isAgentAttachmentLimitError, and the turn fails before the user message is saved. On history replay the turn continues: the file is left out of message_file_map, the message carries a text note that names it, and the server logs the omission with the message id. The child-run encoder rejects a share with a provider-bound file that it cannot encode, in validate() and before it reads any bytes, so an archive-only share is no longer answered with no file message. Decoded file text is bounded. The text-part fallback and the plain-text source of a native Anthropic document now go through processTextWithTokenLimit with fileTokenLimit. fileContextCharLimit is tracked per request, so it covers the current turn, history replay and child runs together. The text is cut to the remaining characters before tokens are counted, and a file that gets no budget is reported as `text_limit`. A native Google/Vertex upload allowlist no longer turns off safe encoding. Listing a type for the native API does not change what that API accepts. So on native Google/Vertex, textual application types always go as text, and Office files are omitted whatever supportedMimeTypes says. The opt-in still works for Gemini behind an OpenAI-compatible gateway. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The "Static checks" job failed on import order in attachments.ts and document.ts. This is the output of `npm run sort-imports` for the two files. No code change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Testing against 1. Code-interpreter output reaches
|
|
Follow-up to my earlier comment, with one result that narrows it: on the same instance Same repro, same prompt, only the agent's provider changed:
Provider taken from the stored agent record rather than from the model's own account of The asymmetry looks like it sits in Separately, and possibly useful for scoping this PR: on current Happy to run anything specific against either provider if it would help — the matrix is |
|
@tommctech thanks for the repro and the provider comparison. I read the code and I agree with your trace. The branch you point at is not from this PR: it is the same on the PR base ( Two things follow for your case:
I would like to keep #16473 at its current scope. It is in review with changes requested, and your case needs a decision that a maintainer should make: whether to route generated files like uploads, to send every non-PDF textual type as text on Chat Completions, or both. The second option changes what every OpenAI-compatible gateway receives, so it needs its own review. Please open it as its own issue and link it here. Your two comments already hold the repro, the stored file record and the OpenAI/Anthropic table. The observation that the stored Your matrix result for the upload path on current |
|
The rerun I promised above. The #16472 failures still reproduce on current Setup.
A correction to my comment above: I named "Claude behind a gateway" as a route that gives the 400. That is wrong. The 400 routes are GPT and Gemini behind a gateway, and native Gemini. Claude behind a gateway is the silent-drop case. Why this differs from the nine-type matrix. The precondition is So the PR is still needed for the legacy-chooser path. With this branch, row 1 goes as a text part ( @tommctech, can you confirm the value of |
|
Confirmed: I then set it and re-ran, which adds a route your table does not cover: native OpenAI, Setup:
All seven failures carry the same provider message, verbatim: Two details that may be useful: The flag alone is sufficient — no tool resource and no UI choice are needed. All nine Same instance, same build, default UX: all nine pass, and for the seven that carry Scope limit, so the table is not read for more than it shows: my harness stops a case at Happy to re-run any of this — it is scripted, so another route or type set is cheap. |
OpenAI chat completions accepts only PDF as `file.file_data` and answers 400 "Expected a base64-encoded data URL with an application/pdf MIME type" for `text/plain`, CSV, HTML and JSON, which recurs on every later turn because the file is re-sent from history. Azure OpenAI accepts those types, and the code cannot tell the two apart behind a gateway, so a textual type now goes as a text part under the inherited list, which both read. An endpoint that lists the type in its own `supportedMimeTypes` keeps the file part. The responses API keeps `input_file`, which OpenAI documents for textual types. Measured by @tommctech on v0.8.8 with `legacyFileUploadUX: true` on api.openai.com (LibreChat-AI#16473).
|
Thanks, that settles the first question. With the flag unset your matrix ran on the default UX, and the two result sets agree. Your second table adds a route I had not measured: native OpenAI on Chat Completions. I checked it against this branch instead of a rerun. At The comment in the code that kept
I left docx, xlsx and zip as they are. Azure accepts docx as a |
|
Opened as #16802, with both pieces: the generated-file path and the Noted on Thanks for the rerun and for the detail throughout. Happy to measure anything else on the native OpenAI or Anthropic routes; it is scripted, so it costs us a couple of minutes. |


Summary
With "Upload to Provider", some files on the inherited
supportedMimeTypeslist went to the provider as file parts that the provider always rejects. The 400 then recurred on every later turn, because the file is re-sent from history. Azure OpenAI rejectsapplication/sql,application/x-sh,application/xml, zip and octet-stream. Gemini rejects docx, xlsx and every textualapplication/*type (JSON, YAML, XML, SQL, TypeScript). The measurements are in the issue.The inherited list stays the provider opt-in, as #13550 made it for OpenAI (CSV and XLSX to the Responses API keep working, and its test passes unchanged). Only the types that can never go inline change:
supportedMimeTypes. Upload to Code Environment and File Search are not touched.gemini, the same detection 📄 fix: Send Textual Documents to Claude Through Gateways as Text #16055 uses for Claude) gets PDF and textual types. Behind a gateway it also gets the types the endpoint lists. On native Google/Vertex the upload allowlist does not change what the API accepts, so there a listed textual type still goes as text and a listed Office document is still left out.File: "<name>"\n\n<contents>, unless the endpoint lists them: every textual type (isNativelyReadableText) on OpenAI-like Chat Completions,application/sql,x-shandxmlon the OpenAI-like Responses API too, and every textualapplication/*type on Gemini. OpenAI Chat Completions accepts only PDF asfile.file_data(400 "Expected a base64-encoded data URL with an application/pdf MIME type" for text/plain, CSV, HTML and JSON, measured onapi.openai.comwith v0.8.8), while Azure OpenAI accepts those types there; a text part is read by both.text/*stays inline media on Gemini, and the Responses API keepsinput_filefor textual types, which OpenAI documents.A file the model would not receive is no longer dropped silently. The document encoder returns the files it skipped in
omitted, andBaseClient.processAttachmentsadds the provider-bound files that no encoder takes. On the current turn, the send fails withAgentAttachmentUnsupportedError(codeAGENT_ATTACHMENT_UNSUPPORTED, 415), which names each file and tells the user to remove it or to upload it as text or to the code environment. It uses the same path to the user asAgentAttachmentLimitError, before the user message is saved. On history replay the turn continues: the file is left out ofmessage_file_map, the message gets a text note that names it, and the server logs it. A child-run share with such a file is rejected invalidate(), before any bytes are read. For Bedrock, a skipped non-Bedrock type is now reported too, so it rejects the turn and is not dropped.Decoded text is bounded. The text-part fallback and the plain-text source of a native Anthropic document use
fileTokenLimitper file (processTextWithTokenLimit), andfileContextCharLimitacross the request (the current turn, history replay and child runs). The text is cut, not rejected, so replay cannot fail on it. This budget is separate from the extracted-text count inassertAgentAttachmentLimits.Fixes #16472
How it works
isProviderDocumentCandidate(packages/api/src/files/encode/utils.ts) replaces the inlinecheckTypetest inBaseClient.processAttachmentsand in the child-run encoder. It usesisExplicitMimeConfig, the same inherited-list check as audio/video delivery.encodeAndFormatDocumentspassesisConfiguredProviderMediaType(explicit opt-in) tofilterProviderDocumentFilesandformatDocumentBlock. On native Google/Vertex it passes no opt-in.DocumentResult.omitted(OmittedAttachment[]),AgentAttachmentUnsupportedErrorinpackages/api/src/agents/attachments.ts, andprocessAttachments(message, attachments, fileConsumers, { historical }).{ type: 'text' }toinput_text(convertMessagesToResponsesInput). I checked this with @langchain/openai 1.5.5.An admin whose gateway accepts one of these types as a file lists it in the endpoint's
supportedMimeTypes, and the file part comes back.The Gemini and Claude checks read the model name. A gateway alias that hides the family (for example
fast→vertex_ai/gemini-flash-lite-latest) is not detected, so that endpoint must list its types insupportedMimeTypesexplicitly.Change Type
Testing
packages/api:jest src/files/encode src/agents/filesgives 393 passed.jest src/files src/agents src/middlewaregives 6,367 passed. The only failures are in two suites that need things this machine does not have: a Redis server (concurrency.cache_integration) and a shell profile withoutpyenv(hooks/executor). New cases: archives under the inherited list, explicit opt-in, SQL as text on OpenAI chat completions, Responses API, OpenRouter, Google and Vertex, XML as text on OpenAI, Gemini behind a gateway, Office documents on Gemini, PDF, CSV unchanged on Gemini, and (round 3) text/plain, CSV, HTML, JSON, YAML and TypeScript as text parts on OpenAI chat completions, the same types asinput_fileon the responses API, and text/plain and JSON kept as file parts when the endpoint lists them.api:jest app/clients/specs/BaseClient.test.js server/controllers/agents/client.test.jsgives 424 passed, including 📎 fix: Preserve Provider Document Uploads #13550's CSV/XLSX test. Review round 2 added tests for the typed error on the current turn (BaseClient, and a zip through the realAgentClient.processAttachments), the omission note on replay, archive-only and mixed child shares, a 2 MB SQL file underfileTokenLimit: 8/fileContextCharLimit: 100, and listed SQL/DOCX/XLSX on nativegoogleandvertexai. The new BaseClient tests fail ondevwithout the fix. One of them replays an archive attached on an earlier message throughaddPreviousAttachments, the path that made a conversation fail on every later turn: the archive is skipped and the PDF from the same message is still sent.chat.spec.ts,unified-upload.spec.ts) usetext/csv,text/plainandtext/markdownonmock-model-*models; they assert the stored delivery path, not the part shape, which is now a text part on chat completions.input_fileon the responses API.tsc --noEmitforpackages/api, ESLint and Prettier are clean.agents/steering/media.tsdoes not readomittedyet.gpt-5-miniand Vertexgemini-flash-lite-latest.Test Configuration:
Custom OpenAI-compatible endpoints → LiteLLM → Azure OpenAI and Gemini on Vertex AI,
legacyFileUploadUX: true; v0.8.8-rc3/rc4 in production, patch againstdevc8c5478.Checklist
🤖 Generated with Claude Code