Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/phpaisdk/openai-compatible/llms.txt

Use this file to discover all available pages before exploring further.

ChatMessageConverter handles every content type that the portable Content class can represent and converts each one to the correct OpenAI-compatible wire shape. You build messages the same way regardless of whether the content is plain text, a remote image URL, base64-encoded audio, or a PDF file — the converter figures out the right wire format automatically.

How content types map to the wire format

The converter’s convertContent() method dispatches on $content->type and returns the appropriate object for the OpenAI messages[].content array.

Text — Content::TYPE_TEXT

Plain text content is emitted as a {type: text, text: "..."} object. When a message contains only a single text part, the converter flattens it to a bare string instead of wrapping it in a single-element array — this keeps the payload concise and maximizes provider compatibility.
// Single text-only message — flattened to a string
Message::user('Hello!')
// → { "role": "user", "content": "Hello!" }

// Text mixed with other content types — kept as an array
Message::user([Content::text('Describe this image.'), Content::image(...)])
// → { "role": "user", "content": [{"type":"text","text":"Describe this image."}, ...] }
The flattening to a plain string only applies when the message has exactly one content part and that part is text. As soon as a second part (image, audio, or file) is added, all parts — including the text part — are serialized as a typed content array.

Images — Content::TYPE_IMAGE

Images can be passed as remote URLs or as raw base64 data. Both are mapped to the image_url content type; the URL versus data-URI distinction is handled inside the converter. URL image:
Content::image('https://example.com/photo.png')
// → { "type": "image_url", "image_url": { "url": "https://example.com/photo.png" } }
Base64 image (data URI):
Content::image($base64Data, mimeType: 'image/jpeg', encoding: InputEncoding::Base64)
// → { "type": "image_url", "image_url": { "url": "data:image/jpeg;base64,/9j/4AAQ..." } }

Audio — Content::TYPE_AUDIO

Audio content is mapped to input_audio with the raw base64 data and a normalized format string. The MIME type is resolved to one of three format tokens:
MIME typeWire format value
audio/mpeg or audio/mp3mp3
audio/wav or audio/x-wavwav
audio/oggogg
(anything else)wav
Content::audio($base64Data, mimeType: 'audio/wav', encoding: InputEncoding::Base64)
// → {
//     "type": "input_audio",
//     "input_audio": { "data": "UklGRg==", "format": "wav" }
//   }

Files — Content::TYPE_FILE

File content (PDFs, documents, etc.) is mapped to the file content type with a filename and a file_data field. When the content source is base64, file_data is a full data URI; when the source is a URL, the URL is passed directly.
Content::file(
    $base64Data,
    mimeType: 'application/pdf',
    filename: 'report.pdf',
    encoding: InputEncoding::Base64,
)
// → {
//     "type": "file",
//     "file": {
//         "filename":  "report.pdf",
//         "file_data": "data:application/pdf;base64,JVBERi0="
//     }
//   }

Full multimodal example

The following example is drawn directly from ChatWireFormatTest.php and shows all four content types in a single user message:
use AiSdk\Content;
use AiSdk\InputEncoding;
use AiSdk\Message;
use AiSdk\OpenAICompatible\ChatRequestBuilder;
use AiSdk\Requests\TextModelRequest;

$body = ChatRequestBuilder::build(
    'gpt-4o-audio-preview',
    'openai',
    new TextModelRequest(
        messages: [
            Message::user([
                Content::text('Describe these inputs.'),
                Content::image('https://example.com/photo.png'),
                Content::audio('UklGRg==', mimeType: 'audio/wav', encoding: InputEncoding::Base64),
                Content::file('JVBERi0=', mimeType: 'application/pdf', filename: 'report.pdf', encoding: InputEncoding::Base64),
            ]),
        ],
    ),
    stream: false,
);

// $body['messages'][0]['content'] ===
[
    ['type' => 'text',        'text' => 'Describe these inputs.'],
    ['type' => 'image_url',   'image_url'   => ['url' => 'https://example.com/photo.png']],
    ['type' => 'input_audio', 'input_audio' => ['data' => 'UklGRg==', 'format' => 'wav']],
    ['type' => 'file',        'file'        => [
        'filename'  => 'report.pdf',
        'file_data' => 'data:application/pdf;base64,JVBERi0=',
    ]],
]

System prompts

System messages are prepended to the messages array as a plain {role: system, content: "..."} entry. Pass the system prompt through TextModelRequest::$system — never add it manually as a Message::system() entry, as the converter handles the insertion automatically.
new TextModelRequest(
    messages: [Message::user('What does this image show?')],
    system: 'You are a detailed image analyst.',
);

// Produces:
// messages[0] = { "role": "system",  "content": "You are a detailed image analyst." }
// messages[1] = { "role": "user",    "content": "What does this image show?" }

Content type summary

Text

Content::text('...'){type: text, text: "..."} (or bare string when it’s the only part)

Image

Content::image(url){type: image_url, image_url: {url: "..."}}Content::image(base64, ...) → data URI wrapped in image_url

Audio

Content::audio(base64, mimeType: ...){type: input_audio, input_audio: {data, format}}Format resolved from MIME type: mp3, wav, or ogg

File

Content::file(base64, mimeType, filename, ...){type: file, file: {filename, file_data}}file_data is a full data URI when source is base64
For building the full chat request, see Chat Completions. For tool calling alongside multimodal messages, see Tool Calling.

Build docs developers (and LLMs) love