Multimodal normalisation and provider wire formatting — the outbound half.
Everything upstream of this module is about content coming in. This module is
about the moment it goes back out, to a provider, and the T1.2.5 AS-NET
contracts govern that moment.
No credential parameter (AS-NET-3) is enforced structurally: the wire
formatters take exactly one argument — the parts. A function that cannot
receive a credential cannot forward one, which is a stronger guarantee than
remembering not to.
The media-type allowlists are the other half, and they are about the provider
rather than the network: an attacker-chosen MIME string echoed onto the wire is
an injection into the provider's own parser dispatch. There is one per media
kind — images and documents — because a document's media type arrives on the
same untrusted AttachmentRef an image's does.
AS-NET-1 and AS-NET-2 — the scheme allowlist and the host controls — live in
:mod:.outbound, because they govern what the network stack may be asked
for rather than what a provider may be told.
ContentPart
dataclass
ContentPart(part_type: PartType, text: str = '', media_type: str = '', data_b64: str = '')
One provider-neutral piece of a multimodal message.
media_part(kind: str, media_type: str, payload: bytes) -> ContentPart
Normalise bytes into a base64 part, under the media-type allowlist.
Source code in src/symfonic/capabilities/knowledge/multimodal.py
| def media_part(kind: str, media_type: str, payload: bytes) -> ContentPart:
"""Normalise bytes into a base64 part, under the media-type allowlist."""
if kind not in _MEDIA_KINDS:
raise OutboundRejected(
f"part kind {kind!r} has no wire representation; permitted kinds are "
f"{sorted(_MEDIA_KINDS)}."
)
if not payload:
raise OutboundRejected(f"the {kind} part carries an empty payload.")
if not media_type:
raise OutboundRejected(f"the {kind} part declares no media type.")
allowed = _ALLOWED_MEDIA_TYPES[kind]
if media_type not in allowed:
raise OutboundRejected(
f"{kind} media type {media_type!r} is outside the allowlist "
f"{sorted(allowed)}. The media type travels to the provider and selects its "
"decoder, so an attacker-chosen value is an injection into someone else's "
"parser — and a document's media type comes from the same untrusted "
"AttachmentRef an image's does."
)
return ContentPart(
part_type="image" if kind == "image" else "document",
media_type=media_type,
data_b64=base64.b64encode(payload).decode("ascii"),
)
|
text_part
text_part(text: str) -> ContentPart
Normalise a text part. Whitespace-only text is refused, not sent.
Source code in src/symfonic/capabilities/knowledge/multimodal.py
| def text_part(text: str) -> ContentPart:
"""Normalise a text part. Whitespace-only text is refused, not sent."""
stripped = text.strip()
if not stripped:
raise OutboundRejected(
"a text part is empty; an empty block costs a request slot and says nothing."
)
return ContentPart(part_type="text", text=stripped)
|
to_anthropic_blocks
to_anthropic_blocks(parts: Sequence[ContentPart]) -> list[dict[str, object]]
Render parts as Anthropic content blocks.
Source code in src/symfonic/capabilities/knowledge/multimodal.py
| def to_anthropic_blocks(parts: Sequence[ContentPart]) -> list[dict[str, object]]:
"""Render parts as Anthropic content blocks."""
blocks: list[dict[str, object]] = []
for part in parts:
if part.part_type == "text":
blocks.append({"type": "text", "text": part.text})
continue
blocks.append(
{
"type": part.part_type,
"source": {
"type": "base64",
"media_type": part.media_type,
"data": part.data_b64,
},
}
)
return blocks
|
to_openai_blocks
to_openai_blocks(parts: Sequence[ContentPart]) -> list[dict[str, object]]
Render parts as OpenAI content blocks.
A document part is refused rather than approximated. OpenAI's chat content
schema has no document block, and inventing one — a data: image URL
carrying a PDF, say — produces a request the provider rejects at best and
silently misreads at worst.
Source code in src/symfonic/capabilities/knowledge/multimodal.py
| def to_openai_blocks(parts: Sequence[ContentPart]) -> list[dict[str, object]]:
"""Render parts as OpenAI content blocks.
A document part is refused rather than approximated. OpenAI's chat content
schema has no document block, and inventing one — a ``data:`` image URL
carrying a PDF, say — produces a request the provider rejects at best and
silently misreads at worst.
"""
blocks: list[dict[str, object]] = []
for part in parts:
if part.part_type == "text":
blocks.append({"type": "text", "text": part.text})
continue
if part.part_type == "document":
raise OutboundRejected(
"the OpenAI content schema has no document block; extract the document to "
"text and send it as a text part rather than inventing a wire shape."
)
blocks.append(
{
"type": "image_url",
"image_url": {"url": f"data:{part.media_type};base64,{part.data_b64}"},
}
)
return blocks
|