feat(attachments): accept pdf/audio uploads in the UI + admin capability toggles

- composer: accept pdf/audio in the upload picker; client-side kind
  inference for the optimistic chip (server classify_upload stays
  authoritative)
- admin Models tab: supports_pdf + supports_audio_input toggles (flow
  through the field-aware capabilities merge into ModelCapabilities, so
  flipping supports_audio_input on an omni alias enables native input_audio)
- docs: AttachmentInfo.kind, AttachmentUpload, and the TS SDK note pdf/audio
This commit is contained in:
Patrick Buckley
2026-06-15 16:25:01 -07:00
parent 278aec0ce4
commit efae51dc50
7 changed files with 41 additions and 15 deletions
+1 -1
View File
@@ -96,7 +96,7 @@ export interface AttachmentInfo {
filename: string;
mime_type: string;
size_bytes: number;
/** "image" or "text". */
/** "image", "text", "pdf", or "audio". */
kind: string;
}
+4 -1
View File
@@ -74,7 +74,10 @@ class AttachmentInfo(BaseModel):
filename: str = Field(description="Original upload filename")
mime_type: str = Field(description="Canonicalized MIME type")
size_bytes: int = Field(description="Payload size in bytes")
kind: str = Field(description="'image' or 'text'", examples=["image", "text"])
kind: str = Field(
description="'image', 'text', 'pdf', or 'audio'",
examples=["image", "text", "pdf", "audio"],
)
class UploadAttachmentResponse(AttachmentInfo):
+4
View File
@@ -5097,22 +5097,26 @@ const _MODEL_CAP_KEYS = [
"supports_tools",
"supports_streaming",
"supports_vision",
"supports_pdf",
"supports_web_search",
"supports_temperature",
"supports_effort",
"supports_transcription",
"supports_speech_synthesis",
"supports_audio_input",
"supports_rerank",
];
const _MODEL_CAP_DEFAULTS = {
supports_tools: true,
supports_streaming: true,
supports_vision: false,
supports_pdf: false,
supports_web_search: false,
supports_temperature: true,
supports_effort: false,
supports_transcription: false,
supports_speech_synthesis: false,
supports_audio_input: false,
supports_rerank: false,
};
let _modelCapsBaseline = {}; // known-model table values (display + delta base)
+12
View File
@@ -1769,6 +1769,12 @@
></span
><span class="cap-name">Vision</span></label
>
<label class="cap"
><input type="checkbox" data-cap="supports_pdf" /><span
class="cap-led"
></span
><span class="cap-name">PDF documents</span></label
>
<label class="cap"
><input type="checkbox" data-cap="supports_web_search" /><span
class="cap-led"
@@ -1802,6 +1808,12 @@
/><span class="cap-led"></span
><span class="cap-name">Speech (TTS)</span></label
>
<label class="cap"
><input type="checkbox" data-cap="supports_audio_input" /><span
class="cap-led"
></span
><span class="cap-name">Audio input</span></label
>
<label class="cap"
><input type="checkbox" data-cap="supports_rerank" /><span
class="cap-led"
+2 -2
View File
@@ -11,8 +11,8 @@ class AttachmentUpload:
Used by ``upload_attachment`` and by ``create_workstream(attachments=...)``.
``mime_type`` is advisory — the server applies its own magic-byte
sniffing for images and UTF-8 validation for text documents and
rejects anything that doesn't match its allowlist.
sniffing (images, PDF, audio) and UTF-8 validation for text documents,
and rejects anything that doesn't match its allowlist.
"""
filename: str
+6 -9
View File
@@ -35,9 +35,11 @@
* anywhere onto the pane to attach.
*/
var ATTACH_DEFAULT_ACCEPT =
"image/png,image/jpeg,image/gif,image/webp,text/*," +
"image/png,image/jpeg,image/gif,image/webp,application/pdf,text/*," +
"audio/wav,audio/mpeg,audio/ogg,audio/flac,audio/mp4,audio/aac,audio/webm," +
".md,.py,.js,.ts,.tsx,.jsx,.json,.yaml,.yml,.toml,.html,.css,.sh," +
".rs,.go,.java,.c,.cpp,.h,.hpp,.sql,.xml,.ini,.conf";
".rs,.go,.java,.c,.cpp,.h,.hpp,.sql,.xml,.ini,.conf," +
".pdf,.mp3,.wav,.ogg,.flac,.m4a,.aac,.webm";
var AUTO_RESIZE_MAX_PX = 200;
@@ -525,9 +527,7 @@ Composer.prototype._wireEvents = function () {
});
this._on(target, "drop", function (e) {
target.classList.remove(dropClass);
var files = Array.from(
(e.dataTransfer && e.dataTransfer.files) || [],
);
var files = Array.from((e.dataTransfer && e.dataTransfer.files) || []);
if (files.length > 0) {
e.preventDefault();
files.forEach(function (f) {
@@ -613,9 +613,7 @@ Composer.prototype.setBusy = function (b) {
// Placeholder swap applies whenever busy, not only in queue mode —
// callers that don't set busyPlaceholder see no visible change
// because it defaults to idlePlaceholder.
this.inputEl.placeholder = b
? this._busyPlaceholder
: this._idlePlaceholder;
this.inputEl.placeholder = b ? this._busyPlaceholder : this._idlePlaceholder;
// Disabled state — composer owns it unless caller opted out.
if (!opts.externalDisable) {
@@ -806,7 +804,6 @@ Composer.prototype.destroy = function () {
this.el = null;
};
// --- Legacy window bridge ---------------------------------------------------
// Still-classic consumers reach this as a global at event/boot time (after
// this deferred module evaluated). New module code imports instead.
@@ -36,6 +36,17 @@ function formatSize(n) {
return (n / (1024 * 1024)).toFixed(1) + " MB";
}
function _inferKind(file) {
// Optimistic placeholder-chip guess only; the server's classify_upload is
// authoritative and the real kind arrives on the upload response.
var t = (file.type || "").toLowerCase();
var n = (file.name || "").toLowerCase();
if (t.indexOf("image/") === 0) return "image";
if (t === "application/pdf" || n.endsWith(".pdf")) return "pdf";
if (t.indexOf("audio/") === 0) return "audio";
return "text";
}
function _toastError(msg) {
if (typeof window.toast !== "undefined" && window.toast.error) {
window.toast.error(msg);
@@ -172,7 +183,7 @@ export function createAttachmentController(opts) {
filename: file.name,
size_bytes: file.size,
mime_type: file.type || "",
kind: (file.type || "").indexOf("image/") === 0 ? "image" : "text",
kind: _inferKind(file),
uploading: true,
};
pending.set(placeholderId, placeholder);
@@ -300,7 +311,6 @@ export function createAttachmentController(opts) {
};
}
// --- Legacy window bridge ---------------------------------------------------
// Still-classic consumers reach this as a global at event/boot time (after
// this deferred module evaluated). New module code imports instead.