OpenAI Multimodal Content (images / audio / video) — adapters/openai¶
Status: Awaiting use case — placeholder / not yet designed in detail. ← Back to Roadmap
See also: LLM Integration · Vector Store Adapter (RAG retrieval)
Motivation¶
LLM Integration (api/llm + adapters/openai) is text/JSON-only end-to-end today: api/llm's build() hardcodes format.JSON for both request encoding and response decoding, and adapters/openai's wire shape (chatMessage{Role, Content string}) always sends a plain string. There is no way to attach an image, audio clip, or video frame to a Call — even though OpenAI's own Chat Completions API (and the equivalent on Azure OpenAI / other OpenAI-compatible providers) already supports a multimodal content array ([{"type":"text",...}, {"type":"image_url",...}, {"type":"input_audio",...}]) for vision/audio-capable models (gpt-4o, gpt-4o-mini, etc.).
This page is the placeholder for that gap, captured now so the need and its rough shape aren't lost — to be fully specified once a concrete driving use case appears (same bar as stream-flatmap.md's "awaiting use case" status, and vector-store-adapter.md's placeholder precedent).
What's genuinely missing¶
- A way to declare binary/media input alongside the typed request codec. Today
Reqis always JSON-encoded wholesale into a single string content part. Real multimodal use cases (e.g. "classify this uploaded photo", "transcribe and summarize this audio clip") need the request to carry raw bytes (a[]bytefield, or a codec likecodex.Base64Bytes()) that get sent as a separate content part (image_url/input_audio) rather than JSON-embedded as a base64 string inside the text part — the latter technically "works" today (bytes can already round-trip through a byte-slice codec) but defeats the purpose: the model would just see a giant base64 string in the text, not a first-class image/audio input it can actually "look at" or "listen to". chatMessage.Contentneeds to become a union, not a plain string. OpenAI's wire format allowscontentto be either a plain string OR an array of typed parts ({"type":"text","text":...},{"type":"image_url","image_url":{"url":...}},{"type":"input_audio","input_audio":{"data":...,"format":...}}).adapters/openai/client.go'schatMessagestruct andcomplete[Req,Resp]'s message-building logic need to support both shapes — likely keeping the current plain-string path as the default (backward compatible) and adding an opt-in multimodal path.- A new
llm.CallOptto attach media parts — rough sketch:llm.WithImage(field extractor func(Req) ([]byte, string /* mime type */))or similar, mirroring howllm.UserMessagealready lets a caller override text-content rendering. Needs to decide whether media come from a dedicatedReqfield (typed, codec-validated) or a raw side-channel (less consistent with go-codex's "one struct, one call" philosophy — should be avoided if possible). - Response side: some models can also generate images (e.g. via a separate image-generation endpoint, not Chat Completions) — explicitly out of scope for this page; Chat Completions multimodal is input-only (the model describes/analyzes media, it does not return media through this endpoint).
- Size/encoding concerns — base64-encoding a large image/audio/video payload inline vs. referencing a URL (
image_url.urlaccepts either a data URI or a hosted URL) has real payload-size and latency implications; needs a documented recommendation once designed (e.g. prefer hosted URLs for large files, inline base64 only for small images).
Rough shape (to refine when this gets designed properly)¶
// adapters/openai/client.go — sketch, NOT final
type contentPart struct {
Type string `json:"type"` // "text" | "image_url" | "input_audio"
Text string `json:"text,omitempty"`
ImageURL *imageURLPart `json:"image_url,omitempty"`
// ... input_audio, etc.
}
type chatMessage struct {
Role string `json:"role"`
Content any `json:"content"` // string (today) OR []contentPart (multimodal)
}
// api/llm — sketch, NOT final
func WithImage[Req any](extract func(Req) (data []byte, mimeType string)) CallOpt
Open questions (for the real design pass)¶
- Does this belong in
api/llm(protocol-agnostic, since the concept of "attach an image" isn't OpenAI-specific) or purely inadapters/openai(since the wire shape —image_urlvs. some other provider's equivalent — is genuinely provider-specific)? Precedent:api/llmstays protocol-agnostic today (CallHandlehas no HTTP/OpenAI knowledge at all) — likely the abstraction (WithImage/media parts) should live inapi/llmwithadapters/openaitranslating to its specific wire shape, but this needs confirming once a second provider's multimodal shape is examined (Anthropic/Gemini both have their own different multimodal content shapes). - Should media input be a dedicated typed
Reqfield (codec-validated, e.g.codex.Bytes()with aMaxLengthconstraint) or an option-level extractor function (as sketched above)? The former is more consistent with "one struct, one call"; the latter avoids pollutingReqwith transport-shape concerns (mime type, part ordering) that arguably don't belong in the domain type. - Multiple images/media parts per call — one field, or a slice? Needs a concrete use case to decide the ergonomics.
- Should this also cover request-side YAML/TOML format selection (
llm.CallOpt/ports.LLMPatterncurrently have noFormat/CustomFormatoption at all, unlikeFilePattern/CachePattern/SocketPattern) — a smaller, unrelated gap noted in LLM Integration's current limitations — or should that ship independently, sooner, since it doesn't require the wire-shape redesign this page is about? Likely independent and smaller — revisit separately if a concrete non-JSON use case appears.