Easier to read with cream paper, a tint, more contrast or larger text? Choose your colours and text, once, for every page.

Free lesson

Sending images and documents to a model

After this lesson: I can say what changes when the input is an image or a document.

65 min lesson6 commandsAI coding agent lab: use opencode to explain and fix code, and check its work

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

WordWhat it meansExample
Partone piece of a request: text, image or document
Media typethe label for a file's formatimage/png
Payload sizethe bytes a request carries
Page limitthe most pages a document may have
Unit pricethe 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

Code: node
{ "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

PartWhat it isCheckCost in units
1text, 1800 charactersno limit here2
2image, png, 412000 bytesallowed, under 5 MB4
3image, gif, 90000 bytesgif is not on the list4
4document, 24 pagesover the 20 page limit48

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

Command
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

Command
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

Command
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

Command
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

Command
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

Command
docker exec -w /home/node/project agent node shapes.js

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

One possible output

LineWhat it shows
Problem: part 3: kind gif not allowedThe media type is not on the list
Problem: part 4: too many pages24 pages is over the limit of 20
Cost: 58 unitsText 2, two images 8 and a document 48
Text only, live callA short answer to a text-only question, or a labelled Recorded sample

The cells students fill in are left blank in this preview.

About this lab and its source: AI coding agent lab: use opencode to explain and fix code, and check its work

Teaching a class? Set this module for your students with a teacher guide, marked assignments and a lab for every student.