Single-source `BinaryContent` → `BinaryImage` narrowing in `MultiModalContent` validation
- Closes #5801
The rule "an image-typed BinaryContent should be narrowed to the BinaryImage subclass on deserialization" is currently applied from scattered call sites — FilePart.content (via an AfterValidator), and, in #5255, the AG-UI and Vercel adapters (a recursive _narrow_binary_images walk and an inline narrow). The rule is single-sourced (all delegate to BinaryContent.narrow_type) but wired up per-site, so every new MultiModalContent consumer has to remember to re-apply it.
This pushes the narrowing into the MultiModalContent union itself, via an AfterValidator on the BinaryContent arm:
MultiModalContent = Annotated[
ImageUrl | AudioUrl | DocumentUrl | VideoUrl
| Annotated[BinaryContent, pydantic.AfterValidator(BinaryImage.narrow_type)]
| UploadedFile,
pydantic.Discriminator('kind'),
]
BinaryImage shares kind='binary' with BinaryContent, so the Discriminator('kind') still dispatches to the BinaryContent arm and the AfterValidator then narrows image content (a no-op for non-image content, which never raises). Every MultiModalContent / ToolReturnContent consumer — both UI adapters, the Temporal CallToolResult round-trip, and future integrations — now narrows for free.
Scope note: this is the add half of #5801 and stands alone on main. The adapter-local narrowing it obsoletes lives in #5255, so that code is deleted when #5255 rebases onto this (and onto #6191). FilePart.content keeps its own AfterValidator — it's a BinaryContent field, not a MultiModalContent field, so it isn't covered by the union narrowing.
Blast radius is minimal: narrowing only triggers on validation, and most code either works with live objects or (like FilePart) already narrowed. The full test suite passes (5177) with a single one-line adjustment to test_multi_modal_content_types_matches_union to unwrap the Annotated member.
Checklist
- Any AI generated code has been reviewed line-by-line by the human PR author, who stands by it.
- No breaking changes in accordance with the version policy.
- PR title is fit for the release changelog.