What changes when the input is not text
Multimodal input means a request holds images or documents as well as words. The model reads them, but your service now carries large files, checks their type and pays by the image or the page. It matters because a request that was a few hundred bytes can become many megabytes.
A user photographs a receipt and uploads a ten-page contract. One request now has three parts and three ways to be refused.
By the end of this lesson
Note
I can say what changes when the input is an image or a document.
Key words
| Word | What it means | Example |
|---|---|---|
| Part | one piece of a request: text, image or document | |
| Media type | the label for a file's format | image/png |
| Payload size | the bytes a request carries | |
| Page limit | the most pages a document may have | |
| Unit price | the cost of one image or page |
The cells students fill in are left blank in this preview.
The lab takes text only
Warning
The lab's model endpoint accepts text. A Run here cannot send an image or a document. This lesson teaches from a recorded request shape, invented for the lesson, and checks it with code.
A request becomes a list of parts
Text input is one string. Multimodal input is a list of parts, each with a type and its data. The data of a file travels as bytes, often encoded as text, which makes it about a third bigger.
Below is a recorded shape, invented for this lesson. A real provider has its own field names, so always read its documentation.
A recorded request shape
{ "messages": [{ "role": "user", "content": [
{ "type": "text", "text": "What is on this receipt?" },
{ "type": "image", "media": "image/png", "data": "<base64, 412000 bytes>" },
{ "type": "document", "media": "application/pdf", "data": "<base64, 880000 bytes>" }
] }] }Check every part before sending
Check each part against a rule you set: the media type, the byte size and the page count. Check the totals too: how many images, how many bytes in all.
Refuse early with a clear reason. A request that fails at the provider has already cost you the upload and maybe the money.
Worked example: limits and a price
| Part | What it is | Check | Cost in units |
|---|---|---|---|
| 1 | text, 1800 characters | no limit here | 2 |
| 2 | image, png, 412000 bytes | allowed, under 5 MB | 4 |
| 3 | image, gif, 90000 bytes | gif is not on the list | 4 |
| 4 | document, 24 pages | over the 20 page limit | 48 |
The cells students fill in are left blank in this preview.
Cost is per image and per page
Text is priced by the token. An image or a page is usually priced by a fixed amount or a number of tokens set by its size. Add the parts up before the call so a budget rule can refuse a costly one.
Where possible, send less: reduce the image size or send only the pages that matter.
A common mistake
Warning
Many people trust the file name or the extension. A name can say png on a file that is not one. Check the type from the bytes on the server, and also keep the limits on size. The extension is a hint, not a fact.
Write the model helper
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' 'async function ask(messages, sample, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 200 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"status \" + res.status);' ' return (await res.json()).choices[0].message.content;' ' } catch (error) {' ' console.log(\"Recorded sample: no model answered (\" + error.message + \").\");' ' const list = [].concat(sample);' ' const turn = messages.filter(function (m) { return m.role === \"assistant\"; }).length;' ' return list[Math.min(turn, list.length - 1)];' ' }' '}' 'module.exports = { ask };' > model.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the shape checker, part 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' '// One recorded request, invented: real providers each have their own shape.' 'const request = { parts: [' ' { type: \"text\", chars: 1800 },' ' { type: \"image\", kind: \"png\", bytes: 412000 },' ' { type: \"image\", kind: \"gif\", bytes: 90000 },' ' { type: \"document\", pages: 24, bytes: 880000 },' '] };' 'const LIMITS = { images: 3, imageBytes: 5000000, pages: 20, kinds: [\"png\", \"jpeg\"] };' 'const PRICE = { image: 4, page: 2, thousandChars: 1 }; // invented units' > shapes.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the shape checker, part 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function check(req) {' ' const problems = [];' ' let images = 0;' ' let cost = 0;' ' req.parts.forEach(function (p, i) {' ' const n = i + 1;' ' if (p.type === \"text\") cost += Math.ceil(p.chars / 1000) * PRICE.thousandChars;' ' if (p.type === \"image\") {' ' images++;' ' cost += PRICE.image;' ' if (LIMITS.kinds.indexOf(p.kind) < 0) problems.push(\"part \" + n + \": kind \" + p.kind + \" not allowed\");' ' if (p.bytes > LIMITS.imageBytes) problems.push(\"part \" + n + \": image too big\");' ' }' >> shapes.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the shape checker, part 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' if (p.type === \"document\") {' ' cost += p.pages * PRICE.page;' ' if (p.pages > LIMITS.pages) problems.push(\"part \" + n + \": too many pages\");' ' }' ' });' ' if (images > LIMITS.images) problems.push(\"too many images\");' ' return { problems: problems, cost: cost };' '}' >> shapes.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the shape checker, part 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const result = check(request);' 'result.problems.forEach(function (p) { console.log(\"Problem:\", p); });' 'console.log(\"Cost:\", result.cost, \"units (an invented price)\");' 'async function main() {' ' const text = \"Receipt text: bread 2.10, milk 1.05, total 3.15. What is the total?\";' ' const sample = \"The total is 3.15.\";' ' console.log(\"Text only, live call:\", await ask([{ role: \"user\", content: text }], sample, 30));' '}' 'main();' >> shapes.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Check the recorded request
docker exec -w /home/node/project agent node shapes.jsShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
One possible output
| Line | What it shows |
|---|---|
| Problem: part 3: kind gif not allowed | The media type is not on the list |
| Problem: part 4: too many pages | 24 pages is over the limit of 20 |
| Cost: 58 units | Text 2, two images 8 and a document 48 |
| Text only, live call | A short answer to a text-only question, or a labelled Recorded sample |
The cells students fill in are left blank in this preview.